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/esm/core.js
CHANGED
|
@@ -1207,7 +1207,9 @@ class MetaTable {
|
|
|
1207
1207
|
);
|
|
1208
1208
|
}
|
|
1209
1209
|
for (const [key, value] of Object.entries(obj)) {
|
|
1210
|
-
if (key === "$schema")
|
|
1210
|
+
if (key === "$schema") {
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1211
1213
|
this.addEntry(key, migrateElement(value));
|
|
1212
1214
|
}
|
|
1213
1215
|
} catch (err) {
|
|
@@ -1307,7 +1309,9 @@ class MetaTable {
|
|
|
1307
1309
|
* global, e.g. to assign global attributes.
|
|
1308
1310
|
*/
|
|
1309
1311
|
resolveGlobal() {
|
|
1310
|
-
if (!this.elements["*"])
|
|
1312
|
+
if (!this.elements["*"]) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1311
1315
|
const global = this.elements["*"];
|
|
1312
1316
|
delete this.elements["*"];
|
|
1313
1317
|
delete global.tagName;
|
|
@@ -1481,7 +1485,9 @@ function sliceSize(size, begin, end) {
|
|
|
1481
1485
|
return Math.min(size, end - begin);
|
|
1482
1486
|
}
|
|
1483
1487
|
function sliceLocation(location, begin, end, wrap) {
|
|
1484
|
-
if (!location)
|
|
1488
|
+
if (!location) {
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1485
1491
|
const size = sliceSize(location.size, begin, end);
|
|
1486
1492
|
const sliced = {
|
|
1487
1493
|
filename: location.filename,
|
|
@@ -1590,7 +1596,7 @@ var NodeType = /* @__PURE__ */ ((NodeType2) => {
|
|
|
1590
1596
|
})(NodeType || {});
|
|
1591
1597
|
|
|
1592
1598
|
const DOCUMENT_NODE_NAME = "#document";
|
|
1593
|
-
const TEXT_CONTENT = Symbol("textContent");
|
|
1599
|
+
const TEXT_CONTENT = /* @__PURE__ */ Symbol("textContent");
|
|
1594
1600
|
let counter = 0;
|
|
1595
1601
|
class DOMNode {
|
|
1596
1602
|
nodeName;
|
|
@@ -2360,7 +2366,7 @@ class Selector {
|
|
|
2360
2366
|
|
|
2361
2367
|
const TEXT_NODE_NAME = "#text";
|
|
2362
2368
|
function isTextNode(node) {
|
|
2363
|
-
return
|
|
2369
|
+
return node?.nodeType === NodeType.TEXT_NODE;
|
|
2364
2370
|
}
|
|
2365
2371
|
class TextNode extends DOMNode {
|
|
2366
2372
|
text;
|
|
@@ -2393,8 +2399,8 @@ class TextNode extends DOMNode {
|
|
|
2393
2399
|
}
|
|
2394
2400
|
}
|
|
2395
2401
|
|
|
2396
|
-
const ROLE = Symbol("role");
|
|
2397
|
-
const TABINDEX = Symbol("tabindex");
|
|
2402
|
+
const ROLE = /* @__PURE__ */ Symbol("role");
|
|
2403
|
+
const TABINDEX = /* @__PURE__ */ Symbol("tabindex");
|
|
2398
2404
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
2399
2405
|
NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
|
|
2400
2406
|
NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
|
|
@@ -2404,7 +2410,7 @@ var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
|
2404
2410
|
return NodeClosed2;
|
|
2405
2411
|
})(NodeClosed || {});
|
|
2406
2412
|
function isElementNode(node) {
|
|
2407
|
-
return
|
|
2413
|
+
return node?.nodeType === NodeType.ELEMENT_NODE;
|
|
2408
2414
|
}
|
|
2409
2415
|
function isInvalidTagName(tagName) {
|
|
2410
2416
|
return tagName === "" || tagName === "*";
|
|
@@ -3350,7 +3356,7 @@ function parseSeverity(value) {
|
|
|
3350
3356
|
}
|
|
3351
3357
|
}
|
|
3352
3358
|
|
|
3353
|
-
const cacheKey = Symbol("aria-naming");
|
|
3359
|
+
const cacheKey = /* @__PURE__ */ Symbol("aria-naming");
|
|
3354
3360
|
const defaultValue = "allowed";
|
|
3355
3361
|
const prohibitedRoles = [
|
|
3356
3362
|
"caption",
|
|
@@ -3569,10 +3575,10 @@ function isPresentation(node) {
|
|
|
3569
3575
|
}
|
|
3570
3576
|
|
|
3571
3577
|
const cachePrefix = classifyNodeText.name;
|
|
3572
|
-
const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
|
|
3573
|
-
const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
|
|
3574
|
-
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
3575
|
-
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
3578
|
+
const HTML_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|html`);
|
|
3579
|
+
const A11Y_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|a11y`);
|
|
3580
|
+
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
3581
|
+
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = /* @__PURE__ */ Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
3576
3582
|
var TextClassification = /* @__PURE__ */ ((TextClassification2) => {
|
|
3577
3583
|
TextClassification2[TextClassification2["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
3578
3584
|
TextClassification2[TextClassification2["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
@@ -3700,6 +3706,7 @@ function interpolate(text, data) {
|
|
|
3700
3706
|
|
|
3701
3707
|
const ajv$1 = new Ajv({ strict: true, strictTuples: true, strictTypes: true });
|
|
3702
3708
|
ajv$1.addMetaSchema(ajvSchemaDraft);
|
|
3709
|
+
ajv$1.addKeyword(ajvRegexpKeyword);
|
|
3703
3710
|
function getSchemaValidator(ruleId, properties) {
|
|
3704
3711
|
const $id = `rule/${ruleId}`;
|
|
3705
3712
|
const cached = ajv$1.getSchema($id);
|
|
@@ -3956,6 +3963,7 @@ class Rule {
|
|
|
3956
3963
|
*
|
|
3957
3964
|
* @internal
|
|
3958
3965
|
*/
|
|
3966
|
+
/* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
|
|
3959
3967
|
static validateOptions(cls, ruleId, jsonPath, options, filename, config) {
|
|
3960
3968
|
if (!cls) {
|
|
3961
3969
|
return;
|
|
@@ -5180,7 +5188,9 @@ class AttributeAllowedValues extends Rule {
|
|
|
5180
5188
|
const doc = event.document;
|
|
5181
5189
|
walk.depthFirst(doc, (node) => {
|
|
5182
5190
|
const meta = node.meta;
|
|
5183
|
-
if (!meta?.attributes)
|
|
5191
|
+
if (!meta?.attributes) {
|
|
5192
|
+
return;
|
|
5193
|
+
}
|
|
5184
5194
|
for (const attr of node.attributes) {
|
|
5185
5195
|
if (Validator.validateAttribute(attr, meta.attributes)) {
|
|
5186
5196
|
continue;
|
|
@@ -5240,9 +5250,13 @@ class AttributeBooleanStyle extends Rule {
|
|
|
5240
5250
|
const doc = event.document;
|
|
5241
5251
|
walk.depthFirst(doc, (node) => {
|
|
5242
5252
|
const meta = node.meta;
|
|
5243
|
-
if (!meta?.attributes)
|
|
5253
|
+
if (!meta?.attributes) {
|
|
5254
|
+
return;
|
|
5255
|
+
}
|
|
5244
5256
|
for (const attr of node.attributes) {
|
|
5245
|
-
if (!this.isBoolean(attr, meta.attributes))
|
|
5257
|
+
if (!this.isBoolean(attr, meta.attributes)) {
|
|
5258
|
+
continue;
|
|
5259
|
+
}
|
|
5246
5260
|
if (attr.originalAttribute) {
|
|
5247
5261
|
continue;
|
|
5248
5262
|
}
|
|
@@ -5312,7 +5326,9 @@ class AttributeEmptyStyle extends Rule {
|
|
|
5312
5326
|
const doc = event.document;
|
|
5313
5327
|
walk.depthFirst(doc, (node) => {
|
|
5314
5328
|
const meta = node.meta;
|
|
5315
|
-
if (!meta?.attributes)
|
|
5329
|
+
if (!meta?.attributes) {
|
|
5330
|
+
return;
|
|
5331
|
+
}
|
|
5316
5332
|
for (const attr of node.attributes) {
|
|
5317
5333
|
if (!allowsEmpty(attr, meta.attributes)) {
|
|
5318
5334
|
continue;
|
|
@@ -5405,7 +5421,22 @@ class AttributeMisuse extends Rule {
|
|
|
5405
5421
|
}
|
|
5406
5422
|
}
|
|
5407
5423
|
|
|
5424
|
+
const patternNamesValues = [
|
|
5425
|
+
"kebabcase",
|
|
5426
|
+
"camelcase",
|
|
5427
|
+
"underscore",
|
|
5428
|
+
"snakecase",
|
|
5429
|
+
"bem",
|
|
5430
|
+
"tailwind"
|
|
5431
|
+
];
|
|
5432
|
+
const patternNames = new Set(patternNamesValues);
|
|
5433
|
+
function isNamedPattern(value) {
|
|
5434
|
+
return typeof value === "string" && patternNames.has(value);
|
|
5435
|
+
}
|
|
5408
5436
|
function parsePattern(pattern) {
|
|
5437
|
+
if (pattern instanceof RegExp) {
|
|
5438
|
+
return { regexp: pattern, description: pattern.toString() };
|
|
5439
|
+
}
|
|
5409
5440
|
switch (pattern) {
|
|
5410
5441
|
case "kebabcase":
|
|
5411
5442
|
return { regexp: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, description: pattern };
|
|
@@ -5423,7 +5454,21 @@ function parsePattern(pattern) {
|
|
|
5423
5454
|
description: pattern
|
|
5424
5455
|
};
|
|
5425
5456
|
}
|
|
5457
|
+
case "tailwind": {
|
|
5458
|
+
return {
|
|
5459
|
+
regexp: /^!?(?:[-a-z[]|\d+xl:)[\w\-:./\\[\]()#'&>,!=%]*$/,
|
|
5460
|
+
description: "tailwind"
|
|
5461
|
+
};
|
|
5462
|
+
}
|
|
5426
5463
|
default: {
|
|
5464
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
5465
|
+
const regexpSource = pattern.slice(1, -1);
|
|
5466
|
+
const regexp2 = new RegExp(regexpSource);
|
|
5467
|
+
return { regexp: regexp2, description: regexp2.toString() };
|
|
5468
|
+
}
|
|
5469
|
+
console.warn(
|
|
5470
|
+
`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.`
|
|
5471
|
+
);
|
|
5427
5472
|
const regexp = new RegExp(pattern);
|
|
5428
5473
|
return { regexp, description: regexp.toString() };
|
|
5429
5474
|
}
|
|
@@ -5431,27 +5476,49 @@ function parsePattern(pattern) {
|
|
|
5431
5476
|
}
|
|
5432
5477
|
|
|
5433
5478
|
function toArray$2(value) {
|
|
5434
|
-
|
|
5479
|
+
if (Array.isArray(value)) {
|
|
5480
|
+
return value;
|
|
5481
|
+
} else {
|
|
5482
|
+
return [value];
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
function validateAllowedPatterns(patterns, allowedPatterns, ruleId) {
|
|
5486
|
+
const extraneous = patterns.filter(isNamedPattern).filter((p) => !allowedPatterns.has(p));
|
|
5487
|
+
if (extraneous.length > 0) {
|
|
5488
|
+
const quote = (it) => `"${it}"`;
|
|
5489
|
+
const disallowed = naturalJoin(extraneous.map(quote), "and");
|
|
5490
|
+
const allowed = naturalJoin(Array.from(allowedPatterns, quote), "and");
|
|
5491
|
+
throw new Error(
|
|
5492
|
+
`Pattern ${disallowed} cannot be used with "${ruleId}". Allowed patterns: ${allowed}`
|
|
5493
|
+
);
|
|
5494
|
+
}
|
|
5435
5495
|
}
|
|
5436
5496
|
class BasePatternRule extends Rule {
|
|
5437
5497
|
/** Attribute being tested */
|
|
5438
5498
|
attr;
|
|
5439
5499
|
/** Parsed configured patterns */
|
|
5440
5500
|
patterns;
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5501
|
+
constructor({
|
|
5502
|
+
ruleId,
|
|
5503
|
+
attr,
|
|
5504
|
+
options,
|
|
5505
|
+
allowedPatterns
|
|
5506
|
+
}) {
|
|
5446
5507
|
super(options);
|
|
5447
5508
|
const { pattern } = this.options;
|
|
5448
5509
|
this.attr = attr;
|
|
5449
|
-
|
|
5510
|
+
const patterns = toArray$2(pattern);
|
|
5511
|
+
validateAllowedPatterns(patterns, allowedPatterns, ruleId);
|
|
5512
|
+
this.patterns = patterns.map((it) => parsePattern(it));
|
|
5450
5513
|
}
|
|
5451
5514
|
static schema() {
|
|
5452
5515
|
return {
|
|
5453
5516
|
pattern: {
|
|
5454
|
-
|
|
5517
|
+
anyOf: [
|
|
5518
|
+
{ type: "array", items: { anyOf: [{ type: "string" }, { regexp: true }] }, minItems: 1 },
|
|
5519
|
+
{ type: "string" },
|
|
5520
|
+
{ regexp: true }
|
|
5521
|
+
]
|
|
5455
5522
|
}
|
|
5456
5523
|
};
|
|
5457
5524
|
}
|
|
@@ -5490,7 +5557,13 @@ const defaults$q = {
|
|
|
5490
5557
|
};
|
|
5491
5558
|
class ClassPattern extends BasePatternRule {
|
|
5492
5559
|
constructor(options) {
|
|
5493
|
-
super(
|
|
5560
|
+
super({
|
|
5561
|
+
ruleId: "class-pattern",
|
|
5562
|
+
attr: "class",
|
|
5563
|
+
options: { ...defaults$q, ...options },
|
|
5564
|
+
allowedPatterns: patternNames
|
|
5565
|
+
// allow all patterns
|
|
5566
|
+
});
|
|
5494
5567
|
}
|
|
5495
5568
|
static schema() {
|
|
5496
5569
|
return BasePatternRule.schema();
|
|
@@ -6440,7 +6513,9 @@ class EmptyTitle extends Rule {
|
|
|
6440
6513
|
setup() {
|
|
6441
6514
|
this.on("tag:end", (event) => {
|
|
6442
6515
|
const node = event.previous;
|
|
6443
|
-
if (node.tagName !== "title")
|
|
6516
|
+
if (node.tagName !== "title") {
|
|
6517
|
+
return;
|
|
6518
|
+
}
|
|
6444
6519
|
switch (classifyNodeText(node)) {
|
|
6445
6520
|
case TextClassification.DYNAMIC_TEXT:
|
|
6446
6521
|
case TextClassification.STATIC_TEXT:
|
|
@@ -6461,8 +6536,8 @@ const defaults$l = {
|
|
|
6461
6536
|
allowCheckboxDefault: true,
|
|
6462
6537
|
shared: ["radio", "button", "reset", "submit"]
|
|
6463
6538
|
};
|
|
6464
|
-
const UNIQUE_CACHE_KEY = Symbol("form-elements-unique");
|
|
6465
|
-
const SHARED_CACHE_KEY = Symbol("form-elements-shared");
|
|
6539
|
+
const UNIQUE_CACHE_KEY = /* @__PURE__ */ Symbol("form-elements-unique");
|
|
6540
|
+
const SHARED_CACHE_KEY = /* @__PURE__ */ Symbol("form-elements-shared");
|
|
6466
6541
|
function isEnabled(element) {
|
|
6467
6542
|
if (isHTMLHidden(element)) {
|
|
6468
6543
|
return false;
|
|
@@ -6761,7 +6836,9 @@ class HeadingLevel extends Rule {
|
|
|
6761
6836
|
}
|
|
6762
6837
|
onTagStart(event) {
|
|
6763
6838
|
const level = extractLevel(event.target);
|
|
6764
|
-
if (!level)
|
|
6839
|
+
if (!level) {
|
|
6840
|
+
return;
|
|
6841
|
+
}
|
|
6765
6842
|
const root = this.getCurrentRoot();
|
|
6766
6843
|
if (!this.options.allowMultipleH1 && level === 1) {
|
|
6767
6844
|
if (root.h1Count >= 1) {
|
|
@@ -6947,9 +7024,22 @@ class HiddenFocusable extends Rule {
|
|
|
6947
7024
|
const defaults$j = {
|
|
6948
7025
|
pattern: "kebabcase"
|
|
6949
7026
|
};
|
|
7027
|
+
function exclude$1(set, ...values) {
|
|
7028
|
+
const result = new Set(set);
|
|
7029
|
+
for (const value of values) {
|
|
7030
|
+
result.delete(value);
|
|
7031
|
+
}
|
|
7032
|
+
return result;
|
|
7033
|
+
}
|
|
6950
7034
|
class IdPattern extends BasePatternRule {
|
|
6951
7035
|
constructor(options) {
|
|
6952
|
-
|
|
7036
|
+
const allowedPatterns = exclude$1(patternNames, "tailwind");
|
|
7037
|
+
super({
|
|
7038
|
+
ruleId: "id-pattern",
|
|
7039
|
+
attr: "id",
|
|
7040
|
+
options: { ...defaults$j, ...options },
|
|
7041
|
+
allowedPatterns
|
|
7042
|
+
});
|
|
6953
7043
|
}
|
|
6954
7044
|
static schema() {
|
|
6955
7045
|
return BasePatternRule.schema();
|
|
@@ -7253,7 +7343,9 @@ class InputMissingLabel extends Rule {
|
|
|
7253
7343
|
}
|
|
7254
7344
|
}
|
|
7255
7345
|
function findLabelById(root, id) {
|
|
7256
|
-
if (!id)
|
|
7346
|
+
if (!id) {
|
|
7347
|
+
return [];
|
|
7348
|
+
}
|
|
7257
7349
|
return root.querySelectorAll(`label[for="${id}"]`);
|
|
7258
7350
|
}
|
|
7259
7351
|
function findLabelByParent(el) {
|
|
@@ -7292,7 +7384,9 @@ class LongTitle extends Rule {
|
|
|
7292
7384
|
setup() {
|
|
7293
7385
|
this.on("tag:end", (event) => {
|
|
7294
7386
|
const node = event.previous;
|
|
7295
|
-
if (node.tagName !== "title")
|
|
7387
|
+
if (node.tagName !== "title") {
|
|
7388
|
+
return;
|
|
7389
|
+
}
|
|
7296
7390
|
const text = node.textContent;
|
|
7297
7391
|
if (text.length > this.maxlength) {
|
|
7298
7392
|
this.report(node, `title text cannot be longer than ${String(this.maxlength)} characters`);
|
|
@@ -7494,9 +7588,22 @@ class MultipleLabeledControls extends Rule {
|
|
|
7494
7588
|
const defaults$g = {
|
|
7495
7589
|
pattern: "camelcase"
|
|
7496
7590
|
};
|
|
7591
|
+
function exclude(set, ...values) {
|
|
7592
|
+
const result = new Set(set);
|
|
7593
|
+
for (const value of values) {
|
|
7594
|
+
result.delete(value);
|
|
7595
|
+
}
|
|
7596
|
+
return result;
|
|
7597
|
+
}
|
|
7497
7598
|
class NamePattern extends BasePatternRule {
|
|
7498
7599
|
constructor(options) {
|
|
7499
|
-
|
|
7600
|
+
const allowedPatterns = exclude(patternNames, "tailwind");
|
|
7601
|
+
super({
|
|
7602
|
+
ruleId: "name-pattern",
|
|
7603
|
+
attr: "name",
|
|
7604
|
+
options: { ...defaults$g, ...options },
|
|
7605
|
+
allowedPatterns
|
|
7606
|
+
});
|
|
7500
7607
|
}
|
|
7501
7608
|
static schema() {
|
|
7502
7609
|
return BasePatternRule.schema();
|
|
@@ -7752,7 +7859,7 @@ class NoDupClass extends Rule {
|
|
|
7752
7859
|
}
|
|
7753
7860
|
}
|
|
7754
7861
|
|
|
7755
|
-
const CACHE_KEY = Symbol("no-dup-id");
|
|
7862
|
+
const CACHE_KEY = /* @__PURE__ */ Symbol("no-dup-id");
|
|
7756
7863
|
class NoDupID extends Rule {
|
|
7757
7864
|
documentation() {
|
|
7758
7865
|
return {
|
|
@@ -7857,7 +7964,7 @@ Omitted end tags can be ambigious for humans to read and many editors have troub
|
|
|
7857
7964
|
return;
|
|
7858
7965
|
}
|
|
7859
7966
|
const parent = closed.parent;
|
|
7860
|
-
const closedByParent = parent
|
|
7967
|
+
const closedByParent = parent?.tagName === by.tagName;
|
|
7861
7968
|
const closedByDocument = closedByParent && parent.isRootElement();
|
|
7862
7969
|
const sameTag = closed.tagName === by.tagName;
|
|
7863
7970
|
if (closedByDocument) {
|
|
@@ -8219,7 +8326,7 @@ class NoRedundantAriaLabel extends Rule {
|
|
|
8219
8326
|
continue;
|
|
8220
8327
|
}
|
|
8221
8328
|
const label = document.querySelector(`label[for="${id}"]`);
|
|
8222
|
-
if (!ariaLabel ||
|
|
8329
|
+
if (!ariaLabel || label?.textContent.trim() !== ariaLabel.value) {
|
|
8223
8330
|
continue;
|
|
8224
8331
|
}
|
|
8225
8332
|
const message = "aria-label is redundant when label containing same text exists";
|
|
@@ -10038,6 +10145,46 @@ class ValidAutocomplete extends Rule {
|
|
|
10038
10145
|
}
|
|
10039
10146
|
}
|
|
10040
10147
|
|
|
10148
|
+
function isLabelable(target) {
|
|
10149
|
+
const { meta } = target;
|
|
10150
|
+
if (!meta) {
|
|
10151
|
+
return true;
|
|
10152
|
+
}
|
|
10153
|
+
return Boolean(meta.labelable);
|
|
10154
|
+
}
|
|
10155
|
+
class ValidFor extends Rule {
|
|
10156
|
+
documentation() {
|
|
10157
|
+
return {
|
|
10158
|
+
description: `The \`<label>\` \`for\` attribute must reference a labelable form control.`,
|
|
10159
|
+
url: "https://html-validate.org/rules/valid-for.html"
|
|
10160
|
+
};
|
|
10161
|
+
}
|
|
10162
|
+
setup() {
|
|
10163
|
+
this.on("dom:ready", (event) => {
|
|
10164
|
+
const { document } = event;
|
|
10165
|
+
for (const node of document.querySelectorAll("label[for]")) {
|
|
10166
|
+
const attr = node.getAttribute("for");
|
|
10167
|
+
if (!isStaticAttribute(attr) || !attr.value) {
|
|
10168
|
+
continue;
|
|
10169
|
+
}
|
|
10170
|
+
const selector = generateIdSelector(attr.value);
|
|
10171
|
+
const target = document.querySelector(selector);
|
|
10172
|
+
if (!target) {
|
|
10173
|
+
continue;
|
|
10174
|
+
}
|
|
10175
|
+
if (isLabelable(target)) {
|
|
10176
|
+
continue;
|
|
10177
|
+
}
|
|
10178
|
+
this.report({
|
|
10179
|
+
node,
|
|
10180
|
+
message: '<label> "for" attribute must reference a labelable form control',
|
|
10181
|
+
location: attr.valueLocation
|
|
10182
|
+
});
|
|
10183
|
+
}
|
|
10184
|
+
});
|
|
10185
|
+
}
|
|
10186
|
+
}
|
|
10187
|
+
|
|
10041
10188
|
const defaults$4 = {
|
|
10042
10189
|
relaxed: false
|
|
10043
10190
|
};
|
|
@@ -10336,7 +10483,9 @@ class H36 extends Rule {
|
|
|
10336
10483
|
setup() {
|
|
10337
10484
|
this.on("tag:end", (event) => {
|
|
10338
10485
|
const node = event.previous;
|
|
10339
|
-
if (node.tagName !== "input")
|
|
10486
|
+
if (node.tagName !== "input") {
|
|
10487
|
+
return;
|
|
10488
|
+
}
|
|
10340
10489
|
if (node.getAttributeValue("type") !== "image") {
|
|
10341
10490
|
return;
|
|
10342
10491
|
}
|
|
@@ -10662,6 +10811,7 @@ const bundledRules = {
|
|
|
10662
10811
|
"unique-landmark": UniqueLandmark,
|
|
10663
10812
|
"unrecognized-char-ref": UnknownCharReference,
|
|
10664
10813
|
"valid-autocomplete": ValidAutocomplete,
|
|
10814
|
+
"valid-for": ValidFor,
|
|
10665
10815
|
"valid-id": ValidID,
|
|
10666
10816
|
"void-content": VoidContent,
|
|
10667
10817
|
"void-style": VoidStyle,
|
|
@@ -11015,6 +11165,7 @@ const config$1 = {
|
|
|
11015
11165
|
"unique-landmark": "error",
|
|
11016
11166
|
"unrecognized-char-ref": "error",
|
|
11017
11167
|
"valid-autocomplete": "error",
|
|
11168
|
+
"valid-for": "error",
|
|
11018
11169
|
"valid-id": ["error", { relaxed: false }],
|
|
11019
11170
|
void: "off",
|
|
11020
11171
|
"void-content": "error",
|
|
@@ -11062,6 +11213,7 @@ const config = {
|
|
|
11062
11213
|
"script-element": "error",
|
|
11063
11214
|
"unrecognized-char-ref": "error",
|
|
11064
11215
|
"valid-autocomplete": "error",
|
|
11216
|
+
"valid-for": "error",
|
|
11065
11217
|
"valid-id": ["error", { relaxed: true }],
|
|
11066
11218
|
"void-content": "error"
|
|
11067
11219
|
}
|
|
@@ -11668,7 +11820,9 @@ class Config {
|
|
|
11668
11820
|
}
|
|
11669
11821
|
for (const plugin of plugins) {
|
|
11670
11822
|
for (const [name, config] of Object.entries(plugin.configs ?? {})) {
|
|
11671
|
-
if (!config)
|
|
11823
|
+
if (!config) {
|
|
11824
|
+
continue;
|
|
11825
|
+
}
|
|
11672
11826
|
Config.validate(config, name);
|
|
11673
11827
|
configs.set(`${plugin.name}:${name}`, config);
|
|
11674
11828
|
if (plugin.name !== plugin.originalName) {
|
|
@@ -11969,7 +12123,7 @@ class EventHandler {
|
|
|
11969
12123
|
}
|
|
11970
12124
|
|
|
11971
12125
|
const name = "html-validate";
|
|
11972
|
-
const version = "10.
|
|
12126
|
+
const version = "10.6.0";
|
|
11973
12127
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
11974
12128
|
|
|
11975
12129
|
function freeze(src) {
|
|
@@ -12020,6 +12174,7 @@ class Reporter {
|
|
|
12020
12174
|
warningCount: sumWarnings(results)
|
|
12021
12175
|
};
|
|
12022
12176
|
}
|
|
12177
|
+
/* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
|
|
12023
12178
|
add(rule, message, severity, node, location, context) {
|
|
12024
12179
|
if (!(location.filename in this.result)) {
|
|
12025
12180
|
this.result[location.filename] = [];
|
|
@@ -12139,7 +12294,7 @@ class ParserError extends Error {
|
|
|
12139
12294
|
}
|
|
12140
12295
|
|
|
12141
12296
|
function isAttrValueToken(token) {
|
|
12142
|
-
return
|
|
12297
|
+
return token?.type === TokenType.ATTR_VALUE;
|
|
12143
12298
|
}
|
|
12144
12299
|
function svgShouldRetainTag(foreignTagName, tagName) {
|
|
12145
12300
|
return foreignTagName === "svg" && ["title", "desc"].includes(tagName);
|
|
@@ -12485,7 +12640,7 @@ class Parser {
|
|
|
12485
12640
|
* ^^^ ^^^ ^^^ (null) (null)
|
|
12486
12641
|
*/
|
|
12487
12642
|
getAttributeValueLocation(token) {
|
|
12488
|
-
if (
|
|
12643
|
+
if (token?.type !== TokenType.ATTR_VALUE || token.data[2] === "") {
|
|
12489
12644
|
return null;
|
|
12490
12645
|
}
|
|
12491
12646
|
const quote = token.data[3];
|
|
@@ -12501,7 +12656,7 @@ class Parser {
|
|
|
12501
12656
|
*/
|
|
12502
12657
|
getAttributeLocation(key, value) {
|
|
12503
12658
|
const begin = key.location;
|
|
12504
|
-
const end = value
|
|
12659
|
+
const end = value?.type === TokenType.ATTR_VALUE ? value.location : void 0;
|
|
12505
12660
|
return {
|
|
12506
12661
|
filename: begin.filename,
|
|
12507
12662
|
line: begin.line,
|
|
@@ -12523,7 +12678,11 @@ class Parser {
|
|
|
12523
12678
|
throw new Error(`Failed to parse directive "${text}"`);
|
|
12524
12679
|
}
|
|
12525
12680
|
if (!isValidDirective(action)) {
|
|
12526
|
-
|
|
12681
|
+
this.trigger("parse:error", {
|
|
12682
|
+
location: token.location,
|
|
12683
|
+
message: `Unknown directive "${action}"`
|
|
12684
|
+
});
|
|
12685
|
+
return;
|
|
12527
12686
|
}
|
|
12528
12687
|
const [, data, separator2, comment] = match;
|
|
12529
12688
|
const prefix = "html-validate-";
|
|
@@ -12616,7 +12775,9 @@ class Parser {
|
|
|
12616
12775
|
while (!it.done) {
|
|
12617
12776
|
const token = it.value;
|
|
12618
12777
|
yield token;
|
|
12619
|
-
if (token.type === search)
|
|
12778
|
+
if (token.type === search) {
|
|
12779
|
+
return;
|
|
12780
|
+
}
|
|
12620
12781
|
it = this.next(tokenStream);
|
|
12621
12782
|
}
|
|
12622
12783
|
throw new ParserError(
|
|
@@ -12779,6 +12940,9 @@ class Engine {
|
|
|
12779
12940
|
parser.on("directive", (_2, event) => {
|
|
12780
12941
|
this.processDirective(event, parser, directiveContext);
|
|
12781
12942
|
});
|
|
12943
|
+
parser.on("parse:error", (_2, event) => {
|
|
12944
|
+
this.reportError("parser-error", event.message, event.location);
|
|
12945
|
+
});
|
|
12782
12946
|
try {
|
|
12783
12947
|
parser.parseHtml(source);
|
|
12784
12948
|
} catch (e) {
|
|
@@ -12976,7 +13140,9 @@ class Engine {
|
|
|
12976
13140
|
const availableRules = {};
|
|
12977
13141
|
for (const plugin of config.getPlugins()) {
|
|
12978
13142
|
for (const [name, rule] of Object.entries(plugin.rules ?? {})) {
|
|
12979
|
-
if (!rule)
|
|
13143
|
+
if (!rule) {
|
|
13144
|
+
continue;
|
|
13145
|
+
}
|
|
12980
13146
|
availableRules[name] = rule;
|
|
12981
13147
|
}
|
|
12982
13148
|
}
|
|
@@ -13009,6 +13175,7 @@ class Engine {
|
|
|
13009
13175
|
/**
|
|
13010
13176
|
* Load and setup a rule using current config.
|
|
13011
13177
|
*/
|
|
13178
|
+
/* eslint-disable-next-line @typescript-eslint/max-params -- technical debt */
|
|
13012
13179
|
loadRule(ruleId, config, severity, options, parser, report) {
|
|
13013
13180
|
const meta = config.getMetaTable();
|
|
13014
13181
|
const rule = this.instantiateRule(ruleId, options);
|