├── .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 | | foo |
43 | bar |
44 |
45 |
46 |
47 |
48 | | foo |
49 | bar |
50 |
51 |
52 |
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: /^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\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 | [](https://npmjs.com/package/md) [](https://npmjs.com/package/md) [](https://circleci.com/gh/egoist/md) [](https://codecov.io/gh/egoist/md)
4 | [](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 `\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}${type}>\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}${type}>\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 = `
' : '>'
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 |
--------------------------------------------------------------------------------