diff options
Diffstat (limited to 'packages/markdown/remark/src')
-rw-r--r-- | packages/markdown/remark/src/frontmatter-injection.ts | 7 | ||||
-rw-r--r-- | packages/markdown/remark/src/index.ts | 157 | ||||
-rw-r--r-- | packages/markdown/remark/src/internal.ts | 1 | ||||
-rw-r--r-- | packages/markdown/remark/src/load-plugins.ts | 2 | ||||
-rw-r--r-- | packages/markdown/remark/src/types.ts | 22 |
5 files changed, 132 insertions, 57 deletions
diff --git a/packages/markdown/remark/src/frontmatter-injection.ts b/packages/markdown/remark/src/frontmatter-injection.ts index db1a2b704..4f5118ece 100644 --- a/packages/markdown/remark/src/frontmatter-injection.ts +++ b/packages/markdown/remark/src/frontmatter-injection.ts @@ -27,6 +27,13 @@ export function safelyGetAstroData(vfileData: Data): MarkdownAstroData | Invalid return astro; } +export function setAstroData(vfileData: Data, astroData: MarkdownAstroData) { + vfileData.astro = astroData; +} + +/** + * @deprecated Use `setAstroData` instead + */ export function toRemarkInitializeAstroData({ userFrontmatter, }: { diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index d81d1702e..41d08ec9a 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -1,11 +1,16 @@ import type { AstroMarkdownOptions, + MarkdownProcessor, MarkdownRenderingOptions, MarkdownRenderingResult, MarkdownVFile, } from './types.js'; -import { toRemarkInitializeAstroData } from './frontmatter-injection.js'; +import { + InvalidAstroDataError, + safelyGetAstroData, + setAstroData, +} from './frontmatter-injection.js'; import { loadPlugins } from './load-plugins.js'; import { rehypeHeadingIds } from './rehype-collect-headings.js'; import { remarkCollectImages } from './remark-collect-images.js'; @@ -15,13 +20,14 @@ import { remarkShiki } from './remark-shiki.js'; import rehypeRaw from 'rehype-raw'; import rehypeStringify from 'rehype-stringify'; import remarkGfm from 'remark-gfm'; -import markdown from 'remark-parse'; -import markdownToHtml from 'remark-rehype'; +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 } from './frontmatter-injection.js'; export { rehypeHeadingIds } from './rehype-collect-headings.js'; export { remarkCollectImages } from './remark-collect-images.js'; export { remarkPrism } from './remark-prism.js'; @@ -45,30 +51,29 @@ export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'draft // Skip nonessential plugins during performance benchmark runs const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK); -/** Shared utility for rendering markdown */ -export async function renderMarkdown( - content: string, - opts: MarkdownRenderingOptions -): Promise<MarkdownRenderingResult> { - let { - fileURL, +/** + * 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 = markdownConfigDefaults.remarkRehype, + remarkRehype: remarkRehypeOptions = markdownConfigDefaults.remarkRehype, gfm = markdownConfigDefaults.gfm, smartypants = markdownConfigDefaults.smartypants, - frontmatter: userFrontmatter = {}, - } = opts; - const input = new VFile({ value: content, path: fileURL }); + } = opts ?? {}; + + const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins)); + const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins)); - let parser = unified() - .use(markdown) - .use(toRemarkInitializeAstroData({ userFrontmatter })) - .use([]); + const parser = unified().use(remarkParse); - if (!isPerformanceBenchmark && gfm) { + // gfm and smartypants + if (!isPerformanceBenchmark) { if (gfm) { parser.use(remarkGfm); } @@ -77,14 +82,13 @@ export async function renderMarkdown( } } - const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins)); - const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins)); - - loadedRemarkPlugins.forEach(([plugin, pluginOpts]) => { - parser.use([[plugin, pluginOpts]]); - }); + // 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') { @@ -95,45 +99,88 @@ export async function renderMarkdown( parser.use(remarkCollectImages); } - parser.use([ - [ - markdownToHtml as any, - { - allowDangerousHtml: true, - passThrough: [], - ...remarkRehype, - }, - ], - ]); - - loadedRehypePlugins.forEach(([plugin, pluginOpts]) => { - parser.use([[plugin, pluginOpts]]); + // 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]); + parser.use(rehypeHeadingIds); } - parser.use([rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true }); + // Stringify to HTML + parser.use(rehypeRaw).use(rehypeStringify, { allowDangerousHtml: true }); - let vfile: MarkdownVFile; - try { - vfile = await parser.process(input); - } 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 "${input.path}"`); - // eslint-disable-next-line no-console - console.error(err); - throw err; - } + return { + async render(content, renderOpts) { + const vfile = new VFile({ value: content, path: renderOpts?.fileURL }); + setAstroData(vfile.data, { frontmatter: 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, + }); - const headings = vfile?.data.__astroHeadings || []; return { - metadata: { headings, source: content, html: String(vfile.value) }, - code: String(vfile.value), - vfile, + code: result.code, + metadata: { + headings: result.metadata.headings, + source: content, + html: result.code, + }, + vfile: (result as any).__renderMarkdownCompat.result, }; } diff --git a/packages/markdown/remark/src/internal.ts b/packages/markdown/remark/src/internal.ts index 0ab7e34bb..a0f344a3a 100644 --- a/packages/markdown/remark/src/internal.ts +++ b/packages/markdown/remark/src/internal.ts @@ -1,5 +1,6 @@ export { InvalidAstroDataError, safelyGetAstroData, + setAstroData, toRemarkInitializeAstroData, } from './frontmatter-injection.js'; diff --git a/packages/markdown/remark/src/load-plugins.ts b/packages/markdown/remark/src/load-plugins.ts index de6601e7d..8229ddff2 100644 --- a/packages/markdown/remark/src/load-plugins.ts +++ b/packages/markdown/remark/src/load-plugins.ts @@ -14,7 +14,7 @@ async function importPlugin(p: string | unified.Plugin): Promise<unified.Plugin> } catch {} // Try import from user project - const resolved = await importMetaResolve(p, cwdUrlStr); + const resolved = importMetaResolve(p, cwdUrlStr); const importResult = await import(resolved); return importResult.default; } diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index caeebec93..bcab97041 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -58,13 +58,33 @@ export interface ImageMetadata { type: string; } -export interface MarkdownRenderingOptions extends AstroMarkdownOptions { +export interface MarkdownProcessor { + render: ( + content: string, + opts?: MarkdownProcessorRenderOptions + ) => Promise<MarkdownProcessorRenderResult>; +} + +export interface MarkdownProcessorRenderOptions { /** @internal */ fileURL?: URL; /** Used for frontmatter injection plugins */ frontmatter?: Record<string, any>; } +export interface MarkdownProcessorRenderResult { + code: string; + metadata: { + headings: MarkdownHeading[]; + imagePaths: Set<string>; + frontmatter: Record<string, any>; + }; +} + +export interface MarkdownRenderingOptions + extends AstroMarkdownOptions, + MarkdownProcessorRenderOptions {} + export interface MarkdownHeading { depth: number; slug: string; |