Yuki Takei 3 лет назад
Родитель
Сommit
4e41fb5677
18 измененных файлов с 4989 добавлено и 9 удалено
  1. 8 9
      packages/remark-growi-plugin/src/index.js
  2. 35 0
      packages/remark-growi-plugin/src/mdast-util-growi-plugin/complex-types.d.ts
  3. 402 0
      packages/remark-growi-plugin/src/mdast-util-growi-plugin/index.js
  4. 22 0
      packages/remark-growi-plugin/src/mdast-util-growi-plugin/license
  5. 447 0
      packages/remark-growi-plugin/src/mdast-util-growi-plugin/readme.md
  6. 909 0
      packages/remark-growi-plugin/src/mdast-util-growi-plugin/test.js
  7. 7 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/index.js
  8. 294 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/directive-container.js
  9. 118 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/directive-leaf.js
  10. 105 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/directive-text.js
  11. 336 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/factory-attributes.js
  12. 139 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/factory-label.js
  13. 49 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/factory-name.js
  14. 242 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/html.js
  15. 18 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/syntax.js
  16. 22 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/license
  17. 288 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/readme.md
  18. 1548 0
      packages/remark-growi-plugin/src/micromark-extension-growi-plugin/test/index.js

+ 8 - 9
packages/remark-growi-plugin/src/index.js

@@ -4,22 +4,17 @@
  * @typedef {import('mdast-util-directive')} DoNotTouchAsThisImportIncludesDirectivesInTree
  */
 
-import { directiveFromMarkdown, directiveToMarkdown } from 'mdast-util-directive';
-import { directive } from 'micromark-extension-directive';
+import { directiveFromMarkdown, directiveToMarkdown } from './mdast-util-growi-plugin';
+import { directive } from './micromark-extension-growi-plugin/dev';
 
 /**
- * Plugin to support the generic directives proposal (`:cite[smith04]`,
- * `::youtube[Video of a cat in a box]{v=01ab2cd3efg}`, and such).
+ * Plugin to support GROWI plugin (`$lsx(/path, depth=2)`).
  *
  * @type {import('unified').Plugin<void[], Root>}
  */
