convert-markdown-to-html.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { dynamicImport } from '@cspell/dynamic-import';
  2. import { isPopulated } from '@growi/core';
  3. import type { IPagePopulatedToShowRevision } from '@growi/core/dist/interfaces';
  4. import type { Root, Code } from 'mdast';
  5. import type { HydratedDocument } from 'mongoose';
  6. import type * as RehypeMeta from 'rehype-meta';
  7. import type * as RehypeStringify from 'rehype-stringify';
  8. import type * as RemarkParse from 'remark-parse';
  9. import type * as RemarkRehype from 'remark-rehype';
  10. import type * as Unified from 'unified';
  11. import type * as UnistUtilVisit from 'unist-util-visit';
  12. import type { PageDocument } from '~/server/models/page';
  13. interface ModuleCache {
  14. remarkParse?: typeof RemarkParse.default;
  15. unified?: typeof Unified.unified;
  16. visit?: typeof UnistUtilVisit.visit;
  17. remarkRehype?: typeof RemarkRehype.default;
  18. rehypeMeta?: typeof RehypeMeta.default;
  19. rehypeStringify?: typeof RehypeStringify.default;
  20. }
  21. let moduleCache: ModuleCache = {};
  22. const initializeModules = async(): Promise<void> => {
  23. if (moduleCache.remarkParse != null
  24. && moduleCache.unified != null
  25. && moduleCache.visit != null
  26. && moduleCache.remarkRehype != null
  27. && moduleCache.rehypeMeta != null
  28. && moduleCache.rehypeStringify != null
  29. ) {
  30. return;
  31. }
  32. const [
  33. { default: remarkParse },
  34. { unified }, { visit },
  35. { default: remarkRehype },
  36. { default: rehypeMeta },
  37. { default: rehypeStringify },
  38. ] = await Promise.all([
  39. dynamicImport<typeof RemarkParse>('remark-parse', __dirname),
  40. dynamicImport<typeof Unified>('unified', __dirname),
  41. dynamicImport<typeof UnistUtilVisit>('unist-util-visit', __dirname),
  42. dynamicImport<typeof RemarkRehype>('remark-rehype', __dirname),
  43. dynamicImport<typeof RehypeMeta>('rehype-meta', __dirname),
  44. dynamicImport<typeof RehypeStringify>('rehype-stringify', __dirname),
  45. ]);
  46. moduleCache = {
  47. remarkParse,
  48. unified,
  49. visit,
  50. remarkRehype,
  51. rehypeMeta,
  52. rehypeStringify,
  53. };
  54. };
  55. export const convertMarkdownToHtml = async(page: HydratedDocument<PageDocument> | IPagePopulatedToShowRevision): Promise<string> => {
  56. await initializeModules();
  57. const {
  58. remarkParse,
  59. unified, visit,
  60. remarkRehype,
  61. rehypeMeta,
  62. rehypeStringify,
  63. } = moduleCache;
  64. if (remarkParse == null
  65. || unified == null
  66. || visit == null
  67. || remarkRehype == null
  68. || rehypeMeta == null
  69. || rehypeStringify == null) {
  70. throw new Error('Failed to initialize required modules');
  71. }
  72. const sanitizeMarkdown = () => {
  73. return (tree: Root) => {
  74. visit(tree, 'code', (node: Code) => {
  75. if (node.lang === 'drawio') {
  76. node.value = '<!-- drawio content replaced -->';
  77. }
  78. });
  79. };
  80. };
  81. const revisionBody = page.revision != null && isPopulated(page.revision) ? page.revision.body : undefined;
  82. const processor = unified()
  83. .use(remarkParse)
  84. .use(sanitizeMarkdown)
  85. .use(remarkRehype)
  86. .use(rehypeMeta, {
  87. title: page.path,
  88. })
  89. .use(rehypeStringify);
  90. return processor.processSync(revisionBody).toString();
  91. };