Yuki Takei 3 лет назад
Родитель
Сommit
cdd6d5fb98

+ 273 - 0
apps/app/src/client/services/renderer/renderer.tsx

@@ -0,0 +1,273 @@
+import assert from 'assert';
+
+import { isClient } from '@growi/core';
+import * as drawioPlugin from '@growi/remark-drawio';
+// eslint-disable-next-line import/extensions
+import * as lsxGrowiPlugin from '@growi/remark-lsx/dist/client/index.mjs';
+import katex from 'rehype-katex';
+import sanitize from 'rehype-sanitize';
+import slug from 'rehype-slug';
+import type { HtmlElementNode } from 'rehype-toc';
+import breaks from 'remark-breaks';
+import math from 'remark-math';
+import deepmerge from 'ts-deepmerge';
+import type { Pluggable } from 'unified';
+
+
+import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
+import { Header } from '~/components/ReactMarkdownComponents/Header';
+import { Table } from '~/components/ReactMarkdownComponents/Table';
+import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
+import { RehypeSanitizeOption } from '~/interfaces/rehype';
+import type { RendererOptions } from '~/interfaces/renderer-options';
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
+import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
+import * as toc from '~/services/renderer/rehype-plugins/relocate-toc';
+import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
+import * as table from '~/services/renderer/remark-plugins/table';
+import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
+import {
+  commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
+} from '~/services/renderer/renderer';
+import { registerGrowiFacade } from '~/utils/growi-facade';
+import loggerFactory from '~/utils/logger';
+
+// import EasyGrid from './PreProcessor/EasyGrid';
+// import BlockdiagConfigurer from './markdown-it/blockdiag';
+
+
+const logger = loggerFactory('growi:cli:services:renderer');
+
+
+assert(isClient(), 'This module must be loaded only from client modules.');
+
+
+export const generateViewOptions = (
+    pagePath: string,
+    config: RendererConfig,
+    storeTocNode: (toc: HtmlElementNode) => void,
+): RendererOptions => {
+
+  const options = generateCommonOptions(pagePath);
+
+  const { remarkPlugins, rehypePlugins, components } = options;
+
+  // add remark plugins
+  remarkPlugins.push(
+    math,
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
+    drawioPlugin.remarkPlugin,
+    xsvToTable.remarkPlugin,
+    lsxGrowiPlugin.remarkPlugin,
+  );
+  if (config.isEnabledLinebreaks) {
+    remarkPlugins.push(breaks);
+  }
+
+  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+    injectCustomSanitizeOption(config);
+  }
+
+  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+    ? [sanitize, deepmerge(
+      commonSanitizeOption,
+      drawioPlugin.sanitizeOption,
+      lsxGrowiPlugin.sanitizeOption,
+    )]
+    : () => {};
+
+  // add rehype plugins
+  rehypePlugins.push(
+    slug,
+    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
+    rehypeSanitizePlugin,
+    katex,
+    [toc.rehypePluginStore, { storeTocNode }],
+  );
+
+  // add components
+  if (components != null) {
+    components.h1 = Header;
+    components.h2 = Header;
+    components.h3 = Header;
+    components.lsx = lsxGrowiPlugin.Lsx;
+    components.drawio = DrawioViewerWithEditButton;
+    components.table = TableWithEditButton;
+  }
+
+  if (config.isEnabledXssPrevention) {
+    verifySanitizePlugin(options, false);
+  }
+  return options;
+};
+
+export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
+
+  const options = generateCommonOptions(undefined);
+
+  const { rehypePlugins } = options;
+
+  // add remark plugins
+  // remarkPlugins.push();
+
+  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+    injectCustomSanitizeOption(config);
+  }
+
+
+  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+    ? [sanitize, deepmerge(
+      commonSanitizeOption,
+    )]
+    : () => {};
+
+  // add rehype plugins
+  rehypePlugins.push(
+    [toc.rehypePluginRestore, { tocNode }],
+    rehypeSanitizePlugin,
+  );
+
+  if (config.isEnabledXssPrevention) {
+    verifySanitizePlugin(options);
+  }
+
+  return options;
+};
+
+export const generateSimpleViewOptions = (
+    config: RendererConfig,
+    pagePath: string,
+    highlightKeywords?: string | string[],
+    overrideIsEnabledLinebreaks?: boolean,
+): RendererOptions => {
+  const options = generateCommonOptions(pagePath);
+
+  const { remarkPlugins, rehypePlugins, components } = options;
+
+  // add remark plugins
+  remarkPlugins.push(
+    math,
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
+    drawioPlugin.remarkPlugin,
+    xsvToTable.remarkPlugin,
+    lsxGrowiPlugin.remarkPlugin,
+    table.remarkPlugin,
+  );
+
+  const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
+
+  if (isEnabledLinebreaks) {
+    remarkPlugins.push(breaks);
+  }
+
+  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+    injectCustomSanitizeOption(config);
+  }
+
+
+  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+    ? [sanitize, deepmerge(
+      commonSanitizeOption,
+      drawioPlugin.sanitizeOption,
+      lsxGrowiPlugin.sanitizeOption,
+    )]
+    : () => {};
+
+  // add rehype plugins
+  rehypePlugins.push(
+    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
+    [keywordHighlighter.rehypePlugin, { keywords: highlightKeywords }],
+    rehypeSanitizePlugin,
+    katex,
+  );
+
+  // add components
+  if (components != null) {
+    components.lsx = lsxGrowiPlugin.LsxImmutable;
+    components.drawio = drawioPlugin.DrawioViewer;
+    components.table = Table;
+  }
+
+  if (config.isEnabledXssPrevention) {
+    verifySanitizePlugin(options, false);
+  }
+  return options;
+};
+
+export const generatePresentationViewOptions = (
+    config: RendererConfig,
+    pagePath: string,
+): RendererOptions => {
+  // based on simple view options
+  const options = generateSimpleViewOptions(config, pagePath);
+
+  if (config.isEnabledXssPrevention) {
+    verifySanitizePlugin(options, false);
+  }
+  return options;
+};
+
+export const generatePreviewOptions = (config: RendererConfig, pagePath: string): RendererOptions => {
+  const options = generateCommonOptions(pagePath);
+
+  const { remarkPlugins, rehypePlugins, components } = options;
+
+  // add remark plugins
+  remarkPlugins.push(
+    math,
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
+    drawioPlugin.remarkPlugin,
+    xsvToTable.remarkPlugin,
+    lsxGrowiPlugin.remarkPlugin,
+    table.remarkPlugin,
+  );
+  if (config.isEnabledLinebreaks) {
+    remarkPlugins.push(breaks);
+  }
+
+  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+    injectCustomSanitizeOption(config);
+  }
+
+  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+    ? [sanitize, deepmerge(
+      commonSanitizeOption,
+      lsxGrowiPlugin.sanitizeOption,
+      drawioPlugin.sanitizeOption,
+      addLineNumberAttribute.sanitizeOption,
+    )]
+    : () => {};
+
+  // add rehype plugins
+  rehypePlugins.push(
+    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
+    addLineNumberAttribute.rehypePlugin,
+    rehypeSanitizePlugin,
+    katex,
+  );
+
+  // add components
+  if (components != null) {
+    components.lsx = lsxGrowiPlugin.LsxImmutable;
+    components.drawio = drawioPlugin.DrawioViewer;
+    components.table = Table;
+  }
+
+  if (config.isEnabledXssPrevention) {
+    verifySanitizePlugin(options, false);
+  }
+  return options;
+};
+
+// register to facade
+if (isClient()) {
+  registerGrowiFacade({
+    markdownRenderer: {
+      optionsGenerators: {
+        generateViewOptions,
+        generatePreviewOptions,
+      },
+    },
+  });
+}

