renderer.tsx 11 KB

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