diff options
Diffstat (limited to 'packages/markdown/remark/src')
-rw-r--r-- | packages/markdown/remark/src/index.ts | 13 | ||||
-rw-r--r-- | packages/markdown/remark/src/rehype-images.ts | 78 | ||||
-rw-r--r-- | packages/markdown/remark/src/remark-collect-images.ts | 32 | ||||
-rw-r--r-- | packages/markdown/remark/src/remark-content-rel-image-error.ts | 53 | ||||
-rw-r--r-- | packages/markdown/remark/src/types.ts | 6 |
5 files changed, 123 insertions, 59 deletions
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index 79b5b3ad5..9e230d75a 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -8,7 +8,7 @@ import type { import { toRemarkInitializeAstroData } from './frontmatter-injection.js'; import { loadPlugins } from './load-plugins.js'; import { rehypeHeadingIds } from './rehype-collect-headings.js'; -import toRemarkContentRelImageError from './remark-content-rel-image-error.js'; +import toRemarkCollectImages from './remark-collect-images.js'; import remarkPrism from './remark-prism.js'; import scopedStyles from './remark-scoped-styles.js'; import remarkShiki from './remark-shiki.js'; @@ -21,6 +21,7 @@ import markdownToHtml from 'remark-rehype'; import remarkSmartypants from 'remark-smartypants'; import { unified } from 'unified'; import { VFile } from 'vfile'; +import { rehypeImages } from './rehype-images.js'; export { rehypeHeadingIds } from './rehype-collect-headings.js'; export * from './types.js'; @@ -53,7 +54,6 @@ export async function renderMarkdown( remarkRehype = markdownConfigDefaults.remarkRehype, gfm = markdownConfigDefaults.gfm, smartypants = markdownConfigDefaults.smartypants, - contentDir, frontmatter: userFrontmatter = {}, } = opts; const input = new VFile({ value: content, path: fileURL }); @@ -89,8 +89,10 @@ export async function renderMarkdown( parser.use([remarkPrism(scopedClassName)]); } - // Apply later in case user plugins resolve relative image paths - parser.use([toRemarkContentRelImageError({ contentDir })]); + if (opts.experimentalAssets) { + // Apply later in case user plugins resolve relative image paths + parser.use([toRemarkCollectImages(opts.resolveImage)]); + } parser.use([ [ @@ -107,6 +109,9 @@ export async function renderMarkdown( parser.use([[plugin, pluginOpts]]); }); + if (opts.experimentalAssets) { + parser.use(rehypeImages(await opts.imageService, opts.assetsDir)); + } parser.use([rehypeHeadingIds, rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true }); let vfile: MarkdownVFile; diff --git a/packages/markdown/remark/src/rehype-images.ts b/packages/markdown/remark/src/rehype-images.ts new file mode 100644 index 000000000..f94960ba0 --- /dev/null +++ b/packages/markdown/remark/src/rehype-images.ts @@ -0,0 +1,78 @@ +import sizeOf from 'image-size'; +import { join as pathJoin } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { visit } from 'unist-util-visit'; +import { pathToFileURL } from 'url'; +import type { MarkdownVFile } from './types.js'; + +export function rehypeImages(imageService: any, assetsDir: URL | undefined) { + return () => + function (tree: any, file: MarkdownVFile) { + visit(tree, (node) => { + if (!assetsDir) return; + if (node.type !== 'element') return; + if (node.tagName !== 'img') return; + + if (node.properties?.src) { + if (file.dirname) { + if (!isRelativePath(node.properties.src) && !isAliasedPath(node.properties.src)) return; + + let fileURL: URL; + if (isAliasedPath(node.properties.src)) { + fileURL = new URL(stripAliasPath(node.properties.src), assetsDir); + } else { + fileURL = pathToFileURL(pathJoin(file.dirname, node.properties.src)); + } + + const fileData = sizeOf(fileURLToPath(fileURL)); + fileURL.searchParams.append('origWidth', fileData.width!.toString()); + fileURL.searchParams.append('origHeight', fileData.height!.toString()); + fileURL.searchParams.append('origFormat', fileData.type!.toString()); + + let options = { + src: { + src: fileURL, + width: fileData.width, + height: fileData.height, + format: fileData.type, + }, + alt: node.properties.alt, + }; + + const imageURL = imageService.getURL(options); + node.properties = Object.assign(node.properties, { + src: imageURL, + ...(imageService.getHTMLAttributes !== undefined + ? imageService.getHTMLAttributes(options) + : {}), + }); + } + } + }); + }; +} + +function isAliasedPath(path: string) { + return path.startsWith('~/assets'); +} + +function stripAliasPath(path: string) { + return path.replace('~/assets/', ''); +} + +function isRelativePath(path: string) { + return startsWithDotDotSlash(path) || startsWithDotSlash(path); +} + +function startsWithDotDotSlash(path: string) { + const c1 = path[0]; + const c2 = path[1]; + const c3 = path[2]; + return c1 === '.' && c2 === '.' && c3 === '/'; +} + +function startsWithDotSlash(path: string) { + const c1 = path[0]; + const c2 = path[1]; + return c1 === '.' && c2 === '/'; +} diff --git a/packages/markdown/remark/src/remark-collect-images.ts b/packages/markdown/remark/src/remark-collect-images.ts new file mode 100644 index 000000000..a9e769429 --- /dev/null +++ b/packages/markdown/remark/src/remark-collect-images.ts @@ -0,0 +1,32 @@ +import type { Image } from 'mdast'; +import { visit } from 'unist-util-visit'; +import type { VFile } from 'vfile'; + +type OptionalResolveImage = ((path: string) => Promise<string>) | undefined; + +export default function toRemarkCollectImages(resolveImage: OptionalResolveImage) { + return () => + async function (tree: any, vfile: VFile) { + if (typeof vfile?.path !== 'string') return; + + const imagePaths = new Set<string>(); + visit(tree, 'image', function raiseError(node: Image) { + imagePaths.add(node.url); + }); + if (imagePaths.size === 0) { + vfile.data.imagePaths = []; + return; + } else if(resolveImage) { + const mapping = new Map<string, string>(); + for(const path of Array.from(imagePaths)) { + const id = await resolveImage(path); + mapping.set(path, id); + } + visit(tree, 'image', function raiseError(node: Image) { + node.url = mapping.get(node.url)!; + }); + } + + vfile.data.imagePaths = Array.from(imagePaths); + }; +} diff --git a/packages/markdown/remark/src/remark-content-rel-image-error.ts b/packages/markdown/remark/src/remark-content-rel-image-error.ts deleted file mode 100644 index 3e3664b20..000000000 --- a/packages/markdown/remark/src/remark-content-rel-image-error.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Image } from 'mdast'; -import { visit } from 'unist-util-visit'; -import { pathToFileURL } from 'url'; -import type { VFile } from 'vfile'; - -/** - * `src/content/` does not support relative image paths. - * This plugin throws an error if any are found - */ -export default function toRemarkContentRelImageError({ contentDir }: { contentDir: URL }) { - return function remarkContentRelImageError() { - return (tree: any, vfile: VFile) => { - if (typeof vfile?.path !== 'string') return; - - const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href); - if (!isContentFile) return; - - const relImagePaths = new Set<string>(); - visit(tree, 'image', function raiseError(node: Image) { - if (isRelativePath(node.url)) { - relImagePaths.add(node.url); - } - }); - if (relImagePaths.size === 0) return; - - const errorMessage = - `Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files)\n` + - [...relImagePaths].map((path) => JSON.stringify(path)).join(',\n'); - - // Throw raw string to use `astro:markdown` default formatting - throw errorMessage; - }; - }; -} - -// Following utils taken from `packages/astro/src/core/path.ts`: - -function isRelativePath(path: string) { - return startsWithDotDotSlash(path) || startsWithDotSlash(path); -} - -function startsWithDotDotSlash(path: string) { - const c1 = path[0]; - const c2 = path[1]; - const c3 = path[2]; - return c1 === '.' && c2 === '.' && c3 === '/'; -} - -function startsWithDotSlash(path: string) { - const c1 = path[0]; - const c2 = path[1]; - return c1 === '.' && c2 === '/'; -} diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index ff3060704..38fe9fc74 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -58,10 +58,12 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions { $?: { scopedClassName: string | null; }; - /** Used to prevent relative image imports from `src/content/` */ - contentDir: URL; /** Used for frontmatter injection plugins */ frontmatter?: Record<string, any>; + experimentalAssets?: boolean; + imageService?: any; + assetsDir?: URL; + resolveImage?: (path: string) => Promise<string>; } export interface MarkdownHeading { |