Browse Source

Merge branch 'master' into feat/7135-slackbot-command-forward-compatibility

Taichi Masuyama 4 years ago
parent
commit
2fec075333
100 changed files with 773 additions and 265 deletions
  1. 1 0
      .devcontainer/devcontainer.json
  2. 0 1
      .devcontainer/docker-compose.yml
  3. 4 0
      .eslintrc.js
  4. 2 2
      .github/workflows/ci-slackbot-proxy.yml
  5. 12 9
      .github/workflows/ci.yml
  6. 1 1
      .github/workflows/release-rc.yml
  7. 2 2
      .github/workflows/release.yml
  8. 4 0
      .markdownlint.yml
  9. 20 0
      .vscode/launch.json
  10. 10 0
      CHANGES.md
  11. 22 4
      package.json
  12. 2 2
      packages/app/.env.development
  13. 1 1
      packages/app/.env.production
  14. 1 0
      packages/app/.eslintignore
  15. 1 1
      packages/app/.gitignore
  16. 22 14
      packages/app/bin/generate-plugin-definitions-source.ts
  17. 2 0
      packages/app/config/ci/.env.local.for-ci
  18. 18 13
      packages/app/docker/Dockerfile
  19. 1 0
      packages/app/jest.config.js
  20. 8 18
      packages/app/package.json
  21. 22 22
      packages/app/resource/locales/en_US/sandbox.md
  22. 20 1
      packages/app/src/client/models/Linker.js
  23. 4 2
      packages/app/src/client/plugin.js
  24. 1 1
      packages/app/src/client/services/AdminGitHubSecurityContainer.js
  25. 1 1
      packages/app/src/client/services/AdminGoogleSecurityContainer.js
  26. 1 1
      packages/app/src/client/services/AdminOidcSecurityContainer.js
  27. 1 1
      packages/app/src/client/services/AdminSamlSecurityContainer.js
  28. 1 1
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  29. 3 1
      packages/app/src/client/services/PageContainer.js
  30. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  31. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  32. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  33. 1 1
      packages/app/src/components/Admin/Users/UserTable.jsx
  34. 3 1
      packages/app/src/components/ComparePathsTable.jsx
  35. 3 1
      packages/app/src/components/ContentLinkButtons.jsx
  36. 1 1
      packages/app/src/components/CreateTemplateModal.jsx
  37. 3 1
      packages/app/src/components/DuplicatedPathsTable.jsx
  38. 1 1
      packages/app/src/components/Fab.jsx
  39. 4 2
      packages/app/src/components/Navbar/AuthorInfo.jsx
  40. 1 1
      packages/app/src/components/Navbar/GrowiSubNavigation.jsx
  41. 1 1
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  42. 3 1
      packages/app/src/components/Page/CopyDropdown.jsx
  43. 3 1
      packages/app/src/components/Page/PageManagement.jsx
  44. 1 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  45. 1 1
      packages/app/src/components/PageAttachment/DeleteAttachmentModal.jsx
  46. 1 1
      packages/app/src/components/PageAttachment/PageAttachmentList.jsx
  47. 1 1
      packages/app/src/components/PageComment/Comment.jsx
  48. 1 1
      packages/app/src/components/PageComment/CommentEditor.jsx
  49. 1 1
      packages/app/src/components/PageComment/DeleteCommentModal.jsx
  50. 6 4
      packages/app/src/components/PageCreateModal.jsx
  51. 10 0
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  52. 7 1
      packages/app/src/components/PageEditor/Editor.jsx
  53. 7 9
      packages/app/src/components/PageEditor/EditorIcon.jsx
  54. 6 19
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  55. 1 1
      packages/app/src/components/PageHistory/Revision.jsx
  56. 2 3
      packages/app/src/components/PageList/Page.jsx
  57. 0 24
      packages/app/src/components/PageList/PagePath.jsx
  58. 1 1
      packages/app/src/components/PagePathAutoComplete.jsx
  59. 3 1
      packages/app/src/components/RevisionComparer/RevisionComparer.jsx
  60. 2 3
      packages/app/src/components/SearchTypeahead.jsx
  61. 2 2
      packages/app/src/components/Sidebar/RecentChanges.jsx
  62. 1 1
      packages/app/src/components/User/UserInfo.jsx
  63. 1 1
      packages/app/src/components/User/UserPictureList.jsx
  64. 1 1
      packages/app/src/migrations/20191126173016-adjust-pages-path.js
  65. 3 6
      packages/app/src/models/linked-page-path.js
  66. 6 1
      packages/app/src/server/crowi/express-init.js
  67. 1 1
      packages/app/src/server/crowi/index.js
  68. 3 4
      packages/app/src/server/middlewares/admin-required.js
  69. 5 3
      packages/app/src/server/models/page.js
  70. 38 0
      packages/app/src/server/plugins/plugin-utils-v4.ts
  71. 15 7
      packages/app/src/server/plugins/plugin-utils.js
  72. 16 11
      packages/app/src/server/plugins/plugin.service.js
  73. 2 2
      packages/app/src/server/routes/apiv3/healthcheck.js
  74. 3 2
      packages/app/src/server/routes/apiv3/page.js
  75. 7 2
      packages/app/src/server/routes/apiv3/pages.js
  76. 2 2
      packages/app/src/server/routes/apiv3/search.js
  77. 2 2
      packages/app/src/server/routes/apiv3/statistics.js
  78. 2 1
      packages/app/src/server/routes/page.js
  79. 2 2
      packages/app/src/server/service/app.ts
  80. 1 1
      packages/app/src/server/service/customize.ts
  81. 3 1
      packages/app/src/server/service/global-notification/global-notification-slack.js
  82. 3 1
      packages/app/src/server/service/page.js
  83. 2 13
      packages/app/src/server/service/socket-io.js
  84. 3 1
      packages/app/src/server/util/createGrowiPagesFromImports.js
  85. 1 1
      packages/app/src/server/views/layout-growi/base/layout.html
  86. 1 0
      packages/app/src/styles/_on-edit.scss
  87. 2 5
      packages/app/tsconfig.base.json
  88. 0 1
      packages/app/tsconfig.build.client.json
  89. 1 0
      packages/core/.eslintignore
  90. 1 0
      packages/core/.gitignore
  91. 27 0
      packages/core/README.md
  92. 60 0
      packages/core/jest.config.js
  93. 24 0
      packages/core/package.json
  94. 21 0
      packages/core/src/index.js
  95. 2 4
      packages/core/src/models/devided-page-path.js
  96. 11 0
      packages/core/src/plugin/interfaces/plugin-definition-v4.ts
  97. 14 0
      packages/core/src/plugin/model/tag-context.js
  98. 71 0
      packages/core/src/plugin/service/tag-cache-manager.js
  99. 59 0
      packages/core/src/plugin/util/args-parser.js
  100. 88 0
      packages/core/src/plugin/util/custom-tag-utils.js

+ 1 - 0
.devcontainer/devcontainer.json

@@ -16,6 +16,7 @@
 	"extensions": [
 	"extensions": [
 		"dbaeumer.vscode-eslint",
 		"dbaeumer.vscode-eslint",
 		"eamodio.gitlens",
 		"eamodio.gitlens",
+    "firsttris.vscode-jest-runner",
 		"msjsdiag.debugger-for-chrome",
 		"msjsdiag.debugger-for-chrome",
 		"firefox-devtools.vscode-firefox-debug",
 		"firefox-devtools.vscode-firefox-debug",
 		"editorconfig.editorconfig",
 		"editorconfig.editorconfig",

+ 0 - 1
.devcontainer/docker-compose.yml

@@ -25,7 +25,6 @@ services:
       - ..:/workspace/growi:delegated
       - ..:/workspace/growi:delegated
       - node_modules:/workspace/growi/node_modules
       - node_modules:/workspace/growi/node_modules
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
-      - ../../node_modules:/workspace/node_modules:delegated
 
 
     tty: true
     tty: true
 
 

+ 4 - 0
.eslintrc.js

@@ -26,5 +26,9 @@ module.exports = {
         FunctionExpression: { body: 1, parameters: 2 },
         FunctionExpression: { body: 1, parameters: 2 },
       },
       },
     ],
     ],
+    'jest/no-standalone-expect': [
+      'error',
+      { additionalTestBlockFunctions: ['each.test'] },
+    ],
   },
   },
 };
 };

+ 2 - 2
.github/workflows/ci-slackbot-proxy.yml

@@ -120,7 +120,7 @@ jobs:
     - name: yarn dev:ci
     - name: yarn dev:ci
       working-directory: ./packages/slackbot-proxy
       working-directory: ./packages/slackbot-proxy
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
         yarn dev:ci
       env:
       env:
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql
@@ -211,7 +211,7 @@ jobs:
     - name: yarn start:prod:ci
     - name: yarn start:prod:ci
       working-directory: ./packages/slackbot-proxy
       working-directory: ./packages/slackbot-proxy
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn start:prod:ci
         yarn start:prod:ci
       env:
       env:
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql

+ 12 - 9
.github/workflows/ci.yml

@@ -51,10 +51,10 @@ jobs:
         yarn list --depth=0
         yarn list --depth=0
     - name: lerna run lint for plugins
     - name: lerna run lint for plugins
       run: |
       run: |
-        yarn lerna run lint --scope @growi/plugin-pukiwiki-like-linker
+        yarn lerna run lint --scope @growi/plugin-*
     - name: lerna run lint for app
     - name: lerna run lint for app
       run: |
       run: |
-        yarn lerna run lint --scope @growi/app
+        yarn lerna run lint --scope @growi/app --scope @growi/core --scope @growi/ui
 
 
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
@@ -130,6 +130,12 @@ jobs:
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi_test
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi_test
 
 
+    - name: Upload coverage report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Coverage Report
+        path: packages/app/coverage
+
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
       if: failure()
       if: failure()
@@ -206,7 +212,7 @@ jobs:
     - name: yarn dev:ci
     - name: yarn dev:ci
       working-directory: ./packages/app
       working-directory: ./packages/app
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
         yarn dev:ci
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
@@ -281,7 +287,7 @@ jobs:
         yarn list --depth=0
         yarn list --depth=0
     - name: Build
     - name: Build
       run: |
       run: |
-        yarn lerna run build --scope @growi/slack --scope @growi/app
+        yarn lerna run build --scope @growi/core --scope @growi/slack --scope @growi/plugin-* --scope @growi/app
     - name: lerna bootstrap --production
     - name: lerna bootstrap --production
       run: |
       run: |
         npx lerna bootstrap -- --production
         npx lerna bootstrap -- --production
@@ -297,20 +303,17 @@ jobs:
     - name: yarn server:ci
     - name: yarn server:ci
       working-directory: ./packages/app
       working-directory: ./packages/app
       run: |
       run: |
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn server:ci
         yarn server:ci
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
     - name: yarn server:ci with MongoDB 3.6
     - name: yarn server:ci with MongoDB 3.6
       working-directory: ./packages/app
       working-directory: ./packages/app
       run: |
       run: |
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn server:ci
         yarn server:ci
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
-    - name: Upload report as artifact
-      uses: actions/upload-artifact@v2
-      with:
-        name: Report
-        path: report
 
 
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master

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