+ 1 - 1
apps/app/src/components/PageComment.tsx

@@ -7,7 +7,6 @@ import { Button } from 'reactstrap';
 
 import { toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
-import { RendererOptions } from '~/services/renderer/renderer';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
@@ -20,6 +19,7 @@ import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
 import { ReplyComments } from './PageComment/ReplyComments';
 
 import styles from './PageComment.module.scss';
+import { RendererOptions } from '~/interfaces/renderer-options';
 
 export const ROOT_ELEM_ID = 'page-comments' as const;
 

+ 20 - 0
apps/app/src/interfaces/renderer-options.ts

@@ -0,0 +1,20 @@
+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 { PluggableList } from 'unified';
+
+export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehypePlugins' | 'components' | 'children'> & {
+  remarkPlugins: PluggableList,
+  rehypePlugins: PluggableList,
+  components?:
+    | Partial<
+        Omit<NormalComponents, keyof SpecialComponents>
+        & SpecialComponents
+        & {
+          [elem: string]: ComponentType<any>,
+        }
+      >
+    | undefined
+};

+ 7 - 270
apps/app/src/services/renderer/renderer.tsx

@@ -1,46 +1,28 @@
-// allow only types to import from react
-import type { ComponentType } from 'react';
-
-import { isClient } from '@growi/core';
-import * as drawioPlugin from '@growi/remark-drawio';
 import growiDirective from '@growi/remark-growi-directive';
