aboutsummaryrefslogtreecommitdiff
path: root/packages/markdown/remark/src/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/markdown/remark/src/index.ts')
-rw-r--r--packages/markdown/remark/src/index.ts200
1 files changed, 200 insertions, 0 deletions
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts
new file mode 100644
index 000000000..1aa713956
--- /dev/null
+++ b/packages/markdown/remark/src/index.ts
@@ -0,0 +1,200 @@
+import type {
+ AstroMarkdownOptions,
+ AstroMarkdownProcessorOptions,
+ MarkdownProcessor,
+ SyntaxHighlightConfig,
+} from './types.js';
+
+import { loadPlugins } from './load-plugins.js';
+import { rehypeHeadingIds } from './rehype-collect-headings.js';
+import { rehypePrism } from './rehype-prism.js';
+import { rehypeShiki } from './rehype-shiki.js';
+import { remarkCollectImages } from './remark-collect-images.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 { defaultExcludeLanguages } from './highlight.js';
+import { rehypeImages } from './rehype-images.js';
+export { rehypeHeadingIds } from './rehype-collect-headings.js';
+export { remarkCollectImages } from './remark-collect-images.js';
+export { rehypePrism } from './rehype-prism.js';
+export { rehypeShiki } from './rehype-shiki.js';
+export {
+ isFrontmatterValid,
+ extractFrontmatter,
+ parseFrontmatter,
+ type ParseFrontmatterOptions,
+ type ParseFrontmatterResult,
+} from './frontmatter.js';
+export {
+ createShikiHighlighter,
+ type ShikiHighlighter,
+ type CreateShikiHighlighterOptions,
+ type ShikiHighlighterHighlightOptions,
+} from './shiki.js';
+export * from './types.js';
+
+export const syntaxHighlightDefaults: Required<SyntaxHighlightConfig> = {
+ type: 'shiki',
+ excludeLangs: defaultExcludeLanguages,
+};
+
+export const markdownConfigDefaults: Required<AstroMarkdownOptions> = {
+ syntaxHighlight: syntaxHighlightDefaults,
+ shikiConfig: {
+ langs: [],
+ theme: 'github-dark',
+ themes: {},
+ wrap: false,
+ transformers: [],
+ langAlias: {},
+ },
+ 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?: AstroMarkdownProcessorOptions,
+): 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,
+ experimentalHeadingIdCompat = false,
+ } = 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) {
+ // Apply later in case user plugins resolve relative image paths
+ parser.use(remarkCollectImages, opts?.image);
+ }
+
+ // Remark -> Rehype
+ parser.use(remarkRehype, {
+ allowDangerousHtml: true,
+ passThrough: [],
+ ...remarkRehypeOptions,
+ });
+
+ if (syntaxHighlight && !isPerformanceBenchmark) {
+ const syntaxHighlightType =
+ typeof syntaxHighlight === 'string' ? syntaxHighlight : syntaxHighlight?.type;
+ const excludeLangs =
+ typeof syntaxHighlight === 'object' ? syntaxHighlight?.excludeLangs : undefined;
+ // Syntax highlighting
+ if (syntaxHighlightType === 'shiki') {
+ parser.use(rehypeShiki, shikiConfig, excludeLangs);
+ } else if (syntaxHighlightType === 'prism') {
+ parser.use(rehypePrism, excludeLangs);
+ }
+ }
+
+ // 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, { experimentalHeadingIdCompat });
+ }
+
+ // Stringify to HTML
+ parser.use(rehypeRaw).use(rehypeStringify, { allowDangerousHtml: true });
+
+ return {
+ async render(content, renderOpts) {
+ const vfile = new VFile({
+ value: content,
+ path: renderOpts?.fileURL,
+ data: {
+ astro: {
+ frontmatter: renderOpts?.frontmatter ?? {},
+ },
+ },
+ });
+
+ const result = 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}"`);
+ console.error(err);
+ throw err;
+ });
+
+ return {
+ code: String(result.value),
+ metadata: {
+ headings: result.data.astro?.headings ?? [],
+ localImagePaths: result.data.astro?.localImagePaths ?? [],
+ remoteImagePaths: result.data.astro?.remoteImagePaths ?? [],
+ frontmatter: result.data.astro?.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 {
+ // 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;
+}