@@ -55,7 +55,7 @@ jobs:
       uses: docker/build-push-action@v2
       uses: docker/build-push-action@v2
       with:
       with:
         context: .
         context: .
-        file: ./docker/Dockerfile
+        file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         platforms: linux/amd64
         push: true
         push: true
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-from: type=local,src=/tmp/.buildx-cache

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

@@ -113,7 +113,7 @@ jobs:
       uses: docker/build-push-action@v2
       uses: docker/build-push-action@v2
       with:
       with:
         context: .
         context: .
-        file: ./docker/Dockerfile
+        file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         platforms: linux/amd64
         push: true
         push: true
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-from: type=local,src=/tmp/.buildx-cache
@@ -131,7 +131,7 @@ jobs:
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         repository: weseek/growi
         repository: weseek/growi
-        readme-filepath: ./docker/README.md
+        readme-filepath: ./packages/app/docker/README.md
 
 
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-release-slack-notification@master
       uses: weseek/ghaction-release-slack-notification@master

+ 4 - 0
.markdownlint.yml

@@ -7,3 +7,7 @@ no-multiple-blanks: false
 no-duplicate-heading: false
 no-duplicate-heading: false
 no-inline-html: false
 no-inline-html: false
 no-trailing-punctuation: false
 no-trailing-punctuation: false
+
+MD002: false
+MD012: false
+MD041: false

+ 20 - 0
.vscode/launch.json

