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

Merge pull request #6252 from weseek/support/apply-remark

support: Apply remark
Yuki Takei 3 лет назад
Родитель
Сommit
ae21e1ce3c

+ 19 - 4
packages/app/next.config.js

@@ -3,7 +3,7 @@ import { I18NextHMRPlugin } from 'i18next-hmr/plugin';
 import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
 
 import { i18n, localePath } from './src/next-i18next.config';
-import { listScopedPackages } from './src/utils/next.config.utils';
+import { listScopedPackages, listPrefixedPackages } from './src/utils/next.config.utils';
 
 
 // setup logger
@@ -15,12 +15,23 @@ const logger = eazyLogger.Logger({
 
 const setupWithTM = () => {
   // define transpiled packages for '@growi/*'
-  const scopedPackages = listScopedPackages(['@growi'], { ignorePackageNames: '@growi/app' });
+  const packages = [
+    ...listScopedPackages(['@growi'], { ignorePackageNames: '@growi/app' }),
+    // listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM
+    'react-markdown',
+    'unified',
+    'comma-separated-tokens',
+    'decode-named-character-reference',
+    'space-separated-tokens',
+    'trim-lines',
+    'emoticon',
+    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
+  ];
 
   logger.info('{bold:Listing scoped packages for transpiling:}');
-  logger.unprefixed('info', `{grey:${JSON.stringify(scopedPackages, null, 2)}}`);
+  logger.unprefixed('info', `{grey:${JSON.stringify(packages, null, 2)}}`);
 
-  return require('next-transpile-modules')(scopedPackages);
+  return require('next-transpile-modules')(packages);
 };
 const withTM = setupWithTM();
 
@@ -33,6 +44,10 @@ const additionalWebpackEntries = {
 
 /** @type {import('next').NextConfig} */
 const nextConfig = {
+  // == DOES NOT WORK
+  // see: https://github.com/vercel/next.js/discussions/27876
+  // experimental: { esmExternals: true }, // Prefer loading of ES Modules over CommonJS
+
   reactStrictMode: true,
   typescript: {
     tsconfigPath: 'tsconfig.build.client.json',

+ 8 - 12
packages/app/package.json

@@ -149,9 +149,16 @@
     "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-image-crop": "^8.3.0",
+    "react-markdown": "^8.0.3",
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
+    "rehype-slug": "^5.0.1",
+    "rehype-toc": "^3.0.2",
+    "remark-breaks": "^3.0.2",
+    "remark-emoji": "^3.0.2",
+    "remark-footnotes": "^4.0.1",
+    "remark-gfm": "^3.0.1",
     "rimraf": "^3.0.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
@@ -200,17 +207,6 @@
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
-    "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.1.1",
-    "markdown-it-drawio-viewer": "^1.3.1",
-    "markdown-it-emoji": "^1.4.0",
-    "markdown-it-emoji-mart": "^0.1.1",
-    "markdown-it-footnote": "^3.0.1",
-    "markdown-it-mathjax": "^2.0.0",
-    "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.3.0",
-    "markdown-it-task-checkbox": "^1.0.6",
-    "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
@@ -236,7 +232,7 @@
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
-    "swr": "^1.1.2",
+    "swr": "^1.3.0",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "ts-node-dev": "^2.0.0",

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

+ 274 - 243
packages/app/src/services/renderer/growi-renderer.ts

@@ -1,274 +1,305 @@
-import { isClient } from '@growi/core';
-import MarkdownIt from 'markdown-it';
+import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import slug from 'rehype-slug';
+// 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 { Nullable } from '~/interfaces/common'; // TODO: Remove this asap when the ContextExtractor is removed
-import { CustomWindow } from '~/interfaces/global';
 import { GrowiRendererConfig, RendererSettings } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
 
-import CsvToTable from './PreProcessor/CsvToTable';
-import EasyGrid from './PreProcessor/EasyGrid';
-import Linker from './PreProcessor/Linker';
-import XssFilter from './PreProcessor/XssFilter';
-import BlockdiagConfigurer from './markdown-it/blockdiag';
-import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
-import EmojiConfigurer from './markdown-it/emoji';
-import FooternoteConfigurer from './markdown-it/footernote';
-import HeaderConfigurer from './markdown-it/header';
-import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
-import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
-import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
-import MathJaxConfigurer from './markdown-it/mathjax';
-import PlantUMLConfigurer from './markdown-it/plantuml';
-import TableConfigurer from './markdown-it/table';
-import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
-import TaskListsConfigurer from './markdown-it/task-lists';
-import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
+// import CsvToTable from './PreProcessor/CsvToTable';
+// import EasyGrid from './PreProcessor/EasyGrid';
+// import Linker from './PreProcessor/Linker';
+// import XssFilter from './PreProcessor/XssFilter';
+// import BlockdiagConfigurer from './markdown-it/blockdiag';
+// import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
+// import EmojiConfigurer from './markdown-it/emoji';
+// import FooternoteConfigurer from './markdown-it/footernote';
+// import HeaderConfigurer from './markdown-it/header';
+// import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
+// import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
+// import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
+// import MathJaxConfigurer from './markdown-it/mathjax';
+// import PlantUMLConfigurer from './markdown-it/plantuml';
+// import TableConfigurer from './markdown-it/table';
+// import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
+// import TaskListsConfigurer from './markdown-it/task-lists';
+// import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
 
 
 const logger = loggerFactory('growi:util:GrowiRenderer');
 
-declare const hljs;
+// 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
+}
 
-type MarkdownSettings = {
-  breaks?: boolean,
+const generateCommonOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  return {
+    remarkPlugins: [gfm],
+    rehypePlugins: [slug],
+  };
 };
 
-export default class GrowiRenderer {
-
-  preProcessors: any[];
-
-  postProcessors: any[];
-
-  md: any;
-
-  isMarkdownItConfigured: boolean;
-
-  markdownItConfigurers: any[];
-
-  growiRendererConfig: GrowiRendererConfig;
-
-  pagePath?: Nullable<string>;
-
-  /**
-   *
-   * @param {string} mode
-   */
-  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() {
-    // 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),
-      );
-    }
-  }
+export const generateViewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
 
-  addConfigurers(configurers: any[]): void {
-    this.markdownItConfigurers.push(...configurers);
-  }
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
-  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;
-  }
+  const { remarkPlugins, rehypePlugins } = options;
 
-  process(markdown, context) {
-    return this.md.render(markdown, context);
+  // add remark plugins
+  remarkPlugins?.push(footnotes);
+  remarkPlugins?.push(emoji);
+  if (rendererSettings.isEnabledLinebreaks) {
+    remarkPlugins?.push(breaks);
   }
-
-  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 interface RendererGenerator {
-  (growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings | null, pagePath?: Nullable<string>): GrowiRenderer
-}
-
-export const generateViewRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
-
-  // Add configurers for viewer
-  renderer.addConfigurers([
-    new FooternoteConfigurer(),
-    new TocAndAnchorConfigurer(),
-    new HeaderLineNumberConfigurer(),
-    new HeaderWithEditLinkConfigurer(),
-    new TableWithHandsontableButtonConfigurer(),
-  ]);
-
-  renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
-  renderer.configure();
-
-  return renderer;
+  // add rehypePlugins
+  // rehypePlugins.push([toc, {
+  //   headings: ['h1', 'h2', 'h3'],
+  //   customizeTOC: storeTocNode,
+  // }]);
+  // renderer.rehypePlugins.push([autoLinkHeadings, {
+  //   behavior: 'append',
+  // }]);
+
+  // // Add configurers for viewer
+  // renderer.addConfigurers([
+  //   new FooternoteConfigurer(),
+  //   new TocAndAnchorConfigurer(),
+  //   new HeaderLineNumberConfigurer(),
+  //   new HeaderWithEditLinkConfigurer(),
+  //   new TableWithHandsontableButtonConfigurer(),
+  // ]);
+
+  // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
+  // renderer.configure();
+
+  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([
-    new FooternoteConfigurer(),
-    new HeaderLineNumberConfigurer(),
-    new TableConfigurer(),
-  ]);
+  // // Add configurers for preview
+  // renderer.addConfigurers([
+  //   new FooternoteConfigurer(),
+  //   new HeaderLineNumberConfigurer(),
+  //   new TableConfigurer(),
+  // ]);
 
-  renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
-  renderer.configure();
+  // 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(),
-  ]);
+  // renderer.addConfigurers([
+  //   new TableConfigurer(),
+  // ]);
 
-  renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaksInComments });
-  renderer.configure();
+  // 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(),
-  ]);
+  // renderer.addConfigurers([
+  //   new TableConfigurer(),
+  // ]);
 
-  renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
-  renderer.configure();
+  // 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);
 };

+ 21 - 0
packages/app/src/utils/next.config.utils.js

@@ -32,3 +32,24 @@ export const listScopedPackages = (scopes, opts = defaultOpts) => {
 
   return scopedPackages;
 };
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const listPrefixedPackages = (prefixes, opts = defaultOpts) => {
+  const prefixedPackages = [];
+
+  fs.readdirSync(nodeModulesPath)
+    .filter(name => prefixes.some(prefix => name.startsWith(prefix)))
+    .filter(name => !name.startsWith('.'))
+    .forEach((folderName) => {
+      const { name, ignoreTranspileModules } = require(path.resolve(
+        nodeModulesPath,
+        folderName,
+        'package.json',
+      ));
+      if (!ignoreTranspileModules && !opts.ignorePackageNames.includes(name)) {
+        prefixedPackages.push(name);
+      }
+    });
+
+  return prefixedPackages;
+};

Разница между файлами не показана из-за своего большого размера
+ 570 - 95
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов