├── .gitattributes ├── .gitignore ├── .editorconfig ├── src ├── defaults.js ├── index.js ├── utils.js ├── inline-rules.js ├── block-rules.js ├── renderer.js ├── parser.js ├── inline-lexer.js └── lexer.js ├── circle.yml ├── test ├── __snapshots__ │ └── index.test.js.snap └── index.test.js ├── LICENSE ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.log 4 | dist/ 5 | coverage 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | import Renderer from './renderer' 2 | 3 | export default { 4 | gfm: true, 5 | tables: true, 6 | taskLists: true, 7 | dataLine: true, 8 | breaks: false, 9 | pedantic: false, 10 | sanitize: false, 11 | sanitizer: null, 12 | mangle: true, 13 | smartLists: false, 14 | silent: false, 15 | highlight: null, 16 | langPrefix: 'lang-', 17 | smartypants: false, 18 | headerPrefix: '', 19 | renderer: new Renderer(), 20 | xhtml: false 21 | } 22 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:latest 7 | branches: 8 | ignore: 9 | - gh-pages # list of branches to ignore 10 | - /release\/.*/ # or ignore regexes 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: dependency-cache-{{ checksum "yarn.lock" }} 15 | - run: 16 | name: install dependences 17 | command: yarn 18 | - save_cache: 19 | key: dependency-cache-{{ checksum "yarn.lock" }} 20 | paths: 21 | - ./node_modules 22 | - run: 23 | name: test 24 | command: yarn test:cov 25 | - run: 26 | name: upload coverage 27 | command: bash <(curl -s https://codecov.io/bash) 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { merge, escape } from './utils' 2 | import defaultOptions from './defaults' 3 | import Parser from './parser' 4 | import Lexer from './lexer' 5 | import Renderer from './renderer' 6 | import InlineLexer from './inline-lexer' 7 | 8 | function md(src, opt) { 9 | try { 10 | if (opt) opt = merge({}, defaultOptions, opt) 11 | return Parser.parse(Lexer.lex(src, opt), opt) 12 | } catch (err) { 13 | err.message += '\nPlease report this to https://github.com/egoist/md.' 14 | if ((opt || defaultOptions).silent) { 15 | return ( 16 | '

An error occurred:

' +
17 |         escape(String(err.message), true) +
18 |         '
' 19 | ) 20 | } 21 | throw err 22 | } 23 | } 24 | 25 | md.Renderer = Renderer 26 | md.Parser = Parser 27 | md.Lexer = Lexer 28 | md.InlineLexer = InlineLexer 29 | 30 | export default md 31 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`data-line 1`] = ` 4 | "
hi
 5 | 
6 |
hello
 7 | 
8 | " 9 | `; 10 | 11 | exports[`headings 1`] = ` 12 | "

hello

13 |

hello

14 |

hi there

15 | " 16 | `; 17 | 18 | exports[`links 1`] = ` 19 | "

a

20 | " 21 | `; 22 | 23 | exports[`links 2`] = ` 24 | "

a

25 | " 26 | `; 27 | 28 | exports[`links 3`] = ` 29 | "

a

30 | " 31 | `; 32 | 33 | exports[`links 4`] = ` 34 | "

a

