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

+ 1 - 0
packages/app/next.config.js

@@ -24,6 +24,7 @@ const setupWithTM = () => {
     'decode-named-character-reference',
     'space-separated-tokens',
     'trim-lines',
+    'emoticon',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
   ];
 

+ 16 - 16
packages/app/src/client/services/ContextExtractor.tsx

@@ -5,7 +5,7 @@ import { pagePathUtils } from '@growi/core';
 
 import { CustomWindow } from '~/interfaces/global';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
+// import { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
 import { useRendererSettings } from '~/stores/renderer';
 import {
   useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
@@ -188,21 +188,21 @@ const ContextExtractorOnce: FC = () => {
 
   // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223
   // Do not access this property from other than reveal.js plugins.
-  (window as CustomWindow).previewRenderer = generatePreviewRenderer(
-    {
-      isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
-      attrWhiteList: configByContextHydrate.attrWhiteList,
-      tagWhiteList: configByContextHydrate.tagWhiteList,
-      highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
-      env: {
-        MATHJAX: configByContextHydrate.env.MATHJAX,
-        PLANTUML_URI: configByContextHydrate.env.PLANTUML_URI,
-        BLOCKDIAG_URI: configByContextHydrate.env.BLOCKDIAG_URI,
-      },
-    },
-    null,
-    path,
-  );
+  // (window as CustomWindow).previewRenderer = generatePreviewRenderer(
+  //   {
+  //     isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
+  //     attrWhiteList: configByContextHydrate.attrWhiteList,
+  //     tagWhiteList: configByContextHydrate.tagWhiteList,
+  //     highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
+  //     env: {
+  //       MATHJAX: configByContextHydrate.env.MATHJAX,
+  //       PLANTUML_URI: configByContextHydrate.env.PLANTUML_URI,
+  //       BLOCKDIAG_URI: configByContextHydrate.env.BLOCKDIAG_URI,
+  //     },
+  //   },
+  //   null,
+  //   path,
+  // );
 
   return null;
 };

+ 11 - 8
packages/app/src/components/Page.jsx

@@ -5,10 +5,10 @@ import React, {
 import dynamic from 'next/dynamic';
 import PropTypes from 'prop-types';
 import { debounce } from 'throttle-debounce';
+
 import MarkdownTable from '~/client/models/MarkdownTable';
 import { blinkSectionHeaderAtBoot } from '~/client/util/blink-section-header';
 import { getOptionsToSave } from '~/client/util/editor';
-import GrowiRenderer from '~/services/renderer/growi-renderer';
 import {
   useIsGuestUser, useIsBlinkedHeaderAtBoot,
 } from '~/stores/context';
@@ -16,7 +16,7 @@ import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
 import { useSWRxCurrentPage } from '~/stores/page';
-import { useViewRenderer } from '~/stores/renderer';
+import { useViewOptions } from '~/stores/renderer';
 import {
   useEditorMode, useIsMobile,
 } from '~/stores/ui';
@@ -138,7 +138,7 @@ class PageSubstance extends React.Component {
 
   render() {
     const {
-      page, isMobile, isGuestUser,
+      rendererOptions, page, isMobile, isGuestUser,
     } = this.props;
     const { path } = page;
     const { _id: revisionId, body: markdown } = page.revision;
@@ -152,7 +152,7 @@ class PageSubstance extends React.Component {
       <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
 
         { revisionId != null && (
-          <RevisionRenderer growiRenderer={this.props.growiRenderer} markdown={markdown} pagePath={path} />
+          <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} pagePath={path} />
         )}
 
         { !isGuestUser && (
@@ -170,7 +170,7 @@ class PageSubstance extends React.Component {
 }
 
 PageSubstance.propTypes = {
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
+  rendererOptions: PropTypes.object.isRequired,
 
   page: PropTypes.any.isRequired,
   pageTags:  PropTypes.arrayOf(PropTypes.string),
@@ -189,7 +189,7 @@ export const Page = (props) => {
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPage?.path);
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: pageTags } = usePageTagsForEditors();
-  const { data: growiRenderer } = useViewRenderer();
+  const { data: rendererOptions } = useViewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { data: isBlinkedAtBoot, mutate: mutateBlinkedAtBoot } = useIsBlinkedHeaderAtBoot();
 
@@ -232,7 +232,10 @@ export const Page = (props) => {
   //   };
   // }, []);
 
-  if (currentPage == null || editorMode == null || isGuestUser == null || growiRenderer == null) {
+  if (currentPage == null || editorMode == null || isGuestUser == null || rendererOptions == null) {
+    logger.warn('Some of materials are missing.', {
+      currentPage: currentPage?._id, editorMode, isGuestUser, rendererOptions,
+    });
     return null;
   }
 
@@ -241,7 +244,7 @@ export const Page = (props) => {
     <PageSubstance
       {...props}
       ref={pageRef}
-      growiRenderer={growiRenderer}
+      rendererOptions={rendererOptions}
       page={currentPage}
       editorMode={editorMode}
       isGuestUser={isGuestUser}

+ 144 - 136
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -1,91 +1,93 @@
-import React, {
-  useCallback, useEffect, useMemo, useState,
-} from 'react';
+import React from 'react';
+
+import ReactMarkdown from 'react-markdown';
 
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
 import { CustomWindow } from '~/interfaces/global';
-import GrowiRenderer from '~/services/renderer/growi-renderer';
+// import GrowiRenderer from '~/services/renderer/growi-renderer';
+import { RendererOptions } from '~/services/renderer/growi-renderer';
 import { useCurrentPathname, useInterceptorManager } from '~/stores/context';
 import { useEditorSettings } from '~/stores/editor';
+import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
-import RevisionBody from './RevisionBody';
+// import RevisionBody from './RevisionBody';
 
 
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 
-function getHighlightedBody(body: string, _keywords: string | string[]): string {
-  const normalizedKeywordsArray: string[] = [];
-
-  const keywords = (typeof _keywords === 'string') ? [_keywords] : _keywords;
-
-  if (keywords.length === 0) {
-    return body;
-  }
-
-  // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
-  // Separate keywords
-  // - Surrounded by double quotation
-  // - Split by both full-width and half-width spaces
-  // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
-  keywords.forEach((keyword, i) => {
-    if (keyword === '') {
-      return;
-    }
-    const k = keyword
-      .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
-      .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
-    normalizedKeywordsArray.push(k);
-  });
-
-  const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
-  const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
-  let keywordRegexp2 = keywordRegxp;
-
-  // for non-chrome browsers compatibility
-  try {
-    // eslint-disable-next-line regex/invalid
-    keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
-  }
-  catch (err) {
-    logger.debug('Failed to initialize regex:', err);
-  }
-
-  const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
-  const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
-
-  const insideTagRegex = /<[^<>]*>/g;
-  const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
-
-  const insideTagStrs = body.match(insideTagRegex);
-  const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
-
-  let returnBody = body;
-  const isSafeHtml = insideTagStrs?.length === betweenTagMatches.length + 1; // to check whether is safe to join
-  if (isSafeHtml) {
-    // highlight
-    const betweenTagStrs: string[] = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
-
-    const arr: string[] = [];
-    insideTagStrs.forEach((str, i) => {
-      arr.push(str);
-      arr.push(betweenTagStrs[i]);
-    });
-    returnBody = arr.join('');
-  }
-  else {
-    // inferior highlighter
-    returnBody = highlighter2(body);
-  }
-
-  return returnBody;
-}
+// function getHighlightedBody(body: string, _keywords: string | string[]): string {
+//   const normalizedKeywordsArray: string[] = [];
+
+//   const keywords = (typeof _keywords === 'string') ? [_keywords] : _keywords;
+
+//   if (keywords.length === 0) {
+//     return body;
+//   }
+
+//   // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
+//   // Separate keywords
+//   // - Surrounded by double quotation
+//   // - Split by both full-width and half-width spaces
+//   // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
+//   keywords.forEach((keyword, i) => {
+//     if (keyword === '') {
+//       return;
+//     }
+//     const k = keyword
+//       .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
+//       .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
+//     normalizedKeywordsArray.push(k);
+//   });
+
+//   const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
+//   const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
+//   let keywordRegexp2 = keywordRegxp;
+
+//   // for non-chrome browsers compatibility
+//   try {
+//     // eslint-disable-next-line regex/invalid
+//     keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
+//   }
+//   catch (err) {
+//     logger.debug('Failed to initialize regex:', err);
+//   }
+
+//   const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
+//   const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
+
+//   const insideTagRegex = /<[^<>]*>/g;
+//   const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
+
+//   const insideTagStrs = body.match(insideTagRegex);
+//   const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
+
+//   let returnBody = body;
+//   const isSafeHtml = insideTagStrs?.length === betweenTagMatches.length + 1; // to check whether is safe to join
+//   if (isSafeHtml) {
+//     // highlight
+//     const betweenTagStrs: string[] = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
+
+//     const arr: string[] = [];
+//     insideTagStrs.forEach((str, i) => {
+//       arr.push(str);
+//       arr.push(betweenTagStrs[i]);
+//     });
+//     returnBody = arr.join('');
+//   }
+//   else {
+//     // inferior highlighter
+//     returnBody = highlighter2(body);
+//   }
+
+//   return returnBody;
+// }
 
 
 type Props = {
-  growiRenderer: GrowiRenderer,
+  rendererOptions: RendererOptions,
   markdown: string,
   pagePath: string,
   highlightKeywords?: string | string[],
@@ -95,75 +97,81 @@ type Props = {
 const RevisionRenderer = (props: Props): JSX.Element => {
 
   const {
-    growiRenderer, markdown, pagePath, highlightKeywords,
+    rendererOptions, markdown, pagePath, highlightKeywords, additionalClassName,
   } = props;
 
-  const [html, setHtml] = useState('');
-
-  const { data: interceptorManager } = useInterceptorManager();
-  const { data: editorSettings } = useEditorSettings();
-  const { data: currentPathname } = useCurrentPathname();
-
-  const currentRenderingContext = useMemo(() => {
-    return {
-      markdown,
-      parsedHTML: '',
-      pagePath,
-      renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
-      currentPathname: decodeURIComponent(currentPathname ?? '/'),
-    };
-  }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
-
-
-  const renderHtml = useCallback(async() => {
-    if (interceptorManager == null) {
-      return;
-    }
-
-    const context = currentRenderingContext;
-
-    await interceptorManager.process('preRender', context);
-    await interceptorManager.process('prePreProcess', context);
-    context.markdown = growiRenderer.preProcess(context.markdown, context);
-    await interceptorManager.process('postPreProcess', context);
-    context.parsedHTML = growiRenderer.process(context.markdown, context);
-    await interceptorManager.process('prePostProcess', context);
-    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
-
-    const isMarkdownEmpty = context.markdown.trim().length === 0;
-    if (highlightKeywords != null && !isMarkdownEmpty) {
-      context.parsedHTML = getHighlightedBody(context.parsedHTML, highlightKeywords);
-    }
-    await interceptorManager.process('postPostProcess', context);
-    await interceptorManager.process('preRenderHtml', context);
-
-    setHtml(context.parsedHTML);
-  }, [currentRenderingContext, growiRenderer, highlightKeywords, interceptorManager]);
-
-  useEffect(() => {
-    if (interceptorManager == null) {
-      return;
-    }
-
-    renderHtml()
-      .then(() => {
-        // const HeaderLink = document.getElementsByClassName('revision-head-link');
-        // const HeaderLinkArray = Array.from(HeaderLink);
-        // addSmoothScrollEvent(HeaderLinkArray as HTMLAnchorElement[], blinkElem);
-
-        // interceptorManager.process('postRenderHtml', currentRenderingContext);
-      });
-
-  }, [currentRenderingContext, interceptorManager, renderHtml]);
-
   return (
-    <RevisionBody
-      html={html}
-      additionalClassName={props.additionalClassName}
-      renderMathJaxOnInit
-    />
+    <ReactMarkdown {...rendererOptions} className={`wiki ${additionalClassName ?? ''}`}>
+      {markdown}
+    </ReactMarkdown>
   );
 
+  // const [html, setHtml] = useState('');
+
+  // const { data: interceptorManager } = useInterceptorManager();
+  // const { data: editorSettings } = useEditorSettings();
+  // const { data: currentPathname } = useCurrentPathname();
+
+  // const currentRenderingContext = useMemo(() => {
+  //   return {
+  //     markdown,
+  //     parsedHTML: '',
+  //     pagePath,
+  //     renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
+  //     currentPathname: decodeURIComponent(currentPathname ?? '/'),
+  //   };
+  // }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
+
+
+  // const renderHtml = useCallback(async() => {
+  //   if (interceptorManager == null) {
+  //     return;
+  //   }
+
+  //   const context = currentRenderingContext;
+
+  //   await interceptorManager.process('preRender', context);
+  //   await interceptorManager.process('prePreProcess', context);
+  //   context.markdown = growiRenderer.preProcess(context.markdown, context);
+  //   await interceptorManager.process('postPreProcess', context);
+  //   context.parsedHTML = growiRenderer.process(context.markdown, context);
+  //   await interceptorManager.process('prePostProcess', context);
+  //   context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
+
+  //   const isMarkdownEmpty = context.markdown.trim().length === 0;
+  //   if (highlightKeywords != null && !isMarkdownEmpty) {
+  //     context.parsedHTML = getHighlightedBody(context.parsedHTML, highlightKeywords);
+  //   }
+  //   await interceptorManager.process('postPostProcess', context);
+  //   await interceptorManager.process('preRenderHtml', context);
+
+  //   setHtml(context.parsedHTML);
+  // }, [currentRenderingContext, growiRenderer, highlightKeywords, interceptorManager]);
+
+  // useEffect(() => {
+  //   if (interceptorManager == null) {
+  //     return;
+  //   }
+
+  //   renderHtml()
+  //     .then(() => {
+  //       // const HeaderLink = document.getElementsByClassName('revision-head-link');
+  //       // const HeaderLinkArray = Array.from(HeaderLink);
+  //       // addSmoothScrollEvent(HeaderLinkArray as HTMLAnchorElement[], blinkElem);
+
+  //       // interceptorManager.process('postRenderHtml', currentRenderingContext);
+  //     });
+
+  // }, [currentRenderingContext, interceptorManager, renderHtml]);
+
+  // return (
+  //   <RevisionBody
+  //     html={html}
+  //     additionalClassName={props.additionalClassName}
+  //     renderMathJaxOnInit
+  //   />
+  // );
+
 };
 
 export default RevisionRenderer;

+ 211 - 238
packages/app/src/services/renderer/growi-renderer.ts

@@ -1,19 +1,11 @@
-import React from 'react';
-
-import rehype2react from 'rehype-react';
+import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import slug from 'rehype-slug';
-import toc, { HtmlElementNode } from 'rehype-toc';
+// import toc, { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import footnotes from 'remark-footnotes';
 import gfm from 'remark-gfm';
-import parse from 'remark-parse';
-import remark2rehype from 'remark-rehype';
-import {
-  unified, Plugin, PluginTuple, Processor,
-} from 'unified';
 
-import { Nullable } from '~/interfaces/common'; // TODO: Remove this asap when the ContextExtractor is removed
 import { GrowiRendererConfig, RendererSettings } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
 
@@ -39,231 +31,215 @@ import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:util:GrowiRenderer');
 
-declare const hljs;
-
-type MarkdownSettings = {
-  breaks?: boolean,
-};
-
-function applyPlugin(processor: Processor, plugin: Plugin | PluginTuple): Processor {
-  if (Array.isArray(plugin)) {
-    return processor.use(...plugin);
-  }
-
-  return processor.use(plugin);
+// declare const hljs;
+
+// type MarkdownSettings = {
+//   breaks?: boolean,
+// };
+
+// export default class GrowiRenderer {
+
+//   growiRendererConfig: GrowiRendererConfig;
+
+//   constructor(growiRendererConfig: GrowiRendererConfig, pagePath?: Nullable<string>) {
+//     this.growiRendererConfig = growiRendererConfig;
+//     this.pagePath = pagePath;
+
+//     if (isClient() && (window as CustomWindow).growiRenderer != null) {
+//       this.preProcessors = (window as CustomWindow).growiRenderer.preProcessors;
+//       this.postProcessors = (window as CustomWindow).growiRenderer.postProcessors;
+//     }
+//     else {
+//       this.preProcessors = [
+//         new EasyGrid(),
+//         new Linker(),
+//         new CsvToTable(),
+//         new XssFilter({
+//           isEnabledXssPrevention: this.growiRendererConfig.isEnabledXssPrevention,
+//           tagWhiteList: this.growiRendererConfig.tagWhiteList,
+//           attrWhiteList: this.growiRendererConfig.attrWhiteList,
+//         }),
+//       ];
+//       this.postProcessors = [
+//       ];
+//     }
+
+//     this.init = this.init.bind(this);
+//     this.addConfigurers = this.addConfigurers.bind(this);
+//     this.setMarkdownSettings = this.setMarkdownSettings.bind(this);
+//     this.configure = this.configure.bind(this);
+//     this.process = this.process.bind(this);
+//     this.codeRenderer = this.codeRenderer.bind(this);
+//   }
+
+//   init() {
+//     let parser: Processor = unified().use(parse);
+//     this.remarkPlugins.forEach((item) => {
+//       parser = applyPlugin(parser, item);
+//     });
+
+//     let rehype: Processor = parser.use(remark2rehype);
+//     this.rehypePlugins.forEach((item) => {
+//       rehype = applyPlugin(rehype, item);
+//     });
+
+//     this.processor = rehype.use(rehype2react, {
+//       createElement: React.createElement,
+//       components: {
+//         // a: NextLink,
+//       },
+//     });
+//   }
+
+//   init() {
+//     // init markdown-it
+//     this.md = new MarkdownIt({
+//       html: true,
+//       linkify: true,
+//       highlight: this.codeRenderer,
+//     });
+
+//     this.isMarkdownItConfigured = false;
+
+//     this.markdownItConfigurers = [
+//       new TaskListsConfigurer(),
+//       new HeaderConfigurer(),
+//       new EmojiConfigurer(),
+//       new MathJaxConfigurer(),
+//       new DrawioViewerConfigurer(),
+//       new PlantUMLConfigurer(this.growiRendererConfig),
+//       new BlockdiagConfigurer(this.growiRendererConfig),
+//     ];
+
+//     if (this.pagePath != null) {
+//       this.markdownItConfigurers.push(
+//         new LinkerByRelativePathConfigurer(this.pagePath),
+//       );
+//     }
+//   }
+
+//   addConfigurers(configurers: any[]): void {
+//     this.markdownItConfigurers.push(...configurers);
+//   }
+
+//   setMarkdownSettings(settings: MarkdownSettings): void {
+//     this.md.set(settings);
+//   }
+
+//   configure(): void {
+//     if (!this.isMarkdownItConfigured) {
+//       this.markdownItConfigurers.forEach((configurer) => {
+//         configurer.configure(this.md);
+//       });
+//     }
+//   }
+
+//   preProcess(markdown, context) {
+//     let processed = markdown;
+//     for (let i = 0; i < this.preProcessors.length; i++) {
+//       if (!this.preProcessors[i].process) {
+//         continue;
+//       }
+//       processed = this.preProcessors[i].process(processed, context);
+//     }
+
+//     return processed;
+//   }
+
+//   process(markdown, context) {
+//     return this.md.render(markdown, context);
+//   }
+
+//   postProcess(html, context) {
+//     let processed = html;
+//     for (let i = 0; i < this.postProcessors.length; i++) {
+//       if (!this.postProcessors[i].process) {
+//         continue;
+//       }
+//       processed = this.postProcessors[i].process(processed, context);
+//     }
+
+//     return processed;
+//   }
+
+//   codeRenderer(code, langExt) {
+//     const noborder = (!this.growiRendererConfig.highlightJsStyleBorder) ? 'hljs-no-border' : '';
+
+//     let citeTag = '';
+//     let hljsLang = 'plaintext';
+//     let showLinenumbers = false;
+
+//     if (langExt) {
+//       // https://regex101.com/r/qGs7eZ/3
+//       const match = langExt.match(/^([^:=\n]+)?(=([^:=\n]*))?(:([^:=\n]*))?(=([^:=\n]*))?$/);
+
+//       const lang = match[1];
+//       const fileName = match[5] || null;
+//       showLinenumbers = (match[2] != null) || (match[6] != null);
+
+//       if (fileName != null) {
+//         citeTag = `<cite>${fileName}</cite>`;
+//       }
+//       if (hljs.getLanguage(lang)) {
+//         hljsLang = lang;
+//       }
+//     }
+
+//     let highlightCode = code;
+//     try {
+//       highlightCode = hljs.highlight(hljsLang, code, true).value;
+
+//       // add line numbers
+//       if (showLinenumbers) {
+//         highlightCode = hljs.lineNumbersValue((highlightCode));
+//       }
+//     }
+//     catch (err) {
+//       logger.error(err);
+//     }
+
+//     return `<pre class="hljs ${noborder}">${citeTag}<code>${highlightCode}</code></pre>`;
+//   }
+
+// }
+
+export type RendererOptions = Partial<ReactMarkdownOptions>;
+
+export interface ReactMarkdownOptionsGenerator {
+  (growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings): RendererOptions
 }
 
-export default class GrowiRenderer {
-
-  // preProcessors: any[];
-
-  // postProcessors: any[];
-
-  // md: any;
-
-  // isMarkdownItConfigured: boolean;
-
-  // markdownItConfigurers: any[];
-
-  // pagePath?: Nullable<string>;
-
-  remarkPlugins: (Plugin | PluginTuple)[] = [
-    gfm,
-  ];
-
-  rehypePlugins: (Plugin | PluginTuple)[] = [
-    slug,
-  ];
-
-  processor?: Processor;
-
-  growiRendererConfig: GrowiRendererConfig;
-
-  constructor(growiRendererConfig: GrowiRendererConfig, pagePath?: Nullable<string>) {
-    this.growiRendererConfig = growiRendererConfig;
-    // this.pagePath = pagePath;
-
-    // if (isClient() && (window as CustomWindow).growiRenderer != null) {
-    //   this.preProcessors = (window as CustomWindow).growiRenderer.preProcessors;
-    //   this.postProcessors = (window as CustomWindow).growiRenderer.postProcessors;
-    // }
-    // else {
-    //   this.preProcessors = [
-    //     new EasyGrid(),
-    //     new Linker(),
-    //     new CsvToTable(),
-    //     new XssFilter({
-    //       isEnabledXssPrevention: this.growiRendererConfig.isEnabledXssPrevention,
-    //       tagWhiteList: this.growiRendererConfig.tagWhiteList,
-    //       attrWhiteList: this.growiRendererConfig.attrWhiteList,
-    //     }),
-    //   ];
-    //   this.postProcessors = [
-    //   ];
-    // }
-
-    // this.init = this.init.bind(this);
-    // this.addConfigurers = this.addConfigurers.bind(this);
-    // this.setMarkdownSettings = this.setMarkdownSettings.bind(this);
-    // this.configure = this.configure.bind(this);
-    // this.process = this.process.bind(this);
-    // this.codeRenderer = this.codeRenderer.bind(this);
-  }
-
-  init() {
-    let parser: Processor = unified().use(parse);
-    this.remarkPlugins.forEach((item) => {
-      parser = applyPlugin(parser, item);
-    });
-
-    let rehype: Processor = parser.use(remark2rehype);
-    this.rehypePlugins.forEach((item) => {
-      rehype = applyPlugin(rehype, item);
-    });
-
-    this.processor = rehype.use(rehype2react, {
-      createElement: React.createElement,
-      components: {
-        // a: NextLink,
-      },
-    });
-  }
+const generateCommonOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  return {
+    remarkPlugins: [gfm],
+    rehypePlugins: [slug],
+  };
+};
 
-  // init() {
-  //   // init markdown-it
-  //   this.md = new MarkdownIt({
-  //     html: true,
-  //     linkify: true,
-  //     highlight: this.codeRenderer,
-  //   });
-
-  //   this.isMarkdownItConfigured = false;
-
-  //   this.markdownItConfigurers = [
-  //     new TaskListsConfigurer(),
-  //     new HeaderConfigurer(),
-  //     new EmojiConfigurer(),
-  //     new MathJaxConfigurer(),
-  //     new DrawioViewerConfigurer(),
-  //     new PlantUMLConfigurer(this.growiRendererConfig),
-  //     new BlockdiagConfigurer(this.growiRendererConfig),
-  //   ];
-
-  //   if (this.pagePath != null) {
-  //     this.markdownItConfigurers.push(
-  //       new LinkerByRelativePathConfigurer(this.pagePath),
-  //     );
-  //   }
-  // }
-
-  // addConfigurers(configurers: any[]): void {
-  //   this.markdownItConfigurers.push(...configurers);
-  // }
-
-  // setMarkdownSettings(settings: MarkdownSettings): void {
-  //   this.md.set(settings);
-  // }
-
-  // configure(): void {
-  //   if (!this.isMarkdownItConfigured) {
-  //     this.markdownItConfigurers.forEach((configurer) => {
-  //       configurer.configure(this.md);
-  //     });
-  //   }
-  // }
-
-  // preProcess(markdown, context) {
-  //   let processed = markdown;
-  //   for (let i = 0; i < this.preProcessors.length; i++) {
-  //     if (!this.preProcessors[i].process) {
-  //       continue;
-  //     }
-  //     processed = this.preProcessors[i].process(processed, context);
-  //   }
-
-  //   return processed;
-  // }
-
-  // process(markdown, context) {
-  //   return this.md.render(markdown, context);
-  // }
-
-  // postProcess(html, context) {
-  //   let processed = html;
-  //   for (let i = 0; i < this.postProcessors.length; i++) {
-  //     if (!this.postProcessors[i].process) {
-  //       continue;
-  //     }
-  //     processed = this.postProcessors[i].process(processed, context);
-  //   }
-
-  //   return processed;
-  // }
-
-  // codeRenderer(code, langExt) {
-  //   const noborder = (!this.growiRendererConfig.highlightJsStyleBorder) ? 'hljs-no-border' : '';
-
-  //   let citeTag = '';
-  //   let hljsLang = 'plaintext';
-  //   let showLinenumbers = false;
-
-  //   if (langExt) {
-  //     // https://regex101.com/r/qGs7eZ/3
-  //     const match = langExt.match(/^([^:=\n]+)?(=([^:=\n]*))?(:([^:=\n]*))?(=([^:=\n]*))?$/);
-
-  //     const lang = match[1];
-  //     const fileName = match[5] || null;
-  //     showLinenumbers = (match[2] != null) || (match[6] != null);
-
-  //     if (fileName != null) {
-  //       citeTag = `<cite>${fileName}</cite>`;
-  //     }
-  //     if (hljs.getLanguage(lang)) {
-  //       hljsLang = lang;
-  //     }
-  //   }
-
-  //   let highlightCode = code;
-  //   try {
-  //     highlightCode = hljs.highlight(hljsLang, code, true).value;
-
-  //     // add line numbers
-  //     if (showLinenumbers) {
-  //       highlightCode = hljs.lineNumbersValue((highlightCode));
-  //     }
-  //   }
-  //   catch (err) {
-  //     logger.error(err);
-  //   }
-
-  //   return `<pre class="hljs ${noborder}">${citeTag}<code>${highlightCode}</code></pre>`;
-  // }
+export const generateViewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
 
-}
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
-export interface RendererGenerator {
-  (growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings | null, pagePath?: Nullable<string>): GrowiRenderer
-}
+  const { remarkPlugins, rehypePlugins } = options;
 
-export const generateViewRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
   // add remark plugins
-  renderer.remarkPlugins.push(footnotes);
-  renderer.remarkPlugins.push(emoji);
+  remarkPlugins?.push(footnotes);
+  remarkPlugins?.push(emoji);
   if (rendererSettings.isEnabledLinebreaks) {
-    renderer.remarkPlugins.push(breaks);
+    remarkPlugins?.push(breaks);
   }
   // add rehypePlugins
-  // renderer.rehypePlugins.push([toc, {
+  // rehypePlugins.push([toc, {
   //   headings: ['h1', 'h2', 'h3'],
   //   customizeTOC: storeTocNode,
   // }]);
   // renderer.rehypePlugins.push([autoLinkHeadings, {
   //   behavior: 'append',
   // }]);
-  renderer.init();
 
   // // Add configurers for viewer
   // renderer.addConfigurers([
@@ -277,14 +253,13 @@ export const generateViewRenderer: RendererGenerator = (
   // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
   // renderer.configure();
 
-  return renderer;
+  return options;
 };
 
-export const generatePreviewRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings | null, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
+export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
   // // Add configurers for preview
   // renderer.addConfigurers([
@@ -296,14 +271,13 @@ export const generatePreviewRenderer: RendererGenerator = (
   // renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
   // renderer.configure();
 
-  return renderer;
+  return options;
 };
 
-export const generateCommentPreviewRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
+export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
   // renderer.addConfigurers([
   //   new TableConfigurer(),
@@ -312,14 +286,13 @@ export const generateCommentPreviewRenderer: RendererGenerator = (
   // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaksInComments });
   // renderer.configure();
 
-  return renderer;
+  return options;
 };
 
-export const generateOthersRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
+export const generateOthersOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
   // renderer.addConfigurers([
   //   new TableConfigurer(),
@@ -328,5 +301,5 @@ export const generateOthersRenderer: RendererGenerator = (
   // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
   // renderer.configure();
 
-  return renderer;
+  return options;
 };

+ 31 - 32
packages/app/src/stores/renderer.tsx

@@ -2,8 +2,9 @@ import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { RendererSettings } from '~/interfaces/services/renderer';
-import GrowiRenderer, {
-  generateViewRenderer, generatePreviewRenderer, generateCommentPreviewRenderer, generateOthersRenderer, RendererGenerator,
+import {
+  ReactMarkdownOptionsGenerator, RendererOptions,
+  generateViewOptions, generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
 } from '~/services/renderer/growi-renderer';
 import { useStaticSWR } from '~/stores/use-static-swr';
 
@@ -14,66 +15,64 @@ export const useRendererSettings = (initialData?: RendererSettings): SWRResponse
 };
 
 // The base hook with common processes
-const _useRendererBase = (rendererId: string, generator: RendererGenerator): SWRResponse<GrowiRenderer, Error> => {
+const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGenerator): SWRResponse<RendererOptions, Error> => {
   const { data: rendererSettings } = useRendererSettings();
-  const { data: currentPath } = useCurrentPagePath();
   const { data: growiRendererConfig } = useGrowiRendererConfig();
 
-  const isAllDataValid = rendererSettings != null && currentPath != null && growiRendererConfig != null;
+  const isAllDataValid = rendererSettings != null && growiRendererConfig != null;
 
   const key = isAllDataValid
-    ? [rendererId, rendererSettings, growiRendererConfig, currentPath]
+    ? [rendererId, rendererSettings, growiRendererConfig]
     : null;
 
-  const swrResult = useSWRImmutable(key);
+  const swrResult = useSWRImmutable<RendererOptions, Error>(key);
 
-  // use mutate because fallbackData does not work
-  // see: https://github.com/weseek/growi/commit/5038473e8d6028c9c91310e374a7b5f48b921a15
   if (isAllDataValid && swrResult.data == null) {
-    swrResult.mutate(generator(growiRendererConfig, rendererSettings, currentPath));
+    swrResult.mutate(generator(growiRendererConfig, rendererSettings));
   }
 
-  return swrResult;
+  // call useSWRImmutable again to foce to update cache
+  return useSWRImmutable<RendererOptions, Error>(key);
 };
 
-export const useViewRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'viewRenderer';
+export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'viewOptions';
 
-  return _useRendererBase(key, generateViewRenderer);
+  return _useOptionsBase(key, generateViewOptions);
 };
 
-export const usePreviewRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'previewRenderer';
+export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'previewOptions';
 
-  return _useRendererBase(key, generatePreviewRenderer);
+  return _useOptionsBase(key, generatePreviewOptions);
 };
 
-export const useCommentPreviewRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'commentPreviewRenderer';
+export const useCommentPreviewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'commentPreviewOptions';
 
-  return _useRendererBase(key, generateCommentPreviewRenderer);
+  return _useOptionsBase(key, generateCommentPreviewOptions);
 };
 
-export const useSearchResultRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'searchResultRenderer';
+export const useSearchResultOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'searchResultOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };
 
-export const useTimelineRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'timelineRenderer';
+export const useTimelineOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'timelineOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };
 
-export const useDraftRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'draftRenderer';
+export const useDraftOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'draftOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };
 
-export const useCustomSidebarRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key: Key = 'customSidebarRenderer';
+export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key: Key = 'customSidebarOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };