Skip to content

Commit 66aaf4e

Browse files
committed
refactor: update LQIP generation with enhanced utility functions and type definitions
1 parent 0fd4750 commit 66aaf4e

File tree

11 files changed

+134
-116
lines changed

11 files changed

+134
-116
lines changed

src/components/Picture.astro

Lines changed: 10 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,27 @@
11
---
2-
import { join } from 'node:path'
3-
import { readFile } from 'node:fs/promises'
2+
import type { Props } from '../types'
43
5-
import type { GetPlaiceholderReturn } from 'plaiceholder'
6-
import { getPlaiceholder } from 'plaiceholder'
4+
import { resolveImageMetadata } from '../utils/resolveImageMetadata'
5+
import { renderSVGNode } from '../utils/renderSVGNode'
6+
import { getLqipStyle } from '../utils/getLqipStyle'
7+
import { getLqip } from '../utils/getLqip'
78
8-
import type { Props as PictureProps } from 'astro/components/Picture.astro'
99
import { Picture as PictureComponent } from 'astro:assets'
1010
11-
type GetSVGReturn = GetPlaiceholderReturn['svg']
12-
type LqipType = 'color' | 'css' | 'base64' | 'svg'
13-
type Props = PictureProps & {
14-
lqip?: LqipType
15-
}
16-
1711
const { class: className, lqip = 'base64', ...props } = Astro.props as Props
1812
19-
const PREFIX = '[astro-lqip]'
13+
const isDevelopment = import.meta.env.MODE === 'development'
2014
2115
const imageMetadata = await resolveImageMetadata(props.src)
22-
const lqipImage = await getLqip(imageMetadata, lqip)
16+
const lqipImage = await getLqip(imageMetadata, isDevelopment, lqip)
2317
2418
let svgHTML = ''
2519
26-
if (lqip === 'svg') {
27-
function styleToString(style: Record<string, string>) {
28-
return Object.entries(style)
29-
.map(([key, val]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}:${val}`)
30-
.join(';')
31-
}
32-
33-
function renderNode([tag, attrs, children]: [string, Record<string, any>, any[]]): string {
34-
let attrString = ''
35-
for (const [k, v] of Object.entries(attrs || {})) {
36-
if (k === 'style') {
37-
attrString += ` style="${styleToString(v)}"`
38-
} else {
39-
attrString += ` ${k}="${v}"`
40-
}
41-
}
42-
43-
if (children && children.length > 0) {
44-
return `<${tag}${attrString}>${children.map(renderNode).join('')}</${tag}>`
45-
} else {
46-
return `<${tag}${attrString} />`
47-
}
48-
}
49-
50-
if (Array.isArray(lqipImage)) {
51-
svgHTML = renderNode(lqipImage as [string, Record<string, any>, any[]])
52-
}
53-
}
54-
55-
async function resolveImageMetadata(src: any) {
56-
if (typeof src === 'string') return null
57-
if ('then' in src && typeof src.then === 'function') return (await src).default
58-
if ('src' in src) return src
59-
return null
60-
}
61-
62-
async function tryGenerateLqip(filePath: string, errorPrefix: string, isDevelopment: boolean, lqipType: LqipType) {
63-
try {
64-
const buffer = await readFile(filePath)
65-
const result = await getPlaiceholder(buffer)
66-
let lqipValue: string | GetSVGReturn | undefined
67-
68-
switch (lqipType) {
69-
case 'color':
70-
lqipValue = result.color?.hex
71-
break
72-
case 'css':
73-
lqipValue = typeof result.css === 'object' && result.css.backgroundImage
74-
? result.css.backgroundImage
75-
: String(result.css)
76-
break
77-
case 'svg':
78-
lqipValue = result.svg
79-
break
80-
case 'base64':
81-
default:
82-
lqipValue = result.base64
83-
break
84-
}
85-
86-
if (isDevelopment) {
87-
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated!`)
88-
} else {
89-
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, filePath)
90-
}
91-
return lqipValue
92-
} catch (err) {
93-
console.error(`${errorPrefix} Error generating LQIP (${lqipType}) in:`, filePath, '\n', err)
94-
return undefined
95-
}
96-
}
97-
98-
async function getLqip(imageMetadata: any, lqipType: LqipType) {
99-
if (!imageMetadata?.src) return undefined
100-
101-
const isDevelopment = import.meta.env.MODE === 'development'
102-
103-
if (isDevelopment && imageMetadata.src.startsWith('/@fs/')) {
104-
const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0]
105-
return await tryGenerateLqip(filePath, PREFIX, isDevelopment, lqipType)
106-
}
107-
108-
if (!isDevelopment && imageMetadata.src.startsWith('/_astro/')) {
109-
const buildPath = join(process.cwd(), 'dist', imageMetadata.src)
110-
return await tryGenerateLqip(buildPath, PREFIX, isDevelopment, lqipType)
111-
}
112-
}
113-
114-
function getLqipStyle(lqipType: LqipType, lqipImage: string | GetSVGReturn | undefined) {
115-
if (!lqipImage) return {}
116-
117-
switch (lqipType) {
118-
case 'css':
119-
return { '--lqip-background': lqipImage }
120-
case 'svg':
121-
return { '--lqip-background': `url('data:image/svg+xml;utf8,${encodeURIComponent(svgHTML)}')` }
122-
case 'color':
123-
return { '--lqip-background': lqipImage }
124-
case 'base64':
125-
default:
126-
return { '--lqip-background': `url('${lqipImage}')` }
127-
}
20+
if (lqip === 'svg' && Array.isArray(lqipImage)) {
21+
svgHTML = renderSVGNode(lqipImage as [string, Record<string, any>, any[]])
12822
}
12923
130-
const lqipStyle = getLqipStyle(lqip, lqipImage)
24+
const lqipStyle = getLqipStyle(lqip, lqipImage, svgHTML)
13125
---
13226

13327
<style is:inline>

src/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const PREFIX = '[astro-lqip]'

src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './plaiceholder.type'
2+
export * from './lqip.type'
3+
export * from './props.type'

src/types/lqip.type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type LqipType = 'color' | 'css' | 'base64' | 'svg'

src/types/plaiceholder.type.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { GetPlaiceholderReturn } from 'plaiceholder'
2+
3+
export type GetSVGReturn = GetPlaiceholderReturn['svg']

src/types/props.type.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Props as PictureProps } from 'astro/components/Picture.astro'
2+
3+
import type { LqipType } from './lqip.type'
4+
5+
export type Props = PictureProps & {
6+
lqip?: LqipType
7+
}