35 | " 36 | `; 37 | 38 | exports[`table 1`] = ` 39 | " 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
foobar
foobar
53 | " 54 | `; 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (https://egoist.moe) 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 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import marked from '../src' 2 | 3 | test('headings', () => { 4 | const html = marked(` 5 | # hello 6 | 7 | # hello 8 | 9 | ## hi there 10 | `) 11 | 12 | expect(html).toMatchSnapshot() 13 | }) 14 | 15 | test('table', () => { 16 | const html = marked(` 17 | |foo|bar| 18 | |---|---| 19 | |foo|bar| 20 | `) 21 | 22 | expect(html).toMatchSnapshot() 23 | }) 24 | 25 | test('links', () => { 26 | const html = marked(` 27 | [a](b) 28 | `) 29 | 30 | expect(html).toMatchSnapshot() 31 | 32 | const html2 = marked( 33 | ` 34 | [a](b) 35 | `, 36 | { linksInNewTab: true } 37 | ) 38 | 39 | expect(html2).toMatchSnapshot() 40 | 41 | const html3 = marked( 42 | ` 43 | [a](b) 44 | `, 45 | { linksInNewTab: () => true } 46 | ) 47 | 48 | expect(html3).toMatchSnapshot() 49 | 50 | const html4 = marked( 51 | ` 52 | [a](b) 53 | `, 54 | { linksInNewTab: () => false } 55 | ) 56 | 57 | expect(html4).toMatchSnapshot() 58 | }) 59 | 60 | test('data-line', () => { 61 | const html = marked(` 62 | \`\`\`js{1,2,3,5-10} 63 | hi 64 | \`\`\` 65 | 66 | \`\`\`css 67 | hello 68 | \`\`\` 69 | `) 70 | 71 | expect(html).toMatchSnapshot() 72 | }) 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "md", 3 | "version": "0.6.0", 4 | "description": "A markdown parser and compiler. Built for speed.", 5 | "repository": { 6 | "url": "egoist/md", 7 | "type": "git" 8 | }, 9 | "main": "dist/md.cjs.js", 10 | "unpkg": "dist/md.js", 11 | "module": "dist/md.es.js", 12 | "files": [ 13 | "dist" 14 | ], 15 | "scripts": { 16 | "test": "npm run lint && jest", 17 | "test:cov": "npm run lint && jest --coverage", 18 | "prepublishOnly": "npm run build", 19 | "lint": "xo", 20 | "build": "bili --format umd,cjs,es,umd-min" 21 | }, 22 | "author": "egoist <0x142857@gmail.com>", 23 | "license": "MIT", 24 | "babel": { 25 | "presets": [ 26 | [ 27 | "env", 28 | { 29 | "targets": { 30 | "node": "current" 31 | } 32 | } 33 | ] 34 | ] 35 | }, 36 | "devDependencies": { 37 | "babel-preset-env": "^1.6.0", 38 | "bili": "^2.2.7", 39 | "eslint-config-rem": "^3.1.0", 40 | "jest": "^22.4.2", 41 | "xo": "^0.18.2" 42 | }, 43 | "xo": { 44 | "extends": "rem/prettier", 45 | "envs": ["jest"], 46 | "rules": { 47 | "complexity": 0, 48 | "no-cond-assign": 0, 49 | "no-unexpected-multiline": 0, 50 | "func-call-spacing": 0, 51 | "no-useless-escape": 0 52 | } 53 | }, 54 | "dependencies": { 55 | "slugo": "^0.2.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function merge(obj) { 2 | let i = 1 3 | let target 4 | let key 5 | 6 | for (; i < arguments.length; i++) { 7 | target = arguments[i] 8 | for (key in target) { 9 | if (Object.prototype.hasOwnProperty.call(target, key)) { 10 | obj[key] = target[key] 11 | } 12 | } 13 | } 14 | 15 | return obj 16 | } 17 | 18 | function noop() {} 19 | noop.exec = noop 20 | 21 | function escape(html, encode) { 22 | return html 23 | .replace(encode ? /&/g : /&(?!#?\w+;)/g, '&') 24 | .replace(//g, '>') 26 | .replace(/"/g, '"') 27 | .replace(/'/g, ''') 28 | } 29 | 30 | function unescape(html) { 31 | // explicitly match decimal, hex, and named HTML entities 32 | return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, (_, n) => { 33 | n = n.toLowerCase() 34 | if (n === 'colon') return ':' 35 | if (n.charAt(0) === '#') { 36 | return n.charAt(1) === 'x' 37 | ? String.fromCharCode(parseInt(n.substring(2), 16)) 38 | : String.fromCharCode(Number(n.substring(1))) 39 | } 40 | return '' 41 | }) 42 | } 43 | 44 | function replace(regex, opt) { 45 | regex = regex.source 46 | opt = opt || '' 47 | return function self(name, val) { 48 | if (!name) return new RegExp(regex, opt) 49 | val = val.source || val 50 | val = val.replace(/(^|[^\[])\^/g, '$1') 51 | regex = regex.replace(name, val) 52 | return self 53 | } 54 | } 55 | 56 | export { merge, noop, escape, unescape, replace } 57 | -------------------------------------------------------------------------------- /src/inline-rules.js: -------------------------------------------------------------------------------- 1 | import { noop, merge, replace } from './utils' 2 | 3 | /** 4 | * Inline-Level Grammar 5 | */ 6 | 7 | const inline = { 8 | escape: /^\\([\\`*{}[\]()#+\-.!_>])/, 9 | autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, 10 | url: noop, 11 | tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, 12 | link: /^!?\[(inside)\]\(href\)/, 13 | reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, 14 | nolink: /^!?\[((?:\[[^\]]*\]|[^[\]])*)\]/, 15 | strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, 16 | em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, 17 | code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, 18 | br: /^ {2,}\n(?!\s*$)/, 19 | del: noop, 20 | text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/ 25 | 26 | inline.link = replace(inline.link)('inside', inline._inside)( 27 | 'href', 28 | inline._href 29 | )() 30 | 31 | inline.reflink = replace(inline.reflink)('inside', inline._inside)() 32 | 33 | /** 34 | * Normal Inline Grammar 35 | */ 36 | 37 | inline.normal = merge({}, inline) 38 | 39 | /** 40 | * Pedantic Inline Grammar 41 | */ 42 | 43 | inline.pedantic = merge({}, inline.normal, { 44 | strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, 45 | em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ 46 | }) 47 | 48 | /** 49 | * GFM Inline Grammar 50 | */ 51 | 52 | inline.gfm = merge({}, inline.normal, { 53 | escape: replace(inline.escape)('])', '~|])')(), 54 | url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, 55 | del: /^~~(?=\S)([\s\S]*?\S)~~/, 56 | text: replace(inline.text)(']|', '~]|')('|', '|https?://|')() 57 | }) 58 | 59 | /** 60 | * GFM + Line Breaks Inline Grammar 61 | */ 62 | 63 | inline.breaks = merge({}, inline.gfm, { 64 | br: replace(inline.br)('{2,}', '*')(), 65 | text: replace(inline.gfm.text)('{2,}', '*')() 66 | }) 67 | 68 | export default inline 69 | -------------------------------------------------------------------------------- /src/block-rules.js: -------------------------------------------------------------------------------- 1 | import { merge, noop, replace } from './utils' 2 | 3 | const block = { 4 | newline: /^\n+/, 5 | code: /^( {4}[^\n]+\n*)+/, 6 | fences: noop, 7 | hr: /^( *[-*_]){3,} *(?:\n+|$)/, 8 | heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, 9 | nptable: noop, 10 | lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, 11 | blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, 12 | list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, 13 | html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, 14 | def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, 15 | table: noop, 16 | paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, 17 | text: /^[^\n]+/ 18 | } 19 | 20 | block.bullet = /(?:[*+-]|\d+\.)/ 21 | block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/ 22 | block.item = replace(block.item, 'gm')(/bull/g, block.bullet)() 23 | 24 | block.list = replace(block.list)(/bull/g, block.bullet)( 25 | 'hr', 26 | '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))' 27 | )('def', '\\n+(?=' + block.def.source + ')')() 28 | 29 | block.blockquote = replace(block.blockquote)('def', block.def)() 30 | 31 | block._tag = 32 | '(?!(?:' + 33 | 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + 34 | '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + 35 | '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b' 36 | 37 | block.html = replace(block.html)('comment', //)( 38 | 'closed', 39 | /<(tag)[\s\S]+?<\/\1>/ 40 | )('closing', /])*?>/)(/tag/g, block._tag)() 41 | 42 | block.paragraph = replace(block.paragraph)('hr', block.hr)( 43 | 'heading', 44 | block.heading 45 | )('lheading', block.lheading)('blockquote', block.blockquote)( 46 | 'tag', 47 | '<' + block._tag 48 | )('def', block.def)() 49 | 50 | /** 51 | * Normal Block Grammar 52 | */ 53 | 54 | block.normal = merge({}, block) 55 | 56 | /** 57 | * GFM Block Grammar 58 | */ 59 | 60 | block.gfm = merge({}, block.normal, { 61 | fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/, 62 | paragraph: /^/, 63 | heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/, 64 | checkbox: /^\[([ x])\] +/ 65 | }) 66 | 67 | block.gfm.paragraph = replace(block.paragraph)( 68 | '(?!', 69 | '(?!' + 70 | block.gfm.fences.source.replace('\\1', '\\2') + 71 | '|' + 72 | block.list.source.replace('\\1', '\\3') + 73 | '|' 74 | )() 75 | 76 | /** 77 | * GFM + Tables Block Grammar 78 | */ 79 | 80 | block.tables = merge({}, block.gfm, { 81 | nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, 82 | table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ 83 | }) 84 | 85 | export default block 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # md 2 | 3 | [![NPM version](https://img.shields.io/npm/v/md.svg?style=flat)](https://npmjs.com/package/md) [![NPM downloads](https://img.shields.io/npm/dm/md.svg?style=flat)](https://npmjs.com/package/md) [![Build Status](https://img.shields.io/circleci/project/egoist/md/master.svg?style=flat)](https://circleci.com/gh/egoist/md) [![codecov](https://codecov.io/gh/egoist/md/branch/master/graph/badge.svg)](https://codecov.io/gh/egoist/md) 4 | [![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?maxAge=2592000&style=flat)](https://github.com/egoist/donate) 5 | 6 | > This is a fork of [marked](https://github.com/markedjs/marked) 7 | 8 | **Why?** 9 | 10 | - Actively maintained 11 | - Rewrote in ES6 and bundled with Rollup for smaller size (15KB) 12 | - Support more GFM extras like [task lists](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments) 13 | 14 | ## Install 15 | 16 | ```bash 17 | yarn add md 18 | ``` 19 | 20 | You can find a CDN version at https://unpkg.com/md/ 21 | 22 | ## Usage 23 | 24 | ```js 25 | const md = require('md') 26 | 27 | const html = md(`## hello world 28 | 29 | A modern **markdown** parser! 30 | 31 | - [ ] todo 32 | - [x] done 33 | `) 34 | ``` 35 | 36 | You can preview the HTML result here: https://egoist.moe/md2html/ ([source](https://github.com/egoist/md2html)) 37 | 38 | ## API 39 | 40 | ### md(src, [options]) 41 | 42 | #### src 43 | 44 | Type: `string`
45 | Required: `true` 46 | 47 | Input markdown string. 48 | 49 | #### options 50 | 51 | All marked [options](https://marked.js.org/#/USING_ADVANCED.md) plus: 52 | 53 | ##### taskLists 54 | 55 | Type: `boolean`
56 | Default: `true` 57 | 58 | Enable GFM task lists, this will only work if `options.gfm` is `true`. 59 | 60 | ##### linksInNewTab 61 | 62 | Type: `boolean | (href: string) => boolean`
63 | Default: `undefined` 64 | 65 | Open links in a new window/tab. 66 | 67 | ##### dataLine 68 | 69 | Type: `boolean`
70 | Default: `true` 71 | 72 | Add `data-line` attribute to `
` tag for code fences, it's useful with the [line-highlight](http://prismjs.com/plugins/line-highlight/) plugin in PrismJS. 
 73 | 
 74 | ````markdown
 75 | ```js{1}
 76 | console.log('hi')
 77 | ```
 78 | ````
 79 | 
 80 | This will yield:
 81 | 
 82 | ```html
 83 | 
console.log('hi')
84 | ``` 85 | 86 | ## Contributing 87 | 88 | 1. Fork it! 89 | 2. Create your feature branch: `git checkout -b my-new-feature` 90 | 3. Commit your changes: `git commit -am 'Add some feature'` 91 | 4. Push to the branch: `git push origin my-new-feature` 92 | 5. Submit a pull request :D 93 | 94 | 95 | ## Development 96 | 97 | ```bash 98 | # lint and unit test 99 | yarn test 100 | 101 | # lint only 102 | yarn lint 103 | 104 | # fix lint issues 105 | yarn lint -- --fix 106 | ``` 107 | 108 | ## Author 109 | 110 | **md** © [egoist](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
111 | Authored and maintained by egoist with help from contributors ([list](https://github.com/egoist/md/contributors)). 112 | 113 | > [egoist.moe](https://egoist.moe) · GitHub [@egoist](https://github.com/egoist) · Twitter [@_egoistlily](https://twitter.com/_egoistlily) 114 | -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | import slugo from 'slugo' 2 | import { escape, unescape } from './utils' 3 | 4 | const highlightLinesRe = /{([\d,-]+)}/ 5 | 6 | export default class Renderer { 7 | constructor(options) { 8 | this.options = options || {} 9 | this._headings = [] 10 | } 11 | 12 | code(code, lang, escaped) { 13 | let dataLine = '' 14 | if (this.options.dataLine && lang && highlightLinesRe.test(lang)) { 15 | dataLine = ` data-line="${highlightLinesRe.exec(lang)[1]}"` 16 | lang = lang.substr(0, lang.indexOf('{')) 17 | } 18 | 19 | if (this.options.highlight) { 20 | const out = this.options.highlight(code, lang) 21 | if (out !== null && out !== code) { 22 | escaped = true 23 | code = out 24 | } 25 | } 26 | 27 | if (!lang) { 28 | return `${escaped 29 | ? code 30 | : escape(code, true)}\n
` 31 | } 32 | 33 | return `${escaped ? code : escape(code, true)}\n\n` 37 | } 38 | 39 | blockquote(quote) { 40 | return `
\n${quote}
\n` 41 | } 42 | 43 | html(html) { 44 | return html 45 | } 46 | 47 | heading(text, level, raw) { 48 | let slug = slugo(raw) 49 | const count = this._headings.filter(h => h === raw).length 50 | if (count > 0) { 51 | slug += `-${count}` 52 | } 53 | this._headings.push(raw) 54 | return `${text}\n` 56 | } 57 | 58 | hr() { 59 | return this.options.xhtml ? '
\n' : '
\n' 60 | } 61 | 62 | list(body, ordered, taskList) { 63 | const type = ordered ? 'ol' : 'ul' 64 | const classNames = taskList ? ' class="task-list"' : '' 65 | return `<${type}${classNames}>\n${body}\n` 66 | } 67 | 68 | listitem(text, checked) { 69 | if (checked === undefined) { 70 | return `
  • ${text}
  • \n` 71 | } 72 | 73 | return ( 74 | '
  • ' + 75 | ' ' + 78 | text + 79 | '
  • \n' 80 | ) 81 | } 82 | 83 | paragraph(text) { 84 | return `

    ${text}

    \n` 85 | } 86 | 87 | table(header, body) { 88 | return `\n\n${header}\n\n${body}\n
    \n` 89 | } 90 | 91 | tablerow(content) { 92 | return `\n${content}\n` 93 | } 94 | 95 | tablecell(content, flags) { 96 | const type = flags.header ? 'th' : 'td' 97 | const tag = flags.align 98 | ? `<${type} style="text-align:${flags.align}">` 99 | : `<${type}>` 100 | return `${tag + content}\n` 101 | } 102 | 103 | // span level renderer 104 | strong(text) { 105 | return `${text}` 106 | } 107 | 108 | em(text) { 109 | return `${text}` 110 | } 111 | 112 | codespan(text) { 113 | return `${text}` 114 | } 115 | 116 | br() { 117 | return this.options.xhtml ? '
    ' : '
    ' 118 | } 119 | 120 | del(text) { 121 | return `${text}` 122 | } 123 | 124 | link(href, title, text) { 125 | if (this.options.sanitize) { 126 | let prot 127 | try { 128 | prot = decodeURIComponent(unescape(href)) 129 | .replace(/[^\w:]/g, '') 130 | .toLowerCase() 131 | } catch (err) { 132 | return '' 133 | } 134 | if ( 135 | // eslint-disable-next-line no-script-url 136 | prot.indexOf('javascript:') === 0 || 137 | prot.indexOf('vbscript:') === 0 || 138 | prot.indexOf('data:') === 0 139 | ) { 140 | // eslint-disable-line no-script-url 141 | return '' 142 | } 143 | } 144 | let out = `${text}` 156 | return out 157 | } 158 | 159 | image(href, title, text) { 160 | let out = `${text}' : '>' 165 | return out 166 | } 167 | 168 | text(text) { 169 | return text 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | import defaultOptions from './defaults' 2 | import InlineLexer from './inline-lexer' 3 | import Renderer from './renderer' 4 | 5 | /** 6 | * Parsing & Compiling 7 | */ 8 | 9 | export default class Parser { 10 | constructor(options = defaultOptions) { 11 | this.tokens = [] 12 | this.token = null 13 | this.options = options 14 | this.options.renderer = this.options.renderer || new Renderer() 15 | this.renderer = this.options.renderer 16 | this.renderer.options = this.options 17 | } 18 | 19 | static parse(src, options, renderer) { 20 | return new Parser(options, renderer).parse(src) 21 | } 22 | 23 | /** 24 | * Parse Loop 25 | */ 26 | 27 | parse(src) { 28 | this.inline = new InlineLexer(src.links, this.options, this.renderer) 29 | this.tokens = src.reverse() 30 | 31 | let out = '' 32 | while (this.next()) { 33 | out += this.tok() 34 | } 35 | 36 | // Remove cached headings 37 | this.renderer._headings = [] 38 | return out 39 | } 40 | 41 | /** 42 | * Next Token 43 | */ 44 | 45 | next() { 46 | this.token = this.tokens.pop() 47 | return this.token 48 | } 49 | 50 | /** 51 | * Preview Next Token 52 | */ 53 | 54 | peek() { 55 | return this.tokens[this.tokens.length - 1] || 0 56 | } 57 | 58 | /** 59 | * Parse Text Tokens 60 | */ 61 | 62 | parseText() { 63 | let body = this.token.text 64 | 65 | while (this.peek().type === 'text') { 66 | body += `\n${this.next().text}` 67 | } 68 | 69 | return this.inline.output(body) 70 | } 71 | 72 | /** 73 | * Parse Current Token 74 | */ 75 | 76 | tok() { 77 | switch (this.token.type) { 78 | case 'space': { 79 | return '' 80 | } 81 | case 'hr': { 82 | return this.renderer.hr() 83 | } 84 | case 'heading': { 85 | return this.renderer.heading( 86 | this.inline.output(this.token.text), 87 | this.token.depth, 88 | this.token.text 89 | ) 90 | } 91 | case 'code': { 92 | return this.renderer.code( 93 | this.token.text, 94 | this.token.lang, 95 | this.token.escaped 96 | ) 97 | } 98 | case 'table': { 99 | let header = '' 100 | let body = '' 101 | let i 102 | let row 103 | let cell 104 | let j 105 | 106 | // header 107 | cell = '' 108 | for (i = 0; i < this.token.header.length; i++) { 109 | cell += this.renderer.tablecell( 110 | this.inline.output(this.token.header[i]), 111 | { header: true, align: this.token.align[i] } 112 | ) 113 | } 114 | header += this.renderer.tablerow(cell) 115 | 116 | for (i = 0; i < this.token.cells.length; i++) { 117 | row = this.token.cells[i] 118 | 119 | cell = '' 120 | for (j = 0; j < row.length; j++) { 121 | cell += this.renderer.tablecell(this.inline.output(row[j]), { 122 | header: false, 123 | align: this.token.align[j] 124 | }) 125 | } 126 | 127 | body += this.renderer.tablerow(cell) 128 | } 129 | return this.renderer.table(header, body) 130 | } 131 | case 'blockquote_start': { 132 | let body = '' 133 | 134 | while (this.next().type !== 'blockquote_end') { 135 | body += this.tok() 136 | } 137 | 138 | return this.renderer.blockquote(body) 139 | } 140 | case 'list_start': { 141 | let body = '' 142 | let taskList = false 143 | const ordered = this.token.ordered 144 | 145 | while (this.next().type !== 'list_end') { 146 | if (this.token.checked !== undefined) { 147 | taskList = true 148 | } 149 | body += this.tok() 150 | } 151 | 152 | return this.renderer.list(body, ordered, taskList) 153 | } 154 | case 'list_item_start': { 155 | let body = '' 156 | const checked = this.token.checked 157 | 158 | while (this.next().type !== 'list_item_end') { 159 | body += this.token.type === 'text' ? this.parseText() : this.tok() 160 | } 161 | 162 | return this.renderer.listitem(body, checked) 163 | } 164 | case 'loose_item_start': { 165 | let body = '' 166 | const checked = this.token.checked 167 | 168 | while (this.next().type !== 'list_item_end') { 169 | body += this.tok() 170 | } 171 | 172 | return this.renderer.listitem(body, checked) 173 | } 174 | case 'html': { 175 | const html = 176 | !this.token.pre && !this.options.pedantic 177 | ? this.inline.output(this.token.text) 178 | : this.token.text 179 | return this.renderer.html(html) 180 | } 181 | case 'paragraph': { 182 | return this.renderer.paragraph(this.inline.output(this.token.text)) 183 | } 184 | case 'text': { 185 | return this.renderer.paragraph(this.parseText()) 186 | } 187 | default: { 188 | throw new Error('Unknow type') 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/inline-lexer.js: -------------------------------------------------------------------------------- 1 | import defaultOptions from './defaults' 2 | import Renderer from './renderer' 3 | import inlineRules from './inline-rules' 4 | import { escape } from './utils' 5 | 6 | class InlineLexer { 7 | constructor(links, options = defaultOptions) { 8 | this.options = options 9 | this.links = links 10 | this.renderer = this.options.renderer || new Renderer() 11 | this.renderer.options = this.options 12 | 13 | if (!this.links) { 14 | throw new Error('Tokens array requires a `links` property.') 15 | } 16 | 17 | if (this.options.gfm) { 18 | if (this.options.breaks) { 19 | this.rules = inlineRules.breaks 20 | } else { 21 | this.rules = inlineRules.gfm 22 | } 23 | } else if (this.options.pedantic) { 24 | this.rules = inlineRules.pedantic 25 | } else { 26 | this.rules = inlineRules.normal 27 | } 28 | } 29 | 30 | static output(src, links, options) { 31 | return new InlineLexer(links, options).output(src) 32 | } 33 | 34 | output(src) { 35 | let out = '' 36 | let link 37 | let text 38 | let href 39 | let cap 40 | 41 | while (src) { 42 | // escape 43 | if ((cap = this.rules.escape.exec(src))) { 44 | src = src.substring(cap[0].length) 45 | out += cap[1] 46 | continue 47 | } 48 | 49 | // autolink 50 | if ((cap = this.rules.autolink.exec(src))) { 51 | src = src.substring(cap[0].length) 52 | if (cap[2] === '@') { 53 | text = 54 | cap[1].charAt(6) === ':' 55 | ? this.mangle(cap[1].substring(7)) 56 | : this.mangle(cap[1]) 57 | href = this.mangle('mailto:') + text 58 | } else { 59 | text = escape(cap[1]) 60 | href = text 61 | } 62 | out += this.renderer.link(href, null, text) 63 | continue 64 | } 65 | 66 | // url (gfm) 67 | if (!this.inLink && (cap = this.rules.url.exec(src))) { 68 | src = src.substring(cap[0].length) 69 | text = escape(cap[1]) 70 | href = text 71 | out += this.renderer.link(href, null, text) 72 | continue 73 | } 74 | 75 | // tag 76 | if ((cap = this.rules.tag.exec(src))) { 77 | if (!this.inLink && /^/i.test(cap[0])) { 80 | this.inLink = false 81 | } 82 | src = src.substring(cap[0].length) 83 | out += this.options.sanitize 84 | ? this.options.sanitizer 85 | ? this.options.sanitizer(cap[0]) 86 | : escape(cap[0]) 87 | : cap[0] 88 | continue 89 | } 90 | 91 | // link 92 | if ((cap = this.rules.link.exec(src))) { 93 | src = src.substring(cap[0].length) 94 | this.inLink = true 95 | out += this.outputLink(cap, { 96 | href: cap[2], 97 | title: cap[3] 98 | }) 99 | this.inLink = false 100 | continue 101 | } 102 | 103 | // reflink, nolink 104 | if ( 105 | (cap = this.rules.reflink.exec(src)) || 106 | (cap = this.rules.nolink.exec(src)) 107 | ) { 108 | src = src.substring(cap[0].length) 109 | link = (cap[2] || cap[1]).replace(/\s+/g, ' ') 110 | link = this.links[link.toLowerCase()] 111 | if (!link || !link.href) { 112 | out += cap[0].charAt(0) 113 | src = cap[0].substring(1) + src 114 | continue 115 | } 116 | this.inLink = true 117 | out += this.outputLink(cap, link) 118 | this.inLink = false 119 | continue 120 | } 121 | 122 | // strong 123 | if ((cap = this.rules.strong.exec(src))) { 124 | src = src.substring(cap[0].length) 125 | out += this.renderer.strong(this.output(cap[2] || cap[1])) 126 | continue 127 | } 128 | 129 | // em 130 | if ((cap = this.rules.em.exec(src))) { 131 | src = src.substring(cap[0].length) 132 | out += this.renderer.em(this.output(cap[2] || cap[1])) 133 | continue 134 | } 135 | 136 | // code 137 | if ((cap = this.rules.code.exec(src))) { 138 | src = src.substring(cap[0].length) 139 | out += this.renderer.codespan(escape(cap[2], true)) 140 | continue 141 | } 142 | 143 | // br 144 | if ((cap = this.rules.br.exec(src))) { 145 | src = src.substring(cap[0].length) 146 | out += this.renderer.br() 147 | continue 148 | } 149 | 150 | // del (gfm) 151 | if ((cap = this.rules.del.exec(src))) { 152 | src = src.substring(cap[0].length) 153 | out += this.renderer.del(this.output(cap[1])) 154 | continue 155 | } 156 | 157 | // text 158 | if ((cap = this.rules.text.exec(src))) { 159 | src = src.substring(cap[0].length) 160 | out += this.renderer.text(escape(this.smartypants(cap[0]))) 161 | continue 162 | } 163 | 164 | if (src) { 165 | throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)) 166 | } 167 | } 168 | 169 | return out 170 | } 171 | 172 | outputLink(cap, link) { 173 | const href = escape(link.href) 174 | const title = link.title ? escape(link.title) : null 175 | 176 | return cap[0].charAt(0) === '!' 177 | ? this.renderer.image(href, title, escape(cap[1])) 178 | : this.renderer.link(href, title, this.output(cap[1])) 179 | } 180 | 181 | smartypants(text) { 182 | if (!this.options.smartypants) return text 183 | return ( 184 | text 185 | // em-dashes 186 | .replace(/---/g, '\u2014') 187 | // en-dashes 188 | .replace(/--/g, '\u2013') 189 | // opening singles 190 | .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') 191 | // closing singles & apostrophes 192 | .replace(/'/g, '\u2019') 193 | // opening doubles 194 | .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') 195 | // closing doubles 196 | .replace(/"/g, '\u201d') 197 | // ellipses 198 | .replace(/\.{3}/g, '\u2026') 199 | ) 200 | } 201 | 202 | mangle(text) { 203 | if (!this.options.mangle) return text 204 | let out = '' 205 | let i = 0 206 | let ch 207 | 208 | for (; i < text.length; i++) { 209 | ch = text.charCodeAt(i) 210 | if (Math.random() > 0.5) { 211 | ch = 'x' + ch.toString(16) 212 | } 213 | out += '&#' + ch + ';' 214 | } 215 | 216 | return out 217 | } 218 | } 219 | 220 | InlineLexer.rules = inlineRules 221 | 222 | export default InlineLexer 223 | -------------------------------------------------------------------------------- /src/lexer.js: -------------------------------------------------------------------------------- 1 | import blockRules from './block-rules' 2 | import defaultOptions from './defaults' 3 | 4 | class Lexer { 5 | constructor(options = defaultOptions) { 6 | this.tokens = [] 7 | this.tokens.links = {} 8 | this.options = options 9 | 10 | if (this.options.gfm) { 11 | if (this.options.tables) { 12 | this.rules = blockRules.tables 13 | } else { 14 | this.rules = blockRules.gfm 15 | } 16 | } else { 17 | this.rules = blockRules.normal 18 | } 19 | } 20 | 21 | static lex(src, options) { 22 | return new Lexer(options).lex(src) 23 | } 24 | 25 | lex(src) { 26 | src = src 27 | .replace(/\r\n|\r/g, '\n') 28 | .replace(/\t/g, ' ') 29 | .replace(/\u00a0/g, ' ') 30 | .replace(/\u2424/g, '\n') 31 | 32 | return this.token(src, true) 33 | } 34 | 35 | token(src, top, bq) { 36 | src = src.replace(/^ +$/gm, '') 37 | 38 | let next 39 | let loose 40 | let cap 41 | let bull 42 | let b 43 | let item 44 | let space 45 | let i 46 | let l 47 | let checked 48 | 49 | while (src) { 50 | // newline 51 | if ((cap = this.rules.newline.exec(src))) { 52 | src = src.substring(cap[0].length) 53 | if (cap[0].length > 1) { 54 | this.tokens.push({ 55 | type: 'space' 56 | }) 57 | } 58 | } 59 | 60 | // code 61 | if ((cap = this.rules.code.exec(src))) { 62 | src = src.substring(cap[0].length) 63 | cap = cap[0].replace(/^ {4}/gm, '') 64 | this.tokens.push({ 65 | type: 'code', 66 | text: this.options.pedantic ? cap : cap.replace(/\n+$/, '') 67 | }) 68 | continue 69 | } 70 | 71 | // fences (gfm) 72 | if ((cap = this.rules.fences.exec(src))) { 73 | src = src.substring(cap[0].length) 74 | this.tokens.push({ 75 | type: 'code', 76 | lang: cap[2], 77 | text: cap[3] || '' 78 | }) 79 | continue 80 | } 81 | 82 | // heading 83 | if ((cap = this.rules.heading.exec(src))) { 84 | src = src.substring(cap[0].length) 85 | this.tokens.push({ 86 | type: 'heading', 87 | depth: cap[1].length, 88 | text: cap[2] 89 | }) 90 | continue 91 | } 92 | 93 | // table no leading pipe (gfm) 94 | if (top && (cap = this.rules.nptable.exec(src))) { 95 | src = src.substring(cap[0].length) 96 | 97 | item = { 98 | type: 'table', 99 | header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), 100 | align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), 101 | cells: cap[3].replace(/\n$/, '').split('\n') 102 | } 103 | 104 | for (i = 0; i < item.align.length; i++) { 105 | if (/^ *-+: *$/.test(item.align[i])) { 106 | item.align[i] = 'right' 107 | } else if (/^ *:-+: *$/.test(item.align[i])) { 108 | item.align[i] = 'center' 109 | } else if (/^ *:-+ *$/.test(item.align[i])) { 110 | item.align[i] = 'left' 111 | } else { 112 | item.align[i] = null 113 | } 114 | } 115 | 116 | for (i = 0; i < item.cells.length; i++) { 117 | item.cells[i] = item.cells[i].split(/ *\| */) 118 | } 119 | 120 | this.tokens.push(item) 121 | 122 | continue 123 | } 124 | 125 | // lheading 126 | if ((cap = this.rules.lheading.exec(src))) { 127 | src = src.substring(cap[0].length) 128 | this.tokens.push({ 129 | type: 'heading', 130 | depth: cap[2] === '=' ? 1 : 2, 131 | text: cap[1] 132 | }) 133 | continue 134 | } 135 | 136 | // hr 137 | if ((cap = this.rules.hr.exec(src))) { 138 | src = src.substring(cap[0].length) 139 | this.tokens.push({ 140 | type: 'hr' 141 | }) 142 | continue 143 | } 144 | 145 | // blockquote 146 | if ((cap = this.rules.blockquote.exec(src))) { 147 | src = src.substring(cap[0].length) 148 | 149 | this.tokens.push({ 150 | type: 'blockquote_start' 151 | }) 152 | 153 | cap = cap[0].replace(/^ *> ?/gm, '') 154 | 155 | // Pass `top` to keep the current 156 | // "toplevel" state. This is exactly 157 | // how markdown.pl works. 158 | this.token(cap, top, true) 159 | 160 | this.tokens.push({ 161 | type: 'blockquote_end' 162 | }) 163 | 164 | continue 165 | } 166 | 167 | // list 168 | if ((cap = this.rules.list.exec(src))) { 169 | src = src.substring(cap[0].length) 170 | bull = cap[2] 171 | 172 | this.tokens.push({ 173 | type: 'list_start', 174 | ordered: bull.length > 1 175 | }) 176 | 177 | // Get each top-level item. 178 | cap = cap[0].match(this.rules.item) 179 | 180 | next = false 181 | l = cap.length 182 | i = 0 183 | 184 | for (; i < l; i++) { 185 | item = cap[i] 186 | 187 | // Remove the list item's bullet 188 | // so it is seen as the next token. 189 | space = item.length 190 | item = item.replace(/^ *([*+-]|\d+\.) +/, '') 191 | 192 | if (this.options.gfm && this.options.taskLists) { 193 | checked = this.rules.checkbox.exec(item) 194 | 195 | if (checked) { 196 | checked = checked[1] === 'x' 197 | item = item.replace(this.rules.checkbox, '') 198 | } else { 199 | checked = undefined 200 | } 201 | } 202 | 203 | // Outdent whatever the 204 | // list item contains. Hacky. 205 | if (item.indexOf('\n ') !== -1) { 206 | space -= item.length 207 | item = this.options.pedantic 208 | ? item.replace(/^ {1,4}/gm, '') 209 | : item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') 210 | } 211 | 212 | // Determine whether the next list item belongs here. 213 | // Backpedal if it does not belong in this list. 214 | if (this.options.smartLists && i !== l - 1) { 215 | b = this.rules.bullet.exec(cap[i + 1])[0] 216 | if (bull !== b && !(bull.length > 1 && b.length > 1)) { 217 | src = cap.slice(i + 1).join('\n') + src 218 | i = l - 1 219 | } 220 | } 221 | 222 | // Determine whether item is loose or not. 223 | // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ 224 | // for discount behavior. 225 | loose = next || /\n\n(?!\s*$)/.test(item) 226 | if (i !== l - 1) { 227 | next = item.charAt(item.length - 1) === '\n' 228 | if (!loose) loose = next 229 | } 230 | 231 | this.tokens.push({ 232 | checked, 233 | type: loose ? 'loose_item_start' : 'list_item_start' 234 | }) 235 | 236 | // Recurse. 237 | this.token(item, false, bq) 238 | 239 | this.tokens.push({ 240 | type: 'list_item_end' 241 | }) 242 | } 243 | 244 | this.tokens.push({ 245 | type: 'list_end' 246 | }) 247 | 248 | continue 249 | } 250 | 251 | // html 252 | if ((cap = this.rules.html.exec(src))) { 253 | src = src.substring(cap[0].length) 254 | this.tokens.push({ 255 | type: this.options.sanitize ? 'paragraph' : 'html', 256 | pre: 257 | !this.options.sanitizer && 258 | (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), 259 | text: cap[0] 260 | }) 261 | continue 262 | } 263 | 264 | // def 265 | if (!bq && top && (cap = this.rules.def.exec(src))) { 266 | src = src.substring(cap[0].length) 267 | this.tokens.links[cap[1].toLowerCase()] = { 268 | href: cap[2], 269 | title: cap[3] 270 | } 271 | continue 272 | } 273 | 274 | // table (gfm) 275 | if (top && (cap = this.rules.table.exec(src))) { 276 | src = src.substring(cap[0].length) 277 | 278 | item = { 279 | type: 'table', 280 | header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), 281 | align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), 282 | cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') 283 | } 284 | 285 | for (i = 0; i < item.align.length; i++) { 286 | if (/^ *-+: *$/.test(item.align[i])) { 287 | item.align[i] = 'right' 288 | } else if (/^ *:-+: *$/.test(item.align[i])) { 289 | item.align[i] = 'center' 290 | } else if (/^ *:-+ *$/.test(item.align[i])) { 291 | item.align[i] = 'left' 292 | } else { 293 | item.align[i] = null 294 | } 295 | } 296 | 297 | for (i = 0; i < item.cells.length; i++) { 298 | item.cells[i] = item.cells[i] 299 | .replace(/^ *\| *| *\| *$/g, '') 300 | .split(/ *\| */) 301 | } 302 | 303 | this.tokens.push(item) 304 | 305 | continue 306 | } 307 | 308 | // top-level paragraph 309 | if (top && (cap = this.rules.paragraph.exec(src))) { 310 | src = src.substring(cap[0].length) 311 | this.tokens.push({ 312 | type: 'paragraph', 313 | text: 314 | cap[1].charAt(cap[1].length - 1) === '\n' 315 | ? cap[1].slice(0, -1) 316 | : cap[1] 317 | }) 318 | continue 319 | } 320 | 321 | // text 322 | if ((cap = this.rules.text.exec(src))) { 323 | // Top-level should never reach here. 324 | src = src.substring(cap[0].length) 325 | this.tokens.push({ 326 | type: 'text', 327 | text: cap[0] 328 | }) 329 | continue 330 | } 331 | 332 | if (src) { 333 | throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)) 334 | } 335 | } 336 | 337 | return this.tokens 338 | } 339 | } 340 | 341 | Lexer.rules = blockRules 342 | 343 | export default Lexer 344 | --------------------------------------------------------------------------------