├── .babelrc ├── .eslintrc.js ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── README.md ├── TwitterLink.js ├── data.js ├── index.js ├── styles.js └── ui.js ├── package.json ├── src ├── attributesMap.js ├── convert.js ├── createGetTagFunction.js ├── groupInlineElements.js ├── index.js ├── inlineElements.js ├── mapAttributes.js ├── selfClosingElements.js ├── step.js ├── textToDOM.js └── toJSX.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "transform-flow-strip-types", 8 | "transform-class-properties", 9 | "transform-object-rest-spread" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:react/recommended"], 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "experimentalObjectRestSpread": true, 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "react" 18 | ], 19 | "rules": { 20 | "linebreak-style": [ 21 | "error", 22 | "unix" 23 | ], 24 | "quotes": [ 25 | "error", 26 | "single" 27 | ], 28 | "semi": [ 29 | "error", 30 | "always" 31 | ] 32 | } 33 | }; -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/nwb/.* 3 | /lib/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rafał Filipek 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSXFromHTML 2 | 3 | With `jsxfromhtml` you can easily convert html strings into React / React Native / Preact / Inferno (you know, JSX in general) components. 4 | 5 | ## Demo 6 | 7 | Like always on [now](https://now.sh) :) - [**jsxfromhtml.now.sh**](https://jsxfromhtml.now.sh/) 8 | 9 | ## Installation 10 | 11 | ``` 12 | yarn add jsxfromhtml 13 | 14 | // or 15 | 16 | npm install -S jsxfromhtml 17 | ``` 18 | 19 | Also for React-Native you have to install some Node specific modules 20 | 21 | ``` 22 | yarn add stream buffer events 23 | 24 | // or 25 | 26 | npm install -S stream buffer events 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Web 32 | 33 | Web is quite simple. All you have to do is: 34 | 35 | ```jsx 36 | const html = `

37 | text 38 |

` 39 | 40 | () => 41 | ``` 42 | 43 | ### Mapping 44 | 45 | But you can do more! `jsxfromhtml` allows you to map HTML tags into custom components. 46 | 47 | ```jsx 48 | const html = `

49 | rafalfilipek 50 |

` 51 | 52 | const SuperLink = (props) => { 53 | const { href, children } = props; 54 | const parts = href.match(/twitter\.com\/(.*)\/?/); 55 | if (parts) { 56 | const name = parts[1]; 57 | return ( 58 | 59 | 60 | {name} 61 | 62 | ); 63 | } else { 64 | return ; 65 | } 66 | }; 67 | 68 | () => 69 | ``` 70 | 71 | With `mapElements` you can map any HTML tag into a custom componetn. In this example we want to use our `SuperLink` component to show profile picture next to the twitter username. 72 | 73 | ### Omitting 74 | 75 | Sometimes you will get tags you don't want. Like `style` or `script`. To get rid of them just map them to `false`. 76 | 77 | ```jsx 78 | const html = `

79 | hello 80 | 81 |

` 82 | ``` 83 | 84 | ### General mapping / React Native 85 | 86 | While `mapElements` is quite cool for web we have to handle all tags in native with just `Text` and `View` components. That's why there are two more props `mapInline` and `mapBlock`. 87 | 88 | ```jsx 89 | const html = `

90 | hello world ! 91 |

