Skip to content

Commit

Permalink
fix(inline): parallelize and improve timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfbecker committed Nov 29, 2020
1 parent 2a1d2ba commit ff3b3ff
Showing 1 changed file with 100 additions and 85 deletions.
185 changes: 100 additions & 85 deletions src/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,98 +17,113 @@ declare global {
* Note: The passed element needs to be attached to a document with a window (`defaultView`) for this so that `getComputedStyle()` can be used.
*/
export async function inlineResources(element: Element): Promise<void> {
if (isSVGImageElement(element)) {
const blob = await withTimeout(5000, `Timeout fetching ${element.href.baseVal}`, () =>
fetchResource(element.href.baseVal)
)
if (!blob.type.startsWith('image/')) {
throw new Error(`Invalid response type: Expected image/* response, got ${blob.type}`)
}
if (blob.type === 'image/svg+xml') {
// If the image is an SVG, inline it into the output SVG.
// Some tools (e.g. Figma) do not support nested SVG.
await Promise.all([
...[...element.children].map(inlineResources),
(async () => {
if (isSVGImageElement(element)) {
const blob = await withTimeout(10000, `Timeout fetching ${element.href.baseVal}`, () =>
fetchResource(element.href.baseVal)
)
if (!blob.type.startsWith('image/')) {
throw new Error(
`Invalid response type inlining <image href="${element.href.baseVal}">: Expected image/* response, got ${blob.type}`
)
}
if (blob.type === 'image/svg+xml') {
// If the image is an SVG, inline it into the output SVG.
// Some tools (e.g. Figma) do not support nested SVG.

assert(element.ownerDocument, 'Expected <image> element to have ownerDocument')
assert(element.ownerDocument, 'Expected <image> element to have ownerDocument')

// Replace <image> with inline <svg>
const embeddedSvgDocument = new DOMParser().parseFromString(
await blob.text(),
'image/svg+xml'
) as XMLDocument
const svgRoot = (embeddedSvgDocument.documentElement as Element) as SVGSVGElement
svgRoot.setAttribute('x', element.getAttribute('x')!)
svgRoot.setAttribute('y', element.getAttribute('y')!)
svgRoot.setAttribute('width', element.getAttribute('width')!)
svgRoot.setAttribute('height', element.getAttribute('height')!)
svgRoot.remove()
element.replaceWith(svgRoot)
try {
// Let handleSvgNode inline the <svg> into a simple <g>
const svgDocument = element.ownerDocument
const mount = svgDocument.createElementNS(svgNamespace, 'g')
assert(element.id, '<image> element must have ID')
handleSvgNode(svgRoot, {
currentSvgParent: mount,
svgDocument,
idPrefix: `${element.id}-`,
options: {
// SVGs embedded through <img> are never interactive.
keepLinks: false,
captureArea: svgRoot.viewBox.baseVal,
},
})
// Replace <image> with inline <svg>
const embeddedSvgDocument = new DOMParser().parseFromString(
await blob.text(),
'image/svg+xml'
) as XMLDocument
const svgRoot = (embeddedSvgDocument.documentElement as Element) as SVGSVGElement
svgRoot.setAttribute('x', element.getAttribute('x')!)
svgRoot.setAttribute('y', element.getAttribute('y')!)
svgRoot.setAttribute('width', element.getAttribute('width')!)
svgRoot.setAttribute('height', element.getAttribute('height')!)
svgRoot.remove()
element.replaceWith(svgRoot)
try {
// Let handleSvgNode inline the <svg> into a simple <g>
const svgDocument = element.ownerDocument
const mount = svgDocument.createElementNS(svgNamespace, 'g')
assert(element.id, '<image> element must have ID')
handleSvgNode(svgRoot, {
currentSvgParent: mount,
svgDocument,
idPrefix: `${element.id}-`,
options: {
// SVGs embedded through <img> are never interactive.
keepLinks: false,
captureArea: svgRoot.viewBox.baseVal,
},
})

// Replace the <svg> element with the <g>
mount.dataset.tag = 'img'
mount.setAttribute('role', 'img')
svgRoot.replaceWith(mount)
} finally {
svgRoot.remove()
}
} else {
// Inline binary images as base64 data: URL
const dataUrl = await blobToDataURL(blob)
element.dataset.src = element.href.baseVal
element.setAttribute('href', dataUrl.href)
}
} else if (isSVGStyleElement(element) && element.sheet) {
try {
const rules = element.sheet.cssRules
for (const rule of rules) {
if (isCSSFontFaceRule(rule)) {
const parsedSourceValue = cssValueParser(rule.style.src)
const promises: Promise<void>[] = []
parsedSourceValue.walk(node => {
if (node.type === 'function' && node.value === 'url' && node.nodes[0]) {
const urlArgumentNode = node.nodes[0]
if (urlArgumentNode.type === 'string' || urlArgumentNode.type === 'word') {
const url = new URL(unescapeStringValue(urlArgumentNode.value))
promises.push(
(async () => {
const blob = await withTimeout(5000, `Timeout fetching ${url.href}`, () =>
fetchResource(url.href)
// Replace the <svg> element with the <g>
mount.dataset.tag = 'img'
mount.setAttribute('role', 'img')
svgRoot.replaceWith(mount)
} finally {
svgRoot.remove()
}
} else {
// Inline binary images as base64 data: URL
const dataUrl = await blobToDataURL(blob)
element.dataset.src = element.href.baseVal
element.setAttribute('href', dataUrl.href)
}
} else if (isSVGStyleElement(element) && element.sheet) {
try {
const rules = element.sheet.cssRules
for (const rule of rules) {
if (isCSSFontFaceRule(rule)) {
const parsedSourceValue = cssValueParser(rule.style.src)
const promises: Promise<void>[] = []
parsedSourceValue.walk(node => {
if (node.type === 'function' && node.value === 'url' && node.nodes[0]) {
const urlArgumentNode = node.nodes[0]
if (urlArgumentNode.type === 'string' || urlArgumentNode.type === 'word') {
const url = new URL(unescapeStringValue(urlArgumentNode.value))
promises.push(
(async () => {
try {
const blob = await withTimeout(
10000,
`Timeout fetching ${url.href}`,
() => fetchResource(url.href)
)
if (
!blob.type.startsWith('font/') &&
blob.type !== 'application/font-woff'
) {
throw new Error(
`Invalid response type inlining font at ${url.href}: Expected font/* response, got ${blob.type}`
)
}
const dataUrl = await blobToDataURL(blob)
urlArgumentNode.value = dataUrl.href
} catch (error) {
console.error(`Error inlining ${url.href}`, error)
}
})()
)
if (!blob.type.startsWith('font/')) {
throw new Error(
`Invalid response type: Expected font/* response, got ${blob.type}`
)
}
const dataUrl = await blobToDataURL(blob)
urlArgumentNode.value = dataUrl.href
})()
)
}
}
}
})
await Promise.all(promises)
rule.style.src = cssValueParser.stringify(parsedSourceValue.nodes)
}
})
await Promise.all(promises)
rule.style.src = cssValueParser.stringify(parsedSourceValue.nodes)
}
} catch (error) {
console.error('Error inlining stylesheet', element.sheet, error)
}
}
} catch (error) {
console.error('Error inlining stylesheet', element.sheet, error)
}
}
await Promise.all([...element.children].map(inlineResources))
})(),
])
}

async function fetchResource(url: string): Promise<Blob> {
Expand Down

0 comments on commit ff3b3ff

Please sign in to comment.