Commits (26)
# html-validate changelog
## [4.14.0](https://gitlab.com/html-validate/html-validate/compare/v4.13.1...v4.14.0) (2021-06-14)
### Features
- new rule `attr-pattern` ([b813aeb](https://gitlab.com/html-validate/html-validate/commit/b813aeb7348d20b1cba2ea3df7c5bd7ac952e324)), closes [#118](https://gitlab.com/html-validate/html-validate/issues/118)
- new rule `input-attributes` ([23ee19e](https://gitlab.com/html-validate/html-validate/commit/23ee19eab292a97427ddc15db1bb77346489c002)), closes [#119](https://gitlab.com/html-validate/html-validate/issues/119)
### [4.13.1](https://gitlab.com/html-validate/html-validate/compare/v4.13.0...v4.13.1) (2021-05-28)
### Bug Fixes
......
......@@ -41,6 +41,7 @@ module.exports = function (grunt) {
includePaths: [
"node_modules/font-awesome/scss/",
"node_modules/bootstrap-sass/assets/stylesheets/",
"node_modules/highlight.js/scss/",
],
},
default: {
......
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
......@@ -5,7 +5,7 @@ $icon-font-path: "fonts/";
@import "font-awesome";
@import "bootstrap";
@import "hljs-github";
@import "github";
@import "anchorlink";
@import "frontpage";
......@@ -116,3 +116,7 @@ table {
position: relative;
top: 1px;
}
.hljs {
background: none;
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/attr-pattern.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/attr-pattern.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 1,
"message": "Attribute \\"foo_bar\\" should match /[a-z0-9-:]+/",
"offset": 3,
"ruleId": "attr-pattern",
"ruleUrl": "https://html-validate.org/rules/attr-pattern.html",
"selector": "p",
"severity": 2,
"size": 7,
},
],
"source": "<p foo_bar=\\"baz\\"></p>",
"warningCount": 0,
},
]
`;
exports[`docs/rules/attr-pattern.md inline validation: multiple 1`] = `Array []`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/input-attributes.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/input-attributes.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 20,
"context": Object {
"attribute": "step",
"type": "text",
},
"line": 1,
"message": "Attribute \\"step\\" is not allowed on <input type=\\"text\\">",
"offset": 19,
"ruleId": "input-attributes",
"ruleUrl": "https://html-validate.org/rules/input-attributes.html",
"selector": "input",
"severity": 2,
"size": 4,
},
],
"source": "<input type=\\"text\\" step=\\"5\\">",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<p foo_bar="baz"></p>`;
markup["correct"] = `<p foo-bar="baz"></p>`;
markup["multiple"] = `<p foo-bar-123></p>
<p myprefix-foo_123!></p>`;
describe("docs/rules/attr-pattern.md", () => {
it("inline validation: incorrect", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"attr-pattern":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"attr-pattern":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: multiple", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"attr-pattern":["error",{"pattern":["[a-z0-9-]+","myprefix-.+"]}]}});
const report = htmlvalidate.validateString(markup["multiple"]);
expect(report.results).toMatchSnapshot();
});
});
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<input type="text" step="5">`;
markup["correct"] = `<input type="number" step="5">`;
describe("docs/rules/input-attributes.md", () => {
it("inline validation: incorrect", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"input-attributes":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"input-attributes":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
......@@ -8,6 +8,7 @@ summary: Require a specific case for attribute names
# Attribute name case (`attr-case`)
Requires a specific case for attribute names.
This rule matches case for letters only, for restricting allowed characters use {@link rules/attr-pattern}.
## Rule details
......
---
docType: rule
name: attr-pattern
category: style
summary: Require attributes to match configured patterns
---
# Attribute name pattern (`attr-pattern`)
Require attributes to match configured patterns.
This rule is case-insensitive, for matching case use {@link rules/attr-case}.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="attr-pattern">
<p foo_bar="baz"></p>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="attr-pattern">
<p foo-bar="baz"></p>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"pattern": "[a-z0-9-:]+",
"ignoreForeign": true
}
```
### `pattern`
- type: `string | string[]`
- default: `[a-z0-9-:]+`
Pattern to match.
Multiple patterns can be set as an array of strings.
With multiple patterns the attribute must match at least one pattern to be considered valid.
For instance, when configured with `{"pattern": ["[a-z0-9-]+", "myprefix-.+"]}` attributes can be either letters and digits or anything with the `myprefix-` prefix:
<validate name="multiple" rules="attr-pattern" attr-pattern='{"pattern": ["[a-z0-9-]+", "myprefix-.+"]}'>
<p foo-bar-123></p>
<p myprefix-foo_123!></p>
</validate>
### `ignoreForeign`
By default attributes on foreign elements (such as `<svg>` and `<math>`) are ignored as they follow their own specifications.
Disable this option if you want to validate attributes on foreign elements as well.
## Version history
- v4.14.0 - Rule added.
---
docType: rule
name: input-attributes
category: content-model
summary: Validates usage of input attributes
---
# Validates usage of input attributes (`input-attributes`)
The `<input>` element uses the `type` attribute to set what type of input field it is.
Depending on what type of input field it is many other attributes is allowed or disallowed.
For instance, the `step` attribute can be used with numerical fields but not with textual input.
This rule validates the usage of these attributes, ensuring the attributes are used only in the proper context.
See [HTML5 specification][whatwg] for a table of attributes and types.
[whatwg]: https://html.spec.whatwg.org/multipage/input.html#concept-input-apply
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="input-attributes">
<input type="text" step="5">
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="input-attributes">
<input type="number" step="5">
</validate>
## Version history
- v4.14.0 - Rule added.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "html-validate",
"version": "4.13.1",
"version": "4.14.0",
"description": "html linter",
"keywords": [
"html",
......@@ -133,16 +133,16 @@
"prompts": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.14.3",
"@babel/preset-env": "7.14.2",
"@babel/core": "7.14.5",
"@babel/preset-env": "7.14.5",
"@commitlint/cli": "12.1.4",
"@html-validate/commitlint-config": "1.3.1",
"@html-validate/eslint-config": "4.4.0",
"@html-validate/eslint-config-jest": "4.4.0",
"@html-validate/eslint-config": "4.4.1",
"@html-validate/eslint-config-jest": "4.4.2",
"@html-validate/eslint-config-typescript": "4.4.0",
"@html-validate/jest-config": "1.2.10",
"@html-validate/prettier-config": "1.1.0",
"@html-validate/semantic-release-config": "1.2.13",
"@html-validate/semantic-release-config": "1.2.15",
"@lodder/grunt-postcss": "3.0.1",
"@types/babar": "0.2.0",
"@types/babel__code-frame": "7.0.2",
......@@ -153,17 +153,17 @@
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.1",
"@types/node": "11.15.54",
"@types/prompts": "2.0.12",
"@types/prompts": "2.0.13",
"autoprefixer": "10.2.6",
"babar": "0.2.0",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
"cssnano": "5.0.4",
"cssnano": "5.0.6",
"dgeni": "0.4.14",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.29.1",
"eslint": "7.27.0",
"eslint": "7.28.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.7.0",
"font-awesome": "4.7.0",
......@@ -174,24 +174,24 @@
"grunt-contrib-connect": "3.0.0",
"grunt-contrib-copy": "1.0.0",
"grunt-sass": "3.1.0",
"highlight.js": "10.7.2",
"highlight.js": "11.0.1",
"husky": "6.0.0",
"jest": "27.0.1",
"jest-diff": "27.0.1",
"jest": "27.0.4",
"jest-diff": "27.0.2",
"jquery": "3.6.0",
"lint-staged": "11.0.0",
"load-grunt-tasks": "5.1.0",
"marked": "2.0.6",
"marked": "2.0.7",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.3.0",
"postcss": "8.3.0",
"prettier": "2.3.0",
"pretty-format": "27.0.1",
"sass": "1.34.0",
"postcss": "8.3.2",
"prettier": "2.3.1",
"pretty-format": "27.0.2",
"sass": "1.34.1",
"semantic-release": "17.4.3",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"ts-jest": "27.0.1",
"ts-jest": "27.0.3",
"typescript": "4.3.2"
},
"peerDependencies": {
......
......@@ -24,6 +24,7 @@ const config: ConfigData = {
"element-required-content": "error",
"empty-heading": "error",
"empty-title": "error",
"input-attributes": "error",
"long-title": "error",
"meta-refresh": "error",
"multiple-labeled-controls": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule attr-pattern should contain contextual documentation (multiple patterns) 1`] = `
Object {
"description": "Attribute \\"foobar\\" should match one of the configured regular expressions:
- \`/[a-z]+/\`
- \`/[0-9]+/\`",
"url": "https://html-validate.org/rules/attr-pattern.html",
}
`;
exports[`rule attr-pattern should contain contextual documentation (single pattern) 1`] = `
Object {
"description": "Attribute \\"foobar\\" should match the regular expression \`/[a-z]+/\`",
"url": "https://html-validate.org/rules/attr-pattern.html",
}
`;
exports[`rule attr-pattern should contain documentation 1`] = `
Object {
"description": "Attribute should match configured pattern",
"url": "https://html-validate.org/rules/attr-pattern.html",
}
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule input-attributes should contain contextual documentation (invalid) 1`] = `
Object {
"description": "Attribute \`missing\` is not allowed on \`<input type=\\"text\\">\`
\`missing\` can only be used when \`type\` is:",
"url": "https://html-validate.org/rules/input-attributes.html",
}
`;
exports[`rule input-attributes should contain contextual documentation 1`] = `
Object {
"description": "Attribute \`alt\` is not allowed on \`<input type=\\"text\\">\`
\`alt\` can only be used when \`type\` is:
- \`image\`",
"url": "https://html-validate.org/rules/input-attributes.html",
}
`;
exports[`rule input-attributes should contain documentation 1`] = `
Object {
"description": "This attribute cannot be used with this input type.",
"url": "https://html-validate.org/rules/input-attributes.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
import { DEFAULT_PATTERN } from "./attr-pattern";
describe("rule attr-pattern", () => {
let htmlvalidate: HtmlValidate;
describe("default configuration", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": "error" },
});
});
it("should not report error when attribute has letters and characters only", () => {
expect.assertions(1);
const markup = "<div foo></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report error when attribute has digits", () => {
expect.assertions(1);
const markup = "<div foo></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report error when attribute has dashes", () => {
expect.assertions(1);
const markup = "<div foo></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report error when attribute has xml namespace", () => {
expect.assertions(1);
const markup = "<div xfoo:bar></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it.each`
attr | description
${"foo_bar"} | ${"underscore"}
`("should report error when attribute has $description", ({ attr }) => {
expect.assertions(2);
const markup = `<div ${attr}></div>`;
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"attr-pattern",
`Attribute "${attr}" should match /${DEFAULT_PATTERN}/`
);
});
});
describe("configured with single pattern", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": ["error", { pattern: "[a-z]+" }] },
});
});
it("should not report error when attribute has allowed characters only", () => {
expect.assertions(1);
const markup = "<div foo></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should report error when attribute has other characters", () => {
expect.assertions(2);
const markup = "<div foo-2000></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError("attr-pattern", 'Attribute "foo-2000" should match /[a-z]+/');
});
});
describe("configured with multiple patterns", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": ["error", { pattern: ["[a-z]+", "[0-9]+"] }] },
});
});
it("should not report error when attributes matches one of the allowed patterns", () => {
expect.assertions(1);
const markup = "<div foo 123></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should report error when attribute doesn't match any pattern", () => {
expect.assertions(2);
const markup = "<div foo-123></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"attr-pattern",
'Attribute "foo-123" should match one of [/[a-z]+/, /[0-9]+/]'
);
});
});
describe('configured with "ignoreForeign" true', () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": ["error", { ignoreForeign: true }] },
});
});
it("should not report error on foreign elements", () => {
expect.assertions(1);
const markup = "<svg foo_bar/>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
});
describe('configured with "ignoreForeign" false', () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": ["error", { ignoreForeign: false }] },
});
});
it("should report error on foreign elements", () => {
expect.assertions(2);
const markup = "<svg foo_bar/>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"attr-pattern",
`Attribute "foo_bar" should match /${DEFAULT_PATTERN}/`
);
});
});
it("should not report duplicate errors for dynamic attributes", () => {
expect.assertions(2);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": "error" },
});
const markup = '<input dynamic-foo_bar="foo">';
const report = htmlvalidate.validateString(markup, {
processAttribute,
});
expect(report).toBeInvalid();
expect(report).toHaveErrors([
{
ruleId: "attr-pattern",
message: `Attribute "dynamic-foo_bar" should match /${DEFAULT_PATTERN}/`,
},
]);
});
it("should throw error if configured with invalid regexp", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": ["error", { pattern: "[" }] },
});
expect(() => htmlvalidate.validateString("")).toThrowErrorMatchingInlineSnapshot(
`"Invalid regular expression: /^[$/: Unterminated character class"`
);
});
it("should throw error if configured with no patterns", () => {
expect.assertions(1);
expect(() => {
return new HtmlValidate({
root: true,
rules: { "attr-pattern": ["error", { pattern: [] }] },
});
}).toThrowErrorMatchingInlineSnapshot(
`"Rule configuration error: /rules/attr-pattern/1/pattern: minItems should NOT have fewer than 1 items"`
);
});
it("should contain documentation", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": "error" },
});
expect(htmlvalidate.getRuleDocumentation("attr-pattern")).toMatchSnapshot();
});
it("should contain contextual documentation (single pattern)", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": "error" },
});
const context = {
attr: "foobar",
pattern: "[a-z]+",
};
expect(htmlvalidate.getRuleDocumentation("attr-pattern", null, context)).toMatchSnapshot();
});
it("should contain contextual documentation (multiple patterns)", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "attr-pattern": "error" },
});
const context = {
attr: "foobar",
pattern: ["[a-z]+", "[0-9]+"],
};
expect(htmlvalidate.getRuleDocumentation("attr-pattern", null, context)).toMatchSnapshot();
});
});
import { HtmlElement } from "../dom";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl, SchemaObject } from "../rule";
interface RuleOptions {
pattern: string | string[];
ignoreForeign: boolean;
}
interface RuleContext {
attr: string;
pattern: string | string[];
}
export const DEFAULT_PATTERN = "[a-z0-9-:]+";
const defaults: RuleOptions = {
pattern: DEFAULT_PATTERN,
ignoreForeign: true,
};
function generateRegexp(pattern: string | string[]): RegExp {
if (Array.isArray(pattern)) {
/* eslint-disable-next-line security/detect-non-literal-regexp */
return new RegExp(`^(${pattern.join("|")})$`, "i");
} else {
/* eslint-disable-next-line security/detect-non-literal-regexp */
return new RegExp(`^${pattern}$`, "i");
}
}
function generateMessage(name: string, pattern: string | string[]): string {
if (Array.isArray(pattern)) {
return `Attribute "${name}" should match one of [${pattern.map((it) => `/${it}/`).join(", ")}]`;
} else {
return `Attribute "${name}" should match /${pattern}/`;
}
}
function generateDescription(name: string, pattern: string | string[]): string {
if (Array.isArray(pattern)) {
return [
`Attribute "${name}" should match one of the configured regular expressions:`,
"",
...pattern.map((it) => `- \`/${it}/\``),
].join("\n");
} else {
return `Attribute "${name}" should match the regular expression \`/${pattern}/\``;
}
}
export default class AttrPattern extends Rule<RuleContext, RuleOptions> {
private pattern: RegExp;
public constructor(options: Partial<RuleOptions>) {
super({ ...defaults, ...options });
this.pattern = generateRegexp(this.options.pattern);
}
public static schema(): SchemaObject {
return {
pattern: {
oneOf: [{ type: "array", items: { type: "string" }, minItems: 1 }, { type: "string" }],
},
ignoreForeign: {
type: "boolean",
},
};
}
public documentation(context?: RuleContext): RuleDocumentation {
let description: string;
if (context) {
description = generateDescription(context.attr, context.pattern);
} else {
description = `Attribute should match configured pattern`;
}
return {
description,
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("attr", (event: AttributeEvent) => {
if (this.isIgnored(event.target)) {
return;
}
/* ignore case for dynamic attributes, the original attributes will be
* checked instead (this prevents duplicated errors for the same source
* attribute) */
if (event.originalAttribute) {
return;
}
if (this.pattern.test(event.key)) {
return;
}
const message = generateMessage(event.key, this.options.pattern);
this.report(event.target, message, event.keyLocation);
});
}
protected isIgnored(node: HtmlElement): boolean {
if (this.options.ignoreForeign) {
return Boolean(node.meta && node.meta.foreign);
} else {
return false;
}
}
}
......@@ -2,6 +2,7 @@ import { RuleConstructor } from "../rule";
import AllowedLinks from "./allowed-links";
import AriaLabelMisuse from "./aria-label-misuse";
import AttrCase from "./attr-case";
import AttrPattern from "./attr-pattern";
import AttrQuotes from "./attr-quotes";
import AttrSpacing from "./attr-spacing";
import AttributeAllowedValues from "./attribute-allowed-values";
......@@ -25,6 +26,7 @@ import EmptyHeading from "./empty-heading";
import EmptyTitle from "./empty-title";
import HeadingLevel from "./heading-level";
import IdPattern from "./id-pattern";
import InputAttributes from "./input-attributes";
import InputMissingLabel from "./input-missing-label";
import LongTitle from "./long-title";
import MetaRefresh from "./meta-refresh";
......@@ -66,6 +68,7 @@ const bundledRules: Record<string, RuleConstructor<any, any>> = {
"allowed-links": AllowedLinks,
"aria-label-misuse": AriaLabelMisuse,
"attr-case": AttrCase,
"attr-pattern": AttrPattern,
"attr-quotes": AttrQuotes,
"attr-spacing": AttrSpacing,
"attribute-allowed-values": AttributeAllowedValues,
......@@ -89,6 +92,7 @@ const bundledRules: Record<string, RuleConstructor<any, any>> = {
"empty-title": EmptyTitle,
"heading-level": HeadingLevel,
"id-pattern": IdPattern,
"input-attributes": InputAttributes,
"input-missing-label": InputMissingLabel,
"long-title": LongTitle,
"meta-refresh": MetaRefresh,
......