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

Merge pull request #4325 from weseek/master

Release v4.4.4
Yuki Takei 4 лет назад
Родитель
Сommit
37bd3c9241
74 измененных файлов с 1719 добавлено и 143 удалено
  1. 17 0
      .github/dependabot.yml
  2. 2 5
      .github/release-drafter.yml
  3. 1 1
      .github/workflows/draft-release.yml
  4. 1 1
      .github/workflows/pr-to-master.yml
  5. 1 1
      .github/workflows/release-rc.yml
  6. 1 1
      .github/workflows/release-slackbot-proxy.yml
  7. 2 2
      .github/workflows/release.yml
  8. 3 3
      CHANGELOG.md
  9. 22 0
      SECURITY.md
  10. 1 1
      lerna.json
  11. 1 1
      package.json
  12. 1 0
      packages/app/.gitignore
  13. 13 1
      packages/app/bin/cdn/cdn-resources-downloader.ts
  14. 1 0
      packages/app/config/cdn.js
  15. 2 0
      packages/app/docker/Dockerfile
  16. 11 9
      packages/app/package.json
  17. 86 0
      packages/app/resource/cdn-manifests.js
  18. 4 1
      packages/app/resource/locales/en_US/admin/admin.json
  19. 37 0
      packages/app/resource/locales/en_US/translation.json
  20. 4 1
      packages/app/resource/locales/ja_JP/admin/admin.json
  21. 36 0
      packages/app/resource/locales/ja_JP/translation.json
  22. 4 1
      packages/app/resource/locales/zh_CN/admin/admin.json
  23. 37 1
      packages/app/resource/locales/zh_CN/translation.json
  24. 7 7
      packages/app/src/client/legacy/crowi.js
  25. 11 0
      packages/app/src/client/services/AdminCustomizeContainer.js
  26. 23 0
      packages/app/src/client/services/EditorContainer.js
  27. 15 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx
  28. 3 0
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  29. 3 0
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  30. 286 0
      packages/app/src/components/Me/EditorSettings.tsx
  31. 7 0
      packages/app/src/components/Me/PersonalSettings.jsx
  32. 3 1
      packages/app/src/components/Navbar/GlobalSearch.jsx
  33. 1 1
      packages/app/src/components/Page/RevisionRenderer.jsx
  34. 42 4
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  35. 8 1
      packages/app/src/components/PageEditor/Editor.jsx
  36. 41 0
      packages/app/src/components/PageEditor/OptionsSelector.jsx
  37. 2 0
      packages/app/src/server/models/config.ts
  38. 41 0
      packages/app/src/server/models/editor-settings.ts
  39. 1 0
      packages/app/src/server/models/user.js
  40. 6 0
      packages/app/src/server/routes/apiv3/customize-setting.js
  41. 92 1
      packages/app/src/server/routes/apiv3/personal-setting.js
  42. 2 6
      packages/app/src/server/routes/apiv3/users.js
  43. 4 4
      packages/app/src/services/cdn-resources-service.js
  44. 3 2
      packages/app/src/styles/_mixins.scss
  45. 6 0
      packages/app/src/styles/_override-codemirror.scss
  46. 1 1
      packages/app/src/styles/_variables.scss
  47. 15 3
      packages/app/src/styles/theme/_apply-colors.scss
  48. 2 1
      packages/app/src/styles/theme/antarctic.scss
  49. 2 1
      packages/app/src/styles/theme/christmas.scss
  50. 4 2
      packages/app/src/styles/theme/default.scss
  51. 2 1
      packages/app/src/styles/theme/future.scss
  52. 2 1
      packages/app/src/styles/theme/halloween.scss
  53. 4 2
      packages/app/src/styles/theme/hufflepuff.scss
  54. 2 1
      packages/app/src/styles/theme/island.scss
  55. 3 1
      packages/app/src/styles/theme/kibela.scss
  56. 4 2
      packages/app/src/styles/theme/mono-blue.scss
  57. 2 1
      packages/app/src/styles/theme/nature.scss
  58. 2 1
      packages/app/src/styles/theme/spring.scss
  59. 2 1
      packages/app/src/styles/theme/wood.scss
  60. 1 0
      packages/codemirror-textlint/.gitignore
  61. 50 0
      packages/codemirror-textlint/package.json
  62. 151 0
      packages/codemirror-textlint/src/index.ts
  63. 9 0
      packages/codemirror-textlint/src/utils/logger/index.ts
  64. 8 0
      packages/codemirror-textlint/tsconfig.base.json
  65. 17 0
      packages/codemirror-textlint/tsconfig.build.json
  66. 6 0
      packages/codemirror-textlint/tsconfig.json
  67. 1 1
      packages/core/package.json
  68. 1 1
      packages/plugin-attachment-refs/package.json
  69. 1 1
      packages/plugin-lsx/package.json
  70. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  71. 1 1
      packages/slack/package.json
  72. 2 2
      packages/slackbot-proxy/package.json
  73. 1 1
      packages/ui/package.json
  74. 527 59
      yarn.lock

+ 17 - 0
.github/dependabot.yml

@@ -0,0 +1,17 @@
+version: 2
+updates:
+  - package-ecosystem: github-actions
+    directory: '/'
+    schedule:
+      interval: daily
+    commit-message:
+      prefix: ci
+      include: scope
+
+  - package-ecosystem: npm
+    directory: '/'
+    schedule:
+      interval: daily
+    commit-message:
+      prefix: ci
+      include: scope

+ 2 - 5
.github/release-drafter.yml

@@ -33,14 +33,11 @@ autolabeler:
     branch:
       - '/^support\/.+/'
     title:
+      - '/^support/i'
+      - '/^chore/i'
       - '/^ci/i'
       - '/^docs/i'
       - '/^test/i'
-  - label: 'exclude from changelog'
-    branch:
-      - '/^chore\/.+/'
-    title:
-      - '/^chore/i'
 include-labels:
   - breaking
   - feature

+ 1 - 1
.github/workflows/draft-release.yml

@@ -19,7 +19,7 @@ jobs:
       - uses: actions/checkout@v2
 
       - name: Retrieve information from package.json
-        uses: myrotvorets/info-from-package-json-action@0.0.2
+        uses: myrotvorets/info-from-package-json-action@1.1.0
         id: package-json
 
       # Drafts your next Release notes as Pull Requests are merged into "master"

+ 1 - 1
.github/workflows/pr-to-master.yml

@@ -27,7 +27,7 @@ jobs:
 
     if: |
       (!contains( github.event.pull_request.labels.*.name, 'exclude from changelog' ) &&
-        !startsWith( github.ref, 'refs/heads/chore/' ))
+        !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:
       - uses: amannn/action-semantic-pull-request@v3.4.2

+ 1 - 1
.github/workflows/release-rc.yml

@@ -15,7 +15,7 @@ jobs:
     - uses: actions/checkout@v2
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
 
     - name: Docker meta

+ 1 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -17,7 +17,7 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
 
     - name: Docker meta

+ 2 - 2
.github/workflows/release.yml

@@ -38,7 +38,7 @@ jobs:
         sh ./packages/app/bin/github-actions/update-readme.sh
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
 
     - name: Update Changelog
@@ -98,7 +98,7 @@ jobs:
         node ./bin/github-actions/bump-versions -i prerelease
 
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@0.0.2
+      uses: myrotvorets/info-from-package-json-action@1.1.0
       id: package-json
 
     - name: Commit

+ 3 - 3
CHANGELOG.md

