mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(core): preserve images with commas in srcset URLs (#6192)
This commit is contained in:
@@ -129,6 +129,12 @@ const bundled = await bundleToSingleHtml("./project", {
|
||||
});
|
||||
```
|
||||
|
||||
Bundling preserves data URLs and commas inside image `srcset` URLs on `<img>`
|
||||
and `<source>` elements. Eligible local candidates are inlined by default; with
|
||||
`inlineAssets: false`, they stay as paths relative to the project root, including
|
||||
when the entry file is in a subdirectory. Width and density descriptors are
|
||||
preserved.
|
||||
|
||||
The compiler also exposes lower-level timing helpers such as
|
||||
`compileTimingAttrs()`, `injectDurations()`, and `extractResolvedMedia()`.
|
||||
|
||||
|
||||
@@ -2096,6 +2096,66 @@ describe("bundleToSingleHtml", () => {
|
||||
expect(bundled).toContain(`url('${inlinedAs("image/png", "fake-sprite")}?v=2#section')`);
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"preserves srcset URL commas with inlineAssets %s",
|
||||
async (inlineAssets) => {
|
||||
const srcset = `${inlinedAs("image/svg+xml", '<svg xmlns="http://www.w3.org/2000/svg"/>')} 1x, https://cdn.example/hero,blue.png 2x`;
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<html><body><div data-composition-id="root" data-width="320" data-height="180" data-duration="1" data-no-timeline><img id="photo" srcset="${srcset}"></div></body></html>`,
|
||||
});
|
||||
try {
|
||||
const bundled = await bundleToSingleHtml(dir, { inlineAssets });
|
||||
const { document } = parseHTML(bundled);
|
||||
expect(document.getElementById("photo")?.getAttribute("srcset")).toBe(srcset);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("inlines a comma-named srcset image beside an existing data URL", async () => {
|
||||
const dataUrl = inlinedAs("image/svg+xml", '<svg xmlns="http://www.w3.org/2000/svg"/>');
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<html><body><div data-composition-id="root" data-width="320" data-height="180" data-duration="1" data-no-timeline><img id="photo" srcset="${dataUrl} 1x, hero,blue.png 2x"></div></body></html>`,
|
||||
"hero,blue.png": "blue-image-bytes",
|
||||
});
|
||||
try {
|
||||
const bundled = await bundleToSingleHtml(dir);
|
||||
const { document } = parseHTML(bundled);
|
||||
expect(document.getElementById("photo")?.getAttribute("srcset")).toBe(
|
||||
`${dataUrl} 1x, ${inlinedAs("image/png", "blue-image-bytes")} 2x`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"rebases complete picture srcset URLs with inlineAssets %s",
|
||||
async (inlineAssets) => {
|
||||
const dataUrl = inlinedAs("image/svg+xml", '<svg xmlns="http://www.w3.org/2000/svg"/>');
|
||||
const dir = makeTempProject({
|
||||
"scenes/scene.html": `<html><body><div data-composition-id="root" data-width="320" data-height="180" data-duration="1" data-no-timeline><picture><source id="photo" srcset="${dataUrl} 160w, hero,blue.png 320w"><img src="hero,blue.png"></picture></div></body></html>`,
|
||||
"scenes/hero,blue.png": "blue-image-bytes",
|
||||
});
|
||||
try {
|
||||
const bundled = await bundleToSingleHtml(dir, {
|
||||
entryFile: "scenes/scene.html",
|
||||
inlineAssets,
|
||||
});
|
||||
const { document } = parseHTML(bundled);
|
||||
const image = inlineAssets
|
||||
? inlinedAs("image/png", "blue-image-bytes")
|
||||
: "scenes/hero%2Cblue.png";
|
||||
expect(document.getElementById("photo")?.getAttribute("srcset")).toBe(
|
||||
`${dataUrl} 160w, ${image} 320w`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("inlines fonts, images and scripts so no relative asset reference survives", async () => {
|
||||
const dir = makeTempProject({
|
||||
"index.html": `<!doctype html>
|
||||
|
||||
@@ -181,20 +181,37 @@ function rebaseRelativePath(urlValue: string, fromDir: string, toDir: string): s
|
||||
return appendSuffixToUrl(rebased, suffix);
|
||||
}
|
||||
|
||||
function rebaseSrcsetPaths(srcsetValue: string, fromDir: string, toDir: string): string {
|
||||
if (!srcsetValue) return srcsetValue;
|
||||
return srcsetValue
|
||||
.split(",")
|
||||
.map((rawCandidate) => {
|
||||
const candidate = rawCandidate.trim();
|
||||
if (!candidate) return candidate;
|
||||
const parts = candidate.split(/\s+/);
|
||||
const first = parts[0] ?? "";
|
||||
if (parts.length === 0 || !isRelativeUrl(first)) return candidate;
|
||||
parts[0] = rebaseRelativePath(first, fromDir, toDir);
|
||||
return parts.join(" ");
|
||||
})
|
||||
.join(", ");
|
||||
function rewriteSrcsetUrls(srcset: string, rewriteUrl: (url: string) => string): string {
|
||||
const parts: string[] = [];
|
||||
let position = 0;
|
||||
let written = 0;
|
||||
|
||||
// Preserve URL commas per https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute
|
||||
while (position < srcset.length) {
|
||||
while (position < srcset.length && /[ \t\n\r\f,]/.test(srcset.charAt(position))) position++;
|
||||
const start = position;
|
||||
while (position < srcset.length && !/[ \t\n\r\f]/.test(srcset.charAt(position))) position++;
|
||||
let end = position;
|
||||
while (end > start && srcset.charAt(end - 1) === ",") end--;
|
||||
if (end > start) {
|
||||
const url = srcset.slice(start, end);
|
||||
const rewritten = rewriteUrl(url);
|
||||
if (rewritten !== url) {
|
||||
parts.push(srcset.slice(written, start), rewritten);
|
||||
written = end;
|
||||
}
|
||||
}
|
||||
if (end < position) continue;
|
||||
|
||||
let inParens = false;
|
||||
while (position < srcset.length) {
|
||||
const char = srcset.charAt(position++);
|
||||
if (char === "(") inParens = true;
|
||||
else if (char === ")") inParens = false;
|
||||
else if (char === "," && !inParens) break;
|
||||
}
|
||||
}
|
||||
return parts.length > 0 ? parts.join("") + srcset.slice(written) : srcset;
|
||||
}
|
||||
|
||||
function rebaseColorGradingLutPath(value: string, fromDir: string, toDir: string): string {
|
||||
@@ -244,7 +261,13 @@ function rebaseEntryAuthoredAssetPaths(
|
||||
}
|
||||
for (const el of [...document.querySelectorAll("[srcset]")]) {
|
||||
const srcset = el.getAttribute("srcset");
|
||||
if (srcset) el.setAttribute("srcset", rebaseSrcsetPaths(srcset, sourceDir, projectDir));
|
||||
if (srcset)
|
||||
el.setAttribute(
|
||||
"srcset",
|
||||
rewriteSrcsetUrls(srcset, (url) =>
|
||||
isRelativeUrl(url) ? rebaseRelativePath(url, sourceDir, projectDir) : url,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const el of [...document.querySelectorAll(`[${HF_COLOR_GRADING_ATTR}]`)]) {
|
||||
const value = el.getAttribute(HF_COLOR_GRADING_ATTR);
|
||||
@@ -489,22 +512,6 @@ function rewriteColorGradingLutWithInlinedAssets(value: string, inlineAsset: Ass
|
||||
return JSON.stringify(parsed);
|
||||
}
|
||||
|
||||
function rewriteSrcsetWithInlinedAssets(srcsetValue: string, inlineAsset: AssetInliner): string {
|
||||
if (!srcsetValue) return srcsetValue;
|
||||
return srcsetValue
|
||||
.split(",")
|
||||
.map((rawCandidate) => {
|
||||
const candidate = rawCandidate.trim();
|
||||
if (!candidate) return candidate;
|
||||
const parts = candidate.split(/\s+/);
|
||||
if (parts.length === 0) return candidate;
|
||||
const maybeInlined = inlineAsset(parts[0] ?? "");
|
||||
if (maybeInlined) parts[0] = maybeInlined;
|
||||
return parts.join(" ");
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function rewriteCssUrlsWithInlinedAssets(cssText: string, inlineAsset: AssetInliner): string {
|
||||
if (!cssText) return cssText;
|
||||
return cssText.replace(
|
||||
@@ -1467,7 +1474,11 @@ async function bundleProject(projectDir: string, options?: BundleOptions): Promi
|
||||
}
|
||||
for (const el of [...document.querySelectorAll("[srcset]")]) {
|
||||
const srcset = el.getAttribute("srcset");
|
||||
if (srcset) el.setAttribute("srcset", rewriteSrcsetWithInlinedAssets(srcset, inlineAsset));
|
||||
if (srcset)
|
||||
el.setAttribute(
|
||||
"srcset",
|
||||
rewriteSrcsetUrls(srcset, (url) => inlineAsset(url) ?? url),
|
||||
);
|
||||
}
|
||||
// Before inlining, so postcss reads paths not font bytes.
|
||||
const liveStyles = [...document.querySelectorAll("style")].filter(isAlwaysAppliedStyle);
|
||||
|
||||
Reference in New Issue
Block a user