` 92 | 93 | () => 94 | ``` 95 | 96 | You will get: 97 | 98 | ```jsx 99 | 100 | hello World ! 101 | 102 | ``` 103 | 104 | As you can see `jsxfromhtml` will wrap every text with is not inside inline tag. 105 | 106 | ### Styling 107 | 108 | Every HTML tag has some default styles. You will probaly want to mimic that in your app. Each component will get `data-tag` prop. 109 | 110 | ```jsx 111 | const html = 'text' 112 | 113 | const InlineElement = (props) => { 114 | const stylesMap = { 115 | strong: { fontWeight: '600' }, 116 | }; 117 | return {props.children} 118 | } 119 | 120 | () => 121 | ``` 122 | 123 | > **Info**: you can still use `mapElements` for other tags. 124 | 125 | ### Attributes 126 | 127 | All html attributes will be converted into proper jsx form. So `for` -> `htmlFor`, `class` -> `className` etc. You will receive them like regular props. 128 | 129 | ```jsx 130 | const html = `
rafalfilipek` 131 | 132 | const InlineElement = (props) => ( 133 | /* props: 134 | - children: rafalfilipek 135 | - className: link 136 | - tag: a 137 | - href: https://twitter.com/rafalfilipek 138 | - title: Rafał Filipek 139 | */ 140 | 141 | ) 142 | 143 | () => 144 | ``` 145 | 146 | ### Map order 147 | 148 | We have `mapElement`, `mapInline`, `mapBlock` props. 149 | 150 | 1. if `mapElements` value for tag is `false` then the tag is omitted. 151 | 2. use value from `mapElements` if defined 152 | 3. for inline elements use value from `mapInline` if defined 153 | 4. for block elements use value from `mapBlock` if defined 154 | 5. use tag name 155 | 156 | ## React Native inline 157 | 158 | There is no `display: inline` in React Native. You can't have two inline tags (like `Text`) in one line. The only solution I know is to wrap them with another `Text` component. This sucks: 159 | 160 | ```jsx 161 | 162 | one 163 | line 164 | 165 | ``` 166 | Thats why `jsxfromhtml` handles that for you 💥! We will group inline tags inside block tags and wrap them with `span` (`Text`) tag 🤙. 167 | 168 | ```jsx 169 | //input 170 |
171 | one 172 | two 173 |

174 | three 175 | four 176 |

177 | five 178 |
179 | 180 | //output 181 |
182 | // group 183 | one 184 | two 185 | 186 |

187 | // group 188 | three 189 | four 190 | 191 |

192 | five 193 |
194 | ``` 195 | 196 | ## FAQ 197 | 198 | **Q: Is there a wrapper for my HTML?** 199 | 200 | A: Yes, always a `div` tag. 201 | 202 | **Q: How is plain text handled inside block tags?** 203 | 204 | A: Text is wrapped with `span` tag. 205 | 206 | **Q: How do you detect a root element?** 207 | 208 | A: The root component has `data-jsx-to-html-root` prop. 209 | 210 | ## LICENSE 211 | 212 | MIT 213 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # How to run? 2 | 3 | ``` 4 | yarn global add nwb 5 | 6 | react run index.js --install 7 | ``` 8 | -------------------------------------------------------------------------------- /demo/TwitterLink.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Container = styled.a` 5 | display: inline-flex; 6 | padding: 0.2rem; 7 | background-color: #03A9F4; 8 | border-radius: .5rem; 9 | align-items: center; 10 | text-decoration: none; 11 | &:hover { 12 | background-color: #2196F3; 13 | } 14 | `; 15 | 16 | const Img = styled.img` 17 | border-radius: .4rem; 18 | border: 2px solid #fff; 19 | width: 1.6rem; 20 | `; 21 | 22 | const Username = styled.span` 23 | margin: 0 .5rem; 24 | color: #fff; 25 | `; 26 | 27 | const TwitterLink = (props) => { 28 | const { href = '' } = props; 29 | const parts = href.match(/twitter\.com\/(.*)\/?/); 30 | if (parts) { 31 | const name = parts[1]; 32 | return ( 33 | 34 | 35 | {name} 36 | 37 | ); 38 | } else { 39 | return
; 40 | } 41 | }; 42 | 43 | TwitterLink.displayName = 'TwitterLink'; 44 | 45 | TwitterLink.propTypes = { 46 | href: PropTypes.string, 47 | }; 48 | 49 | export default TwitterLink; 50 | -------------------------------------------------------------------------------- /demo/data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | twitter: `
3 |

4 | My twitter account: rafal 5 |

6 |

7 | My github account rafal 8 |

9 |
`, 10 | omitting: '

hello bla bla world!

', 11 | simple: '

Just HTML converted into JSX