@@ -1441,7 +1441,7 @@ Upgrading Guide: [https://docs.growi.org/en/admin-guide/upgrading/34x.html](http
 
 - Improvement: Ensure to prevent suspending own account
 - Fix: Ensure to be able to use `.` for username when invited
-- Fix: monospace font for `<code></code>`
+- Fix: monospace font for `<code></code>`
 
 ## v2.1.1
 
@@ -1509,8 +1509,8 @@ Upgrading Guide: [https://docs.growi.org/en/admin-guide/upgrading/34x.html](http
 
 ## v1.2.14
 
-- Fix: Tabs(`a[data-toggle=&amp;amp;quot;tab&amp;amp;quot;][href=&amp;amp;quot;#...&amp;amp;quot;]`) push browser history twice
-- Fix: `a[href=&amp;amp;quot;#edit-form&amp;amp;quot;]` still save history even when disabling pushing states option
+- Fix: Tabs(`a[data-toggle="tab"][href="#..."]`) push browser history twice
+- Fix: `a[href="#edit-form"]` still save history even when disabling pushing states option
 
 ## v1.2.13
 

+ 22 - 0
SECURITY.md

@@ -0,0 +1,22 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 4.4.x   | :white_check_mark: |
+| < 4.3   | :x:                |
+
+## Reporting Security Issues
+
+**please do not report security vulnerabilities through public GitHub issues.**
+
+If you believe you have found a security vulnerability in any GROWI related repository, please report it to us using one of the methods described below.
+
+  * [Join our Slack team](https://growi-slackin.weseek.co.jp/) and send DM to `@yuki` who is the lead developer
+  * Report to JPCERT/CC ([en](https://www.jpcert.or.jp/english/ir/form.html)/[ja](https://www.jpcert.or.jp/form/))
+
+## Preferred Languages
+
+Communication in English and Japanese is possible.  
+In Japanese, we can reply more quickly. 

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 1 - 0
packages/app/.gitignore

@@ -6,6 +6,7 @@
 /dist/
 /transpiled/
 /public/static/js
+/public/static/dict
 /public/static/styles
 /public/uploads
 /tmp/

+ 13 - 1
packages/app/bin/cdn/cdn-resources-downloader.ts

@@ -4,7 +4,9 @@ import urljoin from 'url-join';
 import { Transform } from 'stream';
 import replaceStream from 'replacestream';
 
-import { cdnLocalScriptRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot } from '^/config/cdn';
+import {
+  cdnLocalScriptRoot, cdnLocalDictRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot,
+} from '^/config/cdn';
 import * as cdnManifests from '^/resource/cdn-manifests';
 
 import { CdnResource, CdnManifest } from '~/interfaces/cdn';
@@ -19,6 +21,15 @@ export default class CdnResourcesDownloader {
     const cdnScriptResources: CdnResource[] = cdnManifests.js.map((manifest: CdnManifest) => {
       return { manifest, outDir: cdnLocalScriptRoot };
     });
+
+    const cdnDictResources: CdnResource[] = cdnManifests.dict.map((manifest: CdnManifest) => {
+      return { manifest, outDir: cdnLocalDictRoot };
+    });
+
+    const dictExtensionOptions = {
+      ext: 'gz',
+    };
+
     const cdnStyleResources: CdnResource[] = cdnManifests.style.map((manifest) => {
       return { manifest, outDir: cdnLocalStyleRoot };
     });
@@ -31,6 +42,7 @@ export default class CdnResourcesDownloader {
 
     return Promise.all([
       this.downloadScripts(cdnScriptResources),
+      this.downloadScripts(cdnDictResources, dictExtensionOptions),
       this.downloadStyles(cdnStyleResources, dlStylesOptions),
     ]);
   }

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

@@ -4,5 +4,6 @@ import { projectRoot } from '~/utils/project-dir-utils';
 
 export const cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
 export const cdnLocalScriptWebRoot = '/static/js/cdn';
+export const cdnLocalDictRoot = path.join(projectRoot, 'public/static/dict/cdn');
 export const cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';

+ 2 - 0
packages/app/docker/Dockerfile

@@ -18,6 +18,7 @@ COPY ./yarn.lock .
 COPY ./lerna.json .
 COPY ./packages/app/package.json packages/app/
 COPY ./packages/core/package.json packages/core/
+COPY ./packages/codemirror-textlint/package.json packages/codemirror-textlint/
 COPY ./packages/plugin-attachment-refs/package.json packages/plugin-attachment-refs/
 COPY ./packages/plugin-lsx/package.json packages/plugin-lsx/
 COPY ./packages/plugin-pukiwiki-like-linker/package.json packages/plugin-pukiwiki-like-linker/
@@ -96,6 +97,7 @@ COPY ./tsconfig.base.json ./
 # copy all related packages
 COPY packages/app packages/app
 COPY packages/core packages/core
+COPY packages/codemirror-textlint packages/codemirror-textlint
 COPY packages/plugin-attachment-refs packages/plugin-attachment-refs
 COPY packages/plugin-lsx packages/plugin-lsx
 COPY packages/plugin-pukiwiki-like-linker packages/plugin-pukiwiki-like-linker

+ 11 - 9
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.4.3",
+  "version": "4.4.4-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -54,10 +54,11 @@
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/plugin-attachment-refs": "^4.4.3",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.3",
-    "@growi/plugin-lsx": "^4.4.3",
-    "@growi/slack": "^4.4.3",
+    "@growi/codemirror-textlint": "^4.4.4-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.4-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.4-RC.0",
+    "@growi/plugin-lsx": "^4.4.4-RC.0",
+    "@growi/slack": "^4.4.4-RC.0",
     "@promster/express": "^5.0.1",
     "@promster/server": "^6.0.0",
     "@slack/events-api": "^3.0.0",
@@ -96,8 +97,7 @@
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
     "helmet": "^4.6.0",
-    "nocache": "^3.0.1",
-    "http-errors": "~1.6.2",
+    "http-errors": "~1.8.0",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-node-fs-backend": "^2.1.0",
@@ -114,6 +114,7 @@
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
+    "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "=2.5.0",
@@ -123,7 +124,7 @@
     "passport-http": "^0.3.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
-    "passport-saml": "^1.0.0",
+    "passport-saml": "^2.2.0",
     "passport-twitter": "^1.0.4",
     "prom-client": "^13.0.0",
     "react-card-flip": "^1.0.10",
@@ -153,7 +154,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.3",
+    "@growi/ui": "^4.4.4-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -180,6 +181,7 @@
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
+    "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
     "lodash-webpack-plugin": "^0.11.5",
     "markdown-it": "^10.0.0",

+ 86 - 0
packages/app/resource/cdn-manifests.js

@@ -89,6 +89,92 @@ module.exports = {
       },
     },
   ],
+  dict: [
+    {
+      name: 'base.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/base.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'cc.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/cc.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'check.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/check.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'tid_map.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid_map.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'tid_pos.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid_pos.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'tid.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'unk_char.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_char.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'unk_compat.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_compat.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'unk_invoke.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_invoke.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'unk_map.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_map.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'unk_pos.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_pos.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+    {
+      name: 'unk.dat',
+      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk.dat.gz',
+      args: {
+        integrity: '',
+      },
+    },
+  ],
   style: [
     {
       name: 'lato',

+ 4 - 1
packages/app/resource/locales/en_US/admin/admin.json

@@ -154,7 +154,9 @@
       "stale_notification": "Display notification on stale pages",
       "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
       "show_all_reply_comments": "Show all reply comments",
-      "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted."
+      "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted.",
+      "select_search_scope_children_as_default": "Select 'Only children of this tree' as default value of search range",
+      "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range."
     },
     "code_highlight": "Code highlight",
     "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
@@ -349,6 +351,7 @@
       "allow_specified_long": "Allow specified (The command is allowed from only specified channels)",
       "test_connection": "Test Connection",
       "test_connection_by_pressing_button": "Press the button to test the connection",
+      "test_connection_only_public_channel":"Please test connection in a public channel",
       "error_check_logs_below": "An error has occurred. Please check the logs below.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "add_slack_workspace": "Add a Slack Workspace"

+ 37 - 0
packages/app/resource/locales/en_US/translation.json

@@ -255,6 +255,43 @@
       "This tree": "Only children of this tree"
     }
   },
+  "editor_settings": {
+    "editor_settings": "Editor Settings",
+    "common_settings": {
+      "common_settings": "Common Settings",
+      "common_misspellings": "Textlint rules to find common misspellings from Wikipedia.",
+      "max_comma": "Textlint rule is that limit maximum ten(,) count of sentence. Default: 4",
+      "sentence_length": "Textlint rules that limit Maximum Length of Sentence. Default: 100",
+      "en_capitalization": "Textlint rule that check capitalization in english text.",
+      "no_unmatched_pair": "Textlint rule that check unmatched pairs like ( and ]",
+      "date_weekday_mismatch": "Textlint rule that found mismatch between date and weekday.",
+      "no_kangxi_radicals": "Textlint rule to prevent using kangxi radicals.",
+      "no_surrogate_pair": "Detects surrogate pairs (D800-DBFF and DC00-DFFF) in sentences.",
+      "no_zero_width_spaces": "Textlint rule that disallow zero width spaces.",
+      "period_in_list_item": "Textlint rule that check with or without period in list item.",
+      "use_si_units": "Use of units other than SI unit units is prohibited."
+
+      },
+    "japanese_settings": {
+      "japanese_settings": "Japanese Settings",
+      "ja_no_abusage": "Textlint rules to check for common misuse.",
+      "ja_hiragana_keishikimeishi": "Textlint rules to check easy-to-read Keishikimeishi(pronouns) written in Hiragana than Kanji.",
+      "ja_no_inappropriate_words": "Textlint rules to check for inappropriate expressions",
+      "ja_no_mixed_period": "Textlint rules to check that a paragraph always has a punctuation mark at the end.",
+      "ja_no_redundant_expression": "Textlint rules that prohibits redundant expressions. Redundant expressions are expressions that make sense even if they are omitted from the sentence.",
+      "max_kanji_continuous_len": "Textlint rules that limits the maximum number of consecutive Kanji. Default: 5",
+      "max_ten": "Textlint rule is that limit maximum ten(、) count of sentence.",
+      "no_double_negative_ja": "Textlint rules that detects double negation.",
+      "no_doubled_conjunction": "Textlint rules to check duplicated same conjunctions.",
+      "no_doubled_joshi": "Textlint rules that checks that the same particle appears consecutively in one sentence.",
+      "no_dropping_the_ra": "Textlint rules that detects the word dropping the ra.",
+      "no_hankaku_kana": "Textlint rules that disallow to use Half-width kana.",
+      "prefer_tari_tari": "Textlint rules that checks tari tari.",
+      "ja_unnatural_alphabet": "Detects unnatural alphabets.",
+      "no_mixed_zenkaku_and_hankaku_alphabet": "Check for mixed full-width and half-width alphabets.",
+      "no_nfd": "textlint rule that disallow to use NFD like UTF8-MAC Sonant mark."
+    }
+  },
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",

+ 4 - 1
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -148,7 +148,9 @@
       "stale_notification": "古いページに通知を表示する",
       "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。",
       "show_all_reply_comments": "返信コメントを全て表示する",
-      "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。"
+      "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。",
+      "select_search_scope_children_as_default": "検索範囲のデフォルト設定を「この階層下の子ページ」にする",
+      "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。"
     },
     "code_highlight": "コードハイライト",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
@@ -342,6 +344,7 @@
       "allow_specified-long": "特定のチャンネルを許可 (テキストボックスに入力されたチャンネルのみ許可されます)",
       "test_connection": "連携状況のテストをする",
       "test_connection_by_pressing_button": "以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう",
+      "test_connection_only_public_channel":"連携テストは public チャンネルで確認してください",
       "error_check_logs_below": "エラーが発生しました。下記のログを確認してください。",
       "send_message_to_slack_work_space": "Slack ワークスペースに送信しました",
       "add_slack_workspace": "Slackワークスペースを追加"

+ 36 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -258,6 +258,42 @@
       "This tree": "この階層下の子ページのみ"
     }
   },
