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

Merge pull request #8725 from weseek/fix/144087-144091-use-drawio-in-slide-view

fix: Drawio not available with GROWI slides
Yuki Takei 1 год назад
Родитель
Сommit
0bbe98b336
32 измененных файлов с 271 добавлено и 361 удалено
  1. 15 8
      apps/app/src/client/services/renderer/renderer.tsx
  2. 0 90
      apps/app/src/client/services/renderer/slide-viewer-renderer.tsx
  3. 1 1
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  4. 13 2
      apps/app/src/components/Page/PageView.tsx
  5. 3 1
      apps/app/src/components/Page/RevisionRenderer.tsx
  6. 26 0
      apps/app/src/components/Page/SlideRenderer.tsx
  7. 15 5
      apps/app/src/components/PageEditor/Preview.tsx
  8. 7 4
      apps/app/src/components/PagePresentationModal.tsx
  9. 1 1
      apps/app/src/components/Presentation/Presentation.tsx
  10. 1 1
      apps/app/src/components/Presentation/Slides.tsx
  11. 0 33
      apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx
  12. 10 6
      apps/app/src/components/ShareLinkPageView.tsx
  13. 0 27
      apps/app/src/stores/slide-viewer-renderer.tsx
  14. 14 2
      packages/presentation/package.json
  15. 0 0
      packages/presentation/src/client/components/GrowiSlides.tsx
  16. 0 0
      packages/presentation/src/client/components/MarpSlides.tsx
  17. 0 0
      packages/presentation/src/client/components/Presentation.global.scss
  18. 0 0
      packages/presentation/src/client/components/Presentation.module.scss
  19. 14 18
      packages/presentation/src/client/components/Presentation.tsx
  20. 0 0
      packages/presentation/src/client/components/RichSlideSection.tsx
  21. 0 0
      packages/presentation/src/client/components/Slides.module.scss
  22. 0 0
      packages/presentation/src/client/components/Slides.tsx
  23. 0 0
      packages/presentation/src/client/consts/index.ts
  24. 0 1
      packages/presentation/src/client/index.ts
  25. 0 0
      packages/presentation/src/client/services/growi-marpit.ts
  26. 0 0
      packages/presentation/src/client/services/renderer/extract-sections.ts
  27. 1 0
      packages/presentation/src/services/index.ts
  28. 0 43
      packages/presentation/src/services/parse-slide-frontmatter.ts
  29. 0 89
      packages/presentation/src/services/renderer/slides.ts
  30. 97 0
      packages/presentation/src/services/use-slides-by-frontmatter.ts
  31. 12 1
      packages/presentation/vite.config.ts
  32. 41 28
      yarn.lock

+ 15 - 8
apps/app/src/client/services/renderer/renderer.tsx

@@ -1,7 +1,6 @@
 import assert from 'assert';
 import assert from 'assert';
 
 
 import { isClient } from '@growi/core/dist/utils/browser-utils';
 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';
 import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
 import * as drawio from '@growi/remark-drawio';
 import * as drawio from '@growi/remark-drawio';
 // eslint-disable-next-line import/extensions
 // eslint-disable-next-line import/extensions
@@ -19,7 +18,6 @@ import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
-import { SlideViewer } from '~/components/ReactMarkdownComponents/SlideViewer';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
 import * as mermaid from '~/features/mermaid';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
@@ -68,7 +66,6 @@ export const generateViewOptions = (
     attachment.remarkPlugin,
     attachment.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
-    [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
   );
   );
   if (config.isEnabledLinebreaks) {
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
     remarkPlugins.push(breaks);
@@ -84,7 +81,6 @@ export const generateViewOptions = (
       drawio.sanitizeOption,
       drawio.sanitizeOption,
       mermaid.sanitizeOption,
       mermaid.sanitizeOption,
       attachment.sanitizeOption,
       attachment.sanitizeOption,
-      slides.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
     )]
     )]
@@ -119,7 +115,6 @@ export const generateViewOptions = (
     components.mermaid = mermaid.MermaidViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
     components.attachment = RichAttachment;
     components.img = LightBox;
     components.img = LightBox;
-    components.slide = SlideViewer;
   }
   }
 
 
   if (config.isEnabledXssPrevention) {
   if (config.isEnabledXssPrevention) {
@@ -241,6 +236,21 @@ export const generatePresentationViewOptions = (
   // based on simple view options
   // based on simple view options
   const options = generateSimpleViewOptions(config, pagePath);
   const options = generateSimpleViewOptions(config, pagePath);
 
 
+  const { rehypePlugins } = options;
+
+
+  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+    ? [sanitize, deepmerge(
+      addLineNumberAttribute.sanitizeOption,
+    )]
+    : () => {};
+
+  // add rehype plugins
+  rehypePlugins.push(
+    addLineNumberAttribute.rehypePlugin,
+    rehypeSanitizePlugin,
+  );
+
   if (config.isEnabledXssPrevention) {
   if (config.isEnabledXssPrevention) {
     verifySanitizePlugin(options, false);
     verifySanitizePlugin(options, false);
   }
   }
@@ -262,7 +272,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     attachment.remarkPlugin,
     attachment.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
-    [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
   );
   );
   if (config.isEnabledLinebreaks) {
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
     remarkPlugins.push(breaks);
@@ -281,7 +290,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
       lsxGrowiDirective.sanitizeOption,
       lsxGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       refsGrowiDirective.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
       addLineNumberAttribute.sanitizeOption,
-      slides.sanitizeOption,
     )]
     )]
     : () => {};
     : () => {};
 
 
