Browse Source

Merge pull request #7367 from weseek/feat/presentation

feat: Presentation
Yuki Takei 3 years ago
parent
commit
bf353fd3c7
47 changed files with 749 additions and 1067 deletions
  1. 1 0
      .github/workflows/ci-app-prod.yml
  2. 1 0
      .github/workflows/ci-app.yml
  3. 1 0
      packages/app/docker/Dockerfile
  4. 3 3
      packages/app/package.json
  5. 0 13
      packages/app/public/static/locales/en_US/admin.json
  6. 0 1
      packages/app/public/static/locales/en_US/translation.json
  7. 0 13
      packages/app/public/static/locales/ja_JP/admin.json
  8. 0 1
      packages/app/public/static/locales/ja_JP/translation.json
  9. 0 13
      packages/app/public/static/locales/zh_CN/admin.json
  10. 0 1
      packages/app/public/static/locales/zh_CN/translation.json
  11. 0 42
      packages/app/src/client/legacy/crowi-presentation.js
  12. 0 35
      packages/app/src/client/services/AdminMarkDownContainer.js
  13. 0 107
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  14. 0 379
      packages/app/src/client/util/reveal/plugins/markdown.js
  15. 0 8
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  16. 0 143
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  17. 10 15
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  18. 0 30
      packages/app/src/components/PagePresentationModal.jsx
  19. 25 8
      packages/app/src/components/PagePresentationModal.module.scss
  20. 84 0
      packages/app/src/components/PagePresentationModal.tsx
  21. 36 0
      packages/app/src/migrations/20230213090921-remove-presentation-configurations.js
  22. 0 4
      packages/app/src/server/models/config.ts
  23. 0 58
      packages/app/src/server/routes/apiv3/markdown-setting.js
  24. 14 1
      packages/app/src/services/renderer/renderer.tsx
  25. 3 4
      packages/app/src/stores/modal.tsx
  26. 20 1
      packages/app/src/stores/renderer.tsx
  27. 0 5
      packages/app/src/styles/_layout.scss
  28. 11 8
      packages/app/src/styles/_modal.scss
  29. 0 13
      packages/app/src/styles/_vendor-presentation.scss
  30. 0 148
      packages/app/src/styles/style-presentation.scss
  31. 1 0
      packages/presentation/.eslintignore
  32. 18 0
      packages/presentation/.eslintrc.js
  33. 1 0
      packages/presentation/.gitignore
  34. 3 0
      packages/presentation/README.md
  35. 34 0
      packages/presentation/package.json
  36. 13 0
      packages/presentation/src/components/Presentation.global.scss
  37. 22 0
      packages/presentation/src/components/Presentation.module.scss
  38. 66 0
      packages/presentation/src/components/Presentation.tsx
  39. 4 0
      packages/presentation/src/components/Slides.global.scss
  40. 51 0
      packages/presentation/src/components/Slides.tsx
  41. 8 0
      packages/presentation/src/consts/index.ts
  42. 3 0
      packages/presentation/src/index.ts
  43. 72 0
      packages/presentation/src/services/renderer/extract-sections.ts
  44. 12 0
      packages/presentation/tsconfig.base.json
  45. 16 0
      packages/presentation/tsconfig.build.json
  46. 10 0
      packages/presentation/tsconfig.json
  47. 206 13
      yarn.lock

+ 1 - 0
.github/workflows/ci-app-prod.yml

@@ -15,6 +15,7 @@ on:
       - packages/codemirror-textlint/**
       - packages/core/**
       - packages/preset-themes/**
+      - packages/presentation/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**

+ 1 - 0
.github/workflows/ci-app.yml

@@ -15,6 +15,7 @@ on:
       - packages/codemirror-textlint/**
       - packages/core/**
       - packages/preset-themes/**
+      - packages/presentation/**
       - packages/remark-*/**
       - packages/slack/**
       - packages/ui/**

+ 1 - 0
packages/app/docker/Dockerfile

@@ -99,6 +99,7 @@ COPY packages/codemirror-textlint packages/codemirror-textlint
 COPY packages/preset-themes packages/preset-themes
 COPY packages/slack packages/slack
 COPY packages/hackmd packages/hackmd
+COPY packages/presentation packages/presentation
 COPY packages/remark-drawio packages/remark-drawio
 COPY packages/remark-growi-directive packages/remark-growi-directive
 COPY packages/remark-lsx packages/remark-lsx

+ 3 - 3
packages/app/package.json

@@ -97,8 +97,8 @@
     "cookie-parser": "^1.4.5",
     "csurf": "^1.11.0",
     "csv-to-markdown-table": "^1.1.0",
-    "dayjs": "^1.11.7",
     "date-fns": "^2.23.0",
+    "dayjs": "^1.11.7",
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",
     "diff_match_patch": "^0.1.1",
@@ -203,6 +203,7 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
+    "@growi/presentation": "^6.0.6-RC.0",
     "@growi/ui": "^6.0.6-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
@@ -239,11 +240,10 @@
     "react-hotkeys": "^2.0.0",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
-    "reveal.js": "^4.3.1",
     "sass": "^1.53.0",
-    "simplebar-react": "^2.3.6",
     "simple-line-icons": "^2.5.5",
     "simple-load-script": "^1.0.2",
+    "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",

+ 0 - 13
packages/app/public/static/locales/en_US/admin.json

@@ -408,19 +408,6 @@
       "disallow_indent_change": "Disallow change of indent size by users",
       "disallow_indent_change_desc": "Force users to use ther default indent size."
     },
-    "presentation_header": "Presentation setting",
-    "presentation_desc": "You can change presentation settings.",
-    "presentation_options": {
-      "page_break_setting": "Page break setting",
-      "preset_one_separator": "Preset 1",
-      "preset_one_separator_desc": "3 Blank lines",
-      "preset_one_separator_value": "\\n\\n\\n",
-      "preset_two_separator": "Preset 2",
-      "preset_two_separator_desc": "5 Hyphens",
-      "preset_two_separator_value": "-----",
-      "custom_separator": "Custom",
-      "custom_separator_desc": "Regular expression"
-    },
     "xss_header": "Prevent XSS(cross site scripting) setting",
     "xss_desc": "You can change the handling of HTML tags in markdown text.",
     "xss_options": {

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

@@ -58,7 +58,6 @@
   "attachment_data": "Attachment Data",
   "No_attachments_yet": "No attachments yet.",
   "Presentation Mode": "Presentation",
-  "The end": "The end",
   "Not available for guest": "Not available for guest",
   "Not available in this version": "Not available in this version",
   "No users have liked this yet": "No users have liked this yet",

+ 0 - 13
packages/app/public/static/locales/ja_JP/admin.json

@@ -416,19 +416,6 @@
       "disallow_indent_change": "ユーザによるインデント幅変更を許可しない",
       "disallow_indent_change_desc": "ユーザにデフォルトのインデント幅の使用を強制します。"
     },
-    "presentation_header": "プレゼンテーション設定",
-    "presentation_desc": "プレゼンテーションの設定を変更できます。",
-    "presentation_options": {
-      "page_break_setting": "改頁を設定する",
-      "preset_one_separator": "プリセット 1",
-      "preset_one_separator_desc": "連続した空行3行で改頁します",
-      "preset_one_separator_value": "\\n\\n\\n",
-      "preset_two_separator": "プリセット 2",
-      "preset_two_separator_desc": "連続したハイフン5つで改頁します",
-      "preset_two_separator_value": "-----",
-      "custom_separator": "カスタム",
-      "custom_separator_desc": "正規表現を設定できます"
-    },
     "xss_header": "XSS(Cross Site Scripting)対策設定",
     "xss_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
     "xss_options": {

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

@@ -55,7 +55,6 @@
   "attachment_data": "添付データ",
   "No_attachments_yet": "No attachments yet.",
   "Presentation Mode": "プレゼンテーション",
-  "The end": "おしまい",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available in this version": "このバージョンでは利用できません",
   "No users have liked this yet": "いいねをしているユーザーはいません",

+ 0 - 13
packages/app/public/static/locales/zh_CN/admin.json

@@ -416,19 +416,6 @@
       "disallow_indent_change": "不允许用户更改缩进值",
       "disallow_indent_change_desc": "您可以不允许用户更改缩进值。"
     },
-    "presentation_header": "演示文稿设置",
-    "presentation_desc": "您可以更改演示文稿设置。",
-    "presentation_options": {
-      "page_break_setting": "分页设置",
-      "preset_one_separator": "预设 1",
-      "preset_one_separator_desc": "3 空行",
-      "preset_one_separator_value": "\\n\\n\\n",
-      "preset_two_separator": "预设 2",
-      "preset_two_separator_desc": "5 连字符",
-      "preset_two_separator_value": "-----",
-      "custom_separator": "自定义",
-      "custom_separator_desc": "正则表达式"
-    },
     "xss_header": "阻止XSS(跨站点脚本)设置",
     "xss_desc": "您可以更改标记文本中HTML标记的处理方式。",
     "xss_options": {

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

@@ -55,7 +55,6 @@
   "attachment_data": "Attachment Data",
   "No_attachments_yet": "暂无附件",
 	"Presentation Mode": "演示文稿",
-  "The end": "结束",
   "Not available for guest": "不提供给客人",
   "Not available in this version": "此版本中不提供",
   "No users have liked this yet": "还没有用户喜欢这个",

+ 0 - 42
packages/app/src/client/legacy/crowi-presentation.js

@@ -1,42 +0,0 @@
-import Reveal from 'reveal.js';
-
-Reveal.initialize({
-  controls: true,
-  progress: true,
-  history: true,
-  center: true,
-  transition: 'slide',
-
-  // // This specification method can't be used
-  // //   sice deleting symlink prevented `src` from being resolved -- 2017.06.15 Yuki Takei
-  //
-  // Optional libraries used to extend on reveal.js
-  // dependencies: [
-  //   { src: 'lib/js/classList.js', condition: function() { return !document.body.classList; } },
-  //   { src: 'plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
-  //   { src: 'plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
-  //   { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } },
-  //   { src: 'plugin/zoom-js/zoom.js', async: true, condition: function() { return !!document.body.classList; } },
-  //   { src: 'plugin/notes/notes.js', async: true, condition: function() { return !!document.body.classList; } }
-  // ]
-});
-
-require.ensure([], () => {
-  require('reveal.js/plugin/zoom/zoom');
-  require('reveal.js/plugin/notes/notes');
-  require('../util/reveal/plugins/growi-renderer');
-
-  // fix https://github.com/weseek/crowi-plus/issues/96
-  Reveal.slide(0, 0);
-  Reveal.sync();
-});
-
-Reveal.addEventListener('ready', () => {
-  // event.currentSlide, event.indexh, event.indexv
-  $('.reveal section').each(function() {
-    const $self = $(this);
-    if ($self.children().length !== 1) {
-      $self.addClass('only');
-    }
-  });
-});

+ 0 - 35
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -24,8 +24,6 @@ export default class AdminMarkDownContainer extends Container {
       isEnabledLinebreaksInComments: false,
       adminPreferredIndentSize: 4,
       isIndentSizeForced: false,
-      pageBreakSeparator: 1,
-      pageBreakCustomSeparator: '',
       isEnabledXss: false,
       xssOption: '',
       tagWhiteList: '',
@@ -55,8 +53,6 @@ export default class AdminMarkDownContainer extends Container {
       isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
       adminPreferredIndentSize: markdownParams.adminPreferredIndentSize,
       isIndentSizeForced: markdownParams.isIndentSizeForced,
-      pageBreakSeparator: markdownParams.pageBreakSeparator,
-      pageBreakCustomSeparator: markdownParams.pageBreakCustomSeparator || '',
       isEnabledXss: markdownParams.isEnabledXss,
       xssOption: markdownParams.xssOption,
       tagWhiteList: markdownParams.tagWhiteList || '',
@@ -68,20 +64,6 @@ export default class AdminMarkDownContainer extends Container {
     this.setState({ adminPreferredIndentSize });
   }
 
-  /**
-   * Switch PageBreakSeparator
-   */
-  switchPageBreakSeparator(pageBreakSeparator) {
-    this.setState({ pageBreakSeparator });
-  }
-
-  /**
-   * Set PageBreakCustomSeparator
-   */
-  setPageBreakCustomSeparator(pageBreakCustomSeparator) {
-    this.setState({ pageBreakCustomSeparator });
-  }
-
   /**
    * Switch enableXss
    */
