├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── playground ├── bundle.js ├── dev-server.sh ├── flex.css ├── index.coffee └── index.html ├── src ├── index.coffee └── preprocess.coffee ├── test └── md2react-test.coffee └── testem.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/d866fb556184cc1edffd9d0f1ca205fe1916a7f6/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | lib/ 31 | test/*.js 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | playground/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ## v0.4.0 4 | 5 | - Start changelog 6 | - Update mdast to v0.11 7 | - Support tasklist 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 mizchi 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # md2react [![Circle CI](https://circleci.com/gh/mizchi/md2react/tree/master.svg?style=svg)](https://circleci.com/gh/mizchi/md2react/tree/master) 2 | 3 | ``` 4 | npm install --save md2react 5 | ``` 6 | 7 | See [md2react playground](http://mizchi.github.io/md2react/ "md2react playground") 8 | 9 | ## Example 10 | 11 | ```javascript 12 | global.React = require('react'); 13 | var md2react = require('md2react'); 14 | 15 | var md = '# Hello md2react'; 16 | var html = React.renderToString(md2react(md)); 17 | 18 | /* 19 |

Hello md2react

' 20 | //'

Hello

' 21 | */ 22 | ``` 23 | 24 | ## Checklist 25 | 26 | Compiled elements are given checked/unchecked class if bullet has checkbox. 27 | 28 | ```javascript 29 | var md = '- [x] a\n- [ ] b\n- c'; 30 | var html = React.renderToString(md2react(md, tasklist: true)); 31 | ``` 32 | 33 | ```html 34 |
35 | ``` 36 | 37 | Write your checklist style 38 | 39 | ## API 40 | 41 | - `md2react(markdown: string , mdastOptionsWithSanitize: Object): ReactElement` 42 | 43 | See mdast detail in [wooorm/mdast](https://github.com/wooorm/mdast "wooorm/mdast") 44 | 45 | And `sanitize: true` uses dompurify to raw html input(examle, ``) 46 | 47 | ## ChangeLog 48 | 49 | ### v0.5.1 50 | 51 | - Support table align 52 | 53 | ### v0.5.0 54 | 55 | - Update mdast to 0.12.0 56 | - Fix table align 57 | 58 | ## LICENSE 59 | 60 | MIT 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "md2react", 3 | "version": "0.8.8", 4 | "description": "markdown to react element", 5 | "main": "lib/index.js", 6 | "keywords": [ 7 | "react", 8 | "markdown", 9 | "mdast" 10 | ], 11 | "scripts": { 12 | "build-test": "./node_modules/browserify/bin/cmd.js -t coffeeify --extension='.coffee' test/md2react-test.coffee -o test/bundle.js", 13 | "test": "./node_modules/testem/testem.js ci", 14 | "prepublish": "coffee -o lib -c src/*", 15 | "build-playground": "browserify -t coffeeify --extension='.coffee' playground/index.coffee -o playground/bundle.js" 16 | }, 17 | "author": "mizchi", 18 | "license": "MIT", 19 | "files": [ 20 | "CHANGELOG.md", 21 | "LICENSE", 22 | "lib" 23 | ], 24 | "devDependencies": { 25 | "browserify": "^8.1.3", 26 | "chai": "^3.0.0", 27 | "coffeeify": "^1.0.0", 28 | "mocha": "^2.2.5", 29 | "react": "^0.12.2", 30 | "testem": "^0.8.5" 31 | }, 32 | "dependencies": { 33 | "dompurify": "^0.6.0", 34 | "mdast": "^0.26.2", 35 | "xmldom": "^0.1.19" 36 | }, 37 | "directories": { 38 | "test": "test" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/mizchi/md2react" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/mizchi/md2react/issues" 46 | }, 47 | "homepage": "https://github.com/mizchi/md2react" 48 | } 49 | -------------------------------------------------------------------------------- /playground/dev-server.sh: -------------------------------------------------------------------------------- 1 | wzrd index.coffee:bundle.js -- -t coffeeify --extension=".coffee" 2 | -------------------------------------------------------------------------------- /playground/flex.css: -------------------------------------------------------------------------------- 1 | .flex { 2 | display: -webkit-box; 3 | display: -webkit-flex; 4 | display: -ms-flexbox; 5 | display: flex; 6 | height: 80%; 7 | width: 80%; 8 | margin: 0 10%; 9 | border: 1px solid; 10 | borderRadius: 5px; 11 | borderColor: #999; 12 | } 13 | 14 | .previewContainer li.checked:before { 15 | content: '☑'; 16 | } 17 | 18 | .previewContainer li.unchecked:before { 19 | content: '□'; 20 | } 21 | -------------------------------------------------------------------------------- /playground/index.coffee: -------------------------------------------------------------------------------- 1 | global.React = require('react') 2 | md2react = require('../src/index') 3 | 4 | $ = React.createElement 5 | 6 | defaultMarkdown = ''' 7 | # Hello 8 | 9 | body 10 | 11 | 1. 1 12 | 2. 2 13 | 14 | ------ 15 | 16 | - [ ] unchecked 17 | - [x] checked 18 | - foo 19 | 20 | `a` 21 | 22 | ------ 23 | 24 | ``` 25 | bbb 26 | ``` 27 | 28 | **AA** 29 | 30 | *BB* 31 | 32 | [foo](/foo) 33 | 34 | ![image](http://placehold.it/20x20/27709b/ffffff) 35 | 36 | > aaa 37 | > bbb 38 | 39 | | TH | TH | 40 | | ---- | ---- | 41 | | TD | TD | 42 | | TD | TD | 43 | ''' 44 | 45 | defaultMarkdown = ''' 46 | ```js 47 | var x = 3; 48 | ``` 49 | ''' 50 | 51 | Editor = React.createClass 52 | update: -> 53 | editor = @refs.editor.getDOMNode() 54 | try 55 | content = md2react editor.value, 56 | gfm: true 57 | breaks: true 58 | tables: true 59 | commonmark: true 60 | footnotes: true 61 | # highlight: (code, lang, key) -> # custom highlighter 62 | # "#{lang}: #{code}" 63 | @setState content: content 64 | catch e 65 | console.warn 'markdown parse error' 66 | 67 | componentDidMount: -> 68 | editor = @refs.editor.getDOMNode() 69 | editor.value = defaultMarkdown 70 | @update() 71 | 72 | getInitialState: -> {content: null} 73 | 74 | render: -> 75 | $ 'div', {key: 'root'}, [ 76 | $ 'h1', {style: {textAlign: 'center', fontFamily: '"Poiret One", cursive', fontSize: '25px', height: '50px', lineHeight: '50px'}}, 'md2react playground' 77 | $ 'div', {key: 'layout', className: 'flex'}, [ 78 | $ 'div', {key: 'editorContainer', style:{ 79 | width: '50%', borderRight: '1px solid', borderColor: '#999', overflow: 'hidden'} 80 | }, [ 81 | $ 'textarea', { 82 | ref:'editor' 83 | onChange: @update 84 | style: {height: '100%', width: '100%', border: 0, outline: 0, fontSize: '14px', padding: '5px', overflow: 'auto', fontFamily:'Consolas, Menlo, monospace', resize: 'none', background: 'transparent'} 85 | } 86 | ] 87 | $ 'div',{ 88 | className: 'previewContainer', 89 | key: 'previewContainer', 90 | style: { 91 | width: '50%' 92 | overflow: 'auto' 93 | padding: '5px' 94 | fontFamily: "'Helvetica Neue', Helvetica" 95 | } 96 | }, if @state.content then [@state.content] else '' 97 | ] 98 | $ 'div', {width: '100%', style: {textAlign: 'center', marginTop: '10px'}}, [ 99 | $ 'a', {href:'https://github.com/mizchi/md2react', style: {fontFamily: 'Helvetica Neue, Helvetica', fontSize: '17px'}}, '[Fork me on GitHub](mizchi/md2react)' 100 | ] 101 | ] 102 | 103 | window.addEventListener 'DOMContentLoaded', -> 104 | React.render(React.createElement(Editor, {}), document.body) 105 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | md2react playground 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | HEADER1 13 | HEADER2 14 | 15 | 16 | 17 | 18 | 19 | 20 |
d1d2
21 | 22 | 23 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | mdast = require 'mdast' 2 | preprocess = require './preprocess' 3 | 4 | ATTR_WHITELIST = ['href', 'src', 'target'] 5 | 6 | $ = React.createElement 7 | 8 | defaultHTMLWrapperComponent = React.createClass 9 | _update: -> 10 | current = @props.html 11 | if @_lastHtml isnt current 12 | @_lastHtml = current 13 | node = @refs.htmlWrapper.getDOMNode() 14 | node.contentDocument.body.innerHTML = @props.html 15 | node.style.height = node.contentWindow.document.body.scrollHeight + 'px' 16 | node.style.width = node.contentWindow.document.body.scrollWidth + 'px' 17 | 18 | componentDidUpdate: -> @_update() 19 | componentDidMount: -> @_update() 20 | 21 | render: -> 22 | $ 'iframe', 23 | ref: 'htmlWrapper' 24 | html: @props.html 25 | style: 26 | border: 'none' 27 | 28 | toChildren = (node, defs, parentKey, tableAlign = []) -> 29 | return (for child, i in node.children 30 | compile(child, defs, parentKey+'_'+i, tableAlign)) 31 | 32 | isValidDocument = (doc) -> 33 | parsererrorNS = (new DOMParser()).parseFromString('INVALID', 'text/xml').getElementsByTagName("parsererror")[0].namespaceURI 34 | doc.getElementsByTagNameNS(parsererrorNS, 'parsererror').length == 0 35 | 36 | getPropsFromHTMLNode = (node, attrWhitelist) -> 37 | string = 38 | if node.subtype is 'folded' 39 | node.startTag.value + node.endTag.value 40 | else if node.subtype is 'void' 41 | node.value 42 | else 43 | null 44 | if !string? 45 | return null 46 | 47 | parser = new DOMParser() 48 | doc = parser.parseFromString(string, 'text/html') 49 | if !isValidDocument(doc) 50 | return null 51 | 52 | attrs = doc.body.firstElementChild.attributes 53 | props = {} 54 | for i in [0...attrs.length] 55 | attr = attrs.item(i) 56 | if !attrWhitelist? or (attr.name in attrWhitelist) 57 | props[attr.name] = attr.value 58 | props 59 | 60 | # Override by option 61 | sanitize = null 62 | highlight = null 63 | compile = (node, defs, parentKey='_start', tableAlign = null) -> 64 | key = parentKey+'_'+node.type 65 | 66 | switch node.type 67 | # No child 68 | when 'text' then rawValueWrapper node.value 69 | when 'escape' then '\\' 70 | when 'break' then $ 'br', {key} 71 | when 'horizontalRule' then $ 'hr', {key} 72 | when 'image' then $ 'img', {key, src: node.src, title: node.title, alt: node.alt} 73 | when 'inlineCode' then $ 'code', {key, className:'inlineCode'}, node.value 74 | when 'code' then highlight node.value, node.lang, key 75 | 76 | # Has children 77 | when 'root' then $ 'div', {key}, toChildren(node, defs, key) 78 | when 'strong' then $ 'strong', {key}, toChildren(node, defs, key) 79 | when 'emphasis' then $ 'em', {key}, toChildren(node, defs, key) 80 | when 'delete' then $ 's', {key}, toChildren(node, defs, key) 81 | when 'paragraph' then $ 'p', {key}, toChildren(node, defs, key) 82 | when 'link' then $ 'a', {key, href: node.href, title: node.title}, toChildren(node, defs, key) 83 | when 'heading' then $ ('h'+node.depth.toString()), {key}, toChildren(node, defs, key) 84 | when 'list' then $ (if node.ordered then 'ol' else 'ul'), {key}, toChildren(node, defs, key) 85 | when 'listItem' 86 | className = 87 | if node.checked is true 88 | 'checked' 89 | else if node.checked is false 90 | 'unchecked' 91 | else 92 | '' 93 | $ 'li', {key, className}, toChildren(node, defs, key) 94 | when 'blockquote' then $ 'blockquote', {key}, toChildren(node, defs, key) 95 | 96 | when 'linkReference' 97 | for def in defs 98 | if def.type is 'definition' and def.identifier is node.identifier 99 | return $ 'a', {key, href: def.link, title: def.title}, toChildren(node, defs, key) 100 | # There's no corresponding definition; render reference as plain text. 101 | if node.referenceType is 'full' 102 | $ 'span', {key}, [ 103 | '[' 104 | toChildren(node, defs, key) 105 | ']' 106 | "[#{node.identifier}]" 107 | ] 108 | else # referenceType must be 'shortcut' 109 | $ 'span', {key}, [ 110 | '[' 111 | toChildren(node, defs, key) 112 | ']' 113 | ] 114 | 115 | # Footnote 116 | when 'footnoteReference' 117 | title = '' 118 | for def in defs 119 | if def.footnoteNumber is node.footnoteNumber 120 | title = def.link ? "..." # FIXME: use def.children (stringification needed) 121 | return $ 'sup', {key, id: "fnref#{node.footnoteNumber}"}, [ 122 | $ 'a', {key: key+'-a', href: "#fn#{node.footnoteNumber}", title}, "#{node.footnoteNumber}" 123 | ] 124 | # There's no corresponding definition; render reference as plain text. 125 | $ 'span', {key}, "[^#{node.identifier}]" 126 | when 'footnoteDefinitionCollection' 127 | items = node.children.map (def, i) -> 128 | k = key+'-ol-li'+i 129 | # If `def` has children, we use them as `defBody`. And If `def` doesn't 130 | # have any, then it should have `link` text, so we use it. 131 | defBody = null 132 | if def.children? 133 | # If `def`s last child is a paragraph, append an anchor to `defBody`. 134 | # Otherwise we append nothing like Qiita does. 135 | # FIXME: We should not mutate a given AST. 136 | if (para = def.children[def.children.length - 1]).type is 'paragraph' 137 | para.children.push 138 | type: 'text' 139 | value: ' ' 140 | para.children.push 141 | type: 'link' 142 | href: "#fnref#{def.footnoteNumber}" 143 | children: [{type: 'text', value: '↩'}] 144 | defBody = toChildren(def, defs, key) 145 | else 146 | defBody = $ 'p', {key: k+'-p'}, [ 147 | def.link 148 | ' ' 149 | $ 'a', {key: k+'-p-a', href: "#fnref#{def.footnoteNumber}"}, '↩' 150 | ] 151 | $ 'li', {key: k, id: "fn#{def.footnoteNumber}"}, defBody 152 | $ 'div', {key, className: 'footnotes'}, [ 153 | $ 'hr', {key: key+'-hr'} 154 | $ 'ol', {key: key+'-ol'}, items 155 | ] 156 | 157 | # Table 158 | when 'table' then $ 'table', {key}, toChildren(node, defs, key, node.align) 159 | when 'tableHeader' 160 | $ 'thead', {key}, [ 161 | $ 'tr', {key: key+'-_inner-tr'}, node.children.map (cell, i) -> 162 | k = key+'-th'+i 163 | $ 'th', {key: k, style: {textAlign: tableAlign[i] ? 'left'}}, toChildren(cell, defs, k) 164 | ] 165 | 166 | when 'tableRow' 167 | # $ 'tr', {key} , [$ 'td', {key: key+'_inner-td'}, toChildren(node, defs, key)] 168 | $ 'tbody', {key}, [ 169 | $ 'tr', {key: key+'-_inner-td'}, node.children.map (cell, i) -> 170 | k = key+'-td'+i 171 | $ 'td', {key: k, style: {textAlign: tableAlign[i] ? 'left'}}, toChildren(cell, defs, k) 172 | ] 173 | when 'tableCell' then $ 'span', {key}, toChildren(node, defs, key) 174 | 175 | # Raw html 176 | when 'html' 177 | if node.subtype is 'raw' 178 | $ htmlWrapperComponent, key: key, html: node.value 179 | else if node.subtype is 'computed' 180 | k = key+'_'+node.tagName 181 | props = {} 182 | for name, value of node.attrs ? {} 183 | props[name] = value 184 | props.key = k 185 | if node.children? 186 | $ node.tagName, props, toChildren(node, defs, k) 187 | else 188 | $ node.tagName, props 189 | else if node.subtype is 'folded' 190 | k = key+'_'+node.tagName 191 | props = getPropsFromHTMLNode(node, ATTR_WHITELIST) ? {} 192 | props.key = k 193 | $ node.startTag.tagName, props, toChildren(node, defs, k) 194 | else if node.subtype is 'void' 195 | k = key+'_'+node.tagName 196 | props = getPropsFromHTMLNode(node, ATTR_WHITELIST) ? {} 197 | props.key = k 198 | $ node.tagName, props 199 | else if node.subtype is 'special' 200 | $ 'span', { 201 | key: key + ':special' 202 | style: { 203 | color: 'gray' 204 | } 205 | }, node.value 206 | else 207 | $ 'span', { 208 | key: key + ':parse-error' 209 | style: { 210 | backgroundColor: 'red' 211 | color: 'white' 212 | } 213 | }, node.value 214 | else 215 | throw node.type + ' is unsuppoted node type. report to https://github.com/mizchi/md2react/issues' 216 | 217 | htmlWrapperComponent = null 218 | rawValueWrapper = null 219 | 220 | module.exports = (raw, options = {}) -> 221 | sanitize = options.sanitize ? true 222 | htmlWrapperComponent = options.htmlWrapperComponent ? defaultHTMLWrapperComponent 223 | rawValueWrapper = options.rawValueWrapper ? (text) -> text 224 | 225 | highlight = options.highlight ? (code, lang, key) -> 226 | $ 'pre', {key, className: 'code'}, [ 227 | $ 'code', {key: key+'-_inner-code'}, code 228 | ] 229 | ast = mdast.parse raw, options 230 | [ast, defs] = preprocess(ast, raw, options) 231 | ast = options.preprocessAST?(ast) ? ast 232 | compile(ast, defs) 233 | -------------------------------------------------------------------------------- /src/preprocess.coffee: -------------------------------------------------------------------------------- 1 | preprocess = (root, sourceText, options) -> 2 | # Some yet-to-be-preprocessed HTML nodes are directly convertible to 3 | # "raw" HTML node - convert them. 4 | convertPreToRawHTML(root) 5 | 6 | # Literal `
...
` are often broken up into a series of HTML nodes. 7 | # Put them back together into a single "raw" HTML node. 8 | convertScatteredPreToRawHTML(root, sourceText) 9 | 10 | # Formalize HTML tags. 11 | root.children = decomposeHTMLNodes(root.children) 12 | root.children = foldHTMLNodes(root.children) 13 | 14 | # Process footnotes and and links. 15 | if options.footnotes 16 | mapping = defineFootnoteNumber(root).mapping 17 | applyFootnoteNumber(root, mapping) 18 | defs = removeDefinitions(root) 19 | if options.footnotes 20 | appendFootnoteDefinitionCollection(root, defs) 21 | 22 | # Sanitize HTML tags. 23 | root = wrapHTMLNodeInParagraph(root) 24 | root = sanitizeTag(root) 25 | 26 | [root, defs] 27 | 28 | # Sets `footnoteNumber` property to every footnote reference node. 29 | # Footnote number starts at 1 and is incremented whenever a new footnote 30 | # identifier appears. Returns `{mapping, maxNumber}` 31 | # 32 | # Example (Markdown): 33 | # 34 | # first footnote[^foo] # footnoteNumber for [^foo] is 1 35 | # second footnote[^bar] # footnoteNumber for [^bar] is 2 36 | # use first footnote again[^foo] # footnoteNumber for [^foo] is 1 37 | # yet another footnote[^qux] # footnoteNumber for [^qux] is 3 38 | defineFootnoteNumber = (node, num = 1, mapping = {}) -> 39 | for child in node.children 40 | if child.type is 'footnoteReference' 41 | id = child.identifier 42 | unless mapping[id]? 43 | mapping[id] = num 44 | num += 1 45 | child.footnoteNumber = mapping[id] 46 | if child.children 47 | num = defineFootnoteNumber(child, num, mapping).maxNumber 48 | {mapping, maxNumber: num} 49 | 50 | # Sets `footnoteNumber` property to every footnote definition node using a 51 | # given identifier-to-number mapping. `footnoteNumber` of a definition with 52 | # undefined identifier will be 0. 53 | # 54 | # Example (Markdown): 55 | # 56 | # Given mapping = `{"foo": 1, "bar": 2, "qux": 3}`, 57 | # 58 | # [^bar]: this is bar # footnoteNumber for this node is 2 59 | # [^foo]: this is foo # footnoteNumber for this node is 1 60 | # [^qux]: this is qux # footnoteNumber for this node is 3 61 | # [^xxx]: this is undef # footnoteNumber for this node is 0 62 | applyFootnoteNumber = (node, mapping) -> 63 | return unless node.children? 64 | for child in node.children 65 | # Workaround: 66 | # Footnote definition nodes are supposed to have `footnoteDefinition` type 67 | # but mdast v0.24.0 classifies them as `definition` type if their body 68 | # doesn't contain whitespace (e.g. `[^foo]: body_without_space`). 69 | isFootnoteDefLike = child.type is 'definition' and /^[^]/.test(child.identifier) 70 | if child.type is 'footnoteDefinition' or isFootnoteDefLike 71 | id = if isFootnoteDefLike then child.identifier.slice(1) else child.identifier 72 | child.footnoteNumber = mapping[id] || 0 73 | applyFootnoteNumber(child, mapping) 74 | 75 | # Appends a `footnoteDefinitionCollection` node to `node.children` if `defs` 76 | # contains one or more footnote definition nodes which `footnoteNumber` is > 0. 77 | # Otherwise, do nothing. 78 | # Elements of the collection are sorted by their `footnoteNumber` in ascending 79 | # order. 80 | appendFootnoteDefinitionCollection = (node, defs) -> 81 | footnoteDefs = (def for def in defs when def.footnoteNumber? and def.footnoteNumber > 0) 82 | footnoteDefs.sort (a, b) -> 83 | a.footnoteNumber - b.footnoteNumber 84 | if footnoteDefs.length > 0 85 | node.children.push({type: 'footnoteDefinitionCollection', children: footnoteDefs}) 86 | 87 | # Removes all footnote or link definition nodes from a given AST and returns 88 | # removed nodes. 89 | removeDefinitions = (node) -> 90 | return [] unless node.children? 91 | children = [] 92 | defs = [] 93 | for child in node.children 94 | if child.type in ['definition', 'footnoteDefinition'] 95 | defs.push(child) 96 | else 97 | childDefs = removeDefinitions(child) 98 | Array::push.apply(defs, childDefs) 99 | children.push(child) 100 | node.children = children 101 | defs 102 | 103 | convertPreToRawHTML = (root) -> 104 | for node in root.children 105 | if node.type is 'html' and /^][^]*<\/pre>$/i.test(node.value) 106 | node.subtype = 'raw' 107 | 108 | convertScatteredPreToRawHTML = (root, sourceText) -> 109 | preTexts = [] 110 | startPreNode = null 111 | startParaIndex = null 112 | sourceLines = null 113 | 114 | for node, i in root.children 115 | isStart = ( 116 | node.type is 'html' and 117 | /^]/i.test(node.value) 118 | ) 119 | if isStart 120 | startPreNode = node 121 | startParaIndex = i 122 | 123 | paraLastNode = null 124 | isEnd = ( 125 | startPreNode? and 126 | node.type is 'html' and 127 | /<\/pre>$/i.test(node.value) 128 | ) or ( 129 | startPreNode? and 130 | node.type is 'paragraph' and 131 | (paraLastNode = node.children[node.children.length - 1]) and 132 | paraLastNode.type is 'html' and 133 | /<\/pre>$/i.test(paraLastNode.value) 134 | ) 135 | if isEnd 136 | endPreNode = paraLastNode ? node 137 | sourceLines ?= sourceText.split(/^/m) # split lines _preserving newline character_ 138 | sliceStart = startIndexFromPosition(startPreNode.position, sourceLines) 139 | sliceEnd = endIndexFromPosition(endPreNode.position, sourceLines) 140 | rawHTML = sourceText.slice(sliceStart, sliceEnd) 141 | preTexts.push 142 | startParaIndex: startParaIndex 143 | paraCount: i - startParaIndex + 1 144 | rawHTML: rawHTML 145 | startPreNode = null 146 | startParaIndex = null 147 | 148 | offset = 0 149 | for pre in preTexts 150 | rawHTMLNode = {type: 'html', subtype: 'raw', value: pre.rawHTML} 151 | start = pre.startParaIndex - offset 152 | root.children.splice(start, pre.paraCount, rawHTMLNode) 153 | offset = pre.paraCount - 1 154 | 155 | startIndexFromPosition = (pos, lines) -> 156 | index = 0 157 | for i in [0...(pos.start.line - 1)] 158 | index += lines[i].length 159 | index += pos.start.column - 1 160 | index 161 | 162 | endIndexFromPosition = (pos, lines) -> 163 | index = 0 164 | for i in [0...(pos.end.line - 1)] 165 | index += lines[i].length 166 | index += pos.end.column - 1 167 | index 168 | 169 | # Returns nodes by converting each occurrence of a series of nodes enclosed by 170 | # a "start" and an "end" HTML node into one "folded" HTML node. A folded node 171 | # has `folded` subtype and two additional properties, `startTag` and `endTag`. 172 | # Its children are the enclosed nodes. 173 | foldHTMLNodes = (nodes) -> 174 | processedNodes = [] 175 | for node in nodes 176 | if node.subtype is 'end' 177 | startTagIndex = null 178 | for pNode, index in processedNodes 179 | if pNode.subtype is 'start' and pNode.tagName is node.tagName 180 | startTagIndex = index 181 | if !startTagIndex? 182 | processedNodes.push(node) 183 | continue 184 | startTag = processedNodes[startTagIndex] 185 | children = processedNodes.splice(startTagIndex).slice(1) 186 | folded = 187 | type: 'html' 188 | subtype: 'folded' 189 | tagName: startTag.tagName 190 | startTag: startTag 191 | endTag: node 192 | children: children 193 | processedNodes.push(folded) 194 | else 195 | if node.children? 196 | node.children = foldHTMLNodes(node.children) 197 | processedNodes.push(node) 198 | processedNodes 199 | 200 | # Decomposes HTML nodes in a given array of nodes and their children. Sets 201 | # `subtype` of an HTML node to `malformed` if the node could not be decomposed. 202 | decomposeHTMLNodes = (nodes) -> 203 | processedNodes = [] 204 | for node in nodes 205 | if node.type is 'html' and node.subtype is 'raw' 206 | processedNodes.push(node) 207 | else if node.type is 'html' 208 | fragmentNodes = decomposeHTMLNode(node) 209 | if fragmentNodes? 210 | processedNodes.push(fragmentNodes...) 211 | else 212 | node.subtype = 'malformed' 213 | processedNodes.push(node) 214 | else 215 | if node.children? 216 | node.children = decomposeHTMLNodes(node.children) 217 | processedNodes.push(node) 218 | processedNodes 219 | 220 | # Decomposes a given HTML node into text nodes and "simple" HTML nodes. 221 | # If decomposition failed, returns null. 222 | # 223 | # mdast can emit "complex" HTML node whose value is like "text". 224 | # Take this value as an example, this method breaks it down to three nodes: 225 | # HTML start tag node ""; text node "text"; and HTML end tag node "". 226 | # 227 | # Each decomposed HTML node has the `subtype` property. 228 | # See `createNodeFromHTMLFragment()` for the possible values. 229 | decomposeHTMLNode = (node) -> 230 | value = node.value 231 | # mdast may insert "\n\n" between adjacent HTML tags. 232 | if node.position.start.line is node.position.end.line 233 | value = value.replace(/\n\n/, '') 234 | fragments = decomposeHTMLString(value) 235 | fragments?.map(createNodeFromHTMLFragment) 236 | 237 | # Splits a given string into an array where each element is ether an HTML tag 238 | # or a string which doesn't contain angle brackets, then returns the array. 239 | # If a given string contains lone angle brackets, returns null. 240 | # 241 | # Example: 242 | # Given "foobar
baz" 243 | # Returns ["foo", "", "bar", "
", "
", "baz"] 244 | # Given " oops >_< " 245 | # Returns null 246 | decomposeHTMLString = (str) -> 247 | if str is '' 248 | return null 249 | matches = str.match(/<[^>]*>|[^<>]+/g) 250 | sumLength = matches.reduce(((len, s) -> len+s.length), 0) 251 | if sumLength isnt str.length 252 | null 253 | else 254 | matches 255 | 256 | createNodeFromHTMLFragment = (str) -> 257 | if /^[^<]/.test(str) 258 | return { 259 | type: 'text' 260 | value: str 261 | } 262 | [..., slash, name] = /^<(\/?)([0-9A-Z]+)/i.exec(str) ? [] 263 | subtype = 264 | if !name? 265 | 'special' 266 | else if slash is '/' 267 | 'end' 268 | else if isVoidElement(name) 269 | 'void' 270 | else 271 | 'start' 272 | type: 'html' 273 | subtype: subtype 274 | tagName: name 275 | value: str 276 | 277 | isVoidElement = (elementName) -> 278 | voidElementNames = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'] 279 | voidElementNames.indexOf(elementName) != -1 280 | 281 | # Wraps each top-level HTML node in a paragraph node. 282 | # 283 | # mdast wil put an HTML node directly in `root.children` without wrapping in a 284 | # paragraph node if we write some block element in Markdown: 285 | # 286 | # Markdown: 287 | #
foo 288 | # 289 | # bar
290 | # 291 | # AST: 292 | # root.children = [ { type: "html", value: "
foo\nbar
" } ] 293 | # 294 | # However, we disallow writing block element tag in Markdown and convert such 295 | # an HTML node into a series of text nodes by using `sanitizeTag()`. 296 | # To form a paragraph node which have such text nodes as children, we wrap 297 | # top-level HTML nodes in paragraph nodes before applying `sanitizeTag()`. 298 | wrapHTMLNodeInParagraph = (root) -> 299 | children = [] 300 | for child in root.children 301 | if child.type is 'html' 302 | children.push({type: 'paragraph', children: [child]}) 303 | else 304 | children.push(child) 305 | root.children = children 306 | root 307 | 308 | # A subset of [phrasing content] tags, plus RP and RT. 309 | # We rejected some phrasing content tags from the set because it is able 310 | # to write a semantically incorrect HTML with them, which leads to a crash of 311 | # React. 312 | # 313 | # Rejected tags are kinds of: 314 | # 315 | # - embedded contents like IFRAME, MATH, AUDIO, and VIDEO, except for IMG; 316 | # - interactive contents like BUTTON, KEYGEN, and PROGRESS; 317 | # 318 | # [phrasing content]: http://www.w3.org/TR/2011/WD-html5-20110525/content-models.html#phrasing-content-0 319 | ALLOWED_TAG_NAMES = [ 320 | 'a', 'abbr', 'b', 'br', 'cite', 'code', 'del', 'dfn', 'em', 'i', 'img', 321 | 'input', 'ins', 'kbd', 'mark', 'ruby', 'rp', 'rt', 'q', 's', 'samp', 'small', 322 | 'span', 'strong', 'sub', 'sup', 'u', 'wbr', 323 | ] 324 | 325 | # Flatten a disallowed kind of folded tag node into a series of nodes. 326 | # 327 | # Example: 328 | # node.children = [ { type: 'html, subtype: 'folded', 329 | # startTag: startTag, endTag: endTag, children: [child1, child2] }, ... ] 330 | # # ^this is flattend into: 331 | # node.children = [ startTag, child1, child2, endTag, ... ] 332 | # 333 | # startTag and endTag are now freestanding, so they will be rendered as invalid HTML tags. 334 | sanitizeTag = (node) -> 335 | return node unless node.children? 336 | children = [] 337 | for child in node.children 338 | if child.subtype is 'folded' and child.tagName not in ALLOWED_TAG_NAMES 339 | children.push(child.startTag) 340 | Array::push.apply(children, sanitizeTag(child).children) 341 | children.push(child.endTag) 342 | else 343 | children.push(sanitizeTag(child)) 344 | node.children = children 345 | node 346 | 347 | module.exports = preprocess 348 | -------------------------------------------------------------------------------- /test/md2react-test.coffee: -------------------------------------------------------------------------------- 1 | should = chai.should() 2 | global.React = require 'react' 3 | md2react = require '../src/index' 4 | 5 | options = gfm: true, breaks: true, tasklist: true, footnotes: true 6 | 7 | describe 'text', -> 8 | 9 | it 'should be compiled', -> 10 | React.renderToStaticMarkup md2react 'foo', options 11 | .should.equal '

foo

' 12 | 13 | describe 'escape', -> 14 | 15 | it 'should be compiled', -> 16 | React.renderToStaticMarkup md2react '\\', options 17 | .should.equal '

\\

' 18 | 19 | describe 'break', -> 20 | 21 | it 'should be compiled', -> 22 | React.renderToStaticMarkup md2react ''' 23 | foo 24 | bar 25 | baz 26 | ''' 27 | , options 28 | .should.equal '

foo
bar
baz

' 29 | 30 | describe 'horizontalRule', -> 31 | 32 | it 'should be compiled', -> 33 | React.renderToStaticMarkup md2react ''' 34 | --- 35 | ''', options 36 | .should.equal '

' 37 | 38 | describe 'image', -> 39 | 40 | it 'should be compiled', -> 41 | React.renderToStaticMarkup md2react ''' 42 | ![image](http://example.com/image.png) 43 | ''', options 44 | .should.equal '

image

' 45 | 46 | describe 'inlineCode', -> 47 | 48 | it 'should be compiled', -> 49 | React.renderToStaticMarkup md2react ''' 50 | `var a = 100;` 51 | ''', options 52 | .should.equal '

var a = 100;

' 53 | 54 | describe 'code', -> 55 | 56 | it 'should be compiled', -> 57 | React.renderToStaticMarkup md2react ''' 58 | ``` 59 | var a = 100; 60 | var b = 200; 61 | 62 | var c = 300; 63 | ``` 64 | ''', options 65 | .should.equal '
var a = 100;\nvar b = 200;\n\nvar c = 300;
' 66 | 67 | describe 'root', -> 68 | 69 | it 'should be compiled', -> 70 | React.renderToStaticMarkup md2react '', options 71 | .should.equal '
' 72 | 73 | it 'should be compiled when node has children', -> 74 | React.renderToStaticMarkup md2react '', options 75 | .should.equal '
' 76 | 77 | describe 'strong', -> 78 | 79 | it 'should be compiled', -> 80 | React.renderToStaticMarkup md2react '**foo**', options 81 | .should.equal '

foo

' 82 | 83 | it 'should be compiled when node has children', -> 84 | React.renderToStaticMarkup md2react '**foo~~bar~~baz**', options 85 | .should.equal '

foobarbaz

' 86 | 87 | describe 'emphasis', -> 88 | 89 | it 'should be compiled', -> 90 | React.renderToStaticMarkup md2react '*foo*', options 91 | .should.equal '

foo

' 92 | 93 | it 'should be compiled when node has children', -> 94 | React.renderToStaticMarkup md2react '*foo~~bar~~baz*', options 95 | .should.equal '

foobarbaz

' 96 | 97 | describe 'delete', -> 98 | 99 | it 'should be compiled', -> 100 | React.renderToStaticMarkup md2react '~~foo~~', options 101 | .should.equal '

foo

' 102 | 103 | it 'should be compiled when node has children', -> 104 | React.renderToStaticMarkup md2react '~~foo**bar**baz~~', options 105 | .should.equal '

foobarbaz

' 106 | 107 | describe 'paragraph', -> 108 | 109 | it 'should be compiled', -> 110 | React.renderToStaticMarkup md2react 'foo', options 111 | .should.equal '

foo

' 112 | 113 | it 'should be compiled when node has children', -> 114 | React.renderToStaticMarkup md2react 'foo', options 115 | .should.equal '

foo

' 116 | 117 | describe 'link', -> 118 | 119 | it 'should be compiled', -> 120 | React.renderToStaticMarkup md2react '[foo](http://example.com)', options 121 | .should.equal '

foo

' 122 | 123 | it 'should be compiled when node has children', -> 124 | React.renderToStaticMarkup md2react '[foo**bar**baz](http://example.com)', options 125 | .should.equal '

foobarbaz

' 126 | 127 | describe 'heading', -> 128 | 129 | it 'should be compiled', -> 130 | React.renderToStaticMarkup md2react ''' 131 | # heading1 132 | ## heading2 133 | ### heading3 134 | #### heading4 135 | ##### heading5 136 | ###### heading6 137 | ''', options 138 | .should.equal '

heading1

heading2

heading3

heading4

heading5
heading6
' 139 | 140 | it 'should be compiled when node has children', -> 141 | React.renderToStaticMarkup md2react ''' 142 | # foo**bar**baz 143 | ## foo**bar**baz 144 | ### foo**bar**baz 145 | #### foo**bar**baz 146 | ##### foo**bar**baz 147 | ###### foo**bar**baz 148 | ''', options 149 | .should.equal '

foobarbaz

foobarbaz

foobarbaz

foobarbaz

foobarbaz
foobarbaz
' 150 | 151 | describe 'list', -> 152 | 153 | it 'should be compiled', -> 154 | React.renderToStaticMarkup md2react ''' 155 | - foo 156 | - [ ] bar 157 | - [x] baz 158 | 1. foo 159 | 1. bar 160 | 1. baz 161 | ''', options 162 | .should.equal '
  • foo

  • bar

  • baz

  1. foo

  2. bar

  3. baz

' 163 | 164 | it 'should be compiled when node has children', -> 165 | React.renderToStaticMarkup md2react ''' 166 | - foo**bar**baz 167 | - [ ] bar*baz*qux 168 | - [x] bar~~qux~~quux 169 | 1. foo**bar**baz 170 | 1. bar*baz*qux 171 | 1. bar~~qux~~quux 172 | ''', options 173 | .should.equal '
  • foobarbaz

  • barbazqux

  • barquxquux

  1. foobarbaz

  2. barbazqux

  3. barquxquux

' 174 | 175 | describe 'blockquote', -> 176 | 177 | it 'should be compiled', -> 178 | React.renderToStaticMarkup md2react ''' 179 | > foo 180 | bar 181 | baz 182 | ''', options 183 | .should.equal '

foo
bar
baz

' 184 | 185 | it 'should be compiled when node has children', -> 186 | React.renderToStaticMarkup md2react ''' 187 | > foo**bar**baz 188 | bar*baz*qux 189 | baz~~qux~~quux 190 | ''', options 191 | .should.equal '

foobarbaz
barbazqux
bazquxquux

' 192 | 193 | describe 'linkReference', -> 194 | 195 | it 'should be compiled', -> 196 | React.renderToStaticMarkup md2react ''' 197 | [foo][Example] 198 | [bar][example] 199 | [Example] 200 | [example]: http://example.com 201 | ''', options 202 | .should.equal '

foo
bar
Example

' 203 | 204 | it 'should be compiled when node has children', -> 205 | React.renderToStaticMarkup md2react ''' 206 | [foo**bar**baz][Example] 207 | [foo**bar**baz][example] 208 | [Example] 209 | [example]: http://example.com 210 | ''', options 211 | .should.equal '

foobarbaz
foobarbaz
Example

' 212 | 213 | describe 'footnotes', -> 214 | 215 | it 'should be compiled', -> 216 | React.renderToStaticMarkup md2react ''' 217 | a[^foo]a[^foo]b[^bar] 218 | [^foo]: description1 219 | [^bar]: description2 220 | ''', options 221 | .should.equal '

a1a1b2


  1. description1

  2. description2

' 222 | 223 | it 'should be compiled when node has children', -> 224 | React.renderToStaticMarkup md2react ''' 225 | a[^foo]a[^foo]b[^bar] 226 | [^foo]: description**1** 227 | [^bar]: description[2](http://example.com) 228 | ''', options 229 | .should.equal '

a1a1b2


  1. description**1**

  2. description2

' 230 | 231 | describe 'table', -> 232 | 233 | it 'should be compiled', -> 234 | React.renderToStaticMarkup md2react ''' 235 | | foo | bar | baz | 236 | |:----|:---:|----:| 237 | | 1 | 2 | 3 | 238 | ''', options 239 | .should.equal '
foobarbaz
123
' 240 | 241 | describe 'html', -> 242 | 243 | it 'should be compiled', -> 244 | React.renderToStaticMarkup md2react ''' 245 | foo 246 | ''', options 247 | .should.equal '

foo

' 248 | 249 | describe 'complex', -> 250 | 251 | it 'should be compiled', -> 252 | md = ''' 253 | # Hello 254 | hello 255 | 256 | - a 257 | - b 258 | 259 | ---- 260 | 261 | - [ ] unchecked 262 | - [x] checked 263 | - plain 264 | 265 | ------- 266 | 267 | 1. 1 268 | 2. 2 269 | 270 | ------- 271 | 272 | - [x] a 273 | - [ ] b 274 | - c 275 | 276 | ------ 277 | 278 | `a` 279 | 280 | ~~striked~~ 281 | 282 | 283 | 284 | 285 | ``` 286 | bbb 287 | ``` 288 | 289 | **AA** 290 | 291 | *BB* 292 | 293 | [foo](/foo) 294 | 295 | ![img](/img.png) 296 | 297 | > aaa 298 | > bbb 299 | 300 | 301 | | TH | TH | 302 | | ---- | ---- | 303 | | TD | TD | 304 | | TD | TD | 305 | 306 | - 307 | loose 308 | 309 | - item 310 | 311 | ''' 312 | element = md2react md, gfm: true, breaks: true, tasklist: true 313 | React.renderToStaticMarkup element 314 | .should.equal '

Hello

hello

  • a

  • b


  • unchecked

  • checked

  • plain


  1. 1

  2. 2


  • a

  • b

  • c


a

striked

bbb

AA

BB

foo

img

aaa
bbb

THTH
TDTD
TDTD

-
loose

  • item

' 315 | -------------------------------------------------------------------------------- /testem.yml: -------------------------------------------------------------------------------- 1 | framework: mocha+chai 2 | before_tests: npm run build-test 3 | src_files: 4 | - test/*.coffee 5 | serve_files: 6 | - test/bundle.js 7 | launch_in_dev: 8 | - Chrome 9 | launch_in_ci: 10 | - Chrome 11 | --------------------------------------------------------------------------------