1- import { FilePath , joinSegments , slugifyFilePath } from "../../util/path"
1+ import { FilePath , joinSegments , slugifyFilePath , getFileExtension } from "../../util/path"
22import { QuartzEmitterPlugin } from "../types"
33import path from "path"
44import fs from "fs"
5+ import sharp , { FitEnum } from "sharp"
6+ import { styleText } from "util"
57import { glob } from "../../util/glob"
68import { Argv } from "../../util/ctx"
79import { QuartzConfig } from "../../cfg"
810
11+ interface Options {
12+ resizeImages ?: { width ?: number ; height ?: number ; fit ?: keyof FitEnum }
13+ compressImages ?: boolean
14+ }
15+
16+ const defaultOptions : Options = {
17+ resizeImages : { width : 1700 , fit : "inside" } ,
18+ compressImages : true ,
19+ }
20+
21+ // Accepted Sharp formats https://sharp.pixelplumbing.com/#formats
22+ const imageExtensions = [ ".png" , ".jpg" , ".jpeg" , ".webp" , ".gif" , ".avif" , ".tif" , ".tiff" , ".svg" ]
23+
924const filesToCopy = async ( argv : Argv , cfg : QuartzConfig ) => {
1025 // glob all non MD files in content folder and copy it over
1126 return await glob ( "**" , argv . directory , [ "**/*.md" , ...cfg . configuration . ignorePatterns ] )
1227}
1328
14- const copyFile = async ( argv : Argv , fp : FilePath ) => {
29+ const copyFile = async ( argv : Argv , fp : FilePath , opts : Options ) => {
1530 const src = joinSegments ( argv . directory , fp ) as FilePath
16-
1731 const name = slugifyFilePath ( fp )
1832 const dest = joinSegments ( argv . output , name ) as FilePath
1933
2034 // ensure dir exists
2135 const dir = path . dirname ( dest ) as FilePath
2236 await fs . promises . mkdir ( dir , { recursive : true } )
2337
24- await fs . promises . copyFile ( src , dest )
38+ const ext = getFileExtension ( fp ) ?. toLowerCase ( )
39+ if ( ext && imageExtensions . includes ( ext ) ) {
40+ await copyImage ( src , dest , opts )
41+ } else {
42+ await copyBlob ( src , dest )
43+ }
44+
2545 return dest
2646}
2747
28- export const Assets : QuartzEmitterPlugin = ( ) => {
48+ const copyImage = async ( src : FilePath , dest : FilePath , opts : Options ) => {
49+ if ( ! opts . compressImages ) {
50+ await copyBlob ( src , dest )
51+ } else if ( opts . resizeImages ) {
52+ await sharp ( src ) . resize ( opts . resizeImages ) . toFile ( dest )
53+ } else {
54+ await sharp ( src ) . toFile ( dest )
55+ }
56+ }
57+
58+ const copyBlob = async ( src : FilePath , dest : FilePath ) => {
59+ await fs . promises . copyFile ( src , dest )
60+ }
61+
62+ export const Assets : QuartzEmitterPlugin < Partial < Options > > = ( opts ) => {
63+ opts = { ...defaultOptions , ...opts }
64+
65+ if ( ( opts . resizeImages ?. width || opts . resizeImages ?. height ) && ! opts . compressImages ) {
66+ console . warn (
67+ styleText (
68+ "yellow" ,
69+ "Your asset resizing options are incompatible - enable compression to resize images" ,
70+ ) ,
71+ )
72+ }
73+
2974 return {
3075 name : "Assets" ,
3176 async * emit ( { argv, cfg } ) {
3277 const fps = await filesToCopy ( argv , cfg )
3378 for ( const fp of fps ) {
34- yield copyFile ( argv , fp )
79+ yield copyFile ( argv , fp , opts )
3580 }
3681 } ,
3782 async * partialEmit ( ctx , _content , _resources , changeEvents ) {
@@ -40,7 +85,7 @@ export const Assets: QuartzEmitterPlugin = () => {
4085 if ( ext === ".md" ) continue
4186
4287 if ( changeEvent . type === "add" || changeEvent . type === "change" ) {
43- yield copyFile ( ctx . argv , changeEvent . path )
88+ yield copyFile ( ctx . argv , changeEvent . path , opts )
4489 } else if ( changeEvent . type === "delete" ) {
4590 const name = slugifyFilePath ( changeEvent . path )
4691 const dest = joinSegments ( ctx . argv . output , name ) as FilePath
0 commit comments