renderer.tsx 9.1 KB

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