renderer.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import growiPlugin from '@growi/remark-growi-plugin';
  2. import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
  3. import katex from 'rehype-katex';
  4. import raw from 'rehype-raw';
  5. import sanitize, { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
  6. import slug from 'rehype-slug';
  7. import toc, { HtmlElementNode } from 'rehype-toc';
  8. import breaks from 'remark-breaks';
  9. import emoji from 'remark-emoji';
  10. import gfm from 'remark-gfm';
  11. import math from 'remark-math';
  12. import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
  13. import { Header } from '~/components/ReactMarkdownComponents/Header';
  14. import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
  15. import { RendererConfig } from '~/interfaces/services/renderer';
  16. import loggerFactory from '~/utils/logger';
  17. import { addClass } from './rehype-plugins/add-class';
  18. import { relativeLinks } from './rehype-plugins/relative-links';
  19. import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
  20. import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
  21. // import CsvToTable from './PreProcessor/CsvToTable';
  22. // import EasyGrid from './PreProcessor/EasyGrid';
  23. // import Linker from './PreProcessor/Linker';
  24. // import XssFilter from './PreProcessor/XssFilter';
  25. // import BlockdiagConfigurer from './markdown-it/blockdiag';
  26. // import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
  27. // import EmojiConfigurer from './markdown-it/emoji';
  28. // import FooternoteConfigurer from './markdown-it/footernote';
  29. // import HeaderConfigurer from './markdown-it/header';
  30. // import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
  31. // import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
  32. // import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
  33. // import MathJaxConfigurer from './markdown-it/mathjax';
  34. // import PlantUMLConfigurer from './markdown-it/plantuml';
  35. // import TableConfigurer from './markdown-it/table';
  36. // import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
  37. // import TaskListsConfigurer from './markdown-it/task-lists';
  38. // import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
  39. const logger = loggerFactory('growi:util:GrowiRenderer');
  40. // declare const hljs;
  41. // type MarkdownSettings = {
  42. // breaks?: boolean,
  43. // };
  44. // export default class GrowiRenderer {
  45. // RendererConfig: RendererConfig;
  46. // constructor(RendererConfig: RendererConfig, pagePath?: Nullable<string>) {
  47. // this.RendererConfig = RendererConfig;
  48. // this.pagePath = pagePath;
  49. // if (isClient() && (window as CustomWindow).growiRenderer != null) {
  50. // this.preProcessors = (window as CustomWindow).growiRenderer.preProcessors;
  51. // this.postProcessors = (window as CustomWindow).growiRenderer.postProcessors;
  52. // }
  53. // else {
  54. // this.preProcessors = [
  55. // new EasyGrid(),
  56. // new Linker(),
  57. // new CsvToTable(),
  58. // new XssFilter({
  59. // isEnabledXssPrevention: this.RendererConfig.isEnabledXssPrevention,
  60. // tagWhiteList: this.RendererConfig.tagWhiteList,
  61. // attrWhiteList: this.RendererConfig.attrWhiteList,
  62. // }),
  63. // ];
  64. // this.postProcessors = [
  65. // ];
  66. // }
  67. // this.init = this.init.bind(this);
  68. // this.addConfigurers = this.addConfigurers.bind(this);
  69. // this.setMarkdownSettings = this.setMarkdownSettings.bind(this);
  70. // this.configure = this.configure.bind(this);
  71. // this.process = this.process.bind(this);
  72. // this.codeRenderer = this.codeRenderer.bind(this);
  73. // }
  74. // init() {
  75. // let parser: Processor = unified().use(parse);
  76. // this.remarkPlugins.forEach((item) => {
  77. // parser = applyPlugin(parser, item);
  78. // });
  79. // let rehype: Processor = parser.use(remark2rehype);
  80. // this.rehypePlugins.forEach((item) => {
  81. // rehype = applyPlugin(rehype, item);
  82. // });
  83. // this.processor = rehype.use(rehype2react, {
  84. // createElement: React.createElement,
  85. // components: {
  86. // // a: NextLink,
  87. // },
  88. // });
  89. // }
  90. // init() {
  91. // // init markdown-it
  92. // this.md = new MarkdownIt({
  93. // html: true,
  94. // linkify: true,
  95. // highlight: this.codeRenderer,
  96. // });
  97. // this.isMarkdownItConfigured = false;
  98. // this.markdownItConfigurers = [
  99. // new TaskListsConfigurer(),
  100. // new HeaderConfigurer(),
  101. // new EmojiConfigurer(),
  102. // new MathJaxConfigurer(),
  103. // new DrawioViewerConfigurer(),
  104. // new PlantUMLConfigurer(this.RendererConfig),
  105. // new BlockdiagConfigurer(this.RendererConfig),
  106. // ];
  107. // if (this.pagePath != null) {
  108. // this.markdownItConfigurers.push(
  109. // new LinkerByRelativePathConfigurer(this.pagePath),
  110. // );
  111. // }
  112. // }
  113. // addConfigurers(configurers: any[]): void {
  114. // this.markdownItConfigurers.push(...configurers);
  115. // }
  116. // setMarkdownSettings(settings: MarkdownSettings): void {
  117. // this.md.set(settings);
  118. // }
  119. // configure(): void {
  120. // if (!this.isMarkdownItConfigured) {
  121. // this.markdownItConfigurers.forEach((configurer) => {
  122. // configurer.configure(this.md);
  123. // });
  124. // }
  125. // }
  126. // preProcess(markdown, context) {
  127. // let processed = markdown;
  128. // for (let i = 0; i < this.preProcessors.length; i++) {
  129. // if (!this.preProcessors[i].process) {
  130. // continue;
  131. // }
  132. // processed = this.preProcessors[i].process(processed, context);
  133. // }
  134. // return processed;
  135. // }
  136. // process(markdown, context) {
  137. // return this.md.render(markdown, context);
  138. // }
  139. // postProcess(html, context) {
  140. // let processed = html;
  141. // for (let i = 0; i < this.postProcessors.length; i++) {
  142. // if (!this.postProcessors[i].process) {
  143. // continue;
  144. // }
  145. // processed = this.postProcessors[i].process(processed, context);
  146. // }
  147. // return processed;
  148. // }
  149. // codeRenderer(code, langExt) {
  150. // const noborder = (!this.RendererConfig.highlightJsStyleBorder) ? 'hljs-no-border' : '';
  151. // let citeTag = '';
  152. // let hljsLang = 'plaintext';
  153. // let showLinenumbers = false;
  154. // if (langExt) {
  155. // // https://regex101.com/r/qGs7eZ/3
  156. // const match = langExt.match(/^([^:=\n]+)?(=([^:=\n]*))?(:([^:=\n]*))?(=([^:=\n]*))?$/);
  157. // const lang = match[1];
  158. // const fileName = match[5] || null;
  159. // showLinenumbers = (match[2] != null) || (match[6] != null);
  160. // if (fileName != null) {
  161. // citeTag = `<cite>${fileName}</cite>`;
  162. // }
  163. // if (hljs.getLanguage(lang)) {
  164. // hljsLang = lang;
  165. // }
  166. // }
  167. // let highlightCode = code;
  168. // try {
  169. // highlightCode = hljs.highlight(hljsLang, code, true).value;
  170. // // add line numbers
  171. // if (showLinenumbers) {
  172. // highlightCode = hljs.lineNumbersValue((highlightCode));
  173. // }
  174. // }
  175. // catch (err) {
  176. // logger.error(err);
  177. // }
  178. // return `<pre class="hljs ${noborder}">${citeTag}<code>${highlightCode}</code></pre>`;
  179. // }
  180. // }
  181. export type RendererOptions = Partial<ReactMarkdownOptions>;
  182. const generateCommonOptions = (pagePath: string|undefined, config: RendererConfig): RendererOptions => {
  183. return {
  184. remarkPlugins: [
  185. gfm,
  186. pukiwikiLikeLinker,
  187. growiPlugin,
  188. ],
  189. rehypePlugins: [
  190. slug,
  191. [relativeLinksByPukiwikiLikeLinker, { pagePath }],
  192. [relativeLinks, { pagePath }],
  193. raw,
  194. [sanitize, {
  195. ...sanitizeDefaultSchema,
  196. attributes: {
  197. ...sanitizeDefaultSchema.attributes,
  198. '*': sanitizeDefaultSchema.attributes != null
  199. ? sanitizeDefaultSchema.attributes['*'].concat('class', 'className')
  200. : ['class', 'className'],
  201. },
  202. }],
  203. [addClass, {
  204. table: 'table table-bordered',
  205. }],
  206. ],
  207. components: {
  208. a: NextLink,
  209. code: CodeBlock,
  210. },
  211. };
  212. };
  213. export const generateViewOptions = (
  214. pagePath: string,
  215. config: RendererConfig,
  216. storeTocNode: (node: HtmlElementNode) => void,
  217. ): RendererOptions => {
  218. const options = generateCommonOptions(pagePath, config);
  219. const { remarkPlugins, rehypePlugins, components } = options;
  220. // add remark plugins
  221. if (remarkPlugins != null) {
  222. remarkPlugins.push(emoji);
  223. // remarkPlugins.push(math);
  224. if (config.isEnabledLinebreaks) {
  225. remarkPlugins.push(breaks);
  226. }
  227. }
  228. // store toc node
  229. if (rehypePlugins != null) {
  230. // rehypePlugins.push(katex);
  231. rehypePlugins.push([toc, {
  232. nav: false,
  233. headings: ['h1', 'h2', 'h3'],
  234. customizeTOC: (toc: HtmlElementNode) => {
  235. // method for replace <ol> to <ul>
  236. const replacer = (children) => {
  237. children.forEach((child) => {
  238. if (child.type === 'element' && child.tagName === 'ol') {
  239. child.tagName = 'ul';
  240. }
  241. if (child.children) {
  242. replacer(child.children);
  243. }
  244. });
  245. };
  246. replacer([toc]); // replace <ol> to <ul>
  247. storeTocNode(toc); // store tocNode to global state with swr
  248. return false; // not show toc in body
  249. },
  250. }]);
  251. }
  252. // renderer.rehypePlugins.push([autoLinkHeadings, {
  253. // behavior: 'append',
  254. // }]);
  255. // add components
  256. if (components != null) {
  257. components.h1 = Header;
  258. components.h2 = Header;
  259. components.h3 = Header;
  260. }
  261. // // Add configurers for viewer
  262. // renderer.addConfigurers([
  263. // new FooternoteConfigurer(),
  264. // new TocAndAnchorConfigurer(),
  265. // new HeaderLineNumberConfigurer(),
  266. // new HeaderWithEditLinkConfigurer(),
  267. // new TableWithHandsontableButtonConfigurer(),
  268. // ]);
  269. // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
  270. // renderer.configure();
  271. return options;
  272. };
  273. export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
  274. const options = generateCommonOptions(undefined, config);
  275. const { remarkPlugins, rehypePlugins } = options;
  276. // add remark plugins
  277. if (remarkPlugins != null) {
  278. remarkPlugins.push(emoji);
  279. }
  280. // set toc node
  281. if (rehypePlugins != null) {
  282. rehypePlugins.push([toc, {
  283. headings: ['h1', 'h2', 'h3'],
  284. customizeTOC: () => tocNode,
  285. }]);
  286. }
  287. // renderer.rehypePlugins.push([autoLinkHeadings, {
  288. // behavior: 'append',
  289. // }]);
  290. return options;
  291. };
  292. export const generatePreviewOptions = (config: RendererConfig): RendererOptions => {
  293. const options = generateCommonOptions(undefined, config);
  294. // // Add configurers for preview
  295. // renderer.addConfigurers([
  296. // new FooternoteConfigurer(),
  297. // new HeaderLineNumberConfigurer(),
  298. // new TableConfigurer(),
  299. // ]);
  300. // renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
  301. // renderer.configure();
  302. return options;
  303. };
  304. export const generateCommentPreviewOptions = (config: RendererConfig): RendererOptions => {
  305. const options = generateCommonOptions(undefined, config);
  306. const { remarkPlugins } = options;
  307. // add remark plugins
  308. if (remarkPlugins != null) {
  309. remarkPlugins.push(emoji);
  310. if (config.isEnabledLinebreaksInComments) {
  311. remarkPlugins.push(breaks);
  312. }
  313. }
  314. // renderer.addConfigurers([
  315. // new TableConfigurer(),
  316. // ]);
  317. // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaksInComments });
  318. // renderer.configure();
  319. return options;
  320. };
  321. export const generateOthersOptions = (config: RendererConfig): RendererOptions => {
  322. const options = generateCommonOptions(undefined, config);
  323. // renderer.addConfigurers([
  324. // new TableConfigurer(),
  325. // ]);
  326. // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
  327. // renderer.configure();
  328. return options;
  329. };