renderer.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import assert from 'assert';
  2. import { isClient } from '@growi/core/dist/utils/browser-utils';
  3. import * as drawioPlugin from '@growi/remark-drawio';
  4. // eslint-disable-next-line import/extensions
  5. import * as lsxGrowiPlugin from '@growi/remark-lsx/dist/client/index.mjs';
  6. import katex from 'rehype-katex';
  7. import sanitize from 'rehype-sanitize';
  8. import slug from 'rehype-slug';
  9. import type { HtmlElementNode } from 'rehype-toc';
  10. import breaks from 'remark-breaks';
  11. import math from 'remark-math';
  12. import deepmerge from 'ts-deepmerge';
  13. import type { Pluggable } from 'unified';
  14. import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
  15. import { Header } from '~/components/ReactMarkdownComponents/Header';
  16. import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
  17. import { RehypeSanitizeOption } from '~/interfaces/rehype';
  18. import type { RendererOptions } from '~/interfaces/renderer-options';
  19. import type { RendererConfig } from '~/interfaces/services/renderer';
  20. import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
  21. import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
  22. import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
  23. import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
  24. import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
  25. import {
  26. commonSanitizeOption, generateCommonOptions, injectCustomSanitizeOption, verifySanitizePlugin,
  27. } from '~/services/renderer/renderer';
  28. import loggerFactory from '~/utils/logger';
  29. // import EasyGrid from './PreProcessor/EasyGrid';
  30. import '@growi/remark-lsx/dist/client/style.css';
  31. const logger = loggerFactory('growi:cli:services:renderer');
  32. assert(isClient(), 'This module must be loaded only from client modules.');
  33. export const generateViewOptions = (
  34. pagePath: string,
  35. config: RendererConfig,
  36. storeTocNode: (toc: HtmlElementNode) => void,
  37. ): RendererOptions => {
  38. const options = generateCommonOptions(pagePath);
  39. const { remarkPlugins, rehypePlugins, components } = options;
  40. // add remark plugins
  41. remarkPlugins.push(
  42. math,
  43. [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
  44. drawioPlugin.remarkPlugin,
  45. xsvToTable.remarkPlugin,
  46. lsxGrowiPlugin.remarkPlugin,
  47. );
  48. if (config.isEnabledLinebreaks) {
  49. remarkPlugins.push(breaks);
  50. }
  51. if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
  52. injectCustomSanitizeOption(config);
  53. }
  54. const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
  55. ? [sanitize, deepmerge(
  56. commonSanitizeOption,
  57. drawioPlugin.sanitizeOption,
  58. lsxGrowiPlugin.sanitizeOption,
  59. )]
  60. : () => {};
  61. // add rehype plugins
  62. rehypePlugins.push(
  63. slug,
  64. [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
  65. rehypeSanitizePlugin,
  66. katex,
  67. [relocateToc.rehypePluginStore, { storeTocNode }],
  68. );
  69. // add components
  70. if (components != null) {
  71. components.h1 = Header;
  72. components.h2 = Header;
  73. components.h3 = Header;
  74. components.h4 = Header;
  75. components.h5 = Header;
  76. components.h6 = Header;
  77. components.lsx = lsxGrowiPlugin.Lsx;
  78. components.drawio = DrawioViewerWithEditButton;
  79. components.table = TableWithEditButton;
  80. }
  81. if (config.isEnabledXssPrevention) {
  82. verifySanitizePlugin(options, false);
  83. }
  84. return options;
  85. };
  86. export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
  87. const options = generateCommonOptions(undefined);
  88. const { rehypePlugins } = options;
  89. // add remark plugins
  90. // remarkPlugins.push();
  91. if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
  92. injectCustomSanitizeOption(config);
  93. }
  94. const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
  95. ? [sanitize, deepmerge(
  96. commonSanitizeOption,
  97. )]
  98. : () => {};
  99. // add rehype plugins
  100. rehypePlugins.push(
  101. [relocateToc.rehypePluginRestore, { tocNode }],
  102. rehypeSanitizePlugin,
  103. );
  104. if (config.isEnabledXssPrevention) {
  105. verifySanitizePlugin(options);
  106. }
  107. return options;
  108. };
  109. export const generateSimpleViewOptions = (
  110. config: RendererConfig,
  111. pagePath: string,
  112. highlightKeywords?: string | string[],
  113. overrideIsEnabledLinebreaks?: boolean,
  114. ): RendererOptions => {
  115. const options = generateCommonOptions(pagePath);
  116. const { remarkPlugins, rehypePlugins, components } = options;
  117. // add remark plugins
  118. remarkPlugins.push(
  119. math,
  120. [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
  121. drawioPlugin.remarkPlugin,
  122. xsvToTable.remarkPlugin,
  123. lsxGrowiPlugin.remarkPlugin,
  124. );
  125. const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
  126. if (isEnabledLinebreaks) {
  127. remarkPlugins.push(breaks);
  128. }
  129. if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
  130. injectCustomSanitizeOption(config);
  131. }
  132. const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
  133. ? [sanitize, deepmerge(
  134. commonSanitizeOption,
  135. drawioPlugin.sanitizeOption,
  136. lsxGrowiPlugin.sanitizeOption,
  137. )]
  138. : () => {};
  139. // add rehype plugins
  140. rehypePlugins.push(
  141. [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
  142. [keywordHighlighter.rehypePlugin, { keywords: highlightKeywords }],
  143. rehypeSanitizePlugin,
  144. katex,
  145. );
  146. // add components
  147. if (components != null) {
  148. components.lsx = lsxGrowiPlugin.LsxImmutable;
  149. components.drawio = drawioPlugin.DrawioViewer;
  150. }
  151. if (config.isEnabledXssPrevention) {
  152. verifySanitizePlugin(options, false);
  153. }
  154. return options;
  155. };
  156. export const generatePresentationViewOptions = (
  157. config: RendererConfig,
  158. pagePath: string,
  159. ): RendererOptions => {
  160. // based on simple view options
  161. const options = generateSimpleViewOptions(config, pagePath);
  162. if (config.isEnabledXssPrevention) {
  163. verifySanitizePlugin(options, false);
  164. }
  165. return options;
  166. };
  167. export const generatePreviewOptions = (config: RendererConfig, pagePath: string): RendererOptions => {
  168. const options = generateCommonOptions(pagePath);
  169. const { remarkPlugins, rehypePlugins, components } = options;
  170. // add remark plugins
  171. remarkPlugins.push(
  172. math,
  173. [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
  174. drawioPlugin.remarkPlugin,
  175. xsvToTable.remarkPlugin,
  176. lsxGrowiPlugin.remarkPlugin,
  177. );
  178. if (config.isEnabledLinebreaks) {
  179. remarkPlugins.push(breaks);
  180. }
  181. if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
  182. injectCustomSanitizeOption(config);
  183. }
  184. const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
  185. ? [sanitize, deepmerge(
  186. commonSanitizeOption,
  187. lsxGrowiPlugin.sanitizeOption,
  188. drawioPlugin.sanitizeOption,
  189. addLineNumberAttribute.sanitizeOption,
  190. )]
  191. : () => {};
  192. // add rehype plugins
  193. rehypePlugins.push(
  194. [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
  195. addLineNumberAttribute.rehypePlugin,
  196. rehypeSanitizePlugin,
  197. katex,
  198. );
  199. // add components
  200. if (components != null) {
  201. components.lsx = lsxGrowiPlugin.LsxImmutable;
  202. components.drawio = drawioPlugin.DrawioViewer;
  203. }
  204. if (config.isEnabledXssPrevention) {
  205. verifySanitizePlugin(options, false);
  206. }
  207. return options;
  208. };