├── .gitignore ├── test ├── cjs.js ├── test.mjs └── fixtures │ └── deflist.txt ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .eslintrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── rollup.config.mjs └── index.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | *.log 5 | -------------------------------------------------------------------------------- /test/cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* eslint-env mocha */ 3 | 4 | const assert = require('node:assert') 5 | const fn = require('../') 6 | 7 | describe('CJS', () => { 8 | it('require', () => { 9 | assert.ok(typeof fn === 'function') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/test.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import markdownit from 'markdown-it' 3 | import generate from 'markdown-it-testgen' 4 | 5 | import deflist from '../index.mjs' 6 | 7 | /* eslint-env mocha */ 8 | 9 | describe('markdown-it-deflist', function () { 10 | const md = markdownit().use(deflist) 11 | 12 | generate(fileURLToPath(new URL('fixtures/deflist.txt', import.meta.url)), md) 13 | }) 14 | -------------------------------------------------------------------------------- /.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/**', 'index.mjs' ] 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 3.0.0 / 2023-12-05 2 | ------------------ 3 | 4 | - Rewrite to ESM. 5 | - Remove `dist/` from repo (build on package publish). 6 | 7 | 8 | 2.1.0 / 2020-09-10 9 | ------------------ 10 | 11 | - Deps bump. 12 | - Configs cleanup. 13 | - Fixed #8. 14 | 15 | 16 | 2.0.3 / 2017-07-12 17 | ------------------ 18 | 19 | - Fix freezing on blockquotes inside definitions, #5. 20 | 21 | 22 | 2.0.2 / 2017-05-22 23 | ------------------ 24 | 25 | - Missed browser files rebuild in prev release. 26 | 27 | 28 | 2.0.1 / 2016-02-23 29 | ------------------ 30 | 31 | - Fixed tightness, #2. 32 | 33 | 34 | 2.0.0 / 2015-10-05 35 | ------------------ 36 | 37 | - Markdown-it 5.0.0 support. Use 1.x version for 4.x. 38 | 39 | 40 | 1.0.0 / 2015-03-12 41 | ------------------ 42 | 43 | - Markdown-it 4.0.0 support. Use previous version for 2.x-3.x. 44 | 45 | 46 | 0.1.0 / 2015-01-04 47 | ------------------ 48 | 49 | - First release. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-deflist 2 | 3 | [![CI](https://github.com/markdown-it/markdown-it-deflist/actions/workflows/ci.yml/badge.svg)](https://github.com/markdown-it/markdown-it-deflist/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/markdown-it-deflist.svg?style=flat)](https://www.npmjs.org/package/markdown-it-deflist) 5 | [![Coverage Status](https://img.shields.io/coveralls/markdown-it/markdown-it-deflist/master.svg?style=flat)](https://coveralls.io/r/markdown-it/markdown-it-deflist?branch=master) 6 | 7 | > Definition list (`
`) tag plugin for [markdown-it](https://github.com/markdown-it/markdown-it) markdown parser. 8 | 9 | Syntax is based on [pandoc definition lists](http://johnmacfarlane.net/pandoc/README.html#definition-lists). 10 | 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm install markdown-it-deflist --save 16 | ``` 17 | 18 | ## Use 19 | 20 | ```js 21 | var md = require('markdown-it')() 22 | .use(require('markdown-it-deflist')); 23 | 24 | md.render(/*...*/); 25 | ``` 26 | 27 | _Differences in browser._ If you load script directly into the page, without 28 | package system, module will add itself globally as `window.markdownitDeflist`. 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-deflist", 3 | "version": "3.0.0", 4 | "description": "
tag for markdown-it markdown parser.", 5 | "keywords": [ 6 | "markdown-it-plugin", 7 | "markdown-it", 8 | "markdown", 9 | "definition list" 10 | ], 11 | "repository": "markdown-it/markdown-it-deflist.git", 12 | "license": "MIT", 13 | "main": "dist/index.cjs.js", 14 | "module": "index.mjs", 15 | "exports": { 16 | ".": { 17 | "require": "./dist/index.cjs.js", 18 | "import": "./index.mjs" 19 | }, 20 | "./*": { 21 | "require": "./*", 22 | "import": "./*" 23 | } 24 | }, 25 | "files": [ 26 | "index.mjs", 27 | "lib/", 28 | "dist/" 29 | ], 30 | "scripts": { 31 | "lint": "eslint .", 32 | "build": "rollup -c", 33 | "test": "npm run lint && npm run build && c8 --exclude dist --exclude test -r text -r html -r lcov mocha", 34 | "prepublishOnly": "npm run lint && npm run build" 35 | }, 36 | "devDependencies": { 37 | "@rollup/plugin-babel": "^6.0.4", 38 | "@rollup/plugin-node-resolve": "^15.2.3", 39 | "@rollup/plugin-terser": "^0.4.4", 40 | "c8": "^8.0.1", 41 | "eslint": "^8.55.0", 42 | "eslint-config-standard": "^17.1.0", 43 | "markdown-it": "^13.0.2", 44 | "markdown-it-testgen": "^0.1.6", 45 | "mocha": "^10.2.0", 46 | "rollup": "^4.6.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | function globalName (name) { 9 | const parts = name.split('-') 10 | for (let i = 2; i < parts.length; i++) { 11 | parts[i] = parts[i][0].toUpperCase() + parts[i].slice(1) 12 | } 13 | return parts.join('') 14 | } 15 | 16 | const config_umd_full = { 17 | input: 'index.mjs', 18 | output: [ 19 | { 20 | file: `dist/${pkg.name}.js`, 21 | format: 'umd', 22 | name: globalName(pkg.name), 23 | plugins: [ 24 | // Here terser is used only to force ascii output 25 | terser({ 26 | mangle: false, 27 | compress: false, 28 | format: { comments: 'all', beautify: true, ascii_only: true, indent_level: 2 } 29 | }) 30 | ] 31 | }, 32 | { 33 | file: `dist/${pkg.name}.min.js`, 34 | format: 'umd', 35 | name: globalName(pkg.name), 36 | plugins: [ 37 | terser({ 38 | format: { ascii_only: true } 39 | }) 40 | ] 41 | } 42 | ], 43 | plugins: [ 44 | resolve(), 45 | babel({ babelHelpers: 'bundled' }), 46 | { 47 | banner () { 48 | return `/*! ${pkg.name} ${pkg.version} https://github.com/${pkg.repository} @license ${pkg.license} */` 49 | } 50 | } 51 | ] 52 | } 53 | 54 | const config_cjs_no_deps = { 55 | input: 'index.mjs', 56 | output: { 57 | file: 'dist/index.cjs.js', 58 | format: 'cjs' 59 | }, 60 | external: Object.keys(pkg.dependencies || {}), 61 | plugins: [ 62 | resolve(), 63 | babel({ babelHelpers: 'bundled' }) 64 | ] 65 | } 66 | 67 | let config = [ 68 | config_umd_full, 69 | config_cjs_no_deps 70 | ] 71 | 72 | if (process.env.CJS_ONLY) config = [config_cjs_no_deps] 73 | 74 | export default config 75 | -------------------------------------------------------------------------------- /test/fixtures/deflist.txt: -------------------------------------------------------------------------------- 1 | 2 | Pandoc (with slightly changed indents): 3 | 4 | . 5 | Term 1 6 | 7 | : Definition 1 8 | 9 | Term 2 with *inline markup* 10 | 11 | : Definition 2 12 | 13 | { some code, part of Definition 2 } 14 | 15 | Third paragraph of definition 2. 16 | . 17 |
18 |
Term 1
19 |
20 |

Definition 1

21 |
22 |
Term 2 with inline markup
23 |
24 |

Definition 2

25 |
{ some code, part of Definition 2 }
 26 | 
27 |

Third paragraph of definition 2.

28 |
29 |
30 | . 31 | 32 | Pandoc again: 33 | 34 | . 35 | Term 1 36 | 37 | : Definition 38 | with lazy continuation. 39 | 40 | Second paragraph of the definition. 41 | . 42 |
43 |
Term 1
44 |
45 |

Definition 46 | with lazy continuation.

47 |

Second paragraph of the definition.

48 |
49 |
50 | . 51 | 52 | Well, I might just copy-paste the third one while I'm at it: 53 | 54 | . 55 | Term 1 56 | ~ Definition 1 57 | 58 | Term 2 59 | ~ Definition 2a 60 | ~ Definition 2b 61 | . 62 |
63 |
Term 1
64 |
Definition 1
65 |
Term 2
66 |
Definition 2a
67 |
Definition 2b
68 |
69 | . 70 | 71 | Now, with our custom ones. Spaces after a colon: 72 | 73 | . 74 | Term 1 75 | : paragraph 76 | 77 | Term 2 78 | : code block 79 | . 80 |
81 |
Term 1
82 |
paragraph
83 |
Term 2
84 |
85 |
code block
 86 | 
87 |
88 |
89 | . 90 | 91 | There should be something after a colon by the way: 92 | 93 | . 94 | Non-term 1 95 | : 96 | 97 | Non-term 2 98 | : 99 | . 100 |

Non-term 1 101 | :

102 |

Non-term 2 103 | :

104 | . 105 | 106 | 107 | List is tight iff all dts are tight: 108 | . 109 | Term 1 110 | : foo 111 | : bar 112 | 113 | Term 2 114 | : foo 115 | 116 | : bar 117 | . 118 |
119 |
Term 1
120 |
121 |

foo

122 |
123 |
124 |

bar

125 |
126 |
Term 2
127 |
128 |

foo

129 |
130 |
131 |

bar

132 |
133 |
134 | . 135 | 136 | 137 | Regression test (first paragraphs shouldn't be tight): 138 | . 139 | Term 1 140 | : foo 141 | 142 | bar 143 | Term 2 144 | : foo 145 | . 146 |
147 |
Term 1
148 |
149 |

foo

150 |

bar 151 | Term 2

152 |
153 |
154 |

foo

155 |
156 |
157 | . 158 | 159 | Definition lists should be second last in the queue. Exemplī grātiā, this isn't a valid one: 160 | 161 | . 162 | # test 163 | : just a paragraph with a colon 164 | . 165 |

test

166 |

: just a paragraph with a colon

167 | . 168 | 169 | Nested definition lists: 170 | 171 | . 172 | test 173 | : foo 174 | : bar 175 | : baz 176 | : bar 177 | : foo 178 | . 179 |
180 |
test
181 |
182 |
183 |
foo
184 |
185 |
186 |
bar
187 |
baz
188 |
189 |
190 |
bar
191 |
192 |
193 |
foo
194 |
195 | . 196 | 197 | 198 | Regression test, tabs 199 | . 200 | Term 1 201 | : code block 202 | . 203 |
204 |
Term 1
205 |
206 |
code block
207 | 
208 |
209 |
210 | . 211 | 212 | 213 | Regression test (blockquote inside deflist) 214 | . 215 | foo 216 | : > bar 217 | : baz 218 | . 219 |
220 |
foo
221 |
222 |
223 |

bar

224 |
225 |
226 |
baz
227 |
228 | . 229 | 230 | 231 | Coverage, 1 blank line 232 | . 233 | test 234 | 235 | . 236 |

test

237 | . 238 | 239 | 240 | Coverage, 2 blank lines 241 | . 242 | test 243 | 244 | 245 | . 246 |

test

247 | . 248 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | // Process definition lists 2 | // 3 | export default function deflist_plugin (md) { 4 | const isSpace = md.utils.isSpace 5 | 6 | // Search `[:~][\n ]`, returns next pos after marker on success 7 | // or -1 on fail. 8 | function skipMarker (state, line) { 9 | let start = state.bMarks[line] + state.tShift[line] 10 | const max = state.eMarks[line] 11 | 12 | if (start >= max) { return -1 } 13 | 14 | // Check bullet 15 | const marker = state.src.charCodeAt(start++) 16 | if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 } 17 | 18 | const pos = state.skipSpaces(start) 19 | 20 | // require space after ":" 21 | if (start === pos) { return -1 } 22 | 23 | // no empty definitions, e.g. " : " 24 | if (pos >= max) { return -1 } 25 | 26 | return start 27 | } 28 | 29 | function markTightParagraphs (state, idx) { 30 | const level = state.level + 2 31 | 32 | for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) { 33 | if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') { 34 | state.tokens[i + 2].hidden = true 35 | state.tokens[i].hidden = true 36 | i += 2 37 | } 38 | } 39 | } 40 | 41 | function deflist (state, startLine, endLine, silent) { 42 | if (silent) { 43 | // quirk: validation mode validates a dd block only, not a whole deflist 44 | if (state.ddIndent < 0) { return false } 45 | return skipMarker(state, startLine) >= 0 46 | } 47 | 48 | let nextLine = startLine + 1 49 | if (nextLine >= endLine) { return false } 50 | 51 | if (state.isEmpty(nextLine)) { 52 | nextLine++ 53 | if (nextLine >= endLine) { return false } 54 | } 55 | 56 | if (state.sCount[nextLine] < state.blkIndent) { return false } 57 | let contentStart = skipMarker(state, nextLine) 58 | if (contentStart < 0) { return false } 59 | 60 | // Start list 61 | const listTokIdx = state.tokens.length 62 | let tight = true 63 | 64 | const token_dl_o = state.push('dl_open', 'dl', 1) 65 | const listLines = [startLine, 0] 66 | token_dl_o.map = listLines 67 | 68 | // 69 | // Iterate list items 70 | // 71 | 72 | let dtLine = startLine 73 | let ddLine = nextLine 74 | 75 | // One definition list can contain multiple DTs, 76 | // and one DT can be followed by multiple DDs. 77 | // 78 | // Thus, there is two loops here, and label is 79 | // needed to break out of the second one 80 | // 81 | /* eslint no-labels:0,block-scoped-var:0 */ 82 | OUTER: 83 | for (;;) { 84 | let prevEmptyEnd = false 85 | 86 | const token_dt_o = state.push('dt_open', 'dt', 1) 87 | token_dt_o.map = [dtLine, dtLine] 88 | 89 | const token_i = state.push('inline', '', 0) 90 | token_i.map = [dtLine, dtLine] 91 | token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim() 92 | token_i.children = [] 93 | 94 | state.push('dt_close', 'dt', -1) 95 | 96 | for (;;) { 97 | const token_dd_o = state.push('dd_open', 'dd', 1) 98 | const itemLines = [nextLine, 0] 99 | token_dd_o.map = itemLines 100 | 101 | let pos = contentStart 102 | const max = state.eMarks[ddLine] 103 | let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]) 104 | 105 | while (pos < max) { 106 | const ch = state.src.charCodeAt(pos) 107 | 108 | if (isSpace(ch)) { 109 | if (ch === 0x09) { 110 | offset += 4 - offset % 4 111 | } else { 112 | offset++ 113 | } 114 | } else { 115 | break 116 | } 117 | 118 | pos++ 119 | } 120 | 121 | contentStart = pos 122 | 123 | const oldTight = state.tight 124 | const oldDDIndent = state.ddIndent 125 | const oldIndent = state.blkIndent 126 | const oldTShift = state.tShift[ddLine] 127 | const oldSCount = state.sCount[ddLine] 128 | const oldParentType = state.parentType 129 | state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2 130 | state.tShift[ddLine] = contentStart - state.bMarks[ddLine] 131 | state.sCount[ddLine] = offset 132 | state.tight = true 133 | state.parentType = 'deflist' 134 | 135 | state.md.block.tokenize(state, ddLine, endLine, true) 136 | 137 | // If any of list item is tight, mark list as tight 138 | if (!state.tight || prevEmptyEnd) { 139 | tight = false 140 | } 141 | // Item become loose if finish with empty line, 142 | // but we should filter last element, because it means list finish 143 | prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1) 144 | 145 | state.tShift[ddLine] = oldTShift 146 | state.sCount[ddLine] = oldSCount 147 | state.tight = oldTight 148 | state.parentType = oldParentType 149 | state.blkIndent = oldIndent 150 | state.ddIndent = oldDDIndent 151 | 152 | state.push('dd_close', 'dd', -1) 153 | 154 | itemLines[1] = nextLine = state.line 155 | 156 | if (nextLine >= endLine) { break OUTER } 157 | 158 | if (state.sCount[nextLine] < state.blkIndent) { break OUTER } 159 | contentStart = skipMarker(state, nextLine) 160 | if (contentStart < 0) { break } 161 | 162 | ddLine = nextLine 163 | 164 | // go to the next loop iteration: 165 | // insert DD tag and repeat checking 166 | } 167 | 168 | if (nextLine >= endLine) { break } 169 | dtLine = nextLine 170 | 171 | if (state.isEmpty(dtLine)) { break } 172 | if (state.sCount[dtLine] < state.blkIndent) { break } 173 | 174 | ddLine = dtLine + 1 175 | if (ddLine >= endLine) { break } 176 | if (state.isEmpty(ddLine)) { ddLine++ } 177 | if (ddLine >= endLine) { break } 178 | 179 | if (state.sCount[ddLine] < state.blkIndent) { break } 180 | contentStart = skipMarker(state, ddLine) 181 | if (contentStart < 0) { break } 182 | 183 | // go to the next loop iteration: 184 | // insert DT and DD tags and repeat checking 185 | } 186 | 187 | // Finilize list 188 | state.push('dl_close', 'dl', -1) 189 | 190 | listLines[1] = nextLine 191 | 192 | state.line = nextLine 193 | 194 | // mark paragraphs tight if needed 195 | if (tight) { 196 | markTightParagraphs(state, listTokIdx) 197 | } 198 | 199 | return true 200 | } 201 | 202 | md.block.ruler.before('paragraph', 'deflist', deflist, { alt: ['paragraph', 'reference', 'blockquote'] }) 203 | }; 204 | --------------------------------------------------------------------------------