├── .node-version ├── test ├── .eslintrc.yml ├── markdown-it-incremental-dom.js └── renderer.js ├── .eslintignore ├── .prettierrc.yml ├── docs ├── .eslintrc.yml ├── images │ ├── repainting-innerhtml.gif │ └── repainting-incremental-dom.gif ├── index.js ├── index.css ├── index.html └── docs.md ├── .prettierignore ├── entry.js ├── .eslintrc.yml ├── .travis.yml ├── babel.config.js ├── version.js ├── src ├── markdown-it-incremental-dom.js └── mixins │ ├── rules.js │ └── renderer.js ├── LICENSE ├── webpack.config.js ├── package.json ├── .gitignore ├── CHANGELOG.md └── README.md /.node-version: -------------------------------------------------------------------------------- 1 | v10.13.0 2 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | coverage/ 4 | node_modules 5 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | trailingComma: es5 4 | -------------------------------------------------------------------------------- /docs/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | node: false 4 | es6: false 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .nyc_output/ 3 | dist/ 4 | lib/ 5 | coverage/ 6 | node_modules 7 | package.json 8 | -------------------------------------------------------------------------------- /docs/images/repainting-innerhtml.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/markdown-it-incremental-dom/HEAD/docs/images/repainting-innerhtml.gif -------------------------------------------------------------------------------- /docs/images/repainting-incremental-dom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhatt/markdown-it-incremental-dom/HEAD/docs/images/repainting-incremental-dom.gif -------------------------------------------------------------------------------- /entry.js: -------------------------------------------------------------------------------- 1 | import markdownitIncrementalDOM from './src/markdown-it-incremental-dom' 2 | 3 | export { markdownitIncrementalDOM } 4 | export default markdownitIncrementalDOM 5 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | 5 | extends: 6 | - airbnb-base 7 | - prettier 8 | 9 | rules: 10 | class-methods-use-this: off 11 | func-names: off 12 | no-param-reassign: off 13 | no-undef: off 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10.13.0 4 | - 8.13.0 5 | - 6.14.4 6 | 7 | cache: yarn 8 | sudo: false 9 | 10 | script: 11 | - yarn lint 12 | - yarn format:check 13 | - yarn test:coverage --ci --maxWorkers=2 --verbose 14 | 15 | after_success: yarn coveralls 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: '6.14' } }]], 3 | plugins: [ 4 | ['@babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }], 5 | ], 6 | env: { 7 | test: { 8 | presets: [['@babel/preset-env', { targets: { node: 'current' } }]], 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const unreleased = '## [Unreleased]' 5 | const [date] = new Date().toISOString().split('T') 6 | const version = `## v${process.env.npm_package_version} - ${date}` 7 | 8 | const changelog = path.resolve(__dirname, 'CHANGELOG.md') 9 | const content = fs.readFileSync(changelog, 'utf8') 10 | 11 | fs.writeFileSync( 12 | changelog, 13 | content.replace(unreleased, `${unreleased}\n\n${version}`) 14 | ) 15 | -------------------------------------------------------------------------------- /src/markdown-it-incremental-dom.js: -------------------------------------------------------------------------------- 1 | import renderer from './mixins/renderer' 2 | import rules from './mixins/rules' 3 | 4 | export default function(md, target, opts = {}) { 5 | const options = { incrementalizeDefaultRules: true, ...opts } 6 | const incrementalDOM = !target && window ? window.IncrementalDOM : target 7 | const mixin = renderer(incrementalDOM) 8 | 9 | Object.defineProperty(md, 'IncrementalDOMRenderer', { 10 | get() { 11 | const extended = Object.assign( 12 | Object.create(Object.getPrototypeOf(md.renderer)), 13 | md.renderer, 14 | mixin 15 | ) 16 | 17 | if (options.incrementalizeDefaultRules) { 18 | extended.rules = { ...extended.rules, ...rules(incrementalDOM) } 19 | } 20 | 21 | return extended 22 | }, 23 | }) 24 | 25 | md.renderToIncrementalDOM = (src, env = {}) => 26 | md.IncrementalDOMRenderer.render(md.parse(src, env), md.options, env) 27 | 28 | md.renderInlineToIncrementalDOM = (src, env = {}) => 29 | md.IncrementalDOMRenderer.render(md.parseInline(src, env), md.options, env) 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Yuki Hattori (yukihattori1116@gmail.com) 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 | -------------------------------------------------------------------------------- /src/mixins/rules.js: -------------------------------------------------------------------------------- 1 | export default function(incrementalDom) { 2 | const { elementClose, elementOpen, elementVoid, text } = incrementalDom 3 | 4 | return { 5 | code_inline(tokens, idx, options, env, slf) { 6 | return () => { 7 | elementOpen.apply( 8 | this, 9 | ['code', '', []].concat(slf.renderAttrsToArray(tokens[idx])) 10 | ) 11 | text(tokens[idx].content) 12 | elementClose('code') 13 | } 14 | }, 15 | 16 | code_block(tokens, idx, options, env, slf) { 17 | return () => { 18 | elementOpen.apply( 19 | this, 20 | ['pre', '', []].concat(slf.renderAttrsToArray(tokens[idx])) 21 | ) 22 | elementOpen('code') 23 | text(tokens[idx].content) 24 | elementClose('code') 25 | elementClose('pre') 26 | } 27 | }, 28 | 29 | hardbreak() { 30 | return () => elementVoid('br') 31 | }, 32 | 33 | softbreak(tokens, idx, options) { 34 | return () => (options.breaks ? elementVoid('br') : text('\n')) 35 | }, 36 | 37 | text(tokens, idx) { 38 | return () => text(tokens[idx].content) 39 | }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | ;(() => { 2 | document.addEventListener('DOMContentLoaded', () => { 3 | const md = markdownit().use(markdownitIncrementalDOM) 4 | 5 | const text = document.querySelector('#text') 6 | const target = document.querySelector('#target') 7 | const options = document.querySelectorAll('.markdown-options') 8 | let method = document.querySelector('.markdown-options:checked').value 9 | 10 | const render = function() { 11 | if (method === 'incrementalDOM') { 12 | IncrementalDOM.patch(target, md.renderToIncrementalDOM(text.value)) 13 | } else { 14 | target.innerHTML = md.render(text.value) 15 | } 16 | } 17 | 18 | const initializeRendering = function() { 19 | text.removeAttribute('disabled') 20 | text.removeAttribute('placeholder') 21 | text.addEventListener('input', render) 22 | 23 | Array.prototype.forEach.call(options, elm => { 24 | elm.addEventListener('change', function onChange() { 25 | method = this.value 26 | render() 27 | }) 28 | }) 29 | 30 | render() 31 | } 32 | 33 | text.setAttribute('disabled', 'disabled') 34 | 35 | fetch('./docs.md', { headers: { 'Content-Type': 'text/plain' } }) 36 | .then(res => res.text()) 37 | .then(t => { 38 | text.value = t 39 | initializeRendering() 40 | }) 41 | .catch(() => { 42 | text.value = '*Failed initializing docs.*' 43 | initializeRendering() 44 | }) 45 | }) 46 | })() 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const objectRestSpread = require('@babel/plugin-proposal-object-rest-spread') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const packageConfig = require('./package.json') 5 | 6 | const banner = `${packageConfig.name} ${packageConfig.version} 7 | ${packageConfig.repository.url} 8 | 9 | Includes htmlparser2 10 | https://github.com/fb55/htmlparser2/ 11 | https://github.com/fb55/htmlparser2/raw/master/LICENSE 12 | 13 | @license ${packageConfig.license} 14 | ${packageConfig.repository.url}/raw/master/LICENSE` 15 | 16 | const basename = path.basename(packageConfig.main, '.js') 17 | const browsers = ['> 1%', 'last 2 versions', 'Firefox ESR', 'IE >= 9'] 18 | const configuration = { 19 | output: { 20 | path: path.resolve(__dirname, 'dist'), 21 | filename: '[name].js', 22 | libraryTarget: 'umd', 23 | umdNamedDefine: true, 24 | }, 25 | plugins: [new webpack.BannerPlugin(banner)], 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js$/, 30 | exclude: /node_modules/, 31 | loader: 'babel-loader', 32 | options: { 33 | babelrc: false, 34 | presets: [ 35 | [ 36 | '@babel/preset-env', 37 | { modules: false, targets: { browsers }, useBuiltIns: 'usage' }, 38 | ], 39 | ], 40 | plugins: [[objectRestSpread, { useBuiltIns: true }]], 41 | }, 42 | }, 43 | ], 44 | }, 45 | } 46 | 47 | exports.default = [ 48 | { 49 | ...configuration, 50 | entry: { [basename]: './entry.js' }, 51 | optimization: { minimize: false }, 52 | }, 53 | { ...configuration, entry: { [`${basename}.min`]: './entry.js' } }, 54 | ] 55 | -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | body, 4 | html { 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | a { 11 | text-decoration: none; 12 | } 13 | 14 | a:hover { 15 | text-decoration: underline; 16 | } 17 | 18 | header { 19 | background: #666; 20 | color: #fff; 21 | height: 70px; 22 | line-height: 70px; 23 | padding: 0 0.5em; 24 | } 25 | 26 | header a { 27 | color: #fff; 28 | } 29 | 30 | h1 { 31 | margin: 0; 32 | } 33 | 34 | .nav { 35 | display: block; 36 | float: right; 37 | margin: 0; 38 | padding: 0; 39 | } 40 | 41 | .nav > li { 42 | display: inline; 43 | } 44 | 45 | .nav > li > a { 46 | display: inline-block; 47 | padding: 0 1em; 48 | } 49 | 50 | .container { 51 | height: 100%; 52 | position: relative; 53 | } 54 | 55 | .options { 56 | background: #ddd; 57 | color: #333; 58 | 59 | position: absolute; 60 | left: 0; 61 | top: 70px; 62 | right: 0; 63 | 64 | height: 30px; 65 | line-height: 30px; 66 | padding: 0 0.5em; 67 | } 68 | 69 | .text-container { 70 | background: #f8f8f8; 71 | 72 | position: absolute; 73 | left: 0; 74 | top: 100px; 75 | right: 50%; 76 | bottom: 0; 77 | } 78 | 79 | .text-wrapper { 80 | position: absolute; 81 | top: 0; 82 | left: 0; 83 | right: 0; 84 | bottom: 0; 85 | margin: 20px; 86 | } 87 | 88 | .markdown-container { 89 | position: absolute; 90 | left: 50%; 91 | top: 100px; 92 | right: 0; 93 | bottom: 0; 94 | 95 | overflow: auto; 96 | } 97 | 98 | #target { 99 | margin: 20px; 100 | } 101 | 102 | #text { 103 | background: transparent; 104 | border: 0; 105 | outline: 0; 106 | display: block; 107 | font-size: 16px; 108 | height: 100%; 109 | margin: 0; 110 | padding: 0; 111 | resize: none; 112 | width: 100%; 113 | } 114 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | markdown-it-incremental-dom 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 |
20 |
21 | 37 |

markdown-it-incremental-dom

38 |
39 |
40 | Render Markdown with: 41 | 51 | 60 |
61 | 62 |
63 |
64 | 70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/docs.md: -------------------------------------------------------------------------------- 1 | # markdown-it-incremental-dom 2 | 3 | A [markdown-it](https://github.com/markdown-it/markdown-it) renderer plugin by using [Incremental DOM](https://github.com/google/incremental-dom). Say goodbye `innerHTML`! 4 | 5 | ## Features 6 | 7 | - [markdown-it](https://github.com/markdown-it/markdown-it) could receive better rendering performance powered by [Incremental DOM](https://github.com/google/incremental-dom). 8 | - [API](https://github.com/yhatt/markdown-it-incremental-dom/blob/master/README.md#usage) is compatible with existing render method. 9 | - It can use with [other markdown-it plugins](https://www.npmjs.com/browse/keyword/markdown-it-plugin). (Of course incrementalize!) 10 | 11 | ## How to use 12 | 13 | Refer below for the examples and usages. 14 | 15 | - **Github: [yhatt/markdown-it-incremental-dom](https://github.com/yhatt/markdown-it-incremental-dom)** 16 | - **npm: [markdown-it-incremental-dom](https://www.npmjs.com/package/markdown-it-incremental-dom)** 17 | 18 | ## Why Incremental DOM? 19 | 20 | ### Repainting on `innerHTML` 21 | 22 | `Element.innerHTML` is the simplest way to update HTML. But it triggers repainting the whole passed HTML. 23 | 24 | ![Screen cast of repainting in innerHTML](./images/repainting-innerhtml.gif) 25 | 26 | This is the visualization of repainting in innerHTML, and repainted area will be colorful. As you see, The whole markdown is always repainted. 27 | 28 | It means _the rendering performance would slow in large markdown_ if `innerHTML` would be called many times in a short time. (e.g. live rendering feature) 29 | 30 | ### Incremental DOM 31 | 32 | Google's [Incremental DOM](https://github.com/google/incremental-dom) can update DOMs by in-place. In other words, elements that have not changed contents or arguments are not changed on rendered DOM too. 33 | 34 | ![Screen cast of repainting in innerHTML](./images/repainting-incremental-dom.gif) 35 | 36 | As you see above, repainting is triggered to the differences only. So it would expect to _reduce the impact of repainting to the minimum_ by using Incremental DOM. 37 | 38 | ## Let's try! 39 | 40 | In this page, you can switch rendering method of markdown: `innerHTML` and Incremental DOM. 41 | 42 | If you wanna confirm the behavior of repainting, turn on paint flashing in developer tools of your browser. 43 | 44 | > Helps are here: [Chrome](https://developers.google.com/web/fundamentals/performance/rendering/simplify-paint-complexity-and-reduce-paint-areas#chrome_devtools) and [Firefox](https://developer.mozilla.org/en-US/docs/Tools/Paint_Flashing_Tool) 45 | 46 | ## Author 47 | 48 | Yuki Hattori ([@yhatt](https://github.com/yhatt/)) 49 | 50 | > markdown-it-incremental-dom is the sub-project of [Marp](https://github.com/yhatt/marp/). 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-incremental-dom", 3 | "version": "2.1.0", 4 | "description": "markdown-it renderer plugin by using Incremental DOM.", 5 | "main": "lib/markdown-it-incremental-dom.js", 6 | "engines": { 7 | "node": ">=6.14.4" 8 | }, 9 | "author": { 10 | "name": "Yuki Hattori", 11 | "url": "https://github.com/yhatt" 12 | }, 13 | "keywords": [ 14 | "markdown-it-plugin", 15 | "markdown-it", 16 | "markdown", 17 | "incremental-dom" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/yhatt/markdown-it-incremental-dom" 22 | }, 23 | "scripts": { 24 | "build": "npm-run-all --npm-path yarn --parallel build:*", 25 | "build:commonjs": "yarn --mutex file run clean:lib && babel src --out-dir lib", 26 | "build:browser": "yarn --mutex file run clean:dist && webpack --mode production", 27 | "clean": "npm-run-all --npm-path yarn --parallel clean:*", 28 | "clean:lib": "rimraf lib", 29 | "clean:dist": "rimraf dist", 30 | "coveralls": "cat coverage/lcov.info | coveralls", 31 | "format": "prettier \"**/*.{css,html,js,json,md,yml,yaml}\"", 32 | "format:check": "yarn --mutex file run format -l", 33 | "lint": "eslint .", 34 | "prepack": "npm-run-all --npm-path yarn --parallel lint test:coverage --sequential build", 35 | "preversion": "npm-run-all --npm-path yarn --parallel format:check lint test:coverage", 36 | "test": "jest", 37 | "test:coverage": "jest --coverage", 38 | "version": "node version.js && git add -A CHANGELOG.md" 39 | }, 40 | "license": "MIT", 41 | "files": [ 42 | "lib/", 43 | "dist/" 44 | ], 45 | "devDependencies": { 46 | "@babel/cli": "^7.1.5", 47 | "@babel/core": "^7.1.6", 48 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 49 | "@babel/polyfill": "^7.0.0", 50 | "@babel/preset-env": "^7.1.6", 51 | "babel-core": "^7.0.0-bridge", 52 | "babel-jest": "^23.6.0", 53 | "babel-loader": "8.0.4", 54 | "common-tags": "^1.8.0", 55 | "coveralls": "^3.0.2", 56 | "eslint": "^5.9.0", 57 | "eslint-config-airbnb-base": "^13.1.0", 58 | "eslint-config-prettier": "^3.3.0", 59 | "eslint-plugin-import": "^2.14.0", 60 | "incremental-dom": "^0.6.0", 61 | "jest": "^23.6.0", 62 | "jest-plugin-context": "^2.9.0", 63 | "markdown-it": "^8.4.2", 64 | "markdown-it-footnote": "^3.0.1", 65 | "markdown-it-sub": "^1.0.0", 66 | "npm-run-all": "^4.1.5", 67 | "prettier": "^1.15.2", 68 | "rimraf": "^2.6.2", 69 | "webpack": "^4.26.0", 70 | "webpack-cli": "^3.1.2" 71 | }, 72 | "peerDependencies": { 73 | "incremental-dom": ">=0.5.0", 74 | "markdown-it": ">=4.0.0" 75 | }, 76 | "dependencies": { 77 | "htmlparser2": "^3.10.0" 78 | }, 79 | "jest": { 80 | "collectCoverageFrom": [ 81 | "src/**/*.js" 82 | ], 83 | "coverageThreshold": { 84 | "global": { 85 | "lines": 95 86 | } 87 | }, 88 | "testRegex": "(/(test|__tests__)/(?!_).*|(\\.|/)(test|spec))\\.js$" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/mixins/renderer.js: -------------------------------------------------------------------------------- 1 | import Parser from 'htmlparser2/lib/Parser' 2 | 3 | export default function(incrementalDom) { 4 | const autoClosingStack = [] 5 | 6 | const autoClosing = () => { 7 | const stack = autoClosingStack.shift() 8 | if (!stack) return 9 | 10 | stack.reverse().forEach(tag => incrementalDom.elementClose(tag)) 11 | } 12 | 13 | const { attr, elementOpenEnd, elementVoid, text } = incrementalDom 14 | 15 | const elementOpen = (tag, ...args) => { 16 | if (autoClosingStack.length > 0) autoClosingStack[0].push(tag) 17 | incrementalDom.elementOpen(tag, ...args) 18 | } 19 | 20 | const elementOpenStart = tag => { 21 | if (autoClosingStack.length > 0) autoClosingStack[0].push(tag) 22 | incrementalDom.elementOpenStart(tag) 23 | } 24 | 25 | const elementClose = tag => { 26 | if (autoClosingStack.length > 0) autoClosingStack[0].pop() 27 | incrementalDom.elementClose(tag) 28 | } 29 | 30 | const sanitizeName = name => name.replace(/[^-:\w]/g, '') 31 | 32 | const iDOMParser = new Parser( 33 | { 34 | onopentag: name => elementOpenEnd(sanitizeName(name)), 35 | onopentagname: name => elementOpenStart(sanitizeName(name)), 36 | onattribute: (name, value) => { 37 | const sanitizedName = sanitizeName(name) 38 | if (sanitizedName !== '') attr(sanitizedName, value) 39 | }, 40 | ontext: text, 41 | onclosetag: name => elementClose(sanitizeName(name)), 42 | }, 43 | { 44 | decodeEntities: true, 45 | lowerCaseAttributeNames: false, 46 | lowerCaseTags: false, 47 | } 48 | ) 49 | 50 | const wrapIncrementalDOM = html => 51 | typeof html === 'function' ? html() : iDOMParser.write(html) 52 | 53 | return { 54 | renderAttrsToArray(token) { 55 | if (!token.attrs) return [] 56 | return token.attrs.reduce((v, a) => v.concat(a), []) 57 | }, 58 | 59 | renderInline(tokens, options, env) { 60 | return () => { 61 | autoClosingStack.unshift([]) 62 | tokens.forEach((current, i) => { 63 | const { type } = current 64 | 65 | if (this.rules[type] !== undefined) { 66 | wrapIncrementalDOM(this.rules[type](tokens, i, options, env, this)) 67 | } else { 68 | this.renderToken(tokens, i, options)() 69 | } 70 | }) 71 | autoClosing() 72 | } 73 | }, 74 | 75 | renderToken(tokens, idx) { 76 | return () => { 77 | const token = tokens[idx] 78 | if (token.hidden) return 79 | 80 | if (token.nesting === -1) { 81 | elementClose(token.tag) 82 | } else { 83 | const func = token.nesting === 0 ? elementVoid : elementOpen 84 | 85 | func.apply( 86 | this, 87 | [token.tag, '', []].concat(this.renderAttrsToArray(token)) 88 | ) 89 | } 90 | } 91 | }, 92 | 93 | render(tokens, options, env) { 94 | return () => { 95 | autoClosingStack.unshift([]) 96 | tokens.forEach((current, i) => { 97 | const { type } = current 98 | 99 | if (type === 'inline') { 100 | this.renderInline(current.children, options, env)() 101 | } else if (this.rules[type] !== undefined) { 102 | wrapIncrementalDOM(this.rules[type](tokens, i, options, env, this)) 103 | } else { 104 | this.renderToken(tokens, i, options, env)() 105 | } 106 | }) 107 | autoClosing() 108 | iDOMParser.reset() 109 | } 110 | }, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/markdown-it-incremental-dom.js: -------------------------------------------------------------------------------- 1 | import * as IncrementalDOM from 'incremental-dom' 2 | import context from 'jest-plugin-context' 3 | import MarkdownIt from 'markdown-it' 4 | import { Parser } from 'htmlparser2' 5 | import MarkdownItIncrementalDOM from '../src/markdown-it-incremental-dom' 6 | 7 | describe('markdown-it-incremental-dom', () => { 8 | const md = (opts = {}) => 9 | MarkdownIt().use(MarkdownItIncrementalDOM, IncrementalDOM, opts) 10 | 11 | describe('markdownIt().use', () => { 12 | context('when Incremental DOM argument is omitted', () => { 13 | afterEach(() => delete window.IncrementalDOM) 14 | 15 | it('fails injection if window.IncrementalDOM is not defined', () => 16 | expect(() => MarkdownIt().use(MarkdownItIncrementalDOM)).toThrow()) 17 | 18 | it('succeeds injection if window.IncrementalDOM is defined', () => { 19 | window.IncrementalDOM = IncrementalDOM 20 | expect(() => MarkdownIt().use(MarkdownItIncrementalDOM)).not.toThrow() 21 | }) 22 | }) 23 | 24 | context('with option', () => { 25 | describe('incrementalizeDefaultRules property', () => { 26 | let spy 27 | 28 | beforeEach(() => { 29 | spy = jest.spyOn(Parser.prototype, 'write') 30 | }) 31 | afterEach(() => spy.mockRestore()) 32 | 33 | const mdString = '`code_inline`' 34 | const expectedHTML = 'code_inline' 35 | 36 | context('when it is false', () => { 37 | it('parses HTML string by htmlparser2', () => { 38 | const func = md({ 39 | incrementalizeDefaultRules: false, 40 | }).renderInlineToIncrementalDOM(mdString) 41 | 42 | IncrementalDOM.patch(document.body, func) 43 | expect(spy).toHaveBeenCalledWith(expectedHTML) 44 | }) 45 | }) 46 | 47 | context('when it is true', () => { 48 | it('does not parse HTML string in overridden rule', () => { 49 | const func = md({ 50 | incrementalizeDefaultRules: true, 51 | }).renderInlineToIncrementalDOM(mdString) 52 | 53 | IncrementalDOM.patch(document.body, func) 54 | expect(spy).not.toHaveBeenCalledWith(expectedHTML) 55 | }) 56 | }) 57 | }) 58 | }) 59 | }) 60 | 61 | describe('get IncrementalDOMRenderer', () => { 62 | it('returns IncrementalDOM renderer that is injected into current state', () => { 63 | const instance = md() 64 | const { options } = instance 65 | const tokens = instance.parse('# test') 66 | 67 | instance.renderer.rules.heading_open = () => '[' 68 | instance.renderer.rules.heading_close = () => ']' 69 | 70 | const rendererOne = instance.IncrementalDOMRenderer 71 | IncrementalDOM.patch(document.body, rendererOne.render(tokens, options)) 72 | expect(document.body.innerHTML).toBe('[test]') 73 | 74 | instance.renderer.rules.heading_open = () => '^' 75 | instance.renderer.rules.heading_close = () => '$' 76 | 77 | const rendererTwo = instance.IncrementalDOMRenderer 78 | IncrementalDOM.patch(document.body, rendererTwo.render(tokens, options)) 79 | expect(document.body.innerHTML).toBe('^test$') 80 | }) 81 | }) 82 | 83 | describe('.renderToIncrementalDOM', () => { 84 | it('returns patchable function by specified Incremental DOM', () => { 85 | const func = md().renderToIncrementalDOM('markdown-it-incremental-dom') 86 | 87 | IncrementalDOM.patch(document.body, func) 88 | expect(document.body.innerHTML).toBe('

markdown-it-incremental-dom

') 89 | }) 90 | }) 91 | 92 | describe('.renderInlineToIncrementalDOM', () => { 93 | it('returns patchable function by specified Incremental DOM', () => { 94 | const func = md().renderInlineToIncrementalDOM( 95 | 'markdown-it-incremental-dom' 96 | ) 97 | 98 | IncrementalDOM.patch(document.body, func) 99 | expect(document.body.innerHTML).toBe('markdown-it-incremental-dom') 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist 3 | 4 | ########## gitignore.io ########## 5 | # Created by https://www.gitignore.io/api/node,windows,macos,linux,sublimetext,emacs,vim,visualstudiocode 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules 39 | jspm_packages 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | ### Windows ### 57 | # Windows image file caches 58 | Thumbs.db 59 | ehthumbs.db 60 | 61 | # Folder config file 62 | Desktop.ini 63 | 64 | # Recycle Bin used on file shares 65 | $RECYCLE.BIN/ 66 | 67 | # Windows Installer files 68 | *.cab 69 | *.msi 70 | *.msm 71 | *.msp 72 | 73 | # Windows shortcuts 74 | *.lnk 75 | 76 | ### macOS ### 77 | *.DS_Store 78 | .AppleDouble 79 | .LSOverride 80 | 81 | # Icon must end with two \r 82 | Icon 83 | # Thumbnails 84 | ._* 85 | # Files that might appear in the root of a volume 86 | .DocumentRevisions-V100 87 | .fseventsd 88 | .Spotlight-V100 89 | .TemporaryItems 90 | .Trashes 91 | .VolumeIcon.icns 92 | .com.apple.timemachine.donotpresent 93 | # Directories potentially created on remote AFP share 94 | .AppleDB 95 | .AppleDesktop 96 | Network Trash Folder 97 | Temporary Items 98 | .apdisk 99 | 100 | ### Linux ### 101 | *~ 102 | 103 | # temporary files which can be created if a process still has a handle open of a deleted file 104 | .fuse_hidden* 105 | 106 | # KDE directory preferences 107 | .directory 108 | 109 | # Linux trash folder which might appear on any partition or disk 110 | .Trash-* 111 | 112 | # .nfs files are created when an open file is removed but is still being accessed 113 | .nfs* 114 | 115 | ### SublimeText ### 116 | # cache files for sublime text 117 | *.tmlanguage.cache 118 | *.tmPreferences.cache 119 | *.stTheme.cache 120 | 121 | # project files are user-specific 122 | *.sublime-project 123 | *.sublime-workspace 124 | 125 | # sftp configuration file 126 | sftp-config.json 127 | 128 | # Package control specific files 129 | Package Control.last-run 130 | Package Control.ca-list 131 | Package Control.ca-bundle 132 | Package Control.system-ca-bundle 133 | Package Control.cache/ 134 | Package Control.ca-certs/ 135 | bh_unicode_properties.cache 136 | 137 | # Sublime-github package stores a github token in this file 138 | # https://packagecontrol.io/packages/sublime-github 139 | GitHub.sublime-settings 140 | 141 | ### Emacs ### 142 | # -*- mode: gitignore; -*- 143 | \#*\# 144 | /.emacs.desktop 145 | /.emacs.desktop.lock 146 | *.elc 147 | auto-save-list 148 | tramp 149 | .\#* 150 | 151 | # Org-mode 152 | .org-id-locations 153 | *_archive 154 | 155 | # flymake-mode 156 | *_flymake.* 157 | 158 | # eshell files 159 | /eshell/history 160 | /eshell/lastdir 161 | 162 | # elpa packages 163 | /elpa/ 164 | 165 | # reftex files 166 | *.rel 167 | 168 | # AUCTeX auto folder 169 | /auto/ 170 | 171 | # cask packages 172 | .cask/ 173 | 174 | # Flycheck 175 | flycheck_*.el 176 | 177 | # server auth directory 178 | /server/ 179 | 180 | # projectiles files 181 | .projectile 182 | 183 | # directory configuration 184 | .dir-locals.el 185 | 186 | ### Vim ### 187 | # swap 188 | [._]*.s[a-w][a-z] 189 | [._]s[a-w][a-z] 190 | # session 191 | Session.vim 192 | # temporary 193 | .netrwhist 194 | # auto-generated tag files 195 | tags 196 | 197 | ### VisualStudioCode ### 198 | .vscode/* 199 | !.vscode/settings.json 200 | !.vscode/tasks.json 201 | !.vscode/launch.json 202 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | ### Security 6 | 7 | - Upgrade `npm-run-all` package to get rid of the malicious attack by `flatmap-stream` ([#42](https://github.com/yhatt/markdown-it-incremental-dom/pull/42)) 8 | 9 | ## v2.1.0 - 2018-11-21 10 | 11 | ### Added 12 | 13 | - Support Node 10 LTS 14 | 15 | ### Changed 16 | 17 | - Upgrade dependent packges to latest, includes [incremental-dom 0.6.0](https://github.com/google/incremental-dom/releases/tag/0.6.0) 18 | - Update incremental-dom CDN to use [jsDelivr](https://cdn.jsdelivr.net/npm/incremental-dom@0.6.0/dist/incremental-dom-min.js) instead of Google (0.6.0 is not provided) 19 | - Modernize example codes and demo JS 20 | - Apply Prettier code formatting for configuration files and demo HTML 21 | 22 | ## v2.0.2 - 2018-10-23 23 | 24 | ### Fixed 25 | 26 | - Fix the self-closing tags of SVG and MathML ([#40](https://github.com/yhatt/markdown-it-incremental-dom/pull/40) by [@m93a](https://github.com/m93a)) 27 | 28 | ## v2.0.1 - 2018-08-31 29 | 30 | ### Fixed 31 | 32 | - Fix twice element closing by htmlparser2 ([#34](https://github.com/yhatt/markdown-it-incremental-dom/pull/34)) 33 | 34 | ### Added 35 | 36 | - Add versioning script ([#35](https://github.com/yhatt/markdown-it-incremental-dom/pull/35)) 37 | 38 | ## v2.0.0 - 2018-08-17 39 | 40 | ### Breaking 41 | 42 | - No longer support Node < v6.14.2 (Boron Maintenance LTS release). 43 | 44 | ### Changed 45 | 46 | - Upgrade Node LTS to v8.11.4 and dependent packages to latest version 47 | - Migrate test framework from mocha to Jest 48 | 49 | ## v1.3.0 - 2018-02-15 50 | 51 | ### Added 52 | 53 | - Define `IncrementalDOMRenderer` as public getter ([#28](https://github.com/yhatt/markdown-it-incremental-dom/pull/28)) 54 | 55 | ### Fixed 56 | 57 | - Fix build entry and module path for browser ([#27](https://github.com/yhatt/markdown-it-incremental-dom/pull/27)) 58 | - Support inline SVG correctly ([#29](https://github.com/yhatt/markdown-it-incremental-dom/pull/29)) 59 | - Fix format script to work globstar correctly ([#26](https://github.com/yhatt/markdown-it-incremental-dom/pull/26)) 60 | 61 | ### Changed 62 | 63 | - Upgrade dependencies to latest version ([#30](https://github.com/yhatt/markdown-it-incremental-dom/pull/30)) 64 | 65 | ## v1.2.0 - 2018-01-15 66 | 67 | ### Fixed 68 | 69 | - Fix incrementalized softbreak rule to keep break as text ([#25](https://github.com/yhatt/markdown-it-incremental-dom/pull/25)) 70 | 71 | ### Changed 72 | 73 | - Upgrade Babel to v7 beta ([#23](https://github.com/yhatt/markdown-it-incremental-dom/pull/23)) 74 | - Update README.md and demo page to use jsDelivr CDN ([#24](https://github.com/yhatt/markdown-it-incremental-dom/pull/24)) 75 | 76 | ## v1.1.2 - 2018-01-11 77 | 78 | ### Fixed 79 | 80 | - Reduce bundle size of built for browser ([#22](https://github.com/yhatt/markdown-it-incremental-dom/pull/22)) 81 | 82 | ## v1.1.1 - 2018-01-06 83 | 84 | ### Fixed 85 | 86 | - Sanitize HTML element name and attributes to avoid occurring errors while rendering invalid HTML ([#18](https://github.com/yhatt/markdown-it-incremental-dom/pull/18)) 87 | 88 | ### Changed 89 | 90 | - Upgrade dependencies to latest version ([#12](https://github.com/yhatt/markdown-it-incremental-dom/pull/12)) 91 | - Upgrade node version to v8.9.3 LTS ([#16](https://github.com/yhatt/markdown-it-incremental-dom/pull/16)) 92 | - Use babel-preset-env + polyfill instead of babel-preset-es2015 ([#13](https://github.com/yhatt/markdown-it-incremental-dom/pull/13)) 93 | - Add `incremental-dom >=0.5.0` to peerDependencies ([#15](https://github.com/yhatt/markdown-it-incremental-dom/pull/15)) 94 | - Format source code by prettier ([#17](https://github.com/yhatt/markdown-it-incremental-dom/pull/17)) 95 | 96 | ## v1.0.0 - 2017-03-14 97 | 98 | ### Added 99 | 100 | - For browser: Add banner to show license ([#9](https://github.com/yhatt/markdown-it-incremental-dom/pull/9)) 101 | - For browser: Provide uncompressed version ([#9](https://github.com/yhatt/markdown-it-incremental-dom/pull/9)) 102 | - Override markdown-it's default renderer rules by incrementalized functions for better performance ([#7](https://github.com/yhatt/markdown-it-incremental-dom/pull/7)) 103 | - Option argument on initialize that supported `incrementalizeDefaultRules` ([#7](https://github.com/yhatt/markdown-it-incremental-dom/pull/7)) 104 | - Badges on README.md: Coverage (powered by Coveralls), npm version, and LICENSE ([#6](https://github.com/yhatt/markdown-it-incremental-dom/pull/6)) 105 | - [Demo page](https://yhatt.github.io/markdown-it-incremental-dom/) with explaining of key features ([#3](https://github.com/yhatt/markdown-it-incremental-dom/issue/3)) 106 | 107 | ## v0.1.0 - 2017-02-22 108 | 109 | - Initial release. 110 | -------------------------------------------------------------------------------- /test/renderer.js: -------------------------------------------------------------------------------- 1 | import { stripIndents } from 'common-tags' 2 | import context from 'jest-plugin-context' 3 | import MarkdownIt from 'markdown-it' 4 | import MarkdownItFootnote from 'markdown-it-footnote' 5 | import MarkdownItSub from 'markdown-it-sub' 6 | import * as IncrementalDOM from 'incremental-dom' 7 | import MarkdownItIncrementalDOM from '../src/markdown-it-incremental-dom' 8 | 9 | describe('Renderer', () => { 10 | const md = (opts = {}) => { 11 | const instance = MarkdownIt(opts).use( 12 | MarkdownItIncrementalDOM, 13 | IncrementalDOM 14 | ) 15 | 16 | // returns rendered string 17 | const renderWithIncrementalDOM = (func, args) => { 18 | IncrementalDOM.patch(document.body, func(...args)) 19 | return document.body.innerHTML 20 | } 21 | 22 | instance.idom = (...args) => 23 | renderWithIncrementalDOM(instance.renderToIncrementalDOM, args) 24 | 25 | instance.iidom = (...args) => 26 | renderWithIncrementalDOM(instance.renderInlineToIncrementalDOM, args) 27 | 28 | return instance 29 | } 30 | 31 | const has = q => expect(document.querySelector(q)).toBeTruthy() 32 | 33 | context('with rendering image (tag + attributes)', () => { 34 | it('renders with attributes', () => { 35 | md().idom('![image](src "title")') 36 | const image = document.querySelector('img') 37 | 38 | expect(image).toBeTruthy() 39 | expect(image.getAttribute('src')).toBe('src') 40 | expect(image.getAttribute('alt')).toBe('image') 41 | expect(image.getAttribute('title')).toBe('title') 42 | }) 43 | }) 44 | 45 | context('with rendering fence (requires parsing HTML)', () => { 46 | it('renders parsed HTML correctly', () => { 47 | md().idom('```javascript\nalert("test")\n```') 48 | 49 | const pre = document.querySelector('pre') 50 | expect(pre).toBeTruthy() 51 | 52 | const code = pre.querySelector('code.language-javascript') 53 | expect(code).toBeTruthy() 54 | expect(code.innerHTML.trim()).toBe('alert("test")') 55 | }) 56 | }) 57 | 58 | context('with code block rendering (overrided rule)', () => { 59 | it('renders code block correctly', () => { 60 | md().idom(' ') 61 | 62 | const { innerHTML } = document.querySelector('pre > code') 63 | expect(innerHTML).toBe('<script>\nalert("test")\n</script>') 64 | }) 65 | }) 66 | 67 | context('with inline code rendering (overrided rule)', () => 68 | it('renders correctly', () => 69 | expect(md().iidom('This is `Inline` rendering')).toBe( 70 | 'This is <b>Inline</b> rendering' 71 | )) 72 | ) 73 | 74 | context('with rendering hardbreak (overrided rule)', () => 75 | it('renders
correctly', () => 76 | expect(md().iidom('hardbreak \ntest')).toBe('hardbreak
test')) 77 | ) 78 | 79 | context('with html option', () => { 80 | const markdown = 'test' 81 | 82 | context('with false', () => 83 | it('sanitizes HTML tag', () => 84 | expect(md({ html: false }).idom(markdown)).toBe( 85 | '

<b class="test">test</b>

' 86 | )) 87 | ) 88 | 89 | context('with true', () => { 90 | it('renders HTML tag', () => 91 | expect(md({ html: true }).idom(markdown)).toBe(`

${markdown}

`)) 92 | 93 | it('renders empty element without slash', () => { 94 | md({ html: true }).idom('
') 95 | has('hr + img') 96 | }) 97 | 98 | it('renders invalid HTML', () => { 99 | md({ html: true }).idom('
inva') 100 | expect(document.querySelector('div').textContent).toBe('inva') 101 | }) 102 | 103 | it('renders invalid nesting HTML', () => { 104 | md({ html: true }).idom('\n') 105 | has('table > tr') 106 | }) 107 | 108 | it('renders inline SVG', () => { 109 | const svg = stripIndents` 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | ` 120 | 121 | md({ html: true }).idom(svg) 122 | 123 | has('svg[xmlns][viewBox="0 0 32 32"]') 124 | has('svg > defs > linearGradient#gradation') 125 | has('#gradation > stop + stop') 126 | has('svg > rect[x][y][width][height][fill]') 127 | }) 128 | }) 129 | }) 130 | 131 | context('with breaks option', () => { 132 | const markdown = 'break\ntest' 133 | 134 | context('with false', () => { 135 | it('keeps breaks as text', () => { 136 | md({ breaks: false }).idom(markdown) 137 | 138 | expect(document.querySelector('br')).toBeFalsy() 139 | expect(document.querySelector('p').textContent).toBe(markdown) 140 | }) 141 | }) 142 | 143 | context('with true', () => { 144 | it('renders
on breaks', () => { 145 | md({ breaks: true }).idom(markdown) 146 | has('br') 147 | }) 148 | }) 149 | }) 150 | 151 | context('with overriden renderers', () => { 152 | context('when HTML parser is used only in opening element', () => { 153 | it('renders correctly', () => { 154 | const markdown = md() 155 | markdown.renderer.rules.paragraph_open = () => '

' 156 | markdown.idom('Render correctly') 157 | 158 | expect(document.querySelector('p.overriden')).toBeTruthy() 159 | }) 160 | }) 161 | }) 162 | 163 | context('with other plugins', () => { 164 | context('when markdown-it-sub is injected (simple plugin)', () => { 165 | const instance = md().use(MarkdownItSub) 166 | 167 | it('renders tag correctly', () => 168 | expect(instance.idom('H~2~O')).toBe('

H2O

')) 169 | }) 170 | 171 | context( 172 | 'when markdown-it-footnote is injected (overriding renderer rules)', 173 | () => { 174 | const instance = md().use(MarkdownItFootnote) 175 | const markdown = 'Footnote[^1]\n\n[^1]: test' 176 | 177 | it('renders footnote correctly', () => { 178 | instance.idom(markdown) 179 | 180 | has('sup.footnote-ref > a#fnref1[href="#fn1"]') 181 | has('hr.footnotes-sep') 182 | has('section.footnotes > ol.footnotes-list > li#fn1.footnote-item') 183 | has('#fn1 a.footnote-backref[href="#fnref1"]') 184 | }) 185 | } 186 | ) 187 | }) 188 | }) 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-incremental-dom 2 | 3 | [![Travis CI](https://img.shields.io/travis/yhatt/markdown-it-incremental-dom.svg?style=flat-square)](https://travis-ci.org/yhatt/markdown-it-incremental-dom) 4 | [![Coveralls](https://img.shields.io/coveralls/yhatt/markdown-it-incremental-dom/master.svg?style=flat-square)](https://coveralls.io/github/yhatt/markdown-it-incremental-dom?branch=master) 5 | [![npm](https://img.shields.io/npm/v/markdown-it-incremental-dom.svg?style=flat-square)](https://www.npmjs.com/package/markdown-it-incremental-dom) 6 | [![LICENSE](https://img.shields.io/github/license/yhatt/markdown-it-incremental-dom.svg?style=flat-square)](./LICENSE) 7 | 8 | A [markdown-it](https://github.com/markdown-it/markdown-it) renderer plugin by using [Incremental DOM](https://github.com/google/incremental-dom). 9 | 10 | Let's see key features: **[https://yhatt.github.io/markdown-it-incremental-dom/](https://yhatt.github.io/markdown-it-incremental-dom/)** or [`docs.md`](docs/docs.md) 11 | 12 | [![](./docs/images/repainting-incremental-dom.gif)](https://yhatt.github.io/markdown-it-incremental-dom/) 13 | 14 | ## Requirement 15 | 16 | - [markdown-it](https://github.com/markdown-it/markdown-it) >= 4.0.0 (Recommend latest version >= 8.4.0, that this plugin use it) 17 | - [Incremental DOM](https://github.com/google/incremental-dom) >= 0.5.x 18 | 19 | ## Examples 20 | 21 | ### Node 22 | 23 | ```javascript 24 | import * as IncrementalDOM from 'incremental-dom' 25 | import MarkdownIt from 'markdown-it' 26 | import MarkdownItIncrementalDOM from 'markdown-it-incremental-dom' 27 | 28 | const md = new MarkdownIt().use(MarkdownItIncrementalDOM, IncrementalDOM) 29 | 30 | IncrementalDOM.patch( 31 | document.getElementById('target'), 32 | md.renderToIncrementalDOM('# Hello, Incremental DOM!') 33 | ) 34 | ``` 35 | 36 | ### Browser 37 | 38 | Define as `window.markdownitIncrementalDOM`. 39 | 40 | ```html 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | 59 | 60 | 61 | ``` 62 | 63 | #### CDN 64 | 65 | You can use [the recent version through CDN](https://cdn.jsdelivr.net/npm/markdown-it-incremental-dom@2/dist/markdown-it-incremental-dom.min.js) provides by [jsDelivr](https://www.jsdelivr.com/). 66 | 67 | ```html 68 | 69 | ``` 70 | 71 | - **[Compressed (Recommend)](https://cdn.jsdelivr.net/npm/markdown-it-incremental-dom@2/dist/markdown-it-incremental-dom.min.js)** 72 | - [Uncompressed](https://cdn.jsdelivr.net/npm/markdown-it-incremental-dom@2/dist/markdown-it-incremental-dom.js) 73 | 74 | ## Installation 75 | 76 | We recommend using [yarn](https://yarnpkg.com/) to install. 77 | 78 | ```bash 79 | $ yarn add incremental-dom markdown-it 80 | $ yarn add markdown-it-incremental-dom 81 | ``` 82 | 83 | If you wanna use npm, try this: 84 | 85 | ```bash 86 | $ npm install incremental-dom markdown-it --save 87 | $ npm install markdown-it-incremental-dom --save 88 | ``` 89 | 90 | ## Usage 91 | 92 | When injecting this plugin by `.use()`, _you should pass Incremental DOM class as second argument._ (`window.IncrementalDOM` by default) 93 | 94 | ```javascript 95 | import * as IncrementalDOM from 'incremental-dom' 96 | import MarkdownIt from 'markdown-it' 97 | import MarkdownItIncrementalDOM from 'markdown-it-incremental-dom' 98 | 99 | const md = new MarkdownIt().use(MarkdownItIncrementalDOM, IncrementalDOM) 100 | ``` 101 | 102 | If it is succeed, [2 new rendering methods](#rendering-methods) would be injected to instance. 103 | 104 | > **_TIPS:_** This plugin keeps default rendering methods [`render()`](https://markdown-it.github.io/markdown-it/#MarkdownIt.render) and [`renderInline()`](https://markdown-it.github.io/markdown-it/#MarkdownIt.renderInline). 105 | 106 | ### Option 107 | 108 | You can pass option object as third argument. See below: 109 | 110 | ```javascript 111 | new MarkdownIt().use(MarkdownItIncrementalDOM, IncrementalDOM, { 112 | incrementalizeDefaultRules: false, 113 | }) 114 | ``` 115 | 116 | - **`incrementalizeDefaultRules`**: For better performance, this plugin would override a few default renderer rules only when you calls injected methods. If the other plugins that override default rules have occurred any problem, You can disable overriding by setting `false`. _(`true` by default)_ 117 | 118 | ### Rendering methods 119 | 120 | #### `MarkdownIt.renderToIncrementalDOM(src[, env])` => `Function` 121 | 122 | Similar to [`MarkdownIt.render(src[, env])`](https://markdown-it.github.io/markdown-it/#MarkdownIt.render) but _it returns a function for Incremental DOM_. It means doesn't render Markdown immediately. 123 | 124 | You must render to DOM by using [`IncrementalDOM.patch(node, description)`](http://google.github.io/incremental-dom/#api/patch). Please pass the returned function to the description argument. For example: 125 | 126 | ```javascript 127 | const node = document.getElementById('#target') 128 | const func = md.renderToIncrementalDOM('# Hello, Incremental DOM!') 129 | 130 | // It would render "

Hello, Incremental DOM!

" to
131 | IncrementalDOM.patch(node, func) 132 | ``` 133 | 134 | #### `MarkdownIt.renderInlineToIncrementalDOM(src[, env])` => `Function` 135 | 136 | Similar to `MarkdownIt.renderToIncrementalDOM` but it wraps [`MarkdownIt.renderInline(src[, env])`](https://markdown-it.github.io/markdown-it/#MarkdownIt.renderInline). 137 | 138 | ### Renderer property 139 | 140 | #### _get_ `MarkdownIt.IncrementalDOMRenderer` => [`Renderer`](https://markdown-it.github.io/markdown-it/#Renderer) 141 | 142 | Returns [`Renderer`](https://markdown-it.github.io/markdown-it/#Renderer) instance that includes Incremental DOM support. 143 | 144 | It will inject Incremental DOM features into the current state of [`MarkdownIt.renderer`](https://markdown-it.github.io/markdown-it/#MarkdownIt.prototype.renderer) at getting this property. 145 | 146 | > **_NOTE:_** This property is provided for the expert. Normally you should use `renderToIncrementalDOM()`. 147 | > 148 | > But it might be useful if you have to parse Markdown and operate tokens manually. 149 | > 150 | > ```javascript 151 | > const md = new MarkdownIt() 152 | > const tokens = md.parse('# Hello') 153 | > 154 | > // ...You can operate tokens here... 155 | > 156 | > const patch = md.IncrementalDOMRenderer.render(tokens, md.options) 157 | > IncrementalDOM.patch(document.body, patch) 158 | > ``` 159 | 160 | ## Development 161 | 162 | ```bash 163 | $ git clone https://github.com/yhatt/markdown-it-incremental-dom 164 | 165 | $ yarn install 166 | $ yarn build 167 | ``` 168 | 169 | ### Lint & Format 170 | 171 | ```bash 172 | $ yarn lint # Run ESLint 173 | $ yarn lint --fix # Fix lint 174 | 175 | $ yarn format:check # Run Prettier 176 | $ yarn format --write # Fix formatting by Prettier 177 | ``` 178 | 179 | ### Publish to npm 180 | 181 | ```bash 182 | $ npm publish 183 | ``` 184 | 185 | :warning: Use npm >= 5.0.0. 186 | 187 | ## Author 188 | 189 | Yuki Hattori ([@yhatt](https://github.com/yhatt/)) 190 | 191 | ## License 192 | 193 | This plugin releases under the [MIT License](https://github.com/yhatt/markdown-it-incremental-dom/blob/master/LICENSE). 194 | --------------------------------------------------------------------------------