convert-markdown-to-html.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { dynamicImport } from '@cspell/dynamic-import';
  2. import type { IPage } from '@growi/core/dist/interfaces';
  3. import { DevidedPagePath } from '@growi/core/dist/models';
  4. import type { Code, Root } from 'mdast';
  5. import type * as RehypeMeta from 'rehype-meta';
  6. import type * as RehypeStringify from 'rehype-stringify';
  7. import type * as RemarkParse from 'remark-parse';
  8. import type * as RemarkRehype from 'remark-rehype';
  9. import type * as Unified from 'unified';
  10. import type * as UnistUtilVisit from 'unist-util-visit';
  11. interface ModuleCache {
  12. unified?: typeof Unified.unified;
  13. visit?: typeof UnistUtilVisit.visit;
  14. remarkParse?: typeof RemarkParse.default;
  15. remarkRehype?: typeof RemarkRehype.default;
  16. rehypeMeta?: typeof RehypeMeta.default;
  17. rehypeStringify?: typeof RehypeStringify.default;
  18. }
  19. let moduleCache: ModuleCache = {};
  20. const initializeModules = async (): Promise<void> => {
  21. if (
  22. moduleCache.unified != null &&
  23. moduleCache.visit != null &&
  24. moduleCache.remarkParse != null &&
  25. moduleCache.remarkRehype != null &&
  26. moduleCache.rehypeMeta != null &&
  27. moduleCache.rehypeStringify != null
  28. ) {
  29. return;
  30. }
  31. const [
  32. { unified },
  33. { visit },
  34. { default: remarkParse },
  35. { default: remarkRehype },
  36. { default: rehypeMeta },
  37. { default: rehypeStringify },
  38. ] = await Promise.all([
  39. dynamicImport<typeof Unified>('unified', __dirname),
  40. dynamicImport<typeof UnistUtilVisit>('unist-util-visit', __dirname),
  41. dynamicImport<typeof RemarkParse>('remark-parse', __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. unified,
  48. visit,
  49. remarkParse,
  50. remarkRehype,
  51. rehypeMeta,
  52. rehypeStringify,
  53. };
  54. };
  55. type ConvertMarkdownToHtmlArgs = {
  56. page: IPage;
  57. siteUrl: string | undefined;
  58. };
  59. export const convertMarkdownToHtml = async (
  60. revisionBody: string,
  61. args: ConvertMarkdownToHtmlArgs,
  62. ): Promise<string> => {
  63. await initializeModules();
  64. const {
  65. unified,
  66. visit,
  67. remarkParse,
  68. remarkRehype,
  69. rehypeMeta,
  70. rehypeStringify,
  71. } = moduleCache;
  72. if (
  73. unified == null ||
  74. visit == null ||
  75. remarkParse == null ||
  76. remarkRehype == null ||
  77. rehypeMeta == null ||
  78. rehypeStringify == null
  79. ) {
  80. throw new Error('Failed to initialize required modules');
  81. }
  82. const sanitizeMarkdown = () => {
  83. return (tree: Root) => {
  84. visit(tree, 'code', (node: Code) => {
  85. if (node.lang === 'drawio') {
  86. node.value = '<!-- drawio content replaced -->';
  87. }
  88. });
  89. };
  90. };
  91. const { page, siteUrl } = args;
  92. const { latter: title } = new DevidedPagePath(page.path);
  93. const processor = unified()
  94. .use(remarkParse)
  95. .use(sanitizeMarkdown)
  96. .use(remarkRehype)
  97. .use(rehypeMeta, {
  98. og: true,
  99. type: 'article',
  100. title,
  101. pathname: page.path,
  102. published: page.createdAt,
  103. modified: page.updatedAt,
  104. origin: siteUrl,
  105. })
  106. .use(rehypeStringify);
  107. return processor.processSync(revisionBody).toString();
  108. };