renderer.tsx 9.7 KB

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