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

Merge branch 'dev/7.0.x' into support/clean-obsoleted-codes

Yuki Takei 2 лет назад
Родитель
Сommit
b43b972e1c
38 измененных файлов с 581 добавлено и 161 удалено
  1. 1 1
      .devcontainer/Dockerfile
  2. 6 6
      .github/workflows/ci-app-prod.yml
  3. 3 3
      .github/workflows/ci-app.yml
  4. 3 3
      .github/workflows/ci-slackbot-proxy.yml
  5. 1 1
      .github/workflows/list-unhealthy-branches.yml
  6. 1 1
      .github/workflows/release-slackbot-proxy.yml
  7. 2 2
      .github/workflows/release.yml
  8. 3 3
      .mergify.yml
  9. 1 1
      README.md
  10. 1 1
      README_JP.md
  11. 4 4
      apps/app/docker/Dockerfile
  12. 4 4
      apps/app/package.json
  13. 1 1
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  14. 13 0
      apps/app/src/components/PageEditor/PageEditor.tsx
  15. 1 0
      apps/app/src/components/PageEditor/Preview.module.scss
  16. 3 2
      apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx
  17. 10 3
      apps/app/src/features/search/client/components/SearchForm.tsx
  18. 10 10
      apps/app/src/features/search/client/components/SearchHelp.tsx
  19. 1 1
      apps/app/src/features/search/client/components/SearchMenuItem.tsx
  20. 22 16
      apps/app/src/features/search/client/components/SearchMethodMenuItem.tsx
  21. 4 3
      apps/app/src/features/search/client/components/SearchModal.tsx
  22. 5 6
      apps/app/src/features/search/client/components/SearchResultMenuItem.tsx
  23. 6 1
      apps/app/src/server/routes/apiv3/page/create-page.ts
  24. 3 6
      apps/app/src/services/renderer/remark-plugins/xsv-to-table.ts
  25. 4 4
      apps/slackbot-proxy/docker/Dockerfile
  26. 2 2
      package.json
  27. 8 1
      packages/editor/package.json
  28. 1 1
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  29. 7 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropdownItem.tsx
  30. 16 2
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  31. 7 5
      packages/editor/src/services/file-dropzone/use-file-dropzone/use-file-dropzone.ts
  32. 13 14
      packages/editor/src/services/list-util/insert-newline-continue-markup.ts
  33. 0 0
      packages/editor/src/services/paste-util/paste-markdown-util.ts
  34. 1 0
      packages/editor/src/services/table-util/index.ts
  35. 202 0
      packages/editor/src/services/table-util/insert-new-row-to-table-markdown.ts
  36. 24 0
      packages/editor/src/services/table-util/markdown-table.d.ts
  37. 147 0
      packages/editor/src/services/table-util/markdown-table.js
  38. 40 52
      yarn.lock

+ 1 - 1
.devcontainer/Dockerfile

@@ -3,7 +3,7 @@
 # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
 #-------------------------------------------------------------------------------------------------------------
 
-FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
+FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-20
 
 # The node image includes a non-root user with sudo access. Use the
 # "remoteUser" property in devcontainer.json to use it. On Linux, update

+ 6 - 6
.github/workflows/ci-app-prod.yml

@@ -51,16 +51,16 @@ jobs:
   test-prod-node16:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     with:
-      node-version: 16.x
+      node-version: 18.x
       skip-cypress: true
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
-  test-prod-node18:
+  test-prod-node20:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     with:
-      node-version: 18.x
+      node-version: 20.x
       skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       cypress-report-artifact-name: Cypress report
       cypress-config-video: ${{ inputs.cypress-config-video || false }}
@@ -68,15 +68,15 @@ jobs:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
-  run-reg-suit-node18:
-    needs: [test-prod-node18]
+  run-reg-suit-node20:
+    needs: [test-prod-node20]
 
     uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@dev/7.0.x
 
     if: always()
 
     with:
-      node-version: 18.x
+      node-version: 20.x
       skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       cypress-report-artifact-name: Cypress report
     secrets:

+ 3 - 3
.github/workflows/ci-app.yml

@@ -27,7 +27,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     steps:
       - uses: actions/checkout@v3
@@ -92,7 +92,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     services:
       mongodb:
@@ -174,7 +174,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     services:
       mongodb:

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

@@ -29,7 +29,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     steps:
     - uses: actions/checkout@v3
@@ -94,7 +94,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     services:
       mysql:
@@ -179,7 +179,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     services:
       mysql:

+ 1 - 1
.github/workflows/list-unhealthy-branches.yml

@@ -16,7 +16,7 @@ jobs:
 
     - uses: actions/setup-node@v3
       with:
-        node-version: '16'
+        node-version: '18'
 
     - name: List branches
       id: list-branches

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

@@ -102,7 +102,7 @@ jobs:
 
     - uses: actions/setup-node@v3
       with:
-        node-version: '16'
+        node-version: '18'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 

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

@@ -24,7 +24,7 @@ jobs:
 
     - uses: actions/setup-node@v3
       with:
-        node-version: '18'
+        node-version: '20'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 
@@ -189,7 +189,7 @@ jobs:
 
     - uses: actions/setup-node@v3
       with:
-        node-version: '18'
+        node-version: '20'
         cache: 'yarn'
         cache-dependency-path: '**/yarn.lock'
 

+ 3 - 3
.mergify.yml

@@ -3,9 +3,9 @@ pull_request_rules:
     conditions:
       - author = dependabot[bot]
       - '#approved-reviews-by >= 1'
-      - check-success = "lint (18.x)"
-      - check-success = "test (18.x)"
-      - check-success = "launch-dev (18.x)"
+      - check-success = "lint (20.x)"
+      - check-success = "test (20.x)"
+      - check-success = "launch-dev (20.x)"
       - check-success = "test-prod-node16 / launch-prod"
       - check-success = "test-prod-node18 / launch-prod"
     actions:

+ 1 - 1
README.md

@@ -79,7 +79,7 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 
 ## Dependencies
 
