renderer.tsx 9.7 KB

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