├── test ├── fixtures │ ├── commonmark │ │ └── bad.txt │ └── markdown-it │ │ ├── proto.txt │ │ ├── typographer.txt │ │ ├── fatal.txt │ │ ├── linkify.txt │ │ ├── strikethrough.txt │ │ ├── normalize.txt │ │ ├── smartquotes.txt │ │ ├── xss.txt │ │ └── commonmark_extras.txt ├── mocha.opts ├── .eslintrc ├── markdown-it.js ├── token.js ├── commonmark.js ├── utils.js └── ruler.js ├── Procfile ├── index.js ├── .gitignore ├── benchmark ├── samples │ ├── inline-backticks.md │ ├── block-hr.md │ ├── block-code.md │ ├── block-fences.md │ ├── block-lheading.md │ ├── block-heading.md │ ├── inline-em-flat.md │ ├── inline-em-nested.md │ ├── inline-em-worst.md │ ├── inline-escape.md │ ├── inline-newlines.md │ ├── block-bq-flat.md │ ├── block-ref-nested.md │ ├── inline-entity.md │ ├── rawtabs.md │ ├── inline-links-nested.md │ ├── block-bq-nested.md │ ├── block-html.md │ ├── block-ref-flat.md │ ├── block-list-nested.md │ ├── block-tables.md │ ├── inline-autolink.md │ ├── inline-html.md │ ├── inline-links-flat.md │ ├── block-list-flat.md │ ├── block-ref-list.md │ └── lorem1.txt ├── implementations │ ├── marked │ │ └── index.js │ ├── markdown-it-2.2.1-commonmark │ │ └── index.js │ ├── current │ │ └── index.js │ ├── commonmark-reference │ │ └── index.js │ └── current-commonmark │ │ └── index.js ├── profile.js └── benchmark.js ├── .eslintignore ├── .travis.yml ├── lib ├── common │ ├── entities.js │ ├── html_re.js │ └── html_blocks.js ├── helpers │ ├── index.js │ ├── parse_link_label.js │ ├── parse_link_title.js │ └── parse_link_destination.js ├── rules_core │ ├── inline.js │ ├── block.js │ ├── normalize.js │ ├── state_core.js │ ├── replacements.js │ ├── linkify.js │ └── smartquotes.js ├── rules_block │ ├── code.js │ ├── hr.js │ ├── heading.js │ ├── paragraph.js │ ├── html_block.js │ ├── fence.js │ ├── lheading.js │ ├── reference.js │ ├── table.js │ └── state_block.js ├── rules_inline │ ├── text_collapse.js │ ├── html_inline.js │ ├── newline.js │ ├── backticks.js │ ├── escape.js │ ├── balance_pairs.js │ ├── entity.js │ ├── autolink.js │ ├── text.js │ ├── strikethrough.js │ ├── state_inline.js │ ├── emphasis.js │ ├── image.js │ └── link.js ├── parser_core.js ├── presets │ ├── default.js │ ├── zero.js │ └── commonmark.js ├── parser_block.js ├── token.js └── parser_inline.js ├── support ├── demo_template │ ├── README.md │ ├── index.styl │ ├── index.jade │ └── sample.md ├── demodata.js ├── babelmark-responder.js ├── markdown-it.js ├── specsplit.js └── api_header.md ├── .editorconfig ├── docs ├── README.md ├── 5.0_migration.md ├── 4.0_migration.md ├── security.md └── development.md ├── bower.json ├── .ndocrc ├── CONTRIBUTING.md ├── LICENSE ├── package.json ├── bin └── markdown-it.js ├── Makefile └── .eslintrc.yml /test/fixtures/commonmark/bad.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R spec --inline-diffs -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node support/babelmark-responder.js 2 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | mocha: true 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = require('./lib/'); 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | benchmark/extra/ 2 | node_modules/ 3 | coverage/ 4 | demo/ 5 | apidoc/ 6 | *.log 7 | -------------------------------------------------------------------------------- /benchmark/samples/inline-backticks.md: -------------------------------------------------------------------------------- 1 | `lots`of`backticks` 2 | 3 | ``i``wonder``how``this``will``be``parsed`` 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | demo/ 3 | dist/ 4 | node_modules 5 | support/demo_template/sample.js 6 | benchmark/extra/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | - '6' 5 | after_success: make report-coverage 6 | sudo: false 7 | -------------------------------------------------------------------------------- /benchmark/samples/block-hr.md: -------------------------------------------------------------------------------- 1 | 2 | * * * * * 3 | 4 | - - - - - 5 | 6 | ________ 7 | 8 | 9 | ************************* text 10 | 11 | -------------------------------------------------------------------------------- /benchmark/samples/block-code.md: -------------------------------------------------------------------------------- 1 | 2 | an 3 | example 4 | 5 | of 6 | 7 | 8 | 9 | a code 10 | block 11 | 12 | -------------------------------------------------------------------------------- /benchmark/samples/block-fences.md: -------------------------------------------------------------------------------- 1 | 2 | ``````````text 3 | an 4 | example 5 | ``` 6 | of 7 | 8 | 9 | a fenced 10 | ``` 11 | code 12 | block 13 | `````````` 14 | 15 | -------------------------------------------------------------------------------- /benchmark/samples/block-lheading.md: -------------------------------------------------------------------------------- 1 | heading 2 | --- 3 | 4 | heading 5 | =================================== 6 | 7 | not a heading 8 | ----------------------------------- text 9 | -------------------------------------------------------------------------------- /lib/common/entities.js: -------------------------------------------------------------------------------- 1 | // HTML5 entities map: { name -> utf16string } 2 | // 3 | 'use strict'; 4 | 5 | /*eslint quotes:0*/ 6 | module.exports = require('entities/maps/entities.json'); 7 | -------------------------------------------------------------------------------- /benchmark/samples/block-heading.md: -------------------------------------------------------------------------------- 1 | # heading 2 | ### heading 3 | ##### heading 4 | 5 | # heading # 6 | ### heading ### 7 | ##### heading \#\#\#\#\###### 8 | 9 | ############ not a heading 10 | -------------------------------------------------------------------------------- /benchmark/implementations/marked/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var marked = require('../../extra/lib/node_modules/marked'); 4 | 5 | exports.run = function (data) { 6 | return marked(data); 7 | }; 8 | -------------------------------------------------------------------------------- /benchmark/samples/inline-em-flat.md: -------------------------------------------------------------------------------- 1 | *this* *is* *your* *basic* *boring* *emphasis* 2 | 3 | _this_ _is_ _your_ _basic_ _boring_ _emphasis_ 4 | 5 | **this** **is** **your** **basic** **boring** **emphasis** 6 | -------------------------------------------------------------------------------- /benchmark/samples/inline-em-nested.md: -------------------------------------------------------------------------------- 1 | *this *is *a *bunch* of* nested* emphases* 2 | 3 | __this __is __a __bunch__ of__ nested__ emphases__ 4 | 5 | ***this ***is ***a ***bunch*** of*** nested*** emphases*** 6 | -------------------------------------------------------------------------------- /benchmark/samples/inline-em-worst.md: -------------------------------------------------------------------------------- 1 | *this *is *a *worst *case *for *em *backtracking 2 | 3 | __this __is __a __worst __case __for __em __backtracking 4 | 5 | ***this ***is ***a ***worst ***case ***for ***em ***backtracking 6 | -------------------------------------------------------------------------------- /support/demo_template/README.md: -------------------------------------------------------------------------------- 1 | This repo is generated from __[markdown-it](https://github.com/markdown-it/markdown-it)__ by script. 2 | 3 | Please, use __[markdown-it](https://github.com/markdown-it)__ for all questions & PRs. 4 | -------------------------------------------------------------------------------- /benchmark/implementations/markdown-it-2.2.1-commonmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var md = require('../../extra/lib/node_modules/markdown-it')('commonmark'); 4 | 5 | exports.run = function (data) { 6 | return md.render(data); 7 | }; 8 | -------------------------------------------------------------------------------- /benchmark/implementations/current/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var md = require('../../../')({ 4 | html: true, 5 | linkify: true, 6 | typographer: true 7 | }); 8 | 9 | exports.run = function (data) { 10 | return md.render(data); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/helpers/index.js: -------------------------------------------------------------------------------- 1 | // Just a shortcut for bulk export 2 | 'use strict'; 3 | 4 | 5 | exports.parseLinkLabel = require('./parse_link_label'); 6 | exports.parseLinkDestination = require('./parse_link_destination'); 7 | exports.parseLinkTitle = require('./parse_link_title'); 8 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/proto.txt: -------------------------------------------------------------------------------- 1 | . 2 | [__proto__] 3 | 4 | [__proto__]: blah 5 | . 6 |

proto

7 | . 8 | 9 | 10 | . 11 | [hasOwnProperty] 12 | 13 | [hasOwnProperty]: blah 14 | . 15 |

hasOwnProperty

