Explorar el Código

Merge pull request #7416 from weseek/feat/rich-attachment-outline

feat: Create rich attachment remarkplugin
Ryoji Shimizu hace 3 años
padre
commit
a3befff55c

+ 8 - 2
packages/app/src/client/services/side-effects/page-updated.ts

@@ -1,6 +1,7 @@
 import { useCallback, useEffect } from 'react';
 import { useCallback, useEffect } from 'react';
 
 
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
+import { useCurrentPageId } from '~/stores/context';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useGlobalSocket } from '~/stores/websocket';
 import { useGlobalSocket } from '~/stores/websocket';
 
 
@@ -9,6 +10,7 @@ export const usePageUpdatedEffect = (): void => {
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
 
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
+  const { data: currentPageId } = useCurrentPageId();
 
 
   const setLatestRemotePageData = useCallback((data) => {
   const setLatestRemotePageData = useCallback((data) => {
     const { s2cMessagePageUpdated } = data;
     const { s2cMessagePageUpdated } = data;
@@ -21,8 +23,12 @@ export const usePageUpdatedEffect = (): void => {
       revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
       revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
       hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
       hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
     };
     };
-    setRemoteLatestPageData(remoteData);
-  }, [setRemoteLatestPageData]);
+
+    if (currentPageId != null && currentPageId === s2cMessagePageUpdated.pageId) {
+      setRemoteLatestPageData(remoteData);
+    }
+
+  }, [currentPageId, setRemoteLatestPageData]);
 
 
   // listen socket for someone updating this page
   // listen socket for someone updating this page
   useEffect(() => {
   useEffect(() => {

+ 24 - 0
packages/app/src/components/ReactMarkdownComponents/Attachment.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+
+type AttachmentProps = {
+  className?: string,
+  url: string,
+  attachmentName: string,
+  attachmentId: string,
+}
+
+export const Attachment = React.memo((props: AttachmentProps): JSX.Element => {
+  const { className, url, attachmentName } = props;
+
+  return (
+    <div className="card">
+      <h3 className="card-title m-0">Remark Attachment Component</h3>
+      <div className="card-body">
+        <a className={className} href={url}>
+          {attachmentName}
+        </a>
+      </div>
+    </div>
+  );
+});
+Attachment.displayName = 'Attachment';

+ 1 - 1
packages/app/src/components/ReactMarkdownComponents/Table.tsx

@@ -10,7 +10,7 @@ export const Table = React.memo((props: TableProps): JSX.Element => {
   const { children, className } = props;
   const { children, className } = props;
 
 
   return (
   return (
-    <table className={`${className}`}>
+    <table className={className}>
       {children}
       {children}
     </table>
     </table>
   );
   );

+ 1 - 1
packages/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -43,7 +43,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
           <i className="icon-note"></i>
           <i className="icon-note"></i>
         </button>
         </button>
       )}
       )}
-      <table className={`${className}`}>
+      <table className={className}>
         {children}
         {children}
       </table>
       </table>
     </div>
     </div>

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

@@ -234,8 +234,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const revisionBody = pageWithMeta?.data.revision?.body;
   const revisionBody = pageWithMeta?.data.revision?.body;
 
 
   useCurrentPageId(pageId ?? null);
   useCurrentPageId(pageId ?? null);
-  useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
-  useRemoteRevisionId(pageWithMeta?.data.revision?._id);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
@@ -247,6 +245,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
 
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
+  const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
+
   useSetupGlobalSocket();
   useSetupGlobalSocket();
   useSetupGlobalSocketForPage(pageId);
   useSetupGlobalSocketForPage(pageId);
 
 
@@ -279,6 +280,11 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     }
     }
   }, [mutateEditingMarkdown, revisionBody, props.currentPathname]);
   }, [mutateEditingMarkdown, revisionBody, props.currentPathname]);
 
 