-- Node.js v16.x or v18.x
+- Node.js v18.x or v20.x
 - npm 6.x
 - yarn
 - [Turborepo](https://turbo.build/repo)

+ 1 - 1
README_JP.md

@@ -78,7 +78,7 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 
 ## 依存関係
 
-- Node.js v16.x or v18.x
+- Node.js v18.x or v20.x
 - npm 6.x
 - yarn
 - [Turborepo](https://turbo.build/repo)

+ 4 - 4
apps/app/docker/Dockerfile

@@ -4,7 +4,7 @@
 ##
 ## base
 ##
-FROM node:18-slim AS base
+FROM node:20-slim AS base
 
 ENV optDir /opt
 
@@ -18,7 +18,7 @@ RUN turbo prune --scope=@growi/app --docker
 ##
 ## deps-resolver
 ##
-FROM node:18-slim AS deps-resolver
+FROM node:20-slim AS deps-resolver
 
 ENV optDir /opt
 
@@ -62,7 +62,7 @@ RUN tar -cf node_modules.tar \
 ##
 ## builder
 ##
-FROM node:18-slim AS builder
+FROM node:20-slim AS builder
 
 ENV optDir /opt
 
@@ -107,7 +107,7 @@ RUN tar -cf packages.tar \
 ##
 ## release
 ##
-FROM node:18-slim
+FROM node:20-slim
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
 ENV NODE_ENV production

+ 4 - 4
apps/app/package.json

@@ -62,7 +62,7 @@
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
     "@aws-sdk/client-s3": "3.454.0",
     "@aws-sdk/s3-request-presigner": "3.454.0",
-    "@azure/identity": "^3.3.2",
+    "@azure/identity": "^4.0.1",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
@@ -101,7 +101,7 @@
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "csurf": "^1.11.0",
-    "csv-to-markdown-table": "^1.1.0",
+    "csv-to-markdown-table": "^1.4.1",
     "date-fns": "^2.23.0",
     "dayjs": "^1.11.7",
     "detect-indent": "^7.0.0",
@@ -131,7 +131,7 @@
     "is-iso-date": "^0.0.1",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
-    "markdown-table": "^1.1.1",
+    "markdown-table": "^3.0.3",
     "md5": "^2.2.1",
     "mermaid": "^10.1.0",
     "method-override": "^3.0.0",
@@ -214,7 +214,7 @@
     "xss": "^1.0.14",
     "y-mongodb-provider": "^0.1.7",
     "y-socket.io": "^1.1.0",
-    "yjs": "^13.6.7"
+    "yjs": "^13.6.12"
   },
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",

+ 1 - 1
apps/app/src/components/PageEditor/HandsontableModal.tsx

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
 
 import { useHandsontableModalForEditor } from '@growi/editor/src/stores/use-handsontable';
 import { HotTable } from '@handsontable/react';
-import Handsontable from 'handsontable';
+import type Handsontable from 'handsontable';
 import { useTranslation } from 'next-i18next';
 import {
   Collapse,

+ 13 - 0
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -382,6 +382,19 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     }
   }, [initialValue, isIndentSizeForced, mutateCurrentIndentSize]);
 
+  // set handler to set caret line
+  useEffect(() => {
+    const handler = (lineNumber?: number) => {
+      codeMirrorEditor?.setCaretLine(lineNumber);
+
+      // TODO: scroll to the caret line
+    };
+    globalEmitter.on('setCaretLine', handler);
+
+    return function cleanup() {
+      globalEmitter.removeListener('setCaretLine', handler);
+    };
+  }, [codeMirrorEditor]);
 
   // TODO: Check the reproduction conditions that made this code necessary and confirm reproduction
   // // when transitioning to a different page, if the initialValue is the same,

+ 1 - 0
apps/app/src/components/PageEditor/Preview.module.scss

@@ -3,6 +3,7 @@
 .page-editor-preview-body :global {
   .wiki {
     max-width: 980px;
+    padding: 0px 15px;
     margin: 0 auto;
   }
 }

+ 3 - 2
apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -1,7 +1,7 @@
 import React, { useState, type FC, useCallback } from 'react';
 
 import { createPage } from '~/client/services/page-operation';
-import { useSWRxPageChildren } from '~/stores/page-listing';
+import { useSWRxPageChildren, mutatePageTree } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 
 import { shouldCreateWipPage } from '../../../utils/should-create-wip-page';
@@ -10,6 +10,7 @@ import type { TreeItemToolProps } from '../interfaces';
 import { NewPageCreateButton } from './NewPageCreateButton';
 import { NewPageInput } from './NewPageInput';
 
+
 type UseNewPageInput = {
   Input: FC<TreeItemToolProps>,
   CreateButton: FC<TreeItemToolProps>,
@@ -77,7 +78,7 @@ export const useNewPageInput = (): UseNewPageInput => {
         wip: shouldCreateWipPage(newPagePath),
       });
 
