@@ -10,7 +10,7 @@ import type { PluginOption } from "vite";
1010import cp from "node:child_process" ;
1111import fs from "node:fs/promises" ;
1212import path from "node:path" ;
13- import nodeUrl from "node:url" ;
13+ import nodeURL from "node:url" ;
1414import util from "node:util" ;
1515
1616import { rehypeHeadingIds } from "@astrojs/markdown-remark" ;
@@ -20,14 +20,21 @@ import remarkA11yEmoji from "@fec/remark-a11y-emoji";
2020import { addExtension , createFilter , dataToEsm } from "@rollup/pluginutils" ;
2121import { transformerNotationDiff } from "@shikijs/transformers" ;
2222import compress from "astro-compress" ;
23- import { walk } from "estree-walker" ;
23+ import { walk as walkJS } from "estree-walker" ;
2424import findCacheDirectory from "find-cache-dir" ;
2525import gifsicle from "gifsicle" ;
2626import { customAlphabet } from "nanoid" ;
2727import rehypeAutolinkHeadings from "rehype-autolink-headings" ;
2828import rehypeClassNames from "rehype-class-names" ;
2929import rehypeExternalLinks from "rehype-external-links" ;
3030import sharp from "sharp" ;
31+ import {
32+ ELEMENT_NODE ,
33+ h as createElementNode ,
34+ parse as parseHTML ,
35+ render as renderHTML ,
36+ walk as walkHTML
37+ } from "ultrahtml" ;
3138sharp . cache ( false ) ;
3239
3340//==================================================
@@ -130,70 +137,82 @@ const externalLinksPlugin: [RehypePlugin, ExternalLinksOptions] = [
130137//==================================================
131138
132139const execFile = util . promisify ( cp . execFile ) ;
133- const optimizeImagesIntegration : AstroIntegration = {
134- name : "optimize-images " ,
140+ const optimizeMediaIntegration : AstroIntegration = {
141+ name : "optimize-media " ,
135142 hooks : {
136143 async "astro:build:done" ( { dir, pages } ) {
137- type Resize = "" | "up" | "down" ;
138- type Images = { original : string ; webp : string [ ] ; resize : Resize } [ ] ;
139- type MatchGroups = {
140- resize : Resize ;
141- preSrc : string ;
142- src : string ;
143- postSrc : string ;
144- } ;
145-
146- const distDir = nodeUrl . fileURLToPath ( dir ) ;
147- const images : Images = [ ] ;
148-
149- const resizeSuffixes = [ "a" , "b" , "c" , "d" , "e" ] ;
150- const resizeUpMultipliers = [ 1 , 1.5 , 2 , 3 , 4 ] ;
151- const resizeDownMultipliers = [ 0.25 , 0.375 , 0.5 , 0.75 , 1 ] ;
144+ const resizeValues = [ "" , "up" , "down" ] as const ;
145+ const resizeSuffixes = [ "a" , "b" , "c" , "d" , "e" ] as const ;
146+ const resizeUpMultipliers = [ 1 , 1.5 , 2 , 3 , 4 ] as const ;
147+ const resizeDownMultipliers = [ 0.25 , 0.375 , 0.5 , 0.75 , 1 ] as const ;
148+
149+ const distDir = nodeURL . fileURLToPath ( dir ) ;
150+ const images : {
151+ original : string ;
152+ webp : string [ ] ;
153+ resize : ( typeof resizeValues ) [ number ] ;
154+ } [ ] = [ ] ;
152155
153156 const htmlPaths = pages . map ( ( { pathname } ) =>
154157 path . join ( distDir , pathname , "index.html" )
155158 ) ;
156159 for ( const htmlPath of htmlPaths ) {
157- let html = await fs . readFile ( htmlPath , "utf-8" ) ;
158- const matches = html . matchAll (
159- / < i m g o p t i m i z e - i m a g e r e s i z e = " (?< resize > [ a - z - ] * ) " (?< preSrc > .+ ) s r c = " (?< src > [ ^ " ] + ) " (?< postSrc > .+ ) > / g
160- ) ;
161-
162- for ( const match of matches ) {
163- const { resize, preSrc, src, postSrc } = match . groups as MatchGroups ;
164- const extensionlessPath = src . slice ( 0 , src . lastIndexOf ( "." ) ) ;
165- let webp ;
166-
167- if ( resize === "" ) {
168- webp = [ `${ extensionlessPath } .webp` ] ;
169- } else {
170- webp = resizeSuffixes . map (
171- ( suffix ) => `${ extensionlessPath } ${ suffix } .webp`
172- ) ;
173- }
160+ const htmlAST = parseHTML ( await fs . readFile ( htmlPath , "utf-8" ) ) ;
161+ await walkHTML ( htmlAST , async ( node ) => {
162+ if (
163+ node . type === ELEMENT_NODE &&
164+ node . name === "picture" &&
165+ Object . keys ( node . attributes ) . includes ( "optimize-media" )
166+ ) {
167+ const img = node . children . find ( ( child ) => child . name === "img" ) ;
168+ if ( ! img || img . type !== ELEMENT_NODE ) {
169+ throw new Error ( "'picture' must have an 'img' child." ) ;
170+ }
174171
175- images . push ( { original : src , webp, resize } ) ;
172+ const resize = node . attributes [
173+ "optimize-media-resize"
174+ ] as ( typeof resizeValues ) [ number ] ;
175+ if ( ! resizeValues . includes ( resize ) ) {
176+ throw new Error ( "'resize' attribute must be set." ) ;
177+ }
178+
179+ const { src } = img . attributes ;
180+ if ( typeof src !== "string" ) {
181+ throw new Error ( "'src' attribute must be set." ) ;
182+ }
176183
177- html = html . replace (
178- match [ 0 ] ,
179- `
180- <source srcset="${
184+ const extensionlessPath = src . slice ( 0 , src . lastIndexOf ( "." ) ) ;
185+ let webp ;
186+ if ( resize === "" ) {
187+ webp = [ `${ extensionlessPath } .webp` ] ;
188+ } else {
189+ webp = resizeSuffixes . map (
190+ ( suffix ) => `${ extensionlessPath } ${ suffix } .webp`
191+ ) ;
192+ }
193+
194+ images . push ( { original : src , webp, resize } ) ;
195+
196+ const source = createElementNode ( "source" , {
197+ type : "image/webp" ,
198+ srcset :
181199 webp . length === 1
182200 ? webp [ 0 ]
183201 : webp . map (
184202 ( filePath , i ) => `${ filePath } ${ resizeUpMultipliers [ i ] } x`
185203 )
186- } " type="image/webp">
187- <img ${ preSrc } src="${ src } " ${ postSrc } >
188- `
189- ) ;
190- }
204+ } ) ;
191205
192- await fs . writeFile ( htmlPath , html , "utf-8" ) ;
206+ node . children = [ source , img ] ;
207+ delete node . attributes [ "optimize-media" ] ;
208+ delete node . attributes [ "optimize-media-resize" ] ;
209+ }
210+ } ) ;
211+ await fs . writeFile ( htmlPath , await renderHTML ( htmlAST ) , "utf-8" ) ;
193212 }
194213
195214 const cacheDir = findCacheDirectory ( {
196- name : "astro-optimize-images " ,
215+ name : "astro-optimize-media " ,
197216 create : true
198217 } ) as string ;
199218 const sharpOptions = { limitInputPixels : false , unlimited : true } ;
@@ -390,7 +409,7 @@ const generateIdsPlugin: PluginOption = {
390409
391410 const data = { } ;
392411
393- walk ( this . parse ( code ) , {
412+ walkJS ( this . parse ( code ) , {
394413 enter ( node , parent ) {
395414 if (
396415 node . type === "VariableDeclaration" &&
@@ -447,6 +466,6 @@ export default <AstroUserConfig>{
447466 externalLinksPlugin
448467 ]
449468 } ,
450- integrations : [ mdx ( ) , optimizeImagesIntegration , compress ( { Image : false } ) ] ,
469+ integrations : [ mdx ( ) , optimizeMediaIntegration , compress ( { Image : false } ) ] ,
451470 vite : { css : { preprocessorOptions : { scss } } , plugins : [ generateIdsPlugin ] }
452471} ;
0 commit comments