@@ -140,21 +122,4 @@ export default class AdminMarkDownContainer extends Container {
     });
   }
 
-  /**
-   * Update Presentation Setting
-   */
-  async updatePresentationSetting() {
-
-    const response = await apiv3Put('/markdown-setting/presentation', {
-      pageBreakSeparator: this.state.pageBreakSeparator,
-      pageBreakCustomSeparator: this.state.pageBreakCustomSeparator,
-    });
-
-    this.setState({
-      pageBreakSeparator: response.data.presentationParams.pageBreakSeparator,
-      pageBreakCustomSeparator: response.data.presentationParams.pageBreakCustomSeparator,
-    });
-    return response;
-  }
-
 }

+ 0 - 107
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -1,107 +0,0 @@
-/**
- * reveal.js growi-renderer plugin.
- */
-(function(root, factory) {
-  const growiRendererPlugin = factory();
-  growiRendererPlugin.initialize();
-}(this, () => {
-  /* eslint-disable no-useless-escape */
-  const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$';
-  const DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$';
-  const DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
-  /* eslint-enable no-useless-escape */
-
-  const growiRenderer = window.parent.previewRenderer;
-
-  let marked;
-
-  /**
-   * Add data separator before lines
-   * starting with '#' to markdown.
-   */
-  function divideSlides() {
-    const sections = document.querySelectorAll('[data-markdown]');
-    for (let i = 0, len = sections.length; i < len; i++) {
-      const section = sections[i];
-      const markdown = marked.getMarkdownFromSlide(section);
-      const context = { markdown };
-      const { interceptorManager } = window.parent;
-      let dataSeparator = section.getAttribute('data-separator') || DEFAULT_SLIDE_SEPARATOR;
-      // replace string '\n' to LF code.
-      dataSeparator = dataSeparator.replace(/\\n/g, '\n');
-      const replaceValue = `${dataSeparator}#`;
-      // detach code block.
-      interceptorManager.process('prePreProcess', context);
-      // if there is only '\n' in the first line, replace it.
-      context.markdown = context.markdown.replace(/^\n/, '');
-      // add data separator to markdown.
-      context.markdown = context.markdown.replace(/[\n]+#/g, replaceValue);
-      // restore code block.
-      interceptorManager.process('postPreProcess', context);
-      section.innerHTML = marked.createMarkdownSlide(context.markdown);
-    }
-  }
-
-  /**
-   * Converts data-markdown slides to HTML slides by GrowiRenderer.
-   */
-  function convertSlides() {
-    const sections = document.querySelectorAll('[data-markdown]');
-    let markdown;
-    const { interceptorManager } = window.parent;
-
-    for (let i = 0, len = sections.length; i < len; i++) {
-      const section = sections[i];
-
-      // Only parse the same slide once
-      if (!section.getAttribute('data-markdown-parsed')) {
-        section.setAttribute('data-markdown-parsed', 'true');
-        const notes = section.querySelector('aside.notes');
-        markdown = marked.getMarkdownFromSlide(section);
-        const context = { markdown, currentPathname: decodeURIComponent(window.parent.location.pathname) };
-
-        interceptorManager.process('preRender', context)
-          .then(() => { return interceptorManager.process('prePreProcess', context) })
-          .then(() => {
-            context.markdown = growiRenderer.preProcess(context.markdown, context);
-          })
-          .then(() => { return interceptorManager.process('postPreProcess', context) })
-          .then(() => {
-            context.parsedHTML = growiRenderer.process(context.markdown, context);
-          })
-          .then(() => { return interceptorManager.process('prePostProcess', context) })
-          .then(() => {
-            context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
-          })
-          .then(() => { return interceptorManager.process('postPostProcess', context) })
-          .then(() => { return interceptorManager.process('preRenderHtml', context) })
-          .then(() => { return interceptorManager.process('postRenderHtml', context) })
-          .then(() => {
-            section.innerHTML = context.parsedHTML;
-          });
-        marked.addAttributes(section, section, null, section.getAttribute('data-element-attributes')
-          || section.parentNode.getAttribute('data-element-attributes')
-          || DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
-        section.getAttribute('data-attributes')
-          || section.parentNode.getAttribute('data-attributes')
-          || DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
-
-        // If there were notes, we need to re-add them after
-        // having overwritten the section's HTML
-        if (notes) {
-          section.appendChild(notes);
-        }
-      }
-    }
-  }
-
-  // API
-  return {
-    async initialize() {
-      marked = require('./markdown').default(growiRenderer.process);
-      divideSlides();
-      marked.processSlides();
-      convertSlides();
-    },
-  };
-}));

+ 0 - 379
packages/app/src/client/util/reveal/plugins/markdown.js

@@ -1,379 +0,0 @@
-/**
- * The reveal.js markdown plugin. Handles parsing of
- * markdown inside of presentations as well as loading
- * of external markdown documents.
- * Referred from The reveal.js markdown plugin.
- * https://github.com/hakimel/reveal.js/blob/master/plugin/markdown/markdown.js
- */
-export default function( marked ) {
-
-  const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$',
-    DEFAULT_NOTES_SEPARATOR = 'notes?:',
-    DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$',
-    DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
-
-  const SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__';
-
-
-  /**
-   * Retrieves the markdown contents of a slide section
-   * element. Normalizes leading tabs/whitespace.
-   */
-  function getMarkdownFromSlide( section ) {
-
-    // look for a <script> or <textarea data-template> wrapper
-    let template = section.querySelector( '[data-template]' ) || section.querySelector( 'script' );
-
-    // strip leading whitespace so it isn't evaluated as code
-    let text = ( template || section ).textContent;
-
-    // restore script end tags
-    text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' );
-
-    let leadingWs = text.match( /^\n?(\s*)/ )[1].length,
-      leadingTabs = text.match( /^\n?(\t*)/ )[1].length;
-
-    if ( leadingTabs > 0 ) {
-      text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}', 'g'), '\n' );
-    }
-    else if ( leadingWs > 1 ) {
-      text = text.replace( new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n' );
-    }
-
-    return text;
-
-  }
-
-  /**
-   * Given a markdown slide section element, this will
-   * return all arguments that aren't related to markdown
-   * parsing. Used to forward any other user-defined arguments
-   * to the output markdown slide.
-   */
-  function getForwardedAttributes( section ) {
-
-    let attributes = section.attributes;
-    let result = [];
-
-    for ( let i = 0, len = attributes.length; i < len; i++ ) {
-      let name = attributes[i].name,
-        value = attributes[i].value;
-
-      // disregard attributes that are used for markdown loading/parsing
-      if ( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue;
-
-      if ( value ) {
-        result.push( name + '="' + value + '"' );
-      }
-      else {
-        result.push( name );
-      }
-    }
-
-    return result.join( ' ' );
-
-  }
-
-  /**
-   * Inspects the given options and fills out default
-   * values for what's not defined.
-   */
-  function getSlidifyOptions( options ) {
-
-    options = options || {};
-    options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR;
-    options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR;
-    options.attributes = options.attributes || '';
-
-    return options;
-
-  }
-
-  /**
-   * Helper function for constructing a markdown slide.
-   */
-  function createMarkdownSlide( content, options ) {
-
-    options = getSlidifyOptions( options );
-
-    let notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) );
-
-    if ( notesMatch.length === 2 ) {
-      content = notesMatch[0] + '<aside class="notes">' + marked(notesMatch[1].trim()) + '</aside>';
-    }
-
-    // prevent script end tags in the content from interfering
-    // with parsing
-    content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER );
-
-    return '<script type="text/template">' + content + '</script>';
-
-  }
-
-  /**
-   * Parses a data string into multiple slides based
-   * on the passed in separator arguments.
-   */
-  function slidify( markdown, options ) {
-
-    options = getSlidifyOptions( options );
-
-    let separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ),
-      horizontalSeparatorRegex = new RegExp( options.separator );
-
-    let matches,
-      lastIndex = 0,
-      isHorizontal,
-      wasHorizontal = true,
-      content,
-      sectionStack = [];
-
-    // iterate until all blocks between separators are stacked up
-    while ( (matches = separatorRegex.exec( markdown )) != null ) {
-      // notes = null;
-
-      // determine direction (horizontal by default)
-      isHorizontal = horizontalSeparatorRegex.test( matches[0] );
-
-      if ( !isHorizontal && wasHorizontal ) {
-        // create vertical stack
-        sectionStack.push( [] );
-      }
-
-      // pluck slide content from markdown input
-      content = markdown.substring( lastIndex, matches.index );
-
-      if ( isHorizontal && wasHorizontal ) {
-        // add to horizontal stack
-        sectionStack.push( content );
-      }
-      else {
-        // add to vertical stack
-        sectionStack[sectionStack.length-1].push( content );
-      }
-
-      lastIndex = separatorRegex.lastIndex;
-      wasHorizontal = isHorizontal;
-    }
-
-    // add the remaining slide
-    ( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) );
-
-    let markdownSections = '';
-
-    // flatten the hierarchical stack, and insert <section data-markdown> tags
-    for ( let i = 0, len = sectionStack.length; i < len; i++ ) {
-      // vertical
-      if ( sectionStack[i] instanceof Array ) {
-        markdownSections += '<section '+ options.attributes +'>';
-
-        sectionStack[i].forEach( function( child ) {
-          markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>';
-        } );
-
-        markdownSections += '</section>';
-      }
-      else {
-        markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>';
-      }
-    }
-
-    return markdownSections;
-
-  }
-
-  /**
-   * Parses any current data-markdown slides, splits
-   * multi-slide markdown into separate sections and
-   * handles loading of external markdown.
-   */
-  function processSlides() {
-
-    let sections = document.querySelectorAll( '[data-markdown]'),
-      section;
-
-    for ( let i = 0, len = sections.length; i < len; i++ ) {
-
-      section = sections[i];
-
-      if ( section.getAttribute( 'data-markdown' ).length ) {
-
-        let xhr = new XMLHttpRequest(),
-          url = section.getAttribute( 'data-markdown' );
-
-        let datacharset = section.getAttribute( 'data-charset' );
-
-        // see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
-        if ( datacharset != null && datacharset != '' ) {
-          xhr.overrideMimeType( 'text/html; charset=' + datacharset );
-        }
-
-        xhr.onreadystatechange = function() {
-          if ( xhr.readyState === 4 ) {
-            // file protocol yields status code 0 (useful for local debug, mobile applications etc.)
-            if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) {
-
-              section.outerHTML = slidify( xhr.responseText, {
-                separator: section.getAttribute( 'data-separator' ),
-                verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
-                notesSeparator: section.getAttribute( 'data-separator-notes' ),
-                attributes: getForwardedAttributes( section )
-              });
-
-            }
-            else {
-
-              section.outerHTML = '<section data-state="alert">' +
-                'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
-                'Check your browser\'s JavaScript console for more details.' +
-                '<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
-                '</section>';
-
-            }
-          }
-        };
-
-        xhr.open( 'GET', url, false );
-
-        try {
-          xhr.send();
-        }
-        catch ( e ) {
-          alert( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e );
-        }
-
-      }
-      else if ( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) {
-
-        section.outerHTML = slidify( getMarkdownFromSlide( section ), {
-          separator: section.getAttribute( 'data-separator' ),
-          verticalSeparator: section.getAttribute( 'data-separator-vertical' ),
-          notesSeparator: section.getAttribute( 'data-separator-notes' ),
-          attributes: getForwardedAttributes( section )
-        });
-
-      }
-      else {
-        section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) );
-      }
-    }
-
-  }
-
-  /**
-   * Check if a node value has the attributes pattern.
-   * If yes, extract it and add that value as one or several attributes
-   * the the terget element.
-   *
-   * You need Cache Killer on Chrome to see the effect on any FOM transformation
-   * directly on refresh (F5)
-   * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277
-   */
-  function addAttributeInElement( node, elementTarget, separator ) {
-
-    let mardownClassesInElementsRegex = new RegExp( separator, 'mg' );
-    let mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"=]+?)\"", 'mg' );
-    let nodeValue = node.nodeValue;
-    let matches = mardownClassesInElementsRegex.exec( nodeValue );
-    if ( matches != null ) {
-
-      let classes = matches[1];
-      nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( mardownClassesInElementsRegex.lastIndex );
-      node.nodeValue = nodeValue;
-      let matchesClass;
-      while ( (matchesClass = mardownClassRegex.exec( classes )) != null ) {
-        elementTarget.setAttribute( matchesClass[1], matchesClass[2] );
-      }
-      return true;
-    }
-    return false;
-  }
-
-  /**
-   * Add attributes to the parent element of a text node,
-   * or the element of an attribute node.
-   */
-  function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) {
-
-    if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) {
-      let previousParentElement = element;
-      for ( let i = 0; i < element.childNodes.length; i++ ) {
-        let childElement = element.childNodes[i];
-        if ( i > 0 ) {
-          let j = i - 1;
-          while ( j >= 0 ) {
-            let aPreviousChildElement = element.childNodes[j];
-            if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != 'BR' ) {
-              previousParentElement = aPreviousChildElement;
-              break;
-            }
-            j = j - 1;
-          }
-        }
-        let parentSection = section;
-        if ( childElement.nodeName ==  'section' ) {
-          parentSection = childElement ;
-          previousParentElement = childElement ;
-        }
-        if ( typeof childElement.setAttribute == 'function' || childElement.nodeType == Node.COMMENT_NODE ) {
-          addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes );
-        }
-      }
-    }
-
-    if ( element.nodeType == Node.COMMENT_NODE ) {
-      if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) == false ) {
-        addAttributeInElement( element, section, separatorSectionAttributes );
-      }
-    }
-  }
-
-  /**
-   * Converts any current data-markdown slides in the
-   * DOM to HTML.
-   */
-  function convertSlides() {
-
-    let sections = document.querySelectorAll( '[data-markdown]');
-
-    for ( let i = 0, len = sections.length; i < len; i++ ) {
-
-      let section = sections[i];
-
-      // Only parse the same slide once
-      if ( !section.getAttribute( 'data-markdown-parsed' ) ) {
-
-        section.setAttribute( 'data-markdown-parsed', true );
-
-        let notes = section.querySelector( 'aside.notes' );
-        let markdown = getMarkdownFromSlide( section );
-
-        section.innerHTML = marked( markdown );
-        addAttributes(   section, section, null, section.getAttribute( 'data-element-attributes' ) ||
-                section.parentNode.getAttribute( 'data-element-attributes' ) ||
-                DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
-        section.getAttribute( 'data-attributes' ) ||
-                section.parentNode.getAttribute( 'data-attributes' ) ||
-                DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR);
-
-        // If there were notes, we need to re-add them after
-        // having overwritten the section's HTML
-        if ( notes ) {
-          section.appendChild( notes );
-        }
-
-      }
-
-    }
-
-  }
-
-  // API
-  return {
-    getMarkdownFromSlide: getMarkdownFromSlide,
-    createMarkdownSlide: createMarkdownSlide,
-    processSlides: processSlides,
-    addAttributes: addAttributes,
-    convertSlides: convertSlides
-  };
-}

