Просмотр исходного кода

Merge branch 'imprv/148445-15079-update-dependencies' into impv/148445-151932-add-type-remark-growi-directive

reiji-h 1 год назад
Родитель
Сommit
7856b53c33
26 измененных файлов с 235 добавлено и 184 удалено
  1. 1 1
      apps/app/src/client/components/Page/SlideRenderer.tsx
  2. 1 1
      apps/app/src/client/components/PagePresentationModal.tsx
  3. 3 4
      apps/app/src/client/components/ReactMarkdownComponents/Header.tsx
  4. 2 3
      apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx
  5. 5 5
      apps/app/src/client/services/renderer/renderer.tsx
  6. 3 6
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  7. 5 5
      apps/app/src/features/mermaid/services/mermaid.ts
  8. 2 5
      apps/app/src/interfaces/renderer-options.ts
  9. 2 3
      apps/app/src/services/renderer/rehype-plugins/add-class.ts
  10. 1 1
      apps/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts
  11. 2 2
      apps/app/src/services/renderer/rehype-plugins/relative-links-by-pukiwiki-like-linker.spec.ts
  12. 1 1
      apps/app/src/services/renderer/rehype-plugins/relative-links.spec.ts
  13. 1 1
      apps/app/src/services/renderer/rehype-plugins/relative-links.ts
  14. 8 10
      apps/app/src/services/renderer/rehype-plugins/relocate-toc.ts
  15. 21 11
      apps/app/src/services/renderer/remark-plugins/attachment.ts
  16. 31 0
      apps/app/src/services/renderer/remark-plugins/codeblock.ts
  17. 9 10
      apps/app/src/services/renderer/remark-plugins/xsv-to-table.ts
  18. 4 1
      apps/app/src/services/renderer/renderer.tsx
  19. 6 5
      packages/presentation/src/client/components/RichSlideSection.tsx
  20. 1 1
      packages/presentation/src/client/consts/index.ts
  21. 1 1
      packages/presentation/src/client/services/renderer/extract-sections.ts
  22. 5 4
      packages/presentation/src/services/use-slides-by-frontmatter.ts
  23. 23 10
      packages/remark-drawio/src/services/renderer/remark-drawio.ts
  24. 50 45
      packages/remark-growi-directive/src/mdast-util-growi-directive/lib/index.js
  25. 19 20
      packages/remark-growi-directive/src/remark-growi-directive.js
  26. 28 28
      packages/remark-growi-directive/test/mdast-util-growi-directive.test.js

+ 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 {

+ 2 - 3
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,
@@ -24,7 +24,6 @@ type TableWithEditButtonProps = {
 }
 
 export const TableWithEditButton = React.memo((props: TableWithEditButtonProps): JSX.Element => {
-
   const { children, node, className } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
@@ -61,5 +60,5 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
       </table>
     </div>
   );
-});
+}) as typeof TableWithEditButton;
 TableWithEditButton.displayName = 'TableWithEditButton';

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

@@ -71,7 +71,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,
@@ -129,7 +129,7 @@ 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),
     )]
@@ -176,7 +176,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,
@@ -227,7 +227,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 +265,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,

+ 3 - 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';
 
@@ -66,15 +65,13 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
     </PrismAsyncLight>
   );
 }
+export const CodeBlock = ({ className, children }: {className: string, children: JSX.Element}): JSX.Element => {
 
-export const CodeBlock: CodeComponent = ({ inline, className, children }: CodeProps) => {
-
-  if (inline) {
+  // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
+  if (className === 'inline') {
     return <code className={`code-inline ${className ?? ''}`}>{children}</code>;
   }
 
-  // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
-
   const match = /language-(\w+)(:?.+)?/.exec(className || '');
   const lang = match && match[1] ? match[1] : '';
   const name = match && match[2] ? match[2].slice(1) : null;

+ 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>,
         }

+ 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;

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

@@ -1,10 +1,21 @@
 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';
 
+declare module 'mdast' {
+  interface LinkData {
+    hName?: string,
+    hProperties?: {
+      attachmentId?: string,
+      url?: string,
+      attachmentName?: PhrasingContent,
+    }
+  }
+}
+
 const SUPPORTED_ATTRIBUTES = ['attachmentId', 'url', 'attachmentName'];
 
 const isAttachmentLink = (url: string): boolean => {
@@ -13,25 +24,24 @@ 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 data = node.data ?? (node.data = {});
   data.hName = 'attachment';
   data.hProperties = {
     attachmentId,
     url: node.url,
-    attachmentName: (node.children as any)[0]?.value,
+    attachmentName: node.children[0] ?? '',
   };
 };
 
 
 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);
       }
     });
   };

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

