diff options
author | 2023-03-07 16:12:21 +0100 | |
---|---|---|
committer | 2023-03-07 16:12:21 +0100 | |
commit | 694918a56b01104831296be0c25456135a63c784 (patch) | |
tree | d4388a15eea1e59542f2f44df08ca0f0b483e545 /packages/markdown/remark/src | |
parent | 377530a810f6f581d60c80ca25b8f69909ddf9c9 (diff) | |
download | astro-694918a56b01104831296be0c25456135a63c784.tar.gz astro-694918a56b01104831296be0c25456135a63c784.tar.zst astro-694918a56b01104831296be0c25456135a63c784.zip |
Implement RFC "A core story for images" (#6344)
* feat(assets): Add Vite plugin
* feat(images): Set up Image component
* fix(types): Attempt to fix type generation
* Revert "fix(types): Attempt to fix type generation"
This reverts commit 063aa276e2f30f928a108a930f603a234e22ff11.
* fix(image): Fix image types causing build to fail
* feat(image): Implement client side part
* feat(services): Allow arbitrary transforms parameters
* fix(image): Fix paths and types
* config(types): Update config types to provide completions for available services
* feat(image): Add serving in dev
* feat(image): Improve type error messages
* refactor(image): Move sharp's parseParams to baseService
* refactor(image): Skip work in dev for remote servies
* feat(image): Add support for remote images
* feat(image): Add squoosh service
* chore: update export map
* refactor(image): Abstract attributes handling by services
* config(vercel): Remove test image service
* feat(image): Support for relative images in Markdown (WIP)
* feat(images): Add support for relative images in Markdown
* feat(image): Update with RFC feedback
* fix(image): Fix alt error on getImage
* feat(image): Add support for assets validation through content collections
* feat(image): Remove validateTransform
* feat(image): Move to assets folder
* fix(image): Fix package exports
* feat(image): Add static imports references to virtual moduel
* fix(image): Fix images from content collections not working when embedded
* chore: lockfile
* fix(markdown): Fix type
* fix(images): Flag enhanced images behing an experimental flag
* config(example): Update images example conifg
* fix(image): Fix types
* fix(image): Fix asset type for strict, allow arbritary input and output formats
* chore: fix example check
* feat(image): Emit assets for ESM imported images
* Add initial core image tests (#6381)
* feat(images): Make frontmatter extraction more generic than images for future
* feat(image): Add support for building
* fix(image): Fix types
* fix(images): Fix compatibility with image integration
* feat(images): Cuter generation stats
* fix(images): Globals are unsafe, it turns out
* fix(images): Only generate images if flag is enabled
* fix(images): Only create `addStaticImage` in build
* feat(images): Add SSR endpoint
* fix(images): Only inject route in SSR
* Add tests for SSR
* Remove console.log
* Updated lockfile
* rename to satisfy the link gods
* skip build tests for now
* fix(images): Fix WASM files not being copied in dev
* feat(images): Add quality presets
* fix build tests running
* Remove console.log
* Add tests for getImage
* Test local services
* Test the content collections API
* Add tests for quality
* Skipping content collections test
* feat(image): Add support for `~/assets` alias
* test(image): Add tests for aliases in dev
* Fix windows + content collections
* test(image): Add tests for aliased images and images in Markdown
* Fix markdown images being built
* Should be posix join
* Use the optimized image
* fix test
* Fixes windows smoke
* fix(image): Nits
* feat(images): Add automatic update for `env.d.ts` when experimental images are enabled
* fix(images): Revert env.d.ts change if the user opted-out of the experimental image support
* chore: remove bad image example project
* feat(image): Rename `experimental.images` to `experimental.assets`
* fix(images): Remove unused code in MDX integration
* chore: Remove unrelated change
* fix(images): Remove export from astro/components
* Fix, esm import on Win
* test(images): Add test for format
* fix(images): Add `client-image.d.ts` to export map
* chore: changeset
* fix(images): Adjust with feedback, no more automatic refine, asset() -> image()
* fix(images): Fix types
* fix(images): Remove unnecessary spread
* fix(images): Better types for parseUrl and transform
* fix(images): Fix types
* fix(images): Adjust from feedback
* fix(images): Pass width and height through getHTMLAttributes even if they're not added by the uesr
* fix(images): Recusirsively extract frontmatter assets
* fix(images): Use a reduce instead
* feat(images): Add support for data: URIs
* chore: changeset
* docs(images): Misc docs fixes
* Update .changeset/gold-rocks-cry.md
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update .changeset/gold-rocks-cry.md
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
* Update packages/astro/src/@types/astro.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/services/service.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/services/service.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/services/service.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/types.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
* Update packages/astro/src/assets/types.ts
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
---------
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Matthew Phillips <matthew@matthewphillips.info>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
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 { |