+ 0 - 8
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -12,7 +12,6 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
-import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 
 const logger = loggerFactory('growi:MarkDown');
@@ -56,13 +55,6 @@ const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
       </Card>
       <IndentForm />
 
-      {/* Presentation Setting */}
-      <h2 className="admin-setting-header">{ t('markdown_settings.presentation_header') }</h2>
-      <Card className="card well my-3">
-        <CardBody className="px-0 py-2">{ t('markdown_settings.presentation_desc') }</CardBody>
-      </Card>
-      <PresentationForm />
-
       {/* XSS Setting */}
       <h2 className="admin-setting-header">{ t('markdown_settings.xss_header') }</h2>
       <Card className="card well my-3">

+ 0 - 143
packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -1,143 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:markdown:presentation');
-
-class PresentationForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t } = this.props;
-
-    try {
-      await this.props.adminMarkDownContainer.updatePresentationSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.presentation_header'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-
-  render() {
-    const { t, adminMarkDownContainer } = this.props;
-    const { pageBreakSeparator, pageBreakCustomSeparator } = adminMarkDownContainer.state;
-
-    return (
-      <fieldset className="form-group col-12 my-2">
-
-        <label className="col-8 offset-4 col-form-label font-weight-bold text-left mt-3">
-          {t('markdown_settings.presentation_options.page_break_setting')}
-        </label>
-
-        <div className="form-group col-12 my-3">
-          <div className="row">
-            <div className="col-md-4 col-sm-12 align-self-start mb-4">
-              <div className="custom-control custom-radio">
-                <input
-                  type="radio"
-                  className="custom-control-input"
-                  id="pageBreakOption1"
-                  checked={pageBreakSeparator === 1}
-                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
-                />
-                <label className="custom-control-label w-100" htmlFor="pageBreakOption1">
-                  <p className="font-weight-bold">{ t('markdown_settings.presentation_options.preset_one_separator') }</p>
-                  <div className="mt-3">
-                    { t('markdown_settings.presentation_options.preset_one_separator_desc') }
-                    <input
-                      className="form-control"
-                      type="text"
-                      value={t('markdown_settings.presentation_options.preset_one_separator_value')}
-                      readOnly
-                    />
-                  </div>
-                </label>
-              </div>
-            </div>
-
-            <div className="col-md-4 col-sm-12 align-self-start mb-4">
-              <div className="custom-control custom-radio">
-                <input
-                  type="radio"
-                  className="custom-control-input"
-                  id="pageBreakOption2"
-                  checked={pageBreakSeparator === 2}
-                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
-                />
-                <label className="custom-control-label w-100" htmlFor="pageBreakOption2">
-                  <p className="font-weight-bold">{ t('markdown_settings.presentation_options.preset_two_separator') }</p>
-                  <div className="mt-3">
-                    { t('markdown_settings.presentation_options.preset_two_separator_desc') }
-                    <input
-                      className="form-control"
-                      type="text"
-                      value={t('markdown_settings.presentation_options.preset_two_separator_value')}
-                      readOnly
-                    />
-                  </div>
-                </label>
-              </div>
-            </div>
-            <div className="col-md-4 col-sm-12 align-self-start mb-4">
-              <div className="custom-control custom-radio">
-                <input
-                  type="radio"
-                  id="pageBreakOption3"
-                  className="custom-control-input"
-                  checked={pageBreakSeparator === 3}
-                  onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
-                />
-                <label className="custom-control-label w-100" htmlFor="pageBreakOption3">
-                  <p className="font-weight-bold">{ t('markdown_settings.presentation_options.custom_separator') }</p>
-                  <div className="mt-3">
-                    { t('markdown_settings.presentation_options.custom_separator_desc') }
-                    <input
-                      className="form-control"
-                      defaultValue={pageBreakCustomSeparator}
-                      onChange={(e) => { adminMarkDownContainer.setPageBreakCustomSeparator(e.target.value) }}
-                    />
-                  </div>
-                </label>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminMarkDownContainer.state.retrieveError != null} />
-      </fieldset>
-    );
-  }
-
-}
-
-PresentationForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-
-};
-
-const PresentationFormWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <PresentationForm t={t} {...props} />;
-};
-
-const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
-
-export default PresentationFormWrapper;

