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

Merge branch 'master' into imprv/112602-do-not-use-api-to-get-tag-on-shared-page

Shun Miyazawa 3 лет назад
Родитель
Сommit
211e7bd63c
41 измененных файлов с 145 добавлено и 300 удалено
  1. 5 0
      packages/app/public/static/locales/en_US/translation.json
  2. 5 0
      packages/app/public/static/locales/ja_JP/translation.json
  3. 5 0
      packages/app/public/static/locales/zh_CN/translation.json
  4. 1 11
      packages/app/resource/locales/en_US/sandbox.md
  5. 0 10
      packages/app/resource/locales/ja_JP/sandbox.md
  6. 1 11
      packages/app/resource/locales/zh_CN/sandbox.md
  7. 10 3
      packages/app/src/components/PageEditor.tsx
  8. 12 3
      packages/app/src/components/PageEditorByHackmd.tsx
  9. 1 1
      packages/app/src/components/PrivateLegacyPages.tsx
  10. 1 1
      packages/app/src/components/SearchPage.tsx
  11. 0 0
      packages/app/src/components/SearchPage/SearchPageBase.module.scss
  12. 3 2
      packages/app/src/components/SearchPage/SearchPageBase.tsx
  13. 2 2
      packages/app/src/pages/[[...path]].page.tsx
  14. 4 1
      packages/app/src/pages/_search.page.tsx
  15. 1 7
      packages/app/src/pages/admin/[...path].page.tsx
  16. 2 1
      packages/app/src/pages/admin/index.page.tsx
  17. 3 1
      packages/app/src/pages/installer.page.tsx
  18. 3 1
      packages/app/src/pages/invited.page.tsx
  19. 3 2
      packages/app/src/pages/login/index.page.tsx
  20. 1 1
      packages/app/src/pages/me/[[...path]].page.tsx
  21. 3 3
      packages/app/src/pages/share/[[...path]].page.tsx
  22. 1 1
      packages/app/src/pages/tags.page.tsx
  23. 5 2
      packages/app/src/pages/trash.page.tsx
  24. 3 1
      packages/app/src/pages/user-activation.page.tsx
  25. 0 2
      packages/app/src/pages/utils/commons.ts
  26. 2 63
      packages/remark-growi-directive/src/mdast-util-growi-directive/index.js
  27. 0 2
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-leaf.js
  28. 0 2
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-text.js
  29. 0 79
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js
  30. 1 25
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/html.js
  31. 1 1
      packages/remark-growi-directive/test/fixtures/leaf/input.md
  32. 1 1
      packages/remark-growi-directive/test/fixtures/leaf/output.md
  33. 5 5
      packages/remark-growi-directive/test/fixtures/leaf/tree.json
  34. 1 1
      packages/remark-growi-directive/test/fixtures/text/input.md
  35. 1 1
      packages/remark-growi-directive/test/fixtures/text/output.md
  36. 7 7
      packages/remark-growi-directive/test/fixtures/text/tree.json
  37. 18 8
      packages/remark-growi-directive/test/mdast-util-growi-directive.test.js
  38. 24 30
      packages/remark-growi-directive/test/micromark-extension-growi-directive.test.js
  39. 4 4
      packages/remark-lsx/src/components/Lsx.tsx
  40. 4 3
      packages/remark-lsx/src/server/routes/lsx.js
  41. 1 1
      packages/remark-lsx/src/services/renderer/lsx.ts

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

@@ -85,6 +85,7 @@
   "No diff": "No diff",
   "User ID": "User ID",
   "User Information": "User information",
+  "User Activation": "User Activation",
   "Basic Info": "Basic info",
   "Name": "Name",
   "Email": "Email",
@@ -164,6 +165,7 @@
     "no_page_list": "There are no pages under this page."
   },
   "installer": {
+    "title": "Installer",
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
@@ -551,6 +553,7 @@
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
   },
   "search_result": {
+    "title": "Search",
     "result_meta": "Search results for:",
     "deletion_mode_btn_lavel": "Select and delete page",
     "cancel": "Cancel",
@@ -606,6 +609,7 @@
     }
   },
   "login": {
+    "title": "Login",
     "sign_in_error": "Login error",
     "registration_successful": "registration_successful. Please wait for administrator approval.",
     "Setup": "Setup",
@@ -613,6 +617,7 @@
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
   },
   "invited": {
+    "title": "Invited",
     "discription_heading": "Create Account",
     "discription": "Create an your account with the invited email address"
   },

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

@@ -82,6 +82,7 @@
   "User ID": "ユーザーID",
   "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
+  "User Activation": "ユーザーアクティベーション",
   "Basic Info": "ユーザーの基本情報",
   "Name": "名前",
   "Email": "メールアドレス",
