├── .gitignore ├── test ├── cjs.js ├── default.mjs ├── misc.mjs ├── api.mjs └── fixtures │ └── default.txt ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .eslintrc.yml ├── CHANGELOG.md ├── LICENSE ├── package.json ├── rollup.config.mjs ├── README.md └── 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/default.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import markdownit from 'markdown-it' 3 | import generate from 'markdown-it-testgen' 4 | 5 | import container from '../index.mjs' 6 | 7 | /* eslint-env mocha */ 8 | 9 | describe('default container', function () { 10 | const md = markdownit().use(container, 'name') 11 | 12 | generate(fileURLToPath(new URL('fixtures/default.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 4.0.0 / 2023-12-05 2 | ------------------ 3 | 4 | - Rewrite to ESM. 5 | - Remove `dist/` from repo (build on package publish). 6 | 7 | 8 | 3.0.0 / 2020-06-02 9 | ------------------ 10 | 11 | - Don't assume there are no classes, #26. 12 | - Added bordering markup to validator params. 13 | - Fix tests to work with latest `markdown-it`. 14 | - Dev deps bump. 15 | - Reorganize configs. 16 | 17 | 18 | 2.0.0 / 2015-10-05 19 | ------------------ 20 | 21 | - Markdown-it 5.0.0 support. Use 1.x version for 4.x. 22 | 23 | 24 | 1.0.0 / 2015-03-13 25 | ------------------ 26 | 27 | - First release. 28 | -------------------------------------------------------------------------------- /test/misc.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import markdownit from 'markdown-it' 3 | 4 | import container from '../index.mjs' 5 | 6 | describe('coverage', function () { 7 | it('marker coverage', function () { 8 | const tok = markdownit() 9 | .use(container, 'fox', { 10 | marker: 'foo', 11 | validate: function (p) { assert.equal(p, 'fox'); return 1 } 12 | }) 13 | .parse('foofoofoofox\ncontent\nfoofoofoofoo\n') 14 | 15 | assert.equal(tok[0].markup, 'foofoofoo') 16 | assert.equal(tok[0].info, 'fox') 17 | assert.equal(tok[4].markup, 'foofoofoofoo') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 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-container", 3 | "version": "4.0.0", 4 | "description": "Plugin to create block-level custom containers for markdown-it markdown parser", 5 | "repository": "markdown-it/markdown-it-container", 6 | "license": "MIT", 7 | "keywords": [ 8 | "markdown-it-plugin", 9 | "markdown-it", 10 | "markdown" 11 | ], 12 | "main": "dist/index.cjs.js", 13 | "module": "index.mjs", 14 | "exports": { 15 | ".": { 16 | "require": "./dist/index.cjs.js", 17 | "import": "./index.mjs" 18 | }, 19 | "./*": { 20 | "require": "./*", 21 | "import": "./*" 22 | } 23 | }, 24 | "files": [ 25 | "index.mjs", 26 | "lib/", 27 | "dist/" 28 | ], 29 | "scripts": { 30 | "lint": "eslint .", 31 | "build": "rollup -c", 32 | "test": "npm run lint && npm run build && c8 --exclude dist --exclude test -r text -r html -r lcov mocha", 33 | "prepublishOnly": "npm run lint && npm run build" 34 | }, 35 | "devDependencies": { 36 | "@rollup/plugin-babel": "^6.0.4", 37 | "@rollup/plugin-node-resolve": "^15.2.3", 38 | "@rollup/plugin-terser": "^0.4.4", 39 | "c8": "^8.0.1", 40 | "eslint": "^8.55.0", 41 | "eslint-config-standard": "^17.1.0", 42 | "markdown-it": "^13.0.2", 43 | "markdown-it-testgen": "^0.1.6", 44 | "mocha": "^10.2.0", 45 | "rollup": "^4.6.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-container 2 | 3 | [![CI](https://github.com/markdown-it/markdown-it-container/actions/workflows/ci.yml/badge.svg)](https://github.com/markdown-it/markdown-it-container/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/markdown-it-container.svg?style=flat)](https://www.npmjs.org/package/markdown-it-container) 5 | [![Coverage Status](https://img.shields.io/coveralls/markdown-it/markdown-it-container/master.svg?style=flat)](https://coveralls.io/r/markdown-it/markdown-it-container?branch=master) 6 | 7 | > Plugin for creating block-level custom containers for [markdown-it](https://github.com/markdown-it/markdown-it) markdown parser. 8 | 9 | __v2.+ requires `markdown-it` v5.+, see changelog.__ 10 | 11 | With this plugin you can create block containers like: 12 | 13 | ``` 14 | ::: warning 15 | *here be dragons* 16 | ::: 17 | ``` 18 | 19 | .... and specify how they should be rendered. If no renderer defined, `
` with 20 | container name class will be created: 21 | 22 | ```html 23 |
24 | here be dragons 25 |
26 | ``` 27 | 28 | Markup is the same as for [fenced code blocks](http://spec.commonmark.org/0.18/#fenced-code-blocks). 29 | Difference is, that marker use another character and content is rendered as markdown markup. 30 | 31 | 32 | ## Installation 33 | 34 | node.js, browser: 35 | 36 | ```bash 37 | $ npm install markdown-it-container --save 38 | $ bower install markdown-it-container --save 39 | ``` 40 | 41 | 42 | ## API 43 | 44 | ```js 45 | var md = require('markdown-it')() 46 | .use(require('markdown-it-container'), name [, options]); 47 | ``` 48 | 49 | Params: 50 | 51 | - __name__ - container name (mandatory) 52 | - __options:__ 53 | - __validate__ - optional, function to validate tail after opening marker, should 54 | return `true` on success. 55 | - __render__ - optional, renderer function for opening/closing tokens. 56 | - __marker__ - optional (`:`), character to use in delimiter. 57 | 58 | 59 | ## Example 60 | 61 | ```js 62 | var md = require('markdown-it')(); 63 | 64 | md.use(require('markdown-it-container'), 'spoiler', { 65 | 66 | validate: function(params) { 67 | return params.trim().match(/^spoiler\s+(.*)$/); 68 | }, 69 | 70 | render: function (tokens, idx) { 71 | var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/); 72 | 73 | if (tokens[idx].nesting === 1) { 74 | // opening tag 75 | return '
' + md.utils.escapeHtml(m[1]) + '\n'; 76 | 77 | } else { 78 | // closing tag 79 | return '
\n'; 80 | } 81 | } 82 | }); 83 | 84 | console.log(md.render('::: spoiler click me\n*content*\n:::\n')); 85 | 86 | // Output: 87 | // 88 | //
click me 89 | //

content

90 | //
91 | ``` 92 | 93 | ## License 94 | 95 | [MIT](https://github.com/markdown-it/markdown-it-container/blob/master/LICENSE) 96 | -------------------------------------------------------------------------------- /test/api.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | import markdownit from 'markdown-it' 3 | 4 | import container from '../index.mjs' 5 | 6 | /* eslint-env mocha */ 7 | 8 | describe('api', function () { 9 | it('renderer', function () { 10 | const res = markdownit() 11 | .use(container, 'spoiler', { 12 | render: function (tokens, idx) { 13 | return tokens[idx].nesting === 1 14 | ? '
click me\n' 15 | : '
\n' 16 | } 17 | }) 18 | .render('::: spoiler\n*content*\n:::\n') 19 | 20 | assert.equal(res, '
click me\n

content

\n
\n') 21 | }) 22 | 23 | it('2 char marker', function () { 24 | const res = markdownit() 25 | .use(container, 'spoiler', { 26 | marker: '->' 27 | }) 28 | .render('->->-> spoiler\n*content*\n->->->\n') 29 | 30 | assert.equal(res, '
\n

content

\n
\n') 31 | }) 32 | 33 | it('marker should not collide with fence', function () { 34 | const res = markdownit() 35 | .use(container, 'spoiler', { 36 | marker: '`' 37 | }) 38 | .render('``` spoiler\n*content*\n```\n') 39 | 40 | assert.equal(res, '
\n

content

\n
\n') 41 | }) 42 | 43 | it('marker should not collide with fence #2', function () { 44 | const res = markdownit() 45 | .use(container, 'spoiler', { 46 | marker: '`' 47 | }) 48 | .render('\n``` not spoiler\n*content*\n```\n') 49 | 50 | assert.equal(res, '
*content*\n
\n') 51 | }) 52 | 53 | describe('validator', function () { 54 | it('should skip rule if return value is falsy', function () { 55 | const res = markdownit() 56 | .use(container, 'name', { 57 | validate: function () { return false } 58 | }) 59 | .render(':::foo\nbar\n:::\n') 60 | 61 | assert.equal(res, '

:::foo\nbar\n:::

\n') 62 | }) 63 | 64 | it('should accept rule if return value is true', function () { 65 | const res = markdownit() 66 | .use(container, 'name', { 67 | validate: function () { return true } 68 | }) 69 | .render(':::foo\nbar\n:::\n') 70 | 71 | assert.equal(res, '
\n

bar

\n
\n') 72 | }) 73 | 74 | it('rule should call it', function () { 75 | let count = 0 76 | 77 | markdownit() 78 | .use(container, 'name', { 79 | validate: function () { count++ } 80 | }) 81 | .parse(':\n::\n:::\n::::\n:::::\n', {}) 82 | 83 | // called by paragraph and lheading 3 times each 84 | assert(count > 0) 85 | assert(count % 3 === 0) 86 | }) 87 | 88 | it('should not trim params', function () { 89 | markdownit() 90 | .use(container, 'name', { 91 | validate: function (p) { assert.equal(p, ' \tname '); return 1 } 92 | }) 93 | .parse('::: \tname \ncontent\n:::\n', {}) 94 | }) 95 | 96 | it('should allow analyze mark', function () { 97 | const md = markdownit() 98 | .use(container, 'name', { 99 | validate: function (__, mark) { return mark.length >= 4 } 100 | }) 101 | 102 | assert.equal(md.render(':::\nfoo\n:::\n'), '

:::\nfoo\n:::

\n') 103 | assert.equal(md.render('::::\nfoo\n::::\n'), '
\n

foo

\n
\n') 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /test/fixtures/default.txt: -------------------------------------------------------------------------------- 1 | 2 | Simple container 3 | . 4 | ::: name 5 | *content* 6 | ::: 7 | . 8 |
9 |

content

10 |
11 | . 12 | 13 | 14 | Could contain block elements too 15 | . 16 | ::: name 17 | ### heading 18 | 19 | ----------- 20 | ::: 21 | . 22 |
23 |

heading

24 |
25 |
26 | . 27 | 28 | 29 | Ending marker could be longer 30 | . 31 | ::::: name ::::: 32 | hello world 33 | :::::::::::::::: 34 | . 35 |
36 |

hello world

37 |
38 | . 39 | 40 | 41 | Nested blocks 42 | . 43 | ::::: name 44 | :::: name 45 | xxx 46 | :::: 47 | ::::: 48 | . 49 |
50 |
51 |

xxx

52 |
53 |
54 | . 55 | 56 | 57 | Incorrectly nested blocks 58 | . 59 | :::: name 60 | this block is closed with 5 markers below 61 | 62 | ::::: name 63 | auto-closed block 64 | 65 | ::::: 66 | :::: 67 | . 68 |
69 |

this block is closed with 5 markers below

70 |
71 |

auto-closed block

72 |
73 |
74 |

::::

75 | . 76 | 77 | 78 | Marker could be indented up to 3 spaces 79 | . 80 | ::: name 81 | content 82 | ::: 83 | ::: 84 | . 85 |
86 |

content 87 | :::

88 |
89 | . 90 | 91 | 92 | But that's a code block 93 | . 94 | ::: name 95 | content 96 | ::: 97 | . 98 |
::: name
 99 | content
100 | :::
101 | 
102 | . 103 | 104 | 105 | Some more indent checks 106 | . 107 | ::: name 108 | not a code block 109 | 110 | code block 111 | ::: 112 | . 113 |
114 |

not a code block

115 |
code block
116 | 
117 |
118 | . 119 | 120 | 121 | Name could be adjacent to marker 122 | . 123 | :::name 124 | xxx 125 | ::: 126 | . 127 |
128 |

xxx

129 |
130 | . 131 | 132 | 133 | Or several spaces apart 134 | . 135 | ::: name 136 | xxx 137 | ::: 138 | . 139 |
140 |

xxx

141 |
142 | . 143 | 144 | 145 | It could contain params 146 | . 147 | ::: name arg=123 foo=456 148 | xxx 149 | ::: 150 | . 151 |
152 |

xxx

153 |
154 | . 155 | 156 | 157 | But closing marker can't 158 | . 159 | ::: name 160 | xxx 161 | ::: arg=123 162 | . 163 |
164 |

xxx 165 | ::: arg=123

166 |
167 | . 168 | 169 | 170 | This however isn't a valid one 171 | . 172 | ::: namename 173 | xxx 174 | ::: 175 | . 176 |

::: namename 177 | xxx 178 | :::

179 | . 180 | 181 | 182 | Containers self-close at the end of the document 183 | . 184 | ::: name 185 | xxx 186 | . 187 |
188 |

xxx

189 |
190 | . 191 | 192 | 193 | They should terminate paragraphs 194 | . 195 | blah blah 196 | ::: name 197 | content 198 | ::: 199 | . 200 |

blah blah

201 |
202 |

content

203 |
204 | . 205 | 206 | 207 | They could be nested in lists 208 | . 209 | - ::: name 210 | - xxx 211 | ::: 212 | . 213 | 222 | . 223 | 224 | 225 | Or in blockquotes 226 | . 227 | > ::: name 228 | > xxx 229 | >> yyy 230 | > zzz 231 | > ::: 232 | . 233 |
234 |
235 |

xxx

236 |
237 |

yyy 238 | zzz

239 |
240 |
241 |
242 | . 243 | 244 | 245 | List indentation quirks 246 | . 247 | - ::: name 248 | xxx 249 | yyy 250 | ::: 251 | 252 | - ::: name 253 | xxx 254 | yyy 255 | ::: 256 | . 257 | 265 |

:::

266 | 273 |

yyy 274 | :::

275 | . 276 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | // Process block-level custom containers 2 | // 3 | export default function container_plugin (md, name, options) { 4 | // Second param may be useful if you decide 5 | // to increase minimal allowed marker length 6 | function validateDefault (params/*, markup */) { 7 | return params.trim().split(' ', 2)[0] === name 8 | } 9 | 10 | function renderDefault (tokens, idx, _options, env, slf) { 11 | // add a class to the opening tag 12 | if (tokens[idx].nesting === 1) { 13 | tokens[idx].attrJoin('class', name) 14 | } 15 | 16 | return slf.renderToken(tokens, idx, _options, env, slf) 17 | } 18 | 19 | options = options || {} 20 | 21 | const min_markers = 3 22 | const marker_str = options.marker || ':' 23 | const marker_char = marker_str.charCodeAt(0) 24 | const marker_len = marker_str.length 25 | const validate = options.validate || validateDefault 26 | const render = options.render || renderDefault 27 | 28 | function container (state, startLine, endLine, silent) { 29 | let pos 30 | let auto_closed = false 31 | let start = state.bMarks[startLine] + state.tShift[startLine] 32 | let max = state.eMarks[startLine] 33 | 34 | // Check out the first character quickly, 35 | // this should filter out most of non-containers 36 | // 37 | if (marker_char !== state.src.charCodeAt(start)) { return false } 38 | 39 | // Check out the rest of the marker string 40 | // 41 | for (pos = start + 1; pos <= max; pos++) { 42 | if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { 43 | break 44 | } 45 | } 46 | 47 | const marker_count = Math.floor((pos - start) / marker_len) 48 | if (marker_count < min_markers) { return false } 49 | pos -= (pos - start) % marker_len 50 | 51 | const markup = state.src.slice(start, pos) 52 | const params = state.src.slice(pos, max) 53 | if (!validate(params, markup)) { return false } 54 | 55 | // Since start is found, we can report success here in validation mode 56 | // 57 | if (silent) { return true } 58 | 59 | // Search for the end of the block 60 | // 61 | let nextLine = startLine 62 | 63 | for (;;) { 64 | nextLine++ 65 | if (nextLine >= endLine) { 66 | // unclosed block should be autoclosed by end of document. 67 | // also block seems to be autoclosed by end of parent 68 | break 69 | } 70 | 71 | start = state.bMarks[nextLine] + state.tShift[nextLine] 72 | max = state.eMarks[nextLine] 73 | 74 | if (start < max && state.sCount[nextLine] < state.blkIndent) { 75 | // non-empty line with negative indent should stop the list: 76 | // - ``` 77 | // test 78 | break 79 | } 80 | 81 | if (marker_char !== state.src.charCodeAt(start)) { continue } 82 | 83 | if (state.sCount[nextLine] - state.blkIndent >= 4) { 84 | // closing fence should be indented less than 4 spaces 85 | continue 86 | } 87 | 88 | for (pos = start + 1; pos <= max; pos++) { 89 | if (marker_str[(pos - start) % marker_len] !== state.src[pos]) { 90 | break 91 | } 92 | } 93 | 94 | // closing code fence must be at least as long as the opening one 95 | if (Math.floor((pos - start) / marker_len) < marker_count) { continue } 96 | 97 | // make sure tail has spaces only 98 | pos -= (pos - start) % marker_len 99 | pos = state.skipSpaces(pos) 100 | 101 | if (pos < max) { continue } 102 | 103 | // found! 104 | auto_closed = true 105 | break 106 | } 107 | 108 | const old_parent = state.parentType 109 | const old_line_max = state.lineMax 110 | state.parentType = 'container' 111 | 112 | // this will prevent lazy continuations from ever going past our end marker 113 | state.lineMax = nextLine 114 | 115 | const token_o = state.push('container_' + name + '_open', 'div', 1) 116 | token_o.markup = markup 117 | token_o.block = true 118 | token_o.info = params 119 | token_o.map = [startLine, nextLine] 120 | 121 | state.md.block.tokenize(state, startLine + 1, nextLine) 122 | 123 | const token_c = state.push('container_' + name + '_close', 'div', -1) 124 | token_c.markup = state.src.slice(start, pos) 125 | token_c.block = true 126 | 127 | state.parentType = old_parent 128 | state.lineMax = old_line_max 129 | state.line = nextLine + (auto_closed ? 1 : 0) 130 | 131 | return true 132 | } 133 | 134 | md.block.ruler.before('fence', 'container_' + name, container, { 135 | alt: ['paragraph', 'reference', 'blockquote', 'list'] 136 | }) 137 | md.renderer.rules['container_' + name + '_open'] = render 138 | md.renderer.rules['container_' + name + '_close'] = render 139 | }; 140 | --------------------------------------------------------------------------------