+ 10 - 15
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -36,7 +36,6 @@ import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
 import { NotAvailable } from '../NotAvailable';
-import { NotAvailableForNow } from '../NotAvailableForNow';
 import { Skeleton } from '../Skeleton';
 
 import type { AuthorInfoProps } from './AuthorInfo';
@@ -85,23 +84,19 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openAccessoriesModal } = usePageAccessoriesModal();
 
-  const hrefForPresentationModal = `${pageId}/?presentation=1`;
-
   return (
     <>
       {/* Presentation */}
-      <NotAvailableForNow>
-        <DropdownItem
-          onClick={() => openPresentationModal(hrefForPresentationModal)}
-          data-testid="open-presentation-modal-btn"
-          className="grw-page-control-dropdown-item"
-        >
-          <i className="icon-fw grw-page-control-dropdown-icon">
-            <PresentationIcon />
-          </i>
-          { t('Presentation Mode') }
-        </DropdownItem>
-      </NotAvailableForNow>
+      <DropdownItem
+        onClick={() => openPresentationModal()}
+        data-testid="open-presentation-modal-btn"
+        className="grw-page-control-dropdown-item"
+      >
+        <i className="icon-fw grw-page-control-dropdown-icon">
+          <PresentationIcon />
+        </i>
+        { t('Presentation Mode') }
+      </DropdownItem>
 
       {/* Export markdown */}
       <DropdownItem

+ 0 - 30
packages/app/src/components/PagePresentationModal.jsx

@@ -1,30 +0,0 @@
-import React from 'react';
-
-import {
-  Modal, ModalBody,
-} from 'reactstrap';
-
-import { usePagePresentationModal } from '~/stores/modal';
-
-import styles from './PagePresentationModal.module.scss';
-
-const PagePresentationModal = () => {
-
-  const { data: presentationData, close: closePresentationModal } = usePagePresentationModal();
-
-  return (
-    <Modal
-      isOpen={presentationData.isOpened}
-      toggle={closePresentationModal}
-      data-testid="page-presentation-modal"
-      className={`grw-presentation-modal ${styles['grw-presentation-modal']} grw-body-only-modal-expanded`}
-      unmountOnClose={false}
-    >
-      <ModalBody className="modal-body">
-        <iframe src={presentationData.href} />
-      </ModalBody>
-    </Modal>
-  );
-};
-
-export default PagePresentationModal;

+ 25 - 8
packages/app/src/components/PagePresentationModal.module.scss

@@ -1,11 +1,28 @@
+@use '~/styles/_modal';
+
 .grw-presentation-modal :global {
-  .modal-body {
-    background: black;
-
-    iframe {
-      width: 100%;
-      height: 100%;
-      border: 0;
-    }
+
+  @include modal.expand-modal-fullscreen(false, false, 0px);
+
+  .modal-content {
+    background-color: transparent;
+    border-radius: 0;
+  }
+
+  .grw-presentation-controls {
+    position: absolute;
+    top: 1rem;
+    right: 1rem;
+    z-index: 110; // over ".reveal .slides"
+  }
+
+  .close {
+    display: inline-block;
+    width: 3rem;
+    height: 3rem;
+    font-size: 1.5rem;
+    color: var(--color-global);
+    opacity: 0.3;
   }
+
 }

+ 84 - 0
packages/app/src/components/PagePresentationModal.tsx

@@ -0,0 +1,84 @@
+import React, { useCallback } from 'react';
+
+import type { PresentationProps } from '@growi/presentation';
+import dynamic from 'next/dynamic';
+import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import {
+  Modal, ModalBody,
+} from 'reactstrap';
+
+
+import { usePagePresentationModal } from '~/stores/modal';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { usePresentationViewOptions } from '~/stores/renderer';
+import { useNextThemes } from '~/stores/use-next-themes';
+
+
+import styles from './PagePresentationModal.module.scss';
+
+
+const Presentation = dynamic<PresentationProps>(() => import('@growi/presentation').then(mod => mod.Presentation), {
+  ssr: false,
+  loading: () => (
+    <i className="fa fa-4x fa-spinner fa-pulse text-muted"></i>
+  ),
+});
+
+
+const PagePresentationModal = (): JSX.Element => {
+
+  const { data: presentationModalData, close: closePresentationModal } = usePagePresentationModal();
+
+  const { isDarkMode } = useNextThemes();
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: rendererOptions } = usePresentationViewOptions();
+
+  const requestFullscreen = useCallback(() => {
+    document.documentElement.requestFullscreen();
+  }, []);
+
+  const isOpen = presentationModalData?.isOpened ?? false;
+
+  if (!isOpen) {
+    return <></>;
+  }
+
+  const markdown = currentPage?.revision.body;
+
+  return (
+    <Modal
+      isOpen={isOpen}
+      toggle={closePresentationModal}
+      data-testid="page-presentation-modal"
+      className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
+    >
+      <div className="grw-presentation-controls d-flex">
+        <button className="close btn-fullscreen" type="button" aria-label="fullscreen" onClick={requestFullscreen}>
+          <i className="ti ti-fullscreen" aria-hidden></i>
+        </button>
+        <button className="close btn-close" type="button" aria-label="close" onClick={closePresentationModal}>
+          <i className="ti ti-close" aria-hidden></i>
+        </button>
+      </div>
+      <ModalBody className="modal-body d-flex justify-content-center align-items-center">
+        { rendererOptions != null && (
+          <Presentation
+            options={{
+              rendererOptions: rendererOptions as ReactMarkdownOptions,
+              revealOptions: {
+                embedded: true,
+                hash: true,
+              },
+              isDarkMode,
+            }}
+          >
+            {markdown}
+          </Presentation>
+        ) }
+      </ModalBody>
+    </Modal>
+  );
+};
+
+export default PagePresentationModal;

+ 36 - 0
packages/app/src/migrations/20230213090921-remove-presentation-configurations.js

@@ -0,0 +1,36 @@
+// eslint-disable-next-line import/no-named-as-default
+import Config from '~/server/models/config';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:remove-presentation-configurations');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    await Config.findOneAndDelete({ key: 'markdown:presentation:pageBreakSeparator' });
+    await Config.findOneAndDelete({ key: 'markdown:presentation:pageBreakCustomSeparator' });
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    logger.info('Rollback migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    const insertConfig = new Config({
+      ns: 'crowi',
+      key: 'markdown:presentation:pageBreakSeparator',
+      value: 2,
+    });
+
+    await insertConfig.save();
+
+    logger.info('Migration has been successfully rollbacked');
+  },
+};

