renderer.tsx 11 KB

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