mirror of
https://github.com/withastro/astro.git
synced 2025-01-13 22:11:20 -05:00
e9a72d9a91
* Bump shikiji, use transformers API, expose transformers API * update astro config schema * include shikiji-core * Use default import * address css-variables theme * Remove shikiji markdoc * Improve schema transformers handling * Fix tests * Update changeset * bump shikiji version * Update .changeset/six-scissors-worry.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update wording Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> --------- Co-authored-by: bluwy <bjornlu.dev@gmail.com> Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
import type { AstroMarkdownOptions, MarkdownProcessor, 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 { createShikiHighlighter, replaceCssVariables, type ShikiHighlighter } from './shiki.js';
|
|
export * from './types.js';
|
|
|
|
export const markdownConfigDefaults: Required<AstroMarkdownOptions> = {
|
|
syntaxHighlight: 'shiki',
|
|
shikiConfig: {
|
|
langs: [],
|
|
theme: 'github-dark',
|
|
experimentalThemes: {},
|
|
wrap: false,
|
|
transformers: [],
|
|
},
|
|
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, {
|
|
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 ?? {},
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|