+ 0 - 4
packages/app/src/server/models/config.ts

@@ -153,8 +153,6 @@ export const defaultMarkdownConfigs: { [key: string]: any } = {
   'markdown:isEnabledLinebreaksInComments': true,
   'markdown:adminPreferredIndentSize': 4,
   'markdown:isIndentSizeForced': false,
-  'markdown:presentation:pageBreakSeparator': 1,
-  'markdown:presentation:pageBreakCustomSeparator': undefined,
 };
 
 export const defaultNotificationConfigs: { [key: string]: any } = {
@@ -207,8 +205,6 @@ schema.statics.getLocalconfig = function(crowi) {
     isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
     adminPreferredIndentSize: crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
     isIndentSizeForced: crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
-    pageBreakSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
-    pageBreakCustomSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
     isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
     isEnabledTimeline: crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
     isAllReplyShown: crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),

+ 0 - 58
packages/app/src/server/routes/apiv3/markdown-setting.js

@@ -24,9 +24,6 @@ const validator = {
     body('adminPreferredIndentSize').isIn([2, 4]),
     body('isIndentSizeForced').isBoolean(),
   ],
-  presentationSetting: [
-    body('pageBreakSeparator').isInt().not().isEmpty(),
-  ],
   xssSetting: [
     body('isEnabledXss').isBoolean(),
     body('tagWhiteList').isArray(),
@@ -123,8 +120,6 @@ module.exports = (crowi) => {
       isEnabledLinebreaksInComments: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
       adminPreferredIndentSize: await crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
       isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
-      pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
-      pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
       xssOption: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
       tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
@@ -211,59 +206,6 @@ module.exports = (crowi) => {
 
   });
 
-  /**
-   * @swagger
-   *
-   *    /markdown-setting/presentation:
-   *      put:
-   *        tags: [MarkDownSetting]
-   *        operationId: updatePresentationMarkdownSetting
-   *        summary: /markdown-setting/presentation
-   *        description: Update presentation
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/PresentationParams'
-   *        responses:
-   *          200:
-   *            description: Succeeded to update presentation setting
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  $ref: '#/components/schemas/PresentationParams'
-   */
-  router.put('/presentation', loginRequiredStrictly, adminRequired, addActivity, validator.presentationSetting, apiV3FormValidator, async(req, res) => {
-    if (req.body.pageBreakSeparator === 3 && req.body.pageBreakCustomSeparator === '') {
-      return res.apiv3Err(new ErrorV3('customRegularExpression is required'));
-    }
-
-    const requestPresentationParams = {
-      'markdown:presentation:pageBreakSeparator': req.body.pageBreakSeparator,
-      'markdown:presentation:pageBreakCustomSeparator': req.body.pageBreakCustomSeparator,
-    };
-
-    try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('markdown', requestPresentationParams);
-      const presentationParams = {
-        pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
-        pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator') || '',
-      };
-
-      const parameters = { action: SupportedAction.ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return res.apiv3({ presentationParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating presentation';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-presentation-failed'));
-    }
-
-  });
-
   /**
    * @swagger
    *

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

@@ -68,7 +68,7 @@ export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehy
 };
 
 const baseSanitizeSchema = {
-  tagNames: ['iframe'],
+  tagNames: ['iframe', 'section'],
   attributes: {
     iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
     '*': ['class', 'className', 'style'],
@@ -299,6 +299,19 @@ export const generateSimpleViewOptions = (
   return options;
 };
 
+export const generatePresentationViewOptions = (
+    config: RendererConfig,
+    pagePath: string,
+): RendererOptions => {
+  // based on simple view options
+  const options = generateSimpleViewOptions(config, pagePath);
+
+  if (config.isEnabledXssPrevention) {
+    verifySanitizePlugin(options, false);
+  }
+  return options;
+};
+
 export const generateSSRViewOptions = (
     config: RendererConfig,
     pagePath: string,

+ 3 - 4
packages/app/src/stores/modal.tsx

@@ -245,11 +245,10 @@ export const usePutBackPageModal = (status?: PutBackPageModalStatus): SWRRespons
 */
 type PresentationModalStatus = {
   isOpened: boolean,
-  href?: string
 }
 
 type PresentationModalStatusUtils = {
-  open(href: string): Promise<PresentationModalStatus | undefined>
+  open(): Promise<PresentationModalStatus | undefined>
   close(): Promise<PresentationModalStatus | undefined>
 }
 
@@ -257,13 +256,13 @@ export const usePagePresentationModal = (
     status?: PresentationModalStatus,
 ): SWRResponse<PresentationModalStatus, Error> & PresentationModalStatusUtils => {
   const initialData: PresentationModalStatus = {
-    isOpened: false, href: '?presentation=1',
+    isOpened: false,
   };
   const swrResponse = useStaticSWR<PresentationModalStatus, Error>('presentationModalStatus', status, { fallbackData: initialData });
 
   return {
     ...swrResponse,
-    open: (href: string) => swrResponse.mutate({ isOpened: true, href }),
+    open: () => swrResponse.mutate({ isOpened: true }, { revalidate: true }),
     close: () => swrResponse.mutate({ isOpened: false }),
   };
 };

+ 20 - 1
packages/app/src/stores/renderer.tsx

@@ -6,7 +6,7 @@ import useSWR, { type SWRResponse } from 'swr';
 import {
   type RendererOptions,
   generateSimpleViewOptions, generatePreviewOptions,
-  generateViewOptions, generateTocOptions,
+  generateViewOptions, generateTocOptions, generatePresentationViewOptions,
 } from '~/services/renderer/renderer';
 import { getGrowiFacade } from '~/utils/growi-facade';
 
@@ -162,3 +162,22 @@ export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> =
     },
   );
 };
+
+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,
+    ([, currentPagePath, rendererConfig]) => generatePresentationViewOptions(rendererConfig, currentPagePath),
+    {
+      fallbackData: isAllDataValid ? generatePresentationViewOptions(rendererConfig, currentPagePath) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
+  );
+};

+ 0 - 5
packages/app/src/styles/_layout.scss

@@ -1,11 +1,6 @@
 @use './variables' as var;
 @use './bootstrap/init' as bs;
 
-body {
-  overflow-y: scroll !important;
-  overscroll-behavior-y: none;
-}
-
 .dynamic-layout-root:not(.growi-layout-fluid) .grw-container-convertible {
   @extend .container-lg;
 }

+ 11 - 8
packages/app/src/styles/_modal.scss

