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

Merge pull request #9025 from weseek/impv/148445-151932-add-type-remark-growi-directive

imprv: Add types to remark-growi-directive
reiji-h 1 год назад
Родитель
Сommit
de819f56e8

+ 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

+ 3 - 2
packages/remark-attachment-refs/src/client/services/renderer/refs.ts

@@ -1,4 +1,5 @@
 import { pathUtils } from '@growi/core/dist/utils';
+import type { TextGrowiPluginDirective, LeafGrowiPluginDirective } from '@growi/remark-growi-directive';
 import { remarkGrowiDirectivePluginType } from '@growi/remark-growi-directive';
 import type { Nodes as HastNode } from 'hast';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
@@ -21,11 +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: any) => {
-      // TODO: growi-directive types
+    visit(tree, (node: GrowiPluginDirective) => {
       if (node.type === remarkGrowiDirectivePluginType.Text || node.type === remarkGrowiDirectivePluginType.Leaf) {
         if (typeof node.name !== 'string') {
           return;

+ 1 - 0
packages/remark-growi-directive/package.json

@@ -18,6 +18,7 @@
   "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",

+ 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 { 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 - 270
packages/remark-growi-directive/src/mdast-util-growi-directive/index.js

@@ -1,270 +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').Handle} ToMarkdownHandle
- * @typedef {import('mdast-util-to-markdown').Context} Context
- * @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension
- *
- * @typedef {import('./complex-types').LeafDirective} LeafDirective
- * @typedef {import('./complex-types').TextDirective} TextDirective
- * @typedef {LeafDirective|TextDirective} Directive
- */
-
-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 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 {Context} context
- * @returns {string}
- */
-function attributes(node, context) {
-  const quote = context.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);
-
-}
+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 - 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/test/mdast-util-growi-directive.test.js

@@ -3,8 +3,8 @@ import { toMarkdown } from 'mdast-util-to-markdown';
 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';
 
 

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

@@ -7,7 +7,7 @@ import { htmlVoidElements } from 'html-void-elements';
 import { micromark } from 'micromark';
 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';
 
 

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

+ 5 - 4
packages/remark-lsx/src/client/services/renderer/lsx.ts

@@ -1,6 +1,7 @@
 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 type { Nodes as HastNode } from 'hast';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
@@ -13,13 +14,13 @@ 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) => {
-    // TODO: setting growi-directive types
-    visit(tree, (node: any) => {
-      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';