+  "editor_settings": {
+    "editor_settings": "エディター設定",
+    "common_settings": {
+      "common_settings": "共通設定",
+      "common_misspellings": "ウィキペディアから一般的なスペルミスを見つけます。",
+      "max_comma": "文の読点(,)を最大10個に制限します。初期値: 4。",
+      "sentence_length": "文の最大文字数を制限します。初期値: 100。",
+      "en_capitalization": "英文の大文字化をチェックします",
+      "no_unmatched_pair": "( と ] のような一致しないペアをチェックします",
+      "date_weekday_mismatch": "日付と平日の不一致を検出します。",
+      "no_kangxi_radicals": "康熙帝の部首の使用を防ぎます。",
+      "no_surrogate_pair": "文中のサロゲートペア(D800-DBFFおよびDC00-DFFF)を検出します。",
+      "no_zero_width_spaces": "ゼロ幅スペースを許可しません。",
+      "period_in_list_item": "リストアイテムのピリオドの有無をチェックします。",
+      "use_si_units": "SI単位系以外の使用を禁止します。"
+      },
+    "japanese_settings": {
+      "japanese_settings": "日本語設定",
+      "ja_hiragana_keishikimeishi": "漢字よりひらがなで書かれた読みやすい形式名詞をチェックします。",
+      "ja_no_abusage": "よくある誤用をチェックします。",
+      "ja_no_inappropriate_words": "不適切表現をチェックします。",
+      "ja_no_mixed_period": "パラグラフの末尾に必ず句点記号を付けていることをチェックします。",
+      "ja_no_redundant_expression": "冗長な表現を禁止します。冗長な表現とは、その文から省いても意味が通じるような表現を示しています。",
+      "max_kanji_continuous_len": "漢字が連続する最大文字数を制限します。初期値: 5。",
+      "max_ten": "一文に利用できる、の数を制限します。一文の読点の数が多いと冗長で読みにくい文章となるため、読点の数を一定数以下にするルールです。 読点の数を減らすためには、句点(。)で文を区切る必要があります。",
+      "no_double_negative_ja": "二重否定を検出します。",
+      "no_doubled_conjunction": "同じ接続詞が連続して出現していないかどうかをチェックします。",
+      "no_doubled_joshi": "1つの文中に同じ助詞が連続して出てくるのをチェックします。",
+      "no_dropping_the_ra": "ら抜き言葉を検出します。",
+      "no_hankaku_kana": "半角カナの利用を禁止します。",
+      "prefer_tari_tari": "「〜たり〜たりする」をチェックします。",
+      "ja_unnatural_alphabet": "不自然なアルファベットを検知します。",
+      "no_mixed_zenkaku_and_hankaku_alphabet": "全角と半角アルファベットを混在をチェックします。",
+      "no_nfd": "UTF8-MAC濁点のようなNFDの使用を禁止します。"
+    }
+  },
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",

+ 4 - 1
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -158,7 +158,9 @@
       "stale_notification": "在过期页上显示通知",
       "stale_notification_desc": "显示自上次更新以来超过1年的页面通知。",
       "show_all_reply_comments": "显示所有回复评论",
-      "show_all_reply_comments_desc": "当设置值为“关”时,将忽略最近两个之外的注释。"
+      "show_all_reply_comments_desc": "当设置值为“关”时,将忽略最近两个之外的注释。",
+      "select_search_scope_children_as_default": "选择“当前分支以下内容”, 作为搜索范围的默认值",
+      "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。"
     },
     "code_highlight": "代码突出显示",
     "nocdn_desc": "当强制应用环境变量<code>NO_CDN=true</code><br>Github样式时,此函数被禁用。",
@@ -352,6 +354,7 @@
       "allow_specified_long": "允许指定(该命令只允许来自指定的通道)",
       "test_connection": "测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
+      "test_connection_only_public_channel":"请在一个公共频道中测试连接",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "add_slack_workspace": "添加Slack Workspace"

+ 37 - 1
packages/app/resource/locales/zh_CN/translation.json

@@ -236,7 +236,43 @@
 			"All pages": "所有页面",
 			"This tree": "当前分支以下内容"
 		}
-	},
+  },
+  "editor_settings": {
+    "editor_settings": "编辑器设置",
+    "common_settings": {
+      "common_settings": "常用设置",
+      "common_misspellings": "从 Wikipedia 中查找常见拼写错误的 Textlint。",
+      "max_comma": "Textlint 规则是限制句子的最大十(,)个计数。默认:4。",
+      "sentence_length": "限制最大句子长度的 Textlint 默认: 100。",
+      "en_capitalization": "检查英文文本大小写的 Textlint 规则。",
+      "no_unmatched_pair": "检查不匹配对的 Textlint 规则,如 ( 和 ]",
+      "date_weekday_mismatch": "发现日期和工作日之间不匹配的 Textlint 规则。",
+      "no_kangxi_radicals": "防止使用康熙部首的 Textlint 规则。",
+      "no_surrogate_pair": "检测句子中的代理对(D800-DBFF 和 DC00-DFFF)。",
+      "no_zero_width_spaces": "不允许零宽度空格的 Textlint 规则。",
+      "period_in_list_item": "在列表项中检查是否有句点的 Textlint 规则。",
+      "use_si_units": "禁止使用 SI 单位以外的单位。"
+      },
+    "japanese_settings": {
+      "japanese_settings": "日语设置",
+      "ja_no_abusage": "用于检查常见误用的 Textlint 规则。",
+      "ja_hiragana_keishikimeishi": "Textlint 规则检查易于阅读的 Keishikimeishi(代词)用平假名而不是汉字编写。",
+      "ja_no_inappropriate_words": "Textlint 规则来检查不适当的表达",
+      "ja_no_mixed_period": "Textlint 规则用于检查段落末尾是否总是有标点符号。",
+      "ja_no_redundant_expression": "禁止冗余表达式的 Textlint 规则。冗余表达式是即使从句子中省略也有意义的表达式。",
+      "max_kanji_continuous_len": "限制连续汉字的最大数量的 Textlint 规则。默认:5。",
+      "max_ten": "Textlint 规则是限制句子的最大十(、)个计数。",
+      "no_double_negative_ja": "检测双重否定的 Textlint 规则。",
+      "no_doubled_conjunction": "Textlint 规则来检查重复的相同连词。",
+      "no_doubled_joshi": "Textlint 规则,用于检查同一个粒子是否连续出现在一个句子中。",
+      "no_dropping_the_ra": "检测丢弃 ra 的单词的 Textlint 规则。",
+      "no_hankaku_kana": "不允许使用半角假名的 Textlint 规则。",
+      "prefer_tari_tari": "检查 tari tari 的 Textlint 规则。",
+      "ja_unnatural_alphabet": "检测不自然的字母。",
+      "no_mixed_zenkaku_and_hankaku_alphabet": "检查混合的全角和半角字母。",
+      "no_nfd": "禁止使用 UTF8-MAC 浊音等 NFD。"
+    }
+  },
 	"copy_to_clipboard": {
 		"Copy to clipboard": "复制到剪贴板",
 		"Page path": "页面路径",

+ 7 - 7
packages/app/src/client/legacy/crowi.js

@@ -140,17 +140,17 @@ Crowi.findSectionHeader = function(hash) {
   return null;
 };
 
-Crowi.unhighlightSelectedSection = function(hash) {
+Crowi.unblinkSelectedSection = function(hash) {
   const elem = Crowi.findSectionHeader(hash);
   if (elem != null) {
-    elem.classList.remove('highlighted');
+    elem.classList.remove('blink');
   }
 };
 
-Crowi.highlightSelectedSection = function(hash) {
+Crowi.blinkSelectedSection = function(hash) {
   const elem = Crowi.findSectionHeader(hash);
   if (elem != null) {
-    elem.classList.add('highlighted');
+    elem.classList.add('blink');
   }
 };
 
@@ -219,14 +219,14 @@ window.addEventListener('load', () => {
     });
   }
 
-  Crowi.highlightSelectedSection(window.location.hash);
+  Crowi.blinkSelectedSection(window.location.hash);
   Crowi.modifyScrollTop();
   Crowi.initClassesByOS();
 });
 
 window.addEventListener('hashchange', (e) => {
-  Crowi.unhighlightSelectedSection(Crowi.findHashFromUrl(e.oldURL));
-  Crowi.highlightSelectedSection(Crowi.findHashFromUrl(e.newURL));
+  Crowi.unblinkSelectedSection(Crowi.findHashFromUrl(e.oldURL));
+  Crowi.blinkSelectedSection(Crowi.findHashFromUrl(e.newURL));
   Crowi.modifyScrollTop();
   const { appContainer } = window;
   const navigationContainer = appContainer.getContainer('NavigationContainer');

+ 11 - 0
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -35,6 +35,7 @@ export default class AdminCustomizeContainer extends Container {
 
       isEnabledStaleNotification: false,
       isAllReplyShown: false,
+      isSearchScopeChildrenAsDefault: false,
       currentHighlightJsStyleId: '',
       isHighlightJsStyleBorderEnabled: false,
       currentCustomizeTitle: '',
@@ -89,6 +90,7 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: customizeParams.pageLimitationXL,
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
         isAllReplyShown: customizeParams.isAllReplyShown,
+        isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
         currentHighlightJsStyleId: customizeParams.styleName,
         isHighlightJsStyleBorderEnabled: customizeParams.styleBorder,
         currentCustomizeTitle: customizeParams.customizeTitle,
@@ -183,6 +185,13 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ isAllReplyShown: !this.state.isAllReplyShown });
   }
 
+  /**
+   * Switch isSearchScopeChildrenAsDefault
+   */
+  switchIsSearchScopeChildrenAsDefault() {
+    this.setState({ isSearchScopeChildrenAsDefault: !this.state.isSearchScopeChildrenAsDefault });
+  }
+
   /**
    * Switch highlightJsStyle
    */
@@ -295,6 +304,7 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: this.state.pageLimitationXL,
         isEnabledStaleNotification: this.state.isEnabledStaleNotification,
         isAllReplyShown: this.state.isAllReplyShown,
