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

Merge branch 'support/apply-nextjs-2' into imprv/1000057-show-security-settings

kaori 3 лет назад
Родитель
Сommit
7d02744fab
96 измененных файлов с 1952 добавлено и 925 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 0 1
      packages/app/.env.development
  4. 2 2
      packages/app/bin/github-actions/update-readme.sh
  5. 6 6
      packages/app/docker/README.md
  6. 19 4
      packages/app/next.config.js
  7. 16 19
      packages/app/package.json
  8. 6 0
      packages/app/public/static/locales/en_US/translation.json
  9. 6 0
      packages/app/public/static/locales/ja_JP/translation.json
  10. 6 0
      packages/app/public/static/locales/zh_CN/translation.json
  11. 0 2
      packages/app/src/client/legacy/crowi.js
  12. 4 4
      packages/app/src/client/models/MarkdownTable.js
  13. 1 2
      packages/app/src/client/plugin.js
  14. 8 30
      packages/app/src/client/services/AppContainer.js
  15. 39 1
      packages/app/src/client/services/ContextExtractor.tsx
  16. 2 2
      packages/app/src/client/services/PageContainer.js
  17. 0 208
      packages/app/src/client/util/GrowiRenderer.js
  18. 0 23
      packages/app/src/client/util/PreProcessor/XssFilter.js
  19. 1 0
      packages/app/src/client/util/blink-section-header.ts
  20. 4 0
      packages/app/src/client/util/i18n.js
  21. 0 16
      packages/app/src/client/util/markdown-it/mathjax.js
  22. 4 7
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  23. 23 18
      packages/app/src/components/Admin/App/AppSetting.jsx
  24. 1 2
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  25. 4 6
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  26. 2 4
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  27. 2 4
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  28. 2 4
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  29. 10 2
      packages/app/src/components/MyDraftList/Draft.jsx
  30. 6 3
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  31. 18 0
      packages/app/src/components/NotCreatablePage.tsx
  32. 141 137
      packages/app/src/components/Page.jsx
  33. 7 5
      packages/app/src/components/Page/DisplaySwitcher.tsx
  34. 0 1
      packages/app/src/components/Page/RevisionBody.jsx
  35. 8 2
      packages/app/src/components/Page/RevisionLoader.jsx
  36. 0 205
      packages/app/src/components/Page/RevisionRenderer.jsx
  37. 177 0
      packages/app/src/components/Page/RevisionRenderer.tsx
  38. 9 3
      packages/app/src/components/PageComment.tsx
  39. 1 1
      packages/app/src/components/PageComment/CommentEditor.tsx
  40. 6 1
      packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx
  41. 8 0
      packages/app/src/components/PageEditor.tsx
  42. 5 6
      packages/app/src/components/PageEditor/Preview.tsx
  43. 12 3
      packages/app/src/components/PageTimeline.jsx
  44. 4 4
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  45. 1 1
      packages/app/src/components/Sidebar.tsx
  46. 6 1
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  47. 7 3
      packages/app/src/components/Sidebar/SidebarNav.tsx
  48. 6 3
      packages/app/src/components/TableOfContents.jsx
  49. 33 0
      packages/app/src/interfaces/activity.ts
  50. 4 3
      packages/app/src/interfaces/global.ts
  51. 22 0
      packages/app/src/interfaces/services/renderer.ts
  52. 45 12
      packages/app/src/pages/[[...path]].page.tsx
  53. 2 2
      packages/app/src/server/middlewares/login-form-validator.ts
  54. 41 13
      packages/app/src/server/routes/apiv3/slack-integration-settings.js
  55. 1 1
      packages/app/src/server/routes/page.js
  56. 0 6
      packages/app/src/server/service/config-loader.ts
  57. 0 0
      packages/app/src/services/renderer/PostProcessor/.keep
  58. 0 0
      packages/app/src/services/renderer/PreProcessor/CsvToTable.js
  59. 0 0
      packages/app/src/services/renderer/PreProcessor/EasyGrid.js
  60. 0 0
      packages/app/src/services/renderer/PreProcessor/Linker.js
  61. 29 0
      packages/app/src/services/renderer/PreProcessor/XssFilter.ts
  62. 305 0
      packages/app/src/services/renderer/growi-renderer.ts
  63. 0 0
      packages/app/src/services/renderer/interceptor/detach-code-blocks.js
  64. 0 0
      packages/app/src/services/renderer/interceptor/drawio-interceptor.js
  65. 5 4
      packages/app/src/services/renderer/markdown-it/blockdiag.ts
  66. 0 0
      packages/app/src/services/renderer/markdown-it/drawio-viewer.js
  67. 0 0
      packages/app/src/services/renderer/markdown-it/emoji-mart-data.ts
  68. 0 0
      packages/app/src/services/renderer/markdown-it/emoji.js
  69. 0 0
      packages/app/src/services/renderer/markdown-it/footernote.js
  70. 0 0
      packages/app/src/services/renderer/markdown-it/header-line-number.js
  71. 0 0
      packages/app/src/services/renderer/markdown-it/header-with-edit-link.js
  72. 0 0
      packages/app/src/services/renderer/markdown-it/header.js
  73. 5 9
      packages/app/src/services/renderer/markdown-it/link-by-relative-path.ts
  74. 7 0
      packages/app/src/services/renderer/markdown-it/mathjax.js
  75. 5 4
      packages/app/src/services/renderer/markdown-it/plantuml.ts
  76. 0 0
      packages/app/src/services/renderer/markdown-it/table-with-handsontable-button.js
  77. 0 0
      packages/app/src/services/renderer/markdown-it/table.js
  78. 0 4
      packages/app/src/services/renderer/markdown-it/task-lists.js
  79. 0 0
      packages/app/src/services/renderer/markdown-it/toc-and-anchor.js
  80. 0 13
      packages/app/src/services/xss/xssOption.js
  81. 24 0
      packages/app/src/services/xss/xssOption.ts
  82. 22 1
      packages/app/src/stores/context.tsx
  83. 10 3
      packages/app/src/stores/page.tsx
  84. 78 0
      packages/app/src/stores/renderer.tsx
  85. 21 0
      packages/app/src/utils/next.config.utils.js
  86. 118 1
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts
  87. 1 1
      packages/codemirror-textlint/package.json
  88. 1 1
      packages/core/package.json
  89. 1 1
      packages/plugin-attachment-refs/package.json
  90. 1 1
      packages/plugin-lsx/package.json
  91. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  92. 2 2
      packages/plugin-pukiwiki-like-linker/src/client-entry.js
  93. 1 1
      packages/slack/package.json
  94. 2 2
      packages/slackbot-proxy/package.json
  95. 1 1
      packages/ui/package.json
  96. 572 90
      yarn.lock

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 0 - 1
packages/app/.env.development

@@ -7,7 +7,6 @@ MIGRATIONS_DIR=src/migrations/
 APP_SITE_URL=http://localhost:3000
 FILE_UPLOAD=mongodb
 # MONGO_GRIDFS_TOTAL_LIMIT=10485760
-MATHJAX=1
 # NO_CDN=true
 MONGO_URI="mongodb://mongo:27017/growi"
 # REDIS_URI="http://redis:6379"

+ 2 - 2
packages/app/bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.0-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md

+ 6 - 6
packages/app/docker/README.md

