397d8f3d84
* Upgrade @astrojs/markdown-support deps and update types * Add changeset * Update changeset * Switch astro-markdown-plugins example to use rehype-autolink-headings Usage of remark-autolink-headings is discouraged in favor of the rehype counterpart: https://github.com/remarkjs/remark-autolink-headings\#remark-autolink-headings * Add stricter types for unified plugins This includes a few suggestions from a code review: - use vfile.toString instead of vfile.value.toString - refactor plugins to follow unified best practices instead of returning functions that return a plugin - use any instead of any[] for plugin options types * Narrow down types to more specific hast or mdast typings
84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
import type { AstroMarkdownOptions, MarkdownRenderingOptions } from './types';
|
|
|
|
import createCollectHeaders from './rehype-collect-headers.js';
|
|
import scopedStyles from './remark-scoped-styles.js';
|
|
import remarkExpressions from './remark-expressions.js';
|
|
import rehypeExpressions from './rehype-expressions.js';
|
|
import { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js';
|
|
import { loadPlugins } from './load-plugins.js';
|
|
import raw from 'rehype-raw';
|
|
|
|
import { unified } from 'unified';
|
|
import markdown from 'remark-parse';
|
|
import markdownToHtml from 'remark-rehype';
|
|
import rehypeStringify from 'rehype-stringify';
|
|
import remarkSlug from 'remark-slug';
|
|
|
|
export { AstroMarkdownOptions, MarkdownRenderingOptions };
|
|
|
|
/** Internal utility for rendering a full markdown file and extracting Frontmatter data */
|
|
export async function renderMarkdownWithFrontmatter(contents: string, opts?: MarkdownRenderingOptions | null) {
|
|
// Dynamic import to ensure that "gray-matter" isn't built by Snowpack
|
|
const { default: matter } = await import('gray-matter');
|
|
const { data: frontmatter, content } = matter(contents);
|
|
const value = await renderMarkdown(content, opts);
|
|
return { ...value, frontmatter };
|
|
}
|
|
|
|
/** Shared utility for rendering markdown */
|
|
export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) {
|
|
const { $: { scopedClassName = null } = {}, footnotes: useFootnotes = true, gfm: useGfm = true, remarkPlugins = [], rehypePlugins = [] } = opts ?? {};
|
|
const { headers, rehypeCollectHeaders } = createCollectHeaders();
|
|
let parser = unified()
|
|
.use(markdown)
|
|
.use(remarkSlug)
|
|
.use([remarkExpressions, { addResult: true }]);
|
|
|
|
if (remarkPlugins.length === 0) {
|
|
if (useGfm) {
|
|
remarkPlugins.push('remark-gfm');
|
|
}
|
|
|
|
if (useFootnotes) {
|
|
remarkPlugins.push('remark-footnotes');
|
|
}
|
|
|
|
remarkPlugins.push('@silvenon/remark-smartypants');
|
|
}
|
|
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(remarkCodeBlock);
|
|
parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression'] });
|
|
parser.use(rehypeExpressions);
|
|
|
|
loadedRehypePlugins.forEach(([plugin, opts]) => {
|
|
parser.use(plugin, opts);
|
|
});
|
|
|
|
let result: string;
|
|
try {
|
|
const vfile = await parser
|
|
.use(raw)
|
|
.use(rehypeCollectHeaders)
|
|
.use(rehypeCodeBlock)
|
|
.use(rehypeStringify, { entities: { useNamedReferences: true } })
|
|
.process(content);
|
|
result = vfile.toString();
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
|
|
return {
|
|
astro: { headers, source: content, html: result.toString() },
|
|
content: result.toString(),
|
|
};
|
|
}
|