فهرست منبع

Merge branch 'imprv/115672-presentation-preview' into imprv/115672-126744-ignore-custom-type

reiji-h 2 سال پیش
والد
کامیت
b59245b95d

+ 7 - 1
apps/app/public/static/locales/en_US/admin.json

@@ -480,7 +480,13 @@
       "show_all_reply_comments": "Show all reply comments",
       "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted.",
       "select_search_scope_children_as_default": "Select 'Only children of this tree' as default value of search range",
-      "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range."
+      "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range.",
+      "enable_marp": "Enable Marp ",
+      "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
+      "marp_official_site": "The Marp Official Site",
+      "marp_official_site_link": "https://marp.app",
+      "marp_in_growi" : "GROWI Docs - Create slide using Marp",
+      "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     "custom_title": "Custom title",
     "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",

+ 7 - 1
apps/app/public/static/locales/ja_JP/admin.json

@@ -488,7 +488,13 @@
       "show_all_reply_comments": "返信コメントを全て表示する",
       "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。",
       "select_search_scope_children_as_default": "検索範囲のデフォルト設定を「この階層下の子ページ」にする",
-      "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。"
+      "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。",
+      "enable_marp": "Marp を有効化する",
+      "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
+      "marp_official_site": "参考:Marp 公式サイト",
+      "marp_official_site_link": "https://marp.app",
+      "marp_in_growi" : "参考:GROWI Docs - Marp でスライドを作成する",
+      "marp_in_growi_link": "https://docs.growi.org/ja/guide/features/marp.html"
     },
     "custom_title": "カスタム Title",
     "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",

+ 7 - 1
apps/app/public/static/locales/zh_CN/admin.json

@@ -488,7 +488,13 @@
       "show_all_reply_comments": "显示所有回复评论",
       "show_all_reply_comments_desc": "当设置值为“关”时,将忽略最近两个之外的注释。",
       "select_search_scope_children_as_default": "选择“当前分支以下内容”, 作为搜索范围的默认值",
-      "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。"
+      "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。",
+      "enable_marp": "启用 Marp",
+      "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
+      "marp_official_site": "参考资料:Marp 官方网站",
+      "marp_official_site_link": "https://marp.app",
+      "marp_in_growi" : "参考资料:GROWI Docs - Create slide using Marp",
+      "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     "custom_title": "自定义标题",
     "custom_title_detail": "您可以自定义<code>&lt;title&gt;</code>标记。<br><code>&123;&123;sitename&&125;&125;</code>将自动替换为应用程序名称,并且<code>&123;&123;page&&125;&125;</code>将替换为页面名称/路径。",

+ 11 - 0
apps/app/src/client/services/AdminCustomizeContainer.js

@@ -34,6 +34,7 @@ export default class AdminCustomizeContainer extends Container {
       isEnabledStaleNotification: false,
       isAllReplyShown: false,
       isSearchScopeChildrenAsDefault: false,
+      isEnabledMarp: false,
       currentCustomizeTitle: '',
       currentCustomizeNoscript: '',
       currentCustomizeCss: '',
@@ -71,6 +72,7 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
         isAllReplyShown: customizeParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
+        isEnabledMarp: customizeParams.isEnabledMarp,
         currentCustomizeTitle: customizeParams.customizeTitle,
         currentCustomizeNoscript: customizeParams.customizeNoscript,
         currentCustomizeCss: customizeParams.customizeCss,
@@ -149,6 +151,13 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ isSearchScopeChildrenAsDefault: !this.state.isSearchScopeChildrenAsDefault });
   }
 
+  /**
+   * Switch isEnabledMarp
+   */
+  switchIsEnabledMarp() {
+    this.setState({ isEnabledMarp: !this.state.isEnabledMarp });
+  }
+
   /**
    * Change customize Title
    */
@@ -194,6 +203,7 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledStaleNotification: this.state.isEnabledStaleNotification,
         isAllReplyShown: this.state.isAllReplyShown,
         isSearchScopeChildrenAsDefault: this.state.isSearchScopeChildrenAsDefault,