src/utils/generateLqip.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { readFile } from 'node:fs/promises'
2+
3+
import type { GetSVGReturn, LqipType } from '../types'
4+
5+
import { PREFIX } from '../constants'
6+
7+
import { getPlaiceholder } from 'plaiceholder'
8+
9+
export async function generateLqip(filePath: string, isDevelopment: boolean, lqipType: LqipType) {
10+
try {
11+
const buffer = await readFile(filePath)
12+
const plaiceholderResult = await getPlaiceholder(buffer)
13+
let lqipValue: string | GetSVGReturn | undefined
14+
15+
switch (lqipType) {
16+
case 'color':
17+
lqipValue = plaiceholderResult.color?.hex
18+
break
19+
case 'css':
20+
lqipValue = typeof plaiceholderResult.css === 'object' && plaiceholderResult.css.backgroundImage
21+
? plaiceholderResult.css.backgroundImage
22+
: String(plaiceholderResult.css)
23+
break
24+
case 'svg':
25+
lqipValue = plaiceholderResult.svg
26+
break
27+
case 'base64':
28+
default:
29+
lqipValue = plaiceholderResult.base64
30+
break
31+
}
32+
33+
if (isDevelopment) {
34+
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated!`)
35+
} else {
36+
console.log(`${PREFIX} LQIP (${lqipType}) successfully generated for:`, filePath)
37+
}
38+
39+
return lqipValue
40+
} catch (err) {
41+
console.error(`${PREFIX} Error generating LQIP (${lqipType}) in:`, filePath, '\n', err)
42+
return undefined
43+
}
44+
}

src/utils/getLqip.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { join } from 'node:path'
2+
3+
import type { ImageMetadata } from 'astro'
4+
import type { LqipType } from '../types'
5+
6+
import { generateLqip } from './generateLqip'
7+
8+
export async function getLqip(imageMetadata: ImageMetadata, envMode: boolean, lqipType: LqipType) {
9+
if (!imageMetadata?.src) return undefined
10+
11+
if (envMode && imageMetadata.src.startsWith('/@fs/')) {
12+
const filePath = imageMetadata.src.replace(/^\/@fs/, '').split('?')[0]
13+
return await generateLqip(filePath, envMode, lqipType)
14+
}
15+
16+
if (!envMode && imageMetadata.src.startsWith('/_astro/')) {
17+
const buildPath = join(process.cwd(), 'dist', imageMetadata.src)
18+
return await generateLqip(buildPath, envMode, lqipType)
19+
}
20+
}

src/utils/getLqipStyle.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { GetSVGReturn, LqipType } from '../types'
2+
3+
export function getLqipStyle(lqipType: LqipType, lqipImage: string | GetSVGReturn | undefined, svgHTML: string = '') {
4+
if (!lqipImage) return {}
5+
6+
switch (lqipType) {
7+
case 'css':
8+
return { '--lqip-background': lqipImage }
9+
case 'svg':
10+
return { '--lqip-background': `url('data:image/svg+xml;utf8,${encodeURIComponent(svgHTML)}')` }
11+
case 'color':
12+
return { '--lqip-background': lqipImage }
13+
case 'base64':
14+
default:
15+
return { '--lqip-background': `url('${lqipImage}')` }
16+
}
17+
}

src/utils/renderSVGNode.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
function styleToString(style: Record<string, string>) {
2+
return Object.entries(style)
3+
.map(([key, val]) => `${key.replace(/([A-Z])/g, '-$1').toLowerCase()}:${val}`)
4+
.join(';')
5+
}
6+
7+
export function renderSVGNode([tag, attrs, children]: [string, Record<string, any>, any[]]): string {
8+
let attrString = ''
9+
for (const [k, v] of Object.entries(attrs || {})) {
10+
if (k === 'style') {
11+
attrString += ` style="${styleToString(v)}"`
12+
} else {
13+
attrString += ` ${k}="${v}"`
14+
}
15+
}
16+
17+
if (children && children.length > 0) {
18+
return `<${tag}${attrString}>${children.map(renderSVGNode).join('')}</${tag}>`
19+
} else {
20+
return `<${tag}${attrString} />`
21+
}
22+
}

0 commit comments

Comments
 (0)