Commit 777acf31 authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(rules): add `include` and `exclude` options to `no-dup-id`

parent 23467e60
Loading
Loading
Loading
Loading
Loading
+31 −0
Original line number Diff line number Diff line
@@ -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.
+111 −0
Original line number Diff line number Diff line
@@ -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();
		});
	});
});
+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");

@@ -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.",
@@ -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)) {