@@ -0,0 +1,31 @@
+
+import type { Root } from 'hast';
+import { selectAll } from 'hast-util-select';
+import type { InlineCode } from 'mdast';
+import type { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+import { addClassToProperties } from '../rehype-plugins/add-class';
+
+export const remarkPlugin: Plugin = () => {
+  return (tree) => {
+    visit(tree, 'inlineCode', (node: InlineCode) => {
+      const data = node.data || (node.data = {});
+      // setting inline for rehypePlugin
+      data.hProperties = { inline: true };
+    });
+  };
+};
+
+export const rehypePlugin: Plugin = () => {
+  return (tree: Root) => {
+    const codeElements = selectAll('code', tree);
+    codeElements.forEach((element) => {
+      // if inlineCode, properties.inline exists.
+      if (element.properties?.inline != null) {
+        element.properties.inline = true;
+        addClassToProperties(element.properties, 'inline');
+      }
+    });
+  };
+};

+ 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);
       }
     });
   };

+ 4 - 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 codeBlocks 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,
+      codeBlocks.remarkPlugin,
     ],
     remarkRehypeOptions: {
       clobberPrefix: '', // remove clobber prefix
@@ -108,6 +110,7 @@ export const generateCommonOptions = (pagePath: string|undefined): RendererOptio
       [addClass.rehypePlugin, {
         table: 'table table-bordered',
       }],
+      codeBlocks.rehypePlugin,
     ],
     components: {
       a: NextLink,
@@ -137,7 +140,7 @@ export const generateSSRViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       getCommonSanitizeOption(config),
     )]

+ 6 - 5
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>
@@ -29,7 +30,7 @@ export const RichSlideSection = React.memo((props: RichSlideSectionProps): JSX.E
       {children}
     </OriginalRichSlideSection>
   );
-});
+}) as typeof RichSlideSection;
 
 
 export const PresentationRichSlideSection = React.memo((props: RichSlideSectionProps): JSX.Element => {
@@ -40,4 +41,4 @@ export const PresentationRichSlideSection = React.memo((props: RichSlideSectionP
       {children}
     </OriginalRichSlideSection>
   );
-});
+}) as typeof PresentationRichSlideSection;

+ 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(() => {

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

@@ -1,21 +1,36 @@
 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'];
 
+declare module 'mdast' {
+  interface Data {
+    hName?: string,
+    hProperties?: {
+      diagramIndex?: number,
+      bol?: number,
+      eol?: number,
+      key?: string,
+    }
+  }
+}
+
 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 = node.data ?? (node.data = {});
   data.hName = 'drawio';
   data.hProperties = {
     diagramIndex: index,
@@ -27,11 +42,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);
       }
     });
   };

+ 50 - 45
packages/remark-growi-directive/src/mdast-util-growi-directive/lib/index.js

@@ -31,54 +31,59 @@ export const DirectiveType = Object.freeze({
 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],
+export function directiveFromMarkdown() {
+  return {
+    canContainEols: [DirectiveType.Text],
+    enter: {
+      directiveLeaf: enterLeaf,
+      directiveLeafAttributes: enterAttributes,
+
+      directiveText: enterText,
+      directiveTextAttributes: enterAttributes,
     },
-    {
-      character: '\n',
-      inConstruct: [DirectiveType.Leaf],
+    exit: {
+      directiveLeaf: exit,
+      directiveLeafAttributeName: exitAttributeName,
+      directiveLeafAttributeValue: exitAttributeValue,
+      directiveLeafAttributes: exitAttributes,
+      directiveLeafName: exitName,
+
+      directiveText: exit,
+      directiveTextAttributeName: exitAttributeName,
+      directiveTextAttributeValue: exitAttributeValue,
+      directiveTextAttributes: exitAttributes,
+      directiveTextName: exitName,
+
     },
-    {
-      before: '[^$]',
-      character: '$',
-      after: '[A-Za-z]',
-      inConstruct: ['phrasing'],
+  };
+}
+
+/** @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,
     },
-    { atBreak: true, character: '$', after: '$' },
-  ],
-  handlers: {
-    [DirectiveType.Leaf]: handleDirective,
-    [DirectiveType.Text]: handleDirective,
-  },
-};
+  };
+}
 
 /** @type {FromMarkdownHandle} */
 function enterLeaf(token) {

+ 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());
 }

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

@@ -13,7 +13,7 @@ describe('markdown -> mdast', () => {
     expect(
       fromMarkdown('a $b[c](d) e.', {
         extensions: [directive()],
-        mdastExtensions: [directiveFromMarkdown],
+        mdastExtensions: [directiveFromMarkdown()],
       }).children[0],
     ).toEqual({
       type: 'paragraph',
@@ -65,7 +65,7 @@ describe('markdown -> mdast', () => {
     expect(
       fromMarkdown('$a[b](c)', {
         extensions: [directive()],
-        mdastExtensions: [directiveFromMarkdown],
+        mdastExtensions: [directiveFromMarkdown()],
       }).children[0],
     ).toEqual({
       type: DirectiveType.Leaf,
@@ -92,7 +92,7 @@ describe('markdown -> mdast', () => {
   it('should support content in a label', () => {
     const tree = fromMarkdown('x $a[b *c*\nd]', {
       extensions: [directive()],
-      mdastExtensions: [directiveFromMarkdown],
+      mdastExtensions: [directiveFromMarkdown()],
     });
 
     removePosition(tree, { force: true });
@@ -126,7 +126,7 @@ describe('markdown -> mdast', () => {
   it('should support attributes', () => {
     const tree = fromMarkdown('x $a(#b.c.d e=f g="h&amp;i&unknown;j")', {
       extensions: [directive()],
-      mdastExtensions: [directiveFromMarkdown],
+      mdastExtensions: [directiveFromMarkdown()],
     });
 
     removePosition(tree, { force: true });
@@ -156,7 +156,7 @@ describe('markdown -> mdast', () => {
   it('should support EOLs in attributes', () => {
     const tree = fromMarkdown('$a(b\nc="d\ne")', {
       extensions: [directive()],
-      mdastExtensions: [directiveFromMarkdown],
+      mdastExtensions: [directiveFromMarkdown()],
     });
 
     removePosition(tree, { force: true });
@@ -193,7 +193,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' b.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $ b.\n');
   });
@@ -210,7 +210,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' c.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b c.\n');
   });
@@ -230,7 +230,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' d.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b[c] d.\n');
   });
@@ -250,7 +250,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' f.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b[c\\[d\\]e] f.\n');
   });
@@ -270,7 +270,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' e.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b[c\nd] e.\n');
   });
@@ -294,7 +294,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' k.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b(c="d" e="f" g j="2") k.\n');
   });
@@ -315,7 +315,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' k.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b(#d .a.b.c key="value") k.\n');
   });
@@ -336,7 +336,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' k.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b(x="y&#x22;\'\r\nz") k.\n');
   });
