瀏覽代碼

Merge pull request #7817 from weseek/imprv/115672-115674-presentation-preview

imprv: presentation style preview
Yuki Takei 2 年之前
父節點
當前提交
1e273a78cb

+ 2 - 0
apps/app/next.config.js

@@ -26,6 +26,8 @@ const getTranspilePackages = () => {
     'character-entities-legacy',
     'comma-separated-tokens',
     'decode-named-character-reference',
+    'devlop',
+    'fault',
     'escape-string-regexp',
     'hastscript',
     'html-void-elements',

+ 1 - 1
apps/app/package.json

@@ -184,12 +184,12 @@
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^3.0.2",
     "remark-emoji": "^3.0.2",
+    "remark-frontmatter": "^4.0.1",
     "remark-gfm": "^3.0.1",
     "remark-math": "^5.1.1",
     "remark-toc": "^8.0.1",
     "remark-wiki-link": "^1.0.4",
     "sanitize-filename": "^1.6.3",
-    "remark-frontmatter": "^4.0.1",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",

+ 9 - 0
apps/app/src/client/services/renderer/renderer.tsx

@@ -1,6 +1,7 @@
 import assert from 'assert';
 
 import { isClient } from '@growi/core/dist/utils/browser-utils';
+import * as slides from '@growi/presentation';
 import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client/index.mjs';
 import * as drawio from '@growi/remark-drawio';
 // eslint-disable-next-line import/extensions
@@ -18,6 +19,8 @@ import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
+// eslint-disable-next-line import/no-cycle
+import { SlideViewer } from '~/components/ReactMarkdownComponents/SlideViewer';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
@@ -64,6 +67,7 @@ export const generateViewOptions = (
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
+    slides.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
   );
@@ -81,6 +85,7 @@ export const generateViewOptions = (
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
       attachment.sanitizeOption,
+      slides.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
     )]
@@ -115,6 +120,7 @@ export const generateViewOptions = (
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
+    components.slide = SlideViewer;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -257,6 +263,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     attachment.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
+    slides.remarkPlugin,
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
@@ -275,6 +282,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
+      slides.sanitizeOption,
     )]
     : () => {};
 
@@ -299,6 +307,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
     components.img = LightBox;