+        isSearchScopeChildrenAsDefault: this.state.isSearchScopeChildrenAsDefault,
       });
       const { customizedParams } = response.data;
       this.setState({
@@ -307,6 +317,7 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: customizedParams.pageLimitationXL,
         isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
         isAllReplyShown: customizedParams.isAllReplyShown,
+        isSearchScopeChildrenAsDefault: customizedParams.isSearchScopeChildrenAsDefault,
       });
     }
     catch (err) {

+ 23 - 0
packages/app/src/client/services/EditorContainer.js

@@ -15,6 +15,7 @@ export default class EditorContainer extends Container {
 
     this.appContainer = appContainer;
     this.appContainer.registerContainer(this);
+    this.retrieveEditorSettings = this.retrieveEditorSettings.bind(this);
 
     const mainContent = document.querySelector('#content-main');
 
@@ -35,6 +36,9 @@ export default class EditorContainer extends Container {
 
       editorOptions: {},
       previewOptions: {},
+      isTextlintEnabled: false,
+      textlintRules: [],
+
       indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
     };
 
@@ -195,4 +199,23 @@ export default class EditorContainer extends Container {
     return null;
   }
 
+
+  /**
+   * Retrieve Editor Settings
+   */
+  async retrieveEditorSettings() {
+    const { data } = await this.appContainer.apiv3Get('/personal-setting/editor-settings');
+
+    if (data?.textlintSettings == null) {
+      return;
+    }
+
+    const { isTextlintEnabled = false, textlintRules = [] } = data.textlintSettings;
+
+    this.setState({
+      isTextlintEnabled,
+      textlintRules,
+    });
+  }
+
 }

+ 15 - 0
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -139,6 +139,21 @@ class CustomizeFunctionSetting extends React.Component {
               </div>
             </div>
 
+            <div className="form-group row">
+              <div className="offset-md-3 col-md-6 text-left">
+                <CustomizeFunctionOption
+                  optionId="isSearchScopeChildrenAsDefault"
+                  label={t('admin:customize_setting.function_options.select_search_scope_children_as_default')}
+                  isChecked={adminCustomizeContainer.state.isSearchScopeChildrenAsDefault || false}
+                  onChecked={() => { adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault() }}
+                >
+                  <p className="form-text text-muted">
+                    {t('admin:customize_setting.function_options.select_search_scope_children_as_default_desc')}
+                  </p>
+                </CustomizeFunctionOption>
+              </div>
+            </div>
+
             <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           </div>
         </div>

+ 3 - 0
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -138,6 +138,9 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         title={<><span className="mr-2">⑤</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
+        <p className="text-center text-warning">
+          <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+        </p>
         <div className="d-flex justify-content-center">
           <form className="form-row align-items-center" onSubmit={e => submitForm(e)}>
             <div className="input-group col-8">

+ 3 - 0
packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -264,6 +264,9 @@ const TestProcess = ({
   return (
     <>
       <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
+      <p className="text-center text-warning">
+        <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+      </p>
       <div className="d-flex justify-content-center">
         <form className="form-row justify-content-center" onSubmit={e => submitForm(e)}>
           <div className="input-group col-8">

+ 286 - 0
packages/app/src/components/Me/EditorSettings.tsx

@@ -0,0 +1,286 @@
+import React, {
+  Dispatch,
+  FC, SetStateAction, useCallback, useEffect, useState,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import AppContainer from '~/client/services/AppContainer';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
+
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+type EditorSettingsBodyProps = {
+  appContainer: AppContainer
+}
+
+type RuleListGroupProps = {
+  title: string;
+  ruleList: RulesMenuItem[]
+  textlintRules: LintRule[]
+  setTextlintRules: Dispatch<SetStateAction<LintRule[]>>
+}
+
+type LintRule = {
+  name: string
+  options?: unknown
+  isEnabled?: boolean
+}
+
+type RulesMenuItem = {
+  name: string
+  description: string
+}
+
+
+const commonRulesMenuItems = [
+  {
+    name: 'common-misspellings',
+    description: 'editor_settings.common_settings.common_misspellings',
+  },
+  {
+    name: 'max-comma',
+    description: 'editor_settings.common_settings.max_comma',
+  },
+  {
+    name: 'sentence-length',
+    description: 'editor_settings.common_settings.sentence_length',
+  },
+  {
+    name: 'en-capitalization',
+    description: 'editor_settings.common_settings.en_capitalization',
+  },
+  {
+    name: 'no-unmatched-pair',
+    description: 'editor_settings.common_settings.no_unmatched_pair',
+  },
+  {
+    name: 'date-weekday-mismatch',
+    description: 'editor_settings.common_settings.date_weekday_mismatch',
+  },
+  {
+    name: 'no-kangxi-radicals',
+    description: 'editor_settings.common_settings.no_kangxi_radicals',
+  },
+  {
+    name: 'no-surrogate-pair',
+    description: 'editor_settings.common_settings.no_surrogate_pair',
+  },
+  {
+    name: 'no-zero-width-spaces',
+    description: 'editor_settings.common_settings.no_zero_width_spaces',
+  },
+  {
+    name: 'period-in-list-item',
+    description: 'editor_settings.common_settings.period_in_list_item',
+  },
+  {
+    name: 'use-si-units',
+    description: 'editor_settings.common_settings.use_si_units',
+  },
+];
+
+const japaneseRulesMenuItems = [
+  {
+    name: 'ja-hiragana-keishikimeishi',
+    description: 'editor_settings.japanese_settings.ja_hiragana_keishikimeishi',
+  },
+  {
+    name: 'ja-no-abusage',
+    description: 'editor_settings.japanese_settings.ja_no_abusage',
+  },
+  {
+    name: 'ja-no-inappropriate-words',
+    description: 'editor_settings.japanese_settings.ja_no_inappropriate_words',
+  },
+  {
+    name: 'ja-no-mixed-period',
+    description: 'editor_settings.japanese_settings.ja_no_mixed_period',
+  },
+  {
+    name: 'ja-no-redundant-expression',
+    description: 'editor_settings.japanese_settings.ja_no_redundant_expression',
+  },
+  {
+    name: 'max-kanji-continuous-len',
+    description: 'editor_settings.japanese_settings.max_kanji_continuous_len',
+  },
+  {
+    name: 'max-ten',
+    description: 'editor_settings.japanese_settings.max_ten',
+  },
+  {
+    name: 'no-double-negative-ja',
+    description: 'editor_settings.japanese_settings.no_double_negative_ja',
+  },
+  {
+    name: 'no-doubled-conjunction',
+    description: 'editor_settings.japanese_settings.no_doubled_conjunction',
+  },
+  {
+    name: 'no-doubled-joshi',
+    description: 'editor_settings.japanese_settings.no_doubled_joshi',
+  },
+  {
+    name: 'no-dropping-the-ra',
+    description: 'editor_settings.japanese_settings.no_dropping_the_ra',
+  },
+  {
+    name: 'no-hankaku-kana',
+    description: 'editor_settings.japanese_settings.no_hankaku_kana',
+  },
+  {
+    name: 'prefer-tari-tari',
+    description: 'editor_settings.japanese_settings.prefer_tari_tari',
+  },
+  {
+    name: 'ja-unnatural-alphabet',
+    description: 'editor_settings.japanese_settings.ja_unnatural_alphabet',
+  },
+  {
+    name: 'no-mixed-zenkaku-and-hankaku-alphabet',
+    description: 'editor_settings.japanese_settings.no_mixed_zenkaku_and_hankaku_alphabet',
+  },
+  {
+    name: 'no-nfd',
+    description: 'editor_settings.japanese_settings.no_nfd',
+  },
+
+];
+
+
+const RuleListGroup: FC<RuleListGroupProps> = ({
+  title, ruleList, textlintRules, setTextlintRules,
+}) => {
+  const { t } = useTranslation();
+
+  const isCheckedRule = (ruleName: string) => (
+    textlintRules.find(stateRule => (
+      stateRule.name === ruleName
+    ))?.isEnabled || false
+  );
+
+  const ruleCheckboxHandler = (isChecked: boolean, ruleName: string) => {
+    setTextlintRules(prevState => (
+      prevState.filter(rule => rule.name !== ruleName).concat({ name: ruleName, isEnabled: isChecked })
+    ));
+  };
+
+  return (
+    <>
+      <h2 className="border-bottom my-4">{t(title)}</h2>
+      <div className="form-group row">
+        <div className="offset-md-3 col-md-6 text-left">
+          {ruleList.map(rule => (
+            <div
+              key={rule.name}
+              className="custom-control custom-switch custom-checkbox-success"
+            >
+              <input
+                type="checkbox"
+                className="custom-control-input"
+                id={rule.name}
+                checked={isCheckedRule(rule.name)}
+                onChange={e => ruleCheckboxHandler(e.target.checked, rule.name)}
+              />
+              <label className="custom-control-label" htmlFor={rule.name}>
+                <strong>{rule.name}</strong>
+              </label>
+              <p className="form-text text-muted small">
+                {t(rule.description)}
+              </p>
+            </div>
+          ))}
+        </div>
+      </div>
+    </>
+  );
+};
+
+
+RuleListGroup.propTypes = {
+  title: PropTypes.string.isRequired,
+  ruleList: PropTypes.array.isRequired,
+  textlintRules: PropTypes.array.isRequired,
+  setTextlintRules: PropTypes.func.isRequired,
+};
+
+
+const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
+  const { t } = useTranslation();
+  const { appContainer } = props;
+  const [textlintRules, setTextlintRules] = useState<LintRule[]>([]);
+
+  const initializeEditorSettings = useCallback(async() => {
+    const { data } = await appContainer.apiv3Get('/personal-setting/editor-settings');
+    const retrievedRules: LintRule[] = data?.textlintSettings?.textlintRules;
+
+    // If database is empty, add default rules to state
+    if (retrievedRules != null && retrievedRules.length > 0) {
+      setTextlintRules(retrievedRules);
+    }
+    else {
+      const createRulesFromDefaultList = (rule: { name: string }) => (
+        {
+          name: rule.name,
+          isEnabled: true,
+        }
+      );
+
+      const defaultCommonRules = commonRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
+      const defaultJapaneseRules = japaneseRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
+      setTextlintRules([...defaultCommonRules, ...defaultJapaneseRules]);
+    }
+
+  }, [appContainer]);
+
+  useEffect(() => {
+    initializeEditorSettings();
+  }, [initializeEditorSettings]);
+
+  const updateRulesHandler = async() => {
+    try {
+      const { data } = await appContainer.apiv3Put('/personal-setting/editor-settings', { textlintSettings: { textlintRules: [...textlintRules] } });
+      setTextlintRules(data.textlintSettings.textlintRules);
+      toastSuccess(t('toaster.update_successed', { target: 'Updated Textlint Settings' }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <>
+      <RuleListGroup
+        title="editor_settings.common_settings.common_settings"
+        ruleList={commonRulesMenuItems}
+        textlintRules={textlintRules}
+        setTextlintRules={setTextlintRules}
+      />
+      <RuleListGroup
+        title="editor_settings.japanese_settings.japanese_settings"
+        ruleList={japaneseRulesMenuItems}
+        textlintRules={textlintRules}
+        setTextlintRules={setTextlintRules}
+      />
+
+      <div className="row my-3">
+        <div className="offset-4 col-5">
+          <button
+            type="button"
+            className="btn btn-primary"
+            onClick={updateRulesHandler}
+          >
+            {t('Update')}
+          </button>
+        </div>
+      </div>
+    </>
+  );
+};
+
+export const EditorSettings = withUnstatedContainers(EditorSettingsBody, [AppContainer]);
+
+EditorSettingsBody.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};

+ 7 - 0
packages/app/src/components/Me/PersonalSettings.jsx

@@ -8,6 +8,7 @@ import UserSettings from './UserSettings';
 import PasswordSettings from './PasswordSettings';
 import ExternalAccountLinkedMe from './ExternalAccountLinkedMe';
 import ApiSettings from './ApiSettings';
+import { EditorSettings } from './EditorSettings';
 
 const PersonalSettings = (props) => {
 
@@ -39,6 +40,12 @@ const PersonalSettings = (props) => {
         i18n: t('API Settings'),
         index: 3,
       },
+      editor_settings: {
+        Icon: () => <i className="icon-fw icon-pencil"></i>,
+        Content: EditorSettings,
+        i18n: t('editor_settings.editor_settings'),
+        index: 4,
+      },
     };
   }, [t]);
 

+ 3 - 1
packages/app/src/components/Navbar/GlobalSearch.jsx

@@ -14,9 +14,11 @@ class GlobalSearch extends React.Component {
   constructor(props) {
     super(props);
 
+    const isSearchScopeChildrenAsDefault = this.props.appContainer.getConfig().isSearchScopeChildrenAsDefault;
+
     this.state = {
       text: '',
-      isScopeChildren: false,
+      isScopeChildren: isSearchScopeChildrenAsDefault,
     };
 
     this.onInputChange = this.onInputChange.bind(this);

+ 1 - 1
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -69,7 +69,7 @@ class RevisionRenderer extends React.PureComponent {
         .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
         .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
       const keywordExp = new RegExp(`(${k}(?!(.*?")))`, 'ig');
-      returnBody = returnBody.replace(keywordExp, '<em class="highlighted">$&</em>');
+      returnBody = returnBody.replace(keywordExp, '<em class="highlighted-keyword">$&</em>');
     });
 
     return returnBody;

+ 42 - 4
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -7,9 +7,12 @@ import * as codemirror from 'codemirror';
 import { Button } from 'reactstrap';
 import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
 
+import { JSHINT } from 'jshint';
+
 import * as loadScript from 'simple-load-script';
 import * as loadCssSync from 'load-css-file';
 
+import { createValidator } from '@growi/codemirror-textlint';
 import InterceptorManager from '~/services/interceptor-manager';
 import loggerFactory from '~/utils/logger';
 
@@ -30,6 +33,9 @@ import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
 import DrawioModal from './DrawioModal';
 
+
+window.JSHINT = JSHINT;
+
 // set save handler
 codemirror.commands.save = (instance) => {
   if (instance.codeMirrorEditor != null) {
@@ -56,6 +62,8 @@ require('codemirror/addon/fold/foldgutter.css');
 require('codemirror/addon/fold/markdown-fold');
 require('codemirror/addon/fold/brace-fold');
 require('codemirror/addon/display/placeholder');
+require('codemirror/addon/lint/lint');
+require('codemirror/addon/lint/lint.css');
 require('~/client/util/codemirror/autorefresh.ext');
 require('~/client/util/codemirror/gfm-growi.mode');
 // import modes to highlight
@@ -144,8 +152,11 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
   init() {
     this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
-    this.cmNoCdnScriptRoot = '/js/cdn';
-    this.cmNoCdnStyleRoot = '/styles/cdn';
+    this.cmNoCdnScriptRoot = '/static/js/cdn';
+    this.cmNoCdnStyleRoot = '/static/styles/cdn';
+    window.kuromojin = this.props.noCdn
+      ? { dicPath: '/static/dict/cdn' }
+      : { dicPath: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict' };
 
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptors([
@@ -162,6 +173,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
       this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
       this.setState({ isEnabledEmojiAutoComplete: true });
     }
+
+    this.initializeTextlint();
   }
 
   componentDidMount() {
@@ -187,6 +200,16 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.setKeymapMode(keymapMode);
   }
 
+  async initializeTextlint() {
+    if (this.props.onInitializeTextlint != null) {
+      await this.props.onInitializeTextlint();
+      // If database has empty array, pass null instead to enable all default rules
+      const rulesForValidator = this.props.textlintRules?.length !== 0 ? this.props.textlintRules : null;
+      this.textlintValidator = createValidator(rulesForValidator);
+      this.codemirrorLintConfig = { getAnnotations: this.textlintValidator, async: true };
+    }
+  }
+
   getCodeMirror() {
     return this.cm.editor;
   }
@@ -860,15 +883,24 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
   render() {
     const mode = this.state.isGfmMode ? 'gfm-growi' : undefined;
+    const lint = this.props.isTextlintEnabled ? this.codemirrorLintConfig : false;
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
+    const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..';
 
-    const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plane Text..';
+    const gutters = [];
+    if (this.props.lineNumbers != null) {
+      gutters.push('CodeMirror-linenumbers', 'CodeMirror-foldgutter');
+    }
+    if (this.props.isTextlintEnabled === true) {
+      gutters.push('CodeMirror-lint-markers');
+    }
 
     return (
       <React.Fragment>
 
         <ReactCodeMirror
           ref={(c) => { this.cm = c }}
+          detach
           className={additionalClasses}
           placeholder="search"
           editorDidMount={(editor) => {
@@ -893,7 +925,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             matchTags: { bothTags: true },
             // folding
             foldGutter: this.props.lineNumbers,
-            gutters: this.props.lineNumbers ? ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'] : [],
+            gutters,
             // match-highlighter, matchesonscrollbar, annotatescrollbar options
             highlightSelectionMatches: { annotateScrollbar: true },
             // continuelist, indentlist
@@ -905,6 +937,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
               'Shift-Tab': 'indentLess',
               'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
             },
+            lint,
           }}
           onCursor={this.cursorHandler}
           onScroll={(editor, data) => {
@@ -953,11 +986,16 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 CodeMirrorEditor.propTypes = Object.assign({
   editorOptions: PropTypes.object.isRequired,
+  isTextlintEnabled: PropTypes.bool,
+  textlintRules: PropTypes.array,
   emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
   onMarkdownHelpButtonClicked: PropTypes.func,
   onAddAttachmentButtonClicked: PropTypes.func,
+  onInitializeTextlint: PropTypes.func,
 }, AbstractEditor.propTypes);
+
 CodeMirrorEditor.defaultProps = {
   lineNumbers: true,
+  isTextlintEnabled: false,
 };

+ 8 - 1
packages/app/src/components/PageEditor/Editor.jsx

@@ -10,6 +10,7 @@ import {
 import Dropzone from 'react-dropzone';
 
 import EditorContainer from '~/client/services/EditorContainer';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 import Cheatsheet from './Cheatsheet';
 import AbstractEditor from './AbstractEditor';
@@ -18,7 +19,7 @@ import TextAreaEditor from './TextAreaEditor';
 
 import pasteHelper from './PasteHelper';
 
-export default class Editor extends AbstractEditor {
+class Editor extends AbstractEditor {
 
   constructor(props) {
     super(props);
@@ -316,6 +317,9 @@ export default class Editor extends AbstractEditor {
                         ref={(c) => { this.cmEditor = c }}
                         indentSize={editorContainer.state.indentSize}
                         editorOptions={editorContainer.state.editorOptions}
+                        isTextlintEnabled={editorContainer.state.isTextlintEnabled}
+                        textlintRules={editorContainer.state.textlintRules}
+                        onInitializeTextlint={editorContainer.retrieveEditorSettings}
                         onPasteFiles={this.pasteFilesHandler}
                         onDragEnter={this.dragEnterHandler}
                         onMarkdownHelpButtonClicked={this.showMarkdownHelp}
@@ -377,4 +381,7 @@ Editor.propTypes = Object.assign({
   emojiStrategy: PropTypes.object,
   onChange: PropTypes.func,
   onUpload: PropTypes.func,
+  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 }, AbstractEditor.propTypes);
+
+export default withUnstatedContainers(Editor, [EditorContainer]);

+ 41 - 0
packages/app/src/components/PageEditor/OptionsSelector.jsx

@@ -10,6 +10,7 @@ import {
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
+import { toastError } from '~/client/util/apiNotification';
 
 
 export const defaultEditorOptions = {
@@ -51,6 +52,8 @@ class OptionsSelector extends React.Component {
     this.onClickStyleActiveLine = this.onClickStyleActiveLine.bind(this);
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
+    this.switchTextlintEnabledHandler = this.switchTextlintEnabledHandler.bind(this);
+    this.updateIsTextlintEnabledToDB = this.updateIsTextlintEnabledToDB.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
     this.onChangeIndentSize = this.onChangeIndentSize.bind(this);
   }
@@ -111,6 +114,23 @@ class OptionsSelector extends React.Component {
     editorContainer.saveOptsToLocalStorage();
   }
 
+  async updateIsTextlintEnabledToDB(newVal) {
+    const { appContainer } = this.props;
+    try {
+      await appContainer.apiv3Put('/personal-setting/editor-settings', { textlintSettings: { isTextlintEnabled: newVal } });
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  async switchTextlintEnabledHandler() {
+    const { editorContainer } = this.props;
+    const newVal = !editorContainer.state.isTextlintEnabled;
+    editorContainer.setState({ isTextlintEnabled: newVal });
+    this.updateIsTextlintEnabledToDB(newVal);
+  }
+
   onToggleConfigurationDropdown(newValue) {
     this.setState({ isCddMenuOpened: !this.state.isCddMenuOpened });
   }
@@ -207,6 +227,7 @@ class OptionsSelector extends React.Component {
             {this.renderActiveLineMenuItem()}
             {this.renderRealtimeMathJaxMenuItem()}
             {this.renderMarkdownTableAutoFormattingMenuItem()}
+            {this.renderIsTextlintEnabledMenuItem()}
             {/* <DropdownItem divider /> */}
           </DropdownMenu>
 
@@ -286,6 +307,26 @@ class OptionsSelector extends React.Component {
     );
   }
 
+  renderIsTextlintEnabledMenuItem() {
+    const isActive = this.props.editorContainer.state.isTextlintEnabled;
+
+    const iconClasses = ['text-info'];
+    if (isActive) {
+      iconClasses.push('ti-check');
+    }
+    const iconClassName = iconClasses.join(' ');
+
+    return (
+      <DropdownItem toggle={false} onClick={this.switchTextlintEnabledHandler}>
+        <div className="d-flex justify-content-between">
+          <span className="icon-container"></span>
+          <span className="menuitem-label">Textlint</span>
+          <span className="icon-container"><i className={iconClassName}></i></span>
+        </div>
+      </DropdownItem>
+    );
+  }
+
   renderIndentSizeSelector() {
     const { appContainer, editorContainer } = this.props;
     const menuItems = this.typicalIndentSizes.map((indent) => {

+ 2 - 0
packages/app/src/server/models/config.ts

@@ -129,6 +129,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:showPageLimitationXL' : 20,
   'customize:isEnabledStaleNotification': false,
   'customize:isAllReplyShown': false,
+  'customize:isSearchScopeChildrenAsDefault': false,
 
   'notification:owner-page:isEnabled': false,
   'notification:group-page:isEnabled': false,
@@ -205,6 +206,7 @@ schema.statics.getLocalconfig = function(crowi) {
     isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
     isEnabledTimeline: crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
     isAllReplyShown: crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
+    isSearchScopeChildrenAsDefault: crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
     xssOption: crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
     tagWhiteList: crowi.xssService.getTagWhiteList(),
     attrWhiteList: crowi.xssService.getAttrWhiteList(),

+ 41 - 0
packages/app/src/server/models/editor-settings.ts

@@ -0,0 +1,41 @@
+import {
+  Schema, Model, Document,
+} from 'mongoose';
+import { getOrCreateModel } from '../util/mongoose-utils';
+
+
+export interface ILintRule {
+  name: string;
+  options?: unknown;
+  isEnabled?: boolean;
+}
+
+export interface ITextlintSettings {
+  isTexlintEnabled: string;
+  textlintRules: ILintRule[];
+}
+
+export interface IEditorSettings {
+  userId: Schema.Types.ObjectId;
+  textlintSettings: ITextlintSettings;
+}
+
+export interface EditorSettingsDocument extends IEditorSettings, Document {}
+export type EditorSettingsModel = Model<EditorSettingsDocument>
+
+const textlintSettingsSchema = new Schema<ITextlintSettings>({
+  isTextlintEnabled: { type: Boolean, default: false },
+  textlintRules: {
+    type: [
+      { name: { type: String }, options: { type: Object }, isEnabled: { type: Boolean } },
+    ],
+  },
+});
+
+const editorSettingsSchema = new Schema<IEditorSettings>({
+  userId: { type: String },
+  textlintSettings: textlintSettingsSchema,
+});
+
+
+export default getOrCreateModel<EditorSettingsDocument, EditorSettingsModel>('EditorSettings', editorSettingsSchema);

+ 1 - 0
packages/app/src/server/models/user.js

@@ -35,6 +35,7 @@ module.exports = function(crowi) {
     userEvent.on('activated', userEvent.onActivated);
   }
 
+
   const userSchema = new mongoose.Schema({
     userId: String,
     image: String,

+ 6 - 0
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -51,6 +51,8 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *            type: boolean
  *          isAllReplyShown:
  *            type: boolean
+ *          isSearchScopeChildrenAsDefault:
+ *            type: boolean
  *      CustomizeHighlight:
  *        description: CustomizeHighlight
  *        type: object
@@ -112,6 +114,7 @@ module.exports = (crowi) => {
       body('pageLimitationXL').isInt().isInt({ min: 1, max: 1000 }),
       body('isEnabledStaleNotification').isBoolean(),
       body('isAllReplyShown').isBoolean(),
+      body('isSearchScopeChildrenAsDefault').isBoolean(),
     ],
     customizeTitle: [
       body('customizeTitle').isString(),
@@ -166,6 +169,7 @@ module.exports = (crowi) => {
       pageLimitationXL: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
       isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
       isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
+      isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
       styleName: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
       styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
       customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
@@ -363,6 +367,7 @@ module.exports = (crowi) => {
       'customize:showPageLimitationXL': req.body.pageLimitationXL,
       'customize:isEnabledStaleNotification': req.body.isEnabledStaleNotification,
       'customize:isAllReplyShown': req.body.isAllReplyShown,
+      'customize:isSearchScopeChildrenAsDefault': req.body.isSearchScopeChildrenAsDefault,
     };
 
     try {
@@ -377,6 +382,7 @@ module.exports = (crowi) => {
         pageLimitationXL: await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
         isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
         isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
+        isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
       };
       return res.apiv3({ customizedParams });
     }

+ 92 - 1
packages/app/src/server/routes/apiv3/personal-setting.js

@@ -4,6 +4,8 @@ import loggerFactory from '~/utils/logger';
 
 import { listLocaleIds } from '~/utils/locale-utils';
 
+import EditorSettings from '../../models/editor-settings';
+
 const logger = loggerFactory('growi:routes:apiv3:personal-setting');
 
 const express = require('express');
@@ -15,7 +17,7 @@ const router = express.Router();
 /**
  * @swagger
  *  tags:
- *    name: PsersonalSetting
+ *    name: PersonalSetting
  */
 
 /**
@@ -98,6 +100,13 @@ module.exports = (crowi) => {
       body('providerType').isString().not().isEmpty(),
       body('accountId').isString().not().isEmpty(),
     ],
+    editorSettings: [
+      body('textlintSettings.isTextlintEnabled').optional().isBoolean(),
+      body('textlintSettings.textlintRules.*.name').optional().isString(),
+      body('textlintSettings.textlintRules.*.options').optional(),
+      body('textlintSettings.textlintRules.*.isEnabled').optional().isBoolean(),
+
+    ],
   };
 
   /**
@@ -459,5 +468,87 @@ module.exports = (crowi) => {
 
   });
 
+  /**
+   * @swagger
+   *
+   *    /personal-setting/editor-settings:
+   *      put:
+   *        tags: [EditorSetting]
+   *        operationId: putEditorSettings
+   *        summary: /editor-setting
+   *        description: Put editor preferences
+   *        responses:
+   *          200:
+   *            description: params of editor settings
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    currentUser:
+   *                      type: object
+   *                      description: editor settings
+   */
+  router.put('/editor-settings', accessTokenParser, loginRequiredStrictly, csrf, validator.editorSettings, apiV3FormValidator, async(req, res) => {
+    const query = { userId: req.user.id };
+    const textlintSettings = req.body.textlintSettings;
+    const document = {};
+
+    if (textlintSettings == null) {
+      return res.apiv3Err('no-settings-found');
+    }
+
+    if (textlintSettings.isTextlintEnabled != null) {
+      Object.assign(document, { 'textlintSettings.isTextlintEnabled': textlintSettings.isTextlintEnabled });
+    }
+    if (textlintSettings.textlintRules != null) {
+      Object.assign(document, { 'textlintSettings.textlintRules': textlintSettings.textlintRules });
+    }
+
+    // Insert if document does not exist, and return new values
+    // See: https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate
+    const options = { upsert: true, new: true };
+    try {
+      const response = await EditorSettings.findOneAndUpdate(query, { $set: document }, options);
+      return res.apiv3(response);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('updating-editor-settings-failed');
+    }
+  });
+
+
+  /**
+   * @swagger
+   *
+   *    /personal-setting/editor-settings:
+   *      get:
+   *        tags: [EditorSetting]
+   *        operationId: getEditorSettings
+   *        summary: /editor-setting
+   *        description: Get editor preferences
+   *        responses:
+   *          200:
+   *            description: params of editor settings
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    currentUser:
+   *                      type: object
+   *                      description: editor settings
+   */
+  router.get('/editor-settings', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+    try {
+      const query = { userId: req.user.id };
+      const response = await EditorSettings.findOne(query);
+      return res.apiv3(response);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('getting-editor-settings-failed');
+    }
+  });
+
   return router;
 };

+ 2 - 6
packages/app/src/server/routes/apiv3/users.js

@@ -11,6 +11,7 @@ const path = require('path');
 const { body, query } = require('express-validator');
 const { isEmail } = require('validator');
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
+const { serializePageSecurely } = require('../../models/serializers/page-serializer');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
@@ -330,12 +331,7 @@ module.exports = (crowi) => {
     try {
       const result = await Page.findListByCreator(user, req.user, queryOptions);
 
-      // Delete unnecessary data about users
-      result.pages = result.pages.map((page) => {
-        const user = page.lastUpdateUser.toObject();
-        page.lastUpdateUser = user;
-        return page;
-      });
+      result.pages = result.pages.map(page => serializePageSecurely(page));
 
       return res.apiv3(result);
     }

+ 4 - 4
packages/app/src/services/cdn-resources-service.js

@@ -6,10 +6,10 @@ const urljoin = require('url-join');
 
 const { envUtils } = require('growi-commons');
 
-const cdnLocalScriptRoot = 'public/js/cdn';
-const cdnLocalScriptWebRoot = '/js/cdn';
-const cdnLocalStyleRoot = 'public/styles/cdn';
-const cdnLocalStyleWebRoot = '/styles/cdn';
+const cdnLocalScriptRoot = 'public/static/js/cdn';
+const cdnLocalScriptWebRoot = '/static/js/cdn';
+const cdnLocalStyleRoot = 'public/static/styles/cdn';
+const cdnLocalStyleWebRoot = '/static/styles/cdn';
 
 
 class CdnResourcesService {

+ 3 - 2
packages/app/src/styles/_mixins.scss

@@ -253,7 +253,8 @@
     }
   }
 }
-@mixin highlighted($color) {
+
+@mixin blink-bgcolor($bgcolor) {
   @keyframes fadeout {
     100% {
       opacity: 0;
@@ -270,7 +271,7 @@
     width: 100%;
     height: 70%;
     content: '';
-    background-color: $color;
+    background-color: $bgcolor;
     border-radius: 2px;
     animation: fadeout 1s ease-in 1.5s forwards;
   }

+ 6 - 0
packages/app/src/styles/_override-codemirror.scss

@@ -45,3 +45,9 @@
     color: $text-muted;
   }
 }
+
+// patch to fix https://github.com/codemirror/CodeMirror/issues/4089
+// see also https://github.com/codemirror/CodeMirror/commit/51a1e7da60a99e019f026a118dc7c98c2b1f9d62
+.CodeMirror-wrap > div > textarea {
+  font-size: #{$line-height-base}rem;
+}

+ 1 - 1
packages/app/src/styles/_variables.scss

@@ -6,7 +6,7 @@ $growi-blue: #175fa5;
 $grw-marker-yellow: #ff6;
 $grw-marker-red: #f6c;
 $grw-marker-blue: #6cf;
-$grw-marker-cyan: cyan;
+$grw-marker-cyan: #6ff;
 $grw-marker-green: #6f6;
 
 $font-family-for-staff-credit: Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif !default;

+ 15 - 3
packages/app/src/styles/theme/_apply-colors.scss

@@ -17,7 +17,7 @@ $bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcol
 $color-seen-user: #549c79 !default;
 $reload-btn-rc-color: $gray-500;
 $reload-btn-cs-color: $gray-500;
-$bgcolor-highlighted: $grw-marker-yellow !default;
+$bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;
@@ -450,8 +450,20 @@ ul.pagination {
  * GROWI wiki
  */
 .wiki {
-  .highlighted {
-    background: linear-gradient(transparent 60%, $bgcolor-highlighted 60%);
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6,
+  h7 {
+    &.blink {
+      @include blink-bgcolor($bgcolor-blinked-section);
+    }
+  }
+
+  .highlighted-keyword {
+    background: linear-gradient(transparent 60%, $bgcolor-keyword-highlighted 60%);
   }
 
   a {

+ 2 - 1
packages/app/src/styles/theme/antarctic.scss

@@ -51,7 +51,8 @@ html[dark] {
   $bgcolor-global: $themelight;
   $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: $gray-50;
-  //$bgcolor-highlighted: $grw-marker-yellow;
+  $bgcolor-blinked-section: rgba($primary, 0.15);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
 
   // Font colors
   $color-global: black;

+ 2 - 1
packages/app/src/styles/theme/christmas.scss

@@ -43,7 +43,8 @@ html[dark] {
   // Background colors
   $bgcolor-card: $gray-50;
   $bgcolor-inline-code: $gray-100; //optional
-  // $bgcolor-highlighted: $grw-marker-yellow;
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
 
   // Font colors
   $color-global: #112744;

+ 4 - 2
packages/app/src/styles/theme/default.scss

@@ -22,7 +22,8 @@ html[light] {
   $bgcolor-global: white;
   $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: $gray-50;
-  // $bgcolor-highlighted: $grw-marker-yellow;
+  $bgcolor-blinked-section: rgba($primary, 0.1);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
 
   // Font colors
   $color-global: #112744;
@@ -123,7 +124,8 @@ html[dark] {
   $bgcolor-global: #131418;
   $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: darken($bgcolor-global, 5%);
-  $bgcolor-highlighted: $grw-marker-red;
+  $bgcolor-blinked-section: rgba($primary, 0.4);
+  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
 
   // Font colors
   $color-global: $gray-400;

+ 2 - 1
packages/app/src/styles/theme/future.scss

@@ -11,7 +11,8 @@ html[dark] {
   $bgcolor-global: $themecolor;
   $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: darken($themecolor, 5%);
-  $bgcolor-highlighted: $grw-marker-red;
+  $bgcolor-blinked-section: rgba($primary, 0.4);
+  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
 
   // Font colors
   $color-global: #95abba;

+ 2 - 1
packages/app/src/styles/theme/halloween.scss

@@ -39,7 +39,8 @@ html[dark] {
   $bgcolor-global: #050000;
   $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: $bgcolor-global;
-  $bgcolor-highlighted: $grw-marker-cyan;
+  $bgcolor-blinked-section: rgba($primary, 0.4);
+  $bgcolor-keyword-highlighted: darkviolet;
 
   // Font colors
   $color-global: #e9af2b;

+ 4 - 2
packages/app/src/styles/theme/hufflepuff.scss

@@ -35,7 +35,8 @@ html[light] {
   $bgcolor-global: lighten($themelight, 10%);
   $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: $gray-100;
-  $bgcolor-highlighted: $grw-marker-green;
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  $bgcolor-keyword-highlighted: $grw-marker-green;
 
   // Font colors
   $color-global: $subthemecolor;
@@ -174,7 +175,8 @@ html[dark] {
   // $bgcolor-navbar: #27343b;
   $bgcolor-inline-code: $subthemecolor;
   $bgcolor-card: darken($themedark, 5%);
-  $bgcolor-highlighted: $grw-marker-red;
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  $bgcolor-keyword-highlighted: darken($grw-marker-cyan, 40%);
 
   // Font colors
   $color-global: #efe2cf;

+ 2 - 1
packages/app/src/styles/theme/island.scss

@@ -11,7 +11,8 @@ html[dark] {
   $bgcolor-card: $gray-50;
   $bgcolor-global: lighten($color-themelight, 10%);
   $bgcolor-inline-code: $gray-100; //optional
-  // $bgcolor-highlighted: $grw-marker-yellow;
+  $bgcolor-blinked-section: rgba($primary, 0.3);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
 
   // Font colors
   $color-global: #112744;

+ 3 - 1
packages/app/src/styles/theme/kibela.scss

@@ -35,6 +35,8 @@ html[dark] {
   $bgcolor-global: $themelight;
   $bgcolor-inline-code: lighten($subthemecolor, 70%);
   $bgcolor-card: $lightthemecolor;
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
+
   $color-header: $bgcolor-theme;
   $color-global: #3c4a60;
   $color-link: rgb(74, 109, 204);
@@ -45,7 +47,7 @@ html[dark] {
   $primary: $bgcolor-theme;
   $info: lighten($bgcolor-theme, 20%);
 
-  // $bgcolor-highlighted: $grw-marker-yellow;
+  $bgcolor-blinked-section: rgba($primary, 0.2);
 
   // List Group colors
   $color-list: $color-global;

+ 4 - 2
packages/app/src/styles/theme/mono-blue.scss

@@ -14,7 +14,8 @@ html[light] {
   $bgcolor-global: $themelight;
   $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: darken($themelight, 5%);
-  // $bgcolor-highlighted: $grw-marker-yellow;
+  $bgcolor-blinked-section: rgba($primary, 0.1);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
 
   // Font colors
   $color-global: $themecolor;
@@ -112,7 +113,8 @@ html[dark] {
   $bgcolor-navbar: #27343b;
   $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: darken($themedark, 5%);
-  $bgcolor-highlighted: $grw-marker-green;
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
 
   // Font colors
   $color-global: #d3d4d4;

+ 2 - 1
packages/app/src/styles/theme/nature.scss

@@ -45,7 +45,8 @@ html[dark] {
   $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: #f1ffe4;
   $bgcolor-subnav: #fafafa;
-  // $bgcolor-highlighted: $grw-marker-yellow;
+  $bgcolor-blinked-section: rgba($primary, 0.1);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
 
   // Font colors
   $color-global: #460039;

+ 2 - 1
packages/app/src/styles/theme/spring.scss

@@ -34,7 +34,8 @@ html[dark] {
   $bgcolor-global: white;
   $bgcolor-inline-code: $gray-100; //optional
   $bgcolor-card: $gray-50;
-  $bgcolor-highlighted: $grw-marker-cyan;
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  $bgcolor-keyword-highlighted: $grw-marker-cyan;
 
   // Font colors
   $color-global: black;

+ 2 - 1
packages/app/src/styles/theme/wood.scss

@@ -43,7 +43,8 @@ html[dark] {
   // Background colors
   $bgcolor-global: white;
   $bgcolor-card: #ece8de;
-  $bgcolor-highlighted: $grw-marker-blue;
+  $bgcolor-blinked-section: rgba($primary, 0.3);
+  $bgcolor-keyword-highlighted: $grw-marker-blue;
 
   // Font colors
   // $color-global: black;

+ 1 - 0
packages/codemirror-textlint/.gitignore

@@ -0,0 +1 @@
+/dist

+ 50 - 0
packages/codemirror-textlint/package.json

@@ -0,0 +1,50 @@
+{
+  "name": "@growi/codemirror-textlint",
+  "version": "4.4.4-RC.0",
+  "license": "MIT",
+  "main": "dist/index.js",
+  "scripts": {
+    "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
+    "tsc": "tsc -p tsconfig.build.json",
+    "tsc:w": "yarn tsc -w",
+    "lint": "eslint src --ext .ts",
+    "lint:fix": "eslint src --ext .ts --fix"
+  },
+  "dependencies": {},
+  "devDependencies": {
+    "@textlint-rule/textlint-rule-no-unmatched-pair": "^1.0.8",
+    "@textlint/kernel": "^12.0.2",
+    "@types/codemirror": "^5.60.2",
+    "textlint-message-to-codemirror": "^1.0.0",
+    "textlint-plugin-markdown": "^4.0.6",
+    "textlint-rule-common-misspellings": "^1.0.1",
+    "textlint-rule-date-weekday-mismatch": "^1.0.6",
+    "textlint-rule-en-capitalization": "^2.0.3",
+    "textlint-rule-ja-hiragana-keishikimeishi": "^1.1.0",
+    "textlint-rule-ja-no-abusage": "^3.0.0",
+    "textlint-rule-ja-no-inappropriate-words": "^2.0.0",
+    "textlint-rule-ja-no-mixed-period": "^2.1.1",
+    "textlint-rule-ja-no-redundant-expression": "^4.0.0",
+    "textlint-rule-ja-unnatural-alphabet": "^2.0.1",
+    "textlint-rule-max-comma": "^2.0.2",
+    "textlint-rule-max-kanji-continuous-len": "^1.1.1",
+    "textlint-rule-max-ten": "^4.0.2",
+    "textlint-rule-no-double-negative-ja": "^2.0.0",
+    "textlint-rule-no-doubled-conjunction": "^2.0.2",
+    "textlint-rule-no-doubled-joshi": "^4.0.0",
+    "textlint-rule-no-dropping-the-ra": "^3.0.0",
+    "textlint-rule-no-hankaku-kana": "^1.0.2",
+    "textlint-rule-no-kangxi-radicals": "^0.2.0",
+    "textlint-rule-no-mixed-zenkaku-and-hankaku-alphabet": "^1.0.1",
+    "textlint-rule-no-nfd": "^1.0.2",
+    "textlint-rule-no-surrogate-pair": "^1.0.1",
+    "textlint-rule-no-zero-width-spaces": "^1.0.1",
+    "textlint-rule-period-in-list-item": "^0.3.2",
+    "textlint-rule-prefer-tari-tari": "^1.0.3",
+    "textlint-rule-sentence-length": "^3.0.0",
+    "textlint-rule-use-si-units": "^1.0.2"
+  },
+  "peerDependencies": {
+    "codemirror": "^5.62.3"
+  }
+}

+ 151 - 0
packages/codemirror-textlint/src/index.ts

@@ -0,0 +1,151 @@
+import { TextlintKernel, TextlintKernelRule, TextlintRuleOptions } from '@textlint/kernel';
+import textlintToCodeMirror from 'textlint-message-to-codemirror';
+import textlintRuleNoUnmatchedPair from '@textlint-rule/textlint-rule-no-unmatched-pair';
+import textlintRuleCommonMisspellings from 'textlint-rule-common-misspellings';
+import textlintRuleDateWeekdayMismatch from 'textlint-rule-date-weekday-mismatch';
+import textlintRuleEnCapitalization from 'textlint-rule-en-capitalization';
+import textlintRuleJaHiraganaKeishikimeishi from 'textlint-rule-ja-hiragana-keishikimeishi';
+import textlintRuleJaNoAbusage from 'textlint-rule-ja-no-abusage';
+import textlintRuleJaNoInappropriateWords from 'textlint-rule-ja-no-inappropriate-words';
+import textlintRuleJaNoMixedPeriod from 'textlint-rule-ja-no-mixed-period';
+import textlintRuleJaNoRedundantExpression from 'textlint-rule-ja-no-redundant-expression';
+import textlintRuleJaUnnaturalAlphabet from 'textlint-rule-ja-unnatural-alphabet';
+import textlintRuleMaxComma from 'textlint-rule-max-comma';
+import textlintRuleMaxKanjiContinuousLen from 'textlint-rule-max-kanji-continuous-len';
+import textlintRuleMaxTen from 'textlint-rule-max-ten';
+import textlintRuleNoDoubleNegativeJa from 'textlint-rule-no-double-negative-ja';
+import textlintRuleNoDoubledConjunction from 'textlint-rule-no-doubled-conjunction';
+import textlintRuleNoDoubledJoshi from 'textlint-rule-no-doubled-joshi';
+import textlintRuleNoDroppingTheRa from 'textlint-rule-no-dropping-the-ra';
+import textlintRuleNoHankakuKana from 'textlint-rule-no-hankaku-kana';
+import textlintRuleNoKangxiRadicals from 'textlint-rule-no-kangxi-radicals';
+import textlintRuleNoMixedZenkakuAndHankakuAlphabet from 'textlint-rule-no-mixed-zenkaku-and-hankaku-alphabet';
+import textlintRuleNoNfd from 'textlint-rule-no-nfd';
+import textlintRuleNoSurrogatePair from 'textlint-rule-no-surrogate-pair';
+import textlintRuleNoZeroWidthSpaces from 'textlint-rule-no-zero-width-spaces';
+import textlintRulePeriodInListItem from 'textlint-rule-period-in-list-item';
+import textlintRulePreferTariTari from 'textlint-rule-prefer-tari-tari';
+import textlintRuleSentenceLength from 'textlint-rule-sentence-length';
+import textlintRuleUseSiUnits from 'textlint-rule-use-si-units';
+
+import { AsyncLinter, Annotation } from 'codemirror/addon/lint/lint';
+import { loggerFactory } from './utils/logger';
+
+type RulesConfigObj = {
+  name: string,
+  options?: unknown,
+  isEnabled?: boolean,
+}
+
+type RuleExtension = {
+  ext: string
+}
+
+const ruleModulesList = {
+  'no-unmatched-pair': textlintRuleNoUnmatchedPair,
+  'common-misspellings': textlintRuleCommonMisspellings,
+  'date-weekday-mismatch': textlintRuleDateWeekdayMismatch,
+  'en-capitalization': textlintRuleEnCapitalization,
+  'ja-hiragana-keishikimeishi': textlintRuleJaHiraganaKeishikimeishi,
+  'ja-no-abusage': textlintRuleJaNoAbusage,
+  'ja-no-inappropriate-words': textlintRuleJaNoInappropriateWords,
+  'ja-no-mixed-period': textlintRuleJaNoMixedPeriod,
+  'ja-no-redundant-expression': textlintRuleJaNoRedundantExpression,
+  'ja-unnatural-alphabet': textlintRuleJaUnnaturalAlphabet,
+  'max-comma': textlintRuleMaxComma,
+  'max-kanji-continuous-len': textlintRuleMaxKanjiContinuousLen,
+  'max-ten': textlintRuleMaxTen,
+  'no-double-negative-ja': textlintRuleNoDoubleNegativeJa,
+  'no-doubled-conjunction': textlintRuleNoDoubledConjunction,
+  'no-doubled-joshi': textlintRuleNoDoubledJoshi,
+  'no-dropping-the-ra': textlintRuleNoDroppingTheRa,
+  'no-hankaku-kana': textlintRuleNoHankakuKana,
+  'no-kangxi-radicals': textlintRuleNoKangxiRadicals,
+  'no-mixed-zenkaku-and-hankaku-alphabet': textlintRuleNoMixedZenkakuAndHankakuAlphabet,
+  'no-nfd': textlintRuleNoNfd,
+  'no-surrogate-pair': textlintRuleNoSurrogatePair,
+  'no-zero-width-spaces': textlintRuleNoZeroWidthSpaces,
+  'period-in-list-item': textlintRulePeriodInListItem,
+  'prefer-tari-tari': textlintRulePreferTariTari,
+  'sentence-length': textlintRuleSentenceLength,
+  'use-si-units': textlintRuleUseSiUnits,
+};
+
+const logger = loggerFactory('growi:codemirror:codemirror-textlint');
+const kernel = new TextlintKernel();
+const textlintOption: TextlintRuleOptions<RuleExtension> = {
+  ext: '.md',
+  plugins: [
+    {
+      pluginId: 'markdown',
+      plugin: require('textlint-plugin-markdown'),
+    },
+  ],
+};
+
+const createSetupRules = (rules, ruleOptions): TextlintKernelRule[] => (
+  Object.keys(rules).map(ruleName => (
+    {
+      ruleId: ruleName,
+      rule: rules[ruleName],
+      options: ruleOptions[ruleName],
+    }
+  ))
+);
+
+
+export const createValidator = (rulesConfigArray: RulesConfigObj[] | null): AsyncLinter<RulesConfigObj[] | null> => {
+  if (rulesConfigArray != null) {
+    const filteredConfigArray = rulesConfigArray
+      .filter((rule) => {
+        if (ruleModulesList[rule.name] == null) {
+          logger.error(`Textlint rule ${rule.name} is not installed`);
+        }
+        return (ruleModulesList[rule.name] != null && rule.isEnabled !== false);
+      });
+
+    const rules = filteredConfigArray
+      .reduce((rules, rule) => {
+        rules[rule.name] = ruleModulesList[rule.name];
+        return rules;
+      }, {});
+
+    const rulesOption = filteredConfigArray
+      .reduce((rules, rule) => {
+        rules[rule.name] = rule.options || {};
+        return rules;
+      }, {});
+
+    Object.assign(
+      textlintOption,
+      { rules: createSetupRules(rules, rulesOption) },
+    );
+  }
+
+  const defaultSetupRules: TextlintKernelRule[] = Object.entries(ruleModulesList)
+    .map(ruleName => ({
+      ruleId: ruleName[0],
+      rule: ruleName[1],
+    }));
+
+  if (rulesConfigArray == null) {
+    Object.assign(
+      textlintOption,
+      { rules: defaultSetupRules },
+    );
+  }
+
+  return (text, callback) => {
+    if (!text) {
+      callback([]);
+      return;
+    }
+    kernel
+      .lintText(text, textlintOption)
+      .then((result) => {
+        const lintMessages = result.messages;
+        const lintErrors: Annotation[] = lintMessages.map(textlintToCodeMirror);
+        callback(lintErrors);
+      });
+  };
+};

+ 9 - 0
packages/codemirror-textlint/src/utils/logger/index.ts

@@ -0,0 +1,9 @@
+import Logger from 'bunyan';
+import { createLogger } from 'universal-bunyan';
+
+export const loggerFactory = function(name: string): Logger {
+  return createLogger({
+    name,
+    config: { default: 'info' },
+  });
+};

+ 8 - 0
packages/codemirror-textlint/tsconfig.base.json

@@ -0,0 +1,8 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+  },
+  "include": [
+    "src"
+  ]
+}

+ 17 - 0
packages/codemirror-textlint/tsconfig.build.json

@@ -0,0 +1,17 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "rootDir": "./src",
+    "outDir": "dist",
+    "declaration": true,
+    "noResolve": false,
+    "preserveConstEnums": true,
+    "sourceMap": true,
+    "noEmit": false,
+    "inlineSources": true,
+
+    "baseUrl": ".",
+    "paths": {
+    }
+  }
+}

+ 6 - 0
packages/codemirror-textlint/tsconfig.json

@@ -0,0 +1,6 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": "."
+  }
+}

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "4.4.3",
+  "version": "4.4.4-RC.0",
   "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": "4.4.3",
+  "version": "4.4.4-RC.0",
   "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": "4.4.3",
+  "version": "4.4.4-RC.0",
   "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": "4.4.3",
+  "version": "4.4.4-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "4.4.3",
+  "version": "4.4.4-RC.0",
   "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": "4.4.3",
+  "version": "4.4.4-RC.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -23,7 +23,7 @@
   "// comments for dependencies": {},
   "dependencies": {
     "@godaddy/terminus": "^4.8.0",
-    "@growi/slack": "^4.4.3",
+    "@growi/slack": "^4.4.4-RC.0",
     "@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": "4.4.3",
+  "version": "4.4.4-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

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


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