@@ -166,6 +167,7 @@
     "no_page_list": "このページの配下にはページが存在しません。"
   },
   "installer": {
+    "title": "インストーラー",
     "setup": "セットアップ",
     "create_initial_account": "最初のアカウントの作成",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
@@ -551,6 +553,7 @@
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
   },
   "search_result": {
+    "title": "検索",
     "result_meta": "検索結果:",
     "deletion_mode_btn_lavel": "ページを指定して削除",
     "cancel": "キャンセル",
@@ -606,6 +609,7 @@
     }
   },
   "login": {
+    "title": "ログイン",
     "sign_in_error": "ログインエラー",
     "registration_successful": "登録が完了しました。管理者の承認をお待ちください。",
     "Setup": "セットアップ",
@@ -613,6 +617,7 @@
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
   },
   "invited": {
+    "title": "招待",
     "discription_heading": "アカウント作成",
     "discription": "招待を受け取ったメールアドレスでアカウントを作成します"
   },

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

@@ -86,6 +86,7 @@
 	"My Drafts": "My Drafts",
 	"User Settings": "用户设置",
 	"User Information": "用户信息",
+  "User Activation": "用户激活",
 	"Basic Info": "基础信息",
 	"Name": "姓名",
 	"Email": "邮箱",
@@ -171,6 +172,7 @@
     "no_page_list": "There are no pages under this page."
   },
 	"installer": {
+    "title": "安装",
 		"setup": "安装",
 		"create_initial_account": "创建初始用户",
 		"initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
@@ -555,6 +557,7 @@
     "link_sharing_is_disabled": "链接共享已被禁用"
   },
 	"search_result": {
+    "title": "搜索",
 		"result_meta": "搜索结果:",
 		"deletion_mode_btn_lavel": "选择并删除页面",
 		"cancel": "取消",
@@ -610,6 +613,7 @@
     }
   },
 	"login": {
+    "title": "登录",
 		"sign_in_error": "登录错误",
 		"registration_successful": "注册成功。请等待管理员批准",
 		"Setup": "安装程序",
@@ -617,6 +621,7 @@
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
 	},
   "invited": {
+    "invited": "邀请函",
     "discription_heading": "创建账户",
     "discription": "用被邀请的电子邮件地址创建一个你的账户"
   },

+ 1 - 11
packages/app/resource/locales/en_US/sandbox.md