@@ -357,7 +357,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' e.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b(id="c#d") e.\n');
   });
@@ -378,7 +378,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' g.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b(c.d e<f) g.\n');
   });
@@ -401,7 +401,7 @@ describe('mdast -> markdown', () => {
             { type: 'text', value: ' k.' },
           ],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a $b(c.d e f<g hij) k.\n');
   });
@@ -411,7 +411,7 @@ describe('mdast -> markdown', () => {
     expect(
       toMarkdown(
         { type: DirectiveType.Leaf },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('$\n');
   });
@@ -421,7 +421,7 @@ describe('mdast -> markdown', () => {
     expect(
       toMarkdown(
         { type: DirectiveType.Leaf, name: 'a' },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('$a\n');
   });
@@ -434,7 +434,7 @@ describe('mdast -> markdown', () => {
           name: 'a',
           children: [{ type: 'text', value: 'b' }],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('$a[b]\n');
   });
@@ -447,7 +447,7 @@ describe('mdast -> markdown', () => {
           name: 'a',
           children: [{ type: 'text', value: 'b\nc' }],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('$a[b&#xA;c]\n');
   });
@@ -461,7 +461,7 @@ describe('mdast -> markdown', () => {
           attributes: { '#b': '', '.c.d': '', key: 'e\nf' },
           children: [],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('$a(#b .c.d key="e&#xA;f")\n');
   });
@@ -473,7 +473,7 @@ describe('mdast -> markdown', () => {
           type: 'paragraph',
           children: [{ type: 'text', value: 'a$b' }],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('a\\$b\n');
   });
@@ -483,7 +483,7 @@ describe('mdast -> markdown', () => {
       toMarkdown({
         type: 'paragraph',
         children: [{ type: 'text', value: 'a$9' }],
-      }, { extensions: [directiveToMarkdown] }),
+      }, { extensions: [directiveToMarkdown()] }),
     ).toBe('a$9\n');
   });
 
@@ -492,7 +492,7 @@ describe('mdast -> markdown', () => {
       toMarkdown({
         type: 'paragraph',
         children: [{ type: 'text', value: 'a$c' }],
-      }, { extensions: [directiveToMarkdown] }),
+      }, { extensions: [directiveToMarkdown()] }),
     ).toBe('a\\$c\n');
   });
 
@@ -503,7 +503,7 @@ describe('mdast -> markdown', () => {
           type: 'paragraph',
           children: [{ type: 'text', value: '$\na' }],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('$\na\n');
   });
@@ -515,7 +515,7 @@ describe('mdast -> markdown', () => {
           type: 'paragraph',
           children: [{ type: 'text', value: '$a' }],
         },
-        { extensions: [directiveToMarkdown] },
+        { extensions: [directiveToMarkdown()] },
       ),
     ).toBe('\\$a\n');
   });
@@ -525,7 +525,7 @@ describe('mdast -> markdown', () => {
       toMarkdown({
         type: 'paragraph',
         children: [{ type: 'text', value: '$\na' }],
-      }, { extensions: [directiveToMarkdown] }),
+      }, { extensions: [directiveToMarkdown()] }),
     ).toBe('$\na\n');
   });
 
@@ -537,7 +537,7 @@ describe('mdast -> markdown', () => {
           { type: DirectiveType.Text, name: 'red', children: [] },
           { type: 'text', value: '$' },
         ],
-      }, { extensions: [directiveToMarkdown] }),
+      }, { extensions: [directiveToMarkdown()] }),
     ).toBe('$red$\n');
   });