CodeBlock.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import type { JSX, ReactNode } from 'react';
  2. import { PrismAsyncLight } from 'react-syntax-highlighter';
  3. import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
  4. import styles from './CodeBlock.module.scss';
  5. // remove font-family
  6. Object.entries<object>(oneDark).forEach(([key, value]) => {
  7. if ('fontFamily' in value) {
  8. delete oneDark[key].fontFamily;
  9. }
  10. });
  11. type InlineCodeBlockProps = {
  12. children: ReactNode;
  13. className?: string;
  14. };
  15. const InlineCodeBlockSubstance = (props: InlineCodeBlockProps): JSX.Element => {
  16. const { children, className, ...rest } = props;
  17. return (
  18. <code className={`code-inline ${className ?? ''}`} {...rest}>
  19. {children}
  20. </code>
  21. );
  22. };
  23. function extractChildrenToIgnoreReactNode(children: ReactNode): ReactNode {
  24. if (children == null) {
  25. return children;
  26. }
  27. // Single element array
  28. if (Array.isArray(children) && children.length === 1) {
  29. return extractChildrenToIgnoreReactNode(children[0]);
  30. }
  31. // Multiple element array
  32. if (Array.isArray(children) && children.length > 1) {
  33. return children
  34. .map((node) => extractChildrenToIgnoreReactNode(node))
  35. .join('');
  36. }
  37. // object
  38. if (typeof children === 'object') {
  39. const grandChildren =
  40. (children as any).children ?? (children as any).props.children;
  41. return extractChildrenToIgnoreReactNode(grandChildren);
  42. }
  43. return String(children).replace(/\n$/, '');
  44. }
  45. function CodeBlockSubstance({
  46. lang,
  47. children,
  48. }: {
  49. lang: string;
  50. children: ReactNode;
  51. }): JSX.Element {
  52. // return alternative element
  53. // in order to fix "CodeBlock string is be [object Object] if searched"
  54. // see: https://github.com/growilabs/growi/pull/7484
  55. //
  56. // Note: You can also remove this code if the user requests to see the code highlighted in Prism as-is.
  57. const isSimpleString =
  58. typeof children === 'string' ||
  59. (Array.isArray(children) &&
  60. children.length === 1 &&
  61. typeof children[0] === 'string');
  62. if (!isSimpleString) {
  63. return (
  64. <div style={oneDark['pre[class*="language-"]']}>
  65. <code
  66. className={`language-${lang}`}
  67. style={oneDark['code[class*="language-"]']}
  68. >
  69. {children}
  70. </code>
  71. </div>
  72. );
  73. }
  74. return (
  75. <PrismAsyncLight PreTag="div" style={oneDark} language={lang}>
  76. {extractChildrenToIgnoreReactNode(children)}
  77. </PrismAsyncLight>
  78. );
  79. }
  80. type CodeBlockProps = {
  81. children: ReactNode;
  82. className?: string;
  83. inline?: true;
  84. };
  85. export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
  86. // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
  87. const { className, children, inline } = props;
  88. if (inline) {
  89. return (
  90. <InlineCodeBlockSubstance className={`code-inline ${className ?? ''}`}>
  91. {children}
  92. </InlineCodeBlockSubstance>
  93. );
  94. }
  95. const match = /language-(\w+)(:?.+)?/.exec(className || '');
  96. const lang = match && match[1] ? match[1] : '';
  97. const name = match && match[2] ? match[2].slice(1) : null;
  98. return (
  99. <>
  100. {name != null && (
  101. <cite
  102. className={`code-highlighted-title ${styles['code-highlighted-title']}`}
  103. >
  104. {name}
  105. </cite>
  106. )}
  107. <CodeBlockSubstance lang={lang}>{children}</CodeBlockSubstance>
  108. </>
  109. );
  110. };