html-validate 10.4.0 → 10.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli.js +3 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core.js +213 -46
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/matchers.js.map +1 -1
- package/dist/cjs/tsdoc-metadata.json +1 -1
- package/dist/esm/cli.js +3 -1
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/core.js +213 -46
- package/dist/esm/core.js.map +1 -1
- package/dist/esm/matchers.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/dist/types/browser.d.ts +14 -0
- package/dist/types/index.d.ts +14 -0
- package/package.json +3 -3
package/dist/cjs/core.js
CHANGED
|
@@ -1216,7 +1216,9 @@ class MetaTable {
|
|
|
1216
1216
|
);
|
|
1217
1217
|
}
|
|
1218
1218
|
for (const [key, value] of Object.entries(obj)) {
|
|
1219
|
-
if (key === "$schema")
|
|
1219
|
+
if (key === "$schema") {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1220
1222
|
this.addEntry(key, migrateElement(value));
|
|
1221
1223
|
}
|
|
1222
1224
|
} catch (err) {
|
|
@@ -1316,7 +1318,9 @@ class MetaTable {
|
|
|
1316
1318
|
* global, e.g. to assign global attributes.
|
|
1317
1319
|
*/
|
|
1318
1320
|
resolveGlobal() {
|
|
1319
|
-
if (!this.elements["*"])
|
|
1321
|
+
if (!this.elements["*"]) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1320
1324
|
const global = this.elements["*"];
|
|
1321
1325
|
delete this.elements["*"];
|
|
1322
1326
|
delete global.tagName;
|
|
@@ -1490,7 +1494,9 @@ function sliceSize(size, begin, end) {
|
|
|
1490
1494
|
return Math.min(size, end - begin);
|
|
1491
1495
|
}
|
|
1492
1496
|
function sliceLocation(location, begin, end, wrap) {
|
|
1493
|
-
if (!location)
|
|
1497
|
+
if (!location) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1494
1500
|
const size = sliceSize(location.size, begin, end);
|
|
1495
1501
|
const sliced = {
|
|
1496
1502
|
filename: location.filename,
|
|
@@ -1599,7 +1605,7 @@ var NodeType = /* @__PURE__ */ ((NodeType2) => {
|
|
|
1599
1605
|
})(NodeType || {});
|
|
1600
1606
|
|
|
1601
1607
|
const DOCUMENT_NODE_NAME = "#document";
|
|
1602
|
-
const TEXT_CONTENT = Symbol("textContent");
|
|
1608
|
+
const TEXT_CONTENT = /* @__PURE__ */ Symbol("textContent");
|
|
1603
1609
|
let counter = 0;
|
|
1604
1610
|
class DOMNode {
|
|
1605
1611
|
nodeName;
|
|
@@ -2369,7 +2375,7 @@ class Selector {
|
|
|
2369
2375
|
|
|
2370
2376
|
const TEXT_NODE_NAME = "#text";
|
|
2371
2377
|
function isTextNode(node) {
|
|
2372
|
-
return
|
|
2378
|
+
return node?.nodeType === NodeType.TEXT_NODE;
|
|
2373
2379
|
}
|
|
2374
2380
|
class TextNode extends DOMNode {
|
|
2375
2381
|
text;
|
|
@@ -2402,8 +2408,8 @@ class TextNode extends DOMNode {
|
|
|
2402
2408
|
}
|
|
2403
2409
|
}
|
|
2404
2410
|
|
|
2405
|
-
const ROLE = Symbol("role");
|
|
2406
|
-
const TABINDEX = Symbol("tabindex");
|
|
2411
|
+
const ROLE = /* @__PURE__ */ Symbol("role");
|
|
2412
|
+
const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
|
|
2407
2413
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
2408
2414
|
NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
|
|
2409
2415
|
NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
|
|
@@ -2413,7 +2419,7 @@ var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
|
2413
2419
|
return NodeClosed2;
|
|
2414
2420
|
})(NodeClosed || {});
|
|
2415
2421
|
function isElementNode(node) {
|
|
2416
|
-
return
|
|
2422
|
+
return node?.nodeType === NodeType.ELEMENT_NODE;
|
|
2417
2423
|
}
|
|
2418
2424
|
function isInvalidTagName(tagName) {
|
|
2419
2425
|
return tagName === "" || tagName === "*";
|
|
@@ -3359,7 +3365,7 @@ function parseSeverity(value) {
|
|
|
3359
3365
|
}
|
|
3360
3366
|
}
|
|
3361
3367
|
|
|
3362
|
-
const cacheKey = Symbol("aria-naming");
|
|
3368
|
+
const cacheKey = /* @__PURE__ */ Symbol("aria-naming");
|
|
3363
3369
|
const defaultValue = "allowed";
|
|
3364
3370
|
const prohibitedRoles = [
|
|
3365
3371
|
"caption",
|
|
@@ -3578,10 +3584,10 @@ function isPresentation(node) {
|
|
|
3578
3584
|
}
|
|
3579
3585
|
|
|
3580
3586
|
const cachePrefix = classifyNodeText.name;
|
|
3581
|
-
const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
|
|
3582
|
-
const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
|
|
3583
|
-
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
3584
|
-
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
3587
|
+
const HTML_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|html`);
|
|
3588
|
+
const A11Y_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|a11y`);
|
|
3589
|
+
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
3590
|
+
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
3585
3591
|
var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
|
|
3586
3592
|
TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
3587
3593
|
TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
@@ -3709,6 +3715,7 @@ function interpolate(text, data) {
|
|
|
3709
3715
|
|
|
3710
3716
|
const ajv$1 = new Ajv__default.default({ strict: true, strictTuples: true, strictTypes: true });
|
|
3711
3717
|
ajv$1.addMetaSchema(ajvSchemaDraft);
|
|
3718
|
+
ajv$1.addKeyword(ajvRegexpKeyword);
|
|
3712
3719
|
function getSchemaValidator(ruleId, properties) {
|
|
3713
3720
|
const $id = `rule/${ruleId}`;
|
|
3714
3721
|
const cached = ajv$1.getSchema($id);
|
|
@@ -3965,6 +3972,7 @@ class Rule {
|
|
|
3965
3972
|
*
|
|
3966
3973
|
* @internal
|
|
3967
3974
|
*/
|
|
3975
|
+
/* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
|
|
3968
3976
|
static validateOptions(cls, ruleId, jsonPath, options, filename, config) {
|
|
3969
3977
|
if (!cls) {
|
|
3970
3978
|
return;
|
|
@@ -5189,7 +5197,9 @@ class AttributeAllowedValues extends Rule {
|
|
|
5189
5197
|
const doc = event.document;
|
|
5190
5198
|
walk.depthFirst(doc, (node) => {
|
|
5191
5199
|
const meta = node.meta;
|
|
5192
|
-
if (!meta?.attributes)
|
|
5200
|
+
if (!meta?.attributes) {
|
|
5201
|
+
return;
|
|
5202
|
+
}
|
|
5193
5203
|
for (const attr of node.attributes) {
|
|
5194
5204
|
if (Validator.validateAttribute(attr, meta.attributes)) {
|
|
5195
5205
|
continue;
|
|
@@ -5249,9 +5259,13 @@ class AttributeBooleanStyle extends Rule {
|
|
|
5249
5259
|
const doc = event.document;
|
|
5250
5260
|
walk.depthFirst(doc, (node) => {
|
|
5251
5261
|
const meta = node.meta;
|
|
5252
|
-
if (!meta?.attributes)
|
|
5262
|
+
if (!meta?.attributes) {
|
|
5263
|
+
return;
|
|
5264
|
+
}
|
|
5253
5265
|
for (const attr of node.attributes) {
|
|
5254
|
-
if (!this.isBoolean(attr, meta.attributes))
|
|
5266
|
+
if (!this.isBoolean(attr, meta.attributes)) {
|
|
5267
|
+
continue;
|
|
5268
|
+
}
|
|
5255
5269
|
if (attr.originalAttribute) {
|
|
5256
5270
|
continue;
|
|
5257
5271
|
}
|
|
@@ -5321,7 +5335,9 @@ class AttributeEmptyStyle extends Rule {
|
|
|
5321
5335
|
const doc = event.document;
|
|
5322
5336
|
walk.depthFirst(doc, (node) => {
|
|
5323
5337
|
const meta = node.meta;
|
|
5324
|
-
if (!meta?.attributes)
|
|
5338
|
+
if (!meta?.attributes) {
|
|
5339
|
+
return;
|
|
5340
|
+
}
|
|
5325
5341
|
for (const attr of node.attributes) {
|
|
5326
5342
|
if (!allowsEmpty(attr, meta.attributes)) {
|
|
5327
5343
|
continue;
|
|
@@ -5414,7 +5430,22 @@ class AttributeMisuse extends Rule {
|
|
|
5414
5430
|
}
|
|
5415
5431
|
}
|
|
5416
5432
|
|
|
5433
|
+
const patternNamesValues = [
|
|
5434
|
+
"kebabcase",
|
|
5435
|
+
"camelcase",
|
|
5436
|
+
"underscore",
|
|
5437
|
+
"snakecase",
|
|
5438
|
+
"bem",
|
|
5439
|
+
"tailwind"
|
|
5440
|
+
];
|
|
5441
|
+
const patternNames = new Set(patternNamesValues);
|
|
5442
|
+
function isNamedPattern(value) {
|
|
5443
|
+
return typeof value === "string" && patternNames.has(value);
|
|
5444
|
+
}
|
|
5417
5445
|
function parsePattern(pattern) {
|
|
5446
|
+
if (pattern instanceof RegExp) {
|
|
5447
|
+
return { regexp: pattern, description: pattern.toString() };
|
|
5448
|
+
}
|
|
5418
5449
|
switch (pattern) {
|
|
5419
5450
|
case "kebabcase":
|
|
5420
5451
|
return { regexp: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, description: pattern };
|
|
@@ -5432,7 +5463,21 @@ function parsePattern(pattern) {
|
|
|
5432
5463
|
description: pattern
|
|
5433
5464
|
};
|
|
5434
5465
|
}
|
|
5466
|
+
case "tailwind": {
|
|
5467
|
+
return {
|
|
5468
|
+
regexp: /^!?(?:[-a-z[]|\d+xl:)[\w\-:./\\[\]()#'&>,!=%]*$/,
|
|
5469
|
+
description: "tailwind"
|
|
5470
|
+
};
|
|
5471
|
+
}
|
|
5435
5472
|
default: {
|
|
5473
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
5474
|
+
const regexpSource = pattern.slice(1, -1);
|
|
5475
|
+
const regexp2 = new RegExp(regexpSource);
|
|
5476
|
+
return { regexp: regexp2, description: regexp2.toString() };
|
|
5477
|
+
}
|
|
5478
|
+
console.warn(
|
|
5479
|
+
`Custom pattern "${pattern}" should be wrapped in forward slashes, e.g., "/${pattern}/". Support for unwrapped patterns is deprecated and will be removed in a future version.`
|
|
5480
|
+
);
|
|
5436
5481
|
const regexp = new RegExp(pattern);
|
|
5437
5482
|
return { regexp, description: regexp.toString() };
|
|
5438
5483
|
}
|
|
@@ -5440,27 +5485,49 @@ function parsePattern(pattern) {
|
|
|
5440
5485
|
}
|
|
5441
5486
|
|
|
5442
5487
|
function toArray$2(value) {
|
|
5443
|
-
|
|
5488
|
+
if (Array.isArray(value)) {
|
|
5489
|
+
return value;
|
|
5490
|
+
} else {
|
|
5491
|
+
return [value];
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
function validateAllowedPatterns(patterns, allowedPatterns, ruleId) {
|
|
5495
|
+
const extraneous = patterns.filter(isNamedPattern).filter((p) => !allowedPatterns.has(p));
|
|
5496
|
+
if (extraneous.length > 0) {
|
|
5497
|
+
const quote = (it) => `"${it}"`;
|
|
5498
|
+
const disallowed = utils_naturalJoin.naturalJoin(extraneous.map(quote), "and");
|
|
5499
|
+
const allowed = utils_naturalJoin.naturalJoin(Array.from(allowedPatterns, quote), "and");
|
|
5500
|
+
throw new Error(
|
|
5501
|
+
`Pattern ${disallowed} cannot be used with "${ruleId}". Allowed patterns: ${allowed}`
|
|
5502
|
+
);
|
|
5503
|
+
}
|
|
5444
5504
|
}
|
|
5445
5505
|
class BasePatternRule extends Rule {
|
|
5446
5506
|
/** Attribute being tested */
|
|
5447
5507
|
attr;
|
|
5448
5508
|
/** Parsed configured patterns */
|
|
5449
5509
|
patterns;
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5510
|
+
constructor({
|
|
5511
|
+
ruleId,
|
|
5512
|
+
attr,
|
|
5513
|
+
options,
|
|
5514
|
+
allowedPatterns
|
|
5515
|
+
}) {
|
|
5455
5516
|
super(options);
|
|
5456
5517
|
const { pattern } = this.options;
|
|
5457
5518
|
this.attr = attr;
|
|
5458
|
-
|
|
5519
|
+
const patterns = toArray$2(pattern);
|
|
5520
|
+
validateAllowedPatterns(patterns, allowedPatterns, ruleId);
|
|
5521
|
+
this.patterns = patterns.map((it) => parsePattern(it));
|
|
5459
5522
|
}
|
|
5460
5523
|
static schema() {
|
|
5461
5524
|
return {
|
|
5462
5525
|
pattern: {
|
|
5463
|
-
|
|
5526
|
+
anyOf: [
|
|
5527
|
+
{ type: "array", items: { anyOf: [{ type: "string" }, { regexp: true }] }, minItems: 1 },
|
|
5528
|
+
{ type: "string" },
|
|
5529
|
+
{ regexp: true }
|
|
5530
|
+
]
|
|
5464
5531
|
}
|
|
5465
5532
|
};
|
|
5466
5533
|
}
|
|
@@ -5499,7 +5566,13 @@ const defaults$q = {
|
|
|
5499
5566
|
};
|
|
5500
5567
|
class ClassPattern extends BasePatternRule {
|
|
5501
5568
|
constructor(options) {
|
|
5502
|
-
super(
|
|
5569
|
+
super({
|
|
5570
|
+
ruleId: "class-pattern",
|
|
5571
|
+
attr: "class",
|
|
5572
|
+
options: { ...defaults$q, ...options },
|
|
5573
|
+
allowedPatterns: patternNames
|
|
5574
|
+
// allow all patterns
|
|
5575
|
+
});
|
|
5503
5576
|
}
|
|
5504
5577
|
static schema() {
|
|
5505
5578
|
return BasePatternRule.schema();
|
|
@@ -6449,7 +6522,9 @@ class EmptyTitle extends Rule {
|
|
|
6449
6522
|
setup() {
|
|
6450
6523
|
this.on("tag:end", (event) => {
|
|
6451
6524
|
const node = event.previous;
|
|
6452
|
-
if (node.tagName !== "title")
|
|
6525
|
+
if (node.tagName !== "title") {
|
|
6526
|
+
return;
|
|
6527
|
+
}
|
|
6453
6528
|
switch (classifyNodeText(node)) {
|
|
6454
6529
|
case TextClassification.DYNAMIC_TEXT:
|
|
6455
6530
|
case TextClassification.STATIC_TEXT:
|
|
@@ -6470,8 +6545,8 @@ const defaults$l = {
|
|
|
6470
6545
|
allowCheckboxDefault: true,
|
|
6471
6546
|
shared: ["radio", "button", "reset", "submit"]
|
|
6472
6547
|
};
|
|
6473
|
-
const UNIQUE_CACHE_KEY = Symbol("form-elements-unique");
|
|
6474
|
-
const SHARED_CACHE_KEY = Symbol("form-elements-shared");
|
|
6548
|
+
const UNIQUE_CACHE_KEY = /* @__PURE__ */ Symbol("form-elements-unique");
|
|
6549
|
+
const SHARED_CACHE_KEY = /* @__PURE__ */ Symbol("form-elements-shared");
|
|
6475
6550
|
function isEnabled(element) {
|
|
6476
6551
|
if (isHTMLHidden(element)) {
|
|
6477
6552
|
return false;
|
|
@@ -6770,7 +6845,9 @@ class HeadingLevel extends Rule {
|
|
|
6770
6845
|
}
|
|
6771
6846
|
onTagStart(event) {
|
|
6772
6847
|
const level = extractLevel(event.target);
|
|
6773
|
-
if (!level)
|
|
6848
|
+
if (!level) {
|
|
6849
|
+
return;
|
|
6850
|
+
}
|
|
6774
6851
|
const root = this.getCurrentRoot();
|
|
6775
6852
|
if (!this.options.allowMultipleH1 && level === 1) {
|
|
6776
6853
|
if (root.h1Count >= 1) {
|
|
@@ -6956,9 +7033,22 @@ class HiddenFocusable extends Rule {
|
|
|
6956
7033
|
const defaults$j = {
|
|
6957
7034
|
pattern: "kebabcase"
|
|
6958
7035
|
};
|
|
7036
|
+
function exclude$1(set, ...values) {
|
|
7037
|
+
const result = new Set(set);
|
|
7038
|
+
for (const value of values) {
|
|
7039
|
+
result.delete(value);
|
|
7040
|
+
}
|
|
7041
|
+
return result;
|
|
7042
|
+
}
|
|
6959
7043
|
class IdPattern extends BasePatternRule {
|
|
6960
7044
|
constructor(options) {
|
|
6961
|
-
|
|
7045
|
+
const allowedPatterns = exclude$1(patternNames, "tailwind");
|
|
7046
|
+
super({
|
|
7047
|
+
ruleId: "id-pattern",
|
|
7048
|
+
attr: "id",
|
|
7049
|
+
options: { ...defaults$j, ...options },
|
|
7050
|
+
allowedPatterns
|
|
7051
|
+
});
|
|
6962
7052
|
}
|
|
6963
7053
|
static schema() {
|
|
6964
7054
|
return BasePatternRule.schema();
|
|
@@ -7262,7 +7352,9 @@ class InputMissingLabel extends Rule {
|
|
|
7262
7352
|
}
|
|
7263
7353
|
}
|
|
7264
7354
|
function findLabelById(root, id) {
|
|
7265
|
-
if (!id)
|
|
7355
|
+
if (!id) {
|
|
7356
|
+
return [];
|
|
7357
|
+
}
|
|
7266
7358
|
return root.querySelectorAll(`label[for="${id}"]`);
|
|
7267
7359
|
}
|
|
7268
7360
|
function findLabelByParent(el) {
|
|
@@ -7301,7 +7393,9 @@ class LongTitle extends Rule {
|
|
|
7301
7393
|
setup() {
|
|
7302
7394
|
this.on("tag:end", (event) => {
|
|
7303
7395
|
const node = event.previous;
|
|
7304
|
-
if (node.tagName !== "title")
|
|
7396
|
+
if (node.tagName !== "title") {
|
|
7397
|
+
return;
|
|
7398
|
+
}
|
|
7305
7399
|
const text = node.textContent;
|
|
7306
7400
|
if (text.length > this.maxlength) {
|
|
7307
7401
|
this.report(node, `title text cannot be longer than ${String(this.maxlength)} characters`);
|
|
@@ -7503,9 +7597,22 @@ class MultipleLabeledControls extends Rule {
|
|
|
7503
7597
|
const defaults$g = {
|
|
7504
7598
|
pattern: "camelcase"
|
|
7505
7599
|
};
|
|
7600
|
+
function exclude(set, ...values) {
|
|
7601
|
+
const result = new Set(set);
|
|
7602
|
+
for (const value of values) {
|
|
7603
|
+
result.delete(value);
|
|
7604
|
+
}
|
|
7605
|
+
return result;
|
|
7606
|
+
}
|
|
7506
7607
|
class NamePattern extends BasePatternRule {
|
|
7507
7608
|
constructor(options) {
|
|
7508
|
-
|
|
7609
|
+
const allowedPatterns = exclude(patternNames, "tailwind");
|
|
7610
|
+
super({
|
|
7611
|
+
ruleId: "name-pattern",
|
|
7612
|
+
attr: "name",
|
|
7613
|
+
options: { ...defaults$g, ...options },
|
|
7614
|
+
allowedPatterns
|
|
7615
|
+
});
|
|
7509
7616
|
}
|
|
7510
7617
|
static schema() {
|
|
7511
7618
|
return BasePatternRule.schema();
|
|
@@ -7761,7 +7868,7 @@ class NoDupClass extends Rule {
|
|
|
7761
7868
|
}
|
|
7762
7869
|
}
|
|
7763
7870
|
|
|
7764
|
-
const CACHE_KEY = Symbol("no-dup-id");
|
|
7871
|
+
const CACHE_KEY = /* @__PURE__ */ Symbol("no-dup-id");
|
|
7765
7872
|
class NoDupID extends Rule {
|
|
7766
7873
|
documentation() {
|
|
7767
7874
|
return {
|
|
@@ -7866,7 +7973,7 @@ Omitted end tags can be ambigious for humans to read and many editors have troub
|
|
|
7866
7973
|
return;
|
|
7867
7974
|
}
|
|
7868
7975
|
const parent = closed.parent;
|
|
7869
|
-
const closedByParent = parent
|
|
7976
|
+
const closedByParent = parent?.tagName === by.tagName;
|
|
7870
7977
|
const closedByDocument = closedByParent && parent.isRootElement();
|
|
7871
7978
|
const sameTag = closed.tagName === by.tagName;
|
|
7872
7979
|
if (closedByDocument) {
|
|
@@ -8228,7 +8335,7 @@ class NoRedundantAriaLabel extends Rule {
|
|
|
8228
8335
|
continue;
|
|
8229
8336
|
}
|
|
8230
8337
|
const label = document.querySelector(`label[for="${id}"]`);
|
|
8231
|
-
if (!ariaLabel ||
|
|
8338
|
+
if (!ariaLabel || label?.textContent.trim() !== ariaLabel.value) {
|
|
8232
8339
|
continue;
|
|
8233
8340
|
}
|
|
8234
8341
|
const message = "aria-label is redundant when label containing same text exists";
|
|
@@ -10047,6 +10154,46 @@ class ValidAutocomplete extends Rule {
|
|
|
10047
10154
|
}
|
|
10048
10155
|
}
|
|
10049
10156
|
|
|
10157
|
+
function isLabelable(target) {
|
|
10158
|
+
const { meta } = target;
|
|
10159
|
+
if (!meta) {
|
|
10160
|
+
return true;
|
|
10161
|
+
}
|
|
10162
|
+
return Boolean(meta.labelable);
|
|
10163
|
+
}
|
|
10164
|
+
class ValidFor extends Rule {
|
|
10165
|
+
documentation() {
|
|
10166
|
+
return {
|
|
10167
|
+
description: `The \`<label>\` \`for\` attribute must reference a labelable form control.`,
|
|
10168
|
+
url: "https://html-validate.org/rules/valid-for.html"
|
|
10169
|
+
};
|
|
10170
|
+
}
|
|
10171
|
+
setup() {
|
|
10172
|
+
this.on("dom:ready", (event) => {
|
|
10173
|
+
const { document } = event;
|
|
10174
|
+
for (const node of document.querySelectorAll("label[for]")) {
|
|
10175
|
+
const attr = node.getAttribute("for");
|
|
10176
|
+
if (!isStaticAttribute(attr) || !attr.value) {
|
|
10177
|
+
continue;
|
|
10178
|
+
}
|
|
10179
|
+
const selector = generateIdSelector(attr.value);
|
|
10180
|
+
const target = document.querySelector(selector);
|
|
10181
|
+
if (!target) {
|
|
10182
|
+
continue;
|
|
10183
|
+
}
|
|
10184
|
+
if (isLabelable(target)) {
|
|
10185
|
+
continue;
|
|
10186
|
+
}
|
|
10187
|
+
this.report({
|
|
10188
|
+
node,
|
|
10189
|
+
message: '<label> "for" attribute must reference a labelable form control',
|
|
10190
|
+
location: attr.valueLocation
|
|
10191
|
+
});
|
|
10192
|
+
}
|
|
10193
|
+
});
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
|
|
10050
10197
|
const defaults$4 = {
|
|
10051
10198
|
relaxed: false
|
|
10052
10199
|
};
|
|
@@ -10345,7 +10492,9 @@ class H36 extends Rule {
|
|
|
10345
10492
|
setup() {
|
|
10346
10493
|
this.on("tag:end", (event) => {
|
|
10347
10494
|
const node = event.previous;
|
|
10348
|
-
if (node.tagName !== "input")
|
|
10495
|
+
if (node.tagName !== "input") {
|
|
10496
|
+
return;
|
|
10497
|
+
}
|
|
10349
10498
|
if (node.getAttributeValue("type") !== "image") {
|
|
10350
10499
|
return;
|
|
10351
10500
|
}
|
|
@@ -10671,6 +10820,7 @@ const bundledRules = {
|
|
|
10671
10820
|
"unique-landmark": UniqueLandmark,
|
|
10672
10821
|
"unrecognized-char-ref": UnknownCharReference,
|
|
10673
10822
|
"valid-autocomplete": ValidAutocomplete,
|
|
10823
|
+
"valid-for": ValidFor,
|
|
10674
10824
|
"valid-id": ValidID,
|
|
10675
10825
|
"void-content": VoidContent,
|
|
10676
10826
|
"void-style": VoidStyle,
|
|
@@ -11024,6 +11174,7 @@ const config$1 = {
|
|
|
11024
11174
|
"unique-landmark": "error",
|
|
11025
11175
|
"unrecognized-char-ref": "error",
|
|
11026
11176
|
"valid-autocomplete": "error",
|
|
11177
|
+
"valid-for": "error",
|
|
11027
11178
|
"valid-id": ["error", { relaxed: false }],
|
|
11028
11179
|
void: "off",
|
|
11029
11180
|
"void-content": "error",
|
|
@@ -11071,6 +11222,7 @@ const config = {
|
|
|
11071
11222
|
"script-element": "error",
|
|
11072
11223
|
"unrecognized-char-ref": "error",
|
|
11073
11224
|
"valid-autocomplete": "error",
|
|
11225
|
+
"valid-for": "error",
|
|
11074
11226
|
"valid-id": ["error", { relaxed: true }],
|
|
11075
11227
|
"void-content": "error"
|
|
11076
11228
|
}
|
|
@@ -11677,7 +11829,9 @@ class Config {
|
|
|
11677
11829
|
}
|
|
11678
11830
|
for (const plugin of plugins) {
|
|
11679
11831
|
for (const [name, config] of Object.entries(plugin.configs ?? {})) {
|
|
11680
|
-
if (!config)
|
|
11832
|
+
if (!config) {
|
|
11833
|
+
continue;
|
|
11834
|
+
}
|
|
11681
11835
|
Config.validate(config, name);
|
|
11682
11836
|
configs.set(`${plugin.name}:${name}`, config);
|
|
11683
11837
|
if (plugin.name !== plugin.originalName) {
|
|
@@ -11978,7 +12132,7 @@ class EventHandler {
|
|
|
11978
12132
|
}
|
|
11979
12133
|
|
|
11980
12134
|
const name = "html-validate";
|
|
11981
|
-
const version = "10.
|
|
12135
|
+
const version = "10.6.0";
|
|
11982
12136
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
11983
12137
|
|
|
11984
12138
|
function freeze(src) {
|
|
@@ -12029,6 +12183,7 @@ class Reporter {
|
|
|
12029
12183
|
warningCount: sumWarnings(results)
|
|
12030
12184
|
};
|
|
12031
12185
|
}
|
|
12186
|
+
/* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
|
|
12032
12187
|
add(rule, message, severity, node, location, context) {
|
|
12033
12188
|
if (!(location.filename in this.result)) {
|
|
12034
12189
|
this.result[location.filename] = [];
|
|
@@ -12148,7 +12303,7 @@ class ParserError extends Error {
|
|
|
12148
12303
|
}
|
|
12149
12304
|
|
|
12150
12305
|
function isAttrValueToken(token) {
|
|
12151
|
-
return
|
|
12306
|
+
return token?.type === TokenType.ATTR_VALUE;
|
|
12152
12307
|
}
|
|
12153
12308
|
function svgShouldRetainTag(foreignTagName, tagName) {
|
|
12154
12309
|
return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
|
|
@@ -12494,7 +12649,7 @@ class Parser {
|
|
|
12494
12649
|
* ^^^ ^^^ ^^^ (null) (null)
|
|
12495
12650
|
*/
|
|
12496
12651
|
getAttributeValueLocation(token) {
|
|
12497
|
-
if (
|
|
12652
|
+
if (token?.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
|
|
12498
12653
|
return null;
|
|
12499
12654
|
}
|
|
12500
12655
|
const quote = token.data[3];
|
|
@@ -12510,7 +12665,7 @@ class Parser {
|
|
|
12510
12665
|
*/
|
|
12511
12666
|
getAttributeLocation(key, value) {
|
|
12512
12667
|
const begin = key.location;
|
|
12513
|
-
const end = value
|
|
12668
|
+
const end = value?.type === TokenType.ATTR_VALUE ? value.location : void 0;
|
|
12514
12669
|
return {
|
|
12515
12670
|
filename: begin.filename,
|
|
12516
12671
|
line: begin.line,
|
|
@@ -12532,7 +12687,11 @@ class Parser {
|
|
|
12532
12687
|
throw new Error(`Failed to parse directive "${text}"`);
|
|
12533
12688
|
}
|
|
12534
12689
|
if (!isValidDirective(action)) {
|
|
12535
|
-
|
|
12690
|
+
this.trigger("parse:error", {
|
|
12691
|
+
location: token.location,
|
|
12692
|
+
message: `Unknown directive "${action}"`
|
|
12693
|
+
});
|
|
12694
|
+
return;
|
|
12536
12695
|
}
|
|
12537
12696
|
const [, data, separator2, comment] = match;
|
|
12538
12697
|
const prefix = "html-validate-";
|
|
@@ -12625,7 +12784,9 @@ class Parser {
|
|
|
12625
12784
|
while (!it.done) {
|
|
12626
12785
|
const token = it.value;
|
|
12627
12786
|
yield token;
|
|
12628
|
-
if (token.type === search)
|
|
12787
|
+
if (token.type === search) {
|
|
12788
|
+
return;
|
|
12789
|
+
}
|
|
12629
12790
|
it = this.next(tokenStream);
|
|
12630
12791
|
}
|
|
12631
12792
|
throw new ParserError(
|
|
@@ -12788,6 +12949,9 @@ class Engine {
|
|
|
12788
12949
|
parser.on("directive", (_2, event) => {
|
|
12789
12950
|
this.processDirective(event, parser, directiveContext);
|
|
12790
12951
|
});
|
|
12952
|
+
parser.on("parse:error", (_2, event) => {
|
|
12953
|
+
this.reportError("parser-error", event.message, event.location);
|
|
12954
|
+
});
|
|
12791
12955
|
try {
|
|
12792
12956
|
parser.parseHtml(source);
|
|
12793
12957
|
} catch (e) {
|
|
@@ -12985,7 +13149,9 @@ class Engine {
|
|
|
12985
13149
|
const availableRules = {};
|
|
12986
13150
|
for (const plugin of config.getPlugins()) {
|
|
12987
13151
|
for (const [name, rule] of Object.entries(plugin.rules ?? {})) {
|
|
12988
|
-
if (!rule)
|
|
13152
|
+
if (!rule) {
|
|
13153
|
+
continue;
|
|
13154
|
+
}
|
|
12989
13155
|
availableRules[name] = rule;
|
|
12990
13156
|
}
|
|
12991
13157
|
}
|
|
@@ -13018,6 +13184,7 @@ class Engine {
|
|
|
13018
13184
|
/**
|
|
13019
13185
|
* Load and setup a rule using current config.
|
|
13020
13186
|
*/
|
|
13187
|
+
/* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
|
|
13021
13188
|
loadRule(ruleId, config, severity, options, parser, report) {
|
|
13022
13189
|
const meta = config.getMetaTable();
|
|
13023
13190
|
const rule = this.instantiateRule(ruleId, options);
|