RevisionRenderer.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import React, { useEffect, useState } from 'react';
  2. import dynamic from 'next/dynamic';
  3. import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
  4. import ReactMarkdown from 'react-markdown';
  5. import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
  6. import remarkFrontmatter from 'remark-frontmatter';
  7. import remarkParse from 'remark-parse';
  8. import remarkStringify from 'remark-stringify';
  9. import { unified } from 'unified';
  10. import { visit } from 'unist-util-visit';
  11. import type { RendererOptions } from '~/interfaces/renderer-options';
  12. import loggerFactory from '~/utils/logger';
  13. import 'katex/dist/katex.min.css';
  14. const logger = loggerFactory('components:Page:RevisionRenderer');
  15. type Props = {
  16. rendererOptions: RendererOptions,
  17. markdown: string,
  18. additionalClassName?: string,
  19. isSlidesOverviewEnabled?: boolean,
  20. }
  21. const ErrorFallback: React.FC<FallbackProps> = React.memo(({ error, resetErrorBoundary }) => {
  22. return (
  23. <div role="alert">
  24. <p>Something went wrong:</p>
  25. <pre>{error.message}</pre>
  26. <button className='btn btn-primary' onClick={resetErrorBoundary}>Reload</button>
  27. </div>
  28. );
  29. });
  30. ErrorFallback.displayName = 'ErrorFallback';
  31. const Slides = dynamic(() => import('@growi/presentation').then(mod => mod.Slides), { ssr: false });
  32. const RevisionRenderer = React.memo((props: Props): JSX.Element => {
  33. const {
  34. rendererOptions, markdown, additionalClassName, isSlidesOverviewEnabled,
  35. } = props;
  36. const [hasSlideFlag, setHasSlideFlag] = useState<boolean>();
  37. const [hasMarpFlag, setHasMarpFlag] = useState<boolean>();
  38. // use useEffect to avoid ssr
  39. useEffect(() => {
  40. if (isSlidesOverviewEnabled) {
  41. const processMarkdown = () => (tree) => {
  42. setHasSlideFlag(false);
  43. setHasMarpFlag(false);
  44. visit(tree, 'yaml', (node) => {
  45. if (node.value != null) {
  46. const lines = node.value.split('\n');
  47. lines.forEach((line) => {
  48. const [key, value] = line.split(':').map(part => part.trim());
  49. if (key === 'slide' && value === 'true') {
  50. setHasSlideFlag(true);
  51. }
  52. else if (key === 'marp' && value === 'true') {
  53. setHasMarpFlag(true);
  54. }
  55. });
  56. }
  57. });
  58. };
  59. unified()
  60. .use(remarkParse)
  61. .use(remarkStringify)
  62. .use(remarkFrontmatter, ['yaml'])
  63. .use(processMarkdown)
  64. .process(markdown);
  65. }
  66. }, [markdown, setHasSlideFlag, setHasMarpFlag, isSlidesOverviewEnabled]);
  67. if (isSlidesOverviewEnabled && (hasSlideFlag || hasMarpFlag)) {
  68. const options = {
  69. rendererOptions: rendererOptions as ReactMarkdownOptions,
  70. isDarkMode: false,
  71. disableSeparationsByHeader: false,
  72. hasMarpFlag,
  73. };
  74. return (
  75. <Slides
  76. options={options}
  77. hasMarpFlag={hasMarpFlag}
  78. >{markdown}</Slides>
  79. );
  80. }
  81. return (
  82. <ErrorBoundary FallbackComponent={ErrorFallback}>
  83. <ReactMarkdown
  84. {...rendererOptions}
  85. className={`wiki ${additionalClassName ?? ''}`}
  86. >
  87. {markdown}
  88. </ReactMarkdown>
  89. </ErrorBoundary>
  90. );
  91. });
  92. RevisionRenderer.displayName = 'RevisionRenderer';
  93. export default RevisionRenderer;