Skip to content
This repository has been archived by the owner on Dec 22, 2023. It is now read-only.

Commit

Permalink
Merge pull request #45 from aknuds1/attribute-mapping
Browse files Browse the repository at this point in the history
Attribute mapping.
  • Loading branch information
aknuds1 committed Sep 23, 2016
2 parents a74e7b2 + 4096f4a commit 01952fa
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 49 deletions.
3 changes: 1 addition & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ module.exports = {
Parser: parser,
ProcessingInstructions: processingInstructions,
IsValidNodeDefinitions: isValidNodeDefinitions,
ProcessNodeDefinitions: processNodeDefinitions
ProcessNodeDefinitions: processNodeDefinitions,
};

81 changes: 81 additions & 0 deletions lib/camel-case-attribute-names.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// These are all sourced from https://facebook.github.io/react/docs/tags-and-attributes.html -
// all attributes regardless of whether they have a different case to their HTML equivalents are
// listed to reduce the chance of human error and make it easier to just copy-paste the new list if
// it changes.
'use strict';
var HTML_ATTRIBUTES = [
'accept', 'acceptCharset', 'accessKey', 'action', 'allowFullScreen', 'allowTransparency',
'alt', 'async', 'autoComplete', 'autoFocus', 'autoPlay', 'capture', 'cellPadding',
'cellSpacing', 'challenge', 'charSet', 'checked', 'cite', 'classID', 'className',
'colSpan', 'cols', 'content', 'contentEditable', 'contextMenu', 'controls', 'coords',
'crossOrigin', 'data', 'dateTime', 'default', 'defer', 'dir', 'disabled', 'download',
'draggable', 'encType', 'form', 'formAction', 'formEncType', 'formMethod', 'formNoValidate',
'formTarget', 'frameBorder', 'headers', 'height', 'hidden', 'high', 'href', 'hrefLang',
'htmlFor', 'httpEquiv', 'icon', 'id', 'inputMode', 'integrity', 'is', 'keyParams', 'keyType',
'kind', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginHeight', 'marginWidth',
'max', 'maxLength', 'media', 'mediaGroup', 'method', 'min', 'minLength', 'multiple', 'muted',
'name', 'noValidate', 'nonce', 'open', 'optimum', 'pattern', 'placeholder', 'poster',
'preload', 'profile', 'radioGroup', 'readOnly', 'rel', 'required', 'reversed', 'role',
'rowSpan', 'rows', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected',
'shape', 'size', 'sizes', 'span', 'spellCheck', 'src', 'srcDoc', 'srcLang', 'srcSet', 'start',
'step', 'style', 'summary', 'tabIndex', 'target', 'title', 'type', 'useMap', 'value', 'width',
'wmode', 'wrap',
];

var NON_STANDARD_ATTRIBUTES = [
'autoCapitalize', 'autoCorrect', 'color', 'itemProp', 'itemScope', 'itemType', 'itemRef',
'itemID', 'security', 'unselectable', 'results', 'autoSave',
];