-      mutateChildren();
+      mutatePageTree();
 
       if (!hasDescendants) {
         stateHandlers?.setIsOpen(true);

+ 10 - 3
apps/app/src/features/search/client/components/SearchForm.tsx

@@ -2,7 +2,7 @@ import React, {
   useCallback, useRef, useEffect, useMemo,
 } from 'react';
 
-import { GetInputProps } from '../interfaces/downshift';
+import type { GetInputProps } from '../interfaces/downshift';
 
 type Props = {
   searchKeyword: string,
@@ -35,7 +35,7 @@ export const SearchForm = (props: Props): JSX.Element => {
 
   const inputOptions = useMemo(() => {
     return getInputProps({
-      type: 'search',
+      type: 'text',
       placeholder: 'Search...',
       className: 'form-control',
       ref: inputRef,
@@ -52,11 +52,18 @@ export const SearchForm = (props: Props): JSX.Element => {
 
   return (
     <form
-      className="w-100"
+      className="w-100 position-relative"
       onSubmit={submitHandler}
       data-testid="search-form"
     >
       <input {...inputOptions} />
+      <button
+        type="button"
+        className="btn btn-neutral-secondary text-muted position-absolute bottom-0 end-0 w-auto h-100 border-0"
+        onClick={() => { onChange?.('') }}
+      >
+        <span className="material-symbols-outlined p-0">cancel</span>
+      </button>
     </form>
   );
 };

+ 10 - 10
apps/app/src/features/search/client/components/SearchHelp.tsx

@@ -11,40 +11,40 @@ export const SearchHelp = (): JSX.Element => {
   return (
     <>
       <button type="button" className="btn border-0 text-muted d-flex justify-content-center align-items-center ms-1 p-0" onClick={() => setIsOpen(!isOpen)}>
-        <span className="material-symbols-outlined me-2">help</span>
-        { t('search_help.title') }
-        <span className="material-symbols-outlined ms-2">{isOpen ? 'expand_less' : 'expand_more'}</span>
+        <span className="material-symbols-outlined me-2 p-0">help</span>
+        <span>{t('search_help.title')}</span>
+        <span className="material-symbols-outlined ms-2 p-0">{isOpen ? 'expand_less' : 'expand_more'}</span>
       </button>
       <Collapse isOpen={isOpen}>
-        <table className="table m-0">
+        <table className="table table-borderless m-0">
           <tbody>
-            <tr>
+            <tr className="border-bottom">
               <th className="py-2">
                 <code>word1</code> <code>word2</code><br />
                 <small className="text-muted">({ t('search_help.and.syntax help') })</small>
               </th>
               <td><h6 className="m-0 text-muted">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
             </tr>
-            <tr>
+            <tr className="border-bottom">
               <th className="py-2">
                 <code>&quot;This is GROWI&quot;</code><br />
                 <small className="text-muted">({ t('search_help.phrase.syntax help') })</small>
               </th>
               <td><h6 className="m-0 text-muted">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
             </tr>
-            <tr>
+            <tr className="border-bottom">
               <th className="py-2"><code>-keyword</code></th>
               <td><h6 className="m-0 text-muted">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
             </tr>
-            <tr>
+            <tr className="border-bottom">
               <th className="py-2"><code>prefix:/user/</code></th>
               <td><h6 className="m-0 text-muted">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
             </tr>
-            <tr>
+            <tr className="border-bottom">
               <th className="py-2"><code>-prefix:/user/</code></th>
               <td><h6 className="m-0 text-muted">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
             </tr>
-            <tr>
+            <tr className="border-bottom">
               <th className="py-2"><code>tag:wiki</code></th>
               <td><h6 className="m-0 text-muted">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
             </tr>

+ 1 - 1
apps/app/src/features/search/client/components/SearchMenuItem.tsx

@@ -21,7 +21,7 @@ export const SearchMenuItem = (props: Props): JSX.Element => {
     getItemProps({
       index,
       item: { url },
-      className: `d-flex p-1 text-muted ${isActive ? 'active' : ''}`,
+      className: `d-flex align-items-center px-2 py-1 text-muted ${isActive ? 'active' : ''}`,
     })
   );
 

+ 22 - 16
apps/app/src/features/search/client/components/SearchMethodMenuItem.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { DevidedPagePath } from '@growi/core/dist/models';
 import { useTranslation } from 'next-i18next';
 
 import { useCurrentPagePath } from '~/stores/page';
@@ -23,10 +24,15 @@ export const SearchMethodMenuItem = (props: Props): JSX.Element => {
 
   const { data: currentPagePath } = useCurrentPagePath();
 
+  const dPagePath = (new DevidedPagePath(currentPagePath ?? '', true, true));
+  const currentPageName = `
+  ${(!(dPagePath.isRoot || dPagePath.isFormerRoot) ? '...' : '')}/${(dPagePath.isRoot ? '' : `${dPagePath.latter}/`)}
+  `;
+
   const shouldShowMenuItem = searchKeyword.trim().length > 0;
 
   return (
-    <>
+    <div>
       { shouldShowMenuItem && (
         <div data-testid="search-all-menu-item">
           <SearchMenuItem
@@ -35,15 +41,14 @@ export const SearchMethodMenuItem = (props: Props): JSX.Element => {
             getItemProps={getItemProps}
             url={`/_search?q=${searchKeyword}`}
           >
-            <span className="material-symbols-outlined fs-4 me-3">search</span>
-            <span>{searchKeyword}</span>
-            <div className="ms-auto">
-              <span>{t('search_method_menu_item.search_in_all')}</span>
+            <span className="material-symbols-outlined fs-4 me-3 p-0">search</span>
+            <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
+              <span className="text-break me-auto">{searchKeyword}</span>
+              <span className="small text-body-tertiary">{t('search_method_menu_item.search_in_all')}</span>
             </div>
           </SearchMenuItem>
         </div>
       )}
-
       <div data-testid="search-prefix-menu-item">
         <SearchMenuItem
           index={shouldShowMenuItem ? 1 : 0}
@@ -51,11 +56,11 @@ export const SearchMethodMenuItem = (props: Props): JSX.Element => {
           getItemProps={getItemProps}
           url={`/_search?q=prefix:${currentPagePath} ${searchKeyword}`}
         >
-          <span className="material-symbols-outlined fs-4 me-3">search</span>
-          <code>prefix: {currentPagePath}</code>
-          <span className="ms-2">{searchKeyword}</span>
-          <div className="ms-auto">
-            <span>{t('search_method_menu_item.only_children_of_this_tree')}</span>
+          <span className="material-symbols-outlined fs-4 me-3 p-0">search</span>
+          <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
+            <code className="text-break">{currentPageName}</code>
+            <span className="ms-md-2 text-break me-auto">{searchKeyword}</span>
+            <span className="small text-body-tertiary">{t('search_method_menu_item.only_children_of_this_tree')}</span>
           </div>
         </SearchMenuItem>
       </div>
@@ -67,13 +72,14 @@ export const SearchMethodMenuItem = (props: Props): JSX.Element => {
           getItemProps={getItemProps}
           url={`/_search?q="${searchKeyword}"`}
         >
-          <span className="material-symbols-outlined fs-4 me-3">search</span>
-          <span>{`"${searchKeyword}"`}</span>
-          <div className="ms-auto">
-            <span>{t('search_method_menu_item.exact_mutch')}</span>
+          <span className="material-symbols-outlined fs-4 me-3 p-0">search</span>
+          <div className="w-100 d-flex align-items-md-center flex-md-row align-items-start flex-column">
+            <span className="text-break me-auto">{`"${searchKeyword}"`}</span>
+            <span className="small text-body-tertiary">{t('search_method_menu_item.exact_mutch')}</span>
           </div>
         </SearchMenuItem>
       ) }
-    </>
+    </div>
+
   );
 };

+ 4 - 3
apps/app/src/features/search/client/components/SearchModal.tsx

@@ -56,7 +56,7 @@ const SearchModal = (): JSX.Element => {
 
   return (
     <Modal size="lg" isOpen={searchModalData?.isOpened ?? false} toggle={closeSearchModal} data-testid="search-modal">
-      <ModalBody>
+      <ModalBody className="pb-2">
         <Downshift
           onSelect={selectSearchMenuItemHandler}
           stateReducer={stateReducer}
@@ -83,11 +83,11 @@ const SearchModal = (): JSX.Element => {
                   className="btn border-0 d-flex justify-content-center p-0"
                   onClick={closeSearchModal}
                 >
-                  <span className="material-symbols-outlined fs-4 ms-3">close</span>
+                  <span className="material-symbols-outlined fs-4 ms-3 py-0">close</span>
                 </button>
               </div>
 
-              <ul {...getMenuProps()} className="list-unstyled">
+              <ul {...getMenuProps()} className="list-unstyled m-0">
                 <div className="border-top mt-3 mb-2" />
                 <SearchMethodMenuItem
                   activeIndex={highlightedIndex}
@@ -100,6 +100,7 @@ const SearchModal = (): JSX.Element => {
                   searchKeyword={searchKeyword}
                   getItemProps={getItemProps}
                 />
+                <div className="border-top mt-2 mb-2" />
               </ul>
             </div>
           )}

+ 5 - 6
apps/app/src/features/search/client/components/SearchResultMenuItem.tsx

@@ -46,7 +46,7 @@ export const SearchResultMenuItem = (props: Props): JSX.Element => {
   }
 
   return (
-    <>
+    <div>
       {searchResult?.data
         .map((item, index) => (
           <SearchMenuItem
@@ -62,14 +62,13 @@ export const SearchResultMenuItem = (props: Props): JSX.Element => {
               <PagePathLabel path={item.data.path} />
             </span>
 
-            <span className="ms-2 d-flex justify-content-center align-items-center">
-              <span className="material-symbols-outlined fs-5">footprint</span>
-              <span>{item.data.seenUsers.length}</span>
+            <span className="text-body-tertiary ms-2 d-flex justify-content-center align-items-center">
+              <span className="material-symbols-outlined fs-6 p-0">footprint</span>
+              <span className="fs-6">{item.data.seenUsers.length}</span>
             </span>
           </SearchMenuItem>
         ))
       }
-      <div className="border-top mt-2 mb-2" />
-    </>
+    </div>
   );
 };

+ 6 - 1
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -2,7 +2,7 @@ import type {
   IPage, IUser, IUserHasId,
 } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
-import { isCreatablePage, isUserPage } from '@growi/core/dist/utils/page-path-utils';
+import { isCreatablePage, isUserPage, isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import { attachTitleHeader, normalizePath } from '@growi/core/dist/utils/path-utils';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
@@ -63,6 +63,11 @@ async function determinePath(_parentPath?: string, _path?: string, optionalParen
   if (_parentPath != null) {
     const parentPath = normalizePath(_parentPath);
 
+    // when parentPath is user's homepage
+    if (isUsersHomepage(parentPath)) {
+      return generateUntitledPath(parentPath, basePathname);
+    }
+
     // when parentPath is valid
     if (isCreatablePage(parentPath)) {
       return generateUntitledPath(parentPath, basePathname);

+ 3 - 6
apps/app/src/services/renderer/remark-plugins/xsv-to-table.ts

@@ -2,8 +2,8 @@ import csvToMarkdownTable from 'csv-to-markdown-table';
 import { fromMarkdown } from 'mdast-util-from-markdown';
 import { gfmTableFromMarkdown } from 'mdast-util-gfm-table';
 import { gfmTable } from 'micromark-extension-gfm-table';
-import { Plugin } from 'unified';
-import { Node } from 'unist';
+import type { Plugin } from 'unified';
+import type { Node } from 'unist';
 import { visit } from 'unist-util-visit';
 
 type Lang = 'csv' | 'csv-h' | 'tsv' | 'tsv-h';
@@ -12,13 +12,10 @@ function isXsv(lang: unknown): lang is Lang {
   return /^(csv|csv-h|tsv|tsv-h)$/.test(lang as string);
 }
 
-// workaround for the broken type definition of csv-to-markdown-table -- 2022.09.15 Yuki Takei
-const csvToMarkdown = csvToMarkdownTable.csvToMarkdown ?? csvToMarkdownTable;
-
 function rewriteNode(node: Node, lang: Lang) {
   const tableContents = node.value as string;
 
-  const tableDoc = csvToMarkdown(
+  const tableDoc = csvToMarkdownTable(
     tableContents,
     lang === 'csv' || lang === 'csv-h' ? ',' : '\t',
     lang === 'csv-h' || lang === 'tsv-h',

+ 4 - 4
apps/slackbot-proxy/docker/Dockerfile

@@ -3,7 +3,7 @@
 ##
 ## base
 ##
-FROM node:18-slim AS base
+FROM node:20-slim AS base
 
 ENV optDir /opt
 
@@ -17,7 +17,7 @@ RUN turbo prune --scope=@growi/slackbot-proxy --docker
 ##
 ## deps-resolver
 ##
-FROM node:18-slim AS deps-resolver
+FROM node:20-slim AS deps-resolver
 
 ENV optDir /opt
 
@@ -57,7 +57,7 @@ RUN tar -cf node_modules.tar \
 ##
 ## builder
 ##
-FROM node:18-slim AS builder
+FROM node:20-slim AS builder
 
 ENV optDir /opt
 
@@ -95,7 +95,7 @@ RUN tar -cf packages.tar \
 ##
 ## release
 ##
-FROM node:18-slim
+FROM node:20-slim
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
 ENV NODE_ENV production

+ 2 - 2
package.json

@@ -96,8 +96,8 @@
     "vitest-mock-extended": "^1.1.3"
   },
   "engines": {
-    "node": "^16 || ^18",
-    "npm": ">=8.5 < 9",
+    "node": "^18 || ^20",
+    "npm": ">=8.5 < 9.6.6",
     "yarn": ">=1.22 <2"
   }
 }

+ 8 - 1
packages/editor/package.json

@@ -17,9 +17,13 @@
     "version": "yarn version --no-git-tag-version --preid=RC"
   },
   "dependencies": {
+    "markdown-table": "^3.0.3",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
+  "// comments for devDependencies": {
+    "string-width": "5.0.0 or above exports only ESM."
+  },
   "devDependencies": {
     "@codemirror/lang-markdown": "^6.2.0",
     "@codemirror/language": "^6.8.0",
@@ -41,16 +45,19 @@
     "cm6-theme-material-dark": "^0.2.0",
     "cm6-theme-nord": "^0.2.0",
     "codemirror": "^6.0.1",
+    "csv-to-markdown-table": "^1.4.1",
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-react-refresh": "^0.4.1",
+    "markdown-table": "^3.0.3",
     "react-dropzone": "^14.2.3",
     "react-hook-form": "^7.45.4",
     "react-toastify": "^9.1.3",
     "reactstrap": "^9.2.0",
+    "string-width": "=4.2.2",
     "swr": "^2.2.2",
     "ts-deepmerge": "^6.2.0",
     "y-codemirror.next": "^0.3.2",
     "y-socket.io": "^1.1.0",
-    "yjs": "^13.6.7"
+    "yjs": "^13.6.12"
   }
 }

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -14,7 +14,7 @@ import {
 } from '../../services';
 import {
   adjustPasteData, getStrFromBol,
-} from '../../services/list-util/markdown-list-util';
+} from '../../services/paste-util/paste-markdown-util';
 import { useCodeMirrorEditorIsolated } from '../../stores';
 
 import { Toolbar } from './Toolbar';

+ 7 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropdownItem.tsx

@@ -25,7 +25,13 @@ export const AttachmentsDropdownItem = (props: Props): JSX.Element => {
     getRootProps,
     getInputProps,
     open,
-  } = useFileDropzone({ onUpload, acceptedUploadFileType });
+  } = useFileDropzone({
+    onUpload,
+    acceptedUploadFileType,
+    dropzoneOpts: {
+      noClick: true, noDrag: true, noKeyboard: true,
+    },
+  });
 
   return (
     <div {...getRootProps()} className="dropzone">

+ 16 - 2
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -10,6 +10,7 @@ import {
   EditorState, Prec, type Extension,
 } from '@codemirror/state';
 import { keymap, EditorView } from '@codemirror/view';
+import type { Command } from '@codemirror/view';
 import { tags } from '@lezer/highlight';
 import { useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
 import deepmerge from 'ts-deepmerge';
@@ -19,6 +20,8 @@ import deepmerge from 'ts-deepmerge';
 import { yUndoManagerKeymap } from 'y-codemirror.next';
 
 import { emojiAutocompletionSettings } from '../../extensions/emojiAutocompletionSettings';
+import { insertNewlineContinueMarkup } from '../../list-util/insert-newline-continue-markup';
+import { insertNewRowToMarkdownTable, isInTable } from '../../table-util/insert-new-row-to-table-markdown';
 
 import { useAppendExtensions, type AppendExtensions } from './utils/append-extensions';
 import { useFocus, type Focus } from './utils/focus';
@@ -26,18 +29,29 @@ import { FoldDrawio, useFoldDrawio } from './utils/fold-drawio';
 import { useGetDoc, type GetDoc } from './utils/get-doc';
 import { useInitDoc, type InitDoc } from './utils/init-doc';
 import { useInsertMarkdownElements, type InsertMarkdowElements } from './utils/insert-markdown-elements';
-import { insertNewlineContinueMarkup } from './utils/insert-newline-continue-markup';
 import { useInsertPrefix, type InsertPrefix } from './utils/insert-prefix';
 import { useInsertText, type InsertText } from './utils/insert-text';
 import { useReplaceText, type ReplaceText } from './utils/replace-text';
 import { useSetCaretLine, type SetCaretLine } from './utils/set-caret-line';
 
 
+const onPressEnter: Command = (editor) => {
+
+  if (isInTable(editor)) {
+    insertNewRowToMarkdownTable(editor);
+    return true;
+  }
+
+  insertNewlineContinueMarkup(editor);
+
+  return true;
+};
+
 // set new markdownKeymap instead of default one
 // https://github.com/codemirror/lang-markdown/blob/main/src/index.ts#L17
 const markdownKeymap = [
   { key: 'Backspace', run: deleteCharBackward },
-  { key: 'Enter', run: insertNewlineContinueMarkup },
+  { key: 'Enter', run: onPressEnter },
 ];
 
 const markdownHighlighting = HighlightStyle.define([

+ 7 - 5
packages/editor/src/services/file-dropzone/use-file-dropzone/use-file-dropzone.ts

@@ -35,11 +35,13 @@ export const useFileDropzone = (props: Props): FileDropzoneState => {
 
   }, [onUpload, setIsUploading, acceptedUploadFileType]);
 
-  const accept: Accept | undefined = acceptedUploadFileType === AcceptedUploadFileType.IMAGE
-    ? {
-      'image/*': [],
-    }
-    : undefined;
+  let accept: Accept | undefined;
+  if (acceptedUploadFileType === AcceptedUploadFileType.ALL) {
+    accept = { 'application/*': [] };
+  }
+  else if (acceptedUploadFileType === AcceptedUploadFileType.IMAGE) {
+    accept = { 'image/*': [] };
+  }
 
   const dzState = useDropzone({
     onDrop: dropHandler,

+ 13 - 14
packages/editor/src/services/codemirror-editor/use-codemirror-editor/utils/insert-newline-continue-markup.ts → packages/editor/src/services/list-util/insert-newline-continue-markup.ts

@@ -1,25 +1,26 @@
-import type { ChangeSpec, StateCommand } from '@codemirror/state';
+import type { ChangeSpec } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
 
 // https://regex101.com/r/7BN2fR/5
 const indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
 const indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
-export const insertNewlineContinueMarkup: StateCommand = ({ state, dispatch }) => {
+export const insertNewlineContinueMarkup = (editor: EditorView): void => {
 
   const changes: ChangeSpec[] = [];
 
   let selection;
 
-  const curPos = state.selection.main.head;
+  const curPos = editor.state.selection.main.head;
 
-  const aboveLine = state.doc.lineAt(curPos).number;
-  const bolPos = state.doc.line(aboveLine).from;
+  const aboveLine = editor.state.doc.lineAt(curPos).number;
+  const bolPos = editor.state.doc.line(aboveLine).from;
 
-  const strFromBol = state.sliceDoc(bolPos, curPos);
+  const strFromBol = editor.state.sliceDoc(bolPos, curPos);
 
   // If the text before the cursor is only markdown symbols
   if (indentAndMarkOnlyRE.test(strFromBol)) {
-    const insert = state.lineBreak;
+    const insert = editor.state.lineBreak;
 
     changes.push({
       from: bolPos,
@@ -33,10 +34,10 @@ export const insertNewlineContinueMarkup: StateCommand = ({ state, dispatch }) =
     const indentAndMark = strFromBol.match(indentAndMarkRE)?.[0];
 
     if (indentAndMark == null) {
-      return false;
+      return;
     }
 
-    const insert = state.lineBreak + indentAndMark;
+    const insert = editor.state.lineBreak + indentAndMark;
     const nextCurPos = curPos + insert.length;
 
     selection = { anchor: nextCurPos };
@@ -49,7 +50,7 @@ export const insertNewlineContinueMarkup: StateCommand = ({ state, dispatch }) =
 
   // If the text before the cursor is regular text
   else {
-    const insert = state.lineBreak;
+    const insert = editor.state.lineBreak;
     const nextCurPos = curPos + insert.length;
 
     selection = { anchor: nextCurPos };
@@ -60,11 +61,9 @@ export const insertNewlineContinueMarkup: StateCommand = ({ state, dispatch }) =
     });
   }
 
-  dispatch(state.update({
+  editor.dispatch({
     changes,
     selection,
     userEvent: 'input',
-  }));
-
-  return true;
+  });
 };

+ 0 - 0
packages/editor/src/services/list-util/markdown-list-util.ts → packages/editor/src/services/paste-util/paste-markdown-util.ts


+ 1 - 0
packages/editor/src/services/table-util/index.ts

@@ -0,0 +1 @@
+export * from './markdown-table';

+ 202 - 0
packages/editor/src/services/table-util/insert-new-row-to-table-markdown.ts

@@ -0,0 +1,202 @@
+import { EditorView } from '@codemirror/view';
+
+import { MarkdownTable } from './markdown-table';
+
+// https://regex101.com/r/7BN2fR/10
+const linePartOfTableRE = /^([^\r\n|]*)\|(([^\r\n|]*\|)+)$/;
+// https://regex101.com/r/1UuWBJ/3
+export const emptyLineOfTableRE = /^([^\r\n|]*)\|((\s*\|)+)$/;
+
+const getCurPos = (editor: EditorView): number => {
+  return editor.state.selection.main.head;
+};
+
+export const isInTable = (editor: EditorView): boolean => {
+  const curPos = getCurPos(editor);
+  const lineText = editor.state.doc.lineAt(curPos).text;
+  return linePartOfTableRE.test(lineText);
+};
+
+const getBot = (editor: EditorView): number => {
+  if (!isInTable(editor)) {
+    return getCurPos(editor);
+  }
+
+  const doc = editor.state.doc;
+  const firstLine = 1;
+  let line = doc.lineAt(getCurPos(editor)).number - 1;
+  for (; line >= firstLine; line--) {
+    const strLine = doc.line(line).text;
+    if (!linePartOfTableRE.test(strLine)) {
+      break;
+    }
+  }
+  const botLine = Math.max(firstLine, line + 1);
+  return doc.line(botLine).from;
+};
+
+const getEot = (editor: EditorView): number => {
+  if (!isInTable(editor)) {
+    return getCurPos(editor);
+  }
+
+  const doc = editor.state.doc;
+  const lastLine = doc.lines;
+
+  let line = doc.lineAt(getCurPos(editor)).number + 1;
+
+  for (; line <= lastLine; line++) {
+    const strLine = doc.line(line).text;
+    if (!linePartOfTableRE.test(strLine)) {
+      break;
+    }
+  }
+
+  const eotLine = line - 1;
+
+  return doc.line(eotLine).to;
+};
+
+const getStrFromBot = (editor: EditorView): string => {
+  return editor.state.sliceDoc(getBot(editor), getCurPos(editor));
+};
+
+const getStrToEot = (editor: EditorView): string => {
+  return editor.state.sliceDoc(getCurPos(editor), getEot(editor));
+};
+
+const addRowToMarkdownTable = (mdtable: MarkdownTable): any => {
+  const numCol = mdtable.table.length > 0 ? mdtable.table[0].length : 1;
+  const newRow: string[] = new Array(numCol);
+
+  newRow.fill('');
+
+  mdtable.table.push(newRow);
+};
+
+export const mergeMarkdownTable = (mdtableList: MarkdownTable[]): MarkdownTable => {
+  let newTable: any[] = [];
+  const options = mdtableList[0].options;
+  mdtableList.forEach((mdtable) => {
+    newTable = newTable.concat(mdtable.table);
+  });
+  return (new MarkdownTable(newTable, options));
+};
+
+const addRow = (editor: EditorView) => {
+  const strFromBot = getStrFromBot(editor);
+
+  let table = MarkdownTable.fromMarkdownString(strFromBot);
+
+  addRowToMarkdownTable(table);
+
+  const strToEot = getStrToEot(editor);
+
+  const tableBottom = MarkdownTable.fromMarkdownString(strToEot);
+
+  if (tableBottom.table.length > 0) {
+    table = mergeMarkdownTable([table, tableBottom]);
+  }
+
+  const curPos = getCurPos(editor);
+
+  const curLine = editor.state.doc.lineAt(curPos).number;
+  const nextLine = curLine + 1;
+
+  const botPos = getBot(editor);
+  const eotPos = getEot(editor);
+
+  editor.dispatch({
+    changes: {
+      from: botPos,
+      to: eotPos,
+      insert: table.toString(),
+    },
+  });
+
+  const nextCurPos = editor.state.doc.line(nextLine).from + 2;
+
+  editor.dispatch({
+    selection: { anchor: nextCurPos },
+  });
+};
+
+const removeRow = (editor: EditorView) => {
+
+  const curPos = getCurPos(editor);
+
+  const curLine = editor.state.doc.lineAt(curPos).number;
+
+  const bolPos = editor.state.doc.line(curLine).from;
+  const eolPos = editor.state.doc.line(curLine).to;
+
+  const nextCurPos = editor.state.doc.lineAt(getCurPos(editor)).to + 1;
+
+  editor.dispatch({
+    changes: {
+      from: bolPos,
+      to: eolPos,
+    },
+  });
+
+  editor.dispatch({
+    selection: { anchor: nextCurPos },
+  });
+};
+
+const reformTable = (editor: EditorView) => {
+  const tableStr = getStrFromBot(editor) + getStrToEot(editor);
+  const table = MarkdownTable.fromMarkdownString(tableStr);
+
+  const curPos = getCurPos(editor);
+  const botPos = getBot(editor);
+  const eotPos = getEot(editor);
+
+  const curLine = editor.state.doc.lineAt(curPos).number;
+  const nextLine = curLine + 1;
+
+  const eolPos = editor.state.doc.line(curLine).to;
+  const strToEol = editor.state.sliceDoc(curPos, eolPos);
+
+  const isLastRow = getStrToEot(editor) === strToEol;
+
+  editor.dispatch({
+    changes: {
+      from: botPos,
+      to: eotPos,
+      insert: table.toString(),
+    },
+  });
+
+  const nextCurPos = isLastRow ? editor.state.doc.line(curLine).to : editor.state.doc.line(nextLine).from + 2;
+
+  editor.dispatch({
+    selection: { anchor: nextCurPos },
+  });
+};
+
+export const insertNewRowToMarkdownTable = (editor: EditorView): void => {
+
+  const curPos = getCurPos(editor);
+
+  const curLine = editor.state.doc.lineAt(curPos).number;
+
+  const bolPos = editor.state.doc.line(curLine).from;
+  const eolPos = editor.state.doc.line(curLine).to;
+
+  const strFromBol = editor.state.sliceDoc(bolPos, curPos);
+  const strToEol = editor.state.sliceDoc(curPos, eolPos);
+
+  const isLastRow = getStrToEot(editor) === strToEol;
+  const isEndOfLine = curPos === eolPos;
+
+  if (isEndOfLine) {
+    addRow(editor);
+  }
+  else if (isLastRow && emptyLineOfTableRE.test(strFromBol + strToEol)) {
+    removeRow(editor);
+  }
+  else {
+    reformTable(editor);
+  }
+};

+ 24 - 0
packages/editor/src/services/table-util/markdown-table.d.ts

@@ -0,0 +1,24 @@
+export declare class MarkdownTable {
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  static fromHTMLTableTag(str: any): MarkdownTable;
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  static fromDSV(str: any, delimiter: any): MarkdownTable;
+
+  static fromMarkdownString(str: string): MarkdownTable;
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  constructor(table: any, options: any);
+
+  table: any;
+
+  options: any;
+
+  toString(): any;
+
+  clone(): MarkdownTable;
+
+  normalizeCells(): MarkdownTable;
+
+}

+ 147 - 0
packages/editor/src/services/table-util/markdown-table.js

@@ -0,0 +1,147 @@
+import csvToMarkdown from 'csv-to-markdown-table';
+import { markdownTable } from 'markdown-table';
+import stringWidth from 'string-width';
+
+// https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
+// https://regex101.com/r/7BN2fR/7
+const tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
+const tableAlignmentLineNegRE = /^[^-:]*$/; // it is need to check to ignore empty row which is matched above RE
+const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
+
+const defaultOptions = { stringLength: stringWidth };
+
+/**
+ * markdown table class for markdown-table module
+ *   ref. https://github.com/wooorm/markdown-table
+ */
+export class MarkdownTable {
+
+  constructor(table, options) {
+    this.table = table || [];
+    this.options = Object.assign(options || {}, defaultOptions);
+
+    this.toString = this.toString.bind(this);
+  }
+
+  toString() {
+    return markdownTable(this.table, this.options);
+  }
+
+  /**
+   * returns cloned Markdowntable instance
+   * (This method clones only the table field.)
+   */
+  clone() {
+    const newTable = [];
+    for (let i = 0; i < this.table.length; i++) {
+      newTable.push([].concat(this.table[i]));
+    }
+    return new MarkdownTable(newTable, this.options);
+  }
+
+  /**
+   * normalize all cell data(trim & convert the newline character to space or pad '' if cell data is null)
+   */
+  normalizeCells() {
+    for (let i = 0; i < this.table.length; i++) {
+      for (let j = 0; j < this.table[i].length; j++) {
+        if (this.table[i][j] != null) {
+          this.table[i][j] = this.table[i][j].trim().replace(/\r?\n/g, ' ');
+        }
+        else {
+          this.table[i][j] = '';
+        }
+      }
+    }
+
+    return this;
+  }
+
+  /**
+   * return a MarkdownTable instance made from a string of HTML table tag
+   *
+   * If a parser error occurs, an error object with an error message is thrown.
+   * The error message is a innerHTML, so must not assign it into element.innerHTML because it can lead to Mutation-based XSS
+   */
+  static fromHTMLTableTag(str) {
+    // set up DOMParser
+    const domParser = new (window.DOMParser)();
+
+    // use DOMParser to prevent DOM based XSS (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
+    const dom = domParser.parseFromString(str, 'application/xml');
+
+    if (dom.querySelector('parsererror')) {
+      throw new Error(dom.documentElement.innerHTML);
+    }
+
+    const tableElement = dom.querySelector('table');
+    const trElements = tableElement.querySelectorAll('tr');
+
+    const table = [];
+    let maxRowSize = 0;
+    for (let i = 0; i < trElements.length; i++) {
+      const row = [];
+      const cellElements = trElements[i].querySelectorAll('th,td');
+      for (let j = 0; j < cellElements.length; j++) {
+        row.push(cellElements[j].innerHTML);
+      }
+      table.push(row);
+
+      if (maxRowSize < row.length) maxRowSize = row.length;
+    }
+
+    const align = [];
+    for (let i = 0; i < maxRowSize; i++) {
+      align.push('');
+    }
+
+    return new MarkdownTable(table, { align });
+  }
+
+  /**
+   * return a MarkdownTable instance made from a string of delimiter-separated values
+   */
+  static fromDSV(str, delimiter) {
+    return MarkdownTable.fromMarkdownString(csvToMarkdown(str, delimiter, true));
+  }
+
+  /**
+   * return a MarkdownTable instance
+   *   ref. https://github.com/wooorm/markdown-table
+   * @param {string} str markdown string
+   */
+  static fromMarkdownString(str) {
+    const arrMDTableLines = str.split(/(\r\n|\r|\n)/);
+    const contents = [];
+    let aligns = [];
+    for (let n = 0; n < arrMDTableLines.length; n++) {
+      const line = arrMDTableLines[n];
+
+      if (tableAlignmentLineRE.test(line) && !tableAlignmentLineNegRE.test(line)) {
+        // parse line which described alignment
+        const alignRuleRE = [
+          { align: 'c', regex: /^:-+:$/ },
+          { align: 'l', regex: /^:-+$/ },
+          { align: 'r', regex: /^-+:$/ },
+        ];
+        let lineText = '';
+        lineText = line.replace(/^\||\|$/g, ''); // strip off pipe charactor which is placed head of line and last of line.
+        lineText = lineText.replace(/\s*/g, '');
+        aligns = lineText.split(/\|/).map((col) => {
+          const rule = alignRuleRE.find((rule) => { return col.match(rule.regex) });
+          return (rule != null) ? rule.align : '';
+        });
+      }
+      else if (linePartOfTableRE.test(line)) {
+        // parse line whether header or body
+        let lineText = '';
+        lineText = line.replace(/\s*\|\s*/g, '|');
+        lineText = lineText.replace(/^\||\|$/g, ''); // strip off pipe charactor which is placed head of line and last of line.
+        const row = lineText.split(/\|/);
+        contents.push(row);
+      }
+    }
+    return (new MarkdownTable(contents, { align: aligns }));
+  }
+
+}

+ 40 - 52
yarn.lock

@@ -844,27 +844,25 @@
     "@azure/abort-controller" "^1.0.0"
     tslib "^2.2.0"
 
-"@azure/identity@^3.3.2":
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-3.3.2.tgz#052c33f1e5f952fd4701fb5cffc4da82994a5f28"
-  integrity sha512-aDLwgMXpNBEXOlfCP9r5Rn+inmbnTbadlOnrKI2dPS9Lpf4gHvpYBV+DEZKttakfJ+qn4iWWb7zONQSO3A4XSA==
+"@azure/identity@^4.0.1":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.0.1.tgz#16a885d384fd06447a21da92c08960df492fe91e"
+  integrity sha512-yRdgF03SFLqUMZZ1gKWt0cs0fvrDIkq2bJ6Oidqcoo5uM85YMBnXWMzYKK30XqIT76lkFyAaoAAy5knXhrG4Lw==
   dependencies:
     "@azure/abort-controller" "^1.0.0"
     "@azure/core-auth" "^1.5.0"
     "@azure/core-client" "^1.4.0"
     "@azure/core-rest-pipeline" "^1.1.0"
     "@azure/core-tracing" "^1.0.0"
-    "@azure/core-util" "^1.0.0"
+    "@azure/core-util" "^1.3.0"
     "@azure/logger" "^1.0.0"
-    "@azure/msal-browser" "^2.37.1"
-    "@azure/msal-common" "^13.1.0"
-    "@azure/msal-node" "^1.17.3"
+    "@azure/msal-browser" "^3.5.0"
+    "@azure/msal-node" "^2.5.1"
     events "^3.0.0"
     jws "^4.0.0"
     open "^8.0.0"
     stoppable "^1.1.0"
     tslib "^2.2.0"
-    uuid "^8.3.0"
 
 "@azure/logger@^1.0.0":
   version "1.0.4"
@@ -873,24 +871,24 @@
   dependencies:
     tslib "^2.2.0"
 
-"@azure/msal-browser@^2.37.1":
-  version "2.38.3"
-  resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-2.38.3.tgz#2f131fa9b7a8a9546fc8d34e5d99ce4c18b04147"
-  integrity sha512-2WuLFnWWPR1IdvhhysT18cBbkXx1z0YIchVss5AwVA95g7CU5CpT3d+5BcgVGNXDXbUU7/5p0xYHV99V5z8C/A==
+"@azure/msal-browser@^3.5.0":
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.10.0.tgz#8925659e8d1a4bd21e389cca4683eb52658c778e"
+  integrity sha512-mnmi8dCXVNZI+AGRq0jKQ3YiodlIC4W9npr6FCB9WN6NQT+6rq+cIlxgUb//BjLyzKsnYo+i4LROGeMyU+6v1A==
   dependencies:
-    "@azure/msal-common" "13.3.1"
+    "@azure/msal-common" "14.7.1"
 
-"@azure/msal-common@13.3.1", "@azure/msal-common@^13.1.0":
-  version "13.3.1"
-  resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-13.3.1.tgz#012465bf940d12375dc47387b754ccf9d6b92180"
-  integrity sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==
+"@azure/msal-common@14.7.1":
+  version "14.7.1"
+  resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.7.1.tgz#b13443fbacc87ce2019a91e81a6582ea73847c75"
+  integrity sha512-v96btzjM7KrAu4NSEdOkhQSTGOuNUIIsUdB8wlyB9cdgl5KqEKnTonHUZ8+khvZ6Ap542FCErbnTyDWl8lZ2rA==
 
-"@azure/msal-node@^1.17.3":
-  version "1.18.4"
-  resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-1.18.4.tgz#c921b0447c92fb3b0cb1ebf5a9a76fcad2ec7c21"
-  integrity sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==
+"@azure/msal-node@^2.5.1":
+  version "2.6.4"
+  resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.6.4.tgz#457bd86a52461178ab2d1ba3d9d6705d95b2186e"
+  integrity sha512-nNvEPx009/80UATCToF+29NZYocn01uKrB91xtFr7bSqkqO1PuQGXRyYwryWRztUrYZ1YsSbw9A+LmwOhpVvcg==
   dependencies:
-    "@azure/msal-common" "13.3.1"
+    "@azure/msal-common" "14.7.1"
     jsonwebtoken "^9.0.0"
     uuid "^8.3.0"
 
@@ -1844,6 +1842,7 @@
 "@growi/editor@link:packages/editor":
   version "6.2.0-RC.0"
   dependencies:
+    markdown-table "^3.0.3"
     react "^18.2.0"
     react-dom "^18.2.0"
 
@@ -2865,14 +2864,7 @@
   resolved "https://registry.yarnpkg.com/@replit/codemirror-vscode-keymap/-/codemirror-vscode-keymap-6.0.2.tgz#cc9b9092db5afb9800fda5a03801b4f6600b427e"
   integrity sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==
 
-"@restart/hooks@^0.4.0":
-  version "0.4.5"
-  resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.5.tgz#e7acbea237bfc9e479970500cf87538b41a1ed02"
-  integrity sha512-tLGtY0aHeIfT7aPwUkvQuhIy3+q3w4iqmUzFLPlOAf/vNUacLaBt1j/S//jv/dQhenRh8jvswyMojCwmLvJw8A==
-  dependencies:
-    dequal "^2.0.2"
-
-"@restart/hooks@^0.4.7":
+"@restart/hooks@^0.4.0", "@restart/hooks@^0.4.7":
   version "0.4.16"
   resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.16.tgz#95ae8ac1cc7e2bd4fed5e39800ff85604c6d59fb"
   integrity sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==
@@ -6760,10 +6752,10 @@ csurf@^1.11.0:
     csrf "3.1.0"
     http-errors "~1.7.3"
 
-csv-to-markdown-table@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/csv-to-markdown-table/-/csv-to-markdown-table-1.1.0.tgz#1c4546b4a6d7265d7715df51825c1852a7286247"
-  integrity sha512-gsnCustJ+9ckvdsivA8pRkBSUbr7vaMK5uuXU+gn5df93hUe2EqGPTazAJFGjc3vy0R9hjKHoLRjphTFy04bPg==
+csv-to-markdown-table@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/csv-to-markdown-table/-/csv-to-markdown-table-1.4.1.tgz#7167eb16cf76da45abd54e13993e99f029c05754"
+  integrity sha512-jhLkfM7LXGQCuhxCwIw0QmpHCbMXy8ouC+T8KKoKaZ43DQAezpHCxNl74j2S9Sb4SEnVgMK8/RqJfNUk6xMHRQ==
 
 cubic2quad@^1.2.1:
   version "1.2.1"
@@ -11522,10 +11514,10 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
-lib0@^0.2.31, lib0@^0.2.42, lib0@^0.2.52, lib0@^0.2.74, lib0@^0.2.82:
-  version "0.2.85"
-  resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.85.tgz#2ccc3b6e02bd6165a4b8e68f89db5f9e7787dfc5"
-  integrity sha512-vtAhVttLXCu3ps2OIsTz8CdKYKdcMo7ds1MNBIcSXz6vrY8sxASqpTi4vmsAIn7xjWvyT7haKcWW6woP6jebjQ==
+lib0@^0.2.31, lib0@^0.2.42, lib0@^0.2.52, lib0@^0.2.82, lib0@^0.2.86:
+  version "0.2.89"
+  resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.89.tgz#f695ba69be34e28f73b3eeb5da92006f3897a470"
+  integrity sha512-5j19vcCjsQhvLG6mcDD+nprtJUCbmqLz5Hzt5xgi9SV6RIW/Dty7ZkVZHGBuPOADMKjQuKDvuQTH495wsmw8DQ==
   dependencies:
     isomorphic.js "^0.2.4"
 
@@ -11964,14 +11956,10 @@ markdown-it@^13.0.1:
     mdurl "^1.0.1"
     uc.micro "^1.0.5"
 
-markdown-table@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.1.tgz#4b3dd3a133d1518b8ef0dbc709bf2a1b4824bc8c"
-
-markdown-table@^3.0.0:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c"
-  integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==
+markdown-table@^3.0.0, markdown-table@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd"
+  integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==
 
 material-icons@^1.11.3:
   version "1.13.12"
@@ -18655,12 +18643,12 @@ yauzl@^2.10.0:
     buffer-crc32 "~0.2.3"
     fd-slicer "~1.1.0"
 
-yjs@^13.6.7:
-  version "13.6.7"
-  resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.7.tgz#f1176c37f65eb566cf390bd813e2099d598795f4"
-  integrity sha512-mCZTh4kjvUS2DnaktsYN6wLH3WZCJBLqrTdkWh1bIDpA/sB/GNFaLA/dyVJj2Hc7KwONuuoC/vWe9bwBBosZLQ==
+yjs@^13.6.12:
+  version "13.6.12"
+  resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.6.12.tgz#dc8be640270f04c4bb92c1984fdabbc13fc9c49f"
+  integrity sha512-KOT8ILoyVH2f/PxPadeu5kVVS055D1r3x1iFfJVJzFdnN98pVGM8H07NcKsO+fG3F7/0tf30Vnokf5YIqhU/iw==
   dependencies:
-    lib0 "^0.2.74"
+    lib0 "^0.2.86"
 
 yn@3.1.1:
   version "3.1.1"