@@ -10,12 +10,12 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.0.11`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/docker/Dockerfile)
-* [`5.0.11-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/docker/Dockerfile)
-* [`4.5.22`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
-* [`4.5.22-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
-* [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
-* [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
+* [`5.1.0`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
+* [`5.1.0-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
+* [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
+* [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
+* [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
+* [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 
 
 What is GROWI?

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

+ 16 - 19
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -63,11 +63,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.12-RC.0",
-    "@growi/plugin-attachment-refs": "^5.0.12-RC.0",
-    "@growi/plugin-lsx": "^5.0.12-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.12-RC.0",
-    "@growi/slack": "^5.0.12-RC.0",
+    "@growi/codemirror-textlint": "^5.1.0-RC.2",
+    "@growi/plugin-attachment-refs": "^5.1.0-RC.2",
+    "@growi/plugin-lsx": "^5.1.0-RC.2",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.0-RC.2",
+    "@growi/slack": "^5.1.0-RC.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -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",
@@ -171,7 +178,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^5.0.12-RC.0",
+    "@alienfast/i18next-loader": "^1.1.4",
+    "@growi/ui": "^5.1.0-RC.2",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -199,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",
@@ -235,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",

+ 6 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "English"
+  },
   "Help": "Help",
   "view": "View",
   "Edit": "Edit",
@@ -195,6 +198,9 @@
     "page_not_exist": "This page does not exist.",
     "page_not_exist_alert": "This page does not exist. Please create a new page."
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "Couldn't create path."
+  },
   "custom_navigation": {
     "no_page_list": "There are no pages under this page.",
     "link_sharing_is_disabled": "Link sharing is disabled."

+ 6 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "日本語"
+  },
   "Help": "ヘルプ",
   "view": "View",
   "Edit": "編集",
@@ -197,6 +200,9 @@
     "page_not_exist": "このページは存在しません。",
     "page_not_exist_alert": "このページは存在しません。新たに作成する必要があります。"
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "パスを作成できませんでした。"
+  },
   "custom_navigation": {
     "no_page_list": "このページの配下にはページが存在しません。",
     "link_sharing_is_disabled": "リンクのシェアは無効化されています"

+ 6 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "简体中文"
+  },
   "Help": "帮助",
   "view": "View",
 	"Edit": "编辑",
@@ -195,6 +198,9 @@
     "page_not_exist": "该页面不存在",
     "page_not_exist_alert": "该页面不存在,请创建一个新页面"
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "无法创建路径"
+  },
   "custom_navigation": {
     "no_page_list": "There are no pages under this page.",
     "link_sharing_is_disabled": "链接共享已被禁用"

+ 0 - 2
packages/app/src/client/legacy/crowi.js

@@ -1,5 +1,3 @@
-const { blinkElem, blinkSectionHeaderAtBoot } = require('../util/blink-section-header');
-
 /* eslint-disable react/jsx-filename-extension */
 require('jquery.cookie');
 

+ 4 - 4
packages/app/src/client/models/MarkdownTable.js

@@ -1,6 +1,6 @@
+import csvToMarkdown from 'csv-to-markdown-table';
 import markdownTable from 'markdown-table';
 import stringWidth from 'string-width';
-import csvToMarkdown from 'csv-to-markdown-table';
 
 // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
 // https://regex101.com/r/7BN2fR/7
@@ -8,9 +8,6 @@ const tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
 const tableAlignmentLineNegRE = /^[^-:]*$/; // it is need to check to ignore empty row which is matched above RE
 const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
-// set up DOMParser
-const domParser = new (window.DOMParser)();
-
 const defaultOptions = { stringLength: stringWidth };
 
 /**
@@ -67,6 +64,9 @@ export default class MarkdownTable {
    * The error message is a innerHTML, so must not assign it into element.innerHTML because it can lead to Mutation-based XSS
    */
   static fromHTMLTableTag(str) {
+    // set up DOMParser
+    const domParser = new (window.DOMParser)();
+
     // use DOMParser to prevent DOM based XSS (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
     const dom = domParser.parseFromString(str, 'application/xml');
 

+ 1 - 2
packages/app/src/client/plugin.js

@@ -8,12 +8,11 @@ export default class GrowiPlugin {
    * process plugin entry
    *
    * @param {AppContainer} appContainer
-   * @param {GrowiRenderer} originRenderer The origin instance of GrowiRenderer
    *
    * @memberof CrowiPlugin
    */
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  installAll(appContainer, originRenderer) {
+  installAll(appContainer) {
     // import plugin definitions
     let definitions = [];
     try {

+ 8 - 30
packages/app/src/client/services/AppContainer.js

@@ -1,7 +1,8 @@
 import { Container } from 'unstated';
 
 
-import GrowiRenderer from '../util/GrowiRenderer';
+import GrowiRenderer, { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
+
 import { i18nFactory } from '../util/i18n';
 
 /**
@@ -26,7 +27,6 @@ export default class AppContainer extends Container {
 
     this.containerInstances = {};
     this.componentInstances = {};
-    this.rendererInstances = {};
   }
 
   /**
@@ -45,8 +45,6 @@ export default class AppContainer extends Container {
 
     this.isDocSaved = true;
 
-    this.originRenderer = new GrowiRenderer(this);
-
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
       this.initPlugins();
@@ -57,18 +55,20 @@ export default class AppContainer extends Container {
 
   initPlugins() {
     const growiPlugin = window.growiPlugin;
-    growiPlugin.installAll(this, this.originRenderer);
+    growiPlugin.installAll(this);
   }
 
   injectToWindow() {
     window.appContainer = this;
 
-    const originRenderer = this.getOriginRenderer();
-    window.growiRenderer = originRenderer;
+    const growiRenderer = new GrowiRenderer(this.getConfig());
+    growiRenderer.init();
+
+    window.growiRenderer = growiRenderer;
 
     // backward compatibility
     window.crowi = this;
-    window.crowiRenderer = originRenderer;
+    window.crowiRenderer = window.growiRenderer;
     window.crowiPlugin = window.growiPlugin;
   }
 
@@ -126,26 +126,4 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
   }
 
-  getOriginRenderer() {
-    return this.originRenderer;
-  }
-
-  /**
-   * factory method
-   */
-  getRenderer(mode) {
-    if (this.rendererInstances[mode] != null) {
-      return this.rendererInstances[mode];
-    }
-
-    const renderer = new GrowiRenderer(this, this.originRenderer);
-    // setup
-    renderer.initMarkdownItConfigurers(mode);
-    renderer.setup(mode);
-    // register
-    this.rendererInstances[mode] = renderer;
-
-    return renderer;
-  }
-
 }

+ 39 - 1
packages/app/src/client/services/ContextExtractor.tsx

@@ -3,7 +3,10 @@ import React, { FC, useEffect, useState } from 'react';
 
 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 { useRendererSettings } from '~/stores/renderer';
 import {
   useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
@@ -20,7 +23,7 @@ import {
   useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader,
   useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useGrowiVersion, useAuditLogEnabled,
-  useActivityExpirationSeconds, useAuditLogAvailableActions,
+  useActivityExpirationSeconds, useAuditLogAvailableActions, useGrowiRendererConfig,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -117,6 +120,23 @@ const ContextExtractorOnce: FC = () => {
   useActivityExpirationSeconds(configByContextHydrate.activityExpirationSeconds);
   useAuditLogAvailableActions(configByContextHydrate.auditLogAvailableActions);
   useGrowiVersion(configByContextHydrate.crowi.version);
+  useRendererSettings({
+    isEnabledLinebreaks: configByContextHydrate.isEnabledLinebreaks,
+    isEnabledLinebreaksInComments: configByContextHydrate.isEnabledLinebreaksInComments,
+    adminPreferredIndentSize: configByContextHydrate.adminPreferredIndentSize,
+    isIndentSizeForced: configByContextHydrate.isIndentSizeForced,
+  });
+  useGrowiRendererConfig({
+    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,
+    },
+  });
 
   // Page
   useDeleteUsername(deleteUsername);
@@ -166,6 +186,24 @@ const ContextExtractorOnce: FC = () => {
   const shouldInitAdminSock = !!currentUser?.isAdmin;
   useSetupGlobalAdminSocket(shouldInitAdminSock);
 
+  // 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,
+  // );
+
   return null;
 };
 

+ 2 - 2
packages/app/src/client/services/PageContainer.js

@@ -13,10 +13,10 @@ import { apiv3Post } from '../util/apiv3-client';
 import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
-} from '../util/interceptor/detach-code-blocks';
+} from '../../services/renderer/interceptor/detach-code-blocks';
 import {
   DrawioInterceptor,
-} from '../util/interceptor/drawio-interceptor';
+} from '../../services/renderer/interceptor/drawio-interceptor';
 
 const { isTrashPage } = pagePathUtils;
 

+ 0 - 208
packages/app/src/client/util/GrowiRenderer.js

@@ -1,208 +0,0 @@
-import MarkdownIt from 'markdown-it';
-
-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';
-
-const logger = loggerFactory('growi:util:GrowiRenderer');
-
-export default class GrowiRenderer {
-
-  /**
-   *
-   * @param {AppContainer} appContainer
-   * @param {GrowiRenderer} originRenderer
-   * @param {string} mode
-   */
-  constructor(appContainer, originRenderer) {
-    this.appContainer = appContainer;
-
-    if (originRenderer != null) {
-      this.preProcessors = originRenderer.preProcessors;
-      this.postProcessors = originRenderer.postProcessors;
-    }
-    else {
-      this.preProcessors = [
-        new EasyGrid(),
-        new Linker(),
-        new CsvToTable(),
-        new XssFilter(appContainer),
-      ];
-      this.postProcessors = [
-      ];
-    }
-
-    this.initMarkdownItConfigurers = this.initMarkdownItConfigurers.bind(this);
-    this.setup = this.setup.bind(this);
-    this.process = this.process.bind(this);
-    this.codeRenderer = this.codeRenderer.bind(this);
-  }
-
-  initMarkdownItConfigurers(mode) {
-    const appContainer = this.appContainer;
-
-    // init markdown-it
-    this.md = new MarkdownIt({
-      html: true,
-      linkify: true,
-      highlight: this.codeRenderer,
-    });
-
-    this.isMarkdownItConfigured = false;
-
-    this.markdownItConfigurers = [
-      new LinkerByRelativePathConfigurer(appContainer),
-      new TaskListsConfigurer(appContainer),
-      new HeaderConfigurer(),
-      new EmojiConfigurer(),
-      new MathJaxConfigurer(appContainer),
-      new DrawioViewerConfigurer(),
-      new PlantUMLConfigurer(appContainer),
-      new BlockdiagConfigurer(appContainer),
-    ];
-
-    // add configurers according to mode
-    switch (mode) {
-      case 'page': {
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(),
-          new TocAndAnchorConfigurer(),
-          new HeaderLineNumberConfigurer(),
-          new HeaderWithEditLinkConfigurer(),
-          new TableWithHandsontableButtonConfigurer(),
-        ]);
-        break;
-      }
-      case 'editor':
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(),
-          new HeaderLineNumberConfigurer(),
-          new TableConfigurer(),
-        ]);
-        break;
-      // case 'comment':
-      //   break;
-      default:
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new TableConfigurer(),
-        ]);
-        break;
-    }
-  }
-
-  /**
-   * setup with crowi config
-   */
-  setup(mode) {
-    const crowiConfig = this.appContainer.config;
-
-    let isEnabledLinebreaks;
-    switch (mode) {
-      case 'comment':
-        isEnabledLinebreaks = crowiConfig.isEnabledLinebreaksInComments;
-        break;
-      default:
-        isEnabledLinebreaks = crowiConfig.isEnabledLinebreaks;
-        break;
-    }
-
-    this.md.set({
-      breaks: isEnabledLinebreaks,
-    });
-
-    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 config = this.appContainer.getConfig();
-    const noborder = (!config.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>`;
-  }
-
-  highlightCode(code, lang) {
-  }
-
-}

+ 0 - 23
packages/app/src/client/util/PreProcessor/XssFilter.js

@@ -1,23 +0,0 @@
-import Xss from '~/services/xss';
-import XssOption from '~/services/xss/xssOption';
-
-export default class XssFilter {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-
-    if (crowi.config.isEnabledXssPrevention) {
-      this.xssOption = new XssOption(crowi.config);
-      this.xss = new Xss(this.xssOption);
-    }
-  }
-
-  process(markdown) {
-    if (this.crowi.config.isEnabledXssPrevention) {
-      return this.xss.process(markdown);
-    }
-
-    return markdown;
-  }
-
-}

+ 1 - 0
packages/app/src/client/util/blink-section-header.ts

@@ -23,5 +23,6 @@ export const blinkSectionHeaderAtBoot = (): HTMLElement | undefined => {
   const elem = document.getElementById(id);
   if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
     blinkElem(elem);
+    return elem;
   }
 };

+ 4 - 0
packages/app/src/client/util/i18n.js

@@ -1,3 +1,4 @@
+
 /* eslint-disable */
 import i18n from 'i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
@@ -15,6 +16,9 @@ Object.values(locales).forEach((locale) => {
   });
 });
 
+/*
+* Note: This file will be deleted. use "~/next-i18next.config" instead
+*/
 // extract metadata list from 'public/static/locales/${locale}/meta.json'
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
 

+ 0 - 16
packages/app/src/client/util/markdown-it/mathjax.js

@@ -1,16 +0,0 @@
-export default class MathJaxConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-
-    const config = crowi.getConfig();
-    this.isEnabled = !!config.env.MATHJAX; // convert to boolean
-  }
-
-  configure(md) {
-    if (this.isEnabled) {
-      md.use(require('markdown-it-mathjax')());
-    }
-  }
-
-}

+ 4 - 7
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -2,19 +2,16 @@
  * reveal.js growi-renderer plugin.
  */
 (function(root, factory) {
-  // get AppContainer instance from parent window
-  const appContainer = window.parent.appContainer;
-
-  const growiRendererPlugin = factory(appContainer);
+  const growiRendererPlugin = factory();
   growiRendererPlugin.initialize();
-}(this, (appContainer) => {
+}(this, () => {
   /* eslint-disable no-useless-escape */
   const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$';
   const DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$';
   const DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
   /* eslint-enable no-useless-escape */
 
-  const growiRenderer = appContainer.getRenderer('editor');
+  const growiRenderer = window.parent.previewRenderer;
 
   let marked;
 
@@ -61,7 +58,7 @@
         section.setAttribute('data-markdown-parsed', 'true');
         const notes = section.querySelector('aside.notes');
         markdown = marked.getMarkdownFromSlide(section);
-        const context = { markdown };
+        const context = { markdown, currentPathname: decodeURIComponent(window.parent.location.pathname) };
 
         interceptorManager.process('preRender', context)
           .then(() => { return interceptorManager.process('prePreProcess', context) })

+ 23 - 18
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -1,11 +1,11 @@
 import React, { useCallback } from 'react';
 
+import { useTranslation, i18n } from 'next-i18next';
 import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { localeMetadatas } from '~/client/util/i18n';
+import { i18n as i18nConfig } from '~/next-i18next.config';
 import loggerFactory from '~/utils/logger';
 
 
@@ -77,22 +77,27 @@ const AppSetting = (props) => {
         </label>
         <div className="col-md-6 py-2">
           {
-            localeMetadatas.map(meta => (
-              <div key={meta.id} className="custom-control custom-radio custom-control-inline">
-                <input
-                  type="radio"
-                  id={`radioLang${meta.id}`}
-                  className="custom-control-input"
-                  name="globalLang"
-                  value={meta.id}
-                  checked={adminAppContainer.state.globalLang === meta.id}
-                  onChange={(e) => {
-                    adminAppContainer.changeGlobalLang(e.target.value);
-                  }}
-                />
-                <label className="custom-control-label" htmlFor={`radioLang${meta.id}`}>{meta.displayName}</label>
-              </div>
-            ))
+            i18nConfig.locales.map((locale) => {
+              const fixedT = i18n.getFixedT(locale);
+              i18n.loadLanguages(i18nConfig.locales);
+
+              return (
+                <div key={locale} className="custom-control custom-radio custom-control-inline">
+                  <input
+                    type="radio"
+                    id={`radioLang${locale}`}
+                    className="custom-control-input"
+                    name="globalLang"
+                    value={locale}
+                    checked={adminAppContainer.state.globalLang === locale}
+                    onChange={(e) => {
+                      adminAppContainer.changeGlobalLang(e.target.value);
+                    }}
+                  />
+                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
+                </div>
+              );
+            })
           }
         </div>
       </div>

+ 1 - 2
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -58,8 +58,7 @@ const AppSettingsPageContents = (props: Props) => {
       <div className="row">
         <div className="col-lg-12">
           <h2 className="admin-setting-header">{t('App Settings')}</h2>
-          {/* TODO: show AppSetting by https://redmine.weseek.co.jp/issues/100056 */}
-          {/* <AppSetting /> */}
+          <AppSetting />
         </div>
       </div>
 

+ 4 - 6
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -5,8 +5,8 @@ import { Card, CardBody } from 'reactstrap';
 
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
-// import PresentationForm from './PresentationForm';
-// import XssForm from './XssForm';
+import PresentationForm from './PresentationForm';
+import XssForm from './XssForm';
 
 const MarkDownSettingContents = React.memo((): JSX.Element => {
   const { t } = useTranslation();
@@ -32,16 +32,14 @@ const MarkDownSettingContents = React.memo((): JSX.Element => {
       <Card className="card well my-3">
         <CardBody className="px-0 py-2">{ t('admin:markdown_setting.presentation_desc') }</CardBody>
       </Card>
-      {/* TODO: show PresentationForm and Xss Form components by https://redmine.weseek.co.jp/issues/100058 */}
-      {/* <PresentationForm /> */}
+      <PresentationForm />
 
       {/* XSS Setting */}
       <h2 className="admin-setting-header">{ t('admin:markdown_setting.xss_header') }</h2>
       <Card className="card well my-3">
         <CardBody className="px-0 py-2">{ t('admin:markdown_setting.xss_desc') }</CardBody>
       </Card>
-      {/* TODO: show PresentationForm and Xss Form components by https://redmine.weseek.co.jp/issues/100058 */}
-      {/* <XssForm /> */}
+      <XssForm />
     </div>
   );
 });

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
@@ -129,7 +128,6 @@ class PresentationForm extends React.Component {
 
 PresentationForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
@@ -140,6 +138,6 @@ const PresentationFormWrapperFC = (props) => {
   return <PresentationForm t={t} {...props} />;
 };
 
-const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
 
 export default PresentationFormWrapper;

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -79,7 +78,6 @@ class WhiteListInput extends React.Component {
 
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
@@ -90,6 +88,6 @@ const PresentationFormWrapperFC = (props) => {
   return <WhiteListInput t={t} {...props} />;
 };
 
-const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
 
 export default WhiteListWrapper;

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
@@ -165,7 +164,6 @@ class XssForm extends React.Component {
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
@@ -175,6 +173,6 @@ const XssFormWrapperFC = (props) => {
   return <XssForm t={t} {...props} />;
 };
 
-const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AdminMarkDownContainer]);
 
 export default XssFormWrapper;

+ 10 - 2
packages/app/src/components/MyDraftList/Draft.jsx

@@ -9,6 +9,8 @@ import {
 } from 'reactstrap';
 
 import AppContainer from '~/client/services/AppContainer';
+import GrowiRenderer from '~/services/renderer/growi-renderer';
+import { useDraftRenderer } from '~/stores/renderer';
 
 import RevisionBody from '../Page/RevisionBody';
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -25,7 +27,7 @@ class Draft extends React.Component {
       showCopiedMessage: false,
     };
 
-    this.growiRenderer = this.props.appContainer.getRenderer('draft');
+    this.growiRenderer = this.props.growiRenderer;
 
     this.changeToolTipLabel = this.changeToolTipLabel.bind(this);
     this.expandPanelHandler = this.expandPanelHandler.bind(this);
@@ -194,6 +196,7 @@ class Draft extends React.Component {
 Draft.propTypes = {
   t: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
 
   index: PropTypes.number.isRequired,
   path: PropTypes.string.isRequired,
@@ -204,7 +207,12 @@ Draft.propTypes = {
 
 const DraftWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <Draft t={t} {...props} />;
+  const { data: growiRenderer } = useDraftRenderer();
+  if (growiRenderer == null) {
+    return <></>;
+  }
+
+  return <Draft t={t} growiRenderer={growiRenderer} {...props} />;
 };
 
 /**

+ 6 - 3
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -5,6 +5,7 @@ import React, {
 import { isServer } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
+import Link from 'next/link';
 import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 
@@ -141,9 +142,11 @@ export const GrowiNavbar = (): JSX.Element => {
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
       {/* Brand Logo  */}
       <div className="navbar-brand mr-0">
-        <a className="grw-logo d-block" href="/">
-          <GrowiLogo />
-        </a>
+        <Link href="/">
+          <a className="grw-logo d-block">
+            <GrowiLogo />
+          </a>
+        </Link>
       </div>
 
       <div className="grw-app-title d-none d-md-block">

+ 18 - 0
packages/app/src/components/NotCreatablePage.tsx

@@ -0,0 +1,18 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+export const NotCreatablePage: FC = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="row not-found-message-row">
+      <div className="col-md-12">
+        <h2 className="text-muted">
+          <i className="icon-ban mr-1" aria-hidden="true"></i>
+          { t('not_creatable_page.could_not_creata_path') }
+        </h2>
+      </div>
+    </div>
+  );
+};

+ 141 - 137
packages/app/src/components/Page.jsx

@@ -1,35 +1,34 @@
-import React, { useEffect, useRef } from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useRef, useState,
+} from 'react';
 
+import dynamic from 'next/dynamic';
 import PropTypes from 'prop-types';
+import { debounce } from 'throttle-debounce';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
-import AppContainer from '~/client/services/AppContainer';
-import EditorContainer from '~/client/services/EditorContainer';
-import PageContainer from '~/client/services/PageContainer';
+import { blinkSectionHeaderAtBoot } from '~/client/util/blink-section-header';
 import { getOptionsToSave } from '~/client/util/editor';
 import {
-  useCurrentPagePath, useIsGuestUser,
+  useIsGuestUser, useIsBlinkedHeaderAtBoot,
 } from '~/stores/context';
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
 import {
-  useEditorMode, useIsMobile, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+  useEditorMode, useIsMobile,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
 import RevisionRenderer from './Page/RevisionRenderer';
-import DrawioModal from './PageEditor/DrawioModal';
-import GridEditModal from './PageEditor/GridEditModal';
-import HandsontableModal from './PageEditor/HandsontableModal';
-import LinkEditModal from './PageEditor/LinkEditModal';
 import mdu from './PageEditor/MarkdownDrawioUtil';
 import mtu from './PageEditor/MarkdownTableUtil';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 const logger = loggerFactory('growi:Page');
 
-class Page extends React.Component {
+class PageSubstance extends React.Component {
 
   constructor(props) {
     super(props);
@@ -39,8 +38,6 @@ class Page extends React.Component {
       currentTargetDrawioArea: null,
     };
 
-    this.growiRenderer = this.props.appContainer.getRenderer('page');
-
     this.gridEditModal = React.createRef();
     this.linkEditModal = React.createRef();
     this.handsontableModal = React.createRef();
@@ -56,10 +53,10 @@ class Page extends React.Component {
    * @param endLineNumber
    */
   launchHandsontableModal(beginLineNumber, endLineNumber) {
-    const markdown = this.props.pageContainer.state.markdown;
-    const tableLines = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
-    this.setState({ currentTargetTableArea: { beginLineNumber, endLineNumber } });
-    this.handsontableModal.current.show(MarkdownTable.fromMarkdownString(tableLines));
+    // const markdown = this.props.pageContainer.state.markdown;
+    // const tableLines = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
+    // this.setState({ currentTargetTableArea: { beginLineNumber, endLineNumber } });
+    // this.handsontableModal.current.show(MarkdownTable.fromMarkdownString(tableLines));
   }
 
   /**
@@ -68,96 +65,102 @@ class Page extends React.Component {
    * @param endLineNumber
    */
   launchDrawioModal(beginLineNumber, endLineNumber) {
-    const markdown = this.props.pageContainer.state.markdown;
-    const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber);
-    const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
-    this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
-    this.drawioModal.current.show(drawioData);
+    // const markdown = this.props.pageContainer.state.markdown;
+    // const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber);
+    // const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
+    // this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
+    // this.drawioModal.current.show(drawioData);
   }
 
   async saveHandlerForHandsontableModal(markdownTable) {
-    const {
-      isSlackEnabled, slackChannels, pageContainer, mutateIsEnabledUnsavedWarning, grant, grantGroupId, grantGroupName, pageTags,
-    } = this.props;
-    const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
-
-    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(
-      markdownTable,
-      this.props.pageContainer.state.markdown,
-      this.state.currentTargetTableArea.beginLineNumber,
-      this.state.currentTargetTableArea.endLineNumber,
-    );
-
-    try {
-      // disable unsaved warning
-      mutateIsEnabledUnsavedWarning(false);
-
-      // eslint-disable-next-line no-unused-vars
-      const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
-      logger.debug('success to save');
-
-      pageContainer.showSuccessToastr();
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      pageContainer.showErrorToastr(error);
-    }
-    finally {
-      this.setState({ currentTargetTableArea: null });
-    }
+    // const {
+    //   isSlackEnabled, slackChannels, pageContainer, mutateIsEnabledUnsavedWarning, grant, grantGroupId, grantGroupName, pageTags,
+    // } = this.props;
+    // const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
+
+    // const newMarkdown = mtu.replaceMarkdownTableInMarkdown(
+    //   markdownTable,
+    //   this.props.pageContainer.state.markdown,
+    //   this.state.currentTargetTableArea.beginLineNumber,
+    //   this.state.currentTargetTableArea.endLineNumber,
+    // );
+
+    // try {
+    //   // disable unsaved warning
+    //   mutateIsEnabledUnsavedWarning(false);
+
+    //   // eslint-disable-next-line no-unused-vars
+    //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
+    //   logger.debug('success to save');
+
+    //   pageContainer.showSuccessToastr();
+    // }
+    // catch (error) {
+    //   logger.error('failed to save', error);
+    //   pageContainer.showErrorToastr(error);
+    // }
+    // finally {
+    //   this.setState({ currentTargetTableArea: null });
+    // }
   }
 
   async saveHandlerForDrawioModal(drawioData) {
-    const {
-      isSlackEnabled, slackChannels, pageContainer, pageTags, grant, grantGroupId, grantGroupName, mutateIsEnabledUnsavedWarning,
-    } = this.props;
-    const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
-
-    const newMarkdown = mdu.replaceDrawioInMarkdown(
-      drawioData,
-      this.props.pageContainer.state.markdown,
-      this.state.currentTargetDrawioArea.beginLineNumber,
-      this.state.currentTargetDrawioArea.endLineNumber,
-    );
-
-    try {
-      // disable unsaved warning
-      mutateIsEnabledUnsavedWarning(false);
-
-      // eslint-disable-next-line no-unused-vars
-      const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
-      logger.debug('success to save');
-
-      pageContainer.showSuccessToastr();
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      pageContainer.showErrorToastr(error);
-    }
-    finally {
-      this.setState({ currentTargetDrawioArea: null });
-    }
+    // const {
+    //   isSlackEnabled, slackChannels, pageContainer, pageTags, grant, grantGroupId, grantGroupName, mutateIsEnabledUnsavedWarning,
+    // } = this.props;
+    // const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
+
+    // const newMarkdown = mdu.replaceDrawioInMarkdown(
+    //   drawioData,
+    //   this.props.pageContainer.state.markdown,
+    //   this.state.currentTargetDrawioArea.beginLineNumber,
+    //   this.state.currentTargetDrawioArea.endLineNumber,
+    // );
+
+    // try {
+    //   // disable unsaved warning
+    //   mutateIsEnabledUnsavedWarning(false);
+
+    //   // eslint-disable-next-line no-unused-vars
+    //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
+    //   logger.debug('success to save');
+
+    //   pageContainer.showSuccessToastr();
+    // }
+    // catch (error) {
+    //   logger.error('failed to save', error);
+    //   pageContainer.showErrorToastr(error);
+    // }
+    // finally {
+    //   this.setState({ currentTargetDrawioArea: null });
+    // }
   }
 
   render() {
     const {
-      pageContainer, pagePath, isMobile, isGuestUser,
+      rendererOptions, page, isMobile, isGuestUser,
     } = this.props;
-    const { markdown, revisionId } = pageContainer.state;
+    const { path } = page;
+    const { _id: revisionId, body: markdown } = page.revision;
+
+    // const DrawioModal = dynamic(() => import('./PageEditor/DrawioModal'), { ssr: false });
+    // const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr: false });
+    // const HandsontableModal = dynamic(() => import('./PageEditor/HandsontableModal'), { ssr: false });
+    // const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
 
     return (
       <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
 
         { revisionId != null && (
-          <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} pagePath={pagePath} />
+          <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} pagePath={path} />
         )}
 
         { !isGuestUser && (
           <>
-            <GridEditModal ref={this.gridEditModal} />
-            <LinkEditModal ref={this.LinkEditModal} />
-            <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} />
-            <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} />
+            {/* <GridEditModal ref={this.gridEditModal} /> */}
+            {/* <LinkEditModal ref={this.LinkEditModal} /> */}
+            {/* <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} /> */}
+            {/* <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} /> */}
           </>
         )}
       </div>
@@ -166,89 +169,90 @@ class Page extends React.Component {
 
 }
 
-Page.propTypes = {
-  // TODO: remove this when omitting unstated is completed
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
+PageSubstance.propTypes = {
+  rendererOptions: PropTypes.object.isRequired,
 
-  pagePath: PropTypes.string.isRequired,
+  page: PropTypes.any.isRequired,
   pageTags:  PropTypes.arrayOf(PropTypes.string),
   editorMode: PropTypes.string.isRequired,
   isGuestUser: PropTypes.bool.isRequired,
   isMobile: PropTypes.bool,
   isSlackEnabled: PropTypes.bool.isRequired,
   slackChannels: PropTypes.string.isRequired,
-  grant: PropTypes.number.isRequired,
-  grantGroupId: PropTypes.string,
-  grantGroupName: PropTypes.string,
 };
 
-const PageWrapper = (props) => {
-  const { data: currentPagePath } = useCurrentPagePath();
+export const Page = (props) => {
+  const { data: currentPage } = useSWRxCurrentPage();
   const { data: editorMode } = useEditorMode();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isMobile } = useIsMobile();
-  const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
+  const { data: slackChannelsData } = useSWRxSlackChannels(currentPage?.path);
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: pageTags } = usePageTagsForEditors();
-  const { data: grant } = useSelectedGrant();
-  const { data: grantGroupId } = useSelectedGrantGroupId();
-  const { data: grantGroupName } = useSelectedGrantGroupName();
+  const { data: rendererOptions } = useViewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { data: isBlinkedAtBoot, mutate: mutateBlinkedAtBoot } = useIsBlinkedHeaderAtBoot();
 
   const pageRef = useRef(null);
 
-  // set handler to open DrawioModal
-  useEffect(() => {
-    const handler = (beginLineNumber, endLineNumber) => {
-      if (pageRef?.current != null) {
-        pageRef.current.launchDrawioModal(beginLineNumber, endLineNumber);
-      }
-    };
-    window.globalEmitter.on('launchDrawioModal', handler);
-
-    return function cleanup() {
-      window.globalEmitter.removeListener('launchDrawioModal', handler);
-    };
-  }, []);
-
-  // set handler to open HandsontableModal
   useEffect(() => {
-    const handler = (beginLineNumber, endLineNumber) => {
-      if (pageRef?.current != null) {
-        pageRef.current.launchHandsontableModal(beginLineNumber, endLineNumber);
-      }
-    };
-    window.globalEmitter.on('launchHandsontableModal', handler);
-
-    return function cleanup() {
-      window.globalEmitter.removeListener('launchHandsontableModal', handler);
-    };
-  }, []);
+    if (isBlinkedAtBoot) {
+      return;
+    }
 
-  if (currentPagePath == null || editorMode == null || isGuestUser == null) {
+    blinkSectionHeaderAtBoot();
+    mutateBlinkedAtBoot(true);
+  }, [mutateBlinkedAtBoot]);
+
+  // // set handler to open DrawioModal
+  // useEffect(() => {
+  //   const handler = (beginLineNumber, endLineNumber) => {
+  //     if (pageRef?.current != null) {
+  //       pageRef.current.launchDrawioModal(beginLineNumber, endLineNumber);
+  //     }
+  //   };
+  //   window.globalEmitter.on('launchDrawioModal', handler);
+
+  //   return function cleanup() {
+  //     window.globalEmitter.removeListener('launchDrawioModal', handler);
+  //   };
+  // }, []);
+
+  // // set handler to open HandsontableModal
+  // useEffect(() => {
+  //   const handler = (beginLineNumber, endLineNumber) => {
+  //     if (pageRef?.current != null) {
+  //       pageRef.current.launchHandsontableModal(beginLineNumber, endLineNumber);
+  //     }
+  //   };
+  //   window.globalEmitter.on('launchHandsontableModal', handler);
+
+  //   return function cleanup() {
+  //     window.globalEmitter.removeListener('launchHandsontableModal', handler);
+  //   };
+  // }, []);
+
+  if (currentPage == null || editorMode == null || isGuestUser == null || rendererOptions == null) {
+    logger.warn('Some of materials are missing.', {
+      currentPage: currentPage?._id, editorMode, isGuestUser, rendererOptions,
+    });
     return null;
   }
 
 
   return (
-    <Page
+    <PageSubstance
       {...props}
       ref={pageRef}
-      pagePath={currentPagePath}
+      rendererOptions={rendererOptions}
+      page={currentPage}
       editorMode={editorMode}
       isGuestUser={isGuestUser}
       isMobile={isMobile}
       isSlackEnabled={isSlackEnabled}
       pageTags={pageTags}
       slackChannels={slackChannelsData.toString()}
-      grant={grant}
-      grantGroupId={grantGroupId}
-      grantGroupName={grantGroupName}
       mutateIsEnabledUnsavedWarning={mutateIsEnabledUnsavedWarning}
     />
   );
 };
-
-export default withUnstatedContainers(PageWrapper, [AppContainer, PageContainer, EditorContainer]);

+ 7 - 5
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -8,7 +8,7 @@ import { TabContent, TabPane } from 'reactstrap';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { isPopulated } from '~/interfaces/common';
 import {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound,
+  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound, useIsNotCreatable,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -16,8 +16,9 @@ import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import CountBadge from '../Common/CountBadge';
 import PageListIcon from '../Icons/PageListIcon';
+import { NotCreatablePage } from '../NotCreatablePage';
 import NotFoundPage from '../NotFoundPage';
-// import Page from '../Page';
+import { Page } from '../Page';
 // import PageEditor from '../PageEditor';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
 import TableOfContents from '../TableOfContents';
@@ -46,6 +47,7 @@ const DisplaySwitcher = (): JSX.Element => {
   const { data: isEditable } = useIsEditable();
   const { data: pageUser } = usePageUser();
   const { data: isNotFound } = useIsNotFound();
+  const { data: isNotCreatable } = useIsNotCreatable();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
 
   const { data: editorMode } = useEditorMode();
@@ -114,9 +116,9 @@ const DisplaySwitcher = (): JSX.Element => {
 
             <div className="flex-grow-1 flex-basis-0 mw-0">
               { isUserPage && <UserInfo pageUser={pageUser} />}
-              {/* { !isNotFound && <Page /> } */}
-              { !isNotFound && revision != null && isPopulated(revision) && revision.body }
-              { isNotFound && <NotFoundPage /> }
+              { !isNotFound && <Page /> }
+              { isNotFound && !isNotCreatable && <NotFoundPage /> }
+              { isNotFound && isNotCreatable && <NotCreatablePage /> }
             </div>
 
           </div>

+ 0 - 1
packages/app/src/components/Page/RevisionBody.jsx

@@ -73,7 +73,6 @@ export default class RevisionBody extends React.PureComponent {
 
 RevisionBody.propTypes = {
   html: PropTypes.string,
-  isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   renderMathJaxInRealtime: PropTypes.bool,
   additionalClassName: PropTypes.string,

+ 8 - 2
packages/app/src/components/Page/RevisionLoader.jsx

@@ -4,8 +4,9 @@ import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { Waypoint } from 'react-waypoint';
 
-import GrowiRenderer from '~/client/util/GrowiRenderer';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import GrowiRenderer from '~/services/renderer/growi-renderer';
+import { useViewRenderer } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
 import RevisionRenderer from './RevisionRenderer';
@@ -134,7 +135,12 @@ RevisionLoader.propTypes = {
 
 const RevisionLoaderWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <RevisionLoader t={t} {...props} />;
+  const { data: growiRenderer } = useViewRenderer();
+  if (growiRenderer == null) {
+    return <></>;
+  }
+
+  return <RevisionLoader t={t} growiRenderer={growiRenderer} {...props} />;
 };
 
 export default RevisionLoaderWrapperFC;

+ 0 - 205
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -1,205 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import AppContainer from '~/client/services/AppContainer';
-import GrowiRenderer from '~/client/util/GrowiRenderer';
-import { blinkElem } from '~/client/util/blink-section-header';
-import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
-import { useEditorSettings } from '~/stores/editor';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import RevisionBody from './RevisionBody';
-
-import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
-
-const logger = loggerFactory('components:Page:RevisionRenderer');
-
-class LegacyRevisionRenderer extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      html: '',
-    };
-
-    this.renderHtml = this.renderHtml.bind(this);
-    this.getHighlightedBody = this.getHighlightedBody.bind(this);
-  }
-
-  initCurrentRenderingContext() {
-    this.currentRenderingContext = {
-      markdown: this.props.markdown,
-      pagePath: this.props.pagePath,
-      renderDrawioInRealtime: this.props.editorSettings?.renderDrawioInRealtime,
-      currentPathname: decodeURIComponent(window.location.pathname),
-    };
-  }
-
-  componentDidMount() {
-    this.initCurrentRenderingContext();
-    this.renderHtml();
-  }
-
-  componentDidUpdate(prevProps) {
-    const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
-    const { markdown, highlightKeywords } = this.props;
-
-    // render only when props.markdown is updated
-    if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
-      this.initCurrentRenderingContext();
-      this.renderHtml();
-      return;
-    }
-
-    const HeaderLink = document.getElementsByClassName('revision-head-link');
-    const HeaderLinkArray = Array.from(HeaderLink);
-    addSmoothScrollEvent(HeaderLinkArray, blinkElem);
-
-    const { interceptorManager } = window;
-
-    interceptorManager.process('postRenderHtml', this.currentRenderingContext);
-  }
-
-  /**
-   * transplanted from legacy code -- Yuki Takei
-   * @param {string} body html strings
-   * @param {string} keywords
-   */
-  getHighlightedBody(body, keywords) {
-    const normalizedKeywordsArray = [];
-    // !!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 = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
-
-      const arr = [];
-      insideTagStrs.forEach((str, i) => {
-        arr.push(str);
-        arr.push(betweenTagStrs[i]);
-      });
-      returnBody = arr.join('');
-    }
-    else {
-      // inferior highlighter
-      returnBody = highlighter2(body);
-    }
-
-    return returnBody;
-  }
-
-  async renderHtml() {
-    const {
-      appContainer, growiRenderer,
-      highlightKeywords,
-    } = this.props;
-
-    const { interceptorManager } = window;
-    const context = this.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 && highlightKeywords.length > 0 && !isMarkdownEmpty) {
-      context.parsedHTML = this.getHighlightedBody(context.parsedHTML, highlightKeywords);
-    }
-    await interceptorManager.process('postPostProcess', context);
-    await interceptorManager.process('preRenderHtml', context);
-
-    this.setState({ html: context.parsedHTML });
-  }
-
-  render() {
-    const config = this.props.appContainer.getConfig();
-    const isMathJaxEnabled = !!config.env.MATHJAX;
-
-    return (
-      <RevisionBody
-        html={this.state.html}
-        isMathJaxEnabled={isMathJaxEnabled}
-        additionalClassName={this.props.additionalClassName}
-        renderMathJaxOnInit
-      />
-    );
-  }
-
-}
-
-LegacyRevisionRenderer.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  highlightKeywords: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
-  additionalClassName: PropTypes.string,
-  editorSettings: PropTypes.any,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const LegacyRevisionRendererWrapper = withUnstatedContainers(LegacyRevisionRenderer, [AppContainer]);
-
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const RevisionRenderer = (props) => {
-  const { data: editorSettings } = useEditorSettings();
-
-  return <LegacyRevisionRendererWrapper {...props} editorSettings={editorSettings} />;
-};
-
-RevisionRenderer.propTypes = {
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  highlightKeywords: PropTypes.arrayOf(PropTypes.string),
-  additionalClassName: PropTypes.string,
-};
-
-export default RevisionRenderer;

+ 177 - 0
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -0,0 +1,177 @@
+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 { 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';
+
+
+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;
+// }
+
+
+type Props = {
+  rendererOptions: RendererOptions,
+  markdown: string,
+  pagePath: string,
+  highlightKeywords?: string | string[],
+  additionalClassName?: string,
+}
+
+const RevisionRenderer = (props: Props): JSX.Element => {
+
+  const {
+    rendererOptions, markdown, pagePath, highlightKeywords, additionalClassName,
+  } = props;
+
+  return (
+    <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;

+ 9 - 3
packages/app/src/components/PageComment.tsx

@@ -8,6 +8,7 @@ import { Button } from 'reactstrap';
 import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
+import { useCommentPreviewRenderer } from '~/stores/renderer';
 
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
@@ -35,6 +36,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
   } = props;
 
   const { data: comments, mutate } = useSWRxPageComment(pageId);
+  const { data: growiRenderer } = useCommentPreviewRenderer();
 
   const [commentToBeDeleted, setCommentToBeDeleted] = useState<ICommentHasId | null>(null);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
@@ -110,7 +112,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
 
   const generateCommentInnerElement = (comment: ICommentHasId) => (
     <Comment
-      growiRenderer={appContainer.getRenderer('comment')}
+      growiRenderer={growiRenderer}
       deleteBtnClicked={onClickDeleteButton}
       comment={comment}
       onComment={mutate}
@@ -122,7 +124,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
     <ReplayComments
       replyList={replyComments}
       deleteBtnClicked={onClickDeleteButton}
-      growiRenderer={appContainer.getRenderer('comment')}
+      growiRenderer={growiRenderer}
       isReadOnly={isReadOnly}
     />
   );
@@ -142,6 +144,10 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
     return <></>;
   }
 
+  if (growiRenderer == null) {
+    return <></>;
+  }
+
   let commentTitleClasses = 'border-bottom py-3 mb-3';
   commentTitleClasses = titleAlign != null ? `${commentTitleClasses} text-${titleAlign}` : `${commentTitleClasses} text-center`;
 
@@ -185,7 +191,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
                     {/* display reply editor */}
                     {(!isReadOnly && showEditorIds.has(comment._id)) && (
                       <CommentEditor
-                        growiRenderer={appContainer.getRenderer('comment')}
+                        growiRenderer={growiRenderer}
                         replyTo={comment._id}
                         onCancelButtonClicked={() => {
                           removeShowEditorId(comment._id);

+ 1 - 1
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -12,10 +12,10 @@ import * as toastr from 'toastr';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
-import GrowiRenderer from '~/client/util/GrowiRenderer';
 import { apiPostForm } from '~/client/util/apiv1-client';
 import { CustomWindow } from '~/interfaces/global';
 import { IInterceptorManager } from '~/interfaces/interceptor-manager';
+import GrowiRenderer from '~/services/renderer/growi-renderer';
 import { useSWRxPageComment } from '~/stores/comment';
 import {
   useCurrentPagePath, useCurrentPageId, useCurrentUser, useRevisionId,

+ 6 - 1
packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx

@@ -5,6 +5,7 @@ import { useSWRxPageComment } from '../../stores/comment';
 import AppContainer from '~/client/services/AppContainer';
 
 import CommentEditor from './CommentEditor';
+import { useCommentPreviewRenderer } from '~/stores/renderer';
 
 type Props = {
   appContainer: AppContainer,
@@ -15,9 +16,13 @@ const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
 
   const { pageId } = props;
   const { mutate } = useSWRxPageComment(pageId);
+  const { data: growiRenderer } = useCommentPreviewRenderer();
+
+  if (growiRenderer == null) {
+    return <></>;
+  }
 
   const { appContainer } = props;
-  const growiRenderer = appContainer.getRenderer('comment');
 
   return (
     <CommentEditor

+ 8 - 0
packages/app/src/components/PageEditor.tsx

@@ -20,6 +20,7 @@ import {
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
+import { usePreviewRenderer } from '~/stores/renderer';
 import {
   EditorMode,
   useEditorMode, useIsMobile, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
@@ -99,6 +100,8 @@ const PageEditor = (props: Props): JSX.Element => {
   const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
+  const { data: growiRenderer } = usePreviewRenderer();
+
   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   const [markdown, setMarkdown] = useState<string>(pageContainer.state.markdown!);
 
@@ -389,6 +392,10 @@ const PageEditor = (props: Props): JSX.Element => {
     return <></>;
   }
 
+  if (growiRenderer == null) {
+    return <></>;
+  }
+
   const config = props.appContainer.getConfig();
   const isUploadable = config.upload.image || config.upload.file;
   const isUploadableFile = config.upload.file;
@@ -425,6 +432,7 @@ const PageEditor = (props: Props): JSX.Element => {
       <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
         <Preview
           markdown={markdown}
+          growiRenderer={growiRenderer}
           ref={previewRef}
           isMathJaxEnabled={isMathJaxEnabled}
           renderMathJaxOnInit={false}

+ 5 - 6
packages/app/src/components/PageEditor/Preview.tsx

@@ -5,6 +5,7 @@ import React, {
 
 import AppContainer from '~/client/services/AppContainer';
 import InterceptorManager from '~/services/interceptor-manager';
+import GrowiRenderer from '~/services/renderer/growi-renderer';
 import { useEditorSettings } from '~/stores/editor';
 
 import RevisionBody from '../Page/RevisionBody';
@@ -15,9 +16,9 @@ declare const interceptorManager: InterceptorManager;
 
 
 type Props = {
+  growiRenderer: GrowiRenderer,
   markdown?: string,
   pagePath?: string,
-  isMathJaxEnabled?: boolean,
   renderMathJaxOnInit?: boolean,
   onScroll?: (scrollTop: number) => void,
 }
@@ -27,7 +28,7 @@ type UnstatedProps = Props & { appContainer: AppContainer };
 const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivElement>): JSX.Element => {
 
   const {
-    appContainer,
+    growiRenderer,
     markdown, pagePath,
   } = props;
 
@@ -35,8 +36,6 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
 
   const { data: editorSettings } = useEditorSettings();
 
-  const growiRenderer = appContainer.getRenderer('editor');
-
   const context = useMemo(() => {
     return {
       markdown,
@@ -61,7 +60,7 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
     }
 
     setHtml(context.parsedHTML ?? '');
-  }, [interceptorManager, context, growiRenderer]);
+  }, [context, growiRenderer]);
 
   useEffect(() => {
     if (markdown == null) {
@@ -82,7 +81,7 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
         parsedHTML: html,
       });
     }
-  }, [context, html, interceptorManager]);
+  }, [context, html]);
 
   return (
     <div

+ 12 - 3
packages/app/src/components/PageTimeline.jsx

@@ -6,6 +6,8 @@ import { useTranslation } from 'next-i18next';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import GrowiRenderer from '~/services/renderer/growi-renderer';
+import { useTimelineRenderer } from '~/stores/renderer';
 
 import RevisionLoader from './Page/RevisionLoader';
 import PaginationWrapper from './PaginationWrapper';
@@ -48,9 +50,9 @@ class PageTimeline extends React.Component {
   }
 
   UNSAFE_componentWillMount() {
-    const { appContainer } = this.props;
+    const { growiRenderer } = this.props;
     // initialize GrowiRenderer
-    this.growiRenderer = appContainer.getRenderer('timeline');
+    this.growiRenderer = growiRenderer;
   }
 
   async componentDidMount() {
@@ -110,13 +112,20 @@ class PageTimeline extends React.Component {
 PageTimeline.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pages: PropTypes.arrayOf(PropTypes.object),
 };
 
 const PageTimelineWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <PageTimeline t={t} {...props} />;
+  const { data: growiRenderer } = useTimelineRenderer();
+
+  if (growiRenderer == null) {
+    return <></>;
+  }
+
+  return <PageTimeline t={t} growiRenderer={growiRenderer} {...props} />;
 };
 
 /**

+ 4 - 4
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -15,6 +15,7 @@ import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
 import { useDescendantsPageListForCurrentPathTermManager, usePageTreeTermManager } from '~/stores/page-listing';
+import { useSearchResultRenderer } from '~/stores/renderer';
 import { useFullTextSearchTermManager } from '~/stores/search';
 
 
@@ -119,8 +120,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
-  const growiRenderer = appContainer.getRenderer('searchresult');
-
+  const { data: growiRenderer } = useSearchResultRenderer();
 
   const duplicateItemClickedHandler = useCallback(async(pageToDuplicate) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -193,8 +193,8 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     );
   }, [page, showPageControlDropdown, forceHideMenuItems, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler]);
 
-  // return if page is null
-  if (page == null) return <></>;
+  // return if page or growiRenderer is null
+  if (page == null || growiRenderer == null) return <></>;
 
   return (
     <div key={page._id} data-testid="search-result-content" className="search-result-content grw-page-path-text-muted-container d-flex flex-column">

+ 1 - 1
packages/app/src/components/Sidebar.tsx

@@ -15,6 +15,7 @@ import {
 } from '~/stores/ui';
 
 import DrawerToggler from './Navbar/DrawerToggler';
+import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
 import { SidebarNav } from './Sidebar/SidebarNav';
 import { StickyStretchableScrollerProps } from './StickyStretchableScroller';
 
@@ -86,7 +87,6 @@ const SidebarContentsWrapper = () => {
 
 
 const Sidebar = (): JSX.Element => {
-  const NavigationResizeHexagon = dynamic(() => import('./Sidebar/NavigationResizeHexagon').then(mod => mod.NavigationResizeHexagon), { ssr: false });
 
   // const { data: isDrawerMode } = useDrawerMode(); Todo Universalize
   const isDrawerMode = false; // dummy

+ 6 - 1
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -7,6 +7,7 @@ import { useSWRxPageByPath } from '~/stores/page';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import RevisionRenderer from '../Page/RevisionRenderer';
 import { IRevision } from '~/interfaces/revision';
+import { useCustomSidebarRenderer } from '~/stores/renderer';
 
 const logger = loggerFactory('growi:cli:CustomSidebar');
 
@@ -29,10 +30,14 @@ const CustomSidebar: FC<Props> = (props: Props) => {
 
   const { appContainer } = props;
 
-  const renderer = appContainer.getRenderer('sidebar');
+  const { data: renderer } = useCustomSidebarRenderer();
 
   const { data: page, error, mutate } = useSWRxPageByPath('/Sidebar');
 
+  if (renderer == null) {
+    return <></>;
+  }
+
   const isLoading = page === undefined && error == null;
   const markdown = (page?.revision as IRevision | undefined)?.body;
 

+ 7 - 3
packages/app/src/components/Sidebar/SidebarNav.tsx

@@ -1,5 +1,7 @@
 import React, { FC, memo, useCallback } from 'react';
 
+import Link from 'next/link';
+
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentUser } from '~/stores/context';
@@ -60,9 +62,11 @@ const SecondaryItem: FC<SecondaryItemProps> = memo((props: SecondaryItemProps) =
   const { iconName, href, isBlank } = props;
 
   return (
-    <a href={href} className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
-      <i className="material-icons">{iconName}</i>
-    </a>
+    <Link href={href}>
+      <a className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
+        <i className="material-icons">{iconName}</i>
+      </a>
+    </Link>
   );
 });
 SecondaryItem.displayName = 'SecondaryItem';

+ 6 - 3
packages/app/src/components/TableOfContents.jsx

@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
 import PageContainer from '~/client/services/PageContainer';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { useGlobalEventEmitter } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
 
@@ -25,6 +26,8 @@ const TableOfContents = (props) => {
   const { pageUser } = pageContainer.state;
   const isUserPage = pageUser != null;
 
+  const { data: globalEmitter } = useGlobalEventEmitter();
+
   const [tocHtml, setTocHtml] = useState('');
 
   const calcViewHeight = useCallback(() => {
@@ -56,12 +59,12 @@ const TableOfContents = (props) => {
   // set handler to render ToC
   useEffect(() => {
     const handler = html => setTocHtml(html);
-    window.globalEmitter.on('renderTocHtml', handler);
+    globalEmitter.on('renderTocHtml', handler);
 
     return function cleanup() {
-      window.globalEmitter.removeListener('renderTocHtml', handler);
+      globalEmitter.removeListener('renderTocHtml', handler);
     };
-  }, []);
+  }, [globalEmitter]);
 
   return (
     <StickyStretchableScroller

+ 33 - 0
packages/app/src/interfaces/activity.ts

@@ -123,6 +123,17 @@ const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED = 'ADMIN_GLOBAL_NOTIFICA
 const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED';
 const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE';
 const ACTION_ADMIN_SLACK_WORKSPACE_CREATE = 'ADMIN_SLACK_WORKSPACE_CREATE';
+const ACTION_ADMIN_SLACK_WORKSPACE_DELETE = 'ADMIN_SLACK_WORKSPACE_DELETE';
+const ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE = 'ADMIN_SLACK_BOT_TYPE_UPDATE';
+const ACTION_ADMIN_SLACK_BOT_TYPE_DELETE = 'ADMIN_SLACK_BOT_TYPE_UPDATE';
+const ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE = 'ADMIN_SLACK_ACCESS_TOKEN_REGENERATE';
+const ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY = 'ADMIN_SLACK_MAKE_APP_PRIMARY';
+const ACTION_ADMIN_SLACK_PERMISSION_UPDATE = 'ADMIN_SLACK_PERMISSION_UPDATE';
+const ACTION_ADMIN_SLACK_PROXY_URI_UPDATE = 'ADMIN_SLACK_PROXY_URI_UPDATE';
+const ACTION_ADMIN_SLACK_RELATION_TEST = 'ADMIN_SLACK_RELATION_TEST';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE = 'ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE = 'ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST = 'ADMIN_SLACK_WITHOUT_PROXY_TEST';
 const ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE = 'ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE';
 const ACTION_ADMIN_USERS_INVITE = 'ADMIN_USERS_INVITE';
 const ACTION_ADMIN_USER_GROUP_CREATE = 'ADMIN_USER_GROUP_CREATE';
@@ -278,6 +289,17 @@ export const SupportedAction = {
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_SLACK_WORKSPACE_CREATE,
+  ACTION_ADMIN_SLACK_WORKSPACE_DELETE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_DELETE,
+  ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE,
+  ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY,
+  ACTION_ADMIN_SLACK_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_PROXY_URI_UPDATE,
+  ACTION_ADMIN_SLACK_RELATION_TEST,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST,
   ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE,
   ACTION_ADMIN_USERS_INVITE,
   ACTION_ADMIN_USER_GROUP_CREATE,
@@ -435,6 +457,17 @@ export const LargeActionGroup = {
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_SLACK_WORKSPACE_CREATE,
+  ACTION_ADMIN_SLACK_WORKSPACE_DELETE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_DELETE,
+  ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE,
+  ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY,
+  ACTION_ADMIN_SLACK_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_PROXY_URI_UPDATE,
+  ACTION_ADMIN_SLACK_RELATION_TEST,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST,
   ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE,
   ACTION_ADMIN_USERS_INVITE,
   ACTION_ADMIN_USER_GROUP_CREATE,

+ 4 - 3
packages/app/src/interfaces/global.ts

@@ -1,12 +1,13 @@
 import EventEmitter from 'events';
 
+import GrowiRenderer from '~/services/renderer/growi-renderer';
 import Xss from '~/services/xss';
 
 import { IGraphViewer } from './graph-viewer';
-import { IInterceptorManager } from './interceptor-manager';
 
 export type CustomWindow = Window
                          & typeof globalThis
-                         & { interceptorManager: IInterceptorManager }
                          & { globalEmitter: EventEmitter }
-                         & { GraphViewer: IGraphViewer };
+                         & { GraphViewer: IGraphViewer }
+                         & { growiRenderer: GrowiRenderer }
+                         & { previewRenderer: GrowiRenderer }; // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223

+ 22 - 0
packages/app/src/interfaces/services/renderer.ts

@@ -0,0 +1,22 @@
+import { XssOptionConfig } from '~/services/xss/xssOption';
+
+export type RendererSettings = {
+  isEnabledLinebreaks: boolean,
+  isEnabledLinebreaksInComments: boolean,
+  adminPreferredIndentSize: number,
+  isIndentSizeForced: boolean,
+};
+
+// export type GrowiHydratedEnv = {
+//   DRAWIO_URI: string | null,
+//   HACKMD_URI: string | null,
+//   NO_CDN: string | null,
+//   GROWI_CLOUD_URI: string | null,
+//   GROWI_APP_ID_FOR_GROWI_CLOUD: string | null,
+// }
+
+export type GrowiRendererConfig = {
+  highlightJsStyleBorder: boolean
+  plantumlUri: string | null,
+  blockdiagUri: string | null,
+} & XssOptionConfig;

+ 45 - 12
packages/app/src/pages/[[...path]].page.tsx

@@ -1,5 +1,7 @@
 import React, { useEffect } from 'react';
 
+import EventEmitter from 'events';
+
 import { isClient, pagePathUtils, pathUtils } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import {
@@ -18,12 +20,15 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 // import { useIndentSize } from '~/stores/editor';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
+import { CustomWindow } from '~/interfaces/global';
 import { IPageWithMeta } from '~/interfaces/page';
+import { GrowiRendererConfig, RendererSettings } from '~/interfaces/services/renderer';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { PageModel, PageDocument } from '~/server/models/page';
 import UserUISettings, { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import Xss from '~/services/xss';
 import { useSWRxCurrentPage, useSWRxPageInfo, useSWRxPage } from '~/stores/page';
+import { useRendererSettings } from '~/stores/renderer';
 import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
 } from '~/stores/ui';
@@ -44,11 +49,12 @@ import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 import {
   useCurrentUser, useCurrentPagePath,
   useOwnerOfCurrentPage, useIsLatestRevision,
-  useIsForbidden, useIsNotFound, useIsTrashPage, useShared, useShareLinkId, useIsSharedUser, useIsAbleToDeleteCompletely,
+  useIsForbidden, useIsNotFound, useIsNotCreatable, useIsTrashPage, useShared, useShareLinkId, useIsSharedUser, useIsAbleToDeleteCompletely,
   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup,
-  useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useMathJax,
-  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname, useIsSlackConfigured,
+  useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri,
+  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
+  useIsSlackConfigured, useGrowiRendererConfig, useIsBlinkedHeaderAtBoot,
 } from '../stores/context';
 import { useXss } from '../stores/xss';
 
@@ -59,7 +65,9 @@ import {
 
 
 const logger = loggerFactory('growi:pages:all');
-const { isPermalink: _isPermalink, isUsersHomePage, isTrashPage: _isTrashPage } = pagePathUtils;
+const {
+  isPermalink: _isPermalink, isUsersHomePage, isTrashPage: _isTrashPage, isCreatablePage,
+} = pagePathUtils;
 const { removeHeadingSlash } = pathUtils;
 
 
@@ -83,6 +91,7 @@ type Props = CommonProps & {
   isIdenticalPathPage?: boolean,
   isForbidden: boolean,
   isNotFound: boolean,
+  IsNotCreatable: boolean,
   // isAbleToDeleteCompletely: boolean,
 
   isSearchServiceConfigured: boolean,
@@ -95,7 +104,6 @@ type Props = CommonProps & {
   // hasSlackConfig: boolean,
   // drawioUri: string,
   // hackmdUri: string,
-  // mathJax: string,
   // noCdn: string,
   // highlightJsStyle: string,
   // isAllReplyShown: boolean,
@@ -107,6 +115,9 @@ type Props = CommonProps & {
   // adminPreferredIndentSize: number,
   // isIndentSizeForced: boolean,
 
+  rendererSettings: RendererSettings,
+  growiRendererConfig: GrowiRendererConfig,
+
   // UI
   userUISettings: UserUISettingsDocument | null
   // Sidebar
@@ -121,6 +132,11 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   const { data: currentUser } = useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
 
+  // register global EventEmitter
+  if (isClient()) {
+    (window as CustomWindow).globalEmitter = new EventEmitter();
+  }
+
   // commons
   useAppTitle(props.appTitle);
   useSiteUrl(props.siteUrl);
@@ -142,12 +158,14 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
   useIsForbidden(props.isForbidden);
   useIsNotFound(props.isNotFound);
+  useIsNotCreatable(props.IsNotCreatable);
   // useIsTrashPage(_isTrashPage(props.currentPagePath));
   // useShared();
   // useShareLinkId(props.shareLinkId);
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
   useIsSharedUser(false); // this page cann't be routed for '/share'
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
+  useIsBlinkedHeaderAtBoot(false);
 
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
@@ -159,16 +177,14 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useHasSlackConfig(props.hasSlackConfig);
   // useDrawioUri(props.drawioUri);
   // useHackmdUri(props.hackmdUri);
-  // useMathJax(props.mathJax);
   // useNoCdn(props.noCdn);
   // useIndentSize(props.adminPreferredIndentSize);
 
-  // useRendererSettings({
-  //   isEnabledLinebreaks: props.isEnabledLinebreaks,
-  //   isEnabledLinebreaksInComments: props.isEnabledLinebreaksInComments,
-  //   adminPreferredIndentSize: props.adminPreferredIndentSize,
-  //   isIndentSizeForced: props.isIndentSizeForced,
-  // });
+  useRendererSettings(props.rendererSettings);
+  useGrowiRendererConfig(props.growiRendererConfig);
+  // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
+  // useGrowiRendererConfig(props.growiRendererConfigStr != null ? JSON.parse(props.growiRendererConfigStr) : undefined);
+
 
   // const { data: editorMode } = useEditorMode();
 
@@ -343,6 +359,8 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
   else if (page == null) {
     props.isNotFound = true;
 
+    props.IsNotCreatable = !isCreatablePage(currentPathname);
+
     // check the page is forbidden or just does not exist.
     const count = isPermalink ? await Page.count({ _id: pageId }) : await Page.count({ path: currentPathname });
     props.isForbidden = count > 0;
@@ -411,6 +429,21 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   // props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
   // props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
 
+  props.rendererSettings = {
+    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+  };
+  props.growiRendererConfig = {
+    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+    attrWhiteList: crowi.xssService.getAttrWhiteList(),
+    tagWhiteList: crowi.xssService.getTagWhiteList(),
+    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    plantumlUri: process.env.PLANTUML_URI ?? null,
+    blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
+  };
+
   props.sidebarConfig = {
     isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
     isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),

+ 2 - 2
packages/app/src/server/middlewares/login-form-validator.ts

@@ -47,8 +47,8 @@ export const inviteValidation = (req, res, next) => {
 export const loginRules = () => {
   return [
     body('loginForm.username')
-      .matches(/^[\da-zA-Z\-_.@]+$/)
-      .withMessage('Username has invalid characters')
+      .matches(/^[\da-zA-Z\-_.+@]+$/)
+      .withMessage('Username or E-mail has invalid characters')
       .not()
       .isEmpty()
       .withMessage('Username field is required'),

+ 41 - 13
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -1,4 +1,3 @@
-import { SlackbotType, defaultSupportedSlackEventActions } from '@growi/slack';
 
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
@@ -6,6 +5,8 @@ import loggerFactory from '~/utils/logger';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
+import { SlackbotType, defaultSupportedSlackEventActions } from '@growi/slack';
+
 
 const {
   getConnectionStatus, getConnectionStatuses,
@@ -310,7 +311,8 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put botType setting.
    */
-  router.put('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, validator.botType, apiV3FormValidator, async(req, res) => {
+  // eslint-disable-next-line max-len
+  router.put('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, addActivity, validator.botType, apiV3FormValidator, async(req, res) => {
     const { currentBotType } = req.body;
 
     if (currentBotType == null) {
@@ -319,6 +321,8 @@ module.exports = (crowi) => {
 
     try {
       await handleBotTypeChanging(req, res, currentBotType);
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE });
     }
     catch (error) {
       const msg = 'Error occured in updating Custom bot setting';
@@ -345,9 +349,11 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to delete botType setting.
    */
-  router.delete('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, apiV3FormValidator, async(req, res) => {
+  router.delete('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, addActivity, apiV3FormValidator, async(req, res) => {
     try {
       await handleBotTypeChanging(req, res, null);
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_BOT_TYPE_DELETE });
     }
     catch (error) {
       const msg = 'Error occured in resetting all';
@@ -369,7 +375,7 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put CustomBotWithoutProxy setting.
    */
-  router.put('/without-proxy/update-settings', loginRequiredStrictly, adminRequired, async(req, res) => {
+  router.put('/without-proxy/update-settings', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Not CustomBotWithoutProxy';
@@ -384,6 +390,9 @@ module.exports = (crowi) => {
     try {
       await updateSlackBotSettings(requestParams);
       crowi.slackIntegrationService.publishUpdatedMessage();
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE });
+
       return res.apiv3();
     }
     catch (error) {
@@ -406,8 +415,8 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put CustomBotWithoutProxy permissions.
    */
-
-  router.put('/without-proxy/update-permissions', loginRequiredStrictly, adminRequired, validator.updatePermissionsWithoutProxy, async(req, res) => {
+  // eslint-disable-next-line max-len
+  router.put('/without-proxy/update-permissions', loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithoutProxy, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Not CustomBotWithoutProxy';
@@ -423,6 +432,9 @@ module.exports = (crowi) => {
     try {
       await updateSlackBotSettings(params);
       crowi.slackIntegrationService.publishUpdatedMessage();
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE });
+
       return res.apiv3();
     }
     catch (error) {
@@ -496,7 +508,7 @@ module.exports = (crowi) => {
    *          200:
    *            description: Succeeded to delete access tokens for slack
    */
-  router.delete('/slack-app-integrations/:id', validator.deleteIntegration, apiV3FormValidator, async(req, res) => {
+  router.delete('/slack-app-integrations/:id', validator.deleteIntegration, apiV3FormValidator, addActivity, async(req, res) => {
     const { id } = req.params;
 
     try {
@@ -508,6 +520,8 @@ module.exports = (crowi) => {
         await SlackAppIntegration.updateOne({}, { isPrimary: true });
       }
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WORKSPACE_DELETE });
+
       return res.apiv3({ response });
     }
     catch (error) {
@@ -517,7 +531,7 @@ module.exports = (crowi) => {
     }
   });
 
-  router.put('/proxy-uri', loginRequiredStrictly, adminRequired, validator.proxyUri, apiV3FormValidator, async(req, res) => {
+  router.put('/proxy-uri', loginRequiredStrictly, adminRequired, addActivity, validator.proxyUri, apiV3FormValidator, async(req, res) => {
     const { proxyUri } = req.body;
 
     const requestParams = { 'slackbot:proxyUri': proxyUri };
@@ -525,6 +539,9 @@ module.exports = (crowi) => {
     try {
       await updateSlackBotSettings(requestParams);
       crowi.slackIntegrationService.publishUpdatedMessage();
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_PROXY_URI_UPDATE });
+
       return res.apiv3({});
     }
     catch (error) {
@@ -549,7 +566,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to make it primary
    */
   // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/make-primary', loginRequiredStrictly, adminRequired, validator.makePrimary, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/make-primary', loginRequiredStrictly, adminRequired, addActivity, validator.makePrimary, apiV3FormValidator, async(req, res) => {
 
     const { id } = req.params;
 
@@ -571,6 +588,8 @@ module.exports = (crowi) => {
         },
       ]);
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY });
+
       return res.apiv3();
     }
     catch (error) {
@@ -594,7 +613,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to regenerate slack app tokens
    */
   // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/regenerate-tokens', loginRequiredStrictly, adminRequired, validator.regenerateTokens, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/regenerate-tokens', loginRequiredStrictly, adminRequired, addActivity, validator.regenerateTokens, apiV3FormValidator, async(req, res) => {
 
     const { id } = req.params;
 
@@ -602,6 +621,8 @@ module.exports = (crowi) => {
       const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
       const slackAppTokens = await SlackAppIntegration.findByIdAndUpdate(id, { tokenGtoP, tokenPtoG });
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE });
+
       return res.apiv3(slackAppTokens, 200);
     }
     catch (error) {
@@ -625,7 +646,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to update supported commands
    */
   // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/permissions', loginRequiredStrictly, adminRequired, validator.updatePermissionsWithProxy, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/permissions', loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithProxy, apiV3FormValidator, async(req, res) => {
     // TODO: look here 78975
     const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands, permissionsForSlackEventActions } = req.body;
     const { id } = req.params;
@@ -659,6 +680,8 @@ module.exports = (crowi) => {
         );
       }
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_PERMISSION_UPDATE });
+
       return res.apiv3({});
     }
     catch (error) {
@@ -682,7 +705,7 @@ module.exports = (crowi) => {
    *             description: Succeeded to delete botType setting.
    */
   // eslint-disable-next-line max-len
-  router.post('/slack-app-integrations/:id/relation-test', loginRequiredStrictly, adminRequired, validator.relationTest, apiV3FormValidator, async(req, res) => {
+  router.post('/slack-app-integrations/:id/relation-test', loginRequiredStrictly, adminRequired, addActivity, validator.relationTest, apiV3FormValidator, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType === SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Not Proxy Type';
@@ -732,6 +755,9 @@ module.exports = (crowi) => {
     catch (error) {
       return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
     }
+
+    activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_RELATION_TEST });
+
     return res.apiv3();
 
   });
@@ -756,7 +782,7 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to connect to slack work space.
    */
-  router.post('/without-proxy/test', loginRequiredStrictly, adminRequired, validator.slackChannel, apiV3FormValidator, async(req, res) => {
+  router.post('/without-proxy/test', loginRequiredStrictly, adminRequired, addActivity, validator.slackChannel, apiV3FormValidator, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Select Without Proxy Type';
@@ -778,6 +804,8 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
     }
 
+    activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST });
+
     return res.apiv3();
   });
 

+ 1 - 1
packages/app/src/server/routes/page.js

@@ -5,6 +5,7 @@ import urljoin from 'url-join';
 
 import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
+import XssOption from '~/services/xss/xssOption';
 import loggerFactory from '~/utils/logger';
 
 import { PathAlreadyExistsError } from '../models/errors';
@@ -160,7 +161,6 @@ module.exports = function(crowi, app) {
 
   const activityEvent = crowi.event('activity');
 
-  const XssOption = require('~/services/xss/xssOption');
   const Xss = require('~/services/xss/index');
   const initializedConfig = {
     isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),

+ 0 - 6
packages/app/src/server/service/config-loader.ts

@@ -73,12 +73,6 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.STRING,
     default: null,
   },
-  MATHJAX: {
-    ns:      'crowi',
-    key:     'app:mathJax',
-    type:    ValueType.STRING,
-    default: null,
-  },
   NO_CDN: {
     ns:      'crowi',
     key:     'app:noCdn',

+ 0 - 0
packages/app/src/client/util/PostProcessor/.keep → packages/app/src/services/renderer/PostProcessor/.keep


+ 0 - 0
packages/app/src/client/util/PreProcessor/CsvToTable.js → packages/app/src/services/renderer/PreProcessor/CsvToTable.js


+ 0 - 0
packages/app/src/client/util/PreProcessor/EasyGrid.js → packages/app/src/services/renderer/PreProcessor/EasyGrid.js


+ 0 - 0
packages/app/src/client/util/PreProcessor/Linker.js → packages/app/src/services/renderer/PreProcessor/Linker.js


+ 29 - 0
packages/app/src/services/renderer/PreProcessor/XssFilter.ts

@@ -0,0 +1,29 @@
+import Xss from '~/services/xss';
+import XssOption, { XssOptionConfig } from '~/services/xss/xssOption';
+
+export default class XssFilter {
+
+  xssOption: XssOption
+
+  xss
+
+  config: XssOptionConfig
+
+  constructor(config: XssOptionConfig) {
+    this.config = config;
+
+    if (config.isEnabledXssPrevention) {
+      this.xssOption = new XssOption(config);
+      this.xss = new Xss(this.xssOption);
+    }
+  }
+
+  process(markdown) {
+    if (this.config.isEnabledXssPrevention) {
+      return this.xss.process(markdown);
+    }
+
+    return markdown;
+  }
+
+}

+ 305 - 0
packages/app/src/services/renderer/growi-renderer.ts

@@ -0,0 +1,305 @@
+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 { 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';
+
+
+const logger = loggerFactory('growi:util:GrowiRenderer');
+
+// 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
+}
+
+const generateCommonOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  return {
+    remarkPlugins: [gfm],
+    rehypePlugins: [slug],
+  };
+};
+
+export const generateViewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
+
+  const { remarkPlugins, rehypePlugins } = options;
+
+  // add remark plugins
+  remarkPlugins?.push(footnotes);
+  remarkPlugins?.push(emoji);
+  if (rendererSettings.isEnabledLinebreaks) {
+    remarkPlugins?.push(breaks);
+  }
+  // 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 generatePreviewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
+
+  // // Add configurers for preview
+  // renderer.addConfigurers([
+  //   new FooternoteConfigurer(),
+  //   new HeaderLineNumberConfigurer(),
+  //   new TableConfigurer(),
+  // ]);
+
+  // renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
+  // renderer.configure();
+
+  return options;
+};
+
+export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
+
+  // renderer.addConfigurers([
+  //   new TableConfigurer(),
+  // ]);
+
+  // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaksInComments });
+  // renderer.configure();
+
+  return options;
+};
+
+export const generateOthersOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
+
+  // renderer.addConfigurers([
+  //   new TableConfigurer(),
+  // ]);
+
+  // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
+  // renderer.configure();
+
+  return options;
+};

+ 0 - 0
packages/app/src/client/util/interceptor/detach-code-blocks.js → packages/app/src/services/renderer/interceptor/detach-code-blocks.js


+ 0 - 0
packages/app/src/client/util/interceptor/drawio-interceptor.js → packages/app/src/services/renderer/interceptor/drawio-interceptor.js


+ 5 - 4
packages/app/src/client/util/markdown-it/blockdiag.js → packages/app/src/services/renderer/markdown-it/blockdiag.ts

@@ -1,10 +1,11 @@
+import { GrowiRendererConfig } from '~/interfaces/services/renderer';
+
 export default class BlockdiagConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-    const config = crowi.getConfig();
+  generateSourceUrl: string;
 
-    this.generateSourceUrl = config.env.BLOCKDIAG_URI || 'https://blockdiag-api.com/';
+  constructor(growiConfig: GrowiRendererConfig) {
+    this.generateSourceUrl = growiConfig.blockdiagUri || 'https://blockdiag-api.com/';
   }
 
   configure(md) {

+ 0 - 0
packages/app/src/client/util/markdown-it/drawio-viewer.js → packages/app/src/services/renderer/markdown-it/drawio-viewer.js


+ 0 - 0
packages/app/src/client/util/markdown-it/emoji-mart-data.ts → packages/app/src/services/renderer/markdown-it/emoji-mart-data.ts


+ 0 - 0
packages/app/src/client/util/markdown-it/emoji.js → packages/app/src/services/renderer/markdown-it/emoji.js


+ 0 - 0
packages/app/src/client/util/markdown-it/footernote.js → packages/app/src/services/renderer/markdown-it/footernote.js


+ 0 - 0
packages/app/src/client/util/markdown-it/header-line-number.js → packages/app/src/services/renderer/markdown-it/header-line-number.js


+ 0 - 0
packages/app/src/client/util/markdown-it/header-with-edit-link.js → packages/app/src/services/renderer/markdown-it/header-with-edit-link.js


+ 0 - 0
packages/app/src/client/util/markdown-it/header.js → packages/app/src/services/renderer/markdown-it/header.js


+ 5 - 9
packages/app/src/client/util/markdown-it/link-by-relative-path.ts → packages/app/src/services/renderer/markdown-it/link-by-relative-path.ts

@@ -5,18 +5,14 @@ const PATTERN_RELATIVE_PATH = new RegExp(/^(\.{1,2})(\/.*)?$/);
 
 export default class LinkerByRelativePathConfigurer {
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  appContainer: any;
+  pagePath: string
 
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(appContainer) {
-    this.appContainer = appContainer;
+  constructor(pagePath: string) {
+    this.pagePath = pagePath;
   }
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  configure(md): void {
-    const pageContainer = this.appContainer.getContainer('PageContainer');
-
+  configure(md, pagePath: string): void {
     // Remember old renderer, if overridden, or proxy to default renderer
     const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
       return self.renderToken(tokens, idx, options);
@@ -32,7 +28,7 @@ export default class LinkerByRelativePathConfigurer {
 
       if (hrefIndex != null && hrefIndex >= 0) {
         const href: string = tokens[idx].attrs[hrefIndex][1];
-        const currentPath: string | null = pageContainer?.state.path;
+        const currentPath: string | null = pagePath;
 
         // resolve relative path and replace
         if (PATTERN_RELATIVE_PATH.test(href) && currentPath != null) {

+ 7 - 0
packages/app/src/services/renderer/markdown-it/mathjax.js

@@ -0,0 +1,7 @@
+export default class MathJaxConfigurer {
+
+  configure(md) {
+    md.use(require('markdown-it-mathjax')());
+  }
+
+}

+ 5 - 4
packages/app/src/client/util/markdown-it/plantuml.js → packages/app/src/services/renderer/markdown-it/plantuml.ts

@@ -1,14 +1,15 @@
 import plantumlEncoder from 'plantuml-encoder';
 import urljoin from 'url-join';
 
+import { GrowiRendererConfig } from '~/interfaces/services/renderer';
+
 export default class PlantUMLConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-    const config = crowi.getConfig();
+  serverUrl: string;
 
+  constructor(growiConfig: GrowiRendererConfig) {
     // Do NOT use HTTPS URL because plantuml.com refuse request except from members
-    this.serverUrl = config.env.PLANTUML_URI || 'http://plantuml.com/plantuml';
+    this.serverUrl = growiConfig.plantumlUri || 'http://plantuml.com/plantuml';
 
     this.generateSource = this.generateSource.bind(this);
   }

+ 0 - 0
packages/app/src/client/util/markdown-it/table-with-handsontable-button.js → packages/app/src/services/renderer/markdown-it/table-with-handsontable-button.js


+ 0 - 0
packages/app/src/client/util/markdown-it/table.js → packages/app/src/services/renderer/markdown-it/table.js


+ 0 - 4
packages/app/src/client/util/markdown-it/task-lists.js → packages/app/src/services/renderer/markdown-it/task-lists.js

@@ -1,9 +1,5 @@
 export default class TaskListsConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
   configure(md) {
     md.use(require('markdown-it-task-checkbox'), {
       disabled: true,

+ 0 - 0
packages/app/src/client/util/markdown-it/toc-and-anchor.js → packages/app/src/services/renderer/markdown-it/toc-and-anchor.js


+ 0 - 13
packages/app/src/services/xss/xssOption.js

@@ -1,13 +0,0 @@
-class XssOption {
-
-  constructor(config) {
-    const recommendedWhitelist = require('~/services/xss/recommended-whitelist');
-    const initializedConfig = (config != null) ? config : {};
-
-    this.isEnabledXssPrevention = initializedConfig.isEnabledXssPrevention || true;
-    this.tagWhiteList = initializedConfig.tagWhiteList || recommendedWhitelist.tags;
-    this.attrWhiteList = initializedConfig.attrWhiteList || recommendedWhitelist.attrs;
-  }
-
-}
-module.exports = XssOption;

+ 24 - 0
packages/app/src/services/xss/xssOption.ts

@@ -0,0 +1,24 @@
+export type XssOptionConfig = {
+  isEnabledXssPrevention: boolean,
+  tagWhiteList: any[],
+  attrWhiteList: any[],
+}
+
+export default class XssOption {
+
+  isEnabledXssPrevention: boolean
+
+  tagWhiteList: any[]
+
+  attrWhiteList: any[]
+
+  constructor(config: XssOptionConfig) {
+    const recommendedWhitelist = require('~/services/xss/recommended-whitelist');
+    const initializedConfig: Partial<XssOptionConfig> = (config != null) ? config : {};
+
+    this.isEnabledXssPrevention = initializedConfig.isEnabledXssPrevention || true;
+    this.tagWhiteList = initializedConfig.tagWhiteList || recommendedWhitelist.tags;
+    this.attrWhiteList = initializedConfig.attrWhiteList || recommendedWhitelist.attrs;
+  }
+
+}

+ 22 - 1
packages/app/src/stores/context.tsx

@@ -1,9 +1,13 @@
-import { pagePathUtils } from '@growi/core';
+import EventEmitter from 'events';
+
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 
 import { SupportedActionType } from '~/interfaces/activity';
+import { CustomWindow } from '~/interfaces/global';
+import { GrowiRendererConfig } from '~/interfaces/services/renderer';
+import InterceptorManager from '~/services/interceptor-manager';
 
 import { TargetAndAncestors } from '../interfaces/page-listing-results';
 import { IUser } from '../interfaces/user';
@@ -14,6 +18,14 @@ import { useStaticSWR } from './use-static-swr';
 type Nullable<T> = T | null;
 
 
+export const useGlobalEventEmitter = (): SWRResponse<EventEmitter, Error> => {
+  return useStaticSWR<EventEmitter, Error>('globalEventEmitter', undefined, { fallbackData: (window as CustomWindow).globalEmitter });
+};
+
+export const useInterceptorManager = (): SWRResponse<InterceptorManager, Error> => {
+  return useStaticSWR<InterceptorManager, Error>('interceptorManager', undefined, { fallbackData: new InterceptorManager() });
+};
+
 export const useCsrfToken = (initialData?: string): SWRResponse<string, Error> => {
   return useStaticSWR<string, Error>('csrfToken', initialData);
 };
@@ -198,6 +210,14 @@ export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean,
   return useStaticSWR('isLatestRevision', initialData);
 };
 
+export const useGrowiRendererConfig = (initialData?: GrowiRendererConfig): SWRResponse<GrowiRendererConfig, any> => {
+  return useStaticSWR('growiRendererConfig', initialData);
+};
+
+export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isBlinkedAtBoot', initialData);
+};
+
 
 /** **********************************************************
  *                     Computed contexts
@@ -209,6 +229,7 @@ export const useIsGuestUser = (): SWRResponse<boolean, Error> => {
   return useSWRImmutable(
     ['isGuestUser', currentUser],
     (key: Key, currentUser: IUser) => currentUser == null,
+    { fallbackData: currentUser == null },
   );
 };
 

+ 10 - 3
packages/app/src/stores/page.tsx

@@ -14,11 +14,10 @@ import { IPageTagsInfo } from '../interfaces/tag';
 
 import { useCurrentPageId } from './context';
 
-export const useSWRxPage = (pageId?: string, shareLinkId?: string, initialData?: IPageHasId): SWRResponse<IPageHasId, Error> => {
+export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResponse<IPageHasId, Error> => {
   return useSWR<IPageHasId, Error>(
     pageId != null ? ['/page', pageId, shareLinkId] : null,
     (endpoint, pageId, shareLinkId) => apiv3Get(endpoint, { pageId, shareLinkId }).then(result => result.data.page),
-    { fallbackData: initialData },
   );
 };
 
@@ -32,7 +31,15 @@ export const useSWRxPageByPath = (path?: string): SWRResponse<IPageHasId, Error>
 export const useSWRxCurrentPage = (shareLinkId?: string, initialData?: IPageHasId): SWRResponse<IPageHasId, Error> => {
   const { data: currentPageId } = useCurrentPageId();
 
-  return useSWRxPage(currentPageId ?? undefined, shareLinkId, initialData);
+  const swrResult = useSWRxPage(currentPageId, shareLinkId);
+
+  // use mutate because fallbackData does not work
+  // see: https://github.com/weseek/growi/commit/5038473e8d6028c9c91310e374a7b5f48b921a15
+  if (initialData != null) {
+    swrResult.mutate(initialData);
+  }
+
+  return swrResult;
 };
 
 

+ 78 - 0
packages/app/src/stores/renderer.tsx

@@ -0,0 +1,78 @@
+import { Key, SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
+
+import { RendererSettings } from '~/interfaces/services/renderer';
+import {
+  ReactMarkdownOptionsGenerator, RendererOptions,
+  generateViewOptions, generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
+} from '~/services/renderer/growi-renderer';
+import { useStaticSWR } from '~/stores/use-static-swr';
+
+import { useCurrentPagePath, useGrowiRendererConfig } from './context';
+
+export const useRendererSettings = (initialData?: RendererSettings): SWRResponse<RendererSettings, Error> => {
+  return useStaticSWR('rendererSettings', initialData);
+};
+
+// The base hook with common processes
+const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGenerator): SWRResponse<RendererOptions, Error> => {
+  const { data: rendererSettings } = useRendererSettings();
+  const { data: growiRendererConfig } = useGrowiRendererConfig();
+
+  const isAllDataValid = rendererSettings != null && growiRendererConfig != null;
+
+  const key = isAllDataValid
+    ? [rendererId, rendererSettings, growiRendererConfig]
+    : null;
+
+  const swrResult = useSWRImmutable<RendererOptions, Error>(key);
+
+  if (isAllDataValid && swrResult.data == null) {
+    swrResult.mutate(generator(growiRendererConfig, rendererSettings));
+  }
+
+  // call useSWRImmutable again to foce to update cache
+  return useSWRImmutable<RendererOptions, Error>(key);
+};
+
+export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'viewOptions';
+
+  return _useOptionsBase(key, generateViewOptions);
+};
+
+export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'previewOptions';
+
+  return _useOptionsBase(key, generatePreviewOptions);
+};
+
+export const useCommentPreviewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'commentPreviewOptions';
+
+  return _useOptionsBase(key, generateCommentPreviewOptions);
+};
+
+export const useSearchResultOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'searchResultOptions';
+
+  return _useOptionsBase(key, generateOthersOptions);
+};
+
+export const useTimelineOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'timelineOptions';
+
+  return _useOptionsBase(key, generateOthersOptions);
+};
+
+export const useDraftOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'draftOptions';
+
+  return _useOptionsBase(key, generateOthersOptions);
+};
+
+export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key: Key = 'customSidebarOptions';
+
+  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;
+};

+ 118 - 1
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -71,7 +71,7 @@ context('Modal for page operation', () => {
 
     cy.get('#grw-subnav-container').within(() => {
       cy.getByTestid('open-page-item-control-btn').click();
-      cy.getByTestid('open-page-move-rename-modal-btn').click();
+      cy.getByTestid('open-page-move-rename-modal-btn').click({force: true});
     });
 
     cy.getByTestid('page-rename-modal').should('be.visible').screenshot(`${ssPrefix}-rename-bootstrap4`);
@@ -141,3 +141,120 @@ context('Page Accessories Modal', () => {
   });
 
 });
+
+context('Tag Oprations', () =>{
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+  });
+
+  it('Successfully add new tag', () => {
+    const ssPrefix = 'tag-operations-add-new-tag-'
+    const tag = 'we';
+    cy.visit('/');
+
+    cy.get('#edit-tags-btn-wrapper-for-tooltip > a').click({force: true});
+    cy.get('#edit-tag-modal').should('be.visible').screenshot(`${ssPrefix}1-edit-tag-input`);
+
+    cy.get('#edit-tag-modal').within(() => {
+      cy.get('.rbt-input-main').type(tag, {force: true});
+      cy.get('#tag-typeahead-asynctypeahead').should('be.visible');
+      cy.get('#tag-typeahead-asynctypeahead-item-0').should('be.visible');
+      cy.screenshot(`${ssPrefix}2-type-tag-name`);
+    });
+
+    cy.get('#edit-tag-modal').within(() => {
+      cy.get('#tag-typeahead-asynctypeahead').should('be.visible');
+      cy.get('#tag-typeahead-asynctypeahead-item-0').should('be.visible');
+      cy.get('a#tag-typeahead-asynctypeahead-item-0').click({force: true})
+      cy.screenshot(`${ssPrefix}3-insert-tag-name`, {capture: 'viewport'});
+    });
+
+    cy.get('#edit-tag-modal').within(() => {
+      cy.get('div.modal-footer > button').click();
+    });
+
+    cy.get('.grw-taglabels-container > form > a').contains(tag).should('exist');
+
+    cy.screenshot(`${ssPrefix}4-click-done`, {capture: 'viewport'});
+
+  });
+
+  it('Successfully duplicate page by generated tag', () => {
+    const ssPrefix = 'tag-operations-page-duplicate-';
+    const tag = 'we';
+    const newPageName = 'our';
+    cy.visit('/');
+    cy.get('.grw-taglabels-container > form > a').contains(tag).click();
+    cy.getByTestid('search-result-base').should('be.visible');
+    cy.getByTestid('search-result-list').should('be.visible');
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.screenshot(`${ssPrefix}1-click-tag-name`, {capture: 'viewport'});
+
+    cy.getByTestid('open-page-item-control-btn').first().click({force: true});
+    cy.screenshot(`${ssPrefix}2-click-three-dots-menu`, {capture: 'viewport'});
+
+    cy.getByTestid('open-page-duplicate-modal-btn').first().click({force: true});
+    cy.getByTestid('page-duplicate-modal').should('be.visible');
+    cy.getByTestid('page-duplicate-modal').within(() => {
+      cy.get('.rbt-input-main').type(newPageName, {force: true});
+    }).screenshot(`${ssPrefix}3-duplicate-page`, {capture: 'viewport'});
+
+    cy.getByTestid('page-duplicate-modal').within(() => {
+      cy.get('.modal-footer > button.btn').click();
+    });
+    cy.visit(`/${newPageName}`);
+    cy.get('#wiki').should('not.be.empty');
+    cy.screenshot(`${ssPrefix}4-duplicated-page`, {capture: 'viewport'});
+  });
+
+  it('Successfully rename page from generated tag', () => {
+    const ssPrefix = 'tag-operations-page-rename-';
+    const tag = 'we';
+    const oldPageName = '/our';
+    const newPageName = '/ourus';
+
+    cy.visit('/');
+    cy.get('.grw-taglabels-container > form > a').contains(tag).click();
+    cy.getByTestid('search-result-base').should('be.visible');
+    cy.getByTestid('search-result-list').should('be.visible');
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.screenshot(`${ssPrefix}1-click-tag-name`, {capture: 'viewport'});
+
+    cy.getByTestid('search-result-list').within(() => {
+      cy.get('.list-group-item').each(($row) => {
+        if($row.find('a').text() === oldPageName){
+          cy.wrap($row).within(() => {
+            cy.getByTestid('open-page-item-control-btn').click();
+          });
+        }
+      });
+    });
+    cy.screenshot(`${ssPrefix}2-click-three-dots-menu`, {capture: 'viewport'});
+
+    cy.getByTestid('search-result-list').within(() => {
+      cy.get('.list-group-item').each(($row) => {
+        if($row.find('a').text() === oldPageName){
+          cy.wrap($row).within(() => {
+            cy.getByTestid('open-page-move-rename-modal-btn').click();
+          });
+        }
+      });
+    });
+
+    cy.getByTestid('page-rename-modal').should('be.visible').within(() => {
+      cy.get('.rbt-input-main').clear().type(newPageName,{force: true});
+    }).screenshot(`${ssPrefix}3-insert-new-page-name`);
+
+    cy.getByTestid('page-rename-modal').should('be.visible').within(() => {
+      cy.get('.modal-footer > button').click();
+    });
+
+    cy.visit(`/${newPageName}`);
+    cy.screenshot(`${ssPrefix}4-new-page-name-applied`, {capture: 'viewport'});
+  });
+
+});

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 2 - 2
packages/plugin-pukiwiki-like-linker/src/client-entry.js

@@ -1,6 +1,6 @@
 import PukiwikiLikeLinker from './resource/js/util/PreProcessor/PukiwikiLikeLinker';
 
-export default (appContainer) => {
+export default () => {
   // add preprocessor to head of array
-  appContainer.originRenderer.preProcessors.unshift(new PukiwikiLikeLinker());
+  window.growiRenderer.preProcessors.unshift(new PukiwikiLikeLinker());
 };

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "5.0.12-slackbot-proxy.0",
+  "version": "5.1.0-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.0.12-RC.0",
+    "@growi/slack": "^5.1.0-RC.2",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

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


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