├── .gitignore ├── test ├── cjs.js ├── test.mjs └── fixtures │ └── abbr.txt ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .eslintrc.yml ├── CHANGELOG.md ├── LICENSE ├── package.json ├── README.md ├── rollup.config.mjs └── index.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /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 abbr from '../index.mjs' 6 | 7 | describe('markdown-it-abbr', function () { 8 | const md = markdownit({ linkify: true }).use(abbr) 9 | 10 | generate(fileURLToPath(new URL('fixtures/abbr.txt', import.meta.url)), md) 11 | }) 12 | -------------------------------------------------------------------------------- /.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 | 2.0.0 / 2023-12-03 2 | ------------------ 3 | 4 | - Rewrite to ESM. 5 | - Remove `dist/` from repo (build on package publish). 6 | 7 | 8 | 1.0.4 / 2016-08-31 9 | ------------------ 10 | 11 | - Fixed infinite loop caused by empty abbreviations like `*[]: foo`. Empty abbreviations are no longer parsed. 12 | 13 | 14 | 1.0.3 / 2016-01-05 15 | ------------------ 16 | 17 | - Fixed uc.micro use. 18 | 19 | 20 | 1.0.2 / 2015-12-31 21 | ------------------ 22 | 23 | - maintenance, bower files rebuild 24 | 25 | 26 | 1.0.1 / 2015-12-31 27 | ------------------ 28 | 29 | - Resolved collision with `linkify-it`. 30 | - Extended list of start/stop punctuation characters to unicode ones. 31 | 32 | 33 | 1.0.0 / 2015-03-12 34 | ------------------ 35 | 36 | - Markdown-it 4.0.0 support. Use previous version for 2.x-3.x. 37 | 38 | 39 | 0.1.0 / 2015-01-04 40 | ------------------ 41 | 42 | - First release. 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-abbr", 3 | "version": "2.0.0", 4 | "description": " tag for markdown-it markdown parser.", 5 | "keywords": [ 6 | "markdown-it-plugin", 7 | "markdown-it", 8 | "markdown", 9 | "abbreviation", 10 | "abbr" 11 | ], 12 | "repository": "markdown-it/markdown-it-abbr", 13 | "license": "MIT", 14 | "main": "dist/index.cjs.js", 15 | "module": "index.mjs", 16 | "exports": { 17 | ".": { 18 | "require": "./dist/index.cjs.js", 19 | "import": "./index.mjs" 20 | }, 21 | "./*": { 22 | "require": "./*", 23 | "import": "./*" 24 | } 25 | }, 26 | "files": [ 27 | "index.mjs", 28 | "lib/", 29 | "dist/" 30 | ], 31 | "scripts": { 32 | "lint": "eslint .", 33 | "build": "rollup -c", 34 | "test": "npm run lint && npm run build && c8 --exclude dist --exclude test -r text -r html -r lcov mocha", 35 | "prepublishOnly": "npm run lint && npm run build" 36 | }, 37 | "devDependencies": { 38 | "@rollup/plugin-babel": "^6.0.4", 39 | "@rollup/plugin-node-resolve": "^15.2.3", 40 | "@rollup/plugin-terser": "^0.4.4", 41 | "c8": "^8.0.1", 42 | "eslint": "^8.55.0", 43 | "eslint-config-standard": "^17.1.0", 44 | "markdown-it": "^13.0.2", 45 | "markdown-it-testgen": "^0.1.6", 46 | "mocha": "^10.2.0", 47 | "rollup": "^4.6.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-abbr 2 | 3 | [![CI](https://github.com/markdown-it/markdown-it-abbr/actions/workflows/ci.yml/badge.svg)](https://github.com/markdown-it/markdown-it-abbr/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/markdown-it-abbr.svg?style=flat)](https://www.npmjs.org/package/markdown-it-abbr) 5 | [![Coverage Status](https://img.shields.io/coveralls/markdown-it/markdown-it-abbr/master.svg?style=flat)](https://coveralls.io/r/markdown-it/markdown-it-abbr?branch=master) 6 | 7 | > Abbreviation (``) tag plugin for [markdown-it](https://github.com/markdown-it/markdown-it) markdown parser. 8 | 9 | __v1.+ requires `markdown-it` v4.+, see changelog.__ 10 | 11 | Markup is based on [php markdown extra](https://michelf.ca/projects/php-markdown/extra/#abbr) definition, but without multiline support. 12 | 13 | Markdown: 14 | 15 | ``` 16 | *[HTML]: Hyper Text Markup Language 17 | *[W3C]: World Wide Web Consortium 18 | The HTML specification 19 | is maintained by the W3C. 20 | ``` 21 | 22 | HTML: 23 | 24 | ```html 25 |

The HTML specification 26 | is maintained by the W3C.

27 | ``` 28 | 29 | ## Install 30 | 31 | node.js, browser: 32 | 33 | ```bash 34 | npm install markdown-it-abbr --save 35 | bower install markdown-it-abbr --save 36 | ``` 37 | 38 | ## Use 39 | 40 | ```js 41 | var md = require('markdown-it')() 42 | .use(require('markdown-it-abbr')); 43 | 44 | md.render(/*...*/) // see example above 45 | ``` 46 | 47 | _Differences in browser._ If you load script directly into the page, without 48 | package system, module will add itself globally as `window.markdownitAbbr`. 49 | 50 | 51 | ## License 52 | 53 | [MIT](https://github.com/markdown-it/markdown-it-abbr/blob/master/LICENSE) 54 | -------------------------------------------------------------------------------- /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/abbr.txt: -------------------------------------------------------------------------------- 1 | An example from php markdown readme: 2 | . 3 | *[HTML]: Hyper Text Markup Language 4 | *[W3C]: World Wide Web Consortium 5 | The HTML specification 6 | is maintained by the W3C. 7 | . 8 |

The HTML specification 9 | is maintained by the W3C.

10 | . 11 | 12 | 13 | No empty abbreviations: 14 | . 15 | *[foo]: 16 | foo 17 | . 18 |

*[foo]: 19 | foo

20 | . 21 | 22 | 23 | Intersecting abbreviations (first should match): 24 | . 25 | *[Bar Foo]: 123 26 | *[Foo Bar]: 456 27 | 28 | Foo Bar Foo 29 | 30 | Bar Foo Bar 31 | . 32 |

Foo Bar Foo

33 |

Bar Foo Bar

34 | . 35 | 36 | 37 | Don't bother with nested abbreviations (yet?): 38 | . 39 | *[JS]: javascript 40 | *[HTTP]: hyper text blah blah 41 | *[JS HTTP]: is awesome 42 | JS HTTP is a collection of low-level javascript HTTP-related modules 43 | . 44 |

JS HTTP is a collection of low-level javascript HTTP-related modules

45 | . 46 | 47 | 48 | Mixing up abbreviations and references: 49 | . 50 | *[foo]: 123 51 | [bar]: 456 52 | *[baz]: 789 53 | [quux]: 012 54 | and a paragraph continuation 55 | 56 | foo [bar] baz [quux] 57 | . 58 |

and a paragraph continuation

59 |

foo bar baz quux

60 | . 61 | 62 | 63 | Don't match the middle of the string: 64 | . 65 | *[foo]: blah 66 | *[bar]: blah 67 | foobar 68 | . 69 |

foobar

70 | . 71 | 72 | 73 | Prefer earlier abbr definitions 74 | . 75 | foo 76 | 77 | *[foo]: bar 78 | *[foo]: baz 79 | . 80 |

foo

81 | . 82 | 83 | 84 | Interaction with linkifier: 85 | . 86 | http://example.com/foo/ 87 | 88 | *[foo]: something 89 | . 90 |

http://example.com/foo/

91 | . 92 | 93 | 94 | Punctuation as a part of abbr 95 | . 96 | "foo" "bar" 97 | 98 | *["foo"]: 123 99 | *["bar"]: 456 100 | . 101 |

"foo" "bar"

102 | . 103 | 104 | 105 | Trailing spaces inside abbreviation 106 | . 107 | *[ test ]: foo bar 108 | 109 | test test test 110 | test test test 111 | . 112 |

test test test 113 | test test test

114 | . 115 | 116 | 117 | Abbreviation that consists of only spaces 118 | . 119 | *[ ]: foo bar 120 | 121 | test test test 122 | test test test 123 | test test test 124 | . 125 |

test test test 126 | test test test 127 | test test test

128 | . 129 | 130 | 131 | Empty abbreviations should not be processed as such 132 | . 133 | *[]: test 134 | 135 | (foo bar) 136 | . 137 |

*[]: test

138 |

(foo bar)

139 | . 140 | 141 | 142 | Security 143 | . 144 | *[hasOwnProperty]: blah 145 | 146 | hasOwnProperty 147 | . 148 |

hasOwnProperty

