html-validate 7.1.2 → 7.3.1
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 +126 -7
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core.d.ts +11 -2
- package/dist/cjs/core.js +314 -110
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/html-validate.js +49 -135
- package/dist/cjs/html-validate.js.map +1 -1
- package/dist/cjs/jest-lib.js +1 -0
- package/dist/cjs/jest-lib.js.map +1 -1
- package/dist/es/cli.js +122 -8
- package/dist/es/cli.js.map +1 -1
- package/dist/es/core.d.ts +11 -2
- package/dist/es/core.js +314 -110
- package/dist/es/core.js.map +1 -1
- package/dist/es/html-validate.js +34 -120
- package/dist/es/html-validate.js.map +1 -1
- package/dist/es/jest-lib.js +1 -0
- package/dist/es/jest-lib.js.map +1 -1
- package/elements/html5.js +16 -1
- package/package.json +21 -45
package/dist/es/core.js
CHANGED
|
@@ -1734,6 +1734,77 @@ function stripslashes(value) {
|
|
|
1734
1734
|
function escapeSelectorComponent(text) {
|
|
1735
1735
|
return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
|
|
1736
1736
|
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Returns true if the character is a delimiter for different kinds of selectors:
|
|
1739
|
+
*
|
|
1740
|
+
* - `.` - begins a class selector
|
|
1741
|
+
* - `#` - begins an id selector
|
|
1742
|
+
* - `[` - begins an attribute selector
|
|
1743
|
+
* - `:` - begins a pseudo class or element selector
|
|
1744
|
+
*/
|
|
1745
|
+
function isDelimiter(ch) {
|
|
1746
|
+
return /[.#[:]/.test(ch);
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Returns true if the character is a quotation mark.
|
|
1750
|
+
*/
|
|
1751
|
+
function isQuotationMark(ch) {
|
|
1752
|
+
return /['"]/.test(ch);
|
|
1753
|
+
}
|
|
1754
|
+
function isPseudoElement(ch, buffer) {
|
|
1755
|
+
return ch === ":" && buffer === ":";
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* @internal
|
|
1759
|
+
*/
|
|
1760
|
+
function* splitPattern(pattern) {
|
|
1761
|
+
if (pattern === "") {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const end = pattern.length;
|
|
1765
|
+
let begin = 0;
|
|
1766
|
+
let cur = 1;
|
|
1767
|
+
let quoted = false;
|
|
1768
|
+
while (cur < end) {
|
|
1769
|
+
const ch = pattern[cur];
|
|
1770
|
+
const buffer = pattern.slice(begin, cur);
|
|
1771
|
+
/* escaped character, ignore whatever is next */
|
|
1772
|
+
if (ch === "\\") {
|
|
1773
|
+
cur += 2;
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
/* if inside quoted string we only look for the end quotation mark */
|
|
1777
|
+
if (quoted) {
|
|
1778
|
+
if (ch === quoted) {
|
|
1779
|
+
quoted = false;
|
|
1780
|
+
}
|
|
1781
|
+
cur += 1;
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
/* if the character is a quotation mark we store the character and the above
|
|
1785
|
+
* condition will look for a similar end quotation mark */
|
|
1786
|
+
if (isQuotationMark(ch)) {
|
|
1787
|
+
quoted = ch;
|
|
1788
|
+
cur += 1;
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
/* special case when using :: pseudo element selector */
|
|
1792
|
+
if (isPseudoElement(ch, buffer)) {
|
|
1793
|
+
cur += 1;
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
/* if the character is a delimiter we yield the string and reset the
|
|
1797
|
+
* position */
|
|
1798
|
+
if (isDelimiter(ch)) {
|
|
1799
|
+
begin = cur;
|
|
1800
|
+
yield buffer;
|
|
1801
|
+
}
|
|
1802
|
+
cur += 1;
|
|
1803
|
+
}
|
|
1804
|
+
/* yield the rest of the string */
|
|
1805
|
+
const tail = pattern.slice(begin, cur);
|
|
1806
|
+
yield tail;
|
|
1807
|
+
}
|
|
1737
1808
|
class Matcher {
|
|
1738
1809
|
}
|
|
1739
1810
|
class ClassMatcher extends Matcher {
|
|
@@ -1799,8 +1870,7 @@ class Pattern {
|
|
|
1799
1870
|
this.selector = pattern;
|
|
1800
1871
|
this.combinator = parseCombinator(match.shift(), pattern);
|
|
1801
1872
|
this.tagName = match.shift() || "*";
|
|
1802
|
-
|
|
1803
|
-
this.pattern = p.map((cur) => this.createMatcher(cur));
|
|
1873
|
+
this.pattern = Array.from(splitPattern(match[0]), (it) => this.createMatcher(it));
|
|
1804
1874
|
}
|
|
1805
1875
|
match(node, context) {
|
|
1806
1876
|
return node.is(this.tagName) && this.pattern.every((cur) => cur.match(node, context));
|
|
@@ -3086,7 +3156,7 @@ var TRANSFORMER_API;
|
|
|
3086
3156
|
/** @public */
|
|
3087
3157
|
const name = "html-validate";
|
|
3088
3158
|
/** @public */
|
|
3089
|
-
const version = "7.1
|
|
3159
|
+
const version = "7.3.1";
|
|
3090
3160
|
/** @public */
|
|
3091
3161
|
const homepage = "https://html-validate.org";
|
|
3092
3162
|
/** @public */
|
|
@@ -3187,6 +3257,18 @@ function getSchemaValidator(ruleId, properties) {
|
|
|
3187
3257
|
};
|
|
3188
3258
|
return ajv$1.compile(schema);
|
|
3189
3259
|
}
|
|
3260
|
+
function isErrorDescriptor(value) {
|
|
3261
|
+
return Boolean(value[0] && value[0].message);
|
|
3262
|
+
}
|
|
3263
|
+
function unpackErrorDescriptor(value) {
|
|
3264
|
+
if (isErrorDescriptor(value)) {
|
|
3265
|
+
return value[0];
|
|
3266
|
+
}
|
|
3267
|
+
else {
|
|
3268
|
+
const [node, message, location, context] = value;
|
|
3269
|
+
return { node, message, location, context };
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3190
3272
|
/**
|
|
3191
3273
|
* @public
|
|
3192
3274
|
*/
|
|
@@ -3287,13 +3369,8 @@ class Rule {
|
|
|
3287
3369
|
static schema() {
|
|
3288
3370
|
return null;
|
|
3289
3371
|
}
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
*
|
|
3293
|
-
* Rule must be enabled both globally and on the specific node for this to
|
|
3294
|
-
* have any effect.
|
|
3295
|
-
*/
|
|
3296
|
-
report(node, message, location, context) {
|
|
3372
|
+
report(...args) {
|
|
3373
|
+
const { node, message, location, context } = unpackErrorDescriptor(args);
|
|
3297
3374
|
if (this.isEnabled() && (!node || node.ruleEnabled(this.name))) {
|
|
3298
3375
|
const where = this.findLocation({ node, location, event: this.event });
|
|
3299
3376
|
const interpolated = interpolate(message, context !== null && context !== void 0 ? context : {});
|
|
@@ -3806,9 +3883,14 @@ class AttrCase extends Rule {
|
|
|
3806
3883
|
return;
|
|
3807
3884
|
}
|
|
3808
3885
|
const letters = event.key.replace(/[^a-z]+/gi, "");
|
|
3809
|
-
if (
|
|
3810
|
-
|
|
3886
|
+
if (this.style.match(letters)) {
|
|
3887
|
+
return;
|
|
3811
3888
|
}
|
|
3889
|
+
this.report({
|
|
3890
|
+
node: event.target,
|
|
3891
|
+
message: `Attribute "${event.key}" should be ${this.style.name}`,
|
|
3892
|
+
location: event.keyLocation,
|
|
3893
|
+
});
|
|
3812
3894
|
});
|
|
3813
3895
|
}
|
|
3814
3896
|
isIgnored(node) {
|
|
@@ -5108,6 +5190,11 @@ class ElementName extends Rule {
|
|
|
5108
5190
|
}
|
|
5109
5191
|
}
|
|
5110
5192
|
|
|
5193
|
+
var ErrorKind;
|
|
5194
|
+
(function (ErrorKind) {
|
|
5195
|
+
ErrorKind["CONTENT"] = "content";
|
|
5196
|
+
ErrorKind["DESCENDANT"] = "descendant";
|
|
5197
|
+
})(ErrorKind || (ErrorKind = {}));
|
|
5111
5198
|
function getTransparentChildren(node, transparent) {
|
|
5112
5199
|
if (typeof transparent === "boolean") {
|
|
5113
5200
|
return node.childElements;
|
|
@@ -5121,10 +5208,28 @@ function getTransparentChildren(node, transparent) {
|
|
|
5121
5208
|
});
|
|
5122
5209
|
}
|
|
5123
5210
|
}
|
|
5211
|
+
function getRuleDescription$1(context) {
|
|
5212
|
+
if (!context) {
|
|
5213
|
+
return [
|
|
5214
|
+
"Some elements has restrictions on what content is allowed.",
|
|
5215
|
+
"This can include both direct children or descendant elements.",
|
|
5216
|
+
];
|
|
5217
|
+
}
|
|
5218
|
+
switch (context.kind) {
|
|
5219
|
+
case ErrorKind.CONTENT:
|
|
5220
|
+
return [
|
|
5221
|
+
`The \`${context.child}\` element is not permitted as content under the parent \`${context.parent}\` element.`,
|
|
5222
|
+
];
|
|
5223
|
+
case ErrorKind.DESCENDANT:
|
|
5224
|
+
return [
|
|
5225
|
+
`The \`${context.child}\` element is not permitted as a descendant of the \`${context.ancestor}\` element.`,
|
|
5226
|
+
];
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5124
5229
|
class ElementPermittedContent extends Rule {
|
|
5125
|
-
documentation() {
|
|
5230
|
+
documentation(context) {
|
|
5126
5231
|
return {
|
|
5127
|
-
description:
|
|
5232
|
+
description: getRuleDescription$1(context).join("\n"),
|
|
5128
5233
|
url: ruleDocumentationUrl("@/rules/element-permitted-content.ts"),
|
|
5129
5234
|
};
|
|
5130
5235
|
}
|
|
@@ -5133,8 +5238,9 @@ class ElementPermittedContent extends Rule {
|
|
|
5133
5238
|
const doc = event.document;
|
|
5134
5239
|
doc.visitDepthFirst((node) => {
|
|
5135
5240
|
const parent = node.parent;
|
|
5136
|
-
/*
|
|
5137
|
-
|
|
5241
|
+
/* istanbul ignore next: satisfy typescript but will visitDepthFirst()
|
|
5242
|
+
* will not yield nodes without a parent */
|
|
5243
|
+
if (!parent) {
|
|
5138
5244
|
return;
|
|
5139
5245
|
}
|
|
5140
5246
|
/* Run each validation step, stop as soon as any errors are
|
|
@@ -5144,7 +5250,6 @@ class ElementPermittedContent extends Rule {
|
|
|
5144
5250
|
[
|
|
5145
5251
|
() => this.validatePermittedContent(node, parent),
|
|
5146
5252
|
() => this.validatePermittedDescendant(node, parent),
|
|
5147
|
-
() => this.validatePermittedAncestors(node),
|
|
5148
5253
|
].some((fn) => fn());
|
|
5149
5254
|
});
|
|
5150
5255
|
});
|
|
@@ -5161,7 +5266,14 @@ class ElementPermittedContent extends Rule {
|
|
|
5161
5266
|
}
|
|
5162
5267
|
validatePermittedContentImpl(cur, parent, rules) {
|
|
5163
5268
|
if (!Validator.validatePermitted(cur, rules)) {
|
|
5164
|
-
|
|
5269
|
+
const child = `<${cur.tagName}>`;
|
|
5270
|
+
const message = `${child} element is not permitted as content under ${parent.annotatedName}`;
|
|
5271
|
+
const context = {
|
|
5272
|
+
kind: ErrorKind.CONTENT,
|
|
5273
|
+
parent: parent.annotatedName,
|
|
5274
|
+
child,
|
|
5275
|
+
};
|
|
5276
|
+
this.report(cur, message, null, context);
|
|
5165
5277
|
return true;
|
|
5166
5278
|
}
|
|
5167
5279
|
/* for transparent elements all/listed children must be validated against
|
|
@@ -5192,21 +5304,15 @@ class ElementPermittedContent extends Rule {
|
|
|
5192
5304
|
if (Validator.validatePermitted(node, rules)) {
|
|
5193
5305
|
continue;
|
|
5194
5306
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
const rules = node.meta.requiredAncestors;
|
|
5205
|
-
if (!rules) {
|
|
5206
|
-
return false;
|
|
5207
|
-
}
|
|
5208
|
-
if (!Validator.validateAncestors(node, rules)) {
|
|
5209
|
-
this.report(node, `Element <${node.tagName}> requires an "${rules[0]}" ancestor`);
|
|
5307
|
+
const child = `<${node.tagName}>`;
|
|
5308
|
+
const ancestor = cur.annotatedName;
|
|
5309
|
+
const message = `${child} element is not permitted as a descendant of ${ancestor}`;
|
|
5310
|
+
const context = {
|
|
5311
|
+
kind: ErrorKind.DESCENDANT,
|
|
5312
|
+
ancestor,
|
|
5313
|
+
child,
|
|
5314
|
+
};
|
|
5315
|
+
this.report(node, message, null, context);
|
|
5210
5316
|
return true;
|
|
5211
5317
|
}
|
|
5212
5318
|
return false;
|
|
@@ -5273,6 +5379,152 @@ class ElementPermittedOrder extends Rule {
|
|
|
5273
5379
|
}
|
|
5274
5380
|
}
|
|
5275
5381
|
|
|
5382
|
+
const CACHE_KEY = Symbol(classifyNodeText.name);
|
|
5383
|
+
var TextClassification;
|
|
5384
|
+
(function (TextClassification) {
|
|
5385
|
+
TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
5386
|
+
TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
5387
|
+
TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
5388
|
+
})(TextClassification || (TextClassification = {}));
|
|
5389
|
+
/**
|
|
5390
|
+
* Checks text content of an element.
|
|
5391
|
+
*
|
|
5392
|
+
* Any text is considered including text from descendant elements. Whitespace is
|
|
5393
|
+
* ignored.
|
|
5394
|
+
*
|
|
5395
|
+
* If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
|
|
5396
|
+
*/
|
|
5397
|
+
function classifyNodeText(node) {
|
|
5398
|
+
if (node.cacheExists(CACHE_KEY)) {
|
|
5399
|
+
return node.cacheGet(CACHE_KEY);
|
|
5400
|
+
}
|
|
5401
|
+
const text = findTextNodes(node);
|
|
5402
|
+
/* if any text is dynamic classify as dynamic */
|
|
5403
|
+
if (text.some((cur) => cur.isDynamic)) {
|
|
5404
|
+
return node.cacheSet(CACHE_KEY, TextClassification.DYNAMIC_TEXT);
|
|
5405
|
+
}
|
|
5406
|
+
/* if any text has non-whitespace character classify as static */
|
|
5407
|
+
if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
|
|
5408
|
+
return node.cacheSet(CACHE_KEY, TextClassification.STATIC_TEXT);
|
|
5409
|
+
}
|
|
5410
|
+
/* default to empty */
|
|
5411
|
+
return node.cacheSet(CACHE_KEY, TextClassification.EMPTY_TEXT);
|
|
5412
|
+
}
|
|
5413
|
+
function findTextNodes(node) {
|
|
5414
|
+
let text = [];
|
|
5415
|
+
for (const child of node.childNodes) {
|
|
5416
|
+
switch (child.nodeType) {
|
|
5417
|
+
case NodeType.TEXT_NODE:
|
|
5418
|
+
text.push(child);
|
|
5419
|
+
break;
|
|
5420
|
+
case NodeType.ELEMENT_NODE:
|
|
5421
|
+
text = text.concat(findTextNodes(child));
|
|
5422
|
+
break;
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
return text;
|
|
5426
|
+
}
|
|
5427
|
+
|
|
5428
|
+
function hasAltText(image) {
|
|
5429
|
+
const alt = image.getAttribute("alt");
|
|
5430
|
+
/* missing or boolean */
|
|
5431
|
+
if (alt === null || alt.value === null) {
|
|
5432
|
+
return false;
|
|
5433
|
+
}
|
|
5434
|
+
return alt.isDynamic || alt.value.toString() !== "";
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
function hasAriaLabel(node) {
|
|
5438
|
+
const label = node.getAttribute("aria-label");
|
|
5439
|
+
/* missing or boolean */
|
|
5440
|
+
if (label === null || label.value === null) {
|
|
5441
|
+
return false;
|
|
5442
|
+
}
|
|
5443
|
+
return label.isDynamic || label.value.toString() !== "";
|
|
5444
|
+
}
|
|
5445
|
+
|
|
5446
|
+
/**
|
|
5447
|
+
* Joins a list of words into natural language.
|
|
5448
|
+
*
|
|
5449
|
+
* - `["foo"]` becomes `"foo"`
|
|
5450
|
+
* - `["foo", "bar"]` becomes `"foo or bar"`
|
|
5451
|
+
* - `["foo", "bar", "baz"]` becomes `"foo, bar or baz"`
|
|
5452
|
+
* - and so on...
|
|
5453
|
+
*
|
|
5454
|
+
* @internal
|
|
5455
|
+
* @param values - List of words to join
|
|
5456
|
+
* @param conjunction - Conjunction for the last element.
|
|
5457
|
+
* @returns String with the words naturally joined with a conjunction.
|
|
5458
|
+
*/
|
|
5459
|
+
function naturalJoin(values, conjunction = "or") {
|
|
5460
|
+
switch (values.length) {
|
|
5461
|
+
case 0:
|
|
5462
|
+
return "";
|
|
5463
|
+
case 1:
|
|
5464
|
+
return values[0];
|
|
5465
|
+
case 2:
|
|
5466
|
+
return `${values[0]} ${conjunction} ${values[1]}`;
|
|
5467
|
+
default:
|
|
5468
|
+
return `${values.slice(0, -1).join(", ")} ${conjunction} ${values.slice(-1)[0]}`;
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
function isTagnameOnly(value) {
|
|
5473
|
+
return Boolean(value.match(/^[a-zA-Z0-9-]+$/));
|
|
5474
|
+
}
|
|
5475
|
+
function getRuleDescription(context) {
|
|
5476
|
+
if (!context) {
|
|
5477
|
+
return [
|
|
5478
|
+
"Some elements has restrictions on what content is allowed.",
|
|
5479
|
+
"This can include both direct children or descendant elements.",
|
|
5480
|
+
];
|
|
5481
|
+
}
|
|
5482
|
+
const escaped = context.ancestor.map((it) => `\`${it}\``);
|
|
5483
|
+
return [`The \`${context.child}\` element requires a ${naturalJoin(escaped)} ancestor.`];
|
|
5484
|
+
}
|
|
5485
|
+
class ElementRequiredAncestor extends Rule {
|
|
5486
|
+
documentation(context) {
|
|
5487
|
+
return {
|
|
5488
|
+
description: getRuleDescription(context).join("\n"),
|
|
5489
|
+
url: ruleDocumentationUrl("@/rules/element-required-ancestor.ts"),
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
setup() {
|
|
5493
|
+
this.on("dom:ready", (event) => {
|
|
5494
|
+
const doc = event.document;
|
|
5495
|
+
doc.visitDepthFirst((node) => {
|
|
5496
|
+
const parent = node.parent;
|
|
5497
|
+
/* istanbul ignore next: satisfy typescript but will visitDepthFirst()
|
|
5498
|
+
* will not yield nodes without a parent */
|
|
5499
|
+
if (!parent) {
|
|
5500
|
+
return;
|
|
5501
|
+
}
|
|
5502
|
+
this.validateRequiredAncestors(node);
|
|
5503
|
+
});
|
|
5504
|
+
});
|
|
5505
|
+
}
|
|
5506
|
+
validateRequiredAncestors(node) {
|
|
5507
|
+
if (!node.meta) {
|
|
5508
|
+
return;
|
|
5509
|
+
}
|
|
5510
|
+
const rules = node.meta.requiredAncestors;
|
|
5511
|
+
if (!rules) {
|
|
5512
|
+
return;
|
|
5513
|
+
}
|
|
5514
|
+
if (Validator.validateAncestors(node, rules)) {
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5517
|
+
const ancestor = rules.map((it) => (isTagnameOnly(it) ? `<${it}>` : `"${it}"`));
|
|
5518
|
+
const child = `<${node.tagName}>`;
|
|
5519
|
+
const message = `<${node.tagName}> element requires a ${naturalJoin(ancestor)} ancestor`;
|
|
5520
|
+
const context = {
|
|
5521
|
+
ancestor,
|
|
5522
|
+
child,
|
|
5523
|
+
};
|
|
5524
|
+
this.report(node, message, null, context);
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
|
|
5276
5528
|
class ElementRequiredAttributes extends Rule {
|
|
5277
5529
|
documentation(context) {
|
|
5278
5530
|
const docs = {
|
|
@@ -5350,52 +5602,6 @@ class ElementRequiredContent extends Rule {
|
|
|
5350
5602
|
}
|
|
5351
5603
|
}
|
|
5352
5604
|
|
|
5353
|
-
const CACHE_KEY = Symbol(classifyNodeText.name);
|
|
5354
|
-
var TextClassification;
|
|
5355
|
-
(function (TextClassification) {
|
|
5356
|
-
TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
5357
|
-
TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
5358
|
-
TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
5359
|
-
})(TextClassification || (TextClassification = {}));
|
|
5360
|
-
/**
|
|
5361
|
-
* Checks text content of an element.
|
|
5362
|
-
*
|
|
5363
|
-
* Any text is considered including text from descendant elements. Whitespace is
|
|
5364
|
-
* ignored.
|
|
5365
|
-
*
|
|
5366
|
-
* If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
|
|
5367
|
-
*/
|
|
5368
|
-
function classifyNodeText(node) {
|
|
5369
|
-
if (node.cacheExists(CACHE_KEY)) {
|
|
5370
|
-
return node.cacheGet(CACHE_KEY);
|
|
5371
|
-
}
|
|
5372
|
-
const text = findTextNodes(node);
|
|
5373
|
-
/* if any text is dynamic classify as dynamic */
|
|
5374
|
-
if (text.some((cur) => cur.isDynamic)) {
|
|
5375
|
-
return node.cacheSet(CACHE_KEY, TextClassification.DYNAMIC_TEXT);
|
|
5376
|
-
}
|
|
5377
|
-
/* if any text has non-whitespace character classify as static */
|
|
5378
|
-
if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
|
|
5379
|
-
return node.cacheSet(CACHE_KEY, TextClassification.STATIC_TEXT);
|
|
5380
|
-
}
|
|
5381
|
-
/* default to empty */
|
|
5382
|
-
return node.cacheSet(CACHE_KEY, TextClassification.EMPTY_TEXT);
|
|
5383
|
-
}
|
|
5384
|
-
function findTextNodes(node) {
|
|
5385
|
-
let text = [];
|
|
5386
|
-
for (const child of node.childNodes) {
|
|
5387
|
-
switch (child.nodeType) {
|
|
5388
|
-
case NodeType.TEXT_NODE:
|
|
5389
|
-
text.push(child);
|
|
5390
|
-
break;
|
|
5391
|
-
case NodeType.ELEMENT_NODE:
|
|
5392
|
-
text = text.concat(findTextNodes(child));
|
|
5393
|
-
break;
|
|
5394
|
-
}
|
|
5395
|
-
}
|
|
5396
|
-
return text;
|
|
5397
|
-
}
|
|
5398
|
-
|
|
5399
5605
|
const selector = ["h1", "h2", "h3", "h4", "h5", "h6"].join(",");
|
|
5400
5606
|
class EmptyHeading extends Rule {
|
|
5401
5607
|
documentation() {
|
|
@@ -7576,24 +7782,6 @@ class TelNonBreaking extends Rule {
|
|
|
7576
7782
|
}
|
|
7577
7783
|
}
|
|
7578
7784
|
|
|
7579
|
-
function hasAltText(image) {
|
|
7580
|
-
const alt = image.getAttribute("alt");
|
|
7581
|
-
/* missing or boolean */
|
|
7582
|
-
if (alt === null || alt.value === null) {
|
|
7583
|
-
return false;
|
|
7584
|
-
}
|
|
7585
|
-
return alt.isDynamic || alt.value.toString() !== "";
|
|
7586
|
-
}
|
|
7587
|
-
|
|
7588
|
-
function hasAriaLabel(node) {
|
|
7589
|
-
const label = node.getAttribute("aria-label");
|
|
7590
|
-
/* missing or boolean */
|
|
7591
|
-
if (label === null || label.value === null) {
|
|
7592
|
-
return false;
|
|
7593
|
-
}
|
|
7594
|
-
return label.isDynamic || label.value.toString() !== "";
|
|
7595
|
-
}
|
|
7596
|
-
|
|
7597
7785
|
/**
|
|
7598
7786
|
* Check if attribute is present and non-empty or dynamic.
|
|
7599
7787
|
*/
|
|
@@ -9771,13 +9959,13 @@ class VoidStyle extends Rule {
|
|
|
9771
9959
|
return;
|
|
9772
9960
|
}
|
|
9773
9961
|
if (this.shouldBeOmitted(node)) {
|
|
9774
|
-
this.
|
|
9962
|
+
this.reportError(node, `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`);
|
|
9775
9963
|
}
|
|
9776
9964
|
if (this.shouldBeSelfClosed(node)) {
|
|
9777
|
-
this.
|
|
9965
|
+
this.reportError(node, `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`);
|
|
9778
9966
|
}
|
|
9779
9967
|
}
|
|
9780
|
-
|
|
9968
|
+
reportError(node, message) {
|
|
9781
9969
|
const context = {
|
|
9782
9970
|
style: this.style,
|
|
9783
9971
|
tagName: node.tagName,
|
|
@@ -10089,6 +10277,7 @@ const bundledRules = {
|
|
|
10089
10277
|
"element-permitted-content": ElementPermittedContent,
|
|
10090
10278
|
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
10091
10279
|
"element-permitted-order": ElementPermittedOrder,
|
|
10280
|
+
"element-required-ancestor": ElementRequiredAncestor,
|
|
10092
10281
|
"element-required-attributes": ElementRequiredAttributes,
|
|
10093
10282
|
"element-required-content": ElementRequiredContent,
|
|
10094
10283
|
"empty-heading": EmptyHeading,
|
|
@@ -10196,6 +10385,7 @@ const config$1 = {
|
|
|
10196
10385
|
"element-permitted-content": "error",
|
|
10197
10386
|
"element-permitted-occurrences": "error",
|
|
10198
10387
|
"element-permitted-order": "error",
|
|
10388
|
+
"element-required-ancestor": "error",
|
|
10199
10389
|
"element-required-attributes": "error",
|
|
10200
10390
|
"element-required-content": "error",
|
|
10201
10391
|
"empty-heading": "error",
|
|
@@ -10254,6 +10444,7 @@ const config = {
|
|
|
10254
10444
|
"element-permitted-content": "error",
|
|
10255
10445
|
"element-permitted-occurrences": "error",
|
|
10256
10446
|
"element-permitted-order": "error",
|
|
10447
|
+
"element-required-ancestor": "error",
|
|
10257
10448
|
"element-required-attributes": "error",
|
|
10258
10449
|
"element-required-content": "error",
|
|
10259
10450
|
"multiple-labeled-controls": "error",
|
|
@@ -10333,6 +10524,7 @@ class ResolvedConfig {
|
|
|
10333
10524
|
});
|
|
10334
10525
|
}
|
|
10335
10526
|
catch (err) {
|
|
10527
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10336
10528
|
const message = err instanceof Error ? err.message : String(err);
|
|
10337
10529
|
throw new NestedError(`When transforming "${source.filename}": ${message}`, ensureError(err));
|
|
10338
10530
|
}
|
|
@@ -10345,7 +10537,7 @@ class ResolvedConfig {
|
|
|
10345
10537
|
* Wrapper around [[transformSource]] which reads a file before passing it
|
|
10346
10538
|
* as-is to transformSource.
|
|
10347
10539
|
*
|
|
10348
|
-
* @param
|
|
10540
|
+
* @param filename - Filename to transform (according to configured
|
|
10349
10541
|
* transformations)
|
|
10350
10542
|
* @returns A list of transformed sources ready for validation.
|
|
10351
10543
|
*/
|
|
@@ -10501,7 +10693,9 @@ class Config {
|
|
|
10501
10693
|
var _a;
|
|
10502
10694
|
const valid = validator(configData);
|
|
10503
10695
|
if (!valid) {
|
|
10504
|
-
throw new SchemaValidationError(filename, `Invalid configuration`, configData, configurationSchema,
|
|
10696
|
+
throw new SchemaValidationError(filename, `Invalid configuration`, configData, configurationSchema,
|
|
10697
|
+
/* istanbul ignore next: will be set when a validation error has occurred */
|
|
10698
|
+
(_a = validator.errors) !== null && _a !== void 0 ? _a : []);
|
|
10505
10699
|
}
|
|
10506
10700
|
if (configData.rules) {
|
|
10507
10701
|
const normalizedRules = Config.getRulesObject(configData.rules);
|
|
@@ -10610,6 +10804,7 @@ class Config {
|
|
|
10610
10804
|
metaTable.loadFromObject(legacyRequire(entry));
|
|
10611
10805
|
}
|
|
10612
10806
|
catch (err) {
|
|
10807
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10613
10808
|
const message = err instanceof Error ? err.message : String(err);
|
|
10614
10809
|
throw new ConfigError(`Failed to load elements from "${entry}": ${message}`, ensureError(err));
|
|
10615
10810
|
}
|
|
@@ -10631,6 +10826,7 @@ class Config {
|
|
|
10631
10826
|
*
|
|
10632
10827
|
* @internal primary purpose is unittests
|
|
10633
10828
|
*/
|
|
10829
|
+
/* istanbul ignore next: used for testing only */
|
|
10634
10830
|
get() {
|
|
10635
10831
|
const config = { ...this.config };
|
|
10636
10832
|
if (config.elements) {
|
|
@@ -10653,6 +10849,7 @@ class Config {
|
|
|
10653
10849
|
*/
|
|
10654
10850
|
getRules() {
|
|
10655
10851
|
var _a;
|
|
10852
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10656
10853
|
return Config.getRulesObject((_a = this.config.rules) !== null && _a !== void 0 ? _a : {});
|
|
10657
10854
|
}
|
|
10658
10855
|
static getRulesObject(src) {
|
|
@@ -10687,6 +10884,7 @@ class Config {
|
|
|
10687
10884
|
return plugin;
|
|
10688
10885
|
}
|
|
10689
10886
|
catch (err) {
|
|
10887
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10690
10888
|
const message = err instanceof Error ? err.message : String(err);
|
|
10691
10889
|
throw new ConfigError(`Failed to load plugin "${moduleName}": ${message}`, ensureError(err));
|
|
10692
10890
|
}
|
|
@@ -11602,9 +11800,9 @@ class Reporter {
|
|
|
11602
11800
|
if (!(location.filename in this.result)) {
|
|
11603
11801
|
this.result[location.filename] = [];
|
|
11604
11802
|
}
|
|
11605
|
-
|
|
11803
|
+
const ruleUrl = (_a = rule.documentation(context)) === null || _a === void 0 ? void 0 : _a.url;
|
|
11804
|
+
const entry = {
|
|
11606
11805
|
ruleId: rule.name,
|
|
11607
|
-
ruleUrl: (_a = rule.documentation(context)) === null || _a === void 0 ? void 0 : _a.url,
|
|
11608
11806
|
severity,
|
|
11609
11807
|
message,
|
|
11610
11808
|
offset: location.offset,
|
|
@@ -11614,8 +11812,14 @@ class Reporter {
|
|
|
11614
11812
|
selector() {
|
|
11615
11813
|
return node ? node.generateSelector() : null;
|
|
11616
11814
|
},
|
|
11617
|
-
|
|
11618
|
-
|
|
11815
|
+
};
|
|
11816
|
+
if (ruleUrl) {
|
|
11817
|
+
entry.ruleUrl = ruleUrl;
|
|
11818
|
+
}
|
|
11819
|
+
if (context) {
|
|
11820
|
+
entry.context = context;
|
|
11821
|
+
}
|
|
11822
|
+
this.result[location.filename].push(entry);
|
|
11619
11823
|
}
|
|
11620
11824
|
addManual(filename, message) {
|
|
11621
11825
|
if (!(filename in this.result)) {
|
|
@@ -12029,7 +12233,7 @@ class Engine {
|
|
|
12029
12233
|
offset: location.offset,
|
|
12030
12234
|
line: location.line,
|
|
12031
12235
|
column: location.column,
|
|
12032
|
-
size: location.size
|
|
12236
|
+
size: location.size,
|
|
12033
12237
|
selector: () => null,
|
|
12034
12238
|
});
|
|
12035
12239
|
}
|
|
@@ -12459,12 +12663,12 @@ class FileSystemConfigLoader extends ConfigLoader {
|
|
|
12459
12663
|
* `null` if no configuration files are found.
|
|
12460
12664
|
*/
|
|
12461
12665
|
fromFilename(filename) {
|
|
12462
|
-
var _a;
|
|
12463
12666
|
if (filename === "inline") {
|
|
12464
12667
|
return null;
|
|
12465
12668
|
}
|
|
12466
|
-
|
|
12467
|
-
|
|
12669
|
+
const cache = this.cache.get(filename);
|
|
12670
|
+
if (cache) {
|
|
12671
|
+
return cache;
|
|
12468
12672
|
}
|
|
12469
12673
|
let found = false;
|
|
12470
12674
|
let current = path.resolve(path.dirname(filename));
|