16 | . 17 | -------------------------------------------------------------------------------- /benchmark/samples/inline-escape.md: -------------------------------------------------------------------------------- 1 | 2 | \t\e\s\t\i\n\g \e\s\c\a\p\e \s\e\q\u\e\n\c\e\s 3 | 4 | \!\\\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\? 5 | 6 | \@ \[ \] \^ \_ \` \{ \| \} \~ \- \' 7 | 8 | \ 9 | \\ 10 | \\\ 11 | \\\\ 12 | \\\\\ 13 | 14 | \ \ \ \ 15 | 16 | -------------------------------------------------------------------------------- /benchmark/samples/inline-newlines.md: -------------------------------------------------------------------------------- 1 | 2 | this\ 3 | should\ 4 | be\ 5 | separated\ 6 | by\ 7 | newlines 8 | 9 | this 10 | should 11 | be 12 | separated 13 | by 14 | newlines 15 | too 16 | 17 | this 18 | should 19 | not 20 | be 21 | separated 22 | by 23 | newlines 24 | 25 | -------------------------------------------------------------------------------- /benchmark/implementations/commonmark-reference/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commonmark = require('../../extra/lib/node_modules/commonmark'); 4 | var parser = new commonmark.Parser(); 5 | var renderer = new commonmark.HtmlRenderer(); 6 | 7 | exports.run = function (data) { 8 | return renderer.render(parser.parse(data)); 9 | }; 10 | -------------------------------------------------------------------------------- /benchmark/samples/block-bq-flat.md: -------------------------------------------------------------------------------- 1 | > the simple example of a blockquote 2 | > the simple example of a blockquote 3 | > the simple example of a blockquote 4 | > the simple example of a blockquote 5 | ... continuation 6 | ... continuation 7 | ... continuation 8 | ... continuation 9 | 10 | empty blockquote: 11 | 12 | > 13 | > 14 | > 15 | > 16 | 17 | -------------------------------------------------------------------------------- /benchmark/samples/block-ref-nested.md: -------------------------------------------------------------------------------- 1 | [[[[[[[foo]]]]]]] 2 | 3 | [[[[[[[foo]]]]]]]: bar 4 | [[[[[[foo]]]]]]: bar 5 | [[[[[foo]]]]]: bar 6 | [[[[foo]]]]: bar 7 | [[[foo]]]: bar 8 | [[foo]]: bar 9 | [foo]: bar 10 | 11 | [*[*[*[*[foo]*]*]*]*] 12 | 13 | [*[*[*[*[foo]*]*]*]*]: bar 14 | [*[*[*[foo]*]*]*]: bar 15 | [*[*[foo]*]*]: bar 16 | [*[foo]*]: bar 17 | [foo]: bar 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [{.,}*.{js{,*},y{a,}ml}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.{md,txt}] 14 | indent_style = space 15 | indent_size = 4 16 | trim_trailing_whitespace = false 17 | 18 | [Makefile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /lib/rules_core/inline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function inline(state) { 4 | var tokens = state.tokens, tok, i, l; 5 | 6 | // Parse inlines 7 | for (i = 0, l = tokens.length; i < l; i++) { 8 | tok = tokens[i]; 9 | if (tok.type === 'inline') { 10 | state.md.inline.parse(tok.content, state.md, state.env, tok.children); 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /benchmark/samples/inline-entity.md: -------------------------------------------------------------------------------- 1 | entities: 2 | 3 |   & © Æ Ď ¾ ℋ ⅆ ∲ 4 | 5 | # Ӓ Ϡ � 6 | 7 | non-entities: 8 | 9 | &18900987654321234567890; &1234567890098765432123456789009876543212345678987654; 10 | 11 | &qwertyuioppoiuytrewqwer; &oiuytrewqwertyuioiuytrewqwertyuioytrewqwertyuiiuytri; 12 | -------------------------------------------------------------------------------- /benchmark/samples/rawtabs.md: -------------------------------------------------------------------------------- 1 | 2 | this is a test for tab expansion, be careful not to replace them with spaces 3 | 4 | 1 4444 5 | 22 333 6 | 333 22 7 | 4444 1 8 | 9 | 10 | tab-indented line 11 | space-indented line 12 | tab-indented line 13 | 14 | 15 | a lot of spaces in between here 16 | 17 | a lot of tabs in between here 18 | 19 | -------------------------------------------------------------------------------- /benchmark/samples/inline-links-nested.md: -------------------------------------------------------------------------------- 1 | Valid links: 2 | 3 | [[[[[[[[](test)](test)](test)](test)](test)](test)](test)] 4 | 5 | [ [[[[[[[[[[[[[[[[[[ [](test) ]]]]]]]]]]]]]]]]]] ](test) 6 | 7 | Invalid links: 8 | 9 | [[[[[[[[[ 10 | 11 | [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ [ 12 | 13 | ![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![![ 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This folder contains useful info for plugin developers. 2 | 3 | If you just use `markdown-it` in your app, see 4 | [README](https://github.com/markdown-it/markdown-it#markdown-it) and 5 | [API docs](https://markdown-it.github.io/markdown-it/). 6 | 7 | __Content__: 8 | 9 | - [Parser architecture & design principles](architecture.md) 10 | - [Some guidelines for plugin developers](development.md) 11 | -------------------------------------------------------------------------------- /test/markdown-it.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var path = require('path'); 5 | 6 | 7 | var generate = require('markdown-it-testgen'); 8 | 9 | 10 | describe('markdown-it', function () { 11 | var md = require('../')({ 12 | html: true, 13 | langPrefix: '', 14 | typographer: true, 15 | linkify: true 16 | }); 17 | 18 | generate(path.join(__dirname, 'fixtures/markdown-it'), md); 19 | }); 20 | -------------------------------------------------------------------------------- /support/demodata.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Build demo data for embedding into html 4 | 5 | /*eslint-disable no-console*/ 6 | 'use strict'; 7 | 8 | var fs = require('fs'); 9 | var path = require('path'); 10 | 11 | console.log(JSON.stringify({ 12 | self: { 13 | demo: { 14 | source: fs.readFileSync(path.join(__dirname, './demo_template/sample.md'), 'utf8') 15 | } 16 | } 17 | }, null, 2)); 18 | -------------------------------------------------------------------------------- /benchmark/samples/block-bq-nested.md: -------------------------------------------------------------------------------- 1 | >>>>>> deeply nested blockquote 2 | >>>>> deeply nested blockquote 3 | >>>> deeply nested blockquote 4 | >>> deeply nested blockquote 5 | >> deeply nested blockquote 6 | > deeply nested blockquote 7 | 8 | > deeply nested blockquote 9 | >> deeply nested blockquote 10 | >>> deeply nested blockquote 11 | >>>> deeply nested blockquote 12 | >>>>> deeply nested blockquote 13 | >>>>>> deeply nested blockquote 14 | -------------------------------------------------------------------------------- /lib/rules_core/block.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | module.exports = function block(state) { 5 | var token; 6 | 7 | if (state.inlineMode) { 8 | token = new state.Token('inline', '', 0); 9 | token.content = state.src; 10 | token.map = [ 0, 1 ]; 11 | token.children = []; 12 | state.tokens.push(token); 13 | } else { 14 | state.md.block.parse(state.src, state.md, state.env, state.tokens); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /benchmark/samples/block-html.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | blah blah 4 | 5 |
6 | 7 | 8 | 9 | 12 | 13 |
10 | **test** 11 |
14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 |
20 | 21 | test 22 | 23 |
28 | 29 | 32 | 33 | -------------------------------------------------------------------------------- /lib/rules_core/normalize.js: -------------------------------------------------------------------------------- 1 | // Normalize input string 2 | 3 | 'use strict'; 4 | 5 | 6 | var NEWLINES_RE = /\r[\n\u0085]?|[\u2424\u2028\u0085]/g; 7 | var NULL_RE = /\u0000/g; 8 | 9 | 10 | module.exports = function inline(state) { 11 | var str; 12 | 13 | // Normalize newlines 14 | str = state.src.replace(NEWLINES_RE, '\n'); 15 | 16 | // Replace NULL characters 17 | str = str.replace(NULL_RE, '\uFFFD'); 18 | 19 | state.src = str; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/rules_core/state_core.js: -------------------------------------------------------------------------------- 1 | // Core state object 2 | // 3 | 'use strict'; 4 | 5 | var Token = require('../token'); 6 | 7 | 8 | function StateCore(src, md, env) { 9 | this.src = src; 10 | this.env = env; 11 | this.tokens = []; 12 | this.inlineMode = false; 13 | this.md = md; // link to parser instance 14 | } 15 | 16 | // re-export Token class to use in core rules 17 | StateCore.prototype.Token = Token; 18 | 19 | 20 | module.exports = StateCore; 21 | -------------------------------------------------------------------------------- /benchmark/implementations/current-commonmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var md = require('../../../')('commonmark'); 4 | 5 | // Replace normalizers to more primitive, for more "honest" compare. 6 | // Default ones can cause 1.5x slowdown. 7 | var encode = md.utils.lib.mdurl.encode; 8 | 9 | md.normalizeLink = function (url) { return encode(url); }; 10 | md.normalizeLinkText = function (str) { return str; }; 11 | 12 | exports.run = function (data) { 13 | return md.render(data); 14 | }; 15 | -------------------------------------------------------------------------------- /benchmark/profile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console:0*/ 3 | 'use strict'; 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | var md = require('../')({ 9 | html: true, 10 | linkify: false, 11 | typographer: false 12 | }); 13 | 14 | // var data = fs.readFileSync(path.join(__dirname, '/samples/lorem1.txt'), 'utf8'); 15 | var data = fs.readFileSync(path.join(__dirname, '../test/fixtures/commonmark/spec.txt'), 'utf8'); 16 | 17 | for (var i = 0; i < 20; i++) { 18 | md.render(data); 19 | } 20 | -------------------------------------------------------------------------------- /benchmark/samples/block-ref-flat.md: -------------------------------------------------------------------------------- 1 | [1] [2] [3] [1] [2] [3] 2 | 3 | [looooooooooooooooooooooooooooooooooooooooooooooooooong label] 4 | 5 | [1]: 6 | [2]: http://something.example.com/foo/bar 'test' 7 | [3]: 8 | http://foo/bar 9 | [ looooooooooooooooooooooooooooooooooooooooooooooooooong label ]: 10 | 111 11 | 'test' 12 | [[[[[[[[[[[[[[[[[[[[ this should not slow down anything ]]]]]]]]]]]]]]]]]]]]: q 13 | (as long as it is not referenced anywhere) 14 | 15 | [[[[[[[[[[[[[[[[[[[[]: this is not a valid reference 16 | -------------------------------------------------------------------------------- /benchmark/samples/block-list-nested.md: -------------------------------------------------------------------------------- 1 | 2 | - this 3 | - is 4 | - a 5 | - deeply 6 | - nested 7 | - bullet 8 | - list 9 | 10 | 11 | 1. this 12 | 2. is 13 | 3. a 14 | 4. deeply 15 | 5. nested 16 | 6. unordered 17 | 7. list 18 | 19 | 20 | - 1 21 | - 2 22 | - 3 23 | - 4 24 | - 5 25 | - 6 26 | - 7 27 | - 6 28 | - 5 29 | - 4 30 | - 3 31 | - 2 32 | - 1 33 | 34 | 35 | - - - - - - - - - deeply-nested one-element item 36 | 37 | -------------------------------------------------------------------------------- /test/token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var Token = require('../lib/token'); 5 | 6 | 7 | describe('Token', function () { 8 | 9 | it('attr', function () { 10 | var t = new Token('test_token', 'tok', 1); 11 | 12 | assert.strictEqual(t.attrs, null); 13 | assert.equal(t.attrIndex('foo'), -1); 14 | 15 | t.attrPush([ 'foo', 'bar' ]); 16 | t.attrPush([ 'baz', 'bad' ]); 17 | 18 | assert.equal(t.attrIndex('foo'), 0); 19 | assert.equal(t.attrIndex('baz'), 1); 20 | assert.equal(t.attrIndex('none'), -1); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /benchmark/samples/block-tables.md: -------------------------------------------------------------------------------- 1 | | Heading 1 | Heading 2 2 | | --------- | --------- 3 | | Cell 1 | Cell 2 4 | | Cell 3 | Cell 4 5 | 6 | | Header 1 | Header 2 | Header 3 | Header 4 | 7 | | :------: | -------: | :------- | -------- | 8 | | Cell 1 | Cell 2 | Cell 3 | Cell 4 | 9 | | Cell 5 | Cell 6 | Cell 7 | Cell 8 | 10 | 11 | Test code 12 | 13 | Header 1 | Header 2 14 | -------- | -------- 15 | Cell 1 | Cell 2 16 | Cell 3 | Cell 4 17 | 18 | Header 1|Header 2|Header 3|Header 4 19 | :-------|:------:|-------:|-------- 20 | Cell 1 |Cell 2 |Cell 3 |Cell 4 21 | *Cell 5*|Cell 6 |Cell 7 |Cell 8 22 | -------------------------------------------------------------------------------- /benchmark/samples/inline-autolink.md: -------------------------------------------------------------------------------- 1 | closed (valid) autolinks: 2 | 3 | 4 | 5 | 6 | 7 | 8 | these are not autolinks: 9 | 10 | 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it", 3 | "main": "dist/markdown-it.js", 4 | "homepage": "https://github.com/markdown-it/markdown-it", 5 | "description": "Markdown parser, done right. Commonmark support, extensions, syntax plugins, high speed - all in one.", 6 | "keywords": [ 7 | "markdown", 8 | "md", 9 | "commonmark", 10 | "parser" 11 | ], 12 | "license": "MIT", 13 | "ignore": [ 14 | "**/.*", 15 | "benchmark", 16 | "bower_components", 17 | "coverage", 18 | "demo", 19 | "docs", 20 | "apidoc", 21 | "lib", 22 | "node_modules", 23 | "support", 24 | "test", 25 | "Makefile", 26 | "index*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.ndocrc: -------------------------------------------------------------------------------- 1 | # 2 | # Common nodeca config 3 | ################################################################################ 4 | 5 | --index "./support/api_header.md" 6 | --package "./package.json" 7 | --gh-ribbon "https://github.com/{package.repository}" 8 | --output "apidoc" 9 | --render "html" 10 | --link-format "https://github.com/{package.repository}/blob/master/{file}#L{line}" 11 | --broken-links "show" 12 | 13 | 14 | # 15 | # Paths with sources 16 | ################################################################################ 17 | 18 | lib 19 | 20 | 21 | # 22 | # Project specific configuration 23 | ################################################################################ 24 | 25 | --show-all 26 | -------------------------------------------------------------------------------- /benchmark/samples/inline-html.md: -------------------------------------------------------------------------------- 1 | Taking commonmark tests from the spec for benchmarking here: 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | <33> <__> 14 | 15 | 16 | 17 | 28 | 29 | foo 31 | 32 | foo 33 | 34 | foo 35 | 36 | foo 37 | 38 | foo &<]]> 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### If you commit changes: 2 | 3 | 1. Make sure all tests pass. 4 | 2. Run `./benchmark/benchmark.js`, make sure that performance not degraded. 5 | 3. DON'T include auto-generated browser files to commit. 6 | 7 | ### Other things: 8 | 9 | 1. Prefer [gitter](https://gitter.im/markdown-it/markdown-it) for short "questions". 10 | Keep issues for bug reports, suggestions and so on. 11 | 2. Make sure to read [dev info](https://github.com/markdown-it/markdown-it/tree/master/docs) 12 | prior to ask about plugins development. 13 | 3. __Provide examples with [demo](https://markdown-it.github.io/) when possible.__ 14 | 4. Issues of "question" type are closed after several days of inactivity, 15 | if not qualified as bug report, enhancement etc (see 1). 16 | -------------------------------------------------------------------------------- /benchmark/samples/inline-links-flat.md: -------------------------------------------------------------------------------- 1 | Valid links: 2 | 3 | [this is a link]() 4 | [this is a link]() 5 | [this is a link](http://something.example.com/foo/bar 'test') 6 | ![this is an image]() 7 | ![this is an image]() 8 | ![this is an image](http://something.example.com/foo/bar 'test') 9 | 10 | [escape test](<\>\>\>\>\>\>\>\>\>\>\>\>\>\>> '\'\'\'\'\'\'\'\'\'\'\'\'\'\'') 11 | [escape test \]\]\]\]\]\]\]\]\]\]\]\]\]\]\]\]](\)\)\)\)\)\)\)\)\)\)\)\)\)\)) 12 | 13 | Invalid links: 14 | 15 | [this is not a link 16 | 17 | [this is not a link]( 18 | 19 | [this is not a link](http://something.example.com/foo/bar 'test' 20 | 21 | [this is not a link]((((((((((((((((((((((((((((((((((((((((((((((( 22 | 23 | [this is not a link]((((((((((()))))))))) (((((((((())))))))))) 24 | -------------------------------------------------------------------------------- /benchmark/samples/block-list-flat.md: -------------------------------------------------------------------------------- 1 | - tidy 2 | - bullet 3 | - list 4 | 5 | 6 | - loose 7 | 8 | - bullet 9 | 10 | - list 11 | 12 | 13 | 0. ordered 14 | 1. list 15 | 2. example 16 | 17 | 18 | - 19 | - 20 | - 21 | - 22 | 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 28 | 29 | - an example 30 | of a list item 31 | with a continuation 32 | 33 | this part is inside the list 34 | 35 | this part is just a paragraph 36 | 37 | 38 | 1. test 39 | - test 40 | 1. test 41 | - test 42 | 43 | 44 | 111111111111111111111111111111111111111111. is this a valid bullet? 45 | 46 | - _________________________ 47 | 48 | - this 49 | - is 50 | 51 | a 52 | 53 | long 54 | - loose 55 | - list 56 | 57 | - with 58 | - some 59 | 60 | tidy 61 | 62 | - list 63 | - items 64 | - in 65 | 66 | - between 67 | - _________________________ 68 | -------------------------------------------------------------------------------- /lib/rules_block/code.js: -------------------------------------------------------------------------------- 1 | // Code block (4 spaces padded) 2 | 3 | 'use strict'; 4 | 5 | 6 | module.exports = function code(state, startLine, endLine/*, silent*/) { 7 | var nextLine, last, token; 8 | 9 | if (state.sCount[startLine] - state.blkIndent < 4) { return false; } 10 | 11 | last = nextLine = startLine + 1; 12 | 13 | while (nextLine < endLine) { 14 | if (state.isEmpty(nextLine)) { 15 | nextLine++; 16 | continue; 17 | } 18 | 19 | if (state.sCount[nextLine] - state.blkIndent >= 4) { 20 | nextLine++; 21 | last = nextLine; 22 | continue; 23 | } 24 | break; 25 | } 26 | 27 | state.line = last; 28 | 29 | token = state.push('code_block', 'code', 0); 30 | token.content = state.getLines(startLine, last, 4 + state.blkIndent, true); 31 | token.map = [ startLine, state.line ]; 32 | 33 | return true; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/rules_inline/text_collapse.js: -------------------------------------------------------------------------------- 1 | // Merge adjacent text nodes into one, and re-calculate all token levels 2 | // 3 | 'use strict'; 4 | 5 | 6 | module.exports = function text_collapse(state) { 7 | var curr, last, 8 | level = 0, 9 | tokens = state.tokens, 10 | max = state.tokens.length; 11 | 12 | for (curr = last = 0; curr < max; curr++) { 13 | // re-calculate levels 14 | level += tokens[curr].nesting; 15 | tokens[curr].level = level; 16 | 17 | if (tokens[curr].type === 'text' && 18 | curr + 1 < max && 19 | tokens[curr + 1].type === 'text') { 20 | 21 | // collapse two adjacent text nodes 22 | tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content; 23 | } else { 24 | if (curr !== last) { tokens[last] = tokens[curr]; } 25 | 26 | last++; 27 | } 28 | } 29 | 30 | if (curr !== last) { 31 | tokens.length = last; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/typographer.txt: -------------------------------------------------------------------------------- 1 | . 2 | (bad) 3 | . 4 |

(bad)

5 | . 6 | 7 | 8 | copyright 9 | . 10 | (c) (C) 11 | . 12 |

© ©

13 | . 14 | 15 | 16 | reserved 17 | . 18 | (r) (R) 19 | . 20 |

® ®

21 | . 22 | 23 | 24 | trademark 25 | . 26 | (tm) (TM) 27 | . 28 |

™ ™

29 | . 30 | 31 | 32 | paragraph 33 | . 34 | (p) (P) 35 | . 36 |

§ §

37 | . 38 | 39 | 40 | plus-minus 41 | . 42 | +-5 43 | . 44 |

±5

45 | . 46 | 47 | 48 | ellipsis 49 | . 50 | test.. test... test..... test?..... test!.... 51 | . 52 |

test… test… test… test?.. test!..

53 | . 54 | 55 | 56 | dupes 57 | . 58 | !!!!!! ???? ,, 59 | . 60 |

!!! ??? ,

61 | . 62 | 63 | 64 | dashes 65 | . 66 | ---markdownit --- super--- 67 | 68 | markdownit---awesome 69 | 70 | abc ---- 71 | 72 | --markdownit -- super-- 73 | 74 | markdownit--awesome 75 | . 76 |

—markdownit — super—

77 |

markdownit—awesome

78 |

abc ----

79 |

–markdownit – super–

80 |

markdownit–awesome

81 | . 82 | -------------------------------------------------------------------------------- /benchmark/samples/block-ref-list.md: -------------------------------------------------------------------------------- 1 | [item 1]: <1> 2 | [item 2]: <2> 3 | [item 3]: <3> 4 | [item 4]: <4> 5 | [item 5]: <5> 6 | [item 6]: <6> 7 | [item 7]: <7> 8 | [item 8]: <8> 9 | [item 9]: <9> 10 | [item 10]: <10> 11 | [item 11]: <11> 12 | [item 12]: <12> 13 | [item 13]: <13> 14 | [item 14]: <14> 15 | [item 15]: <15> 16 | [item 16]: <16> 17 | [item 17]: <17> 18 | [item 18]: <18> 19 | [item 19]: <19> 20 | [item 20]: <20> 21 | [item 21]: <21> 22 | [item 22]: <22> 23 | [item 23]: <23> 24 | [item 24]: <24> 25 | [item 25]: <25> 26 | [item 26]: <26> 27 | [item 27]: <27> 28 | [item 28]: <28> 29 | [item 29]: <29> 30 | [item 30]: <30> 31 | [item 31]: <31> 32 | [item 32]: <32> 33 | [item 33]: <33> 34 | [item 34]: <34> 35 | [item 35]: <35> 36 | [item 36]: <36> 37 | [item 37]: <37> 38 | [item 38]: <38> 39 | [item 39]: <39> 40 | [item 40]: <40> 41 | [item 41]: <41> 42 | [item 42]: <42> 43 | [item 43]: <43> 44 | [item 44]: <44> 45 | [item 45]: <45> 46 | [item 46]: <46> 47 | [item 47]: <47> 48 | [item 48]: <48> 49 | [item 49]: <49> 50 | [item 50]: <50> 51 | -------------------------------------------------------------------------------- /test/commonmark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var p = require('path'); 5 | var load = require('markdown-it-testgen').load; 6 | var assert = require('chai').assert; 7 | 8 | 9 | function normalize(text) { 10 | return text.replace(/
\n<\/blockquote>/g, '
'); 11 | } 12 | 13 | 14 | function generate(path, md) { 15 | load(path, function (data) { 16 | data.meta = data.meta || {}; 17 | 18 | var desc = data.meta.desc || p.relative(path, data.file); 19 | 20 | (data.meta.skip ? describe.skip : describe)(desc, function () { 21 | data.fixtures.forEach(function (fixture) { 22 | it(fixture.header ? fixture.header : 'line ' + (fixture.first.range[0] - 1), function () { 23 | assert.strictEqual(md.render(fixture.first.text), normalize(fixture.second.text)); 24 | }); 25 | }); 26 | }); 27 | }); 28 | } 29 | 30 | 31 | describe('CommonMark', function () { 32 | var md = require('../')('commonmark'); 33 | 34 | generate(p.join(__dirname, 'fixtures/commonmark/good.txt'), md); 35 | }); 36 | -------------------------------------------------------------------------------- /docs/5.0_migration.md: -------------------------------------------------------------------------------- 1 | Migration to v5 2 | =============== 3 | 4 | v5 has the same external API as v4, only internals were changed. Some external 5 | plugins may need update (all plugins from `markdown-it` github organization are 6 | up to date). 7 | 8 | 9 | ## For users 10 | 11 | External API did not change. 12 | 13 | - If you use `markdown-it` with plugins, make sure to update them. 14 | 15 | 16 | ## For plugin developers 17 | 18 | - added `stateBlock.sCount` to calculate indents instead of `stateBlock.tShift`, it only differs if tabs are present: 19 | - `stateBlock.tShift` is used to calculate a number of *characters* (tab is 1 character) 20 | - `stateBlock.sCount` is used to calculate the block *offset* (tab is 1-4 characters depending on position) 21 | - added `stateInline.ruler2` and `stateInline.delimiters` needed to parse emphasis-like markup better 22 | - emphasis-like tags now can't be skipped with `stateInline.skipToken`, they treat a sequence of markers (e.g. `***`) as a token instead 23 | - `stateInline.delimiters` is linked with `stateInline.tokens`, so truncating/splicing `stateInline.tokens` may break things 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/common/html_re.js: -------------------------------------------------------------------------------- 1 | // Regexps to match html elements 2 | 3 | 'use strict'; 4 | 5 | var attr_name = '[a-zA-Z_:][a-zA-Z0-9:._-]*'; 6 | 7 | var unquoted = '[^"\'=<>`\\x00-\\x20]+'; 8 | var single_quoted = "'[^']*'"; 9 | var double_quoted = '"[^"]*"'; 10 | 11 | var attr_value = '(?:' + unquoted + '|' + single_quoted + '|' + double_quoted + ')'; 12 | 13 | var attribute = '(?:\\s+' + attr_name + '(?:\\s*=\\s*' + attr_value + ')?)'; 14 | 15 | var open_tag = '<[A-Za-z][A-Za-z0-9\\-]*' + attribute + '*\\s*\\/?>'; 16 | 17 | var close_tag = '<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>'; 18 | var comment = '|'; 19 | var processing = '<[?].*?[?]>'; 20 | var declaration = ']*>'; 21 | var cdata = ''; 22 | 23 | var HTML_TAG_RE = new RegExp('^(?:' + open_tag + '|' + close_tag + '|' + comment + 24 | '|' + processing + '|' + declaration + '|' + cdata + ')'); 25 | var HTML_OPEN_CLOSE_TAG_RE = new RegExp('^(?:' + open_tag + '|' + close_tag + ')'); 26 | 27 | module.exports.HTML_TAG_RE = HTML_TAG_RE; 28 | module.exports.HTML_OPEN_CLOSE_TAG_RE = HTML_OPEN_CLOSE_TAG_RE; 29 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/fatal.txt: -------------------------------------------------------------------------------- 1 | Should not throw exception on invalid chars in URL (`*` not allowed in path) [mailformed URI] 2 | . 3 | [foo](<%test>) 4 | . 5 |

foo

6 | . 7 | 8 | 9 | Should not throw exception on broken utf-8 sequence in URL [mailformed URI] 10 | . 11 | [foo](%C3) 12 | . 13 |

foo

14 | . 15 | 16 | 17 | Should not throw exception on broken utf-16 surrogates sequence in URL [mailformed URI] 18 | . 19 | [foo](�) 20 | . 21 |

foo

22 | . 23 | 24 | 25 | Should not hang comments regexp 26 | . 27 | foo 30 | . 31 |

foo <!— xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ->

32 |

foo <!------------------------------------------------------------------->

33 | . 34 | 35 | 36 | Should not hang cdata regexp 37 | . 38 | foo 39 | . 40 |

foo <![CDATA[ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ]>

41 | . 42 | -------------------------------------------------------------------------------- /lib/common/html_blocks.js: -------------------------------------------------------------------------------- 1 | // List of valid html blocks names, accorting to commonmark spec 2 | // http://jgm.github.io/CommonMark/spec.html#html-blocks 3 | 4 | 'use strict'; 5 | 6 | 7 | module.exports = [ 8 | 'address', 9 | 'article', 10 | 'aside', 11 | 'base', 12 | 'basefont', 13 | 'blockquote', 14 | 'body', 15 | 'caption', 16 | 'center', 17 | 'col', 18 | 'colgroup', 19 | 'dd', 20 | 'details', 21 | 'dialog', 22 | 'dir', 23 | 'div', 24 | 'dl', 25 | 'dt', 26 | 'fieldset', 27 | 'figcaption', 28 | 'figure', 29 | 'footer', 30 | 'form', 31 | 'frame', 32 | 'frameset', 33 | 'h1', 34 | 'h2', 35 | 'h3', 36 | 'h4', 37 | 'h5', 38 | 'h6', 39 | 'head', 40 | 'header', 41 | 'hr', 42 | 'html', 43 | 'iframe', 44 | 'legend', 45 | 'li', 46 | 'link', 47 | 'main', 48 | 'menu', 49 | 'menuitem', 50 | 'meta', 51 | 'nav', 52 | 'noframes', 53 | 'ol', 54 | 'optgroup', 55 | 'option', 56 | 'p', 57 | 'param', 58 | 'pre', 59 | 'section', 60 | 'source', 61 | 'title', 62 | 'summary', 63 | 'table', 64 | 'tbody', 65 | 'td', 66 | 'tfoot', 67 | 'th', 68 | 'thead', 69 | 'title', 70 | 'tr', 71 | 'track', 72 | 'ul' 73 | ]; 74 | -------------------------------------------------------------------------------- /lib/rules_inline/html_inline.js: -------------------------------------------------------------------------------- 1 | // Process html tags 2 | 3 | 'use strict'; 4 | 5 | 6 | var HTML_TAG_RE = require('../common/html_re').HTML_TAG_RE; 7 | 8 | 9 | function isLetter(ch) { 10 | /*eslint no-bitwise:0*/ 11 | var lc = ch | 0x20; // to lower case 12 | return (lc >= 0x61/* a */) && (lc <= 0x7a/* z */); 13 | } 14 | 15 | 16 | module.exports = function html_inline(state, silent) { 17 | var ch, match, max, token, 18 | pos = state.pos; 19 | 20 | if (!state.md.options.html) { return false; } 21 | 22 | // Check start 23 | max = state.posMax; 24 | if (state.src.charCodeAt(pos) !== 0x3C/* < */ || 25 | pos + 2 >= max) { 26 | return false; 27 | } 28 | 29 | // Quick fail on second char 30 | ch = state.src.charCodeAt(pos + 1); 31 | if (ch !== 0x21/* ! */ && 32 | ch !== 0x3F/* ? */ && 33 | ch !== 0x2F/* / */ && 34 | !isLetter(ch)) { 35 | return false; 36 | } 37 | 38 | match = state.src.slice(pos).match(HTML_TAG_RE); 39 | if (!match) { return false; } 40 | 41 | if (!silent) { 42 | token = state.push('html_inline', '', 0); 43 | token.content = state.src.slice(pos, pos + match[0].length); 44 | } 45 | state.pos += match[0].length; 46 | return true; 47 | }; 48 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_label.js: -------------------------------------------------------------------------------- 1 | // Parse link label 2 | // 3 | // this function assumes that first character ("[") already matches; 4 | // returns the end of the label 5 | // 6 | 'use strict'; 7 | 8 | module.exports = function parseLinkLabel(state, start, disableNested) { 9 | var level, found, marker, prevPos, 10 | labelEnd = -1, 11 | max = state.posMax, 12 | oldPos = state.pos; 13 | 14 | state.pos = start + 1; 15 | level = 1; 16 | 17 | while (state.pos < max) { 18 | marker = state.src.charCodeAt(state.pos); 19 | if (marker === 0x5D /* ] */) { 20 | level--; 21 | if (level === 0) { 22 | found = true; 23 | break; 24 | } 25 | } 26 | 27 | prevPos = state.pos; 28 | state.md.inline.skipToken(state); 29 | if (marker === 0x5B /* [ */) { 30 | if (prevPos === state.pos - 1) { 31 | // increase level if we find text `[`, which is not a part of any token 32 | level++; 33 | } else if (disableNested) { 34 | state.pos = oldPos; 35 | return -1; 36 | } 37 | } 38 | } 39 | 40 | if (found) { 41 | labelEnd = state.pos; 42 | } 43 | 44 | // restore old state 45 | state.pos = oldPos; 46 | 47 | return labelEnd; 48 | }; 49 | -------------------------------------------------------------------------------- /support/babelmark-responder.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-env es6 */ 4 | /* eslint-disable no-console */ 5 | 'use strict'; 6 | 7 | const md = require('../')('commonmark'); 8 | const app = require('express')(); 9 | 10 | const version = require('../package.json').version; 11 | 12 | const banner = ` 13 | 14 | 15 | 16 | markdown-it responder for babelmark 17 | 18 | 19 |

markdown-it 20 | responder for Babelmark2

21 |

Usage: /?text=...

22 | 23 | 24 | `; 25 | 26 | app.set('port', (process.env.PORT || 5000)); 27 | 28 | app.get('/', function (req, res) { 29 | if (typeof req.query.text === 'string') { 30 | res.json({ 31 | name: 'markdown-it', 32 | html: md.render(req.query.text.slice(0, 1000)), 33 | version 34 | }); 35 | return; 36 | } 37 | res.setHeader('Content-Type', 'text/html'); 38 | res.send(banner); 39 | }); 40 | 41 | app.listen(app.get('port'), function () { 42 | console.log(`Node app is running on port ${app.get('port')}`); 43 | }); 44 | -------------------------------------------------------------------------------- /lib/rules_inline/newline.js: -------------------------------------------------------------------------------- 1 | // Proceess '\n' 2 | 3 | 'use strict'; 4 | 5 | var isSpace = require('../common/utils').isSpace; 6 | 7 | 8 | module.exports = function newline(state, silent) { 9 | var pmax, max, pos = state.pos; 10 | 11 | if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } 12 | 13 | pmax = state.pending.length - 1; 14 | max = state.posMax; 15 | 16 | // ' \n' -> hardbreak 17 | // Lookup in pending chars is bad practice! Don't copy to other rules! 18 | // Pending string is stored in concat mode, indexed lookups will cause 19 | // convertion to flat mode. 20 | if (!silent) { 21 | if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { 22 | if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { 23 | state.pending = state.pending.replace(/ +$/, ''); 24 | state.push('hardbreak', 'br', 0); 25 | } else { 26 | state.pending = state.pending.slice(0, -1); 27 | state.push('softbreak', 'br', 0); 28 | } 29 | 30 | } else { 31 | state.push('softbreak', 'br', 0); 32 | } 33 | } 34 | 35 | pos++; 36 | 37 | // skip heading spaces for next line 38 | while (pos < max && isSpace(state.src.charCodeAt(pos))) { pos++; } 39 | 40 | state.pos = pos; 41 | return true; 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/linkify.txt: -------------------------------------------------------------------------------- 1 | linkify 2 | . 3 | url http://www.youtube.com/watch?v=5Jt5GEr4AYg. 4 | . 5 |

url http://www.youtube.com/watch?v=5Jt5GEr4AYg.

6 | . 7 | 8 | 9 | don't touch text in links 10 | . 11 | [https://example.com](https://example.com) 12 | . 13 |

https://example.com

14 | . 15 | 16 | 17 | don't touch text in autolinks 18 | . 19 | 20 | . 21 |

https://example.com

22 | . 23 | 24 | 25 | don't touch text in html tags 26 | . 27 | https://example.com 28 | . 29 |

https://example.com

30 | . 31 | 32 | 33 | match links without protocol 34 | . 35 | www.example.org 36 | . 37 |

www.example.org

38 | . 39 | 40 | 41 | emails 42 | . 43 | test@example.com 44 | 45 | mailto:test@example.com 46 | . 47 |

test@example.com

48 |

mailto:test@example.com

49 | . 50 | 51 | 52 | typorgapher should not break href 53 | . 54 | http://example.com/(c) 55 | . 56 |

http://example.com/(c)

57 | . 58 | -------------------------------------------------------------------------------- /lib/rules_inline/backticks.js: -------------------------------------------------------------------------------- 1 | // Parse backticks 2 | 3 | 'use strict'; 4 | 5 | module.exports = function backtick(state, silent) { 6 | var start, max, marker, matchStart, matchEnd, token, 7 | pos = state.pos, 8 | ch = state.src.charCodeAt(pos); 9 | 10 | if (ch !== 0x60/* ` */) { return false; } 11 | 12 | start = pos; 13 | pos++; 14 | max = state.posMax; 15 | 16 | while (pos < max && state.src.charCodeAt(pos) === 0x60/* ` */) { pos++; } 17 | 18 | marker = state.src.slice(start, pos); 19 | 20 | matchStart = matchEnd = pos; 21 | 22 | while ((matchStart = state.src.indexOf('`', matchEnd)) !== -1) { 23 | matchEnd = matchStart + 1; 24 | 25 | while (matchEnd < max && state.src.charCodeAt(matchEnd) === 0x60/* ` */) { matchEnd++; } 26 | 27 | if (matchEnd - matchStart === marker.length) { 28 | if (!silent) { 29 | token = state.push('code_inline', 'code', 0); 30 | token.markup = marker; 31 | token.content = state.src.slice(pos, matchStart) 32 | .replace(/[ \n]+/g, ' ') 33 | .trim(); 34 | } 35 | state.pos = matchEnd; 36 | return true; 37 | } 38 | } 39 | 40 | if (!silent) { state.pending += marker; } 41 | state.pos += marker.length; 42 | return true; 43 | }; 44 | -------------------------------------------------------------------------------- /lib/rules_inline/escape.js: -------------------------------------------------------------------------------- 1 | // Proceess escaped chars and hardbreaks 2 | 3 | 'use strict'; 4 | 5 | var isSpace = require('../common/utils').isSpace; 6 | 7 | var ESCAPED = []; 8 | 9 | for (var i = 0; i < 256; i++) { ESCAPED.push(0); } 10 | 11 | '\\!"#$%&\'()*+,./:;<=>?@[]^_`{|}~-' 12 | .split('').forEach(function (ch) { ESCAPED[ch.charCodeAt(0)] = 1; }); 13 | 14 | 15 | module.exports = function escape(state, silent) { 16 | var ch, pos = state.pos, max = state.posMax; 17 | 18 | if (state.src.charCodeAt(pos) !== 0x5C/* \ */) { return false; } 19 | 20 | pos++; 21 | 22 | if (pos < max) { 23 | ch = state.src.charCodeAt(pos); 24 | 25 | if (ch < 256 && ESCAPED[ch] !== 0) { 26 | if (!silent) { state.pending += state.src[pos]; } 27 | state.pos += 2; 28 | return true; 29 | } 30 | 31 | if (ch === 0x0A) { 32 | if (!silent) { 33 | state.push('hardbreak', 'br', 0); 34 | } 35 | 36 | pos++; 37 | // skip leading whitespaces from next line 38 | while (pos < max) { 39 | ch = state.src.charCodeAt(pos); 40 | if (!isSpace(ch)) { break; } 41 | pos++; 42 | } 43 | 44 | state.pos = pos; 45 | return true; 46 | } 47 | } 48 | 49 | if (!silent) { state.pending += '\\'; } 50 | state.pos++; 51 | return true; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_title.js: -------------------------------------------------------------------------------- 1 | // Parse link title 2 | // 3 | 'use strict'; 4 | 5 | 6 | var unescapeAll = require('../common/utils').unescapeAll; 7 | 8 | 9 | module.exports = function parseLinkTitle(str, pos, max) { 10 | var code, 11 | marker, 12 | lines = 0, 13 | start = pos, 14 | result = { 15 | ok: false, 16 | pos: 0, 17 | lines: 0, 18 | str: '' 19 | }; 20 | 21 | if (pos >= max) { return result; } 22 | 23 | marker = str.charCodeAt(pos); 24 | 25 | if (marker !== 0x22 /* " */ && marker !== 0x27 /* ' */ && marker !== 0x28 /* ( */) { return result; } 26 | 27 | pos++; 28 | 29 | // if opening marker is "(", switch it to closing marker ")" 30 | if (marker === 0x28) { marker = 0x29; } 31 | 32 | while (pos < max) { 33 | code = str.charCodeAt(pos); 34 | if (code === marker) { 35 | result.pos = pos + 1; 36 | result.lines = lines; 37 | result.str = unescapeAll(str.slice(start + 1, pos)); 38 | result.ok = true; 39 | return result; 40 | } else if (code === 0x0A) { 41 | lines++; 42 | } else if (code === 0x5C /* \ */ && pos + 1 < max) { 43 | pos++; 44 | if (str.charCodeAt(pos) === 0x0A) { 45 | lines++; 46 | } 47 | } 48 | 49 | pos++; 50 | } 51 | 52 | return result; 53 | }; 54 | -------------------------------------------------------------------------------- /lib/rules_block/hr.js: -------------------------------------------------------------------------------- 1 | // Horizontal rule 2 | 3 | 'use strict'; 4 | 5 | var isSpace = require('../common/utils').isSpace; 6 | 7 | 8 | module.exports = function hr(state, startLine, endLine, silent) { 9 | var marker, cnt, ch, token, 10 | pos = state.bMarks[startLine] + state.tShift[startLine], 11 | max = state.eMarks[startLine]; 12 | 13 | // if it's indented more than 3 spaces, it should be a code block 14 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } 15 | 16 | marker = state.src.charCodeAt(pos++); 17 | 18 | // Check hr marker 19 | if (marker !== 0x2A/* * */ && 20 | marker !== 0x2D/* - */ && 21 | marker !== 0x5F/* _ */) { 22 | return false; 23 | } 24 | 25 | // markers can be mixed with spaces, but there should be at least 3 of them 26 | 27 | cnt = 1; 28 | while (pos < max) { 29 | ch = state.src.charCodeAt(pos++); 30 | if (ch !== marker && !isSpace(ch)) { return false; } 31 | if (ch === marker) { cnt++; } 32 | } 33 | 34 | if (cnt < 3) { return false; } 35 | 36 | if (silent) { return true; } 37 | 38 | state.line = startLine + 1; 39 | 40 | token = state.push('hr', 'hr', 0); 41 | token.map = [ startLine, state.line ]; 42 | token.markup = Array(cnt + 1).join(String.fromCharCode(marker)); 43 | 44 | return true; 45 | }; 46 | -------------------------------------------------------------------------------- /lib/rules_inline/balance_pairs.js: -------------------------------------------------------------------------------- 1 | // For each opening emphasis-like marker find a matching closing one 2 | // 3 | 'use strict'; 4 | 5 | 6 | module.exports = function link_pairs(state) { 7 | var i, j, lastDelim, currDelim, 8 | delimiters = state.delimiters, 9 | max = state.delimiters.length; 10 | 11 | for (i = 0; i < max; i++) { 12 | lastDelim = delimiters[i]; 13 | 14 | if (!lastDelim.close) { continue; } 15 | 16 | j = i - lastDelim.jump - 1; 17 | 18 | while (j >= 0) { 19 | currDelim = delimiters[j]; 20 | 21 | if (currDelim.open && 22 | currDelim.marker === lastDelim.marker && 23 | currDelim.end < 0 && 24 | currDelim.level === lastDelim.level) { 25 | 26 | // typeofs are for backward compatibility with plugins 27 | var odd_match = (currDelim.close || lastDelim.open) && 28 | typeof currDelim.length !== 'undefined' && 29 | typeof lastDelim.length !== 'undefined' && 30 | (currDelim.length + lastDelim.length) % 3 === 0; 31 | 32 | if (!odd_match) { 33 | lastDelim.jump = i - j; 34 | lastDelim.open = false; 35 | currDelim.end = i; 36 | currDelim.jump = 0; 37 | break; 38 | } 39 | } 40 | 41 | j -= currDelim.jump + 1; 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /lib/parser_core.js: -------------------------------------------------------------------------------- 1 | /** internal 2 | * class Core 3 | * 4 | * Top-level rules executor. Glues block/inline parsers and does intermediate 5 | * transformations. 6 | **/ 7 | 'use strict'; 8 | 9 | 10 | var Ruler = require('./ruler'); 11 | 12 | 13 | var _rules = [ 14 | [ 'normalize', require('./rules_core/normalize') ], 15 | [ 'block', require('./rules_core/block') ], 16 | [ 'inline', require('./rules_core/inline') ], 17 | [ 'linkify', require('./rules_core/linkify') ], 18 | [ 'replacements', require('./rules_core/replacements') ], 19 | [ 'smartquotes', require('./rules_core/smartquotes') ] 20 | ]; 21 | 22 | 23 | /** 24 | * new Core() 25 | **/ 26 | function Core() { 27 | /** 28 | * Core#ruler -> Ruler 29 | * 30 | * [[Ruler]] instance. Keep configuration of core rules. 31 | **/ 32 | this.ruler = new Ruler(); 33 | 34 | for (var i = 0; i < _rules.length; i++) { 35 | this.ruler.push(_rules[i][0], _rules[i][1]); 36 | } 37 | } 38 | 39 | 40 | /** 41 | * Core.process(state) 42 | * 43 | * Executes core chain rules. 44 | **/ 45 | Core.prototype.process = function (state) { 46 | var i, l, rules; 47 | 48 | rules = this.ruler.getRules(''); 49 | 50 | for (i = 0, l = rules.length; i < l; i++) { 51 | rules[i](state); 52 | } 53 | }; 54 | 55 | Core.prototype.State = require('./rules_core/state_core'); 56 | 57 | 58 | module.exports = Core; 59 | -------------------------------------------------------------------------------- /docs/4.0_migration.md: -------------------------------------------------------------------------------- 1 | Migration to v4 2 | =============== 3 | 4 | v4 has the same external API as v3, but significantly changed internals. Plugin 5 | authors should update their packages. 6 | 7 | ## For users 8 | 9 | External API did not change. 10 | 11 | - If you used `markdown-it` with plugins - make sure to update those. 12 | - If you modified renderer - see dev info below. 13 | - If you did not use plugins and renderer modification - no changes needed. 14 | 15 | 16 | ## For developers 17 | 18 | ### Tokens and renderer 19 | 20 | - [Tokens](https://github.com/markdown-it/markdown-it/blob/master/lib/token.js) 21 | are now classes, and allow arbitrary attributes. 22 | - new tokens are created with `token = state.push(type, tag, nesting)`. 23 | See [this commit](https://github.com/markdown-it/markdown-it/commit/4aabd5592ea55fb43d6a215b316c89c6f6f1f7db) to understand 24 | how to create tokens in new way. Also see changes in plugins from other 25 | repos in this org. 26 | - [Renderer](https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.js) 27 | methods were unified. Number of custom renderer rules were significantly reduced. 28 | Custom renderer functions need update due to tokens format change. 29 | 30 | ### Other changes 31 | 32 | - `.validateUrl()` -> moved to root class `.validateLink()` 33 | - added `.normalizeLink()` & `.normalizeLinkText()` to root class, and removed 34 | `normalizeUrl()` from utils. 35 | - removed `replaceEntities()` in `utils`. 36 | -------------------------------------------------------------------------------- /lib/presets/default.js: -------------------------------------------------------------------------------- 1 | // markdown-it default options 2 | 3 | 'use strict'; 4 | 5 | 6 | module.exports = { 7 | options: { 8 | html: false, // Enable HTML tags in source 9 | xhtmlOut: false, // Use '/' to close single tags (
) 10 | breaks: false, // Convert '\n' in paragraphs into
11 | langPrefix: 'language-', // CSS language prefix for fenced blocks 12 | linkify: false, // autoconvert URL-like texts to links 13 | 14 | // Enable some language-neutral replacements + quotes beautification 15 | typographer: false, 16 | 17 | // Double + single quotes replacement pairs, when typographer enabled, 18 | // and smartquotes on. Could be either a String or an Array. 19 | // 20 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 21 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 22 | quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */ 23 | 24 | // Highlighter function. Should return escaped HTML, 25 | // or '' if the source string is not changed and should be escaped externaly. 26 | // If result starts with = 4) { return false; } 15 | 16 | ch = state.src.charCodeAt(pos); 17 | 18 | if (ch !== 0x23/* # */ || pos >= max) { return false; } 19 | 20 | // count heading level 21 | level = 1; 22 | ch = state.src.charCodeAt(++pos); 23 | while (ch === 0x23/* # */ && pos < max && level <= 6) { 24 | level++; 25 | ch = state.src.charCodeAt(++pos); 26 | } 27 | 28 | if (level > 6 || (pos < max && !isSpace(ch))) { return false; } 29 | 30 | if (silent) { return true; } 31 | 32 | // Let's cut tails like ' ### ' from the end of string 33 | 34 | max = state.skipSpacesBack(max, pos); 35 | tmp = state.skipCharsBack(max, 0x23, pos); // # 36 | if (tmp > pos && isSpace(state.src.charCodeAt(tmp - 1))) { 37 | max = tmp; 38 | } 39 | 40 | state.line = startLine + 1; 41 | 42 | token = state.push('heading_open', 'h' + String(level), 1); 43 | token.markup = '########'.slice(0, level); 44 | token.map = [ startLine, state.line ]; 45 | 46 | token = state.push('inline', '', 0); 47 | token.content = state.src.slice(pos, max).trim(); 48 | token.map = [ startLine, state.line ]; 49 | token.children = []; 50 | 51 | token = state.push('heading_close', 'h' + String(level), -1); 52 | token.markup = '########'.slice(0, level); 53 | 54 | return true; 55 | }; 56 | -------------------------------------------------------------------------------- /lib/rules_block/paragraph.js: -------------------------------------------------------------------------------- 1 | // Paragraph 2 | 3 | 'use strict'; 4 | 5 | 6 | module.exports = function paragraph(state, startLine/*, endLine*/) { 7 | var content, terminate, i, l, token, oldParentType, 8 | nextLine = startLine + 1, 9 | terminatorRules = state.md.block.ruler.getRules('paragraph'), 10 | endLine = state.lineMax; 11 | 12 | oldParentType = state.parentType; 13 | state.parentType = 'paragraph'; 14 | 15 | // jump line-by-line until empty one or EOF 16 | for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { 17 | // this would be a code block normally, but after paragraph 18 | // it's considered a lazy continuation regardless of what's there 19 | if (state.sCount[nextLine] - state.blkIndent > 3) { continue; } 20 | 21 | // quirk for blockquotes, this line should already be checked by that rule 22 | if (state.sCount[nextLine] < 0) { continue; } 23 | 24 | // Some tags can terminate paragraph without empty line. 25 | terminate = false; 26 | for (i = 0, l = terminatorRules.length; i < l; i++) { 27 | if (terminatorRules[i](state, nextLine, endLine, true)) { 28 | terminate = true; 29 | break; 30 | } 31 | } 32 | if (terminate) { break; } 33 | } 34 | 35 | content = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); 36 | 37 | state.line = nextLine; 38 | 39 | token = state.push('paragraph_open', 'p', 1); 40 | token.map = [ startLine, state.line ]; 41 | 42 | token = state.push('inline', '', 0); 43 | token.content = content; 44 | token.map = [ startLine, state.line ]; 45 | token.children = []; 46 | 47 | token = state.push('paragraph_close', 'p', -1); 48 | 49 | state.parentType = oldParentType; 50 | 51 | return true; 52 | }; 53 | -------------------------------------------------------------------------------- /support/markdown-it.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console:0*/ 3 | 4 | 'use strict'; 5 | 6 | 7 | var fs = require('fs'); 8 | var argparse = require('argparse'); 9 | 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | 13 | var cli = new argparse.ArgumentParser({ 14 | prog: 'markdown-it.js', 15 | version: require('../package.json').version, 16 | addHelp: true 17 | }); 18 | 19 | cli.addArgument([ 'file' ], { 20 | help: 'File to read', 21 | nargs: '?', 22 | defaultValue: '-' 23 | }); 24 | 25 | var options = cli.parseArgs(); 26 | 27 | 28 | function readFile(filename, encoding, callback) { 29 | if (options.file === '-') { 30 | // read from stdin 31 | 32 | var chunks = []; 33 | 34 | process.stdin.on('data', function (chunk) { 35 | chunks.push(chunk); 36 | }); 37 | 38 | process.stdin.on('end', function () { 39 | return callback(null, Buffer.concat(chunks).toString(encoding)); 40 | }); 41 | } else { 42 | fs.readFile(filename, encoding, callback); 43 | } 44 | } 45 | 46 | 47 | //////////////////////////////////////////////////////////////////////////////// 48 | 49 | readFile(options.file, 'utf8', function (err, input) { 50 | var output, md; 51 | 52 | if (err) { 53 | if (err.code === 'ENOENT') { 54 | console.error('File not found: ' + options.file); 55 | process.exit(2); 56 | } 57 | 58 | console.error(err.stack || err.message || String(err)); 59 | 60 | process.exit(1); 61 | } 62 | 63 | md = require('..')({ 64 | html: true, 65 | xhtmlOut: true, 66 | typographer: true, 67 | linkify: true 68 | }); 69 | 70 | try { 71 | output = md.render(input); 72 | 73 | } catch (e) { 74 | console.error(e.stack || e.message || String(e)); 75 | 76 | process.exit(1); 77 | } 78 | 79 | process.stdout.write(output); 80 | 81 | process.exit(0); 82 | }); 83 | -------------------------------------------------------------------------------- /lib/presets/zero.js: -------------------------------------------------------------------------------- 1 | // "Zero" preset, with nothing enabled. Useful for manual configuring of simple 2 | // modes. For example, to parse bold/italic only. 3 | 4 | 'use strict'; 5 | 6 | 7 | module.exports = { 8 | options: { 9 | html: false, // Enable HTML tags in source 10 | xhtmlOut: false, // Use '/' to close single tags (
) 11 | breaks: false, // Convert '\n' in paragraphs into
12 | langPrefix: 'language-', // CSS language prefix for fenced blocks 13 | linkify: false, // autoconvert URL-like texts to links 14 | 15 | // Enable some language-neutral replacements + quotes beautification 16 | typographer: false, 17 | 18 | // Double + single quotes replacement pairs, when typographer enabled, 19 | // and smartquotes on. Could be either a String or an Array. 20 | // 21 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 22 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 23 | quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */ 24 | 25 | // Highlighter function. Should return escaped HTML, 26 | // or '' if the source string is not changed and should be escaped externaly. 27 | // If result starts with */) { 27 | result.pos = pos + 1; 28 | result.str = unescapeAll(str.slice(start + 1, pos)); 29 | result.ok = true; 30 | return result; 31 | } 32 | if (code === 0x5C /* \ */ && pos + 1 < max) { 33 | pos += 2; 34 | continue; 35 | } 36 | 37 | pos++; 38 | } 39 | 40 | // no closing '>' 41 | return result; 42 | } 43 | 44 | // this should be ... } else { ... branch 45 | 46 | level = 0; 47 | while (pos < max) { 48 | code = str.charCodeAt(pos); 49 | 50 | if (code === 0x20) { break; } 51 | 52 | // ascii control characters 53 | if (code < 0x20 || code === 0x7F) { break; } 54 | 55 | if (code === 0x5C /* \ */ && pos + 1 < max) { 56 | pos += 2; 57 | continue; 58 | } 59 | 60 | if (code === 0x28 /* ( */) { 61 | level++; 62 | if (level > 1) { break; } 63 | } 64 | 65 | if (code === 0x29 /* ) */) { 66 | level--; 67 | if (level < 0) { break; } 68 | } 69 | 70 | pos++; 71 | } 72 | 73 | if (start === pos) { return result; } 74 | 75 | result.str = unescapeAll(str.slice(start, pos)); 76 | result.lines = lines; 77 | result.pos = pos; 78 | result.ok = true; 79 | return result; 80 | }; 81 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/strikethrough.txt: -------------------------------------------------------------------------------- 1 | . 2 | ~~Strikeout~~ 3 | . 4 |

Strikeout

5 | . 6 | 7 | . 8 | x ~~~~foo~~ bar~~ 9 | . 10 |

x foo bar

11 | . 12 | 13 | . 14 | x ~~foo ~~bar~~~~ 15 | . 16 |

x foo bar

17 | . 18 | 19 | . 20 | x ~~~~foo~~~~ 21 | . 22 |

x foo

23 | . 24 | 25 | . 26 | x ~~a ~~foo~~~~~~~~~~~bar~~ b~~ 27 | 28 | x ~~a ~~foo~~~~~~~~~~~~bar~~ b~~ 29 | . 30 |

x a foo~~~bar b

31 |

x a foo~~~~bar b

32 | . 33 | 34 | 35 | Strikeouts have the same priority as emphases: 36 | . 37 | **~~test**~~ 38 | 39 | ~~**test~~** 40 | . 41 |

~~test~~

42 |

**test**

43 | . 44 | 45 | 46 | Strikeouts have the same priority as emphases with respect to links: 47 | . 48 | [~~link]()~~ 49 | 50 | ~~[link~~]() 51 | . 52 |

~~link~~

53 |

~~link~~

54 | . 55 | 56 | 57 | Strikeouts have the same priority as emphases with respect to backticks: 58 | . 59 | ~~`code~~` 60 | 61 | `~~code`~~ 62 | . 63 |

~~code~~

64 |

~~code~~

65 | . 66 | 67 | 68 | Nested strikeouts: 69 | . 70 | ~~foo ~~bar~~ baz~~ 71 | 72 | ~~f **o ~~o b~~ a** r~~ 73 | . 74 |

foo bar baz

75 |

f o o b a r

76 | . 77 | 78 | 79 | Should not have a whitespace between text and "~~": 80 | . 81 | foo ~~ bar ~~ baz 82 | . 83 |

foo ~~ bar ~~ baz

84 | . 85 | 86 | 87 | Newline should be considered a whitespace: 88 | . 89 | ~~test 90 | ~~ 91 | 92 | ~~ 93 | test~~ 94 | 95 | ~~ 96 | test 97 | ~~ 98 | . 99 |

~~test 100 | ~~

101 |

~~ 102 | test~~

103 |

~~ 104 | test 105 | ~~

106 | . 107 | 108 | From CommonMark test suite, replacing `**` with our marker: 109 | 110 | . 111 | a~~"foo"~~ 112 | . 113 |

a~~“foo”~~

114 | . 115 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/normalize.txt: -------------------------------------------------------------------------------- 1 | 2 | Encode link destination, decode text inside it: 3 | 4 | . 5 | 6 | . 7 |

http://example.com/αβγδ

