renderer.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import growiDirective from '@growi/remark-growi-directive';
  2. import type { Schema as SanitizeOption } from 'hast-util-sanitize';
  3. import katex from 'rehype-katex';
  4. import raw from 'rehype-raw';
  5. import sanitize from 'rehype-sanitize';
  6. import slug from 'rehype-slug';
  7. import breaks from 'remark-breaks';
  8. import remarkDirective from 'remark-directive';
  9. import remarkFrontmatter from 'remark-frontmatter';
  10. import gfm from 'remark-gfm';
  11. import math from 'remark-math';
  12. import deepmerge from 'ts-deepmerge';
  13. import type { Pluggable, PluginTuple } from 'unified';
  14. import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
  15. import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
  16. import type { RendererOptions } from '~/interfaces/renderer-options';
  17. import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
  18. import type { RendererConfig } from '~/interfaces/services/renderer';
  19. import loggerFactory from '~/utils/logger';
  20. import {
  21. attributes as recommendedAttributes,
  22. tagNames as recommendedTagNames,
  23. } from './recommended-whitelist';
  24. import * as addClass from './rehype-plugins/add-class';
  25. import * as addInlineProperty from './rehype-plugins/add-inline-code-property';
  26. import { relativeLinks } from './rehype-plugins/relative-links';
  27. import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
  28. import * as codeBlock from './remark-plugins/codeblock';
  29. import * as echoDirective from './remark-plugins/echo-directive';
  30. import * as emoji from './remark-plugins/emoji';
  31. import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
  32. import * as xsvToTable from './remark-plugins/xsv-to-table';
  33. // import EasyGrid from './PreProcessor/EasyGrid';
  34. const _logger = loggerFactory('growi:services:renderer');
  35. type SanitizePlugin = PluginTuple<[SanitizeOption]>;
  36. let currentInitializedSanitizeType: RehypeSanitizeType =
  37. RehypeSanitizeType.RECOMMENDED;
  38. let commonSanitizeOption: SanitizeOption;
  39. export const getCommonSanitizeOption = (
  40. config: RendererConfig,
  41. ): SanitizeOption => {
  42. if (
  43. commonSanitizeOption == null ||
  44. config.sanitizeType !== currentInitializedSanitizeType
  45. ) {
  46. // initialize
  47. commonSanitizeOption = deepmerge(
  48. {
  49. tagNames:
  50. config.sanitizeType === RehypeSanitizeType.RECOMMENDED
  51. ? recommendedTagNames
  52. : (config.customTagWhitelist ?? recommendedTagNames),
  53. attributes:
  54. config.sanitizeType === RehypeSanitizeType.RECOMMENDED
  55. ? recommendedAttributes
  56. : (config.customAttrWhitelist ?? recommendedAttributes),
  57. clobberPrefix: '', // remove clobber prefix
  58. },
  59. codeBlock.sanitizeOption,
  60. );
  61. currentInitializedSanitizeType = config.sanitizeType;
  62. }
  63. return commonSanitizeOption;
  64. };
  65. const isSanitizePlugin = (
  66. pluggable: Pluggable,
  67. ): pluggable is SanitizePlugin => {
  68. if (!Array.isArray(pluggable) || pluggable.length < 2) {
  69. return false;
  70. }
  71. const sanitizeOption = pluggable[1];
  72. return 'tagNames' in sanitizeOption && 'attributes' in sanitizeOption;
  73. };
  74. const hasSanitizePlugin = (
  75. options: RendererOptions,
  76. shouldBeTheLastItem: boolean,
  77. ): boolean => {
  78. const { rehypePlugins } = options;
  79. if (rehypePlugins == null || rehypePlugins.length === 0) {
  80. return false;
  81. }
  82. return shouldBeTheLastItem
  83. ? isSanitizePlugin(rehypePlugins.slice(-1)[0]) // evaluate the last one
  84. : rehypePlugins.some((rehypePlugin) => isSanitizePlugin(rehypePlugin));
  85. };
  86. export const verifySanitizePlugin = (
  87. options: RendererOptions,
  88. shouldBeTheLastItem = true,
  89. ): void => {
  90. if (hasSanitizePlugin(options, shouldBeTheLastItem)) {
  91. return;
  92. }
  93. throw new Error(
  94. "The specified options does not have sanitize plugin in 'rehypePlugins'",
  95. );
  96. };
  97. export const generateCommonOptions = (
  98. pagePath: string | undefined,
  99. ): RendererOptions => {
  100. return {
  101. remarkPlugins: [
  102. gfm,
  103. emoji.remarkPlugin,
  104. pukiwikiLikeLinker,
  105. growiDirective,
  106. remarkDirective,
  107. echoDirective.remarkPlugin,
  108. remarkFrontmatter,
  109. codeBlock.remarkPlugin,
  110. ],
  111. remarkRehypeOptions: {
  112. clobberPrefix: '', // remove clobber prefix
  113. allowDangerousHtml: true,
  114. },
  115. rehypePlugins: [
  116. [relativeLinksByPukiwikiLikeLinker, { pagePath }],
  117. [relativeLinks, { pagePath }],
  118. raw,
  119. [
  120. addClass.rehypePlugin,
  121. {
  122. table: 'table table-bordered',
  123. },
  124. ],
  125. addInlineProperty.rehypePlugin,
  126. ],
  127. components: {
  128. a: NextLink,
  129. code: CodeBlock,
  130. },
  131. };
  132. };
  133. export const generateSSRViewOptions = (
  134. config: RendererConfig,
  135. pagePath: string,
  136. ): RendererOptions => {
  137. const options = generateCommonOptions(pagePath);
  138. const { remarkPlugins, rehypePlugins } = options;
  139. // add remark plugins
  140. remarkPlugins.push(math, xsvToTable.remarkPlugin);
  141. const isEnabledLinebreaks = config.isEnabledLinebreaks;
  142. if (isEnabledLinebreaks) {
  143. remarkPlugins.push(breaks);
  144. }
  145. const rehypeSanitizePlugin: Pluggable | (() => void) =
  146. config.isEnabledXssPrevention
  147. ? [sanitize, getCommonSanitizeOption(config)]
  148. : () => {};
  149. // add rehype plugins
  150. rehypePlugins.push(slug, rehypeSanitizePlugin, katex);
  151. // add components
  152. // if (components != null) {
  153. // }
  154. if (config.isEnabledXssPrevention) {
  155. verifySanitizePlugin(options, false);
  156. }
  157. return options;
  158. };