├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .remarkignore ├── index.d.ts ├── index.js ├── lib └── index.js ├── license ├── package.json ├── readme.md ├── test ├── fixtures │ ├── container │ │ ├── input.md │ │ ├── output.md │ │ └── tree.json │ ├── leaf │ │ ├── input.md │ │ ├── output.md │ │ └── tree.json │ └── text │ │ ├── input.md │ │ ├── output.md │ │ └── tree.json └── index.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: unifiedjs/beep-boop-beta@main 6 | with: 7 | repo-token: ${{secrets.GITHUB_TOKEN}} 8 | name: bb 9 | on: 10 | issues: 11 | types: [closed, edited, labeled, opened, reopened, unlabeled] 12 | pull_request_target: 13 | types: [closed, edited, labeled, opened, reopened, unlabeled] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v5 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.log 3 | *.map 4 | *.tsbuildinfo 5 | .DS_Store 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | !/index.d.ts 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | test/fixtures/ 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type {ToMarkdownOptions} from 'mdast-util-directive' 2 | 3 | export {default} from './lib/index.js' 4 | 5 | /** 6 | * Configuration for `remark-directive`. 7 | * 8 | * Currently supports 9 | * `collapseEmptyAttributes`, 10 | * `preferShortcut`, 11 | * `preferUnquoted`, 12 | * `quoteSmart`, 13 | * and `quote` as serialization options. 14 | */ 15 | export interface Options extends ToMarkdownOptions {} 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {default} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {} from 'mdast-util-directive' 3 | * @import {Root} from 'mdast' 4 | * @import {} from 'remark-arse' 5 | * @import {} from 'remark-stringify' 6 | * @import {Processor} from 'unified' 7 | */ 8 | 9 | import {directiveFromMarkdown, directiveToMarkdown} from 'mdast-util-directive' 10 | import {directive} from 'micromark-extension-directive' 11 | 12 | /** 13 | * Add support for generic directives. 14 | * 15 | * ###### Notes 16 | * 17 | * Doesn’t handle the directives: create your own plugin to do that. 18 | * 19 | * @returns {undefined} 20 | * Nothing. 21 | */ 22 | export default function remarkDirective() { 23 | // @ts-expect-error: TS is wrong about `this`. 24 | // eslint-disable-next-line unicorn/no-this-assignment 25 | const self = /** @type {Processor} */ (this) 26 | const data = self.data() 27 | 28 | const micromarkExtensions = 29 | data.micromarkExtensions || (data.micromarkExtensions = []) 30 | const fromMarkdownExtensions = 31 | data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []) 32 | const toMarkdownExtensions = 33 | data.toMarkdownExtensions || (data.toMarkdownExtensions = []) 34 | 35 | micromarkExtensions.push(directive()) 36 | fromMarkdownExtensions.push(directiveFromMarkdown()) 37 | toMarkdownExtensions.push(directiveToMarkdown()) 38 | } 39 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/remarkjs/remark-directive/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/mdast": "^4.0.0", 9 | "mdast-util-directive": "^3.0.0", 10 | "micromark-extension-directive": "^4.0.0", 11 | "unified": "^11.0.0" 12 | }, 13 | "description": "remark plugin to support directives", 14 | "devDependencies": { 15 | "@types/node": "^22.0.0", 16 | "c8": "^10.0.0", 17 | "is-hidden": "^2.0.0", 18 | "prettier": "^3.0.0", 19 | "remark": "^15.0.0", 20 | "remark-cli": "^12.0.0", 21 | "remark-preset-wooorm": "^11.0.0", 22 | "to-vfile": "^8.0.0", 23 | "type-coverage": "^2.0.0", 24 | "typescript": "^5.0.0", 25 | "xo": "^0.60.0" 26 | }, 27 | "exports": "./index.js", 28 | "files": [ 29 | "index.d.ts", 30 | "index.js", 31 | "lib/" 32 | ], 33 | "funding": { 34 | "type": "opencollective", 35 | "url": "https://opencollective.com/unified" 36 | }, 37 | "keywords": [ 38 | "container", 39 | "directive", 40 | "generic", 41 | "markdown", 42 | "mdast", 43 | "plugin", 44 | "remark-plugin", 45 | "remark", 46 | "unified" 47 | ], 48 | "license": "MIT", 49 | "name": "remark-directive", 50 | "prettier": { 51 | "bracketSpacing": false, 52 | "singleQuote": true, 53 | "semi": false, 54 | "tabWidth": 2, 55 | "trailingComma": "none", 56 | "useTabs": false 57 | }, 58 | "remarkConfig": { 59 | "plugins": [ 60 | "remark-preset-wooorm" 61 | ] 62 | }, 63 | "repository": "remarkjs/remark-directive", 64 | "scripts": { 65 | "build": "tsc --build --clean && tsc --build && type-coverage", 66 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 67 | "prepack": "npm run build && npm run format", 68 | "test-api": "node --conditions development test/index.js", 69 | "test-coverage": "c8 --100 --reporter lcov -- npm run test-api", 70 | "test": "npm run build && npm run format && npm run test-coverage" 71 | }, 72 | "sideEffects": false, 73 | "typeCoverage": { 74 | "atLeast": 100, 75 | "strict": true 76 | }, 77 | "type": "module", 78 | "version": "4.0.0", 79 | "xo": { 80 | "overrides": [ 81 | { 82 | "files": [ 83 | "**/*.d.ts" 84 | ], 85 | "rules": { 86 | "@typescript-eslint/array-type": [ 87 | "error", 88 | { 89 | "default": "generic" 90 | } 91 | ], 92 | "@typescript-eslint/ban-types": [ 93 | "error", 94 | { 95 | "extendDefaults": true 96 | } 97 | ], 98 | "@typescript-eslint/consistent-type-definitions": [ 99 | "error", 100 | "interface" 101 | ] 102 | } 103 | }, 104 | { 105 | "files": [ 106 | "test/**/*.js" 107 | ], 108 | "rules": { 109 | "no-await-in-loop": "off" 110 | } 111 | } 112 | ], 113 | "prettier": true, 114 | "rules": { 115 | "logical-assignment-operators": "off" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # remark-directive 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | 8 | **[remark][github-remark]** plugin to support the 9 | [generic directives proposal][commonmark-directive-proposal] 10 | (`:cite[smith04]`, 11 | `::youtube[Video of a cat in a box]{v=01ab2cd3efg}`, 12 | and such). 13 | 14 | ## Contents 15 | 16 | * [What is this?](#what-is-this) 17 | * [When should I use this?](#when-should-i-use-this) 18 | * [Install](#install) 19 | * [Use](#use) 20 | * [API](#api) 21 | * [`unified().use(remarkDirective[, options])`](#unifieduseremarkdirective-options) 22 | * [`Options`](#options) 23 | * [Examples](#examples) 24 | * [Example: YouTube](#example-youtube) 25 | * [Example: Styled blocks](#example-styled-blocks) 26 | * [Authoring](#authoring) 27 | * [HTML](#html) 28 | * [CSS](#css) 29 | * [Syntax](#syntax) 30 | * [Syntax tree](#syntax-tree) 31 | * [Types](#types) 32 | * [Compatibility](#compatibility) 33 | * [Security](#security) 34 | * [Related](#related) 35 | * [Contribute](#contribute) 36 | * [License](#license) 37 | 38 | ## What is this? 39 | 40 | This package is a [unified][github-unified] 41 | ([remark][github-remark]) 42 | plugin to add support for directives: 43 | one syntax for arbitrary extensions in markdown. 44 | 45 | ## When should I use this? 46 | 47 | Directives are one of the four ways to extend markdown: 48 | an arbitrary extension syntax 49 | (see [Extending markdown][github-micromark-extending-markdown] 50 | in micromark’s docs for the alternatives and more info). 51 | This mechanism works well when you control the content: 52 | who authors it, 53 | what tools handle it, 54 | and where it’s displayed. 55 | When authors can read a guide on how to embed a tweet but are not expected to 56 | know the ins and outs of HTML or JavaScript. 57 | Directives don’t work well if you don’t know who authors content, 58 | what tools handle it, 59 | and where it ends up. 60 | Example use cases are a docs website for a project or product, 61 | or blogging tools and static site generators. 62 | 63 | If you *just* want to turn markdown into HTML (with maybe a few extensions such 64 | as this one), 65 | we recommend [`micromark`][github-micromark] with 66 | [`micromark-extension-directive`][github-micromark-extension-directive] instead. 67 | If you don’t use plugins and want to access the syntax tree, 68 | you can use 69 | [`mdast-util-from-markdown`][github-mdast-util-from-markdown] with 70 | [`mdast-util-directive`][github-mdast-util-directive]. 71 | 72 | ## Install 73 | 74 | This package is [ESM only][github-gist-esm]. 75 | In Node.js (version 16+), 76 | install with [npm][npmjs-install]: 77 | 78 | ```sh 79 | npm install remark-directive 80 | ``` 81 | 82 | In Deno with [`esm.sh`][esmsh]: 83 | 84 | ```js 85 | import remarkDirective from 'https://esm.sh/remark-directive@3' 86 | ``` 87 | 88 | In browsers with [`esm.sh`][esmsh]: 89 | 90 | ```html 91 | 94 | ``` 95 | 96 | ## Use 97 | 98 | Say our document `example.md` contains: 99 | 100 | ```markdown 101 | :::main{#readme} 102 | 103 | Lorem:br 104 | ipsum. 105 | 106 | ::hr{.red} 107 | 108 | A :i[lovely] language know as :abbr[HTML]{title="HyperText Markup Language"}. 109 | 110 | ::: 111 | ``` 112 | 113 | …and our module `example.js` contains: 114 | 115 | ```js 116 | /** 117 | * @import {} from 'mdast-util-directive' 118 | * @import {} from 'mdast-util-to-hast' 119 | * @import {Root} from 'mdast' 120 | */ 121 | 122 | import {h} from 'hastscript' 123 | import rehypeFormat from 'rehype-format' 124 | import rehypeStringify from 'rehype-stringify' 125 | import remarkDirective from 'remark-directive' 126 | import remarkParse from 'remark-parse' 127 | import remarkRehype from 'remark-rehype' 128 | import {read} from 'to-vfile' 129 | import {unified} from 'unified' 130 | import {visit} from 'unist-util-visit' 131 | 132 | const file = await unified() 133 | .use(remarkParse) 134 | .use(remarkDirective) 135 | .use(myRemarkPlugin) 136 | .use(remarkRehype) 137 | .use(rehypeFormat) 138 | .use(rehypeStringify) 139 | .process(await read('example.md')) 140 | 141 | console.log(String(file)) 142 | 143 | // This plugin is an example to let users write HTML with directives. 144 | // It’s informative but rather useless. 145 | // See below for others examples. 146 | function myRemarkPlugin() { 147 | /** 148 | * @param {Root} tree 149 | * Tree. 150 | * @returns {undefined} 151 | * Nothing. 152 | */ 153 | return function (tree) { 154 | visit(tree, function (node) { 155 | if ( 156 | node.type === 'containerDirective' || 157 | node.type === 'leafDirective' || 158 | node.type === 'textDirective' 159 | ) { 160 | const data = node.data || (node.data = {}) 161 | const hast = h(node.name, node.attributes || {}) 162 | 163 | data.hName = hast.tagName 164 | data.hProperties = hast.properties 165 | } 166 | }) 167 | } 168 | } 169 | ``` 170 | 171 | …then running `node example.js` yields: 172 | 173 | ```html 174 |
175 |

Lorem
ipsum.

176 |
177 |

A lovely language know as HTML.

178 |
179 | ``` 180 | 181 | ## API 182 | 183 | This package exports no identifiers. 184 | The default export is [`remarkDirective`][api-remark-directive]. 185 | 186 | ### `unified().use(remarkDirective[, options])` 187 | 188 | Add support for generic directives. 189 | 190 | ###### Parameters 191 | 192 | * `options` 193 | ([`Options`][api-options], optional) 194 | — configuration 195 | 196 | ###### Returns 197 | 198 | Nothing (`undefined`). 199 | 200 | ###### Notes 201 | 202 | Doesn’t handle the directives: 203 | [create your own plugin][unifiedjs-create-remark-plugin] to do that. 204 | 205 | ### `Options` 206 | 207 | Configuration (TypeScript type). 208 | 209 | ###### Fields 210 | 211 | * `collapseEmptyAttributes` 212 | (`boolean`, default: `true`) 213 | — collapse empty attributes: get `title` instead of `title=""` 214 | * `preferShortcut` 215 | (`boolean`, default: `true`) 216 | — prefer `#` and `.` shortcuts for `id` and `class` 217 | * `preferUnquoted` 218 | (`boolean`, default: `false`) 219 | — leave attributes unquoted if that results in less bytes 220 | * `quoteSmart` 221 | (`boolean`, default: `false`) 222 | — use the other quote if that results in less bytes 223 | * `quote` 224 | (`'"'` or `"'"`, 225 | default: 226 | the [`quote`][github-remark-stringify-quote] used by `remark-stringify` for 227 | titles) 228 | — preferred quote to use around attribute values 229 | 230 | ## Examples 231 | 232 | ### Example: YouTube 233 | 234 | This example shows how directives can be used for YouTube embeds. 235 | It’s based on the example in Use above. 236 | If `myRemarkPlugin` was replaced with this function: 237 | 238 | ```js 239 | /** 240 | * @import {} from 'mdast-util-directive' 241 | * @import {} from 'mdast-util-to-hast' 242 | * @import {Root} from 'mdast' 243 | * @import {VFile} from 'vfile' 244 | */ 245 | 246 | import {visit} from 'unist-util-visit' 247 | 248 | // This plugin is an example to turn `::youtube` into iframes. 249 | function myRemarkPlugin() { 250 | /** 251 | * @param {Root} tree 252 | * Tree. 253 | * @param {VFile} file 254 | * File. 255 | * @returns {undefined} 256 | * Nothing. 257 | */ 258 | return (tree, file) => { 259 | visit(tree, function (node) { 260 | if ( 261 | node.type === 'containerDirective' || 262 | node.type === 'leafDirective' || 263 | node.type === 'textDirective' 264 | ) { 265 | if (node.name !== 'youtube') return 266 | 267 | const data = node.data || (node.data = {}) 268 | const attributes = node.attributes || {} 269 | const id = attributes.id 270 | 271 | if (node.type === 'textDirective') { 272 | file.fail( 273 | 'Unexpected `:youtube` text directive, use two colons for a leaf directive', 274 | node 275 | ) 276 | } 277 | 278 | if (!id) { 279 | file.fail('Unexpected missing `id` on `youtube` directive', node) 280 | } 281 | 282 | data.hName = 'iframe' 283 | data.hProperties = { 284 | src: 'https://www.youtube.com/embed/' + id, 285 | width: 200, 286 | height: 200, 287 | frameBorder: 0, 288 | allow: 'picture-in-picture', 289 | allowFullScreen: true 290 | } 291 | } 292 | }) 293 | } 294 | } 295 | ``` 296 | 297 | …and `example.md` contains: 298 | 299 | ```markdown 300 | # Cat videos 301 | 302 | ::youtube[Video of a cat in a box]{#01ab2cd3efg} 303 | ``` 304 | 305 | …then running `node example.js` yields: 306 | 307 | ```html 308 |

Cat videos

309 | 310 | ``` 311 | 312 | ### Example: Styled blocks 313 | 314 | > 👉 **Note**: 315 | > this is sometimes called admonitions, callouts, etc. 316 | 317 | This example shows how directives can be used to style blocks. 318 | It’s based on the example in Use above. 319 | If `myRemarkPlugin` was replaced with this function: 320 | 321 | ```js 322 | /** 323 | * @import {} from 'mdast-util-directive' 324 | * @import {} from 'mdast-util-to-hast' 325 | * @import {Root} from 'mdast' 326 | */ 327 | 328 | import {h} from 'hastscript' 329 | import {visit} from 'unist-util-visit' 330 | 331 | // This plugin is an example to turn `::note` into divs, 332 | // passing arbitrary attributes. 333 | function myRemarkPlugin() { 334 | /** 335 | * @param {Root} tree 336 | * Tree. 337 | * @returns {undefined} 338 | * Nothing. 339 | */ 340 | return (tree) => { 341 | visit(tree, (node) => { 342 | if ( 343 | node.type === 'containerDirective' || 344 | node.type === 'leafDirective' || 345 | node.type === 'textDirective' 346 | ) { 347 | if (node.name !== 'note') return 348 | 349 | const data = node.data || (node.data = {}) 350 | const tagName = node.type === 'textDirective' ? 'span' : 'div' 351 | 352 | data.hName = tagName 353 | data.hProperties = h(tagName, node.attributes || {}).properties 354 | } 355 | }) 356 | } 357 | } 358 | ``` 359 | 360 | …and `example.md` contains: 361 | 362 | ```markdown 363 | # How to use xxx 364 | 365 | You can use xxx. 366 | 367 | :::note{.warning} 368 | if you chose xxx, you should also use yyy somewhere… 369 | ::: 370 | ``` 371 | 372 | …then running `node example` yields: 373 | 374 | ```html 375 |

How to use xxx

376 |

You can use xxx.

377 |
378 |

if you chose xxx, you should also use yyy somewhere…

379 |
380 | ``` 381 | 382 | ## Authoring 383 | 384 | When authoring markdown with directives, 385 | keep in mind that they don’t work in most places. 386 | On your own site it can be great! 387 | 388 | ## HTML 389 | 390 | You can define how directives are turned into HTML. 391 | If directives are not handled, 392 | they do not emit anything. 393 | 394 | ## CSS 395 | 396 | How to display directives is left as an exercise for the reader. 397 | 398 | ## Syntax 399 | 400 | See [*Syntax* in 401 | `micromark-extension-directive`](https://github.com/micromark/micromark-extension-directive#syntax). 402 | 403 | ## Syntax tree 404 | 405 | See [*Syntax tree* in 406 | `mdast-util-directive`](https://github.com/syntax-tree/mdast-util-directive#syntax-tree). 407 | 408 | ## Types 409 | 410 | This package is fully typed with [TypeScript][]. 411 | It exports no additional options. 412 | 413 | If you’re working with the syntax tree, 414 | you can register the new node types with `@types/mdast` by adding a reference: 415 | 416 | ```js 417 | /** 418 | * @import {} from 'mdast-util-directive' 419 | * @import {Root} from 'mdast' 420 | */ 421 | 422 | import {visit} from 'unist-util-visit' 423 | 424 | function myRemarkPlugin() { 425 | /** 426 | * @param {Root} tree 427 | * Tree. 428 | * @returns {undefined} 429 | * Nothing. 430 | */ 431 | return (tree) => { 432 | visit(tree, function (node) { 433 | console.log(node) // `node` can now be one of the nodes for directives. 434 | }) 435 | } 436 | } 437 | ``` 438 | 439 | ## Compatibility 440 | 441 | Projects maintained by the unified collective are compatible with maintained 442 | versions of Node.js. 443 | 444 | When we cut a new major release, 445 | we drop support for unmaintained versions of Node. 446 | This means we try to keep the current release line, 447 | `remark-directive@3`, 448 | compatible with Node.js 16. 449 | 450 | ## Security 451 | 452 | Use of `remark-directive` does not involve **[rehype][github-rehype]** 453 | ([hast][github-hast]) 454 | or user content so there are no openings for 455 | [cross-site scripting (XSS)][wikipedia-xss] attacks. 456 | 457 | ## Related 458 | 459 | * [`remark-gfm`](https://github.com/remarkjs/remark-gfm) 460 | — support GFM 461 | (autolink literals, footnotes, strikethrough, tables, tasklists) 462 | * [`remark-frontmatter`](https://github.com/remarkjs/remark-frontmatter) 463 | — support frontmatter 464 | (YAML, TOML, and more) 465 | * [`remark-math`](https://github.com/remarkjs/remark-math) 466 | — support math 467 | * [`remark-mdx`](https://github.com/mdx-js/mdx/tree/main/packages/remark-mdx) 468 | — support MDX 469 | (ESM, JSX, expressions) 470 | 471 | ## Contribute 472 | 473 | See [`contributing.md`][health-contributing] 474 | in 475 | [`remarkjs/.github`][health] 476 | for ways to get started. 477 | See [`support.md`][health-support] for ways to get help. 478 | 479 | This project has a [code of conduct][health-coc]. 480 | By interacting with this repository, 481 | organization, 482 | or community you agree to abide by its terms. 483 | 484 | ## License 485 | 486 | [MIT][file-license] © [Titus Wormer][wooorm] 487 | 488 | 489 | 490 | [api-options]: #options 491 | 492 | [api-remark-directive]: #unifieduseremarkdirective-options 493 | 494 | [badge-build-image]: https://github.com/remarkjs/remark-directive/workflows/main/badge.svg 495 | 496 | [badge-build-url]: https://github.com/remarkjs/remark-directive/actions 497 | 498 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/remarkjs/remark-directive.svg 499 | 500 | [badge-coverage-url]: https://codecov.io/github/remarkjs/remark-directive 501 | 502 | [badge-downloads-image]: https://img.shields.io/npm/dm/remark-directive.svg 503 | 504 | [badge-downloads-url]: https://www.npmjs.com/package/remark-directive 505 | 506 | [badge-size-image]: https://img.shields.io/bundlejs/size/remark-directive 507 | 508 | [badge-size-url]: https://bundlejs.com/?q=remark-directive 509 | 510 | [commonmark-directive-proposal]: https://talk.commonmark.org/t/generic-directives-plugins-syntax/444 511 | 512 | [esmsh]: https://esm.sh 513 | 514 | [file-license]: license 515 | 516 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 517 | 518 | [github-hast]: https://github.com/syntax-tree/hast 519 | 520 | [github-mdast-util-directive]: https://github.com/syntax-tree/mdast-util-directive 521 | 522 | [github-mdast-util-from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown 523 | 524 | [github-micromark]: https://github.com/micromark/micromark 525 | 526 | [github-micromark-extending-markdown]: https://github.com/micromark/micromark#extending-markdown 527 | 528 | [github-micromark-extension-directive]: https://github.com/micromark/micromark-extension-directive 529 | 530 | [github-rehype]: https://github.com/rehypejs/rehype 531 | 532 | [github-remark]: https://github.com/remarkjs/remark 533 | 534 | [github-remark-stringify-quote]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify#options 535 | 536 | [github-unified]: https://github.com/unifiedjs/unified 537 | 538 | [health]: https://github.com/remarkjs/.github 539 | 540 | [health-coc]: https://github.com/remarkjs/.github/blob/main/code-of-conduct.md 541 | 542 | [health-contributing]: https://github.com/remarkjs/.github/blob/main/contributing.md 543 | 544 | [health-support]: https://github.com/remarkjs/.github/blob/main/support.md 545 | 546 | [npmjs-install]: https://docs.npmjs.com/cli/install 547 | 548 | [typescript]: https://www.typescriptlang.org 549 | 550 | [unifiedjs-create-remark-plugin]: https://unifiedjs.com/learn/guide/create-a-remark-plugin/ 551 | 552 | [wikipedia-xss]: https://en.wikipedia.org/wiki/Cross-site_scripting 553 | 554 | [wooorm]: https://wooorm.com 555 | -------------------------------------------------------------------------------- /test/fixtures/container/input.md: -------------------------------------------------------------------------------- 1 | :::a 2 | ::: 3 | 4 | :::a[b] 5 | ::: 6 | 7 | :::a{b} 8 | ::: 9 | 10 | :::a[b]{c} 11 | ::: 12 | 13 | :::a[b *c* d **e**] 14 | ::: 15 | 16 | :::a{#b.c.d id=e class="f g" h="i & j k"} 17 | ::: 18 | 19 | :::::a 20 | ::::b 21 | :::c 22 | ::::: 23 | 24 | :::::a 25 | b 26 | ::::c 27 | d 28 | :::e 29 | - - f 30 | ::: 31 | > g h 32 | ::::: 33 | -------------------------------------------------------------------------------- /test/fixtures/container/output.md: -------------------------------------------------------------------------------- 1 | :::a 2 | ::: 3 | 4 | :::a[b] 5 | ::: 6 | 7 | :::a{b} 8 | ::: 9 | 10 | :::a[b]{c} 11 | ::: 12 | 13 | :::a[b *c* d **e**] 14 | ::: 15 | 16 | :::a{#e .c.d.f.g h="i & j k"} 17 | ::: 18 | 19 | :::::a 20 | ::::b 21 | :::c 22 | ::: 23 | :::: 24 | ::::: 25 | 26 | :::::a 27 | b 28 | 29 | ::::c 30 | d 31 | 32 | :::e 33 | * * f 34 | ::: 35 | 36 | > g h 37 | :::: 38 | ::::: 39 | -------------------------------------------------------------------------------- /test/fixtures/container/tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "containerDirective", 6 | "name": "a", 7 | "attributes": {}, 8 | "children": [], 9 | "position": { 10 | "start": { 11 | "line": 1, 12 | "column": 1, 13 | "offset": 0 14 | }, 15 | "end": { 16 | "line": 2, 17 | "column": 4, 18 | "offset": 8 19 | } 20 | } 21 | }, 22 | { 23 | "type": "containerDirective", 24 | "name": "a", 25 | "attributes": {}, 26 | "children": [ 27 | { 28 | "type": "paragraph", 29 | "data": { 30 | "directiveLabel": true 31 | }, 32 | "children": [ 33 | { 34 | "type": "text", 35 | "value": "b", 36 | "position": { 37 | "start": { 38 | "line": 4, 39 | "column": 6, 40 | "offset": 15 41 | }, 42 | "end": { 43 | "line": 4, 44 | "column": 7, 45 | "offset": 16 46 | } 47 | } 48 | } 49 | ], 50 | "position": { 51 | "start": { 52 | "line": 4, 53 | "column": 5, 54 | "offset": 14 55 | }, 56 | "end": { 57 | "line": 4, 58 | "column": 8, 59 | "offset": 17 60 | } 61 | } 62 | } 63 | ], 64 | "position": { 65 | "start": { 66 | "line": 4, 67 | "column": 1, 68 | "offset": 10 69 | }, 70 | "end": { 71 | "line": 5, 72 | "column": 4, 73 | "offset": 21 74 | } 75 | } 76 | }, 77 | { 78 | "type": "containerDirective", 79 | "name": "a", 80 | "attributes": { 81 | "b": "" 82 | }, 83 | "children": [], 84 | "position": { 85 | "start": { 86 | "line": 7, 87 | "column": 1, 88 | "offset": 23 89 | }, 90 | "end": { 91 | "line": 8, 92 | "column": 4, 93 | "offset": 34 94 | } 95 | } 96 | }, 97 | { 98 | "type": "containerDirective", 99 | "name": "a", 100 | "attributes": { 101 | "c": "" 102 | }, 103 | "children": [ 104 | { 105 | "type": "paragraph", 106 | "data": { 107 | "directiveLabel": true 108 | }, 109 | "children": [ 110 | { 111 | "type": "text", 112 | "value": "b", 113 | "position": { 114 | "start": { 115 | "line": 10, 116 | "column": 6, 117 | "offset": 41 118 | }, 119 | "end": { 120 | "line": 10, 121 | "column": 7, 122 | "offset": 42 123 | } 124 | } 125 | } 126 | ], 127 | "position": { 128 | "start": { 129 | "line": 10, 130 | "column": 5, 131 | "offset": 40 132 | }, 133 | "end": { 134 | "line": 10, 135 | "column": 8, 136 | "offset": 43 137 | } 138 | } 139 | } 140 | ], 141 | "position": { 142 | "start": { 143 | "line": 10, 144 | "column": 1, 145 | "offset": 36 146 | }, 147 | "end": { 148 | "line": 11, 149 | "column": 4, 150 | "offset": 50 151 | } 152 | } 153 | }, 154 | { 155 | "type": "containerDirective", 156 | "name": "a", 157 | "attributes": {}, 158 | "children": [ 159 | { 160 | "type": "paragraph", 161 | "data": { 162 | "directiveLabel": true 163 | }, 164 | "children": [ 165 | { 166 | "type": "text", 167 | "value": "b ", 168 | "position": { 169 | "start": { 170 | "line": 13, 171 | "column": 6, 172 | "offset": 57 173 | }, 174 | "end": { 175 | "line": 13, 176 | "column": 8, 177 | "offset": 59 178 | } 179 | } 180 | }, 181 | { 182 | "type": "emphasis", 183 | "children": [ 184 | { 185 | "type": "text", 186 | "value": "c", 187 | "position": { 188 | "start": { 189 | "line": 13, 190 | "column": 9, 191 | "offset": 60 192 | }, 193 | "end": { 194 | "line": 13, 195 | "column": 10, 196 | "offset": 61 197 | } 198 | } 199 | } 200 | ], 201 | "position": { 202 | "start": { 203 | "line": 13, 204 | "column": 8, 205 | "offset": 59 206 | }, 207 | "end": { 208 | "line": 13, 209 | "column": 11, 210 | "offset": 62 211 | } 212 | } 213 | }, 214 | { 215 | "type": "text", 216 | "value": " d ", 217 | "position": { 218 | "start": { 219 | "line": 13, 220 | "column": 11, 221 | "offset": 62 222 | }, 223 | "end": { 224 | "line": 13, 225 | "column": 14, 226 | "offset": 65 227 | } 228 | } 229 | }, 230 | { 231 | "type": "strong", 232 | "children": [ 233 | { 234 | "type": "text", 235 | "value": "e", 236 | "position": { 237 | "start": { 238 | "line": 13, 239 | "column": 16, 240 | "offset": 67 241 | }, 242 | "end": { 243 | "line": 13, 244 | "column": 17, 245 | "offset": 68 246 | } 247 | } 248 | } 249 | ], 250 | "position": { 251 | "start": { 252 | "line": 13, 253 | "column": 14, 254 | "offset": 65 255 | }, 256 | "end": { 257 | "line": 13, 258 | "column": 19, 259 | "offset": 70 260 | } 261 | } 262 | } 263 | ], 264 | "position": { 265 | "start": { 266 | "line": 13, 267 | "column": 5, 268 | "offset": 56 269 | }, 270 | "end": { 271 | "line": 13, 272 | "column": 20, 273 | "offset": 71 274 | } 275 | } 276 | } 277 | ], 278 | "position": { 279 | "start": { 280 | "line": 13, 281 | "column": 1, 282 | "offset": 52 283 | }, 284 | "end": { 285 | "line": 14, 286 | "column": 4, 287 | "offset": 75 288 | } 289 | } 290 | }, 291 | { 292 | "type": "containerDirective", 293 | "name": "a", 294 | "attributes": { 295 | "id": "e", 296 | "class": "c d f g", 297 | "h": "i & j k" 298 | }, 299 | "children": [], 300 | "position": { 301 | "start": { 302 | "line": 16, 303 | "column": 1, 304 | "offset": 77 305 | }, 306 | "end": { 307 | "line": 17, 308 | "column": 4, 309 | "offset": 126 310 | } 311 | } 312 | }, 313 | { 314 | "type": "containerDirective", 315 | "name": "a", 316 | "attributes": {}, 317 | "children": [ 318 | { 319 | "type": "containerDirective", 320 | "name": "b", 321 | "attributes": {}, 322 | "children": [ 323 | { 324 | "type": "containerDirective", 325 | "name": "c", 326 | "attributes": {}, 327 | "children": [], 328 | "position": { 329 | "start": { 330 | "line": 21, 331 | "column": 1, 332 | "offset": 141 333 | }, 334 | "end": { 335 | "line": 22, 336 | "column": 1, 337 | "offset": 146 338 | } 339 | } 340 | } 341 | ], 342 | "position": { 343 | "start": { 344 | "line": 20, 345 | "column": 1, 346 | "offset": 135 347 | }, 348 | "end": { 349 | "line": 22, 350 | "column": 1, 351 | "offset": 146 352 | } 353 | } 354 | } 355 | ], 356 | "position": { 357 | "start": { 358 | "line": 19, 359 | "column": 1, 360 | "offset": 128 361 | }, 362 | "end": { 363 | "line": 22, 364 | "column": 6, 365 | "offset": 151 366 | } 367 | } 368 | }, 369 | { 370 | "type": "containerDirective", 371 | "name": "a", 372 | "attributes": {}, 373 | "children": [ 374 | { 375 | "type": "paragraph", 376 | "children": [ 377 | { 378 | "type": "text", 379 | "value": "b", 380 | "position": { 381 | "start": { 382 | "line": 25, 383 | "column": 1, 384 | "offset": 160 385 | }, 386 | "end": { 387 | "line": 25, 388 | "column": 2, 389 | "offset": 161 390 | } 391 | } 392 | } 393 | ], 394 | "position": { 395 | "start": { 396 | "line": 25, 397 | "column": 1, 398 | "offset": 160 399 | }, 400 | "end": { 401 | "line": 25, 402 | "column": 2, 403 | "offset": 161 404 | } 405 | } 406 | }, 407 | { 408 | "type": "containerDirective", 409 | "name": "c", 410 | "attributes": {}, 411 | "children": [ 412 | { 413 | "type": "paragraph", 414 | "children": [ 415 | { 416 | "type": "text", 417 | "value": "d", 418 | "position": { 419 | "start": { 420 | "line": 27, 421 | "column": 1, 422 | "offset": 168 423 | }, 424 | "end": { 425 | "line": 27, 426 | "column": 2, 427 | "offset": 169 428 | } 429 | } 430 | } 431 | ], 432 | "position": { 433 | "start": { 434 | "line": 27, 435 | "column": 1, 436 | "offset": 168 437 | }, 438 | "end": { 439 | "line": 27, 440 | "column": 2, 441 | "offset": 169 442 | } 443 | } 444 | }, 445 | { 446 | "type": "containerDirective", 447 | "name": "e", 448 | "attributes": {}, 449 | "children": [ 450 | { 451 | "type": "list", 452 | "ordered": false, 453 | "start": null, 454 | "spread": false, 455 | "children": [ 456 | { 457 | "type": "listItem", 458 | "spread": false, 459 | "checked": null, 460 | "children": [ 461 | { 462 | "type": "list", 463 | "ordered": false, 464 | "start": null, 465 | "spread": false, 466 | "children": [ 467 | { 468 | "type": "listItem", 469 | "spread": false, 470 | "checked": null, 471 | "children": [ 472 | { 473 | "type": "paragraph", 474 | "children": [ 475 | { 476 | "type": "text", 477 | "value": "f", 478 | "position": { 479 | "start": { 480 | "line": 29, 481 | "column": 5, 482 | "offset": 179 483 | }, 484 | "end": { 485 | "line": 29, 486 | "column": 6, 487 | "offset": 180 488 | } 489 | } 490 | } 491 | ], 492 | "position": { 493 | "start": { 494 | "line": 29, 495 | "column": 5, 496 | "offset": 179 497 | }, 498 | "end": { 499 | "line": 29, 500 | "column": 6, 501 | "offset": 180 502 | } 503 | } 504 | } 505 | ], 506 | "position": { 507 | "start": { 508 | "line": 29, 509 | "column": 3, 510 | "offset": 177 511 | }, 512 | "end": { 513 | "line": 29, 514 | "column": 6, 515 | "offset": 180 516 | } 517 | } 518 | } 519 | ], 520 | "position": { 521 | "start": { 522 | "line": 29, 523 | "column": 3, 524 | "offset": 177 525 | }, 526 | "end": { 527 | "line": 29, 528 | "column": 6, 529 | "offset": 180 530 | } 531 | } 532 | } 533 | ], 534 | "position": { 535 | "start": { 536 | "line": 29, 537 | "column": 1, 538 | "offset": 175 539 | }, 540 | "end": { 541 | "line": 29, 542 | "column": 6, 543 | "offset": 180 544 | } 545 | } 546 | } 547 | ], 548 | "position": { 549 | "start": { 550 | "line": 29, 551 | "column": 1, 552 | "offset": 175 553 | }, 554 | "end": { 555 | "line": 29, 556 | "column": 6, 557 | "offset": 180 558 | } 559 | } 560 | } 561 | ], 562 | "position": { 563 | "start": { 564 | "line": 28, 565 | "column": 1, 566 | "offset": 170 567 | }, 568 | "end": { 569 | "line": 30, 570 | "column": 4, 571 | "offset": 184 572 | } 573 | } 574 | }, 575 | { 576 | "type": "blockquote", 577 | "children": [ 578 | { 579 | "type": "paragraph", 580 | "children": [ 581 | { 582 | "type": "text", 583 | "value": "g h", 584 | "position": { 585 | "start": { 586 | "line": 31, 587 | "column": 3, 588 | "offset": 187 589 | }, 590 | "end": { 591 | "line": 31, 592 | "column": 6, 593 | "offset": 190 594 | } 595 | } 596 | } 597 | ], 598 | "position": { 599 | "start": { 600 | "line": 31, 601 | "column": 3, 602 | "offset": 187 603 | }, 604 | "end": { 605 | "line": 31, 606 | "column": 6, 607 | "offset": 190 608 | } 609 | } 610 | } 611 | ], 612 | "position": { 613 | "start": { 614 | "line": 31, 615 | "column": 1, 616 | "offset": 185 617 | }, 618 | "end": { 619 | "line": 31, 620 | "column": 6, 621 | "offset": 190 622 | } 623 | } 624 | } 625 | ], 626 | "position": { 627 | "start": { 628 | "line": 26, 629 | "column": 1, 630 | "offset": 162 631 | }, 632 | "end": { 633 | "line": 32, 634 | "column": 1, 635 | "offset": 191 636 | } 637 | } 638 | } 639 | ], 640 | "position": { 641 | "start": { 642 | "line": 24, 643 | "column": 1, 644 | "offset": 153 645 | }, 646 | "end": { 647 | "line": 32, 648 | "column": 6, 649 | "offset": 196 650 | } 651 | } 652 | } 653 | ], 654 | "position": { 655 | "start": { 656 | "line": 1, 657 | "column": 1, 658 | "offset": 0 659 | }, 660 | "end": { 661 | "line": 33, 662 | "column": 1, 663 | "offset": 197 664 | } 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /test/fixtures/leaf/input.md: -------------------------------------------------------------------------------- 1 | ::a 2 | 3 | ::a[b] 4 | 5 | ::a{b} 6 | 7 | ::a[b]{c} 8 | 9 | ::a[b *c* d **e**] 10 | 11 | ::a{#b.c.d id=e class="f g" h="i & j k"} 12 | -------------------------------------------------------------------------------- /test/fixtures/leaf/output.md: -------------------------------------------------------------------------------- 1 | ::a 2 | 3 | ::a[b] 4 | 5 | ::a{b} 6 | 7 | ::a[b]{c} 8 | 9 | ::a[b *c* d **e**] 10 | 11 | ::a{#e .c.d.f.g h="i & j k"} 12 | -------------------------------------------------------------------------------- /test/fixtures/leaf/tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "leafDirective", 6 | "name": "a", 7 | "attributes": {}, 8 | "children": [], 9 | "position": { 10 | "start": { 11 | "line": 1, 12 | "column": 1, 13 | "offset": 0 14 | }, 15 | "end": { 16 | "line": 1, 17 | "column": 4, 18 | "offset": 3 19 | } 20 | } 21 | }, 22 | { 23 | "type": "leafDirective", 24 | "name": "a", 25 | "attributes": {}, 26 | "children": [ 27 | { 28 | "type": "text", 29 | "value": "b", 30 | "position": { 31 | "start": { 32 | "line": 3, 33 | "column": 5, 34 | "offset": 9 35 | }, 36 | "end": { 37 | "line": 3, 38 | "column": 6, 39 | "offset": 10 40 | } 41 | } 42 | } 43 | ], 44 | "position": { 45 | "start": { 46 | "line": 3, 47 | "column": 1, 48 | "offset": 5 49 | }, 50 | "end": { 51 | "line": 3, 52 | "column": 7, 53 | "offset": 11 54 | } 55 | } 56 | }, 57 | { 58 | "type": "leafDirective", 59 | "name": "a", 60 | "attributes": { 61 | "b": "" 62 | }, 63 | "children": [], 64 | "position": { 65 | "start": { 66 | "line": 5, 67 | "column": 1, 68 | "offset": 13 69 | }, 70 | "end": { 71 | "line": 5, 72 | "column": 7, 73 | "offset": 19 74 | } 75 | } 76 | }, 77 | { 78 | "type": "leafDirective", 79 | "name": "a", 80 | "attributes": { 81 | "c": "" 82 | }, 83 | "children": [ 84 | { 85 | "type": "text", 86 | "value": "b", 87 | "position": { 88 | "start": { 89 | "line": 7, 90 | "column": 5, 91 | "offset": 25 92 | }, 93 | "end": { 94 | "line": 7, 95 | "column": 6, 96 | "offset": 26 97 | } 98 | } 99 | } 100 | ], 101 | "position": { 102 | "start": { 103 | "line": 7, 104 | "column": 1, 105 | "offset": 21 106 | }, 107 | "end": { 108 | "line": 7, 109 | "column": 10, 110 | "offset": 30 111 | } 112 | } 113 | }, 114 | { 115 | "type": "leafDirective", 116 | "name": "a", 117 | "attributes": {}, 118 | "children": [ 119 | { 120 | "type": "text", 121 | "value": "b ", 122 | "position": { 123 | "start": { 124 | "line": 9, 125 | "column": 5, 126 | "offset": 36 127 | }, 128 | "end": { 129 | "line": 9, 130 | "column": 7, 131 | "offset": 38 132 | } 133 | } 134 | }, 135 | { 136 | "type": "emphasis", 137 | "children": [ 138 | { 139 | "type": "text", 140 | "value": "c", 141 | "position": { 142 | "start": { 143 | "line": 9, 144 | "column": 8, 145 | "offset": 39 146 | }, 147 | "end": { 148 | "line": 9, 149 | "column": 9, 150 | "offset": 40 151 | } 152 | } 153 | } 154 | ], 155 | "position": { 156 | "start": { 157 | "line": 9, 158 | "column": 7, 159 | "offset": 38 160 | }, 161 | "end": { 162 | "line": 9, 163 | "column": 10, 164 | "offset": 41 165 | } 166 | } 167 | }, 168 | { 169 | "type": "text", 170 | "value": " d ", 171 | "position": { 172 | "start": { 173 | "line": 9, 174 | "column": 10, 175 | "offset": 41 176 | }, 177 | "end": { 178 | "line": 9, 179 | "column": 13, 180 | "offset": 44 181 | } 182 | } 183 | }, 184 | { 185 | "type": "strong", 186 | "children": [ 187 | { 188 | "type": "text", 189 | "value": "e", 190 | "position": { 191 | "start": { 192 | "line": 9, 193 | "column": 15, 194 | "offset": 46 195 | }, 196 | "end": { 197 | "line": 9, 198 | "column": 16, 199 | "offset": 47 200 | } 201 | } 202 | } 203 | ], 204 | "position": { 205 | "start": { 206 | "line": 9, 207 | "column": 13, 208 | "offset": 44 209 | }, 210 | "end": { 211 | "line": 9, 212 | "column": 18, 213 | "offset": 49 214 | } 215 | } 216 | } 217 | ], 218 | "position": { 219 | "start": { 220 | "line": 9, 221 | "column": 1, 222 | "offset": 32 223 | }, 224 | "end": { 225 | "line": 9, 226 | "column": 19, 227 | "offset": 50 228 | } 229 | } 230 | }, 231 | { 232 | "type": "leafDirective", 233 | "name": "a", 234 | "attributes": { 235 | "id": "e", 236 | "class": "c d f g", 237 | "h": "i & j k" 238 | }, 239 | "children": [], 240 | "position": { 241 | "start": { 242 | "line": 11, 243 | "column": 1, 244 | "offset": 52 245 | }, 246 | "end": { 247 | "line": 11, 248 | "column": 45, 249 | "offset": 96 250 | } 251 | } 252 | } 253 | ], 254 | "position": { 255 | "start": { 256 | "line": 1, 257 | "column": 1, 258 | "offset": 0 259 | }, 260 | "end": { 261 | "line": 12, 262 | "column": 1, 263 | "offset": 97 264 | } 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /test/fixtures/text/input.md: -------------------------------------------------------------------------------- 1 | One :a, two :a[b], three :a{b}, four :a[b]{c}. 2 | 3 | :a[b *c* 4 | d **e**]. 5 | 6 | :a{#b.c.d id=e class="f g" h="i & j 7 | k"}. 8 | -------------------------------------------------------------------------------- /test/fixtures/text/output.md: -------------------------------------------------------------------------------- 1 | One :a, two :a[b], three :a{b}, four :a[b]{c}. 2 | 3 | :a[b *c* 4 | d **e**]. 5 | 6 | :a{#e .c.d.f.g h="i & j 7 | k"}. 8 | -------------------------------------------------------------------------------- /test/fixtures/text/tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "root", 3 | "children": [ 4 | { 5 | "type": "paragraph", 6 | "children": [ 7 | { 8 | "type": "text", 9 | "value": "One ", 10 | "position": { 11 | "start": { 12 | "line": 1, 13 | "column": 1, 14 | "offset": 0 15 | }, 16 | "end": { 17 | "line": 1, 18 | "column": 5, 19 | "offset": 4 20 | } 21 | } 22 | }, 23 | { 24 | "type": "textDirective", 25 | "name": "a", 26 | "attributes": {}, 27 | "children": [], 28 | "position": { 29 | "start": { 30 | "line": 1, 31 | "column": 5, 32 | "offset": 4 33 | }, 34 | "end": { 35 | "line": 1, 36 | "column": 7, 37 | "offset": 6 38 | } 39 | } 40 | }, 41 | { 42 | "type": "text", 43 | "value": ", two ", 44 | "position": { 45 | "start": { 46 | "line": 1, 47 | "column": 7, 48 | "offset": 6 49 | }, 50 | "end": { 51 | "line": 1, 52 | "column": 13, 53 | "offset": 12 54 | } 55 | } 56 | }, 57 | { 58 | "type": "textDirective", 59 | "name": "a", 60 | "attributes": {}, 61 | "children": [ 62 | { 63 | "type": "text", 64 | "value": "b", 65 | "position": { 66 | "start": { 67 | "line": 1, 68 | "column": 16, 69 | "offset": 15 70 | }, 71 | "end": { 72 | "line": 1, 73 | "column": 17, 74 | "offset": 16 75 | } 76 | } 77 | } 78 | ], 79 | "position": { 80 | "start": { 81 | "line": 1, 82 | "column": 13, 83 | "offset": 12 84 | }, 85 | "end": { 86 | "line": 1, 87 | "column": 18, 88 | "offset": 17 89 | } 90 | } 91 | }, 92 | { 93 | "type": "text", 94 | "value": ", three ", 95 | "position": { 96 | "start": { 97 | "line": 1, 98 | "column": 18, 99 | "offset": 17 100 | }, 101 | "end": { 102 | "line": 1, 103 | "column": 26, 104 | "offset": 25 105 | } 106 | } 107 | }, 108 | { 109 | "type": "textDirective", 110 | "name": "a", 111 | "attributes": { 112 | "b": "" 113 | }, 114 | "children": [], 115 | "position": { 116 | "start": { 117 | "line": 1, 118 | "column": 26, 119 | "offset": 25 120 | }, 121 | "end": { 122 | "line": 1, 123 | "column": 31, 124 | "offset": 30 125 | } 126 | } 127 | }, 128 | { 129 | "type": "text", 130 | "value": ", four ", 131 | "position": { 132 | "start": { 133 | "line": 1, 134 | "column": 31, 135 | "offset": 30 136 | }, 137 | "end": { 138 | "line": 1, 139 | "column": 38, 140 | "offset": 37 141 | } 142 | } 143 | }, 144 | { 145 | "type": "textDirective", 146 | "name": "a", 147 | "attributes": { 148 | "c": "" 149 | }, 150 | "children": [ 151 | { 152 | "type": "text", 153 | "value": "b", 154 | "position": { 155 | "start": { 156 | "line": 1, 157 | "column": 41, 158 | "offset": 40 159 | }, 160 | "end": { 161 | "line": 1, 162 | "column": 42, 163 | "offset": 41 164 | } 165 | } 166 | } 167 | ], 168 | "position": { 169 | "start": { 170 | "line": 1, 171 | "column": 38, 172 | "offset": 37 173 | }, 174 | "end": { 175 | "line": 1, 176 | "column": 46, 177 | "offset": 45 178 | } 179 | } 180 | }, 181 | { 182 | "type": "text", 183 | "value": ".", 184 | "position": { 185 | "start": { 186 | "line": 1, 187 | "column": 46, 188 | "offset": 45 189 | }, 190 | "end": { 191 | "line": 1, 192 | "column": 47, 193 | "offset": 46 194 | } 195 | } 196 | } 197 | ], 198 | "position": { 199 | "start": { 200 | "line": 1, 201 | "column": 1, 202 | "offset": 0 203 | }, 204 | "end": { 205 | "line": 1, 206 | "column": 47, 207 | "offset": 46 208 | } 209 | } 210 | }, 211 | { 212 | "type": "paragraph", 213 | "children": [ 214 | { 215 | "type": "textDirective", 216 | "name": "a", 217 | "attributes": {}, 218 | "children": [ 219 | { 220 | "type": "text", 221 | "value": "b ", 222 | "position": { 223 | "start": { 224 | "line": 3, 225 | "column": 4, 226 | "offset": 51 227 | }, 228 | "end": { 229 | "line": 3, 230 | "column": 6, 231 | "offset": 53 232 | } 233 | } 234 | }, 235 | { 236 | "type": "emphasis", 237 | "children": [ 238 | { 239 | "type": "text", 240 | "value": "c", 241 | "position": { 242 | "start": { 243 | "line": 3, 244 | "column": 7, 245 | "offset": 54 246 | }, 247 | "end": { 248 | "line": 3, 249 | "column": 8, 250 | "offset": 55 251 | } 252 | } 253 | } 254 | ], 255 | "position": { 256 | "start": { 257 | "line": 3, 258 | "column": 6, 259 | "offset": 53 260 | }, 261 | "end": { 262 | "line": 3, 263 | "column": 9, 264 | "offset": 56 265 | } 266 | } 267 | }, 268 | { 269 | "type": "text", 270 | "value": "\nd ", 271 | "position": { 272 | "start": { 273 | "line": 3, 274 | "column": 9, 275 | "offset": 56 276 | }, 277 | "end": { 278 | "line": 4, 279 | "column": 3, 280 | "offset": 59 281 | } 282 | } 283 | }, 284 | { 285 | "type": "strong", 286 | "children": [ 287 | { 288 | "type": "text", 289 | "value": "e", 290 | "position": { 291 | "start": { 292 | "line": 4, 293 | "column": 5, 294 | "offset": 61 295 | }, 296 | "end": { 297 | "line": 4, 298 | "column": 6, 299 | "offset": 62 300 | } 301 | } 302 | } 303 | ], 304 | "position": { 305 | "start": { 306 | "line": 4, 307 | "column": 3, 308 | "offset": 59 309 | }, 310 | "end": { 311 | "line": 4, 312 | "column": 8, 313 | "offset": 64 314 | } 315 | } 316 | } 317 | ], 318 | "position": { 319 | "start": { 320 | "line": 3, 321 | "column": 1, 322 | "offset": 48 323 | }, 324 | "end": { 325 | "line": 4, 326 | "column": 9, 327 | "offset": 65 328 | } 329 | } 330 | }, 331 | { 332 | "type": "text", 333 | "value": ".", 334 | "position": { 335 | "start": { 336 | "line": 4, 337 | "column": 9, 338 | "offset": 65 339 | }, 340 | "end": { 341 | "line": 4, 342 | "column": 10, 343 | "offset": 66 344 | } 345 | } 346 | } 347 | ], 348 | "position": { 349 | "start": { 350 | "line": 3, 351 | "column": 1, 352 | "offset": 48 353 | }, 354 | "end": { 355 | "line": 4, 356 | "column": 10, 357 | "offset": 66 358 | } 359 | } 360 | }, 361 | { 362 | "type": "paragraph", 363 | "children": [ 364 | { 365 | "type": "textDirective", 366 | "name": "a", 367 | "attributes": { 368 | "id": "e", 369 | "class": "c d f g", 370 | "h": "i & j\nk" 371 | }, 372 | "children": [], 373 | "position": { 374 | "start": { 375 | "line": 6, 376 | "column": 1, 377 | "offset": 68 378 | }, 379 | "end": { 380 | "line": 7, 381 | "column": 4, 382 | "offset": 111 383 | } 384 | } 385 | }, 386 | { 387 | "type": "text", 388 | "value": ".", 389 | "position": { 390 | "start": { 391 | "line": 7, 392 | "column": 4, 393 | "offset": 111 394 | }, 395 | "end": { 396 | "line": 7, 397 | "column": 5, 398 | "offset": 112 399 | } 400 | } 401 | } 402 | ], 403 | "position": { 404 | "start": { 405 | "line": 6, 406 | "column": 1, 407 | "offset": 68 408 | }, 409 | "end": { 410 | "line": 7, 411 | "column": 5, 412 | "offset": 112 413 | } 414 | } 415 | } 416 | ], 417 | "position": { 418 | "start": { 419 | "line": 1, 420 | "column": 1, 421 | "offset": 0 422 | }, 423 | "end": { 424 | "line": 8, 425 | "column": 1, 426 | "offset": 113 427 | } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('mdast').Root} Root 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import fs from 'node:fs/promises' 7 | import process from 'node:process' 8 | import test from 'node:test' 9 | import {isHidden} from 'is-hidden' 10 | import {remark} from 'remark' 11 | import remarkDirective from 'remark-directive' 12 | 13 | test('remarkDirective', async function (t) { 14 | await t.test('should expose the public api', async function () { 15 | assert.deepEqual(Object.keys(await import('remark-directive')).sort(), [ 16 | 'default' 17 | ]) 18 | }) 19 | 20 | await t.test('should not throw if not passed options', async function () { 21 | assert.doesNotThrow(function () { 22 | remark().use(remarkDirective).freeze() 23 | }) 24 | }) 25 | }) 26 | 27 | test('fixtures', async function (t) { 28 | const base = new URL('fixtures/', import.meta.url) 29 | const folders = await fs.readdir(base) 30 | 31 | let index = -1 32 | 33 | while (++index < folders.length) { 34 | const folder = folders[index] 35 | 36 | if (isHidden(folder)) continue 37 | 38 | await t.test(folder, async function () { 39 | const folderUrl = new URL(folder + '/', base) 40 | const inputUrl = new URL('input.md', folderUrl) 41 | const outputUrl = new URL('output.md', folderUrl) 42 | const treeUrl = new URL('tree.json', folderUrl) 43 | 44 | const input = String(await fs.readFile(inputUrl)) 45 | 46 | /** @type {Root} */ 47 | let expected 48 | /** @type {string} */ 49 | let output 50 | 51 | const processor = remark().use(remarkDirective) 52 | const actual = processor.parse(input) 53 | 54 | try { 55 | output = String(await fs.readFile(outputUrl)) 56 | } catch { 57 | output = input 58 | } 59 | 60 | try { 61 | if ('UPDATE' in process.env) { 62 | throw new Error('Updating…') 63 | } 64 | 65 | expected = JSON.parse(String(await fs.readFile(treeUrl))) 66 | } catch { 67 | expected = actual 68 | 69 | // New fixture. 70 | await fs.writeFile(treeUrl, JSON.stringify(actual, undefined, 2) + '\n') 71 | } 72 | 73 | assert.deepEqual(actual, expected) 74 | 75 | assert.equal(String(await processor.process(input)), String(output)) 76 | }) 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js", "index.d.ts"] 16 | } 17 | --------------------------------------------------------------------------------