', 12 | }; 13 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | 3 | import JsxHtml from '../lib'; 4 | 5 | import './styles'; 6 | import { Container, Box, Title, Header, Info, Pre } from './ui'; 7 | import data from './data'; 8 | 9 | import TwitterLink from './TwitterLink'; 10 | 11 | export default class App extends PureComponent { 12 | 13 | render() { 14 | return ( 15 |
16 |
JSXFromHTML
17 | 18 | 19 | Simple 20 | Input: 21 |
{data.simple}
22 | Output: 23 | 24 | Info: 25 |

26 | You can check generated JSX in React DevTools. 27 |

28 |
29 | 30 | Omitting 31 | Input: 32 |
{data.omitting}
33 | Output: 34 | 35 | Info: 36 |

37 | To remove strong tags you just have to map them to false. 38 |

39 |
40 | 41 | Custom Twitter Link 42 | Input: 43 |
{data.twitter}
44 | Output: 45 | 46 | Info: 47 |

48 | Twitter links are converted by TwitterLink component. 49 |

50 |
51 |

52 | jsxfromhtml by rafalfilipek 53 |

54 |
55 |
56 | ); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /demo/styles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components'; 2 | 3 | injectGlobal` 4 | html, body { 5 | font-size: 16px; 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 7 | background-color: #2196F3; 8 | margin: 0; 9 | padding: 2rem; 10 | color: #333; 11 | letter-spacing: 0.3px; 12 | } 13 | *, *::before, *::after { 14 | box-sizing: border-box; 15 | } 16 | pre, code { 17 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 18 | } 19 | a { 20 | color: #4CAF50; 21 | } 22 | `; -------------------------------------------------------------------------------- /demo/ui.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | max-width: 800px; 5 | min-width: 600px; 6 | margin: 0 auto; 7 | padding: 1rem; 8 | border-radius: .5rem; 9 | background-color: #fff; 10 | `; 11 | 12 | export const Box = styled.div` 13 | border-bottom: 1px solid #eaeaea; 14 | padding-bottom: 1rem; 15 | margin-bottom: 2rem; 16 | `; 17 | 18 | export const Pre = styled.pre` 19 | white-space: word-wrap; 20 | margin: 0; 21 | font-size: .8rem; 22 | line-height: 1.3rem; 23 | padding: 1rem; 24 | background-color: #fafafa; 25 | border-radius: .3rem; 26 | margin-bottom: 1rem; 27 | `; 28 | 29 | export const Info = styled.div` 30 | text-transform: uppercase; 31 | font-weight: bold; 32 | letter-spacing: .2rem; 33 | font-size: 0.8rem; 34 | color: #444; 35 | padding: .5rem 0; 36 | `; 37 | 38 | export const Title = styled(Info)` 39 | font-size: 1.5rem; 40 | font-weight: normal; 41 | padding-bottom: 1rem; 42 | text-align: center; 43 | `; 44 | 45 | export const Header = styled.h1` 46 | text-align: center; 47 | color: #fff; 48 | font-weight: normal; 49 | letter-spacing: .3rem; 50 | `; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsxfromhtml", 3 | "version": "1.1.0", 4 | "description": "Universal HTML to JSX compiler", 5 | "main": "lib/index.js", 6 | "author": "Rafał Filipek ", 7 | "license": "MIT", 8 | "files": [ 9 | "lib", 10 | "src" 11 | ], 12 | "scripts": { 13 | "format": "prettier --write --parser flow --single-quote ./src/*", 14 | "flow-copy": "flow-copy-source src lib", 15 | "compile": "babel -d ./lib src", 16 | "clean": "rm -rf lib", 17 | "build": "npm run clean && npm run compile && npm run flow-copy", 18 | "precommit": "lint-staged", 19 | "prepublish": "npm run build" 20 | }, 21 | "dependencies": { 22 | "htmlparser2": "^3.9.2", 23 | "stream": "^0.0.2" 24 | }, 25 | "devDependencies": { 26 | "babel-cli": "^6.23.0", 27 | "babel-eslint": "^7.1.1", 28 | "babel-preset-env": "^1.2.1", 29 | "eslint": "^3.17.1", 30 | "eslint-plugin-react": "^6.10.0", 31 | "flow-bin": "^0.41.0", 32 | "flow-copy-source": "^1.1.0", 33 | "husky": "^0.13.2", 34 | "jest": "^19.0.2", 35 | "lint-staged": "^3.4.0", 36 | "nwb": "^0.15.6", 37 | "prettier": "^0.21.0", 38 | "publish-please": "^2.2.0" 39 | }, 40 | "lint-staged": { 41 | "*.js": [ 42 | "prettier --write --parser flow --single-quote ./src/*", 43 | "git add" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/attributesMap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const map: { 4 | [key: string]: string 5 | } = { 6 | accesskey: 'accessKey', 7 | allowfullscreen: 'allowFullScreen', 8 | allowtransparency: 'allowTransparency', 9 | autocomplete: 'autoComplete', 10 | autofocus: 'autoFocus', 11 | autoplay: 'autoPlay', 12 | cellpadding: 'cellPadding', 13 | cellspacing: 'cellSpacing', 14 | charset: 'charSet', 15 | classid: 'classID', 16 | colspan: 'colSpan', 17 | contenteditable: 'contentEditable', 18 | contextmenu: 'contextMenu', 19 | crossorigin: 'crossOrigin', 20 | datetime: 'dateTime', 21 | enctype: 'encType', 22 | formaction: 'formAction', 23 | formenctype: 'formEncType', 24 | formmethod: 'formMethod', 25 | formnovalidate: 'formNoValidate', 26 | formtarget: 'formTarget', 27 | frameborder: 'frameBorder', 28 | hreflang: 'hrefLang', 29 | inputmode: 'inputMode', 30 | keyparams: 'keyParams', 31 | keytype: 'keyType', 32 | marginheight: 'marginHeight', 33 | marginwidth: 'marginWidth', 34 | maxlength: 'maxLength', 35 | mediagroup: 'mediaGroup', 36 | minlength: 'minLength', 37 | novalidate: 'noValidate', 38 | radiogroup: 'radioGroup', 39 | readonly: 'readOnly', 40 | rowspan: 'rowSpan', 41 | spellcheck: 'spellCheck', 42 | srcdoc: 'srcDoc', 43 | srclang: 'srcLang', 44 | srcset: 'srcSet', 45 | tabindex: 'tabIndex', 46 | usemap: 'useMap', 47 | autocapitalize: 'autoCapitalize', 48 | autocorrect: 'autoCorrect', 49 | autosave: 'autoSave', 50 | itemprop: 'itemProp', 51 | itemscope: 'itemScope', 52 | itemtype: 'itemType', 53 | itemref: 'itemRef', 54 | itemid: 'itemID', 55 | class: 'className', 56 | for: 'htmlFor', 57 | 'accept-charset': 'acceptCharset', 58 | 'http-equiv': 'httpEquiv', 59 | 'alignment-baseline': 'alignmentBaseline', 60 | 'arabic-form': 'arabicForm', 61 | 'baseline-shift': 'baselineShift', 62 | 'cap-height': 'capHeight', 63 | 'clip-path': 'clipPath', 64 | 'clip-rule': 'clipRule', 65 | 'color-interpolation': 'colorInterpolation', 66 | 'color-interpolation-filters': 'colorInterpolationFilters', 67 | 'color-profile': 'colorProfile', 68 | 'color-rendering': 'colorRendering', 69 | 'fill-opacity': 'fillOpacity', 70 | 'fill-rule': 'fillRule', 71 | 'flood-color': 'floodColor', 72 | 'flood-opacity': 'floodOpacity', 73 | 'font-family': 'fontFamily', 74 | 'font-size': 'fontSize', 75 | 'font-size-adjust': 'fontSizeAdjust', 76 | 'font-stretch': 'fontStretch', 77 | 'font-style': 'fontStyle', 78 | 'font-variant': 'fontVariant', 79 | 'font-weight': 'fontWeight', 80 | 'glyph-name': 'glyphName', 81 | 'glyph-orientation-horizontal': 'glyphOrientationHorizontal', 82 | 'glyph-orientation-vertical': 'glyphOrientationVertical', 83 | 'horiz-adv-x': 'horizAdvX', 84 | 'horiz-origin-x': 'horizOriginX', 85 | 'marker-end': 'markerEnd', 86 | 'marker-mid': 'markerMid', 87 | 'marker-start': 'markerStart', 88 | 'overline-position': 'overlinePosition', 89 | 'overline-thickness': 'overlineThickness', 90 | 'panose-1': 'panose1', 91 | 'paint-order': 'paintOrder', 92 | 'stop-color': 'stopColor', 93 | 'stop-opacity': 'stopOpacity', 94 | 'strikethrough-position': 'strikethroughPosition', 95 | 'strikethrough-thickness': 'strikethroughThickness', 96 | 'stroke-dasharray': 'strokeDasharray', 97 | 'stroke-dashoffset': 'strokeDashoffset', 98 | 'stroke-linecap': 'strokeLinecap', 99 | 'stroke-linejoin': 'strokeLinejoin', 100 | 'stroke-miterlimit': 'strokeMiterlimit', 101 | 'stroke-opacity': 'strokeOpacity', 102 | 'stroke-width': 'strokeWidth', 103 | 'text-anchor': 'textAnchor', 104 | 'text-decoration': 'textDecoration', 105 | 'text-rendering': 'textRendering', 106 | 'underline-position': 'underlinePosition', 107 | 'underline-thickness': 'underlineThickness', 108 | 'unicode-bidi': 'unicodeBidi', 109 | 'unicode-range': 'unicodeRange', 110 | 'units-per-em': 'unitsPerEm', 111 | 'v-alphabetic': 'vAlphabetic', 112 | 'v-hanging': 'vHanging', 113 | 'v-ideographic': 'vIdeographic', 114 | 'v-mathematical': 'vMathematical', 115 | 'vert-adv-y': 'vertAdvY', 116 | 'vert-origin-x': 'vertOriginX', 117 | 'vert-origin-y': 'vertOriginY', 118 | viewbox: 'viewBox', 119 | 'word-spacing': 'wordSpacing', 120 | 'writing-mode': 'writingMode', 121 | 'x-height': 'xHeight' 122 | }; 123 | 124 | export default map; -------------------------------------------------------------------------------- /src/convert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import createGetTagFunction from './createGetTagFunction'; 4 | import textToDOM from './textToDOM'; 5 | import toJSX from './toJSX'; 6 | 7 | type ElementsMap = { [key: string]: string | boolean } | false; 8 | 9 | type ConvertFunc = ( 10 | html: string, 11 | mapElements?: ElementsMap, 12 | mapInline?: string | Function, 13 | mapBlock?: string | Function 14 | ) => any; 15 | 16 | const convert: ConvertFunc = (html, mapElements, mapInline, mapBlock) => { 17 | const getTag = createGetTagFunction(mapElements, mapInline, mapBlock); 18 | const dom = textToDOM(html); 19 | return toJSX(dom, getTag); 20 | }; 21 | export default convert; 22 | -------------------------------------------------------------------------------- /src/createGetTagFunction.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import inlineElements from './inlineElements'; 4 | 5 | type ElementsMap = { [key: string]: string | boolean } | false; 6 | 7 | export type GetTagFunc = (name: string) => any; 8 | 9 | type CreateGetTagFunc = ( 10 | mapElements?: ElementsMap, 11 | mapInline?: string | Function, 12 | mapBlock?: string | Function 13 | ) => GetTagFunc; 14 | 15 | const createGetTagFunction: CreateGetTagFunc = ( 16 | mapElements = {}, 17 | mapInline = null, 18 | mapBlock = null 19 | ) => name => { 20 | if (mapElements) { 21 | if (mapElements[name]) { 22 | return mapElements[name]; 23 | } else if (mapElements[name] === false) { 24 | return null; 25 | } 26 | } 27 | const isInline = inlineElements.indexOf(name) !== -1; 28 | if (isInline && mapInline) { 29 | return mapInline; 30 | } 31 | if (!isInline && mapBlock) { 32 | return mapBlock; 33 | } 34 | return name; 35 | }; 36 | 37 | export default createGetTagFunction; 38 | -------------------------------------------------------------------------------- /src/groupInlineElements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import inlineElements from './inlineElements'; 4 | 5 | const getFromGroup = group => group.length === 1 6 | ? group[0] 7 | : { 8 | name: 'span', 9 | type: 'tag', 10 | attribs: {}, 11 | data: '', 12 | children: group.map(el => { 13 | el.parent = { 14 | name: 'span', 15 | type: 'tag', 16 | attribs: {}, 17 | data: '' 18 | }; 19 | return el; 20 | }) 21 | }; 22 | 23 | const groupInlineElements = (collection: Object[]) => { 24 | const result = []; 25 | let inGroup = false; 26 | let group = []; 27 | collection.forEach(el => { 28 | if (!inlineElements.indexOf(el.name) === -1) { 29 | result.push(el); 30 | if (inGroup) { 31 | result.push(getFromGroup(group)); 32 | inGroup = false; 33 | group = []; 34 | } 35 | } else { 36 | inGroup = true; 37 | group.push(el); 38 | } 39 | }); 40 | if (inGroup) { 41 | result.push(getFromGroup(group)); 42 | } 43 | return result; 44 | }; 45 | 46 | export default groupInlineElements; 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { PropTypes, PureComponent } from 'react'; 4 | 5 | import convert from './convert'; 6 | 7 | type Props = { 8 | html: string, 9 | mapElements?: { [key: string]: any }, 10 | mapInline?: string | Function, 11 | mapBlock?: string | Function, 12 | children?: Function | null, 13 | }; 14 | 15 | class JsxHtml extends PureComponent { 16 | static displayName = 'JsxHtml'; 17 | 18 | props: Props; 19 | 20 | static propTypes = { 21 | html: PropTypes.string, 22 | mapElements: PropTypes.object, 23 | mapInline: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 24 | mapBlock: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 25 | children: PropTypes.func, 26 | }; 27 | 28 | static defaultProps: Props = { 29 | html: '', 30 | mapElements: {}, 31 | mapInline: '', 32 | mapBlock: '', 33 | children: null, 34 | }; 35 | 36 | state = {}; 37 | 38 | mounted = false; 39 | 40 | componentDidMount() { 41 | this.mounted = true; 42 | this.setContent(); 43 | } 44 | 45 | componentDidUpdate(prevProps: Props) { 46 | if (this.props.html !== prevProps.html) { 47 | this.setContent(); 48 | } 49 | } 50 | 51 | componentWillUnmount() { 52 | this.mounted = false; 53 | } 54 | 55 | setContent() { 56 | const content = convert( 57 | this.props.html, 58 | this.props.mapElements, 59 | this.props.mapInline, 60 | this.props.mapBlock 61 | ); 62 | if (this.mounted) { 63 | this.setState({ content }); 64 | } 65 | } 66 | 67 | render() { 68 | if (typeof this.props.children === 'function') { 69 | return this.props.children(this.state.content || null); 70 | } 71 | return this.state.content || null; 72 | } 73 | } 74 | 75 | export default JsxHtml; 76 | -------------------------------------------------------------------------------- /src/inlineElements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const list: string[] = [ 4 | 'a', 5 | 'b', 6 | 'big', 7 | 'i', 8 | 'small', 9 | 'tt', 10 | 'abbr', 11 | 'acronym', 12 | 'cite', 13 | 'code', 14 | 'dfn', 15 | 'em', 16 | 'kbd', 17 | 'strong', 18 | 'samp', 19 | 'time', 20 | 'var', 21 | 'bdo', 22 | 'br', 23 | 'img', 24 | 'map', 25 | 'object', 26 | 'q', 27 | 'script', 28 | 'style', 29 | 'span', 30 | 'sub', 31 | 'sup', 32 | 'button', 33 | 'input', 34 | 'label', 35 | 'select', 36 | 'textarea' 37 | ]; 38 | 39 | export default list; 40 | -------------------------------------------------------------------------------- /src/mapAttributes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import attributesMap from './attributesMap'; 4 | 5 | type Attrs = { [key: string]: string }; 6 | 7 | type MapAttributesFunc = (attrs: Attrs) => Attrs; 8 | 9 | const mapAttributes: MapAttributesFunc = (attrs = {}) => Object.keys( 10 | attrs 11 | ).reduce( 12 | (memo, key) => { 13 | memo[attributesMap[key] || key] = attrs[key]; 14 | return memo; 15 | }, 16 | {} 17 | ); 18 | 19 | export default mapAttributes; 20 | -------------------------------------------------------------------------------- /src/selfClosingElements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const list: string[] = [ 4 | 'area', 5 | 'base', 6 | 'br', 7 | 'col', 8 | 'command', 9 | 'embed', 10 | 'hr', 11 | 'img', 12 | 'input', 13 | 'keygen', 14 | 'link', 15 | 'meta', 16 | 'param', 17 | 'source', 18 | 'track', 19 | 'wbr' 20 | ]; 21 | 22 | export default list; 23 | -------------------------------------------------------------------------------- /src/step.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | 5 | import selfClosingElements from './selfClosingElements'; 6 | import mapAttributes from './mapAttributes'; 7 | import groupInlineElements from './groupInlineElements'; 8 | import inlineElements from './inlineElements'; 9 | 10 | type GetTagFunc = (name: string) => any; 11 | 12 | export type DOMElement = { 13 | type: 'tag' | 'text' | 'comment', 14 | name: string, 15 | data?: string, 16 | attribs: { [key: string]: any }, 17 | children: DOMElement[], 18 | parent?: DOMElement, 19 | }; 20 | 21 | type StepFunc = ( 22 | dom: DOMElement, 23 | index: number, 24 | parent?: DOMElement | null, 25 | getTag: GetTagFunc 26 | ) => React.Element<*> | null | string; 27 | 28 | const step: StepFunc = (dom, index, parent, getTag) => { 29 | if (dom.type === 'comment') { 30 | return null; 31 | } 32 | if (dom.type === 'tag' || dom.type === 'script' || dom.type === 'style') { 33 | const Component = getTag(dom.name); 34 | if (Component === null) { 35 | return null; 36 | } 37 | if (selfClosingElements.indexOf(dom.name) !== -1) { 38 | return ( 39 | 44 | ); 45 | } 46 | const isParentBlock = parent && inlineElements.indexOf(parent.name) === -1; 47 | const children = isParentBlock 48 | ? groupInlineElements(dom.children) 49 | : dom.children; 50 | return ( 51 | 56 | {children.map((item, index) => step(item, index, item.parent, getTag))} 57 | 58 | ); 59 | } 60 | if (dom.type === 'text') { 61 | if ((dom.data || '').trim() === '') { 62 | return null; 63 | } 64 | if (parent && inlineElements.indexOf(parent.name) !== -1) { 65 | return dom.data || null; 66 | } 67 | const Component = getTag('span'); 68 | return {dom.data}; 69 | } 70 | return null; 71 | }; 72 | 73 | export default step; 74 | -------------------------------------------------------------------------------- /src/textToDOM.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Parser, DomHandler } from 'htmlparser2'; 4 | 5 | const textToDOM = (html: string): any => { 6 | const handler = new DomHandler(); 7 | const parser = new Parser(handler); 8 | parser.write(html); 9 | parser.done(); 10 | return handler.dom; 11 | }; 12 | 13 | export default textToDOM; 14 | -------------------------------------------------------------------------------- /src/toJSX.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | 5 | import { type GetTagFunc } from './createGetTagFunction'; 6 | import step, { type DOMElement } from './step'; 7 | 8 | const toJSX = (dom: DOMElement, getTag: GetTagFunc) => { 9 | if (!Array.isArray(dom)) { 10 | const Component = getTag('span'); 11 | return ; 12 | } 13 | const rootNode = { 14 | name: 'div', 15 | type: 'tag', 16 | children: dom, 17 | attribs: { 'data-jsx-to-html-root': true } 18 | }; 19 | return step( 20 | rootNode, 21 | 0, 22 | { name: 'div', type: 'tag', data: '', children: [rootNode], attribs: {} }, 23 | getTag 24 | ); 25 | }; 26 | 27 | export default toJSX; 28 | --------------------------------------------------------------------------------