var SVG_ATTRIBUTES = [
'accentHeight', 'accumulate', 'additive', 'alignmentBaseline', 'allowReorder', 'alphabetic',
'amplitude', 'arabicForm', 'ascent', 'attributeName', 'attributeType', 'autoReverse',
'azimuth', 'baseFrequency', 'baseProfile', 'baselineShift', 'bbox', 'begin', 'bias', 'by',
'calcMode', 'capHeight', 'clip', 'clipPath', 'clipPathUnits', 'clipRule', 'colorInterpolation',
'colorInterpolationFilters', 'colorProfile', 'colorRendering', 'contentScriptType',
'contentStyleType', 'cursor', 'cx', 'cy', 'd', 'decelerate', 'descent', 'diffuseConstant',
'direction', 'display', 'divisor', 'dominantBaseline', 'dur', 'dx', 'dy', 'edgeMode',
'elevation', 'enableBackground', 'end', 'exponent', 'externalResourcesRequired', 'fill',
'fillOpacity', 'fillRule', 'filter', 'filterRes', 'filterUnits', 'floodColor', 'floodOpacity',
'focusable', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch', 'fontStyle',
'fontVariant', 'fontWeight', 'format', 'from', 'fx', 'fy', 'g1', 'g2', 'glyphName',
'glyphOrientationHorizontal', 'glyphOrientationVertical', 'glyphRef', 'gradientTransform',
'gradientUnits', 'hanging', 'horizAdvX', 'horizOriginX', 'ideographic', 'imageRendering',
'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kernelMatrix', 'kernelUnitLength',
'kerning', 'keyPoints', 'keySplines', 'keyTimes', 'lengthAdjust', 'letterSpacing',
'lightingColor', 'limitingConeAngle', 'local', 'markerEnd', 'markerHeight', 'markerMid',
'markerStart', 'markerUnits', 'markerWidth', 'mask', 'maskContentUnits', 'maskUnits',
'mathematical', 'mode', 'numOctaves', 'offset', 'opacity', 'operator', 'order',
'orient', 'orientation', 'origin', 'overflow', 'overlinePosition', 'overlineThickness',
'paintOrder', 'panose1', 'pathLength', 'patternContentUnits', 'patternTransform',
'patternUnits', 'pointerEvents', 'points', 'pointsAtX', 'pointsAtY', 'pointsAtZ',
'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits', 'r', 'radius', 'refX', 'refY',
'renderingIntent', 'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures',
'restart', 'result', 'rotate', 'rx', 'ry', 'scale', 'seed', 'shapeRendering', 'slope',
'spacing', 'specularConstant', 'specularExponent', 'speed', 'spreadMethod', 'startOffset',
'stdDeviation', 'stemh', 'stemv', 'stitchTiles', 'stopColor', 'stopOpacity',
'strikethroughPosition', 'strikethroughThickness', 'string', 'stroke', 'strokeDasharray',
'strokeDashoffset', 'strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit',
'strokeOpacity', 'strokeWidth', 'surfaceScale', 'systemLanguage', 'tableValues', 'targetX',
'targetY', 'textAnchor', 'textDecoration', 'textLength', 'textRendering', 'to', 'transform',
'u1', 'u2', 'underlinePosition', 'underlineThickness', 'unicode', 'unicodeBidi',
'unicodeRange', 'unitsPerEm', 'vAlphabetic', 'vHanging', 'vIdeographic', 'vMathematical',
'values', 'vectorEffect', 'version', 'vertAdvY', 'vertOriginX', 'vertOriginY', 'viewBox',
'viewTarget', 'visibility', 'widths', 'wordSpacing', 'writingMode', 'x', 'x1', 'x2',
'xChannelSelector', 'xHeight', 'xlinkActuate', 'xlinkArcrole', 'xlinkHref', 'xlinkRole',
'xlinkShow', 'xlinkTitle', 'xlinkType', 'xmlBase', 'xmlLang', 'xmlSpace', 'y', 'y1', 'y2',
'yChannelSelector', 'z', 'zoomAndPan',
];

var camelCaseMap = HTML_ATTRIBUTES
.concat(NON_STANDARD_ATTRIBUTES)
.concat(SVG_ATTRIBUTES)
.reduce(function (soFar, attr) {
var lower = attr.toLowerCase();
if (lower !== attr) {
soFar[lower] = attr;
}
return soFar;
}, {});

module.exports = camelCaseMap;
24 changes: 13 additions & 11 deletions lib/parser.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
'use strict';

var compact = require('lodash.compact');
var find = require('lodash.find');
var map = require('lodash.map');
var htmlParser = require('htmlparser2');
Expand All @@ -18,29 +16,33 @@ var Html2React = function(React, options) {
var traverseDom = function(node, isValidNode, processingInstructions, index) {
if (isValidNode(node)) {
var processingInstruction = find(processingInstructions || [],
function (processingInstruction) {
return processingInstruction.shouldProcessNode(node);
});
function (processingInstruction) {
return processingInstruction.shouldProcessNode(node);
});
if (processingInstruction != null) {
var children = compact(map(node.children, function (child, i) {
var children = map(node.children || [], function (child, i) {
return traverseDom(child, isValidNode, processingInstructions, i);
}));
}).filter(function (x) {
return x != null;
});
return processingInstruction.processNode(node, children, index);
} else {
return false;
}
} else {
return false;
return false;
}
};

var parseWithInstructions = function(html, isValidNode, processingInstructions) {
var domTree = parseHtmlToTree(html);
// TODO: Deal with HTML that contains more than one root level node
if (domTree && domTree.length !== 1) {
throw new Error('html-to-react currently only supports HTML with one single root element. ' +
'The HTML provided contains ' + domTree.length + ' root elements. You can fix that by simply wrapping your HTML ' +
'in a <div> element.');
throw new Error(
'html-to-react currently only supports HTML with one single root element. ' +
'The HTML provided contains ' + domTree.length +
' root elements. You can fix that by simply wrapping your HTML ' +
'in a <div> element.');
}
return traverseDom(domTree[0], isValidNode, processingInstructions, 0);
};
Expand Down
36 changes: 19 additions & 17 deletions lib/process-node-definitions.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
'use strict';

var isEmpty = require('lodash.isempty');
var map = require('lodash.map');
var fromPairs = require('lodash.frompairs');
var camelCase = require('lodash.camelcase');
var forEach = require('lodash.foreach');
var includes = require('lodash.includes');
var merge = require('lodash.merge');
var ent = require('ent');
var camelCaseAttrMap = require('./camel-case-attribute-names');

