diff --git a/packages/lint/src/rules/core.test.ts b/packages/lint/src/rules/core.test.ts
index 15341e0f7..f3c001435 100644
--- a/packages/lint/src/rules/core.test.ts
+++ b/packages/lint/src/rules/core.test.ts
@@ -77,6 +77,37 @@ ${headContent}
}
describe("core rules", () => {
+ it("warns when an id starts with a digit and is unsafe in a hash selector", async () => {
+ const html = `
+
+
+
+`;
+
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((item) => item.code === "id_requires_css_escape");
+
+ expect(finding?.severity).toBe("warning");
+ expect(finding?.elementId).toBe("123-frame");
+ expect(finding?.fixHint).toContain("CSS.escape");
+ });
+
+ it("accepts ids that start with a letter", async () => {
+ const html = `
+
+
+
+`;
+
+ const result = await lintHyperframeHtml(html);
+
+ expect(result.findings.find((item) => item.code === "id_requires_css_escape")).toBeUndefined();
+ });
+
it("reports error when root is missing data-composition-id", async () => {
const html = `
diff --git a/packages/lint/src/rules/core.ts b/packages/lint/src/rules/core.ts
index 016fa2584..d297d4a76 100644
--- a/packages/lint/src/rules/core.ts
+++ b/packages/lint/src/rules/core.ts
@@ -181,6 +181,25 @@ function findVisibleMarkupCommentLeak(source: string): string | null {
}
export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
+ // id_requires_css_escape
+ ({ tags }) => {
+ const findings: HyperframeLintFinding[] = [];
+ for (const tag of tags) {
+ const id = readAttr(tag.raw, "id");
+ if (!id || !/^\d/.test(id)) continue;
+ findings.push({
+ code: "id_requires_css_escape",
+ severity: "warning",
+ message: `id="${id}" starts with a digit, so the common selector \`#${id}\` throws a SyntaxError in querySelector().`,
+ elementId: id,
+ fixHint:
+ "Rename the id to start with a letter (recommended), or build selectors with `#${CSS.escape(id)}` at runtime.",
+ snippet: truncateSnippet(tag.raw),
+ });
+ }
+ return findings;
+ },
+
// root_missing_composition_id + root_missing_dimensions
({ rootTag }) => {
const findings: HyperframeLintFinding[] = [];