@@ -233,16 +233,6 @@ You can create links using `[Display text](URL)`.
 
 [Google](https://www.google.co.jp/)
 
-## Crowi compatibility
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 
 This is the most flexible linker.
@@ -254,7 +244,7 @@ Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 
 [[./Bootstrap4]]  
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 
 # :memo: Lists
 

+ 0 - 10
packages/app/resource/locales/ja_JP/sandbox.md

@@ -232,16 +232,6 @@ ___
 
 [Google](https://www.google.co.jp/)
 
-## Crowi 互換
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 
 最も柔軟な Linker です。

+ 1 - 11
packages/app/resource/locales/zh_CN/sandbox.md

@@ -233,16 +233,6 @@ You can create links using `[Display text](URL)`.
 
 [Google](https://www.google.co.jp/)
 
-## Crowi compatibility
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 
 This is the most flexible linker.
@@ -250,7 +240,7 @@ Both the page description and link address can be displayed on the page.
 
 ```
 [[./Bootstrap4]]
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 
 [[./Bootstrap4]]  

+ 10 - 3
packages/app/src/components/PageEditor.tsx

@@ -2,8 +2,9 @@ import React, {
   useCallback, useEffect, useMemo, useRef, useState,
 } from 'react';
 
-
 import EventEmitter from 'events';
+import nodePath from 'path';
+
 
 import {
   IPageHasId, PageGrant, pathUtils,
@@ -31,6 +32,7 @@ import {
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { usePageTreeTermManager } from '~/stores/page-listing';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
@@ -88,6 +90,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
+  const { advance: advancePt } = usePageTreeTermManager();
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -104,7 +107,8 @@ const PageEditor = React.memo((): JSX.Element => {
 
     let initialValue = '';
     if (isEnabledAttachTitleHeader && currentPathname != null) {
-      initialValue += `${pathUtils.attachTitleHeader(currentPathname)}\n`;
+      const pageTitle = nodePath.basename(currentPathname);
+      initialValue += `${pathUtils.attachTitleHeader(pageTitle)}\n`;
     }
     if (templateBodyData != null) {
       initialValue += `${templateBodyData}\n`;
@@ -204,6 +208,9 @@ const PageEditor = React.memo((): JSX.Element => {
         options,
       );
 
+      // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
+      advancePt();
+
       return page;
     }
     catch (error) {
@@ -221,7 +228,7 @@ const PageEditor = React.memo((): JSX.Element => {
     }
 
   // eslint-disable-next-line max-len
-  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
+  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId, advancePt]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {

+ 12 - 3
packages/app/src/components/PageEditorByHackmd.tsx

@@ -25,6 +25,7 @@ import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { usePageTreeTermManager } from '~/stores/page-listing';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
   EditorMode,
@@ -63,6 +64,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: grantData } = useSelectedGrant();
   const { data: hackmdUri } = useHackmdUri();
   const saveOrUpdate = useSaveOrUpdate();
+  const { advance: advancePt } = usePageTreeTermManager();
 
   const { returnPathForURL } = pathUtils;
 
@@ -127,6 +129,9 @@ export const PageEditorByHackmd = (): JSX.Element => {
       else {
         updateStateAfterSave?.();
         mutateIsHackmdDraftUpdatingInRealtime(false);
+
+        // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
+        advancePt();
       }
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
@@ -136,7 +141,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
     }
   // eslint-disable-next-line max-len
-  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime]);
+  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime, advancePt]);
 
   // set handler to save and reload Page
   useEffect(() => {
@@ -258,6 +263,9 @@ export const PageEditorByHackmd = (): JSX.Element => {
       updateStateAfterSave?.();
       mutateTagsInfo();
 
+      // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
+      advancePt();
+
       mutateIsEnabledUnsavedWarning(false);
 
       logger.debug('success to save');
@@ -268,8 +276,9 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error.message);
     }
-  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
-      saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t]);
+  }, [
+    currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
+    saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, advancePt, mutateIsEnabledUnsavedWarning, t]);
 
   /**
    * onChange event of HackmdEditor handler

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

@@ -29,7 +29,7 @@ import PaginationWrapper from './PaginationWrapper';
 import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
-import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage2/SearchPageBase';
+import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
 
 
 // TODO: replace with "customize:showPageLimitationS"

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

@@ -16,7 +16,7 @@ import { NotAvailableForGuest } from './NotAvailableForGuest';
 import PaginationWrapper from './PaginationWrapper';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
-import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage2/SearchPageBase';
+import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
 
 
 // TODO: replace with "customize:showPageLimitationS"

+ 0 - 0
packages/app/src/components/SearchPage2/SearchPageBase.module.scss → packages/app/src/components/SearchPage/SearchPageBase.module.scss


+ 3 - 2
packages/app/src/components/SearchPage2/SearchPageBase.tsx → packages/app/src/components/SearchPage/SearchPageBase.tsx

@@ -14,7 +14,8 @@ import { usePageDeleteModal } from '~/stores/modal';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { SearchResultList } from '../SearchPage/SearchResultList';
+
+import { SearchResultList } from './SearchResultList';
 
 import styles from './SearchPageBase.module.scss';
 
@@ -41,7 +42,7 @@ type Props = {
 }
 
 const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
-  const SearchResultContent = dynamic(import('../SearchPage/SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false });
+  const SearchResultContent = dynamic(import('./SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false });
   const {
     pages,
     searchingKeyword,

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

@@ -76,7 +76,7 @@ import {
 
 import { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage,
 } from './utils/commons';
 
 
@@ -297,7 +297,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitleForPage(props, pagePath ?? '');
 
 
   const sideContents = !props.isNotFound && !props.isNotCreatable

+ 4 - 1
packages/app/src/pages/_search.page.tsx

@@ -5,6 +5,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 
+import { useTranslation } from 'next-i18next';
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -57,6 +58,8 @@ type Props = CommonProps & {
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
   const { userUISettings } = props;
 
+  const { t } = useTranslation();
+
   // commons
   useCsrfToken(props.csrfToken);
 
@@ -88,7 +91,7 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
     return <PutbackPageModal />;
   };
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('search_result.title'));
 
   return (
     <>

+ 1 - 7
packages/app/src/pages/admin/[...path].page.tsx

@@ -2,9 +2,8 @@ import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import dynamic from 'next/dynamic';
-import Head from 'next/head';
 
-import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
+import { CommonProps } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
@@ -18,13 +17,8 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
   useCurrentUser(props.currentUser ?? null);
 
-  const title = generateCustomTitle(props, 'GROWI');
-
   return (
     <AdminLayout>
-      <Head>
-        <title>{title}</title>
-      </Head>
       <AdminNotFoundPage />
     </AdminLayout>
   );

+ 2 - 1
packages/app/src/pages/admin/index.page.tsx

@@ -35,7 +35,8 @@ const AdminHomePage: NextPage<Props> = (props) => {
 
   const { t } = useTranslation('admin');
 
-  const title = t('wiki_management_home_page');
+  const title = generateCustomTitle(props, t('wiki_management_home_page'));
+
   const injectableContainers: Container<any>[] = [];
 
   if (isClient()) {

+ 3 - 1
packages/app/src/pages/installer.page.tsx

@@ -4,6 +4,7 @@ import { pagePathUtils } from '@growi/core';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Head from 'next/head';
 
@@ -33,6 +34,7 @@ type Props = CommonProps & {
 };
 
 const InstallerPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
   // commons
   useAppTitle(props.appTitle);
@@ -40,7 +42,7 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
   useConfidential(props.confidential);
   useCsrfToken(props.csrfToken);
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('installer.title'));
   const classNames: string[] = [];
 
   return (

+ 3 - 1
packages/app/src/pages/invited.page.tsx

@@ -3,6 +3,7 @@ import React from 'react';
 import type { IUserHasId, IUser } from '@growi/core';
 import { USER_STATUS } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
@@ -26,12 +27,13 @@ type Props = CommonProps & {
 }
 
 const InvitedPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
   useCsrfToken(props.csrfToken);
   useCurrentPathname(props.currentPathname);
   useCurrentUser(props.currentUser);
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('invited.title'));
   const classNames: string[] = ['invited-page'];
 
   return (

+ 3 - 2
packages/app/src/pages/login/index.page.tsx

@@ -3,6 +3,7 @@ import React from 'react';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Head from 'next/head';
 
@@ -19,7 +20,6 @@ import {
   useCurrentPathname,
 } from '~/stores/context';
 
-
 import styles from './index.module.scss';
 
 
@@ -38,6 +38,7 @@ type Props = CommonProps & {
 };
 
 const LoginPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
   // commons
   useCsrfToken(props.csrfToken);
@@ -45,7 +46,7 @@ const LoginPage: NextPage<Props> = (props: Props) => {
   // page
   useCurrentPathname(props.currentPathname);
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('login.title'));
   const classNames: string[] = ['login-page', styles['login-page']];
 
   return (

+ 1 - 1
packages/app/src/pages/me/[[...path]].page.tsx

@@ -111,7 +111,7 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
 
   useRendererConfig(props.rendererConfig);
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, targetPage.title);
 
   return (
     <>

+ 3 - 3
packages/app/src/pages/share/[[...path]].page.tsx

@@ -29,7 +29,7 @@ import loggerFactory from '~/utils/logger';
 
 import { NextPageWithLayout } from '../_app.page';
 import {
-  CommonProps, getServerSideCommonProps, generateCustomTitle, getNextI18NextConfig,
+  CommonProps, getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig,
 } from '../utils/commons';
 
 const logger = loggerFactory('growi:next-page:share');
@@ -73,7 +73,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useShareLinkId(props.shareLink?._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentUser(props.currentUser);
-  useCurrentPathname(props.currentPathname);
+  const { data: currentPathname } = useCurrentPathname(props.currentPathname);
   useRendererConfig(props.rendererConfig);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
@@ -88,7 +88,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
   const shareLink = props.shareLink;
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitleForPage(props, currentPathname ?? '');
 
 
   const sideContents = shareLink != null

+ 1 - 1
packages/app/src/pages/tags.page.tsx

@@ -80,7 +80,7 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 
   useRendererConfig(props.rendererConfig);
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('Tags'));
 
   return (
     <>

+ 5 - 2
packages/app/src/pages/trash.page.tsx

@@ -26,8 +26,9 @@ import {
 
 import { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle,
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage,
 } from './utils/commons';
+import { useTranslation } from 'next-i18next';
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const EmptyTrashModal = dynamic(() => import('~/components/EmptyTrashModal'), { ssr: false });
@@ -70,12 +71,14 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 
   useRendererConfig(props.rendererConfig);
 
+  const { t } = useTranslation();
+
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isGuestUser } = useIsGuestUser();
 
   const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitleForPage(props, '/trash');
 
   return (
     <>

+ 3 - 1
packages/app/src/pages/user-activation.page.tsx

@@ -1,4 +1,5 @@
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Head from 'next/head';
 
@@ -22,8 +23,9 @@ type Props = CommonProps & {
 }
 
 const UserActivationPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('User Activation'));
 
   return (
     <NoLoginLayout>

+ 0 - 2
packages/app/src/pages/utils/commons.ts

@@ -105,7 +105,6 @@ export const getNextI18NextConfig = async(
 export const generateCustomTitle = (props: CommonProps, title: string): string => {
   return props.customTitleTemplate
     .replace('{{sitename}}', props.appTitle)
-    .replace('{{page}}', title)
     .replace('{{pagepath}}', title)
     .replace('{{pagename}}', title);
 };
@@ -121,6 +120,5 @@ export const generateCustomTitleForPage = (props: CommonProps, pagePath: string)
   return props.customTitleTemplate
     .replace('{{sitename}}', props.appTitle)
     .replace('{{pagepath}}', pagePath)
-    .replace('{{page}}', dPagePath.latter) // for backward compatibility
     .replace('{{pagename}}', dPagePath.latter);
 };

+ 2 - 63
packages/remark-growi-directive/src/mdast-util-growi-directive/index.js

@@ -40,16 +40,12 @@ export const directiveFromMarkdown = {
   },
   exit: {
     directiveLeaf: exit,
-    directiveLeafAttributeClassValue: exitAttributeClassValue,
-    directiveLeafAttributeIdValue: exitAttributeIdValue,
     directiveLeafAttributeName: exitAttributeName,
     directiveLeafAttributeValue: exitAttributeValue,
     directiveLeafAttributes: exitAttributes,
     directiveLeafName: exitName,
 
     directiveText: exit,
-    directiveTextAttributeClassValue: exitAttributeClassValue,
-    directiveTextAttributeIdValue: exitAttributeIdValue,
     directiveTextAttributeName: exitAttributeName,
     directiveTextAttributeValue: exitAttributeValue,
     directiveTextAttributes: exitAttributes,
@@ -118,22 +114,6 @@ function enterAttributes() {
   this.buffer(); // Capture EOLs
 }
 
-/** @type {FromMarkdownHandle} */
-function exitAttributeIdValue(token) {
-  const list = /** @type {Array.<[string, string]>} */ (
-    this.getData('directiveAttributes')
-  );
-  list.push(['id', parseEntities(this.sliceSerialize(token))]);
-}
-
-/** @type {FromMarkdownHandle} */
-function exitAttributeClassValue(token) {
-  const list = /** @type {Array.<[string, string]>} */ (
-    this.getData('directiveAttributes')
-  );
-  list.push(['class', parseEntities(this.sliceSerialize(token))]);
-}
-
 /** @type {FromMarkdownHandle} */
 function exitAttributeValue(token) {
   const list = /** @type {Array.<[string, string]>} */ (
@@ -165,12 +145,7 @@ function exitAttributes() {
   while (++index < list.length) {
     const attribute = list[index];
 
-    if (attribute[0] === 'class' && cleaned.class) {
-      cleaned.class += ` ${attribute[1]}`;
-    }
-    else {
-      cleaned[attribute[0]] = attribute[1];
-    }
+    cleaned[attribute[0]] = attribute[1];
   }
 
   this.setData('directiveAttributes');
@@ -252,46 +227,10 @@ function attributes(node, context) {
     ) {
       const value = String(attrs[key]);
 
-      if (key === 'id') {
-        id = shortcut.test(value) ? `#${value}` : quoted('id', value);
-      }
-      else if (key === 'class') {
-        const list = value.split(/[\t\n\r ]+/g);
-        /** @type {Array.<string>} */
-        const classesFullList = [];
-        /** @type {Array.<string>} */
-        const classesList = [];
-        let index = -1;
-
-        while (++index < list.length) {
-          (shortcut.test(list[index]) ? classesList : classesFullList).push(
-            list[index],
-          );
-        }
-
-        classesFull = classesFullList.length > 0
-          ? quoted('class', classesFullList.join(' '))
-          : '';
-        classes = classesList.length > 0 ? `.${classesList.join('.')}` : '';
-      }
-      else {
-        values.push(quoted(key, value));
-      }
+      values.push(quoted(key, value));
     }
   }
 