+    components.slide = SlideViewer;
   }
 
   if (config.isEnabledXssPrevention) {

+ 41 - 0
apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx

@@ -0,0 +1,41 @@
+import React from 'react';
+
+import { MARP_CONTAINER_CLASS_NAME } from '@growi/presentation';
+import dynamic from 'next/dynamic';
+import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+
+// TODO: Fix Dependency cycle
+// https://redmine.weseek.co.jp/issues/126744
+// eslint-disable-next-line import/no-cycle
+import { usePresentationViewOptions } from '~/stores/renderer';
+
+
+const Slides = dynamic(() => import('@growi/presentation').then(mod => mod.Slides), { ssr: false });
+
+type SlideViewerProps = {
+  marp: string,
+  children: string,
+}
+
+export const SlideViewer: React.FC<SlideViewerProps> = React.memo((props: SlideViewerProps) => {
+  const {
+    marp, children,
+  } = props;
+
+  const { data: rendererOptions } = usePresentationViewOptions();
+
+  return (
+    <div className={`${MARP_CONTAINER_CLASS_NAME}`}>
+      <div className="slides">
+        <Slides
+          hasMarpFlag={marp === 'marp'}
+          options={{ rendererOptions: rendererOptions as ReactMarkdownOptions }}
+        >
+          {children}
+        </Slides>
+      </div>
+    </div>
+  );
+});
+
+SlideViewer.displayName = 'SlideViewer';

+ 1 - 0
apps/app/src/stores/renderer.tsx

@@ -1,3 +1,4 @@
+/* eslint-disable import/no-cycle */
 import { useCallback } from 'react';
 
 import type { HtmlElementNode } from 'rehype-toc';

+ 4 - 1
packages/presentation/package.json

@@ -30,7 +30,10 @@
     "@marp-team/marp-core": "^3.6.0",
     "@types/reveal.js": "^4.4.1",
     "eslint-plugin-regex": "^1.8.0",
-    "reveal.js": "^4.4.0"
+    "reveal.js": "^4.4.0",
+    "mdast-util-frontmatter": "^1.0.0",
+    "mdast-util-gfm": "^2.0.1",
+    "mdast-util-to-markdown": "^1.3.0"
   },
   "peerDependencies": {
     "next": "^13",

+ 26 - 3
packages/presentation/src/components/Slides.tsx

@@ -24,15 +24,39 @@ const marp = new Marp({
   math: false,
 });
 
+// TODO: to change better slide style
+// https://redmine.weseek.co.jp/issues/125680
+const marpSlideTheme = marp.themeSet.add(`
+    /*!
+     * @theme slide_preview
+     */
+    section {
+      max-width: 90%;
+    }
+`);
+marp.themeSet.default = marpSlideTheme;
+
 
 type Props = {
   options: PresentationOptions,
   children?: string,
+  hasMarpFlag?: boolean,
 }
 
 export const Slides = (props: Props): JSX.Element => {
-  const { options, children } = props;
-  const { rendererOptions, isDarkMode, disableSeparationByHeader } = options;
+  const { options, children, hasMarpFlag } = props;
+  const {
+    rendererOptions, isDarkMode, disableSeparationByHeader,
+  } = options;
+
+
+  // TODO: can Marp rendering
+  // https://redmine.weseek.co.jp/issues/115673
+  if (hasMarpFlag) {
+    return (
+      <></>
+    );
+  }
 
   rendererOptions.remarkPlugins?.push([
     extractSections.remarkPlugin,
@@ -43,7 +67,6 @@ export const Slides = (props: Props): JSX.Element => {
   ]);
 
   const { css } = marp.render('', { htmlAsArray: true });
-
   return (
     <>
       <Head>

+ 1 - 0
packages/presentation/src/index.ts

@@ -1,2 +1,3 @@
 export * from './components/Presentation';
 export * from './components/Slides';
+export * from './services/renderer/slides';

+ 70 - 0
packages/presentation/src/services/renderer/slides.ts

@@ -0,0 +1,70 @@
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { Root } from 'mdast';
+import { frontmatterToMarkdown } from 'mdast-util-frontmatter';
+import { gfmToMarkdown } from 'mdast-util-gfm';
+import { toMarkdown } from 'mdast-util-to-markdown';
+import type { Plugin } from 'unified';
+import type { Node } from 'unist';
+import { visit } from 'unist-util-visit';
+
+const SUPPORTED_ATTRIBUTES = ['children', 'marp'];
+
+const rewriteNode = (tree: Node, node: Node) => {
+  let slide = false;
+  let marp = false;
+
+  const lines = (node.value as string).split('\n');
+
+  lines.forEach((line) => {
+    const [key, value] = line.split(':').map(part => part.trim());
+
+    if (key === 'slide' && value === 'true') {
+      slide = true;
+    }
+    else if (key === 'marp' && value === 'true') {
+      marp = true;
+    }
+  });
+
+  if (marp || slide) {
+
+    const newNode: Node = {
+      type: 'root',
+      data: {},
+      position: tree.position,
+      children: tree.children,
+    };
+
+    const data = newNode.data ?? (newNode.data = {});
+    tree.children = [newNode];
+    data.hName = 'slide';
+    data.hProperties = {
+      marp: marp ? 'marp' : '',
+      children: toMarkdown(tree as Root, {
+        extensions: [
+          frontmatterToMarkdown(['yaml']),
+          gfmToMarkdown(),
+          // TODO: add new extension remark-growi-directive to markdown
+          // https://redmine.weseek.co.jp/issues/126744
+        ],
+      }),
+    };
+  }
+};
+
+export const remarkPlugin: Plugin = function() {
+  return (tree) => {
+    visit(tree, (node) => {
+      if (node.type === 'yaml' && node.value != null) {
+        rewriteNode(tree, node);
+      }
+    });
+  };
+};
+
+export const sanitizeOption: SanitizeOption = {
+  tagNames: ['slide'],
+  attributes: {
+    slide: SUPPORTED_ATTRIBUTES,
+  },
+};

+ 13 - 0
yarn.lock

@@ -11420,6 +11420,19 @@ mdast-util-gfm@^2.0.0:
     mdast-util-gfm-task-list-item "^1.0.0"
     mdast-util-to-markdown "^1.0.0"
 
+mdast-util-gfm@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6"
+  integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==
+  dependencies:
+    mdast-util-from-markdown "^1.0.0"
+    mdast-util-gfm-autolink-literal "^1.0.0"
+    mdast-util-gfm-footnote "^1.0.0"
+    mdast-util-gfm-strikethrough "^1.0.0"
+    mdast-util-gfm-table "^1.0.0"
+    mdast-util-gfm-task-list-item "^1.0.0"
+    mdast-util-to-markdown "^1.0.0"
+
 mdast-util-math@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-math/-/mdast-util-math-2.0.1.tgz#141b8e7e43731d2a7423c5eb8c0335c05d257ad2"