149 | . 150 | 151 | 152 | Coverage 1 153 | . 154 | *[ 155 | 156 | *[test 157 | 158 | * 159 | . 160 |

*[

161 |

*[test

162 |

*<test>

163 | . 164 | 165 | 166 | Coverage 2 167 | . 168 | [] \[\] 169 | 170 | *[[]]: test 171 | . 172 |

[] []

173 |

*[[]]: test

174 | . 175 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | // Enclose abbreviations in tags 2 | // 3 | export default function abbr_plugin (md) { 4 | const escapeRE = md.utils.escapeRE 5 | const arrayReplaceAt = md.utils.arrayReplaceAt 6 | 7 | // ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on; 8 | // you can check character classes here: 9 | // http://www.unicode.org/Public/UNIDATA/UnicodeData.txt 10 | const OTHER_CHARS = ' \r\n$+<=>^`|~' 11 | 12 | const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source 13 | const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source 14 | 15 | function abbr_def (state, startLine, endLine, silent) { 16 | let labelEnd 17 | let pos = state.bMarks[startLine] + state.tShift[startLine] 18 | const max = state.eMarks[startLine] 19 | 20 | if (pos + 2 >= max) { return false } 21 | 22 | if (state.src.charCodeAt(pos++) !== 0x2A/* * */) { return false } 23 | if (state.src.charCodeAt(pos++) !== 0x5B/* [ */) { return false } 24 | 25 | const labelStart = pos 26 | 27 | for (; pos < max; pos++) { 28 | const ch = state.src.charCodeAt(pos) 29 | if (ch === 0x5B /* [ */) { 30 | return false 31 | } else if (ch === 0x5D /* ] */) { 32 | labelEnd = pos 33 | break 34 | } else if (ch === 0x5C /* \ */) { 35 | pos++ 36 | } 37 | } 38 | 39 | if (labelEnd < 0 || state.src.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { 40 | return false 41 | } 42 | 43 | if (silent) { return true } 44 | 45 | const label = state.src.slice(labelStart, labelEnd).replace(/\\(.)/g, '$1') 46 | const title = state.src.slice(labelEnd + 2, max).trim() 47 | if (label.length === 0) { return false } 48 | if (title.length === 0) { return false } 49 | if (!state.env.abbreviations) { state.env.abbreviations = {} } 50 | // prepend ':' to avoid conflict with Object.prototype members 51 | if (typeof state.env.abbreviations[':' + label] === 'undefined') { 52 | state.env.abbreviations[':' + label] = title 53 | } 54 | 55 | state.line = startLine + 1 56 | return true 57 | } 58 | 59 | function abbr_replace (state) { 60 | const blockTokens = state.tokens 61 | 62 | if (!state.env.abbreviations) { return } 63 | 64 | const regSimple = new RegExp('(?:' + 65 | Object.keys(state.env.abbreviations).map(function (x) { 66 | return x.substr(1) 67 | }).sort(function (a, b) { 68 | return b.length - a.length 69 | }).map(escapeRE).join('|') + 70 | ')') 71 | 72 | const regText = '(^|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE + 73 | '|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' + 74 | '(' + Object.keys(state.env.abbreviations).map(function (x) { 75 | return x.substr(1) 76 | }).sort(function (a, b) { 77 | return b.length - a.length 78 | }).map(escapeRE).join('|') + ')' + 79 | '($|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE + 80 | '|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' 81 | 82 | const reg = new RegExp(regText, 'g') 83 | 84 | for (let j = 0, l = blockTokens.length; j < l; j++) { 85 | if (blockTokens[j].type !== 'inline') { continue } 86 | let tokens = blockTokens[j].children 87 | 88 | // We scan from the end, to keep position when new tags added. 89 | for (let i = tokens.length - 1; i >= 0; i--) { 90 | const currentToken = tokens[i] 91 | if (currentToken.type !== 'text') { continue } 92 | 93 | let pos = 0 94 | const text = currentToken.content 95 | reg.lastIndex = 0 96 | const nodes = [] 97 | 98 | // fast regexp run to determine whether there are any abbreviated words 99 | // in the current token 100 | if (!regSimple.test(text)) { continue } 101 | 102 | let m 103 | 104 | while ((m = reg.exec(text))) { 105 | if (m.index > 0 || m[1].length > 0) { 106 | const token = new state.Token('text', '', 0) 107 | token.content = text.slice(pos, m.index + m[1].length) 108 | nodes.push(token) 109 | } 110 | 111 | const token_o = new state.Token('abbr_open', 'abbr', 1) 112 | token_o.attrs = [['title', state.env.abbreviations[':' + m[2]]]] 113 | nodes.push(token_o) 114 | 115 | const token_t = new state.Token('text', '', 0) 116 | token_t.content = m[2] 117 | nodes.push(token_t) 118 | 119 | const token_c = new state.Token('abbr_close', 'abbr', -1) 120 | nodes.push(token_c) 121 | 122 | reg.lastIndex -= m[3].length 123 | pos = reg.lastIndex 124 | } 125 | 126 | if (!nodes.length) { continue } 127 | 128 | if (pos < text.length) { 129 | const token = new state.Token('text', '', 0) 130 | token.content = text.slice(pos) 131 | nodes.push(token) 132 | } 133 | 134 | // replace current node 135 | blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes) 136 | } 137 | } 138 | } 139 | 140 | md.block.ruler.before('reference', 'abbr_def', abbr_def, { alt: ['paragraph', 'reference'] }) 141 | 142 | md.core.ruler.after('linkify', 'abbr_replace', abbr_replace) 143 | }; 144 | --------------------------------------------------------------------------------