Procházet zdrojové kódy

Merge pull request #9048 from weseek/imprv/148445-upgrade-remark-growi-directive

imprv: Upgrade unified and remark-growi-directive
mergify[bot] před 1 rokem
rodič
revize
ccf94db671
64 změnil soubory, kde provedl 2316 přidání a 2239 odebrání
  1. 5 0
      .changeset/real-onions-vanish.md
  2. 8 0
      apps/app/next.config.js
  3. 14 14
      apps/app/package.json
  4. 1 1
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  5. 1 1
      apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts
  6. 1 1
      apps/app/src/client/components/Page/SlideRenderer.tsx
  7. 1 1
      apps/app/src/client/components/PagePresentationModal.tsx
  8. 3 4
      apps/app/src/client/components/ReactMarkdownComponents/Header.tsx
  9. 5 5
      apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx
  10. 10 5
      apps/app/src/client/services/renderer/renderer.tsx
  11. 12 6
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  12. 5 5
      apps/app/src/features/mermaid/services/mermaid.ts
  13. 2 5
      apps/app/src/interfaces/renderer-options.ts
  14. 3 1
      apps/app/src/interfaces/services/rehype-sanitize.ts
  15. 9 0
      apps/app/src/services/renderer/recommended-whitelist.spec.ts
  16. 2 1
      apps/app/src/services/renderer/recommended-whitelist.ts
  17. 2 3
      apps/app/src/services/renderer/rehype-plugins/add-class.ts
  18. 1 1
      apps/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts
  19. 2 2
      apps/app/src/services/renderer/rehype-plugins/relative-links-by-pukiwiki-like-linker.spec.ts
  20. 1 1
      apps/app/src/services/renderer/rehype-plugins/relative-links.spec.ts
  21. 1 1
      apps/app/src/services/renderer/rehype-plugins/relative-links.ts
  22. 8 10
      apps/app/src/services/renderer/rehype-plugins/relocate-toc.ts
  23. 11 11
      apps/app/src/services/renderer/remark-plugins/attachment.ts
  24. 23 0
      apps/app/src/services/renderer/remark-plugins/codeblock.ts
  25. 9 10
      apps/app/src/services/renderer/remark-plugins/xsv-to-table.ts
  26. 3 1
      apps/app/src/services/renderer/renderer.tsx
  27. 1 2
      apps/app/src/stores/renderer.tsx
  28. 12 12
      packages/presentation/package.json
  29. 1 1
      packages/presentation/src/client/components/GrowiSlides.tsx
  30. 11 7
      packages/presentation/src/client/components/RichSlideSection.tsx
  31. 1 1
      packages/presentation/src/client/consts/index.ts
  32. 1 1
      packages/presentation/src/client/services/renderer/extract-sections.ts
  33. 5 4
      packages/presentation/src/services/use-slides-by-frontmatter.ts
  34. 5 5
      packages/remark-attachment-refs/package.json
  35. 6 4
      packages/remark-attachment-refs/src/client/services/renderer/refs.ts
  36. 3 3
      packages/remark-drawio/package.json
  37. 17 10
      packages/remark-drawio/src/services/renderer/remark-drawio.ts
  38. 17 18
      packages/remark-growi-directive/package.json
  39. 7 2
      packages/remark-growi-directive/src/index.js
  40. 0 32
      packages/remark-growi-directive/src/mdast-util-growi-directive/complex-types.d.ts
  41. 0 4
      packages/remark-growi-directive/src/mdast-util-growi-directive/consts.js
  42. 177 0
      packages/remark-growi-directive/src/mdast-util-growi-directive/index.d.ts
  43. 2 267
      packages/remark-growi-directive/src/mdast-util-growi-directive/index.js
  44. 276 0
      packages/remark-growi-directive/src/mdast-util-growi-directive/lib/index.js
  45. 1 2
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-leaf.js
  46. 1 2
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-text.js
  47. 1 2
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js
  48. 1 3
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-label.js
  49. 1 1
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-name.js
  50. 1 1
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/html.js
  51. 1 1
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/syntax.js
  52. 1 1
      packages/remark-growi-directive/src/micromark-factory-attributes-devider/index.js
  53. 19 20
      packages/remark-growi-directive/src/remark-growi-directive.js
  54. 421 472
      packages/remark-growi-directive/test/mdast-util-growi-directive.test.js
  55. 694 905
      packages/remark-growi-directive/test/micromark-extension-growi-directive.test.js
  56. 19 22
      packages/remark-growi-directive/test/remark-growi-directive.test.js
  57. 5 0
      packages/remark-growi-directive/tsconfig.json
  58. 13 0
      packages/remark-growi-directive/vitest.config.ts
  59. 4 4
      packages/remark-lsx/package.json
  60. 1 1
      packages/remark-lsx/src/client/components/LsxPageList/LsxListView.tsx
  61. 1 1
      packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx
  62. 8 6
      packages/remark-lsx/src/client/services/renderer/lsx.ts
  63. 1 1
      packages/remark-lsx/src/client/utils/page-node.spec.ts
  64. 437 331
      yarn.lock

+ 5 - 0
.changeset/real-onions-vanish.md

@@ -0,0 +1,5 @@
+---
+"@growi/remark-growi-directive": minor
+---
+
+Convert unit test by tape to Vitest

+ 8 - 0
apps/app/next.config.js

@@ -48,6 +48,14 @@ const getTranspilePackages = () => {
     'emoticon',
     'direction', // for hast-util-select
     'bcp-47-match', // for hast-util-select
+    'parse-entities',
+    'character-reference-invalid',
+    'is-hexadecimal',
+    'is-alphabetical',
+    'is-alphanumerical',
+    'github-slugger',
+    'html-url-attributes',
+    'estree-util-is-identifier-name',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
   ];
 

+ 14 - 14
apps/app/package.json

@@ -121,7 +121,7 @@
     "extensible-custom-error": "^0.0.7",
     "form-data": "^4.0.0",
     "graceful-fs": "^4.1.11",
-    "hast-util-select": "^5.0.5",
+    "hast-util-select": "^6.0.2",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "i18next": "^23.10.1",
@@ -171,7 +171,7 @@
     "react-error-boundary": "^3.1.4",
     "react-i18next": "^14.1.0",
     "react-image-crop": "^8.3.0",
-    "react-markdown": "^8.0.7",
+    "react-markdown": "^9.0.1",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
     "react-stickynode": "^4.1.1",
@@ -180,18 +180,18 @@
     "reactstrap": "^9.2.2",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
-    "rehype-katex": "^6.0.2",
-    "rehype-raw": "^6.1.1",
-    "rehype-sanitize": "^5.0.1",
-    "rehype-slug": "^5.0.1",
+    "rehype-katex": "^7.0.0",
+    "rehype-raw": "^7.0.0",
+    "rehype-sanitize": "^6.0.0",
+    "rehype-slug": "^6.0.0",
     "rehype-toc": "^3.0.2",
-    "remark-breaks": "^3.0.2",
-    "remark-emoji": "^3.0.2",
-    "remark-frontmatter": "^4.0.1",
-    "remark-gfm": "^3.0.1",
-    "remark-math": "^5.1.1",
-    "remark-toc": "^8.0.1",
-    "remark-wiki-link": "^1.0.4",
+    "remark-breaks": "^4.0.0",
+    "remark-emoji": "^5.0.0",
+    "remark-frontmatter": "^5.0.0",
+    "remark-gfm": "^4.0.0",
+    "remark-math": "^6.0.0",
+    "remark-toc": "^9.0.0",
+    "remark-wiki-link": "^2.0.1",
     "sanitize-filename": "^1.6.3",
     "socket.io": "^4.7.5",
     "stream-to-promise": "^3.0.0",
@@ -273,7 +273,7 @@
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
     "react-toastify": "^9.1.3",
-    "rehype-rewrite": "^3.0.6",
+    "rehype-rewrite": "^4.0.2",
     "replacestream": "^4.0.3",
     "sass": "^1.53.0",
     "simple-load-script": "^1.0.2",

+ 1 - 1
apps/app/playwright/20-basic-features/access-to-page.spec.ts