8 | . 9 | 10 | . 11 | [foo](http://example.com/α%CE%B2γ%CE%B4) 12 | . 13 |

foo

14 | . 15 | 16 | Should decode punycode: 17 | 18 | . 19 | 20 | . 21 |

http://☃.net/

22 | . 23 | 24 | . 25 | 26 | . 27 |

http://☃.net/

28 | . 29 | 30 | Invalid punycode: 31 | 32 | . 33 | 34 | . 35 |

http://xn--xn.com/

36 | . 37 | 38 | Invalid punycode (non-ascii): 39 | 40 | . 41 | 42 | . 43 |

http://xn--γ.com/

44 | . 45 | 46 | Two slashes should start a domain: 47 | 48 | . 49 | [](//☃.net/) 50 | . 51 |

52 | . 53 | 54 | Don't encode domains in unknown schemas: 55 | 56 | . 57 | [](skype:γγγ) 58 | . 59 |

60 | . 61 | 62 | Should auto-add protocol to autolinks: 63 | 64 | . 65 | test google.com foo 66 | . 67 |

test google.com foo

68 | . 69 | 70 | Should support IDN in autolinks: 71 | 72 | . 73 | test http://xn--n3h.net/ foo 74 | . 75 |

test http://☃.net/ foo

76 | . 77 | 78 | . 79 | test http://☃.net/ foo 80 | . 81 |

test http://☃.net/ foo

82 | . 83 | 84 | . 85 | test //xn--n3h.net/ foo 86 | . 87 |

test //☃.net/ foo

88 | . 89 | 90 | . 91 | test xn--n3h.net foo 92 | . 93 |

test ☃.net foo

94 | . 95 | 96 | . 97 | test xn--n3h@xn--n3h.net foo 98 | . 99 |

test xn--n3h@☃.net foo

100 | . 101 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/smartquotes.txt: -------------------------------------------------------------------------------- 1 | Should parse nested quotes: 2 | . 3 | "foo 'bar' baz" 4 | 5 | 'foo 'bar' baz' 6 | . 7 |

“foo ‘bar’ baz”

8 |

‘foo ‘bar’ baz’

9 | . 10 | 11 | 12 | Should not overlap quotes: 13 | . 14 | 'foo "bar' baz" 15 | . 16 |

‘foo "bar’ baz"

17 | . 18 | 19 | 20 | Should match quotes on the same level: 21 | . 22 | "foo *bar* baz" 23 | . 24 |

“foo bar baz”

25 | . 26 | 27 | 28 | Should handle adjacent nested quotes: 29 | . 30 | '"double in single"' 31 | 32 | "'single in double'" 33 | . 34 |

‘“double in single”’

35 |

“‘single in double’”

36 | . 37 | 38 | 39 | 40 | Should not match quotes on different levels: 41 | . 42 | *"foo* bar" 43 | 44 | "foo *bar"* 45 | 46 | *"foo* bar *baz"* 47 | . 48 |

"foo bar"

49 |

"foo bar"

50 |

"foo bar baz"

51 | . 52 | 53 | Smartquotes should not overlap with other tags: 54 | . 55 | *foo "bar* *baz" quux* 56 | . 57 |

foo "bar baz" quux

58 | . 59 | 60 | 61 | Should try and find matching quote in this case: 62 | . 63 | "foo "bar 'baz" 64 | . 65 |

"foo “bar 'baz”

66 | . 67 | 68 | 69 | Should not touch 'inches' in quotes: 70 | . 71 | "Monitor 21"" and "Monitor"" 72 | . 73 |

“Monitor 21"” and “Monitor”"

74 | . 75 | 76 | 77 | Should render an apostrophe as a rsquo: 78 | . 79 | This isn't and can't be the best approach to implement this... 80 | . 81 |

This isn’t and can’t be the best approach to implement this…

82 | . 83 | 84 | 85 | Apostrophe could end the word, that's why original smartypants replaces all of them as rsquo: 86 | . 87 | users' stuff 88 | . 89 |

users’ stuff

90 | . 91 | 92 | Quotes between punctuation chars: 93 | 94 | . 95 | "(hai)". 96 | . 97 |

“(hai)”.

98 | . 99 | 100 | Quotes at the start/end of the tokens: 101 | . 102 | "*foo* bar" 103 | 104 | "foo *bar*" 105 | 106 | "*foo bar*" 107 | . 108 |

foo bar”

109 |

“foo bar

110 |

foo bar

111 | . 112 | -------------------------------------------------------------------------------- /lib/presets/commonmark.js: -------------------------------------------------------------------------------- 1 | // Commonmark default options 2 | 3 | 'use strict'; 4 | 5 | 6 | module.exports = { 7 | options: { 8 | html: true, // Enable HTML tags in source 9 | xhtmlOut: true, // Use '/' to close single tags (
) 10 | breaks: false, // Convert '\n' in paragraphs into
11 | langPrefix: 'language-', // CSS language prefix for fenced blocks 12 | linkify: false, // autoconvert URL-like texts to links 13 | 14 | // Enable some language-neutral replacements + quotes beautification 15 | typographer: false, 16 | 17 | // Double + single quotes replacement pairs, when typographer enabled, 18 | // and smartquotes on. Could be either a String or an Array. 19 | // 20 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 21 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 22 | quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */ 23 | 24 | // Highlighter function. Should return escaped HTML, 25 | // or '' if the source string is not changed and should be escaped externaly. 26 | // If result starts with ' 2 | 3 | 'use strict'; 4 | 5 | 6 | /*eslint max-len:0*/ 7 | var EMAIL_RE = /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/; 8 | var AUTOLINK_RE = /^<([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)>/; 9 | 10 | 11 | module.exports = function autolink(state, silent) { 12 | var tail, linkMatch, emailMatch, url, fullUrl, token, 13 | pos = state.pos; 14 | 15 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } 16 | 17 | tail = state.src.slice(pos); 18 | 19 | if (tail.indexOf('>') < 0) { return false; } 20 | 21 | if (AUTOLINK_RE.test(tail)) { 22 | linkMatch = tail.match(AUTOLINK_RE); 23 | 24 | url = linkMatch[0].slice(1, -1); 25 | fullUrl = state.md.normalizeLink(url); 26 | if (!state.md.validateLink(fullUrl)) { return false; } 27 | 28 | if (!silent) { 29 | token = state.push('link_open', 'a', 1); 30 | token.attrs = [ [ 'href', fullUrl ] ]; 31 | token.markup = 'autolink'; 32 | token.info = 'auto'; 33 | 34 | token = state.push('text', '', 0); 35 | token.content = state.md.normalizeLinkText(url); 36 | 37 | token = state.push('link_close', 'a', -1); 38 | token.markup = 'autolink'; 39 | token.info = 'auto'; 40 | } 41 | 42 | state.pos += linkMatch[0].length; 43 | return true; 44 | } 45 | 46 | if (EMAIL_RE.test(tail)) { 47 | emailMatch = tail.match(EMAIL_RE); 48 | 49 | url = emailMatch[0].slice(1, -1); 50 | fullUrl = state.md.normalizeLink('mailto:' + url); 51 | if (!state.md.validateLink(fullUrl)) { return false; } 52 | 53 | if (!silent) { 54 | token = state.push('link_open', 'a', 1); 55 | token.attrs = [ [ 'href', fullUrl ] ]; 56 | token.markup = 'autolink'; 57 | token.info = 'auto'; 58 | 59 | token = state.push('text', '', 0); 60 | token.content = state.md.normalizeLinkText(url); 61 | 62 | token = state.push('link_close', 'a', -1); 63 | token.markup = 'autolink'; 64 | token.info = 'auto'; 65 | } 66 | 67 | state.pos += emailMatch[0].length; 68 | return true; 69 | } 70 | 71 | return false; 72 | }; 73 | -------------------------------------------------------------------------------- /lib/rules_inline/text.js: -------------------------------------------------------------------------------- 1 | // Skip text characters for text token, place those to pending buffer 2 | // and increment current pos 3 | 4 | 'use strict'; 5 | 6 | 7 | // Rule to skip pure text 8 | // '{}$%@~+=:' reserved for extentions 9 | 10 | // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, _, `, {, |, }, or ~ 11 | 12 | // !!!! Don't confuse with "Markdown ASCII Punctuation" chars 13 | // http://spec.commonmark.org/0.15/#ascii-punctuation-character 14 | function isTerminatorChar(ch) { 15 | switch (ch) { 16 | case 0x0A/* \n */: 17 | case 0x21/* ! */: 18 | case 0x23/* # */: 19 | case 0x24/* $ */: 20 | case 0x25/* % */: 21 | case 0x26/* & */: 22 | case 0x2A/* * */: 23 | case 0x2B/* + */: 24 | case 0x2D/* - */: 25 | case 0x3A/* : */: 26 | case 0x3C/* < */: 27 | case 0x3D/* = */: 28 | case 0x3E/* > */: 29 | case 0x40/* @ */: 30 | case 0x5B/* [ */: 31 | case 0x5C/* \ */: 32 | case 0x5D/* ] */: 33 | case 0x5E/* ^ */: 34 | case 0x5F/* _ */: 35 | case 0x60/* ` */: 36 | case 0x7B/* { */: 37 | case 0x7D/* } */: 38 | case 0x7E/* ~ */: 39 | return true; 40 | default: 41 | return false; 42 | } 43 | } 44 | 45 | module.exports = function text(state, silent) { 46 | var pos = state.pos; 47 | 48 | while (pos < state.posMax && !isTerminatorChar(state.src.charCodeAt(pos))) { 49 | pos++; 50 | } 51 | 52 | if (pos === state.pos) { return false; } 53 | 54 | if (!silent) { state.pending += state.src.slice(state.pos, pos); } 55 | 56 | state.pos = pos; 57 | 58 | return true; 59 | }; 60 | 61 | // Alternative implementation, for memory. 62 | // 63 | // It costs 10% of performance, but allows extend terminators list, if place it 64 | // to `ParcerInline` property. Probably, will switch to it sometime, such 65 | // flexibility required. 66 | 67 | /* 68 | var TERMINATOR_RE = /[\n!#$%&*+\-:<=>@[\\\]^_`{}~]/; 69 | 70 | module.exports = function text(state, silent) { 71 | var pos = state.pos, 72 | idx = state.src.slice(pos).search(TERMINATOR_RE); 73 | 74 | // first char is terminator -> empty text 75 | if (idx === 0) { return false; } 76 | 77 | // no terminator -> text till end of string 78 | if (idx < 0) { 79 | if (!silent) { state.pending += state.src.slice(pos); } 80 | state.pos = state.src.length; 81 | return true; 82 | } 83 | 84 | if (!silent) { state.pending += state.src.slice(pos, pos + idx); } 85 | 86 | state.pos += idx; 87 | 88 | return true; 89 | };*/ 90 | -------------------------------------------------------------------------------- /lib/rules_block/html_block.js: -------------------------------------------------------------------------------- 1 | // HTML block 2 | 3 | 'use strict'; 4 | 5 | 6 | var block_names = require('../common/html_blocks'); 7 | var HTML_OPEN_CLOSE_TAG_RE = require('../common/html_re').HTML_OPEN_CLOSE_TAG_RE; 8 | 9 | // An array of opening and corresponding closing sequences for html tags, 10 | // last argument defines whether it can terminate a paragraph or not 11 | // 12 | var HTML_SEQUENCES = [ 13 | [ /^<(script|pre|style)(?=(\s|>|$))/i, /<\/(script|pre|style)>/i, true ], 14 | [ /^/, true ], 15 | [ /^<\?/, /\?>/, true ], 16 | [ /^/, true ], 17 | [ /^/, true ], 18 | [ new RegExp('^|$))', 'i'), /^$/, true ], 19 | [ new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*$'), /^$/, false ] 20 | ]; 21 | 22 | 23 | module.exports = function html_block(state, startLine, endLine, silent) { 24 | var i, nextLine, token, lineText, 25 | pos = state.bMarks[startLine] + state.tShift[startLine], 26 | max = state.eMarks[startLine]; 27 | 28 | // if it's indented more than 3 spaces, it should be a code block 29 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } 30 | 31 | if (!state.md.options.html) { return false; } 32 | 33 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } 34 | 35 | lineText = state.src.slice(pos, max); 36 | 37 | for (i = 0; i < HTML_SEQUENCES.length; i++) { 38 | if (HTML_SEQUENCES[i][0].test(lineText)) { break; } 39 | } 40 | 41 | if (i === HTML_SEQUENCES.length) { return false; } 42 | 43 | if (silent) { 44 | // true if this sequence can be a terminator, false otherwise 45 | return HTML_SEQUENCES[i][2]; 46 | } 47 | 48 | nextLine = startLine + 1; 49 | 50 | // If we are here - we detected HTML block. 51 | // Let's roll down till block end. 52 | if (!HTML_SEQUENCES[i][1].test(lineText)) { 53 | for (; nextLine < endLine; nextLine++) { 54 | if (state.sCount[nextLine] < state.blkIndent) { break; } 55 | 56 | pos = state.bMarks[nextLine] + state.tShift[nextLine]; 57 | max = state.eMarks[nextLine]; 58 | lineText = state.src.slice(pos, max); 59 | 60 | if (HTML_SEQUENCES[i][1].test(lineText)) { 61 | if (lineText.length !== 0) { nextLine++; } 62 | break; 63 | } 64 | } 65 | } 66 | 67 | state.line = nextLine; 68 | 69 | token = state.push('html_block', '', 0); 70 | token.map = [ startLine, nextLine ]; 71 | token.content = state.getLines(startLine, nextLine, state.blkIndent, true); 72 | 73 | return true; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/rules_block/fence.js: -------------------------------------------------------------------------------- 1 | // fences (``` lang, ~~~ lang) 2 | 3 | 'use strict'; 4 | 5 | 6 | module.exports = function fence(state, startLine, endLine, silent) { 7 | var marker, len, params, nextLine, mem, token, markup, 8 | haveEndMarker = false, 9 | pos = state.bMarks[startLine] + state.tShift[startLine], 10 | max = state.eMarks[startLine]; 11 | 12 | // if it's indented more than 3 spaces, it should be a code block 13 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } 14 | 15 | if (pos + 3 > max) { return false; } 16 | 17 | marker = state.src.charCodeAt(pos); 18 | 19 | if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { 20 | return false; 21 | } 22 | 23 | // scan marker length 24 | mem = pos; 25 | pos = state.skipChars(pos, marker); 26 | 27 | len = pos - mem; 28 | 29 | if (len < 3) { return false; } 30 | 31 | markup = state.src.slice(mem, pos); 32 | params = state.src.slice(pos, max); 33 | 34 | if (params.indexOf(String.fromCharCode(marker)) >= 0) { return false; } 35 | 36 | // Since start is found, we can report success here in validation mode 37 | if (silent) { return true; } 38 | 39 | // search end of block 40 | nextLine = startLine; 41 | 42 | for (;;) { 43 | nextLine++; 44 | if (nextLine >= endLine) { 45 | // unclosed block should be autoclosed by end of document. 46 | // also block seems to be autoclosed by end of parent 47 | break; 48 | } 49 | 50 | pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]; 51 | max = state.eMarks[nextLine]; 52 | 53 | if (pos < max && state.sCount[nextLine] < state.blkIndent) { 54 | // non-empty line with negative indent should stop the list: 55 | // - ``` 56 | // test 57 | break; 58 | } 59 | 60 | if (state.src.charCodeAt(pos) !== marker) { continue; } 61 | 62 | if (state.sCount[nextLine] - state.blkIndent >= 4) { 63 | // closing fence should be indented less than 4 spaces 64 | continue; 65 | } 66 | 67 | pos = state.skipChars(pos, marker); 68 | 69 | // closing code fence must be at least as long as the opening one 70 | if (pos - mem < len) { continue; } 71 | 72 | // make sure tail has spaces only 73 | pos = state.skipSpaces(pos); 74 | 75 | if (pos < max) { continue; } 76 | 77 | haveEndMarker = true; 78 | // found! 79 | break; 80 | } 81 | 82 | // If a fence has heading spaces, they should be removed from its inner block 83 | len = state.sCount[startLine]; 84 | 85 | state.line = nextLine + (haveEndMarker ? 1 : 0); 86 | 87 | token = state.push('fence', 'code', 0); 88 | token.info = params; 89 | token.content = state.getLines(startLine + 1, nextLine, len, true); 90 | token.markup = markup; 91 | token.map = [ startLine, state.line ]; 92 | 93 | return true; 94 | }; 95 | -------------------------------------------------------------------------------- /bin/markdown-it.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console:0*/ 3 | 4 | 'use strict'; 5 | 6 | 7 | var fs = require('fs'); 8 | var argparse = require('argparse'); 9 | 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | 13 | var cli = new argparse.ArgumentParser({ 14 | prog: 'markdown-it', 15 | version: require('../package.json').version, 16 | addHelp: true 17 | }); 18 | 19 | cli.addArgument([ '--no-html' ], { 20 | help: 'Disable embedded HTML', 21 | action: 'storeTrue' 22 | }); 23 | 24 | cli.addArgument([ '-l', '--linkify' ], { 25 | help: 'Autolink text', 26 | action: 'storeTrue' 27 | }); 28 | 29 | cli.addArgument([ '-t', '--typographer' ], { 30 | help: 'Enable smartquotes and other typographic replacements', 31 | action: 'storeTrue' 32 | }); 33 | 34 | cli.addArgument([ '--trace' ], { 35 | help: 'Show stack trace on error', 36 | action: 'storeTrue' 37 | }); 38 | 39 | cli.addArgument([ 'file' ], { 40 | help: 'File to read', 41 | nargs: '?', 42 | defaultValue: '-' 43 | }); 44 | 45 | cli.addArgument([ '-o', '--output' ], { 46 | help: 'File to write', 47 | defaultValue: '-' 48 | }); 49 | 50 | var options = cli.parseArgs(); 51 | 52 | 53 | function readFile(filename, encoding, callback) { 54 | if (options.file === '-') { 55 | // read from stdin 56 | var chunks = []; 57 | 58 | process.stdin.on('data', function (chunk) { chunks.push(chunk); }); 59 | 60 | process.stdin.on('end', function () { 61 | return callback(null, Buffer.concat(chunks).toString(encoding)); 62 | }); 63 | } else { 64 | fs.readFile(filename, encoding, callback); 65 | } 66 | } 67 | 68 | 69 | //////////////////////////////////////////////////////////////////////////////// 70 | 71 | readFile(options.file, 'utf8', function (err, input) { 72 | var output, md; 73 | 74 | if (err) { 75 | if (err.code === 'ENOENT') { 76 | console.error('File not found: ' + options.file); 77 | process.exit(2); 78 | } 79 | 80 | console.error( 81 | options.trace && err.stack || 82 | err.message || 83 | String(err)); 84 | 85 | process.exit(1); 86 | } 87 | 88 | md = require('..')({ 89 | html: !options['no-html'], 90 | xhtmlOut: false, 91 | typographer: options.typographer, 92 | linkify: options.linkify 93 | }); 94 | 95 | try { 96 | output = md.render(input); 97 | 98 | } catch (e) { 99 | console.error( 100 | options.trace && e.stack || 101 | e.message || 102 | String(e)); 103 | 104 | process.exit(1); 105 | } 106 | 107 | if (options.output === '-') { 108 | // write to stdout 109 | process.stdout.write(output); 110 | } else { 111 | fs.writeFileSync(options.output, output); 112 | } 113 | }); 114 | -------------------------------------------------------------------------------- /lib/rules_block/lheading.js: -------------------------------------------------------------------------------- 1 | // lheading (---, ===) 2 | 3 | 'use strict'; 4 | 5 | 6 | module.exports = function lheading(state, startLine, endLine/*, silent*/) { 7 | var content, terminate, i, l, token, pos, max, level, marker, 8 | nextLine = startLine + 1, oldParentType, 9 | terminatorRules = state.md.block.ruler.getRules('paragraph'); 10 | 11 | // if it's indented more than 3 spaces, it should be a code block 12 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } 13 | 14 | oldParentType = state.parentType; 15 | state.parentType = 'paragraph'; // use paragraph to match terminatorRules 16 | 17 | // jump line-by-line until empty one or EOF 18 | for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { 19 | // this would be a code block normally, but after paragraph 20 | // it's considered a lazy continuation regardless of what's there 21 | if (state.sCount[nextLine] - state.blkIndent > 3) { continue; } 22 | 23 | // 24 | // Check for underline in setext header 25 | // 26 | if (state.sCount[nextLine] >= state.blkIndent) { 27 | pos = state.bMarks[nextLine] + state.tShift[nextLine]; 28 | max = state.eMarks[nextLine]; 29 | 30 | if (pos < max) { 31 | marker = state.src.charCodeAt(pos); 32 | 33 | if (marker === 0x2D/* - */ || marker === 0x3D/* = */) { 34 | pos = state.skipChars(pos, marker); 35 | pos = state.skipSpaces(pos); 36 | 37 | if (pos >= max) { 38 | level = (marker === 0x3D/* = */ ? 1 : 2); 39 | break; 40 | } 41 | } 42 | } 43 | } 44 | 45 | // quirk for blockquotes, this line should already be checked by that rule 46 | if (state.sCount[nextLine] < 0) { continue; } 47 | 48 | // Some tags can terminate paragraph without empty line. 49 | terminate = false; 50 | for (i = 0, l = terminatorRules.length; i < l; i++) { 51 | if (terminatorRules[i](state, nextLine, endLine, true)) { 52 | terminate = true; 53 | break; 54 | } 55 | } 56 | if (terminate) { break; } 57 | } 58 | 59 | if (!level) { 60 | // Didn't find valid underline 61 | return false; 62 | } 63 | 64 | content = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); 65 | 66 | state.line = nextLine + 1; 67 | 68 | token = state.push('heading_open', 'h' + String(level), 1); 69 | token.markup = String.fromCharCode(marker); 70 | token.map = [ startLine, state.line ]; 71 | 72 | token = state.push('inline', '', 0); 73 | token.content = content; 74 | token.map = [ startLine, state.line - 1 ]; 75 | token.children = []; 76 | 77 | token = state.push('heading_close', 'h' + String(level), -1); 78 | token.markup = String.fromCharCode(marker); 79 | 80 | state.parentType = oldParentType; 81 | 82 | return true; 83 | }; 84 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/xss.txt: -------------------------------------------------------------------------------- 1 | . 2 | [normal link](javascript) 3 | . 4 |

normal link

5 | . 6 | 7 | 8 | Should not allow some protocols in links and images 9 | . 10 | [xss link](javascript:alert(1)) 11 | 12 | [xss link](JAVASCRIPT:alert(1)) 13 | 14 | [xss link](vbscript:alert(1)) 15 | 16 | [xss link](VBSCRIPT:alert(1)) 17 | 18 | [xss link](file:///123) 19 | . 20 |

[xss link](javascript:alert(1))

21 |

[xss link](JAVASCRIPT:alert(1))

22 |

[xss link](vbscript:alert(1))

23 |

[xss link](VBSCRIPT:alert(1))

24 |

[xss link](file:///123)

25 | . 26 | 27 | 28 | . 29 | [xss link]("><script>alert("xss")</script>) 30 | 31 | [xss link](Javascript:alert(1)) 32 | 33 | [xss link](&#74;avascript:alert(1)) 34 | 35 | [xss link](\Javascript:alert(1)) 36 | . 37 |

xss link

38 |

[xss link](Javascript:alert(1))

39 |

xss link

40 |

xss link

41 | . 42 | 43 | . 44 | [xss link]() 45 | . 46 |

[xss link](<javascript:alert(1)>)

47 | . 48 | 49 | . 50 | [xss link](javascript:alert(1)) 51 | . 52 |

[xss link](javascript:alert(1))

53 | . 54 | 55 | 56 | Should not allow data-uri except some whitelisted mimes 57 | . 58 | ![](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7) 59 | . 60 |

61 | . 62 | 63 | . 64 | [xss link](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K) 65 | . 66 |

[xss link](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K)

67 | . 68 | 69 | . 70 | [normal link](/javascript:link) 71 | . 72 |

normal link

73 | . 74 | 75 | 76 | Image parser use the same code base as link. 77 | . 78 | ![xss link](javascript:alert(1)) 79 | . 80 |

![xss link](javascript:alert(1))

81 | . 82 | 83 | 84 | Autolinks 85 | . 86 | 87 | 88 | 89 | . 90 |

<javascript:alert(1)>

91 |

<javascript:alert(1)>

92 | . 93 | 94 | 95 | Linkifier 96 | . 97 | javascript:alert(1) 98 | 99 | javascript:alert(1) 100 | . 101 |

javascript:alert(1)

102 |

javascript:alert(1)

103 | . 104 | 105 | 106 | References 107 | . 108 | [test]: javascript:alert(1) 109 | . 110 |

[test]: javascript:alert(1)

111 | . 112 | 113 | 114 | Make sure we decode entities before split: 115 | . 116 | ```js custom-class 117 | test1 118 | ``` 119 | 120 | ```js custom-class 121 | test2 122 | ``` 123 | . 124 |
test1
125 | 
126 |
test2
127 | 
128 | . 129 | -------------------------------------------------------------------------------- /support/demo_template/index.styl: -------------------------------------------------------------------------------- 1 | html, body, .full-height 2 | height 100% 3 | 4 | 5 | body 6 | overflow-x hidden 7 | padding-bottom 160px 8 | background-color #fbfbfb 9 | 10 | // hack to allign emojies to line height 11 | .emoji 12 | height: 1.2em; 13 | 14 | .demo-options 15 | margin-bottom 30px 16 | 17 | .opt__strict .not-strict 18 | opacity 0.3 19 | 20 | .checkbox 21 | margin-right 10px 22 | 23 | .source 24 | width 100% 25 | font-family Menlo, Monaco, Consolas, "Courier New", monospace 26 | font-size 13px 27 | padding 2px 28 | 29 | .result-html 30 | padding 2px 10px 31 | overflow auto 32 | background-color #fff 33 | border 1px solid #ccc 34 | border-radius 4px 35 | img 36 | max-width 35% 37 | 38 | .result-src 39 | .result-debug 40 | display none 41 | 42 | .result-src-content 43 | .result-debug-content 44 | white-space pre 45 | 46 | .result-as-html 47 | .result-html 48 | display block 49 | .result-src 50 | .result-debug 51 | display none 52 | .result-as-src 53 | .result-src 54 | display block 55 | .result-html 56 | .result-debug 57 | display none 58 | .result-as-debug 59 | .result-debug 60 | display block 61 | .result-html 62 | .result-src 63 | display none 64 | 65 | .demo-control 66 | position absolute 67 | right 15px 68 | top -17px 69 | border-radius 6px 6px 0 0 70 | font-size 12px 71 | background-color #ddd 72 | a 73 | padding 0 20px 74 | a:first-child 75 | padding-left 30px 76 | a:last-child 77 | padding-right 30px 78 | 79 | // twbs fix 80 | .hljs 81 | padding 9.5px 82 | code 83 | white-space pre 84 | 85 | 86 | .footnotes 87 | -moz-column-count 2 88 | -webkit-column-count 2 89 | column-count 2 90 | 91 | .footnotes-list 92 | padding-left 2em 93 | 94 | // custom container 95 | .warning 96 | background-color #ff8 97 | padding 20px 98 | border-radius 6px 99 | 100 | .gh-ribbon 101 | display block 102 | position absolute 103 | right -60px 104 | top 44px 105 | transform rotate(45deg) 106 | width 230px 107 | z-index 10000 108 | white-space nowrap 109 | font-family "Helvetica Neue", Helvetica, Arial, sans-serif 110 | background-color #686868 111 | box-shadow 0 0 2px rgba(102,102,102,0.4) 112 | padding 1px 0 113 | 114 | a 115 | text-decoration none !important 116 | border 1px solid #ccc 117 | color #fff 118 | display block 119 | font-size 13px 120 | font-weight 700 121 | outline medium none 122 | padding 4px 50px 2px 123 | text-align center 124 | 125 | //////////////////////////////////////////////////////////////////////////////// 126 | // Override default responsiveness 127 | // 128 | .form-inline 129 | .radio 130 | .checkbox 131 | display inline-block 132 | margin-bottom 0 133 | margin-top 0 134 | 135 | .form-group 136 | display inline-block 137 | margin-bottom 0 138 | vertical-align: middle 139 | 140 | .form-control 141 | display inline-block 142 | vertical-align middle 143 | width auto 144 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('chai').assert; 5 | 6 | 7 | describe('Utils', function () { 8 | 9 | it('fromCodePoint', function () { 10 | var fromCodePoint = require('../lib/common/utils').fromCodePoint; 11 | 12 | assert.strictEqual(fromCodePoint(0x20), ' '); 13 | assert.strictEqual(fromCodePoint(0x1F601), '😁'); 14 | }); 15 | 16 | it('isValidEntityCode', function () { 17 | var isValidEntityCode = require('../lib/common/utils').isValidEntityCode; 18 | 19 | assert.strictEqual(isValidEntityCode(0x20), true); 20 | assert.strictEqual(isValidEntityCode(0xD800), false); 21 | assert.strictEqual(isValidEntityCode(0xFDD0), false); 22 | assert.strictEqual(isValidEntityCode(0x1FFFF), false); 23 | assert.strictEqual(isValidEntityCode(0x1FFFE), false); 24 | assert.strictEqual(isValidEntityCode(0x00), false); 25 | assert.strictEqual(isValidEntityCode(0x0B), false); 26 | assert.strictEqual(isValidEntityCode(0x0E), false); 27 | assert.strictEqual(isValidEntityCode(0x7F), false); 28 | }); 29 | 30 | /*it('replaceEntities', function () { 31 | var replaceEntities = require('../lib/common/utils').replaceEntities; 32 | 33 | assert.strictEqual(replaceEntities('&'), '&'); 34 | assert.strictEqual(replaceEntities(' '), ' '); 35 | assert.strictEqual(replaceEntities(' '), ' '); 36 | assert.strictEqual(replaceEntities('&&'), '&&'); 37 | 38 | assert.strictEqual(replaceEntities('&am;'), '&am;'); 39 | assert.strictEqual(replaceEntities('�'), '�'); 40 | });*/ 41 | 42 | it('assign', function () { 43 | var assign = require('../lib/common/utils').assign; 44 | 45 | assert.deepEqual(assign({ a: 1 }, null, { b: 2 }), { a: 1, b: 2 }); 46 | assert.throws(function () { 47 | assign({}, 123); 48 | }); 49 | }); 50 | 51 | it('escapeRE', function () { 52 | var escapeRE = require('../lib/common/utils').escapeRE; 53 | 54 | assert.strictEqual(escapeRE(' .?*+^$[]\\(){}|-'), ' \\.\\?\\*\\+\\^\\$\\[\\]\\\\\\(\\)\\{\\}\\|\\-'); 55 | }); 56 | 57 | it('isWhiteSpace', function () { 58 | var isWhiteSpace = require('../lib/common/utils').isWhiteSpace; 59 | 60 | assert.strictEqual(isWhiteSpace(0x2000), true); 61 | assert.strictEqual(isWhiteSpace(0x09), true); 62 | 63 | assert.strictEqual(isWhiteSpace(0x30), false); 64 | }); 65 | 66 | it('isMdAsciiPunct', function () { 67 | var isMdAsciiPunct = require('../lib/common/utils').isMdAsciiPunct; 68 | 69 | assert.strictEqual(isMdAsciiPunct(0x30), false); 70 | 71 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').forEach(function (ch) { 72 | assert.strictEqual(isMdAsciiPunct(ch.charCodeAt(0)), true); 73 | }); 74 | }); 75 | 76 | it('unescapeMd', function () { 77 | var unescapeMd = require('../lib/common/utils').unescapeMd; 78 | 79 | assert.strictEqual(unescapeMd('\\foo'), '\\foo'); 80 | assert.strictEqual(unescapeMd('foo'), 'foo'); 81 | 82 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').forEach(function (ch) { 83 | assert.strictEqual(unescapeMd('\\' + ch), ch); 84 | }); 85 | }); 86 | 87 | }); 88 | -------------------------------------------------------------------------------- /lib/rules_inline/strikethrough.js: -------------------------------------------------------------------------------- 1 | // ~~strike through~~ 2 | // 3 | 'use strict'; 4 | 5 | 6 | // Insert each marker as a separate text token, and add it to delimiter list 7 | // 8 | module.exports.tokenize = function strikethrough(state, silent) { 9 | var i, scanned, token, len, ch, 10 | start = state.pos, 11 | marker = state.src.charCodeAt(start); 12 | 13 | if (silent) { return false; } 14 | 15 | if (marker !== 0x7E/* ~ */) { return false; } 16 | 17 | scanned = state.scanDelims(state.pos, true); 18 | len = scanned.length; 19 | ch = String.fromCharCode(marker); 20 | 21 | if (len < 2) { return false; } 22 | 23 | if (len % 2) { 24 | token = state.push('text', '', 0); 25 | token.content = ch; 26 | len--; 27 | } 28 | 29 | for (i = 0; i < len; i += 2) { 30 | token = state.push('text', '', 0); 31 | token.content = ch + ch; 32 | 33 | state.delimiters.push({ 34 | marker: marker, 35 | jump: i, 36 | token: state.tokens.length - 1, 37 | level: state.level, 38 | end: -1, 39 | open: scanned.can_open, 40 | close: scanned.can_close 41 | }); 42 | } 43 | 44 | state.pos += scanned.length; 45 | 46 | return true; 47 | }; 48 | 49 | 50 | // Walk through delimiter list and replace text tokens with tags 51 | // 52 | module.exports.postProcess = function strikethrough(state) { 53 | var i, j, 54 | startDelim, 55 | endDelim, 56 | token, 57 | loneMarkers = [], 58 | delimiters = state.delimiters, 59 | max = state.delimiters.length; 60 | 61 | for (i = 0; i < max; i++) { 62 | startDelim = delimiters[i]; 63 | 64 | if (startDelim.marker !== 0x7E/* ~ */) { 65 | continue; 66 | } 67 | 68 | if (startDelim.end === -1) { 69 | continue; 70 | } 71 | 72 | endDelim = delimiters[startDelim.end]; 73 | 74 | token = state.tokens[startDelim.token]; 75 | token.type = 's_open'; 76 | token.tag = 's'; 77 | token.nesting = 1; 78 | token.markup = '~~'; 79 | token.content = ''; 80 | 81 | token = state.tokens[endDelim.token]; 82 | token.type = 's_close'; 83 | token.tag = 's'; 84 | token.nesting = -1; 85 | token.markup = '~~'; 86 | token.content = ''; 87 | 88 | if (state.tokens[endDelim.token - 1].type === 'text' && 89 | state.tokens[endDelim.token - 1].content === '~') { 90 | 91 | loneMarkers.push(endDelim.token - 1); 92 | } 93 | } 94 | 95 | // If a marker sequence has an odd number of characters, it's splitted 96 | // like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the 97 | // start of the sequence. 98 | // 99 | // So, we have to move all those markers after subsequent s_close tags. 100 | // 101 | while (loneMarkers.length) { 102 | i = loneMarkers.pop(); 103 | j = i + 1; 104 | 105 | while (j < state.tokens.length && state.tokens[j].type === 's_close') { 106 | j++; 107 | } 108 | 109 | j--; 110 | 111 | if (i !== j) { 112 | token = state.tokens[j]; 113 | state.tokens[j] = state.tokens[i]; 114 | state.tokens[i] = token; 115 | } 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /lib/rules_core/replacements.js: -------------------------------------------------------------------------------- 1 | // Simple typographyc replacements 2 | // 3 | // (c) (C) → © 4 | // (tm) (TM) → ™ 5 | // (r) (R) → ® 6 | // +- → ± 7 | // (p) (P) -> § 8 | // ... → … (also ?.... → ?.., !.... → !..) 9 | // ???????? → ???, !!!!! → !!!, `,,` → `,` 10 | // -- → –, --- → — 11 | // 12 | 'use strict'; 13 | 14 | // TODO: 15 | // - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾ 16 | // - miltiplication 2 x 4 -> 2 × 4 17 | 18 | var RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; 19 | 20 | // Workaround for phantomjs - need regex without /g flag, 21 | // or root check will fail every second time 22 | var SCOPED_ABBR_TEST_RE = /\((c|tm|r|p)\)/i; 23 | 24 | var SCOPED_ABBR_RE = /\((c|tm|r|p)\)/ig; 25 | var SCOPED_ABBR = { 26 | c: '©', 27 | r: '®', 28 | p: '§', 29 | tm: '™' 30 | }; 31 | 32 | function replaceFn(match, name) { 33 | return SCOPED_ABBR[name.toLowerCase()]; 34 | } 35 | 36 | function replace_scoped(inlineTokens) { 37 | var i, token, inside_autolink = 0; 38 | 39 | for (i = inlineTokens.length - 1; i >= 0; i--) { 40 | token = inlineTokens[i]; 41 | 42 | if (token.type === 'text' && !inside_autolink) { 43 | token.content = token.content.replace(SCOPED_ABBR_RE, replaceFn); 44 | } 45 | 46 | if (token.type === 'link_open' && token.info === 'auto') { 47 | inside_autolink--; 48 | } 49 | 50 | if (token.type === 'link_close' && token.info === 'auto') { 51 | inside_autolink++; 52 | } 53 | } 54 | } 55 | 56 | function replace_rare(inlineTokens) { 57 | var i, token, inside_autolink = 0; 58 | 59 | for (i = inlineTokens.length - 1; i >= 0; i--) { 60 | token = inlineTokens[i]; 61 | 62 | if (token.type === 'text' && !inside_autolink) { 63 | if (RARE_RE.test(token.content)) { 64 | token.content = token.content 65 | .replace(/\+-/g, '±') 66 | // .., ..., ....... -> … 67 | // but ?..... & !..... -> ?.. & !.. 68 | .replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..') 69 | .replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ',') 70 | // em-dash 71 | .replace(/(^|[^-])---([^-]|$)/mg, '$1\u2014$2') 72 | // en-dash 73 | .replace(/(^|\s)--(\s|$)/mg, '$1\u2013$2') 74 | .replace(/(^|[^-\s])--([^-\s]|$)/mg, '$1\u2013$2'); 75 | } 76 | } 77 | 78 | if (token.type === 'link_open' && token.info === 'auto') { 79 | inside_autolink--; 80 | } 81 | 82 | if (token.type === 'link_close' && token.info === 'auto') { 83 | inside_autolink++; 84 | } 85 | } 86 | } 87 | 88 | 89 | module.exports = function replace(state) { 90 | var blkIdx; 91 | 92 | if (!state.md.options.typographer) { return; } 93 | 94 | for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { 95 | 96 | if (state.tokens[blkIdx].type !== 'inline') { continue; } 97 | 98 | if (SCOPED_ABBR_TEST_RE.test(state.tokens[blkIdx].content)) { 99 | replace_scoped(state.tokens[blkIdx].children); 100 | } 101 | 102 | if (RARE_RE.test(state.tokens[blkIdx].content)) { 103 | replace_rare(state.tokens[blkIdx].children); 104 | } 105 | 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /benchmark/benchmark.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console:0*/ 3 | 4 | 'use strict'; 5 | 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var util = require('util'); 9 | var Benchmark = require('benchmark'); 10 | var ansi = require('ansi'); 11 | var cursor = ansi(process.stdout); 12 | 13 | var IMPLS_DIRECTORY = path.join(__dirname, 'implementations'); 14 | var IMPLS_PATHS = {}; 15 | var IMPLS = []; 16 | 17 | 18 | fs.readdirSync(IMPLS_DIRECTORY).sort().forEach(function (name) { 19 | var file = path.join(IMPLS_DIRECTORY, name); 20 | var code = require(file); 21 | 22 | IMPLS_PATHS[name] = file; 23 | IMPLS.push({ 24 | name: name, 25 | code: code 26 | }); 27 | }); 28 | 29 | 30 | var SAMPLES_DIRECTORY = path.join(__dirname, 'samples'); 31 | var SAMPLES = []; 32 | 33 | fs.readdirSync(SAMPLES_DIRECTORY).sort().forEach(function (sample) { 34 | var filepath = path.join(SAMPLES_DIRECTORY, sample), 35 | extname = path.extname(filepath), 36 | basename = path.basename(filepath, extname); 37 | 38 | var content = {}; 39 | 40 | content.string = fs.readFileSync(filepath, 'utf8'); 41 | 42 | var title = util.format('(%d bytes)', content.string.length); 43 | 44 | function onComplete() { 45 | cursor.write('\n'); 46 | } 47 | 48 | 49 | var suite = new Benchmark.Suite(title, { 50 | 51 | onStart: function onStart() { 52 | console.log('\nSample: %s %s', sample, title); 53 | }, 54 | 55 | onComplete: onComplete 56 | 57 | }); 58 | 59 | 60 | IMPLS.forEach(function (impl) { 61 | suite.add(impl.name, { 62 | 63 | onCycle: function onCycle(event) { 64 | cursor.horizontalAbsolute(); 65 | cursor.eraseLine(); 66 | cursor.write(' > ' + event.target); 67 | }, 68 | 69 | onComplete: onComplete, 70 | 71 | fn: function () { 72 | impl.code.run(content.string); 73 | return; 74 | } 75 | }); 76 | }); 77 | 78 | 79 | SAMPLES.push({ 80 | name: basename, 81 | title: title, 82 | content: content, 83 | suite: suite 84 | }); 85 | }); 86 | 87 | 88 | function select(patterns) { 89 | var result = []; 90 | 91 | if (!(patterns instanceof Array)) { 92 | patterns = [ patterns ]; 93 | } 94 | 95 | function checkName(name) { 96 | return patterns.length === 0 || patterns.some(function (regexp) { 97 | return regexp.test(name); 98 | }); 99 | } 100 | 101 | SAMPLES.forEach(function (sample) { 102 | if (checkName(sample.name)) { 103 | result.push(sample); 104 | } 105 | }); 106 | 107 | return result; 108 | } 109 | 110 | 111 | function run(files) { 112 | var selected = select(files); 113 | 114 | if (selected.length > 0) { 115 | console.log('Selected samples: (%d of %d)', selected.length, SAMPLES.length); 116 | selected.forEach(function (sample) { 117 | console.log(' > %s', sample.name); 118 | }); 119 | } else { 120 | console.log('There isn\'t any sample matches any of these patterns: %s', util.inspect(files)); 121 | } 122 | 123 | selected.forEach(function (sample) { 124 | sample.suite.run(); 125 | }); 126 | } 127 | 128 | module.exports.IMPLS_DIRECTORY = IMPLS_DIRECTORY; 129 | module.exports.IMPLS_PATHS = IMPLS_PATHS; 130 | module.exports.IMPLS = IMPLS; 131 | module.exports.SAMPLES_DIRECTORY = SAMPLES_DIRECTORY; 132 | module.exports.SAMPLES = SAMPLES; 133 | module.exports.select = select; 134 | module.exports.run = run; 135 | 136 | run(process.argv.slice(2).map(function (source) { 137 | return new RegExp(source, 'i'); 138 | })); 139 | -------------------------------------------------------------------------------- /benchmark/samples/lorem1.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, __consectetur__ adipiscing elit. Cras imperdiet nec erat ac condimentum. Nulla vel rutrum ligula. Sed hendrerit interdum orci a posuere. Vivamus ut velit aliquet, mollis purus eget, iaculis nisl. Proin posuere malesuada ante. Proin auctor orci eros, ac molestie lorem dictum nec. Vestibulum sit amet erat est. Morbi luctus sed elit ac luctus. Proin blandit, enim vitae egestas posuere, neque elit ultricies dui, vel mattis nibh enim ac lorem. Maecenas molestie nisl sit amet velit dictum lobortis. Aliquam erat volutpat. 2 | 3 | Vivamus sagittis, diam in [vehicula](https://github.com/markdown-it/markdown-it) lobortis, sapien arcu mattis erat, vel aliquet sem urna et risus. Ut feugiat sapien vitae mi elementum laoreet. Suspendisse potenti. Aliquam erat nisl, aliquam pretium libero aliquet, sagittis eleifend nunc. In hac habitasse platea dictumst. Integer turpis augue, tincidunt dignissim mauris id, rhoncus dapibus purus. Maecenas et enim odio. Nullam massa metus, varius quis vehicula sed, pharetra mollis erat. In quis viverra velit. Vivamus placerat, est nec hendrerit varius, enim dui hendrerit magna, ut pulvinar nibh lorem vel lacus. Mauris a orci iaculis, hendrerit eros sed, gravida leo. In dictum mauris vel augue varius, ac ullamcorper nisl ornare. In eu posuere velit, ac fermentum arcu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nullam sed malesuada leo, at interdum elit. 4 | 5 | Nullam ut tincidunt nunc. [Pellentesque][1] metus lacus, commodo eget justo ut, rutrum varius nunc. Sed non rhoncus risus. Morbi sodales gravida pulvinar. Duis malesuada, odio volutpat elementum vulputate, massa magna scelerisque ante, et accumsan tellus nunc in sem. Donec mattis arcu et velit aliquet, non sagittis justo vestibulum. Suspendisse volutpat felis lectus, nec consequat ipsum mattis id. Donec dapibus vehicula facilisis. In tincidunt mi nisi, nec faucibus tortor euismod nec. Suspendisse ante ligula, aliquet vitae libero eu, vulputate dapibus libero. Sed bibendum, sapien at posuere interdum, libero est sollicitudin magna, ac gravida tellus purus eu ipsum. Proin ut quam arcu. 6 | 7 | Suspendisse potenti. Donec ante velit, ornare at augue quis, tristique laoreet sem. Etiam in ipsum elit. Nullam cursus dolor sit amet nulla feugiat tristique. Phasellus ac tellus tincidunt, imperdiet purus eget, ullamcorper ipsum. Cras eu tincidunt sem. Nullam sed dapibus magna. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id venenatis tortor. In consectetur sollicitudin pharetra. Etiam convallis nisi nunc, et aliquam turpis viverra sit amet. Maecenas faucibus sodales tortor. Suspendisse lobortis mi eu leo viverra volutpat. Pellentesque velit ante, vehicula sodales congue ut, elementum a urna. Cras tempor, ipsum eget luctus rhoncus, arcu ligula fermentum urna, vulputate pharetra enim enim non libero. 8 | 9 | Proin diam quam, elementum in eleifend id, elementum et metus. Cras in justo consequat justo semper ultrices. Sed dignissim lectus a ante mollis, nec vulputate ante molestie. Proin in porta nunc. Etiam pulvinar turpis sed velit porttitor, vel adipiscing velit fringilla. Cras ac tellus vitae purus pharetra tincidunt. Sed cursus aliquet aliquet. Cras eleifend commodo malesuada. In turpis turpis, ullamcorper ut tincidunt a, ullamcorper a nunc. Etiam luctus tellus ac dapibus gravida. Ut nec lacus laoreet neque ullamcorper volutpat. 10 | 11 | Nunc et leo erat. Aenean mattis ultrices lorem, eget adipiscing dolor ultricies eu. In hac habitasse platea dictumst. Vivamus cursus feugiat sapien quis aliquam. Mauris quam libero, porta vel volutpat ut, blandit a purus. Vivamus vestibulum dui vel tortor molestie, sit amet feugiat sem commodo. Nulla facilisi. Sed molestie arcu eget tellus vestibulum tristique. 12 | 13 | [1]: https://github.com/markdown-it 14 | -------------------------------------------------------------------------------- /support/specsplit.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console:0*/ 3 | 4 | // Fixtures generator from commonmark specs. Split spec to working / not working 5 | // examples, or show total stat. 6 | 7 | 'use strict'; 8 | 9 | 10 | var fs = require('fs'); 11 | var util = require('util'); 12 | var argparse = require('argparse'); 13 | 14 | 15 | var cli = new argparse.ArgumentParser({ 16 | prog: 'specsplit', 17 | version: require('../package.json').version, 18 | addHelp: true 19 | }); 20 | 21 | cli.addArgument([ 'type' ], { 22 | help: 'type of examples to filter', 23 | nargs: '?', 24 | choices: [ 'good', 'bad' ] 25 | }); 26 | 27 | cli.addArgument([ 'spec' ], { 28 | help: 'spec file to read' 29 | }); 30 | 31 | var options = cli.parseArgs(); 32 | 33 | //////////////////////////////////////////////////////////////////////////////// 34 | 35 | function normalize(text) { 36 | return text.replace(/
\n<\/blockquote>/g, '
'); 37 | } 38 | 39 | //////////////////////////////////////////////////////////////////////////////// 40 | 41 | function readFile(filename, encoding, callback) { 42 | if (options.file === '-') { 43 | // read from stdin 44 | 45 | var chunks = []; 46 | 47 | process.stdin.on('data', function (chunk) { 48 | chunks.push(chunk); 49 | }); 50 | 51 | process.stdin.on('end', function () { 52 | return callback(null, Buffer.concat(chunks).toString(encoding)); 53 | }); 54 | } else { 55 | fs.readFile(filename, encoding, callback); 56 | } 57 | } 58 | 59 | 60 | //////////////////////////////////////////////////////////////////////////////// 61 | 62 | readFile(options.spec, 'utf8', function (error, input) { 63 | var good = [], bad = [], 64 | markdown = require('..')('commonmark'); 65 | 66 | if (error) { 67 | if (error.code === 'ENOENT') { 68 | process.stderr.write('File not found: ' + options.spec); 69 | process.exit(2); 70 | } 71 | 72 | process.stderr.write(error.stack || error.message || String(error)); 73 | process.exit(1); 74 | } 75 | 76 | input = input.replace(/→/g, '\t'); 77 | 78 | markdown.parse(input, {}) 79 | .filter(function (token) { 80 | return token.tag === 'code' && 81 | token.info.trim() === 'example'; 82 | }) 83 | .forEach(function (token) { 84 | var arr = token.content.split(/^\.\s*?$/m, 2); 85 | var md = arr[0]; 86 | var html = arr[1].replace(/^\n/, ''); 87 | 88 | var result = { 89 | md: md, 90 | html: html, 91 | line: token.map[0], 92 | err: '' 93 | }; 94 | 95 | try { 96 | if (markdown.render(md) === normalize(html)) { 97 | good.push(result); 98 | } else { 99 | result.err = markdown.render(md); 100 | bad.push(result); 101 | } 102 | } catch (___) { 103 | // bad.push(result); 104 | throw ___; 105 | } 106 | }); 107 | 108 | if (!options.type) { 109 | console.log(util.format('passed samples - %s, failed samples - %s', good.length, bad.length)); 110 | } else { 111 | var data = options.type === 'good' ? good : bad; 112 | 113 | data.forEach(function (sample) { 114 | console.log(util.format( 115 | '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n' + 116 | 'src line: %s\n\n.\n%s.\n%s.\n', 117 | sample.line, sample.md, sample.html)); 118 | if (sample.err) { 119 | console.log(util.format('error:\n\n%s\n', sample.err)); 120 | } 121 | }); 122 | } 123 | 124 | process.exit(0); 125 | }); 126 | -------------------------------------------------------------------------------- /lib/parser_block.js: -------------------------------------------------------------------------------- 1 | /** internal 2 | * class ParserBlock 3 | * 4 | * Block-level tokenizer. 5 | **/ 6 | 'use strict'; 7 | 8 | 9 | var Ruler = require('./ruler'); 10 | 11 | 12 | var _rules = [ 13 | // First 2 params - rule name & source. Secondary array - list of rules, 14 | // which can be terminated by this one. 15 | [ 'table', require('./rules_block/table'), [ 'paragraph', 'reference' ] ], 16 | [ 'code', require('./rules_block/code') ], 17 | [ 'fence', require('./rules_block/fence'), [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 18 | [ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'reference', 'list' ] ], 19 | [ 'hr', require('./rules_block/hr'), [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 20 | [ 'list', require('./rules_block/list'), [ 'paragraph', 'reference', 'blockquote' ] ], 21 | [ 'reference', require('./rules_block/reference') ], 22 | [ 'heading', require('./rules_block/heading'), [ 'paragraph', 'reference', 'blockquote' ] ], 23 | [ 'lheading', require('./rules_block/lheading') ], 24 | [ 'html_block', require('./rules_block/html_block'), [ 'paragraph', 'reference', 'blockquote' ] ], 25 | [ 'paragraph', require('./rules_block/paragraph') ] 26 | ]; 27 | 28 | 29 | /** 30 | * new ParserBlock() 31 | **/ 32 | function ParserBlock() { 33 | /** 34 | * ParserBlock#ruler -> Ruler 35 | * 36 | * [[Ruler]] instance. Keep configuration of block rules. 37 | **/ 38 | this.ruler = new Ruler(); 39 | 40 | for (var i = 0; i < _rules.length; i++) { 41 | this.ruler.push(_rules[i][0], _rules[i][1], { alt: (_rules[i][2] || []).slice() }); 42 | } 43 | } 44 | 45 | 46 | // Generate tokens for input range 47 | // 48 | ParserBlock.prototype.tokenize = function (state, startLine, endLine) { 49 | var ok, i, 50 | rules = this.ruler.getRules(''), 51 | len = rules.length, 52 | line = startLine, 53 | hasEmptyLines = false, 54 | maxNesting = state.md.options.maxNesting; 55 | 56 | while (line < endLine) { 57 | state.line = line = state.skipEmptyLines(line); 58 | if (line >= endLine) { break; } 59 | 60 | // Termination condition for nested calls. 61 | // Nested calls currently used for blockquotes & lists 62 | if (state.sCount[line] < state.blkIndent) { break; } 63 | 64 | // If nesting level exceeded - skip tail to the end. That's not ordinary 65 | // situation and we should not care about content. 66 | if (state.level >= maxNesting) { 67 | state.line = endLine; 68 | break; 69 | } 70 | 71 | // Try all possible rules. 72 | // On success, rule should: 73 | // 74 | // - update `state.line` 75 | // - update `state.tokens` 76 | // - return true 77 | 78 | for (i = 0; i < len; i++) { 79 | ok = rules[i](state, line, endLine, false); 80 | if (ok) { break; } 81 | } 82 | 83 | // set state.tight iff we had an empty line before current tag 84 | // i.e. latest empty line should not count 85 | state.tight = !hasEmptyLines; 86 | 87 | // paragraph might "eat" one newline after it in nested lists 88 | if (state.isEmpty(state.line - 1)) { 89 | hasEmptyLines = true; 90 | } 91 | 92 | line = state.line; 93 | 94 | if (line < endLine && state.isEmpty(line)) { 95 | hasEmptyLines = true; 96 | line++; 97 | state.line = line; 98 | } 99 | } 100 | }; 101 | 102 | 103 | /** 104 | * ParserBlock.parse(str, md, env, outTokens) 105 | * 106 | * Process input string and push block tokens into `outTokens` 107 | **/ 108 | ParserBlock.prototype.parse = function (src, md, env, outTokens) { 109 | var state; 110 | 111 | if (!src) { return; } 112 | 113 | state = new this.State(src, md, env, outTokens); 114 | 115 | this.tokenize(state, state.line, state.lineMax); 116 | }; 117 | 118 | 119 | ParserBlock.prototype.State = require('./rules_block/state_block'); 120 | 121 | 122 | module.exports = ParserBlock; 123 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH := ./node_modules/.bin:${PATH} 2 | 3 | NPM_PACKAGE := $(shell node -e 'process.stdout.write(require("./package.json").name)') 4 | NPM_VERSION := $(shell node -e 'process.stdout.write(require("./package.json").version)') 5 | 6 | TMP_PATH := /tmp/${NPM_PACKAGE}-$(shell date +%s) 7 | 8 | REMOTE_NAME ?= origin 9 | REMOTE_REPO ?= $(shell git config --get remote.${REMOTE_NAME}.url) 10 | 11 | CURR_HEAD := $(firstword $(shell git show-ref --hash HEAD | cut -b -6) master) 12 | GITHUB_PROJ := https://github.com//markdown-it/${NPM_PACKAGE} 13 | 14 | 15 | demo: lint 16 | rm -rf ./demo 17 | mkdir ./demo 18 | ./support/demodata.js > ./support/demo_template/sample.json 19 | jade ./support/demo_template/index.jade --pretty \ 20 | --obj ./support/demo_template/sample.json \ 21 | --out ./demo 22 | stylus -u autoprefixer-stylus \ 23 | < ./support/demo_template/index.styl \ 24 | > ./demo/index.css 25 | rm -rf ./support/demo_template/sample.json 26 | browserify ./ -s markdownit > ./demo/markdown-it.js 27 | browserify ./support/demo_template/index.js > ./demo/index.js 28 | cp ./support/demo_template/README.md ./demo/ 29 | 30 | gh-demo: demo 31 | touch ./demo/.nojekyll 32 | cd ./demo \ 33 | && git init . \ 34 | && git add . \ 35 | && git commit -m "Auto-generate demo" \ 36 | && git remote add origin git@github.com:markdown-it/markdown-it.github.io.git \ 37 | && git push --force origin master 38 | rm -rf ./demo 39 | 40 | lint: 41 | eslint . 42 | 43 | test: lint 44 | mocha 45 | echo "CommonMark stat:\n" 46 | ./support/specsplit.js test/fixtures/commonmark/spec.txt 47 | 48 | coverage: 49 | rm -rf coverage 50 | istanbul cover node_modules/.bin/_mocha 51 | 52 | report-coverage: 53 | -istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 54 | 55 | doc: 56 | rm -rf ./apidoc 57 | ndoc --link-format "https://github.com/{package.repository}/blob/${CURR_HEAD}/{file}#L{line}" 58 | 59 | gh-doc: doc 60 | touch ./apidoc/.nojekyll 61 | cd ./apidoc \ 62 | && git init . \ 63 | && git add . \ 64 | && git commit -m "Auto-generate API doc" \ 65 | && git remote add remote git@github.com:markdown-it/markdown-it.git \ 66 | && git push --force remote +master:gh-pages 67 | rm -rf ./apidoc 68 | 69 | publish: 70 | @if test 0 -ne `git status --porcelain | wc -l` ; then \ 71 | echo "Unclean working tree. Commit or stash changes first." >&2 ; \ 72 | exit 128 ; \ 73 | fi 74 | @if test 0 -ne `git fetch ; git status | grep '^# Your branch' | wc -l` ; then \ 75 | echo "Local/Remote history differs. Please push/pull changes." >&2 ; \ 76 | exit 128 ; \ 77 | fi 78 | @if test 0 -ne `git tag -l ${NPM_VERSION} | wc -l` ; then \ 79 | echo "Tag ${NPM_VERSION} exists. Update package.json" >&2 ; \ 80 | exit 128 ; \ 81 | fi 82 | git tag ${NPM_VERSION} && git push origin ${NPM_VERSION} 83 | npm publish ${GITHUB_PROJ}/tarball/${NPM_VERSION} 84 | 85 | browserify: 86 | rm -rf ./dist 87 | mkdir dist 88 | # Browserify 89 | ( printf "/*! ${NPM_PACKAGE} ${NPM_VERSION} ${GITHUB_PROJ} @license MIT */" ; \ 90 | browserify ./ -s markdownit \ 91 | ) > dist/markdown-it.js 92 | # Minify 93 | uglifyjs dist/markdown-it.js -b beautify=false,ascii-only=true -c -m \ 94 | --preamble "/*! ${NPM_PACKAGE} ${NPM_VERSION} ${GITHUB_PROJ} @license MIT */" \ 95 | > dist/markdown-it.min.js 96 | 97 | benchmark-deps: 98 | npm install --prefix benchmark/extra/ -g marked@0.3.6 commonmark@0.26.0 markdown-it/markdown-it.git#2.2.1 99 | 100 | specsplit: 101 | ./support/specsplit.js good ./test/fixtures/commonmark/spec.txt > ./test/fixtures/commonmark/good.txt 102 | ./support/specsplit.js bad ./test/fixtures/commonmark/spec.txt > ./test/fixtures/commonmark/bad.txt 103 | ./support/specsplit.js ./test/fixtures/commonmark/spec.txt 104 | 105 | todo: 106 | grep 'TODO' -n -r ./lib 2>/dev/null || test true 107 | 108 | 109 | .PHONY: publish lint test todo demo coverage doc 110 | .SILENT: help lint test todo 111 | -------------------------------------------------------------------------------- /lib/rules_inline/state_inline.js: -------------------------------------------------------------------------------- 1 | // Inline parser state 2 | 3 | 'use strict'; 4 | 5 | 6 | var Token = require('../token'); 7 | var isWhiteSpace = require('../common/utils').isWhiteSpace; 8 | var isPunctChar = require('../common/utils').isPunctChar; 9 | var isMdAsciiPunct = require('../common/utils').isMdAsciiPunct; 10 | 11 | 12 | function StateInline(src, md, env, outTokens) { 13 | this.src = src; 14 | this.env = env; 15 | this.md = md; 16 | this.tokens = outTokens; 17 | 18 | this.pos = 0; 19 | this.posMax = this.src.length; 20 | this.level = 0; 21 | this.pending = ''; 22 | this.pendingLevel = 0; 23 | 24 | this.cache = {}; // Stores { start: end } pairs. Useful for backtrack 25 | // optimization of pairs parse (emphasis, strikes). 26 | 27 | this.delimiters = []; // Emphasis-like delimiters 28 | } 29 | 30 | 31 | // Flush pending text 32 | // 33 | StateInline.prototype.pushPending = function () { 34 | var token = new Token('text', '', 0); 35 | token.content = this.pending; 36 | token.level = this.pendingLevel; 37 | this.tokens.push(token); 38 | this.pending = ''; 39 | return token; 40 | }; 41 | 42 | 43 | // Push new token to "stream". 44 | // If pending text exists - flush it as text token 45 | // 46 | StateInline.prototype.push = function (type, tag, nesting) { 47 | if (this.pending) { 48 | this.pushPending(); 49 | } 50 | 51 | var token = new Token(type, tag, nesting); 52 | 53 | if (nesting < 0) { this.level--; } 54 | token.level = this.level; 55 | if (nesting > 0) { this.level++; } 56 | 57 | this.pendingLevel = this.level; 58 | this.tokens.push(token); 59 | return token; 60 | }; 61 | 62 | 63 | // Scan a sequence of emphasis-like markers, and determine whether 64 | // it can start an emphasis sequence or end an emphasis sequence. 65 | // 66 | // - start - position to scan from (it should point at a valid marker); 67 | // - canSplitWord - determine if these markers can be found inside a word 68 | // 69 | StateInline.prototype.scanDelims = function (start, canSplitWord) { 70 | var pos = start, lastChar, nextChar, count, can_open, can_close, 71 | isLastWhiteSpace, isLastPunctChar, 72 | isNextWhiteSpace, isNextPunctChar, 73 | left_flanking = true, 74 | right_flanking = true, 75 | max = this.posMax, 76 | marker = this.src.charCodeAt(start); 77 | 78 | // treat beginning of the line as a whitespace 79 | lastChar = start > 0 ? this.src.charCodeAt(start - 1) : 0x20; 80 | 81 | while (pos < max && this.src.charCodeAt(pos) === marker) { pos++; } 82 | 83 | count = pos - start; 84 | 85 | // treat end of the line as a whitespace 86 | nextChar = pos < max ? this.src.charCodeAt(pos) : 0x20; 87 | 88 | isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)); 89 | isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)); 90 | 91 | isLastWhiteSpace = isWhiteSpace(lastChar); 92 | isNextWhiteSpace = isWhiteSpace(nextChar); 93 | 94 | if (isNextWhiteSpace) { 95 | left_flanking = false; 96 | } else if (isNextPunctChar) { 97 | if (!(isLastWhiteSpace || isLastPunctChar)) { 98 | left_flanking = false; 99 | } 100 | } 101 | 102 | if (isLastWhiteSpace) { 103 | right_flanking = false; 104 | } else if (isLastPunctChar) { 105 | if (!(isNextWhiteSpace || isNextPunctChar)) { 106 | right_flanking = false; 107 | } 108 | } 109 | 110 | if (!canSplitWord) { 111 | can_open = left_flanking && (!right_flanking || isLastPunctChar); 112 | can_close = right_flanking && (!left_flanking || isNextPunctChar); 113 | } else { 114 | can_open = left_flanking; 115 | can_close = right_flanking; 116 | } 117 | 118 | return { 119 | can_open: can_open, 120 | can_close: can_close, 121 | length: count 122 | }; 123 | }; 124 | 125 | 126 | // re-export Token class to use in block rules 127 | StateInline.prototype.Token = Token; 128 | 129 | 130 | module.exports = StateInline; 131 | -------------------------------------------------------------------------------- /lib/rules_inline/emphasis.js: -------------------------------------------------------------------------------- 1 | // Process *this* and _that_ 2 | // 3 | 'use strict'; 4 | 5 | 6 | // Insert each marker as a separate text token, and add it to delimiter list 7 | // 8 | module.exports.tokenize = function emphasis(state, silent) { 9 | var i, scanned, token, 10 | start = state.pos, 11 | marker = state.src.charCodeAt(start); 12 | 13 | if (silent) { return false; } 14 | 15 | if (marker !== 0x5F /* _ */ && marker !== 0x2A /* * */) { return false; } 16 | 17 | scanned = state.scanDelims(state.pos, marker === 0x2A); 18 | 19 | for (i = 0; i < scanned.length; i++) { 20 | token = state.push('text', '', 0); 21 | token.content = String.fromCharCode(marker); 22 | 23 | state.delimiters.push({ 24 | // Char code of the starting marker (number). 25 | // 26 | marker: marker, 27 | 28 | // Total length of these series of delimiters. 29 | // 30 | length: scanned.length, 31 | 32 | // An amount of characters before this one that's equivalent to 33 | // current one. In plain English: if this delimiter does not open 34 | // an emphasis, neither do previous `jump` characters. 35 | // 36 | // Used to skip sequences like "*****" in one step, for 1st asterisk 37 | // value will be 0, for 2nd it's 1 and so on. 38 | // 39 | jump: i, 40 | 41 | // A position of the token this delimiter corresponds to. 42 | // 43 | token: state.tokens.length - 1, 44 | 45 | // Token level. 46 | // 47 | level: state.level, 48 | 49 | // If this delimiter is matched as a valid opener, `end` will be 50 | // equal to its position, otherwise it's `-1`. 51 | // 52 | end: -1, 53 | 54 | // Boolean flags that determine if this delimiter could open or close 55 | // an emphasis. 56 | // 57 | open: scanned.can_open, 58 | close: scanned.can_close 59 | }); 60 | } 61 | 62 | state.pos += scanned.length; 63 | 64 | return true; 65 | }; 66 | 67 | 68 | // Walk through delimiter list and replace text tokens with tags 69 | // 70 | module.exports.postProcess = function emphasis(state) { 71 | var i, 72 | startDelim, 73 | endDelim, 74 | token, 75 | ch, 76 | isStrong, 77 | delimiters = state.delimiters, 78 | max = state.delimiters.length; 79 | 80 | for (i = 0; i < max; i++) { 81 | startDelim = delimiters[i]; 82 | 83 | if (startDelim.marker !== 0x5F/* _ */ && startDelim.marker !== 0x2A/* * */) { 84 | continue; 85 | } 86 | 87 | // Process only opening markers 88 | if (startDelim.end === -1) { 89 | continue; 90 | } 91 | 92 | endDelim = delimiters[startDelim.end]; 93 | 94 | // If the next delimiter has the same marker and is adjacent to this one, 95 | // merge those into one strong delimiter. 96 | // 97 | // `whatever` -> `whatever` 98 | // 99 | isStrong = i + 1 < max && 100 | delimiters[i + 1].end === startDelim.end - 1 && 101 | delimiters[i + 1].token === startDelim.token + 1 && 102 | delimiters[startDelim.end - 1].token === endDelim.token - 1 && 103 | delimiters[i + 1].marker === startDelim.marker; 104 | 105 | ch = String.fromCharCode(startDelim.marker); 106 | 107 | token = state.tokens[startDelim.token]; 108 | token.type = isStrong ? 'strong_open' : 'em_open'; 109 | token.tag = isStrong ? 'strong' : 'em'; 110 | token.nesting = 1; 111 | token.markup = isStrong ? ch + ch : ch; 112 | token.content = ''; 113 | 114 | token = state.tokens[endDelim.token]; 115 | token.type = isStrong ? 'strong_close' : 'em_close'; 116 | token.tag = isStrong ? 'strong' : 'em'; 117 | token.nesting = -1; 118 | token.markup = isStrong ? ch + ch : ch; 119 | token.content = ''; 120 | 121 | if (isStrong) { 122 | state.tokens[delimiters[i + 1].token].content = ''; 123 | state.tokens[delimiters[startDelim.end - 1].token].content = ''; 124 | i++; 125 | } 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development recommendations 2 | 3 | Before continuing, make sure you've read: 4 | 5 | 1. [README](https://github.com/markdown-it/markdown-it#markdown-it) 6 | 2. [API documentation](https://markdown-it.github.io/markdown-it/) 7 | 3. [Architecture description](architecture.md) 8 | 9 | 10 | ## General considerations for plugins. 11 | 12 | 1. Try to find the right place for your plugin rule: 13 | - Will it conflict with existing markup (by priority)? 14 | - If yes - you need to write an inline or block rule. 15 | - If no - you can morph tokens within core chains. 16 | - Remember that token morphing in core chains is always more simple than writing 17 | block or inline rules, if you don't copy existing ones. However, 18 | block and inline rules are usually faster. 19 | - Sometimes, it's enough to only modify the renderer, for example, to add 20 | header IDs or `target="_blank"` for the links. 21 | - Plugins should not require the `markdown-it` package as dependency in `package.json`. 22 | If you need access to internals, those are available via a parser instance, 23 | passed on plugin load. See properties of main class and nested objects. 24 | 2. Search existing 25 | [plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin) 26 | or [rules](https://github.com/markdown-it/markdown-it/tree/master/lib), 27 | doing something similar. It can be more simple to modify existing code, 28 | instead of writing all from scratch. 29 | 3. If you did all steps above, but still has questions - ask in 30 | [tracker](https://github.com/markdown-it/markdown-it/issues). But, please: 31 | - Be specific. Generic questions like "how to do plugins" and 32 | "how to learn programming" are not accepted. 33 | - Don't ask us to break [CommonMark](http://commonmark.org/) specification. 34 | Such things should be discussed first on [CommonMark forum](http://talk.commonmark.org/). 35 | 36 | 37 | ## Notes for NPM packages 38 | 39 | To simplify search: 40 | 41 | - add to `package.json` keyswords `markdown-it` and `markdown-it-plugin` for plugins. 42 | - add keyword `markdown-it` for any other related packages. 43 | 44 | 45 | ## FAQ 46 | 47 | 48 | #### I need async rule, how to do it? 49 | 50 | Sorry. You can't do it directly. All complex parsers are sync by nature. But you 51 | can use workarounds: 52 | 53 | 1. On parse phase, replace content by random number and store it in `env`. 54 | 2. Do async processing over collected data. 55 | 3. Render content and replace those random numbers with text; or replace first, then render. 56 | 57 | Alternatively, you can render HTML, then parse it to DOM, or 58 | [cheerio](https://github.com/cheeriojs/cheerio) AST, and apply transformations 59 | in a more convenient way. 60 | 61 | 62 | #### How to replace part of text token with link? 63 | 64 | The right sequence is to split text to several tokens and add link tokens in between. 65 | The result will be: `text` + `link_open` + `text` + `link_close` + `text`. 66 | 67 | See implementations of [linkify](https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/linkify.js) and [emoji](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/replace.js) - those do text token splits. 68 | 69 | __Note.__ Don't try to replace text with HTML markup! That's not secure. 70 | 71 | 72 | #### Why my inline rule is not executed? 73 | 74 | The inline parser skips pieces of texts to optimize speed. It stops only on [a small set of chars](https://github.com/markdown-it/markdown-it/blob/master/lib/rules_inline/text.js), which can be tokens. We did not made this list extensible for performance reasons too. 75 | 76 | If you are absolutely sure that something important is missing there - create a 77 | ticket and we will consider adding it as a new charcode. 78 | 79 | 80 | #### Why do you reject some useful things? 81 | 82 | We do a markdown parser. It should keep the "markdown spirit". Other things should 83 | be kept separate, in plugins, for example. We have no clear criteria, sorry. 84 | Probably, you will find [CommonMark forum](http://talk.commonmark.org/) a useful read to understand us better. 85 | 86 | Of course, if you find the architecture of this parser interesting for another type 87 | of markup, you are welcome to reuse it in another project. 88 | -------------------------------------------------------------------------------- /support/demo_template/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title markdown-it demo 5 | meta(charset='UTF-8') 6 | meta(http-equiv='X-UA-Compatible' content='IE=edge') 7 | meta(name='viewport' content='width=device-width, initial-scale=1') 8 | 9 | script(src='https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.5/es5-shim.min.js') 10 | 11 | script(src='https://cdn.jsdelivr.net/jquery/1.11.1/jquery.min.js') 12 | script(src='https://cdn.jsdelivr.net/lodash/2.4.1/lodash.js') 13 | 14 | script(src='https://cdn.jsdelivr.net/bootstrap/3.2.0/js/bootstrap.min.js') 15 | link(rel='stylesheet' href='https://cdn.jsdelivr.net/bootstrap/3.2.0/css/bootstrap.css') 16 | 17 | link(rel='stylesheet' href='https://cdn.jsdelivr.net/highlight.js/9.1.0/styles/github.min.css') 18 | 19 | script(src='markdown-it.js') 20 | script(src='https://twemoji.maxcdn.com/twemoji.min.js') 21 | 22 | link(rel='stylesheet' href='index.css') 23 | script(src='index.js') 24 | 25 | // Ancient IE support - load shiv & kill broken highlighter 26 | 30 | 31 | // GA counter 32 | script. 33 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 34 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 35 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 36 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 37 | 38 | ga('create', 'UA-26895916-4', 'auto'); 39 | ga('require', 'displayfeatures'); 40 | ga('require', 'linkid', 'linkid.js'); 41 | ga('send', 'pageview'); 42 | 43 | body 44 | .container 45 | - var s = self.self.demo; 46 | h1 markdown-it 47 | | 48 | small demo 49 | 50 | .form-inline.demo-options 51 | .checkbox.not-strict 52 | label._tip(title='enable html tags in source text') 53 | input#html(type='checkbox') 54 | | html 55 | .checkbox.not-strict 56 | label._tip(title='produce xtml output (add / to single tags (
instead of
)') 57 | input#xhtmlOut(type='checkbox') 58 | | xhtmlOut 59 | .checkbox.not-strict 60 | label._tip(title='newlines in paragraphs are rendered as
') 61 | input#breaks(type='checkbox') 62 | | breaks 63 | .checkbox.not-strict 64 | label._tip(title='autoconvert link-like texts to links') 65 | input#linkify(type='checkbox') 66 | | linkify 67 | .checkbox.not-strict 68 | label._tip(title='do typographyc replacements, (c) -> © and so on') 69 | input#typographer(type='checkbox') 70 | | typographer 71 | .checkbox.not-strict 72 | label._tip(title='enable output highlight for fenced blocks') 73 | input#_highlight(type='checkbox') 74 | | highlight 75 | .form-group.not-strict 76 | input#langPrefix.form-control._tip( 77 | type='input' 78 | placeholder='language prefix' 79 | title='css class language prefix for fenced code blocks' 80 | ) 81 | .checkbox 82 | label._tip(title='force strict CommonMark mode - output will be equal to reference parser') 83 | input#_strict(type='checkbox') 84 | | CommonMark strict 85 | 86 | .container.full-height 87 | .row.full-height 88 | .col-xs-6.full-height 89 | .demo-control 90 | a.source-clear(href='#') clear 91 | a#permalink(href='./' title='Share this snippet as link'): strong permalink 92 | textarea.source.full-height= s.source 93 | section.col-xs-6.full-height 94 | .demo-control 95 | a(href='#' data-result-as='html') html 96 | a(href='#' data-result-as='src') source 97 | a(href='#' data-result-as='debug') debug 98 | .result-html.full-height 99 | pre.hljs.result-src.full-height 100 | code.result-src-content.full-height 101 | pre.hljs.result-debug.full-height 102 | code.result-debug-content.full-height 103 | 104 | .gh-ribbon 105 | a(href='https://github.com/markdown-it/markdown-it' target='_blank') Fork me on GitHub 106 | -------------------------------------------------------------------------------- /test/ruler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('chai').assert; 5 | var Ruler = require('../lib/ruler'); 6 | 7 | describe('Ruler', function () { 8 | 9 | it('should replace rule (.at)', function () { 10 | var ruler = new Ruler(); 11 | var res = 0; 12 | 13 | ruler.push('test', function foo() { res = 1; }); 14 | ruler.at('test', function bar() { res = 2; }); 15 | 16 | var rules = ruler.getRules(''); 17 | 18 | assert.strictEqual(rules.length, 1); 19 | rules[0](); 20 | assert.strictEqual(res, 2); 21 | }); 22 | 23 | 24 | it('should inject before/after rule', function () { 25 | var ruler = new Ruler(); 26 | var res = 0; 27 | 28 | ruler.push('test', function foo() { res = 1; }); 29 | ruler.before('test', 'before_test', function fooBefore() { res = -10; }); 30 | ruler.after('test', 'after_test', function fooAfter() { res = 10; }); 31 | 32 | var rules = ruler.getRules(''); 33 | 34 | assert.strictEqual(rules.length, 3); 35 | rules[0](); 36 | assert.strictEqual(res, -10); 37 | rules[1](); 38 | assert.strictEqual(res, 1); 39 | rules[2](); 40 | assert.strictEqual(res, 10); 41 | }); 42 | 43 | 44 | it('should enable/disable rule', function () { 45 | var rules, ruler = new Ruler(); 46 | 47 | ruler.push('test', function foo() {}); 48 | ruler.push('test2', function bar() {}); 49 | 50 | rules = ruler.getRules(''); 51 | assert.strictEqual(rules.length, 2); 52 | 53 | ruler.disable('test'); 54 | rules = ruler.getRules(''); 55 | assert.strictEqual(rules.length, 1); 56 | ruler.disable('test2'); 57 | rules = ruler.getRules(''); 58 | assert.strictEqual(rules.length, 0); 59 | 60 | ruler.enable('test'); 61 | rules = ruler.getRules(''); 62 | assert.strictEqual(rules.length, 1); 63 | ruler.enable('test2'); 64 | rules = ruler.getRules(''); 65 | assert.strictEqual(rules.length, 2); 66 | }); 67 | 68 | 69 | it('should enable/disable multiple rule', function () { 70 | var rules, ruler = new Ruler(); 71 | 72 | ruler.push('test', function foo() {}); 73 | ruler.push('test2', function bar() {}); 74 | 75 | ruler.disable([ 'test', 'test2' ]); 76 | rules = ruler.getRules(''); 77 | assert.strictEqual(rules.length, 0); 78 | ruler.enable([ 'test', 'test2' ]); 79 | rules = ruler.getRules(''); 80 | assert.strictEqual(rules.length, 2); 81 | }); 82 | 83 | 84 | it('should enable rules by whitelist', function () { 85 | var rules, ruler = new Ruler(); 86 | 87 | ruler.push('test', function foo() {}); 88 | ruler.push('test2', function bar() {}); 89 | 90 | ruler.enableOnly('test'); 91 | rules = ruler.getRules(''); 92 | assert.strictEqual(rules.length, 1); 93 | }); 94 | 95 | 96 | it('should support multiple chains', function () { 97 | var rules, ruler = new Ruler(); 98 | 99 | ruler.push('test', function foo() {}); 100 | ruler.push('test2', function bar() {}, { alt: [ 'alt1' ] }); 101 | ruler.push('test2', function bar() {}, { alt: [ 'alt1', 'alt2' ] }); 102 | 103 | rules = ruler.getRules(''); 104 | assert.strictEqual(rules.length, 3); 105 | rules = ruler.getRules('alt1'); 106 | assert.strictEqual(rules.length, 2); 107 | rules = ruler.getRules('alt2'); 108 | assert.strictEqual(rules.length, 1); 109 | }); 110 | 111 | 112 | it('should fail on invalid rule name', function () { 113 | var ruler = new Ruler(); 114 | 115 | ruler.push('test', function foo() {}); 116 | 117 | assert.throws(function () { 118 | ruler.at('invalid name', function bar() {}); 119 | }); 120 | assert.throws(function () { 121 | ruler.before('invalid name', function bar() {}); 122 | }); 123 | assert.throws(function () { 124 | ruler.after('invalid name', function bar() {}); 125 | }); 126 | assert.throws(function () { 127 | ruler.enable('invalid name'); 128 | }); 129 | assert.throws(function () { 130 | ruler.disable('invalid name'); 131 | }); 132 | }); 133 | 134 | 135 | it('should not fail on invalid rule name in silent mode', function () { 136 | var ruler = new Ruler(); 137 | 138 | ruler.push('test', function foo() {}); 139 | 140 | assert.doesNotThrow(function () { 141 | ruler.enable('invalid name', true); 142 | }); 143 | assert.doesNotThrow(function () { 144 | ruler.enableOnly('invalid name', true); 145 | }); 146 | assert.doesNotThrow(function () { 147 | ruler.disable('invalid name', true); 148 | }); 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /lib/rules_inline/image.js: -------------------------------------------------------------------------------- 1 | // Process ![image]( "title") 2 | 3 | 'use strict'; 4 | 5 | var normalizeReference = require('../common/utils').normalizeReference; 6 | var isSpace = require('../common/utils').isSpace; 7 | 8 | 9 | module.exports = function image(state, silent) { 10 | var attrs, 11 | code, 12 | content, 13 | label, 14 | labelEnd, 15 | labelStart, 16 | pos, 17 | ref, 18 | res, 19 | title, 20 | token, 21 | tokens, 22 | start, 23 | href = '', 24 | oldPos = state.pos, 25 | max = state.posMax; 26 | 27 | if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false; } 28 | if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false; } 29 | 30 | labelStart = state.pos + 2; 31 | labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false); 32 | 33 | // parser failed to find ']', so it's not a valid link 34 | if (labelEnd < 0) { return false; } 35 | 36 | pos = labelEnd + 1; 37 | if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { 38 | // 39 | // Inline link 40 | // 41 | 42 | // [link]( "title" ) 43 | // ^^ skipping these spaces 44 | pos++; 45 | for (; pos < max; pos++) { 46 | code = state.src.charCodeAt(pos); 47 | if (!isSpace(code) && code !== 0x0A) { break; } 48 | } 49 | if (pos >= max) { return false; } 50 | 51 | // [link]( "title" ) 52 | // ^^^^^^ parsing link destination 53 | start = pos; 54 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax); 55 | if (res.ok) { 56 | href = state.md.normalizeLink(res.str); 57 | if (state.md.validateLink(href)) { 58 | pos = res.pos; 59 | } else { 60 | href = ''; 61 | } 62 | } 63 | 64 | // [link]( "title" ) 65 | // ^^ skipping these spaces 66 | start = pos; 67 | for (; pos < max; pos++) { 68 | code = state.src.charCodeAt(pos); 69 | if (!isSpace(code) && code !== 0x0A) { break; } 70 | } 71 | 72 | // [link]( "title" ) 73 | // ^^^^^^^ parsing link title 74 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax); 75 | if (pos < max && start !== pos && res.ok) { 76 | title = res.str; 77 | pos = res.pos; 78 | 79 | // [link]( "title" ) 80 | // ^^ skipping these spaces 81 | for (; pos < max; pos++) { 82 | code = state.src.charCodeAt(pos); 83 | if (!isSpace(code) && code !== 0x0A) { break; } 84 | } 85 | } else { 86 | title = ''; 87 | } 88 | 89 | if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { 90 | state.pos = oldPos; 91 | return false; 92 | } 93 | pos++; 94 | } else { 95 | // 96 | // Link reference 97 | // 98 | if (typeof state.env.references === 'undefined') { return false; } 99 | 100 | if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) { 101 | start = pos + 1; 102 | pos = state.md.helpers.parseLinkLabel(state, pos); 103 | if (pos >= 0) { 104 | label = state.src.slice(start, pos++); 105 | } else { 106 | pos = labelEnd + 1; 107 | } 108 | } else { 109 | pos = labelEnd + 1; 110 | } 111 | 112 | // covers label === '' and label === undefined 113 | // (collapsed reference link and shortcut reference link respectively) 114 | if (!label) { label = state.src.slice(labelStart, labelEnd); } 115 | 116 | ref = state.env.references[normalizeReference(label)]; 117 | if (!ref) { 118 | state.pos = oldPos; 119 | return false; 120 | } 121 | href = ref.href; 122 | title = ref.title; 123 | } 124 | 125 | // 126 | // We found the end of the link, and know for a fact it's a valid link; 127 | // so all that's left to do is to call tokenizer. 128 | // 129 | if (!silent) { 130 | content = state.src.slice(labelStart, labelEnd); 131 | 132 | state.md.inline.parse( 133 | content, 134 | state.md, 135 | state.env, 136 | tokens = [] 137 | ); 138 | 139 | token = state.push('image', 'img', 0); 140 | token.attrs = attrs = [ [ 'src', href ], [ 'alt', '' ] ]; 141 | token.children = tokens; 142 | token.content = content; 143 | 144 | if (title) { 145 | attrs.push([ 'title', title ]); 146 | } 147 | } 148 | 149 | state.pos = pos; 150 | state.posMax = max; 151 | return true; 152 | }; 153 | -------------------------------------------------------------------------------- /lib/rules_inline/link.js: -------------------------------------------------------------------------------- 1 | // Process [link]( "stuff") 2 | 3 | 'use strict'; 4 | 5 | var normalizeReference = require('../common/utils').normalizeReference; 6 | var isSpace = require('../common/utils').isSpace; 7 | 8 | 9 | module.exports = function link(state, silent) { 10 | var attrs, 11 | code, 12 | label, 13 | labelEnd, 14 | labelStart, 15 | pos, 16 | res, 17 | ref, 18 | title, 19 | token, 20 | href = '', 21 | oldPos = state.pos, 22 | max = state.posMax, 23 | start = state.pos, 24 | parseReference = true; 25 | 26 | if (state.src.charCodeAt(state.pos) !== 0x5B/* [ */) { return false; } 27 | 28 | labelStart = state.pos + 1; 29 | labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, true); 30 | 31 | // parser failed to find ']', so it's not a valid link 32 | if (labelEnd < 0) { return false; } 33 | 34 | pos = labelEnd + 1; 35 | if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { 36 | // 37 | // Inline link 38 | // 39 | 40 | // might have found a valid shortcut link, disable reference parsing 41 | parseReference = false; 42 | 43 | // [link]( "title" ) 44 | // ^^ skipping these spaces 45 | pos++; 46 | for (; pos < max; pos++) { 47 | code = state.src.charCodeAt(pos); 48 | if (!isSpace(code) && code !== 0x0A) { break; } 49 | } 50 | if (pos >= max) { return false; } 51 | 52 | // [link]( "title" ) 53 | // ^^^^^^ parsing link destination 54 | start = pos; 55 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax); 56 | if (res.ok) { 57 | href = state.md.normalizeLink(res.str); 58 | if (state.md.validateLink(href)) { 59 | pos = res.pos; 60 | } else { 61 | href = ''; 62 | } 63 | } 64 | 65 | // [link]( "title" ) 66 | // ^^ skipping these spaces 67 | start = pos; 68 | for (; pos < max; pos++) { 69 | code = state.src.charCodeAt(pos); 70 | if (!isSpace(code) && code !== 0x0A) { break; } 71 | } 72 | 73 | // [link]( "title" ) 74 | // ^^^^^^^ parsing link title 75 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax); 76 | if (pos < max && start !== pos && res.ok) { 77 | title = res.str; 78 | pos = res.pos; 79 | 80 | // [link]( "title" ) 81 | // ^^ skipping these spaces 82 | for (; pos < max; pos++) { 83 | code = state.src.charCodeAt(pos); 84 | if (!isSpace(code) && code !== 0x0A) { break; } 85 | } 86 | } else { 87 | title = ''; 88 | } 89 | 90 | if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { 91 | // parsing a valid shortcut link failed, fallback to reference 92 | parseReference = true; 93 | } 94 | pos++; 95 | } 96 | 97 | if (parseReference) { 98 | // 99 | // Link reference 100 | // 101 | if (typeof state.env.references === 'undefined') { return false; } 102 | 103 | if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) { 104 | start = pos + 1; 105 | pos = state.md.helpers.parseLinkLabel(state, pos); 106 | if (pos >= 0) { 107 | label = state.src.slice(start, pos++); 108 | } else { 109 | pos = labelEnd + 1; 110 | } 111 | } else { 112 | pos = labelEnd + 1; 113 | } 114 | 115 | // covers label === '' and label === undefined 116 | // (collapsed reference link and shortcut reference link respectively) 117 | if (!label) { label = state.src.slice(labelStart, labelEnd); } 118 | 119 | ref = state.env.references[normalizeReference(label)]; 120 | if (!ref) { 121 | state.pos = oldPos; 122 | return false; 123 | } 124 | href = ref.href; 125 | title = ref.title; 126 | } 127 | 128 | // 129 | // We found the end of the link, and know for a fact it's a valid link; 130 | // so all that's left to do is to call tokenizer. 131 | // 132 | if (!silent) { 133 | state.pos = labelStart; 134 | state.posMax = labelEnd; 135 | 136 | token = state.push('link_open', 'a', 1); 137 | token.attrs = attrs = [ [ 'href', href ] ]; 138 | if (title) { 139 | attrs.push([ 'title', title ]); 140 | } 141 | 142 | state.md.inline.tokenize(state); 143 | 144 | token = state.push('link_close', 'a', -1); 145 | } 146 | 147 | state.pos = pos; 148 | state.posMax = max; 149 | return true; 150 | }; 151 | -------------------------------------------------------------------------------- /lib/rules_core/linkify.js: -------------------------------------------------------------------------------- 1 | // Replace link-like texts with link nodes. 2 | // 3 | // Currently restricted by `md.validateLink()` to http/https/ftp 4 | // 5 | 'use strict'; 6 | 7 | 8 | var arrayReplaceAt = require('../common/utils').arrayReplaceAt; 9 | 10 | 11 | function isLinkOpen(str) { 12 | return /^\s]/i.test(str); 13 | } 14 | function isLinkClose(str) { 15 | return /^<\/a\s*>/i.test(str); 16 | } 17 | 18 | 19 | module.exports = function linkify(state) { 20 | var i, j, l, tokens, token, currentToken, nodes, ln, text, pos, lastPos, 21 | level, htmlLinkLevel, url, fullUrl, urlText, 22 | blockTokens = state.tokens, 23 | links; 24 | 25 | if (!state.md.options.linkify) { return; } 26 | 27 | for (j = 0, l = blockTokens.length; j < l; j++) { 28 | if (blockTokens[j].type !== 'inline' || 29 | !state.md.linkify.pretest(blockTokens[j].content)) { 30 | continue; 31 | } 32 | 33 | tokens = blockTokens[j].children; 34 | 35 | htmlLinkLevel = 0; 36 | 37 | // We scan from the end, to keep position when new tags added. 38 | // Use reversed logic in links start/end match 39 | for (i = tokens.length - 1; i >= 0; i--) { 40 | currentToken = tokens[i]; 41 | 42 | // Skip content of markdown links 43 | if (currentToken.type === 'link_close') { 44 | i--; 45 | while (tokens[i].level !== currentToken.level && tokens[i].type !== 'link_open') { 46 | i--; 47 | } 48 | continue; 49 | } 50 | 51 | // Skip content of html tag links 52 | if (currentToken.type === 'html_inline') { 53 | if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) { 54 | htmlLinkLevel--; 55 | } 56 | if (isLinkClose(currentToken.content)) { 57 | htmlLinkLevel++; 58 | } 59 | } 60 | if (htmlLinkLevel > 0) { continue; } 61 | 62 | if (currentToken.type === 'text' && state.md.linkify.test(currentToken.content)) { 63 | 64 | text = currentToken.content; 65 | links = state.md.linkify.match(text); 66 | 67 | // Now split string to nodes 68 | nodes = []; 69 | level = currentToken.level; 70 | lastPos = 0; 71 | 72 | for (ln = 0; ln < links.length; ln++) { 73 | 74 | url = links[ln].url; 75 | fullUrl = state.md.normalizeLink(url); 76 | if (!state.md.validateLink(fullUrl)) { continue; } 77 | 78 | urlText = links[ln].text; 79 | 80 | // Linkifier might send raw hostnames like "example.com", where url 81 | // starts with domain name. So we prepend http:// in those cases, 82 | // and remove it afterwards. 83 | // 84 | if (!links[ln].schema) { 85 | urlText = state.md.normalizeLinkText('http://' + urlText).replace(/^http:\/\//, ''); 86 | } else if (links[ln].schema === 'mailto:' && !/^mailto:/i.test(urlText)) { 87 | urlText = state.md.normalizeLinkText('mailto:' + urlText).replace(/^mailto:/, ''); 88 | } else { 89 | urlText = state.md.normalizeLinkText(urlText); 90 | } 91 | 92 | pos = links[ln].index; 93 | 94 | if (pos > lastPos) { 95 | token = new state.Token('text', '', 0); 96 | token.content = text.slice(lastPos, pos); 97 | token.level = level; 98 | nodes.push(token); 99 | } 100 | 101 | token = new state.Token('link_open', 'a', 1); 102 | token.attrs = [ [ 'href', fullUrl ] ]; 103 | token.level = level++; 104 | token.markup = 'linkify'; 105 | token.info = 'auto'; 106 | nodes.push(token); 107 | 108 | token = new state.Token('text', '', 0); 109 | token.content = urlText; 110 | token.level = level; 111 | nodes.push(token); 112 | 113 | token = new state.Token('link_close', 'a', -1); 114 | token.level = --level; 115 | token.markup = 'linkify'; 116 | token.info = 'auto'; 117 | nodes.push(token); 118 | 119 | lastPos = links[ln].lastIndex; 120 | } 121 | if (lastPos < text.length) { 122 | token = new state.Token('text', '', 0); 123 | token.content = text.slice(lastPos); 124 | token.level = level; 125 | nodes.push(token); 126 | } 127 | 128 | // replace current node 129 | blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes); 130 | } 131 | } 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /lib/token.js: -------------------------------------------------------------------------------- 1 | // Token class 2 | 3 | 'use strict'; 4 | 5 | 6 | /** 7 | * class Token 8 | **/ 9 | 10 | /** 11 | * new Token(type, tag, nesting) 12 | * 13 | * Create new token and fill passed properties. 14 | **/ 15 | function Token(type, tag, nesting) { 16 | /** 17 | * Token#type -> String 18 | * 19 | * Type of the token (string, e.g. "paragraph_open") 20 | **/ 21 | this.type = type; 22 | 23 | /** 24 | * Token#tag -> String 25 | * 26 | * html tag name, e.g. "p" 27 | **/ 28 | this.tag = tag; 29 | 30 | /** 31 | * Token#attrs -> Array 32 | * 33 | * Html attributes. Format: `[ [ name1, value1 ], [ name2, value2 ] ]` 34 | **/ 35 | this.attrs = null; 36 | 37 | /** 38 | * Token#map -> Array 39 | * 40 | * Source map info. Format: `[ line_begin, line_end ]` 41 | **/ 42 | this.map = null; 43 | 44 | /** 45 | * Token#nesting -> Number 46 | * 47 | * Level change (number in {-1, 0, 1} set), where: 48 | * 49 | * - `1` means the tag is opening 50 | * - `0` means the tag is self-closing 51 | * - `-1` means the tag is closing 52 | **/ 53 | this.nesting = nesting; 54 | 55 | /** 56 | * Token#level -> Number 57 | * 58 | * nesting level, the same as `state.level` 59 | **/ 60 | this.level = 0; 61 | 62 | /** 63 | * Token#children -> Array 64 | * 65 | * An array of child nodes (inline and img tokens) 66 | **/ 67 | this.children = null; 68 | 69 | /** 70 | * Token#content -> String 71 | * 72 | * In a case of self-closing tag (code, html, fence, etc.), 73 | * it has contents of this tag. 74 | **/ 75 | this.content = ''; 76 | 77 | /** 78 | * Token#markup -> String 79 | * 80 | * '*' or '_' for emphasis, fence string for fence, etc. 81 | **/ 82 | this.markup = ''; 83 | 84 | /** 85 | * Token#info -> String 86 | * 87 | * fence infostring 88 | **/ 89 | this.info = ''; 90 | 91 | /** 92 | * Token#meta -> Object 93 | * 94 | * A place for plugins to store an arbitrary data 95 | **/ 96 | this.meta = null; 97 | 98 | /** 99 | * Token#block -> Boolean 100 | * 101 | * True for block-level tokens, false for inline tokens. 102 | * Used in renderer to calculate line breaks 103 | **/ 104 | this.block = false; 105 | 106 | /** 107 | * Token#hidden -> Boolean 108 | * 109 | * If it's true, ignore this element when rendering. Used for tight lists 110 | * to hide paragraphs. 111 | **/ 112 | this.hidden = false; 113 | } 114 | 115 | 116 | /** 117 | * Token.attrIndex(name) -> Number 118 | * 119 | * Search attribute index by name. 120 | **/ 121 | Token.prototype.attrIndex = function attrIndex(name) { 122 | var attrs, i, len; 123 | 124 | if (!this.attrs) { return -1; } 125 | 126 | attrs = this.attrs; 127 | 128 | for (i = 0, len = attrs.length; i < len; i++) { 129 | if (attrs[i][0] === name) { return i; } 130 | } 131 | return -1; 132 | }; 133 | 134 | 135 | /** 136 | * Token.attrPush(attrData) 137 | * 138 | * Add `[ name, value ]` attribute to list. Init attrs if necessary 139 | **/ 140 | Token.prototype.attrPush = function attrPush(attrData) { 141 | if (this.attrs) { 142 | this.attrs.push(attrData); 143 | } else { 144 | this.attrs = [ attrData ]; 145 | } 146 | }; 147 | 148 | 149 | /** 150 | * Token.attrSet(name, value) 151 | * 152 | * Set `name` attribute to `value`. Override old value if exists. 153 | **/ 154 | Token.prototype.attrSet = function attrSet(name, value) { 155 | var idx = this.attrIndex(name), 156 | attrData = [ name, value ]; 157 | 158 | if (idx < 0) { 159 | this.attrPush(attrData); 160 | } else { 161 | this.attrs[idx] = attrData; 162 | } 163 | }; 164 | 165 | 166 | /** 167 | * Token.attrGet(name) 168 | * 169 | * Get the value of attribute `name`, or null if it does not exist. 170 | **/ 171 | Token.prototype.attrGet = function attrGet(name) { 172 | var idx = this.attrIndex(name), value = null; 173 | if (idx >= 0) { 174 | value = this.attrs[idx][1]; 175 | } 176 | return value; 177 | }; 178 | 179 | 180 | /** 181 | * Token.attrJoin(name, value) 182 | * 183 | * Join value to existing attribute via space. Or create new attribute if not 184 | * exists. Useful to operate with token classes. 185 | **/ 186 | Token.prototype.attrJoin = function attrJoin(name, value) { 187 | var idx = this.attrIndex(name); 188 | 189 | if (idx < 0) { 190 | this.attrPush([ name, value ]); 191 | } else { 192 | this.attrs[idx][1] = this.attrs[idx][1] + ' ' + value; 193 | } 194 | }; 195 | 196 | 197 | module.exports = Token; 198 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | browser: false 4 | es6: false 5 | 6 | rules: 7 | accessor-pairs: 2 8 | array-bracket-spacing: [ 2, "always", { "singleValue": true, "objectsInArrays": true, "arraysInArrays": true } ] 9 | block-scoped-var: 2 10 | block-spacing: 2 11 | brace-style: [ 2, '1tbs', { allowSingleLine: true } ] 12 | # Postponed 13 | #callback-return: 2 14 | comma-dangle: 2 15 | comma-spacing: 2 16 | comma-style: 2 17 | computed-property-spacing: [ 2, never ] 18 | consistent-this: [ 2, self ] 19 | consistent-return: 2 20 | # ? change to multi 21 | curly: [ 2, 'multi-line' ] 22 | dot-notation: 2 23 | eol-last: 2 24 | eqeqeq: 2 25 | func-style: [ 2, declaration ] 26 | # Postponed 27 | #global-require: 2 28 | guard-for-in: 2 29 | handle-callback-err: 2 30 | 31 | indent: [ 2, 2, { VariableDeclarator: { var: 2, let: 2, const: 3 }, SwitchCase: 1 } ] 32 | 33 | # key-spacing: [ 2, { "align": "value" } ] 34 | keyword-spacing: 2 35 | linebreak-style: 2 36 | max-depth: [ 1, 6 ] 37 | max-nested-callbacks: [ 1, 4 ] 38 | # string can exceed 80 chars, but should not overflow github website :) 39 | max-len: [ 2, 120, 1000 ] 40 | new-cap: 2 41 | new-parens: 2 42 | # Postponed 43 | #newline-after-var: 2 44 | no-alert: 2 45 | no-array-constructor: 2 46 | no-bitwise: 2 47 | no-caller: 2 48 | #no-case-declarations: 2 49 | no-catch-shadow: 2 50 | no-cond-assign: 2 51 | no-console: 1 52 | no-constant-condition: 2 53 | #no-control-regex: 2 54 | no-debugger: 2 55 | no-delete-var: 2 56 | no-div-regex: 2 57 | no-dupe-args: 2 58 | no-dupe-keys: 2 59 | no-duplicate-case: 2 60 | no-else-return: 2 61 | # Tend to drop 62 | # no-empty: 1 63 | no-empty-character-class: 2 64 | no-empty-pattern: 2 65 | no-eq-null: 2 66 | no-eval: 2 67 | no-ex-assign: 2 68 | no-extend-native: 2 69 | no-extra-bind: 2 70 | no-extra-boolean-cast: 2 71 | no-extra-semi: 2 72 | no-fallthrough: 2 73 | no-floating-decimal: 2 74 | no-func-assign: 2 75 | # Postponed 76 | #no-implicit-coercion: [2, { "boolean": true, "number": true, "string": true } ] 77 | no-implied-eval: 2 78 | no-inner-declarations: 2 79 | no-invalid-regexp: 2 80 | no-irregular-whitespace: 2 81 | no-iterator: 2 82 | no-label-var: 2 83 | no-labels: 2 84 | no-lone-blocks: 2 85 | no-lonely-if: 2 86 | no-loop-func: 2 87 | no-mixed-requires: 2 88 | no-mixed-spaces-and-tabs: 2 89 | # Postponed 90 | #no-native-reassign: 2 91 | no-negated-in-lhs: 2 92 | # Postponed 93 | #no-nested-ternary: 2 94 | no-new: 2 95 | no-new-func: 2 96 | no-new-object: 2 97 | no-new-require: 2 98 | no-new-wrappers: 2 99 | no-obj-calls: 2 100 | no-octal: 2 101 | no-octal-escape: 2 102 | no-path-concat: 2 103 | no-proto: 2 104 | no-redeclare: 2 105 | # Postponed 106 | #no-regex-spaces: 2 107 | no-return-assign: 2 108 | no-self-compare: 2 109 | no-sequences: 2 110 | no-shadow: 2 111 | no-shadow-restricted-names: 2 112 | no-sparse-arrays: 2 113 | no-trailing-spaces: 2 114 | no-undef: 2 115 | no-undef-init: 2 116 | no-undefined: 2 117 | no-unexpected-multiline: 2 118 | no-unreachable: 2 119 | no-unused-expressions: 2 120 | no-unused-vars: 2 121 | no-use-before-define: 2 122 | no-void: 2 123 | no-with: 2 124 | object-curly-spacing: [ 2, always, { "objectsInObjects": true, "arraysInObjects": true } ] 125 | operator-assignment: 1 126 | # Postponed 127 | #operator-linebreak: [ 2, after ] 128 | semi: 2 129 | semi-spacing: 2 130 | space-before-function-paren: [ 2, { "anonymous": "always", "named": "never" } ] 131 | space-in-parens: [ 2, never ] 132 | space-infix-ops: 2 133 | space-unary-ops: 2 134 | # Postponed 135 | #spaced-comment: [ 1, always, { exceptions: [ '/', '=' ] } ] 136 | strict: [ 2, global ] 137 | quotes: [ 2, single, avoid-escape ] 138 | quote-props: [ 1, 'as-needed', { "keywords": true } ] 139 | radix: 2 140 | use-isnan: 2 141 | valid-typeof: 2 142 | yoda: [ 2, never, { "exceptRange": true } ] 143 | -------------------------------------------------------------------------------- /lib/parser_inline.js: -------------------------------------------------------------------------------- 1 | /** internal 2 | * class ParserInline 3 | * 4 | * Tokenizes paragraph content. 5 | **/ 6 | 'use strict'; 7 | 8 | 9 | var Ruler = require('./ruler'); 10 | 11 | 12 | //////////////////////////////////////////////////////////////////////////////// 13 | // Parser rules 14 | 15 | var _rules = [ 16 | [ 'text', require('./rules_inline/text') ], 17 | [ 'newline', require('./rules_inline/newline') ], 18 | [ 'escape', require('./rules_inline/escape') ], 19 | [ 'backticks', require('./rules_inline/backticks') ], 20 | [ 'strikethrough', require('./rules_inline/strikethrough').tokenize ], 21 | [ 'emphasis', require('./rules_inline/emphasis').tokenize ], 22 | [ 'link', require('./rules_inline/link') ], 23 | [ 'image', require('./rules_inline/image') ], 24 | [ 'autolink', require('./rules_inline/autolink') ], 25 | [ 'html_inline', require('./rules_inline/html_inline') ], 26 | [ 'entity', require('./rules_inline/entity') ] 27 | ]; 28 | 29 | var _rules2 = [ 30 | [ 'balance_pairs', require('./rules_inline/balance_pairs') ], 31 | [ 'strikethrough', require('./rules_inline/strikethrough').postProcess ], 32 | [ 'emphasis', require('./rules_inline/emphasis').postProcess ], 33 | [ 'text_collapse', require('./rules_inline/text_collapse') ] 34 | ]; 35 | 36 | 37 | /** 38 | * new ParserInline() 39 | **/ 40 | function ParserInline() { 41 | var i; 42 | 43 | /** 44 | * ParserInline#ruler -> Ruler 45 | * 46 | * [[Ruler]] instance. Keep configuration of inline rules. 47 | **/ 48 | this.ruler = new Ruler(); 49 | 50 | for (i = 0; i < _rules.length; i++) { 51 | this.ruler.push(_rules[i][0], _rules[i][1]); 52 | } 53 | 54 | /** 55 | * ParserInline#ruler2 -> Ruler 56 | * 57 | * [[Ruler]] instance. Second ruler used for post-processing 58 | * (e.g. in emphasis-like rules). 59 | **/ 60 | this.ruler2 = new Ruler(); 61 | 62 | for (i = 0; i < _rules2.length; i++) { 63 | this.ruler2.push(_rules2[i][0], _rules2[i][1]); 64 | } 65 | } 66 | 67 | 68 | // Skip single token by running all rules in validation mode; 69 | // returns `true` if any rule reported success 70 | // 71 | ParserInline.prototype.skipToken = function (state) { 72 | var ok, i, pos = state.pos, 73 | rules = this.ruler.getRules(''), 74 | len = rules.length, 75 | maxNesting = state.md.options.maxNesting, 76 | cache = state.cache; 77 | 78 | 79 | if (typeof cache[pos] !== 'undefined') { 80 | state.pos = cache[pos]; 81 | return; 82 | } 83 | 84 | if (state.level < maxNesting) { 85 | for (i = 0; i < len; i++) { 86 | // Increment state.level and decrement it later to limit recursion. 87 | // It's harmless to do here, because no tokens are created. But ideally, 88 | // we'd need a separate private state variable for this purpose. 89 | // 90 | state.level++; 91 | ok = rules[i](state, true); 92 | state.level--; 93 | 94 | if (ok) { break; } 95 | } 96 | } else { 97 | // Too much nesting, just skip until the end of the paragraph. 98 | // 99 | // NOTE: this will cause links to behave incorrectly in the following case, 100 | // when an amount of `[` is exactly equal to `maxNesting + 1`: 101 | // 102 | // [[[[[[[[[[[[[[[[[[[[[foo]() 103 | // 104 | // TODO: remove this workaround when CM standard will allow nested links 105 | // (we can replace it by preventing links from being parsed in 106 | // validation mode) 107 | // 108 | state.pos = state.posMax; 109 | } 110 | 111 | if (!ok) { state.pos++; } 112 | cache[pos] = state.pos; 113 | }; 114 | 115 | 116 | // Generate tokens for input range 117 | // 118 | ParserInline.prototype.tokenize = function (state) { 119 | var ok, i, 120 | rules = this.ruler.getRules(''), 121 | len = rules.length, 122 | end = state.posMax, 123 | maxNesting = state.md.options.maxNesting; 124 | 125 | while (state.pos < end) { 126 | // Try all possible rules. 127 | // On success, rule should: 128 | // 129 | // - update `state.pos` 130 | // - update `state.tokens` 131 | // - return true 132 | 133 | if (state.level < maxNesting) { 134 | for (i = 0; i < len; i++) { 135 | ok = rules[i](state, false); 136 | if (ok) { break; } 137 | } 138 | } 139 | 140 | if (ok) { 141 | if (state.pos >= end) { break; } 142 | continue; 143 | } 144 | 145 | state.pending += state.src[state.pos++]; 146 | } 147 | 148 | if (state.pending) { 149 | state.pushPending(); 150 | } 151 | }; 152 | 153 | 154 | /** 155 | * ParserInline.parse(str, md, env, outTokens) 156 | * 157 | * Process input string and push inline tokens into `outTokens` 158 | **/ 159 | ParserInline.prototype.parse = function (str, md, env, outTokens) { 160 | var i, rules, len; 161 | var state = new this.State(str, md, env, outTokens); 162 | 163 | this.tokenize(state); 164 | 165 | rules = this.ruler2.getRules(''); 166 | len = rules.length; 167 | 168 | for (i = 0; i < len; i++) { 169 | rules[i](state); 170 | } 171 | }; 172 | 173 | 174 | ParserInline.prototype.State = require('./rules_inline/state_inline'); 175 | 176 | 177 | module.exports = ParserInline; 178 | -------------------------------------------------------------------------------- /support/demo_template/sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | __Advertisement :)__ 3 | 4 | - __[pica](https://nodeca.github.io/pica/demo/)__ - high quality and fast image 5 | resize in browser. 6 | - __[babelfish](https://github.com/nodeca/babelfish/)__ - developer friendly 7 | i18n with plurals support and easy syntax. 8 | 9 | You will like those projects! 10 | 11 | --- 12 | 13 | # h1 Heading 8-) 14 | ## h2 Heading 15 | ### h3 Heading 16 | #### h4 Heading 17 | ##### h5 Heading 18 | ###### h6 Heading 19 | 20 | 21 | ## Horizontal Rules 22 | 23 | ___ 24 | 25 | --- 26 | 27 | *** 28 | 29 | 30 | ## Typographic replacements 31 | 32 | Enable typographer option to see result. 33 | 34 | (c) (C) (r) (R) (tm) (TM) (p) (P) +- 35 | 36 | test.. test... test..... test?..... test!.... 37 | 38 | !!!!!! ???? ,, -- --- 39 | 40 | "Smartypants, double quotes" and 'single quotes' 41 | 42 | 43 | ## Emphasis 44 | 45 | **This is bold text** 46 | 47 | __This is bold text__ 48 | 49 | *This is italic text* 50 | 51 | _This is italic text_ 52 | 53 | ~~Strikethrough~~ 54 | 55 | 56 | ## Blockquotes 57 | 58 | 59 | > Blockquotes can also be nested... 60 | >> ...by using additional greater-than signs right next to each other... 61 | > > > ...or with spaces between arrows. 62 | 63 | 64 | ## Lists 65 | 66 | Unordered 67 | 68 | + Create a list by starting a line with `+`, `-`, or `*` 69 | + Sub-lists are made by indenting 2 spaces: 70 | - Marker character change forces new list start: 71 | * Ac tristique libero volutpat at 72 | + Facilisis in pretium nisl aliquet 73 | - Nulla volutpat aliquam velit 74 | + Very easy! 75 | 76 | Ordered 77 | 78 | 1. Lorem ipsum dolor sit amet 79 | 2. Consectetur adipiscing elit 80 | 3. Integer molestie lorem at massa 81 | 82 | 83 | 1. You can use sequential numbers... 84 | 1. ...or keep all the numbers as `1.` 85 | 86 | Start numbering with offset: 87 | 88 | 57. foo 89 | 1. bar 90 | 91 | 92 | ## Code 93 | 94 | Inline `code` 95 | 96 | Indented code 97 | 98 | // Some comments 99 | line 1 of code 100 | line 2 of code 101 | line 3 of code 102 | 103 | 104 | Block code "fences" 105 | 106 | ``` 107 | Sample text here... 108 | ``` 109 | 110 | Syntax highlighting 111 | 112 | ``` js 113 | var foo = function (bar) { 114 | return bar++; 115 | }; 116 | 117 | console.log(foo(5)); 118 | ``` 119 | 120 | ## Tables 121 | 122 | | Option | Description | 123 | | ------ | ----------- | 124 | | data | path to data files to supply the data that will be passed into templates. | 125 | | engine | engine to be used for processing templates. Handlebars is the default. | 126 | | ext | extension to be used for dest files. | 127 | 128 | Right aligned columns 129 | 130 | | Option | Description | 131 | | ------:| -----------:| 132 | | data | path to data files to supply the data that will be passed into templates. | 133 | | engine | engine to be used for processing templates. Handlebars is the default. | 134 | | ext | extension to be used for dest files. | 135 | 136 | 137 | ## Links 138 | 139 | [link text](http://dev.nodeca.com) 140 | 141 | [link with title](http://nodeca.github.io/pica/demo/ "title text!") 142 | 143 | Autoconverted link https://github.com/nodeca/pica (enable linkify to see) 144 | 145 | 146 | ## Images 147 | 148 | ![Minion](https://octodex.github.com/images/minion.png) 149 | ![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") 150 | 151 | Like links, Images also have a footnote style syntax 152 | 153 | ![Alt text][id] 154 | 155 | With a reference later in the document defining the URL location: 156 | 157 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" 158 | 159 | 160 | ## Plugins 161 | 162 | The killer feature of `markdown-it` is very effective support of 163 | [syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). 164 | 165 | 166 | ### [Emojies](https://github.com/markdown-it/markdown-it-emoji) 167 | 168 | > Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum: 169 | > 170 | > Shortcuts (emoticons): :-) :-( 8-) ;) 171 | 172 | see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji. 173 | 174 | 175 | ### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup) 176 | 177 | - 19^th^ 178 | - H~2~O 179 | 180 | 181 | ### [\](https://github.com/markdown-it/markdown-it-ins) 182 | 183 | ++Inserted text++ 184 | 185 | 186 | ### [\](https://github.com/markdown-it/markdown-it-mark) 187 | 188 | ==Marked text== 189 | 190 | 191 | ### [Footnotes](https://github.com/markdown-it/markdown-it-footnote) 192 | 193 | Footnote 1 link[^first]. 194 | 195 | Footnote 2 link[^second]. 196 | 197 | Inline footnote^[Text of inline footnote] definition. 198 | 199 | Duplicated footnote reference[^second]. 200 | 201 | [^first]: Footnote **can have markup** 202 | 203 | and multiple paragraphs. 204 | 205 | [^second]: Footnote text. 206 | 207 | 208 | ### [Definition lists](https://github.com/markdown-it/markdown-it-deflist) 209 | 210 | Term 1 211 | 212 | : Definition 1 213 | with lazy continuation. 214 | 215 | Term 2 with *inline markup* 216 | 217 | : Definition 2 218 | 219 | { some code, part of Definition 2 } 220 | 221 | Third paragraph of definition 2. 222 | 223 | _Compact style:_ 224 | 225 | Term 1 226 | ~ Definition 1 227 | 228 | Term 2 229 | ~ Definition 2a 230 | ~ Definition 2b 231 | 232 | 233 | ### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr) 234 | 235 | This is HTML abbreviation example. 236 | 237 | It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. 238 | 239 | *[HTML]: Hyper Text Markup Language 240 | 241 | ### [Custom containers](https://github.com/markdown-it/markdown-it-container) 242 | 243 | ::: warning 244 | *here be dragons* 245 | ::: 246 | -------------------------------------------------------------------------------- /lib/rules_block/reference.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var normalizeReference = require('../common/utils').normalizeReference; 5 | var isSpace = require('../common/utils').isSpace; 6 | 7 | 8 | module.exports = function reference(state, startLine, _endLine, silent) { 9 | var ch, 10 | destEndPos, 11 | destEndLineNo, 12 | endLine, 13 | href, 14 | i, 15 | l, 16 | label, 17 | labelEnd, 18 | oldParentType, 19 | res, 20 | start, 21 | str, 22 | terminate, 23 | terminatorRules, 24 | title, 25 | lines = 0, 26 | pos = state.bMarks[startLine] + state.tShift[startLine], 27 | max = state.eMarks[startLine], 28 | nextLine = startLine + 1; 29 | 30 | // if it's indented more than 3 spaces, it should be a code block 31 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } 32 | 33 | if (state.src.charCodeAt(pos) !== 0x5B/* [ */) { return false; } 34 | 35 | // Simple check to quickly interrupt scan on [link](url) at the start of line. 36 | // Can be useful on practice: https://github.com/markdown-it/markdown-it/issues/54 37 | while (++pos < max) { 38 | if (state.src.charCodeAt(pos) === 0x5D /* ] */ && 39 | state.src.charCodeAt(pos - 1) !== 0x5C/* \ */) { 40 | if (pos + 1 === max) { return false; } 41 | if (state.src.charCodeAt(pos + 1) !== 0x3A/* : */) { return false; } 42 | break; 43 | } 44 | } 45 | 46 | endLine = state.lineMax; 47 | 48 | // jump line-by-line until empty one or EOF 49 | terminatorRules = state.md.block.ruler.getRules('reference'); 50 | 51 | oldParentType = state.parentType; 52 | state.parentType = 'reference'; 53 | 54 | for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { 55 | // this would be a code block normally, but after paragraph 56 | // it's considered a lazy continuation regardless of what's there 57 | if (state.sCount[nextLine] - state.blkIndent > 3) { continue; } 58 | 59 | // quirk for blockquotes, this line should already be checked by that rule 60 | if (state.sCount[nextLine] < 0) { continue; } 61 | 62 | // Some tags can terminate paragraph without empty line. 63 | terminate = false; 64 | for (i = 0, l = terminatorRules.length; i < l; i++) { 65 | if (terminatorRules[i](state, nextLine, endLine, true)) { 66 | terminate = true; 67 | break; 68 | } 69 | } 70 | if (terminate) { break; } 71 | } 72 | 73 | str = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); 74 | max = str.length; 75 | 76 | for (pos = 1; pos < max; pos++) { 77 | ch = str.charCodeAt(pos); 78 | if (ch === 0x5B /* [ */) { 79 | return false; 80 | } else if (ch === 0x5D /* ] */) { 81 | labelEnd = pos; 82 | break; 83 | } else if (ch === 0x0A /* \n */) { 84 | lines++; 85 | } else if (ch === 0x5C /* \ */) { 86 | pos++; 87 | if (pos < max && str.charCodeAt(pos) === 0x0A) { 88 | lines++; 89 | } 90 | } 91 | } 92 | 93 | if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return false; } 94 | 95 | // [label]: destination 'title' 96 | // ^^^ skip optional whitespace here 97 | for (pos = labelEnd + 2; pos < max; pos++) { 98 | ch = str.charCodeAt(pos); 99 | if (ch === 0x0A) { 100 | lines++; 101 | } else if (isSpace(ch)) { 102 | /*eslint no-empty:0*/ 103 | } else { 104 | break; 105 | } 106 | } 107 | 108 | // [label]: destination 'title' 109 | // ^^^^^^^^^^^ parse this 110 | res = state.md.helpers.parseLinkDestination(str, pos, max); 111 | if (!res.ok) { return false; } 112 | 113 | href = state.md.normalizeLink(res.str); 114 | if (!state.md.validateLink(href)) { return false; } 115 | 116 | pos = res.pos; 117 | lines += res.lines; 118 | 119 | // save cursor state, we could require to rollback later 120 | destEndPos = pos; 121 | destEndLineNo = lines; 122 | 123 | // [label]: destination 'title' 124 | // ^^^ skipping those spaces 125 | start = pos; 126 | for (; pos < max; pos++) { 127 | ch = str.charCodeAt(pos); 128 | if (ch === 0x0A) { 129 | lines++; 130 | } else if (isSpace(ch)) { 131 | /*eslint no-empty:0*/ 132 | } else { 133 | break; 134 | } 135 | } 136 | 137 | // [label]: destination 'title' 138 | // ^^^^^^^ parse this 139 | res = state.md.helpers.parseLinkTitle(str, pos, max); 140 | if (pos < max && start !== pos && res.ok) { 141 | title = res.str; 142 | pos = res.pos; 143 | lines += res.lines; 144 | } else { 145 | title = ''; 146 | pos = destEndPos; 147 | lines = destEndLineNo; 148 | } 149 | 150 | // skip trailing spaces until the rest of the line 151 | while (pos < max) { 152 | ch = str.charCodeAt(pos); 153 | if (!isSpace(ch)) { break; } 154 | pos++; 155 | } 156 | 157 | if (pos < max && str.charCodeAt(pos) !== 0x0A) { 158 | if (title) { 159 | // garbage at the end of the line after title, 160 | // but it could still be a valid reference if we roll back 161 | title = ''; 162 | pos = destEndPos; 163 | lines = destEndLineNo; 164 | while (pos < max) { 165 | ch = str.charCodeAt(pos); 166 | if (!isSpace(ch)) { break; } 167 | pos++; 168 | } 169 | } 170 | } 171 | 172 | if (pos < max && str.charCodeAt(pos) !== 0x0A) { 173 | // garbage at the end of the line 174 | return false; 175 | } 176 | 177 | label = normalizeReference(str.slice(1, labelEnd)); 178 | if (!label) { 179 | // CommonMark 0.20 disallows empty labels 180 | return false; 181 | } 182 | 183 | // Reference can not terminate anything. This check is for safety only. 184 | /*istanbul ignore if*/ 185 | if (silent) { return true; } 186 | 187 | if (typeof state.env.references === 'undefined') { 188 | state.env.references = {}; 189 | } 190 | if (typeof state.env.references[label] === 'undefined') { 191 | state.env.references[label] = { title: title, href: href }; 192 | } 193 | 194 | state.parentType = oldParentType; 195 | 196 | state.line = startLine + lines + 1; 197 | return true; 198 | }; 199 | -------------------------------------------------------------------------------- /lib/rules_core/smartquotes.js: -------------------------------------------------------------------------------- 1 | // Convert straight quotation marks to typographic ones 2 | // 3 | 'use strict'; 4 | 5 | 6 | var isWhiteSpace = require('../common/utils').isWhiteSpace; 7 | var isPunctChar = require('../common/utils').isPunctChar; 8 | var isMdAsciiPunct = require('../common/utils').isMdAsciiPunct; 9 | 10 | var QUOTE_TEST_RE = /['"]/; 11 | var QUOTE_RE = /['"]/g; 12 | var APOSTROPHE = '\u2019'; /* ’ */ 13 | 14 | 15 | function replaceAt(str, index, ch) { 16 | return str.substr(0, index) + ch + str.substr(index + 1); 17 | } 18 | 19 | function process_inlines(tokens, state) { 20 | var i, token, text, t, pos, max, thisLevel, item, lastChar, nextChar, 21 | isLastPunctChar, isNextPunctChar, isLastWhiteSpace, isNextWhiteSpace, 22 | canOpen, canClose, j, isSingle, stack, openQuote, closeQuote; 23 | 24 | stack = []; 25 | 26 | for (i = 0; i < tokens.length; i++) { 27 | token = tokens[i]; 28 | 29 | thisLevel = tokens[i].level; 30 | 31 | for (j = stack.length - 1; j >= 0; j--) { 32 | if (stack[j].level <= thisLevel) { break; } 33 | } 34 | stack.length = j + 1; 35 | 36 | if (token.type !== 'text') { continue; } 37 | 38 | text = token.content; 39 | pos = 0; 40 | max = text.length; 41 | 42 | /*eslint no-labels:0,block-scoped-var:0*/ 43 | OUTER: 44 | while (pos < max) { 45 | QUOTE_RE.lastIndex = pos; 46 | t = QUOTE_RE.exec(text); 47 | if (!t) { break; } 48 | 49 | canOpen = canClose = true; 50 | pos = t.index + 1; 51 | isSingle = (t[0] === "'"); 52 | 53 | // Find previous character, 54 | // default to space if it's the beginning of the line 55 | // 56 | lastChar = 0x20; 57 | 58 | if (t.index - 1 >= 0) { 59 | lastChar = text.charCodeAt(t.index - 1); 60 | } else { 61 | for (j = i - 1; j >= 0; j--) { 62 | if (tokens[j].type !== 'text') { continue; } 63 | 64 | lastChar = tokens[j].content.charCodeAt(tokens[j].content.length - 1); 65 | break; 66 | } 67 | } 68 | 69 | // Find next character, 70 | // default to space if it's the end of the line 71 | // 72 | nextChar = 0x20; 73 | 74 | if (pos < max) { 75 | nextChar = text.charCodeAt(pos); 76 | } else { 77 | for (j = i + 1; j < tokens.length; j++) { 78 | if (tokens[j].type !== 'text') { continue; } 79 | 80 | nextChar = tokens[j].content.charCodeAt(0); 81 | break; 82 | } 83 | } 84 | 85 | isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)); 86 | isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)); 87 | 88 | isLastWhiteSpace = isWhiteSpace(lastChar); 89 | isNextWhiteSpace = isWhiteSpace(nextChar); 90 | 91 | if (isNextWhiteSpace) { 92 | canOpen = false; 93 | } else if (isNextPunctChar) { 94 | if (!(isLastWhiteSpace || isLastPunctChar)) { 95 | canOpen = false; 96 | } 97 | } 98 | 99 | if (isLastWhiteSpace) { 100 | canClose = false; 101 | } else if (isLastPunctChar) { 102 | if (!(isNextWhiteSpace || isNextPunctChar)) { 103 | canClose = false; 104 | } 105 | } 106 | 107 | if (nextChar === 0x22 /* " */ && t[0] === '"') { 108 | if (lastChar >= 0x30 /* 0 */ && lastChar <= 0x39 /* 9 */) { 109 | // special case: 1"" - count first quote as an inch 110 | canClose = canOpen = false; 111 | } 112 | } 113 | 114 | if (canOpen && canClose) { 115 | // treat this as the middle of the word 116 | canOpen = false; 117 | canClose = isNextPunctChar; 118 | } 119 | 120 | if (!canOpen && !canClose) { 121 | // middle of word 122 | if (isSingle) { 123 | token.content = replaceAt(token.content, t.index, APOSTROPHE); 124 | } 125 | continue; 126 | } 127 | 128 | if (canClose) { 129 | // this could be a closing quote, rewind the stack to get a match 130 | for (j = stack.length - 1; j >= 0; j--) { 131 | item = stack[j]; 132 | if (stack[j].level < thisLevel) { break; } 133 | if (item.single === isSingle && stack[j].level === thisLevel) { 134 | item = stack[j]; 135 | 136 | if (isSingle) { 137 | openQuote = state.md.options.quotes[2]; 138 | closeQuote = state.md.options.quotes[3]; 139 | } else { 140 | openQuote = state.md.options.quotes[0]; 141 | closeQuote = state.md.options.quotes[1]; 142 | } 143 | 144 | // replace token.content *before* tokens[item.token].content, 145 | // because, if they are pointing at the same token, replaceAt 146 | // could mess up indices when quote length != 1 147 | token.content = replaceAt(token.content, t.index, closeQuote); 148 | tokens[item.token].content = replaceAt( 149 | tokens[item.token].content, item.pos, openQuote); 150 | 151 | pos += closeQuote.length - 1; 152 | if (item.token === i) { pos += openQuote.length - 1; } 153 | 154 | text = token.content; 155 | max = text.length; 156 | 157 | stack.length = j; 158 | continue OUTER; 159 | } 160 | } 161 | } 162 | 163 | if (canOpen) { 164 | stack.push({ 165 | token: i, 166 | pos: t.index, 167 | single: isSingle, 168 | level: thisLevel 169 | }); 170 | } else if (canClose && isSingle) { 171 | token.content = replaceAt(token.content, t.index, APOSTROPHE); 172 | } 173 | } 174 | } 175 | } 176 | 177 | 178 | module.exports = function smartquotes(state) { 179 | /*eslint max-depth:0*/ 180 | var blkIdx; 181 | 182 | if (!state.md.options.typographer) { return; } 183 | 184 | for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { 185 | 186 | if (state.tokens[blkIdx].type !== 'inline' || 187 | !QUOTE_TEST_RE.test(state.tokens[blkIdx].content)) { 188 | continue; 189 | } 190 | 191 | process_inlines(state.tokens[blkIdx].children, state); 192 | } 193 | }; 194 | -------------------------------------------------------------------------------- /support/api_header.md: -------------------------------------------------------------------------------- 1 | # markdown-it 2 | 3 | ## Install 4 | 5 | **node.js** & **bower**: 6 | 7 | ```bash 8 | npm install markdown-it --save 9 | bower install markdown-it --save 10 | ``` 11 | 12 | **browser (CDN):** 13 | 14 | - [jsDeliver CDN](http://www.jsdelivr.com/#!markdown-it "jsDelivr CDN") 15 | - [cdnjs.com CDN](https://cdnjs.com/libraries/markdown-it "cdnjs.com") 16 | 17 | 18 | ## Usage examples 19 | 20 | See also: 21 | 22 | - [Development info](https://github.com/markdown-it/markdown-it/tree/master/docs) - 23 | for plugins writers. 24 | 25 | 26 | ### Simple 27 | 28 | ```js 29 | // node.js, "classic" way: 30 | var MarkdownIt = require('markdown-it'), 31 | md = new MarkdownIt(); 32 | var result = md.render('# markdown-it rulezz!'); 33 | 34 | // node.js, the same, but with sugar: 35 | var md = require('markdown-it')(); 36 | var result = md.render('# markdown-it rulezz!'); 37 | 38 | // browser without AMD, added to "window" on script load 39 | // Note, there is no dash in "markdownit". 40 | var md = window.markdownit(); 41 | var result = md.render('# markdown-it rulezz!'); 42 | ``` 43 | 44 | Single line rendering, without paragraph wrap: 45 | 46 | ```js 47 | var md = require('markdown-it')(); 48 | var result = md.renderInline('__markdown-it__ rulezz!'); 49 | ``` 50 | 51 | 52 | ### Init with presets and options 53 | 54 | (*) presets define combinations of active rules and options. Can be 55 | `"commonmark"`, `"zero"` or `"default"` (if skipped). See 56 | [API docs](https://markdown-it.github.io/markdown-it/#MarkdownIt.new) for more details. 57 | 58 | ```js 59 | // commonmark mode 60 | var md = require('markdown-it')('commonmark'); 61 | 62 | // default mode 63 | var md = require('markdown-it')(); 64 | 65 | // enable everything 66 | var md = require('markdown-it')({ 67 | html: true, 68 | linkify: true, 69 | typographer: true 70 | }); 71 | 72 | // full options list (defaults) 73 | var md = require('markdown-it')({ 74 | html: false, // Enable HTML tags in source 75 | xhtmlOut: false, // Use '/' to close single tags (
). 76 | // This is only for full CommonMark compatibility. 77 | breaks: false, // Convert '\n' in paragraphs into
78 | langPrefix: 'language-', // CSS language prefix for fenced blocks. Can be 79 | // useful for external highlighters. 80 | linkify: false, // Autoconvert URL-like text to links 81 | 82 | // Enable some language-neutral replacement + quotes beautification 83 | typographer: false, 84 | 85 | // Double + single quotes replacement pairs, when typographer enabled, 86 | // and smartquotes on. Could be either a String or an Array. 87 | // 88 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 89 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 90 | quotes: '“”‘’', 91 | 92 | // Highlighter function. Should return escaped HTML, 93 | // or '' if the source string is not changed and should be escaped externaly. 94 | // If result starts with `): 131 | 132 | ```js 133 | var hljs = require('highlight.js') // https://highlightjs.org/ 134 | 135 | // Actual default values 136 | var md = require('markdown-it')({ 137 | highlight: function (str, lang) { 138 | if (lang && hljs.getLanguage(lang)) { 139 | try { 140 | return '
' +
141 |                hljs.highlight(lang, str, true).value +
142 |                '
'; 143 | } catch (__) {} 144 | } 145 | 146 | return '
' + md.utils.escapeHtml(str) + '
'; 147 | } 148 | }); 149 | ``` 150 | 151 | ### Linkify 152 | 153 | `linkify: true` uses [linkify-it](https://github.com/markdown-it/linkify-it). To 154 | configure linkify-it, access the linkify instance through `md.linkify`: 155 | 156 | ```js 157 | md.linkify.tlds('.py', false); // disables .py as top level domain 158 | ``` 159 | 160 | ## Syntax extensions 161 | 162 | Embedded (enabled by default): 163 | 164 | - [Tables](https://help.github.com/articles/github-flavored-markdown/#tables) (GFM) 165 | - [Strikethrough](https://help.github.com/articles/github-flavored-markdown/#strikethrough) (GFM) 166 | 167 | Via plugins: 168 | 169 | - [subscript](https://github.com/markdown-it/markdown-it-sub) 170 | - [superscript](https://github.com/markdown-it/markdown-it-sup) 171 | - [footnote](https://github.com/markdown-it/markdown-it-footnote) 172 | - [definition list](https://github.com/markdown-it/markdown-it-deflist) 173 | - [abbreviation](https://github.com/markdown-it/markdown-it-abbr) 174 | - [emoji](https://github.com/markdown-it/markdown-it-emoji) 175 | - [custom container](https://github.com/markdown-it/markdown-it-container) 176 | - [insert](https://github.com/markdown-it/markdown-it-ins) 177 | - [mark](https://github.com/markdown-it/markdown-it-mark) 178 | - ... and [others](https://www.npmjs.org/browse/keyword/markdown-it-plugin) 179 | 180 | 181 | ### Manage rules 182 | 183 | By default all rules are enabled, but can be restricted by options. On plugin 184 | load all its rules are enabled automatically. 185 | 186 | ```js 187 | // Activate/deactivate rules, with curring 188 | var md = require('markdown-it')() 189 | .disable([ 'link', 'image' ]) 190 | .enable([ 'link' ]) 191 | .enable('image'); 192 | 193 | // Enable everything 194 | md = require('markdown-it')('full', { 195 | html: true, 196 | linkify: true, 197 | typographer: true, 198 | }); 199 | ``` 200 | -------------------------------------------------------------------------------- /lib/rules_block/table.js: -------------------------------------------------------------------------------- 1 | // GFM table, non-standard 2 | 3 | 'use strict'; 4 | 5 | var isSpace = require('../common/utils').isSpace; 6 | 7 | 8 | function getLine(state, line) { 9 | var pos = state.bMarks[line] + state.blkIndent, 10 | max = state.eMarks[line]; 11 | 12 | return state.src.substr(pos, max - pos); 13 | } 14 | 15 | function escapedSplit(str) { 16 | var result = [], 17 | pos = 0, 18 | max = str.length, 19 | ch, 20 | escapes = 0, 21 | lastPos = 0, 22 | backTicked = false, 23 | lastBackTick = 0; 24 | 25 | ch = str.charCodeAt(pos); 26 | 27 | while (pos < max) { 28 | if (ch === 0x60/* ` */) { 29 | if (backTicked) { 30 | // make \` close code sequence, but not open it; 31 | // the reason is: `\` is correct code block 32 | backTicked = false; 33 | lastBackTick = pos; 34 | } else if (escapes % 2 === 0) { 35 | backTicked = true; 36 | lastBackTick = pos; 37 | } 38 | } else if (ch === 0x7c/* | */ && (escapes % 2 === 0) && !backTicked) { 39 | result.push(str.substring(lastPos, pos)); 40 | lastPos = pos + 1; 41 | } 42 | 43 | if (ch === 0x5c/* \ */) { 44 | escapes++; 45 | } else { 46 | escapes = 0; 47 | } 48 | 49 | pos++; 50 | 51 | // If there was an un-closed backtick, go back to just after 52 | // the last backtick, but as if it was a normal character 53 | if (pos === max && backTicked) { 54 | backTicked = false; 55 | pos = lastBackTick + 1; 56 | } 57 | 58 | ch = str.charCodeAt(pos); 59 | } 60 | 61 | result.push(str.substring(lastPos)); 62 | 63 | return result; 64 | } 65 | 66 | 67 | module.exports = function table(state, startLine, endLine, silent) { 68 | var ch, lineText, pos, i, nextLine, columns, columnCount, token, 69 | aligns, t, tableLines, tbodyLines; 70 | 71 | // should have at least two lines 72 | if (startLine + 2 > endLine) { return false; } 73 | 74 | nextLine = startLine + 1; 75 | 76 | if (state.sCount[nextLine] < state.blkIndent) { return false; } 77 | 78 | // if it's indented more than 3 spaces, it should be a code block 79 | if (state.sCount[nextLine] - state.blkIndent >= 4) { return false; } 80 | 81 | // first character of the second line should be '|', '-', ':', 82 | // and no other characters are allowed but spaces; 83 | // basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp 84 | 85 | pos = state.bMarks[nextLine] + state.tShift[nextLine]; 86 | if (pos >= state.eMarks[nextLine]) { return false; } 87 | 88 | ch = state.src.charCodeAt(pos++); 89 | if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */) { return false; } 90 | 91 | while (pos < state.eMarks[nextLine]) { 92 | ch = state.src.charCodeAt(pos); 93 | 94 | if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */ && !isSpace(ch)) { return false; } 95 | 96 | pos++; 97 | } 98 | 99 | lineText = getLine(state, startLine + 1); 100 | 101 | columns = lineText.split('|'); 102 | aligns = []; 103 | for (i = 0; i < columns.length; i++) { 104 | t = columns[i].trim(); 105 | if (!t) { 106 | // allow empty columns before and after table, but not in between columns; 107 | // e.g. allow ` |---| `, disallow ` ---||--- ` 108 | if (i === 0 || i === columns.length - 1) { 109 | continue; 110 | } else { 111 | return false; 112 | } 113 | } 114 | 115 | if (!/^:?-+:?$/.test(t)) { return false; } 116 | if (t.charCodeAt(t.length - 1) === 0x3A/* : */) { 117 | aligns.push(t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right'); 118 | } else if (t.charCodeAt(0) === 0x3A/* : */) { 119 | aligns.push('left'); 120 | } else { 121 | aligns.push(''); 122 | } 123 | } 124 | 125 | lineText = getLine(state, startLine).trim(); 126 | if (lineText.indexOf('|') === -1) { return false; } 127 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false; } 128 | columns = escapedSplit(lineText.replace(/^\||\|$/g, '')); 129 | 130 | // header row will define an amount of columns in the entire table, 131 | // and align row shouldn't be smaller than that (the rest of the rows can) 132 | columnCount = columns.length; 133 | if (columnCount > aligns.length) { return false; } 134 | 135 | if (silent) { return true; } 136 | 137 | token = state.push('table_open', 'table', 1); 138 | token.map = tableLines = [ startLine, 0 ]; 139 | 140 | token = state.push('thead_open', 'thead', 1); 141 | token.map = [ startLine, startLine + 1 ]; 142 | 143 | token = state.push('tr_open', 'tr', 1); 144 | token.map = [ startLine, startLine + 1 ]; 145 | 146 | for (i = 0; i < columns.length; i++) { 147 | token = state.push('th_open', 'th', 1); 148 | token.map = [ startLine, startLine + 1 ]; 149 | if (aligns[i]) { 150 | token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ]; 151 | } 152 | 153 | token = state.push('inline', '', 0); 154 | token.content = columns[i].trim(); 155 | token.map = [ startLine, startLine + 1 ]; 156 | token.children = []; 157 | 158 | token = state.push('th_close', 'th', -1); 159 | } 160 | 161 | token = state.push('tr_close', 'tr', -1); 162 | token = state.push('thead_close', 'thead', -1); 163 | 164 | token = state.push('tbody_open', 'tbody', 1); 165 | token.map = tbodyLines = [ startLine + 2, 0 ]; 166 | 167 | for (nextLine = startLine + 2; nextLine < endLine; nextLine++) { 168 | if (state.sCount[nextLine] < state.blkIndent) { break; } 169 | 170 | lineText = getLine(state, nextLine).trim(); 171 | if (lineText.indexOf('|') === -1) { break; } 172 | if (state.sCount[nextLine] - state.blkIndent >= 4) { break; } 173 | columns = escapedSplit(lineText.replace(/^\||\|$/g, '')); 174 | 175 | token = state.push('tr_open', 'tr', 1); 176 | for (i = 0; i < columnCount; i++) { 177 | token = state.push('td_open', 'td', 1); 178 | if (aligns[i]) { 179 | token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ]; 180 | } 181 | 182 | token = state.push('inline', '', 0); 183 | token.content = columns[i] ? columns[i].trim() : ''; 184 | token.children = []; 185 | 186 | token = state.push('td_close', 'td', -1); 187 | } 188 | token = state.push('tr_close', 'tr', -1); 189 | } 190 | token = state.push('tbody_close', 'tbody', -1); 191 | token = state.push('table_close', 'table', -1); 192 | 193 | tableLines[1] = tbodyLines[1] = nextLine; 194 | state.line = nextLine; 195 | return true; 196 | }; 197 | -------------------------------------------------------------------------------- /lib/rules_block/state_block.js: -------------------------------------------------------------------------------- 1 | // Parser state class 2 | 3 | 'use strict'; 4 | 5 | var Token = require('../token'); 6 | var isSpace = require('../common/utils').isSpace; 7 | 8 | 9 | function StateBlock(src, md, env, tokens) { 10 | var ch, s, start, pos, len, indent, offset, indent_found; 11 | 12 | this.src = src; 13 | 14 | // link to parser instance 15 | this.md = md; 16 | 17 | this.env = env; 18 | 19 | // 20 | // Internal state vartiables 21 | // 22 | 23 | this.tokens = tokens; 24 | 25 | this.bMarks = []; // line begin offsets for fast jumps 26 | this.eMarks = []; // line end offsets for fast jumps 27 | this.tShift = []; // offsets of the first non-space characters (tabs not expanded) 28 | this.sCount = []; // indents for each line (tabs expanded) 29 | 30 | // An amount of virtual spaces (tabs expanded) between beginning 31 | // of each line (bMarks) and real beginning of that line. 32 | // 33 | // It exists only as a hack because blockquotes override bMarks 34 | // losing information in the process. 35 | // 36 | // It's used only when expanding tabs, you can think about it as 37 | // an initial tab length, e.g. bsCount=21 applied to string `\t123` 38 | // means first tab should be expanded to 4-21%4 === 3 spaces. 39 | // 40 | this.bsCount = []; 41 | 42 | // block parser variables 43 | this.blkIndent = 0; // required block content indent 44 | // (for example, if we are in list) 45 | this.line = 0; // line index in src 46 | this.lineMax = 0; // lines count 47 | this.tight = false; // loose/tight mode for lists 48 | this.ddIndent = -1; // indent of the current dd block (-1 if there isn't any) 49 | 50 | // can be 'blockquote', 'list', 'root', 'paragraph' or 'reference' 51 | // used in lists to determine if they interrupt a paragraph 52 | this.parentType = 'root'; 53 | 54 | this.level = 0; 55 | 56 | // renderer 57 | this.result = ''; 58 | 59 | // Create caches 60 | // Generate markers. 61 | s = this.src; 62 | indent_found = false; 63 | 64 | for (start = pos = indent = offset = 0, len = s.length; pos < len; pos++) { 65 | ch = s.charCodeAt(pos); 66 | 67 | if (!indent_found) { 68 | if (isSpace(ch)) { 69 | indent++; 70 | 71 | if (ch === 0x09) { 72 | offset += 4 - offset % 4; 73 | } else { 74 | offset++; 75 | } 76 | continue; 77 | } else { 78 | indent_found = true; 79 | } 80 | } 81 | 82 | if (ch === 0x0A || pos === len - 1) { 83 | if (ch !== 0x0A) { pos++; } 84 | this.bMarks.push(start); 85 | this.eMarks.push(pos); 86 | this.tShift.push(indent); 87 | this.sCount.push(offset); 88 | this.bsCount.push(0); 89 | 90 | indent_found = false; 91 | indent = 0; 92 | offset = 0; 93 | start = pos + 1; 94 | } 95 | } 96 | 97 | // Push fake entry to simplify cache bounds checks 98 | this.bMarks.push(s.length); 99 | this.eMarks.push(s.length); 100 | this.tShift.push(0); 101 | this.sCount.push(0); 102 | this.bsCount.push(0); 103 | 104 | this.lineMax = this.bMarks.length - 1; // don't count last fake line 105 | } 106 | 107 | // Push new token to "stream". 108 | // 109 | StateBlock.prototype.push = function (type, tag, nesting) { 110 | var token = new Token(type, tag, nesting); 111 | token.block = true; 112 | 113 | if (nesting < 0) { this.level--; } 114 | token.level = this.level; 115 | if (nesting > 0) { this.level++; } 116 | 117 | this.tokens.push(token); 118 | return token; 119 | }; 120 | 121 | StateBlock.prototype.isEmpty = function isEmpty(line) { 122 | return this.bMarks[line] + this.tShift[line] >= this.eMarks[line]; 123 | }; 124 | 125 | StateBlock.prototype.skipEmptyLines = function skipEmptyLines(from) { 126 | for (var max = this.lineMax; from < max; from++) { 127 | if (this.bMarks[from] + this.tShift[from] < this.eMarks[from]) { 128 | break; 129 | } 130 | } 131 | return from; 132 | }; 133 | 134 | // Skip spaces from given position. 135 | StateBlock.prototype.skipSpaces = function skipSpaces(pos) { 136 | var ch; 137 | 138 | for (var max = this.src.length; pos < max; pos++) { 139 | ch = this.src.charCodeAt(pos); 140 | if (!isSpace(ch)) { break; } 141 | } 142 | return pos; 143 | }; 144 | 145 | // Skip spaces from given position in reverse. 146 | StateBlock.prototype.skipSpacesBack = function skipSpacesBack(pos, min) { 147 | if (pos <= min) { return pos; } 148 | 149 | while (pos > min) { 150 | if (!isSpace(this.src.charCodeAt(--pos))) { return pos + 1; } 151 | } 152 | return pos; 153 | }; 154 | 155 | // Skip char codes from given position 156 | StateBlock.prototype.skipChars = function skipChars(pos, code) { 157 | for (var max = this.src.length; pos < max; pos++) { 158 | if (this.src.charCodeAt(pos) !== code) { break; } 159 | } 160 | return pos; 161 | }; 162 | 163 | // Skip char codes reverse from given position - 1 164 | StateBlock.prototype.skipCharsBack = function skipCharsBack(pos, code, min) { 165 | if (pos <= min) { return pos; } 166 | 167 | while (pos > min) { 168 | if (code !== this.src.charCodeAt(--pos)) { return pos + 1; } 169 | } 170 | return pos; 171 | }; 172 | 173 | // cut lines range from source. 174 | StateBlock.prototype.getLines = function getLines(begin, end, indent, keepLastLF) { 175 | var i, lineIndent, ch, first, last, queue, lineStart, 176 | line = begin; 177 | 178 | if (begin >= end) { 179 | return ''; 180 | } 181 | 182 | queue = new Array(end - begin); 183 | 184 | for (i = 0; line < end; line++, i++) { 185 | lineIndent = 0; 186 | lineStart = first = this.bMarks[line]; 187 | 188 | if (line + 1 < end || keepLastLF) { 189 | // No need for bounds check because we have fake entry on tail. 190 | last = this.eMarks[line] + 1; 191 | } else { 192 | last = this.eMarks[line]; 193 | } 194 | 195 | while (first < last && lineIndent < indent) { 196 | ch = this.src.charCodeAt(first); 197 | 198 | if (isSpace(ch)) { 199 | if (ch === 0x09) { 200 | lineIndent += 4 - (lineIndent + this.bsCount[line]) % 4; 201 | } else { 202 | lineIndent++; 203 | } 204 | } else if (first - lineStart < this.tShift[line]) { 205 | // patched tShift masked characters to look like spaces (blockquotes, list markers) 206 | lineIndent++; 207 | } else { 208 | break; 209 | } 210 | 211 | first++; 212 | } 213 | 214 | if (lineIndent > indent) { 215 | // partially expanding tabs in code blocks, e.g '\t\tfoobar' 216 | // with indent=2 becomes ' \tfoobar' 217 | queue[i] = new Array(lineIndent - indent + 1).join(' ') + this.src.slice(first, last); 218 | } else { 219 | queue[i] = this.src.slice(first, last); 220 | } 221 | } 222 | 223 | return queue.join(''); 224 | }; 225 | 226 | // re-export Token class to use in block rules 227 | StateBlock.prototype.Token = Token; 228 | 229 | 230 | module.exports = StateBlock; 231 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/commonmark_extras.txt: -------------------------------------------------------------------------------- 1 | Issue #246. Double escaping in ALT 2 | . 3 | ![&](#) 4 | . 5 |

&

6 | . 7 | 8 | Strip markdown in ALT tags 9 | . 10 | ![*strip* [markdown __in__ alt](#)](#) 11 | . 12 |

strip markdown in alt

13 | . 14 | 15 | Issue #55: 16 | . 17 | ![test] 18 | 19 | ![test](foo bar) 20 | . 21 |

![test]

22 |

![test](foo bar)

23 | . 24 | 25 | 26 | Issue #35. `<` should work as punctuation 27 | . 28 | an **(:**
29 | . 30 |

an (:

31 | . 32 | 33 | 34 | Should unescape only needed things in link destinations/titles: 35 | . 36 | [test](<\f\o\o\>\\>) 37 | 38 | [test](foo "\\\"\b\a\r") 39 | . 40 |

test

41 |

test

42 | . 43 | 44 | 45 | Not a closing tag 46 | . 47 | 48 | . 49 |

</ 123>

50 | . 51 | 52 | 53 | 54 | Escaping entities in links: 55 | . 56 | [](<"> "&ö") 57 | 58 | [](<\"> "\&\ö") 59 | 60 | [](<\\"> "\\"\\ö") 61 | . 62 |

63 |

64 |

65 | . 66 | 67 | 68 | Checking combination of replaceEntities and unescapeMd: 69 | . 70 | ~~~ &&bad;\&\\& 71 | just a funny little fence 72 | ~~~ 73 | . 74 |
just a funny little fence
 75 | 
76 | . 77 | 78 | Underscore between punctuation chars should be able to close emphasis. 79 | 80 | . 81 | _(hai)_. 82 | . 83 |

(hai).

84 | . 85 | 86 | Those are two separate blockquotes: 87 | . 88 | - > foo 89 | > bar 90 | . 91 |
    92 |
  • 93 |
    94 |

    foo

    95 |
    96 |
  • 97 |
98 |
99 |

bar

100 |
101 | . 102 | 103 | Regression test (code block + regular paragraph) 104 | . 105 | > foo 106 | > bar 107 | . 108 |
109 |
foo
110 | 
111 |

bar

112 |
113 | . 114 | 115 | Blockquotes inside indented lists should terminate correctly 116 | . 117 | - a 118 | > b 119 | ``` 120 | c 121 | ``` 122 | - d 123 | . 124 |
    125 |
  • a 126 |
    127 |

    b

    128 |
    129 |
    c
    130 | 
    131 |
  • 132 |
  • d
  • 133 |
134 | . 135 | 136 | Don't output empty class here: 137 | . 138 | ``` 139 | test 140 | ``` 141 | . 142 |
test
143 | 
144 | . 145 | 146 | Setext header text supports lazy continuations: 147 | . 148 | - foo 149 | bar 150 | === 151 | . 152 |
    153 |
  • 154 |

    foo 155 | bar

    156 |
  • 157 |
158 | . 159 | 160 | But setext header underline doesn't: 161 | . 162 | - foo 163 | bar 164 | === 165 | . 166 |
    167 |
  • foo 168 | bar 169 | ===
  • 170 |
171 | . 172 | 173 | Info string in fenced code block can't contain marker used for the fence 174 | . 175 | ~~~test~ 176 | 177 | ~~~test` 178 | . 179 |

~~~test~

180 |
181 | . 182 | 183 | Tabs should be stripped from the beginning of the line 184 | . 185 | foo 186 | bar 187 | baz 188 | . 189 |

foo 190 | bar 191 | baz

192 | . 193 | 194 | Tabs should not cause hardbreak, EOL tabs aren't stripped in commonmark 0.27 195 | . 196 | foo1 197 | foo2 198 | bar 199 | . 200 |

foo1 201 | foo2
202 | bar

203 | . 204 | 205 | List item terminating quote should not be paragraph continuation 206 | . 207 | 1. foo 208 | > quote 209 | 2. bar 210 | . 211 |
    212 |
  1. foo 213 |
    214 |

    quote

    215 |
    216 |
  2. 217 |
  3. bar
  4. 218 |
219 | . 220 | 221 | Coverage. Directive can terminate paragraph. 222 | . 223 | a 224 | a

227 | * 234 | . 235 |

foo@bar.com

236 | . 237 | 238 | 239 | Coverage. Unpaired nested backtick (silent mode) 240 | . 241 | *`foo* 242 | . 243 |

`foo

244 | . 245 | 246 | 247 | Coverage. Entities. 248 | . 249 | *&* 250 | 251 | * * 252 | 253 | *&* 254 | . 255 |

&

256 |

257 |

&

258 | . 259 | 260 | 261 | Coverage. Escape. 262 | . 263 | *\a* 264 | . 265 |

\a

266 | . 267 | 268 | 269 | Coverage. parseLinkDestination 270 | . 271 | [foo](< 272 | bar>) 273 | 274 | [foo]([foo](< 277 | bar>)

278 |

[foo](<bar)

279 | . 280 | 281 | 282 | Coverage. parseLinkTitle 283 | . 284 | [foo](bar "ba) 285 | 286 | [foo](bar "ba\ 287 | z") 288 | . 289 |

[foo](bar "ba)

290 |

foo

292 | . 293 | 294 | 295 | Coverage. Image 296 | . 297 | ![test]( x ) 298 | . 299 |

test

300 | . 301 | . 302 | ![test][foo] 303 | 304 | [bar]: 123 305 | . 306 |

![test][foo]

307 | . 308 | . 309 | ![test][[[ 310 | 311 | [bar]: 123 312 | . 313 |

![test][[[

314 | . 315 | . 316 | ![test]( 317 | . 318 |

![test](

319 | . 320 | 321 | 322 | Coverage. Link 323 | . 324 | [test]( 325 | . 326 |

[test](

327 | . 328 | 329 | 330 | Coverage. Reference 331 | . 332 | [ 333 | test\ 334 | ]: 123 335 | foo 336 | bar 337 | . 338 |

foo 339 | bar

340 | . 341 | . 342 | [ 343 | test 344 | ] 345 | . 346 |

[ 347 | test 348 | ]

349 | . 350 | . 351 | > [foo]: bar 352 | [foo] 353 | . 354 |
355 |

foo

356 | . 357 | 358 | Coverage. Tabs in blockquotes. 359 | . 360 | > test 361 | 362 | > test 363 | 364 | > test 365 | 366 | > --- 367 | > test 368 | 369 | > --- 370 | > test 371 | 372 | > --- 373 | > test 374 | 375 | > test 376 | 377 | > test 378 | 379 | > test 380 | 381 | > --- 382 | > test 383 | 384 | > --- 385 | > test 386 | 387 | > --- 388 | > test 389 | . 390 |
391 |
  test
392 | 
393 |
394 |
395 |
 test
396 | 
397 |
398 |
399 |
test
400 | 
401 |
402 |
403 |
404 |
  test
405 | 
406 |
407 |
408 |
409 |
 test
410 | 
411 |
412 |
413 |
414 |
test
415 | 
416 |
417 |
418 |
  	test
419 | 
420 |
421 |
422 |
 	test
423 | 
424 |
425 |
426 |
	test
427 | 
428 |
429 |
430 |
431 |
  	test
432 | 
433 |
434 |
435 |
436 |
 	test
437 | 
438 |
439 |
440 |
441 |
	test
442 | 
443 |
444 | . 445 | 446 | Coverage. Tabs in lists. 447 | . 448 | 1. foo 449 | 450 | bar 451 | . 452 |
    453 |
  1. 454 |

    foo

    455 |
     bar
    456 | 
    457 |
  2. 458 |
459 | . 460 | 461 | Coverage. Various tags not interrupting blockquotes because of indentation: 462 | . 463 | > foo 464 | - - - - 465 | 466 | > foo 467 | # not a heading 468 | 469 | > foo 470 | ``` 471 | not a fence 472 | ``` 473 | . 474 |
475 |

foo 476 | - - - -

477 |
478 |
479 |

foo 480 | # not a heading

481 |
482 |
483 |

foo 484 | not a fence

485 |
486 | . 487 | --------------------------------------------------------------------------------