@@ -44,6 +44,26 @@
         "url": "http://localhost:3000",
         "url": "http://localhost:3000",
         "webRoot": "${workspaceFolder}/packages/app/public",
         "webRoot": "${workspaceFolder}/packages/app/public",
         "pathMappings": [
         "pathMappings": [
+          {
+            "url": "webpack:///core",
+            "path": "${workspaceFolder}/packages/core"
+          },
+          {
+            "url": "webpack:///plugin-attachment-refs",
+            "path": "${workspaceFolder}/packages/plugin-attachment-refs"
+          },
+          {
+            "url": "webpack:///plugin-pukiwiki-like-linker",
+            "path": "${workspaceFolder}/packages/plugin-pukiwiki-like-linker"
+          },
+          {
+            "url": "webpack:///plugin-lsx",
+            "path": "${workspaceFolder}/packages/plugin-lsx"
+          },
+          {
+            "url": "webpack:///ui",
+            "path": "${workspaceFolder}/packages/ui"
+          },
           {
           {
             "url": "webpack:///src",
             "url": "webpack:///src",
             "path": "${workspaceFolder}/packages/app/src"
             "path": "${workspaceFolder}/packages/app/src"

+ 10 - 0
CHANGES.md

@@ -2,10 +2,20 @@
 
 
 ## v4.3.3-RC
 ## v4.3.3-RC
 
 
+* Improvement: Add attachment button in editor navbar
+* Fix: Encode spaces in page path in LinkEditModal
+* Fix: Layout is broken when editing users page ([#4128](https://github.com/weseek/growi/issues/4128))
+* Support: Create @growi/core package
+* Support: Create @growi/ui package
+* Support: Improve error handling for @growi/slackbot-proxy
+* Support: Include official plugins as sub packages
 * Support: Upgrade libs
 * Support: Upgrade libs
     * @slack/web-api
     * @slack/web-api
+    * date-fns
     * escape-string-regexp
     * escape-string-regexp
+    * helmet
     * morgan
     * morgan
+    * socket.io
 
 
 ## v4.3.2
 ## v4.3.2
 
 

+ 22 - 4
package.json

@@ -21,13 +21,17 @@
   },
   },
   "private": true,
   "private": true,
   "workspaces": {
   "workspaces": {
-    "packages": ["packages/*"],
-    "nohoist": ["**/slackbot-proxy/bootstrap"]
+    "packages": [
+      "packages/*"
+    ],
+    "nohoist": [
+      "**/slackbot-proxy/bootstrap"
+    ]
   },
   },
   "scripts": {
   "scripts": {
     "start": "yarn app:server",
     "start": "yarn app:server",
     "prestart": "yarn app:build",
     "prestart": "yarn app:build",
-    "app:build": "yarn lerna run build --scope @growi/app --scope @growi/slack --scope @growi/plugin-pukiwiki-like-linker",
+    "app:build": "yarn lerna run build --scope @growi/app --scope @growi/slack --scope @growi/plugin-*",
     "app:server": "yarn lerna run server --scope @growi/app",
     "app:server": "yarn lerna run server --scope @growi/app",
     "slackbot-proxy:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
@@ -37,8 +41,17 @@
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
   },
   },
   "dependencies": {
   "dependencies": {
+    "cross-env": "^7.0.0",
+    "dotenv-flow": "^3.2.0",
+    "npm-run-all": "^4.1.5",
+    "ts-node": "^9.1.1",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@types/jest": "^26.0.22",
+    "@types/node": "^14.14.35",
+    "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "eslint": "^7.31.0",
     "eslint": "^7.31.0",
@@ -48,7 +61,12 @@
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react-hooks": "^4.2.0",
     "eslint-plugin-react-hooks": "^4.2.0",
-    "lerna": "^4.0.0"
+    "jest": "^27.0.6",
+    "jest-date-mock": "^1.0.8",
+    "jest-localstorage-mock": "^2.4.14",
+    "lerna": "^4.0.0",
+    "rewire": "^5.0.0",
+    "ts-jest": "^27.0.4"
   },
   },
   "engines": {
   "engines": {
     "node": "^12 || ^14",
     "node": "^12 || ^14",

+ 2 - 2
packages/app/.env.development

@@ -3,7 +3,7 @@
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 ##
 FILE_UPLOAD=mongodb
 FILE_UPLOAD=mongodb
-# MONGO_GRIDFS_TOTAL_LIMIT=10485760 # 10MB
+# MONGO_GRIDFS_TOTAL_LIMIT=10485760
 MATHJAX=1
 MATHJAX=1
 # NO_CDN=true
 # NO_CDN=true
 MONGO_URI="mongodb://mongo:27017/growi"
 MONGO_URI="mongodb://mongo:27017/growi"
@@ -17,7 +17,7 @@ HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 # PUBLISH_OPEN_API=true
 # PUBLISH_OPEN_API=true
 # USER_UPPER_LIMIT=0
 # USER_UPPER_LIMIT=0
 # DEV_HTTPS=true
 # DEV_HTTPS=true
-# FORCE_WIKI_MODE=private # 'public', 'private', undefined
+# FORCE_WIKI_MODE=private
 # PROMSTER_ENABLED=true
 # PROMSTER_ENABLED=true
 # SLACK_SIGNING_SECRET=''
 # SLACK_SIGNING_SECRET=''
 # SLACK_BOT_TOKEN=''
 # SLACK_BOT_TOKEN=''

+ 1 - 1
packages/app/.env.production

@@ -2,4 +2,4 @@
 ## Handled by Next.js with dotenv or dotenv-flow
 ## Handled by Next.js with dotenv or dotenv-flow
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 ##
-FORMAT_NODE_LOG=false # default: true
+FORMAT_NODE_LOG=false

+ 1 - 0
packages/app/.eslintignore

@@ -1,3 +1,4 @@
+/dist/**
 /public/**
 /public/**
 /src/client/legacy/thirdparty-js/**
 /src/client/legacy/thirdparty-js/**
 /src/client/util/reveal/plugins/markdown.js
 /src/client/util/reveal/plugins/markdown.js

+ 1 - 1
packages/app/.gitignore

@@ -13,7 +13,7 @@
 
 
 # dist (for GROWI v4.x and below)
 # dist (for GROWI v4.x and below)
 /public/*.chunk.js
 /public/*.chunk.js
-/public/*.chunk.js.LICENSE
+/public/*.chunk.js.LICENSE.txt
 /public/*.bundle.js
 /public/*.bundle.js
 /public/manifest.json
 /public/manifest.json
 /public/dll
 /public/dll

+ 22 - 14
packages/app/bin/generate-plugin-definitions-source.ts

@@ -7,6 +7,8 @@ import fs from 'graceful-fs';
 import normalize from 'normalize-path';
 import normalize from 'normalize-path';
 import swig from 'swig-templates';
 import swig from 'swig-templates';
 
 
+import { PluginDefinitionV4 } from '@growi/core';
+
 import PluginUtils from '../src/server/plugins/plugin-utils';
 import PluginUtils from '../src/server/plugins/plugin-utils';
 import loggerFactory from '../src/utils/logger';
 import loggerFactory from '../src/utils/logger';
 import { resolveFromRoot } from '../src/utils/project-dir-utils';
 import { resolveFromRoot } from '../src/utils/project-dir-utils';
@@ -23,26 +25,32 @@ const OUT = resolveFromRoot('tmp/plugins/plugin-definitions.js');
 const pluginNames: string[] = pluginUtils.listPluginNames();
 const pluginNames: string[] = pluginUtils.listPluginNames();
 logger.info('Detected plugins: ', pluginNames);
 logger.info('Detected plugins: ', pluginNames);
 
 
-// get definitions
-const definitions = pluginNames
-  .map((name) => {
-    return pluginUtils.generatePluginDefinition(name, true);
-  })
-  .map((definition) => {
-    if (definition == null) {
-      return null;
+async function main(): Promise<void> {
+
+  // get definitions
+  const definitions: PluginDefinitionV4[] = [];
+  for (const pluginName of pluginNames) {
+    // eslint-disable-next-line no-await-in-loop
+    const definition = await pluginUtils.generatePluginDefinition(pluginName, true);
+    if (definition != null) {
+      definitions.push(definition);
     }
     }
+  }
 
 
+  definitions.map((definition) => {
     // convert backslash to slash
     // convert backslash to slash
     definition.entries = definition.entries.map((entryPath) => {
     definition.entries = definition.entries.map((entryPath) => {
       return normalize(entryPath);
       return normalize(entryPath);
     });
     });
     return definition;
     return definition;
-  })
-  .filter(definition => definition != null);
+  });
+
+  const compiledTemplate = swig.compileFile(TEMPLATE);
+  const code = compiledTemplate({ definitions });
+
+  // write
+  fs.writeFileSync(OUT, code);
 
 
-const compiledTemplate = swig.compileFile(TEMPLATE);
-const code = compiledTemplate({ definitions });
+}
 
 
-// write
-fs.writeFileSync(OUT, code);
+main();

+ 2 - 0
packages/app/config/ci/.env.local.for-ci

@@ -1,2 +1,4 @@
+FORMAT_NODE_LOG=true
+
 # disable Elasticsearch
 # disable Elasticsearch
 ELASTICSEARCH_URI=
 ELASTICSEARCH_URI=

+ 18 - 13
packages/app/docker/Dockerfile

@@ -17,18 +17,21 @@ COPY ./package.json .
 COPY ./yarn.lock .
 COPY ./yarn.lock .
 COPY ./lerna.json .
 COPY ./lerna.json .
 COPY ./packages/app/package.json packages/app/
 COPY ./packages/app/package.json packages/app/
-COPY ./packages/slack/package.json packages/slack/
+COPY ./packages/core/package.json packages/core/
+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/
 COPY ./packages/plugin-pukiwiki-like-linker/package.json packages/plugin-pukiwiki-like-linker/
+COPY ./packages/slack/package.json packages/slack/
+COPY ./packages/ui/package.json packages/ui/
 
 
 # setup
 # setup
 RUN yarn config set network-timeout 300000
 RUN yarn config set network-timeout 300000
 RUN npx lerna bootstrap
 RUN npx lerna bootstrap
 
 
 # make artifacts
 # make artifacts
-RUN tar cf node_modules.tar node_modules \
-  packages/app/node_modules \
-  packages/slack/node_modules \
-  packages/plugin-pukiwiki-like-linker/node_modules
+RUN tar cf node_modules.tar \
+  node_modules \
+  packages/*/node_modules
 
 
 
 
 
 
@@ -41,10 +44,9 @@ FROM deps-resolver AS deps-resolver-prod
 RUN yarn install --production
 RUN yarn install --production
 
 
 # make artifacts
 # make artifacts
-RUN tar cf node_modules.tar node_modules \
-  packages/app/node_modules \
-  packages/slack/node_modules \
-  packages/plugin-pukiwiki-like-linker/node_modules
+RUN tar cf node_modules.tar \
+  node_modules \
+  packages/*/node_modules
 
 
 
 
 
 
@@ -92,9 +94,13 @@ COPY ./yarn.lock ./
 COPY ./lerna.json ./
 COPY ./lerna.json ./
 COPY ./tsconfig.base.json ./
 COPY ./tsconfig.base.json ./
 # copy all related packages
 # copy all related packages
-COPY packages/slack packages/slack
 COPY packages/app packages/app
 COPY packages/app packages/app
+COPY packages/core packages/core
+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
 COPY packages/plugin-pukiwiki-like-linker packages/plugin-pukiwiki-like-linker
+COPY packages/slack packages/slack
+COPY packages/ui packages/ui
 
 
 # build
 # build
 RUN yarn lerna run build
 RUN yarn lerna run build
@@ -113,9 +119,8 @@ RUN tar cf packages.tar \
   packages/app/.env.production \
   packages/app/.env.production \
   packages/app/tsconfig.base.json \
   packages/app/tsconfig.base.json \
   packages/app/tsconfig.json \
   packages/app/tsconfig.json \
-  packages/slack/package.json \
-  packages/slack/dist
-
+  packages/*/package.json \
+  packages/*/dist
 
 
 
 
 
 

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

@@ -5,6 +5,7 @@
 const MODULE_NAME_MAPPING = {
 const MODULE_NAME_MAPPING = {
   '^\\^/(.+)$': '<rootDir>/$1',
   '^\\^/(.+)$': '<rootDir>/$1',
   '^~/(.+)$': '<rootDir>/src/$1',
   '^~/(.+)$': '<rootDir>/src/$1',
+  '^@growi/(.+)$': '<rootDir>/../$1/src',
 };
 };
 
 
 module.exports = {
 module.exports = {

+ 8 - 18
packages/app/package.json

@@ -55,7 +55,9 @@
   "dependencies": {
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
+    "@growi/plugin-attachment-refs": "^4.3.3-RC",
     "@growi/plugin-pukiwiki-like-linker": "^4.3.3-RC",
     "@growi/plugin-pukiwiki-like-linker": "^4.3.3-RC",
+    "@growi/plugin-lsx": "^4.3.3-RC",
     "@growi/slack": "^4.3.3-RC",
     "@growi/slack": "^4.3.3-RC",
     "@kobalab/socket.io-session": "^1.0.3",
     "@kobalab/socket.io-session": "^1.0.3",
     "@promster/express": "^5.0.1",
     "@promster/express": "^5.0.1",
@@ -77,12 +79,10 @@
     "connect-mongo": "^4.4.1",
     "connect-mongo": "^4.4.1",
     "connect-redis": "^4.0.4",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
-    "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
     "csrf": "^3.1.0",
-    "date-fns": "^2.0.0",
+    "date-fns": "^2.23.0",
     "detect-indent": "^6.0.0",
     "detect-indent": "^6.0.0",
     "diff": "^5.0.0",
     "diff": "^5.0.0",
-    "dotenv-flow": "^3.2.0",
     "elasticsearch": "^16.0.0",
     "elasticsearch": "^16.0.0",
     "entities": "^2.0.0",
     "entities": "^2.0.0",
     "esa-nodejs": "^0.0.7",
     "esa-nodejs": "^0.0.7",
@@ -96,9 +96,8 @@
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
     "growi-commons": "^5.0.4",
-    "growi-plugin-attachment-refs": "^2.0.2",
-    "growi-plugin-lsx": "^4.0.3",
-    "helmet": "^3.13.0",
+    "helmet": "^4.6.0",
+    "nocache": "^3.0.1",
     "http-errors": "~1.6.2",
     "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-express-middleware": "^2.0.0",
@@ -118,7 +117,6 @@
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "nodemailer": "^6.6.2",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
-    "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
     "openid-client": "=2.5.0",
     "passport": "^0.4.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
@@ -134,14 +132,11 @@
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
-    "socket.io": "^2.3.0",
+    "socket.io": "^4.0.0",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "swagger-jsdoc": "^3.4.0",
     "swagger-jsdoc": "^3.4.0",
     "swig-templates": "^2.0.2",
     "swig-templates": "^2.0.2",
-    "ts-node": "^9.1.1",
-    "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
@@ -159,11 +154,11 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
     "@atlaskit/navigation-next": "^8.0.5",
+    "@growi/ui": "^4.3.3-RC",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
     "@types/multer": "^1.4.5",
     "@types/multer": "^1.4.5",
-    "@types/node": "^14.14.35",
     "@types/react-dom": "^17.0.9",
     "@types/react-dom": "^17.0.9",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",
     "bootstrap": "^4.5.0",
     "bootstrap": "^4.5.0",
@@ -183,8 +178,6 @@
     "hard-source-webpack-plugin": "^0.13.1",
     "hard-source-webpack-plugin": "^0.13.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
-    "jest": "^27.0.6",
-    "jest-date-mock": "^1.0.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
@@ -222,15 +215,13 @@
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-i18next": "^11.1.0",
     "react-i18next": "^11.1.0",
-    "react-images": "1.0.0",
-    "react-motion": "^0.5.2",
     "react-waypoint": "^10.1.0",
     "react-waypoint": "^10.1.0",
     "reactstrap": "^8.9.0",
     "reactstrap": "^8.9.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
     "sass-loader": "^8.0.0",
     "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
-    "socket.io-client": "^2.3.0",
+    "socket.io-client": "^4.0.0",
     "sticky-events": "^3.1.3",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
     "styled-components": "^5.0.1",
@@ -240,7 +231,6 @@
     "terser-webpack-plugin": "^4.1.0",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",
-    "ts-jest": "^27.0.4",
     "ts-loader": "^8.3.0",
     "ts-loader": "^8.3.0",
     "ts-node-dev": "^1.1.6",
     "ts-node-dev": "^1.1.6",
     "tsc-alias": "^1.2.9",
     "tsc-alias": "^1.2.9",

+ 22 - 22
packages/app/resource/locales/en_US/sandbox.md

@@ -37,7 +37,7 @@ Add one `#` per level at the start of the line
 
 
 ## Block paragraph
 ## Block paragraph
 
 
-Pararaphs are created by inserting a newline character
+Paragraphs are created by inserting a newline character
 A paragraph can be created by pressing Enter at the end of the previous paragraph.
 A paragraph can be created by pressing Enter at the end of the previous paragraph.
 
 
 ```
 ```
@@ -53,17 +53,17 @@ paragraph2
 ## Br new line
 ## Br new line
 
 
 Add two spaces before break.
 Add two spaces before break.
-***This behaviour can be modified in the options menu.***
+***This behavior can be modified in the options menu.***
 
 
 ```
 ```
-hoge
-fuga(two spaces)
-piyo
+foo
+bar(two spaces)
+baz
 ```
 ```
 
 
-hoge
-fuga
-piyo
+foo
+bar
+baz
 
 
 ## Blockquotes
 ## Blockquotes
 
 
@@ -84,7 +84,7 @@ Add one `>` per level at the start of the line
 Wrap code with three back quotes or tildes.
 Wrap code with three back quotes or tildes.
 
 
 ```
 ```
-print 'hoge'
+print 'foo'
 ```
 ```
 
 
 ### Syntax highlight and file name
 ### Syntax highlight and file name
@@ -131,16 +131,16 @@ This is  `Inline Code`.
 Code blocks should be preceded by four spaces or one tab.
 Code blocks should be preceded by four spaces or one tab.
 
 
 ```
 ```
-    class Hoge
-        def hoge
-            print 'hoge'
+    class Foo
+        def foo
+            print 'foo'
         end
         end
     end
     end
 ```
 ```
 
 
-    class Hoge
-        def hoge
-            print 'hoge'
+    class Foo
+        def foo
+            print 'foo'
         end
         end
     end
     end
 
 
@@ -166,7 +166,7 @@ ___
 
 
 ### Italic
 ### Italic
 
 
-To italicize text, add One asterisk or underscores before and after a word or phrase.
+To italicize text, add one asterisk or underscores before and after a word or phrase.
 
 
 ```
 ```
 This is *Italic* .
 This is *Italic* .
@@ -178,7 +178,7 @@ This is _Italic_ .
 
 
 ### Bold
 ### Bold
 
 
-To bold text, add two asterisks or underscores before and after a word or phrase.
+To make text bold, add two asterisks or underscores before and after a word or phrase.
 
 
 ```
 ```
 This is **bold**.
 This is **bold**.
@@ -263,7 +263,7 @@ Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 
 ## Ul Bulleted list
 ## Ul Bulleted list
 
 
-To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items. 
+To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items.
 Items can be nested using indentation.
 Items can be nested using indentation.
 
 
 ```
 ```
@@ -286,7 +286,7 @@ Items can be nested using indentation.
 
 
 ## Ol Numbered List
 ## Ol Numbered List
 
 
-To create an ordered list, add line items with numbers followed by periods. 
+To create an ordered list, add line items with numbers followed by periods.
 The numbers don’t have to be in numerical order, but the list should start with the number one.
 The numbers don’t have to be in numerical order, but the list should start with the number one.
 
 
 ```
 ```
@@ -449,9 +449,9 @@ See [emojione](https://www.emojione.com/)
 
 
 # :heavy_plus_sign: More..
 # :heavy_plus_sign: More..
 
 
-- Try to attach Bootstrap4 Tags?
+- Want to attach Bootstrap4 Tags?
     - :arrow_right: [/Sandbox/Bootstrap4]
     - :arrow_right: [/Sandbox/Bootstrap4]
-- Try to draw Diagrams?
+- Want to draw Diagrams?
     - :arrow_right: [/Sandbox/Diagrams]
     - :arrow_right: [/Sandbox/Diagrams]
-- Try to write Math Formulas?
+- Want to write Math Formulas?
     - :arrow_right: [/Sandbox/Math]
     - :arrow_right: [/Sandbox/Math]

+ 20 - 1
packages/app/src/client/models/Linker.js

@@ -1,3 +1,8 @@
+
+import { pagePathUtils } from '@growi/core';
+
+const { encodeSpaces } = pagePathUtils;
+
 export default class Linker {
 export default class Linker {
 
 
   constructor(
   constructor(
@@ -5,10 +10,15 @@ export default class Linker {
       label = '',
       label = '',
       link = '',
       link = '',
   ) {
   ) {
+
     this.type = type;
     this.type = type;
     this.label = label;
     this.label = label;
     this.link = link;
     this.link = link;
 
 
+    if (type === Linker.types.markdownLink) {
+      this.initWhenMarkdownLink();
+    }
+
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
   }
   }
 
 
@@ -25,9 +35,18 @@ export default class Linker {
     markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
     markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
   }
   }
 
 
+  initWhenMarkdownLink() {
+    // fill label with link if empty
+    if (this.label === '') {
+      this.label = this.link;
+    }
+    // encode spaces
+    this.link = encodeSpaces(this.link);
+  }
+
   generateMarkdownText() {
   generateMarkdownText() {
     if (this.type === Linker.types.pukiwikiLink) {
     if (this.type === Linker.types.pukiwikiLink) {
-      if (this.label === this.link) return `[[${this.link}]]`;
+      if (this.label === '') return `[[${this.link}]]`;
       return `[[${this.label}>${this.link}]]`;
       return `[[${this.label}>${this.link}]]`;
     }
     }
     if (this.type === Linker.types.growiLink) {
     if (this.type === Linker.types.growiLink) {

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

@@ -29,15 +29,17 @@ export default class GrowiPlugin {
       const meta = definition.meta;
       const meta = definition.meta;
 
 
       switch (meta.pluginSchemaVersion) {
       switch (meta.pluginSchemaVersion) {
-        // v1 is deprecated
+        // v1, v2 and v3 is deprecated
         case 1:
         case 1:
           logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           break;
           break;
-        // v2 is deprecated
         case 2:
         case 2:
           logger.warn('pluginSchemaVersion 2 is deprecated', definition);
           logger.warn('pluginSchemaVersion 2 is deprecated', definition);
           break;
           break;
         case 3:
         case 3:
+          logger.warn('pluginSchemaVersion 2 is deprecated', definition);
+          break;
+        case 4:
           definition.entries.forEach((entry) => {
           definition.entries.forEach((entry) => {
             entry(appContainer);
             entry(appContainer);
           });
           });

+ 1 - 1
packages/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminOidcSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminSamlSecurityContainer.js

@@ -1,7 +1,7 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 3 - 1
packages/app/src/client/services/PageContainer.js

@@ -3,8 +3,8 @@ import { Container } from 'unstated';
 
 
 import * as entities from 'entities';
 import * as entities from 'entities';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { isTrashPage } from '~/utils/path-utils';
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
 
 
 import {
 import {
@@ -16,6 +16,8 @@ import {
   DrawioInterceptor,
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
 } from '../util/interceptor/drawio-interceptor';
 
 
+const { isTrashPage } = pagePathUtils;
+
 const logger = loggerFactory('growi:services:PageContainer');
 const logger = loggerFactory('growi:services:PageContainer');
 
 
 /**
 /**

+ 1 - 1
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -4,11 +4,11 @@ import { withTranslation } from 'react-i18next';
 
 
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import UserPicture from '../../User/UserPicture';
 
 
 class UserGroupUserFormByInput extends React.Component {
 class UserGroupUserFormByInput extends React.Component {
 
 

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';

+ 1 - 1
packages/app/src/components/Admin/Users/UserTable.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import UserMenu from './UserMenu';
 import UserMenu from './UserMenu';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';

+ 3 - 1
packages/app/src/components/ComparePathsTable.jsx

@@ -2,10 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 
 function ComparePathsTable(props) {
 function ComparePathsTable(props) {
   const {
   const {

+ 3 - 1
packages/app/src/components/ContentLinkButtons.jsx

@@ -1,7 +1,7 @@
 import React, { useMemo } from 'react';
 import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -10,6 +10,8 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 
 
+const { isTopPage } = pagePathUtils;
+
 const WIKI_HEADER_LINK = 120;
 const WIKI_HEADER_LINK = 120;
 
 
 /**
 /**

+ 1 - 1
packages/app/src/components/CreateTemplateModal.jsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 

+ 3 - 1
packages/app/src/components/DuplicatedPathsTable.jsx

@@ -2,10 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 
 function DuplicatedPathsTable(props) {
 function DuplicatedPathsTable(props) {
   const {
   const {

+ 1 - 1
packages/app/src/components/Fab.jsx

@@ -62,7 +62,7 @@ const Fab = (props) => {
   }
   }
 
 
   return (
   return (
-    <div className="grw-fab d-none d-md-block">
+    <div className="grw-fab d-none d-md-block d-edit-none">
       {currentUser != null && renderPageCreateButton()}
       {currentUser != null && renderPageCreateButton()}
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
         <button
         <button

+ 4 - 2
packages/app/src/components/Navbar/AuthorInfo.jsx

@@ -1,9 +1,11 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
-import { userPageRoot } from '~/utils/path-utils';
+import { UserPicture } from '@growi/ui';
+import { pagePathUtils } from '@growi/core';
+
+const { userPageRoot } = pagePathUtils;
 
 
-import UserPicture from '../User/UserPicture';
 
 
 const formatType = 'yyyy/MM/dd HH:mm';
 const formatType = 'yyyy/MM/dd HH:mm';
 const AuthorInfo = (props) => {
 const AuthorInfo = (props) => {

+ 1 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { DevidedPagePath } from '@growi/core';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
-import DevidedPagePath from '~/models/devided-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';

+ 1 - 1
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -5,6 +5,7 @@ import { withTranslation } from 'react-i18next';
 
 
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -18,7 +19,6 @@ import {
   updateUserPreferenceWithOsSettings,
   updateUserPreferenceWithOsSettings,
 } from '~/client/util/color-scheme';
 } from '~/client/util/color-scheme';
 
 
-import UserPicture from '../User/UserPicture';
 
 
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';

+ 3 - 1
packages/app/src/components/Page/CopyDropdown.jsx

@@ -12,7 +12,9 @@ import {
 
 
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 
 
-import { encodeSpaces } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
+
+const { encodeSpaces } = pagePathUtils;
 
 
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (
 const DropdownItemContents = ({ title, contents }) => (

+ 3 - 1
packages/app/src/components/Page/PageManagement.jsx

@@ -4,7 +4,7 @@ import { UncontrolledTooltip } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -15,6 +15,8 @@ import CreateTemplateModal from '../CreateTemplateModal';
 import PagePresentationModal from '../PagePresentationModal';
 import PagePresentationModal from '../PagePresentationModal';
 import PresentationIcon from '../Icons/PresentationIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 
 
+const { isTopPage } = pagePathUtils;
+
 
 
 const PageManagement = (props) => {
 const PageManagement = (props) => {
   const {
   const {

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

@@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import UserPicture from '../User/UserPicture';
 import PutbackPageModal from '../PutbackPageModal';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import PageDeleteModal from '../PageDeleteModal';
 import PageDeleteModal from '../PageDeleteModal';

+ 1 - 1
packages/app/src/components/PageAttachment/DeleteAttachmentModal.jsx

@@ -6,7 +6,7 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
 export default class DeleteAttachmentModal extends React.Component {
 export default class DeleteAttachmentModal extends React.Component {

+ 1 - 1
packages/app/src/components/PageAttachment/PageAttachmentList.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import Attachment from './Attachment';
+import { Attachment } from '@growi/ui';
 
 
 export default class PageAttachmentList extends React.Component {
 export default class PageAttachmentList extends React.Component {
 
 

+ 1 - 1
packages/app/src/components/PageComment/Comment.jsx

@@ -6,6 +6,7 @@ import { format } from 'date-fns';
 
 
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
+import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 
 
@@ -13,7 +14,6 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import RevisionBody from '../Page/RevisionBody';
 import RevisionBody from '../Page/RevisionBody';
-import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
 import Username from '../User/Username';
 import CommentEditor from './CommentEditor';
 import CommentEditor from './CommentEditor';
 import CommentControl from './CommentControl';
 import CommentControl from './CommentControl';

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

@@ -8,6 +8,7 @@ import {
 
 
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
+import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import CommentContainer from '~/client/services/CommentContainer';
 import CommentContainer from '~/client/services/CommentContainer';
@@ -15,7 +16,6 @@ import EditorContainer from '~/client/services/EditorContainer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import UserPicture from '../User/UserPicture';
 import Editor from '../PageEditor/Editor';
 import Editor from '../PageEditor/Editor';
 import SlackNotification from '../SlackNotification';
 import SlackNotification from '../SlackNotification';
 
 

+ 1 - 1
packages/app/src/components/PageComment/DeleteCommentModal.jsx

@@ -7,7 +7,7 @@ import {
 
 
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
 export default class DeleteCommentModal extends React.Component {
 export default class DeleteCommentModal extends React.Component {

+ 6 - 4
packages/app/src/components/PageCreateModal.jsx

@@ -7,10 +7,8 @@ import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 
 
-import { pathUtils } from 'growi-commons';
-import {
-  userPageRoot, isCreatablePage, generateEditorPath,
-} from '~/utils/path-utils';
+import { pagePathUtils, pathUtils } from '@growi/core';
+
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -19,6 +17,10 @@ import { toastError } from '~/client/util/apiNotification';
 
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 
+const {
+  userPageRoot, isCreatablePage, generateEditorPath,
+} = pagePathUtils;
+
 const PageCreateModal = (props) => {
 const PageCreateModal = (props) => {
   const { t, appContainer, navigationContainer } = props;
   const { t, appContainer, navigationContainer } = props;
 
 

+ 10 - 0
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -801,6 +801,15 @@ export default class CodeMirrorEditor extends AbstractEditor {
       >
       >
         <EditorIcon icon="CheckList" />
         <EditorIcon icon="CheckList" />
       </Button>,
       </Button>,
+      <Button
+        key="nav-item-attachment"
+        color={null}
+        size="sm"
+        title="Attachment"
+        onClick={this.props.onAddAttachmentButtonClicked}
+      >
+        <EditorIcon icon="Attachment" />
+      </Button>,
       <Button
       <Button
         key="nav-item-link"
         key="nav-item-link"
         color={null}
         color={null}
@@ -947,6 +956,7 @@ CodeMirrorEditor.propTypes = Object.assign({
   emojiStrategy: PropTypes.object,
   emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
   lineNumbers: PropTypes.bool,
   onMarkdownHelpButtonClicked: PropTypes.func,
   onMarkdownHelpButtonClicked: PropTypes.func,
+  onAddAttachmentButtonClicked: PropTypes.func,
 }, AbstractEditor.propTypes);
 }, AbstractEditor.propTypes);
 CodeMirrorEditor.defaultProps = {
 CodeMirrorEditor.defaultProps = {
   lineNumbers: true,
   lineNumbers: true,

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

@@ -39,6 +39,7 @@ export default class Editor extends AbstractEditor {
     this.dropHandler = this.dropHandler.bind(this);
     this.dropHandler = this.dropHandler.bind(this);
 
 
     this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
     this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
+    this.addAttachmentHandler = this.addAttachmentHandler.bind(this);
 
 
     this.getAcceptableType = this.getAcceptableType.bind(this);
     this.getAcceptableType = this.getAcceptableType.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
@@ -187,6 +188,10 @@ export default class Editor extends AbstractEditor {
     this.setState({ isCheatsheetModalShown: true });
     this.setState({ isCheatsheetModalShown: true });
   }
   }
 
 
+  addAttachmentHandler() {
+    this.dropzone.open();
+  }
+
   getDropzoneClassName(isDragAccept, isDragReject) {
   getDropzoneClassName(isDragAccept, isDragReject) {
     let className = 'dropzone';
     let className = 'dropzone';
     if (!this.props.isUploadable) {
     if (!this.props.isUploadable) {
@@ -314,6 +319,7 @@ export default class Editor extends AbstractEditor {
                         onPasteFiles={this.pasteFilesHandler}
                         onPasteFiles={this.pasteFilesHandler}
                         onDragEnter={this.dragEnterHandler}
                         onDragEnter={this.dragEnterHandler}
                         onMarkdownHelpButtonClicked={this.showMarkdownHelp}
                         onMarkdownHelpButtonClicked={this.showMarkdownHelp}
+                        onAddAttachmentButtonClicked={this.addAttachmentHandler}
                         {...this.props}
                         {...this.props}
                       />
                       />
                     )}
                     )}
@@ -341,7 +347,7 @@ export default class Editor extends AbstractEditor {
             <button
             <button
               type="button"
               type="button"
               className="btn btn-outline-secondary btn-block btn-open-dropzone"
               className="btn btn-outline-secondary btn-block btn-open-dropzone"
-              onClick={() => { this.dropzone.open() }}
+              onClick={this.addAttachmentHandler}
             >
             >
               <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
               <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
               Attach files
               Attach files

+ 7 - 9
packages/app/src/components/PageEditor/EditorIcon.jsx

@@ -118,15 +118,13 @@ const EditorIcon = (props) => {
           <path d="M22.12,17H19.75l-3.12-4H18a1,1,0,0,0,1-1V8a1,1,0,0,0-1-1H12a1,1,0,0,0-1,1v4a1,1,0,0,0,1,1h1.38l-2.92,4H7.88A.94.94,0,0,0,7,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,14,22V18a.94.94,0,0,0-.88-1H11.63l3.13-4h.47l3.13,4H16.88A.94.94,0,0,0,16,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,23,22V18A.94.94,0,0,0,22.12,17ZM13,22H8V18h5ZM12,8h6v4H12ZM22,22H17V18h5Z" />
           <path d="M22.12,17H19.75l-3.12-4H18a1,1,0,0,0,1-1V8a1,1,0,0,0-1-1H12a1,1,0,0,0-1,1v4a1,1,0,0,0,1,1h1.38l-2.92,4H7.88A.94.94,0,0,0,7,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,14,22V18a.94.94,0,0,0-.88-1H11.63l3.13-4h.47l3.13,4H16.88A.94.94,0,0,0,16,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,23,22V18A.94.94,0,0,0,22.12,17ZM13,22H8V18h5ZM12,8h6v4H12ZM22,22H17V18h5Z" />
         </svg>
         </svg>
       );
       );
-    // Unused icon
-    // case 'attachment':
-    //   return (
-    //     <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-    //       <rect fillOpacity="0" width="30" height="30" />
-    //       <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
-    //     </svg>
-    // );
-
+    case 'Attachment':
+      return (
+        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
+          <rect fillOpacity="0" width="30" height="30" />
+          <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
+        </svg>
+      );
   }
   }
 
 
 
 

+ 6 - 19
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -13,17 +13,18 @@ import {
 import path from 'path';
 import path from 'path';
 import validator from 'validator';
 import validator from 'validator';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import PreviewWithSuspense from './PreviewWithSuspense';
-import PagePreviewIcon from '../Icons/PagePreviewIcon';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import Linker from '~/client/models/Linker';
 
 
+import PreviewWithSuspense from './PreviewWithSuspense';
+import PagePreviewIcon from '../Icons/PagePreviewIcon';
 import SearchTypeahead from '../SearchTypeahead';
 import SearchTypeahead from '../SearchTypeahead';
-import Linker from '~/client/models/Linker';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
+
 class LinkEditModal extends React.PureComponent {
 class LinkEditModal extends React.PureComponent {
 
 
   constructor(props) {
   constructor(props) {
@@ -175,22 +176,8 @@ class LinkEditModal extends React.PureComponent {
     this.setState({ markdown, previewError, permalink });
     this.setState({ markdown, previewError, permalink });
   }
   }
 
 
-  getLinkForPreview() {
-    const linker = this.generateLink();
-
-    if (this.isUsePermanentLink && this.permalink != null) {
-      linker.link = this.permalink;
-    }
-
-    if (linker.label === '') {
-      linker.label = linker.link;
-    }
-
-    return linker;
-  }
-
   renderLinkPreview() {
   renderLinkPreview() {
-    const linker = this.getLinkForPreview();
+    const linker = this.generateLink();
     return (
     return (
       <div className="d-flex justify-content-between mb-3 flex-column flex-sm-row">
       <div className="d-flex justify-content-between mb-3 flex-column flex-sm-row">
         <div className="card card-disabled w-100 p-1 mb-0">
         <div className="card card-disabled w-100 p-1 mb-0">
@@ -245,7 +232,7 @@ class LinkEditModal extends React.PureComponent {
   }
   }
 
 
   save() {
   save() {
-    const linker = this.getLinkForPreview();
+    const linker = this.generateLink();
 
 
     if (this.props.onSave != null) {
     if (this.props.onSave != null) {
       this.props.onSave(linker.generateMarkdownText());
       this.props.onSave(linker.generateMarkdownText());

+ 1 - 1
packages/app/src/components/PageHistory/Revision.jsx

@@ -1,9 +1,9 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
+import { UserPicture } from '@growi/ui';
 import UserDate from '../User/UserDate';
 import UserDate from '../User/UserDate';
 import Username from '../User/Username';
 import Username from '../User/Username';
-import UserPicture from '../User/UserPicture';
 
 
 export default class Revision extends React.Component {
 export default class Revision extends React.Component {
 
 

+ 2 - 3
packages/app/src/components/PageList/Page.jsx

@@ -1,9 +1,8 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import UserPicture from '../User/UserPicture';
-import PageListMeta from './PageListMeta';
-import PagePathLabel from './PagePathLabel';
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+
 
 
 export default class Page extends React.Component {
 export default class Page extends React.Component {
 
 

+ 0 - 24
packages/app/src/components/PageList/PagePath.jsx

@@ -1,24 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import PagePathLabel from './PagePathLabel';
-
-/**
- * !!DEPRECATED!!
- *
- * maintained for backward compatibility for growi-lsx-plugin(<= 3.1.1)
- */
-const PagePath = props => (
-  <PagePathLabel isLatterOnly={props.isShortPathOnly} {...props} />
-);
-
-PagePath.propTypes = {
-  isShortPathOnly: PropTypes.bool,
-  ...PagePathLabel.propTypes,
-};
-
-PagePath.defaultProps = {
-  ...PagePathLabel.defaultProps,
-};
-
-export default PagePath;

+ 1 - 1
packages/app/src/components/PagePathAutoComplete.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import SearchTypeahead from './SearchTypeahead';
 import SearchTypeahead from './SearchTypeahead';
 
 

+ 3 - 1
packages/app/src/components/RevisionComparer/RevisionComparer.jsx

@@ -6,7 +6,7 @@ import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { encodeSpaces } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
@@ -14,6 +14,8 @@ import RevisionComparerContainer from '~/client/services/RevisionComparerContain
 
 
 import RevisionDiff from '../PageHistory/RevisionDiff';
 import RevisionDiff from '../PageHistory/RevisionDiff';
 
 
+const { encodeSpaces } = pagePathUtils;
+
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (
 const DropdownItemContents = ({ title, contents }) => (
   <>
   <>

+ 2 - 3
packages/app/src/components/SearchTypeahead.jsx

@@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
 import { noop } from 'lodash/noop';
 import { noop } from 'lodash/noop';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
-import UserPicture from './User/UserPicture';
-import PageListMeta from './PageList/PageListMeta';
-import PagePathLabel from './PageList/PagePathLabel';
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 

+ 2 - 2
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -3,10 +3,11 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { UserPicture } from '@growi/ui';
+import { DevidedPagePath } from '@growi/core';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import DevidedPagePath from '~/models/devided-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -14,7 +15,6 @@ import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
-import UserPicture from '../User/UserPicture';
 
 
 const logger = loggerFactory('growi:History');
 const logger = loggerFactory('growi:History');
 class RecentChanges extends React.Component {
 class RecentChanges extends React.Component {

+ 1 - 1
packages/app/src/components/User/UserInfo.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import UserPicture from './UserPicture';
+import { UserPicture } from '@growi/ui';
 
 
 const UserInfo = (props) => {
 const UserInfo = (props) => {
   const { pageUser } = props;
   const { pageUser } = props;

+ 1 - 1
packages/app/src/components/User/UserPictureList.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import UserPicture from './UserPicture';
+import { UserPicture } from '@growi/ui';
 
 
 export default class UserPictureList extends React.Component {
 export default class UserPictureList extends React.Component {
 
 

+ 1 - 1
packages/app/src/migrations/20191126173016-adjust-pages-path.js

@@ -1,5 +1,5 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import config from '^/config/migrate';
 import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';

+ 3 - 6
packages/app/src/models/linked-page-path.js

@@ -1,12 +1,11 @@
-const { pathUtils } = require('growi-commons');
-const { isTrashPage } = require('~/utils/path-utils');
+import { pagePathUtils, DevidedPagePath, pathUtils } from '@growi/core';
 
 
-const DevidedPagePath = require('./devided-page-path');
+const { isTrashPage } = pagePathUtils;
 
 
 /**
 /**
  * Linked Array Structured PagePath Model
  * Linked Array Structured PagePath Model
  */
  */
-class LinkedPagePath {
+export default class LinkedPagePath {
 
 
   constructor(path, skipNormalize = false) {
   constructor(path, skipNormalize = false) {
 
 
@@ -34,5 +33,3 @@ class LinkedPagePath {
   }
   }
 
 
 }
 }
-
-module.exports = LinkedPagePath;

+ 6 - 1
packages/app/src/server/crowi/express-init.js

@@ -53,7 +53,12 @@ module.exports = function(crowi, app) {
       nsSeparator: '::',
       nsSeparator: '::',
     });
     });
 
 
-  app.use(helmet());
+  app.use(helmet({
+    contentSecurityPolicy: false,
+    expectCt: false,
+    referrerPolicy: false,
+    permittedCrossDomainPolicies: false,
+  }));
 
 
   app.use((req, res, next) => {
   app.use((req, res, next) => {
     const now = new Date();
     const now = new Date();

+ 1 - 1
packages/app/src/server/crowi/index.js

@@ -427,7 +427,7 @@ Crowi.prototype.start = async function() {
 
 
   // setup plugins
   // setup plugins
   this.pluginService = new PluginService(this, express);
   this.pluginService = new PluginService(this, express);
-  this.pluginService.autoDetectAndLoadPlugins();
+  await this.pluginService.autoDetectAndLoadPlugins();
 
 
   const server = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
   const server = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
 
 

+ 3 - 4
packages/app/src/server/middlewares/admin-required.js

@@ -7,14 +7,13 @@ module.exports = (crowi, fallback = null) => {
   return async(req, res, next) => {
   return async(req, res, next) => {
     if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
     if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
       if (req.user.admin) {
       if (req.user.admin) {
-        next();
-        return;
+        return next();
       }
       }
 
 
       logger.warn('This user is not admin.');
       logger.warn('This user is not admin.');
 
 
       if (fallback != null) {
       if (fallback != null) {
-        return fallback(req, res);
+        return fallback(req, res, next);
       }
       }
       return res.redirect('/');
       return res.redirect('/');
     }
     }
@@ -22,7 +21,7 @@ module.exports = (crowi, fallback = null) => {
     logger.warn('This user has not logged in.');
     logger.warn('This user has not logged in.');
 
 
     if (fallback != null) {
     if (fallback != null) {
-      return fallback(req, res);
+      return fallback(req, res, next);
     }
     }
     return res.redirect('/login');
     return res.redirect('/login');
   };
   };

+ 5 - 3
packages/app/src/server/models/page.js

@@ -1,3 +1,4 @@
+import { templateChecker, pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 // disable no-return-await for model functions
 // disable no-return-await for model functions
@@ -15,8 +16,9 @@ const differenceInYears = require('date-fns/differenceInYears');
 
 
 const { pathUtils } = require('growi-commons');
 const { pathUtils } = require('growi-commons');
 const escapeStringRegexp = require('escape-string-regexp');
 const escapeStringRegexp = require('escape-string-regexp');
-const templateChecker = require('~/utils/template-checker');
-const { isTopPage, isTrashPage } = require('~/utils/path-utils');
+
+const { isTopPage, isTrashPage } = pagePathUtils;
+const { checkTemplatePath } = templateChecker;
 
 
 const logger = loggerFactory('growi:models:page');
 const logger = loggerFactory('growi:models:page');
 
 
@@ -311,7 +313,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   pageSchema.methods.isTemplate = function() {
   pageSchema.methods.isTemplate = function() {
-    return templateChecker(this.path);
+    return checkTemplatePath(this.path);
   };
   };
 
 
   pageSchema.methods.isLatestRevision = function() {
   pageSchema.methods.isLatestRevision = function() {

+ 38 - 0
packages/app/src/server/plugins/plugin-utils-v4.ts

@@ -0,0 +1,38 @@
+import path from 'path';
+
+import { PluginMetaV4, PluginDefinitionV4 } from '@growi/core';
+
+export class PluginUtilsV4 {
+
+  /**
+   * return a definition objects that has following structure:
+   *
+   * {
+   *   name: 'crowi-plugin-X',
+   *   meta: require('crowi-plugin-X'),
+   *   entries: [
+   *     'crowi-plugin-X/lib/client-entry'
+   *   ]
+   * }
+   *
+   *
+   * @param {string} pluginName
+   * @return
+   * @memberOf PluginService
+   */
+  async generatePluginDefinition(name: string, isForClient = false): Promise<PluginDefinitionV4> {
+    const meta: PluginMetaV4 = await import(name);
+    let entries = (isForClient) ? meta.clientEntries : meta.serverEntries;
+
+    entries = entries.map((entryPath) => {
+      return path.join(name, entryPath);
+    });
+
+    return {
+      name,
+      meta,
+      entries,
+    };
+  }
+
+}

+ 15 - 7
packages/app/src/server/plugins/plugin-utils.js

@@ -1,13 +1,13 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
 
-const fs = require('graceful-fs');
+import { PluginUtilsV4 } from './plugin-utils-v4';
 
 
-const PluginUtilsV2 = require('./plugin-utils-v2');
+const fs = require('graceful-fs');
 
 
 const logger = loggerFactory('growi:plugins:plugin-utils');
 const logger = loggerFactory('growi:plugins:plugin-utils');
 
 
-const pluginUtilsV2 = new PluginUtilsV2();
+const pluginUtilsV4 = new PluginUtilsV4();
 
 
 class PluginUtils {
 class PluginUtils {
 
 
@@ -26,19 +26,27 @@ class PluginUtils {
    * @return
    * @return
    * @memberOf PluginService
    * @memberOf PluginService
    */
    */
-  generatePluginDefinition(name, isForClient = false) {
+  async generatePluginDefinition(name, isForClient = false) {
     const meta = require(name);
     const meta = require(name);
     let definition;
     let definition;
 
 
     switch (meta.pluginSchemaVersion) {
     switch (meta.pluginSchemaVersion) {
-      // v1 is deprecated
+      // v1, v2 and v3 is deprecated
       case 1:
       case 1:
         logger.debug('pluginSchemaVersion 1 is deprecated');
         logger.debug('pluginSchemaVersion 1 is deprecated');
         break;
         break;
-      // v2 or above
       case 2:
       case 2:
+        logger.debug('pluginSchemaVersion 2 is deprecated');
+        break;
+      case 3:
+        logger.debug('pluginSchemaVersion 3 is deprecated');
+        break;
+      // v4 or above
+      case 4:
+        definition = await pluginUtilsV4.generatePluginDefinition(name, isForClient);
+        break;
       default:
       default:
-        definition = pluginUtilsV2.generatePluginDefinition(name, isForClient);
+        logger.warn('Unsupported schema version', meta.pluginSchemaVersion);
     }
     }
 
 
     return definition;
     return definition;

+ 16 - 11
packages/app/src/server/plugins/plugin.service.js

@@ -12,13 +12,13 @@ class PluginService {
     this.pluginUtils = new PluginUtils();
     this.pluginUtils = new PluginUtils();
   }
   }
 
 
-  autoDetectAndLoadPlugins() {
+  async autoDetectAndLoadPlugins() {
     const isEnabledPlugins = this.crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins');
     const isEnabledPlugins = this.crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins');
 
 
     // import plugins
     // import plugins
     if (isEnabledPlugins) {
     if (isEnabledPlugins) {
       logger.debug('Plugins are enabled');
       logger.debug('Plugins are enabled');
-      this.loadPlugins(this.pluginUtils.listPluginNames(this.crowi.rootDir));
+      return this.loadPlugins(this.pluginUtils.listPluginNames(this.crowi.rootDir));
     }
     }
 
 
   }
   }
@@ -28,29 +28,34 @@ class PluginService {
    *
    *
    * @memberOf PluginService
    * @memberOf PluginService
    */
    */
-  loadPlugins(pluginNames) {
-    pluginNames
-      .map((name) => {
-        return this.pluginUtils.generatePluginDefinition(name);
-      })
-      .forEach((definition) => {
+  async loadPlugins(pluginNames) {
+    // get definitions
+    const definitions = [];
+    for (const pluginName of pluginNames) {
+      // eslint-disable-next-line no-await-in-loop
+      const definition = await this.pluginUtils.generatePluginDefinition(pluginName);
+      if (definition != null) {
         this.loadPlugin(definition);
         this.loadPlugin(definition);
-      });
+      }
+    }
   }
   }
 
 
   loadPlugin(definition) {
   loadPlugin(definition) {
     const meta = definition.meta;
     const meta = definition.meta;
 
 
     switch (meta.pluginSchemaVersion) {
     switch (meta.pluginSchemaVersion) {
-      // v1 is deprecated
+      // v1, v2 and v3 is deprecated
       case 1:
       case 1:
         logger.warn('pluginSchemaVersion 1 is deprecated', definition);
         logger.warn('pluginSchemaVersion 1 is deprecated', definition);
         break;
         break;
-      // v2 is deprecated
       case 2:
       case 2:
         logger.warn('pluginSchemaVersion 2 is deprecated', definition);
         logger.warn('pluginSchemaVersion 2 is deprecated', definition);
         break;
         break;
       case 3:
       case 3:
+        logger.warn('pluginSchemaVersion 3 is deprecated', definition);
+        break;
+      // v4 or above
+      case 4:
         logger.info(`load plugin '${definition.name}'`);
         logger.info(`load plugin '${definition.name}'`);
         definition.entries.forEach((entryPath) => {
         definition.entries.forEach((entryPath) => {
           const entry = require(entryPath);
           const entry = require(entryPath);

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

@@ -6,7 +6,7 @@ const express = require('express');
 
 
 const router = express.Router();
 const router = express.Router();
 
 
-const helmet = require('helmet');
+const noCache = require('nocache');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 /**
 /**
@@ -122,7 +122,7 @@ module.exports = (crowi) => {
    *                  info:
    *                  info:
    *                    $ref: '#/components/schemas/HealthcheckInfo'
    *                    $ref: '#/components/schemas/HealthcheckInfo'
    */
    */
-  router.get('/', helmet.noCache(), async(req, res) => {
+  router.get('/', noCache(), async(req, res) => {
     let checkServices = req.query.checkServices || [];
     let checkServices = req.query.checkServices || [];
     let isStrictly = req.query.strictly != null;
     let isStrictly = req.query.strictly != null;
 
 

+ 3 - 2
packages/app/src/server/routes/apiv3/page.js

@@ -1,13 +1,14 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+
 const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
 
 
 const express = require('express');
 const express = require('express');
 const { body, query } = require('express-validator');
 const { body, query } = require('express-validator');
 
 
 const router = express.Router();
 const router = express.Router();
-
-const { convertToNewAffiliationPath } = require('~/utils/path-utils');
+const { convertToNewAffiliationPath } = pagePathUtils;
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 
 

+ 7 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -1,3 +1,4 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
@@ -6,9 +7,11 @@ const pathUtils = require('growi-commons').pathUtils;
 
 
 const { body } = require('express-validator');
 const { body } = require('express-validator');
 const { query } = require('express-validator');
 const { query } = require('express-validator');
-const { isCreatablePage } = require('~/utils/path-utils');
+
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
+const { isCreatablePage } = pagePathUtils;
+
 const router = express.Router();
 const router = express.Router();
 
 
 const LIMIT_FOR_LIST = 10;
 const LIMIT_FOR_LIST = 10;
@@ -458,7 +461,9 @@ module.exports = (crowi) => {
   ];
   ];
 
 
   router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
   router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
-    const { isTrashPage } = require('~/utils/path-utils');
+
+
+    const { isTrashPage } = pagePathUtils;
 
 
     const { path } = req.query;
     const { path } = req.query;
     const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
     const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;

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

@@ -7,7 +7,7 @@ const { body } = require('express-validator');
 
 
 const router = express.Router();
 const router = express.Router();
 
 
-const helmet = require('helmet');
+const noCache = require('nocache');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
@@ -41,7 +41,7 @@ module.exports = (crowi) => {
    *                  info:
    *                  info:
    *                    type: object
    *                    type: object
    */
    */
-  router.get('/indices', helmet.noCache(), accessTokenParser, loginRequired, adminRequired, async(req, res) => {
+  router.get('/indices', noCache(), accessTokenParser, loginRequired, adminRequired, async(req, res) => {
     const { searchService } = crowi;
     const { searchService } = crowi;
 
 
     if (!searchService.isConfigured) {
     if (!searchService.isConfigured) {

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

@@ -6,7 +6,7 @@ const express = require('express');
 
 
 const router = express.Router();
 const router = express.Router();
 
 
-const helmet = require('helmet');
+const noCache = require('nocache');
 
 
 const USER_STATUS_MASTER = {
 const USER_STATUS_MASTER = {
   1: 'registered',
   1: 'registered',
@@ -97,7 +97,7 @@ module.exports = (crowi) => {
    *                    type: object
    *                    type: object
    *                    description: Statistics for all user
    *                    description: Statistics for all user
    */
    */
-  router.get('/user', helmet.noCache(), async(req, res) => {
+  router.get('/user', noCache(), async(req, res) => {
     const data = req.user == null ? await getUserStatisticsForNotLoggedIn() : await getUserStatistics();
     const data = req.user == null ? await getUserStatisticsForNotLoggedIn() : await getUserStatistics();
     res.status(200).send({ data });
     res.status(200).send({ data });
   });
   });

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

@@ -1,6 +1,7 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-const { isCreatablePage } = require('~/utils/path-utils');
+const { isCreatablePage } = pagePathUtils;
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');

+ 2 - 2
packages/app/src/server/service/app.ts

@@ -1,4 +1,4 @@
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -119,7 +119,7 @@ class AppService implements S2sMessageHandlable {
   }
   }
 
 
   async setupAfterInstall() {
   async setupAfterInstall() {
-    this.crowi.pluginService.autoDetectAndLoadPlugins();
+    await this.crowi.pluginService.autoDetectAndLoadPlugins();
     this.crowi.setupRoutesAtLast();
     this.crowi.setupRoutesAtLast();
     this.crowi.setupGlobalErrorHandlers();
     this.crowi.setupGlobalErrorHandlers();
 
 

+ 1 - 1
packages/app/src/server/service/customize.ts

@@ -1,8 +1,8 @@
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 import uglifycss from 'uglifycss';
 import uglifycss from 'uglifycss';
 
 
+import { DevidedPagePath } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import DevidedPagePath from '~/models/devided-page-path';
 
 
 import S2sMessage from '../models/vo/s2s-message';
 import S2sMessage from '../models/vo/s2s-message';
 import { S2sMessageHandlable } from './s2s-messaging/handlable';
 import { S2sMessageHandlable } from './s2s-messaging/handlable';

+ 3 - 1
packages/app/src/server/service/global-notification/global-notification-slack.js

@@ -1,9 +1,11 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+
 const logger = loggerFactory('growi:service:GlobalNotificationSlackService'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:service:GlobalNotificationSlackService'); // eslint-disable-line no-unused-vars
 const urljoin = require('url-join');
 const urljoin = require('url-join');
 
 
-const { encodeSpaces } = require('~/utils/path-utils');
+const { encodeSpaces } = pagePathUtils;
 
 
 /**
 /**
  * sub service class of GlobalNotificationSetting
  * sub service class of GlobalNotificationSetting

+ 3 - 1
packages/app/src/server/service/page.js

@@ -1,3 +1,4 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
@@ -7,7 +8,8 @@ const logger = loggerFactory('growi:models:page');
 const debug = require('debug')('growi:models:page');
 const debug = require('debug')('growi:models:page');
 const { Writable } = require('stream');
 const { Writable } = require('stream');
 const { createBatchStream } = require('~/server/util/batch-stream');
 const { createBatchStream } = require('~/server/util/batch-stream');
-const { isTrashPage } = require('~/utils/path-utils');
+
+const { isTrashPage } = pagePathUtils;
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 
 
 const BULK_REINDEX_SIZE = 100;
 const BULK_REINDEX_SIZE = 100;

+ 2 - 13
packages/app/src/server/service/socket-io.js

@@ -117,22 +117,11 @@ class SocketIoService {
     });
     });
   }
   }
 
 
-  async getClients(namespace) {
-    return new Promise((resolve, reject) => {
-      namespace.clients((error, clients) => {
-        if (error) {
-          reject(error);
-        }
-        resolve(clients);
-      });
-    });
-  }
-
   async checkConnectionLimitsForAdmin(socket, next) {
   async checkConnectionLimitsForAdmin(socket, next) {
     const namespaceName = socket.nsp.name;
     const namespaceName = socket.nsp.name;
 
 
     if (namespaceName === '/admin') {
     if (namespaceName === '/admin') {
-      const clients = await this.getClients(this.getAdminSocket());
+      const clients = await this.getAdminSocket().allSockets();
       const clientsCount = clients.length;
       const clientsCount = clients.length;
 
 
       logger.debug('Current count of clients for \'/admin\':', clientsCount);
       logger.debug('Current count of clients for \'/admin\':', clientsCount);
@@ -178,7 +167,7 @@ class SocketIoService {
       next();
       next();
     }
     }
 
 
-    const clients = await this.getClients(this.getDefaultSocket());
+    const clients = await this.getDefaultSocket().allSockets();
     const clientsCount = clients.length;
     const clientsCount = clients.length;
 
 
     logger.debug('Current count of clients for \'/\':', clientsCount);
     logger.debug('Current count of clients for \'/\':', clientsCount);

+ 3 - 1
packages/app/src/server/util/createGrowiPagesFromImports.js

@@ -1,4 +1,6 @@
-const { isCreatablePage } = require('~/utils/path-utils');
+import { pagePathUtils } from '@growi/core';
+
+const { isCreatablePage } = pagePathUtils;
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');

+ 1 - 1
packages/app/src/server/views/layout-growi/base/layout.html

@@ -33,7 +33,7 @@
     </div>
     </div>
   </div>
   </div>
 
 
-  <footer class="footer">
+  <footer class="footer d-edit-none">
     {% block content_footer %}{% endblock %}
     {% block content_footer %}{% endblock %}
   </footer>
   </footer>
 
 

+ 1 - 0
packages/app/src/styles/_on-edit.scss

@@ -144,6 +144,7 @@ body.on-edit {
         flex-flow: row nowrap;
         flex-flow: row nowrap;
         width: 100%;
         width: 100%;
         overflow-x: auto;
         overflow-x: auto;
+        overflow-y: hidden;
         scrollbar-width: thin;
         scrollbar-width: thin;
       }
       }
     }
     }

+ 2 - 5
packages/app/tsconfig.base.json

@@ -1,9 +1,6 @@
 {
 {
   "extends": "../../tsconfig.base.json",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
   "compilerOptions": {
-  },
-  "exclude": [
-    "node_modules",
-    "**/*.test.ts"
-  ]
+    "jsx": "react"
+  }
 }
 }

+ 0 - 1
packages/app/tsconfig.build.client.json

@@ -2,7 +2,6 @@
   "extends": "./tsconfig.base.json",
   "extends": "./tsconfig.base.json",
   "compilerOptions": {
   "compilerOptions": {
     "module": "esnext",
     "module": "esnext",
-    "jsx": "react",
     "noFallthroughCasesInSwitch": true,
     "noFallthroughCasesInSwitch": true,
     "noUnusedLocals": true,
     "noUnusedLocals": true,
     "noUnusedParameters": true,
     "noUnusedParameters": true,

+ 1 - 0
packages/core/.eslintignore

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

+ 1 - 0
packages/core/.gitignore

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

+ 27 - 0
packages/core/README.md

@@ -0,0 +1,27 @@
+# growi-commons
+
+[![dependencies status](https://david-dm.org/weseek/growi-commons.svg)](https://david-dm.org/weseek/growi-commons)
+[![devDependencies Status](https://david-dm.org/weseek/growi-commons/dev-status.svg)](https://david-dm.org/weseek/growi-commons?type=dev)
+[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+
+[GROWI](https://growi.org) Commons Libraries to develop GROWI and plugins
+
+
+Overview
+--------
+
+growi-commons package is includes some functions, classes and modules to develop GROWI substance and GROWI plugins.
+
+Install
+--------
+
+1. install plugin
+
+    ```
+    $ npm install --save growi-commons
+    ```
+
+Documentation
+------------
+
+See https://docs.growi.org/api/commons/

+ 60 - 0
packages/core/jest.config.js

@@ -0,0 +1,60 @@
+// For a detailed explanation regarding each configuration property, visit:
+// https://jestjs.io/docs/en/configuration.html
+
+const MODULE_NAME_MAPPING = {
+  '^~/(.+)$': '<rootDir>/src/$1',
+};
+
+module.exports = {
+
+  preset: 'ts-jest/presets/js-with-ts',
+
+  moduleNameMapper: MODULE_NAME_MAPPING,
+
+  // Automatically clear mock calls and instances between every test
+  clearMocks: true,
+
+  // Indicates whether the coverage information should be collected while executing the test
+  collectCoverage: true,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  // collectCoverageFrom: undefined,
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+  // An array of regexp pattern strings used to skip coverage collection
+  coveragePathIgnorePatterns: [
+    '/node_modules/',
+  ],
+
+  // An object that configures minimum threshold enforcement for coverage results
+  // TODO: activate -- 2020.03.24 Yuki Takei
+  // coverageThreshold: {
+  //   global: {
+  //     branches: 70,
+  //     functions: 70,
+  //     lines: 70,
+  //     statements: 70,
+  //   },
+  // },
+
+  // An array of file extensions your modules use
+  moduleFileExtensions: [
+    'js',
+    'json',
+    'jsx',
+    'ts',
+    'tsx',
+    'node',
+  ],
+
+  // The test environment that will be used for testing
+  testEnvironment: 'node',
+
+  // The glob patterns Jest uses to detect test files
+  testMatch: [
+    '**/src/**/__tests__/**/*.[jt]s?(x)',
+    '**/src/**/?(*.)+(spec|test).[tj]s?(x)',
+  ],
+};

+ 24 - 0
packages/core/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "@growi/core",
+  "version": "4.3.3-RC",
+  "description": "GROWI Core Libraries",
+  "license": "MIT",
+  "keywords": [
+    "growi"
+  ],
+  "main": "dist/cjs/index.js",
+  "module": "dist/esm/index.js",
+  "files": ["dist"],
+  "scripts": {
+    "build": "run-p build:*",
+    "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
+    "build:esm": "tsc -p tsconfig.build.esm.json && tsc-alias -p tsconfig.build.esm.json",
+    "lint:js": "eslint **/*.{js,ts}",
+    "lint:styles": "stylelint src/styles/scss/**/*.scss",
+    "lint": "npm-run-all -p lint:*",
+    "test": "jest --verbose"
+  },
+  "dependencies": {},
+  "devDependencies": {
+  }
+}

+ 21 - 0
packages/core/src/index.js

@@ -0,0 +1,21 @@
+import * as _pathUtils from './utils/path-utils';
+import * as _pagePathUtils from './utils/page-path-utils';
+import * as _templateChecker from './utils/template-checker';
+
+// module.exports = {
+//   BasicInterceptor: require('./utils/basic-interceptor'),
+//   envUtils: require('./utils/env-utils'),
+//   // plugin
+//   customTagUtils: require('./plugin/util/custom-tag-utils'),
+//   TagCacheManager: require('./plugin/service/tag-cache-manager'),
+//   // service
+//   LocalStorageManager: require('./service/localstorage-manager'),
+// };
+
+export * from './plugin/interfaces/plugin-definition-v4';
+export * from './models/devided-page-path';
+
+// export utils
+export const pathUtils = _pathUtils;
+export const pagePathUtils = _pagePathUtils;
+export const templateChecker = _templateChecker;

+ 2 - 4
packages/app/src/models/devided-page-path.js → packages/core/src/models/devided-page-path.js

@@ -1,11 +1,11 @@
-const { pathUtils } = require('growi-commons');
+import * as pathUtils from '../utils/path-utils';
 
 
 // https://regex101.com/r/BahpKX/2
 // https://regex101.com/r/BahpKX/2
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
 // https://regex101.com/r/WVpPpY/1
 // https://regex101.com/r/WVpPpY/1
 const PATTERN_DEFAULT = /^((.*)\/)?([^/]+)$/;
 const PATTERN_DEFAULT = /^((.*)\/)?([^/]+)$/;
 
 
-class DevidedPagePath {
+export class DevidedPagePath {
 
 
   constructor(path, skipNormalize = false, evalDatePath = false) {
   constructor(path, skipNormalize = false, evalDatePath = false) {
 
 
@@ -43,5 +43,3 @@ class DevidedPagePath {
   }
   }
 
 
 }
 }
-
-module.exports = DevidedPagePath;

+ 11 - 0
packages/core/src/plugin/interfaces/plugin-definition-v4.ts

@@ -0,0 +1,11 @@
+export type PluginMetaV4 = {
+  pluginSchemaVersion: number,
+  serverEntries: string[],
+  clientEntries: string[],
+};
+
+export type PluginDefinitionV4 = {
+  name: string,
+  meta: PluginMetaV4,
+  entries: string[],
+};

+ 14 - 0
packages/core/src/plugin/model/tag-context.js

@@ -0,0 +1,14 @@
+/**
+ * Context class for custom-tag-utils#findTagAndReplace
+ */
+class TagContext {
+
+  constructor(initArgs = {}) {
+    this.tagExpression = initArgs.tagExpression || null;
+    this.method = initArgs.method || null;
+    this.args = initArgs.args || null;
+  }
+
+}
+
+module.exports = TagContext;

+ 71 - 0
packages/core/src/plugin/service/tag-cache-manager.js

@@ -0,0 +1,71 @@
+const LocalStorageManager = require('../../service/localstorage-manager');
+
+/**
+ * Service Class for caching React state and TagContext
+ */
+class TagCacheManager {
+
+  /**
+   * @callback generateCacheKey
+   * @param {TagContext} tagContext - TagContext instance
+   * @returns {string} Cache key from TagContext
+   *
+   */
+
+  /**
+   * Constructor
+   * @param {string} cacheNs Used as LocalStorageManager namespace
+   * @param {generateCacheKey} generateCacheKey
+   */
+  constructor(cacheNs, generateCacheKey) {
+    if (cacheNs == null) {
+      throw new Error('args \'cacheNs\' is required.');
+    }
+    if (generateCacheKey == null) {
+      throw new Error('args \'generateCacheKey\' is required.');
+    }
+    if (typeof generateCacheKey !== 'function') {
+      throw new Error('args \'generateCacheKey\' should be function.');
+    }
+
+    this.cacheNs = cacheNs;
+    this.generateCacheKey = generateCacheKey;
+  }
+
+  /**
+   * Retrieve state cache object from local storage
+   * @param {TagContext} tagContext
+   * @returns {object} a cache object that correspont to the specified `tagContext`
+   */
+  getStateCache(tagContext) {
+    const localStorageManager = LocalStorageManager.getInstance();
+
+    const key = this.generateCacheKey(tagContext);
+    const stateCache = localStorageManager.retrieveFromSessionStorage(this.cacheNs, key);
+
+    return stateCache;
+  }
+
+  /**
+   * store state object of React Component with specified key
+   *
+   * @param {TagContext} tagContext
+   * @param {object} state state object of React Component
+   */
+  cacheState(tagContext, state) {
+    const localStorageManager = LocalStorageManager.getInstance();
+    const key = this.generateCacheKey(tagContext);
+    localStorageManager.saveToSessionStorage(this.cacheNs, key, state);
+  }
+
+  /**
+   * clear all state caches
+   */
+  clearAllStateCaches() {
+    const localStorageManager = LocalStorageManager.getInstance();
+    localStorageManager.clearAllStateCaches(this.cacheNs);
+  }
+
+}
+
+module.exports = TagCacheManager;

+ 59 - 0
packages/core/src/plugin/util/args-parser.js

@@ -0,0 +1,59 @@
+/**
+ * Arguments parser for custom tag
+ */
+class ArgsParser {
+
+  /**
+   * @typedef ParseArgsResult
+   * @property {string} firstArgsKey - key of the first argument
+   * @property {string} firstArgsValue - value of the first argument
+   * @property {object} options - key of the first argument
+   */
+
+  /**
+   * parse plugin argument strings
+   *
+   * @static
+   * @param {string} str
+   * @returns {ParseArgsResult}
+   */
+  static parse(str) {
+    let firstArgsKey = null;
+    let firstArgsValue = null;
+    const options = {};
+
+    if (str != null && str.length > 0) {
+      const splittedArgs = str.split(',');
+
+      splittedArgs.forEach((rawArg, index) => {
+        const arg = rawArg.trim();
+
+        // parse string like 'key1=value1, key2=value2, ...'
+        // see https://regex101.com/r/pYHcOM/1
+        const match = arg.match(/([^=]+)=?(.+)?/);
+
+        if (match == null) {
+          return;
+        }
+
+        const key = match[1];
+        const value = match[2] || true;
+        options[key] = value;
+
+        if (index === 0) {
+          firstArgsKey = key;
+          firstArgsValue = value;
+        }
+      });
+    }
+
+    return {
+      firstArgsKey,
+      firstArgsValue,
+      options,
+    };
+  }
+
+}
+
+module.exports = ArgsParser;

+ 88 - 0
packages/core/src/plugin/util/custom-tag-utils.js

@@ -0,0 +1,88 @@
+const TagContext = require('../model/tag-context');
+
+/**
+ * @private
+ *
+ * create random strings
+ * @see http://qiita.com/ryounagaoka/items/4736c225bdd86a74d59c
+ *
+ * @param {number} length
+ * @return {string} random strings
+ */
+function createRandomStr(length) {
+  const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
+  let generated = '';
+  for (let i = 0; i < length; i++) {
+    generated += bag[Math.floor(Math.random() * bag.length)];
+  }
+  return generated;
+}
+
+/**
+ * @typedef FindTagAndReplaceResult
+ * @property {string} html - HTML string
+ * @property {Object} tagContextMap - Object.<string, [TagContext]{@link ../model/tag-context.html#TagContext}>
+ *
+ * @memberof customTagUtils
+ */
+/**
+ * @param {RegExp} tagPattern
+ * @param {string} html
+ * @param {function} replace replace function
+ * @return {FindTagAndReplaceResult}
+ *
+ * @memberof customTagUtils
+ */
+function findTagAndReplace(tagPattern, html, replace) {
+  let replacedHtml = html;
+  const tagContextMap = {};
+
+  if (tagPattern == null || html == null) {
+    return { html: replacedHtml, tagContextMap };
+  }
+
+  // see: https://regex101.com/r/NQq3s9/9
+  const pattern = new RegExp(`\\$(${tagPattern.source})\\((.*?)\\)(?=[<\\[\\s\\$])|\\$(${tagPattern.source})\\((.*)\\)(?![<\\[\\s\\$])`, 'g');
+
+  replacedHtml = html.replace(pattern, (all, group1, group2, group3, group4) => {
+    const tagExpression = all;
+    const method = (group1 || group3).trim();
+    const args = (group2 || group4 || '').trim();
+
+    // create contexts
+    const tagContext = new TagContext({ tagExpression, method, args });
+
+    if (replace != null) {
+      return replace(tagContext);
+    }
+
+    // replace with empty dom
+    const domId = `${method}-${createRandomStr(8)}`;
+    tagContextMap[domId] = tagContext;
+    return `<div id="${domId}"></div>`;
+  });
+
+  return { html: replacedHtml, tagContextMap };
+}
+
+/**
+ * @namespace customTagUtils
+ */
+module.exports = {
+  findTagAndReplace,
+  /**
+   * Context class used by findTagAndReplace
+   * @memberof customTagUtils
+   */
+  TagContext,
+  /**
+   * [ArgsParser]{@link ./args-parser#ArgsParser}
+   * @memberof customTagUtils
+   */
+  ArgsParser: require('./args-parser'),
+  /**
+   * [OptionParser]{@link ./option-parser#OptionParser}
+   * @memberof customTagUtils
+   */
+  OptionParser: require('./option-parser'),
+};

Some files were not shown because too many files changed in this diff