summaryrefslogtreecommitdiff
path: root/packages/markdown/remark/src/index.ts
blob: 5c707a4dbe11eb281d0c1ce053aff7cbf9b55001 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import type { AstroMarkdownOptions, MarkdownRenderingOptions } from './types';

import createCollectHeaders from './rehype-collect-headers.js';
import scopedStyles from './remark-scoped-styles.js';
import { remarkExpressions, loadRemarkExpressions } from './remark-expressions.js';
import rehypeExpressions from './rehype-expressions.js';
import rehypeIslands from './rehype-islands.js';
import { remarkJsx, loadRemarkJsx } from './remark-jsx.js';
import rehypeJsx from './rehype-jsx.js';
import remarkPrism from './remark-prism.js';
import remarkUnwrap from './remark-unwrap.js';
import { loadPlugins } from './load-plugins.js';

import { unified } from 'unified';
import markdown from 'remark-parse';
import markdownToHtml from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import rehypeRaw from 'rehype-raw';
import matter from 'gray-matter';

export { AstroMarkdownOptions, MarkdownRenderingOptions };

/** Internal utility for rendering a full markdown file and extracting Frontmatter data */
export async function renderMarkdownWithFrontmatter(contents: string, opts?: MarkdownRenderingOptions | null) {
  const { data: frontmatter, content } = matter(contents);
  const value = await renderMarkdown(content, opts);
  return { ...value, frontmatter };
}

export const DEFAULT_REMARK_PLUGINS = [
  'remark-gfm',
  'remark-footnotes',
  // TODO: reenable smartypants!
  // '@silvenon/remark-smartypants'
];

export const DEFAULT_REHYPE_PLUGINS = [
  // empty
];

/** Shared utility for rendering markdown */
export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) {
  const { remarkPlugins = DEFAULT_REMARK_PLUGINS, rehypePlugins = DEFAULT_REHYPE_PLUGINS } = opts ?? {};
  const scopedClassName = opts?.$?.scopedClassName;
  const mode = opts?.mode ?? 'mdx';
  const isMDX = mode === 'mdx';
  const { headers, rehypeCollectHeaders } = createCollectHeaders();

  await Promise.all([loadRemarkExpressions(), loadRemarkJsx()]); // Vite bug: dynamically import() these because of CJS interop (this will cache)

  let parser = unified()
    .use(markdown)
    .use(isMDX ? [remarkJsx] : [])
    .use(isMDX ? [remarkExpressions] : [])
    .use([remarkUnwrap]);

  const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
  const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));

  loadedRemarkPlugins.forEach(([plugin, opts]) => {
    parser.use([[plugin, opts]]);
  });

  if (scopedClassName) {
    parser.use([scopedStyles(scopedClassName)]);
  }

  parser.use([remarkPrism(scopedClassName)]);
  parser.use([[markdownToHtml as any, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression', 'mdxJsxTextElement', 'mdxJsxFlowElement'] }]]);

  loadedRehypePlugins.forEach(([plugin, opts]) => {
    parser.use([[plugin, opts]]);
  });

  parser
    .use(isMDX ? [rehypeJsx] : [])
    .use(isMDX ? [rehypeExpressions] : [])
    .use(isMDX ? [] : [rehypeRaw])
    .use(rehypeIslands);

  let result: string;
  try {
    const vfile = await parser.use([rehypeCollectHeaders]).use(rehypeStringify, { allowDangerousHtml: true }).process(content);
    result = vfile.toString();
  } catch (err) {
    console.error(err);
    throw err;
  }

  return {
    metadata: { headers, source: content, html: result.toString() },
    code: result.toString(),
  };
}

export default renderMarkdownWithFrontmatter;