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:
Miguel Ángel
2026-07-02 17:45:09 -07:00
committed by GitHub
parent d2f1adc2af
commit 8ee4b7dfda
2 changed files with 58 additions and 0 deletions
+31
View File
@@ -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>