fix(lint): decode JSON attribute entities

This commit is contained in:
James
2026-08-25 07:27:00 +00:00
parent 71e8e92ae9
commit 3cacac5bdc
2 changed files with 23 additions and 19 deletions
@@ -1049,6 +1049,24 @@ describe("composition rules", () => {
expect(finding).toBeUndefined();
});
it("does not warn for an HTML-entity-encoded declarations array", async () => {
const declarations = JSON.stringify([
{
id: "title",
type: "string",
label: "Title",
description: 'A "quoted" title',
default: "Hello",
},
]).replaceAll('"', """);
const html = `<html data-composition-variables='${declarations}'><body><div data-composition-id="x"></div></body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find(
(f) => f.code === "invalid_composition_variables_declaration",
);
expect(finding).toBeUndefined();
});
it("does not warn when data-composition-variables is absent", async () => {
const html = `<html><body><div data-composition-id="x"></div></body></html>`;
const result = await lintHyperframeHtml(html);
+5 -19
View File
@@ -194,27 +194,13 @@ export function readDecodedAttr(tagSource: string, attr: string): string | null
}
/**
* Read an attribute that may legitimately contain the opposite quote
* character. `readAttr` truncates `data-variable-values='{"title":"Hello"}'`
* at the first internal `"` because its `[^"']+` class excludes both quote
* types. This variant alternates: a double-quoted value never contains an
* unescaped `"`, and a single-quoted value never contains an unescaped `'`,
* so each branch can use a quote-specific class.
*
* Use for attributes whose values are JSON or otherwise carry the opposite
* quote character. Existing single-token attributes (`id`, `class`, etc.)
* stick with `readAttr` for consistency with the rest of the lint code.
* Read a JSON-bearing attribute with browser-equivalent character-reference
* decoding. Imported or formatter-serialized HTML commonly stores JSON quotes
* as `&quot;`; lint must inspect the same decoded value that `getAttribute()`
* exposes at runtime.
*/
export function readJsonAttr(tagSource: string, attr: string): string | null {
if (!tagSource) return null;
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// See readAttr: `(?<![\w-])` prevents a short name from matching the tail of a
// longer hyphenated attribute (e.g. "id" inside `data-hf-id`).
const match = tagSource.match(
new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"),
);
if (!match) return null;
return match[1] ?? match[2] ?? null;
return readDecodedAttr(tagSource, attr);
}
export function collectCompositionIds(tags: OpenTag[]): Set<string> {