@@ -306,7 +314,6 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.mermaid = mermaid.MermaidViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
     components.attachment = RichAttachment;
     components.img = LightBox;
     components.img = LightBox;
-    components.slide = SlideViewer;
   }
   }
 
 
   if (config.isEnabledXssPrevention) {
   if (config.isEnabledXssPrevention) {

+ 0 - 90
apps/app/src/client/services/renderer/slide-viewer-renderer.tsx

@@ -1,90 +0,0 @@
-import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
-import * as drawio from '@growi/remark-drawio';
-// eslint-disable-next-line import/extensions
-import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
-import katex from 'rehype-katex';
-import sanitize from 'rehype-sanitize';
-import math from 'remark-math';
-import deepmerge from 'ts-deepmerge';
-import type { Pluggable } from 'unified';
-
-import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
-import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
-import * as mermaid from '~/features/mermaid';
-import { RehypeSanitizeOption } from '~/interfaces/rehype';
-import type { RendererOptions } from '~/interfaces/renderer-options';
-import type { RendererConfig } from '~/interfaces/services/renderer';
-import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
-import * as attachment from '~/services/renderer/remark-plugins/attachment';
-import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
-import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
-import {
-  commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
-} from '~/services/renderer/renderer';
-
-
-export const generatePresentationViewOptions = (
-    config: RendererConfig,
-    pagePath: string,
-): RendererOptions => {
-  const options = generateCommonOptions(pagePath);
-
-  const { remarkPlugins, rehypePlugins, components } = options;
-
-  // add remark plugins
-  remarkPlugins.push(
-    math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
-    drawio.remarkPlugin,
-    mermaid.remarkPlugin,
-    xsvToTable.remarkPlugin,
-    attachment.remarkPlugin,
-    lsxGrowiDirective.remarkPlugin,
-    refsGrowiDirective.remarkPlugin,
-  );
-
-  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
-    injectCustomSanitizeOption(config);
-  }
-
-
-  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      commonSanitizeOption,
-      drawio.sanitizeOption,
-      mermaid.sanitizeOption,
-      attachment.sanitizeOption,
-      lsxGrowiDirective.sanitizeOption,
-      refsGrowiDirective.sanitizeOption,
-      addLineNumberAttribute.sanitizeOption,
-    )]
-    : () => {};
-
-  // add rehype plugins
-  rehypePlugins.push(
-    [lsxGrowiDirective.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
-    [refsGrowiDirective.rehypePlugin, { pagePath }],
-    rehypeSanitizePlugin,
-    addLineNumberAttribute.rehypePlugin,
-    katex,
-  );
-
-  // add components
-  if (components != null) {
-    components.lsx = lsxGrowiDirective.LsxImmutable;
-    components.ref = refsGrowiDirective.RefImmutable;
-    components.refs = refsGrowiDirective.RefsImmutable;
-    components.refimg = refsGrowiDirective.RefImgImmutable;
-    components.refsimg = refsGrowiDirective.RefsImgImmutable;
-    components.gallery = refsGrowiDirective.GalleryImmutable;
-    components.drawio = drawio.DrawioViewer;
-    components.mermaid = mermaid.MermaidViewer;
-    components.attachment = RichAttachment;
-    components.img = LightBox;
-  }
-
-  if (config.isEnabledXssPrevention) {
-    verifySanitizePlugin(options, false);
-  }
-  return options;
-};

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx

@@ -53,7 +53,7 @@ const CustomizePresentationSetting = (props: Props): JSX.Element => {
                 href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
                 href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
                 target="_blank"
                 target="_blank"
                 rel="noopener noreferrer"
                 rel="noopener noreferrer"
-              >{`${t('admin:customize_settings.presenattion_options.marp_in_growi')}`}
+              >{`${t('admin:customize_settings.presentation_options.marp_in_growi')}`}
               </a>
               </a>
             </p>
             </p>
           </CustomizePresentationOption>
           </CustomizePresentationOption>

+ 13 - 2
apps/app/src/components/Page/PageView.tsx

@@ -4,6 +4,7 @@ import React, {
 
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useShouldExpandContent } from '~/client/services/layout';
@@ -40,6 +41,7 @@ const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod =>
 const UsersHomepageFooter = dynamic<UsersHomepageFooterProps>(() => import('../UsersHomepageFooter')
 const UsersHomepageFooter = dynamic<UsersHomepageFooterProps>(() => import('../UsersHomepageFooter')
   .then(mod => mod.UsersHomepageFooter), { ssr: false });
   .then(mod => mod.UsersHomepageFooter), { ssr: false });
 const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
 const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
+const SlideRenderer = dynamic(() => import('./SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
 
 
 
 
 type Props = {
 type Props = {
@@ -74,6 +76,10 @@ export const PageView = (props: Props): JSX.Element => {
   const shouldExpandContent = useShouldExpandContent(page);
   const shouldExpandContent = useShouldExpandContent(page);
 
 
 
 
+  const markdown = page?.revision?.body;
+  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
+
+
   // ***************************  Auto Scroll  ***************************
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
   useEffect(() => {
     // do nothing if hash is empty
     // do nothing if hash is empty
@@ -90,6 +96,7 @@ export const PageView = (props: Props): JSX.Element => {
   }, [isCommentsLoaded]);
   }, [isCommentsLoaded]);
   // *******************************  end  *******************************
   // *******************************  end  *******************************
 
 
+
   const specialContents = useMemo(() => {
   const specialContents = useMemo(() => {
     if (isIdenticalPathPage) {
     if (isIdenticalPathPage) {
       return <IdenticalPathPage />;
       return <IdenticalPathPage />;
@@ -128,15 +135,19 @@ export const PageView = (props: Props): JSX.Element => {
       return <NotFoundPage path={pagePath} />;
       return <NotFoundPage path={pagePath} />;
     }
     }
 
 
-    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
     const markdown = page.revision.body;
     const markdown = page.revision.body;
+    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
 
 
     return (
     return (
       <>
       <>
         <PageContentsUtilities />
         <PageContentsUtilities />
 
 
         <div className="flex-expand-vert justify-content-between">
         <div className="flex-expand-vert justify-content-between">
-          <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+
+          { isSlide != null
+            ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+            : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+          }
 
 
           { !isIdenticalPathPage && !isNotFound && (
           { !isIdenticalPathPage && !isNotFound && (
             <div id="comments-container" ref={commentsContainerRef}>
             <div id="comments-container" ref={commentsContainerRef}>

+ 3 - 1
apps/app/src/components/Page/RevisionRenderer.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
-import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
+import type { FallbackProps } from 'react-error-boundary';
+import { ErrorBoundary } from 'react-error-boundary';
 import ReactMarkdown from 'react-markdown';
 import ReactMarkdown from 'react-markdown';
 
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererOptions } from '~/interfaces/renderer-options';
@@ -8,6 +9,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import 'katex/dist/katex.min.css';
 import 'katex/dist/katex.min.css';
 
 
+
 const logger = loggerFactory('components:Page:RevisionRenderer');
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 
 type Props = {
 type Props = {

+ 26 - 0
apps/app/src/components/Page/SlideRenderer.tsx

@@ -0,0 +1,26 @@
+import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+
+import { usePresentationViewOptions } from '~/stores/renderer';
+
+import { Slides } from '../Presentation/Slides';
+
+type SlideRendererProps = {
+  markdown: string,
+  marp?: boolean,
+};
+
+export const SlideRenderer = (props: SlideRendererProps): JSX.Element => {
+
+  const { markdown, marp = false } = props;
+
+  const { data: rendererOptions } = usePresentationViewOptions();
+
+  return (
+    <Slides
+      hasMarpFlag={marp}
+      options={{ rendererOptions: rendererOptions as ReactMarkdownOptions }}
+    >
+      {markdown}
+    </Slides>
+  );
+};

+ 15 - 5
apps/app/src/components/PageEditor/Preview.tsx

@@ -1,10 +1,12 @@
 import type { CSSProperties } from 'react';
 import type { CSSProperties } from 'react';
-import React from 'react';
+
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 
 
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererOptions } from '~/interfaces/renderer-options';
+import { useIsEnabledMarp } from '~/stores/context';
 
 
 import RevisionRenderer from '../Page/RevisionRenderer';
 import RevisionRenderer from '../Page/RevisionRenderer';
-
+import { SlideRenderer } from '../Page/SlideRenderer';
 
 
 import styles from './Preview.module.scss';
 import styles from './Preview.module.scss';
 
 
@@ -28,17 +30,25 @@ const Preview = (props: Props): JSX.Element => {
     expandContentWidth,
     expandContentWidth,
   } = props;
   } = props;
 
 
+  const { data: isEnabledMarp } = useIsEnabledMarp();
+  const isSlide = useSlidesByFrontmatter(markdown, isEnabledMarp);
+
   const fluidLayoutClass = expandContentWidth ? 'fluid-layout' : '';
   const fluidLayoutClass = expandContentWidth ? 'fluid-layout' : '';
 
 
+
   return (
   return (
     <div
     <div
       data-testid="page-editor-preview-body"
       data-testid="page-editor-preview-body"
       className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       className={`${moduleClass} ${fluidLayoutClass} ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       style={style}
       style={style}
     >
     >
-      { markdown != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
-      ) }
+      { markdown != null
+        && (
+          isSlide != null
+            ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+            : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
+        )
+      }
     </div>
     </div>
   );
   );
 
 

+ 7 - 4
apps/app/src/components/PagePresentationModal.tsx

@@ -1,6 +1,7 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
-import type { PresentationProps } from '@growi/presentation';
+import type { PresentationProps } from '@growi/presentation/dist/client';
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import { useFullScreen } from '@growi/ui/dist/utils';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -39,6 +40,10 @@ const PagePresentationModal = (): JSX.Element => {
 
 
   const { data: isEnabledMarp } = useIsEnabledMarp();
   const { data: isEnabledMarp } = useIsEnabledMarp();
 
 
+  const markdown = currentPage?.revision?.body;
+
+  const isSlide = useSlidesByFrontmatter(markdown, isEnabledMarp);
+
   const toggleFullscreenHandler = useCallback(() => {
   const toggleFullscreenHandler = useCallback(() => {
     if (fullscreen.active) {
     if (fullscreen.active) {
       fullscreen.exit();
       fullscreen.exit();
@@ -61,8 +66,6 @@ const PagePresentationModal = (): JSX.Element => {
     return <></>;
     return <></>;
   }
   }
 
 
-  const markdown = currentPage?.revision?.body;
-
   return (
   return (
     <Modal
     <Modal
       isOpen={isOpen}
       isOpen={isOpen}
@@ -92,7 +95,7 @@ const PagePresentationModal = (): JSX.Element => {
               },
               },
               isDarkMode,
               isDarkMode,
             }}
             }}
-            isEnabledMarp={isEnabledMarp}
+            marp={isSlide?.marp}
           >
           >
             {markdown}
             {markdown}
           </Presentation>
           </Presentation>

+ 1 - 1
apps/app/src/components/Presentation/Presentation.tsx

@@ -1,4 +1,4 @@
-import { Presentation as PresentationSubstance, type PresentationProps } from '@growi/presentation';
+import { Presentation as PresentationSubstance, type PresentationProps } from '@growi/presentation/dist/client';
 
 
 import '@growi/presentation/dist/style.css';
 import '@growi/presentation/dist/style.css';
 
 

+ 1 - 1
apps/app/src/components/Presentation/Slides.tsx

@@ -1,4 +1,4 @@
-import { Slides as SlidesSubstance, type SlidesProps } from '@growi/presentation';
+import { Slides as SlidesSubstance, type SlidesProps } from '@growi/presentation/dist/client';
 
 
 import '@growi/presentation/dist/style.css';
 import '@growi/presentation/dist/style.css';
 
 

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

@@ -1,33 +0,0 @@
-import React from 'react';
-
-import dynamic from 'next/dynamic';
-import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
-
-import { usePresentationViewOptions } from '~/stores/slide-viewer-renderer';
-
-
-const Slides = dynamic(() => import('../Presentation/Slides').then(mod => mod.Slides), { ssr: false });
-
-type SlideViewerProps = {
-  marp: string | undefined,
-  children: string,
-}
-
-export const SlideViewer: React.FC<SlideViewerProps> = React.memo((props: SlideViewerProps) => {
-  const {
-    marp, children,
-  } = props;
-
-  const { data: rendererOptions } = usePresentationViewOptions();
-
-  return (
-    <Slides
-      hasMarpFlag={marp != null}
-      options={{ rendererOptions: rendererOptions as ReactMarkdownOptions }}
-    >
-      {children}
-    </Slides>
-  );
-});
-
-SlideViewer.displayName = 'SlideViewer';

+ 10 - 6
apps/app/src/components/ShareLinkPageView.tsx

@@ -1,12 +1,14 @@
 import React, { useMemo } from 'react';
 import React, { useMemo } from 'react';
 
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
+import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
 import { useShouldExpandContent } from '~/client/services/layout';
 import { useShouldExpandContent } from '~/client/services/layout';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
+import { useIsEnabledMarp } from '~/stores/context';
 import { useIsNotFound } from '~/stores/page';
 import { useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -23,7 +25,7 @@ const logger = loggerFactory('growi:Page');
 
 
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('./PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('./PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
 const ForbiddenPage = dynamic(() => import('./ForbiddenPage'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('./ForbiddenPage'), { ssr: false });
-
+const SlideRenderer = dynamic(() => import('./Page/SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
 
 
 type Props = {
 type Props = {
   pagePath: string,
   pagePath: string,
@@ -47,6 +49,10 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
 
 
   const shouldExpandContent = useShouldExpandContent(page);
   const shouldExpandContent = useShouldExpandContent(page);
 
 
+  const markdown = page?.revision?.body;
+
+  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
+
   const isNotFound = isNotFoundMeta || page == null || shareLink == null;
   const isNotFound = isNotFoundMeta || page == null || shareLink == null;
 
 
   const specialContents = useMemo(() => {
   const specialContents = useMemo(() => {
@@ -85,11 +91,9 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
     const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
     const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
     const markdown = page.revision.body;
     const markdown = page.revision.body;
 
 
-    return (
-      <>
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      </>
-    );
+    return isSlide != null
+      ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+      : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />;
   };
   };
 
 
   return (
   return (

+ 0 - 27
apps/app/src/stores/slide-viewer-renderer.tsx

@@ -1,27 +0,0 @@
-import useSWR, { type SWRResponse } from 'swr';
-
-import type { RendererOptions } from '~/interfaces/renderer-options';
-import { useRendererConfig } from '~/stores/context';
-import { useCurrentPagePath } from '~/stores/page';
-
-
-export const usePresentationViewOptions = (): SWRResponse<RendererOptions, Error> => {
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: rendererConfig } = useRendererConfig();
-
-  const isAllDataValid = currentPagePath != null && rendererConfig != null;
-
-  return useSWR(
-    isAllDataValid
-      ? ['presentationViewOptions', currentPagePath, rendererConfig]
-      : null,
-    async([, currentPagePath, rendererConfig]) => {
-      const { generatePresentationViewOptions } = await import('~/client/services/renderer/slide-viewer-renderer');
-      return generatePresentationViewOptions(rendererConfig, currentPagePath);
-    },
-    {
-      revalidateOnFocus: false,
-      revalidateOnReconnect: false,
-    },
-  );
-};

+ 14 - 2
packages/presentation/package.json

@@ -13,6 +13,17 @@
     "dist"
     "dist"
   ],
   ],
   "type": "module",
   "type": "module",
+  "exports": {
+    "./dist/client": {
+      "import": "./dist/client/index.js"
+    },
+    "./dist/services": {
+      "import": "./dist/services/index.js"
+    },
+    "./dist/style.css": {
+      "import": "./dist/style.css"
+    }
+  },
   "scripts": {
   "scripts": {
     "build": "vite build",
     "build": "vite build",
     "clean": "shx rm -rf dist",
     "clean": "shx rm -rf dist",
@@ -28,7 +39,8 @@
     "@growi/core": "link:../core"
     "@growi/core": "link:../core"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@marp-team/marp-core": "^3.6.0",
+    "@marp-team/marp-core": "^3.9.0",
+    "@marp-team/marpit": "^2.6.1",
     "@types/reveal.js": "^4.4.1",
     "@types/reveal.js": "^4.4.1",
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "hast-util-sanitize": "^4.1.0",
     "hast-util-sanitize": "^4.1.0",
@@ -38,6 +50,7 @@
     "mdast-util-to-markdown": "^1.3.0",
     "mdast-util-to-markdown": "^1.3.0",
     "react-markdown": "^8.0.7",
     "react-markdown": "^8.0.7",
     "remark-frontmatter": "^4.0.1",
     "remark-frontmatter": "^4.0.1",
+    "remark-parse": "^10.0.0",
     "remark-stringify": "^10.0.0",
     "remark-stringify": "^10.0.0",
     "reveal.js": "^4.4.0",
     "reveal.js": "^4.4.0",
     "unified": "^10.1.2",
     "unified": "^10.1.2",
@@ -45,7 +58,6 @@
     "unist-util-visit": "^4.0.0"
     "unist-util-visit": "^4.0.0"
   },
   },
   "peerDependencies": {
   "peerDependencies": {
-    "@marp-team/marpit": "*",
     "next": "^14",
     "next": "^14",
     "react": "^18.2.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
     "react-dom": "^18.2.0"

+ 0 - 0
packages/presentation/src/components/GrowiSlides.tsx → packages/presentation/src/client/components/GrowiSlides.tsx


+ 0 - 0
packages/presentation/src/components/MarpSlides.tsx → packages/presentation/src/client/components/MarpSlides.tsx


+ 0 - 0
packages/presentation/src/components/Presentation.global.scss → packages/presentation/src/client/components/Presentation.global.scss


+ 0 - 0
packages/presentation/src/components/Presentation.module.scss → packages/presentation/src/client/components/Presentation.module.scss


+ 14 - 18
packages/presentation/src/components/Presentation.tsx → packages/presentation/src/client/components/Presentation.tsx

@@ -1,9 +1,8 @@
-import React, { useEffect } from 'react';
+import { useEffect } from 'react';
 
 
 import Reveal from 'reveal.js';
 import Reveal from 'reveal.js';
 
 
 import type { PresentationOptions } from '../consts';
 import type { PresentationOptions } from '../consts';
-import { parseSlideFrontmatterInMarkdown } from '../services/parse-slide-frontmatter';
 
 
 import { Slides } from './Slides';
 import { Slides } from './Slides';
 
 
@@ -34,37 +33,34 @@ const removeAllHiddenElements = () => {
 
 
 export type PresentationProps = {
 export type PresentationProps = {
   options: PresentationOptions,
   options: PresentationOptions,
-  isEnabledMarp: boolean,
+  marp?: boolean,
   children?: string,
   children?: string,
 }
 }
 
 
 export const Presentation = (props: PresentationProps): JSX.Element => {
 export const Presentation = (props: PresentationProps): JSX.Element => {
-  const { options, isEnabledMarp, children } = props;
+  const { options, marp, children } = props;
   const { revealOptions } = options;
   const { revealOptions } = options;
 
 
-  const [marp] = parseSlideFrontmatterInMarkdown(children);
-  const hasMarpFlag = isEnabledMarp && marp;
-
   useEffect(() => {
   useEffect(() => {
-    let deck: Reveal.Api;
-    if (children != null) {
-      deck = new Reveal({ ...baseRevealOptions, ...revealOptions });
-      deck.initialize()
-        .then(() => deck.slide(0)); // navigate to the first slide
-
-      deck.on('ready', removeAllHiddenElements);
-      deck.on('slidechanged', removeAllHiddenElements);
+    if (children == null) {
+      return;
     }
     }
+    const deck = new Reveal({ ...baseRevealOptions, ...revealOptions });
+    deck.initialize()
+      .then(() => deck.slide(0)); // navigate to the first slide
+
+    deck.on('ready', removeAllHiddenElements);
+    deck.on('slidechanged', removeAllHiddenElements);
 
 
     return function cleanup() {
     return function cleanup() {
-      deck?.off('ready', removeAllHiddenElements);
-      deck?.off('slidechanged', removeAllHiddenElements);
+      deck.off('ready', removeAllHiddenElements);
+      deck.off('slidechanged', removeAllHiddenElements);
     };
     };
   }, [children, revealOptions]);
   }, [children, revealOptions]);
 
 
   return (
   return (
     <div className={`grw-presentation ${styles['grw-presentation']} reveal`}>
     <div className={`grw-presentation ${styles['grw-presentation']} reveal`}>
-      <Slides options={options} hasMarpFlag={hasMarpFlag} presentation>{children}</Slides>
+      <Slides options={options} hasMarpFlag={marp} presentation>{children}</Slides>
     </div>
     </div>
   );
   );
 };
 };

+ 0 - 0
packages/presentation/src/components/RichSlideSection.tsx → packages/presentation/src/client/components/RichSlideSection.tsx


+ 0 - 0
packages/presentation/src/components/Slides.module.scss → packages/presentation/src/client/components/Slides.module.scss


+ 0 - 0
packages/presentation/src/components/Slides.tsx → packages/presentation/src/client/components/Slides.tsx


+ 0 - 0
packages/presentation/src/consts/index.ts → packages/presentation/src/client/consts/index.ts


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

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

+ 0 - 0
packages/presentation/src/services/growi-marpit.ts → packages/presentation/src/client/services/growi-marpit.ts


+ 0 - 0
packages/presentation/src/services/renderer/extract-sections.ts → packages/presentation/src/client/services/renderer/extract-sections.ts


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

@@ -0,0 +1 @@
+export * from './use-slides-by-frontmatter';

+ 0 - 43
packages/presentation/src/services/parse-slide-frontmatter.ts

@@ -1,43 +0,0 @@
-import remarkFrontmatter from 'remark-frontmatter';
-import remarkParse from 'remark-parse';
-import remarkStringify from 'remark-stringify';
-import { unified } from 'unified';
-
-
-export const parseSlideFrontmatter = (frontmatter: string): [boolean, boolean] => {
-
-  let marp = false;
-  let slide = false;
-
-  const lines = frontmatter.split('\n');
-  lines.forEach((line) => {
-    const [key, value] = line.split(':').map(part => part.trim());
-    if (key === 'marp' && value === 'true') {
-      marp = true;
-    }
-    if (key === 'slide' && value === 'true') {
-      slide = true;
-    }
-  });
-
-  return [marp, slide];
-};
-
-export const parseSlideFrontmatterInMarkdown = (markdown?: string): [boolean, boolean] => {
-
-  let marp = false;
-  let slide = false;
-
-  unified()
-    .use(remarkParse)
-    .use(remarkStringify)
-    .use(remarkFrontmatter, ['yaml'])
-    .use(() => ((obj) => {
-      if (obj.children[0]?.type === 'yaml') {
-        [marp, slide] = parseSlideFrontmatter(obj.children[0]?.value as string);
-      }
-    }))
-    .process(markdown as string);
-
-  return [marp, slide];
-};

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

@@ -1,89 +0,0 @@
-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';
-
-import { parseSlideFrontmatter } from '../parse-slide-frontmatter';
-
-const SUPPORTED_ATTRIBUTES = ['children', 'marp'];
-
-const nodeToMakrdown = (node: Node) => {
-  return toMarkdown(node as Root, {
-    extensions: [
-      frontmatterToMarkdown(['yaml']),
-      gfmToMarkdown(),
-    ],
-  });
-};
-
-// Allow node tree to be converted to markdown
-const removeCustomType = (tree: Node) => {
-  // Try toMarkdown() on all Node.
-  visit(tree, (node) => {
-    const tmp = node?.children;
-    node.children = [];
-    try {
-      nodeToMakrdown(node);
-    }
-    catch (err) {
-      // if some Node cannot convert to markdown, change to a convertible type
-      node.type = 'text';
-      node.value = '';
-    }
-    finally {
-      node.children = tmp;
-    }
-  });
-};
-
-const rewriteNode = (tree: Node, node: Node, isEnabledMarp: boolean) => {
-
-  const [marp, slide] = parseSlideFrontmatter(node.value as string);
-
-  if ((marp && isEnabledMarp) || slide) {
-
-    removeCustomType(tree);
-
-    const markdown = nodeToMakrdown(tree);
-
-    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 && isEnabledMarp) ? '' : undefined,
-      children: markdown,
-    };
-  }
-};
-
-type SlidePluginParams = {
-  isEnabledMarp: boolean,
-}
-
-export const remarkPlugin: Plugin<[SlidePluginParams]> = (options) => {
-  return (tree) => {
-    visit(tree, (node) => {
-      if (node.type === 'yaml' && node.value != null) {
-        rewriteNode(tree, node, options.isEnabledMarp);
-      }
-    });
-  };
-};
-
-export const sanitizeOption: SanitizeOption = {
-  tagNames: ['slide'],
-  attributes: {
-    slide: SUPPORTED_ATTRIBUTES,
-  },
-};

+ 97 - 0
packages/presentation/src/services/use-slides-by-frontmatter.ts

@@ -0,0 +1,97 @@
+import { useEffect, useState } from 'react';
+
+import type { Processor } from 'unified';
+
+type ParseResult = {
+  marp: boolean | undefined,
+  slide: boolean | undefined,
+}
+
+const parseSlideFrontmatter = (frontmatter: string): ParseResult => {
+
+  let marp;
+  let slide;
+
+  const lines = frontmatter.split('\n');
+  lines.forEach((line) => {
+    const [key, value] = line.split(':').map(part => part.trim());
+    if (key === 'marp' && value === 'true') {
+      marp = true;
+    }
+    if (key === 'slide' && value === 'true') {
+      slide = true;
+    }
+  });
+
+  return { marp, slide };
+};
+
+
+type ProcessorOpts = {
+  onParsed?: (result: ParseResult) => void,
+  onSkipped?: () => void,
+};
+
+const generateFrontmatterProcessor = async(opts?: ProcessorOpts) => {
+
+  const remarkFrontmatter = (await import('remark-frontmatter')).default;
+  const remarkParse = (await import('remark-parse')).default;
+  const remarkStringify = (await import('remark-stringify')).default;
+  const unified = (await import('unified')).unified;
+
+  return unified()
+    .use(remarkParse)
+    .use(remarkStringify)
+    .use(remarkFrontmatter, ['yaml'])
+    .use(() => ((obj) => {
+      if (obj.children[0]?.type === 'yaml') {
+        const result = parseSlideFrontmatter(obj.children[0]?.value);
+        opts?.onParsed?.(result);
+      }
+      else {
+        opts?.onSkipped?.();
+      }
+    }));
+};
+
+export type UseSlide = {
+  marp?: boolean,
+}
+
+/**
+ * Frontmatter parser for slide
+ * @param markdown Markdwon document
+ * @returns An UseSlide instance. If the markdown does not contain neither "marp" or "slide" attribute in frontmatter, it returns undefined.
+ */
+export const useSlidesByFrontmatter = (markdown?: string, isEnabledMarp?: boolean): UseSlide | undefined => {
+
+  const [processor, setProcessor] = useState<Processor|undefined>();
+  const [parseResult, setParseResult] = useState<UseSlide|undefined>();
+
+  useEffect(() => {
+    if (processor != null) {
+      return;
+    }
+
+    (async() => {
+      const p = await generateFrontmatterProcessor({
+        onParsed: result => setParseResult(result.marp || result.slide ? result : undefined),
+        onSkipped: () => setParseResult(undefined),
+      });
+      setProcessor(p);
+    })();
+  }, [processor]);
+
+  useEffect(() => {
+    if (markdown == null || processor == null) {
+      return;
+    }
+
+    processor.process(markdown);
+  }, [markdown, processor]);
+
+  return parseResult != null
+    ? { marp: isEnabledMarp && parseResult?.marp }
+    : undefined;
+
+};

+ 12 - 1
packages/presentation/vite.config.ts

@@ -1,4 +1,7 @@
+import path from 'path';
+
 import react from '@vitejs/plugin-react';
 import react from '@vitejs/plugin-react';
+import glob from 'glob';
 import { nodeExternals } from 'rollup-plugin-node-externals';
 import { nodeExternals } from 'rollup-plugin-node-externals';
 import { defineConfig } from 'vite';
 import { defineConfig } from 'vite';
 import dts from 'vite-plugin-dts';
 import dts from 'vite-plugin-dts';
@@ -20,9 +23,17 @@ export default defineConfig({
     outDir: 'dist',
     outDir: 'dist',
     sourcemap: true,
     sourcemap: true,
     lib: {
     lib: {
-      entry: 'src/index.ts',
+      entry: glob.sync(path.resolve(__dirname, 'src/**/*.ts'), {
+        ignore: '**/*.spec.ts',
+      }),
       name: 'presentation-libs',
       name: 'presentation-libs',
       formats: ['es'],
       formats: ['es'],
     },
     },
+    rollupOptions: {
+      output: {
+        preserveModules: true,
+        preserveModulesRoot: 'src',
+      },
+    },
   },
   },
 });
 });

+ 41 - 28
yarn.lock

@@ -2554,18 +2554,18 @@
     google-libphonenumber ">=3.2.10"
     google-libphonenumber ">=3.2.10"
     lodash ">=4.17.15"
     lodash ">=4.17.15"
 
 
-"@marp-team/marp-core@^3.6.0":
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/@marp-team/marp-core/-/marp-core-3.6.0.tgz#858958b96208370fe51f4df98642e2bfe2d090c7"
-  integrity sha512-V2tA5nLR4HR+hTGrMTaSLB7bDKi+xz1FrhytOyHV/U2ztRRjAKtlQnul+xPZ7LpRxVsLNwbmO/Alw1AwUx7eXQ==
+"@marp-team/marp-core@^3.9.0":
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/@marp-team/marp-core/-/marp-core-3.9.0.tgz#c554443bdcf84db635221b3a4085d1a656dd8bfc"
+  integrity sha512-gi6nq0rsB1oMA8ReppW4XxmS4fisQiAsD0ZoUgLeG4h6SWatveCAA7fZyxnXfwA2UC8pNb7ktPqYdRsxvuwntA==
   dependencies:
   dependencies:
-    "@marp-team/marpit" "^2.4.2"
+    "@marp-team/marpit" "^2.6.1"
     "@marp-team/marpit-svg-polyfill" "^2.1.0"
     "@marp-team/marpit-svg-polyfill" "^2.1.0"
-    highlight.js "^11.7.0"
-    katex "^0.16.4"
+    highlight.js "11.8.0"
+    katex "^0.16.9"
     mathjax-full "^3.2.2"
     mathjax-full "^3.2.2"
-    postcss "^8.4.21"
-    postcss-selector-parser "^6.0.11"
+    postcss "^8.4.31"
+    postcss-selector-parser "^6.0.13"
     xss "^1.0.14"
     xss "^1.0.14"
 
 
 "@marp-team/marpit-svg-polyfill@^2.1.0":
 "@marp-team/marpit-svg-polyfill@^2.1.0":
@@ -2573,18 +2573,18 @@
   resolved "https://registry.yarnpkg.com/@marp-team/marpit-svg-polyfill/-/marpit-svg-polyfill-2.1.0.tgz#40e7ce3a2aa7496748541cc7053e6779d2f866ac"
   resolved "https://registry.yarnpkg.com/@marp-team/marpit-svg-polyfill/-/marpit-svg-polyfill-2.1.0.tgz#40e7ce3a2aa7496748541cc7053e6779d2f866ac"
   integrity sha512-VqCoAKwv1HJdzZp36dDPxznz2JZgRjkVSSPHpCzk72G2N753F0HPKXjevdjxmzN6gir9bUGBgMD1SguWJIi11A==
   integrity sha512-VqCoAKwv1HJdzZp36dDPxznz2JZgRjkVSSPHpCzk72G2N753F0HPKXjevdjxmzN6gir9bUGBgMD1SguWJIi11A==
 
 
-"@marp-team/marpit@^2.4.2":
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/@marp-team/marpit/-/marpit-2.4.2.tgz#f74240f14c15a4c5f0bd6b51afabc674750799be"
-  integrity sha512-MBCMaSjkeDzF+Exq8CmhmwsMTzU7fK4NyzA7zl+hreBGppAPvOq5NCnIR2O6nOm9F0QJ9Vmxhs7ZP/BXZ+lWBw==
+"@marp-team/marpit@^2.6.1":
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/@marp-team/marpit/-/marpit-2.6.1.tgz#6937e6bc6b1b0cc9efc6c1e0948fc165df038edc"
+  integrity sha512-Hg7fZ8SqXwLjxeIFzSnlXkXEmt0ZXPeMJneEn9n1M495a34C4xtkgEgL8R1MW2IRCh4Yibn0xmGKcaf+GuqR2A==
   dependencies:
   dependencies:
     color-string "^1.9.1"
     color-string "^1.9.1"
     cssesc "^3.0.0"
     cssesc "^3.0.0"
     js-yaml "^4.1.0"
     js-yaml "^4.1.0"
     lodash.kebabcase "^4.1.1"
     lodash.kebabcase "^4.1.1"
-    markdown-it "^13.0.1"
+    markdown-it "^13.0.2"
     markdown-it-front-matter "^0.2.3"
     markdown-it-front-matter "^0.2.3"
-    postcss "^8.4.19"
+    postcss "^8.4.29"
 
 
 "@microsoft/api-extractor-model@7.28.13":
 "@microsoft/api-extractor-model@7.28.13":
   version "7.28.13"
   version "7.28.13"
@@ -6451,7 +6451,7 @@ commander@^6.2.1:
   resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
   resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
   integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
   integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
 
 
-commander@^8.0.0, commander@^8.1.0:
+commander@^8.0.0, commander@^8.1.0, commander@^8.3.0:
   version "8.3.0"
   version "8.3.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
@@ -9851,7 +9851,12 @@ helmet@^4.6.0:
   resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df"
   resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df"
   integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==
   integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==
 
 
-highlight.js@11.9.0, highlight.js@^11.7.0:
+highlight.js@11.8.0:
+  version "11.8.0"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.8.0.tgz#966518ea83257bae2e7c9a48596231856555bb65"
+  integrity sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==
+
+highlight.js@11.9.0:
   version "11.9.0"
   version "11.9.0"
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
   integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
   integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
@@ -11451,12 +11456,12 @@ katex@^0.15.0:
   dependencies:
   dependencies:
     commander "^8.0.0"
     commander "^8.0.0"
 
 
-katex@^0.16.4:
-  version "0.16.4"
-  resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.4.tgz#87021bc3bbd80586ef715aeb476794cba6a49ad4"
-  integrity sha512-WudRKUj8yyBeVDI4aYMNxhx5Vhh2PjpzQw1GRu/LVGqL4m1AxwD1GcUp0IMbdJaf5zsjtj8ghP0DOQRYhroNkw==
+katex@^0.16.9:
+  version "0.16.10"
+  resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.10.tgz#6f81b71ac37ff4ec7556861160f53bc5f058b185"
+  integrity sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==
   dependencies:
   dependencies:
-    commander "^8.0.0"
+    commander "^8.3.0"
 
 
 keycloak-js@^17.0.1:
 keycloak-js@^17.0.1:
   version "17.0.1"
   version "17.0.1"
@@ -12116,10 +12121,10 @@ markdown-it-front-matter@^0.2.3:
   resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.2.3.tgz#d6fa0f4b362e02086dd4ce8219fadf3f4c9cfa37"
   resolved "https://registry.yarnpkg.com/markdown-it-front-matter/-/markdown-it-front-matter-0.2.3.tgz#d6fa0f4b362e02086dd4ce8219fadf3f4c9cfa37"
   integrity sha512-s9+rcClLmZsZc3YL8Awjg/YO/VdphlE20LJ9Bx5a8RAFLI5a1vq6Mll8kOzG6w/wy8yhFLBupaa6Mfd60GATkA==
   integrity sha512-s9+rcClLmZsZc3YL8Awjg/YO/VdphlE20LJ9Bx5a8RAFLI5a1vq6Mll8kOzG6w/wy8yhFLBupaa6Mfd60GATkA==
 
 
-markdown-it@^13.0.1:
-  version "13.0.1"
-  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430"
-  integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==
+markdown-it@^13.0.2:
+  version "13.0.2"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.2.tgz#1bc22e23379a6952e5d56217fbed881e0c94d536"
+  integrity sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==
   dependencies:
   dependencies:
     argparse "^2.0.1"
     argparse "^2.0.1"
     entities "~3.0.1"
     entities "~3.0.1"
@@ -14460,7 +14465,15 @@ postcss-scss@^4.0.3:
   resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.3.tgz#36c23c19a804274e722e83a54d20b838ab4767ac"
   resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.3.tgz#36c23c19a804274e722e83a54d20b838ab4767ac"
   integrity sha512-j4KxzWovfdHsyxwl1BxkUal/O4uirvHgdzMKS1aWJBAV0qh2qj5qAZqpeBfVUYGWv+4iK9Az7SPyZ4fyNju1uA==
   integrity sha512-j4KxzWovfdHsyxwl1BxkUal/O4uirvHgdzMKS1aWJBAV0qh2qj5qAZqpeBfVUYGWv+4iK9Az7SPyZ4fyNju1uA==
 
 
-postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.7:
+postcss-selector-parser@^6.0.13:
+  version "6.0.16"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04"
+  integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-selector-parser@^6.0.7:
   version "6.0.11"
   version "6.0.11"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
   integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
   integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
@@ -14500,7 +14513,7 @@ postcss@^7.0.0:
     picocolors "^0.2.1"
     picocolors "^0.2.1"
     source-map "^0.6.1"
     source-map "^0.6.1"
 
 
-postcss@^8.3.11, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.31, postcss@^8.4.38:
+postcss@^8.3.11, postcss@^8.4.29, postcss@^8.4.31, postcss@^8.4.38:
   version "8.4.38"
   version "8.4.38"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
   integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
   integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==