├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── camel-case-attribute-names.js ├── is-valid-node-definitions.js ├── parser.js ├── process-node-definitions.js ├── processing-instructions.js └── should-process-node-definitions.js ├── package.json └── test └── html-to-react-tests.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | insert_final_newline = true 7 | max_line_length = 100 8 | 9 | [*.js] 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 5 5 | - 6 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.0.0](https://github.com/mikenikles/html-to-react/tree/v1.0.0) 4 | 5 | [Full Changelog](https://github.com/mikenikles/html-to-react/compare/v0.1.0...v1.0.0) 6 | 7 | **Fixed bugs:** 8 | 9 | - Travis build fails due to a ReferenceError [\#11](https://github.com/mikenikles/html-to-react/issues/11) 10 | 11 | **Closed issues:** 12 | 13 | - Should not insert spans into tables even if there is white space [\#30](https://github.com/mikenikles/html-to-react/issues/30) 14 | - Img, br and hr tags produce warnings [\#29](https://github.com/mikenikles/html-to-react/issues/29) 15 | - using import instead of require [\#26](https://github.com/mikenikles/html-to-react/issues/26) 16 | - Cherry pick lodash [\#22](https://github.com/mikenikles/html-to-react/issues/22) 17 | - problem with textarea [\#9](https://github.com/mikenikles/html-to-react/issues/9) 18 | - Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method [\#7](https://github.com/mikenikles/html-to-react/issues/7) 19 | - State of the library? [\#6](https://github.com/mikenikles/html-to-react/issues/6) 20 | - best way to pass callbacks? [\#5](https://github.com/mikenikles/html-to-react/issues/5) 21 | 22 | **Merged pull requests:** 23 | 24 | - Treat textarea as a void element [\#33](https://github.com/mikenikles/html-to-react/pull/33) ([mikenikles](https://github.com/mikenikles)) 25 | - Clean up NPM scripts [\#32](https://github.com/mikenikles/html-to-react/pull/32) ([mikenikles](https://github.com/mikenikles)) 26 | - Pass through options to parser [\#27](https://github.com/mikenikles/html-to-react/pull/27) ([benjeffery](https://github.com/benjeffery)) 27 | - Use modularized lodash [\#25](https://github.com/mikenikles/html-to-react/pull/25) ([no23reason](https://github.com/no23reason)) 28 | - Test with node 4 and 5 on travis [\#24](https://github.com/mikenikles/html-to-react/pull/24) ([thangngoc89](https://github.com/thangngoc89)) 29 | - Fix npm test [\#21](https://github.com/mikenikles/html-to-react/pull/21) ([mikenikles](https://github.com/mikenikles)) 30 | - Create element keys per sequence [\#20](https://github.com/mikenikles/html-to-react/pull/20) ([aknuds1](https://github.com/aknuds1)) 31 | - Add editorconfig [\#19](https://github.com/mikenikles/html-to-react/pull/19) ([aknuds1](https://github.com/aknuds1)) 32 | - Decode text nodes before passing to React [\#18](https://github.com/mikenikles/html-to-react/pull/18) ([aknuds1](https://github.com/aknuds1)) 33 | - Deal with void element tags [\#17](https://github.com/mikenikles/html-to-react/pull/17) ([mikenikles](https://github.com/mikenikles)) 34 | - Make sure tests fail on console warnings [\#16](https://github.com/mikenikles/html-to-react/pull/16) ([mikenikles](https://github.com/mikenikles)) 35 | - Upgrade React to the latest version [\#15](https://github.com/mikenikles/html-to-react/pull/15) ([mikenikles](https://github.com/mikenikles)) 36 | - Fix the blanket dependency for now [\#12](https://github.com/mikenikles/html-to-react/pull/12) ([mikenikles](https://github.com/mikenikles)) 37 | - Fix key warning [\#10](https://github.com/mikenikles/html-to-react/pull/10) ([lithin](https://github.com/lithin)) 38 | - Fixing parser's error message when multiple roots are found [\#1](https://github.com/mikenikles/html-to-react/pull/1) ([Yomguithereal](https://github.com/Yomguithereal)) 39 | 40 | ## [v0.1.0](https://github.com/mikenikles/html-to-react/tree/v0.1.0) (2015-06-20) 41 | [Full Changelog](https://github.com/mikenikles/html-to-react/compare/v0.0.6...v0.1.0) 42 | 43 | ## [v0.0.6](https://github.com/mikenikles/html-to-react/tree/v0.0.6) (2015-06-20) 44 | [Full Changelog](https://github.com/mikenikles/html-to-react/compare/v0.0.5...v0.0.6) 45 | 46 | ## [v0.0.5](https://github.com/mikenikles/html-to-react/tree/v0.0.5) (2015-06-20) 47 | [Full Changelog](https://github.com/mikenikles/html-to-react/compare/v0.0.4...v0.0.5) 48 | 49 | ## [v0.0.4](https://github.com/mikenikles/html-to-react/tree/v0.0.4) (2015-06-20) 50 | [Full Changelog](https://github.com/mikenikles/html-to-react/compare/v0.0.3...v0.0.4) 51 | 52 | ## [v0.0.3](https://github.com/mikenikles/html-to-react/tree/v0.0.3) (2015-06-20) 53 | [Full Changelog](https://github.com/mikenikles/html-to-react/compare/v0.0.2...v0.0.3) 54 | 55 | ## [v0.0.2](https://github.com/mikenikles/html-to-react/tree/v0.0.2) (2015-06-20) 56 | 57 | 58 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mike Nikles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html-to-react [![Build Status](https://travis-ci.org/mikenikles/html-to-react.svg?branch=master)](https://travis-ci.org/mikenikles/html-to-react) [![npm version](https://badge.fury.io/js/html-to-react.svg)](http://badge.fury.io/js/html-to-react) [![Dependency Status](https://david-dm.org/mikenikles/html-to-react.svg)](https://david-dm.org/mikenikles/html-to-react) [![Coverage Status](https://coveralls.io/repos/mikenikles/html-to-react/badge.svg?branch=master)](https://coveralls.io/r/mikenikles/html-to-react?branch=master) 2 | A lightweight library that converts raw HTML to a React DOM structure. 3 | 4 | ## Project Moved 5 | 6 | As part of [#43](https://github.com/mikenikles/html-to-react/issues/43), this project moved to https://github.com/aknuds1/html-to-react. Please file any issues or PRs at the new location. 7 | 8 | ## Why? 9 | I had a scenario where an HTML template was generated by a different team, yet I wanted to leverage React for the parts 10 | I did have control over. The template basically contains something like: 11 | 12 | ``` 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | ``` 26 | 27 | I had to replace each `
` that contains a `data-report-id` attribute with an actual report, which was nothing more 28 | than a React component. 29 | 30 | Simply replacing the `
` elements with a React component would end up with multiple top-level React components 31 | that have no common parent. 32 | 33 | The **html-to-react** module solves this problem by parsing each DOM element and converting it to a React tree with one 34 | single parent. 35 | 36 | ## Installation 37 | 38 | `$ npm install --save html-to-react` 39 | 40 | ## Examples 41 | 42 | ### Simple 43 | 44 | The following example parses each node and its attributes and returns a tree of React components. 45 | 46 | ```javascript 47 | var React = require('react'); 48 | var HtmlToReact = new require('html-to-react'); 49 | 50 | var htmlInput = '

Title

A paragraph

'; 51 | var htmlToReactParser = new HtmlToReact.Parser(React); 52 | var reactComponent = htmlToReactParser.parse(htmlInput); 53 | var reactHtml = React.renderToStaticMarkup(reactComponent); 54 | 55 | assert.equal(reactHtml, htmlInput); // true 56 | ``` 57 | 58 | ### With custom processing instructions 59 | 60 | If certain DOM nodes require specific processing, for example if you want to capitalize each `

` tag, the following 61 | example demonstrates this: 62 | 63 | ```javascript 64 | var React = require('react'); 65 | var HtmlToReact = new require('html-to-react'); 66 | 67 | var htmlInput = '

Title

Paragraph

Another title

'; 68 | var htmlExpected = '

TITLE

Paragraph

ANOTHER TITLE

'; 69 | 70 | var isValidNode = function() { 71 | return true; 72 | }; 73 | 74 | // Order matters. Instructions are processed in the order they're defined 75 | var processNodeDefinitions = new HtmlToReact.ProcessNodeDefinitions(React); 76 | var processingInstructions = [ 77 | { 78 | // Custom

processing 79 | shouldProcessNode: function(node) { 80 | return node.parent && node.parent.name && node.parent.name === 'h1'; 81 | }, 82 | processNode: function(node, children) { 83 | return node.data.toUpperCase(); 84 | } 85 | }, { 86 | // Anything else 87 | shouldProcessNode: function(node) { 88 | return true; 89 | }, 90 | processNode: processNodeDefinitions.processDefaultNode 91 | }]; 92 | var htmlToReactParser = new HtmlToReact.Parser(React); 93 | var reactComponent = htmlToReactParser.parseWithInstructions(htmlInput, isValidNode, processingInstructions); 94 | var reactHtml = React.renderToStaticMarkup(reactComponent); 95 | assert.equal(reactHtml, htmlExpected); 96 | ``` 97 | 98 | ## Tests & Coverage 99 | 100 | `$ npm run test-locally` 101 | 102 | `$ npm run test-html-coverage` 103 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parser = require('./lib/parser'); 4 | var processingInstructions = require('./lib/processing-instructions'); 5 | var isValidNodeDefinitions = require('./lib/is-valid-node-definitions'); 6 | var processNodeDefinitions = require('./lib/process-node-definitions'); 7 | 8 | module.exports = { 9 | Parser: parser, 10 | ProcessingInstructions: processingInstructions, 11 | IsValidNodeDefinitions: isValidNodeDefinitions, 12 | ProcessNodeDefinitions: processNodeDefinitions, 13 | }; 14 | -------------------------------------------------------------------------------- /lib/camel-case-attribute-names.js: -------------------------------------------------------------------------------- 1 | // These are all sourced from https://facebook.github.io/react/docs/tags-and-attributes.html - 2 | // all attributes regardless of whether they have a different case to their HTML equivalents are 3 | // listed to reduce the chance of human error and make it easier to just copy-paste the new list if 4 | // it changes. 5 | 'use strict'; 6 | var HTML_ATTRIBUTES = [ 7 | 'accept', 'acceptCharset', 'accessKey', 'action', 'allowFullScreen', 'allowTransparency', 8 | 'alt', 'async', 'autoComplete', 'autoFocus', 'autoPlay', 'capture', 'cellPadding', 9 | 'cellSpacing', 'challenge', 'charSet', 'checked', 'cite', 'classID', 'className', 10 | 'colSpan', 'cols', 'content', 'contentEditable', 'contextMenu', 'controls', 'coords', 11 | 'crossOrigin', 'data', 'dateTime', 'default', 'defer', 'dir', 'disabled', 'download', 12 | 'draggable', 'encType', 'form', 'formAction', 'formEncType', 'formMethod', 'formNoValidate', 13 | 'formTarget', 'frameBorder', 'headers', 'height', 'hidden', 'high', 'href', 'hrefLang', 14 | 'htmlFor', 'httpEquiv', 'icon', 'id', 'inputMode', 'integrity', 'is', 'keyParams', 'keyType', 15 | 'kind', 'label', 'lang', 'list', 'loop', 'low', 'manifest', 'marginHeight', 'marginWidth', 16 | 'max', 'maxLength', 'media', 'mediaGroup', 'method', 'min', 'minLength', 'multiple', 'muted', 17 | 'name', 'noValidate', 'nonce', 'open', 'optimum', 'pattern', 'placeholder', 'poster', 18 | 'preload', 'profile', 'radioGroup', 'readOnly', 'rel', 'required', 'reversed', 'role', 19 | 'rowSpan', 'rows', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 20 | 'shape', 'size', 'sizes', 'span', 'spellCheck', 'src', 'srcDoc', 'srcLang', 'srcSet', 'start', 21 | 'step', 'style', 'summary', 'tabIndex', 'target', 'title', 'type', 'useMap', 'value', 'width', 22 | 'wmode', 'wrap', 23 | ]; 24 | 25 | var NON_STANDARD_ATTRIBUTES = [ 26 | 'autoCapitalize', 'autoCorrect', 'color', 'itemProp', 'itemScope', 'itemType', 'itemRef', 27 | 'itemID', 'security', 'unselectable', 'results', 'autoSave', 28 | ]; 29 | 30 | var SVG_ATTRIBUTES = [ 31 | 'accentHeight', 'accumulate', 'additive', 'alignmentBaseline', 'allowReorder', 'alphabetic', 32 | 'amplitude', 'arabicForm', 'ascent', 'attributeName', 'attributeType', 'autoReverse', 33 | 'azimuth', 'baseFrequency', 'baseProfile', 'baselineShift', 'bbox', 'begin', 'bias', 'by', 34 | 'calcMode', 'capHeight', 'clip', 'clipPath', 'clipPathUnits', 'clipRule', 'colorInterpolation', 35 | 'colorInterpolationFilters', 'colorProfile', 'colorRendering', 'contentScriptType', 36 | 'contentStyleType', 'cursor', 'cx', 'cy', 'd', 'decelerate', 'descent', 'diffuseConstant', 37 | 'direction', 'display', 'divisor', 'dominantBaseline', 'dur', 'dx', 'dy', 'edgeMode', 38 | 'elevation', 'enableBackground', 'end', 'exponent', 'externalResourcesRequired', 'fill', 39 | 'fillOpacity', 'fillRule', 'filter', 'filterRes', 'filterUnits', 'floodColor', 'floodOpacity', 40 | 'focusable', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch', 'fontStyle', 41 | 'fontVariant', 'fontWeight', 'format', 'from', 'fx', 'fy', 'g1', 'g2', 'glyphName', 42 | 'glyphOrientationHorizontal', 'glyphOrientationVertical', 'glyphRef', 'gradientTransform', 43 | 'gradientUnits', 'hanging', 'horizAdvX', 'horizOriginX', 'ideographic', 'imageRendering', 44 | 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kernelMatrix', 'kernelUnitLength', 45 | 'kerning', 'keyPoints', 'keySplines', 'keyTimes', 'lengthAdjust', 'letterSpacing', 46 | 'lightingColor', 'limitingConeAngle', 'local', 'markerEnd', 'markerHeight', 'markerMid', 47 | 'markerStart', 'markerUnits', 'markerWidth', 'mask', 'maskContentUnits', 'maskUnits', 48 | 'mathematical', 'mode', 'numOctaves', 'offset', 'opacity', 'operator', 'order', 49 | 'orient', 'orientation', 'origin', 'overflow', 'overlinePosition', 'overlineThickness', 50 | 'paintOrder', 'panose1', 'pathLength', 'patternContentUnits', 'patternTransform', 51 | 'patternUnits', 'pointerEvents', 'points', 'pointsAtX', 'pointsAtY', 'pointsAtZ', 52 | 'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits', 'r', 'radius', 'refX', 'refY', 53 | 'renderingIntent', 'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures', 54 | 'restart', 'result', 'rotate', 'rx', 'ry', 'scale', 'seed', 'shapeRendering', 'slope', 55 | 'spacing', 'specularConstant', 'specularExponent', 'speed', 'spreadMethod', 'startOffset', 56 | 'stdDeviation', 'stemh', 'stemv', 'stitchTiles', 'stopColor', 'stopOpacity', 57 | 'strikethroughPosition', 'strikethroughThickness', 'string', 'stroke', 'strokeDasharray', 58 | 'strokeDashoffset', 'strokeLinecap', 'strokeLinejoin', 'strokeMiterlimit', 59 | 'strokeOpacity', 'strokeWidth', 'surfaceScale', 'systemLanguage', 'tableValues', 'targetX', 60 | 'targetY', 'textAnchor', 'textDecoration', 'textLength', 'textRendering', 'to', 'transform', 61 | 'u1', 'u2', 'underlinePosition', 'underlineThickness', 'unicode', 'unicodeBidi', 62 | 'unicodeRange', 'unitsPerEm', 'vAlphabetic', 'vHanging', 'vIdeographic', 'vMathematical', 63 | 'values', 'vectorEffect', 'version', 'vertAdvY', 'vertOriginX', 'vertOriginY', 'viewBox', 64 | 'viewTarget', 'visibility', 'widths', 'wordSpacing', 'writingMode', 'x', 'x1', 'x2', 65 | 'xChannelSelector', 'xHeight', 'xlinkActuate', 'xlinkArcrole', 'xlinkHref', 'xlinkRole', 66 | 'xlinkShow', 'xlinkTitle', 'xlinkType', 'xmlBase', 'xmlLang', 'xmlSpace', 'y', 'y1', 'y2', 67 | 'yChannelSelector', 'z', 'zoomAndPan', 68 | ]; 69 | 70 | var camelCaseMap = HTML_ATTRIBUTES 71 | .concat(NON_STANDARD_ATTRIBUTES) 72 | .concat(SVG_ATTRIBUTES) 73 | .reduce(function (soFar, attr) { 74 | var lower = attr.toLowerCase(); 75 | if (lower !== attr) { 76 | soFar[lower] = attr; 77 | } 78 | return soFar; 79 | }, {}); 80 | 81 | module.exports = camelCaseMap; 82 | -------------------------------------------------------------------------------- /lib/is-valid-node-definitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function alwaysValid() { 4 | return true; 5 | } 6 | 7 | module.exports = { 8 | alwaysValid: alwaysValid 9 | }; 10 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var find = require('lodash.find'); 3 | var map = require('lodash.map'); 4 | var htmlParser = require('htmlparser2'); 5 | var ProcessingInstructions = require('./processing-instructions'); 6 | var IsValidNodeDefinitions = require('./is-valid-node-definitions'); 7 | 8 | var Html2React = function(React, options) { 9 | var parseHtmlToTree = function(html) { 10 | var handler = new htmlParser.DomHandler(); 11 | var parser = new htmlParser.Parser(handler, options); 12 | parser.parseComplete(html); 13 | return handler.dom; 14 | }; 15 | 16 | var traverseDom = function(node, isValidNode, processingInstructions, index) { 17 | if (isValidNode(node)) { 18 | var processingInstruction = find(processingInstructions || [], 19 | function (processingInstruction) { 20 | return processingInstruction.shouldProcessNode(node); 21 | }); 22 | if (processingInstruction != null) { 23 | var children = map(node.children || [], function (child, i) { 24 | return traverseDom(child, isValidNode, processingInstructions, i); 25 | }).filter(function (x) { 26 | return x != null; 27 | }); 28 | return processingInstruction.processNode(node, children, index); 29 | } else { 30 | return false; 31 | } 32 | } else { 33 | return false; 34 | } 35 | }; 36 | 37 | var parseWithInstructions = function(html, isValidNode, processingInstructions) { 38 | var domTree = parseHtmlToTree(html); 39 | // TODO: Deal with HTML that contains more than one root level node 40 | if (domTree && domTree.length !== 1) { 41 | throw new Error( 42 | 'html-to-react currently only supports HTML with one single root element. ' + 43 | 'The HTML provided contains ' + domTree.length + 44 | ' root elements. You can fix that by simply wrapping your HTML ' + 45 | 'in a
element.'); 46 | } 47 | return traverseDom(domTree[0], isValidNode, processingInstructions, 0); 48 | }; 49 | 50 | var parse = function(html) { 51 | var processingInstructions = new ProcessingInstructions(React); 52 | return parseWithInstructions(html, 53 | IsValidNodeDefinitions.alwaysValid, 54 | processingInstructions.defaultProcessingInstructions); 55 | }; 56 | 57 | return { 58 | parse: parse, 59 | parseWithInstructions: parseWithInstructions, 60 | }; 61 | }; 62 | 63 | module.exports = Html2React; 64 | -------------------------------------------------------------------------------- /lib/process-node-definitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isEmpty = require('lodash.isempty'); 4 | var map = require('lodash.map'); 5 | var fromPairs = require('lodash.frompairs'); 6 | var camelCase = require('lodash.camelcase'); 7 | var includes = require('lodash.includes'); 8 | var merge = require('lodash.merge'); 9 | var ent = require('ent'); 10 | var camelCaseAttrMap = require('./camel-case-attribute-names'); 11 | 12 | // https://github.com/facebook/react/blob/15.0-stable/src/renderers/dom/shared/ReactDOMComponent.js#L457 13 | var voidElementTags = [ 14 | 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 15 | 'source', 'track', 'wbr', 'menuitem', 'textarea', 16 | ]; 17 | 18 | function createStyleJsonFromString(styleString) { 19 | if (!styleString) { 20 | return {}; 21 | } 22 | var styles = styleString.split(';'); 23 | var singleStyle, key, value, jsonStyles = {}; 24 | for (var i = 0; i < styles.length; i++) { 25 | singleStyle = styles[i].split(':'); 26 | key = camelCase(singleStyle[0]); 27 | value = singleStyle[1]; 28 | if (key.length > 0 && value.length > 0) { 29 | jsonStyles[key] = value; 30 | } 31 | } 32 | return jsonStyles; 33 | } 34 | 35 | var ProcessNodeDefinitions = function(React) { 36 | function processDefaultNode(node, children, index) { 37 | if (node.type === 'text') { 38 | return ent.decode(node.data); 39 | } else if (node.type === 'comment') { 40 | // FIXME: The following doesn't work as the generated HTML results in "<!-- This is a comment -->" 41 | //return ''; 42 | return false; 43 | } 44 | 45 | var elementProps = { 46 | key: index, 47 | }; 48 | // Process attributes 49 | if (!isEmpty(node.attribs)) { 50 | elementProps = merge(elementProps, fromPairs(map(node.attribs, function (value, key) { 51 | if (key === 'style') { 52 | value = createStyleJsonFromString(node.attribs.style); 53 | } else if (key === 'class') { 54 | key = 'className'; 55 | } else if (camelCaseAttrMap[key]) { 56 | key = camelCaseAttrMap[key]; 57 | } 58 | 59 | return [key, value || key,]; 60 | }))); 61 | } 62 | 63 | if (includes(voidElementTags, node.name)) { 64 | return React.createElement(node.name, elementProps) 65 | } else { 66 | var allChildren = node.data != null ? [node.data,].concat(children) : children; 67 | return React.createElement.apply( 68 | this, [node.name, elementProps,].concat(allChildren) 69 | ); 70 | } 71 | } 72 | 73 | return { 74 | processDefaultNode: processDefaultNode, 75 | }; 76 | }; 77 | 78 | module.exports = ProcessNodeDefinitions; 79 | -------------------------------------------------------------------------------- /lib/processing-instructions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ShouldProcessNodeDefinitions = require('./should-process-node-definitions'); 4 | var ProcessNodeDefinitions = require('./process-node-definitions'); 5 | 6 | var ProcessingInstructions = function(React) { 7 | var processNodeDefinitions = new ProcessNodeDefinitions(React); 8 | 9 | return { 10 | defaultProcessingInstructions: [{ 11 | shouldProcessNode: ShouldProcessNodeDefinitions.shouldProcessEveryNode, 12 | processNode: processNodeDefinitions.processDefaultNode, 13 | },], 14 | }; 15 | }; 16 | 17 | module.exports = ProcessingInstructions; 18 | -------------------------------------------------------------------------------- /lib/should-process-node-definitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function shouldProcessEveryNode(node) { 4 | return true; 5 | } 6 | 7 | module.exports = { 8 | shouldProcessEveryNode: shouldProcessEveryNode 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-to-react", 3 | "version": "1.0.0", 4 | "description": "A lightweight library that converts raw HTML to a React DOM structure.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 8 | "test-html-coverage": "istanbul cover ./node_modules/mocha/bin/_mocha; open coverage/lcov-report/index.html" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mikenikles/html-to-react.git" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "react-component", 17 | "html" 18 | ], 19 | "author": "Mike Nikles", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/mikenikles/html-to-react/issues" 23 | }, 24 | "homepage": "https://github.com/mikenikles/html-to-react", 25 | "config": { 26 | "blanket": { 27 | "pattern": [ 28 | "" 29 | ], 30 | "data-cover-never": [ 31 | "node_modules", 32 | "test" 33 | ] 34 | } 35 | }, 36 | "dependencies": { 37 | "ent": "^2.2.0", 38 | "htmlparser2": "^3.8.3", 39 | "lodash.camelcase": "^4.3.0", 40 | "lodash.find": "^4.6.0", 41 | "lodash.frompairs": "^4.0.1", 42 | "lodash.includes": "^4.3.0", 43 | "lodash.isempty": "^4.4.0", 44 | "lodash.map": "^4.6.0", 45 | "lodash.merge": "^4.6.0" 46 | }, 47 | "devDependencies": { 48 | "coveralls": "2.11.9", 49 | "istanbul": "0.4.3", 50 | "lodash": "^4.16.1", 51 | "mocha": "2.4.5", 52 | "mocha-lcov-reporter": "1.2.0", 53 | "react": "^15.0", 54 | "react-dom": "^15.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/html-to-react-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var React = require('react'); 5 | var ReactDOMServer = require('react-dom/server') 6 | var _ = require('lodash'); 7 | 8 | var Parser = require('../index').Parser; 9 | var ProcessNodeDefinitions = require('../index').ProcessNodeDefinitions; 10 | 11 | describe('Html2React', function() { 12 | var parser = new Parser(React); 13 | 14 | describe('parse valid HTML', function() { 15 | it('should return a valid HTML string', function() { 16 | var htmlInput = '

Does this work?

'; 17 | 18 | var reactComponent = parser.parse(htmlInput); 19 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 20 | 21 | assert.equal(reactHtml, htmlInput); 22 | }); 23 | 24 | it('should return a valid HTML string with nested elements', function() { 25 | var htmlInput = '

Heading

'; 26 | 27 | var reactComponent = parser.parse(htmlInput); 28 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 29 | 30 | assert.equal(reactHtml, htmlInput); 31 | }); 32 | 33 | it('should return a valid HTML string with inline styles', function() { 34 | var htmlInput = '
'; 35 | 36 | var reactComponent = parser.parse(htmlInput); 37 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 38 | 39 | assert.equal(reactHtml, htmlInput); 40 | }); 41 | 42 | it('should return a valid HTML string with empty inline styles', function() { 43 | var htmlInput = '
'; 44 | var htmlExpected = '
'; 45 | 46 | var reactComponent = parser.parse(htmlInput); 47 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 48 | 49 | assert.equal(reactHtml, htmlExpected); 50 | }); 51 | 52 | it('should return a valid HTML string with data attributes', function() { 53 | var htmlInput = '
'; 54 | 55 | var reactComponent = parser.parse(htmlInput); 56 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 57 | 58 | assert.equal(reactHtml, htmlInput); 59 | }); 60 | 61 | it('should return a valid HTML string with aria attributes', function() { 62 | var htmlInput = '
'; 63 | 64 | var reactComponent = parser.parse(htmlInput); 65 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 66 | 67 | assert.equal(reactHtml, htmlInput); 68 | }); 69 | 70 | it('should return a valid HTML string with a class attribute', function() { 71 | var htmlInput = '
'; 72 | 73 | var reactComponent = parser.parse(htmlInput); 74 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 75 | 76 | assert.equal(reactHtml, htmlInput); 77 | }); 78 | 79 | it('should return a valid HTML string with a react camelCase attribute', function() { 80 | var htmlInput = '
'; 81 | 82 | var reactComponent = parser.parse(htmlInput); 83 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 84 | 85 | assert.equal(reactHtml, htmlInput); 86 | }); 87 | 88 | // FIXME: See lib/process-node-definitions.js -> processDefaultNode() 89 | it.skip('should return a valid HTML string with comments', function() { 90 | var htmlInput = '
'; 91 | 92 | var reactComponent = parser.parse(htmlInput); 93 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 94 | 95 | assert.equal(reactHtml, htmlInput); 96 | }); 97 | 98 | // FIXME: If / when React implements HTML comments, this test can be removed 99 | it('should return a valid HTML string without comments', function() { 100 | var htmlInput = '
'; 101 | var htmlExpected = '
'; 102 | 103 | var reactComponent = parser.parse(htmlInput); 104 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 105 | 106 | assert.equal(reactHtml, htmlExpected); 107 | }); 108 | 109 | it('should parse br elements without warnings', function() { 110 | var htmlInput = '

Line one
Line two
Line three

'; 111 | var htmlExpected = '

Line one
Line two
Line three

'; 112 | 113 | var reactComponent = parser.parse(htmlInput); 114 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 115 | 116 | assert.equal(reactHtml, htmlExpected); 117 | }); 118 | 119 | it('should not generate children for br tags', function() { 120 | var htmlInput = '
'; 121 | 122 | var reactComponent = parser.parse(htmlInput); 123 | assert.strictEqual((reactComponent.props.children || []).length, 0); 124 | }); 125 | 126 | it('should parse void elements with all attributes and no warnings', function() { 127 | var htmlInput = '

'; 128 | 129 | var reactComponent = parser.parse(htmlInput); 130 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 131 | 132 | assert.equal(reactHtml, htmlInput); 133 | }); 134 | 135 | // Covers issue #9 136 | it('should parse textarea elements', function() { 137 | var htmlInput = ''; 138 | 139 | var reactComponent = parser.parse(htmlInput); 140 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 141 | 142 | assert.equal(reactHtml, htmlInput); 143 | }); 144 | 145 | it('should decode character entities in text nodes', function () { 146 | var htmlInput = '
1 < 2
'; 147 | 148 | var reactComponent = parser.parse(htmlInput); 149 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 150 | 151 | assert.equal(reactHtml, htmlInput); 152 | }); 153 | 154 | it('should not generate children for childless elements', function () { 155 | var htmlInput = '
'; 156 | 157 | var reactComponent = parser.parse(htmlInput); 158 | 159 | assert.strictEqual((reactComponent.props.children || []).length, 0); 160 | }); 161 | 162 | it('should fill in the key name with boolean attribute', function() { 163 | var htmlInput = ''; 164 | var htmlExpected = '' 165 | 166 | var reactComponent = parser.parse(htmlInput); 167 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 168 | 169 | assert.equal(reactHtml, htmlExpected); 170 | }); 171 | }); 172 | 173 | describe('parse invalid HTML', function() { 174 | it('should throw an error when trying parsing multiple root elements', function() { 175 | var htmlInput = '
'; 176 | 177 | assert.throws(function() { 178 | parser.parse(htmlInput); 179 | }, Error); 180 | }); 181 | 182 | it('should throw an error with a specific message when parsing multiple root elements', function() { 183 | var htmlInput = '
'; 184 | 185 | assert.throws(function() { 186 | parser.parse(htmlInput); 187 | }, /contains 3 root elements/); 188 | }); 189 | 190 | it('should fix missing closing tags', function() { 191 | var htmlInput = '

'; 192 | var htmlExpected = '

'; 193 | 194 | var reactComponent = parser.parse(htmlInput); 195 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 196 | 197 | assert.equal(reactHtml, htmlExpected); 198 | }); 199 | }); 200 | 201 | describe('with custom processing instructions', function() { 202 | var parser = new Parser(React); 203 | var processNodeDefinitions = new ProcessNodeDefinitions(React); 204 | 205 | describe('parse valid HTML', function() { 206 | it('should return nothing with only a single

element', function() { 207 | var htmlInput = '

Does this work?

'; 208 | var isValidNode = function() { 209 | return true; 210 | }; 211 | var processingInstructions = [{ 212 | shouldProcessNode: function(node) { 213 | return node.name && node.name !== 'p'; 214 | }, 215 | processNode: processNodeDefinitions.processDefaultNode, 216 | },]; 217 | var reactComponent = parser.parseWithInstructions(htmlInput, isValidNode, processingInstructions); 218 | 219 | // With only 1

element, nothing is rendered 220 | assert.equal(reactComponent, false); 221 | }); 222 | 223 | it('should return a single

element within a div of

and

as siblings', function() { 224 | var htmlInput = '

Title

Paragraph

'; 225 | var htmlExpected = '

Title

'; 226 | 227 | var isValidNode = function() { 228 | return true; 229 | }; 230 | 231 | var processingInstructions = [{ 232 | shouldProcessNode: function(node) { 233 | return node.type === 'text' || node.name !== 'p'; 234 | }, 235 | processNode: processNodeDefinitions.processDefaultNode, 236 | },]; 237 | var reactComponent = parser.parseWithInstructions(htmlInput, isValidNode, processingInstructions); 238 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 239 | assert.equal(reactHtml, htmlExpected); 240 | }); 241 | 242 | it('should return capitalized content for all

elements', function() { 243 | var htmlInput = '

Title

Paragraph

Another title

'; 244 | var htmlExpected = '

TITLE

Paragraph

ANOTHER TITLE

'; 245 | 246 | var isValidNode = function() { 247 | return true; 248 | }; 249 | 250 | var processingInstructions = [ 251 | { 252 | // Custom

processing 253 | shouldProcessNode: function(node) { 254 | return node.parent && node.parent.name && node.parent.name === 'h1'; 255 | }, 256 | processNode: function(node, children) { 257 | return node.data.toUpperCase(); 258 | }, 259 | }, { 260 | // Anything else 261 | shouldProcessNode: function(node) { 262 | return true; 263 | }, 264 | processNode: processNodeDefinitions.processDefaultNode, 265 | },]; 266 | var reactComponent = parser.parseWithInstructions(htmlInput, isValidNode, processingInstructions); 267 | var reactHtml = ReactDOMServer.renderToStaticMarkup(reactComponent); 268 | assert.equal(reactHtml, htmlExpected); 269 | }); 270 | 271 | it('should generate keys for sequence items', function () { 272 | var htmlInput = '
  • Item 1
  • Item 2
  • <
'; 273 | 274 | var reactComponent = parser.parse(htmlInput); 275 | 276 | var children = _.filter(_.flatten(reactComponent.props.children), function (c) { 277 | return _.has(c, 'key'); 278 | }); 279 | var keys = _.map(children, function (child) { 280 | return child.key; 281 | }); 282 | assert.deepStrictEqual(keys, ['0', '1', ]); 283 | }); 284 | 285 | it('should return false in case of invalid node', function() { 286 | var htmlInput = '

'; 287 | var processingInstructions = [{ 288 | shouldProcessNode: function(node) { return true; }, 289 | processNode: processNodeDefinitions.processDefaultNode, 290 | }, ]; 291 | var reactComponent = parser.parseWithInstructions(htmlInput, 292 | function () { return false }, processingInstructions); 293 | 294 | assert.equal(reactComponent, false); 295 | }); 296 | }); 297 | }); 298 | }); 299 | --------------------------------------------------------------------------------