-export default function remarkDirective() {
+export default function remarkGrowiPlugin() {
   const data = this.data();
 
-  add('micromarkExtensions', directive());
-  add('fromMarkdownExtensions', directiveFromMarkdown);
-  add('toMarkdownExtensions', directiveToMarkdown);
-
   /**
    * @param {string} field
    * @param {unknown} value
@@ -33,4 +28,8 @@ export default function remarkDirective() {
 
     list.push(value);
   }
+
+  add('micromarkExtensions', directive());
+  add('fromMarkdownExtensions', directiveFromMarkdown);
+  add('toMarkdownExtensions', directiveToMarkdown);
 }

+ 35 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/complex-types.d.ts

@@ -0,0 +1,35 @@
+import type {Parent} from 'unist'
+import type {PhrasingContent, BlockContent} from 'mdast'
+
+type DirectiveAttributes = Record<string, string>
+
+interface DirectiveFields {
+  name: string
+  attributes?: DirectiveAttributes
+}
+
+export interface TextDirective extends Parent, DirectiveFields {
+  type: 'textDirective'
+  children: PhrasingContent[]
+}
+
+export interface LeafDirective extends Parent, DirectiveFields {
+  type: 'leafDirective'
+  children: PhrasingContent[]
+}
+
+export interface ContainerDirective extends Parent, DirectiveFields {
+  type: 'containerDirective'
+  children: BlockContent[]
+}
+
+declare module 'mdast' {
+  interface StaticPhrasingContentMap {
+    textDirective: TextDirective
+  }
+
+  interface BlockContentMap {
+    containerDirective: ContainerDirective
+    leafDirective: LeafDirective
+  }
+}

+ 402 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/index.js

@@ -0,0 +1,402 @@
+/**
+ * @typedef {import('mdast').BlockContent} BlockContent
+ * @typedef {import('mdast').Root} Root
+ * @typedef {import('mdast').Paragraph} Paragraph
+ * @typedef {import('mdast-util-from-markdown').Handle} FromMarkdownHandle
+ * @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension
+ * @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext
+ * @typedef {import('mdast-util-from-markdown').Token} Token
+ * @typedef {import('mdast-util-to-markdown/lib/types.js').Handle} ToMarkdownHandle
+ * @typedef {import('mdast-util-to-markdown/lib/types.js').Context} Context
+ * @typedef {import('mdast-util-to-markdown/lib/types.js').Options} ToMarkdownExtension
+ * @typedef {import('./complex-types').ContainerDirective} ContainerDirective
+ * @typedef {import('./complex-types').LeafDirective} LeafDirective
+ * @typedef {import('./complex-types').TextDirective} TextDirective
+ * @typedef {ContainerDirective|LeafDirective|TextDirective} Directive
+ */
+
+import {parseEntities} from 'parse-entities'
+import {stringifyEntitiesLight} from 'stringify-entities'
+import {visitParents} from 'unist-util-visit-parents'
+import {containerFlow} from 'mdast-util-to-markdown/lib/util/container-flow.js'
+import {containerPhrasing} from 'mdast-util-to-markdown/lib/util/container-phrasing.js'
+import {checkQuote} from 'mdast-util-to-markdown/lib/util/check-quote.js'
+import {track} from 'mdast-util-to-markdown/lib/util/track.js'
+
+const own = {}.hasOwnProperty
+
+const shortcut = /^[^\t\n\r "#'.<=>`}]+$/
+
+handleDirective.peek = peekDirective
+
+/** @type {FromMarkdownExtension} */
+export const directiveFromMarkdown = {
+  canContainEols: ['textDirective'],
+  enter: {
+    directiveContainer: enterContainer,
+    directiveContainerAttributes: enterAttributes,
+    directiveContainerLabel: enterContainerLabel,
+
+    directiveLeaf: enterLeaf,
+    directiveLeafAttributes: enterAttributes,
+
+    directiveText: enterText,
+    directiveTextAttributes: enterAttributes
+  },
+  exit: {
+    directiveContainer: exit,
+    directiveContainerAttributeClassValue: exitAttributeClassValue,
+    directiveContainerAttributeIdValue: exitAttributeIdValue,
+    directiveContainerAttributeName: exitAttributeName,
+    directiveContainerAttributeValue: exitAttributeValue,
+    directiveContainerAttributes: exitAttributes,
+    directiveContainerLabel: exitContainerLabel,
+    directiveContainerName: exitName,
+
+    directiveLeaf: exit,
+    directiveLeafAttributeClassValue: exitAttributeClassValue,
+    directiveLeafAttributeIdValue: exitAttributeIdValue,
+    directiveLeafAttributeName: exitAttributeName,
+    directiveLeafAttributeValue: exitAttributeValue,
+    directiveLeafAttributes: exitAttributes,
+    directiveLeafName: exitName,
+
+    directiveText: exit,
+    directiveTextAttributeClassValue: exitAttributeClassValue,
+    directiveTextAttributeIdValue: exitAttributeIdValue,
+    directiveTextAttributeName: exitAttributeName,
+    directiveTextAttributeValue: exitAttributeValue,
+    directiveTextAttributes: exitAttributes,
+    directiveTextName: exitName
+  }
+}
+
+/** @type {ToMarkdownExtension} */
+export const directiveToMarkdown = {
+  unsafe: [
+    {
+      character: '\r',
+      inConstruct: ['leafDirectiveLabel', 'containerDirectiveLabel']
+    },
+    {
+      character: '\n',
+      inConstruct: ['leafDirectiveLabel', 'containerDirectiveLabel']
+    },
+    {
+      before: '[^:]',
+      character: ':',
+      after: '[A-Za-z]',
+      inConstruct: ['phrasing']
+    },
+    {atBreak: true, character: ':', after: ':'}
+  ],
+  handlers: {
+    containerDirective: handleDirective,
+    leafDirective: handleDirective,
+    textDirective: handleDirective
+  }
+}
+
+/** @type {FromMarkdownHandle} */
+function enterContainer(token) {
+  enter.call(this, 'containerDirective', token)
+}
+
+/** @type {FromMarkdownHandle} */
+function enterLeaf(token) {
+  enter.call(this, 'leafDirective', token)
+}
+
+/** @type {FromMarkdownHandle} */
+function enterText(token) {
+  enter.call(this, 'textDirective', token)
+}
+
+/**
+ * @this {CompileContext}
+ * @param {Directive['type']} type
+ * @param {Token} token
+ */
+function enter(type, token) {
+  this.enter({type, name: '', attributes: {}, children: []}, token)
+}
+
+/**
+ * @this {CompileContext}
+ * @param {Token} token
+ */
+function exitName(token) {
+  const node = /** @type {Directive} */ (this.stack[this.stack.length - 1])
+  node.name = this.sliceSerialize(token)
+}
+
+/** @type {FromMarkdownHandle} */
+function enterContainerLabel(token) {
+  this.enter(
+    {type: 'paragraph', data: {directiveLabel: true}, children: []},
+    token
+  )
+}
+
+/** @type {FromMarkdownHandle} */
+function exitContainerLabel(token) {
+  this.exit(token)
+}
+
+/** @type {FromMarkdownHandle} */
+function enterAttributes() {
+  this.setData('directiveAttributes', [])
+  this.buffer() // Capture EOLs
+}
+
+/** @type {FromMarkdownHandle} */
+function exitAttributeIdValue(token) {
+  const list = /** @type {Array.<[string, string]>} */ (
+    this.getData('directiveAttributes')
+  )
+  list.push(['id', parseEntities(this.sliceSerialize(token))])
+}
+
+/** @type {FromMarkdownHandle} */
+function exitAttributeClassValue(token) {
+  const list = /** @type {Array.<[string, string]>} */ (
+    this.getData('directiveAttributes')
+  )
+  list.push(['class', parseEntities(this.sliceSerialize(token))])
+}
+
+/** @type {FromMarkdownHandle} */
+function exitAttributeValue(token) {
+  const list = /** @type {Array.<[string, string]>} */ (
+    this.getData('directiveAttributes')
+  )
+  list[list.length - 1][1] = parseEntities(this.sliceSerialize(token))
+}
+
+/** @type {FromMarkdownHandle} */
+function exitAttributeName(token) {
+  const list = /** @type {Array.<[string, string]>} */ (
+    this.getData('directiveAttributes')
+  )
+
+  // Attribute names in CommonMark are significantly limited, so character
+  // references can’t exist.
+  list.push([this.sliceSerialize(token), ''])
+}
+
+/** @type {FromMarkdownHandle} */
+function exitAttributes() {
+  const list = /** @type {Array.<[string, string]>} */ (
+    this.getData('directiveAttributes')
+  )
+  /** @type {Record.<string, string>} */
+  const cleaned = {}
+  let index = -1
+
+  while (++index < list.length) {
+    const attribute = list[index]
+
+    if (attribute[0] === 'class' && cleaned.class) {
+      cleaned.class += ' ' + attribute[1]
+    } else {
+      cleaned[attribute[0]] = attribute[1]
+    }
+  }
+
+  this.setData('directiveAttributes')
+  this.resume() // Drop EOLs
+  const node = /** @type {Directive} */ (this.stack[this.stack.length - 1])
+  node.attributes = cleaned
+}
+
+/** @type {FromMarkdownHandle} */
+function exit(token) {
+  this.exit(token)
+}
+
+/**
+ * @type {ToMarkdownHandle}
+ * @param {Directive} node
+ */
+function handleDirective(node, _, context, safeOptions) {
+  const tracker = track(safeOptions)
+  const sequence = fence(node)
+  const exit = context.enter(node.type)
+  let value = tracker.move(sequence + (node.name || ''))
+  /** @type {Directive|Paragraph|undefined} */
+  let label = node
+
+  if (node.type === 'containerDirective') {
+    const head = (node.children || [])[0]
+    label = inlineDirectiveLabel(head) ? head : undefined
+  }
+
+  if (label && label.children && label.children.length > 0) {
+    const exit = context.enter('label')
+    const subexit = context.enter(node.type + 'Label')
+    value += tracker.move('[')
+    value += tracker.move(
+      containerPhrasing(label, context, {
+        ...tracker.current(),
+        before: value,
+        after: ']'
+      })
+    )
+    value += tracker.move(']')
+    subexit()
+    exit()
+  }
+
+  value += tracker.move(attributes(node, context))
+
+  if (node.type === 'containerDirective') {
+    const head = (node.children || [])[0]
+    let shallow = node
+
+    if (inlineDirectiveLabel(head)) {
+      shallow = Object.assign({}, node, {children: node.children.slice(1)})
+    }
+
+    if (shallow && shallow.children && shallow.children.length > 0) {
+      value += tracker.move('\n')
+      value += tracker.move(containerFlow(shallow, context, tracker.current()))
+    }
+
+    value += tracker.move('\n' + sequence)
+  }
+
+  exit()
+  return value
+}
+
+/** @type {ToMarkdownHandle} */
+function peekDirective() {
+  return ':'
+}
+
+/**
+ * @param {Directive} node
+ * @param {Context} context
+ * @returns {string}
+ */
+function attributes(node, context) {
+  const quote = checkQuote(context)
+  const subset = node.type === 'textDirective' ? [quote] : [quote, '\n', '\r']
+  const attrs = node.attributes || {}
+  /** @type {Array.<string>} */
+  const values = []
+  /** @type {string|undefined} */
+  let classesFull
+  /** @type {string|undefined} */
+  let classes
+  /** @type {string|undefined} */
+  let id
+  /** @type {string} */
+  let key
+
+  for (key in attrs) {
+    if (
+      own.call(attrs, key) &&
+      attrs[key] !== undefined &&
+      attrs[key] !== null
+    ) {
+      const value = String(attrs[key])
+
+      if (key === 'id') {
+        id = shortcut.test(value) ? '#' + value : quoted('id', value)
+      } else if (key === 'class') {
+        const list = value.split(/[\t\n\r ]+/g)
+        /** @type {Array.<string>} */
+        const classesFullList = []
+        /** @type {Array.<string>} */
+        const classesList = []
+        let index = -1
+
+        while (++index < list.length) {
+          ;(shortcut.test(list[index]) ? classesList : classesFullList).push(
+            list[index]
+          )
+        }
+
+        classesFull =
+          classesFullList.length > 0
+            ? quoted('class', classesFullList.join(' '))
+            : ''
+        classes = classesList.length > 0 ? '.' + classesList.join('.') : ''
+      } else {
+        values.push(quoted(key, value))
+      }
+    }
+  }
+
+  if (classesFull) {
+    values.unshift(classesFull)
+  }
+
+  if (classes) {
+    values.unshift(classes)
+  }
+
+  if (id) {
+    values.unshift(id)
+  }
+
+  return values.length > 0 ? '{' + values.join(' ') + '}' : ''
+
+  /**
+   * @param {string} key
+   * @param {string} value
+   * @returns {string}
+   */
+  function quoted(key, value) {
+    return (
+      key +
+      (value
+        ? '=' + quote + stringifyEntitiesLight(value, {subset}) + quote
+        : '')
+    )
+  }
+}
+
+/**
+ * @param {BlockContent} node
+ * @returns {node is Paragraph & {data: {directiveLabel: boolean}}}
+ */
+function inlineDirectiveLabel(node) {
+  return Boolean(
+    node && node.type === 'paragraph' && node.data && node.data.directiveLabel
+  )
+}
+
+/**
+ * @param {Directive} node
+ * @returns {string}
+ */
+function fence(node) {
+  let size = 0
+
+  if (node.type === 'containerDirective') {
+    visitParents(node, 'containerDirective', onvisit)
+    size += 3
+  } else if (node.type === 'leafDirective') {
+    size = 2
+  } else {
+    size = 1
+  }
+
+  return ':'.repeat(size)
+
+  /** @type {import('unist-util-visit-parents/complex-types').BuildVisitor<Root, Directive>} */
+  function onvisit(_, parents) {
+    let index = parents.length
+    let nesting = 0
+
+    while (index--) {
+      if (parents[index].type === 'containerDirective') {
+        nesting++
+      }
+    }
+
+    if (nesting > size) size = nesting
+  }
+}

+ 22 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/license

@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 447 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/readme.md

@@ -0,0 +1,447 @@
+# mdast-util-directive
+
+[![Build][build-badge]][build]
+[![Coverage][coverage-badge]][coverage]
+[![Downloads][downloads-badge]][downloads]
+[![Size][size-badge]][size]
+[![Sponsors][sponsors-badge]][collective]
+[![Backers][backers-badge]][collective]
+[![Chat][chat-badge]][chat]
+
+[mdast][] extensions to parse and serialize [generic directives proposal][prop]
+(`:cite[smith04]`, `::youtube[Video of a cat in a box]{v=01ab2cd3efg}`, and
+such).
+
+## Contents
+
+*   [What is this?](#what-is-this)
+*   [When to use this](#when-to-use-this)
+*   [Install](#install)
+*   [Use](#use)
+*   [API](#api)
+    *   [`directiveFromMarkdown`](#directivefrommarkdown)
+    *   [`directiveToMarkdown`](#directivetomarkdown)
+*   [Syntax tree](#syntax-tree)
+    *   [Nodes](#nodes)
+    *   [Mixin](#mixin)
+    *   [`Directive`](#directive)
+*   [Types](#types)
+*   [Compatibility](#compatibility)
+*   [Related](#related)
+*   [Contribute](#contribute)
+*   [License](#license)
+
+## What is this?
+
+This package contains extensions that add support for generic directives to
+[`mdast-util-from-markdown`][mdast-util-from-markdown] and
+[`mdast-util-to-markdown`][mdast-util-to-markdown].
+
+This package handles the syntax tree.
+You can use this with some more code to match your specific needs, to allow for
+anything from callouts, citations, styled blocks, forms, embeds, spoilers, etc.
+[Traverse the tree][traversal] to change directives to whatever you please.
+
+## When to use this
+
+These tools are all rather low-level.
+In most cases, you’d want to use [`remark-directive`][remark-directive] with
+remark instead.
+
+Directives are one of the four ways to extend markdown: an arbitrary extension
+syntax (see [Extending markdown][extending-mmarkdown] in micromark’s docs for
+the alternatives and more info).
+This mechanism works well when you control the content: who authors it, what
+tools handle it, and where it’s displayed.
+When authors can read a guide on how to embed a tweet but are not expected to
+know the ins and outs of HTML or JavaScript.
+Directives don’t work well if you don’t know who authors content, what tools
+handle it, and where it ends up.
+Example use cases are a docs website for a project or product, or blogging tools
+and static site generators.
+
+When working with `mdast-util-from-markdown`, you must combine this package with
+[`micromark-extension-directive`][extension].
+
+This utility does not handle how directives are turned to HTML.
+You must [traverse the tree][traversal] to change directives to whatever you
+please.
+
+## Install
+
+This package is [ESM only][esm].
+In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]:
+
+```sh
+npm install mdast-util-directive
+```
+
+In Deno with [`esm.sh`][esmsh]:
+
+```js
+import {directiveFromMarkdown, directiveToMarkdown} from 'https://esm.sh/mdast-util-directive@2'
+```
+
+In browsers with [`esm.sh`][esmsh]:
+
+```html
+<script type="module">
+  import {directiveFromMarkdown, directiveToMarkdown} from 'https://esm.sh/mdast-util-directive@2?bundle'
+</script>
+```
+
+## Use
+
+Say our document `example.md` contains:
+
+```markdown
+A lovely language know as :abbr[HTML]{title="HyperText Markup Language"}.
+```
+
+…and our module `example.js` looks as follows:
+
+```js
+import fs from 'node:fs/promises'
+import {fromMarkdown} from 'mdast-util-from-markdown'
+import {toMarkdown} from 'mdast-util-to-markdown'
+import {directive} from 'micromark-extension-directive'
+import {directiveFromMarkdown, directiveToMarkdown} from 'mdast-util-directive'
+
+const doc = await fs.readFile('example.md')
+
+const tree = fromMarkdown(doc, {
+  extensions: [directive()],
+  mdastExtensions: [directiveFromMarkdown]
+})
+
+console.log(tree)
+
+const out = toMarkdown(tree, {extensions: [directiveToMarkdown]})
+
+console.log(out)
+```
+
+…now running `node example.js` yields (positional info removed for brevity):
+
+```js
+{
+  type: 'root',
+  children: [
+    {
+      type: 'paragraph',
+      children: [
+        {type: 'text', value: 'A lovely language know as '},
+        {
+          type: 'textDirective',
+          name: 'abbr',
+          attributes: {title: 'HyperText Markup Language'},
+          children: [{type: 'text', value: 'HTML'}]
+        },
+        {type: 'text', value: '.'}
+      ]
+    }
+  ]
+}
+```
+
+```markdown
+A lovely language know as :abbr[HTML]{title="HyperText Markup Language"}.
+```
+
+## API
+
+This package exports the identifiers `directiveFromMarkdown` and
+`directiveToMarkdown`.
+There is no default export.
+
+### `directiveFromMarkdown`
+
+Extension for [`mdast-util-from-markdown`][mdast-util-from-markdown].
+
+### `directiveToMarkdown`
+
+Extension for [`mdast-util-to-markdown`][mdast-util-to-markdown].
+
+There are no options, but passing [`options.quote`][quote] to
+`mdast-util-to-markdown` is honored for attributes.
+
+## Syntax tree
+
+The following interfaces are added to **[mdast][]** by this utility.
+
+### Nodes
+
+#### `TextDirective`
+
+```idl
+interface TextDirective <: Parent {
+  type: 'textDirective'
+  children: [PhrasingContent]
+}
+
+TextDirective includes Directive
+```
+
+**TextDirective** (**[Parent][dfn-parent]**) is a directive.
+It can be used where **[phrasing][dfn-phrasing-content]** content is expected.
+Its content model is also **[phrasing][dfn-phrasing-content]** content.
+It includes the mixin **[Directive][dfn-mxn-directive]**.
+
+For example, the following Markdown:
+
+```markdown
+:name[Label]{#x.y.z key=value}
+```
+
+Yields:
+
+```js
+{
+  type: 'textDirective',
+  name: 'name',
+  attributes: {id: 'x', class: 'y z', key: 'value'},
+  children: [{type: 'text', value: 'Label'}]
+}
+```
+
+#### `LeafDirective`
+
+```idl
+interface LeafDirective <: Parent {
+  type: 'leafDirective'
+  children: [PhrasingContent]
+}
+
+LeafDirective includes Directive
+```
+
+**LeafDirective** (**[Parent][dfn-parent]**) is a directive.
+It can be used where **[flow][dfn-flow-content]** content is expected.
+Its content model is **[phrasing][dfn-phrasing-content]** content.
+It includes the mixin **[Directive][dfn-mxn-directive]**.
+
+For example, the following Markdown:
+
+```markdown
+::youtube[Label]{v=123}
+```
+
+Yields:
+
+```js
+{
+  type: 'leafDirective',
+  name: 'youtube',
+  attributes: {v: '123'},
+  children: [{type: 'text', value: 'Label'}]
+}
+```
+
+#### `ContainerDirective`
+
+```idl
+interface ContainerDirective <: Parent {
+  type: 'containerDirective'
+  children: [FlowContent]
+}
+
+ContainerDirective includes Directive
+```
+
+**ContainerDirective** (**[Parent][dfn-parent]**) is a directive.
+It can be used where **[flow][dfn-flow-content]** content is expected.
+Its content model is also **[flow][dfn-flow-content]** content.
+It includes the mixin **[Directive][dfn-mxn-directive]**.
+
+The phrasing in the label is, when available, added as a paragraph with a
+`directiveLabel: true` field, as the head of its content.
+
+For example, the following Markdown:
+
+```markdown
+:::spoiler[Open at your own peril]
+He dies.
+:::
+```
+
+Yields:
+
+```js
+{
+  type: 'containerDirective',
+  name: 'spoiler',
+  attributes: {},
+  children: [
+    {
+      type: 'paragraph',
+      data: {directiveLabel: true},
+      children: [{type: 'text', value: 'Open at your own peril'}]
+    },
+    {
+      type: 'paragraph',
+      children: [{type: 'text', value: 'He dies.'}]
+    }
+  ]
+}
+```
+
+### Mixin
+
+### `Directive`
+
+```idl
+interface mixin Directive {
+  name: string
+  attributes: Attributes?
+}
+
+interface Attributes {}
+typedef string AttributeName
+typedef string AttributeValue
+```
+
+**Directive** represents something defined by an extension.
+
+The `name` field must be present and represents an identifier of an extension.
+
+The `attributes` field represents information associated with the node.
+The value of the `attributes` field implements the **Attributes** interface.
+
+In the **Attributes** interface, every field must be an `AttributeName` and
+every value an `AttributeValue`.
+The fields and values can be anything: there are no semantics (such as by HTML
+or hast).
+
+> In JSON, the value `null` must be treated as if the attribute was not
+> included.
+> In JavaScript, both `null` and `undefined` must be similarly ignored.
+
+## Types
+
+This package is fully typed with [TypeScript][].
+It exports the additional types `ContainerDirective`, `LeafDirective`,
+`TextDirective`, and `Directive`.
+
+It also registers the node types with `@types/mdast`.
+If you’re working with the syntax tree, make sure to import this utility
+somewhere in your types, as that registers the new node types in the tree.
+
+```js
+/**
+ * @typedef {import('mdast-util-directive')}
+ */
+
+import {visit} from 'unist-util-visit'
+
+/** @type {import('mdast').Root} */
+const tree = getMdastNodeSomeHow()
+
+visit(tree, (node) => {
+  // `node` can now be one of the nodes for directives.
+})
+```
+
+## Compatibility
+
+Projects maintained by the unified collective are compatible with all maintained
+versions of Node.js.
+As of now, that is Node.js 12.20+, 14.14+, and 16.0+.
+Our projects sometimes work with older versions, but this is not guaranteed.
+
+This plugin works with `mdast-util-from-markdown` version 1+ and
+`mdast-util-to-markdown` version 1+.
+
+## Related
+
+*   [`remarkjs/remark-directive`][remark-directive]
+    — remark plugin to support generic directives
+*   [`micromark/micromark-extension-directive`][extension]
+    — micromark extension to parse directives
+
+## Contribute
+
+See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for
+ways to get started.
+See [`support.md`][support] for ways to get help.
+
+This project has a [code of conduct][coc].
+By interacting with this repository, organization, or community you agree to
+abide by its terms.
+
+## License
+
+[MIT][license] © [Titus Wormer][author]
+
+<!-- Definitions -->
+
+[build-badge]: https://github.com/syntax-tree/mdast-util-directive/workflows/main/badge.svg
+
+[build]: https://github.com/syntax-tree/mdast-util-directive/actions
+
+[coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/mdast-util-directive.svg
+
+[coverage]: https://codecov.io/github/syntax-tree/mdast-util-directive
+
+[downloads-badge]: https://img.shields.io/npm/dm/mdast-util-directive.svg
+
+[downloads]: https://www.npmjs.com/package/mdast-util-directive
+
+[size-badge]: https://img.shields.io/bundlephobia/minzip/mdast-util-directive.svg
+
+[size]: https://bundlephobia.com/result?p=mdast-util-directive
+
+[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg
+
+[backers-badge]: https://opencollective.com/unified/backers/badge.svg
+
+[collective]: https://opencollective.com/unified
+
+[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg
+
+[chat]: https://github.com/syntax-tree/unist/discussions
+
+[npm]: https://docs.npmjs.com/cli/install
+
+[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
+
+[esmsh]: https://esm.sh
+
+[typescript]: https://www.typescriptlang.org
+
+[license]: license
+
+[author]: https://wooorm.com
+
+[health]: https://github.com/syntax-tree/.github
+
+[contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md
+
+[support]: https://github.com/syntax-tree/.github/blob/main/support.md
+
+[coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md
+
+[mdast]: https://github.com/syntax-tree/mdast
+
+[mdast-util-from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown
+
+[mdast-util-to-markdown]: https://github.com/syntax-tree/mdast-util-to-markdown
+
+[quote]: https://github.com/syntax-tree/mdast-util-to-markdown#optionsquote
+
+[extension]: https://github.com/micromark/micromark-extension-directive
+
+[remark-directive]: https://github.com/remarkjs/remark-directive
+
+[extending-mmarkdown]: https://github.com/micromark/micromark#extending-markdown
+
+[prop]: https://talk.commonmark.org/t/generic-directives-plugins-syntax/444
+
+[traversal]: https://unifiedjs.com/learn/recipe/tree-traversal/
+
+[dfn-parent]: https://github.com/syntax-tree/mdast#parent
+
+[dfn-flow-content]: https://github.com/syntax-tree/mdast#flowcontent
+
+[dfn-phrasing-content]: https://github.com/syntax-tree/mdast#phrasingcontent
+
+[dfn-mxn-directive]: #directive

+ 909 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/test.js

@@ -0,0 +1,909 @@
+import test from 'tape'
+import {fromMarkdown} from 'mdast-util-from-markdown'
+import {toMarkdown} from 'mdast-util-to-markdown'
+import {removePosition} from 'unist-util-remove-position'
+import {directive} from 'micromark-extension-directive'
+import {directiveFromMarkdown, directiveToMarkdown} from './index.js'
+
+test('markdown -> mdast', (t) => {
+  t.deepEqual(
+    fromMarkdown('a :b[c]{d} e.', {
+      extensions: [directive()],
+      mdastExtensions: [directiveFromMarkdown]
+    }).children[0],
+    {
+      type: 'paragraph',
+      children: [
+        {
+          type: 'text',
+          value: 'a ',
+          position: {
+            start: {line: 1, column: 1, offset: 0},
+            end: {line: 1, column: 3, offset: 2}
+          }
+        },
+        {
+          type: 'textDirective',
+          name: 'b',
+          attributes: {d: ''},
+          children: [
+            {
+              type: 'text',
+              value: 'c',
+              position: {
+                start: {line: 1, column: 6, offset: 5},
+                end: {line: 1, column: 7, offset: 6}
+              }
+            }
+          ],
+          position: {
+            start: {line: 1, column: 3, offset: 2},
+            end: {line: 1, column: 11, offset: 10}
+          }
+        },
+        {
+          type: 'text',
+          value: ' e.',
+          position: {
+            start: {line: 1, column: 11, offset: 10},
+            end: {line: 1, column: 14, offset: 13}
+          }
+        }
+      ],
+      position: {
+        start: {line: 1, column: 1, offset: 0},
+        end: {line: 1, column: 14, offset: 13}
+      }
+    },
+    'should support directives (text)'
+  )
+
+  t.deepEqual(
+    fromMarkdown('::a[b]{c}', {
+      extensions: [directive()],
+      mdastExtensions: [directiveFromMarkdown]
+    }).children[0],
+    {
+      type: 'leafDirective',
+      name: 'a',
+      attributes: {c: ''},
+      children: [
+        {
+          type: 'text',
+          value: 'b',
+          position: {
+            start: {line: 1, column: 5, offset: 4},
+            end: {line: 1, column: 6, offset: 5}
+          }
+        }
+      ],
+      position: {
+        start: {line: 1, column: 1, offset: 0},
+        end: {line: 1, column: 10, offset: 9}
+      }
+    },
+    'should support directives (leaf)'
+  )
+
+  t.deepEqual(
+    fromMarkdown(':::a[b]{c}\nd', {
+      extensions: [directive()],
+      mdastExtensions: [directiveFromMarkdown]
+    }).children[0],
+    {
+      type: 'containerDirective',
+      name: 'a',
+      attributes: {c: ''},
+      children: [
+        {
+          type: 'paragraph',
+          data: {directiveLabel: true},
+          children: [
+            {
+              type: 'text',
+              value: 'b',
+              position: {
+                start: {line: 1, column: 6, offset: 5},
+                end: {line: 1, column: 7, offset: 6}
+              }
+            }
+          ],
+          position: {
+            start: {line: 1, column: 5, offset: 4},
+            end: {line: 1, column: 8, offset: 7}
+          }
+        },
+        {
+          type: 'paragraph',
+          children: [
+            {
+              type: 'text',
+              value: 'd',
+              position: {
+                start: {line: 2, column: 1, offset: 11},
+                end: {line: 2, column: 2, offset: 12}
+              }
+            }
+          ],
+          position: {
+            start: {line: 2, column: 1, offset: 11},
+            end: {line: 2, column: 2, offset: 12}
+          }
+        }
+      ],
+      position: {
+        start: {line: 1, column: 1, offset: 0},
+        end: {line: 2, column: 2, offset: 12}
+      }
+    },
+    'should support directives (container)'
+  )
+
+  t.deepEqual(
+    removePosition(
+      fromMarkdown(':a[b *c*\nd]', {
+        extensions: [directive()],
+        mdastExtensions: [directiveFromMarkdown]
+      }),
+      true
+    ),
+    {
+      type: 'root',
+      children: [
+        {
+          type: 'paragraph',
+          children: [
+            {
+              type: 'textDirective',
+              name: 'a',
+              attributes: {},
+              children: [
+                {type: 'text', value: 'b '},
+                {type: 'emphasis', children: [{type: 'text', value: 'c'}]},
+                {type: 'text', value: '\nd'}
+              ]
+            }
+          ]
+        }
+      ]
+    },
+    'should support content in a label'
+  )
+
+  t.deepEqual(
+    removePosition(
+      fromMarkdown(':a{#b.c.d e=f g="h&amp;i&unknown;j"}', {
+        extensions: [directive()],
+        mdastExtensions: [directiveFromMarkdown]
+      }),
+      true
+    ),
+    {
+      type: 'root',
+      children: [
+        {
+          type: 'paragraph',
+          children: [
+            {
+              type: 'textDirective',
+              name: 'a',
+              attributes: {id: 'b', class: 'c d', e: 'f', g: 'h&i&unknown;j'},
+              children: []
+            }
+          ]
+        }
+      ]
+    },
+    'should support attributes'
+  )
+
+  t.deepEqual(
+    removePosition(
+      fromMarkdown(':a{b\nc="d\ne"}', {
+        extensions: [directive()],
+        mdastExtensions: [directiveFromMarkdown]
+      }),
+      true
+    ),
+    {
+      type: 'root',
+      children: [
+        {
+          type: 'paragraph',
+          children: [
+            {
+              type: 'textDirective',
+              name: 'a',
+              attributes: {b: '', c: 'd\ne'},
+              children: []
+            }
+          ]
+        }
+      ]
+    },
+    'should support EOLs in attributes'
+  )
+
+  t.deepEqual(
+    removePosition(
+      fromMarkdown('::::a\n:::b\n:c\n:::\n::::', {
+        extensions: [directive()],
+        mdastExtensions: [directiveFromMarkdown]
+      }),
+      true
+    ),
+    {
+      type: 'root',
+      children: [
+        {
+          type: 'containerDirective',
+          name: 'a',
+          attributes: {},
+          children: [
+            {
+              type: 'containerDirective',
+              name: 'b',
+              attributes: {},
+              children: [
+                {
+                  type: 'paragraph',
+                  children: [
+                    {
+                      type: 'textDirective',
+                      name: 'c',
+                      attributes: {},
+                      children: []
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    },
+    'should support directives in directives'
+  )
+
+  t.end()
+})
+
+test('mdast -> markdown', (t) => {
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          // @ts-expect-error: `children`, `name` missing.
+          {type: 'textDirective'},
+          {type: 'text', value: ' b.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a : b.\n',
+    'should try to serialize a directive (text) w/o `name`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          // @ts-expect-error: `children` missing.
+          {type: 'textDirective', name: 'b'},
+          {type: 'text', value: ' c.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b c.\n',
+    'should serialize a directive (text) w/ `name`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            children: [{type: 'text', value: 'c'}]
+          },
+          {type: 'text', value: ' d.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b[c] d.\n',
+    'should serialize a directive (text) w/ `children`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            children: [{type: 'text', value: 'c[d]e'}]
+          },
+          {type: 'text', value: ' f.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b[c\\[d\\]e] f.\n',
+    'should escape brackets in a directive (text) label'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            children: [{type: 'text', value: 'c\nd'}]
+          },
+          {type: 'text', value: ' e.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b[c\nd] e.\n',
+    'should support EOLs in a directive (text) label'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            // @ts-expect-error: should contain only `string`s
+            attributes: {c: 'd', e: 'f', g: '', h: null, i: undefined, j: 2},
+            children: []
+          },
+          {type: 'text', value: ' k.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b{c="d" e="f" g j="2"} k.\n',
+    'should serialize a directive (text) w/ `attributes`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            attributes: {class: 'a b\nc', id: 'd', key: 'value'},
+            children: []
+          },
+          {type: 'text', value: ' k.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b{#d .a.b.c key="value"} k.\n',
+    'should serialize a directive (text) w/ `id`, `class` attributes'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            attributes: {x: 'y"\'\r\nz'},
+            children: []
+          },
+          {type: 'text', value: ' k.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b{x="y&#x22;\'\r\nz"} k.\n',
+    'should encode the quote in an attribute value (text)'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            attributes: {x: 'y"\'\r\nz'},
+            children: []
+          },
+          {type: 'text', value: ' k.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b{x="y&#x22;\'\r\nz"} k.\n',
+    'should encode the quote in an attribute value (text)'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            attributes: {id: 'c#d'},
+            children: []
+          },
+          {type: 'text', value: ' e.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b{id="c#d"} e.\n',
+    'should not use the `id` shortcut if impossible characters exist'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            attributes: {class: 'c.d e<f'},
+            children: []
+          },
+          {type: 'text', value: ' g.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b{class="c.d e<f"} g.\n',
+    'should not use the `class` shortcut if impossible characters exist'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'text', value: 'a '},
+          {
+            type: 'textDirective',
+            name: 'b',
+            attributes: {class: 'c.d e f<g hij'},
+            children: []
+          },
+          {type: 'text', value: ' k.'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a :b{.e.hij class="c.d f<g"} k.\n',
+    'should not use the `class` shortcut if impossible characters exist (but should use it for classes that don’t)'
+  )
+
+  t.deepEqual(
+    // @ts-expect-error: `children`, `name` missing.
+    toMarkdown({type: 'leafDirective'}, {extensions: [directiveToMarkdown]}),
+    '::\n',
+    'should try to serialize a directive (leaf) w/o `name`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      // @ts-expect-error: `children` missing.
+      {type: 'leafDirective', name: 'a'},
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::a\n',
+    'should serialize a directive (leaf) w/ `name`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'leafDirective',
+        name: 'a',
+        children: [{type: 'text', value: 'b'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::a[b]\n',
+    'should serialize a directive (leaf) w/ `children`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'leafDirective',
+        name: 'a',
+        children: [{type: 'text', value: 'b'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::a[b]\n',
+    'should serialize a directive (leaf) w/ `children`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'leafDirective',
+        name: 'a',
+        children: [{type: 'text', value: 'b\nc'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::a[b&#xA;c]\n',
+    'should serialize a directive (leaf) w/ EOLs in `children`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'leafDirective',
+        name: 'a',
+        attributes: {id: 'b', class: 'c d', key: 'e\nf'},
+        children: []
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::a{#b .c.d key="e&#xA;f"}\n',
+    'should serialize a directive (leaf) w/ EOLs in `attributes`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      // @ts-expect-error: `children`, `name` missing.
+      {type: 'containerDirective'},
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::\n:::\n',
+    'should try to serialize a directive (container) w/o `name`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      // @ts-expect-error: `children` missing.
+      {type: 'containerDirective', name: 'a'},
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::a\n:::\n',
+    'should serialize a directive (container) w/ `name`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [{type: 'paragraph', children: [{type: 'text', value: 'b'}]}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::a\nb\n:::\n',
+    'should serialize a directive (container) w/ `children`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [
+          {type: 'heading', depth: 1, children: [{type: 'text', value: 'b'}]}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::a\n# b\n:::\n',
+    'should serialize a directive (container) w/ `children` (heading)'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [
+          {type: 'paragraph', children: [{type: 'text', value: 'b\nc'}]}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::a\nb\nc\n:::\n',
+    'should serialize a directive (container) w/ EOLs in `children`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        attributes: {id: 'b', class: 'c d', key: 'e\nf'},
+        children: []
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::a{#b .c.d key="e&#xA;f"}\n:::\n',
+    'should serialize a directive (container) w/ EOLs in `attributes`'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [
+          {
+            type: 'paragraph',
+            data: {directiveLabel: true},
+            children: [{type: 'text', value: 'b'}]
+          }
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::a[b]\n:::\n',
+    'should serialize the first paragraph w/ `data.directiveLabel` as a label in a directive (container)'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [
+          {
+            type: 'containerDirective',
+            name: 'b',
+            children: [
+              {
+                type: 'paragraph',
+                children: [{type: 'text', value: 'c'}]
+              }
+            ]
+          }
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::::a\n:::b\nc\n:::\n::::\n',
+    'should serialize the outer containers w/ more colons than inner containers'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [
+          {
+            type: 'containerDirective',
+            name: 'b',
+            children: [
+              {
+                type: 'paragraph',
+                children: [{type: 'text', value: 'c'}]
+              }
+            ]
+          },
+          {
+            type: 'containerDirective',
+            name: 'd',
+            children: [
+              {
+                type: 'paragraph',
+                children: [{type: 'text', value: 'e'}]
+              }
+            ]
+          }
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::::a\n:::b\nc\n:::\n\n:::d\ne\n:::\n::::\n',
+    'should serialize w/ `3 + nesting`, not the total count (1)'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [
+          {
+            type: 'containerDirective',
+            name: 'b',
+            children: [
+              {
+                type: 'containerDirective',
+                name: 'c',
+                children: [
+                  {
+                    type: 'paragraph',
+                    children: [{type: 'text', value: 'd'}]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':::::a\n::::b\n:::c\nd\n:::\n::::\n:::::\n',
+    'should serialize w/ `3 + nesting`, not the total count (2)'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'containerDirective',
+        name: 'a',
+        children: [
+          {
+            type: 'blockquote',
+            children: [
+              {
+                type: 'containerDirective',
+                name: 'b',
+                children: [
+                  {
+                    type: 'paragraph',
+                    children: [{type: 'text', value: 'c'}]
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '::::a\n> :::b\n> c\n> :::\n::::\n',
+    'should serialize w/ `3 + nesting`, not the total count (3)'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: 'a:b'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a\\:b\n',
+    'should escape a `:` in phrasing when followed by an alpha'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: 'a:9'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a:9\n',
+    'should not escape a `:` in phrasing when followed by a non-alpha'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: 'a::c'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    'a::c\n',
+    'should not escape a `:` in phrasing when preceded by a colon'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: ':\na'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':\na\n',
+    'should not escape a `:` at a break'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: ':a'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '\\:a\n',
+    'should not escape a `:` at a break when followed by an alpha'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: '::\na'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '\\::\na\n',
+    'should escape a `:` at a break when followed by a colon'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: ':::\na'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '\\:::\na\n',
+    'should escape a `:` at a break when followed by two colons'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [{type: 'text', value: ':::\na'}]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    '\\:::\na\n',
+    'should escape a `:` at a break when followed by two colons'
+  )
+
+  t.deepEqual(
+    toMarkdown(
+      {
+        type: 'paragraph',
+        children: [
+          {type: 'textDirective', name: 'red', children: []},
+          {type: 'text', value: ':'}
+        ]
+      },
+      {extensions: [directiveToMarkdown]}
+    ),
+    ':red:\n',
+    'should escape a `:` after a text directive'
+  )
+
+  t.end()
+})

+ 7 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/index.js

@@ -0,0 +1,7 @@
+/**
+ * @typedef {import('./lib/html.js').Handle} Handle
+ * @typedef {import('./lib/html.js').HtmlOptions} HtmlOptions
+ */
+
+export {directive} from './lib/syntax.js'
+export {directiveHtml} from './lib/html.js'

+ 294 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/directive-container.js

@@ -0,0 +1,294 @@
+/**
+ * @typedef {import('micromark-util-types').Construct} Construct
+ * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
+ * @typedef {import('micromark-util-types').State} State
+ * @typedef {import('micromark-util-types').Token} Token
+ */
+
+import {ok as assert} from 'uvu/assert'
+import {factorySpace} from 'micromark-factory-space'
+import {markdownLineEnding} from 'micromark-util-character'
+import {codes} from 'micromark-util-symbol/codes.js'
+import {constants} from 'micromark-util-symbol/constants.js'
+import {types} from 'micromark-util-symbol/types.js'
+import {factoryAttributes} from './factory-attributes.js'
+import {factoryLabel} from './factory-label.js'
+import {factoryName} from './factory-name.js'
+
+/** @type {Construct} */
+export const directiveContainer = {
+  tokenize: tokenizeDirectiveContainer,
+  concrete: true
+}
+
+const label = {tokenize: tokenizeLabel, partial: true}
+const attributes = {tokenize: tokenizeAttributes, partial: true}
+const nonLazyLine = {tokenize: tokenizeNonLazyLine, partial: true}
+
+/** @type {Tokenizer} */
+function tokenizeDirectiveContainer(effects, ok, nok) {
+  const self = this
+  const tail = self.events[self.events.length - 1]
+  const initialSize =
+    tail && tail[1].type === types.linePrefix
+      ? tail[2].sliceSerialize(tail[1], true).length
+      : 0
+  let sizeOpen = 0
+  /** @type {Token} */
+  let previous
+
+  return start
+
+  /** @type {State} */
+  function start(code) {
+    assert(code === codes.colon, 'expected `:`')
+    effects.enter('directiveContainer')
+    effects.enter('directiveContainerFence')
+    effects.enter('directiveContainerSequence')
+    return sequenceOpen(code)
+  }
+
+  /** @type {State} */
+  function sequenceOpen(code) {
+    if (code === codes.colon) {
+      effects.consume(code)
+      sizeOpen++
+      return sequenceOpen
+    }
+
+    if (sizeOpen < constants.codeFencedSequenceSizeMin) {
+      return nok(code)
+    }
+
+    effects.exit('directiveContainerSequence')
+    return factoryName.call(
+      self,
+      effects,
+      afterName,
+      nok,
+      'directiveContainerName'
+    )(code)
+  }
+
+  /** @type {State} */
+  function afterName(code) {
+    return code === codes.leftSquareBracket
+      ? effects.attempt(label, afterLabel, afterLabel)(code)
+      : afterLabel(code)
+  }
+
+  /** @type {State} */
+  function afterLabel(code) {
+    return code === codes.leftCurlyBrace
+      ? effects.attempt(attributes, afterAttributes, afterAttributes)(code)
+      : afterAttributes(code)
+  }
+
+  /** @type {State} */
+  function afterAttributes(code) {
+    return factorySpace(effects, openAfter, types.whitespace)(code)
+  }
+
+  /** @type {State} */
+  function openAfter(code) {
+    effects.exit('directiveContainerFence')
+
+    if (code === codes.eof) {
+      return afterOpening(code)
+    }
+
+    if (markdownLineEnding(code)) {
+      if (self.interrupt) {
+        return ok(code)
+      }
+
+      return effects.attempt(nonLazyLine, contentStart, afterOpening)(code)
+    }
+
+    return nok(code)
+  }
+
+  /** @type {State} */
+  function afterOpening(code) {
+    effects.exit('directiveContainer')
+    return ok(code)
+  }
+
+  /** @type {State} */
+  function contentStart(code) {
+    if (code === codes.eof) {
+      effects.exit('directiveContainer')
+      return ok(code)
+    }
+
+    effects.enter('directiveContainerContent')
+    return lineStart(code)
+  }
+
+  /** @type {State} */
+  function lineStart(code) {
+    if (code === codes.eof) {
+      return after(code)
+    }
+
+    return effects.attempt(
+      {tokenize: tokenizeClosingFence, partial: true},
+      after,
+      initialSize
+        ? factorySpace(effects, chunkStart, types.linePrefix, initialSize + 1)
+        : chunkStart
+    )(code)
+  }
+
+  /** @type {State} */
+  function chunkStart(code) {
+    if (code === codes.eof) {
+      return after(code)
+    }
+
+    const token = effects.enter(types.chunkDocument, {
+      contentType: constants.contentTypeDocument,
+      previous
+    })
+    if (previous) previous.next = token
+    previous = token
+    return contentContinue(code)
+  }
+
+  /** @type {State} */
+  function contentContinue(code) {
+    if (code === codes.eof) {
+      const t = effects.exit(types.chunkDocument)
+      self.parser.lazy[t.start.line] = false
+      return after(code)
+    }
+
+    if (markdownLineEnding(code)) {
+      return effects.check(nonLazyLine, nonLazyLineAfter, lineAfter)(code)
+    }
+
+    effects.consume(code)
+    return contentContinue
+  }
+
+  /** @type {State} */
+  function nonLazyLineAfter(code) {
+    effects.consume(code)
+    const t = effects.exit(types.chunkDocument)
+    self.parser.lazy[t.start.line] = false
+    return lineStart
+  }
+
+  /** @type {State} */
+  function lineAfter(code) {
+    const t = effects.exit(types.chunkDocument)
+    self.parser.lazy[t.start.line] = false
+    return after(code)
+  }
+
+  /** @type {State} */
+  function after(code) {
+    effects.exit('directiveContainerContent')
+    effects.exit('directiveContainer')
+    return ok(code)
+  }
+
+  /** @type {Tokenizer} */
+  function tokenizeClosingFence(effects, ok, nok) {
+    let size = 0
+
+    return factorySpace(
+      effects,
+      closingPrefixAfter,
+      types.linePrefix,
+      constants.tabSize
+    )
+
+    /** @type {State} */
+    function closingPrefixAfter(code) {
+      effects.enter('directiveContainerFence')
+      effects.enter('directiveContainerSequence')
+      return closingSequence(code)
+    }
+
+    /** @type {State} */
+    function closingSequence(code) {
+      if (code === codes.colon) {
+        effects.consume(code)
+        size++
+        return closingSequence
+      }
+
+      if (size < sizeOpen) return nok(code)
+      effects.exit('directiveContainerSequence')
+      return factorySpace(effects, closingSequenceEnd, types.whitespace)(code)
+    }
+
+    /** @type {State} */
+    function closingSequenceEnd(code) {
+      if (code === codes.eof || markdownLineEnding(code)) {
+        effects.exit('directiveContainerFence')
+        return ok(code)
+      }
+
+      return nok(code)
+    }
+  }
+}
+
+/** @type {Tokenizer} */
+function tokenizeLabel(effects, ok, nok) {
+  // Always a `[`
+  return factoryLabel(
+    effects,
+    ok,
+    nok,
+    'directiveContainerLabel',
+    'directiveContainerLabelMarker',
+    'directiveContainerLabelString',
+    true
+  )
+}
+
+/** @type {Tokenizer} */
+function tokenizeAttributes(effects, ok, nok) {
+  // Always a `{`
+  return factoryAttributes(
+    effects,
+    ok,
+    nok,
+    'directiveContainerAttributes',
+    'directiveContainerAttributesMarker',
+    'directiveContainerAttribute',
+    'directiveContainerAttributeId',
+    'directiveContainerAttributeClass',
+    'directiveContainerAttributeName',
+    'directiveContainerAttributeInitializerMarker',
+    'directiveContainerAttributeValueLiteral',
+    'directiveContainerAttributeValue',
+    'directiveContainerAttributeValueMarker',
+    'directiveContainerAttributeValueData',
+    true
+  )
+}
+
+/** @type {Tokenizer} */
+function tokenizeNonLazyLine(effects, ok, nok) {
+  const self = this
+
+  return start
+
+  /** @type {State} */
+  function start(code) {
+    assert(markdownLineEnding(code), 'expected eol')
+    effects.enter(types.lineEnding)
+    effects.consume(code)
+    effects.exit(types.lineEnding)
+    return lineStart
+  }
+
+  /** @type {State} */
+  function lineStart(code) {
+    return self.parser.lazy[self.now().line] ? nok(code) : ok(code)
+  }
+}

+ 118 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/directive-leaf.js

@@ -0,0 +1,118 @@
+/**
+ * @typedef {import('micromark-util-types').Construct} Construct
+ * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
+ * @typedef {import('micromark-util-types').State} State
+ */
+
+import {ok as assert} from 'uvu/assert'
+import {factorySpace} from 'micromark-factory-space'
+import {markdownLineEnding} from 'micromark-util-character'
+import {codes} from 'micromark-util-symbol/codes.js'
+import {types} from 'micromark-util-symbol/types.js'
+import {factoryAttributes} from './factory-attributes.js'
+import {factoryLabel} from './factory-label.js'
+import {factoryName} from './factory-name.js'
+
+/** @type {Construct} */
+export const directiveLeaf = {tokenize: tokenizeDirectiveLeaf}
+
+const label = {tokenize: tokenizeLabel, partial: true}
+const attributes = {tokenize: tokenizeAttributes, partial: true}
+
+/** @type {Tokenizer} */
+function tokenizeDirectiveLeaf(effects, ok, nok) {
+  const self = this
+
+  return start
+
+  /** @type {State} */
+  function start(code) {
+    assert(code === codes.colon, 'expected `:`')
+    effects.enter('directiveLeaf')
+    effects.enter('directiveLeafSequence')
+    effects.consume(code)
+    return inStart
+  }
+
+  /** @type {State} */
+  function inStart(code) {
+    if (code === codes.colon) {
+      effects.consume(code)
+      effects.exit('directiveLeafSequence')
+      return factoryName.call(
+        self,
+        effects,
+        afterName,
+        nok,
+        'directiveLeafName'
+      )
+    }
+
+    return nok(code)
+  }
+
+  /** @type {State} */
+  function afterName(code) {
+    return code === codes.leftSquareBracket
+      ? effects.attempt(label, afterLabel, afterLabel)(code)
+      : afterLabel(code)
+  }
+
+  /** @type {State} */
+  function afterLabel(code) {
+    return code === codes.leftCurlyBrace
+      ? effects.attempt(attributes, afterAttributes, afterAttributes)(code)
+      : afterAttributes(code)
+  }
+
+  /** @type {State} */
+  function afterAttributes(code) {
+    return factorySpace(effects, end, types.whitespace)(code)
+  }
+
+  /** @type {State} */
+  function end(code) {
+    if (code === codes.eof || markdownLineEnding(code)) {
+      effects.exit('directiveLeaf')
+      return ok(code)
+    }
+
+    return nok(code)
+  }
+}
+
+/** @type {Tokenizer} */
+function tokenizeLabel(effects, ok, nok) {
+  // Always a `[`
+  return factoryLabel(
+    effects,
+    ok,
+    nok,
+    'directiveLeafLabel',
+    'directiveLeafLabelMarker',
+    'directiveLeafLabelString',
+    true
+  )
+}
+
+/** @type {Tokenizer} */
+function tokenizeAttributes(effects, ok, nok) {
+  // Always a `{`
+  return factoryAttributes(
+    effects,
+    ok,
+    nok,
+    'directiveLeafAttributes',
+    'directiveLeafAttributesMarker',
+    'directiveLeafAttribute',
+    'directiveLeafAttributeId',
+    'directiveLeafAttributeClass',
+    'directiveLeafAttributeName',
+    'directiveLeafAttributeInitializerMarker',
+    'directiveLeafAttributeValueLiteral',
+    'directiveLeafAttributeValue',
+    'directiveLeafAttributeValueMarker',
+    'directiveLeafAttributeValueData',
+    true
+  )
+}

+ 105 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/directive-text.js

@@ -0,0 +1,105 @@
+/**
+ * @typedef {import('micromark-util-types').Construct} Construct
+ * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
+ * @typedef {import('micromark-util-types').Previous} Previous
+ * @typedef {import('micromark-util-types').State} State
+ */
+
+import {ok as assert} from 'uvu/assert'
+import {codes} from 'micromark-util-symbol/codes.js'
+import {types} from 'micromark-util-symbol/types.js'
+import {factoryAttributes} from './factory-attributes.js'
+import {factoryLabel} from './factory-label.js'
+import {factoryName} from './factory-name.js'
+
+/** @type {Construct} */
+export const directiveText = {
+  tokenize: tokenizeDirectiveText,
+  previous
+}
+
+const label = {tokenize: tokenizeLabel, partial: true}
+const attributes = {tokenize: tokenizeAttributes, partial: true}
+
+/** @type {Previous} */
+function previous(code) {
+  // If there is a previous code, there will always be a tail.
+  return (
+    code !== codes.colon ||
+    this.events[this.events.length - 1][1].type === types.characterEscape
+  )
+}
+
+/** @type {Tokenizer} */
+function tokenizeDirectiveText(effects, ok, nok) {
+  const self = this
+
+  return start
+
+  /** @type {State} */
+  function start(code) {
+    assert(code === codes.colon, 'expected `:`')
+    assert(previous.call(self, self.previous), 'expected correct previous')
+    effects.enter('directiveText')
+    effects.enter('directiveTextMarker')
+    effects.consume(code)
+    effects.exit('directiveTextMarker')
+    return factoryName.call(self, effects, afterName, nok, 'directiveTextName')
+  }
+
+  /** @type {State} */
+  function afterName(code) {
+    return code === codes.colon
+      ? nok(code)
+      : code === codes.leftSquareBracket
+      ? effects.attempt(label, afterLabel, afterLabel)(code)
+      : afterLabel(code)
+  }
+
+  /** @type {State} */
+  function afterLabel(code) {
+    return code === codes.leftCurlyBrace
+      ? effects.attempt(attributes, afterAttributes, afterAttributes)(code)
+      : afterAttributes(code)
+  }
+
+  /** @type {State} */
+  function afterAttributes(code) {
+    effects.exit('directiveText')
+    return ok(code)
+  }
+}
+
+/** @type {Tokenizer} */
+function tokenizeLabel(effects, ok, nok) {
+  // Always a `[`
+  return factoryLabel(
+    effects,
+    ok,
+    nok,
+    'directiveTextLabel',
+    'directiveTextLabelMarker',
+    'directiveTextLabelString'
+  )
+}
+
+/** @type {Tokenizer} */
+function tokenizeAttributes(effects, ok, nok) {
+  // Always a `{`
+  return factoryAttributes(
+    effects,
+    ok,
+    nok,
+    'directiveTextAttributes',
+    'directiveTextAttributesMarker',
+    'directiveTextAttribute',
+    'directiveTextAttributeId',
+    'directiveTextAttributeClass',
+    'directiveTextAttributeName',
+    'directiveTextAttributeInitializerMarker',
+    'directiveTextAttributeValueLiteral',
+    'directiveTextAttributeValue',
+    'directiveTextAttributeValueMarker',
+    'directiveTextAttributeValueData'
+  )
+}

+ 336 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/factory-attributes.js

@@ -0,0 +1,336 @@
+/**
+ * @typedef {import('micromark-util-types').Effects} Effects
+ * @typedef {import('micromark-util-types').State} State
+ * @typedef {import('micromark-util-types').Code} Code
+ */
+
+import {ok as assert} from 'uvu/assert'
+import {factorySpace} from 'micromark-factory-space'
+import {factoryWhitespace} from 'micromark-factory-whitespace'
+import {
+  asciiAlpha,
+  asciiAlphanumeric,
+  markdownLineEnding,
+  markdownLineEndingOrSpace,
+  markdownSpace
+} from 'micromark-util-character'
+import {codes} from 'micromark-util-symbol/codes.js'
+import {types} from 'micromark-util-symbol/types.js'
+
+/**
+ * @param {Effects} effects
+ * @param {State} ok
+ * @param {State} nok
+ * @param {string} attributesType
+ * @param {string} attributesMarkerType
+ * @param {string} attributeType
+ * @param {string} attributeIdType
+ * @param {string} attributeClassType
+ * @param {string} attributeNameType
+ * @param {string} attributeInitializerType
+ * @param {string} attributeValueLiteralType
+ * @param {string} attributeValueType
+ * @param {string} attributeValueMarker
+ * @param {string} attributeValueData
+ * @param {boolean} [disallowEol=false]
+ */
+/* eslint-disable-next-line max-params */
+export function factoryAttributes(
+  effects,
+  ok,
+  nok,
+  attributesType,
+  attributesMarkerType,
+  attributeType,
+  attributeIdType,
+  attributeClassType,
+  attributeNameType,
+  attributeInitializerType,
+  attributeValueLiteralType,
+  attributeValueType,
+  attributeValueMarker,
+  attributeValueData,
+  disallowEol
+) {
+  /** @type {string} */
+  let type
+  /** @type {Code|undefined} */
+  let marker
+
+  return start
+
+  /** @type {State} */
+  function start(code) {
+    assert(code === codes.leftCurlyBrace, 'expected `{`')
+    effects.enter(attributesType)
+    effects.enter(attributesMarkerType)
+    effects.consume(code)
+    effects.exit(attributesMarkerType)
+    return between
+  }
+
+  /** @type {State} */
+  function between(code) {
+    if (code === codes.numberSign) {
+      type = attributeIdType
+      return shortcutStart(code)
+    }
+
+    if (code === codes.dot) {
+      type = attributeClassType
+      return shortcutStart(code)
+    }
+
+    if (code === codes.colon || code === codes.underscore || asciiAlpha(code)) {
+      effects.enter(attributeType)
+      effects.enter(attributeNameType)
+      effects.consume(code)
+      return name
+    }
+
+    if (disallowEol && markdownSpace(code)) {
+      return factorySpace(effects, between, types.whitespace)(code)
+    }
+
+    if (!disallowEol && markdownLineEndingOrSpace(code)) {
+      return factoryWhitespace(effects, between)(code)
+    }
+
+    return end(code)
+  }
+
+  /** @type {State} */
+  function shortcutStart(code) {
+    effects.enter(attributeType)
+    effects.enter(type)
+    effects.enter(type + 'Marker')
+    effects.consume(code)
+    effects.exit(type + 'Marker')
+    return shortcutStartAfter
+  }
+
+  /** @type {State} */
+  function shortcutStartAfter(code) {
+    if (
+      code === codes.eof ||
+      code === codes.quotationMark ||
+      code === codes.numberSign ||
+      code === codes.apostrophe ||
+      code === codes.dot ||
+      code === codes.lessThan ||
+      code === codes.equalsTo ||
+      code === codes.greaterThan ||
+      code === codes.graveAccent ||
+      code === codes.rightCurlyBrace ||
+      markdownLineEndingOrSpace(code)
+    ) {
+      return nok(code)
+    }
+
+    effects.enter(type + 'Value')
+    effects.consume(code)
+    return shortcut
+  }
+
+  /** @type {State} */
+  function shortcut(code) {
+    if (
+      code === codes.eof ||
+      code === codes.quotationMark ||
+      code === codes.apostrophe ||
+      code === codes.lessThan ||
+      code === codes.equalsTo ||
+      code === codes.greaterThan ||
+      code === codes.graveAccent
+    ) {
+      return nok(code)
+    }
+
+    if (
+      code === codes.numberSign ||
+      code === codes.dot ||
+      code === codes.rightCurlyBrace ||
+      markdownLineEndingOrSpace(code)
+    ) {
+      effects.exit(type + 'Value')
+      effects.exit(type)
+      effects.exit(attributeType)
+      return between(code)
+    }
+
+    effects.consume(code)
+    return shortcut
+  }
+
+  /** @type {State} */
+  function name(code) {
+    if (
+      code === codes.dash ||
+      code === codes.dot ||
+      code === codes.colon ||
+      code === codes.underscore ||
+      asciiAlphanumeric(code)
+    ) {
+      effects.consume(code)
+      return name
+    }
+
+    effects.exit(attributeNameType)
+
+    if (disallowEol && markdownSpace(code)) {
+      return factorySpace(effects, nameAfter, types.whitespace)(code)
+    }
+
+    if (!disallowEol && markdownLineEndingOrSpace(code)) {
+      return factoryWhitespace(effects, nameAfter)(code)
+    }
+
+    return nameAfter(code)
+  }
+
+  /** @type {State} */
+  function nameAfter(code) {
+    if (code === codes.equalsTo) {
+      effects.enter(attributeInitializerType)
+      effects.consume(code)
+      effects.exit(attributeInitializerType)
+      return valueBefore
+    }
+
+    // Attribute w/o value.
+    effects.exit(attributeType)
+    return between(code)
+  }
+
+  /** @type {State} */
+  function valueBefore(code) {
+    if (
+      code === codes.eof ||
+      code === codes.lessThan ||
+      code === codes.equalsTo ||
+      code === codes.greaterThan ||
+      code === codes.graveAccent ||
+      code === codes.rightCurlyBrace ||
+      (disallowEol && markdownLineEnding(code))
+    ) {
+      return nok(code)
+    }
+
+    if (code === codes.quotationMark || code === codes.apostrophe) {
+      effects.enter(attributeValueLiteralType)
+      effects.enter(attributeValueMarker)
+      effects.consume(code)
+      effects.exit(attributeValueMarker)
+      marker = code
+      return valueQuotedStart
+    }
+
+    if (disallowEol && markdownSpace(code)) {
+      return factorySpace(effects, valueBefore, types.whitespace)(code)
+    }
+
+    if (!disallowEol && markdownLineEndingOrSpace(code)) {
+      return factoryWhitespace(effects, valueBefore)(code)
+    }
+
+    effects.enter(attributeValueType)
+    effects.enter(attributeValueData)
+    effects.consume(code)
+    marker = undefined
+    return valueUnquoted
+  }
+
+  /** @type {State} */
+  function valueUnquoted(code) {
+    if (
+      code === codes.eof ||
+      code === codes.quotationMark ||
+      code === codes.apostrophe ||
+      code === codes.lessThan ||
+      code === codes.equalsTo ||
+      code === codes.greaterThan ||
+      code === codes.graveAccent
+    ) {
+      return nok(code)
+    }
+
+    if (code === codes.rightCurlyBrace || markdownLineEndingOrSpace(code)) {
+      effects.exit(attributeValueData)
+      effects.exit(attributeValueType)
+      effects.exit(attributeType)
+      return between(code)
+    }
+
+    effects.consume(code)
+    return valueUnquoted
+  }
+
+  /** @type {State} */
+  function valueQuotedStart(code) {
+    if (code === marker) {
+      effects.enter(attributeValueMarker)
+      effects.consume(code)
+      effects.exit(attributeValueMarker)
+      effects.exit(attributeValueLiteralType)
+      effects.exit(attributeType)
+      return valueQuotedAfter
+    }
+
+    effects.enter(attributeValueType)
+    return valueQuotedBetween(code)
+  }
+
+  /** @type {State} */
+  function valueQuotedBetween(code) {
+    if (code === marker) {
+      effects.exit(attributeValueType)
+      return valueQuotedStart(code)
+    }
+
+    if (code === codes.eof) {
+      return nok(code)
+    }
+
+    // Note: blank lines can’t exist in content.
+    if (markdownLineEnding(code)) {
+      return disallowEol
+        ? nok(code)
+        : factoryWhitespace(effects, valueQuotedBetween)(code)
+    }
+
+    effects.enter(attributeValueData)
+    effects.consume(code)
+    return valueQuoted
+  }
+
+  /** @type {State} */
+  function valueQuoted(code) {
+    if (code === marker || code === codes.eof || markdownLineEnding(code)) {
+      effects.exit(attributeValueData)
+      return valueQuotedBetween(code)
+    }
+
+    effects.consume(code)
+    return valueQuoted
+  }
+
+  /** @type {State} */
+  function valueQuotedAfter(code) {
+    return code === codes.rightCurlyBrace || markdownLineEndingOrSpace(code)
+      ? between(code)
+      : end(code)
+  }
+
+  /** @type {State} */
+  function end(code) {
+    if (code === codes.rightCurlyBrace) {
+      effects.enter(attributesMarkerType)
+      effects.consume(code)
+      effects.exit(attributesMarkerType)
+      effects.exit(attributesType)
+      return ok
+    }
+
+    return nok(code)
+  }
+}

+ 139 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/factory-label.js

@@ -0,0 +1,139 @@
+/**
+ * @typedef {import('micromark-util-types').Effects} Effects
+ * @typedef {import('micromark-util-types').State} State
+ * @typedef {import('micromark-util-types').Token} Token
+ */
+
+import {ok as assert} from 'uvu/assert'
+import {markdownLineEnding} from 'micromark-util-character'
+import {codes} from 'micromark-util-symbol/codes.js'
+import {constants} from 'micromark-util-symbol/constants.js'
+import {types} from 'micromark-util-symbol/types.js'
+
+// This is a fork of:
+// <https://github.com/micromark/micromark/tree/main/packages/micromark-factory-label>
+// to allow empty labels, balanced brackets (such as for nested directives),
+// text instead of strings, and optionally disallows EOLs.
+
+/**
+ * @param {Effects} effects
+ * @param {State} ok
+ * @param {State} nok
+ * @param {string} type
+ * @param {string} markerType
+ * @param {string} stringType
+ * @param {boolean} [disallowEol=false]
+ */
+// eslint-disable-next-line max-params
+export function factoryLabel(
+  effects,
+  ok,
+  nok,
+  type,
+  markerType,
+  stringType,
+  disallowEol
+) {
+  let size = 0
+  let balance = 0
+  /** @type {Token|undefined} */
+  let previous
+
+  return start
+
+  /** @type {State} */
+  function start(code) {
+    assert(code === codes.leftSquareBracket, 'expected `[`')
+    effects.enter(type)
+    effects.enter(markerType)
+    effects.consume(code)
+    effects.exit(markerType)
+    return afterStart
+  }
+
+  /** @type {State} */
+  function afterStart(code) {
+    if (code === codes.rightSquareBracket) {
+      effects.enter(markerType)
+      effects.consume(code)
+      effects.exit(markerType)
+      effects.exit(type)
+      return ok
+    }
+
+    effects.enter(stringType)
+    return lineStart(code)
+  }
+
+  /** @type {State} */
+  function lineStart(code) {
+    if (code === codes.rightSquareBracket && !balance) {
+      return atClosingBrace(code)
+    }
+
+    const token = effects.enter(types.chunkText, {
+      contentType: constants.contentTypeText,
+      previous
+    })
+    if (previous) previous.next = token
+    previous = token
+    return data(code)
+  }
+
+  /** @type {State} */
+  function data(code) {
+    if (code === codes.eof || size > constants.linkReferenceSizeMax) {
+      return nok(code)
+    }
+
+    if (
+      code === codes.leftSquareBracket &&
+      ++balance > constants.linkResourceDestinationBalanceMax
+    ) {
+      return nok(code)
+    }
+
+    if (code === codes.rightSquareBracket && !balance--) {
+      effects.exit(types.chunkText)
+      return atClosingBrace(code)
+    }
+
+    if (markdownLineEnding(code)) {
+      if (disallowEol) {
+        return nok(code)
+      }
+
+      effects.consume(code)
+      effects.exit(types.chunkText)
+      return lineStart
+    }
+
+    effects.consume(code)
+    return code === codes.backslash ? dataEscape : data
+  }
+
+  /** @type {State} */
+  function dataEscape(code) {
+    if (
+      code === codes.leftSquareBracket ||
+      code === codes.backslash ||
+      code === codes.rightSquareBracket
+    ) {
+      effects.consume(code)
+      size++
+      return data
+    }
+
+    return data(code)
+  }
+
+  /** @type {State} */
+  function atClosingBrace(code) {
+    effects.exit(stringType)
+    effects.enter(markerType)
+    effects.consume(code)
+    effects.exit(markerType)
+    effects.exit(type)
+    return ok
+  }
+}

+ 49 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/factory-name.js

@@ -0,0 +1,49 @@
+/**
+ * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext
+ * @typedef {import('micromark-util-types').Effects} Effects
+ * @typedef {import('micromark-util-types').State} State
+ */
+
+import {asciiAlpha, asciiAlphanumeric} from 'micromark-util-character'
+import {codes} from 'micromark-util-symbol/codes.js'
+
+/**
+ * @this {TokenizeContext}
+ * @param {Effects} effects
+ * @param {State} ok
+ * @param {State} nok
+ * @param {string} type
+ */
+export function factoryName(effects, ok, nok, type) {
+  const self = this
+
+  return start
+
+  /** @type {State} */
+  function start(code) {
+    if (asciiAlpha(code)) {
+      effects.enter(type)
+      effects.consume(code)
+      return name
+    }
+
+    return nok(code)
+  }
+
+  /** @type {State} */
+  function name(code) {
+    if (
+      code === codes.dash ||
+      code === codes.underscore ||
+      asciiAlphanumeric(code)
+    ) {
+      effects.consume(code)
+      return name
+    }
+
+    effects.exit(type)
+    return self.previous === codes.dash || self.previous === codes.underscore
+      ? nok(code)
+      : ok(code)
+  }
+}

+ 242 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/html.js

@@ -0,0 +1,242 @@
+/**
+ * @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
+ * @typedef {import('micromark-util-types').Handle} _Handle
+ * @typedef {import('micromark-util-types').CompileContext} CompileContext
+ */
+
+/**
+ * @typedef {[string, string]} Attribute
+ * @typedef {'containerDirective'|'leafDirective'|'textDirective'} DirectiveType
+ *
+ * @typedef Directive
+ * @property {DirectiveType} type
+ * @property {string} name
+ * @property {string} [label]
+ * @property {Record<string, string>} [attributes]
+ * @property {string} [content]
+ * @property {number} [_fenceCount]
+ *
+ * @typedef {(this: CompileContext, directive: Directive) => boolean|void} Handle
+ *
+ * @typedef {Record<string, Handle>} HtmlOptions
+ */
+
+import {ok as assert} from 'uvu/assert'
+import {parseEntities} from 'parse-entities'
+
+const own = {}.hasOwnProperty
+
+/**
+ * @param {HtmlOptions} [options]
+ * @returns {HtmlExtension}
+ */
+export function directiveHtml(options = {}) {
+  return {
+    enter: {
+      directiveContainer() {
+        return enter.call(this, 'containerDirective')
+      },
+      directiveContainerAttributes: enterAttributes,
+      directiveContainerLabel: enterLabel,
+      directiveContainerContent() {
+        this.buffer()
+      },
+
+      directiveLeaf() {
+        return enter.call(this, 'leafDirective')
+      },
+      directiveLeafAttributes: enterAttributes,
+      directiveLeafLabel: enterLabel,
+
+      directiveText() {
+        return enter.call(this, 'textDirective')
+      },
+      directiveTextAttributes: enterAttributes,
+      directiveTextLabel: enterLabel
+    },
+    exit: {
+      directiveContainer: exit,
+      directiveContainerAttributeClassValue: exitAttributeClassValue,
+      directiveContainerAttributeIdValue: exitAttributeIdValue,
+      directiveContainerAttributeName: exitAttributeName,
+      directiveContainerAttributeValue: exitAttributeValue,
+      directiveContainerAttributes: exitAttributes,
+      directiveContainerContent: exitContainerContent,
+      directiveContainerFence: exitContainerFence,
+      directiveContainerLabel: exitLabel,
+      directiveContainerName: exitName,
+
+      directiveLeaf: exit,
+      directiveLeafAttributeClassValue: exitAttributeClassValue,
+      directiveLeafAttributeIdValue: exitAttributeIdValue,
+      directiveLeafAttributeName: exitAttributeName,
+      directiveLeafAttributeValue: exitAttributeValue,
+      directiveLeafAttributes: exitAttributes,
+      directiveLeafLabel: exitLabel,
+      directiveLeafName: exitName,
+
+      directiveText: exit,
+      directiveTextAttributeClassValue: exitAttributeClassValue,
+      directiveTextAttributeIdValue: exitAttributeIdValue,
+      directiveTextAttributeName: exitAttributeName,
+      directiveTextAttributeValue: exitAttributeValue,
+      directiveTextAttributes: exitAttributes,
+      directiveTextLabel: exitLabel,
+      directiveTextName: exitName
+    }
+  }
+
+  /**
+   * @this {CompileContext}
+   * @param {DirectiveType} type
+   */
+  function enter(type) {
+    /** @type {Directive[]} */
+    // @ts-expect-error
+    let stack = this.getData('directiveStack')
+    if (!stack) this.setData('directiveStack', (stack = []))
+    stack.push({type, name: ''})
+  }
+
+  /** @type {_Handle} */
+  function exitName(token) {
+    /** @type {Directive[]} */
+    // @ts-expect-error
+    const stack = this.getData('directiveStack')
+    stack[stack.length - 1].name = this.sliceSerialize(token)
+  }
+
+  /** @type {_Handle} */
+  function enterLabel() {
+    this.buffer()
+  }
+
+  /** @type {_Handle} */
+  function exitLabel() {
+    const data = this.resume()
+    /** @type {Directive[]} */
+    // @ts-expect-error
+    const stack = this.getData('directiveStack')
+    stack[stack.length - 1].label = data
+  }
+
+  /** @type {_Handle} */
+  function enterAttributes() {
+    this.buffer()
+    this.setData('directiveAttributes', [])
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeIdValue(token) {
+    /** @type {Attribute[]} */
+    // @ts-expect-error
+    const attributes = this.getData('directiveAttributes')
+    attributes.push(['id', parseEntities(this.sliceSerialize(token))])
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeClassValue(token) {
+    /** @type {Attribute[]} */
+    // @ts-expect-error
+    const attributes = this.getData('directiveAttributes')
+
+    attributes.push(['class', parseEntities(this.sliceSerialize(token))])
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeName(token) {
+    // Attribute names in CommonMark are significantly limited, so character
+    // references can’t exist.
+    /** @type {Attribute[]} */
+    // @ts-expect-error
+    const attributes = this.getData('directiveAttributes')
+
+    attributes.push([this.sliceSerialize(token), ''])
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeValue(token) {
+    /** @type {Attribute[]} */
+    // @ts-expect-error
+    const attributes = this.getData('directiveAttributes')
+    attributes[attributes.length - 1][1] = parseEntities(
+      this.sliceSerialize(token)
+    )
+  }
+
+  /** @type {_Handle} */
+  function exitAttributes() {
+    /** @type {Directive[]} */
+    // @ts-expect-error
+    const stack = this.getData('directiveStack')
+    /** @type {Attribute[]} */
+    // @ts-expect-error
+    const attributes = this.getData('directiveAttributes')
+    /** @type {Directive['attributes']} */
+    const cleaned = {}
+    /** @type {Attribute} */
+    let attribute
+    let index = -1
+
+    while (++index < attributes.length) {
+      attribute = attributes[index]
+
+      if (attribute[0] === 'class' && cleaned.class) {
+        cleaned.class += ' ' + attribute[1]
+      } else {
+        cleaned[attribute[0]] = attribute[1]
+      }
+    }
+
+    this.resume()
+    this.setData('directiveAttributes')
+    stack[stack.length - 1].attributes = cleaned
+  }
+
+  /** @type {_Handle} */
+  function exitContainerContent() {
+    const data = this.resume()
+    /** @type {Directive[]} */
+    // @ts-expect-error
+    const stack = this.getData('directiveStack')
+    stack[stack.length - 1].content = data
+  }
+
+  /** @type {_Handle} */
+  function exitContainerFence() {
+    /** @type {Directive[]} */
+    // @ts-expect-error
+    const stack = this.getData('directiveStack')
+    const directive = stack[stack.length - 1]
+    if (!directive._fenceCount) directive._fenceCount = 0
+    directive._fenceCount++
+    if (directive._fenceCount === 1) this.setData('slurpOneLineEnding', true)
+  }
+
+  /** @type {_Handle} */
+  function exit() {
+    /** @type {Directive} */
+    // @ts-expect-error
+    const directive = this.getData('directiveStack').pop()
+    /** @type {boolean|undefined} */
+    let found
+    /** @type {boolean|void} */
+    let result
+
+    assert(directive.name, 'expected `name`')
+
+    if (own.call(options, directive.name)) {
+      result = options[directive.name].call(this, directive)
+      found = result !== false
+    }
+
+    if (!found && own.call(options, '*')) {
+      result = options['*'].call(this, directive)
+      found = result !== false
+    }
+
+    if (!found && directive.type !== 'textDirective') {
+      this.setData('slurpOneLineEnding', true)
+    }
+  }
+}

+ 18 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/dev/lib/syntax.js

@@ -0,0 +1,18 @@
+/**
+ * @typedef {import('micromark-util-types').Extension} Extension
+ */
+
+import {codes} from 'micromark-util-symbol/codes.js'
+import {directiveContainer} from './directive-container.js'
+import {directiveLeaf} from './directive-leaf.js'
+import {directiveText} from './directive-text.js'
+
+/**
+ * @returns {Extension}
+ */
+export function directive() {
+  return {
+    text: {[codes.colon]: directiveText},
+    flow: {[codes.colon]: [directiveContainer, directiveLeaf]}
+  }
+}

+ 22 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/license

@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 288 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/readme.md

@@ -0,0 +1,288 @@
+# micromark-extension-directive
+
+[![Build][build-badge]][build]
+[![Coverage][coverage-badge]][coverage]
+[![Downloads][downloads-badge]][downloads]
+[![Size][size-badge]][size]
+[![Sponsors][sponsors-badge]][collective]
+[![Backers][backers-badge]][collective]
+[![Chat][chat-badge]][chat]
+
+**[micromark][]** extension to support the [generic directives proposal][prop]
+(`:cite[smith04]`, `::youtube[Video of a cat in a box]{v=01ab2cd3efg}`, and
+such).
+
+Generic directives solve the need for an infinite number of potential extensions
+to markdown in a single markdown-esque way.
+However, it’s just [a proposal][prop] and may never be specced.
+
+## When to use this
+
+If you’re using [`micromark`][micromark] or
+[`mdast-util-from-markdown`][from-markdown], use this package.
+Alternatively, if you’re using **[remark][]**, use
+[`remark-directive`][remark-directive].
+
+## Install
+
+This package is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c):
+Node 12+ is needed to use it and it must be `import`ed instead of `require`d.
+
+[npm][]:
+
+```sh
+npm install micromark-extension-directive
+```
+
+## Use
+
+Say we have the following file, `example.md`:
+
+```markdown
+A lovely language know as :abbr[HTML]{title="HyperText Markup Language"}.
+```
+
+And our script, `example.js`, looks as follows:
+
+```js
+import fs from 'node:fs'
+import {micromark} from 'micromark'
+import {directive, directiveHtml} from 'micromark-extension-directive'
+
+const output = micromark(fs.readFileSync('example.md'), {
+  extensions: [directive()],
+  htmlExtensions: [directiveHtml({abbr})]
+})
+
+console.log(output)
+
+function abbr(d) {
+  if (d.type !== 'textDirective') return false
+
+  this.tag('<abbr')
+
+  if (d.attributes && 'title' in d.attributes) {
+    this.tag(' title="' + this.encode(d.attributes.title) + '"')
+  }
+
+  this.tag('>')
+  this.raw(d.label || '')
+  this.tag('</abbr>')
+}
+```
+
+Now, running `node example` yields (abbreviated):
+
+```html
+<p>A lovely language know as <abbr title="HyperText Markup Language">HTML</abbr>.</p>
+```
+
+## API
+
+This package exports the following identifiers: `directive`, `directiveHtml`.
+There is no default export.
+
+The export map supports the endorsed
+[`development` condition](https://nodejs.org/api/packages.html#packages_resolving_user_conditions).
+Run `node --conditions development module.js` to get instrumented dev code.
+Without this condition, production code is loaded.
+
+### `directive(syntaxOptions?)`
+
+### `directiveHtml(htmlOptions?)`
+
+Functions that can be called with options to get an extension for micromark to
+parse directives (can be passed in `extensions`) and one to compile them to HTML
+(can be passed in `htmlExtensions`).
+
+###### `syntaxOptions`
+
+None yet, but might be added in the future.
+
+###### `htmlOptions`
+
+An object mapping names of directives to handlers
+([`Record<string, Handle>`][handle]).
+The special name `'*'` is the fallback to handle all unhandled directives.
+
+### `function handle(directive)`
+
+How to handle a `directive` ([`Directive`][directive]).
+
+##### Returns
+
+`boolean` or `void` — `false` can be used to signal that the directive could not
+be handled, in which case the fallback is used (when given).
+
+### `Directive`
+
+An object representing a directive.
+
+###### Fields
+
+*   `type` (`'textDirective'|'leafDirective'|'containerDirective'`)
+*   `name` (`string`) — name of directive
+*   `label` (`string?`) — compiled HTML content that was in `[brackets]`
+*   `attributes` (`Record<string, string>?`) — object w/ HTML attributes
+*   `content` (`string?`) — compiled HTML content inside container directive
+
+## Syntax
+
+The syntax looks like this:
+
+```markdown
+Directives in text can form with a single colon, such as :cite[smith04].
+Their syntax is `:name[label]{attributes}`.
+
+Leafs (block without content) can form by using two colons:
+
+::youtube[Video of a cat in a box]{vid=01ab2cd3efg}
+
+Their syntax is `::name[label]{attributes}` on its own line.
+
+Containers (blocks with content) can form by using three colons:
+
+:::spoiler
+He dies.
+:::
+
+The `name` part is required.  The first character must be a letter, other
+characters can be alphanumerical, `-`, and `_`.
+`-` or `_` cannot end a name.
+
+The `[label]` part is optional (`:x` and `:x[]` are equivalent)†.
+When used, it can include text constructs such as emphasis and so on: `x[a *b*
+c]`.
+
+The `{attributes}` part is optional (`:x` and `:x{}` are equivalent)†.
+When used, it is handled like HTML attributes, such as that `{a}`, `{a=""}`,
+, `{a=''}` but also `{a=b}`, `{a="b"}`, and `{a='b'}` are equivalent.
+Shortcuts are available for `id=` (`{#readme}` for `{id=readme}`) and
+`class` (`{.big}` for `{class=big}`).
+When multiple ids are found, the last is used; when multiple classes are found,
+they are combined: `{.red class=green .blue}` is equivalent to
+`{.red .green .blue}` and `{class="red green blue"}`.
+
+† there is one case where a name must be followed by an empty label or empty
+attributes: a *text* directive that only has a name, cannot be followed by a
+colon. So, `:red:` doesn’t work. Use either `:red[]` or `:red{}` instead.
+The reason for this is to allow GitHub emoji (gemoji) and directives to coexist.
+
+Containers can be nested by using more colons outside:
+
+::::spoiler
+He dies.
+
+:::spoiler
+She is born.
+:::
+::::
+
+The closing fence must include the same or more colons as the opening.
+If no closing is found, the container runs to the end of its parent container
+(block quote, list item, document, or other container).
+
+::::spoiler
+These three are not enough to close
+:::
+So this line is also part of the container.
+```
+
+Note that while other implementations are sometimes loose in what they allow,
+this implementation mimics CommonMark as closely as possible:
+
+*   Whitespace is not allowed between colons and name (~~`: a`~~), name and
+    label (~~`:a []`~~), name and attributes (~~`:a {}`~~), or label and
+    attributes (~~`:a[] {}`~~) — because it’s not allowed in links either
+    (~~`[] ()`~~)
+*   No trailing colons allowed on the opening fence of a container
+    (~~`:::a:::`~~) — because it’s not allowed in fenced code either
+*   The label and attributes in a leaf or container cannot include line endings
+    (~~`::a[b\nc]`~~) — because it’s not allowed in fenced code either
+
+## Related
+
+*   [`remarkjs/remark`][remark]
+    — markdown processor powered by plugins
+*   [`remarkjs/remark-directive`][remark-directive]
+    — remark plugin using this to support directive
+*   [`micromark/micromark`][micromark]
+    — the smallest commonmark-compliant markdown parser that exists
+*   [`syntax-tree/mdast-util-directive`][mdast-util-directive]
+    — mdast utility to support generic directives
+*   [`syntax-tree/mdast-util-from-markdown`][from-markdown]
+    — mdast parser using `micromark` to create mdast from markdown
+*   [`syntax-tree/mdast-util-to-markdown`][to-markdown]
+    — mdast serializer to create markdown from mdast
+
+## Contribute
+
+See [`contributing.md` in `micromark/.github`][contributing] for ways to get
+started.
+See [`support.md`][support] for ways to get help.
+
+This project has a [code of conduct][coc].
+By interacting with this repository, organization, or community you agree to
+abide by its terms.
+
+## License
+
+[MIT][license] © [Titus Wormer][author]
+
+<!-- Definitions -->
+
+[build-badge]: https://github.com/micromark/micromark-extension-directive/workflows/main/badge.svg
+
+[build]: https://github.com/micromark/micromark-extension-directive/actions
+
+[coverage-badge]: https://img.shields.io/codecov/c/github/micromark/micromark-extension-directive.svg
+
+[coverage]: https://codecov.io/github/micromark/micromark-extension-directive
+
+[downloads-badge]: https://img.shields.io/npm/dm/micromark-extension-directive.svg
+
+[downloads]: https://www.npmjs.com/package/micromark-extension-directive
+
+[size-badge]: https://img.shields.io/bundlephobia/minzip/micromark-extension-directive.svg
+
+[size]: https://bundlephobia.com/result?p=micromark-extension-directive
+
+[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg
+
+[backers-badge]: https://opencollective.com/unified/backers/badge.svg
+
+[collective]: https://opencollective.com/unified
+
+[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg
+
+[chat]: https://github.com/micromark/micromark/discussions
+
+[npm]: https://docs.npmjs.com/cli/install
+
+[license]: license
+
+[author]: https://wooorm.com
+
+[contributing]: https://github.com/micromark/.github/blob/HEAD/contributing.md
+
+[support]: https://github.com/micromark/.github/blob/HEAD/support.md
+
+[coc]: https://github.com/micromark/.github/blob/HEAD/code-of-conduct.md
+
+[micromark]: https://github.com/micromark/micromark
+
+[from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown
+
+[to-markdown]: https://github.com/syntax-tree/mdast-util-to-markdown
+
+[remark]: https://github.com/remarkjs/remark
+
+[prop]: https://talk.commonmark.org/t/generic-directives-plugins-syntax/444
+
+[mdast-util-directive]: https://github.com/syntax-tree/mdast-util-directive
+
+[remark-directive]: https://github.com/remarkjs/remark-directive
+
+[handle]: #function-handledirective
+
+[directive]: #directive

+ 1548 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/test/index.js

@@ -0,0 +1,1548 @@
+/**
+ * @typedef {import('../dev/index.js').HtmlOptions} HtmlOptions
+ * @typedef {import('../dev/index.js').Handle} Handle
+ */
+
+import test from 'tape'
+import {micromark} from 'micromark'
+import {htmlVoidElements} from 'html-void-elements'
+import {directive as syntax, directiveHtml as html} from '../dev/index.js'
+
+const own = {}.hasOwnProperty
+
+test('micromark-extension-directive (syntax)', (t) => {
+  t.test('text', (t) => {
+    t.equal(
+      micromark('\\:a', options()),
+      '<p>:a</p>',
+      'should support an escaped colon which would otherwise be a directive'
+    )
+
+    t.equal(
+      micromark('\\::a', options()),
+      '<p>:</p>',
+      'should support a directive after an escaped colon'
+    )
+
+    t.equal(
+      micromark('a ::b', options()),
+      '<p>a ::b</p>',
+      'should not support a directive after a colon'
+    )
+
+    t.equal(
+      micromark(':', options()),
+      '<p>:</p>',
+      'should not support a colon not followed by an alpha'
+    )
+
+    t.equal(
+      micromark(':a', options()),
+      '<p></p>',
+      'should support a colon followed by an alpha'
+    )
+
+    t.equal(
+      micromark(':9', options()),
+      '<p>:9</p>',
+      'should not support a colon followed by a digit'
+    )
+
+    t.equal(
+      micromark(':-', options()),
+      '<p>:-</p>',
+      'should not support a colon followed by a dash'
+    )
+
+    t.equal(
+      micromark(':_', options()),
+      '<p>:_</p>',
+      'should not support a colon followed by an underscore'
+    )
+
+    t.equal(
+      micromark(':a9', options()),
+      '<p></p>',
+      'should support a digit in a name'
+    )
+
+    t.equal(
+      micromark(':a-b', options()),
+      '<p></p>',
+      'should support a dash in a name'
+    )
+
+    t.equal(
+      micromark(':a-', options()),
+      '<p>:a-</p>',
+      'should *not* support a dash at the end of a name'
+    )
+
+    t.equal(
+      micromark(':a_b', options()),
+      '<p></p>',
+      'should support an underscore in a name'
+    )
+
+    t.equal(
+      micromark(':a_', options()),
+      '<p>:a_</p>',
+      'should *not* support an underscore at the end of a name'
+    )
+
+    t.equal(
+      micromark(':a:', options()),
+      '<p>:a:</p>',
+      'should *not* support a colon right after a name'
+    )
+
+    t.equal(
+      micromark(':+1:', options()),
+      '<p>:+1:</p>',
+      'should not interfere w/ gemoji (1)'
+    )
+
+    t.equal(
+      micromark(':heart:', options()),
+      '<p>:heart:</p>',
+      'should not interfere w/ gemoji (2)'
+    )
+
+    t.equal(
+      micromark(':call_me_hand:', options()),
+      '<p>:call_me_hand:</p>',
+      'should not interfere w/ gemoji (3)'
+    )
+
+    t.equal(
+      micromark('_:directive_', options()),
+      '<p><em>:directive</em></p>',
+      'should not interfere w/ emphasis (`_`)'
+    )
+
+    t.equal(
+      micromark(':a[', options()),
+      '<p>[</p>',
+      'should support a name followed by an unclosed `[`'
+    )
+
+    t.equal(
+      micromark(':a{', options()),
+      '<p>{</p>',
+      'should support a name followed by an unclosed `{`'
+    )
+
+    t.equal(
+      micromark(':a[b', options()),
+      '<p>[b</p>',
+      'should support a name followed by an unclosed `[` w/ content'
+    )
+
+    t.equal(
+      micromark(':a{b', options()),
+      '<p>{b</p>',
+      'should support a name followed by an unclosed `{` w/ content'
+    )
+
+    t.equal(
+      micromark(':a[]', options()),
+      '<p></p>',
+      'should support an empty label'
+    )
+
+    t.equal(
+      micromark(':a[ \t]', options()),
+      '<p></p>',
+      'should support a whitespace only label'
+    )
+
+    t.equal(
+      micromark(':a[\n]', options()),
+      '<p></p>',
+      'should support an eol in an label'
+    )
+
+    t.equal(
+      micromark(':a[a b c]asd', options()),
+      '<p>asd</p>',
+      'should support content in an label'
+    )
+
+    t.equal(
+      micromark(':a[a *b* c]asd', options()),
+      '<p>asd</p>',
+      'should support markdown in an label'
+    )
+
+    t.equal(
+      micromark('a :b[c :d[e] f] g', options()),
+      '<p>a  g</p>',
+      'should support a directive in an label'
+    )
+
+    t.equal(
+      micromark(':a[]asd', options()),
+      '<p>asd</p>',
+      'should support content after a label'
+    )
+
+    t.equal(
+      micromark(':a{}', options()),
+      '<p></p>',
+      'should support empty attributes'
+    )
+
+    t.equal(
+      micromark(':a{ \t}', options()),
+      '<p></p>',
+      'should support whitespace only attributes'
+    )
+
+    t.equal(
+      micromark(':a{\n}', options()),
+      '<p></p>',
+      'should support an eol in attributes'
+    )
+
+    t.equal(
+      micromark(':a{a b c}', options()),
+      '<p></p>',
+      'should support attributes w/o values'
+    )
+
+    t.equal(
+      micromark(':a{a=b c=d}', options()),
+      '<p></p>',
+      'should support attributes w/ unquoted values'
+    )
+
+    t.equal(
+      micromark(':a{.a .b}', options()),
+      '<p></p>',
+      'should support attributes w/ class shortcut'
+    )
+
+    t.equal(
+      micromark(':a{.a.b}', options()),
+      '<p></p>',
+      'should support attributes w/ class shortcut w/o whitespace between'
+    )
+
+    t.equal(
+      micromark(':a{#a #b}', options()),
+      '<p></p>',
+      'should support attributes w/ id shortcut'
+    )
+
+    t.equal(
+      micromark(':a{#a#b}', options()),
+      '<p></p>',
+      'should support attributes w/ id shortcut w/o whitespace between'
+    )
+
+    t.equal(
+      micromark(':a{#a.b.c#d e f=g #h.i.j}', options()),
+      '<p></p>',
+      'should support attributes w/ shortcuts combined w/ other attributes'
+    )
+
+    t.equal(
+      micromark(':a{..b}', options()),
+      '<p>{..b}</p>',
+      'should not support an empty shortcut (`.`)'
+    )
+
+    t.equal(
+      micromark(':a{.#b}', options()),
+      '<p>{.#b}</p>',
+      'should not support an empty shortcut (`#`)'
+    )
+
+    t.equal(
+      micromark(':a{.}', options()),
+      '<p>{.}</p>',
+      'should not support an empty shortcut (`}`)'
+    )
+
+    t.equal(
+      micromark(':a{.a=b}', options()),
+      '<p>{.a=b}</p>',
+      'should not support certain characters in shortcuts (`=`)'
+    )
+
+    t.equal(
+      micromark(':a{.a"b}', options()),
+      '<p>{.a&quot;b}</p>',
+      'should not support certain characters in shortcuts (`"`)'
+    )
+
+    t.equal(
+      micromark(':a{.a<b}', options()),
+      '<p>{.a&lt;b}</p>',
+      'should not support certain characters in shortcuts (`<`)'
+    )
+
+    t.equal(
+      micromark(':a{.a💚b}', options()),
+      '<p></p>',
+      'should support most characters in shortcuts'
+    )
+
+    t.equal(
+      micromark(':a{_}', options()),
+      '<p></p>',
+      'should support an underscore in attribute names'
+    )
+
+    t.equal(
+      micromark(':a{xml:lang}', options()),
+      '<p></p>',
+      'should support a colon in attribute names'
+    )
+
+    t.equal(
+      micromark(':a{a="b" c="d e f"}', options()),
+      '<p></p>',
+      'should support double quoted attributes'
+    )
+
+    t.equal(
+      micromark(":a{a='b' c='d e f'}", options()),
+      '<p></p>',
+      'should support single quoted attributes'
+    )
+
+    t.equal(
+      micromark(':a{a = b c\t=\t\'d\' f  =\r"g"}', options()),
+      '<p></p>',
+      'should support whitespace around initializers'
+    )
+
+    t.equal(
+      micromark(':a{b==}', options()),
+      '<p>{b==}</p>',
+      'should not support `=` to start an unquoted attribute value'
+    )
+
+    t.equal(
+      micromark(':a{b=}', options()),
+      '<p>{b=}</p>',
+      'should not support a missing attribute value after `=`'
+    )
+
+    t.equal(
+      micromark(":a{b=c'}", options()),
+      "<p>{b=c'}</p>",
+      'should not support an apostrophe in an unquoted attribute value'
+    )
+
+    t.equal(
+      micromark(':a{b=c`}', options()),
+      '<p>{b=c`}</p>',
+      'should not support a grave accent in an unquoted attribute value'
+    )
+
+    t.equal(
+      micromark(':a{b=a💚b}', options()),
+      '<p></p>',
+      'should support most other characters in unquoted attribute values'
+    )
+
+    t.equal(
+      micromark(':a{b="c', options()),
+      '<p>{b=&quot;c</p>',
+      'should not support an EOF in a quoted attribute value'
+    )
+
+    t.equal(
+      micromark(':a{b="a💚b"}', options()),
+      '<p></p>',
+      'should support most other characters in quoted attribute values'
+    )
+
+    t.equal(
+      micromark(':a{b="\nc\r  d"}', options()),
+      '<p></p>',
+      'should support EOLs in quoted attribute values'
+    )
+
+    t.equal(
+      micromark(':a{b="c"', options()),
+      '<p>{b=&quot;c&quot;</p>',
+      'should not support an EOF after a quoted attribute value'
+    )
+
+    t.end()
+  })
+
+  t.test('leaf', (t) => {
+    t.equal(micromark('::b', options()), '', 'should support a directive')
+
+    t.equal(
+      micromark(':', options()),
+      '<p>:</p>',
+      'should not support one colon'
+    )
+
+    t.equal(
+      micromark('::', options()),
+      '<p>::</p>',
+      'should not support two colons not followed by an alpha'
+    )
+
+    t.equal(
+      micromark('::a', options()),
+      '',
+      'should support two colons followed by an alpha'
+    )
+
+    t.equal(
+      micromark('::9', options()),
+      '<p>::9</p>',
+      'should not support two colons followed by a digit'
+    )
+
+    t.equal(
+      micromark('::-', options()),
+      '<p>::-</p>',
+      'should not support two colons followed by a dash'
+    )
+
+    t.equal(
+      micromark('::a9', options()),
+      '',
+      'should support a digit in a name'
+    )
+
+    t.equal(
+      micromark('::a-b', options()),
+      '',
+      'should support a dash in a name'
+    )
+
+    t.equal(
+      micromark('::a[', options()),
+      '<p>::a[</p>',
+      'should not support a name followed by an unclosed `[`'
+    )
+
+    t.equal(
+      micromark('::a{', options()),
+      '<p>::a{</p>',
+      'should not support a name followed by an unclosed `{`'
+    )
+
+    t.equal(
+      micromark('::a[b', options()),
+      '<p>::a[b</p>',
+      'should not support a name followed by an unclosed `[` w/ content'
+    )
+
+    t.equal(
+      micromark('::a{b', options()),
+      '<p>::a{b</p>',
+      'should not support a name followed by an unclosed `{` w/ content'
+    )
+
+    t.equal(micromark('::a[]', options()), '', 'should support an empty label')
+
+    t.equal(
+      micromark('::a[ \t]', options()),
+      '',
+      'should support a whitespace only label'
+    )
+
+    t.equal(
+      micromark('::a[\n]', options()),
+      '<p>::a[\n]</p>',
+      'should not support an eol in an label'
+    )
+
+    t.equal(
+      micromark('::a[a b c]', options()),
+      '',
+      'should support content in an label'
+    )
+
+    t.equal(
+      micromark('::a[a *b* c]', options()),
+      '',
+      'should support markdown in an label'
+    )
+
+    t.equal(
+      micromark('::a[]asd', options()),
+      '<p>::a[]asd</p>',
+      'should not support content after a label'
+    )
+
+    t.equal(
+      micromark('::a{}', options()),
+      '',
+      'should support empty attributes'
+    )
+
+    t.equal(
+      micromark('::a{ \t}', options()),
+      '',
+      'should support whitespace only attributes'
+    )
+
+    t.equal(
+      micromark('::a{\n}', options()),
+      '<p>::a{\n}</p>',
+      'should not support an eol in attributes'
+    )
+
+    t.equal(
+      micromark('::a{a b c}', options()),
+      '',
+      'should support attributes w/o values'
+    )
+
+    t.equal(
+      micromark('::a{a=b c=d}', options()),
+      '',
+      'should support attributes w/ unquoted values'
+    )
+
+    t.equal(
+      micromark('::a{.a .b}', options()),
+      '',
+      'should support attributes w/ class shortcut'
+    )
+
+    t.equal(
+      micromark('::a{#a #b}', options()),
+      '',
+      'should support attributes w/ id shortcut'
+    )
+
+    t.equal(
+      micromark('::a{.a💚b}', options()),
+      '',
+      'should support most characters in shortcuts'
+    )
+
+    t.equal(
+      micromark('::a{a="b" c="d e f"}', options()),
+      '',
+      'should support double quoted attributes'
+    )
+
+    t.equal(
+      micromark("::a{a='b' c='d e f'}", options()),
+      '',
+      'should support single quoted attributes'
+    )
+
+    t.equal(
+      micromark("::a{a = b c\t=\t'd'}", options()),
+      '',
+      'should support whitespace around initializers'
+    )
+
+    t.equal(
+      micromark('::a{f  =\rg}', options()),
+      '<p>::a{f  =\rg}</p>',
+      'should not support EOLs around initializers'
+    )
+
+    t.equal(
+      micromark('::a{b==}', options()),
+      '<p>::a{b==}</p>',
+      'should not support `=` to start an unquoted attribute value'
+    )
+
+    t.equal(
+      micromark('::a{b=a💚b}', options()),
+      '',
+      'should support most other characters in unquoted attribute values'
+    )
+
+    t.equal(
+      micromark('::a{b="c', options()),
+      '<p>::a{b=&quot;c</p>',
+      'should not support an EOF in a quoted attribute value'
+    )
+
+    t.equal(
+      micromark('::a{b="a💚b"}', options()),
+      '',
+      'should support most other characters in quoted attribute values'
+    )
+
+    t.equal(
+      micromark('::a{b="\nc\r  d"}', options()),
+      '<p>::a{b=&quot;\nc\rd&quot;}</p>',
+      'should not support EOLs in quoted attribute values'
+    )
+
+    t.equal(
+      micromark('::a{b="c"', options()),
+      '<p>::a{b=&quot;c&quot;</p>',
+      'should not support an EOF after a quoted attribute value'
+    )
+
+    t.equal(
+      micromark('::a{b=c} \t ', options()),
+      '',
+      'should support whitespace after directives'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n>a', options()),
+      '<blockquote>\n<p>a</p>\n</blockquote>',
+      'should support a block quote after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n```js\na', options()),
+      '<pre><code class="language-js">a\n</code></pre>\n',
+      'should support code (fenced) after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n    a', options()),
+      '<pre><code>a\n</code></pre>',
+      'should support code (indented) after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n[a]: b', options()),
+      '',
+      'should support a definition after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n# a', options()),
+      '<h1>a</h1>',
+      'should support a heading (atx) after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\na\n=', options()),
+      '<h1>a</h1>',
+      'should support a heading (setext) after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n<!-->', options()),
+      '<!-->',
+      'should support html after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n* a', options()),
+      '<ul>\n<li>a</li>\n</ul>',
+      'should support a list after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\na', options()),
+      '<p>a</p>',
+      'should support a paragraph after a leaf'
+    )
+
+    t.equal(
+      micromark('::a{b=c}\n***', options()),
+      '<hr />',
+      'should support a thematic break after a leaf'
+    )
+
+    t.equal(
+      micromark('>a\n::a{b=c}', options()),
+      '<blockquote>\n<p>a</p>\n</blockquote>\n',
+      'should support a block quote before a leaf'
+    )
+
+    t.equal(
+      micromark('```js\na\n```\n::a{b=c}', options()),
+      '<pre><code class="language-js">a\n</code></pre>\n',
+      'should support code (fenced) before a leaf'
+    )
+
+    t.equal(
+      micromark('    a\n::a{b=c}', options()),
+      '<pre><code>a\n</code></pre>\n',
+      'should support code (indented) before a leaf'
+    )
+
+    t.equal(
+      micromark('[a]: b\n::a{b=c}', options()),
+      '',
+      'should support a definition before a leaf'
+    )
+
+    t.equal(
+      micromark('# a\n::a{b=c}', options()),
+      '<h1>a</h1>\n',
+      'should support a heading (atx) before a leaf'
+    )
+
+    t.equal(
+      micromark('a\n=\n::a{b=c}', options()),
+      '<h1>a</h1>\n',
+      'should support a heading (setext) before a leaf'
+    )
+
+    t.equal(
+      micromark('<!-->\n::a{b=c}', options()),
+      '<!-->\n',
+      'should support html before a leaf'
+    )
+
+    t.equal(
+      micromark('* a\n::a{b=c}', options()),
+      '<ul>\n<li>a</li>\n</ul>\n',
+      'should support a list before a leaf'
+    )
+
+    t.equal(
+      micromark('a\n::a{b=c}', options()),
+      '<p>a</p>\n',
+      'should support a paragraph before a leaf'
+    )
+
+    t.equal(
+      micromark('***\n::a{b=c}', options()),
+      '<hr />\n',
+      'should support a thematic break before a leaf'
+    )
+
+    t.equal(
+      micromark('> ::a\nb', options({'*': h})),
+      '<blockquote><a></a>\n</blockquote>\n<p>b</p>',
+      'should not support lazyness (1)'
+    )
+
+    t.equal(
+      micromark('> a\n::b', options({'*': h})),
+      '<blockquote>\n<p>a</p>\n</blockquote>\n<b></b>',
+      'should not support lazyness (2)'
+    )
+
+    t.end()
+  })
+
+  t.test('container', (t) => {
+    t.equal(micromark(':::b', options()), '', 'should support a directive')
+
+    t.equal(
+      micromark(':', options()),
+      '<p>:</p>',
+      'should not support one colon'
+    )
+
+    t.equal(
+      micromark('::', options()),
+      '<p>::</p>',
+      'should not support two colons not followed by an alpha'
+    )
+
+    t.equal(
+      micromark(':::', options()),
+      '<p>:::</p>',
+      'should not support three colons not followed by an alpha'
+    )
+
+    t.equal(
+      micromark(':::a', options()),
+      '',
+      'should support three colons followed by an alpha'
+    )
+
+    t.equal(
+      micromark(':::9', options()),
+      '<p>:::9</p>',
+      'should not support three colons followed by a digit'
+    )
+
+    t.equal(
+      micromark(':::-', options()),
+      '<p>:::-</p>',
+      'should not support three colons followed by a dash'
+    )
+
+    t.equal(
+      micromark(':::a9', options()),
+      '',
+      'should support a digit in a name'
+    )
+
+    t.equal(
+      micromark(':::a-b', options()),
+      '',
+      'should support a dash in a name'
+    )
+
+    t.equal(
+      micromark(':::a[', options()),
+      '<p>:::a[</p>',
+      'should not support a name followed by an unclosed `[`'
+    )
+
+    t.equal(
+      micromark(':::a{', options()),
+      '<p>:::a{</p>',
+      'should not support a name followed by an unclosed `{`'
+    )
+
+    t.equal(
+      micromark(':::a[b', options()),
+      '<p>:::a[b</p>',
+      'should not support a name followed by an unclosed `[` w/ content'
+    )
+
+    t.equal(
+      micromark(':::a{b', options()),
+      '<p>:::a{b</p>',
+      'should not support a name followed by an unclosed `{` w/ content'
+    )
+
+    t.equal(micromark(':::a[]', options()), '', 'should support an empty label')
+
+    t.equal(
+      micromark(':::a[ \t]', options()),
+      '',
+      'should support a whitespace only label'
+    )
+
+    t.equal(
+      micromark(':::a[\n]', options()),
+      '<p>:::a[\n]</p>',
+      'should not support an eol in an label'
+    )
+
+    t.equal(
+      micromark(':::a[a b c]', options()),
+      '',
+      'should support content in an label'
+    )
+
+    t.equal(
+      micromark(':::a[a *b* c]', options()),
+      '',
+      'should support markdown in an label'
+    )
+
+    t.equal(
+      micromark(':::a[]asd', options()),
+      '<p>:::a[]asd</p>',
+      'should not support content after a label'
+    )
+
+    t.equal(
+      micromark(':::a{}', options()),
+      '',
+      'should support empty attributes'
+    )
+
+    t.equal(
+      micromark(':::a{ \t}', options()),
+      '',
+      'should support whitespace only attributes'
+    )
+
+    t.equal(
+      micromark(':::a{\n}', options()),
+      '<p>:::a{\n}</p>',
+      'should not support an eol in attributes'
+    )
+
+    t.equal(
+      micromark(':::a{a b c}', options()),
+      '',
+      'should support attributes'
+    )
+
+    t.equal(
+      micromark(':::a{f  =\rg}', options()),
+      '<p>:::a{f  =\rg}</p>',
+      'should not support EOLs around initializers'
+    )
+
+    t.equal(
+      micromark(':::a{b="c', options()),
+      '<p>:::a{b=&quot;c</p>',
+      'should not support an EOF in a quoted attribute value'
+    )
+
+    t.equal(
+      micromark(':::a{b="\nc\r  d"}', options()),
+      '<p>:::a{b=&quot;\nc\rd&quot;}</p>',
+      'should not support EOLs in quoted attribute values'
+    )
+
+    t.equal(
+      micromark(':::a{b="c"', options()),
+      '<p>:::a{b=&quot;c&quot;</p>',
+      'should not support an EOF after a quoted attribute value'
+    )
+
+    t.equal(
+      micromark(':::a{b=c} \t ', options()),
+      '',
+      'should support whitespace after directives'
+    )
+
+    t.equal(
+      micromark(':::a\n', options()),
+      '',
+      'should support no closing fence'
+    )
+
+    t.equal(
+      micromark(':::a\n:::', options()),
+      '',
+      'should support an immediate closing fence'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\nb', options()),
+      '<p>b</p>',
+      'should support content after a closing fence'
+    )
+
+    t.equal(
+      micromark(':::a\n::\nb', options()),
+      '',
+      'should not close w/ a “closing” fence of two colons'
+    )
+
+    t.equal(
+      micromark(':::a\n::::\nb', options()),
+      '<p>b</p>',
+      'should close w/ a closing fence of more colons'
+    )
+
+    t.equal(
+      micromark('::::a\n::::\nb', options()),
+      '<p>b</p>',
+      'should support more opening colons'
+    )
+
+    t.equal(
+      micromark(':::::a\n::::\nb', options()),
+      '',
+      'should not close w/ a “closing” fence of less colons than the opening'
+    )
+
+    t.equal(
+      micromark(':::a\n::: \t\nc', options()),
+      '<p>c</p>',
+      'should close w/ a closing fence followed by white space'
+    )
+
+    t.equal(
+      micromark(':::a\n::: b\nc', options()),
+      '',
+      'should not close w/ a “closing” fence followed by other characters'
+    )
+
+    t.equal(
+      micromark(':::a\n  :::\nc', options()),
+      '<p>c</p>',
+      'should close w/ an indented closing fence'
+    )
+
+    t.equal(
+      micromark(':::a\n\t:::\nc', options()),
+      '',
+      'should not close w/ when the “closing” fence is indented at a tab size'
+    )
+
+    t.equal(
+      micromark(':::a\n     :::\nc', options()),
+      '',
+      'should not close w/ when the “closing” fence is indented more than a tab size'
+    )
+
+    t.equal(
+      micromark(':::a\n\n  \n\ta', options()),
+      '',
+      'should support blank lines in content'
+    )
+
+    t.equal(
+      micromark(':::a\n\ta\n', options()),
+      '',
+      'should support an EOL EOF'
+    )
+
+    t.equal(
+      micromark('  :::a\n  b\n  :::\nc', options()),
+      '<p>c</p>',
+      'should support an indented directive'
+    )
+
+    t.equal(
+      micromark('  :::a\n\t:::\nc', options()),
+      '',
+      'should still not close an indented directive when the “closing” fence is indented a tab size'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n>a', options()),
+      '<blockquote>\n<p>a</p>\n</blockquote>',
+      'should support a block quote after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n```js\na', options()),
+      '<pre><code class="language-js">a\n</code></pre>\n',
+      'should support code (fenced) after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n    a', options()),
+      '<pre><code>a\n</code></pre>',
+      'should support code (indented) after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n[a]: b', options()),
+      '',
+      'should support a definition after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n# a', options()),
+      '<h1>a</h1>',
+      'should support a heading (atx) after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\na\n=', options()),
+      '<h1>a</h1>',
+      'should support a heading (setext) after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n<!-->', options()),
+      '<!-->',
+      'should support html after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n* a', options()),
+      '<ul>\n<li>a</li>\n</ul>',
+      'should support a list after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\na', options()),
+      '<p>a</p>',
+      'should support a paragraph after a container'
+    )
+
+    t.equal(
+      micromark(':::a\n:::\n***', options()),
+      '<hr />',
+      'should support a thematic break after a container'
+    )
+
+    t.equal(
+      micromark('>a\n:::a\nb', options()),
+      '<blockquote>\n<p>a</p>\n</blockquote>\n',
+      'should support a block quote before a container'
+    )
+
+    t.equal(
+      micromark('```js\na\n```\n:::a\nb', options()),
+      '<pre><code class="language-js">a\n</code></pre>\n',
+      'should support code (fenced) before a container'
+    )
+
+    t.equal(
+      micromark('    a\n:::a\nb', options()),
+      '<pre><code>a\n</code></pre>\n',
+      'should support code (indented) before a container'
+    )
+
+    t.equal(
+      micromark('[a]: b\n:::a\nb', options()),
+      '',
+      'should support a definition before a container'
+    )
+
+    t.equal(
+      micromark('# a\n:::a\nb', options()),
+      '<h1>a</h1>\n',
+      'should support a heading (atx) before a container'
+    )
+
+    t.equal(
+      micromark('a\n=\n:::a\nb', options()),
+      '<h1>a</h1>\n',
+      'should support a heading (setext) before a container'
+    )
+
+    t.equal(
+      micromark('<!-->\n:::a\nb', options()),
+      '<!-->\n',
+      'should support html before a container'
+    )
+
+    t.equal(
+      micromark('* a\n:::a\nb', options()),
+      '<ul>\n<li>a</li>\n</ul>\n',
+      'should support a list before a container'
+    )
+
+    t.equal(
+      micromark('a\n:::a\nb', options()),
+      '<p>a</p>\n',
+      'should support a paragraph before a container'
+    )
+
+    t.equal(
+      micromark('***\n:::a\nb', options()),
+      '<hr />\n',
+      'should support a thematic break before a container'
+    )
+
+    t.equal(
+      micromark(' :::x\n ', options({'*': h})),
+      '<x></x>',
+      'should support prefixed containers (1)'
+    )
+
+    t.equal(
+      micromark(' :::x\n - a', options({'*': h})),
+      '<x>\n<ul>\n<li>a</li>\n</ul>\n</x>',
+      'should support prefixed containers (2)'
+    )
+
+    t.equal(
+      micromark(' :::x\n - a\n > b', options({'*': h})),
+      '<x>\n<ul>\n<li>a</li>\n</ul>\n<blockquote>\n<p>b</p>\n</blockquote>\n</x>',
+      'should support prefixed containers (3)'
+    )
+
+    t.equal(
+      micromark(' :::x\n - a\n > b\n :::', options({'*': h})),
+      '<x>\n<ul>\n<li>a</li>\n</ul>\n<blockquote>\n<p>b</p>\n</blockquote>\n</x>',
+      'should support prefixed containers (4)'
+    )
+
+    t.equal(
+      micromark('> :::a\nb', options({'*': h})),
+      '<blockquote><a></a>\n</blockquote>\n<p>b</p>',
+      'should not support lazyness (1)'
+    )
+
+    t.equal(
+      micromark('> :::a\n> b\nc', options({'*': h})),
+      '<blockquote><a>\n<p>b</p>\n</a>\n</blockquote>\n<p>c</p>',
+      'should not support lazyness (2)'
+    )
+
+    t.equal(
+      micromark('> a\n:::b', options({'*': h})),
+      '<blockquote>\n<p>a</p>\n</blockquote>\n<b></b>',
+      'should not support lazyness (3)'
+    )
+
+    t.equal(
+      micromark('> :::a\n:::', options({'*': h})),
+      '<blockquote><a></a>\n</blockquote>\n<p>:::</p>',
+      'should not support lazyness (4)'
+    )
+
+    t.end()
+  })
+
+  t.end()
+})
+
+test('micromark-extension-directive (compile)', (t) => {
+  t.equal(
+    micromark(
+      [
+        ':abbr',
+        ':abbr[HTML]',
+        ':abbr{title="HyperText Markup Language"}',
+        ':abbr[HTML]{title="HyperText Markup Language"}'
+      ].join('\n\n'),
+      options({abbr})
+    ),
+    [
+      '<p><abbr></abbr></p>',
+      '<p><abbr>HTML</abbr></p>',
+      '<p><abbr title="HyperText Markup Language"></abbr></p>',
+      '<p><abbr title="HyperText Markup Language">HTML</abbr></p>'
+    ].join('\n'),
+    'should support a directives (abbr)'
+  )
+
+  t.equal(
+    micromark(
+      [
+        'Text:',
+        ':youtube',
+        ':youtube[Cat in a box a]',
+        ':youtube{v=1}',
+        ':youtube[Cat in a box b]{v=2}',
+        'Leaf:',
+        '::youtube',
+        '::youtube[Cat in a box c]',
+        '::youtube{v=3}',
+        '::youtube[Cat in a box d]{v=4}',
+        'Container:',
+        ':::youtube\nw\n:::',
+        ':::youtube[Cat in a box e]\nx\n:::',
+        ':::youtube{v=5}\ny\n:::',
+        ':::youtube[Cat in a box f]{v=6}\nz\n:::'
+      ].join('\n\n'),
+      options({youtube})
+    ),
+    [
+      '<p>Text:</p>',
+      '<p></p>',
+      '<p></p>',
+      '<p><iframe src="https://www.youtube.com/embed/1" allowfullscreen></iframe></p>',
+      '<p><iframe src="https://www.youtube.com/embed/2" allowfullscreen title="Cat in a box b"></iframe></p>',
+      '<p>Leaf:</p>',
+      '<iframe src="https://www.youtube.com/embed/3" allowfullscreen></iframe>',
+      '<iframe src="https://www.youtube.com/embed/4" allowfullscreen title="Cat in a box d"></iframe>',
+      '<p>Container:</p>',
+      '<iframe src="https://www.youtube.com/embed/5" allowfullscreen>',
+      '<p>y</p>',
+      '</iframe>',
+      '<iframe src="https://www.youtube.com/embed/6" allowfullscreen title="Cat in a box f">',
+      '<p>z</p>',
+      '</iframe>'
+    ].join('\n'),
+    'should support directives (youtube)'
+  )
+
+  t.equal(
+    micromark(':youtube[Cat in a box]\n:br', options({youtube, '*': h})),
+    '<p><youtube>Cat in a box</youtube>\n<br></p>',
+    'should support fall through directives (`*`)'
+  )
+
+  t.equal(
+    micromark(':a[:img{src="x" alt=y}]{href="z"}', options({'*': h})),
+    '<p><a href="z"><img src="x" alt="y"></a></p>',
+    'should support fall through directives (`*`)'
+  )
+
+  t.end()
+})
+
+test('content', (t) => {
+  t.equal(
+    micromark(':abbr[x\\&y&amp;z]', options({abbr})),
+    '<p><abbr>x&amp;y&amp;z</abbr></p>',
+    'should support character escapes and character references in label'
+  )
+
+  t.equal(
+    micromark(':abbr[x\\[y\\]z]', options({abbr})),
+    '<p><abbr>x[y]z</abbr></p>',
+    'should support escaped brackets in a label'
+  )
+
+  t.equal(
+    micromark(':abbr[x[y]z]', options({abbr})),
+    '<p><abbr>x[y]z</abbr></p>',
+    'should support balanced brackets in a label'
+  )
+
+  t.equal(
+    micromark(
+      ':abbr[1[2[3[4[5[6[7[8[9[10[11[12[13[14[15[16[17[18[19[20[21[22[23[24[25[26[27[28[29[30[31[32[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]',
+      options({abbr})
+    ),
+    '<p><abbr>1[2[3[4[5[6[7[8[9[10[11[12[13[14[15[16[17[18[19[20[21[22[23[24[25[26[27[28[29[30[31[32[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]</abbr></p>',
+    'should support balanced brackets in a label, 32 levels deep'
+  )
+
+  t.equal(
+    micromark(
+      ':abbr[1[2[3[4[5[6[7[8[9[10[11[12[13[14[15[16[17[18[19[20[21[22[23[24[25[26[27[28[29[30[31[32[33[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]',
+      options({abbr})
+    ),
+    '<p><abbr></abbr>[1[2[3[4[5[6[7[8[9[10[11[12[13[14[15[16[17[18[19[20[21[22[23[24[25[26[27[28[29[30[31[32[33[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]</p>',
+    'should *not* support balanced brackets in a label, 33 levels deep'
+  )
+
+  t.equal(
+    micromark(':abbr[a\nb\rc]', options({abbr})),
+    '<p><abbr>a\nb\rc</abbr></p>',
+    'should support EOLs in a label'
+  )
+
+  t.equal(
+    micromark(':abbr[\na\r]', options({abbr})),
+    '<p><abbr>\na\r</abbr></p>',
+    'should support EOLs at the edges of a label (1)'
+  )
+
+  t.equal(
+    micromark(':abbr[\n]', options({abbr})),
+    '<p><abbr>\n</abbr></p>',
+    'should support EOLs at the edges of a label (2)'
+  )
+
+  t.equal(
+    micromark(':abbr[a\n:abbr[b]\nc]', options({abbr})),
+    '<p><abbr>a\n<abbr>b</abbr>\nc</abbr></p>',
+    'should support EOLs around nested directives'
+  )
+
+  t.equal(
+    micromark(':abbr[:abbr[\n]]', options({abbr})),
+    '<p><abbr><abbr>\n</abbr></abbr></p>',
+    'should support EOLs inside nested directives (1)'
+  )
+
+  t.equal(
+    micromark(':abbr[:abbr[a\nb]]', options({abbr})),
+    '<p><abbr><abbr>a\nb</abbr></abbr></p>',
+    'should support EOLs inside nested directives (2)'
+  )
+
+  t.equal(
+    micromark(':abbr[:abbr[\nb\n]]', options({abbr})),
+    '<p><abbr><abbr>\nb\n</abbr></abbr></p>',
+    'should support EOLs inside nested directives (3)'
+  )
+
+  t.equal(
+    micromark(':abbr[:abbr[\\\n]]', options({abbr})),
+    '<p><abbr><abbr><br />\n</abbr></abbr></p>',
+    'should support EOLs inside nested directives (4)'
+  )
+
+  t.equal(
+    micromark(':abbr[a *b* **c** d]', options({abbr})),
+    '<p><abbr>a <em>b</em> <strong>c</strong> d</abbr></p>',
+    'should support markdown in a label'
+  )
+
+  t.equal(
+    micromark(':abbr{title=a&apos;b}', options({abbr})),
+    '<p><abbr title="a\'b"></abbr></p>',
+    'should support character references in unquoted attribute values'
+  )
+
+  t.equal(
+    micromark(':abbr{title="a&apos;b"}', options({abbr})),
+    '<p><abbr title="a\'b"></abbr></p>',
+    'should support character references in double attribute values'
+  )
+
+  t.equal(
+    micromark(":abbr{title='a&apos;b'}", options({abbr})),
+    '<p><abbr title="a\'b"></abbr></p>',
+    'should support character references in single attribute values'
+  )
+
+  t.equal(
+    micromark(':abbr{title="a&somethingelse;b"}', options({abbr})),
+    '<p><abbr title="a&amp;somethingelse;b"></abbr></p>',
+    'should support unknown character references in attribute values'
+  )
+
+  t.equal(
+    micromark(':span{a\nb}', options({'*': h})),
+    '<p><span a="" b=""></span></p>',
+    'should support EOLs between attributes'
+  )
+
+  t.equal(
+    micromark(':span{\na\n}', options({'*': h})),
+    '<p><span a=""></span></p>',
+    'should support EOLs at the edges of attributes'
+  )
+
+  t.equal(
+    micromark(':span{a\r= b}', options({'*': h})),
+    '<p><span a="b"></span></p>',
+    'should support EOLs before initializer'
+  )
+
+  t.equal(
+    micromark(':span{a=\r\nb}', options({'*': h})),
+    '<p><span a="b"></span></p>',
+    'should support EOLs after initializer'
+  )
+
+  t.equal(
+    micromark(':span{a=b\nc}', options({'*': h})),
+    '<p><span a="b" c=""></span></p>',
+    'should support EOLs between an unquoted attribute value and a next attribute name'
+  )
+
+  t.equal(
+    micromark(':span{a="b\nc"}', options({'*': h})),
+    '<p><span a="b\nc"></span></p>',
+    'should support EOLs in a double quoted attribute value'
+  )
+
+  t.equal(
+    micromark(":span{a='b\nc'}", options({'*': h})),
+    '<p><span a="b\nc"></span></p>',
+    'should support EOLs in a single quoted attribute value'
+  )
+
+  t.equal(
+    micromark(':span{#a#b}', options({'*': h})),
+    '<p><span id="b"></span></p>',
+    'should support `id` shortcuts'
+  )
+
+  t.equal(
+    micromark(':span{id=a id="b" #c#d}', options({'*': h})),
+    '<p><span id="d"></span></p>',
+    'should support `id` shortcuts after `id` attributes'
+  )
+
+  t.equal(
+    micromark(':span{.a.b}', options({'*': h})),
+    '<p><span class="a b"></span></p>',
+    'should support `class` shortcuts'
+  )
+
+  t.equal(
+    micromark(':span{class=a class="b c" .d.e}', options({'*': h})),
+    '<p><span class="a b c d e"></span></p>',
+    'should support `class` shortcuts after `class` attributes'
+  )
+
+  t.equal(
+    micromark('::::div{.big}\n:::div{.small}\nText', options({'*': h})),
+    '<div class="big">\n<div class="small">\n<p>Text</p>\n</div>\n</div>',
+    'should support container directives in container directives'
+  )
+
+  t.equal(
+    micromark(':::div{.big}\n::hr{.small}', options({'*': h})),
+    '<div class="big">\n<hr class="small">\n</div>',
+    'should support leaf directives in container directives'
+  )
+
+  t.equal(
+    micromark(':::div{.big}\n:b[Text]', options({'*': h})),
+    '<div class="big">\n<p><b>Text</b></p>\n</div>',
+    'should support text directives in container directives'
+  )
+
+  t.equal(
+    micromark(':::section\n* a\n:::', options({'*': h})),
+    '<section>\n<ul>\n<li>a</li>\n</ul>\n</section>',
+    'should support lists in container directives'
+  )
+
+  t.equal(
+    micromark(':::section[]\n* a\n:::', options({'*': h})),
+    '<section>\n<ul>\n<li>a</li>\n</ul>\n</section>',
+    'should support lists w/ label brackets in container directives'
+  )
+
+  t.equal(
+    micromark(':::section{}\n* a\n:::', options({'*': h})),
+    '<section>\n<ul>\n<li>a</li>\n</ul>\n</section>',
+    'should support lists w/ attribute braces in container directives'
+  )
+
+  t.equal(
+    micromark(':::i\n- +\na', options()),
+    '',
+    'should support lazy containers in an unclosed container directive'
+  )
+
+  t.end()
+})
+
+/** @type {Handle} */
+function abbr(d) {
+  if (d.type !== 'textDirective') return false
+
+  this.tag('<abbr')
+
+  if (d.attributes && 'title' in d.attributes) {
+    this.tag(' title="' + this.encode(d.attributes.title) + '"')
+  }
+
+  this.tag('>')
+  this.raw(d.label || '')
+  this.tag('</abbr>')
+}
+
+/** @type {Handle} */
+function youtube(d) {
+  const attrs = d.attributes || {}
+  const v = attrs.v
+  /** @type {string} */
+  let prop
+
+  if (!v) return false
+
+  const list = [
+    'src="https://www.youtube.com/embed/' + this.encode(v) + '"',
+    'allowfullscreen'
+  ]
+
+  if (d.label) {
+    list.push('title="' + this.encode(d.label) + '"')
+  }
+
+  for (prop in attrs) {
+    if (prop !== 'v') {
+      list.push(this.encode(prop) + '="' + this.encode(attrs[prop]) + '"')
+    }
+  }
+
+  this.tag('<iframe ' + list.join(' ') + '>')
+
+  if (d.content) {
+    this.lineEndingIfNeeded()
+    this.raw(d.content)
+    this.lineEndingIfNeeded()
+  }
+
+  this.tag('</iframe>')
+}
+
+/** @type {Handle} */
+function h(d) {
+  const content = d.content || d.label
+  const attrs = d.attributes || {}
+  /** @type {Array.<string>} */
+  const list = []
+  /** @type {string} */
+  let prop
+
+  for (prop in attrs) {
+    if (own.call(attrs, prop)) {
+      list.push(this.encode(prop) + '="' + this.encode(attrs[prop]) + '"')
+    }
+  }
+
+  this.tag('<' + d.name)
+  if (list.length > 0) this.tag(' ' + list.join(' '))
+  this.tag('>')
+
+  if (content) {
+    if (d.type === 'containerDirective') this.lineEndingIfNeeded()
+    this.raw(content)
+    if (d.type === 'containerDirective') this.lineEndingIfNeeded()
+  }
+
+  if (!htmlVoidElements.includes(d.name)) this.tag('</' + d.name + '>')
+}
+
+/**
+ * @param {HtmlOptions} [options]
+ */
+function options(options) {
+  return {
+    allowDangerousHtml: true,
+    extensions: [syntax()],
+    htmlExtensions: [html(options)]
+  }
+}