@@ -1,12 +1,15 @@
-@mixin expand-modal-fullscreen($hasModalHeader: true, $hasModalFooter: true) {
+@mixin expand-modal-fullscreen($hasModalHeader: true, $hasModalFooter: true, $margin: 1rem) {
   // full-screen modal
   width: auto;
   max-width: unset !important;
-  height: calc(100vh - 30px);
-  margin: 15px !important;
+  height: calc(100vh - $margin * 2);
+  margin: $margin !important;
 
   .modal-content {
-    height: calc(100vh - 30px);
+    height: calc(100vh - $margin * 2);
+  }
+  .modal-body {
+    padding: $margin;
   }
 
   // expand .modal-body (with calculating height)
@@ -14,14 +17,14 @@
     $modal-header: 54px;
     $modal-footer: 46px;
 
-    $margin: 0px;
+    $margin-body: 0px;
     @if $hasModalHeader {
-      $margin: $margin + $modal-header;
+      $margin-body: $margin-body + $modal-header;
     }
     @if $hasModalFooter {
-      $margin: $margin + $modal-footer;
+      $margin-body: $margin-body + $modal-footer;
     }
-    height: calc(100% - #{$margin});
+    height: calc(100% - #{$margin-body});
   }
 }
 

+ 0 - 13
packages/app/src/styles/_vendor-presentation.scss

@@ -1,13 +0,0 @@
-// import bootstrap configurations
-@import '~bootstrap/scss/functions';
-@import '~bootstrap/scss/variables';
-@import '~bootstrap/scss/mixins';
-@import '~bootstrap/scss/utilities';
-
-@import '~reveal.js/dist/reveal.css';
-@import '~reveal.js/dist/theme/black.css';
-
-// hljs
-.reveal {
-  @import 'hljs';
-}

+ 0 - 148
packages/app/src/styles/style-presentation.scss

@@ -1,148 +0,0 @@
-// import variable
-@import 'variables';
-
-@import 'mixins';
-@import 'override-bootstrap-variables';
-
-@import 'vendor-presentation';
-
-.reveal {
-  font-size: 32px;
-  section * {
-    font-family: Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif !important;
-  }
-
-  .present {
-    max-height: 100%;
-    overflow-y: scroll;
-  }
-
-  .slides > section {
-    //text-align: left;
-    padding: 0;
-
-    &.only.present {
-      h1,
-      h2,
-      h3,
-      h4,
-      h5,
-      h6 {
-        font-size: 2.5em;
-      }
-    }
-
-    p {
-      line-height: 1.6;
-
-      &:first-child {
-        margin-top: 0;
-      }
-    }
-
-    pre {
-      code {
-        padding: 20px 40px;
-      }
-    }
-    blockquote {
-      width: 80%;
-      padding: 20px 60px;
-    }
-
-    ul {
-      margin-top: 0.2em;
-      margin-bottom: 0.1em;
-      > li {
-        margin-bottom: 0.5em;
-        line-height: 1.6;
-
-        > ul > li {
-          font-size: 0.85em;
-        }
-      }
-    }
-
-    h1:first-child {
-      font-size: 2.2em;
-    }
-    h2:first-child {
-      font-size: 1.8em;
-    }
-    h3,
-    h4,
-    h5,
-    h6 {
-      &:first-child {
-        font-size: 1.5em;
-      }
-    }
-
-    // {{{ table (copied from bootstrap .table
-    table {
-      width: 100%;
-      margin-bottom: 1em;
-
-      border-collapse: collapse;
-      tr,
-      td,
-      th {
-        border-collapse: collapse;
-      }
-
-      // Cells
-      > thead,
-      > tbody,
-      > tfoot {
-        > tr {
-          > th,
-          > td {
-            padding: 1em;
-            vertical-align: top;
-            border-top: 1px solid $gray-400;
-          }
-        }
-      }
-      // Bottom align for column headings
-      > thead > tr > th {
-        vertical-align: bottom;
-        border-bottom: 2px solid $gray-500;
-      }
-      // Remove top border from thead by default
-      > caption + thead,
-      > colgroup + thead,
-      > thead:first-child {
-        > tr:first-child {
-          > th,
-          > td {
-            border-top: 0;
-          }
-        }
-      }
-      // Account for multiple tbody instances
-      > tbody + tbody {
-        border-top: 2px solid $gray-500;
-      }
-
-      // .table-bordered
-      border: 1px solid $gray-400;
-      > thead,
-      > tbody,
-      > tfoot {
-        > tr {
-          > th,
-          > td {
-            border: 1px solid $gray-400;
-          }
-        }
-      }
-      > thead > tr {
-        > th,
-        > td {
-          border-bottom-width: 2px;
-        }
-      }
-    }
-    // }}}
-  }
-}

+ 1 - 0
packages/presentation/.eslintignore

@@ -0,0 +1 @@
+/dist/**

+ 18 - 0
packages/presentation/.eslintrc.js

@@ -0,0 +1,18 @@
+module.exports = {
+  extends: [
+    'weseek/react',
+    'weseek/typescript',
+  ],
+  env: {
+  },
+  globals: {
+  },
+  settings: {
+    // resolve path aliases by eslint-import-resolver-typescript
+    'import/resolver': {
+      typescript: {},
+    },
+  },
+  rules: {
+  },
+};

+ 1 - 0
packages/presentation/.gitignore

@@ -0,0 +1 @@
+/dist

+ 3 - 0
packages/presentation/README.md

@@ -0,0 +1,3 @@
+# presentation
+
+[GROWI][growi] sub-package for presentation

+ 34 - 0
packages/presentation/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "@growi/presentation",
+  "version": "6.0.6-RC.0",
+  "description": "GROWI plugin for presentation",
+  "license": "MIT",
+  "keywords": [
+    "growi",
+    "growi-plugin"
+  ],
+  "main": "dist/index.js",
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
+    "clean": "npx -y shx rm -rf dist",
+    "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
+    "lint:styles": "stylelint --allow-empty-input src/**/*.scss src/**/*.css",
+    "lint": "run-p lint:*",
+    "test": ""
+  },
+  "dependencies": {
+    "@growi/core": "^6.0.6-RC.0"
+  },
+  "devDependencies": {
+    "@marp-team/marp-core": "^3.4.2",
+    "@types/reveal.js": "^4.4.1",
+    "eslint-plugin-regex": "^1.8.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-markdown": "^8.0.3",
+    "reveal.js": "^4.4.0"
+  }
+}

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

@@ -0,0 +1,13 @@
+:root[data-theme='light'] {
+  .reveal-viewport {
+    // adjust marp default theme
+    background-color: #fff;
+  }
+}
+
+:root[data-theme='dark'] {
+  .reveal-viewport {
+    // adjust marp default theme
+    background-color: #0d1117;
+  }
+}

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

@@ -0,0 +1,22 @@
+.grw-presentation {
+
+  // workaround from https://github.com/css-modules/css-modules/issues/295#issuecomment-952885628
+  &:global(.reveal) :global {
+    .slides {
+      text-align: unset;
+
+      section {
+        position: absolute;
+        width: 100%;
+      }
+    }
+    .controls {
+      bottom: 24px;
+      color: var(--color-global);
+    }
+    .progress {
+      color: var(--accent);
+    }
+  }
+
+}

+ 66 - 0
packages/presentation/src/components/Presentation.tsx

@@ -0,0 +1,66 @@
+import React, { useEffect } from 'react';
+
+import Reveal from 'reveal.js';
+
+import type { PresentationOptions } from '../consts';
+
+import { MARP_CONTAINER_CLASS_NAME, Slides } from './Slides';
+
+import 'reveal.js/dist/reveal.css';
+import './Presentation.global.scss';
+
+import styles from './Presentation.module.scss';
+
+
+const baseRevealOptions: Reveal.Options = {
+  // adjust size to the marp preset size
+  width: 1280,
+  height: 720,
+  maxScale: 1.2,
+  slideNumber: 'c/t',
+};
+
+/**
+ * Remove all [hidden] in order to activate transitions
+ *   cz: All of .past and .future elements are hidden by `display: none !important`
+ * @see https://getbootstrap.com/docs/4.6/content/reboot/#html5-hidden-attribute
+ */
+const removeAllHiddenElements = () => {
+  const sections = document.querySelectorAll('.grw-presentation section');
+  sections.forEach(section => section.removeAttribute('hidden'));
+};
+
+export type PresentationProps = {
+  options: PresentationOptions,
+  children?: string,
+}
+
+export const Presentation = (props: PresentationProps): JSX.Element => {
+  const { options, children } = props;
+  const { revealOptions } = options;
+
+  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);
+    }
+
+    return function cleanup() {
+      deck?.off('ready', removeAllHiddenElements);
+      deck?.off('slidechanged', removeAllHiddenElements);
+    };
+  }, [children, revealOptions]);
+
+  return (
+    <div className={`grw-presentation ${styles['grw-presentation']} reveal ${MARP_CONTAINER_CLASS_NAME}`}>
+      <div className="slides">
+        <Slides options={options}>{children}</Slides>
+      </div>
+    </div>
+  );
+};