// https://github.com/facebook/react/blob/0.14-stable/src/renderers/dom/shared/ReactDOMComponent.js#L457
// https://github.com/facebook/react/blob/15.0-stable/src/renderers/dom/shared/ReactDOMComponent.js#L457
var voidElementTags = [
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr', 'textarea',
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param',
'source', 'track', 'wbr', 'menuitem', 'textarea',
];

function createStyleJsonFromString(styleString) {
Expand Down Expand Up @@ -42,20 +46,18 @@ var ProcessNodeDefinitions = function(React) {
key: index,
};
// Process attributes
if (node.attribs) {
forEach(node.attribs, function(value, key) {
switch (key || '') {
case 'style':
elementProps.style = createStyleJsonFromString(node.attribs.style);
break;
case 'class':
elementProps.className = value;
break;
default:
elementProps[key] = value || key;
break;
if (!isEmpty(node.attribs)) {
elementProps = merge(elementProps, fromPairs(map(node.attribs, function (value, key) {
if (key === 'style') {
value = createStyleJsonFromString(node.attribs.style);
} else if (key === 'class') {
key = 'className';
} else if (camelCaseAttrMap[key]) {
key = camelCaseAttrMap[key];
}
});

return [key, value || key,];
})));
}

if (includes(voidElementTags, node.name)) {
Expand Down
5 changes: 2 additions & 3 deletions lib/processing-instructions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ var ProcessingInstructions = function(React) {
return {
defaultProcessingInstructions: [{
shouldProcessNode: ShouldProcessNodeDefinitions.shouldProcessEveryNode,
processNode: processNodeDefinitions.processDefaultNode
}]
processNode: processNodeDefinitions.processDefaultNode,
},],
};
};

module.exports = ProcessingInstructions;

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,21 @@
"dependencies": {
"ent": "^2.2.0",
"htmlparser2": "^3.8.3",
"lodash.camelcase": "^4.1.0",
"lodash.compact": "^3.0.1",
"lodash.find": "^4.3.0",
"lodash.foreach": "^4.2.0",
"lodash.includes": "^4.1.2",
"lodash.map": "^4.3.0"
"lodash.camelcase": "^4.3.0",
"lodash.find": "^4.6.0",
"lodash.frompairs": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isempty": "^4.4.0",
"lodash.map": "^4.6.0",
"lodash.merge": "^4.6.0"
},
"devDependencies": {
"coveralls": "2.11.9",
"istanbul": "0.4.3",
"lodash": "^4.11.1",
"lodash": "^4.16.1",
"mocha": "2.4.5",
"mocha-lcov-reporter": "1.2.0",
"react": "^0.14.7",
"react-dom": "^0.14.7"
"react": "^15.0",
"react-dom": "^15.0"
}
}
23 changes: 16 additions & 7 deletions test/html-to-react-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ describe('Html2React', function() {
assert.equal(reactHtml, htmlInput);
});

it('should return a valid HTML string with a react camelCase attribute', function() {
var htmlInput = '<div contenteditable="true"></div>';

var reactComponent = parser.parse(htmlInput);
var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent);

assert.equal(reactHtml, htmlInput);
});

// FIXME: See lib/process-node-definitions.js -> processDefaultNode()
it.skip('should return a valid HTML string with comments', function() {
var htmlInput = '<div><!-- This is a comment --></div>';
Expand Down Expand Up @@ -203,8 +212,8 @@ describe('Html2React', function() {
shouldProcessNode: function(node) {
return node.name && node.name !== 'p';
},
processNode: processNodeDefinitions.processDefaultNode
}];
processNode: processNodeDefinitions.processDefaultNode,
},];
var reactComponent = parser.parseWithInstructions(htmlInput, isValidNode, processingInstructions);

// With only 1 <p> element, nothing is rendered
Expand All @@ -223,8 +232,8 @@ describe('Html2React', function() {
shouldProcessNode: function(node) {
return node.type === 'text' || node.name !== 'p';
},
processNode: processNodeDefinitions.processDefaultNode
}];
processNode: processNodeDefinitions.processDefaultNode,
},];
var reactComponent = parser.parseWithInstructions(htmlInput, isValidNode, processingInstructions);
var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent);
assert.equal(reactHtml, htmlExpected);
Expand All @@ -246,14 +255,14 @@ describe('Html2React', function() {
},
processNode: function(node, children) {
return node.data.toUpperCase();
}
},
}, {
// Anything else
shouldProcessNode: function(node) {
return true;
},
processNode: processNodeDefinitions.processDefaultNode
}];
processNode: processNodeDefinitions.processDefaultNode,
},];
var reactComponent = parser.parseWithInstructions(htmlInput, isValidNode, processingInstructions);
var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent);
assert.equal(reactHtml, htmlExpected);
Expand Down

0 comments on commit 01952fa

Please sign in to comment.