-// eslint-disable-next-line import/extensions
-import * as lsxGrowiPlugin from '@growi/remark-lsx/dist/client/index.mjs';
 import type { Schema as SanitizeOption } from 'hast-util-sanitize';
-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 katex from 'rehype-katex';
 import raw from 'rehype-raw';
 import sanitize, { defaultSchema as rehypeSanitizeDefaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
-import type { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import gfm from 'remark-gfm';
 import math from 'remark-math';
 import deepmerge from 'ts-deepmerge';
-import type { PluggableList, Pluggable, PluginTuple } from 'unified';
+import type { Pluggable, PluginTuple } from 'unified';
 
 
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
-import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
-import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { Table } from '~/components/ReactMarkdownComponents/Table';
-import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
+import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';
 
 import * as addClass from './rehype-plugins/add-class';
-import * as addLineNumberAttribute from './rehype-plugins/add-line-number-attribute';
-import * as keywordHighlighter from './rehype-plugins/keyword-highlighter';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
-import * as toc from './rehype-plugins/relocate-toc';
-import * as plantuml from './remark-plugins/plantuml';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import * as table from './remark-plugins/table';
 import * as xsvToTable from './remark-plugins/xsv-to-table';
@@ -49,23 +31,10 @@ import * as xsvToTable from './remark-plugins/xsv-to-table';
 // import BlockdiagConfigurer from './markdown-it/blockdiag';
 
 
-const logger = loggerFactory('growi:util:GrowiRenderer');
+const logger = loggerFactory('growi:services:renderer');
 
 
 type SanitizePlugin = PluginTuple<[SanitizeOption]>;
-export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehypePlugins' | 'components' | 'children'> & {
-  remarkPlugins: PluggableList,
-  rehypePlugins: PluggableList,
-  components?:
-    | Partial<
-        Omit<NormalComponents, keyof SpecialComponents>
-        & SpecialComponents
-        & {
-          [elem: string]: ComponentType<any>,
-        }
-      >
-    | undefined
-};
 
 const baseSanitizeSchema = {
   tagNames: ['iframe', 'section'],
@@ -77,7 +46,7 @@ const baseSanitizeSchema = {
   },
 };
 
-const commonSanitizeOption: SanitizeOption = deepmerge(
+export const commonSanitizeOption: SanitizeOption = deepmerge(
   rehypeSanitizeDefaultSchema,
   baseSanitizeSchema,
   {
@@ -87,7 +56,7 @@ const commonSanitizeOption: SanitizeOption = deepmerge(
 
 let isInjectedCustomSanitaizeOption = false;
 
-const injectCustomSanitizeOption = (config: RendererConfig) => {
+export const injectCustomSanitizeOption = (config: RendererConfig): void => {
   if (!isInjectedCustomSanitaizeOption && config.isEnabledXssPrevention && config.xssOption === RehypeSanitizeOption.CUSTOM) {
     commonSanitizeOption.tagNames = baseSanitizeSchema.tagNames.concat(config.tagWhiteList ?? []);
     commonSanitizeOption.attributes = deepmerge(baseSanitizeSchema.attributes, config.attrWhiteList ?? {});
@@ -114,7 +83,7 @@ const hasSanitizePlugin = (options: RendererOptions, shouldBeTheLastItem: boolea
     : rehypePlugins.some(rehypePlugin => isSanitizePlugin(rehypePlugin));
 };
 
-const verifySanitizePlugin = (options: RendererOptions, shouldBeTheLastItem = true): void => {
+export const verifySanitizePlugin = (options: RendererOptions, shouldBeTheLastItem = true): void => {
   if (hasSanitizePlugin(options, shouldBeTheLastItem)) {
     return;
   }
@@ -122,7 +91,7 @@ const verifySanitizePlugin = (options: RendererOptions, shouldBeTheLastItem = tr
   throw new Error('The specified options does not have sanitize plugin in \'rehypePlugins\'');
 };
 
-const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
+export const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
   return {
     remarkPlugins: [
       gfm,
@@ -149,170 +118,6 @@ const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
   };
 };
 
-export const generateViewOptions = (
-    pagePath: string,
-    config: RendererConfig,
-    storeTocNode: (toc: HtmlElementNode) => void,
-): RendererOptions => {
-
-  const options = generateCommonOptions(pagePath);
-
-  const { remarkPlugins, rehypePlugins, components } = options;
-
-  // add remark plugins
-  remarkPlugins.push(
-    math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
-    drawioPlugin.remarkPlugin,
-    xsvToTable.remarkPlugin,
-    lsxGrowiPlugin.remarkPlugin,
-  );
-  if (config.isEnabledLinebreaks) {
-    remarkPlugins.push(breaks);
-  }
-
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      commonSanitizeOption,
-      drawioPlugin.sanitizeOption,
-      lsxGrowiPlugin.sanitizeOption,
-    )]
-    : () => {};
-
-  // add rehype plugins
-  rehypePlugins.push(
-    slug,
-    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
-    rehypeSanitizePlugin,
-    katex,
-    [toc.rehypePluginStore, { storeTocNode }],
-  );
-
-  // add components
-  if (components != null) {
-    components.h1 = Header;
-    components.h2 = Header;
-    components.h3 = Header;
-    components.lsx = lsxGrowiPlugin.Lsx;
-    components.drawio = DrawioViewerWithEditButton;
-    components.table = TableWithEditButton;
-  }
-
-  if (config.isEnabledXssPrevention) {
-    verifySanitizePlugin(options, false);
-  }
-  return options;
-};
-
-export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
-
-  const options = generateCommonOptions(undefined);
-
-  const { rehypePlugins } = options;
-
-  // add remark plugins
-  // remarkPlugins.push();
-
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      commonSanitizeOption,
-    )]
-    : () => {};
-
-  // add rehype plugins
-  rehypePlugins.push(
-    [toc.rehypePluginRestore, { tocNode }],
-    rehypeSanitizePlugin,
-  );
-
-  if (config.isEnabledXssPrevention) {
-    verifySanitizePlugin(options);
-  }
-
-  return options;
-};
-
-export const generateSimpleViewOptions = (
-    config: RendererConfig,
-    pagePath: string,
-    highlightKeywords?: string | string[],
-    overrideIsEnabledLinebreaks?: boolean,
-): RendererOptions => {
-  const options = generateCommonOptions(pagePath);
-
-  const { remarkPlugins, rehypePlugins, components } = options;
-
-  // add remark plugins
-  remarkPlugins.push(
-    math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
-    drawioPlugin.remarkPlugin,
-    xsvToTable.remarkPlugin,
-    lsxGrowiPlugin.remarkPlugin,
-    table.remarkPlugin,
-  );
-
-  const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
-
-  if (isEnabledLinebreaks) {
-    remarkPlugins.push(breaks);
-  }
-
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      commonSanitizeOption,
-      drawioPlugin.sanitizeOption,
-      lsxGrowiPlugin.sanitizeOption,
-    )]
-    : () => {};
-
-  // add rehype plugins
-  rehypePlugins.push(
-    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
-    [keywordHighlighter.rehypePlugin, { keywords: highlightKeywords }],
-    rehypeSanitizePlugin,
-    katex,
-  );
-
-  // add components
-  if (components != null) {
-    components.lsx = lsxGrowiPlugin.LsxImmutable;
-    components.drawio = drawioPlugin.DrawioViewer;
-    components.table = Table;
-  }
-
-  if (config.isEnabledXssPrevention) {
-    verifySanitizePlugin(options, false);
-  }
-  return options;
-};
-
-export const generatePresentationViewOptions = (
-    config: RendererConfig,
-    pagePath: string,
-): RendererOptions => {
-  // based on simple view options
-  const options = generateSimpleViewOptions(config, pagePath);
-
-  if (config.isEnabledXssPrevention) {
-    verifySanitizePlugin(options, false);
-  }
-  return options;
-};
 
 export const generateSSRViewOptions = (
     config: RendererConfig,
@@ -326,7 +131,6 @@ export const generateSSRViewOptions = (
   remarkPlugins.push(
     math,
     xsvToTable.remarkPlugin,
-    lsxGrowiPlugin.remarkPlugin,
     table.remarkPlugin,
   );
 
@@ -343,21 +147,18 @@ export const generateSSRViewOptions = (
   const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
     ? [sanitize, deepmerge(
       commonSanitizeOption,
-      lsxGrowiPlugin.sanitizeOption,
     )]
     : () => {};
 
   // add rehype plugins
   rehypePlugins.push(
     slug,
-    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     katex,
   );
 
   // add components
   if (components != null) {
-    components.lsx = lsxGrowiPlugin.LsxImmutable;
     components.table = Table;
   }
 
@@ -366,67 +167,3 @@ export const generateSSRViewOptions = (
   }
   return options;
 };
-
-export const generatePreviewOptions = (config: RendererConfig, pagePath: string): RendererOptions => {
-  const options = generateCommonOptions(pagePath);
-
-  const { remarkPlugins, rehypePlugins, components } = options;
-
-  // add remark plugins
-  remarkPlugins.push(
-    math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
-    drawioPlugin.remarkPlugin,
-    xsvToTable.remarkPlugin,
-    lsxGrowiPlugin.remarkPlugin,
-    table.remarkPlugin,
-  );
-  if (config.isEnabledLinebreaks) {
-    remarkPlugins.push(breaks);
-  }
-
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      commonSanitizeOption,
-      lsxGrowiPlugin.sanitizeOption,
-      drawioPlugin.sanitizeOption,
-      addLineNumberAttribute.sanitizeOption,
-    )]
-    : () => {};
-
-  // add rehype plugins
-  rehypePlugins.push(
-    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
-    addLineNumberAttribute.rehypePlugin,
-    rehypeSanitizePlugin,
-    katex,
-  );
-
-  // add components
-  if (components != null) {
-    components.lsx = lsxGrowiPlugin.LsxImmutable;
-    components.drawio = drawioPlugin.DrawioViewer;
-    components.table = Table;
-  }
-
-  if (config.isEnabledXssPrevention) {
-    verifySanitizePlugin(options, false);
-  }
-  return options;
-};
-
-// register to facade
-if (isClient()) {
-  registerGrowiFacade({
-    markdownRenderer: {
-      optionsGenerators: {
-        generateViewOptions,
-        generatePreviewOptions,
-      },
-    },
-  });
-}

+ 40 - 33
apps/app/src/stores/renderer.tsx

@@ -3,11 +3,7 @@ import { useCallback } from 'react';
 import type { HtmlElementNode } from 'rehype-toc';
 import useSWR, { type SWRResponse } from 'swr';
 
-import {
-  type RendererOptions,
-  generateSimpleViewOptions, generatePreviewOptions,
-  generateViewOptions, generateTocOptions, generatePresentationViewOptions,
-} from '~/services/renderer/renderer';
+import type { RendererOptions } from '~/interfaces/renderer-options';
 import { getGrowiFacade } from '~/utils/growi-facade';
 
 
@@ -33,14 +29,17 @@ export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
     isAllDataValid
       ? ['viewOptions', currentPagePath, rendererConfig]
       : null,
-    ([, currentPagePath, rendererConfig]) => {
-      // determine options generator
-      const optionsGenerator = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGenerateViewOptions ?? generateViewOptions;
-      return optionsGenerator(currentPagePath, rendererConfig, storeTocNodeHandler);
+    async([, currentPagePath, rendererConfig]) => {
+      const customGenerater = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGenerateViewOptions;
+      if (customGenerater != null) {
+        return customGenerater(currentPagePath, rendererConfig, storeTocNodeHandler);
+      }
+
+      const { generateViewOptions } = await import('~/client/services/renderer/renderer');
+      return generateViewOptions(currentPagePath, rendererConfig, storeTocNodeHandler);
     },
     {
       keepPreviousData: true,
-      fallbackData: isAllDataValid ? generateViewOptions(currentPagePath, rendererConfig, storeTocNodeHandler) : undefined,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,
     },
@@ -58,10 +57,12 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
     isAllDataValid
       ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
       : null,
-    ([, , tocNode, rendererConfig]) => generateTocOptions(rendererConfig, tocNode),
+    async([, , tocNode, rendererConfig]) => {
+      const { generateTocOptions } = await import('~/client/services/renderer/renderer');
+      return generateTocOptions(rendererConfig, tocNode);
+    },
     {
       keepPreviousData: true,
-      fallbackData: isAllDataValid ? generateTocOptions(rendererConfig, tocNode) : undefined,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,
     },
@@ -78,14 +79,17 @@ export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
     isAllDataValid
       ? ['previewOptions', rendererConfig, currentPagePath]
       : null,
-    ([, rendererConfig, pagePath]) => {
-      // determine options generator
-      const optionsGenerator = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGeneratePreviewOptions ?? generatePreviewOptions;
-      return optionsGenerator(rendererConfig, pagePath);
+    async([, rendererConfig, pagePath]) => {
+      const customGenerater = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGeneratePreviewOptions;
+      if (customGenerater != null) {
+        return customGenerater(rendererConfig, pagePath);
+      }
+
+      const { generatePreviewOptions } = await import('~/client/services/renderer/renderer');
+      generatePreviewOptions(rendererConfig, pagePath);
     },
     {
       keepPreviousData: true,
-      fallbackData: isAllDataValid ? generatePreviewOptions(rendererConfig, currentPagePath) : undefined,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,
     },
@@ -102,20 +106,17 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
     isAllDataValid
       ? ['commentPreviewOptions', rendererConfig, currentPagePath]
       : null,
-    ([, rendererConfig, currentPagePath]) => generateSimpleViewOptions(
-      rendererConfig,
-      currentPagePath,
-      undefined,
-      rendererConfig.isEnabledLinebreaksInComments,
-    ),
-    {
-      keepPreviousData: true,
-      fallbackData: isAllDataValid ? generateSimpleViewOptions(
+    async([, rendererConfig, currentPagePath]) => {
+      const { generateSimpleViewOptions } = await import('~/client/services/renderer/renderer');
+      return generateSimpleViewOptions(
         rendererConfig,
         currentPagePath,
         undefined,
         rendererConfig.isEnabledLinebreaksInComments,
-      ) : undefined,
+      );
+    },
+    {
+      keepPreviousData: true,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,
     },
@@ -132,9 +133,11 @@ export const useSelectedPagePreviewOptions = (pagePath: string, highlightKeyword
     isAllDataValid
       ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
       : null,
-    ([, rendererConfig, pagePath, highlightKeywords]) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
+    async([, rendererConfig, pagePath, highlightKeywords]) => {
+      const { generateSimpleViewOptions } = await import('~/client/services/renderer/renderer');
+      return generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords);
+    },
     {
-      fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords) : undefined,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,
     },
@@ -153,10 +156,12 @@ export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> =
     isAllDataValid
       ? ['customSidebarOptions', rendererConfig]
       : null,
-    ([, rendererConfig]) => generateSimpleViewOptions(rendererConfig, '/'),
+    async([, rendererConfig]) => {
+      const { generateSimpleViewOptions } = await import('~/client/services/renderer/renderer');
+      return generateSimpleViewOptions(rendererConfig, '/');
+    },
     {
       keepPreviousData: true,
-      fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, '/') : undefined,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,
     },
@@ -173,9 +178,11 @@ export const usePresentationViewOptions = (): SWRResponse<RendererOptions, Error
     isAllDataValid
       ? ['presentationViewOptions', currentPagePath, rendererConfig]
       : null,
-    ([, currentPagePath, rendererConfig]) => generatePresentationViewOptions(rendererConfig, currentPagePath),
+    async([, currentPagePath, rendererConfig]) => {
+      const { generatePresentationViewOptions } = await import('~/client/services/renderer/renderer');
+      return generatePresentationViewOptions(rendererConfig, currentPagePath);
+    },
     {
-      fallbackData: isAllDataValid ? generatePresentationViewOptions(rendererConfig, currentPagePath) : undefined,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,
     },