Loading docs/rules/no-dup-id.md +31 −0 Original line number Diff line number Diff line Loading @@ -27,3 +27,34 @@ Examples of **correct** code for this rule: <div id="foo"></div> <div id="bar"></div> </validate> ## Options This rule takes an optional object: ```json { "include": null, "exclude": null } ``` ### `include` - type: `string[] | null` - default: `null` If set only IDs matching one or more patterns in this array are checked for duplicates. Supports wildcard with `*` (e.g. `foo-*`) and regexp with `/../` (e.g. `/^foo-\d+$/`). ### `exclude` - type: `string[] | null` - default: `null` If set IDs matching one or more patterns in this array are ignored. Supports wildcard with `*` (e.g. `foo-*`) and regexp with `/../` (e.g. `/^foo-\d+$/`). ## Version history - %version% - `include` and `exclude` options added. src/rules/no-dup-id.spec.ts +111 −0 Original line number Diff line number Diff line Loading @@ -126,4 +126,115 @@ describe("rule no-dup-id", () => { const docs = await htmlvalidate.getRuleDocumentation("no-dup-id"); expect(docs).toMatchSnapshot(); }); describe("configured with include", () => { let htmlvalidate: HtmlValidate; beforeAll(() => { htmlvalidate = new HtmlValidate({ root: true, rules: { "no-dup-id": ["error", { include: ["foo-*"] }] }, }); }); it("should report error when duplicated id matches include pattern", async () => { expect.assertions(2); const markup = /* HTML */ ` <p id="foo-1"></p> <p id="foo-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeInvalid(); expect(report).toMatchInlineCodeframe(` "error: Duplicate ID "foo-1" (no-dup-id) 1 | 2 | <p id="foo-1"></p> > 3 | <p id="foo-1"></p> | ^^^^^ 4 | Selector: p:nth-child(2)" `); }); it("should not report error when duplicated id does not match include pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="bar-1"></p> <p id="bar-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeValid(); }); }); describe("configured with exclude", () => { let htmlvalidate: HtmlValidate; beforeAll(() => { htmlvalidate = new HtmlValidate({ root: true, rules: { "no-dup-id": ["error", { exclude: ["foo-*"] }] }, }); }); it("should not report error when duplicated id matches exclude pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="foo-1"></p> <p id="foo-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeValid(); }); it("should report error when duplicated id does not match exclude pattern", async () => { expect.assertions(2); const markup = /* HTML */ ` <p id="bar-1"></p> <p id="bar-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeInvalid(); expect(report).toMatchInlineCodeframe(` "error: Duplicate ID "bar-1" (no-dup-id) 1 | 2 | <p id="bar-1"></p> > 3 | <p id="bar-1"></p> | ^^^^^ 4 | Selector: p:nth-child(2)" `); }); }); describe("configured with regexp pattern", () => { let htmlvalidate: HtmlValidate; beforeAll(() => { htmlvalidate = new HtmlValidate({ root: true, rules: { "no-dup-id": ["error", { include: ["/^foo-\\d+$/"] }] }, }); }); it("should report error when duplicated id matches regexp pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="foo-42"></p> <p id="foo-42"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeInvalid(); }); it("should not report error when duplicated id does not match regexp pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="foo-bar"></p> <p id="foo-bar"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeValid(); }); }); }); src/rules/no-dup-id.ts +49 −2 Original line number Diff line number Diff line import { type HtmlElement } from "../dom"; import { type DOMReadyEvent } from "../event"; import { type RuleDocumentation, Rule, ruleDocumentationUrl } from "../rule"; import { type RuleDocumentation, type SchemaObject, Rule, ruleDocumentationUrl } from "../rule"; import { type IncludeExcludeOptions, keywordPatternMatcher } from "./helper"; const CACHE_KEY = Symbol("no-dup-id"); Loading @@ -10,7 +11,49 @@ declare module "../dom/cache" { } } export default class NoDupID extends Rule { type RuleOptions = IncludeExcludeOptions; const defaults: RuleOptions = { include: null, exclude: null, }; export default class NoDupID extends Rule<void, RuleOptions> { public constructor(options: Partial<RuleOptions>) { super({ ...defaults, ...options }); } public static override schema(): SchemaObject { return { exclude: { anyOf: [ { items: { type: "string", }, type: "array", }, { type: "null", }, ], }, include: { anyOf: [ { items: { type: "string", }, type: "array", }, { type: "null", }, ], }, }; } public override documentation(): RuleDocumentation { return { description: "The ID of an element must be unique.", Loading Loading @@ -44,6 +87,10 @@ export default class NoDupID extends Rule { const id = attr.value.toString(); if (this.isKeywordIgnored(id, keywordPatternMatcher)) { continue; } const existing = useRootExisting ? rootExisting : getExisting(el, document.root); if (existing.has(id)) { Loading Loading
docs/rules/no-dup-id.md +31 −0 Original line number Diff line number Diff line Loading @@ -27,3 +27,34 @@ Examples of **correct** code for this rule: <div id="foo"></div> <div id="bar"></div> </validate> ## Options This rule takes an optional object: ```json { "include": null, "exclude": null } ``` ### `include` - type: `string[] | null` - default: `null` If set only IDs matching one or more patterns in this array are checked for duplicates. Supports wildcard with `*` (e.g. `foo-*`) and regexp with `/../` (e.g. `/^foo-\d+$/`). ### `exclude` - type: `string[] | null` - default: `null` If set IDs matching one or more patterns in this array are ignored. Supports wildcard with `*` (e.g. `foo-*`) and regexp with `/../` (e.g. `/^foo-\d+$/`). ## Version history - %version% - `include` and `exclude` options added.
src/rules/no-dup-id.spec.ts +111 −0 Original line number Diff line number Diff line Loading @@ -126,4 +126,115 @@ describe("rule no-dup-id", () => { const docs = await htmlvalidate.getRuleDocumentation("no-dup-id"); expect(docs).toMatchSnapshot(); }); describe("configured with include", () => { let htmlvalidate: HtmlValidate; beforeAll(() => { htmlvalidate = new HtmlValidate({ root: true, rules: { "no-dup-id": ["error", { include: ["foo-*"] }] }, }); }); it("should report error when duplicated id matches include pattern", async () => { expect.assertions(2); const markup = /* HTML */ ` <p id="foo-1"></p> <p id="foo-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeInvalid(); expect(report).toMatchInlineCodeframe(` "error: Duplicate ID "foo-1" (no-dup-id) 1 | 2 | <p id="foo-1"></p> > 3 | <p id="foo-1"></p> | ^^^^^ 4 | Selector: p:nth-child(2)" `); }); it("should not report error when duplicated id does not match include pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="bar-1"></p> <p id="bar-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeValid(); }); }); describe("configured with exclude", () => { let htmlvalidate: HtmlValidate; beforeAll(() => { htmlvalidate = new HtmlValidate({ root: true, rules: { "no-dup-id": ["error", { exclude: ["foo-*"] }] }, }); }); it("should not report error when duplicated id matches exclude pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="foo-1"></p> <p id="foo-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeValid(); }); it("should report error when duplicated id does not match exclude pattern", async () => { expect.assertions(2); const markup = /* HTML */ ` <p id="bar-1"></p> <p id="bar-1"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeInvalid(); expect(report).toMatchInlineCodeframe(` "error: Duplicate ID "bar-1" (no-dup-id) 1 | 2 | <p id="bar-1"></p> > 3 | <p id="bar-1"></p> | ^^^^^ 4 | Selector: p:nth-child(2)" `); }); }); describe("configured with regexp pattern", () => { let htmlvalidate: HtmlValidate; beforeAll(() => { htmlvalidate = new HtmlValidate({ root: true, rules: { "no-dup-id": ["error", { include: ["/^foo-\\d+$/"] }] }, }); }); it("should report error when duplicated id matches regexp pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="foo-42"></p> <p id="foo-42"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeInvalid(); }); it("should not report error when duplicated id does not match regexp pattern", async () => { expect.assertions(1); const markup = /* HTML */ ` <p id="foo-bar"></p> <p id="foo-bar"></p> `; const report = await htmlvalidate.validateString(markup); expect(report).toBeValid(); }); }); });
src/rules/no-dup-id.ts +49 −2 Original line number Diff line number Diff line import { type HtmlElement } from "../dom"; import { type DOMReadyEvent } from "../event"; import { type RuleDocumentation, Rule, ruleDocumentationUrl } from "../rule"; import { type RuleDocumentation, type SchemaObject, Rule, ruleDocumentationUrl } from "../rule"; import { type IncludeExcludeOptions, keywordPatternMatcher } from "./helper"; const CACHE_KEY = Symbol("no-dup-id"); Loading @@ -10,7 +11,49 @@ declare module "../dom/cache" { } } export default class NoDupID extends Rule { type RuleOptions = IncludeExcludeOptions; const defaults: RuleOptions = { include: null, exclude: null, }; export default class NoDupID extends Rule<void, RuleOptions> { public constructor(options: Partial<RuleOptions>) { super({ ...defaults, ...options }); } public static override schema(): SchemaObject { return { exclude: { anyOf: [ { items: { type: "string", }, type: "array", }, { type: "null", }, ], }, include: { anyOf: [ { items: { type: "string", }, type: "array", }, { type: "null", }, ], }, }; } public override documentation(): RuleDocumentation { return { description: "The ID of an element must be unique.", Loading Loading @@ -44,6 +87,10 @@ export default class NoDupID extends Rule { const id = attr.value.toString(); if (this.isKeywordIgnored(id, keywordPatternMatcher)) { continue; } const existing = useRootExisting ? rootExisting : getExisting(el, document.root); if (existing.has(id)) { Loading