+  useEffect(() => {
+    mutateRemoteRevisionId(pageWithMeta?.data.revision?._id);
+    mutateRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
+  }, [mutateRemoteRevisionId, mutateRevisionIdHackmdSynced, pageWithMeta?.data.revision?._id, pageWithMeta?.data.revisionHackmdSynced]);
+
   const title = generateCustomTitleForPage(props, pagePath);
   const title = generateCustomTitleForPage(props, pagePath);
 
 
   return (
   return (

+ 47 - 0
packages/app/src/services/renderer/remark-plugins/attachment.ts

@@ -0,0 +1,47 @@
+import { Schema as SanitizeOption } from 'hast-util-sanitize';
+import { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+const SUPPORTED_ATTRIBUTES = ['url', 'attachmentName', 'attachmentId'];
+
+const isAttachmentLink = (url: string) => {
+  // https://regex101.com/r/9qZhiK/1
+  const attachmentUrlFormat = new RegExp(/^\/(attachment)\/([^/^\n]+)$/);
+  return url.match(attachmentUrlFormat);
+};
+
+export const remarkPlugin: Plugin = () => {
+  return (tree) => {
+    // TODO: do not use any for node.children[0].value
+    visit(tree, (node: any) => {
+      if (node.type === 'link') {
+        if (isAttachmentLink(node.url)) {
+          const pathName = node.url.split('/');
+          const data = node.data ?? (node.data = {});
+          data.hName = 'attachment';
+          data.hProperties = {
+            url: node.url,
+            attachmentName: node.children[0].value,
+            attachmentId: pathName[2],
+          };
+
+          // omit position to fix the key regardless of its position
+          // see:
+          //   https://github.com/remarkjs/react-markdown/issues/703
+          //   https://github.com/remarkjs/react-markdown/issues/466
+          //
+          //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L201-L204
+          //   https://github.com/remarkjs/react-markdown/blob/a80dfdee2703d84ac2120d28b0e4998a5b417c85/lib/ast-to-react.js#L217-L222
+          delete node.position;
+        }
+      }
+    });
+  };
+};
+
+export const sanitizeOption: SanitizeOption = {
+  tagNames: ['attachment'],
+  attributes: {
+    attachment: SUPPORTED_ATTRIBUTES,
+  },
+};

+ 14 - 0
packages/app/src/services/renderer/renderer.tsx

@@ -23,6 +23,7 @@ import deepmerge from 'ts-deepmerge';
 import type { PluggableList, Pluggable, PluginTuple } from 'unified';
 import type { PluggableList, Pluggable, PluginTuple } from 'unified';
 
 
 
 
+import { Attachment } from '~/components/ReactMarkdownComponents/Attachment';
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
 import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
@@ -40,6 +41,7 @@ import * as keywordHighlighter from './rehype-plugins/keyword-highlighter';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import * as toc from './rehype-plugins/relocate-toc';
 import * as toc from './rehype-plugins/relocate-toc';
+import * as attachmentPlugin from './remark-plugins/attachment';
 import * as plantuml from './remark-plugins/plantuml';
 import * as plantuml from './remark-plugins/plantuml';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import * as table from './remark-plugins/table';
 import * as table from './remark-plugins/table';
@@ -166,6 +168,7 @@ export const generateViewOptions = (
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
+    attachmentPlugin.remarkPlugin,
   );
   );
   if (config.isEnabledLinebreaks) {
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
     remarkPlugins.push(breaks);
@@ -180,6 +183,7 @@ export const generateViewOptions = (
       commonSanitizeOption,
       commonSanitizeOption,
       drawioPlugin.sanitizeOption,
       drawioPlugin.sanitizeOption,
       lsxGrowiPlugin.sanitizeOption,
       lsxGrowiPlugin.sanitizeOption,
+      attachmentPlugin.sanitizeOption,
     )]
     )]
     : () => {};
     : () => {};
 
 