+ 4 - 0
packages/presentation/src/components/Slides.global.scss

@@ -0,0 +1,4 @@
+div.marpit > div.slides > section :is(pre, marp-pre) {
+  padding: 0;
+  border: none;
+}

+ 51 - 0
packages/presentation/src/components/Slides.tsx

@@ -0,0 +1,51 @@
+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';
+
+
+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,
+});
+
+
+type Props = {
+  options: PresentationOptions,
+  children?: string,
+}
+
+export const Slides = (props: Props): JSX.Element => {
+  const { options, children } = props;
+  const { rendererOptions, isDarkMode } = options;
+
+  rendererOptions.remarkPlugins?.push([extractSections.remarkPlugin, { isDarkMode }]);
+
+  const { css } = marp.render('', { htmlAsArray: true });
+
+  return (
+    <>
+      <Head>
+        <style>{css}</style>
+      </Head>
+      <ReactMarkdown {...rendererOptions}>
+        { children ?? '## No Contents' }
+      </ReactMarkdown>
+    </>
+  );
+};

+ 8 - 0
packages/presentation/src/consts/index.ts

@@ -0,0 +1,8 @@
+import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Options as RevealOptions } from 'reveal.js';
+
+export type PresentationOptions = {
+  rendererOptions: ReactMarkdownOptions,
+  revealOptions?: RevealOptions,
+  isDarkMode?: boolean,
+}

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

@@ -0,0 +1,3 @@
+export * from './components/Presentation';
+export * from './components/Slides';
+export * from './services/renderer/extract-sections';

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

@@ -0,0 +1,72 @@
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { Plugin } from 'unified';
+import type { Parent, Node } from 'unist';
+import { findAfter } from 'unist-util-find-after';
+import { visit } from 'unist-util-visit';
+
+
+function wrapWithSection(parentNode: Parent, startElem: Node, endElem: Node | null, isDarkMode?: boolean): void {
+  const siblings = parentNode.children;
+
+  const startIndex = siblings.indexOf(startElem);
+  const endIndex = endElem != null ? siblings.indexOf(endElem) : undefined;
+
+  const between = siblings.slice(
+    startIndex,
+    endIndex,
+  );
+
+  const section = {
+    type: 'section',
+    children: between,
+    data: {
+      hName: 'section',
+      hProperties: {
+        className: isDarkMode ? 'invert' : '',
+      },
+    },
+  };
+
+  siblings.splice(startIndex, between.length, section);
+}
+
+function removeElement(parentNode: Parent, elem: Node): void {
+  const siblings = parentNode.children;
+  siblings.splice(siblings.indexOf(elem), 1);
+}
+
+export type ExtractSectionsPluginParams = {
+  isDarkMode?: boolean,
+}
+
+export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) => {
+  const { isDarkMode } = options;
+
+  return (tree) => {
+    // wrap with <section>
+    visit(
+      tree,
+      node => node.type !== 'thematicBreak',
+      (node, index, parent: Parent) => {
+        if (parent == null || parent.type !== 'root') {
+          return;
+        }
+
+        const startElem = node;
+        const endElem = findAfter(parent, startElem, node => node.type === 'thematicBreak');
+
+        wrapWithSection(parent, startElem, endElem, isDarkMode);
+
+        // remove <hr>
+        if (endElem != null) {
+          removeElement(parent, endElem);
+        }
+      },
+    );
+  };
+};
+
+
+export const sanitizeOption: SanitizeOption = {
+  // tagNames: ['slides', 'slide'],
+};

+ 12 - 0
packages/presentation/tsconfig.base.json

@@ -0,0 +1,12 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "jsx": "preserve",
+  },
+  "include": [
+    "src"
+  ],
+  "exclude": [
+    "src/test"
+  ]
+}

+ 16 - 0
packages/presentation/tsconfig.build.json

@@ -0,0 +1,16 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "rootDir": "./src",
+    "outDir": "dist",
+    "declaration": true,
+    "noResolve": false,
+    "preserveConstEnums": true,
+    "sourceMap": false,
+    "noEmit": false,
+
+    "baseUrl": ".",
+    "paths": {
+    }
+  }
+}

+ 10 - 0
packages/presentation/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "extends": "./tsconfig.base.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["./src/*"],
+      "@growi/*": ["../*/src"]
+    }
+  }
+}

+ 206 - 13
yarn.lock

@@ -2922,6 +2922,38 @@
     semver "^7.3.5"
     tar "^6.1.11"
 
+"@marp-team/marp-core@^3.4.2":
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/@marp-team/marp-core/-/marp-core-3.4.2.tgz#07e9444c0be0fc969edfa33d4c31f304f18e7b2e"
+  integrity sha512-eJFK/Xk8d9Fe8yCwXreAA+QjSiQVSVLUP1FbscN2Jr0Jo0NRhvC8cob9yTq6XdPkkcZCyTW/v61W668eKMdaww==
+  dependencies:
+    "@marp-team/marpit" "^2.4.2"
+    "@marp-team/marpit-svg-polyfill" "^2.0.0"
+    highlight.js "^11.7.0"
+    katex "^0.16.4"
+    mathjax-full "^3.2.2"
+    postcss "^8.4.21"
+    postcss-selector-parser "^6.0.11"
+    xss "^1.0.14"
+
+"@marp-team/marpit-svg-polyfill@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@marp-team/marpit-svg-polyfill/-/marpit-svg-polyfill-2.0.0.tgz#4efbf248c86b48f2dac0d5e6b4d8bf5bac7dd0b2"
+  integrity sha512-KIp5EaUkwFaeYOAb0b9B7+3XAjZYAUDJi3LRSnH1y27jJY2TCKrXnKIATfPJXSkn9QjTy69fbwCZ07B93QhupA==
+
+"@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==
+  dependencies:
+    color-string "^1.9.1"
+    cssesc "^3.0.0"
+    js-yaml "^4.1.0"
+    lodash.kebabcase "^4.1.1"
+    markdown-it "^13.0.1"
+    markdown-it-front-matter "^0.2.3"
+    postcss "^8.4.19"
+
 "@next/bundle-analyzer@^12.2.3":
   version "12.2.3"
   resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-12.2.3.tgz#8b4b934d28c09b9c11c4a074fbcc444402c8e017"
@@ -4451,6 +4483,11 @@
   resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
   integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
 
+"@types/reveal.js@^4.4.1":
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/@types/reveal.js/-/reveal.js-4.4.1.tgz#970e29b6ed6c07ad693797d5157ba2fb3e94840c"
+  integrity sha512-oMcIAaP9rFCODHdGC2RM71cbBGOPc4prolkPEN98LUQm9b64ePjNpD/Fb2b/oep2w0XDN4aYCdUmip6kuLEJBg==
+
 "@types/rewire@^2.5.28":
   version "2.5.28"
   resolved "https://registry.yarnpkg.com/@types/rewire/-/rewire-2.5.28.tgz#ff34de38c4269fe74e2597195d4918c25d42ebad"
@@ -6844,14 +6881,22 @@ color-convert@^2.0.1:
   dependencies:
     color-name "~1.1.4"
 
+color-name@^1.0.0, color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
 color-name@^1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
 
-color-name@~1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
-  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+color-string@^1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
+  integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+  dependencies:
+    color-name "^1.0.0"
+    simple-swizzle "^0.2.2"
 
 color-support@^1.1.2:
   version "1.1.3"
@@ -6908,6 +6953,16 @@ commander@6.2.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75"
   integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==
 
+commander@9.2.0:
+  version "9.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-9.2.0.tgz#6e21014b2ed90d8b7c9647230d8b7a94a4a419a9"
+  integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==
+
+commander@^2.20.3:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
 commander@^2.7.1:
   version "2.20.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
@@ -8454,6 +8509,11 @@ entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
+entities@~3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
+  integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
+
 env-editor@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/env-editor/-/env-editor-1.1.0.tgz#bd510b6cb1528a64b17273aaeba272c050e786e9"
@@ -9394,7 +9454,7 @@ esm-utils@^2.0.0:
   resolved "https://registry.yarnpkg.com/esm-utils/-/esm-utils-2.2.0.tgz#c950c0f3ea191f3830f0187f246b0cf2620c1e66"
   integrity sha512-kYj4yNRo4W3by0f1mj4AfRh1nsRTTpQG921Ik3AfyUq6upGlkI1fnMLypHn6XtFzZPdCYH1k9mtQA5MyZF9m+w==
 
-esm@3.2.25:
+esm@3.2.25, esm@^3.2.25:
   version "3.2.25"
   resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
   integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
@@ -11367,6 +11427,11 @@ highlight.js@^10.4.1, highlight.js@^10.7.1, highlight.js@~10.7.0:
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
   integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
 
+highlight.js@^11.7.0:
+  version "11.7.0"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
+  integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
+
 hogan.js@3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd"