@@ -23,7 +23,7 @@ test('/Sandbox/Math is successfully loaded', async({ page }) => {
   await page.goto('/Sandbox/Math');
 
   // Expect the Math-specific elements to be present
-  await expect(page.locator('.math').first()).toBeVisible();
+  await expect(page.locator('.katex').first()).toBeVisible();
 });
 
 test('Sandbox with edit is successfully loaded', async({ page }) => {

+ 1 - 1
apps/app/playwright/21-basic-features-for-guest/access-to-page.spec.ts

@@ -15,7 +15,7 @@ test('/Sandbox/math is successfully loaded', async({ page }) => {
   await page.goto('/Sandbox/Math');
 
   // Check if the math elements are visible
-  await expect(page.locator('.math').first()).toBeVisible();
+  await expect(page.locator('.katex').first()).toBeVisible();
 });
 
 test('Access to /me page', async({ page }) => {

+ 1 - 1
apps/app/src/client/components/Page/SlideRenderer.tsx

@@ -1,4 +1,4 @@
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Options as ReactMarkdownOptions } from 'react-markdown';
 
 import { usePresentationViewOptions } from '~/stores/renderer';
 

+ 1 - 1
apps/app/src/client/components/PagePresentationModal.tsx

@@ -5,7 +5,7 @@ import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import dynamic from 'next/dynamic';
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Options as ReactMarkdownOptions } from 'react-markdown';
 import {
   Modal, ModalBody,
 } from 'reactstrap';

+ 3 - 4
apps/app/src/client/components/ReactMarkdownComponents/Header.tsx

@@ -2,8 +2,8 @@ import { useCallback, useEffect, useState } from 'react';
 
 import type EventEmitter from 'events';
 
+import type { Element } from 'hast';
 import { useRouter } from 'next/router';
-import type { Element } from 'react-markdown/lib/rehype-filter';
 
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import {
@@ -54,13 +54,12 @@ const EditLink = (props: EditLinkProps): JSX.Element => {
 type HeaderProps = {
   children: React.ReactNode,
   node: Element,
-  level: number,
   id?: string,
 }
 
 export const Header = (props: HeaderProps): JSX.Element => {
   const {
-    node, id, children, level,
+    node, id, children,
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
@@ -73,7 +72,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
 
   const [isActive, setActive] = useState(false);
 
-  const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
+  const CustomTag = node.tagName as keyof JSX.IntrinsicElements;
 
   const activateByHash = useCallback((url: string) => {
     try {

+ 5 - 5
apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
 
 import type EventEmitter from 'events';
 
-import type { Element } from 'react-markdown/lib/rehype-filter';
+import type { Element } from 'hast';
 
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
@@ -23,8 +23,7 @@ type TableWithEditButtonProps = {
   className?: string
 }
 
-export const TableWithEditButton = React.memo((props: TableWithEditButtonProps): JSX.Element => {
-
+const TableWithEditButtonNoMemorized = (props: TableWithEditButtonProps): JSX.Element => {
   const { children, node, className } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
@@ -61,5 +60,6 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
       </table>
     </div>
   );
-});
-TableWithEditButton.displayName = 'TableWithEditButton';
+};
+TableWithEditButtonNoMemorized.displayName = 'TableWithEditButton';
+export const TableWithEditButton = React.memo(TableWithEditButtonNoMemorized) as typeof TableWithEditButtonNoMemorized;

+ 10 - 5
apps/app/src/client/services/renderer/renderer.tsx

@@ -26,6 +26,7 @@ import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
 import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
 import * as attachment from '~/services/renderer/remark-plugins/attachment';
+import * as codeBlock from '~/services/renderer/remark-plugins/codeblock';
 import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
@@ -71,7 +72,7 @@ export const generateViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
       presentation.sanitizeOption,
@@ -80,6 +81,7 @@ export const generateViewOptions = (
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 
@@ -129,9 +131,10 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   // add remark plugins
   // remarkPlugins.push();
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 
@@ -176,7 +179,7 @@ export const generateSimpleViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
       presentation.sanitizeOption,
@@ -185,6 +188,7 @@ export const generateSimpleViewOptions = (
       attachment.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 
@@ -227,7 +231,7 @@ export const generatePresentationViewOptions = (
   const { rehypePlugins } = options;
 
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       addLineNumberAttribute.sanitizeOption,
     )]
@@ -265,7 +269,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
       drawio.sanitizeOption,
@@ -274,6 +278,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
+      codeBlock.sanitizeOption,
     )]
     : () => {};
 

+ 12 - 6
apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -1,6 +1,5 @@
 import type { ReactNode } from 'react';
 
-import type { CodeComponent, CodeProps } from 'react-markdown/lib/ast-to-react';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
 import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 
@@ -45,7 +44,8 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   // see: https://github.com/weseek/growi/pull/7484
   //
   // Note: You can also remove this code if the user requests to see the code highlighted in Prism as-is.
-  const isSimpleString = Array.isArray(children) && children.length === 1 && typeof children[0] === 'string';
+
+  const isSimpleString = typeof children === 'string' || (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string');
   if (!isSimpleString) {
     return (
       <div style={oneDark['pre[class*="language-"]']}>
@@ -67,13 +67,19 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   );
 }
 
-export const CodeBlock: CodeComponent = ({ inline, className, children }: CodeProps) => {
+type CodeBlockProps = {
+  children: ReactNode,
+  className?: string,
+  inline?: string, // "" or undefined
+}
 
-  if (inline) {
-    return <code className={`code-inline ${className ?? ''}`}>{children}</code>;
-  }
+export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
 
   // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
+  const { className, children, inline } = props;
+  if (inline != null) {
+    return <code className={`code-inline ${className ?? ''}`}>{children}</code>;
+  }
 
   const match = /language-(\w+)(:?.+)?/.exec(className || '');
   const lang = match && match[1] ? match[1] : '';

+ 5 - 5
apps/app/src/features/mermaid/services/mermaid.ts

@@ -1,9 +1,9 @@
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
-import { Plugin } from 'unified';
-import { Node } from 'unist';
+import type { Code } from 'mdast';
+import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 
-function rewriteNode(node: Node) {
+function rewriteNode(node: Code) {
   // replace node
   const data = node.data ?? (node.data = {});
   data.hName = 'mermaid';
@@ -11,8 +11,8 @@ function rewriteNode(node: Node) {
 
 export const remarkPlugin: Plugin = function() {
   return (tree) => {
-    visit(tree, (node) => {
-      if (node.type === 'code' && node.lang === 'mermaid') {
+    visit(tree, 'code', (node: Code) => {
+      if (node.lang === 'mermaid') {
         rewriteNode(node);
       }
     });

+ 2 - 5
apps/app/src/interfaces/renderer-options.ts

@@ -1,8 +1,6 @@
 import type { ComponentType } from 'react';
 
-import type { SpecialComponents } from 'react-markdown/lib/ast-to-react';
-import type { NormalComponents } from 'react-markdown/lib/complex-types';
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Options as ReactMarkdownOptions, Components } from 'react-markdown';
 import type { PluggableList } from 'unified';
 
 export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehypePlugins' | 'components' | 'children'> & {
@@ -10,8 +8,7 @@ export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehy
   rehypePlugins: PluggableList,
   components?:
     | Partial<
-        Omit<NormalComponents, keyof SpecialComponents>
-        & SpecialComponents
+        Components
         & {
           [elem: string]: ComponentType<any>,
         }

+ 3 - 1
apps/app/src/interfaces/services/rehype-sanitize.ts

@@ -1,4 +1,6 @@
-import type { Attributes } from 'hast-util-sanitize/lib';
+import type { defaultSchema } from 'hast-util-sanitize';
+
+type Attributes = typeof defaultSchema.attributes;
 
 export const RehypeSanitizeType = {
   RECOMMENDED: 'Recommended',

+ 9 - 0
apps/app/src/services/renderer/recommended-whitelist.spec.ts

@@ -14,6 +14,9 @@ describe('recommended-whitelist', () => {
 
   test('.attributes should return data attributes', () => {
     expect(attributes).not.toBeNull();
+
+    assert(attributes != null);
+
     expect(Object.keys(attributes)).includes('*');
     expect(attributes['*']).includes('alt');
     expect(attributes['*']).includes('align');
@@ -25,12 +28,18 @@ describe('recommended-whitelist', () => {
 
   test('.attributes should return iframe attributes', () => {
     expect(attributes).not.toBeNull();
+
+    assert(attributes != null);
+
     expect(Object.keys(attributes)).includes('iframe');
     expect(attributes.iframe).includes('src');
   });
 
   test('.attributes should return video attributes', () => {
     expect(attributes).not.toBeNull();
+
+    assert(attributes != null);
+
     expect(Object.keys(attributes)).includes('video');
     expect(attributes.iframe).includes('src');
   });

+ 2 - 1
apps/app/src/services/renderer/recommended-whitelist.ts

@@ -1,7 +1,8 @@
 import { defaultSchema } from 'hast-util-sanitize';
-import type { Attributes } from 'hast-util-sanitize/lib';
 import deepmerge from 'ts-deepmerge';
 
+type Attributes = typeof defaultSchema.attributes;
+
 /**
  * reference: https://meta.stackexchange.com/questions/1777/what-html-tags-are-allowed-on-stack-exchange-sites,
  *            https://github.com/jch/html-pipeline/blob/70b6903b025c668ff3c02a6fa382031661182147/lib/html/pipeline/sanitization_filter.rb#L41

+ 2 - 3
apps/app/src/services/renderer/rehype-plugins/add-class.ts

@@ -1,9 +1,8 @@
 // See: https://github.com/martypdx/rehype-add-classes for the original implementation.
 // Re-implemeted in TypeScript.
+import type { Nodes as HastNode, Element, Properties } from 'hast';
 import { selectAll } from 'hast-util-select';
-import type { Node as HastNode, Element } from 'hast-util-select/lib/types';
-import { Properties } from 'hast-util-select/lib/types';
-import { Plugin } from 'unified';
+import type { Plugin } from 'unified';
 
 export type SelectorName = string; // e.g. 'h1'
 export type ClassName = string; // e.g. 'header'

+ 1 - 1
apps/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts

@@ -1,5 +1,5 @@
+import type { Element } from 'hast';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
-import type { Element } from 'hast-util-select/lib/types';
 import type { Plugin } from 'unified';
 import { visit, EXIT, CONTINUE } from 'unist-util-visit';
 

+ 2 - 2
apps/app/src/services/renderer/rehype-plugins/relative-links-by-pukiwiki-like-linker.spec.ts

@@ -1,5 +1,5 @@
+import type { Nodes as HastNode, Text } from 'hast';
 import { select } from 'hast-util-select';
-import type { Node as HastNode } from 'hast-util-select/lib/types';
 import parse from 'remark-parse';
 import rehype from 'remark-rehype';
 import { unified } from 'unified';
@@ -45,7 +45,7 @@ describe('relativeLinksByPukiwikiLikeLinker', () => {
 
       expect(anchorElement?.children[0]).not.toBeNull();
       expect(anchorElement?.children[0].type).toEqual('text');
-      expect(anchorElement?.children[0].value).toEqual(expectedValue);
+      expect((anchorElement?.children[0] as HastNode as Text).value).toEqual(expectedValue);
 
     });
   });

+ 1 - 1
apps/app/src/services/renderer/rehype-plugins/relative-links.spec.ts

@@ -1,6 +1,6 @@
 
+import type { Nodes as HastNode } from 'hast';
 import { select } from 'hast-util-select';
-import type { Node as HastNode } from 'hast-util-select/lib/types';
 import parse from 'remark-parse';
 import remarkRehype from 'remark-rehype';
 import { unified } from 'unified';

+ 1 - 1
apps/app/src/services/renderer/rehype-plugins/relative-links.ts

@@ -1,7 +1,7 @@
 import assert from 'assert';
 
+import type { Nodes as HastNode, Element } from 'hast';
 import { selectAll } from 'hast-util-select';
-import type { Node as HastNode, Element } from 'hast-util-select/lib/types';
 import isAbsolute from 'is-absolute-url';
 import type { Plugin } from 'unified';
 

+ 8 - 10
apps/app/src/services/renderer/rehype-plugins/relocate-toc.ts

@@ -1,6 +1,7 @@
-import rehypeToc, { type HtmlElementNode } from 'rehype-toc';
+import rehypeToc from 'rehype-toc';
+import type { HtmlElementNode } from 'rehype-toc';
 import type { Plugin } from 'unified';
-import type { Node } from 'unist';
+import { visit } from 'unist-util-visit';
 
 type StoreTocPluginParams = {
   storeTocNode: (toc: HtmlElementNode) => void,
@@ -22,13 +23,10 @@ export const rehypePluginStore: Plugin<[StoreTocPluginParams]> = (options) => {
 
 
 // method for replace <ol> to <ul>
-const replaceOlToUl = (children: Node[]) => {
-  children.forEach((child) => {
-    if (child.type === 'element' && child.tagName === 'ol') {
-      child.tagName = 'ul';
-    }
-    if (child.children != null) {
-      replaceOlToUl(child.children as Node[]);
+const replaceOlToUl = (tree: HtmlElementNode) => {
+  visit(tree, 'element', (node: HtmlElementNode) => {
+    if (node.tagName === 'ol') {
+      node.tagName = 'ul';
     }
   });
 };
@@ -44,7 +42,7 @@ export const rehypePluginRestore: Plugin<[RestoreTocPluginParams]> = (options) =
     headings: ['h1', 'h2', 'h3'],
     customizeTOC: () => {
       if (tocNode != null) {
-        replaceOlToUl([tocNode]); // replace <ol> to <ul>
+        replaceOlToUl(tocNode); // replace <ol> to <ul>
 
         // restore toc
         return tocNode;

+ 11 - 11
apps/app/src/services/renderer/remark-plugins/attachment.ts

@@ -1,8 +1,8 @@
 import path from 'path';
 
-import { Schema as SanitizeOption } from 'hast-util-sanitize';
-import { Plugin } from 'unified';
-import { Node } from 'unist';
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { Link } from 'mdast';
+import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 
 const SUPPORTED_ATTRIBUTES = ['attachmentId', 'url', 'attachmentName'];
@@ -13,25 +13,25 @@ const isAttachmentLink = (url: string): boolean => {
   return attachmentUrlFormat.test(url);
 };
 
-const rewriteNode = (node: Node) => {
-  const attachmentId = path.basename(node.url as string);
+const rewriteNode = (node: Link) => {
+  const attachmentId = path.basename(node.url);
+  const attachmentName = node.children[0] != null && node.children[0].type === 'text' ? node.children[0].value : '';
+
   const data = node.data ?? (node.data = {});
   data.hName = 'attachment';
   data.hProperties = {
     attachmentId,
     url: node.url,
-    attachmentName: (node.children as any)[0]?.value,
+    attachmentName,
   };
 };
 
 
 export const remarkPlugin: Plugin = () => {
   return (tree) => {
-    visit(tree, (node) => {
-      if (node.type === 'link') {
-        if (isAttachmentLink(node.url as string)) {
-          rewriteNode(node);
-        }
+    visit(tree, 'link', (node: Link) => {
+      if (isAttachmentLink(node.url)) {
+        rewriteNode(node);
       }
     });
   };

+ 23 - 0
apps/app/src/services/renderer/remark-plugins/codeblock.ts

@@ -0,0 +1,23 @@
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { InlineCode } from 'mdast';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+
+const SUPPORTED_CODE = ['inline'];
+
+export const remarkPlugin: Plugin = () => {
+  return (tree) => {
+    visit(tree, 'inlineCode', (node: InlineCode) => {
+      const data = node.data || (node.data = {});
+      data.hProperties = { inline: true };
+    });
+  };
+};
+
+export const sanitizeOption: SanitizeOption = {
+  tagNames: ['code'],
+  attributes: {
+    code: SUPPORTED_CODE,
+  },
+};

+ 9 - 10
apps/app/src/services/renderer/remark-plugins/xsv-to-table.ts

@@ -1,4 +1,5 @@
 import csvToMarkdownTable from 'csv-to-markdown-table';
+import type { Code, Table } from 'mdast';
 import { fromMarkdown } from 'mdast-util-from-markdown';
 import { gfmTableFromMarkdown } from 'mdast-util-gfm-table';
 import { gfmTable } from 'micromark-extension-gfm-table';
@@ -8,12 +9,12 @@ import { visit } from 'unist-util-visit';
 
 type Lang = 'csv' | 'csv-h' | 'tsv' | 'tsv-h';
 
-function isXsv(lang: unknown): lang is Lang {
+function isXsv(lang?: string | null | undefined): lang is Lang {
   return /^(csv|csv-h|tsv|tsv-h)$/.test(lang as string);
 }
 
 function rewriteNode(node: Node, lang: Lang) {
-  const tableContents = node.value as string;
+  const tableContents = (node as Code).value;
 
   const tableDoc = csvToMarkdownTable(
     tableContents,
@@ -21,24 +22,22 @@ function rewriteNode(node: Node, lang: Lang) {
     lang === 'csv-h' || lang === 'tsv-h',
   );
   const tableTree = fromMarkdown(tableDoc, {
-    extensions: [gfmTable],
-    mdastExtensions: [gfmTableFromMarkdown],
+    extensions: [gfmTable()],
+    mdastExtensions: [gfmTableFromMarkdown()],
   });
 
   // replace node
   if (tableTree.children[0] != null) {
     node.type = 'table';
-    node.children = tableTree.children[0].children;
+    (node as Table).children = (tableTree.children[0] as Table).children;
   }
 }
 
 export const remarkPlugin: Plugin = function() {
   return (tree) => {
-    visit(tree, (node) => {
-      if (node.type === 'code') {
-        if (isXsv(node.lang)) {
-          rewriteNode(node, node.lang);
-        }
+    visit(tree, 'code', (node: Code) => {
+      if (isXsv(node.lang)) {
+        rewriteNode(node, node.lang);
       }
     });
   };

+ 3 - 1
apps/app/src/services/renderer/renderer.tsx

@@ -25,6 +25,7 @@ import { tagNames as recommendedTagNames, attributes as recommendedAttributes }
 import * as addClass from './rehype-plugins/add-class';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
+import * as codeBlock from './remark-plugins/codeblock';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import * as xsvToTable from './remark-plugins/xsv-to-table';
 
@@ -96,6 +97,7 @@ export const generateCommonOptions = (pagePath: string|undefined): RendererOptio
       pukiwikiLikeLinker,
       growiDirective,
       remarkFrontmatter,
+      codeBlock.remarkPlugin,
     ],
     remarkRehypeOptions: {
       clobberPrefix: '', // remove clobber prefix
@@ -137,7 +139,7 @@ export const generateSSRViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
     )]

+ 1 - 2
apps/app/src/stores/renderer.tsx

@@ -5,11 +5,10 @@ import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr';
 
 import { getGrowiFacade } from '~/features/growi-plugin/client/utils/growi-facade-utils';
 import type { RendererOptions } from '~/interfaces/renderer-options';
-
-
 import {
   useRendererConfig,
 } from '~/stores-universal/context';
+
 import { useCurrentPagePath } from './page';
 import { useCurrentPageTocNode } from './ui';
 

+ 12 - 12
packages/presentation/package.json

@@ -46,19 +46,19 @@
     "@marp-team/marpit": "^2.6.1",
     "@types/reveal.js": "^4.4.1",
     "eslint-plugin-regex": "^1.8.0",
-    "hast-util-sanitize": "^4.1.0",
-    "hast-util-select": "^5.0.5",
-    "mdast-util-frontmatter": "^1.0.0",
-    "mdast-util-gfm": "^2.0.1",
-    "mdast-util-to-markdown": "^1.3.0",
-    "react-markdown": "^8.0.7",
-    "remark-frontmatter": "^4.0.1",
-    "remark-parse": "^10.0.0",
-    "remark-stringify": "^10.0.0",
+    "hast-util-sanitize": "^5.0.1",
+    "hast-util-select": "^6.0.2",
+    "mdast-util-frontmatter": "^2.0.1",
+    "mdast-util-gfm": "^3.0.0",
+    "mdast-util-to-markdown": "^2.1.0",
+    "react-markdown": "^9.0.1",
+    "remark-frontmatter": "^5.0.0",
+    "remark-parse": "^11.0.0",
+    "remark-stringify": "^11.0.0",
     "reveal.js": "^4.4.0",
-    "unified": "^10.1.2",
-    "unist-util-find-after": "^4.0.0",
-    "unist-util-visit": "^4.0.0"
+    "unified": "^11.0.0",
+    "unist-util-find-after": "^5.0.0",
+    "unist-util-visit": "^5.0.0"
   },
   "peerDependencies": {
     "next": "^14",

+ 1 - 1
packages/presentation/src/client/components/GrowiSlides.tsx

@@ -1,5 +1,5 @@
 import Head from 'next/head';
-import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
+import ReactMarkdown from 'react-markdown';
 
 import type { PresentationOptions } from '../consts';
 import { MARP_CONTAINER_CLASS_NAME, presentationMarpit, slideMarpit } from '../services/growi-marpit';

+ 11 - 7
packages/presentation/src/client/components/RichSlideSection.tsx

@@ -1,7 +1,8 @@
-import React, { ReactNode } from 'react';
+import type { ReactNode } from 'react';
+import React from 'react';
 
 type RichSlideSectionProps = {
-  children: ReactNode,
+  children?: ReactNode,
   presentation?: boolean,
 }
 
@@ -13,7 +14,7 @@ const OriginalRichSlideSection = React.memo((props: RichSlideSectionProps): JSX.
       <svg data-marpit-svg="" viewBox="0 0 1280 720">
         <foreignObject width="1280" height="720">
           <section>
-            {children}
+            {children ?? <></>}
           </section>
         </foreignObject>
       </svg>
@@ -21,7 +22,8 @@ const OriginalRichSlideSection = React.memo((props: RichSlideSectionProps): JSX.
   );
 });
 
-export const RichSlideSection = React.memo((props: RichSlideSectionProps): JSX.Element => {
+
+const RichSlideSectionNoMemorized = (props: RichSlideSectionProps): JSX.Element => {
   const { children } = props;
 
   return (
@@ -29,10 +31,11 @@ export const RichSlideSection = React.memo((props: RichSlideSectionProps): JSX.E
       {children}
     </OriginalRichSlideSection>
   );
-});
+};
+export const RichSlideSection = React.memo(RichSlideSectionNoMemorized) as typeof RichSlideSectionNoMemorized;
 
 
-export const PresentationRichSlideSection = React.memo((props: RichSlideSectionProps): JSX.Element => {
+const PresentationRichSlideSectionNoMemorized = (props: RichSlideSectionProps): JSX.Element => {
   const { children } = props;
 
   return (
@@ -40,4 +43,5 @@ export const PresentationRichSlideSection = React.memo((props: RichSlideSectionP
       {children}
     </OriginalRichSlideSection>
   );
-});
+};
+export const PresentationRichSlideSection = React.memo(PresentationRichSlideSectionNoMemorized) as typeof PresentationRichSlideSectionNoMemorized;

+ 1 - 1
packages/presentation/src/client/consts/index.ts

@@ -1,4 +1,4 @@
-import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Options as ReactMarkdownOptions } from 'react-markdown';
 import type { Options as RevealOptions } from 'reveal.js';
 
 export type PresentationOptions = {

+ 1 - 1
packages/presentation/src/client/services/renderer/extract-sections.ts

@@ -5,7 +5,7 @@ import { findAfter } from 'unist-util-find-after';
 import { visit } from 'unist-util-visit';
 
 
-function wrapWithSection(parentNode: Parent, startElem: Node, endElem: Node | null, isDarkMode?: boolean): void {
+function wrapWithSection(parentNode: Parent, startElem: Node, endElem?: Node | null, isDarkMode?: boolean): void {
   const siblings = parentNode.children;
 
   const startIndex = siblings.indexOf(startElem);

+ 5 - 4
packages/presentation/src/services/use-slides-by-frontmatter.ts

@@ -1,5 +1,6 @@
 import { useEffect, useState } from 'react';
 
+import type { Parent, Root } from 'mdast';
 import type { Processor } from 'unified';
 
 type ParseResult = {
@@ -39,11 +40,11 @@ const generateFrontmatterProcessor = async(opts?: ProcessorOpts) => {
   const remarkStringify = (await import('remark-stringify')).default;
   const unified = (await import('unified')).unified;
 
-  return unified()
+  return (unified()
     .use(remarkParse)
     .use(remarkStringify)
     .use(remarkFrontmatter, ['yaml'])
-    .use(() => ((obj) => {
+    .use(() => ((obj: Parent) => {
       if (obj.children[0]?.type === 'yaml') {
         const result = parseSlideFrontmatter(obj.children[0]?.value);
         opts?.onParsed?.(result);
@@ -51,7 +52,7 @@ const generateFrontmatterProcessor = async(opts?: ProcessorOpts) => {
       else {
         opts?.onSkipped?.();
       }
-    }));
+    })));
 };
 
 export type UseSlide = {
@@ -65,7 +66,7 @@ export type UseSlide = {
  */
 export const useSlidesByFrontmatter = (markdown?: string, isEnabledMarp?: boolean): UseSlide | undefined => {
 
-  const [processor, setProcessor] = useState<Processor|undefined>();
+  const [processor, setProcessor] = useState<Processor<Root, undefined, undefined, Root, string>|undefined>();
   const [parseResult, setParseResult] = useState<UseSlide|undefined>();
 
   useEffect(() => {

+ 5 - 5
packages/remark-attachment-refs/package.json

@@ -49,8 +49,8 @@
     "@growi/ui": "link:../ui",
     "axios": "^0.24.0",
     "bunyan": "^1.8.15",
+    "hast-util-select": "^6.0.2",
     "express": "^4.20.0",
-    "hast-util-select": "^5.0.5",
     "mongoose": "^6.11.3",
     "swr": "^2.0.3",
     "universal-bunyan": "^0.9.2",
@@ -59,11 +59,11 @@
   "devDependencies": {
     "csstype": "^3.0.2",
     "eslint-plugin-regex": "^1.8.0",
-    "hast-util-sanitize": "^4.1.0",
-    "hast-util-select": "^5.0.5",
+    "hast-util-sanitize": "^5.0.1",
+    "hast-util-select": "^6.0.2",
     "npm-run-all": "^4.1.5",
-    "unified": "^10.1.2",
-    "unist-util-visit": "^4.0.0"
+    "unified": "^11.0.0",
+    "unist-util-visit": "^5.0.0"
   },
   "peerDependencies": {
     "react": "^18.2.0",

+ 6 - 4
packages/remark-attachment-refs/src/client/services/renderer/refs.ts

@@ -1,9 +1,10 @@
 import { pathUtils } from '@growi/core/dist/utils';
+import type { TextGrowiPluginDirective, LeafGrowiPluginDirective } from '@growi/remark-growi-directive';
 import { remarkGrowiDirectivePluginType } from '@growi/remark-growi-directive';
-import { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { Nodes as HastNode } from 'hast';
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
 import { selectAll } from 'hast-util-select';
-import type { Node as HastNode } from 'hast-util-select/lib/types';
-import { Plugin } from 'unified';
+import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 
 import loggerFactory from '../../../utils/logger';
@@ -21,10 +22,11 @@ const REFS_IMG_SUPPORTED_ATTRIBUTES = [
 ];
 
 type DirectiveAttributes = Record<string, string>
+type GrowiPluginDirective = TextGrowiPluginDirective | LeafGrowiPluginDirective
 
 export const remarkPlugin: Plugin = function() {
   return (tree) => {
-    visit(tree, (node) => {
+    visit(tree, (node: GrowiPluginDirective) => {
       if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
         if (typeof node.name !== 'string') {
           return;

+ 3 - 3
packages/remark-drawio/package.json

@@ -33,11 +33,11 @@
   "dependencies": {},
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
-    "hast-util-sanitize": "^4.1.0",
+    "hast-util-sanitize": "^5.0.1",
     "pako": "^2.1.0",
     "throttle-debounce": "^5.0.0",
-    "unified": "^10.1.2",
-    "unist-util-visit": "^4.0.0"
+    "unified": "^11.0.0",
+    "unist-util-visit": "^5.0.0"
   },
   "peerDependencies": {
     "react": "^18.2.0",

+ 17 - 10
packages/remark-drawio/src/services/renderer/remark-drawio.ts

@@ -1,21 +1,30 @@
+import type { Properties } from 'hast';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type {
+  Code, Node, Paragraph,
+} from 'mdast';
 import type { Plugin } from 'unified';
-import type { Node } from 'unist';
 import { visit } from 'unist-util-visit';
 
 const SUPPORTED_ATTRIBUTES = ['diagramIndex', 'bol', 'eol'];
 
+interface Data {
+  hName?: string,
+  hProperties?: Properties,
+}
+
 type Lang = 'drawio';
 
-function isDrawioBlock(lang: unknown): lang is Lang {
-  return /^drawio$/.test(lang as string);
+function isDrawioBlock(lang?: string | null): lang is Lang {
+  return /^drawio$/.test(lang ?? '');
 }
 
 function rewriteNode(node: Node, index: number) {
-  const data = node.data ?? (node.data = {});
 
   node.type = 'paragraph';
-  node.children = [{ type: 'text', value: node.value }];
+  (node as Paragraph).children = [{ type: 'text', value: (node as Code).value }];
+
+  const data: Data = node.data ?? (node.data = {});
   data.hName = 'drawio';
   data.hProperties = {
     diagramIndex: index,
@@ -27,11 +36,9 @@ function rewriteNode(node: Node, index: number) {
 
 export const remarkPlugin: Plugin = function() {
   return (tree) => {
-    visit(tree, (node, index) => {
-      if (node.type === 'code') {
-        if (isDrawioBlock(node.lang)) {
-          rewriteNode(node, index ?? 0);
-        }
+    visit(tree, 'code', (node: Code, index) => {
+      if (isDrawioBlock(node.lang)) {
+        rewriteNode(node, index ?? 0);
       }
     });
   };

+ 17 - 18
packages/remark-growi-directive/package.json

@@ -18,43 +18,42 @@
   "typings": "dist/index.d.ts",
   "scripts": {
     "build": "yarn tsc -p tsconfig.build.json",
+    "postbuild": "shx cp ./src/mdast-util-growi-directive/index.d.ts ./dist/mdast-util-growi-directive/index.d.ts",
     "clean": "shx rm -rf dist",
     "dev": "yarn build",
     "watch": "yarn tsc -w",
     "test": "cross-env NODE_ENV=test npm run test-coverage",
-    "test-api": "tape --conditions development test/**.test.js",
+    "test-api": "vitest run --coverage",
     "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
     "lint": "yarn eslint \"**/*.{cjs, js,jsx,ts,tsx}\"",
     "lint:fix": "yarn eslint \"**/*.{cjs, js,jsx,ts,tsx}\" --fix"
   },
   "dependencies": {
-    "@types/mdast": "^3.0.0",
-    "@types/unist": "^2.0.0",
-    "mdast-util-to-markdown": "^1.3.0",
-    "micromark-factory-space": "^1.0.0",
-    "micromark-factory-whitespace": "^1.0.0",
-    "micromark-util-character": "^1.0.0",
-    "micromark-util-symbol": "^1.0.0",
-    "micromark-util-types": "^1.0.0",
+    "@types/mdast": "^4.0.0",
+    "@types/unist": "^3.0.0",
+    "mdast-util-to-markdown": "^2.1.0",
+    "micromark-factory-space": "^2.0.0",
+    "micromark-factory-whitespace": "^2.0.0",
+    "micromark-util-character": "^2.1.0",
+    "micromark-util-symbol": "^2.0.0",
+    "micromark-util-types": "^2.0.0",
     "parse-entities": "^4.0.0",
     "stringify-entities": "^4.0.0",
-    "unified": "^10.0.0",
-    "unist-util-visit-parents": "^5.0.0",
+    "unified": "^11.0.0",
+    "unist-util-visit-parents": "^6.0.0",
     "uvu": "^0.5.0"
   },
   "devDependencies": {
-    "@types/tape": "^4.0.0",
-    "c8": "^7.0.0",
+    "c8": "^8.0.0",
     "html-void-elements": "^2.0.0",
     "is-hidden": "^2.0.0",
-    "mdast-util-from-markdown": "^1.0.0",
-    "micromark": "^3.0.0",
-    "remark": "^14.0.0",
+    "mdast-util-from-markdown": "^2.0.1",
+    "micromark": "^4.0.0",
+    "remark": "^15.0.1",
     "rimraf": "^3.0.0",
-    "tape": "^5.0.0",
     "to-vfile": "^7.0.0",
     "type-coverage": "^2.0.0",
-    "unist-util-remove-position": "^4.0.0"
+    "unist-util-remove-position": "^5.0.0"
   },
   "typeCoverage": {
     "atLeast": 100,

+ 7 - 2
packages/remark-growi-directive/src/index.js

@@ -1,6 +1,11 @@
-import { DirectiveType } from './mdast-util-growi-directive/consts.js';
 import { remarkGrowiDirectivePlugin } from './remark-growi-directive.js';
 
-export { DirectiveType as remarkGrowiDirectivePluginType };
+export {
+  DirectiveTypeObject as remarkGrowiDirectivePluginType,
+  LeafGrowiPluginDirective,
+  TextGrowiPluginDirective,
+  LeafGrowiPluginDirectiveData,
+  TextGrowiPluginDirectiveData,
+} from './mdast-util-growi-directive';
 
 export default remarkGrowiDirectivePlugin;

+ 0 - 32
packages/remark-growi-directive/src/mdast-util-growi-directive/complex-types.d.ts

@@ -1,32 +0,0 @@
-import type { PhrasingContent } from 'mdast';
-import type { Parent } from 'unist';
-
-import type { DirectiveType } from './consts.js';
-
-
-type DirectiveAttributes = Record<string, string>
-
-interface DirectiveFields {
-  name: string
-  attributes?: DirectiveAttributes
-}
-
-export interface TextDirective extends Parent, DirectiveFields {
-  type: DirectiveType.Text
-  children: PhrasingContent[]
-}
-
-export interface LeafDirective extends Parent, DirectiveFields {
-  type: DirectiveType.Leaf
-  children: PhrasingContent[]
-}
-
-declare module 'mdast' {
-  interface StaticPhrasingContentMap {
-    [DirectiveType.Text]: TextDirective
-  }
-
-  interface BlockContentMap {
-    [DirectiveType.Leaf]: LeafDirective
-  }
-}

+ 0 - 4
packages/remark-growi-directive/src/mdast-util-growi-directive/consts.js

@@ -1,4 +0,0 @@
-export const DirectiveType = Object.freeze({
-  Text: 'textGrowiPluginDirective',
-  Leaf: 'leafGrowiPluginDirective',
-});

+ 177 - 0
packages/remark-growi-directive/src/mdast-util-growi-directive/index.d.ts

@@ -0,0 +1,177 @@
+import type {
+  Data,
+  Parent,
+  PhrasingContent,
+} from 'mdast';
+
+import { DirectiveType as DirectiveTypeObject } from './lib/index.js';
+
+export { directiveFromMarkdown, directiveToMarkdown } from './lib/index.js';
+export { DirectiveTypeObject };
+
+type DirectiveType = typeof DirectiveTypeObject;
+
+/**
+ * Fields shared by directives.
+ */
+interface DirectiveFields {
+  /**
+   * Directive name.
+   */
+  name: string
+
+  /**
+   * Directive attributes.
+   */
+  attributes?: Record<string, string | null | undefined> | null | undefined
+}
+
+/**
+ * Markdown directive (leaf form).
+ */
+export interface LeafGrowiPluginDirective extends Parent, DirectiveFields {
+  /**
+   * Node type of leaf directive.
+   */
+  type: DirectiveType['Leaf']
+
+  /**
+   * Children of leaf directive.
+   */
+  children: PhrasingContent[]
+
+  /**
+   * Data associated with the mdast leaf directive.
+   */
+  data?: LeafGrowiPluginDirectiveData | undefined
+}
+
+/**
+ * Info associated with mdast leaf directive nodes by the ecosystem.
+ */
+export interface LeafGrowiPluginDirectiveData extends Data {
+  hName?: string,
+  hProperties?: Record<string, string>
+}
+
+/**
+ * Markdown directive (text form).
+ */
+export interface TextGrowiPluginDirective extends Parent, DirectiveFields {
+  /**
+   * Node type of text directive.
+   */
+  type: DirectiveType['Text']
+
+  /**
+   * Children of text directive.
+   */
+  children: PhrasingContent[]
+
+  /**
+   * Data associated with the text leaf directive.
+   */
+  data?: TextGrowiPluginDirectiveData | undefined
+}
+
+/**
+ * Info associated with mdast text directive nodes by the ecosystem.
+ */
+export interface TextGrowiPluginDirectiveData extends Data {
+  hName?: string,
+  hProperties?: Record<string, string>
+}
+
+
+/**
+ * Union of registered mdast directive nodes.
+ *
+ * It is not possible to register custom mdast directive node types.
+ */
+export type Directives = LeafGrowiPluginDirective | TextGrowiPluginDirective
+
+// Add custom data tracked to turn markdown into a tree.
+declare module 'mdast-util-from-markdown' {
+  interface CompileData {
+    /**
+     * Attributes for current directive.
+     */
+    directiveAttributes?: Array<[string, string]> | undefined
+  }
+}
+
+// Add custom data tracked to turn a syntax tree into markdown.
+declare module 'mdast-util-to-markdown' {
+  interface ConstructNameMap {
+    /**
+     * Whole leaf directive.
+     *
+     * ```markdown
+     * > | ::a
+     *     ^^^
+     * ```
+     */
+    leafGrowiPluginDirective: 'leafGrowiPluginDirective'
+
+    /**
+     * Label of a leaf directive.
+     *
+     * ```markdown
+     * > | ::a[b]
+     *        ^^^
+     * ```
+     */
+    leafGrowiPluginDirectiveLabel: 'leafGrowiPluginDirectiveLabel'
+
+    /**
+     * Whole text directive.
+     *
+     * ```markdown
+     * > | :a
+     *     ^^
+     * ```
+     */
+    textGrowiPluginDirective: 'textGrowiPluginDirective'
+
+    /**
+     * Label of a text directive.
+     *
+     * ```markdown
+     * > | :a[b]
+     *       ^^^
+     * ```
+     */
+    textGrowiPluginDirectiveLabel: 'textGrowiPluginDirectiveLabel'
+  }
+}
+
+// Add nodes to content, register `data` on paragraph.
+declare module 'mdast' {
+  interface BlockContentMap {
+    /**
+     * Directive in flow content (such as in the root document, or block
+     * quotes), which contains nothing.
+     */
+    leafGrowiPluginDirective: LeafGrowiPluginDirective
+  }
+
+  interface PhrasingContentMap {
+    /**
+     * Directive in phrasing content (such as in paragraphs, headings).
+     */
+    textGrowiPluginDirective: TextGrowiPluginDirective
+  }
+
+  interface RootContentMap {
+    /**
+     * Directive in flow content (such as in the root document, or block
+     * quotes), which contains nothing.
+     */
+    leafGrowiPluginDirective: LeafGrowiPluginDirective
+
+    /**
+     * Directive in phrasing content (such as in paragraphs, headings).
+     */
+    textGrowiPluginDirective: TextGrowiPluginDirective
+  }
+}

+ 2 - 267
packages/remark-growi-directive/src/mdast-util-growi-directive/index.js

@@ -1,267 +1,2 @@
-/**
- * @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').LeafDirective} LeafDirective
- * @typedef {import('./complex-types').TextDirective} TextDirective
- * @typedef {LeafDirective|TextDirective} Directive
- */
-
-import { checkQuote } from 'mdast-util-to-markdown/lib/util/check-quote.js';
-import { containerPhrasing } from 'mdast-util-to-markdown/lib/util/container-phrasing.js';
-import { track } from 'mdast-util-to-markdown/lib/util/track.js';
-import { parseEntities } from 'parse-entities';
-import { stringifyEntitiesLight } from 'stringify-entities';
-
-import { DirectiveType } from './consts.js';
-
-const own = {}.hasOwnProperty;
-
-const shortcut = /^[^\t\n\r "#'.<=>`}]+$/;
-
-handleDirective.peek = peekDirective;
-
-/** @type {FromMarkdownExtension} */
-export const directiveFromMarkdown = {
-  canContainEols: [DirectiveType.Text],
-  enter: {
-    directiveLeaf: enterLeaf,
-    directiveLeafAttributes: enterAttributes,
-
-    directiveText: enterText,
-    directiveTextAttributes: enterAttributes,
-  },
-  exit: {
-    directiveLeaf: exit,
-    directiveLeafAttributeName: exitAttributeName,
-    directiveLeafAttributeValue: exitAttributeValue,
-    directiveLeafAttributes: exitAttributes,
-    directiveLeafName: exitName,
-
-    directiveText: exit,
-    directiveTextAttributeName: exitAttributeName,
-    directiveTextAttributeValue: exitAttributeValue,
-    directiveTextAttributes: exitAttributes,
-    directiveTextName: exitName,
-  },
-};
-
-/** @type {ToMarkdownExtension} */
-export const directiveToMarkdown = {
-  unsafe: [
-    {
-      character: '\r',
-      inConstruct: [DirectiveType.Leaf],
-    },
-    {
-      character: '\n',
-      inConstruct: [DirectiveType.Leaf],
-    },
-    {
-      before: '[^$]',
-      character: '$',
-      after: '[A-Za-z]',
-      inConstruct: ['phrasing'],
-    },
-    { atBreak: true, character: '$', after: '$' },
-  ],
-  handlers: {
-    [DirectiveType.Leaf]: handleDirective,
-    [DirectiveType.Text]: handleDirective,
-  },
-};
-
-/** @type {FromMarkdownHandle} */
-function enterLeaf(token) {
-  enter.call(this, DirectiveType.Leaf, token);
-}
-
-/** @type {FromMarkdownHandle} */
-function enterText(token) {
-  enter.call(this, DirectiveType.Text, 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 enterAttributes() {
-  this.setData('directiveAttributes', []);
-  this.buffer(); // Capture EOLs
-}
-
-/** @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];
-
-    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} */
-  const label = node;
-
-  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));
-
-  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 === DirectiveType.Text ? [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;
-
-  // eslint-disable-next-line no-restricted-syntax
-  for (key in attrs) {
-    if (
-      own.call(attrs, key)
-      && attrs[key] !== undefined
-      && attrs[key] !== null
-    ) {
-      const value = String(attrs[key]);
-
-      values.push(quoted(key, value));
-    }
-  }
-
-  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 {Directive} node
- * @returns {string}
- */
-function fence(node) {
-  let size = 0;
-
-  if (node.type === DirectiveType.Leaf) {
-    size = 1;
-  }
-  else {
-    size = 1;
-  }
-
-  return '$'.repeat(size);
-
-}
+export { directiveFromMarkdown, directiveToMarkdown } from './lib/index.js';
+export { DirectiveType as DirectiveTypeObject } from './lib/index.js';

+ 276 - 0
packages/remark-growi-directive/src/mdast-util-growi-directive/lib/index.js

@@ -0,0 +1,276 @@
+/**
+ * @typedef {import('mdast').Node} Node
+ * @typedef {import('mdast').Paragraph} Paragraph
+ *
+ * @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext
+ * @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension
+ * @typedef {import('mdast-util-from-markdown').Handle} FromMarkdownHandle
+ * @typedef {import('mdast-util-from-markdown').Token} Token
+ *
+ * @typedef {import('mdast-util-to-markdown').ConstructName} ConstructName
+ * @typedef {import('mdast-util-to-markdown').Handle} ToMarkdownHandle
+ * @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension
+ * @typedef {import('mdast-util-to-markdown').State} State
+ *
+ * @typedef {import('../types/index.js').LeafGrowiPluginDirective} LeafGrowiPluginDirective
+ * @typedef {import('../types/index.js').TextGrowiPluginDirective} TextGrowiPluginDirective
+ * @typedef {import('../types/index.js').Directives} Directives
+ */
+
+import { parseEntities } from 'parse-entities';
+import { stringifyEntitiesLight } from 'stringify-entities';
+
+const own = {}.hasOwnProperty;
+
+const shortcut = /^[^\t\n\r "#'.<=>`}]+$/;
+
+
+export const DirectiveType = Object.freeze({
+  Text: 'textGrowiPluginDirective',
+  Leaf: 'leafGrowiPluginDirective',
+});
+
+handleDirective.peek = peekDirective;
+
+/** @type {FromMarkdownExtension} */
+export function directiveFromMarkdown() {
+  return {
+    canContainEols: [DirectiveType.Text],
+    enter: {
+      directiveLeaf: enterLeaf,
+      directiveLeafAttributes: enterAttributes,
+
+      directiveText: enterText,
+      directiveTextAttributes: enterAttributes,
+    },
+    exit: {
+      directiveLeaf: exit,
+      directiveLeafAttributeName: exitAttributeName,
+      directiveLeafAttributeValue: exitAttributeValue,
+      directiveLeafAttributes: exitAttributes,
+      directiveLeafName: exitName,
+
+      directiveText: exit,
+      directiveTextAttributeName: exitAttributeName,
+      directiveTextAttributeValue: exitAttributeValue,
+      directiveTextAttributes: exitAttributes,
+      directiveTextName: exitName,
+
+    },
+  };
+}
+
+/** @type {ToMarkdownExtension} */
+export function directiveToMarkdown() {
+  return {
+    unsafe: [
+      {
+        character: '\r',
+        inConstruct: [DirectiveType.Leaf],
+      },
+      {
+        character: '\n',
+        inConstruct: [DirectiveType.Leaf],
+      },
+      {
+        before: '[^$]',
+        character: '$',
+        after: '[A-Za-z]',
+        inConstruct: ['phrasing'],
+      },
+      { atBreak: true, character: '$', after: '$' },
+    ],
+    handlers: {
+      [DirectiveType.Leaf]: handleDirective,
+      [DirectiveType.Text]: handleDirective,
+    },
+  };
+}
+
+/** @type {FromMarkdownHandle} */
+function enterLeaf(token) {
+  enter.call(this, DirectiveType.Leaf, token);
+}
+
+/** @type {FromMarkdownHandle} */
+function enterText(token) {
+  enter.call(this, DirectiveType.Text, 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 enterAttributes() {
+  this.data.directiveAttributes = [];
+  this.buffer(); // Capture EOLs
+}
+
+/** @type {FromMarkdownHandle} */
+function exitAttributeValue(token) {
+  const list = /** @type {Array.<[string, string]>} */ (
+    this.data.directiveAttributes
+  );
+  list[list.length - 1][1] = parseEntities(this.sliceSerialize(token));
+}
+
+/** @type {FromMarkdownHandle} */
+function exitAttributeName(token) {
+  const list = /** @type {Array.<[string, string]>} */ (
+    this.data.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.data.directiveAttributes
+  );
+  /** @type {Record.<string, string>} */
+  const cleaned = {};
+  let index = -1;
+
+  while (++index < list.length) {
+    const attribute = list[index];
+
+    cleaned[attribute[0]] = attribute[1];
+  }
+
+  this.data.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 = context.createTracker(safeOptions);
+  const sequence = fence(node);
+  const exit = context.enter(node.type);
+  let value = tracker.move(sequence + (node.name || ''));
+  /** @type {Directive|Paragraph|undefined} */
+  const label = node;
+
+  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(
+      context.containerPhrasing(label, {
+        ...tracker.current(),
+        before: value,
+        after: ']',
+      }),
+    );
+    value += tracker.move(']');
+    subexit();
+    exit();
+  }
+
+  value += tracker.move(attributes(node, context));
+
+  exit();
+  return value;
+}
+
+/** @type {ToMarkdownHandle} */
+function peekDirective() {
+  return '$';
+}
+
+/**
+ * @param {Directive} node
+ * @param {State} state
+ * @returns {string}
+ */
+function attributes(node, state) {
+  const quote = state.options.quote || '"';
+  const subset = node.type === DirectiveType.Text ? [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;
+
+  // eslint-disable-next-line no-restricted-syntax
+  for (key in attrs) {
+    if (
+      own.call(attrs, key)
+      && attrs[key] !== undefined
+      && attrs[key] !== null
+    ) {
+      const value = String(attrs[key]);
+
+      values.push(quoted(key, value));
+    }
+  }
+
+  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 {Directive} node
+ * @returns {string}
+ */
+function fence(node) {
+  let size = 0;
+
+  if (node.type === DirectiveType.Leaf) {
+    size = 1;
+  }
+  else {
+    size = 1;
+  }
+
+  return '$'.repeat(size);
+
+}

+ 1 - 2
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-leaf.js

@@ -6,8 +6,7 @@
 
 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 { codes, types } from 'micromark-util-symbol';
 import { ok as assert } from 'uvu/assert';
 
 import { factoryAttributes } from './factory-attributes.js';

+ 1 - 2
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-text.js

@@ -5,8 +5,7 @@
  * @typedef {import('micromark-util-types').State} State
  */
 
-import { codes } from 'micromark-util-symbol/codes.js';
-import { types } from 'micromark-util-symbol/types.js';
+import { codes, types } from 'micromark-util-symbol';
 import { ok as assert } from 'uvu/assert';
 
 import { factoryAttributes } from './factory-attributes.js';

+ 1 - 2
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js

@@ -11,8 +11,7 @@ import {
   markdownLineEndingOrSpace,
   markdownSpace,
 } from 'micromark-util-character';
-import { codes } from 'micromark-util-symbol/codes.js';
-import { types } from 'micromark-util-symbol/types.js';
+import { codes, types } from 'micromark-util-symbol';
 import { ok as assert } from 'uvu/assert';
 
 import { markdownLineEndingOrSpaceOrComma, factoryAttributesDevider } from '../../micromark-factory-attributes-devider/index.js';

+ 1 - 3
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-label.js

@@ -5,9 +5,7 @@
  */
 
 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 { codes, constants, types } from 'micromark-util-symbol';
 import { ok as assert } from 'uvu/assert';
 
 // This is a fork of:

+ 1 - 1
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-name.js

@@ -5,7 +5,7 @@
  */
 
 import { asciiAlpha, asciiAlphanumeric } from 'micromark-util-character';
-import { codes } from 'micromark-util-symbol/codes.js';
+import { codes } from 'micromark-util-symbol';
 
 /**
  * @this {TokenizeContext}

+ 1 - 1
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/html.js

@@ -23,7 +23,7 @@
 import { parseEntities } from 'parse-entities';
 import { ok as assert } from 'uvu/assert';
 
-import { DirectiveType } from '../../mdast-util-growi-directive/consts.js';
+import { DirectiveType } from '../../mdast-util-growi-directive/lib/index.js';
 
 const own = {}.hasOwnProperty;
 

+ 1 - 1
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/syntax.js

@@ -2,7 +2,7 @@
  * @typedef {import('micromark-util-types').Extension} Extension
  */
 
-import { codes } from 'micromark-util-symbol/codes.js';
+import { codes } from 'micromark-util-symbol';
 
 import { directiveLeaf } from './directive-leaf.js';
 import { directiveText } from './directive-text.js';

+ 1 - 1
packages/remark-growi-directive/src/micromark-factory-attributes-devider/index.js

@@ -4,7 +4,7 @@
  */
 import { factorySpace } from 'micromark-factory-space';
 import { markdownLineEnding, markdownSpace } from 'micromark-util-character';
-import { codes } from 'micromark-util-symbol/codes.js';
+import { codes } from 'micromark-util-symbol';
 
 export function markdownLineEndingOrSpaceOrComma(code) {
   return code !== null && (code < codes.nul || code === codes.space || code === codes.comma);

+ 19 - 20
packages/remark-growi-directive/src/remark-growi-directive.js

@@ -1,7 +1,6 @@
 /**
  * @typedef {import('mdast').Root} Root
- *
- * @typedef {import('mdast-util-directive')} DoNotTouchAsThisImportIncludesDirectivesInTree
+ * @typedef {import('unified').Processor<Root>} Processor
  */
 
 import { directiveFromMarkdown, directiveToMarkdown } from './mdast-util-growi-directive/index.js';
@@ -10,26 +9,26 @@ import { directive } from './micromark-extension-growi-directive/index.js';
 /**
     * Plugin to support GROWI plugin (`$lsx(/path, depth=2)`).
     *
-    * @type {import('unified').Plugin<void[], Root>}
+    * Add support for generic directives.
+    *
+    * ###### Notes
+    *
+    * Doesn’t handle the directives: create your own plugin to do that.
+    *
+    * @returns {undefined}
+    *   Nothing.
     */
 export function remarkGrowiDirectivePlugin() {
-  const data = this.data();
-
-  add('micromarkExtensions', directive());
-  add('fromMarkdownExtensions', directiveFromMarkdown);
-  add('toMarkdownExtensions', directiveToMarkdown);
+  // @ts-expect-error: TS is wrong about `this`.
+  // eslint-disable-next-line @typescript-eslint/no-this-alias
+  const self = /** @type {Processor} */ (this);
+  const data = self.data();
 
-  /**
-      * @param {string} field
-      * @param {unknown} value
-      */
-  function add(field, value) {
-    const list = /** @type {unknown[]} */ (
-      // Other extensions
-      /* c8 ignore next 2 */
-      data[field] ? data[field] : (data[field] = [])
-    );
+  const micromarkExtensions = data.micromarkExtensions || (data.micromarkExtensions = []);
+  const fromMarkdownExtensions = data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
+  const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
 
-    list.push(value);
-  }
+  micromarkExtensions.push(directive());
+  fromMarkdownExtensions.push(directiveFromMarkdown());
+  toMarkdownExtensions.push(directiveToMarkdown());
 }

+ 421 - 472
packages/remark-growi-directive/test/mdast-util-growi-directive.test.js

@@ -1,19 +1,21 @@
 import { fromMarkdown } from 'mdast-util-from-markdown';
 import { toMarkdown } from 'mdast-util-to-markdown';
-import test from 'tape';
 import { removePosition } from 'unist-util-remove-position';
+import { describe, it, expect } from 'vitest';
 
-import { DirectiveType } from '../src/mdast-util-growi-directive/consts.js';
 import { directiveFromMarkdown, directiveToMarkdown } from '../src/mdast-util-growi-directive/index.js';
+import { DirectiveType } from '../src/mdast-util-growi-directive/lib/index.js';
 import { directive } from '../src/micromark-extension-growi-directive/index.js';
 
-test('markdown -> mdast', (t) => {
-  t.deepEqual(
-    fromMarkdown('a $b[c](d) e.', {
-      extensions: [directive()],
-      mdastExtensions: [directiveFromMarkdown],
-    }).children[0],
-    {
+
+describe('markdown -> mdast', () => {
+  it('should support directives (text)', () => {
+    expect(
+      fromMarkdown('a $b[c](d) e.', {
+        extensions: [directive()],
+        mdastExtensions: [directiveFromMarkdown()],
+      }).children[0],
+    ).toEqual({
       type: 'paragraph',
       children: [
         {
@@ -56,16 +58,16 @@ test('markdown -> mdast', (t) => {
         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],
-    {
+  it('should support directives (leaf)', () => {
+    expect(
+      fromMarkdown('$a[b](c)', {
+        extensions: [directive()],
+        mdastExtensions: [directiveFromMarkdown()],
+      }).children[0],
+    ).toEqual({
       type: DirectiveType.Leaf,
       name: 'a',
       attributes: { c: '' },
@@ -83,19 +85,53 @@ test('markdown -> mdast', (t) => {
         start: { line: 1, column: 1, offset: 0 },
         end: { line: 1, column: 9, offset: 8 },
       },
-    },
-    'should support directives (leaf)',
-  );
+    });
+  });
 
-  t.deepEqual(
-    removePosition(
-      fromMarkdown('x $a[b *c*\nd]', {
-        extensions: [directive()],
-        mdastExtensions: [directiveFromMarkdown],
-      }),
-      true,
-    ),
-    {
+
+  it('should support content in a label', () => {
+    const tree = fromMarkdown('x $a[b *c*\nd]', {
+      extensions: [directive()],
+      mdastExtensions: [directiveFromMarkdown()],
+    });
+
+    removePosition(tree, { force: true });
+
+    expect(tree).toEqual(
+      {
+        type: 'root',
+        children: [
+          {
+            type: 'paragraph',
+            children: [
+              { type: 'text', value: 'x ' },
+              {
+                type: DirectiveType.Text,
+                name: 'a',
+                attributes: {},
+                children: [
+                  { type: 'text', value: 'b ' },
+                  { type: 'emphasis', children: [{ type: 'text', value: 'c' }] },
+                  { type: 'text', value: '\nd' },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    );
+  });
+
+
+  it('should support attributes', () => {
+    const tree = fromMarkdown('x $a(#b.c.d e=f g="h&amp;i&unknown;j")', {
+      extensions: [directive()],
+      mdastExtensions: [directiveFromMarkdown()],
+    });
+
+    removePosition(tree, { force: true });
+
+    expect(tree).toEqual({
       type: 'root',
       children: [
         {
@@ -105,491 +141,404 @@ test('markdown -> mdast', (t) => {
             {
               type: DirectiveType.Text,
               name: 'a',
-              attributes: {},
-              children: [
-                { type: 'text', value: 'b ' },
-                { type: 'emphasis', children: [{ type: 'text', value: 'c' }] },
-                { type: 'text', value: '\nd' },
-              ],
+              attributes: {
+                '#b.c.d': '', e: 'f', g: 'h&i&unknown;j',
+              },
+              children: [],
             },
           ],
         },
       ],
-    },
-    'should support content in a label',
-  );
+    });
+  });
 
-  const hoge = removePosition(
-    fromMarkdown('x $a(#b.c.d e=f g="h&amp;i&unknown;j")', {
+
+  it('should support EOLs in attributes', () => {
+    const tree = fromMarkdown('$a(b\nc="d\ne")', {
       extensions: [directive()],
-      mdastExtensions: [directiveFromMarkdown],
-    }),
-    true,
-  );
-
-  t.deepEqual(
-    removePosition(
-      fromMarkdown('x $a(#b.c.d e=f g="h&amp;i&unknown;j")', {
-        extensions: [directive()],
-        mdastExtensions: [directiveFromMarkdown],
-      }),
-      true,
-    ),
-    {
+      mdastExtensions: [directiveFromMarkdown()],
+    });
+
+    removePosition(tree, { force: true });
+
+    expect(tree).toEqual({
       type: 'root',
       children: [
         {
           type: 'paragraph',
           children: [
-            { type: 'text', value: 'x ' },
             {
               type: DirectiveType.Text,
               name: 'a',
-              attributes: {
-                '#b.c.d': '', e: 'f', g: 'h&i&unknown;j',
-              },
+              attributes: { b: '', c: 'd\ne' },
               children: [],
             },
           ],
         },
       ],
-    },
-    'should support attributes',
-  );
+    });
+  });
+});
 
-  t.deepEqual(
-    removePosition(
-      fromMarkdown('$a(b\nc="d\ne")', {
-        extensions: [directive()],
-        mdastExtensions: [directiveFromMarkdown],
-      }),
-      true,
-    ),
-    {
-      type: 'root',
-      children: [
+describe('mdast -> markdown', () => {
+  it('should try to serialize a directive (text) w/o `name`', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            // @ts-expect-error: `children`, `name` missing.
+            { type: DirectiveType.Text },
+            { type: 'text', value: ' b.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $ b.\n');
+  });
+
+  it('should serialize a directive (text) w/ `name`', () => {
+    expect(
+      toMarkdown(
         {
           type: 'paragraph',
           children: [
+            { type: 'text', value: 'a ' },
+            // @ts-expect-error: `children` missing.
+            { type: DirectiveType.Text, name: 'b' },
+            { type: 'text', value: ' c.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b c.\n');
+  });
+
+  it('should serialize a directive (text) w/ `children`', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
             {
               type: DirectiveType.Text,
-              name: 'a',
-              attributes: { b: '', c: 'd\ne' },
+              name: 'b',
+              children: [{ type: 'text', value: 'c' }],
+            },
+            { type: 'text', value: ' d.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b[c] d.\n');
+  });
+
+  it('should escape brackets in a directive (text) label', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              name: 'b',
+              children: [{ type: 'text', value: 'c[d]e' }],
+            },
+            { type: 'text', value: ' f.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b[c\\[d\\]e] f.\n');
+  });
+
+  it('should support EOLs in a directive (text) label', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              name: 'b',
+              children: [{ type: 'text', value: 'c\nd' }],
+            },
+            { type: 'text', value: ' e.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b[c\nd] e.\n');
+  });
+
+  it('should serialize a directive (text) w/ `attributes`', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              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.' },
           ],
         },
-      ],
-    },
-    'should support EOLs in attributes',
-  );
-
-  t.end();
-});
-
-test('mdast -> markdown', (t) => {
-  t.deepEqual(
-    toMarkdown(
-      {
-        type: 'paragraph',
-        children: [
-          { type: 'text', value: 'a ' },
-          // @ts-expect-error: `children`, `name` missing.
-          { type: DirectiveType.Text },
-          { 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: DirectiveType.Text, 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: DirectiveType.Text,
-            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: DirectiveType.Text,
-            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: DirectiveType.Text,
-            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: DirectiveType.Text,
-            name: 'b',
-            // @ts-expect-error: should contain only `string`s
-            attributes: {
-              c: 'd', e: 'f', g: '', h: null, i: undefined, j: 2,
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b(c="d" e="f" g j="2") k.\n');
+  });
+
+  it('should serialize a directive (text) w/ hash, dot notation attributes', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              name: 'b',
+              attributes: { '#d': '', '.a.b.c': '', key: 'value' },
+              children: [],
             },
-            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: DirectiveType.Text,
-            name: 'b',
-            attributes: { '#d': '', '.a.b.c': '', 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/ hash, dot notation attributes',
-  );
-
-  t.deepEqual(
-    toMarkdown(
-      {
-        type: 'paragraph',
-        children: [
-          { type: 'text', value: 'a ' },
-          {
-            type: DirectiveType.Text,
-            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: DirectiveType.Text,
-            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: DirectiveType.Text,
-            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: DirectiveType.Text,
-            name: 'b',
-            attributes: { 'c.d': '', 'e<f': '' },
-            children: [],
-          },
-          { type: 'text', value: ' g.' },
-        ],
-      },
-      { extensions: [directiveToMarkdown] },
-    ),
-    'a $b(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: DirectiveType.Text,
-            name: 'b',
-            attributes: {
-              'c.d': '', e: '', 'f<g': '', hij: '',
+            { type: 'text', value: ' k.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b(#d .a.b.c key="value") k.\n');
+  });
+
+  it('should encode the quote in an attribute value (text)', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              name: 'b',
+              attributes: { x: 'y"\'\r\nz' },
+              children: [],
             },
-            children: [],
-          },
-          { type: 'text', value: ' k.' },
-        ],
-      },
-      { extensions: [directiveToMarkdown] },
-    ),
-    'a $b(c.d e f<g hij) k.\n',
-    'should not use the `class` shortcut if impossible characters exist (but should use it for classes that don’t)',
-  );
+            { type: 'text', value: ' k.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b(x="y&#x22;\'\r\nz") k.\n');
+  });
+
+  it('should not use the `id` shortcut if impossible characters exist', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              name: 'b',
+              attributes: { id: 'c#d' },
+              children: [],
+            },
+            { type: 'text', value: ' e.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b(id="c#d") e.\n');
+  });
+
+  it('should not use the `class` shortcut if impossible characters exist', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              name: 'b',
+              attributes: { 'c.d': '', 'e<f': '' },
+              children: [],
+            },
+            { type: 'text', value: ' g.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b(c.d e<f) g.\n');
+  });
+
+  it('should not use the `class` shortcut if impossible characters exist (but should use it for classes that don\'t)', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [
+            { type: 'text', value: 'a ' },
+            {
+              type: DirectiveType.Text,
+              name: 'b',
+              attributes: {
+                'c.d': '', e: '', 'f<g': '', hij: '',
+              },
+              children: [],
+            },
+            { type: 'text', value: ' k.' },
+          ],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a $b(c.d e f<g hij) k.\n');
+  });
 
-  t.deepEqual(
+  it('should try to serialize a directive (leaf) w/o `name`', () => {
     // @ts-expect-error: `children`, `name` missing.
-    toMarkdown({ type: DirectiveType.Leaf }, { extensions: [directiveToMarkdown] }),
-    '$\n',
-    'should try to serialize a directive (leaf) w/o `name`',
-  );
-
-  t.deepEqual(
-    toMarkdown(
-      // @ts-expect-error: `children` missing.
-      { type: DirectiveType.Leaf, name: 'a' },
-      { extensions: [directiveToMarkdown] },
-    ),
-    '$a\n',
-    'should serialize a directive (leaf) w/ `name`',
-  );
-
-  t.deepEqual(
-    toMarkdown(
-      {
-        type: DirectiveType.Leaf,
-        name: 'a',
-        children: [{ type: 'text', value: 'b' }],
-      },
-      { extensions: [directiveToMarkdown] },
-    ),
-    '$a[b]\n',
-    'should serialize a directive (leaf) w/ `children`',
-  );
-
-  t.deepEqual(
-    toMarkdown(
-      {
-        type: DirectiveType.Leaf,
-        name: 'a',
-        children: [{ type: 'text', value: 'b' }],
-      },
-      { extensions: [directiveToMarkdown] },
-    ),
-    '$a[b]\n',
-    'should serialize a directive (leaf) w/ `children`',
-  );
-
-  t.deepEqual(
-    toMarkdown(
-      {
-        type: DirectiveType.Leaf,
-        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: DirectiveType.Leaf,
-        name: 'a',
-        attributes: { '#b': '', '.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(
-      {
-        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(
-      {
+    expect(
+      toMarkdown(
+        { type: DirectiveType.Leaf },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('$\n');
+  });
+
+  it('should serialize a directive (leaf) w/ `name`', () => {
+    // @ts-expect-error: `children` missing.
+    expect(
+      toMarkdown(
+        { type: DirectiveType.Leaf, name: 'a' },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('$a\n');
+  });
+
+  it('should serialize a directive (leaf) w/ `children`', () => {
+    expect(
+      toMarkdown(
+        {
+          type: DirectiveType.Leaf,
+          name: 'a',
+          children: [{ type: 'text', value: 'b' }],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('$a[b]\n');
+  });
+
+  it('should serialize a directive (leaf) w/ EOLs in `children`', () => {
+    expect(
+      toMarkdown(
+        {
+          type: DirectiveType.Leaf,
+          name: 'a',
+          children: [{ type: 'text', value: 'b\nc' }],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('$a[b&#xA;c]\n');
+  });
+
+  it('should serialize a directive (leaf) w/ EOLs in `attributes`', () => {
+    expect(
+      toMarkdown(
+        {
+          type: DirectiveType.Leaf,
+          name: 'a',
+          attributes: { '#b': '', '.c.d': '', key: 'e\nf' },
+          children: [],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('$a(#b .c.d key="e&#xA;f")\n');
+  });
+
+  it('should escape a `:` in phrasing when followed by an alpha', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [{ type: 'text', value: 'a$b' }],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('a\\$b\n');
+  });
+
+  it('should not escape a `:` in phrasing when followed by a non-alpha', () => {
+    expect(
+      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(
-      {
+      }, { extensions: [directiveToMarkdown()] }),
+    ).toBe('a$9\n');
+  });
+
+  it('should not escape a `:` in phrasing when preceded by a colon', () => {
+    expect(
+      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(
-      {
+      }, { extensions: [directiveToMarkdown()] }),
+    ).toBe('a\\$c\n');
+  });
+
+  it('should not escape a `:` at a break', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [{ type: 'text', value: '$\na' }],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('$\na\n');
+  });
+
+  it('should not escape a `:` at a break when followed by an alpha', () => {
+    expect(
+      toMarkdown(
+        {
+          type: 'paragraph',
+          children: [{ type: 'text', value: '$a' }],
+        },
+        { extensions: [directiveToMarkdown()] },
+      ),
+    ).toBe('\\$a\n');
+  });
+
+  it('should escape a `:` at a break when followed by a colon', () => {
+    expect(
+      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(
-      {
+      }, { extensions: [directiveToMarkdown()] }),
+    ).toBe('$\na\n');
+  });
+
+  it('should escape a `:` after a text directive', () => {
+    expect(
+      toMarkdown({
         type: 'paragraph',
         children: [
           { type: DirectiveType.Text, name: 'red', children: [] },
           { type: 'text', value: '$' },
         ],
-      },
-      { extensions: [directiveToMarkdown] },
-    ),
-    '$red$\n',
-    'should escape a `:` after a text directive',
-  );
+      }, { extensions: [directiveToMarkdown()] }),
+    ).toBe('$red$\n');
+  });
 
-  t.end();
 });

+ 694 - 905
packages/remark-growi-directive/test/micromark-extension-growi-directive.test.js

@@ -5,1048 +5,837 @@
 
 import { htmlVoidElements } from 'html-void-elements';
 import { micromark } from 'micromark';
-import test from 'tape';
+import { describe, it, expect } from 'vitest';
 
-import { DirectiveType } from '../src/mdast-util-growi-directive/consts.js';
+import { DirectiveType } from '../src/mdast-util-growi-directive/lib/index.js';
 import { directive as syntax, directiveHtml as html } from '../src/micromark-extension-growi-directive/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',
-    );
+describe('micromark-extension-directive (syntax)', () => {
+  describe('text', () => {
+    it('should support an escaped colon which would otherwise be a directive', () => {
+      expect(micromark('\\$a', options())).toBe('<p>$a</p>');
+    });
 
-    // t.equal(
-    //   micromark('a :$b', options()),
-    //   '<p>a :$b</p>',
-    //   'should not support a directive after a colon',
-    // );
+    it('should support a directive after an escaped colon', () => {
+      expect(micromark('\\$$a', options())).toBe('<p>$</p>');
+    });
 
-    t.equal(
-      micromark('$', options()),
-      '<p>$</p>',
-      'should not support a colon not followed by an alpha',
-    );
+    // it('should not support a directive after a colon', () => {
+    //   expect(micromark('a :$b', options())).toBe('<p>a :$b</p>');
+    // });
 
-    t.equal(
-      micromark('a $a', options()),
-      '<p>a </p>',
-      'should support a colon followed by an alpha',
-    );
+    it('should not support a colon not followed by an alpha', () => {
+      expect(micromark('$', options())).toBe('<p>$</p>');
+    });
 
-    t.equal(
-      micromark('$9', options()),
-      '<p>$9</p>',
-      'should not support a colon followed by a digit',
-    );
+    it('should support a colon followed by an alpha', () => {
+      expect(micromark('a $a', options())).toBe('<p>a </p>');
+    });
 
-    t.equal(
-      micromark('$-', options()),
-      '<p>$-</p>',
-      'should not support a colon followed by a dash',
-    );
+    it('should not support a colon followed by a digit', () => {
+      expect(micromark('$9', options())).toBe('<p>$9</p>');
+    });
 
-    t.equal(
-      micromark('$_', options()),
-      '<p>$_</p>',
-      'should not support a colon followed by an underscore',
-    );
+    it('should not support a colon followed by a dash', () => {
+      expect(micromark('$-', options())).toBe('<p>$-</p>');
+    });
 
-    t.equal(
-      micromark('a $a9', options()),
-      '<p>a </p>',
-      'should support a digit in a name',
-    );
+    it('should not support a colon followed by an underscore', () => {
+      expect(micromark('$_', options())).toBe('<p>$_</p>');
+    });
 
-    t.equal(
-      micromark('a $a-b', options()),
-      '<p>a </p>',
-      'should support a dash in a name',
-    );
+    it('should support a digit in a name', () => {
+      expect(micromark('a $a9', options())).toBe('<p>a </p>');
+    });
 
-    t.equal(
-      micromark('$a-', options()),
-      '<p>$a-</p>',
-      'should *not* support a dash at the end of a name',
-    );
+    it('should support a dash in a name', () => {
+      expect(micromark('a $a-b', options())).toBe('<p>a </p>');
+    });
 
-    t.equal(
-      micromark('a $a_b', options()),
-      '<p>a </p>',
-      'should support an underscore in a name',
-    );
+    it('should *not* support a dash at the end of a name', () => {
+      expect(micromark('$a-', options())).toBe('<p>$a-</p>');
+    });
 
-    t.equal(
-      micromark('$a_', options()),
-      '<p>$a_</p>',
-      'should *not* support an underscore at the end of a name',
-    );
+    it('should support an underscore in a name', () => {
+      expect(micromark('a $a_b', options())).toBe('<p>a </p>');
+    });
 
-    t.equal(
-      micromark('$a$', options()),
-      '<p>$a$</p>',
-      'should *not* support a colon right after a name',
-    );
+    it('should *not* support an underscore at the end of a name', () => {
+      expect(micromark('$a_', options())).toBe('<p>$a_</p>');
+    });
 
-    t.equal(
-      micromark('_$directive_', options()),
-      '<p><em>$directive</em></p>',
-      'should not interfere w/ emphasis (`_`)',
-    );
+    it('should *not* support a colon right after a name', () => {
+      expect(micromark('$a$', options())).toBe('<p>$a$</p>');
+    });
 
-    t.equal(
-      micromark('$a[', options()),
-      '<p>[</p>',
-      'should support a name followed by an unclosed `[`',
-    );
+    it('should not interfere w/ emphasis (`_`)', () => {
+      expect(micromark('_$directive_', options())).toBe('<p><em>$directive</em></p>');
+    });
 
-    t.equal(
-      micromark('$a(', options()),
-      '<p>(</p>',
-      'should support a name followed by an unclosed `(`',
-    );
+    it('should support a name followed by an unclosed `[`', () => {
+      expect(micromark('$a[', options())).toBe('<p>[</p>');
+    });
 
-    t.equal(
-      micromark('$a[b', options()),
-      '<p>[b</p>',
-      'should support a name followed by an unclosed `[` w/ content',
-    );
+    it('should support a name followed by an unclosed `(`', () => {
+      expect(micromark('$a(', options())).toBe('<p>(</p>');
+    });
 
-    t.equal(
-      micromark('$a(b', options()),
-      '<p>(b</p>',
-      'should support a name followed by an unclosed `(` w/ content',
-    );
+    it('should support a name followed by an unclosed `[` w/ content', () => {
+      expect(micromark('$a[b', options())).toBe('<p>[b</p>');
+    });
 
-    t.equal(
-      micromark('a $a[]', options()),
-      '<p>a </p>',
-      'should support an empty label',
-    );
+    it('should support a name followed by an unclosed `(` w/ content', () => {
+      expect(micromark('$a(b', options())).toBe('<p>(b</p>');
+    });
 
-    t.equal(
-      micromark('a $a[ \t]', options()),
-      '<p>a </p>',
-      'should support a whitespace only label',
-    );
+    it('should support an empty label', () => {
+      expect(micromark('a $a[]', options())).toBe('<p>a </p>');
+    });
 
-    t.equal(
-      micromark('$a[\n]', options()),
-      '<p></p>',
-      'should support an eol in an label',
-    );
+    it('should support a whitespace only label', () => {
+      expect(micromark('a $a[ \t]', options())).toBe('<p>a </p>');
+    });
 
-    t.equal(
-      micromark('$a[a b c]asd', options()),
-      '<p>asd</p>',
-      'should support content in an label',
-    );
+    it('should support an eol in an label', () => {
+      expect(micromark('$a[\n]', options())).toBe('<p></p>');
+    });
 
-    t.equal(
-      micromark('$a[a *b* c]asd', options()),
-      '<p>asd</p>',
-      'should support markdown in an label',
-    );
+    it('should support content in an label', () => {
+      expect(micromark('$a[a b c]asd', options())).toBe('<p>asd</p>');
+    });
 
-    t.equal(
-      micromark('a $b[c :d[e] f] g', options()),
-      '<p>a  g</p>',
-      'should support a directive in an label',
-    );
+    it('should support markdown in an label', () => {
+      expect(micromark('$a[a *b* c]asd', options())).toBe('<p>asd</p>');
+    });
 
-    t.equal(
-      micromark('$a[]asd', options()),
-      '<p>asd</p>',
-      'should support content after a label',
-    );
+    // == Resolved as text directive
+    // t.equal(
+    //   micromark('$a[]asd', options()),
+    //   '<p>$a[]asd</p>',
+    //   'should not support content after a label',
+    // );
 
-    t.equal(
-      micromark('a $a()', options()),
-      '<p>a </p>',
-      'should support empty attributes',
-    );
+    it('should support a directive in an label', () => {
+      expect(
+        micromark('a $b[c :d[e] f] g', options()),
+      ).toBe('<p>a  g</p>');
+    });
+
+    it('should support content after a label', () => {
+      expect(
+        micromark('$a[]asd', options()),
+      ).toBe('<p>asd</p>');
+    });
+
+    it('should support empty attributes', () => {
+      expect(
+        micromark('a $a()', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support whitespace only attributes', () => {
+      expect(
+        micromark('a $a( \t)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support an eol in attributes', () => {
+      expect(micromark('$a(\n)', options())).toBe('<p></p>');
+    });
+
+    it('should support attributes w/o values', () => {
+      expect(micromark('a $a(a b c)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attributes w/ unquoted values', () => {
+      expect(micromark('a $a(a=b c=d)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attributes w/ class shortcut', () => {
+      expect(micromark('a $a(.a .b)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attributes w/ class shortcut w/o whitespace between', () => {
+      expect(micromark('a $a(.a.b)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attributes w/ id shortcut', () => {
+      expect(micromark('a $a(#a #b)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attributes w/ id shortcut w/o whitespace between', () => {
+      expect(micromark('a $a(#a#b)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attributes w/ shortcuts combined w/ other attributes', () => {
+      expect(micromark('a $a(#a.b.c#d e f=g #h.i.j)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attrs which starts w/ continuous dots', () => {
+      expect(micromark('a $a(..b)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attrs which start w/ `#`', () => {
+      expect(micromark('a $a(.#b)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support attrs w/ (`.`)', () => {
+      expect(micromark('a $a(.)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support with the attr `(.a=b)`', () => {
+      expect(micromark('a $a(.a=b)', options())).toBe('<p>a </p>');
+    });
+
+    it('should support with the attr `(.a"b)`', () => {
+      expect(
+        micromark('a $a(.a"b)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support with the attr `(.a<b)`', () => {
+      expect(
+        micromark('a $a(.a<b)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support most characters in shortcuts', () => {
+      expect(
+        micromark('a $a(.a💚b)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support an underscore in attribute names', () => {
+      expect(
+        micromark('a $a(_)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support a colon in attribute names', () => {
+      expect(
+        micromark('a $a(xml:lang)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support double quoted attributes', () => {
+      expect(
+        micromark('a $a(a="b" c="d e f")', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support single quoted attributes', () => {
+      expect(
+        micromark("a $a(a='b' c='d e f')", options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support whitespace around initializers', () => {
+      expect(
+        micromark('a $a(a = b c\t=\t\'d\' f  =\r"g")', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should not support `=` to start an unquoted attribute value', () => {
+      expect(micromark('$a(b==)', options())).toBe('<p>(b==)</p>');
+    });
+
+    it('should not support a missing attribute value after `=`', () => {
+      expect(micromark('$a(b=)', options())).toBe('<p>(b=)</p>');
+    });
+
+    it('should not support an apostrophe in an unquoted attribute value', () => {
+      expect(micromark("$a(b=c')", options())).toBe("<p>(b=c')</p>");
+    });
+
+    it('should not support a grave accent in an unquoted attribute value', () => {
+      expect(micromark('$a(b=c`)', options())).toBe('<p>(b=c`)</p>');
+    });
+
+    it('should support most other characters in attribute keys', () => {
+      expect(
+        micromark('a $a(b💚=a💚b)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support most other characters in unquoted attribute values', () => {
+      expect(
+        micromark('a $a(b=a💚b)', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should not support an EOF in a quoted attribute value', () => {
+      expect(
+        micromark('$a(b="c', options()),
+      ).toBe('<p>(b=&quot;c</p>');
+    });
+
+    it('should support most other characters in quoted attribute values', () => {
+      expect(
+        micromark('a $a(b="a💚b")', options()),
+      ).toBe('<p>a </p>');
+    });
+
+    it('should support EOLs in quoted attribute values', () => {
+      expect(
+        micromark('$a(b="\nc\r  d")', options()),
+      ).toBe(
+        '<p></p>',
+      );
+    });
+
+    it('should not support an EOF after a quoted attribute value', () => {
+      expect(
+        micromark('$a(b="c"', options()),
+      ).toBe(
+        '<p>(b=&quot;c&quot;</p>',
+      );
+    });
 
-    t.equal(
-      micromark('a $a( \t)', options()),
-      '<p>a </p>',
-      'should support whitespace only attributes',
-    );
+  });
 
-    t.equal(
-      micromark('$a(\n)', options()),
-      '<p></p>',
-      'should support an eol in attributes',
-    );
+  describe('leaf', () => {
 
-    t.equal(
-      micromark('a $a(a b c)', options()),
-      '<p>a </p>',
-      'should support attributes w/o values',
-    );
+    it('should support a directive', () => {
+      expect(micromark('$b', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(a=b c=d)', options()),
-      '<p>a </p>',
-      'should support attributes w/ unquoted values',
-    );
+    it('should not support one colon', () => {
+      expect(micromark(':', options())).toBe('<p>:</p>');
+    });
 
-    t.equal(
-      micromark('a $a(.a .b)', options()),
-      '<p>a </p>',
-      'should support attributes w/ class shortcut',
-    );
+    it('should not support two colons not followed by an alpha', () => {
+      expect(micromark('::', options())).toBe('<p>::</p>');
+    });
 
-    t.equal(
-      micromark('a $a(.a.b)', options()),
-      '<p>a </p>',
-      'should support attributes w/ class shortcut w/o whitespace between',
-    );
+    it('should support two colons followed by an alpha', () => {
+      expect(micromark('$a', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(#a #b)', options()),
-      '<p>a </p>',
-      'should support attributes w/ id shortcut',
-    );
+    it('should not support two colons followed by a digit', () => {
+      expect(micromark('$9', options())).toBe('<p>$9</p>');
+    });
 
-    t.equal(
-      micromark('a $a(#a#b)', options()),
-      '<p>a </p>',
-      'should support attributes w/ id shortcut w/o whitespace between',
-    );
+    it('should not support two colons followed by a dash', () => {
+      expect(micromark('$-', options())).toBe('<p>$-</p>');
+    });
 
-    t.equal(
-      micromark('a $a(#a.b.c#d e f=g #h.i.j)', options()),
-      '<p>a </p>',
-      'should support attributes w/ shortcuts combined w/ other attributes',
-    );
+    it('should support a digit in a name', () => {
+      expect(micromark('$a9', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(..b)', options()),
-      '<p>a </p>',
-      'should support attrs which starts w/ continuous dots',
-    );
+    it('should support a dash in a name', () => {
+      expect(micromark('$a-b', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(.#b)', options()),
-      '<p>a </p>',
-      'should support attrs which start w/ `#`',
-    );
+    // == Resolved as text directive
+    // it('should not support a name followed by an unclosed `[`', () => {
+    //   expect(micromark('$a[', options())).toBe('<p>$a[</p>');
+    // });
 
-    t.equal(
-      micromark('a $a(.)', options()),
-      '<p>a </p>',
-      'should support attrs w/ (`.`)',
-    );
+    // == Resolved as text directive
+    // it('should not support a name followed by an unclosed `{`', () => {
+    //   expect(micromark('$a{', options())).toBe('<p>$a{</p>');
+    // });
 
-    t.equal(
-      micromark('a $a(.a=b)', options()),
-      '<p>a </p>',
-      'should support with the attr `(.a=b)`',
-    );
+    // == Resolved as text directive
+    // it('should not support a name followed by an unclosed `[` w/ content', () => {
+    //   expect(micromark('$a[b', options())).toBe('<p>$a[b</p>');
+    // });
 
-    t.equal(
-      micromark('a $a(.a"b)', options()),
-      '<p>a </p>',
-      'should support with the attr `(.a"b)`',
-    );
+    // == Resolved as text directive
+    // it('should not support a name followed by an unclosed `{` w/ content', () => {
+    //   expect(micromark('$a{b', options())).toBe('<p>$a{b</p>');
+    // });
 
-    t.equal(
-      micromark('a $a(.a<b)', options()),
-      '<p>a </p>',
-      'should support with the attr `(.a<b)`',
-    );
+    it('should support an empty label', () => {
+      expect(micromark('$a[]', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(.a💚b)', options()),
-      '<p>a </p>',
-      'should support most characters in shortcuts',
-    );
+    it('should support a whitespace only label', () => {
+      expect(micromark('$a[ \t]', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(_)', options()),
-      '<p>a </p>',
-      'should support an underscore in attribute names',
-    );
+    // == Resolved as text directive
+    // it('should not support an eol in an label', () => {
+    //   expect(micromark('$a[\n]', options())).toBe('<p>$a[\n]</p>');
+    // });
 
-    t.equal(
-      micromark('a $a(xml:lang)', options()),
-      '<p>a </p>',
-      'should support a colon in attribute names',
-    );
+    it('should support content in an label', () => {
+      expect(micromark('$a[a b c]', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(a="b" c="d e f")', options()),
-      '<p>a </p>',
-      'should support double quoted attributes',
-    );
+    it('should support markdown in an label', () => {
+      expect(micromark('$a[a *b* c]', options())).toBe('');
+    });
 
-    t.equal(
-      micromark("a $a(a='b' c='d e f')", options()),
-      '<p>a </p>',
-      'should support single quoted attributes',
-    );
+    // == Resolved as text directive
+    // it('should not support content after a label', () => {
+    //   expect(micromark('$a[]asd', options())).toBe('<p>$a[]asd</p>');
+    // });
 
-    t.equal(
-      micromark('a $a(a = b c\t=\t\'d\' f  =\r"g")', options()),
-      '<p>a </p>',
-      'should support whitespace around initializers',
-    );
+    it('should support empty attributes', () => {
+      expect(micromark('$a()', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a(b==)', options()),
-      '<p>(b==)</p>',
-      'should not support `=` to start an unquoted attribute value',
-    );
+    it('should support whitespace only attributes', () => {
+      expect(micromark('$a( \t)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a(b=)', options()),
-      '<p>(b=)</p>',
-      'should not support a missing attribute value after `=`',
-    );
+    // == Resolved as text directive
+    // it('should not support an eol in attributes', () => {
+    //   expect(micromark('$a(\n)', options())).toBe('<p>$a(\n)</p>');
+    // });
 
-    t.equal(
-      micromark("$a(b=c')", options()),
-      "<p>(b=c')</p>",
-      'should not support an apostrophe in an unquoted attribute value',
-    );
+    it('should support attributes w/o values', () => {
+      expect(micromark('$a(a b c)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a(b=c`)', options()),
-      '<p>(b=c`)</p>',
-      'should not support a grave accent in an unquoted attribute value',
-    );
+    it('should support attributes w/ unquoted values', () => {
+      expect(micromark('$a(a=b c=d)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(b💚=a💚b)', options()),
-      '<p>a </p>',
-      'should support most other characters in attribute keys',
-    );
+    it('should support attributes w/ class shortcut', () => {
+      expect(micromark('$a(.a .b)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(b=a💚b)', options()),
-      '<p>a </p>',
-      'should support most other characters in unquoted attribute values',
-    );
+    it('should support attributes w/ id shortcut', () => {
+      expect(micromark('$a(#a #b)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a(b="c', options()),
-      '<p>(b=&quot;c</p>',
-      'should not support an EOF in a quoted attribute value',
-    );
+    it('should support most characters in shortcuts', () => {
+      expect(micromark('$a(.a💚b)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('a $a(b="a💚b")', options()),
-      '<p>a </p>',
-      'should support most other characters in quoted attribute values',
-    );
+    it('should support double quoted attributes', () => {
+      expect(micromark('$a(a="b" c="d e f")', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a(b="\nc\r  d")', options()),
-      '<p></p>',
-      'should support EOLs in quoted attribute values',
-    );
+    it('should support single quoted attributes', () => {
+      expect(micromark("$a(a='b' c='d e f')", options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a(b="c"', options()),
-      '<p>(b=&quot;c&quot;</p>',
-      'should not support an EOF after a quoted attribute value',
-    );
+    it('should support whitespace around initializers', () => {
+      expect(micromark("$a(a = b c\t=\t'd')", options())).toBe('');
+    });
 
-    t.end();
-  });
+    // == Resolved as text directive
+    // it('should not support EOLs around initializers', () => {
+    //   expect(micromark('$a(f  =\rg)', options())).toBe('<p>$a(f  =\rg)</p>');
+    // });
 
-  t.test('leaf', (t) => {
-    t.equal(micromark('$b', options()), '', 'should support a directive');
+    // == Resolved as text directive
+    // it('should not support `=` to start an unquoted attribute value', () => {
+    //   expect(micromark('$a(b==)', options())).toBe('<p>$a(b==)</p>');
+    // });
 
-    t.equal(
-      micromark(':', options()),
-      '<p>:</p>',
-      'should not support one colon',
-    );
+    it('should support most other characters in attribute keys', () => {
+      expect(micromark('$a(b💚=a💚b)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('::', options()),
-      '<p>::</p>',
-      'should not support two colons not followed by an alpha',
-    );
+    it('should support most other characters in unquoted attribute values', () => {
+      expect(micromark('$a(b=a💚b)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a', options()),
-      '',
-      'should support two colons followed by an alpha',
-    );
+    it('should not support an EOF in a quoted attribute value', () => {
+      expect(micromark('$a(b="c', options())).toBe('<p>(b=&quot;c</p>');
+    });
 
-    t.equal(
-      micromark('$9', options()),
-      '<p>$9</p>',
-      'should not support two colons followed by a digit',
-    );
+    it('should support most other characters in quoted attribute values', () => {
+      expect(micromark('$a(b="a💚b")', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$-', options()),
-      '<p>$-</p>',
-      'should not support two colons followed by a dash',
-    );
+    it('should not support EOLs in quoted attribute values', () => {
+      expect(micromark('$a(b="\nc\r  d")', options())).toBe('<p></p>');
+    });
 
-    t.equal(
-      micromark('$a9', options()),
-      '',
-      'should support a digit in a name',
-    );
+    it('should not support an EOF after a quoted attribute value', () => {
+      expect(micromark('$a(b="c"', options())).toBe('<p>(b=&quot;c&quot;</p>');
+    });
 
-    t.equal(
-      micromark('$a-b', options()),
-      '',
-      'should support a dash in a name',
-    );
+    it('should support whitespace after directives', () => {
+      expect(micromark('$a(b=c) \t ', options())).toBe('');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a[', options()),
-    //   '<p>$a[</p>',
-    //   'should not support a name followed by an unclosed `[`',
-    // );
+    it('should support a block quote after a leaf', () => {
+      expect(micromark('$a(b=c)\n>a', options())).toBe('<blockquote>\n<p>a</p>\n</blockquote>');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a{', options()),
-    //   '<p>$a{</p>',
-    //   'should not support a name followed by an unclosed `{`',
-    // );
+    it('should support code (fenced) after a leaf', () => {
+      expect(micromark('$a(b=c)\n```js\na', options())).toBe('<pre><code class="language-js">a\n</code></pre>\n');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a[b', options()),
-    //   '<p>$a[b</p>',
-    //   'should not support a name followed by an unclosed `[` w/ content',
-    // );
+    it('should support code (indented) after a leaf', () => {
+      expect(micromark('$a(b=c)\n    a', options())).toBe('<pre><code>a\n</code></pre>');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a{b', options()),
-    //   '<p>$a{b</p>',
-    //   'should not support a name followed by an unclosed `{` w/ content',
-    // );
+    it('should support a definition after a leaf', () => {
+      expect(micromark('$a(b=c)\n[a]: b', options())).toBe('');
+    });
 
-    t.equal(micromark('$a[]', options()), '', 'should support an empty label');
+    it('should support a heading (atx) after a leaf', () => {
+      expect(micromark('$a(b=c)\n# a', options())).toBe('<h1>a</h1>');
+    });
 
-    t.equal(
-      micromark('$a[ \t]', options()),
-      '',
-      'should support a whitespace only label',
-    );
+    it('should support a heading (setext) after a leaf', () => {
+      expect(micromark('$a(b=c)\na\n=', options())).toBe('<h1>a</h1>');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a[\n]', options()),
-    //   '<p>$a[\n]</p>',
-    //   'should not support an eol in an label',
-    // );
+    it('should support html after a leaf', () => {
+      expect(micromark('$a(b=c)\n<!-->', options())).toBe('<!-->');
+    });
 
-    t.equal(
-      micromark('$a[a b c]', options()),
-      '',
-      'should support content in an label',
-    );
+    it('should support a list after a leaf', () => {
+      expect(micromark('$a(b=c)\n* a', options())).toBe('<ul>\n<li>a</li>\n</ul>');
+    });
 
-    t.equal(
-      micromark('$a[a *b* c]', options()),
-      '',
-      'should support markdown in an label',
-    );
+    it('should support a paragraph after a leaf', () => {
+      expect(micromark('$a(b=c)\na', options())).toBe('<p>a</p>');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a[]asd', options()),
-    //   '<p>$a[]asd</p>',
-    //   'should not support content after a label',
-    // );
+    it('should support a thematic break after a leaf', () => {
+      expect(micromark('$a(b=c)\n***', options())).toBe('<hr />');
+    });
 
-    t.equal(
-      micromark('$a()', options()),
-      '',
-      'should support empty attributes',
-    );
+    it('should support a block quote before a leaf', () => {
+      expect(micromark('>a\n$a(b=c)', options())).toBe('<blockquote>\n<p>a</p>\n</blockquote>\n');
+    });
 
-    t.equal(
-      micromark('$a( \t)', options()),
-      '',
-      'should support whitespace only attributes',
-    );
+    it('should support code (fenced) before a leaf', () => {
+      expect(micromark('```js\na\n```\n$a(b=c)', options())).toBe('<pre><code class="language-js">a\n</code></pre>\n');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a(\n)', options()),
-    //   '<p>$a(\n)</p>',
-    //   'should not support an eol in attributes',
-    // );
+    it('should support code (indented) before a leaf', () => {
+      expect(micromark('    a\n$a(b=c)', options())).toBe('<pre><code>a\n</code></pre>\n');
+    });
 
-    t.equal(
-      micromark('$a(a b c)', options()),
-      '',
-      'should support attributes w/o values',
-    );
+    it('should support a definition before a leaf', () => {
+      expect(micromark('[a]: b\n$a(b=c)', options())).toBe('');
+    });
 
-    t.equal(
-      micromark('$a(a=b c=d)', options()),
-      '',
-      'should support attributes w/ unquoted values',
-    );
+    it('should support a heading (atx) before a leaf', () => {
+      expect(micromark('# a\n$a(b=c)', options())).toBe('<h1>a</h1>\n');
+    });
 
-    t.equal(
-      micromark('$a(.a .b)', options()),
-      '',
-      'should support attributes w/ class shortcut',
-    );
+    it('should support a heading (setext) before a leaf', () => {
+      expect(micromark('a\n=\n$a(b=c)', options())).toBe('<h1>a</h1>\n');
+    });
 
-    t.equal(
-      micromark('$a(#a #b)', options()),
-      '',
-      'should support attributes w/ id shortcut',
-    );
+    it('should support html before a leaf', () => {
+      expect(micromark('<!-->\n$a(b=c)', options())).toBe('<!-->\n');
+    });
 
-    t.equal(
-      micromark('$a(.a💚b)', options()),
-      '',
-      'should support most characters in shortcuts',
-    );
+    it('should support a list before a leaf', () => {
+      expect(micromark('* a\n$a(b=c)', options())).toBe('<ul>\n<li>a</li>\n</ul>\n');
+    });
 
-    t.equal(
-      micromark('$a(a="b" c="d e f")', options()),
-      '',
-      'should support double quoted attributes',
-    );
+    it('should support a paragraph before a leaf', () => {
+      expect(micromark('a\n$a(b=c)', options())).toBe('<p>a</p>\n');
+    });
 
-    t.equal(
-      micromark("$a(a='b' c='d e f')", options()),
-      '',
-      'should support single quoted attributes',
-    );
+    it('should support a thematic break before a leaf', () => {
+      expect(micromark('***\n$a(b=c)', options())).toBe('<hr />\n');
+    });
 
-    t.equal(
-      micromark("$a(a = b c\t=\t'd')", options()),
-      '',
-      'should support whitespace around initializers',
-    );
+    it('should not support lazyness (1)', () => {
+      expect(micromark('> $a\nb', options({ '*': h }))).toBe('<blockquote><a></a>\n</blockquote>\n<p>b</p>');
+    });
 
-    // == Resolved as text directive
-    // t.equal(
-    //   micromark('$a(f  =\rg)', options()),
-    //   '<p>$a(f  =\rg)</p>',
-    //   'should not support EOLs around initializers',
-    // );
+    it('should not support lazyness (2)', () => {
+      expect(micromark('> a\n$b', options({ '*': h }))).toBe('<blockquote>\n<p>a</p>\n</blockquote>\n<b></b>');
+    });
 
-    // == Resolved as text directive
-    // 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 attribute keys',
-    );
+});
 
-    t.equal(
-      micromark('$a(b=a💚b)', options()),
-      '',
-      'should support most other characters in unquoted attribute values',
+describe('micromark-extension-directive (compile)', () => {
+
+  it('should support a directives (abbr)', () => {
+    expect(
+      micromark(
+        [
+          'a $abbr',
+          'a $abbr[HTML]',
+          'a $abbr(title="HyperText Markup Language")',
+          'a $abbr[HTML](title="HyperText Markup Language")',
+        ].join('\n\n'),
+        options({ abbr }),
+      ),
+    ).toBe(
+      [
+        '<p>a <abbr></abbr></p>',
+        '<p>a <abbr>HTML</abbr></p>',
+        '<p>a <abbr title="HyperText Markup Language"></abbr></p>',
+        '<p>a <abbr title="HyperText Markup Language">HTML</abbr></p>',
+      ].join('\n'),
     );
+  });
 
-    // == Resolved as text directive
-    // 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',
+  it('should support directives (youtube)', () => {
+    expect(
+      micromark(
+        [
+          'Text:',
+          'a $youtube',
+          'a $youtube[Cat in a box a]',
+          'a $youtube(v=1)',
+          'a $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)',
+        ].join('\n\n'),
+        options({ youtube }),
+      ),
+    ).toBe(
+      [
+        '<p>Text:</p>',
+        '<p>a </p>',
+        '<p>a </p>',
+        '<p>a <iframe src="https://www.youtube.com/embed/1" allowfullscreen></iframe></p>',
+        '<p>a <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>',
+      ].join('\n'),
     );
+  });
 
-    // == Resolved as text directive
-    // 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',
+  it('should support directives (lsx)', () => {
+    expect(
+      micromark(
+        [
+          'Text:',
+          'a $lsx',
+          'a $lsx()',
+          'a $lsx(num=1)',
+          'a $lsx(/)',
+          'a $lsx(/,num=5,depth=1)',
+          'a $lsx(/, num=5, depth=1)',
+          'a $lsx(💚)',
+          'Leaf:',
+          '$lsx',
+          '$lsx()',
+          '$lsx(num=1)',
+          '$lsx(/)',
+          '$lsx(/,num=5,depth=1)',
+          '$lsx(/, num=5, depth=1)',
+          '$lsx(💚)',
+        ].join('\n\n'),
+        options({ lsx }),
+      ),
+    ).toBe(
+      [
+        '<p>Text:</p>',
+        '<p>a <lsx ></lsx></p>',
+        '<p>a <lsx ></lsx></p>',
+        '<p>a <lsx num="1"></lsx></p>',
+        '<p>a <lsx prefix="/"></lsx></p>',
+        '<p>a <lsx prefix="/" num="5" depth="1"></lsx></p>',
+        '<p>a <lsx prefix="/" num="5" depth="1"></lsx></p>',
+        '<p>a <lsx prefix="💚"></lsx></p>',
+        '<p>Leaf:</p>',
+        '<lsx ></lsx>',
+        '<lsx ></lsx>',
+        '<lsx num="1"></lsx>',
+        '<lsx prefix="/"></lsx>',
+        '<lsx prefix="/" num="5" depth="1"></lsx>',
+        '<lsx prefix="/" num="5" depth="1"></lsx>',
+        '<lsx prefix="💚"></lsx>',
+      ].join('\n'),
     );
+  });
 
-    t.equal(
-      micromark('$a(b=c)\n>a', options()),
-      '<blockquote>\n<p>a</p>\n</blockquote>',
-      'should support a block quote after a leaf',
-    );
+  it('should support fall through directives (`*`)', () => {
+    expect(
+      micromark('a $youtube[Cat in a box]\n$br a', options({ youtube, '*': h })),
+    ).toBe('<p>a <youtube>Cat in a box</youtube>\n<br> a</p>');
+  });
 
-    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',
-    );
+  it('should support fall through directives (`*`)', () => {
+    expect(
+      micromark('a $a[$img(src="x" alt=y)](href="z")', options({ '*': h })),
+    ).toBe('<p>a <a href="z"><img src="x" alt="y"></a></p>');
+  });
 
-    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',
-    );
+describe('content', () => {
 
-    t.equal(
-      micromark('$a(b=c)\n# a', options()),
-      '<h1>a</h1>',
-      'should support a heading (atx) after a leaf',
-    );
+  it('should support character escapes and character references in label', () => {
+    expect(micromark('a $abbr[x\\&y&amp;z]', options({ abbr })))
+      .toBe('<p>a <abbr>x&amp;y&amp;z</abbr></p>');
+  });
 
-    t.equal(
-      micromark('$a(b=c)\na\n=', options()),
-      '<h1>a</h1>',
-      'should support a heading (setext) after a leaf',
-    );
+  it('should support escaped brackets in a label', () => {
+    expect(micromark('a $abbr[x\\[y\\]z]', options({ abbr })))
+      .toBe('<p>a <abbr>x[y]z</abbr></p>');
+  });
 
-    t.equal(
-      micromark('$a(b=c)\n<!-->', options()),
-      '<!-->',
-      'should support html after a leaf',
-    );
+  it('should support balanced brackets in a label', () => {
+    expect(micromark('a $abbr[x[y]z]', options({ abbr })))
+      .toBe('<p>a <abbr>x[y]z</abbr></p>');
+  });
 
-    t.equal(
-      micromark('$a(b=c)\n* a', options()),
-      '<ul>\n<li>a</li>\n</ul>',
-      'should support a list after a leaf',
-    );
+  it('should support balanced brackets in a label, 32 levels deep', () => {
+    expect(micromark(
+      'a $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 }),
+    )).toBe('<p>a <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>');
+  });
 
-    t.equal(
-      micromark('$a(b=c)\na', options()),
-      '<p>a</p>',
-      'should support a paragraph after a leaf',
+  it('should *not* support balanced brackets in a label, 33 levels deep', () => {
+    expect(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 }),
+    )).toBe(
+      '<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>',
     );
+  });
 
-    t.equal(
-      micromark('$a(b=c)\n***', options()),
-      '<hr />',
-      'should support a thematic break after a leaf',
-    );
+  it('should support EOLs in a label', () => {
+    expect(micromark('$abbr[a\nb\rc]', options({ abbr })))
+      .toBe('<p><abbr>a\nb\rc</abbr></p>');
+  });
 
-    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',
-    );
+  it('should support EOLs at the edges of a label (1)', () => {
+    expect(micromark('$abbr[\na\r]', options({ abbr })))
+      .toBe('<p><abbr>\na\r</abbr></p>');
+  });
 
-    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',
-    );
+  it('should support EOLs at the edges of a label (2)', () => {
+    expect(micromark('$abbr[\n]', options({ abbr })))
+      .toBe('<p><abbr>\n</abbr></p>');
+  });
 
-    t.equal(
-      micromark('    a\n$a(b=c)', options()),
-      '<pre><code>a\n</code></pre>\n',
-      'should support code (indented) before a leaf',
-    );
+  // == does not work but I don't know why.. -- 2022.08.12 Yuki Takei
+  // it('should support EOLs around nested directives', () => {
+  //   expect(micromark('$abbr[a\n$abbr[b]\nc]', options({ abbr })))
+  //     .toBe('<p><abbr>a\n<abbr>b</abbr>\nc</abbr></p>');
+  // });
+
+  it('should support EOLs inside nested directives (1)', () => {
+    expect(micromark('$abbr[$abbr[\n]]', options({ abbr })))
+      .toBe('<p><abbr><abbr>\n</abbr></abbr></p>');
+  });
 
-    t.equal(
-      micromark('[a]: b\n$a(b=c)', options()),
-      '',
-      'should support a definition before a leaf',
-    );
+  it('should support EOLs inside nested directives (2)', () => {
+    expect(micromark('$abbr[$abbr[a\nb]]', options({ abbr })))
+      .toBe('<p><abbr><abbr>a\nb</abbr></abbr></p>');
+  });
 
-    t.equal(
-      micromark('# a\n$a(b=c)', options()),
-      '<h1>a</h1>\n',
-      'should support a heading (atx) before a leaf',
-    );
+  it('should support EOLs inside nested directives (3)', () => {
+    expect(micromark('$abbr[$abbr[\nb\n]]', options({ abbr })))
+      .toBe('<p><abbr><abbr>\nb\n</abbr></abbr></p>');
+  });
 
-    t.equal(
-      micromark('a\n=\n$a(b=c)', options()),
-      '<h1>a</h1>\n',
-      'should support a heading (setext) before a leaf',
-    );
+  it('should support EOLs inside nested directives (4)', () => {
+    expect(micromark('$abbr[$abbr[\\\n]]', options({ abbr })))
+      .toBe('<p><abbr><abbr><br />\n</abbr></abbr></p>');
+  });
 
-    t.equal(
-      micromark('<!-->\n$a(b=c)', options()),
-      '<!-->\n',
-      'should support html before a leaf',
-    );
+  it('should support markdown in a label', () => {
+    expect(micromark('a $abbr[a *b* **c** d]', options({ abbr })))
+      .toBe('<p>a <abbr>a <em>b</em> <strong>c</strong> d</abbr></p>');
+  });
 
-    t.equal(
-      micromark('* a\n$a(b=c)', options()),
-      '<ul>\n<li>a</li>\n</ul>\n',
-      'should support a list before a leaf',
-    );
+  it('should support character references in unquoted attribute values', () => {
+    expect(micromark('a $abbr(title=a&apos;b)', options({ abbr })))
+      .toBe('<p>a <abbr title="a\'b"></abbr></p>');
+  });
 
-    t.equal(
-      micromark('a\n$a(b=c)', options()),
-      '<p>a</p>\n',
-      'should support a paragraph before a leaf',
-    );
+  it('should support character references in double attribute values', () => {
+    expect(micromark('a $abbr(title="a&apos;b")', options({ abbr })))
+      .toBe('<p>a <abbr title="a\'b"></abbr></p>');
+  });
 
-    t.equal(
-      micromark('***\n$a(b=c)', options()),
-      '<hr />\n',
-      'should support a thematic break before a leaf',
-    );
+  it('should support character references in single attribute values', () => {
+    expect(micromark("a $abbr(title='a&apos;b')", options({ abbr })))
+      .toBe('<p>a <abbr title="a\'b"></abbr></p>');
+  });
 
-    t.equal(
-      micromark('> $a\nb', options({ '*': h })),
-      '<blockquote><a></a>\n</blockquote>\n<p>b</p>',
-      'should not support lazyness (1)',
-    );
+  it('should support unknown character references in attribute values', () => {
+    expect(micromark('a $abbr(title="a&somethingelse;b")', options({ abbr })))
+      .toBe('<p>a <abbr title="a&amp;somethingelse;b"></abbr></p>');
+  });
 
-    t.equal(
-      micromark('> a\n$b', options({ '*': h })),
-      '<blockquote>\n<p>a</p>\n</blockquote>\n<b></b>',
-      'should not support lazyness (2)',
-    );
+  it('should support EOLs between attributes', () => {
+    expect(micromark('$span(a\nb)', options({ '*': h })))
+      .toBe('<p><span a="" b=""></span></p>');
+  });
 
-    t.end();
+  it('should support EOLs at the edges of attributes', () => {
+    expect(micromark('$span(\na\n)', options({ '*': h })))
+      .toBe('<p><span a=""></span></p>');
   });
 
-  t.end();
-});
+  it('should support EOLs before initializer', () => {
+    expect(micromark('$span(a\r= b)', options({ '*': h })))
+      .toBe('<p><span a="b"></span></p>');
+  });
 
-test('micromark-extension-directive (compile)', (t) => {
-  t.equal(
-    micromark(
-      [
-        'a $abbr',
-        'a $abbr[HTML]',
-        'a $abbr(title="HyperText Markup Language")',
-        'a $abbr[HTML](title="HyperText Markup Language")',
-      ].join('\n\n'),
-      options({ abbr }),
-    ),
-    [
-      '<p>a <abbr></abbr></p>',
-      '<p>a <abbr>HTML</abbr></p>',
-      '<p>a <abbr title="HyperText Markup Language"></abbr></p>',
-      '<p>a <abbr title="HyperText Markup Language">HTML</abbr></p>',
-    ].join('\n'),
-    'should support a directives (abbr)',
-  );
-
-  t.equal(
-    micromark(
-      [
-        'Text:',
-        'a $youtube',
-        'a $youtube[Cat in a box a]',
-        'a $youtube(v=1)',
-        'a $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)',
-      ].join('\n\n'),
-      options({ youtube }),
-    ),
-    [
-      '<p>Text:</p>',
-      '<p>a </p>',
-      '<p>a </p>',
-      '<p>a <iframe src="https://www.youtube.com/embed/1" allowfullscreen></iframe></p>',
-      '<p>a <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>',
-    ].join('\n'),
-    'should support directives (youtube)',
-  );
-
-  t.equal(
-    micromark(
-      [
-        'Text:',
-        'a $lsx',
-        'a $lsx()',
-        'a $lsx(num=1)',
-        'a $lsx(/)',
-        'a $lsx(/,num=5,depth=1)',
-        'a $lsx(/, num=5, depth=1)',
-        'a $lsx(💚)',
-        'Leaf:',
-        '$lsx',
-        '$lsx()',
-        '$lsx(num=1)',
-        '$lsx(/)',
-        '$lsx(/,num=5,depth=1)',
-        '$lsx(/, num=5, depth=1)',
-        '$lsx(💚)',
-      ].join('\n\n'),
-      options({ lsx }),
-    ),
-    [
-      '<p>Text:</p>',
-      '<p>a <lsx ></lsx></p>',
-      '<p>a <lsx ></lsx></p>',
-      '<p>a <lsx num="1"></lsx></p>',
-      '<p>a <lsx prefix="/"></lsx></p>',
-      '<p>a <lsx prefix="/" num="5" depth="1"></lsx></p>',
-      '<p>a <lsx prefix="/" num="5" depth="1"></lsx></p>',
-      '<p>a <lsx prefix="💚"></lsx></p>',
-      '<p>Leaf:</p>',
-      '<lsx ></lsx>',
-      '<lsx ></lsx>',
-      '<lsx num="1"></lsx>',
-      '<lsx prefix="/"></lsx>',
-      '<lsx prefix="/" num="5" depth="1"></lsx>',
-      '<lsx prefix="/" num="5" depth="1"></lsx>',
-      '<lsx prefix="💚"></lsx>',
-    ].join('\n'),
-    'should support directives (lsx)',
-  );
-
-  t.equal(
-    micromark('a $youtube[Cat in a box]\n$br a', options({ youtube, '*': h })),
-    '<p>a <youtube>Cat in a box</youtube>\n<br> a</p>',
-    'should support fall through directives (`*`)',
-  );
-
-  t.equal(
-    micromark('a $a[$img(src="x" alt=y)](href="z")', options({ '*': h })),
-    '<p>a <a href="z"><img src="x" alt="y"></a></p>',
-    'should support fall through directives (`*`)',
-  );
-
-  t.end();
-});
+  it('should support EOLs after initializer', () => {
+    expect(micromark('$span(a=\r\nb)', options({ '*': h })))
+      .toBe('<p><span a="b"></span></p>');
+  });
 
-test('content', (t) => {
-  t.equal(
-    micromark('a $abbr[x\\&y&amp;z]', options({ abbr })),
-    '<p>a <abbr>x&amp;y&amp;z</abbr></p>',
-    'should support character escapes and character references in label',
-  );
-
-  t.equal(
-    micromark('a $abbr[x\\[y\\]z]', options({ abbr })),
-    '<p>a <abbr>x[y]z</abbr></p>',
-    'should support escaped brackets in a label',
-  );
-
-  t.equal(
-    micromark('a $abbr[x[y]z]', options({ abbr })),
-    '<p>a <abbr>x[y]z</abbr></p>',
-    'should support balanced brackets in a label',
-  );
-
-  t.equal(
-    micromark(
-      'a $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>a <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',
-  );
+  it('should support EOLs between an unquoted attribute value and a next attribute name', () => {
+    expect(micromark('$span(a=b\nc)', options({ '*': h })))
+      .toBe('<p><span a="b" c=""></span></p>');
+  });
 
-  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)',
-  );
+  it('should support EOLs in a double quoted attribute value', () => {
+    expect(micromark('$span(a="b\nc")', options({ '*': h })))
+      .toBe('<p><span a="b\nc"></span></p>');
+  });
 
-  // == does not work but I don't know why.. -- 2022.08.12 Yuki Takei
-  // t.equal(
-  //   micromark('$abbr[a\n$abbr[b]\nc]', options({ abbr })),
-  //   '<p>a <abbr>a\n<abbr>b</abbr>\nc</abbr> a</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('a $abbr[a *b* **c** d]', options({ abbr })),
-    '<p>a <abbr>a <em>b</em> <strong>c</strong> d</abbr></p>',
-    'should support markdown in a label',
-  );
-
-  t.equal(
-    micromark('a $abbr(title=a&apos;b)', options({ abbr })),
-    '<p>a <abbr title="a\'b"></abbr></p>',
-    'should support character references in unquoted attribute values',
-  );
-
-  t.equal(
-    micromark('a $abbr(title="a&apos;b")', options({ abbr })),
-    '<p>a <abbr title="a\'b"></abbr></p>',
-    'should support character references in double attribute values',
-  );
-
-  t.equal(
-    micromark("a $abbr(title='a&apos;b')", options({ abbr })),
-    '<p>a <abbr title="a\'b"></abbr></p>',
-    'should support character references in single attribute values',
-  );
-
-  t.equal(
-    micromark('a $abbr(title="a&somethingelse;b")', options({ abbr })),
-    '<p>a <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('a $span(#a#b)', options({ '*': h })),
-    '<p>a <span #a#b=""></span></p>',
-    'should support attrs which contains `#` (1)',
-  );
-
-  t.equal(
-    micromark('a $span(id=a id="b" #c#d)', options({ '*': h })),
-    '<p>a <span id="b" #c#d=""></span></p>',
-    'should support attrs which contains `#` (2)',
-  );
-
-  t.equal(
-    micromark('a $span(.a.b)', options({ '*': h })),
-    '<p>a <span .a.b=""></span></p>',
-    'should support attrs with dot notation',
-  );
-
-  t.test('spec for growi plugin', (t) => {
-    t.equal(
-      micromark('a $lsx(/Sandbox)', options()),
-      '<p>a </p>',
-      'should support name with slash',
-    );
+  it('should support EOLs in a single quoted attribute value', () => {
+    expect(micromark("$span(a='b\nc')", options({ '*': h })))
+      .toBe('<p><span a="b\nc"></span></p>');
+  });
 
-    t.equal(
-      micromark('a $lsx(key=value, reverse)', options()),
-      '<p>a </p>',
-      'should support name=value and an attribute w/o value',
-    );
+  it('should support attrs which contains `#` (1)', () => {
+    expect(micromark('a $span(#a#b)', options({ '*': h })))
+      .toBe('<p>a <span #a#b=""></span></p>');
+  });
 
-    t.equal(
-      micromark('a $lsx(key=value, reverse, reverse2)', options()),
-      '<p>a </p>',
-      'should support consecutive attributes w/o value',
-    );
+  it('should support attrs which contains `#` (2)', () => {
+    expect(micromark('a $span(id=a id="b" #c#d)', options({ '*': h })))
+      .toBe('<p>a <span id="b" #c#d=""></span></p>');
+  });
 
-    t.equal(
-      micromark('a $lsx(/Sandbox, key=value, reverse)', options()),
-      '<p>a </p>',
-      'should support name=value after an empty value attribute',
-    );
+  it('should support attrs with dot notation', () => {
+    expect(micromark('a $span(.a.b)', options({ '*': h })))
+      .toBe('<p>a <span .a.b=""></span></p>');
+  });
 
-    t.end();
+  describe('spec for growi plugin', () => {
+    it('should support name with slash', () => {
+      expect(micromark('a $lsx(/Sandbox)', options()))
+        .toBe('<p>a </p>');
+    });
+
+    it('should support name=value and an attribute w/o value', () => {
+      expect(micromark('a $lsx(key=value, reverse)', options()))
+        .toBe('<p>a </p>');
+    });
+
+    it('should support consecutive attributes w/o value', () => {
+      expect(micromark('a $lsx(key=value, reverse, reverse2)', options()))
+        .toBe('<p>a </p>');
+    });
+
+    it('should support name=value after an empty value attribute', () => {
+      expect(micromark('a $lsx(/Sandbox, key=value, reverse)', options()))
+        .toBe('<p>a </p>');
+    });
   });
 
-  t.end();
 });
 
 /** @type {Handle} */

+ 19 - 22
packages/remark-growi-directive/test/remark-growi-directive.test.js

@@ -5,36 +5,35 @@
 import fs from 'node:fs';
 import path from 'node:path';
 
+
 import { isHidden } from 'is-hidden';
 import { remark } from 'remark';
-import test from 'tape';
 import { readSync } from 'to-vfile';
 import { unified } from 'unified';
+import { describe, it, expect } from 'vitest';
 
 import { remarkGrowiDirectivePlugin } from '../src/remark-growi-directive.js';
 
-test('directive()', (t) => {
-  t.doesNotThrow(() => {
-    remark().use(remarkGrowiDirectivePlugin).freeze();
-  }, 'should not throw if not passed options');
-
-  t.doesNotThrow(() => {
-    unified().use(remarkGrowiDirectivePlugin).freeze();
-  }, 'should not throw if without parser or compiler');
+describe('directive()', () => {
+  it('should not throw if not passed options', () => {
+    expect(() => {
+      remark().use(remarkGrowiDirectivePlugin).freeze();
+    }).not.toThrow();
+  });
 
-  t.end();
+  it('should not throw if without parser or compiler', () => {
+    expect(() => {
+      unified().use(remarkGrowiDirectivePlugin).freeze();
+    }).not.toThrow();
+  });
 });
 
-test('fixtures', (t) => {
+describe('fixtures', () => {
   const base = path.join('test', 'fixtures');
   const entries = fs.readdirSync(base).filter(d => !isHidden(d));
 
-  t.plan(entries.length);
-
-  let index = -1;
-  while (++index < entries.length) {
-    const fixture = entries[index];
-    t.test(fixture, (st) => {
+  entries.forEach((fixture) => {
+    it(`should handle ${fixture}`, () => {
       const file = readSync(path.join(base, fixture, 'input.md'));
       const input = String(file);
       const outputPath = path.join(base, fixture, 'output.md');
@@ -62,10 +61,8 @@ test('fixtures', (t) => {
         output = input;
       }
 
-      st.deepEqual(actual, expected, 'tree');
-      st.equal(String(proc.processSync(file)), output, 'process');
-
-      st.end();
+      expect(actual).toEqual(expected);
+      expect(String(proc.processSync(file))).toBe(output);
     });
-  }
+  });
 });

+ 5 - 0
packages/remark-growi-directive/tsconfig.json

@@ -1,6 +1,11 @@
 {
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "types": [
+      "vitest/globals"
+    ],
+  },
   "include": [
     "src"
   ],

+ 13 - 0
packages/remark-growi-directive/vitest.config.ts

@@ -0,0 +1,13 @@
+import tsconfigPaths from 'vite-tsconfig-paths';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  plugins: [
+    tsconfigPaths(),
+  ],
+  test: {
+    environment: 'node',
+    clearMocks: true,
+    globals: true,
+  },
+});

+ 4 - 4
packages/remark-lsx/package.json

@@ -44,10 +44,10 @@
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
-    "hast-util-sanitize": "^4.1.0",
-    "hast-util-select": "^5.0.5",
-    "unified": "^10.1.2",
-    "unist-util-visit": "^4.0.0"
+    "hast-util-sanitize": "^5.0.1",
+    "hast-util-select": "^6.0.2",
+    "unified": "^11.0.0",
+    "unist-util-visit": "^5.0.0"
   },
   "peerDependencies": {
     "next": "^14",

+ 1 - 1
packages/remark-lsx/src/client/components/LsxPageList/LsxListView.tsx

@@ -1,7 +1,7 @@
 import React, { useMemo } from 'react';
 
 import type { PageNode } from '../../../interfaces/page-node';
-import { LsxContext } from '../lsx-context';
+import type { LsxContext } from '../lsx-context';
 
 import { LsxPage } from './LsxPage';
 

+ 1 - 1
packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx

@@ -5,7 +5,7 @@ import { PageListMeta, PagePathLabel } from '@growi/ui/dist/components';
 import Link from 'next/link';
 
 import type { PageNode } from '../../../interfaces/page-node';
-import { LsxContext } from '../lsx-context';
+import type { LsxContext } from '../lsx-context';
 
 
 import styles from './LsxPage.module.scss';

+ 8 - 6
packages/remark-lsx/src/client/services/renderer/lsx.ts

@@ -1,24 +1,26 @@
 import assert from 'assert';
 
 import { hasHeadingSlash, removeTrailingSlash, addTrailingSlash } from '@growi/core/dist/utils/path-utils';
+import type { TextGrowiPluginDirective, LeafGrowiPluginDirective } from '@growi/remark-growi-directive';
 import { remarkGrowiDirectivePluginType } from '@growi/remark-growi-directive';
-import { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { Nodes as HastNode } from 'hast';
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
 import { selectAll } from 'hast-util-select';
-import type { Node as HastNode } from 'hast-util-select/lib/types';
 import isAbsolute from 'is-absolute-url';
-import { Plugin } from 'unified';
+import type { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
 const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except', 'isSharedPage'];
 
 type DirectiveAttributes = Record<string, string>
-
+type GrowiPluginDirective = TextGrowiPluginDirective | LeafGrowiPluginDirective
 
 export const remarkPlugin: Plugin = function() {
   return (tree) => {
-    visit(tree, (node) => {
-      if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
+    visit(tree, (node: GrowiPluginDirective) => {
+      if (node.type === remarkGrowiDirectivePluginType.Leaf || node.type === remarkGrowiDirectivePluginType.Text) {
+
         if (typeof node.name !== 'string') {
           return;
         }

+ 1 - 1
packages/remark-lsx/src/client/utils/page-node.spec.ts

@@ -2,7 +2,7 @@ import type { IPageHasId } from '@growi/core';
 import { OptionParser } from '@growi/core/dist/remark-plugins';
 import { mock } from 'vitest-mock-extended';
 
-import { PageNode } from '../../interfaces/page-node';
+import type { PageNode } from '../../interfaces/page-node';
 
 import { generatePageNodeTree } from './page-node';
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 437 - 331
yarn.lock


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů