mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(core): bound inert region scans in timing compiler (#3717)
* fix(core): bound inert region scans in timing compiler * fix(core): use literal search for comment terminators * fix(core): recognize end-bang HTML comment boundaries
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
compileTimingAttrs,
|
||||
@@ -18,6 +19,82 @@ it("source contains no raw NUL bytes", () => {
|
||||
expect(src.includes("\x00")).toBe(false);
|
||||
});
|
||||
|
||||
describe("inert region scanning", () => {
|
||||
const media = '<video id="v" data-start="1" data-duration="2">';
|
||||
|
||||
it.each([
|
||||
`<!-- ${media} <!-- nested -->`,
|
||||
`<ScRiPt type="text/javascript">${media}</sCrIpT \n>`,
|
||||
`<STYLE>${media}</STYLE\u00a0>`,
|
||||
`<script-data>${media}</script>`,
|
||||
`<style:${media}</style>`,
|
||||
`<!-- <script>${media} -->`,
|
||||
`<script><!-- ${media}</script>`,
|
||||
`<style><script>${media}</style>`,
|
||||
])("preserves the existing complete-region boundaries in %j", (region) => {
|
||||
const result = compileTimingAttrs(region + media);
|
||||
expect(result).toEqual({ html: region + compileTimingAttrs(media).html, unresolved: [] });
|
||||
expect(extractResolvedMedia(region + media)).toEqual(extractResolvedMedia(media));
|
||||
});
|
||||
|
||||
it.each(["<!--", "<script>", "<style>", "<scripture>", "<stylesheet>"])(
|
||||
"keeps media outside a complete inert region after %j visible",
|
||||
(prefix) => {
|
||||
expect(compileTimingAttrs(prefix + media).html).toBe(prefix + compileTimingAttrs(media).html);
|
||||
expect(extractResolvedMedia(prefix + media)).toEqual(extractResolvedMedia(media));
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["-->", "--!>"])("recognizes %j as the first comment end like the browser", (end) => {
|
||||
const hidden = '<video id="hidden" data-duration="1">';
|
||||
const visible = '<video id="visible" data-start="1" data-duration="2">';
|
||||
const comment = `<!-- ${hidden} ${end}`;
|
||||
// The trailing delimiter must not extend the comment over visible media.
|
||||
const html = comment + visible + " -->";
|
||||
const dom = new JSDOM(html);
|
||||
expect([...dom.window.document.querySelectorAll("video")].map((el) => el.id)).toEqual([
|
||||
"visible",
|
||||
]);
|
||||
dom.window.close();
|
||||
expect(compileTimingAttrs(html)).toEqual({
|
||||
html: comment + compileTimingAttrs(visible).html + " -->",
|
||||
unresolved: [],
|
||||
});
|
||||
expect(extractResolvedMedia(html).map((el) => el.id)).toEqual(["visible"]);
|
||||
});
|
||||
|
||||
it("closes an end-bang comment without a later standard delimiter", () => {
|
||||
const comment = '<!-- <video id="hidden" data-duration="1"> --!>';
|
||||
expect(compileTimingAttrs(comment + media)).toEqual({
|
||||
html: comment + compileTimingAttrs(media).html,
|
||||
unresolved: [],
|
||||
});
|
||||
expect(extractResolvedMedia(comment + media)).toEqual(extractResolvedMedia(media));
|
||||
});
|
||||
|
||||
it.each(["<!--", "<script", "<style"])(
|
||||
"handles many unclosed %j prefixes while masking other region kinds",
|
||||
(prefix) => {
|
||||
const unclosed = prefix.repeat(100_000);
|
||||
const hidden = prefix === "<!--" ? `<style>${media}</style>` : `<!--${media}-->`;
|
||||
expect(compileTimingAttrs(unclosed + hidden + media)).toEqual({
|
||||
html: unclosed + hidden + compileTimingAttrs(media).html,
|
||||
unresolved: [],
|
||||
});
|
||||
expect(extractResolvedMedia(unclosed + hidden + media)).toEqual(extractResolvedMedia(media));
|
||||
},
|
||||
);
|
||||
|
||||
it("uses the first closing delimiter and resumes scanning after it", () => {
|
||||
const hidden = `<script>${media}</script>`;
|
||||
const html = hidden + media + `</script><!--${media}--><style>${media}</style>`;
|
||||
expect(compileTimingAttrs(html).html).toBe(
|
||||
hidden + compileTimingAttrs(media).html + `</script><!--${media}--><style>${media}</style>`,
|
||||
);
|
||||
expect(extractResolvedMedia(html)).toEqual(extractResolvedMedia(media));
|
||||
});
|
||||
});
|
||||
|
||||
describe("compileTimingAttrs", () => {
|
||||
it.each(["", " ", "0s", "0abc", "0px", "-1s", "Infinity", "NaN"])(
|
||||
"does not partially parse invalid literal data-duration=%j",
|
||||
|
||||
@@ -113,18 +113,39 @@ function setAttr(tag: string, attr: string, value: string): string {
|
||||
// `<video>`/`<audio>` gets rewritten as if it were a real element (issue #1938).
|
||||
// Mask those inert regions with placeholders (no `<`, so the tag regexes skip
|
||||
// them) before scanning, then restore them verbatim.
|
||||
const INERT_REGION_RE =
|
||||
/<!--[\s\S]*?-->|<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>/gi;
|
||||
|
||||
// The NUL delimiters must stay as \u0000 escapes: raw 0x00 bytes make this file
|
||||
// binary to git and are corrupted by Bun's transpiler when bundled (issue #2139).
|
||||
function maskInertRegions(html: string): { masked: string; restore: (s: string) => string } {
|
||||
const stash: string[] = [];
|
||||
const masked = html.replace(INERT_REGION_RE, (region) => {
|
||||
const parts: string[] = [];
|
||||
const opening = /<!--|<script\b|<style\b/gi;
|
||||
const closings = new Map([
|
||||
["<!--", /--!?>/g],
|
||||
["<script", /<\/script\s*>/gi],
|
||||
["<style", /<\/style\s*>/gi],
|
||||
]);
|
||||
let cursor = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = opening.exec(html)) !== null) {
|
||||
const kind = match[0].toLowerCase();
|
||||
const closing = closings.get(kind);
|
||||
if (!closing) continue;
|
||||
closing.lastIndex = opening.lastIndex;
|
||||
const end = closing.exec(html) ? closing.lastIndex : -1;
|
||||
if (end < 0) {
|
||||
// No later opener of this kind can close either. Search each unmatched
|
||||
// suffix only once, while still allowing other kinds of inert regions.
|
||||
closings.delete(kind);
|
||||
continue;
|
||||
}
|
||||
const token = `\u0000HFMASK${stash.length}\u0000`;
|
||||
stash.push(region);
|
||||
return token;
|
||||
});
|
||||
parts.push(html.slice(cursor, match.index), token);
|
||||
stash.push(html.slice(match.index, end));
|
||||
cursor = end;
|
||||
opening.lastIndex = cursor;
|
||||
}
|
||||
parts.push(html.slice(cursor));
|
||||
const masked = parts.join("");
|
||||
const restore = (s: string): string =>
|
||||
// oxlint-disable-next-line no-control-regex -- NUL cannot appear in HTML, which is what makes it a safe mask delimiter
|
||||
s.replace(/\u0000HFMASK(\d+)\u0000/g, (_, i) => stash[Number(i)] ?? "");
|
||||
|
||||
Reference in New Issue
Block a user