Skip to content

Commit 8c42be0

Browse files
committed
refactor: refactor async/await functions and filenames
1 parent 2ce6080 commit 8c42be0

File tree

14 files changed

+332
-396
lines changed

14 files changed

+332
-396
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Options } from './options'
1+
import { Options } from './types'
22

3-
export function applyStyleFromOptions<T extends HTMLElement>(
3+
export function applyStyle<T extends HTMLElement>(
44
node: T,
55
options: Options,
66
): T {

src/cloneNode.ts renamed to src/clone-node.ts

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Options } from './options'
2-
import { getBlobFromURL } from './getBlobFromURL'
3-
import { clonePseudoElements } from './clonePseudoElements'
4-
import { createImage, getMimeType, makeDataUrl, toArray } from './util'
1+
import type { Options } from './types'
2+
import { getMimeType } from './mimes'
3+
import { resourceToDataURL } from './dataurl'
4+
import { clonePseudoElements } from './clone-pseudos'
5+
import { createImage, toArray } from './util'
56

67
async function cloneCanvasElement(canvas: HTMLCanvasElement) {
78
const dataURL = canvas.toDataURL()
@@ -14,11 +15,8 @@ async function cloneCanvasElement(canvas: HTMLCanvasElement) {
1415

1516
async function cloneVideoElement(video: HTMLVideoElement, options: Options) {
1617
const poster = video.poster
17-
const metadata = await getBlobFromURL(poster, options)
18-
const dataURL = makeDataUrl(
19-
metadata.blob,
20-
getMimeType(poster) || metadata.contentType,
21-
)
18+
const contentType = getMimeType(poster)
19+
const dataURL = await resourceToDataURL(poster, contentType, options)
2220
return createImage(dataURL)
2321
}
2422

@@ -54,29 +52,28 @@ async function cloneChildren<T extends HTMLElement>(
5452
return clonedNode
5553
}
5654

57-
return children
58-
.reduce(
59-
(deferred, child) =>
60-
deferred
61-
.then(() => cloneNode(child, options))
62-
.then((clonedChild: HTMLElement | null) => {
63-
if (clonedChild) {
64-
clonedNode.appendChild(clonedChild)
65-
}
66-
}),
67-
Promise.resolve(),
68-
)
69-
.then(() => clonedNode)
55+
await children.reduce(
56+
(deferred, child) =>
57+
deferred
58+
.then(() => cloneNode(child, options))
59+
.then((clonedChild: HTMLElement | null) => {
60+
if (clonedChild) {
61+
clonedNode.appendChild(clonedChild)
62+
}
63+
}),
64+
Promise.resolve(),
65+
)
66+
67+
return clonedNode
7068
}
7169

7270
function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
73-
const sourceStyle = window.getComputedStyle(nativeNode)
7471
const targetStyle = clonedNode.style
75-
7672
if (!targetStyle) {
7773
return
7874
}
7975

76+
const sourceStyle = window.getComputedStyle(nativeNode)
8077
if (sourceStyle.cssText) {
8178
targetStyle.cssText = sourceStyle.cssText
8279
targetStyle.transformOrigin = sourceStyle.transformOrigin
@@ -121,15 +118,13 @@ function cloneSelectValue<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
121118
}
122119

123120
function decorate<T extends HTMLElement>(nativeNode: T, clonedNode: T): T {
124-
if (!(clonedNode instanceof Element)) {
125-
return clonedNode
121+
if (clonedNode instanceof Element) {
122+
cloneCSSStyle(nativeNode, clonedNode)
123+
clonePseudoElements(nativeNode, clonedNode)
124+
cloneInputValue(nativeNode, clonedNode)
125+
cloneSelectValue(nativeNode, clonedNode)
126126
}
127127

128-
cloneCSSStyle(nativeNode, clonedNode)
129-
clonePseudoElements(nativeNode, clonedNode)
130-
cloneInputValue(nativeNode, clonedNode)
131-
cloneSelectValue(nativeNode, clonedNode)
132-
133128
return clonedNode
134129
}
135130

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ function clonePseudoElement<T extends HTMLElement>(
4343
}
4444

4545
const className = uuid()
46-
4746
try {
4847
clonedNode.className = `${clonedNode.className} ${className}`
4948
} catch (err) {

src/dataurl.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Options } from './types'
2+
3+
function getContentFromDataUrl(dataURL: string) {
4+
return dataURL.split(/,/)[1]
5+
}
6+
7+
export function isDataUrl(url: string) {
8+
return url.search(/^(data:)/) !== -1
9+
}
10+
11+
export function makeDataUrl(content: string, mimeType: string) {
12+
return `data:${mimeType};base64,${content}`
13+
}
14+
15+
export async function fetchAsDataURL<T>(
16+
url: string,
17+
init: RequestInit | undefined,
18+
process: (data: { result: string; res: Response }) => T,
19+
): Promise<T> {
20+
const res = await fetch(url, init)
21+
const blob = await res.blob()
22+
return new Promise<T>((resolve, reject) => {
23+
const reader = new FileReader()
24+
reader.onerror = reject
25+
reader.onloadend = () =>
26+
resolve(process({ res, result: reader.result as string }))
27+
reader.readAsDataURL(blob)
28+
})
29+
}
30+
31+
const cache: { [url: string]: string } = {}
32+
33+
function getCacheKey(
34+
url: string,
35+
contentType: string | undefined,
36+
includeQueryParams: boolean | undefined,
37+
) {
38+
let key = url.replace(/\?.*/, '')
39+
40+
if (includeQueryParams) {
41+
key = url
42+
}
43+
44+
// font resource
45+
if (/ttf|otf|eot|woff2?/i.test(key)) {
46+
key = key.replace(/.*\//, '')
47+
}
48+
49+
return contentType ? `[${contentType}]${key}` : key
50+
}
51+
52+
export async function resourceToDataURL(
53+
resourceUrl: string,
54+
contentType: string | undefined,
55+
options: Options,
56+
) {
57+
const cacheKey = getCacheKey(
58+
resourceUrl,
59+
contentType,
60+
options.includeQueryParams,
61+
)
62+
if (cache[cacheKey] != null) {
63+
return cache[cacheKey]
64+
}
65+
66+
// ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
67+
if (options.cacheBust) {
68+
// eslint-disable-next-line no-param-reassign
69+
resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime()
70+
}
71+
72+
let content: string
73+
try {
74+
content = await fetchAsDataURL(
75+
resourceUrl,
76+
options.fetchRequestInit,
77+
({ res, result }) => {
78+
if (!contentType) {
79+
// eslint-disable-next-line no-param-reassign
80+
contentType = res.headers.get('Content-Type') || ''
81+
}
82+
return getContentFromDataUrl(result)
83+
},
84+
)
85+
} catch (error) {
86+
let placeholder = ''
87+
if (options.imagePlaceholder) {
88+
const parts = options.imagePlaceholder.split(/,/)
89+
if (parts && parts[1]) {
90+
placeholder = parts[1]
91+
}
92+
}
93+
94+
let msg = `Failed to fetch resource: ${resourceUrl}`
95+
if (error) {
96+
msg = typeof error === 'string' ? error : error.message
97+
}
98+
99+
if (msg) {
100+
console.error(msg)
101+
}
102+
103+
content = placeholder
104+
}
105+
106+
const dataurl = makeDataUrl(content, contentType || '')
107+
cache[cacheKey] = dataurl
108+
return dataurl
109+
}

src/embed-images.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Options } from './types'
2+
import { embedResources } from './embed-resources'
3+
import { toArray } from './util'
4+
import { isDataUrl, resourceToDataURL } from './dataurl'
5+
import { getMimeType } from './mimes'
6+
7+
async function embedBackground<T extends HTMLElement>(
8+
clonedNode: T,
9+
options: Options,
10+
) {
11+
const background = clonedNode.style?.getPropertyValue('background')
12+
if (background) {
13+
const cssString = await embedResources(background, null, options)
14+
clonedNode.style.setProperty(
15+
'background',
16+
cssString,
17+
clonedNode.style.getPropertyPriority('background'),
18+
)
19+
}
20+
}
21+
22+
async function embedImageNode<T extends HTMLElement | SVGImageElement>(
23+
clonedNode: T,
24+
options: Options,
25+
) {
26+
if (
27+
!(clonedNode instanceof HTMLImageElement && !isDataUrl(clonedNode.src)) &&
28+
!(
29+
clonedNode instanceof SVGImageElement &&
30+
!isDataUrl(clonedNode.href.baseVal)
31+
)
32+
) {
33+
return
34+
}
35+
36+
const url =
37+
clonedNode instanceof HTMLImageElement
38+
? clonedNode.src
39+
: clonedNode.href.baseVal
40+
41+
const dataURL = await resourceToDataURL(url, getMimeType(url), options)
42+
await new Promise((resolve, reject) => {
43+
clonedNode.onload = resolve
44+
clonedNode.onerror = reject
45+
if (clonedNode instanceof HTMLImageElement) {
46+
clonedNode.srcset = ''
47+
clonedNode.src = dataURL
48+
} else {
49+
clonedNode.href.baseVal = dataURL
50+
}
51+
})
52+
}
53+
54+
async function embedChildren<T extends HTMLElement>(
55+
clonedNode: T,
56+
options: Options,
57+
) {
58+
const children = toArray<HTMLElement>(clonedNode.childNodes)
59+
const deferreds = children.map((child) => embedImages(child, options))
60+
await Promise.all(deferreds).then(() => clonedNode)
61+
}
62+
63+
export async function embedImages<T extends HTMLElement>(
64+
clonedNode: T,
65+
options: Options,
66+
) {
67+
if (clonedNode instanceof Element) {
68+
await embedBackground(clonedNode, options)
69+
await embedImageNode(clonedNode, options)
70+
await embedChildren(clonedNode, options)
71+
}
72+
}
Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,51 @@
1-
import { Options } from './options'
2-
import { getBlobFromURL } from './getBlobFromURL'
3-
import { getMimeType, isDataUrl, makeDataUrl, resolveUrl } from './util'
1+
import { Options } from './types'
2+
import { resolveUrl } from './util'
3+
import { getMimeType } from './mimes'
4+
import { isDataUrl, makeDataUrl, resourceToDataURL } from './dataurl'
45

56
const URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g
67
const URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g
78
const FONT_SRC_REGEX = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g
89

9-
export function toRegex(url: string): RegExp {
10+
function toRegex(url: string): RegExp {
1011
// eslint-disable-next-line no-useless-escape
1112
const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1')
1213
return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, 'g')
1314
}
1415

1516
export function parseURLs(cssText: string): string[] {
16-
const result: string[] = []
17+
const urls: string[] = []
1718

1819
cssText.replace(URL_REGEX, (raw, quotation, url) => {
19-
result.push(url)
20+
urls.push(url)
2021
return raw
2122
})
2223

23-
return result.filter((url) => !isDataUrl(url))
24+
return urls.filter((url) => !isDataUrl(url))
2425
}
2526

26-
export function embed(
27+
export async function embed(
2728
cssText: string,
2829
resourceURL: string,
2930
baseURL: string | null,
3031
options: Options,
31-
get?: (url: string) => Promise<string>,
32+
getContentFromUrl?: (url: string) => Promise<string>,
3233
): Promise<string> {
33-
const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL
34-
35-
return Promise.resolve(resolvedURL)
36-
.then<string | { blob: string; contentType: string }>((url) =>
37-
get ? get(url) : getBlobFromURL(url, options),
38-
)
39-
.then((data) => {
40-
if (typeof data === 'string') {
41-
return makeDataUrl(data, getMimeType(resourceURL))
42-
}
43-
44-
return makeDataUrl(
45-
data.blob,
46-
getMimeType(resourceURL) || data.contentType,
47-
)
48-
})
49-
.then((dataURL) => cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`))
50-
.then(
51-
(content) => content,
52-
() => resolvedURL,
53-
)
34+
try {
35+
const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL
36+
const contentType = getMimeType(resourceURL)
37+
let dataURL: string
38+
if (getContentFromUrl) {
39+
const content = await getContentFromUrl(resolvedURL)
40+
dataURL = makeDataUrl(content, contentType)
41+
} else {
42+
dataURL = await resourceToDataURL(resolvedURL, contentType, options)
43+
}
44+
return cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`)
45+
} catch (error) {
46+
// pass
47+
}
48+
return cssText
5449
}
5550

5651
function filterPreferredFontFormat(
@@ -63,7 +58,6 @@ function filterPreferredFontFormat(
6358
// eslint-disable-next-line no-constant-condition
6459
while (true) {
6560
const [src, , format] = URL_WITH_FORMAT_REGEX.exec(match) || []
66-
6761
if (!format) {
6862
return ''
6963
}
@@ -92,7 +86,6 @@ export async function embedResources(
9286
const urls = parseURLs(filteredCSSText)
9387
return urls.reduce(
9488
(deferred, url) =>
95-
// eslint-disable-next-line promise/no-nesting
9689
deferred.then((css) => embed(css, url, baseUrl, options)),
9790
Promise.resolve(filteredCSSText),
9891
)

0 commit comments

Comments
 (0)