mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(lint): stop a leading <svg> defs block from being mistaken for the composition root (#1867)
Two independent post-release feedback reports of the same mechanism: a leading <svg> block (icon/gradient/filter <defs>, referenced by url(#id) elsewhere in the document) placed before the real [data-composition-id] root manufactures root_missing_composition_id + root_missing_dimensions on an otherwise-correct composition. Moving the <svg> after the root cleared both findings for each reporter. findRootTag returned the first body child that wasn't script/style/meta/ link/title, unconditionally — <svg> was never in that skip list, so a leading defs-only <svg> got treated as the root. Fix: skip a leading <svg> when it carries none of the composition markers itself (data-composition-id/data-width/data-height), so an intentionally SVG-rooted composition is still eligible as the root. The first attempt at this only skipped the <svg> open tag, which surfaced a second bug: extractOpenTags is a flat, nesting-unaware scan, so the very next tag it returns after skipping <svg> is the svg's own nested child (<defs>, <filter>, ...), not the sibling after </svg>. Track the svg's closing tag position and skip every tag before it, not just the <svg> tag itself. Tests: skips a leading svg defs block (no false root findings); still treats an <svg> as the root when data-composition-id/data-width/ data-height are declared directly on it. Full lint suite (308 tests) passes.
This commit is contained in:
@@ -112,6 +112,37 @@ describe("core rules", () => {
|
||||
expect(result.findings.find((f) => f.code === "root_missing_dimensions")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips a leading <svg> defs block when detecting the composition root", async () => {
|
||||
// Regression: two independent reports of a leading <svg><defs><filter>...
|
||||
// block (icon/gradient/filter plumbing referenced via url(#id) elsewhere)
|
||||
// getting mistaken for the composition root, since findRootTag returned
|
||||
// the first non-script/style/meta/link/title body child unconditionally.
|
||||
// The <svg> here carries no composition markers, so it must be skipped in
|
||||
// favor of the real root that follows it.
|
||||
const html = `
|
||||
<html><body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs><filter id="glow"><feGaussianBlur stdDeviation="4" /></filter></defs>
|
||||
</svg>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080"></div>
|
||||
<script>window.__timelines = window.__timelines || {};</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
expect(result.findings.find((f) => f.code === "root_missing_composition_id")).toBeUndefined();
|
||||
expect(result.findings.find((f) => f.code === "root_missing_dimensions")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("still treats an <svg> as the root when it carries composition markers itself", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<svg id="root" data-composition-id="c1" data-width="1920" data-height="1080"></svg>
|
||||
<script>window.__timelines = window.__timelines || {};</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
expect(result.findings.find((f) => f.code === "root_missing_composition_id")).toBeUndefined();
|
||||
expect(result.findings.find((f) => f.code === "root_missing_dimensions")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reports error when timeline registry is missing", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -100,6 +100,7 @@ export function findHtmlTag(source: string): OpenTag | null {
|
||||
};
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function findRootTag(source: string): OpenTag | null {
|
||||
const bodyOpenMatch = /<body\b([^>]*)>/i.exec(source);
|
||||
const bodyCloseMatch = /<\/body>/i.exec(source);
|
||||
@@ -123,8 +124,34 @@ export function findRootTag(source: string): OpenTag | null {
|
||||
: source.length;
|
||||
const bodyContent = bodyOpenMatch ? source.slice(bodyStart, bodyEnd) : source;
|
||||
const bodyTags = extractOpenTags(bodyContent);
|
||||
// Set when a leading <svg> defs block is skipped (see below) — extractOpenTags
|
||||
// is a flat, nesting-unaware scan, so without this the very next tag it
|
||||
// returns is the svg's own nested child (<defs>, <filter>, ...), not the
|
||||
// sibling that follows the closed </svg>.
|
||||
let skipBefore = -1;
|
||||
for (const tag of bodyTags) {
|
||||
if (tag.index < skipBefore) continue;
|
||||
if (["script", "style", "meta", "link", "title"].includes(tag.name)) continue;
|
||||
// A leading <svg> block (icon/gradient/filter <defs>, referenced by url(#id)
|
||||
// from elsewhere in the document) is shared visual plumbing, not the
|
||||
// composition root — two independent reports of this being mistaken for
|
||||
// the root, manufacturing root_missing_composition_id/root_missing_dimensions
|
||||
// on an otherwise-correct composition. Only skip it when it carries none of
|
||||
// the composition markers itself, so an intentionally SVG-rooted composition
|
||||
// (data-composition-id/data-width/data-height directly on the <svg>) is
|
||||
// still eligible as the root.
|
||||
if (
|
||||
tag.name === "svg" &&
|
||||
!readAttr(tag.raw, "data-composition-id") &&
|
||||
!readAttr(tag.raw, "data-width") &&
|
||||
!readAttr(tag.raw, "data-height")
|
||||
) {
|
||||
const closeMatch = /<\/svg\s*>/i.exec(bodyContent.slice(tag.index));
|
||||
// No closing tag found (malformed HTML) — skip everything rather than
|
||||
// risk returning one of the svg's own children as the root.
|
||||
skipBefore = closeMatch ? tag.index + closeMatch.index + closeMatch[0].length : Infinity;
|
||||
continue;
|
||||
}
|
||||
return { ...tag, index: tag.index + bodyStart };
|
||||
}
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user