├── .editorconfig ├── .eslintrc.yml ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── development-question.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .ndocrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── benchmark ├── benchmark.mjs ├── implementations │ ├── commonmark-reference │ │ └── index.mjs │ ├── current-commonmark │ │ └── index.mjs │ ├── current │ │ └── index.mjs │ ├── markdown-it-2.2.1-commonmark │ │ └── index.mjs │ └── marked │ │ └── index.mjs ├── profile.mjs └── samples │ ├── README.md │ ├── block-bq-flat.md │ ├── block-bq-nested.md │ ├── block-code.md │ ├── block-fences.md │ ├── block-heading.md │ ├── block-hr.md │ ├── block-html.md │ ├── block-lheading.md │ ├── block-list-flat.md │ ├── block-list-nested.md │ ├── block-ref-flat.md │ ├── block-ref-list.md │ ├── block-ref-nested.md │ ├── block-tables.md │ ├── inline-autolink.md │ ├── inline-backticks.md │ ├── inline-em-flat.md │ ├── inline-em-nested.md │ ├── inline-em-worst.md │ ├── inline-entity.md │ ├── inline-escape.md │ ├── inline-html.md │ ├── inline-links-flat.md │ ├── inline-links-nested.md │ ├── inline-newlines.md │ ├── lorem1.txt │ └── rawtabs.md ├── bin └── markdown-it.mjs ├── docs ├── 4.0_migration.md ├── 5.0_migration.md ├── README.md ├── architecture.md ├── development.md ├── examples │ └── renderer_rules.md └── security.md ├── index.mjs ├── lib ├── common │ ├── html_blocks.mjs │ ├── html_re.mjs │ └── utils.mjs ├── helpers │ ├── index.mjs │ ├── parse_link_destination.mjs │ ├── parse_link_label.mjs │ └── parse_link_title.mjs ├── index.mjs ├── parser_block.mjs ├── parser_core.mjs ├── parser_inline.mjs ├── presets │ ├── commonmark.mjs │ ├── default.mjs │ └── zero.mjs ├── renderer.mjs ├── ruler.mjs ├── rules_block │ ├── blockquote.mjs │ ├── code.mjs │ ├── fence.mjs │ ├── heading.mjs │ ├── hr.mjs │ ├── html_block.mjs │ ├── lheading.mjs │ ├── list.mjs │ ├── paragraph.mjs │ ├── reference.mjs │ ├── state_block.mjs │ └── table.mjs ├── rules_core │ ├── block.mjs │ ├── inline.mjs │ ├── linkify.mjs │ ├── normalize.mjs │ ├── replacements.mjs │ ├── smartquotes.mjs │ ├── state_core.mjs │ └── text_join.mjs ├── rules_inline │ ├── autolink.mjs │ ├── backticks.mjs │ ├── balance_pairs.mjs │ ├── emphasis.mjs │ ├── entity.mjs │ ├── escape.mjs │ ├── fragments_join.mjs │ ├── html_inline.mjs │ ├── image.mjs │ ├── link.mjs │ ├── linkify.mjs │ ├── newline.mjs │ ├── state_inline.mjs │ ├── strikethrough.mjs │ └── text.mjs └── token.mjs ├── package.json ├── support ├── api_header.md ├── build_demo.mjs ├── build_doc.mjs ├── demo_template │ ├── README.md │ ├── index.css │ ├── index.html │ ├── index.mjs │ ├── rollup.config.mjs │ └── sample.md ├── rollup.config.mjs └── specsplit.mjs └── test ├── cjs.js ├── commonmark.mjs ├── fixtures ├── commonmark │ ├── bad.txt │ ├── good.txt │ └── spec.txt └── markdown-it │ ├── commonmark_extras.txt │ ├── fatal.txt │ ├── linkify.txt │ ├── normalize.txt │ ├── proto.txt │ ├── smartquotes.txt │ ├── strikethrough.txt │ ├── tables.txt │ ├── typographer.txt │ └── xss.txt ├── markdown-it.mjs ├── misc.mjs ├── pathological.json ├── pathological.mjs ├── pathological_worker.js ├── ruler.mjs ├── token.mjs └── utils.mjs /.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 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: standard 2 | 3 | overrides: 4 | - 5 | files: [ '*.mjs' ] 6 | rules: 7 | no-restricted-globals: [ 2, require, __dirname ] 8 | - 9 | files: [ 'test/**' ] 10 | env: { mocha: true } 11 | - 12 | files: [ 'lib/**' ] 13 | parserOptions: { ecmaVersion: 2015 } 14 | 15 | ignorePatterns: 16 | - demo/ 17 | - dist/ 18 | - benchmark/extra/ 19 | 20 | rules: 21 | camelcase: 0 22 | no-multi-spaces: 0 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: markdown-it 2 | open_collective: markdown-it 3 | tidelift: "npm/markdown-it" 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | <!-- 11 | 12 | Please note, this package is about IMPLEMENTATION of CommonMark https://commonmark.org/, not about markdown itself. We stay aside of markup discussions. Prior to report a bug, make sure it's about this package, not generic thing. 13 | 14 | **Before you post** 15 | 16 | 1. https://spec.commonmark.org/ - make sure you've read CommonMark spec. 17 | 2. https://spec.commonmark.org/dingus/ - if you think you found parse error, check it in reference implementation first. 18 | 19 | **In your report** 20 | 21 | It will be very helpful, if you can provide permalinks with online samples and explain the difference: 22 | 23 | - https://markdown-it.github.io/ - online demo of `markdown-it`. 24 | - https://spec.commonmark.org/dingus/ - online demo of reference CommonMark's implementation. 25 | 26 | If you wish to provide code sample - make sure it is as small as possible and can be executed. 27 | 28 | --> 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/development-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Development question 3 | about: '' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | <!-- 11 | 12 | Note, we have some time constrains, but we always try to help developers, who write plugins. So: 13 | 14 | - Please, avoid generic programming questions. 15 | - Avoid questions about markdown. Use CommonMark resources for that https://commonmark.org/. 16 | - If you have issue with plugin - report it to plugin's repo/author. 17 | - Make sure you are familiar with dev docs https://github.com/markdown-it/markdown-it/tree/master/docs, and tried to do something. 18 | - Code samples are welcome. 19 | 20 | Also, you may find useful this links (may be someone already solved your problem): 21 | 22 | - https://github.com/markdown-it - list of "officially" provided plugins. 23 | - https://www.npmjs.com/search?q=keywords:markdown-it-plugin - community-written plugins. 24 | 25 | --> 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | <!-- 11 | 12 | Please note, this package is highly extendable. Prior to request new feature, make sure it can not be implemented via plugins. 13 | 14 | You may also find useful this links: 15 | 16 | - https://github.com/markdown-it - list of "officially" provided plugins. 17 | - https://www.npmjs.com/search?q=keywords:markdown-it-plugin - community-written plugins. 18 | - https://github.com/markdown-it/markdown-it/tree/master/docs - docs for plugin developers. 19 | 20 | --> 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: For developpers 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | <!-- 11 | 12 | Note, we have some time constraints, but we always try to help developers, who write plugins. So: 13 | 14 | - Please, avoid generic programming questions. 15 | - Avoid questions about markdown. Use CommonMark resources for that https://commonmark.org/. 16 | - If you have issue with plugin - report it to plugin's repo/author. 17 | - Make sure you are familiar with dev docs https://github.com/markdown-it/markdown-it/tree/master/docs, and tried to do something. 18 | - Code samples are welcome. 19 | 20 | Also, you may find useful this links (may be someone already solved your problem): 21 | 22 | - https://github.com/markdown-it - list of "officially" provided plugins. 23 | - https://www.npmjs.com/search?q=keywords:markdown-it-plugin - community-written plugins. 24 | 25 | --> 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: npm 9 | directory: / 10 | schedule: 11 | interval: daily 12 | allow: 13 | - dependency-type: production 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * 3' 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [ '18' ] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - run: npm install 27 | 28 | - name: Test 29 | run: npm test 30 | 31 | - name: Upload coverage report to coveralls.io 32 | uses: coverallsapp/github-action@master 33 | with: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | benchmark/extra/ 2 | node_modules/ 3 | coverage/ 4 | demo/ 5 | apidoc/ 6 | dist/ 7 | *.log 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### If you commit changes: 2 | 3 | 1. Make sure all tests pass. 4 | 2. Run `./benchmark/benchmark.mjs`, 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /benchmark/benchmark.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint no-console:0 */ 3 | 4 | import fs from 'node:fs' 5 | import util from 'node:util' 6 | import Benchmark from 'benchmark' 7 | import ansi from 'ansi' 8 | const cursor = ansi(process.stdout) 9 | 10 | const IMPLS = [] 11 | 12 | for (const name of fs.readdirSync(new URL('./implementations', import.meta.url)).sort()) { 13 | const filepath = new URL(`./implementations/${name}/index.mjs`, import.meta.url) 14 | const code = (await import(filepath)) 15 | 16 | IMPLS.push({ name, code }) 17 | } 18 | 19 | const SAMPLES = [] 20 | 21 | fs.readdirSync(new URL('./samples', import.meta.url)).sort().forEach(sample => { 22 | const filepath = new URL(`./samples/${sample}`, import.meta.url) 23 | 24 | const content = {} 25 | 26 | content.string = fs.readFileSync(filepath, 'utf8') 27 | 28 | const title = `(${content.string.length} bytes)` 29 | 30 | function onComplete () { cursor.write('\n') } 31 | 32 | const suite = new Benchmark.Suite( 33 | title, 34 | { 35 | onStart: () => { console.log('\nSample: %s %s', sample, title) }, 36 | onComplete 37 | } 38 | ) 39 | 40 | IMPLS.forEach(function (impl) { 41 | suite.add( 42 | impl.name, 43 | { 44 | onCycle: function onCycle (event) { 45 | cursor.horizontalAbsolute() 46 | cursor.eraseLine() 47 | cursor.write(' > ' + event.target) 48 | }, 49 | onComplete, 50 | fn: function () { impl.code.run(content.string) } 51 | } 52 | ) 53 | }) 54 | 55 | SAMPLES.push({ name: sample.split('.')[0], title, content, suite }) 56 | }) 57 | 58 | function select (patterns) { 59 | const result = [] 60 | 61 | if (!(patterns instanceof Array)) { 62 | patterns = [patterns] 63 | } 64 | 65 | function checkName (name) { 66 | return patterns.length === 0 || patterns.some(function (regexp) { 67 | return regexp.test(name) 68 | }) 69 | } 70 | 71 | SAMPLES.forEach(function (sample) { 72 | if (checkName(sample.name)) { 73 | result.push(sample) 74 | } 75 | }) 76 | 77 | return result 78 | } 79 | 80 | function run (files) { 81 | const selected = select(files) 82 | 83 | if (selected.length > 0) { 84 | console.log('Selected samples: (%d of %d)', selected.length, SAMPLES.length) 85 | selected.forEach(function (sample) { 86 | console.log(' > %s', sample.name) 87 | }) 88 | } else { 89 | console.log('There isn\'t any sample matches any of these patterns: %s', util.inspect(files)) 90 | } 91 | 92 | selected.forEach(function (sample) { 93 | sample.suite.run() 94 | }) 95 | } 96 | 97 | run(process.argv.slice(2).map(source => new RegExp(source, 'i'))) 98 | -------------------------------------------------------------------------------- /benchmark/implementations/commonmark-reference/index.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | 3 | const commonmark = createRequire(import.meta.url)('../../extra/lib/node_modules/commonmark') 4 | 5 | const parser = new commonmark.Parser() 6 | const renderer = new commonmark.HtmlRenderer() 7 | 8 | export function run (data) { 9 | return renderer.render(parser.parse(data)) 10 | } 11 | -------------------------------------------------------------------------------- /benchmark/implementations/current-commonmark/index.mjs: -------------------------------------------------------------------------------- 1 | import markdownit from '../../../index.mjs' 2 | 3 | const md = markdownit('commonmark') 4 | 5 | // Replace normalizers to more primitive, for more "honest" compare. 6 | // Default ones can cause 1.5x slowdown. 7 | const encode = md.utils.lib.mdurl.encode 8 | 9 | md.normalizeLink = function (url) { return encode(url) } 10 | md.normalizeLinkText = function (str) { return str } 11 | 12 | export function run (data) { 13 | return md.render(data) 14 | } 15 | -------------------------------------------------------------------------------- /benchmark/implementations/current/index.mjs: -------------------------------------------------------------------------------- 1 | import markdownit from '../../../index.mjs' 2 | 3 | const md = markdownit({ 4 | html: true, 5 | linkify: true, 6 | typographer: true 7 | }) 8 | 9 | export function run (data) { 10 | return md.render(data) 11 | } 12 | -------------------------------------------------------------------------------- /benchmark/implementations/markdown-it-2.2.1-commonmark/index.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | 3 | const markdownit = createRequire(import.meta.url)('../../extra/lib/node_modules/markdown-it') 4 | 5 | const md = markdownit('commonmark') 6 | 7 | export function run (data) { 8 | return md.render(data) 9 | } 10 | -------------------------------------------------------------------------------- /benchmark/implementations/marked/index.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | 3 | const marked = createRequire(import.meta.url)('../../extra/lib/node_modules/marked') 4 | 5 | export function run (data) { 6 | return marked(data) 7 | } 8 | -------------------------------------------------------------------------------- /benchmark/profile.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint no-console:0 */ 3 | 4 | import { readFileSync } from 'fs' 5 | import markdownit from '../index.mjs' 6 | 7 | const md = markdownit({ 8 | html: true, 9 | linkify: false, 10 | typographer: false 11 | }) 12 | 13 | const data = readFileSync(new URL('../test/fixtures/commonmark/spec.txt', import.meta.url), 'utf8') 14 | 15 | for (let i = 0; i < 20; i++) { 16 | md.render(data) 17 | } 18 | -------------------------------------------------------------------------------- /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-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 | -------------------------------------------------------------------------------- /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-heading.md: -------------------------------------------------------------------------------- 1 | # heading 2 | ### heading 3 | ##### heading 4 | 5 | # heading # 6 | ### heading ### 7 | ##### heading \#\#\#\#\###### 8 | 9 | ############ not a heading 10 | -------------------------------------------------------------------------------- /benchmark/samples/block-hr.md: -------------------------------------------------------------------------------- 1 | 2 | * * * * * 3 | 4 | - - - - - 5 | 6 | ________ 7 | 8 | 9 | ************************* text 10 | 11 | -------------------------------------------------------------------------------- /benchmark/samples/block-html.md: -------------------------------------------------------------------------------- 1 | <div class="this is an html block"> 2 | 3 | blah blah 4 | 5 | </div> 6 | 7 | <table> 8 | <tr> 9 | <td> 10 | **test** 11 | </td> 12 | </tr> 13 | </table> 14 | 15 | <table> 16 | 17 | <tr> 18 | 19 | <td> 20 | 21 | test 22 | 23 | </td> 24 | 25 | </tr> 26 | 27 | </table> 28 | 29 | <![CDATA[ 30 | [[[[[[[[[[[... *cdata section - this should not be parsed* ...]]]]]]]]]]] 31 | ]]> 32 | 33 | -------------------------------------------------------------------------------- /benchmark/samples/block-lheading.md: -------------------------------------------------------------------------------- 1 | heading 2 | --- 3 | 4 | heading 5 | =================================== 6 | 7 | not a heading 8 | ----------------------------------- text 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /benchmark/samples/block-ref-flat.md: -------------------------------------------------------------------------------- 1 | [1] [2] [3] [1] [2] [3] 2 | 3 | [looooooooooooooooooooooooooooooooooooooooooooooooooong label] 4 | 5 | [1]: <http://something.example.com/foo/bar> 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-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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | <ftp://1.2.3.4:21/path/foo> 4 | <http://foo.bar.baz?q=hello&id=22&boolean> 5 | <http://veeeeeeeeeeeeeeeeeeery.loooooooooooooooooooooooooooooooong.autolink/> 6 | <teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeest@gmail.com> 7 | 8 | these are not autolinks: 9 | 10 | <ftp://1.2.3.4:21/path/foo 11 | <http://foo.bar.baz?q=hello&id=22&boolean 12 | <http://veeeeeeeeeeeeeeeeeeery.loooooooooooooooooooooooooooooooong.autolink 13 | <teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeest@gmail.com 14 | < http://foo.bar.baz?q=hello&id=22&boolean > 15 | -------------------------------------------------------------------------------- /benchmark/samples/inline-backticks.md: -------------------------------------------------------------------------------- 1 | `lots`of`backticks` 2 | 3 | ``i``wonder``how``this``will``be``parsed`` 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | \<this\> \<is\> \<not\> \<html\> 15 | 16 | -------------------------------------------------------------------------------- /benchmark/samples/inline-html.md: -------------------------------------------------------------------------------- 1 | Taking commonmark tests from the spec for benchmarking here: 2 | 3 | <a><bab><c2c> 4 | 5 | <a/><b2/> 6 | 7 | <a /><b2 8 | data="foo" > 9 | 10 | <a foo="bar" bam = 'baz <em>"</em>' 11 | _boolean zoop:33=zoop:33 /> 12 | 13 | <33> <__> 14 | 15 | <a h*#ref="hi"> 16 | 17 | <a href="hi'> <a href=hi'> 18 | 19 | < a>< 20 | foo><bar/ > 21 | 22 | <a href='bar'title=title> 23 | 24 | </a> 25 | </foo > 26 | 27 | </a href="foo"> 28 | 29 | foo <!-- this is a 30 | comment - with hyphen --> 31 | 32 | foo <!-- not a comment -- two hyphens --> 33 | 34 | foo <?php echo $a; ?> 35 | 36 | foo <!ELEMENT br EMPTY> 37 | 38 | foo <![CDATA[>&<]]> 39 | 40 | <a href="ö"> 41 | 42 | <a href="\*"> 43 | 44 | <a href="\""> 45 | -------------------------------------------------------------------------------- /benchmark/samples/inline-links-flat.md: -------------------------------------------------------------------------------- 1 | Valid links: 2 | 3 | [this is a link]() 4 | [this is a link](<http://something.example.com/foo/bar>) 5 | [this is a link](http://something.example.com/foo/bar 'test') 6 | ![this is an image]() 7 |  8 |  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/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 |  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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/markdown-it.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint no-console:0 */ 3 | 4 | import fs from 'node:fs' 5 | import argparse from 'argparse' 6 | import markdownit from '../index.mjs' 7 | 8 | const cli = new argparse.ArgumentParser({ 9 | prog: 'markdown-it', 10 | add_help: true 11 | }) 12 | 13 | cli.add_argument('-v', '--version', { 14 | action: 'version', 15 | version: JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url))).version 16 | }) 17 | 18 | cli.add_argument('--no-html', { 19 | help: 'Disable embedded HTML', 20 | action: 'store_true' 21 | }) 22 | 23 | cli.add_argument('-l', '--linkify', { 24 | help: 'Autolink text', 25 | action: 'store_true' 26 | }) 27 | 28 | cli.add_argument('-t', '--typographer', { 29 | help: 'Enable smartquotes and other typographic replacements', 30 | action: 'store_true' 31 | }) 32 | 33 | cli.add_argument('--trace', { 34 | help: 'Show stack trace on error', 35 | action: 'store_true' 36 | }) 37 | 38 | cli.add_argument('file', { 39 | help: 'File to read', 40 | nargs: '?', 41 | default: '-' 42 | }) 43 | 44 | cli.add_argument('-o', '--output', { 45 | help: 'File to write', 46 | default: '-' 47 | }) 48 | 49 | const options = cli.parse_args() 50 | 51 | function readFile (filename, encoding, callback) { 52 | if (options.file === '-') { 53 | // read from stdin 54 | const chunks = [] 55 | 56 | process.stdin.on('data', function (chunk) { chunks.push(chunk) }) 57 | 58 | process.stdin.on('end', function () { 59 | return callback(null, Buffer.concat(chunks).toString(encoding)) 60 | }) 61 | } else { 62 | fs.readFile(filename, encoding, callback) 63 | } 64 | } 65 | 66 | readFile(options.file, 'utf8', function (err, input) { 67 | let output 68 | 69 | if (err) { 70 | if (err.code === 'ENOENT') { 71 | console.error('File not found: ' + options.file) 72 | process.exit(2) 73 | } 74 | 75 | console.error( 76 | (options.trace && err.stack) || 77 | err.message || 78 | String(err)) 79 | 80 | process.exit(1) 81 | } 82 | 83 | const md = markdownit({ 84 | html: !options.no_html, 85 | xhtmlOut: false, 86 | typographer: options.typographer, 87 | linkify: options.linkify 88 | }) 89 | 90 | try { 91 | output = md.render(input) 92 | } catch (e) { 93 | console.error( 94 | (options.trace && e.stack) || 95 | e.message || 96 | String(e)) 97 | 98 | process.exit(1) 99 | } 100 | 101 | if (options.output === '-') { 102 | // write to stdout 103 | process.stdout.write(output) 104 | } else { 105 | fs.writeFileSync(options.output, output) 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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` keywords `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.mjs) and [emoji](https://github.com/markdown-it/markdown-it-emoji/blob/master/lib/replace.mjs) - 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.mjs), 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 | -------------------------------------------------------------------------------- /docs/security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Many people don't understand that markdown format does not care much about 4 | security. In many cases you have to pass output to sanitizers. `markdown-it` 5 | provides 2 possible strategies to produce safe output: 6 | 7 | 1. Don't enable HTML. Extend markup features with [plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). We think it's the best choice and use it by default. 8 | - That's ok for 99% of user needs. 9 | - Output will be safe without sanitizer. 10 | 2. Enable HTML and use external sanitizer package. 11 | 12 | Also by default `markdown-it` prohibits some kind of links, which could be used 13 | for XSS: 14 | 15 | - `javascript:`, `vbscript:` 16 | - `file:` 17 | - `data:`, except some images (gif/png/jpeg/webp). 18 | 19 | So, by default `markdown-it` should be safe. We care about it. 20 | 21 | If you find a security problem - contact us via tracker or email. Such reports 22 | are fixed with top priority. 23 | 24 | 25 | ## Plugins 26 | 27 | Usually, plugins operate with tokenized content, and that's enough to provide 28 | safe output. 29 | 30 | But there is one non-evident case you should know - don't allow plugins to 31 | generate arbitrary element `id` and `name`. If those depend on user input - 32 | always add prefixes to avoid DOM clobbering. See [discussion](https://github.com/markdown-it/markdown-it/issues/28) for details. 33 | 34 | So, if you decide to use plugins that add extended class syntax or 35 | autogenerating header anchors - be careful. 36 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | export { default } from './lib/index.mjs' 2 | -------------------------------------------------------------------------------- /lib/common/html_blocks.mjs: -------------------------------------------------------------------------------- 1 | // List of valid html blocks names, according to commonmark spec 2 | // https://spec.commonmark.org/0.30/#html-blocks 3 | 4 | export default [ 5 | 'address', 6 | 'article', 7 | 'aside', 8 | 'base', 9 | 'basefont', 10 | 'blockquote', 11 | 'body', 12 | 'caption', 13 | 'center', 14 | 'col', 15 | 'colgroup', 16 | 'dd', 17 | 'details', 18 | 'dialog', 19 | 'dir', 20 | 'div', 21 | 'dl', 22 | 'dt', 23 | 'fieldset', 24 | 'figcaption', 25 | 'figure', 26 | 'footer', 27 | 'form', 28 | 'frame', 29 | 'frameset', 30 | 'h1', 31 | 'h2', 32 | 'h3', 33 | 'h4', 34 | 'h5', 35 | 'h6', 36 | 'head', 37 | 'header', 38 | 'hr', 39 | 'html', 40 | 'iframe', 41 | 'legend', 42 | 'li', 43 | 'link', 44 | 'main', 45 | 'menu', 46 | 'menuitem', 47 | 'nav', 48 | 'noframes', 49 | 'ol', 50 | 'optgroup', 51 | 'option', 52 | 'p', 53 | 'param', 54 | 'search', 55 | 'section', 56 | 'summary', 57 | 'table', 58 | 'tbody', 59 | 'td', 60 | 'tfoot', 61 | 'th', 62 | 'thead', 63 | 'title', 64 | 'tr', 65 | 'track', 66 | 'ul' 67 | ] 68 | -------------------------------------------------------------------------------- /lib/common/html_re.mjs: -------------------------------------------------------------------------------- 1 | // Regexps to match html elements 2 | 3 | const attr_name = '[a-zA-Z_:][a-zA-Z0-9:._-]*' 4 | 5 | const unquoted = '[^"\'=<>`\\x00-\\x20]+' 6 | const single_quoted = "'[^']*'" 7 | const double_quoted = '"[^"]*"' 8 | 9 | const attr_value = '(?:' + unquoted + '|' + single_quoted + '|' + double_quoted + ')' 10 | 11 | const attribute = '(?:\\s+' + attr_name + '(?:\\s*=\\s*' + attr_value + ')?)' 12 | 13 | const open_tag = '<[A-Za-z][A-Za-z0-9\\-]*' + attribute + '*\\s*\\/?>' 14 | 15 | const close_tag = '<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>' 16 | const comment = '<!---?>|<!--(?:[^-]|-[^-]|--[^>])*-->' 17 | const processing = '<[?][\\s\\S]*?[?]>' 18 | const declaration = '<![A-Za-z][^>]*>' 19 | const cdata = '<!\\[CDATA\\[[\\s\\S]*?\\]\\]>' 20 | 21 | const HTML_TAG_RE = new RegExp('^(?:' + open_tag + '|' + close_tag + '|' + comment + 22 | '|' + processing + '|' + declaration + '|' + cdata + ')') 23 | const HTML_OPEN_CLOSE_TAG_RE = new RegExp('^(?:' + open_tag + '|' + close_tag + ')') 24 | 25 | export { HTML_TAG_RE, HTML_OPEN_CLOSE_TAG_RE } 26 | -------------------------------------------------------------------------------- /lib/helpers/index.mjs: -------------------------------------------------------------------------------- 1 | // Just a shortcut for bulk export 2 | 3 | import parseLinkLabel from './parse_link_label.mjs' 4 | import parseLinkDestination from './parse_link_destination.mjs' 5 | import parseLinkTitle from './parse_link_title.mjs' 6 | 7 | export { 8 | parseLinkLabel, 9 | parseLinkDestination, 10 | parseLinkTitle 11 | } 12 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_destination.mjs: -------------------------------------------------------------------------------- 1 | // Parse link destination 2 | // 3 | 4 | import { unescapeAll } from '../common/utils.mjs' 5 | 6 | export default function parseLinkDestination (str, start, max) { 7 | let code 8 | let pos = start 9 | 10 | const result = { 11 | ok: false, 12 | pos: 0, 13 | str: '' 14 | } 15 | 16 | if (str.charCodeAt(pos) === 0x3C /* < */) { 17 | pos++ 18 | while (pos < max) { 19 | code = str.charCodeAt(pos) 20 | if (code === 0x0A /* \n */) { return result } 21 | if (code === 0x3C /* < */) { return result } 22 | if (code === 0x3E /* > */) { 23 | result.pos = pos + 1 24 | result.str = unescapeAll(str.slice(start + 1, pos)) 25 | result.ok = true 26 | return result 27 | } 28 | if (code === 0x5C /* \ */ && pos + 1 < max) { 29 | pos += 2 30 | continue 31 | } 32 | 33 | pos++ 34 | } 35 | 36 | // no closing '>' 37 | return result 38 | } 39 | 40 | // this should be ... } else { ... branch 41 | 42 | let level = 0 43 | while (pos < max) { 44 | code = str.charCodeAt(pos) 45 | 46 | if (code === 0x20) { break } 47 | 48 | // ascii control characters 49 | if (code < 0x20 || code === 0x7F) { break } 50 | 51 | if (code === 0x5C /* \ */ && pos + 1 < max) { 52 | if (str.charCodeAt(pos + 1) === 0x20) { break } 53 | pos += 2 54 | continue 55 | } 56 | 57 | if (code === 0x28 /* ( */) { 58 | level++ 59 | if (level > 32) { return result } 60 | } 61 | 62 | if (code === 0x29 /* ) */) { 63 | if (level === 0) { break } 64 | level-- 65 | } 66 | 67 | pos++ 68 | } 69 | 70 | if (start === pos) { return result } 71 | if (level !== 0) { return result } 72 | 73 | result.str = unescapeAll(str.slice(start, pos)) 74 | result.pos = pos 75 | result.ok = true 76 | return result 77 | } 78 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_label.mjs: -------------------------------------------------------------------------------- 1 | // Parse link label 2 | // 3 | // this function assumes that first character ("[") already matches; 4 | // returns the end of the label 5 | // 6 | 7 | export default function parseLinkLabel (state, start, disableNested) { 8 | let level, found, marker, prevPos 9 | 10 | const max = state.posMax 11 | const oldPos = state.pos 12 | 13 | state.pos = start + 1 14 | level = 1 15 | 16 | while (state.pos < max) { 17 | marker = state.src.charCodeAt(state.pos) 18 | if (marker === 0x5D /* ] */) { 19 | level-- 20 | if (level === 0) { 21 | found = true 22 | break 23 | } 24 | } 25 | 26 | prevPos = state.pos 27 | state.md.inline.skipToken(state) 28 | if (marker === 0x5B /* [ */) { 29 | if (prevPos === state.pos - 1) { 30 | // increase level if we find text `[`, which is not a part of any token 31 | level++ 32 | } else if (disableNested) { 33 | state.pos = oldPos 34 | return -1 35 | } 36 | } 37 | } 38 | 39 | let labelEnd = -1 40 | 41 | if (found) { 42 | labelEnd = state.pos 43 | } 44 | 45 | // restore old state 46 | state.pos = oldPos 47 | 48 | return labelEnd 49 | } 50 | -------------------------------------------------------------------------------- /lib/helpers/parse_link_title.mjs: -------------------------------------------------------------------------------- 1 | // Parse link title 2 | // 3 | 4 | import { unescapeAll } from '../common/utils.mjs' 5 | 6 | // Parse link title within `str` in [start, max] range, 7 | // or continue previous parsing if `prev_state` is defined (equal to result of last execution). 8 | // 9 | export default function parseLinkTitle (str, start, max, prev_state) { 10 | let code 11 | let pos = start 12 | 13 | const state = { 14 | // if `true`, this is a valid link title 15 | ok: false, 16 | // if `true`, this link can be continued on the next line 17 | can_continue: false, 18 | // if `ok`, it's the position of the first character after the closing marker 19 | pos: 0, 20 | // if `ok`, it's the unescaped title 21 | str: '', 22 | // expected closing marker character code 23 | marker: 0 24 | } 25 | 26 | if (prev_state) { 27 | // this is a continuation of a previous parseLinkTitle call on the next line, 28 | // used in reference links only 29 | state.str = prev_state.str 30 | state.marker = prev_state.marker 31 | } else { 32 | if (pos >= max) { return state } 33 | 34 | let marker = str.charCodeAt(pos) 35 | if (marker !== 0x22 /* " */ && marker !== 0x27 /* ' */ && marker !== 0x28 /* ( */) { return state } 36 | 37 | start++ 38 | pos++ 39 | 40 | // if opening marker is "(", switch it to closing marker ")" 41 | if (marker === 0x28) { marker = 0x29 } 42 | 43 | state.marker = marker 44 | } 45 | 46 | while (pos < max) { 47 | code = str.charCodeAt(pos) 48 | if (code === state.marker) { 49 | state.pos = pos + 1 50 | state.str += unescapeAll(str.slice(start, pos)) 51 | state.ok = true 52 | return state 53 | } else if (code === 0x28 /* ( */ && state.marker === 0x29 /* ) */) { 54 | return state 55 | } else if (code === 0x5C /* \ */ && pos + 1 < max) { 56 | pos++ 57 | } 58 | 59 | pos++ 60 | } 61 | 62 | // no closing marker found, but this link title may continue on the next line (for references) 63 | state.can_continue = true 64 | state.str += unescapeAll(str.slice(start, pos)) 65 | return state 66 | } 67 | -------------------------------------------------------------------------------- /lib/parser_block.mjs: -------------------------------------------------------------------------------- 1 | /** internal 2 | * class ParserBlock 3 | * 4 | * Block-level tokenizer. 5 | **/ 6 | 7 | import Ruler from './ruler.mjs' 8 | import StateBlock from './rules_block/state_block.mjs' 9 | 10 | import r_table from './rules_block/table.mjs' 11 | import r_code from './rules_block/code.mjs' 12 | import r_fence from './rules_block/fence.mjs' 13 | import r_blockquote from './rules_block/blockquote.mjs' 14 | import r_hr from './rules_block/hr.mjs' 15 | import r_list from './rules_block/list.mjs' 16 | import r_reference from './rules_block/reference.mjs' 17 | import r_html_block from './rules_block/html_block.mjs' 18 | import r_heading from './rules_block/heading.mjs' 19 | import r_lheading from './rules_block/lheading.mjs' 20 | import r_paragraph from './rules_block/paragraph.mjs' 21 | 22 | const _rules = [ 23 | // First 2 params - rule name & source. Secondary array - list of rules, 24 | // which can be terminated by this one. 25 | ['table', r_table, ['paragraph', 'reference']], 26 | ['code', r_code], 27 | ['fence', r_fence, ['paragraph', 'reference', 'blockquote', 'list']], 28 | ['blockquote', r_blockquote, ['paragraph', 'reference', 'blockquote', 'list']], 29 | ['hr', r_hr, ['paragraph', 'reference', 'blockquote', 'list']], 30 | ['list', r_list, ['paragraph', 'reference', 'blockquote']], 31 | ['reference', r_reference], 32 | ['html_block', r_html_block, ['paragraph', 'reference', 'blockquote']], 33 | ['heading', r_heading, ['paragraph', 'reference', 'blockquote']], 34 | ['lheading', r_lheading], 35 | ['paragraph', r_paragraph] 36 | ] 37 | 38 | /** 39 | * new ParserBlock() 40 | **/ 41 | function ParserBlock () { 42 | /** 43 | * ParserBlock#ruler -> Ruler 44 | * 45 | * [[Ruler]] instance. Keep configuration of block rules. 46 | **/ 47 | this.ruler = new Ruler() 48 | 49 | for (let i = 0; i < _rules.length; i++) { 50 | this.ruler.push(_rules[i][0], _rules[i][1], { alt: (_rules[i][2] || []).slice() }) 51 | } 52 | } 53 | 54 | // Generate tokens for input range 55 | // 56 | ParserBlock.prototype.tokenize = function (state, startLine, endLine) { 57 | const rules = this.ruler.getRules('') 58 | const len = rules.length 59 | const maxNesting = state.md.options.maxNesting 60 | let line = startLine 61 | let hasEmptyLines = false 62 | 63 | while (line < endLine) { 64 | state.line = line = state.skipEmptyLines(line) 65 | if (line >= endLine) { break } 66 | 67 | // Termination condition for nested calls. 68 | // Nested calls currently used for blockquotes & lists 69 | if (state.sCount[line] < state.blkIndent) { break } 70 | 71 | // If nesting level exceeded - skip tail to the end. That's not ordinary 72 | // situation and we should not care about content. 73 | if (state.level >= maxNesting) { 74 | state.line = endLine 75 | break 76 | } 77 | 78 | // Try all possible rules. 79 | // On success, rule should: 80 | // 81 | // - update `state.line` 82 | // - update `state.tokens` 83 | // - return true 84 | const prevLine = state.line 85 | let ok = false 86 | 87 | for (let i = 0; i < len; i++) { 88 | ok = rules[i](state, line, endLine, false) 89 | if (ok) { 90 | if (prevLine >= state.line) { 91 | throw new Error("block rule didn't increment state.line") 92 | } 93 | break 94 | } 95 | } 96 | 97 | // this can only happen if user disables paragraph rule 98 | if (!ok) throw new Error('none of the block rules matched') 99 | 100 | // set state.tight if we had an empty line before current tag 101 | // i.e. latest empty line should not count 102 | state.tight = !hasEmptyLines 103 | 104 | // paragraph might "eat" one newline after it in nested lists 105 | if (state.isEmpty(state.line - 1)) { 106 | hasEmptyLines = true 107 | } 108 | 109 | line = state.line 110 | 111 | if (line < endLine && state.isEmpty(line)) { 112 | hasEmptyLines = true 113 | line++ 114 | state.line = line 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * ParserBlock.parse(str, md, env, outTokens) 121 | * 122 | * Process input string and push block tokens into `outTokens` 123 | **/ 124 | ParserBlock.prototype.parse = function (src, md, env, outTokens) { 125 | if (!src) { return } 126 | 127 | const state = new this.State(src, md, env, outTokens) 128 | 129 | this.tokenize(state, state.line, state.lineMax) 130 | } 131 | 132 | ParserBlock.prototype.State = StateBlock 133 | 134 | export default ParserBlock 135 | -------------------------------------------------------------------------------- /lib/parser_core.mjs: -------------------------------------------------------------------------------- 1 | /** internal 2 | * class Core 3 | * 4 | * Top-level rules executor. Glues block/inline parsers and does intermediate 5 | * transformations. 6 | **/ 7 | 8 | import Ruler from './ruler.mjs' 9 | import StateCore from './rules_core/state_core.mjs' 10 | 11 | import r_normalize from './rules_core/normalize.mjs' 12 | import r_block from './rules_core/block.mjs' 13 | import r_inline from './rules_core/inline.mjs' 14 | import r_linkify from './rules_core/linkify.mjs' 15 | import r_replacements from './rules_core/replacements.mjs' 16 | import r_smartquotes from './rules_core/smartquotes.mjs' 17 | import r_text_join from './rules_core/text_join.mjs' 18 | 19 | const _rules = [ 20 | ['normalize', r_normalize], 21 | ['block', r_block], 22 | ['inline', r_inline], 23 | ['linkify', r_linkify], 24 | ['replacements', r_replacements], 25 | ['smartquotes', r_smartquotes], 26 | // `text_join` finds `text_special` tokens (for escape sequences) 27 | // and joins them with the rest of the text 28 | ['text_join', r_text_join] 29 | ] 30 | 31 | /** 32 | * new Core() 33 | **/ 34 | function Core () { 35 | /** 36 | * Core#ruler -> Ruler 37 | * 38 | * [[Ruler]] instance. Keep configuration of core rules. 39 | **/ 40 | this.ruler = new Ruler() 41 | 42 | for (let i = 0; i < _rules.length; i++) { 43 | this.ruler.push(_rules[i][0], _rules[i][1]) 44 | } 45 | } 46 | 47 | /** 48 | * Core.process(state) 49 | * 50 | * Executes core chain rules. 51 | **/ 52 | Core.prototype.process = function (state) { 53 | const rules = this.ruler.getRules('') 54 | 55 | for (let i = 0, l = rules.length; i < l; i++) { 56 | rules[i](state) 57 | } 58 | } 59 | 60 | Core.prototype.State = StateCore 61 | 62 | export default Core 63 | -------------------------------------------------------------------------------- /lib/parser_inline.mjs: -------------------------------------------------------------------------------- 1 | /** internal 2 | * class ParserInline 3 | * 4 | * Tokenizes paragraph content. 5 | **/ 6 | 7 | import Ruler from './ruler.mjs' 8 | import StateInline from './rules_inline/state_inline.mjs' 9 | 10 | import r_text from './rules_inline/text.mjs' 11 | import r_linkify from './rules_inline/linkify.mjs' 12 | import r_newline from './rules_inline/newline.mjs' 13 | import r_escape from './rules_inline/escape.mjs' 14 | import r_backticks from './rules_inline/backticks.mjs' 15 | import r_strikethrough from './rules_inline/strikethrough.mjs' 16 | import r_emphasis from './rules_inline/emphasis.mjs' 17 | import r_link from './rules_inline/link.mjs' 18 | import r_image from './rules_inline/image.mjs' 19 | import r_autolink from './rules_inline/autolink.mjs' 20 | import r_html_inline from './rules_inline/html_inline.mjs' 21 | import r_entity from './rules_inline/entity.mjs' 22 | 23 | import r_balance_pairs from './rules_inline/balance_pairs.mjs' 24 | import r_fragments_join from './rules_inline/fragments_join.mjs' 25 | 26 | // Parser rules 27 | 28 | const _rules = [ 29 | ['text', r_text], 30 | ['linkify', r_linkify], 31 | ['newline', r_newline], 32 | ['escape', r_escape], 33 | ['backticks', r_backticks], 34 | ['strikethrough', r_strikethrough.tokenize], 35 | ['emphasis', r_emphasis.tokenize], 36 | ['link', r_link], 37 | ['image', r_image], 38 | ['autolink', r_autolink], 39 | ['html_inline', r_html_inline], 40 | ['entity', r_entity] 41 | ] 42 | 43 | // `rule2` ruleset was created specifically for emphasis/strikethrough 44 | // post-processing and may be changed in the future. 45 | // 46 | // Don't use this for anything except pairs (plugins working with `balance_pairs`). 47 | // 48 | const _rules2 = [ 49 | ['balance_pairs', r_balance_pairs], 50 | ['strikethrough', r_strikethrough.postProcess], 51 | ['emphasis', r_emphasis.postProcess], 52 | // rules for pairs separate '**' into its own text tokens, which may be left unused, 53 | // rule below merges unused segments back with the rest of the text 54 | ['fragments_join', r_fragments_join] 55 | ] 56 | 57 | /** 58 | * new ParserInline() 59 | **/ 60 | function ParserInline () { 61 | /** 62 | * ParserInline#ruler -> Ruler 63 | * 64 | * [[Ruler]] instance. Keep configuration of inline rules. 65 | **/ 66 | this.ruler = new Ruler() 67 | 68 | for (let i = 0; i < _rules.length; i++) { 69 | this.ruler.push(_rules[i][0], _rules[i][1]) 70 | } 71 | 72 | /** 73 | * ParserInline#ruler2 -> Ruler 74 | * 75 | * [[Ruler]] instance. Second ruler used for post-processing 76 | * (e.g. in emphasis-like rules). 77 | **/ 78 | this.ruler2 = new Ruler() 79 | 80 | for (let i = 0; i < _rules2.length; i++) { 81 | this.ruler2.push(_rules2[i][0], _rules2[i][1]) 82 | } 83 | } 84 | 85 | // Skip single token by running all rules in validation mode; 86 | // returns `true` if any rule reported success 87 | // 88 | ParserInline.prototype.skipToken = function (state) { 89 | const pos = state.pos 90 | const rules = this.ruler.getRules('') 91 | const len = rules.length 92 | const maxNesting = state.md.options.maxNesting 93 | const cache = state.cache 94 | 95 | if (typeof cache[pos] !== 'undefined') { 96 | state.pos = cache[pos] 97 | return 98 | } 99 | 100 | let ok = false 101 | 102 | if (state.level < maxNesting) { 103 | for (let i = 0; i < len; i++) { 104 | // Increment state.level and decrement it later to limit recursion. 105 | // It's harmless to do here, because no tokens are created. But ideally, 106 | // we'd need a separate private state variable for this purpose. 107 | // 108 | state.level++ 109 | ok = rules[i](state, true) 110 | state.level-- 111 | 112 | if (ok) { 113 | if (pos >= state.pos) { throw new Error("inline rule didn't increment state.pos") } 114 | break 115 | } 116 | } 117 | } else { 118 | // Too much nesting, just skip until the end of the paragraph. 119 | // 120 | // NOTE: this will cause links to behave incorrectly in the following case, 121 | // when an amount of `[` is exactly equal to `maxNesting + 1`: 122 | // 123 | // [[[[[[[[[[[[[[[[[[[[[foo]() 124 | // 125 | // TODO: remove this workaround when CM standard will allow nested links 126 | // (we can replace it by preventing links from being parsed in 127 | // validation mode) 128 | // 129 | state.pos = state.posMax 130 | } 131 | 132 | if (!ok) { state.pos++ } 133 | cache[pos] = state.pos 134 | } 135 | 136 | // Generate tokens for input range 137 | // 138 | ParserInline.prototype.tokenize = function (state) { 139 | const rules = this.ruler.getRules('') 140 | const len = rules.length 141 | const end = state.posMax 142 | const maxNesting = state.md.options.maxNesting 143 | 144 | while (state.pos < end) { 145 | // Try all possible rules. 146 | // On success, rule should: 147 | // 148 | // - update `state.pos` 149 | // - update `state.tokens` 150 | // - return true 151 | const prevPos = state.pos 152 | let ok = false 153 | 154 | if (state.level < maxNesting) { 155 | for (let i = 0; i < len; i++) { 156 | ok = rules[i](state, false) 157 | if (ok) { 158 | if (prevPos >= state.pos) { throw new Error("inline rule didn't increment state.pos") } 159 | break 160 | } 161 | } 162 | } 163 | 164 | if (ok) { 165 | if (state.pos >= end) { break } 166 | continue 167 | } 168 | 169 | state.pending += state.src[state.pos++] 170 | } 171 | 172 | if (state.pending) { 173 | state.pushPending() 174 | } 175 | } 176 | 177 | /** 178 | * ParserInline.parse(str, md, env, outTokens) 179 | * 180 | * Process input string and push inline tokens into `outTokens` 181 | **/ 182 | ParserInline.prototype.parse = function (str, md, env, outTokens) { 183 | const state = new this.State(str, md, env, outTokens) 184 | 185 | this.tokenize(state) 186 | 187 | const rules = this.ruler2.getRules('') 188 | const len = rules.length 189 | 190 | for (let i = 0; i < len; i++) { 191 | rules[i](state) 192 | } 193 | } 194 | 195 | ParserInline.prototype.State = StateInline 196 | 197 | export default ParserInline 198 | -------------------------------------------------------------------------------- /lib/presets/commonmark.mjs: -------------------------------------------------------------------------------- 1 | // Commonmark default options 2 | 3 | export default { 4 | options: { 5 | // Enable HTML tags in source 6 | html: true, 7 | 8 | // Use '/' to close single tags (<br />) 9 | xhtmlOut: true, 10 | 11 | // Convert '\n' in paragraphs into <br> 12 | breaks: false, 13 | 14 | // CSS language prefix for fenced blocks 15 | langPrefix: 'language-', 16 | 17 | // autoconvert URL-like texts to links 18 | linkify: false, 19 | 20 | // Enable some language-neutral replacements + quotes beautification 21 | typographer: false, 22 | 23 | // Double + single quotes replacement pairs, when typographer enabled, 24 | // and smartquotes on. Could be either a String or an Array. 25 | // 26 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 27 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 28 | quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */ 29 | 30 | // Highlighter function. Should return escaped HTML, 31 | // or '' if the source string is not changed and should be escaped externaly. 32 | // If result starts with <pre... internal wrapper is skipped. 33 | // 34 | // function (/*str, lang*/) { return ''; } 35 | // 36 | highlight: null, 37 | 38 | // Internal protection, recursion limit 39 | maxNesting: 20 40 | }, 41 | 42 | components: { 43 | 44 | core: { 45 | rules: [ 46 | 'normalize', 47 | 'block', 48 | 'inline', 49 | 'text_join' 50 | ] 51 | }, 52 | 53 | block: { 54 | rules: [ 55 | 'blockquote', 56 | 'code', 57 | 'fence', 58 | 'heading', 59 | 'hr', 60 | 'html_block', 61 | 'lheading', 62 | 'list', 63 | 'reference', 64 | 'paragraph' 65 | ] 66 | }, 67 | 68 | inline: { 69 | rules: [ 70 | 'autolink', 71 | 'backticks', 72 | 'emphasis', 73 | 'entity', 74 | 'escape', 75 | 'html_inline', 76 | 'image', 77 | 'link', 78 | 'newline', 79 | 'text' 80 | ], 81 | rules2: [ 82 | 'balance_pairs', 83 | 'emphasis', 84 | 'fragments_join' 85 | ] 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/presets/default.mjs: -------------------------------------------------------------------------------- 1 | // markdown-it default options 2 | 3 | export default { 4 | options: { 5 | // Enable HTML tags in source 6 | html: false, 7 | 8 | // Use '/' to close single tags (<br />) 9 | xhtmlOut: false, 10 | 11 | // Convert '\n' in paragraphs into <br> 12 | breaks: false, 13 | 14 | // CSS language prefix for fenced blocks 15 | langPrefix: 'language-', 16 | 17 | // autoconvert URL-like texts to links 18 | linkify: false, 19 | 20 | // Enable some language-neutral replacements + quotes beautification 21 | typographer: false, 22 | 23 | // Double + single quotes replacement pairs, when typographer enabled, 24 | // and smartquotes on. Could be either a String or an Array. 25 | // 26 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 27 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 28 | quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */ 29 | 30 | // Highlighter function. Should return escaped HTML, 31 | // or '' if the source string is not changed and should be escaped externaly. 32 | // If result starts with <pre... internal wrapper is skipped. 33 | // 34 | // function (/*str, lang*/) { return ''; } 35 | // 36 | highlight: null, 37 | 38 | // Internal protection, recursion limit 39 | maxNesting: 100 40 | }, 41 | 42 | components: { 43 | core: {}, 44 | block: {}, 45 | inline: {} 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/presets/zero.mjs: -------------------------------------------------------------------------------- 1 | // "Zero" preset, with nothing enabled. Useful for manual configuring of simple 2 | // modes. For example, to parse bold/italic only. 3 | 4 | export default { 5 | options: { 6 | // Enable HTML tags in source 7 | html: false, 8 | 9 | // Use '/' to close single tags (<br />) 10 | xhtmlOut: false, 11 | 12 | // Convert '\n' in paragraphs into <br> 13 | breaks: false, 14 | 15 | // CSS language prefix for fenced blocks 16 | langPrefix: 'language-', 17 | 18 | // autoconvert URL-like texts to links 19 | linkify: false, 20 | 21 | // Enable some language-neutral replacements + quotes beautification 22 | typographer: false, 23 | 24 | // Double + single quotes replacement pairs, when typographer enabled, 25 | // and smartquotes on. Could be either a String or an Array. 26 | // 27 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 28 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 29 | quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */ 30 | 31 | // Highlighter function. Should return escaped HTML, 32 | // or '' if the source string is not changed and should be escaped externaly. 33 | // If result starts with <pre... internal wrapper is skipped. 34 | // 35 | // function (/*str, lang*/) { return ''; } 36 | // 37 | highlight: null, 38 | 39 | // Internal protection, recursion limit 40 | maxNesting: 20 41 | }, 42 | 43 | components: { 44 | 45 | core: { 46 | rules: [ 47 | 'normalize', 48 | 'block', 49 | 'inline', 50 | 'text_join' 51 | ] 52 | }, 53 | 54 | block: { 55 | rules: [ 56 | 'paragraph' 57 | ] 58 | }, 59 | 60 | inline: { 61 | rules: [ 62 | 'text' 63 | ], 64 | rules2: [ 65 | 'balance_pairs', 66 | 'fragments_join' 67 | ] 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/rules_block/code.mjs: -------------------------------------------------------------------------------- 1 | // Code block (4 spaces padded) 2 | 3 | export default function code (state, startLine, endLine/*, silent */) { 4 | if (state.sCount[startLine] - state.blkIndent < 4) { return false } 5 | 6 | let nextLine = startLine + 1 7 | let last = nextLine 8 | 9 | while (nextLine < endLine) { 10 | if (state.isEmpty(nextLine)) { 11 | nextLine++ 12 | continue 13 | } 14 | 15 | if (state.sCount[nextLine] - state.blkIndent >= 4) { 16 | nextLine++ 17 | last = nextLine 18 | continue 19 | } 20 | break 21 | } 22 | 23 | state.line = last 24 | 25 | const token = state.push('code_block', 'code', 0) 26 | token.content = state.getLines(startLine, last, 4 + state.blkIndent, false) + '\n' 27 | token.map = [startLine, state.line] 28 | 29 | return true 30 | } 31 | -------------------------------------------------------------------------------- /lib/rules_block/fence.mjs: -------------------------------------------------------------------------------- 1 | // fences (``` lang, ~~~ lang) 2 | 3 | export default function fence (state, startLine, endLine, silent) { 4 | let pos = state.bMarks[startLine] + state.tShift[startLine] 5 | let max = state.eMarks[startLine] 6 | 7 | // if it's indented more than 3 spaces, it should be a code block 8 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false } 9 | 10 | if (pos + 3 > max) { return false } 11 | 12 | const marker = state.src.charCodeAt(pos) 13 | 14 | if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { 15 | return false 16 | } 17 | 18 | // scan marker length 19 | let mem = pos 20 | pos = state.skipChars(pos, marker) 21 | 22 | let len = pos - mem 23 | 24 | if (len < 3) { return false } 25 | 26 | const markup = state.src.slice(mem, pos) 27 | const params = state.src.slice(pos, max) 28 | 29 | if (marker === 0x60 /* ` */) { 30 | if (params.indexOf(String.fromCharCode(marker)) >= 0) { 31 | return false 32 | } 33 | } 34 | 35 | // Since start is found, we can report success here in validation mode 36 | if (silent) { return true } 37 | 38 | // search end of block 39 | let nextLine = startLine 40 | let haveEndMarker = false 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 | const 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 | -------------------------------------------------------------------------------- /lib/rules_block/heading.mjs: -------------------------------------------------------------------------------- 1 | // heading (#, ##, ...) 2 | 3 | import { isSpace } from '../common/utils.mjs' 4 | 5 | export default function heading (state, startLine, endLine, silent) { 6 | let pos = state.bMarks[startLine] + state.tShift[startLine] 7 | let max = state.eMarks[startLine] 8 | 9 | // if it's indented more than 3 spaces, it should be a code block 10 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false } 11 | 12 | let ch = state.src.charCodeAt(pos) 13 | 14 | if (ch !== 0x23/* # */ || pos >= max) { return false } 15 | 16 | // count heading level 17 | let level = 1 18 | ch = state.src.charCodeAt(++pos) 19 | while (ch === 0x23/* # */ && pos < max && level <= 6) { 20 | level++ 21 | ch = state.src.charCodeAt(++pos) 22 | } 23 | 24 | if (level > 6 || (pos < max && !isSpace(ch))) { return false } 25 | 26 | if (silent) { return true } 27 | 28 | // Let's cut tails like ' ### ' from the end of string 29 | 30 | max = state.skipSpacesBack(max, pos) 31 | const tmp = state.skipCharsBack(max, 0x23, pos) // # 32 | if (tmp > pos && isSpace(state.src.charCodeAt(tmp - 1))) { 33 | max = tmp 34 | } 35 | 36 | state.line = startLine + 1 37 | 38 | const token_o = state.push('heading_open', 'h' + String(level), 1) 39 | token_o.markup = '########'.slice(0, level) 40 | token_o.map = [startLine, state.line] 41 | 42 | const token_i = state.push('inline', '', 0) 43 | token_i.content = state.src.slice(pos, max).trim() 44 | token_i.map = [startLine, state.line] 45 | token_i.children = [] 46 | 47 | const token_c = state.push('heading_close', 'h' + String(level), -1) 48 | token_c.markup = '########'.slice(0, level) 49 | 50 | return true 51 | } 52 | -------------------------------------------------------------------------------- /lib/rules_block/hr.mjs: -------------------------------------------------------------------------------- 1 | // Horizontal rule 2 | 3 | import { isSpace } from '../common/utils.mjs' 4 | 5 | export default function hr (state, startLine, endLine, silent) { 6 | const max = state.eMarks[startLine] 7 | // if it's indented more than 3 spaces, it should be a code block 8 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false } 9 | 10 | let pos = state.bMarks[startLine] + state.tShift[startLine] 11 | const marker = state.src.charCodeAt(pos++) 12 | 13 | // Check hr marker 14 | if (marker !== 0x2A/* * */ && 15 | marker !== 0x2D/* - */ && 16 | marker !== 0x5F/* _ */) { 17 | return false 18 | } 19 | 20 | // markers can be mixed with spaces, but there should be at least 3 of them 21 | 22 | let cnt = 1 23 | while (pos < max) { 24 | const ch = state.src.charCodeAt(pos++) 25 | if (ch !== marker && !isSpace(ch)) { return false } 26 | if (ch === marker) { cnt++ } 27 | } 28 | 29 | if (cnt < 3) { return false } 30 | 31 | if (silent) { return true } 32 | 33 | state.line = startLine + 1 34 | 35 | const token = state.push('hr', 'hr', 0) 36 | token.map = [startLine, state.line] 37 | token.markup = Array(cnt + 1).join(String.fromCharCode(marker)) 38 | 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /lib/rules_block/html_block.mjs: -------------------------------------------------------------------------------- 1 | // HTML block 2 | 3 | import block_names from '../common/html_blocks.mjs' 4 | import { HTML_OPEN_CLOSE_TAG_RE } from '../common/html_re.mjs' 5 | 6 | // An array of opening and corresponding closing sequences for html tags, 7 | // last argument defines whether it can terminate a paragraph or not 8 | // 9 | const HTML_SEQUENCES = [ 10 | [/^<(script|pre|style|textarea)(?=(\s|>|$))/i, /<\/(script|pre|style|textarea)>/i, true], 11 | [/^<!--/, /-->/, true], 12 | [/^<\?/, /\?>/, true], 13 | [/^<![A-Z]/, />/, true], 14 | [/^<!\[CDATA\[/, /\]\]>/, true], 15 | [new RegExp('^</?(' + block_names.join('|') + ')(?=(\\s|/?>|$))', 'i'), /^$/, true], 16 | [new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*#39;), /^$/, false] 17 | ] 18 | 19 | export default function html_block (state, startLine, endLine, silent) { 20 | let pos = state.bMarks[startLine] + state.tShift[startLine] 21 | let max = state.eMarks[startLine] 22 | 23 | // if it's indented more than 3 spaces, it should be a code block 24 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false } 25 | 26 | if (!state.md.options.html) { return false } 27 | 28 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false } 29 | 30 | let lineText = state.src.slice(pos, max) 31 | 32 | let i = 0 33 | for (; i < HTML_SEQUENCES.length; i++) { 34 | if (HTML_SEQUENCES[i][0].test(lineText)) { break } 35 | } 36 | if (i === HTML_SEQUENCES.length) { return false } 37 | 38 | if (silent) { 39 | // true if this sequence can be a terminator, false otherwise 40 | return HTML_SEQUENCES[i][2] 41 | } 42 | 43 | let nextLine = startLine + 1 44 | 45 | // If we are here - we detected HTML block. 46 | // Let's roll down till block end. 47 | if (!HTML_SEQUENCES[i][1].test(lineText)) { 48 | for (; nextLine < endLine; nextLine++) { 49 | if (state.sCount[nextLine] < state.blkIndent) { break } 50 | 51 | pos = state.bMarks[nextLine] + state.tShift[nextLine] 52 | max = state.eMarks[nextLine] 53 | lineText = state.src.slice(pos, max) 54 | 55 | if (HTML_SEQUENCES[i][1].test(lineText)) { 56 | if (lineText.length !== 0) { nextLine++ } 57 | break 58 | } 59 | } 60 | } 61 | 62 | state.line = nextLine 63 | 64 | const token = state.push('html_block', '', 0) 65 | token.map = [startLine, nextLine] 66 | token.content = state.getLines(startLine, nextLine, state.blkIndent, true) 67 | 68 | return true 69 | } 70 | -------------------------------------------------------------------------------- /lib/rules_block/lheading.mjs: -------------------------------------------------------------------------------- 1 | // lheading (---, ===) 2 | 3 | export default function lheading (state, startLine, endLine/*, silent */) { 4 | const terminatorRules = state.md.block.ruler.getRules('paragraph') 5 | 6 | // if it's indented more than 3 spaces, it should be a code block 7 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false } 8 | 9 | const oldParentType = state.parentType 10 | state.parentType = 'paragraph' // use paragraph to match terminatorRules 11 | 12 | // jump line-by-line until empty one or EOF 13 | let level = 0 14 | let marker 15 | let nextLine = startLine + 1 16 | 17 | for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { 18 | // this would be a code block normally, but after paragraph 19 | // it's considered a lazy continuation regardless of what's there 20 | if (state.sCount[nextLine] - state.blkIndent > 3) { continue } 21 | 22 | // 23 | // Check for underline in setext header 24 | // 25 | if (state.sCount[nextLine] >= state.blkIndent) { 26 | let pos = state.bMarks[nextLine] + state.tShift[nextLine] 27 | const max = state.eMarks[nextLine] 28 | 29 | if (pos < max) { 30 | marker = state.src.charCodeAt(pos) 31 | 32 | if (marker === 0x2D/* - */ || marker === 0x3D/* = */) { 33 | pos = state.skipChars(pos, marker) 34 | pos = state.skipSpaces(pos) 35 | 36 | if (pos >= max) { 37 | level = (marker === 0x3D/* = */ ? 1 : 2) 38 | break 39 | } 40 | } 41 | } 42 | } 43 | 44 | // quirk for blockquotes, this line should already be checked by that rule 45 | if (state.sCount[nextLine] < 0) { continue } 46 | 47 | // Some tags can terminate paragraph without empty line. 48 | let terminate = false 49 | for (let i = 0, l = terminatorRules.length; i < l; i++) { 50 | if (terminatorRules[i](state, nextLine, endLine, true)) { 51 | terminate = true 52 | break 53 | } 54 | } 55 | if (terminate) { break } 56 | } 57 | 58 | if (!level) { 59 | // Didn't find valid underline 60 | return false 61 | } 62 | 63 | const content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() 64 | 65 | state.line = nextLine + 1 66 | 67 | const token_o = state.push('heading_open', 'h' + String(level), 1) 68 | token_o.markup = String.fromCharCode(marker) 69 | token_o.map = [startLine, state.line] 70 | 71 | const token_i = state.push('inline', '', 0) 72 | token_i.content = content 73 | token_i.map = [startLine, state.line - 1] 74 | token_i.children = [] 75 | 76 | const token_c = state.push('heading_close', 'h' + String(level), -1) 77 | token_c.markup = String.fromCharCode(marker) 78 | 79 | state.parentType = oldParentType 80 | 81 | return true 82 | } 83 | -------------------------------------------------------------------------------- /lib/rules_block/paragraph.mjs: -------------------------------------------------------------------------------- 1 | // Paragraph 2 | 3 | export default function paragraph (state, startLine, endLine) { 4 | const terminatorRules = state.md.block.ruler.getRules('paragraph') 5 | const oldParentType = state.parentType 6 | let nextLine = startLine + 1 7 | state.parentType = 'paragraph' 8 | 9 | // jump line-by-line until empty one or EOF 10 | for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { 11 | // this would be a code block normally, but after paragraph 12 | // it's considered a lazy continuation regardless of what's there 13 | if (state.sCount[nextLine] - state.blkIndent > 3) { continue } 14 | 15 | // quirk for blockquotes, this line should already be checked by that rule 16 | if (state.sCount[nextLine] < 0) { continue } 17 | 18 | // Some tags can terminate paragraph without empty line. 19 | let terminate = false 20 | for (let i = 0, l = terminatorRules.length; i < l; i++) { 21 | if (terminatorRules[i](state, nextLine, endLine, true)) { 22 | terminate = true 23 | break 24 | } 25 | } 26 | if (terminate) { break } 27 | } 28 | 29 | const content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() 30 | 31 | state.line = nextLine 32 | 33 | const token_o = state.push('paragraph_open', 'p', 1) 34 | token_o.map = [startLine, state.line] 35 | 36 | const token_i = state.push('inline', '', 0) 37 | token_i.content = content 38 | token_i.map = [startLine, state.line] 39 | token_i.children = [] 40 | 41 | state.push('paragraph_close', 'p', -1) 42 | 43 | state.parentType = oldParentType 44 | 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /lib/rules_block/reference.mjs: -------------------------------------------------------------------------------- 1 | import { isSpace, normalizeReference } from '../common/utils.mjs' 2 | 3 | export default function reference (state, startLine, _endLine, silent) { 4 | let pos = state.bMarks[startLine] + state.tShift[startLine] 5 | let max = state.eMarks[startLine] 6 | let nextLine = startLine + 1 7 | 8 | // if it's indented more than 3 spaces, it should be a code block 9 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false } 10 | 11 | if (state.src.charCodeAt(pos) !== 0x5B/* [ */) { return false } 12 | 13 | function getNextLine (nextLine) { 14 | const endLine = state.lineMax 15 | 16 | if (nextLine >= endLine || state.isEmpty(nextLine)) { 17 | // empty line or end of input 18 | return null 19 | } 20 | 21 | let isContinuation = false 22 | 23 | // this would be a code block normally, but after paragraph 24 | // it's considered a lazy continuation regardless of what's there 25 | if (state.sCount[nextLine] - state.blkIndent > 3) { isContinuation = true } 26 | 27 | // quirk for blockquotes, this line should already be checked by that rule 28 | if (state.sCount[nextLine] < 0) { isContinuation = true } 29 | 30 | if (!isContinuation) { 31 | const terminatorRules = state.md.block.ruler.getRules('reference') 32 | const oldParentType = state.parentType 33 | state.parentType = 'reference' 34 | 35 | // Some tags can terminate paragraph without empty line. 36 | let terminate = false 37 | for (let i = 0, l = terminatorRules.length; i < l; i++) { 38 | if (terminatorRules[i](state, nextLine, endLine, true)) { 39 | terminate = true 40 | break 41 | } 42 | } 43 | 44 | state.parentType = oldParentType 45 | if (terminate) { 46 | // terminated by another block 47 | return null 48 | } 49 | } 50 | 51 | const pos = state.bMarks[nextLine] + state.tShift[nextLine] 52 | const max = state.eMarks[nextLine] 53 | 54 | // max + 1 explicitly includes the newline 55 | return state.src.slice(pos, max + 1) 56 | } 57 | 58 | let str = state.src.slice(pos, max + 1) 59 | 60 | max = str.length 61 | let labelEnd = -1 62 | 63 | for (pos = 1; pos < max; pos++) { 64 | const ch = str.charCodeAt(pos) 65 | if (ch === 0x5B /* [ */) { 66 | return false 67 | } else if (ch === 0x5D /* ] */) { 68 | labelEnd = pos 69 | break 70 | } else if (ch === 0x0A /* \n */) { 71 | const lineContent = getNextLine(nextLine) 72 | if (lineContent !== null) { 73 | str += lineContent 74 | max = str.length 75 | nextLine++ 76 | } 77 | } else if (ch === 0x5C /* \ */) { 78 | pos++ 79 | if (pos < max && str.charCodeAt(pos) === 0x0A) { 80 | const lineContent = getNextLine(nextLine) 81 | if (lineContent !== null) { 82 | str += lineContent 83 | max = str.length 84 | nextLine++ 85 | } 86 | } 87 | } 88 | } 89 | 90 | if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return false } 91 | 92 | // [label]: destination 'title' 93 | // ^^^ skip optional whitespace here 94 | for (pos = labelEnd + 2; pos < max; pos++) { 95 | const ch = str.charCodeAt(pos) 96 | if (ch === 0x0A) { 97 | const lineContent = getNextLine(nextLine) 98 | if (lineContent !== null) { 99 | str += lineContent 100 | max = str.length 101 | nextLine++ 102 | } 103 | } else if (isSpace(ch)) { 104 | /* eslint no-empty:0 */ 105 | } else { 106 | break 107 | } 108 | } 109 | 110 | // [label]: destination 'title' 111 | // ^^^^^^^^^^^ parse this 112 | const destRes = state.md.helpers.parseLinkDestination(str, pos, max) 113 | if (!destRes.ok) { return false } 114 | 115 | const href = state.md.normalizeLink(destRes.str) 116 | if (!state.md.validateLink(href)) { return false } 117 | 118 | pos = destRes.pos 119 | 120 | // save cursor state, we could require to rollback later 121 | const destEndPos = pos 122 | const destEndLineNo = nextLine 123 | 124 | // [label]: destination 'title' 125 | // ^^^ skipping those spaces 126 | const start = pos 127 | for (; pos < max; pos++) { 128 | const ch = str.charCodeAt(pos) 129 | if (ch === 0x0A) { 130 | const lineContent = getNextLine(nextLine) 131 | if (lineContent !== null) { 132 | str += lineContent 133 | max = str.length 134 | nextLine++ 135 | } 136 | } else if (isSpace(ch)) { 137 | /* eslint no-empty:0 */ 138 | } else { 139 | break 140 | } 141 | } 142 | 143 | // [label]: destination 'title' 144 | // ^^^^^^^ parse this 145 | let titleRes = state.md.helpers.parseLinkTitle(str, pos, max) 146 | while (titleRes.can_continue) { 147 | const lineContent = getNextLine(nextLine) 148 | if (lineContent === null) break 149 | str += lineContent 150 | pos = max 151 | max = str.length 152 | nextLine++ 153 | titleRes = state.md.helpers.parseLinkTitle(str, pos, max, titleRes) 154 | } 155 | let title 156 | 157 | if (pos < max && start !== pos && titleRes.ok) { 158 | title = titleRes.str 159 | pos = titleRes.pos 160 | } else { 161 | title = '' 162 | pos = destEndPos 163 | nextLine = destEndLineNo 164 | } 165 | 166 | // skip trailing spaces until the rest of the line 167 | while (pos < max) { 168 | const ch = str.charCodeAt(pos) 169 | if (!isSpace(ch)) { break } 170 | pos++ 171 | } 172 | 173 | if (pos < max && str.charCodeAt(pos) !== 0x0A) { 174 | if (title) { 175 | // garbage at the end of the line after title, 176 | // but it could still be a valid reference if we roll back 177 | title = '' 178 | pos = destEndPos 179 | nextLine = destEndLineNo 180 | while (pos < max) { 181 | const ch = str.charCodeAt(pos) 182 | if (!isSpace(ch)) { break } 183 | pos++ 184 | } 185 | } 186 | } 187 | 188 | if (pos < max && str.charCodeAt(pos) !== 0x0A) { 189 | // garbage at the end of the line 190 | return false 191 | } 192 | 193 | const label = normalizeReference(str.slice(1, labelEnd)) 194 | if (!label) { 195 | // CommonMark 0.20 disallows empty labels 196 | return false 197 | } 198 | 199 | // Reference can not terminate anything. This check is for safety only. 200 | /* istanbul ignore if */ 201 | if (silent) { return true } 202 | 203 | if (typeof state.env.references === 'undefined') { 204 | state.env.references = {} 205 | } 206 | if (typeof state.env.references[label] === 'undefined') { 207 | state.env.references[label] = { title, href } 208 | } 209 | 210 | state.line = nextLine 211 | return true 212 | } 213 | -------------------------------------------------------------------------------- /lib/rules_core/block.mjs: -------------------------------------------------------------------------------- 1 | export default function block (state) { 2 | let token 3 | 4 | if (state.inlineMode) { 5 | token = new state.Token('inline', '', 0) 6 | token.content = state.src 7 | token.map = [0, 1] 8 | token.children = [] 9 | state.tokens.push(token) 10 | } else { 11 | state.md.block.parse(state.src, state.md, state.env, state.tokens) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/rules_core/inline.mjs: -------------------------------------------------------------------------------- 1 | export default function inline (state) { 2 | const tokens = state.tokens 3 | 4 | // Parse inlines 5 | for (let i = 0, l = tokens.length; i < l; i++) { 6 | const tok = tokens[i] 7 | if (tok.type === 'inline') { 8 | state.md.inline.parse(tok.content, state.md, state.env, tok.children) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/rules_core/linkify.mjs: -------------------------------------------------------------------------------- 1 | // Replace link-like texts with link nodes. 2 | // 3 | // Currently restricted by `md.validateLink()` to http/https/ftp 4 | // 5 | 6 | import { arrayReplaceAt } from '../common/utils.mjs' 7 | 8 | function isLinkOpen (str) { 9 | return /^<a[>\s]/i.test(str) 10 | } 11 | function isLinkClose (str) { 12 | return /^<\/a\s*>/i.test(str) 13 | } 14 | 15 | export default function linkify (state) { 16 | const blockTokens = state.tokens 17 | 18 | if (!state.md.options.linkify) { return } 19 | 20 | for (let j = 0, l = blockTokens.length; j < l; j++) { 21 | if (blockTokens[j].type !== 'inline' || 22 | !state.md.linkify.pretest(blockTokens[j].content)) { 23 | continue 24 | } 25 | 26 | let tokens = blockTokens[j].children 27 | 28 | let htmlLinkLevel = 0 29 | 30 | // We scan from the end, to keep position when new tags added. 31 | // Use reversed logic in links start/end match 32 | for (let i = tokens.length - 1; i >= 0; i--) { 33 | const currentToken = tokens[i] 34 | 35 | // Skip content of markdown links 36 | if (currentToken.type === 'link_close') { 37 | i-- 38 | while (tokens[i].level !== currentToken.level && tokens[i].type !== 'link_open') { 39 | i-- 40 | } 41 | continue 42 | } 43 | 44 | // Skip content of html tag links 45 | if (currentToken.type === 'html_inline') { 46 | if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) { 47 | htmlLinkLevel-- 48 | } 49 | if (isLinkClose(currentToken.content)) { 50 | htmlLinkLevel++ 51 | } 52 | } 53 | if (htmlLinkLevel > 0) { continue } 54 | 55 | if (currentToken.type === 'text' && state.md.linkify.test(currentToken.content)) { 56 | const text = currentToken.content 57 | let links = state.md.linkify.match(text) 58 | 59 | // Now split string to nodes 60 | const nodes = [] 61 | let level = currentToken.level 62 | let lastPos = 0 63 | 64 | // forbid escape sequence at the start of the string, 65 | // this avoids http\://example.com/ from being linkified as 66 | // http:<a href="//example.com/">//example.com/</a> 67 | if (links.length > 0 && 68 | links[0].index === 0 && 69 | i > 0 && 70 | tokens[i - 1].type === 'text_special') { 71 | links = links.slice(1) 72 | } 73 | 74 | for (let ln = 0; ln < links.length; ln++) { 75 | const url = links[ln].url 76 | const fullUrl = state.md.normalizeLink(url) 77 | if (!state.md.validateLink(fullUrl)) { continue } 78 | 79 | let urlText = links[ln].text 80 | 81 | // Linkifier might send raw hostnames like "example.com", where url 82 | // starts with domain name. So we prepend http:// in those cases, 83 | // and remove it afterwards. 84 | // 85 | if (!links[ln].schema) { 86 | urlText = state.md.normalizeLinkText('http://' + urlText).replace(/^http:\/\//, '') 87 | } else if (links[ln].schema === 'mailto:' && !/^mailto:/i.test(urlText)) { 88 | urlText = state.md.normalizeLinkText('mailto:' + urlText).replace(/^mailto:/, '') 89 | } else { 90 | urlText = state.md.normalizeLinkText(urlText) 91 | } 92 | 93 | const pos = links[ln].index 94 | 95 | if (pos > lastPos) { 96 | const token = new state.Token('text', '', 0) 97 | token.content = text.slice(lastPos, pos) 98 | token.level = level 99 | nodes.push(token) 100 | } 101 | 102 | const token_o = new state.Token('link_open', 'a', 1) 103 | token_o.attrs = [['href', fullUrl]] 104 | token_o.level = level++ 105 | token_o.markup = 'linkify' 106 | token_o.info = 'auto' 107 | nodes.push(token_o) 108 | 109 | const token_t = new state.Token('text', '', 0) 110 | token_t.content = urlText 111 | token_t.level = level 112 | nodes.push(token_t) 113 | 114 | const token_c = new state.Token('link_close', 'a', -1) 115 | token_c.level = --level 116 | token_c.markup = 'linkify' 117 | token_c.info = 'auto' 118 | nodes.push(token_c) 119 | 120 | lastPos = links[ln].lastIndex 121 | } 122 | if (lastPos < text.length) { 123 | const token = new state.Token('text', '', 0) 124 | token.content = text.slice(lastPos) 125 | token.level = level 126 | nodes.push(token) 127 | } 128 | 129 | // replace current node 130 | blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes) 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/rules_core/normalize.mjs: -------------------------------------------------------------------------------- 1 | // Normalize input string 2 | 3 | // https://spec.commonmark.org/0.29/#line-ending 4 | const NEWLINES_RE = /\r\n?|\n/g 5 | const NULL_RE = /\0/g 6 | 7 | export default function normalize (state) { 8 | let str 9 | 10 | // Normalize newlines 11 | str = state.src.replace(NEWLINES_RE, '\n') 12 | 13 | // Replace NULL characters 14 | str = str.replace(NULL_RE, '\uFFFD') 15 | 16 | state.src = str 17 | } 18 | -------------------------------------------------------------------------------- /lib/rules_core/replacements.mjs: -------------------------------------------------------------------------------- 1 | // Simple typographic replacements 2 | // 3 | // (c) (C) → © 4 | // (tm) (TM) → ™ 5 | // (r) (R) → ® 6 | // +- → ± 7 | // ... → … (also ?.... → ?.., !.... → !..) 8 | // ???????? → ???, !!!!! → !!!, `,,` → `,` 9 | // -- → –, --- → — 10 | // 11 | 12 | // TODO: 13 | // - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾ 14 | // - multiplications 2 x 4 -> 2 × 4 15 | 16 | const RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/ 17 | 18 | // Workaround for phantomjs - need regex without /g flag, 19 | // or root check will fail every second time 20 | const SCOPED_ABBR_TEST_RE = /\((c|tm|r)\)/i 21 | 22 | const SCOPED_ABBR_RE = /\((c|tm|r)\)/ig 23 | const SCOPED_ABBR = { 24 | c: '©', 25 | r: '®', 26 | tm: '™' 27 | } 28 | 29 | function replaceFn (match, name) { 30 | return SCOPED_ABBR[name.toLowerCase()] 31 | } 32 | 33 | function replace_scoped (inlineTokens) { 34 | let inside_autolink = 0 35 | 36 | for (let i = inlineTokens.length - 1; i >= 0; i--) { 37 | const token = inlineTokens[i] 38 | 39 | if (token.type === 'text' && !inside_autolink) { 40 | token.content = token.content.replace(SCOPED_ABBR_RE, replaceFn) 41 | } 42 | 43 | if (token.type === 'link_open' && token.info === 'auto') { 44 | inside_autolink-- 45 | } 46 | 47 | if (token.type === 'link_close' && token.info === 'auto') { 48 | inside_autolink++ 49 | } 50 | } 51 | } 52 | 53 | function replace_rare (inlineTokens) { 54 | let inside_autolink = 0 55 | 56 | for (let i = inlineTokens.length - 1; i >= 0; i--) { 57 | const token = inlineTokens[i] 58 | 59 | if (token.type === 'text' && !inside_autolink) { 60 | if (RARE_RE.test(token.content)) { 61 | token.content = token.content 62 | .replace(/\+-/g, '±') 63 | // .., ..., ....... -> … 64 | // but ?..... & !..... -> ?.. & !.. 65 | .replace(/\.{2,}/g, '…').replace(/([?!])…/g, '$1..') 66 | .replace(/([?!]){4,}/g, '$1$1$1').replace(/,{2,}/g, ',') 67 | // em-dash 68 | .replace(/(^|[^-])---(?=[^-]|$)/mg, '$1\u2014') 69 | // en-dash 70 | .replace(/(^|\s)--(?=\s|$)/mg, '$1\u2013') 71 | .replace(/(^|[^-\s])--(?=[^-\s]|$)/mg, '$1\u2013') 72 | } 73 | } 74 | 75 | if (token.type === 'link_open' && token.info === 'auto') { 76 | inside_autolink-- 77 | } 78 | 79 | if (token.type === 'link_close' && token.info === 'auto') { 80 | inside_autolink++ 81 | } 82 | } 83 | } 84 | 85 | export default function replace (state) { 86 | let blkIdx 87 | 88 | if (!state.md.options.typographer) { return } 89 | 90 | for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { 91 | if (state.tokens[blkIdx].type !== 'inline') { continue } 92 | 93 | if (SCOPED_ABBR_TEST_RE.test(state.tokens[blkIdx].content)) { 94 | replace_scoped(state.tokens[blkIdx].children) 95 | } 96 | 97 | if (RARE_RE.test(state.tokens[blkIdx].content)) { 98 | replace_rare(state.tokens[blkIdx].children) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/rules_core/smartquotes.mjs: -------------------------------------------------------------------------------- 1 | // Convert straight quotation marks to typographic ones 2 | // 3 | 4 | import { isWhiteSpace, isPunctChar, isMdAsciiPunct } from '../common/utils.mjs' 5 | 6 | const QUOTE_TEST_RE = /['"]/ 7 | const QUOTE_RE = /['"]/g 8 | const APOSTROPHE = '\u2019' /* ’ */ 9 | 10 | function replaceAt (str, index, ch) { 11 | return str.slice(0, index) + ch + str.slice(index + 1) 12 | } 13 | 14 | function process_inlines (tokens, state) { 15 | let j 16 | 17 | const stack = [] 18 | 19 | for (let i = 0; i < tokens.length; i++) { 20 | const token = tokens[i] 21 | 22 | const thisLevel = tokens[i].level 23 | 24 | for (j = stack.length - 1; j >= 0; j--) { 25 | if (stack[j].level <= thisLevel) { break } 26 | } 27 | stack.length = j + 1 28 | 29 | if (token.type !== 'text') { continue } 30 | 31 | let text = token.content 32 | let pos = 0 33 | let max = text.length 34 | 35 | /* eslint no-labels:0,block-scoped-var:0 */ 36 | OUTER: 37 | while (pos < max) { 38 | QUOTE_RE.lastIndex = pos 39 | const t = QUOTE_RE.exec(text) 40 | if (!t) { break } 41 | 42 | let canOpen = true 43 | let canClose = true 44 | pos = t.index + 1 45 | const isSingle = (t[0] === "'") 46 | 47 | // Find previous character, 48 | // default to space if it's the beginning of the line 49 | // 50 | let lastChar = 0x20 51 | 52 | if (t.index - 1 >= 0) { 53 | lastChar = text.charCodeAt(t.index - 1) 54 | } else { 55 | for (j = i - 1; j >= 0; j--) { 56 | if (tokens[j].type === 'softbreak' || tokens[j].type === 'hardbreak') break // lastChar defaults to 0x20 57 | if (!tokens[j].content) continue // should skip all tokens except 'text', 'html_inline' or 'code_inline' 58 | 59 | lastChar = tokens[j].content.charCodeAt(tokens[j].content.length - 1) 60 | break 61 | } 62 | } 63 | 64 | // Find next character, 65 | // default to space if it's the end of the line 66 | // 67 | let nextChar = 0x20 68 | 69 | if (pos < max) { 70 | nextChar = text.charCodeAt(pos) 71 | } else { 72 | for (j = i + 1; j < tokens.length; j++) { 73 | if (tokens[j].type === 'softbreak' || tokens[j].type === 'hardbreak') break // nextChar defaults to 0x20 74 | if (!tokens[j].content) continue // should skip all tokens except 'text', 'html_inline' or 'code_inline' 75 | 76 | nextChar = tokens[j].content.charCodeAt(0) 77 | break 78 | } 79 | } 80 | 81 | const isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)) 82 | const isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)) 83 | 84 | const isLastWhiteSpace = isWhiteSpace(lastChar) 85 | const isNextWhiteSpace = isWhiteSpace(nextChar) 86 | 87 | if (isNextWhiteSpace) { 88 | canOpen = false 89 | } else if (isNextPunctChar) { 90 | if (!(isLastWhiteSpace || isLastPunctChar)) { 91 | canOpen = false 92 | } 93 | } 94 | 95 | if (isLastWhiteSpace) { 96 | canClose = false 97 | } else if (isLastPunctChar) { 98 | if (!(isNextWhiteSpace || isNextPunctChar)) { 99 | canClose = false 100 | } 101 | } 102 | 103 | if (nextChar === 0x22 /* " */ && t[0] === '"') { 104 | if (lastChar >= 0x30 /* 0 */ && lastChar <= 0x39 /* 9 */) { 105 | // special case: 1"" - count first quote as an inch 106 | canClose = canOpen = false 107 | } 108 | } 109 | 110 | if (canOpen && canClose) { 111 | // Replace quotes in the middle of punctuation sequence, but not 112 | // in the middle of the words, i.e.: 113 | // 114 | // 1. foo " bar " baz - not replaced 115 | // 2. foo-"-bar-"-baz - replaced 116 | // 3. foo"bar"baz - not replaced 117 | // 118 | canOpen = isLastPunctChar 119 | canClose = isNextPunctChar 120 | } 121 | 122 | if (!canOpen && !canClose) { 123 | // middle of word 124 | if (isSingle) { 125 | token.content = replaceAt(token.content, t.index, APOSTROPHE) 126 | } 127 | continue 128 | } 129 | 130 | if (canClose) { 131 | // this could be a closing quote, rewind the stack to get a match 132 | for (j = stack.length - 1; j >= 0; j--) { 133 | let item = stack[j] 134 | if (stack[j].level < thisLevel) { break } 135 | if (item.single === isSingle && stack[j].level === thisLevel) { 136 | item = stack[j] 137 | 138 | let openQuote 139 | let closeQuote 140 | if (isSingle) { 141 | openQuote = state.md.options.quotes[2] 142 | closeQuote = state.md.options.quotes[3] 143 | } else { 144 | openQuote = state.md.options.quotes[0] 145 | closeQuote = state.md.options.quotes[1] 146 | } 147 | 148 | // replace token.content *before* tokens[item.token].content, 149 | // because, if they are pointing at the same token, replaceAt 150 | // could mess up indices when quote length != 1 151 | token.content = replaceAt(token.content, t.index, closeQuote) 152 | tokens[item.token].content = replaceAt( 153 | tokens[item.token].content, item.pos, openQuote) 154 | 155 | pos += closeQuote.length - 1 156 | if (item.token === i) { pos += openQuote.length - 1 } 157 | 158 | text = token.content 159 | max = text.length 160 | 161 | stack.length = j 162 | continue OUTER 163 | } 164 | } 165 | } 166 | 167 | if (canOpen) { 168 | stack.push({ 169 | token: i, 170 | pos: t.index, 171 | single: isSingle, 172 | level: thisLevel 173 | }) 174 | } else if (canClose && isSingle) { 175 | token.content = replaceAt(token.content, t.index, APOSTROPHE) 176 | } 177 | } 178 | } 179 | } 180 | 181 | export default function smartquotes (state) { 182 | /* eslint max-depth:0 */ 183 | if (!state.md.options.typographer) { return } 184 | 185 | for (let blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { 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 | -------------------------------------------------------------------------------- /lib/rules_core/state_core.mjs: -------------------------------------------------------------------------------- 1 | // Core state object 2 | // 3 | 4 | import Token from '../token.mjs' 5 | 6 | function StateCore (src, md, env) { 7 | this.src = src 8 | this.env = env 9 | this.tokens = [] 10 | this.inlineMode = false 11 | this.md = md // link to parser instance 12 | } 13 | 14 | // re-export Token class to use in core rules 15 | StateCore.prototype.Token = Token 16 | 17 | export default StateCore 18 | -------------------------------------------------------------------------------- /lib/rules_core/text_join.mjs: -------------------------------------------------------------------------------- 1 | // Join raw text tokens with the rest of the text 2 | // 3 | // This is set as a separate rule to provide an opportunity for plugins 4 | // to run text replacements after text join, but before escape join. 5 | // 6 | // For example, `\:)` shouldn't be replaced with an emoji. 7 | // 8 | 9 | export default function text_join (state) { 10 | let curr, last 11 | const blockTokens = state.tokens 12 | const l = blockTokens.length 13 | 14 | for (let j = 0; j < l; j++) { 15 | if (blockTokens[j].type !== 'inline') continue 16 | 17 | const tokens = blockTokens[j].children 18 | const max = tokens.length 19 | 20 | for (curr = 0; curr < max; curr++) { 21 | if (tokens[curr].type === 'text_special') { 22 | tokens[curr].type = 'text' 23 | } 24 | } 25 | 26 | for (curr = last = 0; curr < max; curr++) { 27 | if (tokens[curr].type === 'text' && 28 | curr + 1 < max && 29 | tokens[curr + 1].type === 'text') { 30 | // collapse two adjacent text nodes 31 | tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content 32 | } else { 33 | if (curr !== last) { tokens[last] = tokens[curr] } 34 | 35 | last++ 36 | } 37 | } 38 | 39 | if (curr !== last) { 40 | tokens.length = last 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/rules_inline/autolink.mjs: -------------------------------------------------------------------------------- 1 | // Process autolinks '<protocol:...>' 2 | 3 | /* eslint max-len:0 */ 4 | const 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])?)*)$/ 5 | /* eslint-disable-next-line no-control-regex */ 6 | const AUTOLINK_RE = /^([a-zA-Z][a-zA-Z0-9+.-]{1,31}):([^<>\x00-\x20]*)$/ 7 | 8 | export default function autolink (state, silent) { 9 | let pos = state.pos 10 | 11 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false } 12 | 13 | const start = state.pos 14 | const max = state.posMax 15 | 16 | for (;;) { 17 | if (++pos >= max) return false 18 | 19 | const ch = state.src.charCodeAt(pos) 20 | 21 | if (ch === 0x3C /* < */) return false 22 | if (ch === 0x3E /* > */) break 23 | } 24 | 25 | const url = state.src.slice(start + 1, pos) 26 | 27 | if (AUTOLINK_RE.test(url)) { 28 | const fullUrl = state.md.normalizeLink(url) 29 | if (!state.md.validateLink(fullUrl)) { return false } 30 | 31 | if (!silent) { 32 | const token_o = state.push('link_open', 'a', 1) 33 | token_o.attrs = [['href', fullUrl]] 34 | token_o.markup = 'autolink' 35 | token_o.info = 'auto' 36 | 37 | const token_t = state.push('text', '', 0) 38 | token_t.content = state.md.normalizeLinkText(url) 39 | 40 | const token_c = state.push('link_close', 'a', -1) 41 | token_c.markup = 'autolink' 42 | token_c.info = 'auto' 43 | } 44 | 45 | state.pos += url.length + 2 46 | return true 47 | } 48 | 49 | if (EMAIL_RE.test(url)) { 50 | const fullUrl = state.md.normalizeLink('mailto:' + url) 51 | if (!state.md.validateLink(fullUrl)) { return false } 52 | 53 | if (!silent) { 54 | const token_o = state.push('link_open', 'a', 1) 55 | token_o.attrs = [['href', fullUrl]] 56 | token_o.markup = 'autolink' 57 | token_o.info = 'auto' 58 | 59 | const token_t = state.push('text', '', 0) 60 | token_t.content = state.md.normalizeLinkText(url) 61 | 62 | const token_c = state.push('link_close', 'a', -1) 63 | token_c.markup = 'autolink' 64 | token_c.info = 'auto' 65 | } 66 | 67 | state.pos += url.length + 2 68 | return true 69 | } 70 | 71 | return false 72 | } 73 | -------------------------------------------------------------------------------- /lib/rules_inline/backticks.mjs: -------------------------------------------------------------------------------- 1 | // Parse backticks 2 | 3 | export default function backtick (state, silent) { 4 | let pos = state.pos 5 | const ch = state.src.charCodeAt(pos) 6 | 7 | if (ch !== 0x60/* ` */) { return false } 8 | 9 | const start = pos 10 | pos++ 11 | const max = state.posMax 12 | 13 | // scan marker length 14 | while (pos < max && state.src.charCodeAt(pos) === 0x60/* ` */) { pos++ } 15 | 16 | const marker = state.src.slice(start, pos) 17 | const openerLength = marker.length 18 | 19 | if (state.backticksScanned && (state.backticks[openerLength] || 0) <= start) { 20 | if (!silent) state.pending += marker 21 | state.pos += openerLength 22 | return true 23 | } 24 | 25 | let matchEnd = pos 26 | let matchStart 27 | 28 | // Nothing found in the cache, scan until the end of the line (or until marker is found) 29 | while ((matchStart = state.src.indexOf('`', matchEnd)) !== -1) { 30 | matchEnd = matchStart + 1 31 | 32 | // scan marker length 33 | while (matchEnd < max && state.src.charCodeAt(matchEnd) === 0x60/* ` */) { matchEnd++ } 34 | 35 | const closerLength = matchEnd - matchStart 36 | 37 | if (closerLength === openerLength) { 38 | // Found matching closer length. 39 | if (!silent) { 40 | const token = state.push('code_inline', 'code', 0) 41 | token.markup = marker 42 | token.content = state.src.slice(pos, matchStart) 43 | .replace(/\n/g, ' ') 44 | .replace(/^ (.+) $/, '$1') 45 | } 46 | state.pos = matchEnd 47 | return true 48 | } 49 | 50 | // Some different length found, put it in cache as upper limit of where closer can be found 51 | state.backticks[closerLength] = matchStart 52 | } 53 | 54 | // Scanned through the end, didn't find anything 55 | state.backticksScanned = true 56 | 57 | if (!silent) state.pending += marker 58 | state.pos += openerLength 59 | return true 60 | } 61 | -------------------------------------------------------------------------------- /lib/rules_inline/balance_pairs.mjs: -------------------------------------------------------------------------------- 1 | // For each opening emphasis-like marker find a matching closing one 2 | // 3 | 4 | function processDelimiters (delimiters) { 5 | const openersBottom = {} 6 | const max = delimiters.length 7 | 8 | if (!max) return 9 | 10 | // headerIdx is the first delimiter of the current (where closer is) delimiter run 11 | let headerIdx = 0 12 | let lastTokenIdx = -2 // needs any value lower than -1 13 | const jumps = [] 14 | 15 | for (let closerIdx = 0; closerIdx < max; closerIdx++) { 16 | const closer = delimiters[closerIdx] 17 | 18 | jumps.push(0) 19 | 20 | // markers belong to same delimiter run if: 21 | // - they have adjacent tokens 22 | // - AND markers are the same 23 | // 24 | if (delimiters[headerIdx].marker !== closer.marker || lastTokenIdx !== closer.token - 1) { 25 | headerIdx = closerIdx 26 | } 27 | 28 | lastTokenIdx = closer.token 29 | 30 | // Length is only used for emphasis-specific "rule of 3", 31 | // if it's not defined (in strikethrough or 3rd party plugins), 32 | // we can default it to 0 to disable those checks. 33 | // 34 | closer.length = closer.length || 0 35 | 36 | if (!closer.close) continue 37 | 38 | // Previously calculated lower bounds (previous fails) 39 | // for each marker, each delimiter length modulo 3, 40 | // and for whether this closer can be an opener; 41 | // https://github.com/commonmark/cmark/commit/34250e12ccebdc6372b8b49c44fab57c72443460 42 | /* eslint-disable-next-line no-prototype-builtins */ 43 | if (!openersBottom.hasOwnProperty(closer.marker)) { 44 | openersBottom[closer.marker] = [-1, -1, -1, -1, -1, -1] 45 | } 46 | 47 | const minOpenerIdx = openersBottom[closer.marker][(closer.open ? 3 : 0) + (closer.length % 3)] 48 | 49 | let openerIdx = headerIdx - jumps[headerIdx] - 1 50 | 51 | let newMinOpenerIdx = openerIdx 52 | 53 | for (; openerIdx > minOpenerIdx; openerIdx -= jumps[openerIdx] + 1) { 54 | const opener = delimiters[openerIdx] 55 | 56 | if (opener.marker !== closer.marker) continue 57 | 58 | if (opener.open && opener.end < 0) { 59 | let isOddMatch = false 60 | 61 | // from spec: 62 | // 63 | // If one of the delimiters can both open and close emphasis, then the 64 | // sum of the lengths of the delimiter runs containing the opening and 65 | // closing delimiters must not be a multiple of 3 unless both lengths 66 | // are multiples of 3. 67 | // 68 | if (opener.close || closer.open) { 69 | if ((opener.length + closer.length) % 3 === 0) { 70 | if (opener.length % 3 !== 0 || closer.length % 3 !== 0) { 71 | isOddMatch = true 72 | } 73 | } 74 | } 75 | 76 | if (!isOddMatch) { 77 | // If previous delimiter cannot be an opener, we can safely skip 78 | // the entire sequence in future checks. This is required to make 79 | // sure algorithm has linear complexity (see *_*_*_*_*_... case). 80 | // 81 | const lastJump = openerIdx > 0 && !delimiters[openerIdx - 1].open 82 | ? jumps[openerIdx - 1] + 1 83 | : 0 84 | 85 | jumps[closerIdx] = closerIdx - openerIdx + lastJump 86 | jumps[openerIdx] = lastJump 87 | 88 | closer.open = false 89 | opener.end = closerIdx 90 | opener.close = false 91 | newMinOpenerIdx = -1 92 | // treat next token as start of run, 93 | // it optimizes skips in **<...>**a**<...>** pathological case 94 | lastTokenIdx = -2 95 | break 96 | } 97 | } 98 | } 99 | 100 | if (newMinOpenerIdx !== -1) { 101 | // If match for this delimiter run failed, we want to set lower bound for 102 | // future lookups. This is required to make sure algorithm has linear 103 | // complexity. 104 | // 105 | // See details here: 106 | // https://github.com/commonmark/cmark/issues/178#issuecomment-270417442 107 | // 108 | openersBottom[closer.marker][(closer.open ? 3 : 0) + ((closer.length || 0) % 3)] = newMinOpenerIdx 109 | } 110 | } 111 | } 112 | 113 | export default function link_pairs (state) { 114 | const tokens_meta = state.tokens_meta 115 | const max = state.tokens_meta.length 116 | 117 | processDelimiters(state.delimiters) 118 | 119 | for (let curr = 0; curr < max; curr++) { 120 | if (tokens_meta[curr] && tokens_meta[curr].delimiters) { 121 | processDelimiters(tokens_meta[curr].delimiters) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/rules_inline/emphasis.mjs: -------------------------------------------------------------------------------- 1 | // Process *this* and _that_ 2 | // 3 | 4 | // Insert each marker as a separate text token, and add it to delimiter list 5 | // 6 | function emphasis_tokenize (state, silent) { 7 | const start = state.pos 8 | const marker = state.src.charCodeAt(start) 9 | 10 | if (silent) { return false } 11 | 12 | if (marker !== 0x5F /* _ */ && marker !== 0x2A /* * */) { return false } 13 | 14 | const scanned = state.scanDelims(state.pos, marker === 0x2A) 15 | 16 | for (let i = 0; i < scanned.length; i++) { 17 | const token = state.push('text', '', 0) 18 | token.content = String.fromCharCode(marker) 19 | 20 | state.delimiters.push({ 21 | // Char code of the starting marker (number). 22 | // 23 | marker, 24 | 25 | // Total length of these series of delimiters. 26 | // 27 | length: scanned.length, 28 | 29 | // A position of the token this delimiter corresponds to. 30 | // 31 | token: state.tokens.length - 1, 32 | 33 | // If this delimiter is matched as a valid opener, `end` will be 34 | // equal to its position, otherwise it's `-1`. 35 | // 36 | end: -1, 37 | 38 | // Boolean flags that determine if this delimiter could open or close 39 | // an emphasis. 40 | // 41 | open: scanned.can_open, 42 | close: scanned.can_close 43 | }) 44 | } 45 | 46 | state.pos += scanned.length 47 | 48 | return true 49 | } 50 | 51 | function postProcess (state, delimiters) { 52 | const max = delimiters.length 53 | 54 | for (let i = max - 1; i >= 0; i--) { 55 | const startDelim = delimiters[i] 56 | 57 | if (startDelim.marker !== 0x5F/* _ */ && startDelim.marker !== 0x2A/* * */) { 58 | continue 59 | } 60 | 61 | // Process only opening markers 62 | if (startDelim.end === -1) { 63 | continue 64 | } 65 | 66 | const endDelim = delimiters[startDelim.end] 67 | 68 | // If the previous delimiter has the same marker and is adjacent to this one, 69 | // merge those into one strong delimiter. 70 | // 71 | // `<em><em>whatever</em></em>` -> `<strong>whatever</strong>` 72 | // 73 | const isStrong = i > 0 && 74 | delimiters[i - 1].end === startDelim.end + 1 && 75 | // check that first two markers match and adjacent 76 | delimiters[i - 1].marker === startDelim.marker && 77 | delimiters[i - 1].token === startDelim.token - 1 && 78 | // check that last two markers are adjacent (we can safely assume they match) 79 | delimiters[startDelim.end + 1].token === endDelim.token + 1 80 | 81 | const ch = String.fromCharCode(startDelim.marker) 82 | 83 | const token_o = state.tokens[startDelim.token] 84 | token_o.type = isStrong ? 'strong_open' : 'em_open' 85 | token_o.tag = isStrong ? 'strong' : 'em' 86 | token_o.nesting = 1 87 | token_o.markup = isStrong ? ch + ch : ch 88 | token_o.content = '' 89 | 90 | const token_c = state.tokens[endDelim.token] 91 | token_c.type = isStrong ? 'strong_close' : 'em_close' 92 | token_c.tag = isStrong ? 'strong' : 'em' 93 | token_c.nesting = -1 94 | token_c.markup = isStrong ? ch + ch : ch 95 | token_c.content = '' 96 | 97 | if (isStrong) { 98 | state.tokens[delimiters[i - 1].token].content = '' 99 | state.tokens[delimiters[startDelim.end + 1].token].content = '' 100 | i-- 101 | } 102 | } 103 | } 104 | 105 | // Walk through delimiter list and replace text tokens with tags 106 | // 107 | function emphasis_post_process (state) { 108 | const tokens_meta = state.tokens_meta 109 | const max = state.tokens_meta.length 110 | 111 | postProcess(state, state.delimiters) 112 | 113 | for (let curr = 0; curr < max; curr++) { 114 | if (tokens_meta[curr] && tokens_meta[curr].delimiters) { 115 | postProcess(state, tokens_meta[curr].delimiters) 116 | } 117 | } 118 | } 119 | 120 | export default { 121 | tokenize: emphasis_tokenize, 122 | postProcess: emphasis_post_process 123 | } 124 | -------------------------------------------------------------------------------- /lib/rules_inline/entity.mjs: -------------------------------------------------------------------------------- 1 | // Process html entity - {, ¯, ", ... 2 | 3 | import { decodeHTML } from 'entities' 4 | import { isValidEntityCode, fromCodePoint } from '../common/utils.mjs' 5 | 6 | const DIGITAL_RE = /^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i 7 | const NAMED_RE = /^&([a-z][a-z0-9]{1,31});/i 8 | 9 | export default function entity (state, silent) { 10 | const pos = state.pos 11 | const max = state.posMax 12 | 13 | if (state.src.charCodeAt(pos) !== 0x26/* & */) return false 14 | 15 | if (pos + 1 >= max) return false 16 | 17 | const ch = state.src.charCodeAt(pos + 1) 18 | 19 | if (ch === 0x23 /* # */) { 20 | const match = state.src.slice(pos).match(DIGITAL_RE) 21 | if (match) { 22 | if (!silent) { 23 | const code = match[1][0].toLowerCase() === 'x' ? parseInt(match[1].slice(1), 16) : parseInt(match[1], 10) 24 | 25 | const token = state.push('text_special', '', 0) 26 | token.content = isValidEntityCode(code) ? fromCodePoint(code) : fromCodePoint(0xFFFD) 27 | token.markup = match[0] 28 | token.info = 'entity' 29 | } 30 | state.pos += match[0].length 31 | return true 32 | } 33 | } else { 34 | const match = state.src.slice(pos).match(NAMED_RE) 35 | if (match) { 36 | const decoded = decodeHTML(match[0]) 37 | if (decoded !== match[0]) { 38 | if (!silent) { 39 | const token = state.push('text_special', '', 0) 40 | token.content = decoded 41 | token.markup = match[0] 42 | token.info = 'entity' 43 | } 44 | state.pos += match[0].length 45 | return true 46 | } 47 | } 48 | } 49 | 50 | return false 51 | } 52 | -------------------------------------------------------------------------------- /lib/rules_inline/escape.mjs: -------------------------------------------------------------------------------- 1 | // Process escaped chars and hardbreaks 2 | 3 | import { isSpace } from '../common/utils.mjs' 4 | 5 | const ESCAPED = [] 6 | 7 | for (let i = 0; i < 256; i++) { ESCAPED.push(0) } 8 | 9 | '\\!"#$%&\'()*+,./:;<=>?@[]^_`{|}~-' 10 | .split('').forEach(function (ch) { ESCAPED[ch.charCodeAt(0)] = 1 }) 11 | 12 | export default function escape (state, silent) { 13 | let pos = state.pos 14 | const max = state.posMax 15 | 16 | if (state.src.charCodeAt(pos) !== 0x5C/* \ */) return false 17 | pos++ 18 | 19 | // '\' at the end of the inline block 20 | if (pos >= max) return false 21 | 22 | let ch1 = state.src.charCodeAt(pos) 23 | 24 | if (ch1 === 0x0A) { 25 | if (!silent) { 26 | state.push('hardbreak', 'br', 0) 27 | } 28 | 29 | pos++ 30 | // skip leading whitespaces from next line 31 | while (pos < max) { 32 | ch1 = state.src.charCodeAt(pos) 33 | if (!isSpace(ch1)) break 34 | pos++ 35 | } 36 | 37 | state.pos = pos 38 | return true 39 | } 40 | 41 | let escapedStr = state.src[pos] 42 | 43 | if (ch1 >= 0xD800 && ch1 <= 0xDBFF && pos + 1 < max) { 44 | const ch2 = state.src.charCodeAt(pos + 1) 45 | 46 | if (ch2 >= 0xDC00 && ch2 <= 0xDFFF) { 47 | escapedStr += state.src[pos + 1] 48 | pos++ 49 | } 50 | } 51 | 52 | const origStr = '\\' + escapedStr 53 | 54 | if (!silent) { 55 | const token = state.push('text_special', '', 0) 56 | 57 | if (ch1 < 256 && ESCAPED[ch1] !== 0) { 58 | token.content = escapedStr 59 | } else { 60 | token.content = origStr 61 | } 62 | 63 | token.markup = origStr 64 | token.info = 'escape' 65 | } 66 | 67 | state.pos = pos + 1 68 | return true 69 | } 70 | -------------------------------------------------------------------------------- /lib/rules_inline/fragments_join.mjs: -------------------------------------------------------------------------------- 1 | // Clean up tokens after emphasis and strikethrough postprocessing: 2 | // merge adjacent text nodes into one and re-calculate all token levels 3 | // 4 | // This is necessary because initially emphasis delimiter markers (*, _, ~) 5 | // are treated as their own separate text tokens. Then emphasis rule either 6 | // leaves them as text (needed to merge with adjacent text) or turns them 7 | // into opening/closing tags (which messes up levels inside). 8 | // 9 | 10 | export default function fragments_join (state) { 11 | let curr, last 12 | let level = 0 13 | const tokens = state.tokens 14 | const max = state.tokens.length 15 | 16 | for (curr = last = 0; curr < max; curr++) { 17 | // re-calculate levels after emphasis/strikethrough turns some text nodes 18 | // into opening/closing tags 19 | if (tokens[curr].nesting < 0) level-- // closing tag 20 | tokens[curr].level = level 21 | if (tokens[curr].nesting > 0) level++ // opening tag 22 | 23 | if (tokens[curr].type === 'text' && 24 | curr + 1 < max && 25 | tokens[curr + 1].type === 'text') { 26 | // collapse two adjacent text nodes 27 | tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content 28 | } else { 29 | if (curr !== last) { tokens[last] = tokens[curr] } 30 | 31 | last++ 32 | } 33 | } 34 | 35 | if (curr !== last) { 36 | tokens.length = last 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/rules_inline/html_inline.mjs: -------------------------------------------------------------------------------- 1 | // Process html tags 2 | 3 | import { HTML_TAG_RE } from '../common/html_re.mjs' 4 | 5 | function isLinkOpen (str) { 6 | return /^<a[>\s]/i.test(str) 7 | } 8 | function isLinkClose (str) { 9 | return /^<\/a\s*>/i.test(str) 10 | } 11 | 12 | function isLetter (ch) { 13 | /* eslint no-bitwise:0 */ 14 | const lc = ch | 0x20 // to lower case 15 | return (lc >= 0x61/* a */) && (lc <= 0x7a/* z */) 16 | } 17 | 18 | export default function html_inline (state, silent) { 19 | if (!state.md.options.html) { return false } 20 | 21 | // Check start 22 | const max = state.posMax 23 | const pos = state.pos 24 | if (state.src.charCodeAt(pos) !== 0x3C/* < */ || 25 | pos + 2 >= max) { 26 | return false 27 | } 28 | 29 | // Quick fail on second char 30 | const 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 | const match = state.src.slice(pos).match(HTML_TAG_RE) 39 | if (!match) { return false } 40 | 41 | if (!silent) { 42 | const token = state.push('html_inline', '', 0) 43 | token.content = match[0] 44 | 45 | if (isLinkOpen(token.content)) state.linkLevel++ 46 | if (isLinkClose(token.content)) state.linkLevel-- 47 | } 48 | state.pos += match[0].length 49 | return true 50 | } 51 | -------------------------------------------------------------------------------- /lib/rules_inline/image.mjs: -------------------------------------------------------------------------------- 1 | // Process  2 | 3 | import { normalizeReference, isSpace } from '../common/utils.mjs' 4 | 5 | export default function image (state, silent) { 6 | let code, content, label, pos, ref, res, title, start 7 | let href = '' 8 | const oldPos = state.pos 9 | const max = state.posMax 10 | 11 | if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false } 12 | if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false } 13 | 14 | const labelStart = state.pos + 2 15 | const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false) 16 | 17 | // parser failed to find ']', so it's not a valid link 18 | if (labelEnd < 0) { return false } 19 | 20 | pos = labelEnd + 1 21 | if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { 22 | // 23 | // Inline link 24 | // 25 | 26 | // [link]( <href> "title" ) 27 | // ^^ skipping these spaces 28 | pos++ 29 | for (; pos < max; pos++) { 30 | code = state.src.charCodeAt(pos) 31 | if (!isSpace(code) && code !== 0x0A) { break } 32 | } 33 | if (pos >= max) { return false } 34 | 35 | // [link]( <href> "title" ) 36 | // ^^^^^^ parsing link destination 37 | start = pos 38 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax) 39 | if (res.ok) { 40 | href = state.md.normalizeLink(res.str) 41 | if (state.md.validateLink(href)) { 42 | pos = res.pos 43 | } else { 44 | href = '' 45 | } 46 | } 47 | 48 | // [link]( <href> "title" ) 49 | // ^^ skipping these spaces 50 | start = pos 51 | for (; pos < max; pos++) { 52 | code = state.src.charCodeAt(pos) 53 | if (!isSpace(code) && code !== 0x0A) { break } 54 | } 55 | 56 | // [link]( <href> "title" ) 57 | // ^^^^^^^ parsing link title 58 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax) 59 | if (pos < max && start !== pos && res.ok) { 60 | title = res.str 61 | pos = res.pos 62 | 63 | // [link]( <href> "title" ) 64 | // ^^ skipping these spaces 65 | for (; pos < max; pos++) { 66 | code = state.src.charCodeAt(pos) 67 | if (!isSpace(code) && code !== 0x0A) { break } 68 | } 69 | } else { 70 | title = '' 71 | } 72 | 73 | if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { 74 | state.pos = oldPos 75 | return false 76 | } 77 | pos++ 78 | } else { 79 | // 80 | // Link reference 81 | // 82 | if (typeof state.env.references === 'undefined') { return false } 83 | 84 | if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) { 85 | start = pos + 1 86 | pos = state.md.helpers.parseLinkLabel(state, pos) 87 | if (pos >= 0) { 88 | label = state.src.slice(start, pos++) 89 | } else { 90 | pos = labelEnd + 1 91 | } 92 | } else { 93 | pos = labelEnd + 1 94 | } 95 | 96 | // covers label === '' and label === undefined 97 | // (collapsed reference link and shortcut reference link respectively) 98 | if (!label) { label = state.src.slice(labelStart, labelEnd) } 99 | 100 | ref = state.env.references[normalizeReference(label)] 101 | if (!ref) { 102 | state.pos = oldPos 103 | return false 104 | } 105 | href = ref.href 106 | title = ref.title 107 | } 108 | 109 | // 110 | // We found the end of the link, and know for a fact it's a valid link; 111 | // so all that's left to do is to call tokenizer. 112 | // 113 | if (!silent) { 114 | content = state.src.slice(labelStart, labelEnd) 115 | 116 | const tokens = [] 117 | state.md.inline.parse( 118 | content, 119 | state.md, 120 | state.env, 121 | tokens 122 | ) 123 | 124 | const token = state.push('image', 'img', 0) 125 | const attrs = [['src', href], ['alt', '']] 126 | token.attrs = attrs 127 | token.children = tokens 128 | token.content = content 129 | 130 | if (title) { 131 | attrs.push(['title', title]) 132 | } 133 | } 134 | 135 | state.pos = pos 136 | state.posMax = max 137 | return true 138 | } 139 | -------------------------------------------------------------------------------- /lib/rules_inline/link.mjs: -------------------------------------------------------------------------------- 1 | // Process [link](<to> "stuff") 2 | 3 | import { normalizeReference, isSpace } from '../common/utils.mjs' 4 | 5 | export default function link (state, silent) { 6 | let code, label, res, ref 7 | let href = '' 8 | let title = '' 9 | let start = state.pos 10 | let parseReference = true 11 | 12 | if (state.src.charCodeAt(state.pos) !== 0x5B/* [ */) { return false } 13 | 14 | const oldPos = state.pos 15 | const max = state.posMax 16 | const labelStart = state.pos + 1 17 | const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, true) 18 | 19 | // parser failed to find ']', so it's not a valid link 20 | if (labelEnd < 0) { return false } 21 | 22 | let pos = labelEnd + 1 23 | if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) { 24 | // 25 | // Inline link 26 | // 27 | 28 | // might have found a valid shortcut link, disable reference parsing 29 | parseReference = false 30 | 31 | // [link]( <href> "title" ) 32 | // ^^ skipping these spaces 33 | pos++ 34 | for (; pos < max; pos++) { 35 | code = state.src.charCodeAt(pos) 36 | if (!isSpace(code) && code !== 0x0A) { break } 37 | } 38 | if (pos >= max) { return false } 39 | 40 | // [link]( <href> "title" ) 41 | // ^^^^^^ parsing link destination 42 | start = pos 43 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax) 44 | if (res.ok) { 45 | href = state.md.normalizeLink(res.str) 46 | if (state.md.validateLink(href)) { 47 | pos = res.pos 48 | } else { 49 | href = '' 50 | } 51 | 52 | // [link]( <href> "title" ) 53 | // ^^ skipping these spaces 54 | start = pos 55 | for (; pos < max; pos++) { 56 | code = state.src.charCodeAt(pos) 57 | if (!isSpace(code) && code !== 0x0A) { break } 58 | } 59 | 60 | // [link]( <href> "title" ) 61 | // ^^^^^^^ parsing link title 62 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax) 63 | if (pos < max && start !== pos && res.ok) { 64 | title = res.str 65 | pos = res.pos 66 | 67 | // [link]( <href> "title" ) 68 | // ^^ skipping these spaces 69 | for (; pos < max; pos++) { 70 | code = state.src.charCodeAt(pos) 71 | if (!isSpace(code) && code !== 0x0A) { break } 72 | } 73 | } 74 | } 75 | 76 | if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) { 77 | // parsing a valid shortcut link failed, fallback to reference 78 | parseReference = true 79 | } 80 | pos++ 81 | } 82 | 83 | if (parseReference) { 84 | // 85 | // Link reference 86 | // 87 | if (typeof state.env.references === 'undefined') { return false } 88 | 89 | if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) { 90 | start = pos + 1 91 | pos = state.md.helpers.parseLinkLabel(state, pos) 92 | if (pos >= 0) { 93 | label = state.src.slice(start, pos++) 94 | } else { 95 | pos = labelEnd + 1 96 | } 97 | } else { 98 | pos = labelEnd + 1 99 | } 100 | 101 | // covers label === '' and label === undefined 102 | // (collapsed reference link and shortcut reference link respectively) 103 | if (!label) { label = state.src.slice(labelStart, labelEnd) } 104 | 105 | ref = state.env.references[normalizeReference(label)] 106 | if (!ref) { 107 | state.pos = oldPos 108 | return false 109 | } 110 | href = ref.href 111 | title = ref.title 112 | } 113 | 114 | // 115 | // We found the end of the link, and know for a fact it's a valid link; 116 | // so all that's left to do is to call tokenizer. 117 | // 118 | if (!silent) { 119 | state.pos = labelStart 120 | state.posMax = labelEnd 121 | 122 | const token_o = state.push('link_open', 'a', 1) 123 | const attrs = [['href', href]] 124 | token_o.attrs = attrs 125 | if (title) { 126 | attrs.push(['title', title]) 127 | } 128 | 129 | state.linkLevel++ 130 | state.md.inline.tokenize(state) 131 | state.linkLevel-- 132 | 133 | state.push('link_close', 'a', -1) 134 | } 135 | 136 | state.pos = pos 137 | state.posMax = max 138 | return true 139 | } 140 | -------------------------------------------------------------------------------- /lib/rules_inline/linkify.mjs: -------------------------------------------------------------------------------- 1 | // Process links like https://example.org/ 2 | 3 | // RFC3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) 4 | const SCHEME_RE = /(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$/i 5 | 6 | export default function linkify (state, silent) { 7 | if (!state.md.options.linkify) return false 8 | if (state.linkLevel > 0) return false 9 | 10 | const pos = state.pos 11 | const max = state.posMax 12 | 13 | if (pos + 3 > max) return false 14 | if (state.src.charCodeAt(pos) !== 0x3A/* : */) return false 15 | if (state.src.charCodeAt(pos + 1) !== 0x2F/* / */) return false 16 | if (state.src.charCodeAt(pos + 2) !== 0x2F/* / */) return false 17 | 18 | const match = state.pending.match(SCHEME_RE) 19 | if (!match) return false 20 | 21 | const proto = match[1] 22 | 23 | const link = state.md.linkify.matchAtStart(state.src.slice(pos - proto.length)) 24 | if (!link) return false 25 | 26 | let url = link.url 27 | 28 | // invalid link, but still detected by linkify somehow; 29 | // need to check to prevent infinite loop below 30 | if (url.length <= proto.length) return false 31 | 32 | // disallow '*' at the end of the link (conflicts with emphasis) 33 | url = url.replace(/\*+$/, '') 34 | 35 | const fullUrl = state.md.normalizeLink(url) 36 | if (!state.md.validateLink(fullUrl)) return false 37 | 38 | if (!silent) { 39 | state.pending = state.pending.slice(0, -proto.length) 40 | 41 | const token_o = state.push('link_open', 'a', 1) 42 | token_o.attrs = [['href', fullUrl]] 43 | token_o.markup = 'linkify' 44 | token_o.info = 'auto' 45 | 46 | const token_t = state.push('text', '', 0) 47 | token_t.content = state.md.normalizeLinkText(url) 48 | 49 | const token_c = state.push('link_close', 'a', -1) 50 | token_c.markup = 'linkify' 51 | token_c.info = 'auto' 52 | } 53 | 54 | state.pos += url.length - proto.length 55 | return true 56 | } 57 | -------------------------------------------------------------------------------- /lib/rules_inline/newline.mjs: -------------------------------------------------------------------------------- 1 | // Proceess '\n' 2 | 3 | import { isSpace } from '../common/utils.mjs' 4 | 5 | export default function newline (state, silent) { 6 | let pos = state.pos 7 | 8 | if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false } 9 | 10 | const pmax = state.pending.length - 1 11 | const max = state.posMax 12 | 13 | // ' \n' -> hardbreak 14 | // Lookup in pending chars is bad practice! Don't copy to other rules! 15 | // Pending string is stored in concat mode, indexed lookups will cause 16 | // convertion to flat mode. 17 | if (!silent) { 18 | if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { 19 | if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { 20 | // Find whitespaces tail of pending chars. 21 | let ws = pmax - 1 22 | while (ws >= 1 && state.pending.charCodeAt(ws - 1) === 0x20) ws-- 23 | 24 | state.pending = state.pending.slice(0, ws) 25 | state.push('hardbreak', 'br', 0) 26 | } else { 27 | state.pending = state.pending.slice(0, -1) 28 | state.push('softbreak', 'br', 0) 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 | -------------------------------------------------------------------------------- /lib/rules_inline/state_inline.mjs: -------------------------------------------------------------------------------- 1 | // Inline parser state 2 | 3 | import Token from '../token.mjs' 4 | import { isWhiteSpace, isPunctChar, isMdAsciiPunct } from '../common/utils.mjs' 5 | 6 | function StateInline (src, md, env, outTokens) { 7 | this.src = src 8 | this.env = env 9 | this.md = md 10 | this.tokens = outTokens 11 | this.tokens_meta = Array(outTokens.length) 12 | 13 | this.pos = 0 14 | this.posMax = this.src.length 15 | this.level = 0 16 | this.pending = '' 17 | this.pendingLevel = 0 18 | 19 | // Stores { start: end } pairs. Useful for backtrack 20 | // optimization of pairs parse (emphasis, strikes). 21 | this.cache = {} 22 | 23 | // List of emphasis-like delimiters for current tag 24 | this.delimiters = [] 25 | 26 | // Stack of delimiter lists for upper level tags 27 | this._prev_delimiters = [] 28 | 29 | // backtick length => last seen position 30 | this.backticks = {} 31 | this.backticksScanned = false 32 | 33 | // Counter used to disable inline linkify-it execution 34 | // inside <a> and markdown links 35 | this.linkLevel = 0 36 | } 37 | 38 | // Flush pending text 39 | // 40 | StateInline.prototype.pushPending = function () { 41 | const token = new Token('text', '', 0) 42 | token.content = this.pending 43 | token.level = this.pendingLevel 44 | this.tokens.push(token) 45 | this.pending = '' 46 | return token 47 | } 48 | 49 | // Push new token to "stream". 50 | // If pending text exists - flush it as text token 51 | // 52 | StateInline.prototype.push = function (type, tag, nesting) { 53 | if (this.pending) { 54 | this.pushPending() 55 | } 56 | 57 | const token = new Token(type, tag, nesting) 58 | let token_meta = null 59 | 60 | if (nesting < 0) { 61 | // closing tag 62 | this.level-- 63 | this.delimiters = this._prev_delimiters.pop() 64 | } 65 | 66 | token.level = this.level 67 | 68 | if (nesting > 0) { 69 | // opening tag 70 | this.level++ 71 | this._prev_delimiters.push(this.delimiters) 72 | this.delimiters = [] 73 | token_meta = { delimiters: this.delimiters } 74 | } 75 | 76 | this.pendingLevel = this.level 77 | this.tokens.push(token) 78 | this.tokens_meta.push(token_meta) 79 | return token 80 | } 81 | 82 | // Scan a sequence of emphasis-like markers, and determine whether 83 | // it can start an emphasis sequence or end an emphasis sequence. 84 | // 85 | // - start - position to scan from (it should point at a valid marker); 86 | // - canSplitWord - determine if these markers can be found inside a word 87 | // 88 | StateInline.prototype.scanDelims = function (start, canSplitWord) { 89 | const max = this.posMax 90 | const marker = this.src.charCodeAt(start) 91 | 92 | // treat beginning of the line as a whitespace 93 | const lastChar = start > 0 ? this.src.charCodeAt(start - 1) : 0x20 94 | 95 | let pos = start 96 | while (pos < max && this.src.charCodeAt(pos) === marker) { pos++ } 97 | 98 | const count = pos - start 99 | 100 | // treat end of the line as a whitespace 101 | const nextChar = pos < max ? this.src.charCodeAt(pos) : 0x20 102 | 103 | const isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)) 104 | const isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)) 105 | 106 | const isLastWhiteSpace = isWhiteSpace(lastChar) 107 | const isNextWhiteSpace = isWhiteSpace(nextChar) 108 | 109 | const left_flanking = 110 | !isNextWhiteSpace && (!isNextPunctChar || isLastWhiteSpace || isLastPunctChar) 111 | const right_flanking = 112 | !isLastWhiteSpace && (!isLastPunctChar || isNextWhiteSpace || isNextPunctChar) 113 | 114 | const can_open = left_flanking && (canSplitWord || !right_flanking || isLastPunctChar) 115 | const can_close = right_flanking && (canSplitWord || !left_flanking || isNextPunctChar) 116 | 117 | return { can_open, can_close, length: count } 118 | } 119 | 120 | // re-export Token class to use in block rules 121 | StateInline.prototype.Token = Token 122 | 123 | export default StateInline 124 | -------------------------------------------------------------------------------- /lib/rules_inline/strikethrough.mjs: -------------------------------------------------------------------------------- 1 | // ~~strike through~~ 2 | // 3 | 4 | // Insert each marker as a separate text token, and add it to delimiter list 5 | // 6 | function strikethrough_tokenize (state, silent) { 7 | const start = state.pos 8 | const marker = state.src.charCodeAt(start) 9 | 10 | if (silent) { return false } 11 | 12 | if (marker !== 0x7E/* ~ */) { return false } 13 | 14 | const scanned = state.scanDelims(state.pos, true) 15 | let len = scanned.length 16 | const ch = String.fromCharCode(marker) 17 | 18 | if (len < 2) { return false } 19 | 20 | let token 21 | 22 | if (len % 2) { 23 | token = state.push('text', '', 0) 24 | token.content = ch 25 | len-- 26 | } 27 | 28 | for (let i = 0; i < len; i += 2) { 29 | token = state.push('text', '', 0) 30 | token.content = ch + ch 31 | 32 | state.delimiters.push({ 33 | marker, 34 | length: 0, // disable "rule of 3" length checks meant for emphasis 35 | token: state.tokens.length - 1, 36 | end: -1, 37 | open: scanned.can_open, 38 | close: scanned.can_close 39 | }) 40 | } 41 | 42 | state.pos += scanned.length 43 | 44 | return true 45 | } 46 | 47 | function postProcess (state, delimiters) { 48 | let token 49 | const loneMarkers = [] 50 | const max = delimiters.length 51 | 52 | for (let i = 0; i < max; i++) { 53 | const startDelim = delimiters[i] 54 | 55 | if (startDelim.marker !== 0x7E/* ~ */) { 56 | continue 57 | } 58 | 59 | if (startDelim.end === -1) { 60 | continue 61 | } 62 | 63 | const endDelim = delimiters[startDelim.end] 64 | 65 | token = state.tokens[startDelim.token] 66 | token.type = 's_open' 67 | token.tag = 's' 68 | token.nesting = 1 69 | token.markup = '~~' 70 | token.content = '' 71 | 72 | token = state.tokens[endDelim.token] 73 | token.type = 's_close' 74 | token.tag = 's' 75 | token.nesting = -1 76 | token.markup = '~~' 77 | token.content = '' 78 | 79 | if (state.tokens[endDelim.token - 1].type === 'text' && 80 | state.tokens[endDelim.token - 1].content === '~') { 81 | loneMarkers.push(endDelim.token - 1) 82 | } 83 | } 84 | 85 | // If a marker sequence has an odd number of characters, it's splitted 86 | // like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the 87 | // start of the sequence. 88 | // 89 | // So, we have to move all those markers after subsequent s_close tags. 90 | // 91 | while (loneMarkers.length) { 92 | const i = loneMarkers.pop() 93 | let j = i + 1 94 | 95 | while (j < state.tokens.length && state.tokens[j].type === 's_close') { 96 | j++ 97 | } 98 | 99 | j-- 100 | 101 | if (i !== j) { 102 | token = state.tokens[j] 103 | state.tokens[j] = state.tokens[i] 104 | state.tokens[i] = token 105 | } 106 | } 107 | } 108 | 109 | // Walk through delimiter list and replace text tokens with tags 110 | // 111 | function strikethrough_postProcess (state) { 112 | const tokens_meta = state.tokens_meta 113 | const max = state.tokens_meta.length 114 | 115 | postProcess(state, state.delimiters) 116 | 117 | for (let curr = 0; curr < max; curr++) { 118 | if (tokens_meta[curr] && tokens_meta[curr].delimiters) { 119 | postProcess(state, tokens_meta[curr].delimiters) 120 | } 121 | } 122 | } 123 | 124 | export default { 125 | tokenize: strikethrough_tokenize, 126 | postProcess: strikethrough_postProcess 127 | } 128 | -------------------------------------------------------------------------------- /lib/rules_inline/text.mjs: -------------------------------------------------------------------------------- 1 | // Skip text characters for text token, place those to pending buffer 2 | // and increment current pos 3 | 4 | // Rule to skip pure text 5 | // '{}$%@~+=:' reserved for extentions 6 | 7 | // !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, _, `, {, |, }, or ~ 8 | 9 | // !!!! Don't confuse with "Markdown ASCII Punctuation" chars 10 | // http://spec.commonmark.org/0.15/#ascii-punctuation-character 11 | function isTerminatorChar (ch) { 12 | switch (ch) { 13 | case 0x0A/* \n */: 14 | case 0x21/* ! */: 15 | case 0x23/* # */: 16 | case 0x24/* $ */: 17 | case 0x25/* % */: 18 | case 0x26/* & */: 19 | case 0x2A/* * */: 20 | case 0x2B/* + */: 21 | case 0x2D/* - */: 22 | case 0x3A/* : */: 23 | case 0x3C/* < */: 24 | case 0x3D/* = */: 25 | case 0x3E/* > */: 26 | case 0x40/* @ */: 27 | case 0x5B/* [ */: 28 | case 0x5C/* \ */: 29 | case 0x5D/* ] */: 30 | case 0x5E/* ^ */: 31 | case 0x5F/* _ */: 32 | case 0x60/* ` */: 33 | case 0x7B/* { */: 34 | case 0x7D/* } */: 35 | case 0x7E/* ~ */: 36 | return true 37 | default: 38 | return false 39 | } 40 | } 41 | 42 | export default function text (state, silent) { 43 | let pos = state.pos 44 | 45 | while (pos < state.posMax && !isTerminatorChar(state.src.charCodeAt(pos))) { 46 | pos++ 47 | } 48 | 49 | if (pos === state.pos) { return false } 50 | 51 | if (!silent) { state.pending += state.src.slice(state.pos, pos) } 52 | 53 | state.pos = pos 54 | 55 | return true 56 | } 57 | 58 | // Alternative implementation, for memory. 59 | // 60 | // It costs 10% of performance, but allows extend terminators list, if place it 61 | // to `ParserInline` property. Probably, will switch to it sometime, such 62 | // flexibility required. 63 | 64 | /* 65 | var TERMINATOR_RE = /[\n!#$%&*+\-:<=>@[\\\]^_`{}~]/; 66 | 67 | module.exports = function text(state, silent) { 68 | var pos = state.pos, 69 | idx = state.src.slice(pos).search(TERMINATOR_RE); 70 | 71 | // first char is terminator -> empty text 72 | if (idx === 0) { return false; } 73 | 74 | // no terminator -> text till end of string 75 | if (idx < 0) { 76 | if (!silent) { state.pending += state.src.slice(pos); } 77 | state.pos = state.src.length; 78 | return true; 79 | } 80 | 81 | if (!silent) { state.pending += state.src.slice(pos, pos + idx); } 82 | 83 | state.pos += idx; 84 | 85 | return true; 86 | }; */ 87 | -------------------------------------------------------------------------------- /lib/token.mjs: -------------------------------------------------------------------------------- 1 | // Token class 2 | 3 | /** 4 | * class Token 5 | **/ 6 | 7 | /** 8 | * new Token(type, tag, nesting) 9 | * 10 | * Create new token and fill passed properties. 11 | **/ 12 | function Token (type, tag, nesting) { 13 | /** 14 | * Token#type -> String 15 | * 16 | * Type of the token (string, e.g. "paragraph_open") 17 | **/ 18 | this.type = type 19 | 20 | /** 21 | * Token#tag -> String 22 | * 23 | * html tag name, e.g. "p" 24 | **/ 25 | this.tag = tag 26 | 27 | /** 28 | * Token#attrs -> Array 29 | * 30 | * Html attributes. Format: `[ [ name1, value1 ], [ name2, value2 ] ]` 31 | **/ 32 | this.attrs = null 33 | 34 | /** 35 | * Token#map -> Array 36 | * 37 | * Source map info. Format: `[ line_begin, line_end ]` 38 | **/ 39 | this.map = null 40 | 41 | /** 42 | * Token#nesting -> Number 43 | * 44 | * Level change (number in {-1, 0, 1} set), where: 45 | * 46 | * - `1` means the tag is opening 47 | * - `0` means the tag is self-closing 48 | * - `-1` means the tag is closing 49 | **/ 50 | this.nesting = nesting 51 | 52 | /** 53 | * Token#level -> Number 54 | * 55 | * nesting level, the same as `state.level` 56 | **/ 57 | this.level = 0 58 | 59 | /** 60 | * Token#children -> Array 61 | * 62 | * An array of child nodes (inline and img tokens) 63 | **/ 64 | this.children = null 65 | 66 | /** 67 | * Token#content -> String 68 | * 69 | * In a case of self-closing tag (code, html, fence, etc.), 70 | * it has contents of this tag. 71 | **/ 72 | this.content = '' 73 | 74 | /** 75 | * Token#markup -> String 76 | * 77 | * '*' or '_' for emphasis, fence string for fence, etc. 78 | **/ 79 | this.markup = '' 80 | 81 | /** 82 | * Token#info -> String 83 | * 84 | * Additional information: 85 | * 86 | * - Info string for "fence" tokens 87 | * - The value "auto" for autolink "link_open" and "link_close" tokens 88 | * - The string value of the item marker for ordered-list "list_item_open" tokens 89 | **/ 90 | this.info = '' 91 | 92 | /** 93 | * Token#meta -> Object 94 | * 95 | * A place for plugins to store an arbitrary data 96 | **/ 97 | this.meta = null 98 | 99 | /** 100 | * Token#block -> Boolean 101 | * 102 | * True for block-level tokens, false for inline tokens. 103 | * Used in renderer to calculate line breaks 104 | **/ 105 | this.block = false 106 | 107 | /** 108 | * Token#hidden -> Boolean 109 | * 110 | * If it's true, ignore this element when rendering. Used for tight lists 111 | * to hide paragraphs. 112 | **/ 113 | this.hidden = false 114 | } 115 | 116 | /** 117 | * Token.attrIndex(name) -> Number 118 | * 119 | * Search attribute index by name. 120 | **/ 121 | Token.prototype.attrIndex = function attrIndex (name) { 122 | if (!this.attrs) { return -1 } 123 | 124 | const attrs = this.attrs 125 | 126 | for (let i = 0, len = attrs.length; i < len; i++) { 127 | if (attrs[i][0] === name) { return i } 128 | } 129 | return -1 130 | } 131 | 132 | /** 133 | * Token.attrPush(attrData) 134 | * 135 | * Add `[ name, value ]` attribute to list. Init attrs if necessary 136 | **/ 137 | Token.prototype.attrPush = function attrPush (attrData) { 138 | if (this.attrs) { 139 | this.attrs.push(attrData) 140 | } else { 141 | this.attrs = [attrData] 142 | } 143 | } 144 | 145 | /** 146 | * Token.attrSet(name, value) 147 | * 148 | * Set `name` attribute to `value`. Override old value if exists. 149 | **/ 150 | Token.prototype.attrSet = function attrSet (name, value) { 151 | const idx = this.attrIndex(name) 152 | const attrData = [name, value] 153 | 154 | if (idx < 0) { 155 | this.attrPush(attrData) 156 | } else { 157 | this.attrs[idx] = attrData 158 | } 159 | } 160 | 161 | /** 162 | * Token.attrGet(name) 163 | * 164 | * Get the value of attribute `name`, or null if it does not exist. 165 | **/ 166 | Token.prototype.attrGet = function attrGet (name) { 167 | const idx = this.attrIndex(name) 168 | let value = null 169 | if (idx >= 0) { 170 | value = this.attrs[idx][1] 171 | } 172 | return value 173 | } 174 | 175 | /** 176 | * Token.attrJoin(name, value) 177 | * 178 | * Join value to existing attribute via space. Or create new attribute if not 179 | * exists. Useful to operate with token classes. 180 | **/ 181 | Token.prototype.attrJoin = function attrJoin (name, value) { 182 | const idx = this.attrIndex(name) 183 | 184 | if (idx < 0) { 185 | this.attrPush([name, value]) 186 | } else { 187 | this.attrs[idx][1] = this.attrs[idx][1] + ' ' + value 188 | } 189 | } 190 | 191 | export default Token 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it", 3 | "version": "14.1.0", 4 | "description": "Markdown-it - modern pluggable markdown parser.", 5 | "keywords": [ 6 | "markdown", 7 | "parser", 8 | "commonmark", 9 | "markdown-it", 10 | "markdown-it-plugin" 11 | ], 12 | "repository": "markdown-it/markdown-it", 13 | "license": "MIT", 14 | "main": "dist/index.cjs.js", 15 | "module": "index.mjs", 16 | "exports": { 17 | ".": { 18 | "import": "./index.mjs", 19 | "require": "./dist/index.cjs.js" 20 | }, 21 | "./*": { 22 | "require": "./*", 23 | "import": "./*" 24 | } 25 | }, 26 | "bin": { 27 | "markdown-it": "bin/markdown-it.mjs" 28 | }, 29 | "scripts": { 30 | "lint": "eslint .", 31 | "test": "npm run lint && CJS_ONLY=1 npm run build && c8 --exclude dist --exclude test -r text -r html -r lcov mocha && node support/specsplit.mjs", 32 | "doc": "node support/build_doc.mjs", 33 | "gh-doc": "npm run doc && gh-pages -d apidoc -f", 34 | "demo": "npm run lint && node support/build_demo.mjs", 35 | "gh-demo": "npm run demo && gh-pages -d demo -f -b master -r git@github.com:markdown-it/markdown-it.github.io.git", 36 | "build": "rollup -c support/rollup.config.mjs", 37 | "benchmark-deps": "npm install --prefix benchmark/extra/ -g marked@0.3.6 commonmark@0.26.0 markdown-it/markdown-it.git#2.2.1", 38 | "specsplit": "support/specsplit.mjs good -o test/fixtures/commonmark/good.txt && support/specsplit.mjs bad -o test/fixtures/commonmark/bad.txt && support/specsplit.mjs", 39 | "todo": "grep 'TODO' -n -r ./lib 2>/dev/null", 40 | "prepublishOnly": "npm test && npm run build && npm run gh-demo && npm run gh-doc" 41 | }, 42 | "files": [ 43 | "index.mjs", 44 | "lib/", 45 | "dist/" 46 | ], 47 | "dependencies": { 48 | "argparse": "^2.0.1", 49 | "entities": "^4.4.0", 50 | "linkify-it": "^5.0.0", 51 | "mdurl": "^2.0.0", 52 | "punycode.js": "^2.3.1", 53 | "uc.micro": "^2.1.0" 54 | }, 55 | "devDependencies": { 56 | "@rollup/plugin-babel": "^6.0.4", 57 | "@rollup/plugin-commonjs": "^25.0.7", 58 | "@rollup/plugin-node-resolve": "^15.2.3", 59 | "@rollup/plugin-terser": "^0.4.4", 60 | "ansi": "^0.3.0", 61 | "benchmark": "~2.1.0", 62 | "c8": "^8.0.1", 63 | "chai": "^4.2.0", 64 | "eslint": "^8.4.1", 65 | "eslint-config-standard": "^17.1.0", 66 | "express": "^4.14.0", 67 | "gh-pages": "^6.1.0", 68 | "highlight.js": "^11.9.0", 69 | "jest-worker": "^29.7.0", 70 | "markdown-it-abbr": "^2.0.0", 71 | "markdown-it-container": "^4.0.0", 72 | "markdown-it-deflist": "^3.0.0", 73 | "markdown-it-emoji": "^3.0.0", 74 | "markdown-it-footnote": "^4.0.0", 75 | "markdown-it-for-inline": "^2.0.1", 76 | "markdown-it-ins": "^4.0.0", 77 | "markdown-it-mark": "^4.0.0", 78 | "markdown-it-sub": "^2.0.0", 79 | "markdown-it-sup": "^2.0.0", 80 | "markdown-it-testgen": "^0.1.3", 81 | "mocha": "^10.2.0", 82 | "ndoc": "^6.0.0", 83 | "needle": "^3.0.0", 84 | "rollup": "^4.5.0", 85 | "shelljs": "^0.8.4", 86 | "supertest": "^6.0.1" 87 | }, 88 | "mocha": { 89 | "inline-diffs": true, 90 | "timeout": 60000 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /support/api_header.md: -------------------------------------------------------------------------------- 1 | # markdown-it 2 | 3 | ## Install 4 | 5 | **node.js**: 6 | 7 | ```bash 8 | npm install markdown-it 9 | ``` 10 | 11 | **browser (CDN):** 12 | 13 | - [jsDeliver CDN](http://www.jsdelivr.com/#!markdown-it "jsDelivr CDN") 14 | - [cdnjs.com CDN](https://cdnjs.com/libraries/markdown-it "cdnjs.com") 15 | 16 | 17 | ## Usage examples 18 | 19 | See also: 20 | 21 | - [Development info](https://github.com/markdown-it/markdown-it/tree/master/docs) - 22 | for plugins writers. 23 | 24 | 25 | ### Simple 26 | 27 | ```js 28 | // node.js 29 | // can use `require('markdown-it')` for CJS 30 | import markdownit from 'markdown-it' 31 | const md = markdownit() 32 | const result = md.render('# markdown-it rulezz!'); 33 | 34 | // browser with UMD build, added to "window" on script load 35 | // Note, there is no dash in "markdownit". 36 | const md = window.markdownit(); 37 | const result = md.render('# markdown-it rulezz!'); 38 | ``` 39 | 40 | Single line rendering, without paragraph wrap: 41 | 42 | ```js 43 | import markdownit from 'markdown-it' 44 | const md = markdownit() 45 | const result = md.renderInline('__markdown-it__ rulezz!'); 46 | ``` 47 | 48 | 49 | ### Init with presets and options 50 | 51 | (*) presets define combinations of active rules and options. Can be 52 | `"commonmark"`, `"zero"` or `"default"` (if skipped). See 53 | [API docs](https://markdown-it.github.io/markdown-it/#MarkdownIt.new) for more details. 54 | 55 | ```js 56 | import markdownit from 'markdown-it' 57 | 58 | // commonmark mode 59 | const md = markdownit('commonmark') 60 | 61 | // default mode 62 | const md = markdownit() 63 | 64 | // enable everything 65 | const md = markdownit({ 66 | html: true, 67 | linkify: true, 68 | typographer: true 69 | }) 70 | 71 | // full options list (defaults) 72 | const md = markdownit({ 73 | // Enable HTML tags in source 74 | html: false, 75 | 76 | // Use '/' to close single tags (<br />). 77 | // This is only for full CommonMark compatibility. 78 | xhtmlOut: false, 79 | 80 | // Convert '\n' in paragraphs into <br> 81 | breaks: false, 82 | 83 | // CSS language prefix for fenced blocks. Can be 84 | // useful for external highlighters. 85 | langPrefix: 'language-', 86 | 87 | // Autoconvert URL-like text to links 88 | linkify: false, 89 | 90 | // Enable some language-neutral replacement + quotes beautification 91 | // For the full list of replacements, see https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.mjs 92 | typographer: false, 93 | 94 | // Double + single quotes replacement pairs, when typographer enabled, 95 | // and smartquotes on. Could be either a String or an Array. 96 | // 97 | // For example, you can use '«»„“' for Russian, '„“‚‘' for German, 98 | // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 99 | quotes: '“”‘’', 100 | 101 | // Highlighter function. Should return escaped HTML, 102 | // or '' if the source string is not changed and should be escaped externally. 103 | // If result starts with <pre... internal wrapper is skipped. 104 | highlight: function (/*str, lang*/) { return ''; } 105 | }); 106 | ``` 107 | 108 | 109 | ### Plugins load 110 | 111 | ```js 112 | import markdownit from 'markdown-it' 113 | 114 | const md = markdownit 115 | .use(plugin1) 116 | .use(plugin2, opts, ...) 117 | .use(plugin3); 118 | ``` 119 | 120 | 121 | ### Syntax highlighting 122 | 123 | Apply syntax highlighting to fenced code blocks with the `highlight` option: 124 | 125 | ```js 126 | import markdownit from 'markdown-it' 127 | import hljs from 'highlight.js' // https://highlightjs.org 128 | 129 | // Actual default values 130 | const md = markdownit({ 131 | highlight: function (str, lang) { 132 | if (lang && hljs.getLanguage(lang)) { 133 | try { 134 | return hljs.highlight(str, { language: lang }).value; 135 | } catch (__) {} 136 | } 137 | 138 | return ''; // use external default escaping 139 | } 140 | }); 141 | ``` 142 | 143 | Or with full wrapper override (if you need assign class to `<pre>` or `<code>`): 144 | 145 | ```js 146 | import markdownit from 'markdown-it' 147 | import hljs from 'highlight.js' // https://highlightjs.org 148 | 149 | // Actual default values 150 | const md = markdownit({ 151 | highlight: function (str, lang) { 152 | if (lang && hljs.getLanguage(lang)) { 153 | try { 154 | return '<pre><code class="hljs">' + 155 | hljs.highlight(str, { language: lang, ignoreIllegals: true }).value + 156 | '</code></pre>'; 157 | } catch (__) {} 158 | } 159 | 160 | return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code></pre>'; 161 | } 162 | }); 163 | ``` 164 | 165 | ### Linkify 166 | 167 | `linkify: true` uses [linkify-it](https://github.com/markdown-it/linkify-it). To 168 | configure linkify-it, access the linkify instance through `md.linkify`: 169 | 170 | ```js 171 | md.linkify.set({ fuzzyEmail: false }); // disables converting email to link 172 | ``` 173 | 174 | ## Syntax extensions 175 | 176 | Embedded (enabled by default): 177 | 178 | - [Tables](https://help.github.com/articles/organizing-information-with-tables/) (GFM) 179 | - [Strikethrough](https://help.github.com/articles/basic-writing-and-formatting-syntax/#styling-text) (GFM) 180 | 181 | Via plugins: 182 | 183 | - [subscript](https://github.com/markdown-it/markdown-it-sub) 184 | - [superscript](https://github.com/markdown-it/markdown-it-sup) 185 | - [footnote](https://github.com/markdown-it/markdown-it-footnote) 186 | - [definition list](https://github.com/markdown-it/markdown-it-deflist) 187 | - [abbreviation](https://github.com/markdown-it/markdown-it-abbr) 188 | - [emoji](https://github.com/markdown-it/markdown-it-emoji) 189 | - [custom container](https://github.com/markdown-it/markdown-it-container) 190 | - [insert](https://github.com/markdown-it/markdown-it-ins) 191 | - [mark](https://github.com/markdown-it/markdown-it-mark) 192 | - ... and [others](https://www.npmjs.org/browse/keyword/markdown-it-plugin) 193 | 194 | 195 | ### Manage rules 196 | 197 | By default all rules are enabled, but can be restricted by options. On plugin 198 | load all its rules are enabled automatically. 199 | 200 | ```js 201 | import markdownit from 'markdown-it' 202 | 203 | // Activate/deactivate rules, with currying 204 | const md = markdownit() 205 | .disable(['link', 'image']) 206 | .enable(['link']) 207 | .enable('image'); 208 | 209 | // Enable everything 210 | const md = markdownit({ 211 | html: true, 212 | linkify: true, 213 | typographer: true, 214 | }); 215 | ``` 216 | -------------------------------------------------------------------------------- /support/build_demo.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import shell from 'shelljs' 4 | import { readFileSync, writeFileSync } from 'fs' 5 | 6 | function escape (input) { 7 | return input 8 | .replaceAll('&', '&') 9 | .replaceAll('<', '<') 10 | .replaceAll('>', '>') 11 | .replaceAll('"', '"') 12 | // .replaceAll("'", '''); 13 | } 14 | 15 | shell.rm('-rf', 'demo') 16 | shell.mkdir('demo') 17 | 18 | shell.cp('support/demo_template/README.md', 'demo/') 19 | shell.cp('support/demo_template/index.css', 'demo/') 20 | 21 | // Read html template and inject escaped sample 22 | const html = readFileSync('support/demo_template/index.html', 'utf8') 23 | const sample = readFileSync('support/demo_template/sample.md', 'utf8') 24 | 25 | const output = html.replace('<!--SAMPLE-->', escape(sample)) 26 | writeFileSync('demo/index.html', output) 27 | 28 | shell.exec('node_modules/.bin/rollup -c support/demo_template/rollup.config.mjs') 29 | -------------------------------------------------------------------------------- /support/build_doc.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import shell from 'shelljs' 4 | 5 | shell.rm('-rf', 'apidoc') 6 | 7 | const head = shell.exec('git show-ref --hash HEAD').stdout.slice(0, 6) 8 | 9 | const link_format = `https://github.com/{package.repository}/blob/${head}/{file}#L{line}` 10 | 11 | shell.exec(`node node_modules/.bin/ndoc --alias mjs:js --link-format "${link_format}"`) 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /support/demo_template/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .full-height { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | overflow-x: hidden; 9 | padding-bottom: 160px; 10 | background-color: #fbfbfb; 11 | } 12 | 13 | /* hack to allign emojies to line height */ 14 | .emoji { 15 | height: 1.2em; 16 | } 17 | 18 | .demo-options { 19 | margin-bottom: 30px; 20 | } 21 | 22 | .opt__strict .not-strict { 23 | opacity: 0.3; 24 | } 25 | 26 | .checkbox { 27 | margin-right: 10px; 28 | } 29 | 30 | .source { 31 | width: 100%; 32 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 33 | font-size: 13px; 34 | padding: 2px; 35 | } 36 | 37 | .result-html { 38 | padding: 2px 10px; 39 | overflow: auto; 40 | background-color: #fff; 41 | border: 1px solid #ccc; 42 | border-radius: 4px; 43 | } 44 | .result-html img { 45 | max-width: 35%; 46 | } 47 | 48 | .result-src, 49 | .result-debug { 50 | display: none; 51 | } 52 | 53 | .result-src-content, 54 | .result-debug-content { 55 | white-space: pre; 56 | } 57 | 58 | .result-as-html .result-html { display: block; } 59 | .result-as-html .result-src, 60 | .result-as-html .result-debug { display: none; } 61 | 62 | .result-as-src .result-src { display: block; } 63 | .result-as-src .result-html, 64 | .result-as-src .result-debug { display: none; } 65 | 66 | .result-as-debug .result-debug { display: block; } 67 | .result-as-debug .result-html, 68 | .result-as-debug .result-src { display: none; } 69 | 70 | .demo-control { 71 | position: absolute; 72 | right: 15px; 73 | top: -17px; 74 | border-radius: 6px 6px 0 0; 75 | font-size: 12px; 76 | background-color: #ddd; 77 | } 78 | .demo-control a { 79 | padding: 0 20px; 80 | } 81 | .demo-control a:first-child { 82 | padding-left: 30px; 83 | } 84 | .demo-control a:last-child { 85 | padding-right: 30px; 86 | } 87 | 88 | /* twbs fix */ 89 | .hljs { 90 | padding: 9.5px; 91 | } 92 | .hljs code { 93 | white-space: pre; 94 | } 95 | 96 | 97 | .footnotes { 98 | -moz-column-count: 2; 99 | column-count: 2; 100 | } 101 | .footnotes-list { 102 | padding-left: 2em; 103 | } 104 | 105 | /* custom container */ 106 | .warning { 107 | background-color: #ff8; 108 | padding: 20px; 109 | border-radius: 6px; 110 | } 111 | 112 | .gh-ribbon { 113 | display: block; 114 | position: absolute; 115 | right: -60px; 116 | top: 44px; 117 | transform: rotate(45deg); 118 | width: 230px; 119 | z-index: 10000; 120 | white-space: nowrap; 121 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 122 | background-color: #686868; 123 | box-shadow: 0 0 2px rgba(102,102,102,0.4); 124 | padding: 1px 0; 125 | } 126 | .gh-ribbon a { 127 | text-decoration: none !important; 128 | border: 1px solid #ccc; 129 | color: #fff; 130 | display: block; 131 | font-size: 13px; 132 | font-weight: 700; 133 | outline: medium none; 134 | padding: 4px 50px 2px; 135 | text-align: center; 136 | } 137 | 138 | /* Override default responsiveness */ 139 | .form-inline .radio, 140 | .form-inline .checkbox { 141 | display: inline-block; 142 | margin-bottom: 0; 143 | margin-top: 0; 144 | } 145 | .form-inline .form-group { 146 | display: inline-block; 147 | margin-bottom: 0; 148 | vertical-align: middle; 149 | } 150 | .form-inline .form-control { 151 | display: inline-block; 152 | vertical-align: middle; 153 | width: auto; 154 | } 155 | 156 | -------------------------------------------------------------------------------- /support/demo_template/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <title>markdown-it demo</title> 5 | <meta charset="UTF-8"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> 7 | <script src="https://cdn.jsdelivr.net/jquery/1.11.1/jquery.min.js"></script> 8 | <script src="https://cdn.jsdelivr.net/lodash/2.4.1/lodash.js"></script> 9 | <script src="https://cdn.jsdelivr.net/bootstrap/3.2.0/js/bootstrap.min.js"></script> 10 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/bootstrap/3.2.0/css/bootstrap.css"> 11 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/highlight.js/9.1.0/styles/github.min.css"> 12 | <script src="markdown-it.js"></script> 13 | <script src="https://unpkg.com/twemoji@14.0.2/dist/twemoji.min.js" crossorigin="anonymous"></script> 14 | <link rel="stylesheet" href="index.css"> 15 | <script src="index.js"></script> 16 | </head> 17 | <body> 18 | <div class="container"> 19 | <h1> 20 | markdown-it <small>demo</small></h1> 21 | <div class="form-inline demo-options"> 22 | <div class="checkbox not-strict"> 23 | <label class="_tip" title="enable html tags in source text"> 24 | <input id="html" type="checkbox"> html 25 | </label> 26 | </div> 27 | <div class="checkbox not-strict"> 28 | <label class="_tip" title="produce xtml output (add / to single tags (<br /> instead of <br>)"> 29 | <input id="xhtmlOut" type="checkbox"> xhtmlOut 30 | </label> 31 | </div> 32 | <div class="checkbox not-strict"> 33 | <label class="_tip" title="newlines in paragraphs are rendered as <br>"> 34 | <input id="breaks" type="checkbox"> breaks 35 | </label> 36 | </div> 37 | <div class="checkbox not-strict"> 38 | <label class="_tip" title="autoconvert link-like texts to links"> 39 | <input id="linkify" type="checkbox"> linkify 40 | </label> 41 | </div> 42 | <div class="checkbox not-strict"> 43 | <label class="_tip" title="do typographic replacements, (c) → © and so on"> 44 | <input id="typographer" type="checkbox"> typographer 45 | </label> 46 | </div> 47 | <div class="checkbox not-strict"> 48 | <label class="_tip" title="enable output highlight for fenced blocks"> 49 | <input id="_highlight" type="checkbox"> highlight 50 | </label> 51 | </div> 52 | <div class="form-group not-strict"> 53 | <input class="form-control _tip" id="langPrefix" type="input" placeholder="language prefix" title="css class language prefix for fenced code blocks"> 54 | </div> 55 | <div class="checkbox"> 56 | <label class="_tip" title="force strict CommonMark mode - output will be equal to reference parser"> 57 | <input id="_strict" type="checkbox"> CommonMark strict 58 | </label> 59 | </div> 60 | </div> 61 | </div> 62 | <div class="container full-height"> 63 | <div class="row full-height"> 64 | <div class="col-xs-6 full-height"> 65 | <div class="demo-control"><a class="source-clear" href="#">clear</a><a id="permalink" href="./" title="Share this snippet as link"><strong>permalink</strong></a></div> 66 | <textarea class="source full-height"><!--SAMPLE--></textarea> 67 | </div> 68 | <section class="col-xs-6 full-height"> 69 | <div class="demo-control"><a href="#" data-result-as="html">html</a><a href="#" data-result-as="src">source</a><a href="#" data-result-as="debug">debug</a></div> 70 | <div class="result-html full-height"></div> 71 | <pre class="hljs result-src full-height"><code class="result-src-content full-height"></code></pre> 72 | <pre class="hljs result-debug full-height"><code class="result-debug-content full-height"></code></pre> 73 | </section> 74 | </div> 75 | </div> 76 | <div class="gh-ribbon"><a href="https://github.com/markdown-it/markdown-it" target="_blank">Fork me on GitHub</a></div> 77 | </body> 78 | </html> 79 | -------------------------------------------------------------------------------- /support/demo_template/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import nodeResolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import terser from '@rollup/plugin-terser' 4 | 5 | const plugins = [ 6 | nodeResolve({ preferBuiltins: true }), 7 | commonjs(), 8 | // Here terser is used only to force ascii output 9 | terser({ 10 | mangle: false, 11 | compress: false, 12 | format: { 13 | comments: 'all', 14 | beautify: true, 15 | ascii_only: true, 16 | indent_level: 2 17 | } 18 | }) 19 | ] 20 | 21 | export default [ 22 | { 23 | input: 'index.mjs', 24 | output: { 25 | file: 'demo/markdown-it.js', 26 | format: 'umd', 27 | name: 'markdownit' 28 | }, 29 | plugins 30 | }, 31 | { 32 | input: 'support/demo_template/index.mjs', 33 | output: { 34 | file: 'demo/index.js', 35 | format: 'iife', 36 | name: 'demo' 37 | }, 38 | plugins 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /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 |  149 |  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: :cry: :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 | ### [\<ins>](https://github.com/markdown-it/markdown-it-ins) 182 | 183 | ++Inserted text++ 184 | 185 | 186 | ### [\<mark>](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 | -------------------------------------------------------------------------------- /support/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import terser from '@rollup/plugin-terser' 3 | import { babel } from '@rollup/plugin-babel' 4 | import { readFileSync } from 'fs' 5 | 6 | const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))) 7 | 8 | const config_umd_full = { 9 | input: 'index.mjs', 10 | output: [ 11 | { 12 | file: `dist/${pkg.name}.js`, 13 | format: 'umd', 14 | name: 'markdownit', 15 | plugins: [ 16 | // Here terser is used only to force ascii output 17 | terser({ 18 | mangle: false, 19 | compress: false, 20 | format: { comments: 'all', beautify: true, ascii_only: true, indent_level: 2 } 21 | }) 22 | ] 23 | }, 24 | { 25 | file: `dist/${pkg.name}.min.js`, 26 | format: 'umd', 27 | name: 'markdownit', 28 | plugins: [ 29 | terser({ 30 | format: { ascii_only: true } 31 | }) 32 | ] 33 | } 34 | ], 35 | plugins: [ 36 | resolve(), 37 | babel({ babelHelpers: 'bundled' }), 38 | { 39 | banner () { 40 | return `/*! ${pkg.name} ${pkg.version} https://github.com/${pkg.repository} @license ${pkg.license} */` 41 | } 42 | } 43 | ] 44 | } 45 | 46 | const config_cjs_no_deps = { 47 | input: 'index.mjs', 48 | output: { 49 | file: 'dist/index.cjs.js', 50 | format: 'cjs' 51 | }, 52 | external: Object.keys(pkg.dependencies), 53 | plugins: [ 54 | resolve(), 55 | babel({ babelHelpers: 'bundled' }) 56 | ] 57 | } 58 | 59 | let config = [ 60 | config_umd_full, 61 | config_cjs_no_deps 62 | ] 63 | 64 | if (process.env.CJS_ONLY) config = [config_cjs_no_deps] 65 | 66 | export default config 67 | -------------------------------------------------------------------------------- /support/specsplit.mjs: -------------------------------------------------------------------------------- 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 | import fs from 'node:fs' 8 | import argparse from 'argparse' 9 | import markdownit from '../index.mjs' 10 | 11 | const cli = new argparse.ArgumentParser({ 12 | add_help: true 13 | }) 14 | 15 | cli.add_argument('type', { 16 | help: 'type of examples to filter', 17 | nargs: '?', 18 | choices: ['good', 'bad'] 19 | }) 20 | 21 | cli.add_argument('-s', '--spec', { 22 | help: 'spec file to read', 23 | default: new URL('../test/fixtures/commonmark/spec.txt', import.meta.url) 24 | }) 25 | 26 | cli.add_argument('-o', '--output', { 27 | help: 'output file, stdout if not set', 28 | default: '-' 29 | }) 30 | 31 | const options = cli.parse_args() 32 | 33 | function normalize (text) { 34 | return text.replace(/<blockquote>\n<\/blockquote>/g, '<blockquote></blockquote>') 35 | } 36 | 37 | function readFile (filename, encoding, callback) { 38 | if (options.file === '-') { 39 | // read from stdin 40 | 41 | const chunks = [] 42 | 43 | process.stdin.on('data', function (chunk) { 44 | chunks.push(chunk) 45 | }) 46 | 47 | process.stdin.on('end', function () { 48 | return callback(null, Buffer.concat(chunks).toString(encoding)) 49 | }) 50 | } else { 51 | fs.readFile(filename, encoding, callback) 52 | } 53 | } 54 | 55 | readFile(options.spec, 'utf8', function (error, input) { 56 | const good = [] 57 | const bad = [] 58 | const markdown = markdownit('commonmark') 59 | 60 | if (error) { 61 | if (error.code === 'ENOENT') { 62 | process.stderr.write('File not found: ' + options.spec) 63 | process.exit(2) 64 | } 65 | 66 | process.stderr.write(error.stack || error.message || String(error)) 67 | process.exit(1) 68 | } 69 | 70 | input = input.replace(/→/g, '\t') 71 | 72 | markdown.parse(input, {}) 73 | .filter(function (token) { 74 | return token.tag === 'code' && 75 | token.info.trim() === 'example' 76 | }) 77 | .forEach(function (token) { 78 | const arr = token.content.split(/^\.\s*?$/m, 2) 79 | const md = arr[0] 80 | const html = arr[1].replace(/^\n/, '') 81 | 82 | const result = { 83 | md, 84 | html, 85 | line: token.map[0], 86 | err: '' 87 | } 88 | 89 | if (markdown.render(md) === normalize(html)) { 90 | good.push(result) 91 | } else { 92 | result.err = markdown.render(md) 93 | bad.push(result) 94 | } 95 | }) 96 | 97 | const out = [] 98 | 99 | if (!options.type) { 100 | out.push(`CM spec stat: passed samples - ${good.length}, failed samples - ${bad.length}`) 101 | } else { 102 | const data = options.type === 'good' ? good : bad 103 | 104 | data.forEach(function (sample) { 105 | out.push( 106 | '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n' + 107 | `src line: ${sample.line}\n\n.\n${sample.md}.\n${sample.html}.\n` 108 | ) 109 | if (sample.err) { 110 | out.push(`error:\n\n${sample.err}\n`) 111 | } 112 | }) 113 | } 114 | 115 | if (options.output !== '-') fs.writeFileSync(options.output, out.join('\n')) 116 | else console.log(out.join('\n')) 117 | 118 | process.exit(0) 119 | }) 120 | -------------------------------------------------------------------------------- /test/cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const md = require('../')() 5 | 6 | describe('CJS', () => { 7 | it('require', () => { 8 | assert.strictEqual(md.render('abc'), '<p>abc</p>\n') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/commonmark.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { relative } from 'node:path' 3 | import { load } from 'markdown-it-testgen' 4 | import markdownit from '../index.mjs' 5 | import { assert } from 'chai' 6 | 7 | function normalize (text) { 8 | return text.replace(/<blockquote>\n<\/blockquote>/g, '<blockquote></blockquote>') 9 | } 10 | 11 | function generate (path, md) { 12 | load(path, function (data) { 13 | data.meta = data.meta || {} 14 | 15 | const desc = data.meta.desc || relative(path, data.file); 16 | 17 | (data.meta.skip ? describe.skip : describe)(desc, function () { 18 | data.fixtures.forEach(function (fixture) { 19 | it(fixture.header ? fixture.header : 'line ' + (fixture.first.range[0] - 1), function () { 20 | assert.strictEqual(md.render(fixture.first.text), normalize(fixture.second.text)) 21 | }) 22 | }) 23 | }) 24 | }) 25 | } 26 | 27 | describe('CommonMark', function () { 28 | const md = markdownit('commonmark') 29 | 30 | generate(fileURLToPath(new URL('fixtures/commonmark/good.txt', import.meta.url)), md) 31 | }) 32 | -------------------------------------------------------------------------------- /test/fixtures/commonmark/bad.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markdown-it/markdown-it/0fe7ccb4b7f30236fb05f623be6924961d296d3d/test/fixtures/commonmark/bad.txt -------------------------------------------------------------------------------- /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 | <p><a href="%25test">foo</a></p> 6 | . 7 | 8 | 9 | Should not throw exception on broken utf-8 sequence in URL [mailformed URI] 10 | . 11 | [foo](%C3) 12 | . 13 | <p><a href="%C3">foo</a></p> 14 | . 15 | 16 | 17 | Should not throw exception on broken utf-16 surrogates sequence in URL [mailformed URI] 18 | . 19 | [foo](�) 20 | . 21 | <p><a href="&#xD800;">foo</a></p> 22 | . 23 | 24 | 25 | Should not hang comments regexp 26 | . 27 | foo <!--- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -> 28 | 29 | foo <!-------------------------------------------------------------------> 30 | . 31 | <p>foo <!— xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -></p> 32 | <p>foo <!-------------------------------------------------------------------></p> 33 | . 34 | 35 | 36 | Should not hang cdata regexp 37 | . 38 | foo <![CDATA[ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ]> 39 | . 40 | <p>foo <![CDATA[ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ]></p> 41 | . 42 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/linkify.txt: -------------------------------------------------------------------------------- 1 | linkify 2 | . 3 | url http://www.youtube.com/watch?v=5Jt5GEr4AYg. 4 | . 5 | <p>url <a href="http://www.youtube.com/watch?v=5Jt5GEr4AYg">http://www.youtube.com/watch?v=5Jt5GEr4AYg</a>.</p> 6 | . 7 | 8 | 9 | don't touch text in links 10 | . 11 | [https://example.com](https://example.com) 12 | . 13 | <p><a href="https://example.com">https://example.com</a></p> 14 | . 15 | 16 | 17 | don't touch text in autolinks 18 | . 19 | <https://example.com> 20 | . 21 | <p><a href="https://example.com">https://example.com</a></p> 22 | . 23 | 24 | 25 | don't touch text in html <a> tags 26 | . 27 | <a href="https://example.com">https://example.com</a> 28 | . 29 | <p><a href="https://example.com">https://example.com</a></p> 30 | . 31 | 32 | 33 | entities inside raw links 34 | . 35 | https://example.com/foo&bar 36 | . 37 | <p><a href="https://example.com/foo&amp;bar">https://example.com/foo&amp;bar</a></p> 38 | . 39 | 40 | 41 | emphasis inside raw links (asterisk, can happen in links with params) 42 | . 43 | https://example.com/foo*bar*baz 44 | . 45 | <p><a href="https://example.com/foo*bar*baz">https://example.com/foo*bar*baz</a></p> 46 | . 47 | 48 | 49 | emphasis inside raw links (underscore) 50 | . 51 | http://example.org/foo._bar_-_baz 52 | . 53 | <p><a href="http://example.org/foo._bar_-_baz">http://example.org/foo._bar_-_baz</a></p> 54 | . 55 | 56 | 57 | backticks inside raw links 58 | . 59 | https://example.com/foo`bar`baz 60 | . 61 | <p><a href="https://example.com/foo%60bar%60baz">https://example.com/foo`bar`baz</a></p> 62 | . 63 | 64 | 65 | links inside raw links 66 | . 67 | https://example.com/foo[123](456)bar 68 | . 69 | <p><a href="https://example.com/foo%5B123%5D(456)bar">https://example.com/foo[123](456)bar</a></p> 70 | . 71 | 72 | 73 | escapes not allowed at the start 74 | . 75 | \https://example.com 76 | . 77 | <p>\https://example.com</p> 78 | . 79 | 80 | 81 | escapes not allowed at comma 82 | . 83 | https\://example.com 84 | . 85 | <p>https://example.com</p> 86 | . 87 | 88 | 89 | escapes not allowed at slashes 90 | . 91 | https:\//aa.org https://bb.org 92 | . 93 | <p>https://aa.org <a href="https://bb.org">https://bb.org</a></p> 94 | . 95 | 96 | 97 | fuzzy link shouldn't match cc.org 98 | . 99 | https:/\/cc.org 100 | . 101 | <p>https://cc.org</p> 102 | . 103 | 104 | 105 | bold links (exclude markup of pairs from link tail) 106 | . 107 | **http://example.com/foobar** 108 | . 109 | <p><strong><a href="http://example.com/foobar">http://example.com/foobar</a></strong></p> 110 | . 111 | 112 | 113 | match links without protocol 114 | . 115 | www.example.org 116 | . 117 | <p><a href="http://www.example.org">www.example.org</a></p> 118 | . 119 | 120 | 121 | emails 122 | . 123 | test@example.com 124 | 125 | mailto:test@example.com 126 | . 127 | <p><a href="mailto:test@example.com">test@example.com</a></p> 128 | <p><a href="mailto:test@example.com">mailto:test@example.com</a></p> 129 | . 130 | 131 | 132 | typorgapher should not break href 133 | . 134 | http://example.com/(c) 135 | . 136 | <p><a href="http://example.com/(c)">http://example.com/(c)</a></p> 137 | . 138 | 139 | 140 | coverage, prefix not valid 141 | . 142 | http:/example.com/ 143 | . 144 | <p>http:/example.com/</p> 145 | . 146 | 147 | 148 | coverage, negative link level 149 | . 150 | </a>[https://example.com](https://example.com) 151 | . 152 | <p></a><a href="https://example.com"><a href="https://example.com">https://example.com</a></a></p> 153 | . 154 | 155 | 156 | emphasis with '*', real link: 157 | . 158 | http://cdecl.ridiculousfish.com/?q=int+%28*f%29+%28float+*%29%3B 159 | . 160 | <p><a href="http://cdecl.ridiculousfish.com/?q=int+%28*f%29+%28float+*%29%3B">http://cdecl.ridiculousfish.com/?q=int+(*f)+(float+*)%3B</a></p> 161 | . 162 | 163 | 164 | emphasis with '_', real link: 165 | . 166 | https://www.sell.fi/sites/default/files/elainlaakarilehti/tieteelliset_artikkelit/kahkonen_t._et_al.canine_pancreatitis-_review.pdf 167 | . 168 | <p><a href="https://www.sell.fi/sites/default/files/elainlaakarilehti/tieteelliset_artikkelit/kahkonen_t._et_al.canine_pancreatitis-_review.pdf">https://www.sell.fi/sites/default/files/elainlaakarilehti/tieteelliset_artikkelit/kahkonen_t._et_al.canine_pancreatitis-_review.pdf</a></p> 169 | . 170 | 171 | regression test, invalid link: 172 | . 173 | i.org[x[x][xx: htt://a.b://a 174 | . 175 | <p><a href="http://i.org">i.org</a>[x[x][xx: htt://a.b://a</p> 176 | . 177 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/normalize.txt: -------------------------------------------------------------------------------- 1 | 2 | Encode link destination, decode text inside it: 3 | 4 | . 5 | <http://example.com/α%CE%B2γ%CE%B4> 6 | . 7 | <p><a href="http://example.com/%CE%B1%CE%B2%CE%B3%CE%B4">http://example.com/αβγδ</a></p> 8 | . 9 | 10 | . 11 | [foo](http://example.com/α%CE%B2γ%CE%B4) 12 | . 13 | <p><a href="http://example.com/%CE%B1%CE%B2%CE%B3%CE%B4">foo</a></p> 14 | . 15 | 16 | 17 | Keep %25 as is because decoding it may break urls, #720 18 | . 19 | <https://www.google.com/search?q=hello%2E%252Ehello> 20 | . 21 | <p><a href="https://www.google.com/search?q=hello%2E%252Ehello">https://www.google.com/search?q=hello.%252Ehello</a></p> 22 | . 23 | 24 | 25 | Should decode punycode: 26 | 27 | . 28 | <http://xn--n3h.net/> 29 | . 30 | <p><a href="http://xn--n3h.net/">http://☃.net/</a></p> 31 | . 32 | 33 | . 34 | <http://☃.net/> 35 | . 36 | <p><a href="http://xn--n3h.net/">http://☃.net/</a></p> 37 | . 38 | 39 | Invalid punycode: 40 | 41 | . 42 | <http://xn--xn.com/> 43 | . 44 | <p><a href="http://xn--xn.com/">http://xn--xn.com/</a></p> 45 | . 46 | 47 | Invalid punycode (non-ascii): 48 | 49 | . 50 | <http://xn--γ.com/> 51 | . 52 | <p><a href="http://xn--xn---emd.com/">http://xn--γ.com/</a></p> 53 | . 54 | 55 | Two slashes should start a domain: 56 | 57 | . 58 | [](//☃.net/) 59 | . 60 | <p><a href="//xn--n3h.net/"></a></p> 61 | . 62 | 63 | Don't encode domains in unknown schemas: 64 | 65 | . 66 | [](skype:γγγ) 67 | . 68 | <p><a href="skype:%CE%B3%CE%B3%CE%B3"></a></p> 69 | . 70 | 71 | Should auto-add protocol to autolinks: 72 | 73 | . 74 | test google.com foo 75 | . 76 | <p>test <a href="http://google.com">google.com</a> foo</p> 77 | . 78 | 79 | Should support IDN in autolinks: 80 | 81 | . 82 | test http://xn--n3h.net/ foo 83 | . 84 | <p>test <a href="http://xn--n3h.net/">http://☃.net/</a> foo</p> 85 | . 86 | 87 | . 88 | test http://☃.net/ foo 89 | . 90 | <p>test <a href="http://xn--n3h.net/">http://☃.net/</a> foo</p> 91 | . 92 | 93 | . 94 | test //xn--n3h.net/ foo 95 | . 96 | <p>test <a href="//xn--n3h.net/">//☃.net/</a> foo</p> 97 | . 98 | 99 | . 100 | test xn--n3h.net foo 101 | . 102 | <p>test <a href="http://xn--n3h.net">☃.net</a> foo</p> 103 | . 104 | 105 | . 106 | test xn--n3h@xn--n3h.net foo 107 | . 108 | <p>test <a href="mailto:xn--n3h@xn--n3h.net">xn--n3h@☃.net</a> foo</p> 109 | . 110 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/proto.txt: -------------------------------------------------------------------------------- 1 | . 2 | [__proto__] 3 | 4 | [__proto__]: blah 5 | . 6 | <p><a href="blah"><strong>proto</strong></a></p> 7 | . 8 | 9 | 10 | . 11 | [hasOwnProperty] 12 | 13 | [hasOwnProperty]: blah 14 | . 15 | <p><a href="blah">hasOwnProperty</a></p> 16 | . 17 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/smartquotes.txt: -------------------------------------------------------------------------------- 1 | Should parse nested quotes: 2 | . 3 | "foo 'bar' baz" 4 | 5 | 'foo 'bar' baz' 6 | . 7 | <p>“foo ‘bar’ baz”</p> 8 | <p>‘foo ‘bar’ baz’</p> 9 | . 10 | 11 | 12 | Should not overlap quotes: 13 | . 14 | 'foo "bar' baz" 15 | . 16 | <p>‘foo "bar’ baz"</p> 17 | . 18 | 19 | 20 | Should match quotes on the same level: 21 | . 22 | "foo *bar* baz" 23 | . 24 | <p>“foo <em>bar</em> baz”</p> 25 | . 26 | 27 | 28 | Should handle adjacent nested quotes: 29 | . 30 | '"double in single"' 31 | 32 | "'single in double'" 33 | . 34 | <p>‘“double in single”’</p> 35 | <p>“‘single in double’”</p> 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 | <p><em>"foo</em> bar"</p> 49 | <p>"foo <em>bar"</em></p> 50 | <p><em>"foo</em> bar <em>baz"</em></p> 51 | . 52 | 53 | Smartquotes should not overlap with other tags: 54 | . 55 | *foo "bar* *baz" quux* 56 | . 57 | <p><em>foo "bar</em> <em>baz" quux</em></p> 58 | . 59 | 60 | 61 | Should try and find matching quote in this case: 62 | . 63 | "foo "bar 'baz" 64 | . 65 | <p>"foo “bar 'baz”</p> 66 | . 67 | 68 | 69 | Should not touch 'inches' in quotes: 70 | . 71 | "Monitor 21"" and "Monitor"" 72 | . 73 | <p>“Monitor 21"” and “Monitor”"</p> 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 | <p>This isn’t and can’t be the best approach to implement this…</p> 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 | <p>users’ stuff</p> 90 | . 91 | 92 | Quotes between punctuation chars: 93 | 94 | . 95 | "(hai)". 96 | . 97 | <p>“(hai)”.</p> 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 | <p>“<em>foo</em> bar”</p> 109 | <p>“foo <em>bar</em>”</p> 110 | <p>“<em>foo bar</em>”</p> 111 | . 112 | 113 | Should treat softbreak as a space: 114 | . 115 | "this" 116 | and "that". 117 | 118 | "this" and 119 | "that". 120 | . 121 | <p>“this” 122 | and “that”.</p> 123 | <p>“this” and 124 | “that”.</p> 125 | . 126 | 127 | Should treat hardbreak as a space: 128 | . 129 | "this"\ 130 | and "that". 131 | 132 | "this" and\ 133 | "that". 134 | . 135 | <p>“this”<br> 136 | and “that”.</p> 137 | <p>“this” and<br> 138 | “that”.</p> 139 | . 140 | 141 | Should allow quotes adjacent to other punctuation characters, #643: 142 | . 143 | The dog---"'man's' best friend" 144 | . 145 | <p>The dog—“‘man’s’ best friend”</p> 146 | . 147 | 148 | Should parse quotes adjacent to code block, #677: 149 | . 150 | "test `code`" 151 | 152 | "`code` test" 153 | . 154 | <p>“test <code>code</code>”</p> 155 | <p>“<code>code</code> test”</p> 156 | . 157 | 158 | Should parse quotes adjacent to inline html, #677: 159 | . 160 | "test <br>" 161 | 162 | "<br> test" 163 | . 164 | <p>“test <br>”</p> 165 | <p>“<br> test”</p> 166 | . 167 | 168 | Should be escapable: 169 | . 170 | "foo" 171 | 172 | \"foo" 173 | 174 | "foo\" 175 | . 176 | <p>“foo”</p> 177 | <p>"foo"</p> 178 | <p>"foo"</p> 179 | . 180 | 181 | Should not replace entities: 182 | . 183 | "foo" 184 | 185 | "foo" 186 | 187 | "foo" 188 | . 189 | <p>"foo"</p> 190 | <p>"foo"</p> 191 | <p>"foo"</p> 192 | . 193 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/strikethrough.txt: -------------------------------------------------------------------------------- 1 | . 2 | ~~Strikeout~~ 3 | . 4 | <p><s>Strikeout</s></p> 5 | . 6 | 7 | . 8 | x ~~~~foo~~ bar~~ 9 | . 10 | <p>x <s><s>foo</s> bar</s></p> 11 | . 12 | 13 | . 14 | x ~~foo ~~bar~~~~ 15 | . 16 | <p>x <s>foo <s>bar</s></s></p> 17 | . 18 | 19 | . 20 | x ~~~~foo~~~~ 21 | . 22 | <p>x <s><s>foo</s></s></p> 23 | . 24 | 25 | . 26 | x ~~a ~~foo~~~~~~~~~~~bar~~ b~~ 27 | 28 | x ~~a ~~foo~~~~~~~~~~~~bar~~ b~~ 29 | . 30 | <p>x <s>a <s>foo</s></s>~~~<s><s>bar</s> b</s></p> 31 | <p>x <s>a <s>foo</s></s>~~~~<s><s>bar</s> b</s></p> 32 | . 33 | 34 | 35 | Strikeouts have the same priority as emphases: 36 | . 37 | **~~test**~~ 38 | 39 | ~~**test~~** 40 | . 41 | <p><strong>~~test</strong>~~</p> 42 | <p><s>**test</s>**</p> 43 | . 44 | 45 | 46 | Strikeouts have the same priority as emphases with respect to links: 47 | . 48 | [~~link]()~~ 49 | 50 | ~~[link~~]() 51 | . 52 | <p><a href="">~~link</a>~~</p> 53 | <p>~~<a href="">link~~</a></p> 54 | . 55 | 56 | 57 | Strikeouts have the same priority as emphases with respect to backticks: 58 | . 59 | ~~`code~~` 60 | 61 | `~~code`~~ 62 | . 63 | <p>~~<code>code~~</code></p> 64 | <p><code>~~code</code>~~</p> 65 | . 66 | 67 | 68 | Nested strikeouts: 69 | . 70 | ~~foo ~~bar~~ baz~~ 71 | 72 | ~~f **o ~~o b~~ a** r~~ 73 | . 74 | <p><s>foo <s>bar</s> baz</s></p> 75 | <p><s>f <strong>o <s>o b</s> a</strong> r</s></p> 76 | . 77 | 78 | 79 | Should not have a whitespace between text and "~~": 80 | . 81 | foo ~~ bar ~~ baz 82 | . 83 | <p>foo ~~ bar ~~ baz</p> 84 | . 85 | 86 | 87 | Should parse strikethrough within link tags: 88 | . 89 | [~~foo~~]() 90 | . 91 | <p><a href=""><s>foo</s></a></p> 92 | . 93 | 94 | 95 | Newline should be considered a whitespace: 96 | . 97 | ~~test 98 | ~~ 99 | 100 | ~~ 101 | test~~ 102 | 103 | ~~ 104 | test 105 | ~~ 106 | . 107 | <p>~~test 108 | ~~</p> 109 | <p>~~ 110 | test~~</p> 111 | <p>~~ 112 | test 113 | ~~</p> 114 | . 115 | 116 | From CommonMark test suite, replacing `**` with our marker: 117 | 118 | . 119 | a~~"foo"~~ 120 | . 121 | <p>a~~“foo”~~</p> 122 | . 123 | 124 | Coverage: single tilde 125 | . 126 | ~a~ 127 | . 128 | <p>~a~</p> 129 | . 130 | 131 | Regression test for #742: 132 | . 133 | -~~~~;~~~~~~ 134 | . 135 | <p>-<s><s>;</s></s>~~</p> 136 | . 137 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/typographer.txt: -------------------------------------------------------------------------------- 1 | . 2 | (bad) 3 | . 4 | <p>(bad)</p> 5 | . 6 | 7 | 8 | copyright 9 | . 10 | (c) (C) 11 | . 12 | <p>© ©</p> 13 | . 14 | 15 | 16 | reserved 17 | . 18 | (r) (R) 19 | . 20 | <p>® ®</p> 21 | . 22 | 23 | 24 | trademark 25 | . 26 | (tm) (TM) 27 | . 28 | <p>™ ™</p> 29 | . 30 | 31 | 32 | plus-minus 33 | . 34 | +-5 35 | . 36 | <p>±5</p> 37 | . 38 | 39 | 40 | ellipsis 41 | . 42 | test.. test... test..... test?..... test!.... 43 | . 44 | <p>test… test… test… test?.. test!..</p> 45 | . 46 | 47 | 48 | dupes 49 | . 50 | !!!!!! ???? ,, 51 | . 52 | <p>!!! ??? ,</p> 53 | . 54 | 55 | copyright should be escapable 56 | . 57 | \(c) 58 | . 59 | <p>(c)</p> 60 | . 61 | 62 | shouldn't replace entities 63 | . 64 | (c) (c) (c) 65 | . 66 | <p>(c) (c) ©</p> 67 | . 68 | 69 | 70 | dashes 71 | . 72 | ---markdownit --- super--- 73 | 74 | markdownit---awesome 75 | 76 | abc ---- 77 | 78 | --markdownit -- super-- 79 | 80 | markdownit--awesome 81 | . 82 | <p>—markdownit — super—</p> 83 | <p>markdownit—awesome</p> 84 | <p>abc ----</p> 85 | <p>–markdownit – super–</p> 86 | <p>markdownit–awesome</p> 87 | . 88 | 89 | dashes should be escapable 90 | . 91 | foo \-- bar 92 | 93 | foo -\- bar 94 | . 95 | <p>foo -- bar</p> 96 | <p>foo -- bar</p> 97 | . 98 | 99 | regression tests for #624 100 | . 101 | 1---2---3 102 | 103 | 1--2--3 104 | 105 | 1 -- -- 3 106 | . 107 | <p>1—2—3</p> 108 | <p>1–2–3</p> 109 | <p>1 – – 3</p> 110 | . 111 | -------------------------------------------------------------------------------- /test/fixtures/markdown-it/xss.txt: -------------------------------------------------------------------------------- 1 | . 2 | [normal link](javascript) 3 | . 4 | <p><a href="javascript">normal link</a></p> 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 | <p>[xss link](javascript:alert(1))</p> 21 | <p>[xss link](JAVASCRIPT:alert(1))</p> 22 | <p>[xss link](vbscript:alert(1))</p> 23 | <p>[xss link](VBSCRIPT:alert(1))</p> 24 | <p>[xss link](file:///123)</p> 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 | <p><a href="%22%3E%3Cscript%3Ealert(%22xss%22)%3C/script%3E">xss link</a></p> 38 | <p>[xss link](Javascript:alert(1))</p> 39 | <p><a href="&#74;avascript:alert(1)">xss link</a></p> 40 | <p><a href="&#74;avascript:alert(1)">xss link</a></p> 41 | . 42 | 43 | . 44 | [xss link](<javascript:alert(1)>) 45 | . 46 | <p>[xss link](<javascript:alert(1)>)</p> 47 | . 48 | 49 | . 50 | [xss link](javascript:alert(1)) 51 | . 52 | <p>[xss link](javascript:alert(1))</p> 53 | . 54 | 55 | 56 | Should not allow data-uri except some whitelisted mimes 57 | . 58 |  59 | . 60 | <p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt=""></p> 61 | . 62 | 63 | . 64 | [xss link](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K) 65 | . 66 | <p>[xss link](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K)</p> 67 | . 68 | 69 | . 70 | [normal link](/javascript:link) 71 | . 72 | <p><a href="/javascript:link">normal link</a></p> 73 | . 74 | 75 | 76 | Image parser use the same code base as link. 77 | . 78 | ) 79 | . 80 | <p>)</p> 81 | . 82 | 83 | 84 | Autolinks 85 | . 86 | <javascript:alert(1)> 87 | 88 | <javascript:alert(1)> 89 | . 90 | <p><javascript:alert(1)></p> 91 | <p><javascript:alert(1)></p> 92 | . 93 | 94 | 95 | Linkifier 96 | . 97 | javascript:alert(1) 98 | 99 | javascript:alert(1) 100 | . 101 | <p>javascript:alert(1)</p> 102 | <p>javascript:alert(1)</p> 103 | . 104 | 105 | 106 | References 107 | . 108 | [test]: javascript:alert(1) 109 | . 110 | <p>[test]: javascript:alert(1)</p> 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 | <pre><code class="js">test1 125 | </code></pre> 126 | <pre><code class="js">test2 127 | </code></pre> 128 | . 129 | -------------------------------------------------------------------------------- /test/markdown-it.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import generate from 'markdown-it-testgen' 3 | import markdownit from '../index.mjs' 4 | 5 | describe('markdown-it', function () { 6 | const md = markdownit({ 7 | html: true, 8 | langPrefix: '', 9 | typographer: true, 10 | linkify: true 11 | }) 12 | 13 | generate(fileURLToPath(new URL('fixtures/markdown-it', import.meta.url)), md) 14 | }) 15 | -------------------------------------------------------------------------------- /test/pathological.json: -------------------------------------------------------------------------------- 1 | { "md5": "80e12450752e4667b3656fa2cd12e9d5" } 2 | -------------------------------------------------------------------------------- /test/pathological.mjs: -------------------------------------------------------------------------------- 1 | import needle from 'needle' 2 | import assert from 'node:assert' 3 | import crypto from 'node:crypto' 4 | import { Worker as JestWorker } from 'jest-worker' 5 | import { readFileSync } from 'fs' 6 | 7 | async function test_pattern (str) { 8 | const worker = new JestWorker( 9 | new URL('./pathological_worker.js', import.meta.url), 10 | { 11 | numWorkers: 1, 12 | enableWorkerThreads: true 13 | } 14 | ) 15 | 16 | let result 17 | const ac = new AbortController() 18 | 19 | try { 20 | result = await Promise.race([ 21 | worker.render(str), 22 | new Promise((resolve, reject) => { 23 | setTimeout(() => reject(new Error('Terminated (timeout exceeded)')), 3000).unref() 24 | }) 25 | ]) 26 | } finally { 27 | ac.abort() 28 | await worker.end() 29 | } 30 | 31 | return result 32 | } 33 | 34 | describe('Pathological sequences speed', () => { 35 | it('Integrity check', async () => { 36 | assert.strictEqual( 37 | await test_pattern('foo'), 38 | '<p>foo</p>\n' 39 | ) 40 | }) 41 | 42 | // Ported from cmark, https://github.com/commonmark/cmark/blob/master/test/pathological_tests.py 43 | describe('Cmark', () => { 44 | it('verify original source crc', async () => { 45 | /* eslint-disable max-len */ 46 | const src = await needle('get', 'https://raw.githubusercontent.com/commonmark/cmark/master/test/pathological_tests.py') 47 | const src_md5 = crypto.createHash('md5').update(src.body).digest('hex') 48 | const tracked_md5 = JSON.parse(readFileSync(new URL('./pathological.json', import.meta.url))).md5 49 | 50 | assert.strictEqual( 51 | src_md5, 52 | tracked_md5, 53 | 'CRC or cmark pathological tests hanged. Verify and update pathological.json' 54 | ) 55 | }) 56 | 57 | it('nested inlines', async () => { 58 | await test_pattern('*'.repeat(60000) + 'a' + '*'.repeat(60000)) 59 | }) 60 | 61 | it('nested strong emph', async () => { 62 | await test_pattern('*a **a '.repeat(5000) + 'b' + ' a** a*'.repeat(5000)) 63 | }) 64 | 65 | it('many emph closers with no openers', async () => { 66 | await test_pattern('a_ '.repeat(30000)) 67 | }) 68 | 69 | it('many emph openers with no closers', async () => { 70 | await test_pattern('_a '.repeat(30000)) 71 | }) 72 | 73 | it('many link closers with no openers', async () => { 74 | await test_pattern('a]'.repeat(10000)) 75 | }) 76 | 77 | it('many link openers with no closers', async () => { 78 | await test_pattern('[a'.repeat(10000)) 79 | }) 80 | 81 | it('mismatched openers and closers', async () => { 82 | await test_pattern('*a_ '.repeat(50000)) 83 | }) 84 | 85 | it('commonmark/cmark#389', async () => { 86 | await test_pattern('*a '.repeat(20000) + '_a*_ '.repeat(20000)) 87 | }) 88 | 89 | it('openers and closers multiple of 3', async () => { 90 | await test_pattern('a**b' + ('c* '.repeat(50000))) 91 | }) 92 | 93 | it('link openers and emph closers', async () => { 94 | await test_pattern('[ a_'.repeat(10000)) 95 | }) 96 | 97 | it('pattern [ (]( repeated', async () => { 98 | await test_pattern('[ (]('.repeat(40000)) 99 | }) 100 | 101 | it('pattern ![[]() repeated', async () => { 102 | await test_pattern('![[]()'.repeat(20000)) 103 | }) 104 | 105 | it('nested brackets', async () => { 106 | await test_pattern('['.repeat(20000) + 'a' + ']'.repeat(20000)) 107 | }) 108 | 109 | it('nested block quotes', async () => { 110 | await test_pattern('> '.repeat(50000) + 'a') 111 | }) 112 | 113 | it('deeply nested lists', async () => { 114 | await test_pattern(Array(1000).fill(0).map(function (_, x) { return ' '.repeat(x) + '* a\n' }).join('')) 115 | }) 116 | 117 | it('U+0000 in input', async () => { 118 | await test_pattern('abc\u0000de\u0000'.repeat(100000)) 119 | }) 120 | 121 | it('backticks', async () => { 122 | await test_pattern(Array(3000).fill(0).map(function (_, x) { return 'e' + '`'.repeat(x) }).join('')) 123 | }) 124 | 125 | it('unclosed links A', async () => { 126 | await test_pattern('[a](<b'.repeat(30000)) 127 | }) 128 | 129 | it('unclosed links B', async () => { 130 | await test_pattern('[a](b'.repeat(30000)) 131 | }) 132 | 133 | it('unclosed <!--', async () => { 134 | await test_pattern('</' + '<!--'.repeat(100000)) 135 | }) 136 | 137 | it('empty lines in deeply nested lists', async () => { 138 | await test_pattern('- '.repeat(30000) + 'x' + '\n'.repeat(30000)) 139 | }) 140 | 141 | it('empty lines in deeply nested lists in blockquote', async () => { 142 | await test_pattern('> ' + '- '.repeat(30000) + 'x\n' + '>\n'.repeat(30000)) 143 | }) 144 | 145 | it('emph in deep blockquote', async () => { 146 | await test_pattern('>'.repeat(100000) + 'a*'.repeat(100000)) 147 | }) 148 | }) 149 | 150 | describe('Markdown-it', () => { 151 | it('emphasis **_* pattern', async () => { 152 | await test_pattern('**_* '.repeat(50000)) 153 | }) 154 | 155 | it('backtick ``\\``\\`` pattern', async () => { 156 | await test_pattern('``\\'.repeat(50000)) 157 | }) 158 | 159 | it('autolinks <<<<...<<> pattern', async () => { 160 | await test_pattern('<'.repeat(400000) + '>') 161 | }) 162 | 163 | it('hardbreak whitespaces pattern', async () => { 164 | await test_pattern('x' + ' '.repeat(150000) + 'x \nx') 165 | }) 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /test/pathological_worker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.render = async (str) => { 4 | return (await import('../index.mjs')).default().render(str) 5 | } 6 | -------------------------------------------------------------------------------- /test/ruler.mjs: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Ruler from '../lib/ruler.mjs' 3 | 4 | describe('Ruler', function () { 5 | it('should replace rule (.at)', function () { 6 | const ruler = new Ruler() 7 | let res = 0 8 | 9 | ruler.push('test', function foo () { res = 1 }) 10 | ruler.at('test', function bar () { res = 2 }) 11 | 12 | const rules = ruler.getRules('') 13 | 14 | assert.strictEqual(rules.length, 1) 15 | rules[0]() 16 | assert.strictEqual(res, 2) 17 | }) 18 | 19 | it('should inject before/after rule', function () { 20 | const ruler = new Ruler() 21 | let res = 0 22 | 23 | ruler.push('test', function foo () { res = 1 }) 24 | ruler.before('test', 'before_test', function fooBefore () { res = -10 }) 25 | ruler.after('test', 'after_test', function fooAfter () { res = 10 }) 26 | 27 | const rules = ruler.getRules('') 28 | 29 | assert.strictEqual(rules.length, 3) 30 | rules[0]() 31 | assert.strictEqual(res, -10) 32 | rules[1]() 33 | assert.strictEqual(res, 1) 34 | rules[2]() 35 | assert.strictEqual(res, 10) 36 | }) 37 | 38 | it('should enable/disable rule', function () { 39 | const ruler = new Ruler() 40 | let rules 41 | 42 | ruler.push('test', function foo () {}) 43 | ruler.push('test2', function bar () {}) 44 | 45 | rules = ruler.getRules('') 46 | assert.strictEqual(rules.length, 2) 47 | 48 | ruler.disable('test') 49 | rules = ruler.getRules('') 50 | assert.strictEqual(rules.length, 1) 51 | ruler.disable('test2') 52 | rules = ruler.getRules('') 53 | assert.strictEqual(rules.length, 0) 54 | 55 | ruler.enable('test') 56 | rules = ruler.getRules('') 57 | assert.strictEqual(rules.length, 1) 58 | ruler.enable('test2') 59 | rules = ruler.getRules('') 60 | assert.strictEqual(rules.length, 2) 61 | }) 62 | 63 | it('should enable/disable multiple rule', function () { 64 | const ruler = new Ruler() 65 | let rules 66 | 67 | ruler.push('test', function foo () {}) 68 | ruler.push('test2', function bar () {}) 69 | 70 | ruler.disable(['test', 'test2']) 71 | rules = ruler.getRules('') 72 | assert.strictEqual(rules.length, 0) 73 | ruler.enable(['test', 'test2']) 74 | rules = ruler.getRules('') 75 | assert.strictEqual(rules.length, 2) 76 | }) 77 | 78 | it('should enable rules by whitelist', function () { 79 | const ruler = new Ruler() 80 | 81 | ruler.push('test', function foo () {}) 82 | ruler.push('test2', function bar () {}) 83 | 84 | ruler.enableOnly('test') 85 | const rules = ruler.getRules('') 86 | assert.strictEqual(rules.length, 1) 87 | }) 88 | 89 | it('should support multiple chains', function () { 90 | const ruler = new Ruler() 91 | let rules 92 | 93 | ruler.push('test', function foo () {}) 94 | ruler.push('test2', function bar () {}, { alt: ['alt1'] }) 95 | ruler.push('test2', function bar () {}, { alt: ['alt1', 'alt2'] }) 96 | 97 | rules = ruler.getRules('') 98 | assert.strictEqual(rules.length, 3) 99 | rules = ruler.getRules('alt1') 100 | assert.strictEqual(rules.length, 2) 101 | rules = ruler.getRules('alt2') 102 | assert.strictEqual(rules.length, 1) 103 | }) 104 | 105 | it('should fail on invalid rule name', function () { 106 | const ruler = new Ruler() 107 | 108 | ruler.push('test', function foo () {}) 109 | 110 | assert.throws(function () { 111 | ruler.at('invalid name', function bar () {}) 112 | }) 113 | assert.throws(function () { 114 | ruler.before('invalid name', function bar () {}) 115 | }) 116 | assert.throws(function () { 117 | ruler.after('invalid name', function bar () {}) 118 | }) 119 | assert.throws(function () { 120 | ruler.enable('invalid name') 121 | }) 122 | assert.throws(function () { 123 | ruler.disable('invalid name') 124 | }) 125 | }) 126 | 127 | it('should not fail on invalid rule name in silent mode', function () { 128 | const ruler = new Ruler() 129 | 130 | ruler.push('test', function foo () {}) 131 | 132 | assert.doesNotThrow(function () { 133 | ruler.enable('invalid name', true) 134 | }) 135 | assert.doesNotThrow(function () { 136 | ruler.enableOnly('invalid name', true) 137 | }) 138 | assert.doesNotThrow(function () { 139 | ruler.disable('invalid name', true) 140 | }) 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /test/token.mjs: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Token from '../lib/token.mjs' 3 | 4 | describe('Token', function () { 5 | it('attr', function () { 6 | const t = new Token('test_token', 'tok', 1) 7 | 8 | assert.strictEqual(t.attrs, null) 9 | assert.equal(t.attrIndex('foo'), -1) 10 | 11 | t.attrPush(['foo', 'bar']) 12 | t.attrPush(['baz', 'bad']) 13 | 14 | assert.equal(t.attrIndex('foo'), 0) 15 | assert.equal(t.attrIndex('baz'), 1) 16 | assert.equal(t.attrIndex('none'), -1) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/utils.mjs: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import * as utils from '../lib/common/utils.mjs' 3 | 4 | describe('Utils', function () { 5 | it('fromCodePoint', function () { 6 | const fromCodePoint = utils.fromCodePoint 7 | 8 | assert.strictEqual(fromCodePoint(0x20), ' ') 9 | assert.strictEqual(fromCodePoint(0x1F601), '😁') 10 | }) 11 | 12 | it('isValidEntityCode', function () { 13 | const isValidEntityCode = utils.isValidEntityCode 14 | 15 | assert.strictEqual(isValidEntityCode(0x20), true) 16 | assert.strictEqual(isValidEntityCode(0xD800), false) 17 | assert.strictEqual(isValidEntityCode(0xFDD0), false) 18 | assert.strictEqual(isValidEntityCode(0x1FFFF), false) 19 | assert.strictEqual(isValidEntityCode(0x1FFFE), false) 20 | assert.strictEqual(isValidEntityCode(0x00), false) 21 | assert.strictEqual(isValidEntityCode(0x0B), false) 22 | assert.strictEqual(isValidEntityCode(0x0E), false) 23 | assert.strictEqual(isValidEntityCode(0x7F), false) 24 | }) 25 | 26 | /* it('replaceEntities', function () { 27 | var replaceEntities = utils.replaceEntities; 28 | 29 | assert.strictEqual(replaceEntities('&'), '&'); 30 | assert.strictEqual(replaceEntities(' '), ' '); 31 | assert.strictEqual(replaceEntities(' '), ' '); 32 | assert.strictEqual(replaceEntities('&&'), '&&'); 33 | 34 | assert.strictEqual(replaceEntities('&am;'), '&am;'); 35 | assert.strictEqual(replaceEntities('�'), '�'); 36 | }); */ 37 | 38 | it('assign', function () { 39 | const assign = utils.assign 40 | 41 | assert.deepEqual(assign({ a: 1 }, null, { b: 2 }), { a: 1, b: 2 }) 42 | assert.throws(function () { 43 | assign({}, 123) 44 | }) 45 | }) 46 | 47 | it('escapeRE', function () { 48 | const escapeRE = utils.escapeRE 49 | 50 | assert.strictEqual(escapeRE(' .?*+^$[]\\(){}|-'), ' \\.\\?\\*\\+\\^\\$\\[\\]\\\\\\(\\)\\{\\}\\|\\-') 51 | }) 52 | 53 | it('isWhiteSpace', function () { 54 | const isWhiteSpace = utils.isWhiteSpace 55 | 56 | assert.strictEqual(isWhiteSpace(0x2000), true) 57 | assert.strictEqual(isWhiteSpace(0x09), true) 58 | 59 | assert.strictEqual(isWhiteSpace(0x30), false) 60 | }) 61 | 62 | it('isMdAsciiPunct', function () { 63 | const isMdAsciiPunct = utils.isMdAsciiPunct 64 | 65 | assert.strictEqual(isMdAsciiPunct(0x30), false) 66 | 67 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').forEach(function (ch) { 68 | assert.strictEqual(isMdAsciiPunct(ch.charCodeAt(0)), true) 69 | }) 70 | }) 71 | 72 | it('unescapeMd', function () { 73 | const unescapeMd = utils.unescapeMd 74 | 75 | assert.strictEqual(unescapeMd('\\foo'), '\\foo') 76 | assert.strictEqual(unescapeMd('foo'), 'foo') 77 | 78 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').forEach(function (ch) { 79 | assert.strictEqual(unescapeMd('\\' + ch), ch) 80 | }) 81 | }) 82 | }) 83 | --------------------------------------------------------------------------------