@@ -200,6 +204,7 @@ export const generateViewOptions = (
     components.lsx = Lsx;
     components.lsx = Lsx;
     components.drawio = DrawioViewerWithEditButton;
     components.drawio = DrawioViewerWithEditButton;
     components.table = TableWithEditButton;
     components.table = TableWithEditButton;
+    components.attachment = Attachment;
   }
   }
 
 
   if (config.isEnabledXssPrevention) {
   if (config.isEnabledXssPrevention) {
@@ -259,6 +264,7 @@ export const generateSimpleViewOptions = (
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     table.remarkPlugin,
     table.remarkPlugin,
+    attachmentPlugin.remarkPlugin,
   );
   );
 
 
   const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
   const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
@@ -277,6 +283,7 @@ export const generateSimpleViewOptions = (
       commonSanitizeOption,
       commonSanitizeOption,
       drawioPlugin.sanitizeOption,
       drawioPlugin.sanitizeOption,
       lsxGrowiPlugin.sanitizeOption,
       lsxGrowiPlugin.sanitizeOption,
+      attachmentPlugin.sanitizeOption,
     )]
     )]
     : () => {};
     : () => {};
 
 
@@ -293,6 +300,7 @@ export const generateSimpleViewOptions = (
     components.lsx = LsxImmutable;
     components.lsx = LsxImmutable;
     components.drawio = drawioPlugin.DrawioViewer;
     components.drawio = drawioPlugin.DrawioViewer;
     components.table = Table;
     components.table = Table;
+    components.attachment = Attachment;
   }
   }
 
 
   if (config.isEnabledXssPrevention) {
   if (config.isEnabledXssPrevention) {
@@ -328,6 +336,7 @@ export const generateSSRViewOptions = (
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     table.remarkPlugin,
     table.remarkPlugin,
+    attachmentPlugin.remarkPlugin,
   );
   );
 
 
   const isEnabledLinebreaks = config.isEnabledLinebreaks;
   const isEnabledLinebreaks = config.isEnabledLinebreaks;
@@ -344,6 +353,7 @@ export const generateSSRViewOptions = (
     ? [sanitize, deepmerge(
     ? [sanitize, deepmerge(
       commonSanitizeOption,
       commonSanitizeOption,
       lsxGrowiPlugin.sanitizeOption,
       lsxGrowiPlugin.sanitizeOption,
+      attachmentPlugin.sanitizeOption,
     )]
     )]
     : () => {};
     : () => {};
 
 
@@ -359,6 +369,7 @@ export const generateSSRViewOptions = (
   if (components != null) {
   if (components != null) {
     components.lsx = LsxImmutable;
     components.lsx = LsxImmutable;
     components.table = Table;
     components.table = Table;
+    components.attachment = Attachment;
   }
   }
 
 
   if (config.isEnabledXssPrevention) {
   if (config.isEnabledXssPrevention) {
@@ -380,6 +391,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     table.remarkPlugin,
     table.remarkPlugin,
+    attachmentPlugin.remarkPlugin,
   );
   );
   if (config.isEnabledLinebreaks) {
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
     remarkPlugins.push(breaks);
@@ -395,6 +407,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
       lsxGrowiPlugin.sanitizeOption,
       lsxGrowiPlugin.sanitizeOption,
       drawioPlugin.sanitizeOption,
       drawioPlugin.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
+      attachmentPlugin.sanitizeOption,
     )]
     )]
     : () => {};
     : () => {};
 
 
@@ -411,6 +424,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.lsx = LsxImmutable;
     components.lsx = LsxImmutable;
     components.drawio = drawioPlugin.DrawioViewer;
     components.drawio = drawioPlugin.DrawioViewer;
     components.table = Table;
     components.table = Table;
+    components.attachment = Attachment;
   }
   }
 
 
   if (config.isEnabledXssPrevention) {
   if (config.isEnabledXssPrevention) {