diff options
Diffstat (limited to 'packages/markdown/remark/src')
-rw-r--r-- | packages/markdown/remark/src/index.ts | 15 | ||||
-rw-r--r-- | packages/markdown/remark/src/rehype-images.ts | 54 | ||||
-rw-r--r-- | packages/markdown/remark/src/remark-collect-images.ts | 56 | ||||
-rw-r--r-- | packages/markdown/remark/src/types.ts | 20 |
4 files changed, 92 insertions, 53 deletions
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index de13523fe..d1b6035e4 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -1,4 +1,8 @@ -import type { AstroMarkdownOptions, MarkdownProcessor } from './types.js'; +import type { + AstroMarkdownOptions, + AstroMarkdownProcessorOptions, + MarkdownProcessor, +} from './types.js'; import { loadPlugins } from './load-plugins.js'; import { rehypeHeadingIds } from './rehype-collect-headings.js'; @@ -59,7 +63,7 @@ const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK); * Create a markdown preprocessor to render multiple markdown files */ export async function createMarkdownProcessor( - opts?: AstroMarkdownOptions, + opts?: AstroMarkdownProcessorOptions, ): Promise<MarkdownProcessor> { const { syntaxHighlight = markdownConfigDefaults.syntaxHighlight, @@ -93,7 +97,7 @@ export async function createMarkdownProcessor( if (!isPerformanceBenchmark) { // Apply later in case user plugins resolve relative image paths - parser.use(remarkCollectImages); + parser.use(remarkCollectImages, opts?.image); } // Remark -> Rehype @@ -118,7 +122,7 @@ export async function createMarkdownProcessor( } // Images / Assets support - parser.use(rehypeImages()); + parser.use(rehypeImages); // Headings if (!isPerformanceBenchmark) { @@ -152,7 +156,8 @@ export async function createMarkdownProcessor( code: String(result.value), metadata: { headings: result.data.astro?.headings ?? [], - imagePaths: result.data.astro?.imagePaths ?? [], + localImagePaths: result.data.astro?.localImagePaths ?? [], + remoteImagePaths: result.data.astro?.remoteImagePaths ?? [], frontmatter: result.data.astro?.frontmatter ?? {}, }, }; diff --git a/packages/markdown/remark/src/rehype-images.ts b/packages/markdown/remark/src/rehype-images.ts index 11d33df9c..92043b5e3 100644 --- a/packages/markdown/remark/src/rehype-images.ts +++ b/packages/markdown/remark/src/rehype-images.ts @@ -1,32 +1,44 @@ +import type { Properties, Root } from 'hast'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; export function rehypeImages() { - return () => - function (tree: any, file: VFile) { - const imageOccurrenceMap = new Map(); + return function (tree: Root, file: VFile) { + if (!file.data.astro?.localImagePaths?.length && !file.data.astro?.remoteImagePaths?.length) { + // No images to transform, nothing to do. + return; + } - visit(tree, (node) => { - if (node.type !== 'element') return; - if (node.tagName !== 'img') return; + const imageOccurrenceMap = new Map(); - if (node.properties?.src) { - node.properties.src = decodeURI(node.properties.src); + visit(tree, 'element', (node) => { + if (node.tagName !== 'img') return; + if (typeof node.properties?.src !== 'string') return; - if (file.data.astro?.imagePaths?.includes(node.properties.src)) { - const { ...props } = node.properties; + const src = decodeURI(node.properties.src); + let newProperties: Properties; - // Initialize or increment occurrence count for this image - const index = imageOccurrenceMap.get(node.properties.src) || 0; - imageOccurrenceMap.set(node.properties.src, index + 1); + if (file.data.astro?.localImagePaths?.includes(src)) { + // Override the original `src` with the new, decoded `src` that Astro will better understand. + newProperties = { ...node.properties, src }; + } else if (file.data.astro?.remoteImagePaths?.includes(src)) { + newProperties = { + // By default, markdown images won't have width and height set. However, just in case another user plugin does set these, we should respect them. + inferSize: 'width' in node.properties && 'height' in node.properties ? undefined : true, + ...node.properties, + src, + }; + } else { + // Not in localImagePaths or remoteImagePaths, we should not transform. + return; + } - node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ ...props, index }); + // Initialize or increment occurrence count for this image + const index = imageOccurrenceMap.get(node.properties.src) || 0; + imageOccurrenceMap.set(node.properties.src, index + 1); - Object.keys(props).forEach((prop) => { - delete node.properties[prop]; - }); - } - } - }); - }; + // Set a special property on the image so later Astro code knows to process this image. + node.properties = { __ASTRO_IMAGE_: JSON.stringify({ ...newProperties, index }) }; + }); + }; } diff --git a/packages/markdown/remark/src/remark-collect-images.ts b/packages/markdown/remark/src/remark-collect-images.ts index f09f1c580..062fabc45 100644 --- a/packages/markdown/remark/src/remark-collect-images.ts +++ b/packages/markdown/remark/src/remark-collect-images.ts @@ -1,42 +1,48 @@ -import type { Image, ImageReference } from 'mdast'; +import type { Root } from 'mdast'; import { definitions } from 'mdast-util-definitions'; import { visit } from 'unist-util-visit'; import type { VFile } from 'vfile'; +import { isRemoteAllowed } from '@astrojs/internal-helpers/remote'; +import type { AstroMarkdownProcessorOptions } from './types.js'; -export function remarkCollectImages() { - return function (tree: any, vfile: VFile) { +export function remarkCollectImages(opts: AstroMarkdownProcessorOptions['image']) { + const domains = opts?.domains ?? []; + const remotePatterns = opts?.remotePatterns ?? []; + + return function (tree: Root, vfile: VFile) { if (typeof vfile?.path !== 'string') return; const definition = definitions(tree); - const imagePaths = new Set<string>(); - visit(tree, ['image', 'imageReference'], (node: Image | ImageReference) => { + const localImagePaths = new Set<string>(); + const remoteImagePaths = new Set<string>(); + visit(tree, (node) => { + let url: string | undefined; if (node.type === 'image') { - if (shouldOptimizeImage(node.url)) imagePaths.add(decodeURI(node.url)); - } - if (node.type === 'imageReference') { + url = decodeURI(node.url); + } else if (node.type === 'imageReference') { const imageDefinition = definition(node.identifier); if (imageDefinition) { - if (shouldOptimizeImage(imageDefinition.url)) - imagePaths.add(decodeURI(imageDefinition.url)); + url = decodeURI(imageDefinition.url); + } + } + + if (!url) return; + + if (URL.canParse(url)) { + if (isRemoteAllowed(url, { domains, remotePatterns })) { + remoteImagePaths.add(url); } + } else if (!url.startsWith('/')) { + // If: + // + not a valid URL + // + AND not an absolute path + // Then it's a local image. + localImagePaths.add(url); } }); vfile.data.astro ??= {}; - vfile.data.astro.imagePaths = Array.from(imagePaths); + vfile.data.astro.localImagePaths = Array.from(localImagePaths); + vfile.data.astro.remoteImagePaths = Array.from(remoteImagePaths); }; } - -function shouldOptimizeImage(src: string) { - // Optimize anything that is NOT external or an absolute path to `public/` - return !isValidUrl(src) && !src.startsWith('/'); -} - -function isValidUrl(str: string): boolean { - try { - new URL(str); - return true; - } catch { - return false; - } -} diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index e6a9d362b..91dd00607 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -3,6 +3,7 @@ import type * as mdast from 'mdast'; import type { Options as RemarkRehypeOptions } from 'remark-rehype'; import type { BuiltinTheme } from 'shiki'; import type * as unified from 'unified'; +import type { RemotePattern } from '@astrojs/internal-helpers/remote'; import type { CreateShikiHighlighterOptions, ShikiHighlighterHighlightOptions } from './shiki.js'; export type { Node } from 'unist'; @@ -11,7 +12,8 @@ declare module 'vfile' { interface DataMap { astro: { headings?: MarkdownHeading[]; - imagePaths?: string[]; + localImagePaths?: string[]; + remoteImagePaths?: string[]; frontmatter?: Record<string, any>; }; } @@ -39,6 +41,9 @@ export interface ShikiConfig extends Pick<CreateShikiHighlighterOptions, 'langs' | 'theme' | 'themes' | 'langAlias'>, Pick<ShikiHighlighterHighlightOptions, 'defaultColor' | 'wrap' | 'transformers'> {} +/** + * Configuration options that end up in the markdown section of AstroConfig + */ export interface AstroMarkdownOptions { syntaxHighlight?: 'shiki' | 'prism' | false; shikiConfig?: ShikiConfig; @@ -49,6 +54,16 @@ export interface AstroMarkdownOptions { smartypants?: boolean; } +/** + * Extra configuration options from other parts of AstroConfig that get injected into this plugin + */ +export interface AstroMarkdownProcessorOptions extends AstroMarkdownOptions { + image?: { + domains?: string[]; + remotePatterns?: RemotePattern[]; + }; +} + export interface MarkdownProcessor { render: ( content: string, @@ -67,7 +82,8 @@ export interface MarkdownProcessorRenderResult { code: string; metadata: { headings: MarkdownHeading[]; - imagePaths: string[]; + localImagePaths: string[]; + remoteImagePaths: string[]; frontmatter: Record<string, any>; }; } |