+        isEnabledMarp: this.state.isEnabledMarp,
       });
       const { customizedParams } = response.data;
       this.setState({
@@ -206,6 +216,7 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
         isAllReplyShown: customizedParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizedParams.isSearchScopeChildrenAsDefault,
+        isEnabledMarp: customizedParams.state.isEnabledMarp,
       });
     }
     catch (err) {

+ 2 - 2
apps/app/src/client/services/renderer/renderer.tsx

@@ -66,9 +66,9 @@ export const generateViewOptions = (
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     attachment.remarkPlugin,
-    slides.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
+    [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
@@ -262,7 +262,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     attachment.remarkPlugin,
     lsxGrowiDirective.remarkPlugin,
     refsGrowiDirective.remarkPlugin,
-    slides.remarkPlugin,
+    [slides.remarkPlugin, { isEnabledMarp: config.isEnabledMarp }],
   );
   if (config.isEnabledLinebreaks) {
     remarkPlugins.push(breaks);

+ 29 - 0
apps/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -133,6 +133,35 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             </div>
           </div>
 
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <CustomizeFunctionOption
+                optionId="isEnabledMarp"
+                label={t('admin:customize_settings.function_options.enable_marp')}
+                isChecked={adminCustomizeContainer.state.isEnabledMarp || false}
+                onChecked={() => { adminCustomizeContainer.switchIsEnabledMarp() }}
+              >
+                <p className="form-text text-muted">
+                  {t('admin:customize_settings.function_options.enable_marp_desc')}
+                  <br></br>
+                  <a
+                    href={`${t('admin:customize_settings.function_options.marp_official_site_link')}`}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                  >{`${t('admin:customize_settings.function_options.marp_official_site')}`}
+                  </a>
+                  <br></br>
+                  <a
+                    href={`${t('admin:customize_settings.function_options.marp_in_gorwi_link')}`}
+                    target="_blank"
+                    rel="noopener noreferrer"
+                  >{`${t('admin:customize_settings.function_options.marp_in_growi')}`}
+                  </a>
+                </p>
+              </CustomizeFunctionOption>
+            </div>
+          </div>
+
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
         </div>
       </div>

+ 1 - 0
apps/app/src/interfaces/services/renderer.ts

@@ -7,6 +7,7 @@ export type RendererConfig = {
   adminPreferredIndentSize: number,
   isIndentSizeForced: boolean,
   highlightJsStyleBorder: boolean,
+  isEnabledMarp: boolean,
 
   drawioUri: string,
   plantumlUri: string,

+ 4 - 1
apps/app/src/pages/[[...path]].page.tsx

@@ -35,7 +35,7 @@ import {
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
   useIsAclEnabled, useIsSearchPage, useIsEnabledAttachTitleHeader,
-  useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPathname,
+  useCsrfToken, useIsSearchScopeChildrenAsDefault, useIsEnabledMarp, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
 } from '~/stores/context';
@@ -150,6 +150,7 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
+  isEnabledMarp: boolean,
 
   isSlackConfigured: boolean,
   // isMailerSetup: boolean,
@@ -218,6 +219,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsIndentSizeForced(props.isIndentSizeForced);
   useDisableLinkSharing(props.disableLinkSharing);
   useRendererConfig(props.rendererConfig);
+  useIsEnabledMarp(props.rendererConfig.isEnabledMarp);
   // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
   // useGrowiRendererConfig(props.growiRendererConfigStr != null ? JSON.parse(props.growiRendererConfigStr) : undefined);
   useIsAllReplyShown(props.isAllReplyShown);
@@ -591,6 +593,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
     adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
     isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
 

+ 5 - 1
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -12,7 +12,7 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp,
 } from '~/stores/context';
 
 import type { CommonProps } from './utils/commons';
@@ -28,6 +28,7 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
+  isEnabledMarp: boolean,
 
   // Render config
   rendererConfig: RendererConfig,
@@ -50,6 +51,7 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+  useIsEnabledMarp(props.isEnabledMarp);
 
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
@@ -84,6 +86,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+  props.isEnabledMarp = configManager.getConfig('crowi', 'customize:isEnabledMarp');
 
   props.sidebarConfig = {
     isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
@@ -93,6 +96,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
     adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
     isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
 

+ 1 - 0
apps/app/src/pages/_search.page.tsx

@@ -122,6 +122,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
     adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
     isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
 

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

@@ -17,7 +17,7 @@ import {
   useCurrentUser, useIsSearchPage, useGrowiCloudUri,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useCsrfToken, useIsSearchScopeChildrenAsDefault,
-  useRegistrationWhitelist, useShowPageLimitationXL, useRendererConfig,
+  useRegistrationWhitelist, useShowPageLimitationXL, useRendererConfig, useIsEnabledMarp,
 } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
@@ -34,6 +34,7 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
+  isEnabledMarp: boolean,
   rendererConfig: RendererConfig,
   showPageLimitationXL: number,
 
@@ -106,6 +107,7 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
 
   useRendererConfig(props.rendererConfig);
+  useIsEnabledMarp(props.rendererConfig.isEnabledMarp);
 
   const title = generateCustomTitle(props, targetPage.title);
 
@@ -163,6 +165,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
     adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
     isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
 

+ 4 - 1
apps/app/src/pages/share/[[...path]].page.tsx

@@ -20,7 +20,7 @@ import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { PageDocument } from '~/server/models/page';
 import {
   useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
-  useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid,
+  useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid, useIsEnabledMarp,
 } from '~/stores/context';
 import { useCurrentPageId, useIsNotFound, useSWRMUTxCurrentPage } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
@@ -41,6 +41,7 @@ type Props = CommonProps & {
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,
+  isEnabledMarp: boolean,
   drawioUri: string | null,
   rendererConfig: RendererConfig,
   skipSSR: boolean,
@@ -92,6 +93,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+  useIsEnabledMarp(props.rendererConfig.isEnabledMarp);
   useIsContainerFluid(props.isContainerFluid);
 
   const { trigger: mutateCurrentPage, data: currentPage } = useSWRMUTxCurrentPage();
@@ -166,6 +168,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     isSharedPage: true,
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    isEnabledMarp: configManager.getConfig('crowi', 'customize:isEnabledMarp'),
     adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
     isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
 

+ 1 - 0
apps/app/src/server/models/config.ts

@@ -131,6 +131,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:isEnabledStaleNotification': false,
   'customize:isAllReplyShown': false,
   'customize:isSearchScopeChildrenAsDefault': false,
+  'customize:isEnabledMarp': false,
   'customize:isSidebarDrawerMode': false,
   'customize:isSidebarClosedAtDockMode': false,
 

+ 6 - 0
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -61,6 +61,8 @@ const router = express.Router();
  *            type: boolean
  *          isSearchScopeChildrenAsDefault:
  *            type: boolean
+ *          isEnabledMarp:
+ *            type: boolean
  *      CustomizeHighlight:
  *        description: CustomizeHighlight
  *        type: object
@@ -125,6 +127,7 @@ module.exports = (crowi) => {
       body('isEnabledStaleNotification').isBoolean(),
       body('isAllReplyShown').isBoolean(),
       body('isSearchScopeChildrenAsDefault').isBoolean(),
+      body('isEnabledMarp').isBoolean(),
     ],
     customizeTitle: [
       body('customizeTitle').isString(),
@@ -181,6 +184,7 @@ module.exports = (crowi) => {
       isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
       isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
       isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
+      isEnabledMarp: await crowi.configManager.getConfig('crowi', 'customize:isEnabledMarp'),
       styleName: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
       styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
       customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
@@ -407,6 +411,7 @@ module.exports = (crowi) => {
       'customize:isEnabledStaleNotification': req.body.isEnabledStaleNotification,
       'customize:isAllReplyShown': req.body.isAllReplyShown,
       'customize:isSearchScopeChildrenAsDefault': req.body.isSearchScopeChildrenAsDefault,
+      'customize:isEnabledMarp': req.body.isEnabledMarp,
     };
 
     try {
@@ -421,6 +426,7 @@ module.exports = (crowi) => {
         isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
         isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
         isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
+        isEnabledMarp: await crowi.configManager.getConfig('crowi', 'customize:isEnabledMarp'),
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_FUNCTION_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);

+ 4 - 0
apps/app/src/stores/context.tsx

@@ -104,6 +104,10 @@ export const useIsSearchScopeChildrenAsDefault = (initialData?: boolean) : SWRRe
   return useContextSWR<boolean, Error>('isSearchScopeChildrenAsDefault', initialData, { fallbackData: false });
 };
 
+export const useIsEnabledMarp = (initialData?: boolean) : SWRResponse<boolean, Error> => {
+  return useContextSWR<boolean, Error>('isEnabledMarp', initialData, { fallbackData: false });
+};
+
 export const useIsSlackConfigured = (initialData?: boolean) : SWRResponse<boolean, Error> => {
   return useContextSWR<boolean, Error>('isSlackConfigured', initialData);
 };

+ 68 - 0
packages/presentation/src/components/GrowiSlides.tsx

@@ -0,0 +1,68 @@
+import { Marp } from '@marp-team/marp-core';
+import { Element } from '@marp-team/marpit';
+import Head from 'next/head';
+import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
+
+import type { PresentationOptions } from '../consts';
+import * as extractSections from '../services/renderer/extract-sections';
+
+import './Slides.global.scss';
+
+const MARP_CONTAINER_CLASS_NAME = 'marpit';
+
+// ----------------------------------------------------
+// TODO: to change better slide style
+// https://redmine.weseek.co.jp/issues/125680
+const marp = new Marp({
+  container: [
+    new Element('div', { class: MARP_CONTAINER_CLASS_NAME }),
+    new Element('div', { class: 'slides' }),
+  ],
+  inlineSVG: false,
+  emoji: undefined,
+  html: false,
+  math: false,
+});
+const marpSlideTheme = marp.themeSet.add(`
+    /*!
+     * @theme slide_preview
+     */
+    section {
+      max-width: 90%;
+    }
+`);
+marp.themeSet.default = marpSlideTheme;
+// ----------------------------------------------------
+
+type Props = {
+  options: PresentationOptions,
+  children?: string,
+}
+
+export const GrowiSlides = (props: Props): JSX.Element => {
+  const { options, children } = props;
+  const {
+    rendererOptions, isDarkMode, disableSeparationByHeader,
+  } = options;
+
+  rendererOptions.remarkPlugins?.push([
+    extractSections.remarkPlugin,
+    {
+      isDarkMode,
+      disableSeparationByHeader,
+    },
+  ]);
+
+  const { css } = marp.render('', { htmlAsArray: true });
+  return (
+    <>
+      <Head>
+        <style>{css}</style>
+      </Head>
+      <ReactMarkdown {...rendererOptions}>
+        { children ?? '## No Contents' }
+      </ReactMarkdown>
+    </>
+  );
+
+};

+ 45 - 0
packages/presentation/src/components/MarpSlides.tsx

@@ -0,0 +1,45 @@
+import { Marp } from '@marp-team/marp-core';
+import { Element } from '@marp-team/marpit';
+import Head from 'next/head';
+
+import './Slides.global.scss';
+
+const MARP_CONTAINER_CLASS_NAME = 'marpit';
+
+const marpit = new Marp({
+  container: [
+    new Element('div', { class: MARP_CONTAINER_CLASS_NAME }),
+    new Element('div', { class: 'slides' }),
+  ],
+  slideContainer: [
+    new Element('div', { class: 'shadow rounded m-2' }),
+  ],
+  inlineSVG: true,
+  emoji: undefined,
+  html: false,
+  math: false,
+});
+
+type Props = {
+  children?: string,
+}
+
+export const MarpSlides = (props: Props): JSX.Element => {
+  const { children } = props;
+
+  const { html, css } = marpit.render(children ?? '');
+  return (
+    <>
+      <Head>
+        <style>{css}</style>
+      </Head>
+      <div
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{
+          // DOMpurify.sanitize delete elements in <svg> so sanitize is not used here.
+          __html: html,
+        }}
+      />
+    </>
+  );
+};

+ 7 - 60
packages/presentation/src/components/Slides.tsx

@@ -1,41 +1,13 @@
-import React from 'react';
-
-import { Marp } from '@marp-team/marp-core';
-import { Element } from '@marp-team/marpit';
-import Head from 'next/head';
-import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
-
 import type { PresentationOptions } from '../consts';
-import * as extractSections from '../services/renderer/extract-sections';
-
-import './Slides.global.scss';
-
-export const MARP_CONTAINER_CLASS_NAME = 'marpit';
 
+import { GrowiSlides } from './GrowiSlides';
+import { MarpSlides } from './MarpSlides';
 
-const marp = new Marp({
-  container: [
-    new Element('div', { class: MARP_CONTAINER_CLASS_NAME }),
-    new Element('div', { class: 'slides' }),
-  ],
-  inlineSVG: false,
-  emoji: undefined,
-  html: false,
-  math: false,
-});
+import './Slides.global.scss';
 
-// TODO: to change better slide style
+// TODO: to remove MARP_CONTAINER_CLASS_NAME
 // https://redmine.weseek.co.jp/issues/125680
-const marpSlideTheme = marp.themeSet.add(`
-    /*!
-     * @theme slide_preview
-     */
-    section {
-      max-width: 90%;
-    }
-`);
-marp.themeSet.default = marpSlideTheme;
-
+export const MARP_CONTAINER_CLASS_NAME = 'marpit';
 
 type Props = {
   options: PresentationOptions,
@@ -45,36 +17,11 @@ type Props = {
 
 export const Slides = (props: Props): JSX.Element => {
   const { options, children, hasMarpFlag } = props;
-  const {
-    rendererOptions, isDarkMode, disableSeparationByHeader,
-  } = options;
-
 
-  // TODO: can Marp rendering
-  // https://redmine.weseek.co.jp/issues/115673
   if (hasMarpFlag) {
-    return (
-      <></>
-    );
+    return <MarpSlides>{children}</MarpSlides>;
   }
 
-  rendererOptions.remarkPlugins?.push([
-    extractSections.remarkPlugin,
-    {
-      isDarkMode,
-      disableSeparationByHeader,
-    },
-  ]);
+  return <GrowiSlides options={options}>{children}</GrowiSlides>;
 
-  const { css } = marp.render('', { htmlAsArray: true });
-  return (
-    <>
-      <Head>
-        <style>{css}</style>
-      </Head>
-      <ReactMarkdown {...rendererOptions}>
-        { children ?? '## No Contents' }
-      </ReactMarkdown>
-    </>
-  );
 };

+ 11 - 3
packages/presentation/src/services/renderer/slides.ts

@@ -9,7 +9,7 @@ import { visit } from 'unist-util-visit';
 
 const SUPPORTED_ATTRIBUTES = ['children', 'marp'];
 
-const rewriteNode = (tree: Node, node: Node) => {
+const rewriteNode = (tree: Node, node: Node, isEnabledMarp: boolean) => {
   let slide = false;
   let marp = false;
 
@@ -26,6 +26,10 @@ const rewriteNode = (tree: Node, node: Node) => {
     }
   });
 
+  if (isEnabledMarp === false) {
+    marp = false;
+  }
+
   if (marp || slide) {
 
     visit(tree, (node) => {
@@ -74,11 +78,15 @@ const rewriteNode = (tree: Node, node: Node) => {
   }
 };
 
-export const remarkPlugin: Plugin = function() {
+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);
+        rewriteNode(tree, node, options.isEnabledMarp);
       }
     });
   };