0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-27 22:19:04 -05:00
astro/packages/markdown/remark/src/index.ts
2023-09-14 20:05:38 +08:00

208 lines
5.8 KiB
TypeScript

import type {
AstroMarkdownOptions,
MarkdownProcessor,
MarkdownRenderingOptions,
MarkdownRenderingResult,
MarkdownVFile,
} from './types.js';
import {
InvalidAstroDataError,
safelyGetAstroData,
setVfileFrontmatter,
} from './frontmatter-injection.js';
import { loadPlugins } from './load-plugins.js';
import { rehypeHeadingIds } from './rehype-collect-headings.js';
import { remarkCollectImages } from './remark-collect-images.js';
import { remarkPrism } from './remark-prism.js';
import { remarkShiki } from './remark-shiki.js';
import rehypeRaw from 'rehype-raw';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import remarkSmartypants from 'remark-smartypants';
import { unified } from 'unified';
import { VFile } from 'vfile';
import { rehypeImages } from './rehype-images.js';
export { InvalidAstroDataError, setVfileFrontmatter } from './frontmatter-injection.js';
export { rehypeHeadingIds } from './rehype-collect-headings.js';
export { remarkCollectImages } from './remark-collect-images.js';
export { remarkPrism } from './remark-prism.js';
export { remarkShiki } from './remark-shiki.js';
export * from './types.js';
export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'> = {
syntaxHighlight: 'shiki',
shikiConfig: {
langs: [],
theme: 'github-dark',
wrap: false,
},
remarkPlugins: [],
rehypePlugins: [],
remarkRehype: {},
gfm: true,
smartypants: true,
};
// Skip nonessential plugins during performance benchmark runs
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
/**
* Create a markdown preprocessor to render multiple markdown files
*/
export async function createMarkdownProcessor(
opts?: AstroMarkdownOptions
): Promise<MarkdownProcessor> {
const {
syntaxHighlight = markdownConfigDefaults.syntaxHighlight,
shikiConfig = markdownConfigDefaults.shikiConfig,
remarkPlugins = markdownConfigDefaults.remarkPlugins,
rehypePlugins = markdownConfigDefaults.rehypePlugins,
remarkRehype: remarkRehypeOptions = markdownConfigDefaults.remarkRehype,
gfm = markdownConfigDefaults.gfm,
smartypants = markdownConfigDefaults.smartypants,
} = opts ?? {};
const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins));
const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins));
const parser = unified().use(remarkParse);
// gfm and smartypants
if (!isPerformanceBenchmark) {
if (gfm) {
parser.use(remarkGfm);
}
if (smartypants) {
parser.use(remarkSmartypants);
}
}
// User remark plugins
for (const [plugin, pluginOpts] of loadedRemarkPlugins) {
parser.use(plugin, pluginOpts);
}
if (!isPerformanceBenchmark) {
// Syntax highlighting
if (syntaxHighlight === 'shiki') {
parser.use(remarkShiki, shikiConfig);
} else if (syntaxHighlight === 'prism') {
parser.use(remarkPrism);
}
// Apply later in case user plugins resolve relative image paths
parser.use(remarkCollectImages);
}
// Remark -> Rehype
parser.use(remarkRehype as any, {
allowDangerousHtml: true,
passThrough: [],
...remarkRehypeOptions,
});
// User rehype plugins
for (const [plugin, pluginOpts] of loadedRehypePlugins) {
parser.use(plugin, pluginOpts);
}
// Images / Assets support
parser.use(rehypeImages());
// Headings
if (!isPerformanceBenchmark) {
parser.use(rehypeHeadingIds);
}
// Stringify to HTML
parser.use(rehypeRaw).use(rehypeStringify, { allowDangerousHtml: true });
return {
async render(content, renderOpts) {
const vfile = new VFile({ value: content, path: renderOpts?.fileURL });
setVfileFrontmatter(vfile, renderOpts?.frontmatter ?? {});
const result: MarkdownVFile = await parser.process(vfile).catch((err) => {
// Ensure that the error message contains the input filename
// to make it easier for the user to fix the issue
err = prefixError(err, `Failed to parse Markdown file "${vfile.path}"`);
// eslint-disable-next-line no-console
console.error(err);
throw err;
});
const astroData = safelyGetAstroData(result.data);
if (astroData instanceof InvalidAstroDataError) {
throw astroData;
}
return {
code: String(result.value),
metadata: {
headings: result.data.__astroHeadings ?? [],
imagePaths: result.data.imagePaths ?? new Set(),
frontmatter: astroData.frontmatter ?? {},
},
// Compat for `renderMarkdown` only. Do not use!
__renderMarkdownCompat: {
result,
},
};
},
};
}
/**
* Shared utility for rendering markdown
*
* @deprecated Use `createMarkdownProcessor` instead for better performance
*/
export async function renderMarkdown(
content: string,
opts: MarkdownRenderingOptions
): Promise<MarkdownRenderingResult> {
const processor = await createMarkdownProcessor(opts);
const result = await processor.render(content, {
fileURL: opts.fileURL,
frontmatter: opts.frontmatter,
});
return {
code: result.code,
metadata: {
headings: result.metadata.headings,
source: content,
html: result.code,
},
vfile: (result as any).__renderMarkdownCompat.result,
};
}
function prefixError(err: any, prefix: string) {
// If the error is an object with a `message` property, attempt to prefix the message
if (err?.message) {
try {
err.message = `${prefix}:\n${err.message}`;
return err;
} catch (error) {
// Any errors here are ok, there's fallback code below
}
}
// If that failed, create a new error with the desired message and attempt to keep the stack
const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`);
try {
wrappedError.stack = err.stack;
wrappedError.cause = err;
} catch {
// It's ok if we could not set the stack or cause - the message is the most important part
}
return wrappedError;
}