-  if (classesFull) {
-    values.unshift(classesFull);
-  }
-
-  if (classes) {
-    values.unshift(classes);
-  }
-
-  if (id) {
-    values.unshift(id);
-  }
-
   return values.length > 0 ? `(${values.join(' ')})` : '';
 
   /**

+ 0 - 2
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-leaf.js

@@ -125,8 +125,6 @@ function tokenizeAttributes(effects, ok, nok) {
     'directiveLeafAttributes',
     'directiveLeafAttributesMarker',
     'directiveLeafAttribute',
-    'directiveLeafAttributeId',
-    'directiveLeafAttributeClass',
     'directiveLeafAttributeName',
     'directiveLeafAttributeInitializerMarker',
     'directiveLeafAttributeValueLiteral',

+ 0 - 2
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-text.js

@@ -96,8 +96,6 @@ function tokenizeAttributes(effects, ok, nok) {
     'directiveTextAttributes',
     'directiveTextAttributesMarker',
     'directiveTextAttribute',
-    'directiveTextAttributeId',
-    'directiveTextAttributeClass',
     'directiveTextAttributeName',
     'directiveTextAttributeInitializerMarker',
     'directiveTextAttributeValueLiteral',

+ 0 - 79
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js

@@ -24,8 +24,6 @@ import { markdownLineEndingOrSpaceOrComma, factoryAttributesDevider } from '../.
  * @param {string} attributesType
  * @param {string} attributesMarkerType
  * @param {string} attributeType
- * @param {string} attributeIdType
- * @param {string} attributeClassType
  * @param {string} attributeNameType
  * @param {string} attributeInitializerType
  * @param {string} attributeValueLiteralType
@@ -42,8 +40,6 @@ export function factoryAttributes(
     attributesType,
     attributesMarkerType,
     attributeType,
-    attributeIdType,
-    attributeClassType,
     attributeNameType,
     attributeInitializerType,
     attributeValueLiteralType,
@@ -71,16 +67,6 @@ export function factoryAttributes(
 
   /** @type {State} */
   function between(code) {
-    if (code === codes.numberSign) {
-      type = attributeIdType;
-      return shortcutStart(code);
-    }
-
-    if (code === codes.dot) {
-      type = attributeClassType;
-      return shortcutStart(code);
-    }
-
     if (disallowEol) {
       if (markdownSpace(code)) {
         return factorySpace(effects, between, types.whitespace)(code);
@@ -110,69 +96,6 @@ export function factoryAttributes(
     return end(code);
   }
 
-  /** @type {State} */
-  function shortcutStart(code) {
-    effects.enter(attributeType);
-    effects.enter(type);
-    effects.enter(`${type}Marker`);
-    effects.consume(code);
-    effects.exit(`${type}Marker`);
-    return shortcutStartAfter;
-  }
-
-  /** @type {State} */
-  function shortcutStartAfter(code) {
-    if (
-      code === codes.eof
-      || code === codes.quotationMark
-      || code === codes.numberSign
-      || code === codes.apostrophe
-      || code === codes.dot
-      || code === codes.lessThan
-      || code === codes.equalsTo
-      || code === codes.greaterThan
-      || code === codes.graveAccent
-      || code === codes.rightParenthesis
-      || code === codes.comma
-    ) {
-      return nok(code);
-    }
-
-    effects.enter(`${type}Value`);
-    effects.consume(code);
-    return shortcut;
-  }
-
-  /** @type {State} */
-  function shortcut(code) {
-    if (
-      code === codes.eof
-      || code === codes.quotationMark
-      || code === codes.apostrophe
-      || code === codes.lessThan
-      || code === codes.equalsTo
-      || code === codes.greaterThan
-      || code === codes.graveAccent
-    ) {
-      return nok(code);
-    }
-
-    if (
-      code === codes.numberSign
-      || code === codes.dot
-      || code === codes.rightParenthesis
-      || markdownLineEndingOrSpaceOrComma(code)
-    ) {
-      effects.exit(`${type}Value`);
-      effects.exit(type);
-      effects.exit(attributeType);
-      return between(code);
-    }
-
-    effects.consume(code);
-    return shortcut;
-  }
-
   /** @type {State} */
   function name(code) {
     if (
@@ -181,9 +104,7 @@ export function factoryAttributes(
         && code !== codes.lineFeed
         && code !== codes.carriageReturnLineFeed
         && code !== codes.quotationMark
-        && code !== codes.numberSign
         && code !== codes.apostrophe
-        && code !== codes.dot
         && code !== codes.lessThan
         && code !== codes.equalsTo
         && code !== codes.greaterThan

+ 1 - 25
packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/html.js

@@ -49,8 +49,6 @@ export function directiveHtml(options = {}) {
     },
     exit: {
       directiveLeaf: exit,
-      directiveLeafAttributeClassValue: exitAttributeClassValue,
-      directiveLeafAttributeIdValue: exitAttributeIdValue,
       directiveLeafAttributeName: exitAttributeName,
       directiveLeafAttributeValue: exitAttributeValue,
       directiveLeafAttributes: exitAttributes,
@@ -58,8 +56,6 @@ export function directiveHtml(options = {}) {
       directiveLeafName: exitName,
 
       directiveText: exit,
-      directiveTextAttributeClassValue: exitAttributeClassValue,
-      directiveTextAttributeIdValue: exitAttributeIdValue,
       directiveTextAttributeName: exitAttributeName,
       directiveTextAttributeValue: exitAttributeValue,
       directiveTextAttributes: exitAttributes,
@@ -105,21 +101,6 @@ export function directiveHtml(options = {}) {
     this.setData('directiveAttributes', []);
   }
 
-  /** @type {_Handle} */
-  function exitAttributeIdValue(token) {
-    /** @type {Attribute[]} */
-    const attributes = this.getData('directiveAttributes');
-    attributes.push(['id', parseEntities(this.sliceSerialize(token))]);
-  }
-
-  /** @type {_Handle} */
-  function exitAttributeClassValue(token) {
-    /** @type {Attribute[]} */
-    const attributes = this.getData('directiveAttributes');
-
-    attributes.push(['class', parseEntities(this.sliceSerialize(token))]);
-  }
-
   /** @type {_Handle} */
   function exitAttributeName(token) {
     // Attribute names in CommonMark are significantly limited, so character
@@ -154,12 +135,7 @@ export function directiveHtml(options = {}) {
     while (++index < attributes.length) {
       attribute = attributes[index];
 
-      if (attribute[0] === 'class' && cleaned.class) {
-        cleaned.class += ` ${attribute[1]}`;
-      }
-      else {
-        cleaned[attribute[0]] = attribute[1];
-      }
+      cleaned[attribute[0]] = attribute[1];
     }
 
     this.resume();

+ 1 - 1
packages/remark-growi-directive/test/fixtures/leaf/input.md

@@ -8,4 +8,4 @@ $a[b](c)
 
 $a[b *c* d **e**]
 
-$a(#b.c.d id=e class="f g" h="i &amp; j k")
+$a(#b.c.d .f.g h="i &amp; j k")

+ 1 - 1
packages/remark-growi-directive/test/fixtures/leaf/output.md

@@ -8,4 +8,4 @@ $a[b](c)
 
 $a[b *c* d **e**]
 
-$a(#e .c.d.f.g h="i & j k")
+$a(#b.c.d .f.g h="i & j k")

+ 5 - 5
packages/remark-growi-directive/test/fixtures/leaf/tree.json

@@ -232,8 +232,8 @@
       "type": "leafGrowiPluginDirective",
       "name": "a",
       "attributes": {
-        "id": "e",
-        "class": "c d f g",
+        "#b.c.d": "",
+        ".f.g": "",
         "h": "i & j k"
       },
       "children": [],
@@ -245,8 +245,8 @@
         },
         "end": {
           "line": 11,
-          "column": 44,
-          "offset": 90
+          "column": 32,
+          "offset": 78
         }
       }
     }
@@ -260,7 +260,7 @@
     "end": {
       "line": 12,
       "column": 1,
-      "offset": 91
+      "offset": 79
     }
   }
 }

+ 1 - 1
packages/remark-growi-directive/test/fixtures/text/input.md

@@ -3,5 +3,5 @@ One $a, two $a[b], three $a(b), four $a[b](c).
 $a[b *c*
 d **e**].
 
-$a(#b.c.d id=e class="f g" h="i &amp; j
+$a(#b.c.d .f.g h="i &amp; j
 k").

+ 1 - 1
packages/remark-growi-directive/test/fixtures/text/output.md

@@ -3,5 +3,5 @@ One $a, two $a[b], three $a(b), four $a[b](c).
 $a[b *c*
 d **e**].
 
-$a(#e .c.d.f.g h="i & j
+$a(#b.c.d .f.g h="i & j
 k").

+ 7 - 7
packages/remark-growi-directive/test/fixtures/text/tree.json

@@ -365,8 +365,8 @@
           "type": "textGrowiPluginDirective",
           "name": "a",
           "attributes": {
-            "id": "e",
-            "class": "c d f g",
+            "#b.c.d": "",
+            ".f.g": "",
             "h": "i & j\nk"
           },
           "children": [],
@@ -379,7 +379,7 @@
             "end": {
               "line": 7,
               "column": 4,
-              "offset": 111
+              "offset": 99
             }
           }
         },
@@ -390,12 +390,12 @@
             "start": {
               "line": 7,
               "column": 4,
-              "offset": 111
+              "offset": 99
             },
             "end": {
               "line": 7,
               "column": 5,
-              "offset": 112
+              "offset": 100
             }
           }
         }
@@ -409,7 +409,7 @@
         "end": {
           "line": 7,
           "column": 5,
-          "offset": 112
+          "offset": 100
         }
       }
     }
@@ -423,7 +423,7 @@
     "end": {
       "line": 8,
       "column": 1,
-      "offset": 113
+      "offset": 101
     }
   }
 }

+ 18 - 8
packages/remark-growi-directive/test/mdast-util-growi-directive.test.js

@@ -119,6 +119,14 @@ test('markdown -> mdast', (t) => {
     'should support content in a label',
   );
 
+  const hoge = removePosition(
+    fromMarkdown('x $a(#b.c.d e=f g="h&amp;i&unknown;j")', {
+      extensions: [directive()],
+      mdastExtensions: [directiveFromMarkdown],
+    }),
+    true,
+  );
+
   t.deepEqual(
     removePosition(
       fromMarkdown('x $a(#b.c.d e=f g="h&amp;i&unknown;j")', {
@@ -138,7 +146,7 @@ test('markdown -> mdast', (t) => {
               type: DirectiveType.Text,
               name: 'a',
               attributes: {
-                id: 'b', class: 'c d', e: 'f', g: 'h&i&unknown;j',
+                '#b.c.d': '', e: 'f', g: 'h&i&unknown;j',
               },
               children: [],
             },
@@ -307,7 +315,7 @@ test('mdast -> markdown', (t) => {
           {
             type: DirectiveType.Text,
             name: 'b',
-            attributes: { class: 'a b\nc', id: 'd', key: 'value' },
+            attributes: { '#d': '', '.a.b.c': '', key: 'value' },
             children: [],
           },
           { type: 'text', value: ' k.' },
@@ -316,7 +324,7 @@ test('mdast -> markdown', (t) => {
       { extensions: [directiveToMarkdown] },
     ),
     'a $b(#d .a.b.c key="value") k.\n',
-    'should serialize a directive (text) w/ `id`, `class` attributes',
+    'should serialize a directive (text) w/ hash, dot notation attributes',
   );
 
   t.deepEqual(
@@ -391,7 +399,7 @@ test('mdast -> markdown', (t) => {
           {
             type: DirectiveType.Text,
             name: 'b',
-            attributes: { class: 'c.d e<f' },
+            attributes: { 'c.d': '', 'e<f': '' },
             children: [],
           },
           { type: 'text', value: ' g.' },
@@ -399,7 +407,7 @@ test('mdast -> markdown', (t) => {
       },
       { extensions: [directiveToMarkdown] },
     ),
-    'a $b(class="c.d e<f") g.\n',
+    'a $b(c.d e<f) g.\n',
     'should not use the `class` shortcut if impossible characters exist',
   );
 
@@ -412,7 +420,9 @@ test('mdast -> markdown', (t) => {
           {
             type: DirectiveType.Text,
             name: 'b',
-            attributes: { class: 'c.d e f<g hij' },
+            attributes: {
+              'c.d': '', e: '', 'f<g': '', hij: '',
+            },
             children: [],
           },
           { type: 'text', value: ' k.' },
@@ -420,7 +430,7 @@ test('mdast -> markdown', (t) => {
       },
       { extensions: [directiveToMarkdown] },
     ),
-    'a $b(.e.hij class="c.d f<g") k.\n',
+    'a $b(c.d e f<g hij) k.\n',
     'should not use the `class` shortcut if impossible characters exist (but should use it for classes that don’t)',
   );
 
@@ -485,7 +495,7 @@ test('mdast -> markdown', (t) => {
       {
         type: DirectiveType.Leaf,
         name: 'a',
-        attributes: { id: 'b', class: 'c d', key: 'e\nf' },
+        attributes: { '#b': '', '.c.d': '', key: 'e\nf' },
         children: [],
       },
       { extensions: [directiveToMarkdown] },

+ 24 - 30
packages/remark-growi-directive/test/micromark-extension-growi-directive.test.js

@@ -231,39 +231,39 @@ test('micromark-extension-directive (syntax)', (t) => {
     );
 
     t.equal(
-      micromark('$a(..b)', options()),
-      '<p>(..b)</p>',
-      'should not support an empty shortcut (`.`)',
+      micromark('a $a(..b)', options()),
+      '<p>a </p>',
+      'should support attrs which starts w/ continuous dots',
     );
 
     t.equal(
-      micromark('$a(.#b)', options()),
-      '<p>(.#b)</p>',
-      'should not support an empty shortcut (`#`)',
+      micromark('a $a(.#b)', options()),
+      '<p>a </p>',
+      'should support attrs which start w/ `#`',
     );
 
     t.equal(
-      micromark('$a(.)', options()),
-      '<p>(.)</p>',
-      'should not support an empty shortcut (`}`)',
+      micromark('a $a(.)', options()),
+      '<p>a </p>',
+      'should support attrs w/ (`.`)',
     );
 
     t.equal(
-      micromark('$a(.a=b)', options()),
-      '<p>(.a=b)</p>',
-      'should not support certain characters in shortcuts (`=`)',
+      micromark('a $a(.a=b)', options()),
+      '<p>a </p>',
+      'should support with the attr `(.a=b)`',
     );
 
     t.equal(
-      micromark('$a(.a"b)', options()),
-      '<p>(.a&quot;b)</p>',
-      'should not support certain characters in shortcuts (`"`)',
+      micromark('a $a(.a"b)', options()),
+      '<p>a </p>',
+      'should support with the attr `(.a"b)`',
     );
 
     t.equal(
-      micromark('$a(.a<b)', options()),
-      '<p>(.a&lt;b)</p>',
-      'should not support certain characters in shortcuts (`<`)',
+      micromark('a $a(.a<b)', options()),
+      '<p>a </p>',
+      'should support with the attr `(.a<b)`',
     );
 
     t.equal(
@@ -1002,26 +1002,20 @@ test('content', (t) => {
 
   t.equal(
     micromark('a $span(#a#b)', options({ '*': h })),
-    '<p>a <span id="b"></span></p>',
-    'should support `id` shortcuts',
+    '<p>a <span #a#b=""></span></p>',
+    'should support attrs which contains `#` (1)',
   );
 
   t.equal(
     micromark('a $span(id=a id="b" #c#d)', options({ '*': h })),
-    '<p>a <span id="d"></span></p>',
-    'should support `id` shortcuts after `id` attributes',
+    '<p>a <span id="b" #c#d=""></span></p>',
+    'should support attrs which contains `#` (2)',
   );
 
   t.equal(
     micromark('a $span(.a.b)', options({ '*': h })),
-    '<p>a <span class="a b"></span></p>',
-    'should support `class` shortcuts',
-  );
-
-  t.equal(
-    micromark('a $span(class=a class="b c" .d.e)', options({ '*': h })),
-    '<p>a <span class="a b c d e"></span></p>',
-    'should support `class` shortcuts after `class` attributes',
+    '<p>a <span .a.b=""></span></p>',
+    'should support attrs with dot notation',
   );
 
   t.test('spec for growi plugin', (t) => {

+ 4 - 4
packages/remark-lsx/src/components/Lsx.tsx

@@ -19,23 +19,23 @@ type Props = {
   sort?: string,
   reverse?: string,
   filter?: string,
+  except?: string,
 
   isImmutable?: boolean,
 };
 
 export const Lsx = React.memo(({
   prefix,
-  num, depth, sort, reverse, filter,
+  num, depth, sort, reverse, filter, except,
   isImmutable,
-  ...props
 }: Props): JSX.Element => {
 
   const lsxContext = useMemo(() => {
     const options = {
-      num, depth, sort, reverse, filter,
+      num, depth, sort, reverse, filter, except,
     };
     return new LsxContext(prefix, options);
-  }, [depth, filter, num, prefix, reverse, sort]);
+  }, [depth, filter, num, prefix, reverse, sort, except]);
 
   const { data, error } = useSWRxNodeTree(lsxContext, isImmutable);
 

+ 4 - 3
packages/remark-lsx/src/server/routes/lsx.js

@@ -1,6 +1,6 @@
 import createError, { isHttpError } from 'http-errors';
 
-const { pagePathUtils, customTagUtils } = require('@growi/core');
+const { pathUtils, pagePathUtils, customTagUtils } = require('@growi/core');
 
 const { OptionParser } = customTagUtils;
 
@@ -8,6 +8,7 @@ const { OptionParser } = customTagUtils;
 const DEFAULT_PAGES_NUM = 50;
 
 
+const { addTrailingSlash } = pathUtils;
 const { isTopPage } = pagePathUtils;
 
 class Lsx {
@@ -105,10 +106,10 @@ class Lsx {
     let filterPath = '';
     if (optionsFilter.charAt(0) === '^') {
       // move '^' to the first of path
-      filterPath = new RegExp(`^${pagePath}${optionsFilter.slice(1, optionsFilter.length)}`);
+      filterPath = new RegExp(`^${addTrailingSlash(pagePath)}${optionsFilter.slice(1, optionsFilter.length)}`);
     }
     else {
-      filterPath = new RegExp(`^${pagePath}.*${optionsFilter}`);
+      filterPath = new RegExp(`^${addTrailingSlash(pagePath)}.*${optionsFilter}`);
     }
 
     if (isExceptFilter) {

+ 1 - 1
packages/remark-lsx/src/services/renderer/lsx.ts

@@ -8,7 +8,7 @@ import { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
-const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter'];
+const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except'];
 
 const { hasHeadingSlash } = pathUtils;