From de045542a59c5e58693a5492275a262ca0803777 Mon Sep 17 00:00:00 2001 From: savelichalex Date: Thu, 13 Aug 2015 16:13:33 +0300 Subject: [PATCH 1/9] Support inline SVG --- lib/convert-tag-attributes.js | 277 ++++++++++++++++++++-- lib/html-to-vdom.js | 2 +- lib/htmlparser-to-vdom.js | 37 +++ lib/svg-attribute-hook.js | 35 +++ lib/svg-namespaces.js | 416 ++++++++++++++++++++++++++++++++++ 5 files changed, 750 insertions(+), 17 deletions(-) create mode 100644 lib/svg-attribute-hook.js create mode 100644 lib/svg-namespaces.js diff --git a/lib/convert-tag-attributes.js b/lib/convert-tag-attributes.js index c59c7c4..6c8e48f 100644 --- a/lib/convert-tag-attributes.js +++ b/lib/convert-tag-attributes.js @@ -1,32 +1,277 @@ -var getPropertyInfo = require('./get-property-info/htmldom'); -var propertySetters = require('./property-setters'); - -var getPropertySetter = function (propInfo) { - if (propInfo.mustUseAttribute) { - return propertySetters.attribute; - } - else { - // Anything we don't set as an attribute is treated as a property - return propertySetters.property; - } +/* + Adapted from https://github.com/facebook/react/blob/c265504fe2fdeadf0e5358879a3c141628b37a23/src/renderers/dom/shared/HTMLDOMPropertyConfig.js + */ +var decode = require('ent').decode; + +var MUST_USE_ATTRIBUTE = 0x1; +var MUST_USE_PROPERTY = 0x2; +var HAS_BOOLEAN_VALUE = 0x8; +var HAS_NUMERIC_VALUE = 0x10; +var HAS_POSITIVE_NUMERIC_VALUE = 0x20 | 0x10; +var HAS_OVERLOADED_BOOLEAN_VALUE = 0x40; + +function checkMask(value, bitmask) { + return (value & bitmask) === bitmask; +} + +var isCustomAttribute = RegExp.prototype.test.bind( + /^(data|aria)-[a-z_][a-z\d_.\-]*$/ +); + +var HTMLDOMPropertyConfig = { + + Properties: { + /** + * Standard Properties + */ + accept: null, + acceptCharset: null, + accessKey: null, + action: null, + allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, + allowTransparency: MUST_USE_ATTRIBUTE, + alt: null, + async: HAS_BOOLEAN_VALUE, + autoComplete: null, + autoFocus: HAS_BOOLEAN_VALUE, + autoPlay: HAS_BOOLEAN_VALUE, + capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, + cellPadding: null, + cellSpacing: null, + charSet: MUST_USE_ATTRIBUTE, + challenge: MUST_USE_ATTRIBUTE, + checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, + classID: MUST_USE_ATTRIBUTE, + // To set className on SVG elements, it's necessary to use .setAttribute; + // this works on HTML elements too in all browsers except IE8. + className: MUST_USE_ATTRIBUTE, + cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE, + colSpan: null, + content: null, + contentEditable: null, + contextMenu: MUST_USE_ATTRIBUTE, + controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, + coords: null, + crossOrigin: null, + data: null, // For `` acts as `src`. + dateTime: MUST_USE_ATTRIBUTE, + defer: HAS_BOOLEAN_VALUE, + dir: null, + disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, + download: HAS_OVERLOADED_BOOLEAN_VALUE, + draggable: null, + encType: null, + form: MUST_USE_ATTRIBUTE, + formAction: MUST_USE_ATTRIBUTE, + formEncType: MUST_USE_ATTRIBUTE, + formMethod: MUST_USE_ATTRIBUTE, + formNoValidate: HAS_BOOLEAN_VALUE, + formTarget: MUST_USE_ATTRIBUTE, + frameBorder: MUST_USE_ATTRIBUTE, + headers: null, + height: MUST_USE_ATTRIBUTE, + hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, + high: null, + href: null, + hrefLang: null, + htmlFor: null, + httpEquiv: null, + icon: null, + id: MUST_USE_PROPERTY, + is: MUST_USE_ATTRIBUTE, + keyParams: MUST_USE_ATTRIBUTE, + keyType: MUST_USE_ATTRIBUTE, + label: null, + lang: null, + list: MUST_USE_ATTRIBUTE, + loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, + low: null, + manifest: MUST_USE_ATTRIBUTE, + marginHeight: null, + marginWidth: null, + max: null, + maxLength: MUST_USE_ATTRIBUTE, + media: MUST_USE_ATTRIBUTE, + mediaGroup: null, + method: null, + min: null, + minLength: MUST_USE_ATTRIBUTE, + multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, + muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, + name: null, + noValidate: HAS_BOOLEAN_VALUE, + open: HAS_BOOLEAN_VALUE, + optimum: null, + pattern: null, + placeholder: null, + poster: null, + preload: null, + radioGroup: null, + readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, + rel: null, + required: HAS_BOOLEAN_VALUE, + role: MUST_USE_ATTRIBUTE, + rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE, + rowSpan: null, + sandbox: null, + scope: null, + scoped: HAS_BOOLEAN_VALUE, + scrolling: null, + seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, + selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, + shape: null, + size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE, + sizes: MUST_USE_ATTRIBUTE, + span: HAS_POSITIVE_NUMERIC_VALUE, + spellCheck: null, + src: null, + srcDoc: MUST_USE_PROPERTY, + srcSet: MUST_USE_ATTRIBUTE, + start: HAS_NUMERIC_VALUE, + step: null, + style: null, + tabIndex: null, + target: null, + title: null, + type: null, + useMap: null, + value: MUST_USE_PROPERTY, + width: MUST_USE_ATTRIBUTE, + wmode: MUST_USE_ATTRIBUTE, + + /** + * Non-standard Properties + */ + // autoCapitalize and autoCorrect are supported in Mobile Safari for + // keyboard hints. + autoCapitalize: null, + autoCorrect: null, + // itemProp, itemScope, itemType are for + // Microdata support. See http://schema.org/docs/gs.html + itemProp: MUST_USE_ATTRIBUTE, + itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, + itemType: MUST_USE_ATTRIBUTE, + // itemID and itemRef are for Microdata support as well but + // only specified in the the WHATWG spec document. See + // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api + itemID: MUST_USE_ATTRIBUTE, + itemRef: MUST_USE_ATTRIBUTE, + // property is supported for OpenGraph in meta tags. + property: null, + // IE-only attribute that controls focus behavior + unselectable: MUST_USE_ATTRIBUTE + } +}; + +var parseStyles = function(input) { + var attributes = input.split(';'); + var styles = attributes.reduce(function(object, attribute){ + var entry = attribute.split(/:(.+)/); + if (entry[0] && entry[1]) { + object[entry[0].trim()] = entry[1].trim(); + } + return object; + },{}); + return styles; +}; + +var propertyToAttributeMapping = { + 'className': 'class', + 'htmlFor': 'for', + 'httpEquiv': 'http-equiv', + 'acceptCharset': 'accept-charset' +}; + +var propertyValueConversions = { + 'style': parseStyles, + 'placeholder': decode, + 'title': decode, + 'alt': decode }; +var getPropertyInfo = (function () { + var propInfoByAttributeName = {}; + + Object.keys(HTMLDOMPropertyConfig.Properties).forEach(function (propName) { + var propConfig = HTMLDOMPropertyConfig.Properties[propName]; + var attributeName = propertyToAttributeMapping[propName] || propName.toLowerCase(); + + var propertyInfo = { + attributeName: attributeName, + propertyName: propName, + + mustUseAttribute: checkMask(propConfig, MUST_USE_ATTRIBUTE), + mustUseProperty: checkMask(propConfig, MUST_USE_PROPERTY), + hasBooleanValue: checkMask(propConfig, HAS_BOOLEAN_VALUE), + hasNumericValue: checkMask(propConfig, HAS_NUMERIC_VALUE), + hasPositiveNumericValue: + checkMask(propConfig, HAS_POSITIVE_NUMERIC_VALUE), + hasOverloadedBooleanValue: + checkMask(propConfig, HAS_OVERLOADED_BOOLEAN_VALUE), + }; + + propInfoByAttributeName[attributeName] = propertyInfo; + }); + + return function (attributeName) { + return propInfoByAttributeName[attributeName]; + }; +})(); + + var convertTagAttributes = function (tag) { var attributes = tag.attribs; - var vNodeProperties = { + var vdomProperties = { attributes: {} }; Object.keys(attributes).forEach(function (attributeName) { + var lowerCased = attributeName.toLowerCase(); + var propInfo = getPropertyInfo(lowerCased); + var value = attributes[attributeName]; - var propInfo = getPropertyInfo(attributeName); + if (isCustomAttribute(attributeName) || !propInfo) { + vdomProperties.attributes[attributeName] = value; + return; + } + + var valueConverter = propertyValueConversions[propInfo.propertyName]; + if (valueConverter) { + value = valueConverter(value); + } + + if (propInfo.mustUseAttribute) { + if (propInfo.hasBooleanValue) { + // Boolean attributes come in as an empty string or the + vdomProperties.attributes[propInfo.attributeName] = ''; + } + else { + vdomProperties.attributes[propInfo.attributeName] = value; + } + } + // Anything we don't set as an attribute is treated as a property + else { + var isTrue; + if (propInfo.hasBooleanValue) { + isTrue = (value === '' || value.toLowerCase() === propInfo.attributeName); + vdomProperties[propInfo.propertyName] = isTrue ? true : false; + } + else if (propInfo.hasOverloadedBooleanValue) { + isTrue = (value === ''); + vdomProperties[propInfo.propertyName] = isTrue ? true : value; + } + else if (propInfo.hasNumericValue || propInfo.hasPositiveNumericValue) { + vdomProperties[propInfo.propertyName] = Number(value); + } + else { + vdomProperties[propInfo.propertyName] = value; + } + } - var propertySetter = getPropertySetter(propInfo); - propertySetter.set(vNodeProperties, propInfo, value); }); - return vNodeProperties; + return vdomProperties; }; module.exports = convertTagAttributes; diff --git a/lib/html-to-vdom.js b/lib/html-to-vdom.js index 5135d39..f37ee87 100644 --- a/lib/html-to-vdom.js +++ b/lib/html-to-vdom.js @@ -22,7 +22,7 @@ module.exports = function initializeHtmlToVdom (VTree, VText) { else { convertedHTML = htmlparserToVdom.convert(tags[0], getVNodeKey); } - + return convertedHTML; }; }; diff --git a/lib/htmlparser-to-vdom.js b/lib/htmlparser-to-vdom.js index 9993453..8a6d9c6 100644 --- a/lib/htmlparser-to-vdom.js +++ b/lib/htmlparser-to-vdom.js @@ -1,5 +1,9 @@ var decode = require('ent').decode; var convertTagAttributes = require('./convert-tag-attributes'); +var thisIsSVGTag = require('./svg-namespaces').thisIsSVGTag, + getSVGNamespace = require('./svg-namespaces').getSVGNamespace, + SVGAttributeNamespace = require('./svg-namespaces').SVGAttributeNamespace, + SVGAttributeHook = require('./svg-attribute-hook'); module.exports = function createConverter (VNode, VText) { var converter = { @@ -25,6 +29,39 @@ module.exports = function createConverter (VNode, VText) { return converter.convert(node, getVNodeKey); }); + if(thisIsSVGTag(tag.name)) { + var _attributes = attributes.attributes; + + for(var _key in _attributes) { + if (!_attributes.hasOwnProperty(_key)) { + continue; + } + + var namespace = SVGAttributeNamespace(_key); + + if (namespace === void 0) { // not a svg attribute + continue; + } + + var value = _attributes[_key]; + + if (typeof value !== 'string' && + typeof value !== 'number' && + typeof value !== 'boolean' + ) { + continue; + } + + if (namespace !== null) { // namespaced attribute + attributes[_key] = SVGAttributeHook(namespace, value); + _attributes[_key] = void 0; + continue; + } + } + + return new VNode(tag.name, attributes, children, key, getSVGNamespace()); + } + return new VNode(tag.name, attributes, children, key); } }; diff --git a/lib/svg-attribute-hook.js b/lib/svg-attribute-hook.js new file mode 100644 index 0000000..64347a3 --- /dev/null +++ b/lib/svg-attribute-hook.js @@ -0,0 +1,35 @@ +'use strict'; + +module.exports = AttributeHook; + +function AttributeHook(namespace, value) { + if (!(this instanceof AttributeHook)) { + return new AttributeHook(namespace, value); + } + + this.namespace = namespace; + this.value = value; +} + +AttributeHook.prototype.hook = function (node, prop, prev) { + if (prev && prev.type === 'AttributeHook' && + prev.value === this.value && + prev.namespace === this.namespace) { + return; + } + + node.setAttributeNS(this.namespace, prop, this.value); +}; + +AttributeHook.prototype.unhook = function (node, prop, next) { + if (next && next.type === 'AttributeHook' && + next.namespace === this.namespace) { + return; + } + + var colonPosition = prop.indexOf(':'); + var localName = colonPosition > -1 ? prop.substr(colonPosition + 1) : prop; + node.removeAttributeNS(this.namespace, localName); +}; + +AttributeHook.prototype.type = 'AttributeHook'; diff --git a/lib/svg-namespaces.js b/lib/svg-namespaces.js new file mode 100644 index 0000000..7f5cbb4 --- /dev/null +++ b/lib/svg-namespaces.js @@ -0,0 +1,416 @@ +'use strict'; + +/* + Adapted from https://github.com/Matt-Esch/virtual-dom/blob/master/virtual-hyperscript/svg-attribute-namespace.js + */ + +var DEFAULT_NAMESPACE = null; +var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; +var EV_NAMESPACE = 'http://www.w3.org/2001/xml-events'; +var XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'; +var XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'; + +// http://www.w3.org/TR/SVGTiny12/elementTable.html +// http://www.w3.org/TR/SVG/eltindex.html +var SVG_ELEMENTS = { + 'listener': SVG_NAMESPACE, +// 'a': SVG_NAMESPACE, + 'animate': SVG_NAMESPACE, + 'altGlyph': SVG_NAMESPACE, + 'altGlyphDef': SVG_NAMESPACE, + 'altGlyphItem': SVG_NAMESPACE, + 'animateColor': SVG_NAMESPACE, + 'animateMotion': SVG_NAMESPACE, + 'animateTransform': SVG_NAMESPACE, + 'animation': SVG_NAMESPACE, +// 'audio': SVG_NAMESPACE, + 'circle': SVG_NAMESPACE, + 'defs': SVG_NAMESPACE, + 'desc': SVG_NAMESPACE, + 'discard': SVG_NAMESPACE, + 'ellipse': SVG_NAMESPACE, + 'feBlend': SVG_NAMESPACE, + 'feColorMatrix': SVG_NAMESPACE, + 'feComponentTransfer': SVG_NAMESPACE, + 'feComposite': SVG_NAMESPACE, + 'feConvolveMatrix': SVG_NAMESPACE, + 'feDiffuseLighting': SVG_NAMESPACE, + 'feDisplacementMap': SVG_NAMESPACE, + 'feDistantLight': SVG_NAMESPACE, + 'feFlood': SVG_NAMESPACE, + 'feFuncA': SVG_NAMESPACE, + 'feFuncB': SVG_NAMESPACE, + 'feFuncG': SVG_NAMESPACE, + 'feFuncR': SVG_NAMESPACE, + 'feGaussianBlur': SVG_NAMESPACE, + 'feImage': SVG_NAMESPACE, + 'feMerge': SVG_NAMESPACE, + 'feMergeNode': SVG_NAMESPACE, + 'feMorphology': SVG_NAMESPACE, + 'feOffset': SVG_NAMESPACE, + 'fePointLight': SVG_NAMESPACE, + 'feSpecularLighting': SVG_NAMESPACE, + 'feSpotLight': SVG_NAMESPACE, + 'feTile': SVG_NAMESPACE, + 'feTurbulence': SVG_NAMESPACE, + 'filter': SVG_NAMESPACE, + 'font': SVG_NAMESPACE, + 'font-face': SVG_NAMESPACE, + 'font-face-format': SVG_NAMESPACE, + 'font-face-name': SVG_NAMESPACE, + 'font-face-src': SVG_NAMESPACE, + 'font-face-uri': SVG_NAMESPACE, + 'foreignObject': SVG_NAMESPACE, + 'g': SVG_NAMESPACE, + 'glyph': SVG_NAMESPACE, + 'glyphRef': SVG_NAMESPACE, + 'handler': SVG_NAMESPACE, + 'hkern': SVG_NAMESPACE, + 'image': SVG_NAMESPACE, + 'line': SVG_NAMESPACE, + 'linearGradient': SVG_NAMESPACE, + 'metadata': SVG_NAMESPACE, + 'missing-glyph': SVG_NAMESPACE, + 'mpath': SVG_NAMESPACE, + 'path': SVG_NAMESPACE, + 'polygon': SVG_NAMESPACE, + 'polyline': SVG_NAMESPACE, + 'prefetch': SVG_NAMESPACE, + 'radialGradient': SVG_NAMESPACE, + 'rect': SVG_NAMESPACE, +// 'script': SVG_NAMESPACE, + 'set': SVG_NAMESPACE, + 'solidColor': SVG_NAMESPACE, + 'stop': SVG_NAMESPACE, + 'svg': SVG_NAMESPACE, + 'switch': SVG_NAMESPACE, + 'symbol': SVG_NAMESPACE, + 'tbreak': SVG_NAMESPACE, + 'text': SVG_NAMESPACE, + 'textArea': SVG_NAMESPACE, + 'title': SVG_NAMESPACE, + 'tspan': SVG_NAMESPACE, + 'tref': SVG_NAMESPACE, + 'use': SVG_NAMESPACE, + 'vkern': SVG_NAMESPACE, +// 'video': SVG_NAMESPACE, +}; + +// http://www.w3.org/TR/SVGTiny12/attributeTable.html +// http://www.w3.org/TR/SVG/attindex.html +var SVG_PROPERTIES = { + 'about': DEFAULT_NAMESPACE, + 'accent-height': DEFAULT_NAMESPACE, + 'accumulate': DEFAULT_NAMESPACE, + 'additive': DEFAULT_NAMESPACE, + 'alignment-baseline': DEFAULT_NAMESPACE, + 'alphabetic': DEFAULT_NAMESPACE, + 'amplitude': DEFAULT_NAMESPACE, + 'arabic-form': DEFAULT_NAMESPACE, + 'ascent': DEFAULT_NAMESPACE, + 'attributeName': DEFAULT_NAMESPACE, + 'attributeType': DEFAULT_NAMESPACE, + 'azimuth': DEFAULT_NAMESPACE, + 'bandwidth': DEFAULT_NAMESPACE, + 'baseFrequency': DEFAULT_NAMESPACE, + 'baseProfile': DEFAULT_NAMESPACE, + 'baseline-shift': DEFAULT_NAMESPACE, + 'bbox': DEFAULT_NAMESPACE, + 'begin': DEFAULT_NAMESPACE, + 'bias': DEFAULT_NAMESPACE, + 'by': DEFAULT_NAMESPACE, + 'calcMode': DEFAULT_NAMESPACE, + 'cap-height': DEFAULT_NAMESPACE, + 'class': DEFAULT_NAMESPACE, + 'clip': DEFAULT_NAMESPACE, + 'clip-path': DEFAULT_NAMESPACE, + 'clip-rule': DEFAULT_NAMESPACE, + 'clipPathUnits': DEFAULT_NAMESPACE, + 'color': DEFAULT_NAMESPACE, + 'color-interpolation': DEFAULT_NAMESPACE, + 'color-interpolation-filters': DEFAULT_NAMESPACE, + 'color-profile': DEFAULT_NAMESPACE, + 'color-rendering': DEFAULT_NAMESPACE, + 'content': DEFAULT_NAMESPACE, + 'contentScriptType': DEFAULT_NAMESPACE, + 'contentStyleType': DEFAULT_NAMESPACE, + 'cursor': DEFAULT_NAMESPACE, + 'cx': DEFAULT_NAMESPACE, + 'cy': DEFAULT_NAMESPACE, + 'd': DEFAULT_NAMESPACE, + 'datatype': DEFAULT_NAMESPACE, + 'defaultAction': DEFAULT_NAMESPACE, + 'descent': DEFAULT_NAMESPACE, + 'diffuseConstant': DEFAULT_NAMESPACE, + 'direction': DEFAULT_NAMESPACE, + 'display': DEFAULT_NAMESPACE, + 'divisor': DEFAULT_NAMESPACE, + 'dominant-baseline': DEFAULT_NAMESPACE, + 'dur': DEFAULT_NAMESPACE, + 'dx': DEFAULT_NAMESPACE, + 'dy': DEFAULT_NAMESPACE, + 'edgeMode': DEFAULT_NAMESPACE, + 'editable': DEFAULT_NAMESPACE, + 'elevation': DEFAULT_NAMESPACE, + 'enable-background': DEFAULT_NAMESPACE, + 'end': DEFAULT_NAMESPACE, + 'ev:event': EV_NAMESPACE, + 'event': DEFAULT_NAMESPACE, + 'exponent': DEFAULT_NAMESPACE, + 'externalResourcesRequired': DEFAULT_NAMESPACE, + 'fill': DEFAULT_NAMESPACE, + 'fill-opacity': DEFAULT_NAMESPACE, + 'fill-rule': DEFAULT_NAMESPACE, + 'filter': DEFAULT_NAMESPACE, + 'filterRes': DEFAULT_NAMESPACE, + 'filterUnits': DEFAULT_NAMESPACE, + 'flood-color': DEFAULT_NAMESPACE, + 'flood-opacity': DEFAULT_NAMESPACE, + 'focusHighlight': DEFAULT_NAMESPACE, + 'focusable': DEFAULT_NAMESPACE, + 'font-family': DEFAULT_NAMESPACE, + 'font-size': DEFAULT_NAMESPACE, + 'font-size-adjust': DEFAULT_NAMESPACE, + 'font-stretch': DEFAULT_NAMESPACE, + 'font-style': DEFAULT_NAMESPACE, + 'font-variant': DEFAULT_NAMESPACE, + 'font-weight': DEFAULT_NAMESPACE, + 'format': DEFAULT_NAMESPACE, + 'from': DEFAULT_NAMESPACE, + 'fx': DEFAULT_NAMESPACE, + 'fy': DEFAULT_NAMESPACE, + 'g1': DEFAULT_NAMESPACE, + 'g2': DEFAULT_NAMESPACE, + 'glyph-name': DEFAULT_NAMESPACE, + 'glyph-orientation-horizontal': DEFAULT_NAMESPACE, + 'glyph-orientation-vertical': DEFAULT_NAMESPACE, + 'glyphRef': DEFAULT_NAMESPACE, + 'gradientTransform': DEFAULT_NAMESPACE, + 'gradientUnits': DEFAULT_NAMESPACE, + 'handler': DEFAULT_NAMESPACE, + 'hanging': DEFAULT_NAMESPACE, + 'height': DEFAULT_NAMESPACE, + 'horiz-adv-x': DEFAULT_NAMESPACE, + 'horiz-origin-x': DEFAULT_NAMESPACE, + 'horiz-origin-y': DEFAULT_NAMESPACE, + 'id': DEFAULT_NAMESPACE, + 'ideographic': DEFAULT_NAMESPACE, + 'image-rendering': DEFAULT_NAMESPACE, + 'in': DEFAULT_NAMESPACE, + 'in2': DEFAULT_NAMESPACE, + 'initialVisibility': DEFAULT_NAMESPACE, + 'intercept': DEFAULT_NAMESPACE, + 'k': DEFAULT_NAMESPACE, + 'k1': DEFAULT_NAMESPACE, + 'k2': DEFAULT_NAMESPACE, + 'k3': DEFAULT_NAMESPACE, + 'k4': DEFAULT_NAMESPACE, + 'kernelMatrix': DEFAULT_NAMESPACE, + 'kernelUnitLength': DEFAULT_NAMESPACE, + 'kerning': DEFAULT_NAMESPACE, + 'keyPoints': DEFAULT_NAMESPACE, + 'keySplines': DEFAULT_NAMESPACE, + 'keyTimes': DEFAULT_NAMESPACE, + 'lang': DEFAULT_NAMESPACE, + 'lengthAdjust': DEFAULT_NAMESPACE, + 'letter-spacing': DEFAULT_NAMESPACE, + 'lighting-color': DEFAULT_NAMESPACE, + 'limitingConeAngle': DEFAULT_NAMESPACE, + 'local': DEFAULT_NAMESPACE, + 'marker-end': DEFAULT_NAMESPACE, + 'marker-mid': DEFAULT_NAMESPACE, + 'marker-start': DEFAULT_NAMESPACE, + 'markerHeight': DEFAULT_NAMESPACE, + 'markerUnits': DEFAULT_NAMESPACE, + 'markerWidth': DEFAULT_NAMESPACE, + 'mask': DEFAULT_NAMESPACE, + 'maskContentUnits': DEFAULT_NAMESPACE, + 'maskUnits': DEFAULT_NAMESPACE, + 'mathematical': DEFAULT_NAMESPACE, + 'max': DEFAULT_NAMESPACE, + 'media': DEFAULT_NAMESPACE, + 'mediaCharacterEncoding': DEFAULT_NAMESPACE, + 'mediaContentEncodings': DEFAULT_NAMESPACE, + 'mediaSize': DEFAULT_NAMESPACE, + 'mediaTime': DEFAULT_NAMESPACE, + 'method': DEFAULT_NAMESPACE, + 'min': DEFAULT_NAMESPACE, + 'mode': DEFAULT_NAMESPACE, + 'name': DEFAULT_NAMESPACE, + 'nav-down': DEFAULT_NAMESPACE, + 'nav-down-left': DEFAULT_NAMESPACE, + 'nav-down-right': DEFAULT_NAMESPACE, + 'nav-left': DEFAULT_NAMESPACE, + 'nav-next': DEFAULT_NAMESPACE, + 'nav-prev': DEFAULT_NAMESPACE, + 'nav-right': DEFAULT_NAMESPACE, + 'nav-up': DEFAULT_NAMESPACE, + 'nav-up-left': DEFAULT_NAMESPACE, + 'nav-up-right': DEFAULT_NAMESPACE, + 'numOctaves': DEFAULT_NAMESPACE, + 'observer': DEFAULT_NAMESPACE, + 'offset': DEFAULT_NAMESPACE, + 'opacity': DEFAULT_NAMESPACE, + 'operator': DEFAULT_NAMESPACE, + 'order': DEFAULT_NAMESPACE, + 'orient': DEFAULT_NAMESPACE, + 'orientation': DEFAULT_NAMESPACE, + 'origin': DEFAULT_NAMESPACE, + 'overflow': DEFAULT_NAMESPACE, + 'overlay': DEFAULT_NAMESPACE, + 'overline-position': DEFAULT_NAMESPACE, + 'overline-thickness': DEFAULT_NAMESPACE, + 'panose-1': DEFAULT_NAMESPACE, + 'path': DEFAULT_NAMESPACE, + 'pathLength': DEFAULT_NAMESPACE, + 'patternContentUnits': DEFAULT_NAMESPACE, + 'patternTransform': DEFAULT_NAMESPACE, + 'patternUnits': DEFAULT_NAMESPACE, + 'phase': DEFAULT_NAMESPACE, + 'playbackOrder': DEFAULT_NAMESPACE, + 'pointer-events': DEFAULT_NAMESPACE, + 'points': DEFAULT_NAMESPACE, + 'pointsAtX': DEFAULT_NAMESPACE, + 'pointsAtY': DEFAULT_NAMESPACE, + 'pointsAtZ': DEFAULT_NAMESPACE, + 'preserveAlpha': DEFAULT_NAMESPACE, + 'preserveAspectRatio': DEFAULT_NAMESPACE, + 'primitiveUnits': DEFAULT_NAMESPACE, + 'propagate': DEFAULT_NAMESPACE, + 'property': DEFAULT_NAMESPACE, + 'r': DEFAULT_NAMESPACE, + 'radius': DEFAULT_NAMESPACE, + 'refX': DEFAULT_NAMESPACE, + 'refY': DEFAULT_NAMESPACE, + 'rel': DEFAULT_NAMESPACE, + 'rendering-intent': DEFAULT_NAMESPACE, + 'repeatCount': DEFAULT_NAMESPACE, + 'repeatDur': DEFAULT_NAMESPACE, + 'requiredExtensions': DEFAULT_NAMESPACE, + 'requiredFeatures': DEFAULT_NAMESPACE, + 'requiredFonts': DEFAULT_NAMESPACE, + 'requiredFormats': DEFAULT_NAMESPACE, + 'resource': DEFAULT_NAMESPACE, + 'restart': DEFAULT_NAMESPACE, + 'result': DEFAULT_NAMESPACE, + 'rev': DEFAULT_NAMESPACE, + 'role': DEFAULT_NAMESPACE, + 'rotate': DEFAULT_NAMESPACE, + 'rx': DEFAULT_NAMESPACE, + 'ry': DEFAULT_NAMESPACE, + 'scale': DEFAULT_NAMESPACE, + 'seed': DEFAULT_NAMESPACE, + 'shape-rendering': DEFAULT_NAMESPACE, + 'slope': DEFAULT_NAMESPACE, + 'snapshotTime': DEFAULT_NAMESPACE, + 'spacing': DEFAULT_NAMESPACE, + 'specularConstant': DEFAULT_NAMESPACE, + 'specularExponent': DEFAULT_NAMESPACE, + 'spreadMethod': DEFAULT_NAMESPACE, + 'startOffset': DEFAULT_NAMESPACE, + 'stdDeviation': DEFAULT_NAMESPACE, + 'stemh': DEFAULT_NAMESPACE, + 'stemv': DEFAULT_NAMESPACE, + 'stitchTiles': DEFAULT_NAMESPACE, + 'stop-color': DEFAULT_NAMESPACE, + 'stop-opacity': DEFAULT_NAMESPACE, + 'strikethrough-position': DEFAULT_NAMESPACE, + 'strikethrough-thickness': DEFAULT_NAMESPACE, + 'string': DEFAULT_NAMESPACE, + 'stroke': DEFAULT_NAMESPACE, + 'stroke-dasharray': DEFAULT_NAMESPACE, + 'stroke-dashoffset': DEFAULT_NAMESPACE, + 'stroke-linecap': DEFAULT_NAMESPACE, + 'stroke-linejoin': DEFAULT_NAMESPACE, + 'stroke-miterlimit': DEFAULT_NAMESPACE, + 'stroke-opacity': DEFAULT_NAMESPACE, + 'stroke-width': DEFAULT_NAMESPACE, + 'surfaceScale': DEFAULT_NAMESPACE, + 'syncBehavior': DEFAULT_NAMESPACE, + 'syncBehaviorDefault': DEFAULT_NAMESPACE, + 'syncMaster': DEFAULT_NAMESPACE, + 'syncTolerance': DEFAULT_NAMESPACE, + 'syncToleranceDefault': DEFAULT_NAMESPACE, + 'systemLanguage': DEFAULT_NAMESPACE, + 'tableValues': DEFAULT_NAMESPACE, + 'target': DEFAULT_NAMESPACE, + 'targetX': DEFAULT_NAMESPACE, + 'targetY': DEFAULT_NAMESPACE, + 'text-anchor': DEFAULT_NAMESPACE, + 'text-decoration': DEFAULT_NAMESPACE, + 'text-rendering': DEFAULT_NAMESPACE, + 'textLength': DEFAULT_NAMESPACE, + 'timelineBegin': DEFAULT_NAMESPACE, + 'title': DEFAULT_NAMESPACE, + 'to': DEFAULT_NAMESPACE, + 'transform': DEFAULT_NAMESPACE, + 'transformBehavior': DEFAULT_NAMESPACE, + 'type': DEFAULT_NAMESPACE, + 'typeof': DEFAULT_NAMESPACE, + 'u1': DEFAULT_NAMESPACE, + 'u2': DEFAULT_NAMESPACE, + 'underline-position': DEFAULT_NAMESPACE, + 'underline-thickness': DEFAULT_NAMESPACE, + 'unicode': DEFAULT_NAMESPACE, + 'unicode-bidi': DEFAULT_NAMESPACE, + 'unicode-range': DEFAULT_NAMESPACE, + 'units-per-em': DEFAULT_NAMESPACE, + 'v-alphabetic': DEFAULT_NAMESPACE, + 'v-hanging': DEFAULT_NAMESPACE, + 'v-ideographic': DEFAULT_NAMESPACE, + 'v-mathematical': DEFAULT_NAMESPACE, + 'values': DEFAULT_NAMESPACE, + 'version': DEFAULT_NAMESPACE, + 'vert-adv-y': DEFAULT_NAMESPACE, + 'vert-origin-x': DEFAULT_NAMESPACE, + 'vert-origin-y': DEFAULT_NAMESPACE, + 'viewBox': DEFAULT_NAMESPACE, + 'viewTarget': DEFAULT_NAMESPACE, + 'visibility': DEFAULT_NAMESPACE, + 'width': DEFAULT_NAMESPACE, + 'widths': DEFAULT_NAMESPACE, + 'word-spacing': DEFAULT_NAMESPACE, + 'writing-mode': DEFAULT_NAMESPACE, + 'x': DEFAULT_NAMESPACE, + 'x-height': DEFAULT_NAMESPACE, + 'x1': DEFAULT_NAMESPACE, + 'x2': DEFAULT_NAMESPACE, + 'xChannelSelector': DEFAULT_NAMESPACE, + 'xlink:actuate': XLINK_NAMESPACE, + 'xlink:arcrole': XLINK_NAMESPACE, + 'xlink:href': XLINK_NAMESPACE, + 'xlink:role': XLINK_NAMESPACE, + 'xlink:show': XLINK_NAMESPACE, + 'xlink:title': XLINK_NAMESPACE, + 'xlink:type': XLINK_NAMESPACE, + 'xml:base': XML_NAMESPACE, + 'xml:id': XML_NAMESPACE, + 'xml:lang': XML_NAMESPACE, + 'xml:space': XML_NAMESPACE, + 'y': DEFAULT_NAMESPACE, + 'y1': DEFAULT_NAMESPACE, + 'y2': DEFAULT_NAMESPACE, + 'yChannelSelector': DEFAULT_NAMESPACE, + 'z': DEFAULT_NAMESPACE, + 'zoomAndPan': DEFAULT_NAMESPACE +}; + +function thisIsSVGTag(tag) { + return SVG_ELEMENTS.hasOwnProperty(tag); +} + +function getSVGNamespace() { + return SVG_NAMESPACE; +} + +function SVGAttributeNamespace(value) { + if (SVG_PROPERTIES.hasOwnProperty(value)) { + return SVG_PROPERTIES[value]; + } +} + +module.exports.thisIsSVGTag = thisIsSVGTag; + +module.exports.getSVGNamespace = getSVGNamespace; + +module.exports.SVGAttributeNamespace = SVGAttributeNamespace; From 1c1ed85a10732b22f507b526d4997c2221c4bdae Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Mon, 15 Feb 2016 15:38:13 -0500 Subject: [PATCH 2/9] Simple SVG unit test --- .../lib/convert-tag-attributes/attributes.js | 24 +++++++++++++++++ test/html-to-vdom/lib/html-to-vdom/index.js | 27 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/test/html-to-vdom/lib/convert-tag-attributes/attributes.js b/test/html-to-vdom/lib/convert-tag-attributes/attributes.js index fd7c62e..ea58f90 100644 --- a/test/html-to-vdom/lib/convert-tag-attributes/attributes.js +++ b/test/html-to-vdom/lib/convert-tag-attributes/attributes.js @@ -45,6 +45,30 @@ describe('convertTagAttributes', function () { }); }); }); + + describe('when converting an svg', function() { + + it('sets them', function() { + var tag = parseHTML('')[0]; + + var converted = convertTagAttributes(tag); + converted.should.eql({ + attributes: { + 'xmlns:rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'xmlns': 'http://www.w3.org/2000/svg', + 'xml:space': 'preserve', + 'height': '180.903', + 'width': '220.34801', + 'version': '1.1', + 'xmlns:cc': 'http://creativecommons.org/ns#', + 'xmlns:dc': 'http://purl.org/dc/elements/1.1/' + } + }); + + }); + + }); + }); }); diff --git a/test/html-to-vdom/lib/html-to-vdom/index.js b/test/html-to-vdom/lib/html-to-vdom/index.js index 2a09fba..bd74073 100644 --- a/test/html-to-vdom/lib/html-to-vdom/index.js +++ b/test/html-to-vdom/lib/html-to-vdom/index.js @@ -173,4 +173,31 @@ describe('html-to-vdom', function () { comment.text.should.eql(''); }); }); + + describe('when converting an svg tag', function () { + it('converts to the fill tag correctly', function () { + var html = '' + + '' + + '' + + '' + + 'image/svg+xml' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + var converted = convertHTML(html); + + converted.tagName.should.eql('svg'); + should.exist(converted.children); + + converted.children.length.should.eql(2); + converted.children[0].tagName.should.eql('metadata'); + converted.children[1].tagName.should.eql('path'); + + }); + }); }); From 66da4187d52df5e6416988d40a5182c4808165ec Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Wed, 17 Feb 2016 10:26:42 -0500 Subject: [PATCH 3/9] SVG unit tests --- test/html-to-vdom/lib/html-to-vdom/index.js | 75 +++++++++++++++------ 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/test/html-to-vdom/lib/html-to-vdom/index.js b/test/html-to-vdom/lib/html-to-vdom/index.js index bd74073..483d405 100644 --- a/test/html-to-vdom/lib/html-to-vdom/index.js +++ b/test/html-to-vdom/lib/html-to-vdom/index.js @@ -175,29 +175,64 @@ describe('html-to-vdom', function () { }); describe('when converting an svg tag', function () { - it('converts to the fill tag correctly', function () { - var html = '' + - '' + - '' + - '' + - 'image/svg+xml' + - '' + - '' + - '' + - '' + - '' + - '' + - ''; - var converted = convertHTML(html); + var svg1 = convertHTML( + '' + + '' + + '' + + '' + + 'image/svg+xml' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); + + var svg2 = convertHTML( + '' + + 'Menu' + + '' + + ''); - converted.tagName.should.eql('svg'); - should.exist(converted.children); + it('converts to the fill tag correctly', function () { + svg1.tagName.should.eql('svg'); + should.exist(svg1.children); - converted.children.length.should.eql(2); - converted.children[0].tagName.should.eql('metadata'); - converted.children[1].tagName.should.eql('path'); + svg1.children.length.should.eql(2); + svg1.children[0].tagName.should.eql('metadata'); + svg1.children[1].tagName.should.eql('path'); + }); + + it('processes attributes correctly', function() { + svg2.tagName.should.eql('svg'); + var attrs = svg2.properties.attributes; + + console.log(JSON.stringify(svg2)); + attrs.height.should.eql("32px"); + attrs.width.should.eql("32px"); + attrs['aria-labelledby'].should.eql("navigation-svg-title"); + attrs.viewBox.should.eql("0 0 32 32"); + + svg2.properties.style['enable-background'].should.eql("new 0 0 32 32"); + }); + + it('processes title and path correctly', function() { + should.exist(svg2.children); + svg2.children.length.should.eql(2); + var title = svg2.children[0] + var path = svg2.children[1] + + title.tagName.should.eql('title'); + title.properties.id.should.eql('navigation-svg-title'); + + path.tagName.should.eql('path'); + path.properties.attributes['class'].should.eql('c-small-navigation__icon') + path.properties.attributes.d.should.eql('M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z'); + console.log(JSON.stringify(path)); }); + }); }); From 39c793763683f95d73db81013c694ce197fdff3f Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Wed, 17 Feb 2016 10:49:03 -0500 Subject: [PATCH 4/9] Cleaned up some lint warning before refactoring --- lib/htmlparser-to-vdom.js | 56 ++++++++++++--------- lib/svg-attribute-hook.js | 4 +- test/html-to-vdom/lib/html-to-vdom/index.js | 14 +++--- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/lib/htmlparser-to-vdom.js b/lib/htmlparser-to-vdom.js index 8a6d9c6..00f315c 100644 --- a/lib/htmlparser-to-vdom.js +++ b/lib/htmlparser-to-vdom.js @@ -30,40 +30,46 @@ module.exports = function createConverter (VNode, VText) { }); if(thisIsSVGTag(tag.name)) { - var _attributes = attributes.attributes; + return convertSvg(tag, attributes, children, key); + } - for(var _key in _attributes) { - if (!_attributes.hasOwnProperty(_key)) { - continue; - } + return new VNode(tag.name, attributes, children, key); + } + }; - var namespace = SVGAttributeNamespace(_key); + function convertSvg(tag, attributes, children, key) { + var _attributes = attributes.attributes; - if (namespace === void 0) { // not a svg attribute - continue; - } + for(var _key in _attributes) { + if (!_attributes.hasOwnProperty(_key)) { + continue; + } - var value = _attributes[_key]; + var namespace = SVGAttributeNamespace(_key); - if (typeof value !== 'string' && - typeof value !== 'number' && - typeof value !== 'boolean' - ) { - continue; - } + if (namespace === void 0) { // not a svg attribute + continue; + } - if (namespace !== null) { // namespaced attribute - attributes[_key] = SVGAttributeHook(namespace, value); - _attributes[_key] = void 0; - continue; - } - } + var value = _attributes[_key]; - return new VNode(tag.name, attributes, children, key, getSVGNamespace()); + if (typeof value !== 'string' && + typeof value !== 'number' && + typeof value !== 'boolean' + ) { + continue; } - return new VNode(tag.name, attributes, children, key); + if (namespace !== null) { // namespaced attribute + attributes[_key] = SVGAttributeHook(namespace, value); + _attributes[_key] = void 0; + continue; + } } - }; + + return new VNode(tag.name, attributes, children, key, getSVGNamespace()); + } + + return converter; }; diff --git a/lib/svg-attribute-hook.js b/lib/svg-attribute-hook.js index 64347a3..28933cc 100644 --- a/lib/svg-attribute-hook.js +++ b/lib/svg-attribute-hook.js @@ -1,7 +1,5 @@ 'use strict'; -module.exports = AttributeHook; - function AttributeHook(namespace, value) { if (!(this instanceof AttributeHook)) { return new AttributeHook(namespace, value); @@ -33,3 +31,5 @@ AttributeHook.prototype.unhook = function (node, prop, next) { }; AttributeHook.prototype.type = 'AttributeHook'; + +module.exports = AttributeHook; diff --git a/test/html-to-vdom/lib/html-to-vdom/index.js b/test/html-to-vdom/lib/html-to-vdom/index.js index 483d405..3f68cb3 100644 --- a/test/html-to-vdom/lib/html-to-vdom/index.js +++ b/test/html-to-vdom/lib/html-to-vdom/index.js @@ -210,13 +210,12 @@ describe('html-to-vdom', function () { var attrs = svg2.properties.attributes; - console.log(JSON.stringify(svg2)); - attrs.height.should.eql("32px"); - attrs.width.should.eql("32px"); - attrs['aria-labelledby'].should.eql("navigation-svg-title"); - attrs.viewBox.should.eql("0 0 32 32"); + attrs.height.should.eql('32px'); + attrs.width.should.eql('32px'); + attrs['aria-labelledby'].should.eql('navigation-svg-title'); + attrs.viewBox.should.eql('0 0 32 32'); - svg2.properties.style['enable-background'].should.eql("new 0 0 32 32"); + svg2.properties.style['enable-background'].should.eql('new 0 0 32 32'); }); it('processes title and path correctly', function() { @@ -224,14 +223,13 @@ describe('html-to-vdom', function () { svg2.children.length.should.eql(2); var title = svg2.children[0] var path = svg2.children[1] - + title.tagName.should.eql('title'); title.properties.id.should.eql('navigation-svg-title'); path.tagName.should.eql('path'); path.properties.attributes['class'].should.eql('c-small-navigation__icon') path.properties.attributes.d.should.eql('M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z'); - console.log(JSON.stringify(path)); }); }); From 48b35dfb5e4179cd53ae6f77937fc4dbc287d682 Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Thu, 18 Feb 2016 10:24:32 -0500 Subject: [PATCH 5/9] More modular refactoring --- lib/convert-tag-attributes.js | 277 +----------------- lib/get-property-info/svg/index.js | 21 ++ .../svg}/svg-attribute-hook.js | 0 .../svg}/svg-namespaces.js | 21 +- lib/htmlparser-to-vdom.js | 12 +- 5 files changed, 49 insertions(+), 282 deletions(-) create mode 100644 lib/get-property-info/svg/index.js rename lib/{ => get-property-info/svg}/svg-attribute-hook.js (100%) rename lib/{ => get-property-info/svg}/svg-namespaces.js (97%) diff --git a/lib/convert-tag-attributes.js b/lib/convert-tag-attributes.js index 6c8e48f..c59c7c4 100644 --- a/lib/convert-tag-attributes.js +++ b/lib/convert-tag-attributes.js @@ -1,277 +1,32 @@ -/* - Adapted from https://github.com/facebook/react/blob/c265504fe2fdeadf0e5358879a3c141628b37a23/src/renderers/dom/shared/HTMLDOMPropertyConfig.js - */ -var decode = require('ent').decode; - -var MUST_USE_ATTRIBUTE = 0x1; -var MUST_USE_PROPERTY = 0x2; -var HAS_BOOLEAN_VALUE = 0x8; -var HAS_NUMERIC_VALUE = 0x10; -var HAS_POSITIVE_NUMERIC_VALUE = 0x20 | 0x10; -var HAS_OVERLOADED_BOOLEAN_VALUE = 0x40; - -function checkMask(value, bitmask) { - return (value & bitmask) === bitmask; -} - -var isCustomAttribute = RegExp.prototype.test.bind( - /^(data|aria)-[a-z_][a-z\d_.\-]*$/ -); - -var HTMLDOMPropertyConfig = { - - Properties: { - /** - * Standard Properties - */ - accept: null, - acceptCharset: null, - accessKey: null, - action: null, - allowFullScreen: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, - allowTransparency: MUST_USE_ATTRIBUTE, - alt: null, - async: HAS_BOOLEAN_VALUE, - autoComplete: null, - autoFocus: HAS_BOOLEAN_VALUE, - autoPlay: HAS_BOOLEAN_VALUE, - capture: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, - cellPadding: null, - cellSpacing: null, - charSet: MUST_USE_ATTRIBUTE, - challenge: MUST_USE_ATTRIBUTE, - checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - classID: MUST_USE_ATTRIBUTE, - // To set className on SVG elements, it's necessary to use .setAttribute; - // this works on HTML elements too in all browsers except IE8. - className: MUST_USE_ATTRIBUTE, - cols: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE, - colSpan: null, - content: null, - contentEditable: null, - contextMenu: MUST_USE_ATTRIBUTE, - controls: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - coords: null, - crossOrigin: null, - data: null, // For `` acts as `src`. - dateTime: MUST_USE_ATTRIBUTE, - defer: HAS_BOOLEAN_VALUE, - dir: null, - disabled: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, - download: HAS_OVERLOADED_BOOLEAN_VALUE, - draggable: null, - encType: null, - form: MUST_USE_ATTRIBUTE, - formAction: MUST_USE_ATTRIBUTE, - formEncType: MUST_USE_ATTRIBUTE, - formMethod: MUST_USE_ATTRIBUTE, - formNoValidate: HAS_BOOLEAN_VALUE, - formTarget: MUST_USE_ATTRIBUTE, - frameBorder: MUST_USE_ATTRIBUTE, - headers: null, - height: MUST_USE_ATTRIBUTE, - hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, - high: null, - href: null, - hrefLang: null, - htmlFor: null, - httpEquiv: null, - icon: null, - id: MUST_USE_PROPERTY, - is: MUST_USE_ATTRIBUTE, - keyParams: MUST_USE_ATTRIBUTE, - keyType: MUST_USE_ATTRIBUTE, - label: null, - lang: null, - list: MUST_USE_ATTRIBUTE, - loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - low: null, - manifest: MUST_USE_ATTRIBUTE, - marginHeight: null, - marginWidth: null, - max: null, - maxLength: MUST_USE_ATTRIBUTE, - media: MUST_USE_ATTRIBUTE, - mediaGroup: null, - method: null, - min: null, - minLength: MUST_USE_ATTRIBUTE, - multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - name: null, - noValidate: HAS_BOOLEAN_VALUE, - open: HAS_BOOLEAN_VALUE, - optimum: null, - pattern: null, - placeholder: null, - poster: null, - preload: null, - radioGroup: null, - readOnly: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - rel: null, - required: HAS_BOOLEAN_VALUE, - role: MUST_USE_ATTRIBUTE, - rows: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE, - rowSpan: null, - sandbox: null, - scope: null, - scoped: HAS_BOOLEAN_VALUE, - scrolling: null, - seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, - selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - shape: null, - size: MUST_USE_ATTRIBUTE | HAS_POSITIVE_NUMERIC_VALUE, - sizes: MUST_USE_ATTRIBUTE, - span: HAS_POSITIVE_NUMERIC_VALUE, - spellCheck: null, - src: null, - srcDoc: MUST_USE_PROPERTY, - srcSet: MUST_USE_ATTRIBUTE, - start: HAS_NUMERIC_VALUE, - step: null, - style: null, - tabIndex: null, - target: null, - title: null, - type: null, - useMap: null, - value: MUST_USE_PROPERTY, - width: MUST_USE_ATTRIBUTE, - wmode: MUST_USE_ATTRIBUTE, - - /** - * Non-standard Properties - */ - // autoCapitalize and autoCorrect are supported in Mobile Safari for - // keyboard hints. - autoCapitalize: null, - autoCorrect: null, - // itemProp, itemScope, itemType are for - // Microdata support. See http://schema.org/docs/gs.html - itemProp: MUST_USE_ATTRIBUTE, - itemScope: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE, - itemType: MUST_USE_ATTRIBUTE, - // itemID and itemRef are for Microdata support as well but - // only specified in the the WHATWG spec document. See - // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api - itemID: MUST_USE_ATTRIBUTE, - itemRef: MUST_USE_ATTRIBUTE, - // property is supported for OpenGraph in meta tags. - property: null, - // IE-only attribute that controls focus behavior - unselectable: MUST_USE_ATTRIBUTE - } -}; - -var parseStyles = function(input) { - var attributes = input.split(';'); - var styles = attributes.reduce(function(object, attribute){ - var entry = attribute.split(/:(.+)/); - if (entry[0] && entry[1]) { - object[entry[0].trim()] = entry[1].trim(); - } - return object; - },{}); - return styles; -}; - -var propertyToAttributeMapping = { - 'className': 'class', - 'htmlFor': 'for', - 'httpEquiv': 'http-equiv', - 'acceptCharset': 'accept-charset' -}; - -var propertyValueConversions = { - 'style': parseStyles, - 'placeholder': decode, - 'title': decode, - 'alt': decode +var getPropertyInfo = require('./get-property-info/htmldom'); +var propertySetters = require('./property-setters'); + +var getPropertySetter = function (propInfo) { + if (propInfo.mustUseAttribute) { + return propertySetters.attribute; + } + else { + // Anything we don't set as an attribute is treated as a property + return propertySetters.property; + } }; -var getPropertyInfo = (function () { - var propInfoByAttributeName = {}; - - Object.keys(HTMLDOMPropertyConfig.Properties).forEach(function (propName) { - var propConfig = HTMLDOMPropertyConfig.Properties[propName]; - var attributeName = propertyToAttributeMapping[propName] || propName.toLowerCase(); - - var propertyInfo = { - attributeName: attributeName, - propertyName: propName, - - mustUseAttribute: checkMask(propConfig, MUST_USE_ATTRIBUTE), - mustUseProperty: checkMask(propConfig, MUST_USE_PROPERTY), - hasBooleanValue: checkMask(propConfig, HAS_BOOLEAN_VALUE), - hasNumericValue: checkMask(propConfig, HAS_NUMERIC_VALUE), - hasPositiveNumericValue: - checkMask(propConfig, HAS_POSITIVE_NUMERIC_VALUE), - hasOverloadedBooleanValue: - checkMask(propConfig, HAS_OVERLOADED_BOOLEAN_VALUE), - }; - - propInfoByAttributeName[attributeName] = propertyInfo; - }); - - return function (attributeName) { - return propInfoByAttributeName[attributeName]; - }; -})(); - - var convertTagAttributes = function (tag) { var attributes = tag.attribs; - var vdomProperties = { + var vNodeProperties = { attributes: {} }; Object.keys(attributes).forEach(function (attributeName) { - var lowerCased = attributeName.toLowerCase(); - var propInfo = getPropertyInfo(lowerCased); - var value = attributes[attributeName]; - if (isCustomAttribute(attributeName) || !propInfo) { - vdomProperties.attributes[attributeName] = value; - return; - } - - var valueConverter = propertyValueConversions[propInfo.propertyName]; - if (valueConverter) { - value = valueConverter(value); - } - - if (propInfo.mustUseAttribute) { - if (propInfo.hasBooleanValue) { - // Boolean attributes come in as an empty string or the - vdomProperties.attributes[propInfo.attributeName] = ''; - } - else { - vdomProperties.attributes[propInfo.attributeName] = value; - } - } - // Anything we don't set as an attribute is treated as a property - else { - var isTrue; - if (propInfo.hasBooleanValue) { - isTrue = (value === '' || value.toLowerCase() === propInfo.attributeName); - vdomProperties[propInfo.propertyName] = isTrue ? true : false; - } - else if (propInfo.hasOverloadedBooleanValue) { - isTrue = (value === ''); - vdomProperties[propInfo.propertyName] = isTrue ? true : value; - } - else if (propInfo.hasNumericValue || propInfo.hasPositiveNumericValue) { - vdomProperties[propInfo.propertyName] = Number(value); - } - else { - vdomProperties[propInfo.propertyName] = value; - } - } + var propInfo = getPropertyInfo(attributeName); + var propertySetter = getPropertySetter(propInfo); + propertySetter.set(vNodeProperties, propInfo, value); }); - return vdomProperties; + return vNodeProperties; }; module.exports = convertTagAttributes; diff --git a/lib/get-property-info/svg/index.js b/lib/get-property-info/svg/index.js new file mode 100644 index 0000000..73bebeb --- /dev/null +++ b/lib/get-property-info/svg/index.js @@ -0,0 +1,21 @@ +var SVG_NAMESPACE = require('./svg-namespaces.js').SVG_NAMESPACE; +var SVG_ELEMENTS = require('./svg-namespaces.js').SVG_ELEMENTS; +var SVG_PROPERTIES = require('./svg-namespaces.js').SVG_PROPERTIES; + +function thisIsSVGTag(tag) { + return SVG_ELEMENTS.hasOwnProperty(tag); +} + +function getSVGNamespace() { + return SVG_NAMESPACE; +} + +function SVGAttributeNamespace(value) { + if (SVG_PROPERTIES.hasOwnProperty(value)) { + return SVG_PROPERTIES[value]; + } +} + +module.exports.thisIsSVGTag = thisIsSVGTag; +module.exports.getSVGNamespace = getSVGNamespace; +module.exports.SVGAttributeNamespace = SVGAttributeNamespace; diff --git a/lib/svg-attribute-hook.js b/lib/get-property-info/svg/svg-attribute-hook.js similarity index 100% rename from lib/svg-attribute-hook.js rename to lib/get-property-info/svg/svg-attribute-hook.js diff --git a/lib/svg-namespaces.js b/lib/get-property-info/svg/svg-namespaces.js similarity index 97% rename from lib/svg-namespaces.js rename to lib/get-property-info/svg/svg-namespaces.js index 7f5cbb4..6ddc7b5 100644 --- a/lib/svg-namespaces.js +++ b/lib/get-property-info/svg/svg-namespaces.js @@ -395,22 +395,15 @@ var SVG_PROPERTIES = { 'zoomAndPan': DEFAULT_NAMESPACE }; -function thisIsSVGTag(tag) { - return SVG_ELEMENTS.hasOwnProperty(tag); -} +module.exports.SVG_NAMESPACE = SVG_NAMESPACE; +module.exports.SVG_ELEMENTS = SVG_ELEMENTS; +module.exports.SVG_PROPERTIES = SVG_PROPERTIES; + + + + -function getSVGNamespace() { - return SVG_NAMESPACE; -} -function SVGAttributeNamespace(value) { - if (SVG_PROPERTIES.hasOwnProperty(value)) { - return SVG_PROPERTIES[value]; - } -} -module.exports.thisIsSVGTag = thisIsSVGTag; -module.exports.getSVGNamespace = getSVGNamespace; -module.exports.SVGAttributeNamespace = SVGAttributeNamespace; diff --git a/lib/htmlparser-to-vdom.js b/lib/htmlparser-to-vdom.js index 00f315c..019e4b8 100644 --- a/lib/htmlparser-to-vdom.js +++ b/lib/htmlparser-to-vdom.js @@ -1,9 +1,7 @@ var decode = require('ent').decode; var convertTagAttributes = require('./convert-tag-attributes'); -var thisIsSVGTag = require('./svg-namespaces').thisIsSVGTag, - getSVGNamespace = require('./svg-namespaces').getSVGNamespace, - SVGAttributeNamespace = require('./svg-namespaces').SVGAttributeNamespace, - SVGAttributeHook = require('./svg-attribute-hook'); +var svg = require('./get-property-info/svg'); +var SVGAttributeHook = require('./get-property-info/svg/svg-attribute-hook'); module.exports = function createConverter (VNode, VText) { var converter = { @@ -29,7 +27,7 @@ module.exports = function createConverter (VNode, VText) { return converter.convert(node, getVNodeKey); }); - if(thisIsSVGTag(tag.name)) { + if(svg.thisIsSVGTag(tag.name)) { return convertSvg(tag, attributes, children, key); } @@ -45,7 +43,7 @@ module.exports = function createConverter (VNode, VText) { continue; } - var namespace = SVGAttributeNamespace(_key); + var namespace = svg.SVGAttributeNamespace(_key); if (namespace === void 0) { // not a svg attribute continue; @@ -67,7 +65,7 @@ module.exports = function createConverter (VNode, VText) { } } - return new VNode(tag.name, attributes, children, key, getSVGNamespace()); + return new VNode(tag.name, attributes, children, key, svg.getSVGNamespace()); } From c949ce137c6ece11c3a5ba639680f0ad8b32f3d6 Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Thu, 18 Feb 2016 13:28:57 -0500 Subject: [PATCH 6/9] Lint --- test/html-to-vdom/lib/html-to-vdom/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/html-to-vdom/lib/html-to-vdom/index.js b/test/html-to-vdom/lib/html-to-vdom/index.js index 3f68cb3..b4c2172 100644 --- a/test/html-to-vdom/lib/html-to-vdom/index.js +++ b/test/html-to-vdom/lib/html-to-vdom/index.js @@ -221,14 +221,14 @@ describe('html-to-vdom', function () { it('processes title and path correctly', function() { should.exist(svg2.children); svg2.children.length.should.eql(2); - var title = svg2.children[0] - var path = svg2.children[1] + var title = svg2.children[0]; + var path = svg2.children[1]; title.tagName.should.eql('title'); title.properties.id.should.eql('navigation-svg-title'); path.tagName.should.eql('path'); - path.properties.attributes['class'].should.eql('c-small-navigation__icon') + path.properties.attributes['class'].should.eql('c-small-navigation__icon'); path.properties.attributes.d.should.eql('M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z'); }); From ce7f7d9f9e89b45bfca5d37097aedd3095c95b06 Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Tue, 20 Sep 2016 11:24:27 -0400 Subject: [PATCH 7/9] Version 0.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73bdbf9..ee0ed89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-to-vdom", - "version": "0.7.0", + "version": "0.7.1", "description": "Converts html into a vtree", "main": "index.js", "scripts": { From 0a4d5ec55cf4c60d18938e80fa724bb19d7c31fa Mon Sep 17 00:00:00 2001 From: Shaun Patterson Date: Tue, 20 Sep 2016 11:25:39 -0400 Subject: [PATCH 8/9] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee0ed89..56da1f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-to-vdom", - "version": "0.7.1", + "version": "0.7.2", "description": "Converts html into a vtree", "main": "index.js", "scripts": { From e65af81ee26dd619caf30c5a14512d0b91cf359e Mon Sep 17 00:00:00 2001 From: Justin Maat Date: Sat, 21 Jan 2017 15:58:51 -0500 Subject: [PATCH 9/9] Small changes to increase coverage and lint errors (#1) * -fixing lint errors -adding tests * fixing test * removing unncessary attribute checks --- lib/htmlparser-to-vdom.js | 56 ++++------ .../svg/svg-attribute-hook.js | 102 ++++++++++++++++++ 2 files changed, 124 insertions(+), 34 deletions(-) create mode 100644 test/get-property-info/svg/svg-attribute-hook.js diff --git a/lib/htmlparser-to-vdom.js b/lib/htmlparser-to-vdom.js index 019e4b8..9761687 100644 --- a/lib/htmlparser-to-vdom.js +++ b/lib/htmlparser-to-vdom.js @@ -4,6 +4,28 @@ var svg = require('./get-property-info/svg'); var SVGAttributeHook = require('./get-property-info/svg/svg-attribute-hook'); module.exports = function createConverter (VNode, VText) { + function convertSvg(tag, attributes, children, key) { + var _attributes = attributes.attributes; + + for(var _key in _attributes) { + var namespace = svg.SVGAttributeNamespace(_key); + + if (namespace === void 0) { // not a svg attribute + continue; + } + + var value = _attributes[_key]; + + if (namespace !== null) { // namespaced attribute + attributes[_key] = new SVGAttributeHook(namespace, value); + _attributes[_key] = void 0; + continue; + } + } + + return new VNode(tag.name, attributes, children, key, svg.getSVGNamespace()); + } + var converter = { convert: function (node, getVNodeKey) { if (node.type === 'tag' || node.type === 'script' || node.type === 'style') { @@ -35,39 +57,5 @@ module.exports = function createConverter (VNode, VText) { } }; - function convertSvg(tag, attributes, children, key) { - var _attributes = attributes.attributes; - - for(var _key in _attributes) { - if (!_attributes.hasOwnProperty(_key)) { - continue; - } - - var namespace = svg.SVGAttributeNamespace(_key); - - if (namespace === void 0) { // not a svg attribute - continue; - } - - var value = _attributes[_key]; - - if (typeof value !== 'string' && - typeof value !== 'number' && - typeof value !== 'boolean' - ) { - continue; - } - - if (namespace !== null) { // namespaced attribute - attributes[_key] = SVGAttributeHook(namespace, value); - _attributes[_key] = void 0; - continue; - } - } - - return new VNode(tag.name, attributes, children, key, svg.getSVGNamespace()); - } - - return converter; }; diff --git a/test/get-property-info/svg/svg-attribute-hook.js b/test/get-property-info/svg/svg-attribute-hook.js new file mode 100644 index 0000000..9f0e44b --- /dev/null +++ b/test/get-property-info/svg/svg-attribute-hook.js @@ -0,0 +1,102 @@ +var SVGAttributeHook = require('../../../lib/get-property-info/svg/svg-attribute-hook'); + + +describe('SVGAttributeHook', function () { + var testNamespace = "http://www.testnamespace.com"; + var testValue = "test-value"; + var attributeHook = SVGAttributeHook(testNamespace, testValue); + + it('creates new AttributeBook with namespace and value', function () { + attributeHook.namespace.should.equal(testNamespace) + attributeHook.value.should.equal(testValue) + }); + + describe('hook', function() { + context('prev is the same AttributeHook', function() { + it('does nothing', function() { + var prev = attributeHook; + var node = {}; + var prop = {}; + + var hooked = attributeHook.hook(node, prop, prev) + + should.equal(hooked, undefined) + }); + }); + + context('prev is different AttributeHook', function() { + it('sets the attribute to the input node', function() { + var otherNamespace = "othernamespace"; + var otherValue = "otherValue"; + var testProp = "testProp"; + var prev = SVGAttributeHook(otherNamespace, otherValue); + var node = { + setAttributeNS: function(namespace, prop, value) { + this.namespace = namespace; + this.value = value; + this.prop = prop; + }, + namespace: null, + value: null, + prop: null, + }; + + attributeHook.hook(node, testProp, prev) + + node.namespace.should.equal(testNamespace) + node.value.should.equal(testValue) + node.prop.should.equal(testProp) + }); + }); + }); + + describe('unhook', function() { + context('node is has same namespace as next', function() { + it('does nothing', function() { + var next = attributeHook; + var node = {}; + var prop = {}; + + var hooked = attributeHook.unhook(node, prop, next) + + should.equal(hooked, undefined) + }); + }); + + context('node has different namespace as next', function() { + it('removes attribute namespace from the node', function() { + var otherNamespace = "othernamespace"; + var otherValue = "otherValue"; + var testProp = "color:red"; + var next = SVGAttributeHook(otherNamespace, otherValue); + var node = { + removeAttributeNS: function(namespace, name) { + this.namespace = name; + }, + namespace: testNamespace + }; + + attributeHook.unhook(node, testProp, next) + + node.namespace.should.equal('red') + }); + + it('removes attribute namespace without colon in prop from the node', function() { + var otherNamespace = "othernamespace"; + var otherValue = "otherValue"; + var testProp = "color"; + var next = SVGAttributeHook(otherNamespace, otherValue); + var node = { + removeAttributeNS: function(namespace, name) { + this.namespace = name; + }, + namespace: testNamespace + }; + + attributeHook.unhook(node, testProp, next) + + node.namespace.should.equal('color') + }); + }); + }); +});