@@ -11977,6 +12042,11 @@ is-arrayish@^0.2.1:
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
   integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
 
+is-arrayish@^0.3.1:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
+  integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
 is-bigint@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
@@ -13399,6 +13469,13 @@ katex@^0.15.0:
   dependencies:
     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==
+  dependencies:
+    commander "^8.0.0"
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -13660,6 +13737,13 @@ lines-and-columns@^2.0.2:
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b"
   integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==
 
+linkify-it@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec"
+  integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==
+  dependencies:
+    uc.micro "^1.0.1"
+
 listenercount@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
@@ -13847,6 +13931,11 @@ lodash.isstring@^4.0.1:
   resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
   integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
 
+lodash.kebabcase@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
+  integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==
+
 lodash.memoize@4.1.2, lodash.memoize@4.x, lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -14161,6 +14250,22 @@ markdown-escapes@^1.0.0:
   resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.3.tgz#6155e10416efaafab665d466ce598216375195f5"
   integrity sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==
 
+markdown-it-front-matter@^0.2.3:
+  version "0.2.3"
+  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==
+
+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==
+  dependencies:
+    argparse "^2.0.1"
+    entities "~3.0.1"
+    linkify-it "^4.0.1"
+    mdurl "^1.0.1"
+    uc.micro "^1.0.5"
+
 markdown-table@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.1.tgz#4b3dd3a133d1518b8ef0dbc709bf2a1b4824bc8c"
@@ -14199,6 +14304,16 @@ math-random@^1.0.1:
   resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c"
   integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==
 
+mathjax-full@^3.2.2:
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/mathjax-full/-/mathjax-full-3.2.2.tgz#43f02e55219db393030985d2b6537ceae82f1fa7"
+  integrity sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==
+  dependencies:
+    esm "^3.2.25"
+    mhchemparser "^4.1.0"
+    mj-context-menu "^0.6.1"
+    speech-rule-engine "^4.0.6"
+
 mathml-tag-names@^2.1.3:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
@@ -14514,6 +14629,11 @@ mdast-util-wiki-link@^0.0.2:
     "@babel/runtime" "^7.12.1"
     mdast-util-to-markdown "^0.6.5"
 
+mdurl@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+  integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
+
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -14669,6 +14789,11 @@ methods@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
 
+mhchemparser@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/mhchemparser/-/mhchemparser-4.1.1.tgz#a2142fdab37a02ec8d1b48a445059287790becd5"
+  integrity sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA==
+
 micro-spelling-correcter@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/micro-spelling-correcter/-/micro-spelling-correcter-1.1.1.tgz#805a06a26ccfcad8f3e5c6a1ac5ff29d4530166e"
@@ -15386,6 +15511,11 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
+mj-context-menu@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/mj-context-menu/-/mj-context-menu-0.6.1.tgz#a043c5282bf7e1cf3821de07b13525ca6f85aa69"
+  integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==
+
 mkdirp-infer-owner@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316"
@@ -17419,6 +17549,14 @@ postcss-scss@^4.0.3:
   resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.3.tgz#36c23c19a804274e722e83a54d20b838ab4767ac"
   integrity sha512-j4KxzWovfdHsyxwl1BxkUal/O4uirvHgdzMKS1aWJBAV0qh2qj5qAZqpeBfVUYGWv+4iK9Az7SPyZ4fyNju1uA==
 
+postcss-selector-parser@^6.0.11:
+  version "6.0.11"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
+  integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
 postcss-selector-parser@^6.0.7:
   version "6.0.8"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz#f023ed7a9ea736cd7ef70342996e8e78645a7914"
@@ -17477,6 +17615,15 @@ postcss@^8.4.18:
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
+postcss@^8.4.19, postcss@^8.4.21:
+  version "8.4.21"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
+  integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
+  dependencies:
+    nanoid "^3.3.4"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
 postcss@^8.4.5:
   version "8.4.5"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95"
@@ -18068,9 +18215,9 @@ react-lifecycles-compat@^3.0.4:
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
 react-markdown@^8.0.3:
-  version "8.0.3"
-  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.3.tgz#e8aba0d2f5a1b2124d476ee1fff9448a2f57e4b3"
-  integrity sha512-We36SfqaKoVNpN1QqsZwWSv/OZt5J15LNgTLWynwAN5b265hrQrsjMtlRNwUvS+YyR3yDM8HpTNc4pK9H/Gc0A==
+  version "8.0.5"
+  resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.5.tgz#c9a70a33ca9aeeafb769c6582e7e38843b9d70ad"
+  integrity sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A==
   dependencies:
     "@types/hast" "^2.0.0"
     "@types/prop-types" "^15.0.0"
@@ -18083,7 +18230,7 @@ react-markdown@^8.0.3:
     remark-parse "^10.0.0"
     remark-rehype "^10.0.0"
     space-separated-tokens "^2.0.0"
-    style-to-object "^0.3.0"
+    style-to-object "^0.4.0"
     unified "^10.0.0"
     unist-util-visit "^4.0.0"
     vfile "^5.0.0"
@@ -20157,10 +20304,10 @@ reusify@^1.0.0:
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
-reveal.js@^4.3.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/reveal.js/-/reveal.js-4.3.1.tgz#24a63300cff92cf5cbdd40a46f5d3699614d49ca"
-  integrity sha512-1kyEnWeUkaCdBdX//XXq9dtBK95ppvIlSwlHelrP8/wrX6LcsYp4HT9WTFoFEOUBfVqkm8C2aHQ367o+UKfcxw==
+reveal.js@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/reveal.js/-/reveal.js-4.4.0.tgz#bff589987a842d99bc8f6eb9b2b59f6d9941bf87"
+  integrity sha512-jIV6C9V2NEUjGzU8L6dUFGpk1KJmq7/EzP2fOW67ggc2c0Cp/PdprWxZ9Qgp46F0T2ZWDCjQ1p3Ytzy5jA6a2w==
 
 rewire@^5.0.0:
   version "5.0.0"
@@ -20727,6 +20874,13 @@ simple-load-script@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/simple-load-script/-/simple-load-script-1.0.2.tgz#d92951fe7b601ad90af8c9429bd4b2ee127ab8a3"
 
+simple-swizzle@^0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+  integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
+  dependencies:
+    is-arrayish "^0.3.1"
+
 simplebar-react@^2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/simplebar-react/-/simplebar-react-2.3.6.tgz#27945a80b221caae8f241155cdba1bb6c201ebff"
@@ -21091,6 +21245,15 @@ specificity@^0.4.1:
   resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019"
   integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==
 
+speech-rule-engine@^4.0.6:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz#b655dacbad3dae04acc0f7665e26ef258397dd09"
+  integrity sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==
+  dependencies:
+    commander "9.2.0"
+    wicked-good-xpath "1.3.0"
+    xmldom-sre "0.1.31"
+
 split-on-first@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
@@ -21564,6 +21727,13 @@ style-to-object@^0.3.0:
   dependencies:
     inline-style-parser "0.1.1"
 
+style-to-object@^0.4.0:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.1.tgz#53cf856f7cf7f172d72939d9679556469ba5de37"
+  integrity sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==
+  dependencies:
+    inline-style-parser "0.1.1"
+
 styled-jsx@5.0.4:
   version "5.0.4"
   resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.4.tgz#5b1bd0b9ab44caae3dd1361295559706e044aa53"
@@ -22822,6 +22992,11 @@ uberproto@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/uberproto/-/uberproto-1.2.0.tgz#61d4eab024f909c4e6ea52be867c4894a4beeb76"
 
+uc.micro@^1.0.1, uc.micro@^1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+  integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
+
 uglify-js@^3.1.4:
   version "3.13.4"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.4.tgz#592588bb9f47ae03b24916e2471218d914955574"
@@ -23764,6 +23939,11 @@ which@^2.0.2:
   dependencies:
     isexe "^2.0.0"
 
+wicked-good-xpath@1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz#81b0e95e8650e49c94b22298fff8686b5553cf6c"
+  integrity sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==
+
 wide-align@^1.1.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
@@ -23982,6 +24162,11 @@ xmldoc@^1.2.0:
   dependencies:
     sax "^1.2.4"
 
+xmldom-sre@0.1.31:
+  version "0.1.31"
+  resolved "https://registry.yarnpkg.com/xmldom-sre/-/xmldom-sre-0.1.31.tgz#10860d5bab2c603144597d04bf2c4980e98067f4"
+  integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==
+
 xmlhttprequest-ssl@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
@@ -24034,6 +24219,14 @@ xpath@0.0.32:
   resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af"
   integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==
 
+xss@^1.0.14:
+  version "1.0.14"
+  resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.14.tgz#4f3efbde75ad0d82e9921cc3c95e6590dd336694"
+  integrity sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==
+  dependencies:
+    commander "^2.20.3"
+    cssfilter "0.0.10"
+
 xss@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.6.tgz#eaf11e9fc476e3ae289944a1009efddd8a124b51"