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/cjs/core.js
CHANGED
|
@@ -1765,6 +1765,77 @@ function stripslashes(value) {
|
|
|
1765
1765
|
function escapeSelectorComponent(text) {
|
|
1766
1766
|
return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
|
|
1767
1767
|
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Returns true if the character is a delimiter for different kinds of selectors:
|
|
1770
|
+
*
|
|
1771
|
+
* - `.` - begins a class selector
|
|
1772
|
+
* - `#` - begins an id selector
|
|
1773
|
+
* - `[` - begins an attribute selector
|
|
1774
|
+
* - `:` - begins a pseudo class or element selector
|
|
1775
|
+
*/
|
|
1776
|
+
function isDelimiter(ch) {
|
|
1777
|
+
return /[.#[:]/.test(ch);
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Returns true if the character is a quotation mark.
|
|
1781
|
+
*/
|
|
1782
|
+
function isQuotationMark(ch) {
|
|
1783
|
+
return /['"]/.test(ch);
|
|
1784
|
+
}
|
|
1785
|
+
function isPseudoElement(ch, buffer) {
|
|
1786
|
+
return ch === ":" && buffer === ":";
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* @internal
|
|
1790
|
+
*/
|
|
1791
|
+
function* splitPattern(pattern) {
|
|
1792
|
+
if (pattern === "") {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const end = pattern.length;
|
|
1796
|
+
let begin = 0;
|
|
1797
|
+
let cur = 1;
|
|
1798
|
+
let quoted = false;
|
|
1799
|
+
while (cur < end) {
|
|
1800
|
+
const ch = pattern[cur];
|
|
1801
|
+
const buffer = pattern.slice(begin, cur);
|
|
1802
|
+
/* escaped character, ignore whatever is next */
|
|
1803
|
+
if (ch === "\\") {
|
|
1804
|
+
cur += 2;
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
/* if inside quoted string we only look for the end quotation mark */
|
|
1808
|
+
if (quoted) {
|
|
1809
|
+
if (ch === quoted) {
|
|
1810
|
+
quoted = false;
|
|
1811
|
+
}
|
|
1812
|
+
cur += 1;
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
/* if the character is a quotation mark we store the character and the above
|
|
1816
|
+
* condition will look for a similar end quotation mark */
|
|
1817
|
+
if (isQuotationMark(ch)) {
|
|
1818
|
+
quoted = ch;
|
|
1819
|
+
cur += 1;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
/* special case when using :: pseudo element selector */
|
|
1823
|
+
if (isPseudoElement(ch, buffer)) {
|
|
1824
|
+
cur += 1;
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
/* if the character is a delimiter we yield the string and reset the
|
|
1828
|
+
* position */
|
|
1829
|
+
if (isDelimiter(ch)) {
|
|
1830
|
+
begin = cur;
|
|
1831
|
+
yield buffer;
|
|
1832
|
+
}
|
|
1833
|
+
cur += 1;
|
|
1834
|
+
}
|
|
1835
|
+
/* yield the rest of the string */
|
|
1836
|
+
const tail = pattern.slice(begin, cur);
|
|
1837
|
+
yield tail;
|
|
1838
|
+
}
|
|
1768
1839
|
class Matcher {
|
|
1769
1840
|
}
|
|
1770
1841
|
class ClassMatcher extends Matcher {
|
|
@@ -1830,8 +1901,7 @@ class Pattern {
|
|
|
1830
1901
|
this.selector = pattern;
|
|
1831
1902
|
this.combinator = parseCombinator(match.shift(), pattern);
|
|
1832
1903
|
this.tagName = match.shift() || "*";
|
|
1833
|
-
|
|
1834
|
-
this.pattern = p.map((cur) => this.createMatcher(cur));
|
|
1904
|
+
this.pattern = Array.from(splitPattern(match[0]), (it) => this.createMatcher(it));
|
|
1835
1905
|
}
|
|
1836
1906
|
match(node, context) {
|
|
1837
1907
|
return node.is(this.tagName) && this.pattern.every((cur) => cur.match(node, context));
|
|
@@ -3117,7 +3187,7 @@ var TRANSFORMER_API;
|
|
|
3117
3187
|
/** @public */
|
|
3118
3188
|
const name = "html-validate";
|
|
3119
3189
|
/** @public */
|
|
3120
|
-
const version = "7.1
|
|
3190
|
+
const version = "7.3.1";
|
|
3121
3191
|
/** @public */
|
|
3122
3192
|
const homepage = "https://html-validate.org";
|
|
3123
3193
|
/** @public */
|
|
@@ -3218,6 +3288,18 @@ function getSchemaValidator(ruleId, properties) {
|
|
|
3218
3288
|
};
|
|
3219
3289
|
return ajv$1.compile(schema);
|
|
3220
3290
|
}
|
|
3291
|
+
function isErrorDescriptor(value) {
|
|
3292
|
+
return Boolean(value[0] && value[0].message);
|
|
3293
|
+
}
|
|
3294
|
+
function unpackErrorDescriptor(value) {
|
|
3295
|
+
if (isErrorDescriptor(value)) {
|
|
3296
|
+
return value[0];
|
|
3297
|
+
}
|
|
3298
|
+
else {
|
|
3299
|
+
const [node, message, location, context] = value;
|
|
3300
|
+
return { node, message, location, context };
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3221
3303
|
/**
|
|
3222
3304
|
* @public
|
|
3223
3305
|
*/
|
|
@@ -3318,13 +3400,8 @@ class Rule {
|
|
|
3318
3400
|
static schema() {
|
|
3319
3401
|
return null;
|
|
3320
3402
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
*
|
|
3324
|
-
* Rule must be enabled both globally and on the specific node for this to
|
|
3325
|
-
* have any effect.
|
|
3326
|
-
*/
|
|
3327
|
-
report(node, message, location, context) {
|
|
3403
|
+
report(...args) {
|
|
3404
|
+
const { node, message, location, context } = unpackErrorDescriptor(args);
|
|
3328
3405
|
if (this.isEnabled() && (!node || node.ruleEnabled(this.name))) {
|
|
3329
3406
|
const where = this.findLocation({ node, location, event: this.event });
|
|
3330
3407
|
const interpolated = interpolate(message, context !== null && context !== void 0 ? context : {});
|
|
@@ -3837,9 +3914,14 @@ class AttrCase extends Rule {
|
|
|
3837
3914
|
return;
|
|
3838
3915
|
}
|
|
3839
3916
|
const letters = event.key.replace(/[^a-z]+/gi, "");
|
|
3840
|
-
if (
|
|
3841
|
-
|
|
3917
|
+
if (this.style.match(letters)) {
|
|
3918
|
+
return;
|
|
3842
3919
|
}
|
|
3920
|
+
this.report({
|
|
3921
|
+
node: event.target,
|
|
3922
|
+
message: `Attribute "${event.key}" should be ${this.style.name}`,
|
|
3923
|
+
location: event.keyLocation,
|
|
3924
|
+
});
|
|
3843
3925
|
});
|
|
3844
3926
|
}
|
|
3845
3927
|
isIgnored(node) {
|
|
@@ -5139,6 +5221,11 @@ class ElementName extends Rule {
|
|
|
5139
5221
|
}
|
|
5140
5222
|
}
|
|
5141
5223
|
|
|
5224
|
+
var ErrorKind;
|
|
5225
|
+
(function (ErrorKind) {
|
|
5226
|
+
ErrorKind["CONTENT"] = "content";
|
|
5227
|
+
ErrorKind["DESCENDANT"] = "descendant";
|
|
5228
|
+
})(ErrorKind || (ErrorKind = {}));
|
|
5142
5229
|
function getTransparentChildren(node, transparent) {
|
|
5143
5230
|
if (typeof transparent === "boolean") {
|
|
5144
5231
|
return node.childElements;
|
|
@@ -5152,10 +5239,28 @@ function getTransparentChildren(node, transparent) {
|
|
|
5152
5239
|
});
|
|
5153
5240
|
}
|
|
5154
5241
|
}
|
|
5242
|
+
function getRuleDescription$1(context) {
|
|
5243
|
+
if (!context) {
|
|
5244
|
+
return [
|
|
5245
|
+
"Some elements has restrictions on what content is allowed.",
|
|
5246
|
+
"This can include both direct children or descendant elements.",
|
|
5247
|
+
];
|
|
5248
|
+
}
|
|
5249
|
+
switch (context.kind) {
|
|
5250
|
+
case ErrorKind.CONTENT:
|
|
5251
|
+
return [
|
|
5252
|
+
`The \`${context.child}\` element is not permitted as content under the parent \`${context.parent}\` element.`,
|
|
5253
|
+
];
|
|
5254
|
+
case ErrorKind.DESCENDANT:
|
|
5255
|
+
return [
|
|
5256
|
+
`The \`${context.child}\` element is not permitted as a descendant of the \`${context.ancestor}\` element.`,
|
|
5257
|
+
];
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5155
5260
|
class ElementPermittedContent extends Rule {
|
|
5156
|
-
documentation() {
|
|
5261
|
+
documentation(context) {
|
|
5157
5262
|
return {
|
|
5158
|
-
description:
|
|
5263
|
+
description: getRuleDescription$1(context).join("\n"),
|
|
5159
5264
|
url: ruleDocumentationUrl("@/rules/element-permitted-content.ts"),
|
|
5160
5265
|
};
|
|
5161
5266
|
}
|
|
@@ -5164,8 +5269,9 @@ class ElementPermittedContent extends Rule {
|
|
|
5164
5269
|
const doc = event.document;
|
|
5165
5270
|
doc.visitDepthFirst((node) => {
|
|
5166
5271
|
const parent = node.parent;
|
|
5167
|
-
/*
|
|
5168
|
-
|
|
5272
|
+
/* istanbul ignore next: satisfy typescript but will visitDepthFirst()
|
|
5273
|
+
* will not yield nodes without a parent */
|
|
5274
|
+
if (!parent) {
|
|
5169
5275
|
return;
|
|
5170
5276
|
}
|
|
5171
5277
|
/* Run each validation step, stop as soon as any errors are
|
|
@@ -5175,7 +5281,6 @@ class ElementPermittedContent extends Rule {
|
|
|
5175
5281
|
[
|
|
5176
5282
|
() => this.validatePermittedContent(node, parent),
|
|
5177
5283
|
() => this.validatePermittedDescendant(node, parent),
|
|
5178
|
-
() => this.validatePermittedAncestors(node),
|
|
5179
5284
|
].some((fn) => fn());
|
|
5180
5285
|
});
|
|
5181
5286
|
});
|
|
@@ -5192,7 +5297,14 @@ class ElementPermittedContent extends Rule {
|
|
|
5192
5297
|
}
|
|
5193
5298
|
validatePermittedContentImpl(cur, parent, rules) {
|
|
5194
5299
|
if (!Validator.validatePermitted(cur, rules)) {
|
|
5195
|
-
|
|
5300
|
+
const child = `<${cur.tagName}>`;
|
|
5301
|
+
const message = `${child} element is not permitted as content under ${parent.annotatedName}`;
|
|
5302
|
+
const context = {
|
|
5303
|
+
kind: ErrorKind.CONTENT,
|
|
5304
|
+
parent: parent.annotatedName,
|
|
5305
|
+
child,
|
|
5306
|
+
};
|
|
5307
|
+
this.report(cur, message, null, context);
|
|
5196
5308
|
return true;
|
|
5197
5309
|
}
|
|
5198
5310
|
/* for transparent elements all/listed children must be validated against
|
|
@@ -5223,21 +5335,15 @@ class ElementPermittedContent extends Rule {
|
|
|
5223
5335
|
if (Validator.validatePermitted(node, rules)) {
|
|
5224
5336
|
continue;
|
|
5225
5337
|
}
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
const rules = node.meta.requiredAncestors;
|
|
5236
|
-
if (!rules) {
|
|
5237
|
-
return false;
|
|
5238
|
-
}
|
|
5239
|
-
if (!Validator.validateAncestors(node, rules)) {
|
|
5240
|
-
this.report(node, `Element <${node.tagName}> requires an "${rules[0]}" ancestor`);
|
|
5338
|
+
const child = `<${node.tagName}>`;
|
|
5339
|
+
const ancestor = cur.annotatedName;
|
|
5340
|
+
const message = `${child} element is not permitted as a descendant of ${ancestor}`;
|
|
5341
|
+
const context = {
|
|
5342
|
+
kind: ErrorKind.DESCENDANT,
|
|
5343
|
+
ancestor,
|
|
5344
|
+
child,
|
|
5345
|
+
};
|
|
5346
|
+
this.report(node, message, null, context);
|
|
5241
5347
|
return true;
|
|
5242
5348
|
}
|
|
5243
5349
|
return false;
|
|
@@ -5304,6 +5410,152 @@ class ElementPermittedOrder extends Rule {
|
|
|
5304
5410
|
}
|
|
5305
5411
|
}
|
|
5306
5412
|
|
|
5413
|
+
const CACHE_KEY = Symbol(classifyNodeText.name);
|
|
5414
|
+
var TextClassification;
|
|
5415
|
+
(function (TextClassification) {
|
|
5416
|
+
TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
5417
|
+
TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
5418
|
+
TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
5419
|
+
})(TextClassification || (TextClassification = {}));
|
|
5420
|
+
/**
|
|
5421
|
+
* Checks text content of an element.
|
|
5422
|
+
*
|
|
5423
|
+
* Any text is considered including text from descendant elements. Whitespace is
|
|
5424
|
+
* ignored.
|
|
5425
|
+
*
|
|
5426
|
+
* If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
|
|
5427
|
+
*/
|
|
5428
|
+
function classifyNodeText(node) {
|
|
5429
|
+
if (node.cacheExists(CACHE_KEY)) {
|
|
5430
|
+
return node.cacheGet(CACHE_KEY);
|
|
5431
|
+
}
|
|
5432
|
+
const text = findTextNodes(node);
|
|
5433
|
+
/* if any text is dynamic classify as dynamic */
|
|
5434
|
+
if (text.some((cur) => cur.isDynamic)) {
|
|
5435
|
+
return node.cacheSet(CACHE_KEY, TextClassification.DYNAMIC_TEXT);
|
|
5436
|
+
}
|
|
5437
|
+
/* if any text has non-whitespace character classify as static */
|
|
5438
|
+
if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
|
|
5439
|
+
return node.cacheSet(CACHE_KEY, TextClassification.STATIC_TEXT);
|
|
5440
|
+
}
|
|
5441
|
+
/* default to empty */
|
|
5442
|
+
return node.cacheSet(CACHE_KEY, TextClassification.EMPTY_TEXT);
|
|
5443
|
+
}
|
|
5444
|
+
function findTextNodes(node) {
|
|
5445
|
+
let text = [];
|
|
5446
|
+
for (const child of node.childNodes) {
|
|
5447
|
+
switch (child.nodeType) {
|
|
5448
|
+
case NodeType.TEXT_NODE:
|
|
5449
|
+
text.push(child);
|
|
5450
|
+
break;
|
|
5451
|
+
case NodeType.ELEMENT_NODE:
|
|
5452
|
+
text = text.concat(findTextNodes(child));
|
|
5453
|
+
break;
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
return text;
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
function hasAltText(image) {
|
|
5460
|
+
const alt = image.getAttribute("alt");
|
|
5461
|
+
/* missing or boolean */
|
|
5462
|
+
if (alt === null || alt.value === null) {
|
|
5463
|
+
return false;
|
|
5464
|
+
}
|
|
5465
|
+
return alt.isDynamic || alt.value.toString() !== "";
|
|
5466
|
+
}
|
|
5467
|
+
|
|
5468
|
+
function hasAriaLabel(node) {
|
|
5469
|
+
const label = node.getAttribute("aria-label");
|
|
5470
|
+
/* missing or boolean */
|
|
5471
|
+
if (label === null || label.value === null) {
|
|
5472
|
+
return false;
|
|
5473
|
+
}
|
|
5474
|
+
return label.isDynamic || label.value.toString() !== "";
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5477
|
+
/**
|
|
5478
|
+
* Joins a list of words into natural language.
|
|
5479
|
+
*
|
|
5480
|
+
* - `["foo"]` becomes `"foo"`
|
|
5481
|
+
* - `["foo", "bar"]` becomes `"foo or bar"`
|
|
5482
|
+
* - `["foo", "bar", "baz"]` becomes `"foo, bar or baz"`
|
|
5483
|
+
* - and so on...
|
|
5484
|
+
*
|
|
5485
|
+
* @internal
|
|
5486
|
+
* @param values - List of words to join
|
|
5487
|
+
* @param conjunction - Conjunction for the last element.
|
|
5488
|
+
* @returns String with the words naturally joined with a conjunction.
|
|
5489
|
+
*/
|
|
5490
|
+
function naturalJoin(values, conjunction = "or") {
|
|
5491
|
+
switch (values.length) {
|
|
5492
|
+
case 0:
|
|
5493
|
+
return "";
|
|
5494
|
+
case 1:
|
|
5495
|
+
return values[0];
|
|
5496
|
+
case 2:
|
|
5497
|
+
return `${values[0]} ${conjunction} ${values[1]}`;
|
|
5498
|
+
default:
|
|
5499
|
+
return `${values.slice(0, -1).join(", ")} ${conjunction} ${values.slice(-1)[0]}`;
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
function isTagnameOnly(value) {
|
|
5504
|
+
return Boolean(value.match(/^[a-zA-Z0-9-]+$/));
|
|
5505
|
+
}
|
|
5506
|
+
function getRuleDescription(context) {
|
|
5507
|
+
if (!context) {
|
|
5508
|
+
return [
|
|
5509
|
+
"Some elements has restrictions on what content is allowed.",
|
|
5510
|
+
"This can include both direct children or descendant elements.",
|
|
5511
|
+
];
|
|
5512
|
+
}
|
|
5513
|
+
const escaped = context.ancestor.map((it) => `\`${it}\``);
|
|
5514
|
+
return [`The \`${context.child}\` element requires a ${naturalJoin(escaped)} ancestor.`];
|
|
5515
|
+
}
|
|
5516
|
+
class ElementRequiredAncestor extends Rule {
|
|
5517
|
+
documentation(context) {
|
|
5518
|
+
return {
|
|
5519
|
+
description: getRuleDescription(context).join("\n"),
|
|
5520
|
+
url: ruleDocumentationUrl("@/rules/element-required-ancestor.ts"),
|
|
5521
|
+
};
|
|
5522
|
+
}
|
|
5523
|
+
setup() {
|
|
5524
|
+
this.on("dom:ready", (event) => {
|
|
5525
|
+
const doc = event.document;
|
|
5526
|
+
doc.visitDepthFirst((node) => {
|
|
5527
|
+
const parent = node.parent;
|
|
5528
|
+
/* istanbul ignore next: satisfy typescript but will visitDepthFirst()
|
|
5529
|
+
* will not yield nodes without a parent */
|
|
5530
|
+
if (!parent) {
|
|
5531
|
+
return;
|
|
5532
|
+
}
|
|
5533
|
+
this.validateRequiredAncestors(node);
|
|
5534
|
+
});
|
|
5535
|
+
});
|
|
5536
|
+
}
|
|
5537
|
+
validateRequiredAncestors(node) {
|
|
5538
|
+
if (!node.meta) {
|
|
5539
|
+
return;
|
|
5540
|
+
}
|
|
5541
|
+
const rules = node.meta.requiredAncestors;
|
|
5542
|
+
if (!rules) {
|
|
5543
|
+
return;
|
|
5544
|
+
}
|
|
5545
|
+
if (Validator.validateAncestors(node, rules)) {
|
|
5546
|
+
return;
|
|
5547
|
+
}
|
|
5548
|
+
const ancestor = rules.map((it) => (isTagnameOnly(it) ? `<${it}>` : `"${it}"`));
|
|
5549
|
+
const child = `<${node.tagName}>`;
|
|
5550
|
+
const message = `<${node.tagName}> element requires a ${naturalJoin(ancestor)} ancestor`;
|
|
5551
|
+
const context = {
|
|
5552
|
+
ancestor,
|
|
5553
|
+
child,
|
|
5554
|
+
};
|
|
5555
|
+
this.report(node, message, null, context);
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5307
5559
|
class ElementRequiredAttributes extends Rule {
|
|
5308
5560
|
documentation(context) {
|
|
5309
5561
|
const docs = {
|
|
@@ -5381,52 +5633,6 @@ class ElementRequiredContent extends Rule {
|
|
|
5381
5633
|
}
|
|
5382
5634
|
}
|
|
5383
5635
|
|
|
5384
|
-
const CACHE_KEY = Symbol(classifyNodeText.name);
|
|
5385
|
-
var TextClassification;
|
|
5386
|
-
(function (TextClassification) {
|
|
5387
|
-
TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
5388
|
-
TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
5389
|
-
TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
5390
|
-
})(TextClassification || (TextClassification = {}));
|
|
5391
|
-
/**
|
|
5392
|
-
* Checks text content of an element.
|
|
5393
|
-
*
|
|
5394
|
-
* Any text is considered including text from descendant elements. Whitespace is
|
|
5395
|
-
* ignored.
|
|
5396
|
-
*
|
|
5397
|
-
* If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
|
|
5398
|
-
*/
|
|
5399
|
-
function classifyNodeText(node) {
|
|
5400
|
-
if (node.cacheExists(CACHE_KEY)) {
|
|
5401
|
-
return node.cacheGet(CACHE_KEY);
|
|
5402
|
-
}
|
|
5403
|
-
const text = findTextNodes(node);
|
|
5404
|
-
/* if any text is dynamic classify as dynamic */
|
|
5405
|
-
if (text.some((cur) => cur.isDynamic)) {
|
|
5406
|
-
return node.cacheSet(CACHE_KEY, TextClassification.DYNAMIC_TEXT);
|
|
5407
|
-
}
|
|
5408
|
-
/* if any text has non-whitespace character classify as static */
|
|
5409
|
-
if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
|
|
5410
|
-
return node.cacheSet(CACHE_KEY, TextClassification.STATIC_TEXT);
|
|
5411
|
-
}
|
|
5412
|
-
/* default to empty */
|
|
5413
|
-
return node.cacheSet(CACHE_KEY, TextClassification.EMPTY_TEXT);
|
|
5414
|
-
}
|
|
5415
|
-
function findTextNodes(node) {
|
|
5416
|
-
let text = [];
|
|
5417
|
-
for (const child of node.childNodes) {
|
|
5418
|
-
switch (child.nodeType) {
|
|
5419
|
-
case NodeType.TEXT_NODE:
|
|
5420
|
-
text.push(child);
|
|
5421
|
-
break;
|
|
5422
|
-
case NodeType.ELEMENT_NODE:
|
|
5423
|
-
text = text.concat(findTextNodes(child));
|
|
5424
|
-
break;
|
|
5425
|
-
}
|
|
5426
|
-
}
|
|
5427
|
-
return text;
|
|
5428
|
-
}
|
|
5429
|
-
|
|
5430
5636
|
const selector = ["h1", "h2", "h3", "h4", "h5", "h6"].join(",");
|
|
5431
5637
|
class EmptyHeading extends Rule {
|
|
5432
5638
|
documentation() {
|
|
@@ -7607,24 +7813,6 @@ class TelNonBreaking extends Rule {
|
|
|
7607
7813
|
}
|
|
7608
7814
|
}
|
|
7609
7815
|
|
|
7610
|
-
function hasAltText(image) {
|
|
7611
|
-
const alt = image.getAttribute("alt");
|
|
7612
|
-
/* missing or boolean */
|
|
7613
|
-
if (alt === null || alt.value === null) {
|
|
7614
|
-
return false;
|
|
7615
|
-
}
|
|
7616
|
-
return alt.isDynamic || alt.value.toString() !== "";
|
|
7617
|
-
}
|
|
7618
|
-
|
|
7619
|
-
function hasAriaLabel(node) {
|
|
7620
|
-
const label = node.getAttribute("aria-label");
|
|
7621
|
-
/* missing or boolean */
|
|
7622
|
-
if (label === null || label.value === null) {
|
|
7623
|
-
return false;
|
|
7624
|
-
}
|
|
7625
|
-
return label.isDynamic || label.value.toString() !== "";
|
|
7626
|
-
}
|
|
7627
|
-
|
|
7628
7816
|
/**
|
|
7629
7817
|
* Check if attribute is present and non-empty or dynamic.
|
|
7630
7818
|
*/
|
|
@@ -9802,13 +9990,13 @@ class VoidStyle extends Rule {
|
|
|
9802
9990
|
return;
|
|
9803
9991
|
}
|
|
9804
9992
|
if (this.shouldBeOmitted(node)) {
|
|
9805
|
-
this.
|
|
9993
|
+
this.reportError(node, `Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`);
|
|
9806
9994
|
}
|
|
9807
9995
|
if (this.shouldBeSelfClosed(node)) {
|
|
9808
|
-
this.
|
|
9996
|
+
this.reportError(node, `Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`);
|
|
9809
9997
|
}
|
|
9810
9998
|
}
|
|
9811
|
-
|
|
9999
|
+
reportError(node, message) {
|
|
9812
10000
|
const context = {
|
|
9813
10001
|
style: this.style,
|
|
9814
10002
|
tagName: node.tagName,
|
|
@@ -10120,6 +10308,7 @@ const bundledRules = {
|
|
|
10120
10308
|
"element-permitted-content": ElementPermittedContent,
|
|
10121
10309
|
"element-permitted-occurrences": ElementPermittedOccurrences,
|
|
10122
10310
|
"element-permitted-order": ElementPermittedOrder,
|
|
10311
|
+
"element-required-ancestor": ElementRequiredAncestor,
|
|
10123
10312
|
"element-required-attributes": ElementRequiredAttributes,
|
|
10124
10313
|
"element-required-content": ElementRequiredContent,
|
|
10125
10314
|
"empty-heading": EmptyHeading,
|
|
@@ -10227,6 +10416,7 @@ const config$1 = {
|
|
|
10227
10416
|
"element-permitted-content": "error",
|
|
10228
10417
|
"element-permitted-occurrences": "error",
|
|
10229
10418
|
"element-permitted-order": "error",
|
|
10419
|
+
"element-required-ancestor": "error",
|
|
10230
10420
|
"element-required-attributes": "error",
|
|
10231
10421
|
"element-required-content": "error",
|
|
10232
10422
|
"empty-heading": "error",
|
|
@@ -10285,6 +10475,7 @@ const config = {
|
|
|
10285
10475
|
"element-permitted-content": "error",
|
|
10286
10476
|
"element-permitted-occurrences": "error",
|
|
10287
10477
|
"element-permitted-order": "error",
|
|
10478
|
+
"element-required-ancestor": "error",
|
|
10288
10479
|
"element-required-attributes": "error",
|
|
10289
10480
|
"element-required-content": "error",
|
|
10290
10481
|
"multiple-labeled-controls": "error",
|
|
@@ -10364,6 +10555,7 @@ class ResolvedConfig {
|
|
|
10364
10555
|
});
|
|
10365
10556
|
}
|
|
10366
10557
|
catch (err) {
|
|
10558
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10367
10559
|
const message = err instanceof Error ? err.message : String(err);
|
|
10368
10560
|
throw new NestedError(`When transforming "${source.filename}": ${message}`, ensureError(err));
|
|
10369
10561
|
}
|
|
@@ -10376,7 +10568,7 @@ class ResolvedConfig {
|
|
|
10376
10568
|
* Wrapper around [[transformSource]] which reads a file before passing it
|
|
10377
10569
|
* as-is to transformSource.
|
|
10378
10570
|
*
|
|
10379
|
-
* @param
|
|
10571
|
+
* @param filename - Filename to transform (according to configured
|
|
10380
10572
|
* transformations)
|
|
10381
10573
|
* @returns A list of transformed sources ready for validation.
|
|
10382
10574
|
*/
|
|
@@ -10532,7 +10724,9 @@ class Config {
|
|
|
10532
10724
|
var _a;
|
|
10533
10725
|
const valid = validator(configData);
|
|
10534
10726
|
if (!valid) {
|
|
10535
|
-
throw new SchemaValidationError(filename, `Invalid configuration`, configData, configurationSchema,
|
|
10727
|
+
throw new SchemaValidationError(filename, `Invalid configuration`, configData, configurationSchema,
|
|
10728
|
+
/* istanbul ignore next: will be set when a validation error has occurred */
|
|
10729
|
+
(_a = validator.errors) !== null && _a !== void 0 ? _a : []);
|
|
10536
10730
|
}
|
|
10537
10731
|
if (configData.rules) {
|
|
10538
10732
|
const normalizedRules = Config.getRulesObject(configData.rules);
|
|
@@ -10641,6 +10835,7 @@ class Config {
|
|
|
10641
10835
|
metaTable.loadFromObject(legacyRequire(entry));
|
|
10642
10836
|
}
|
|
10643
10837
|
catch (err) {
|
|
10838
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10644
10839
|
const message = err instanceof Error ? err.message : String(err);
|
|
10645
10840
|
throw new ConfigError(`Failed to load elements from "${entry}": ${message}`, ensureError(err));
|
|
10646
10841
|
}
|
|
@@ -10662,6 +10857,7 @@ class Config {
|
|
|
10662
10857
|
*
|
|
10663
10858
|
* @internal primary purpose is unittests
|
|
10664
10859
|
*/
|
|
10860
|
+
/* istanbul ignore next: used for testing only */
|
|
10665
10861
|
get() {
|
|
10666
10862
|
const config = { ...this.config };
|
|
10667
10863
|
if (config.elements) {
|
|
@@ -10684,6 +10880,7 @@ class Config {
|
|
|
10684
10880
|
*/
|
|
10685
10881
|
getRules() {
|
|
10686
10882
|
var _a;
|
|
10883
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10687
10884
|
return Config.getRulesObject((_a = this.config.rules) !== null && _a !== void 0 ? _a : {});
|
|
10688
10885
|
}
|
|
10689
10886
|
static getRulesObject(src) {
|
|
@@ -10718,6 +10915,7 @@ class Config {
|
|
|
10718
10915
|
return plugin;
|
|
10719
10916
|
}
|
|
10720
10917
|
catch (err) {
|
|
10918
|
+
/* istanbul ignore next: only used as a fallback */
|
|
10721
10919
|
const message = err instanceof Error ? err.message : String(err);
|
|
10722
10920
|
throw new ConfigError(`Failed to load plugin "${moduleName}": ${message}`, ensureError(err));
|
|
10723
10921
|
}
|
|
@@ -11633,9 +11831,9 @@ class Reporter {
|
|
|
11633
11831
|
if (!(location.filename in this.result)) {
|
|
11634
11832
|
this.result[location.filename] = [];
|
|
11635
11833
|
}
|
|
11636
|
-
|
|
11834
|
+
const ruleUrl = (_a = rule.documentation(context)) === null || _a === void 0 ? void 0 : _a.url;
|
|
11835
|
+
const entry = {
|
|
11637
11836
|
ruleId: rule.name,
|
|
11638
|
-
ruleUrl: (_a = rule.documentation(context)) === null || _a === void 0 ? void 0 : _a.url,
|
|
11639
11837
|
severity,
|
|
11640
11838
|
message,
|
|
11641
11839
|
offset: location.offset,
|
|
@@ -11645,8 +11843,14 @@ class Reporter {
|
|
|
11645
11843
|
selector() {
|
|
11646
11844
|
return node ? node.generateSelector() : null;
|
|
11647
11845
|
},
|
|
11648
|
-
|
|
11649
|
-
|
|
11846
|
+
};
|
|
11847
|
+
if (ruleUrl) {
|
|
11848
|
+
entry.ruleUrl = ruleUrl;
|
|
11849
|
+
}
|
|
11850
|
+
if (context) {
|
|
11851
|
+
entry.context = context;
|
|
11852
|
+
}
|
|
11853
|
+
this.result[location.filename].push(entry);
|
|
11650
11854
|
}
|
|
11651
11855
|
addManual(filename, message) {
|
|
11652
11856
|
if (!(filename in this.result)) {
|
|
@@ -12060,7 +12264,7 @@ class Engine {
|
|
|
12060
12264
|
offset: location.offset,
|
|
12061
12265
|
line: location.line,
|
|
12062
12266
|
column: location.column,
|
|
12063
|
-
size: location.size
|
|
12267
|
+
size: location.size,
|
|
12064
12268
|
selector: () => null,
|
|
12065
12269
|
});
|
|
12066
12270
|
}
|
|
@@ -12490,12 +12694,12 @@ class FileSystemConfigLoader extends ConfigLoader {
|
|
|
12490
12694
|
* `null` if no configuration files are found.
|
|
12491
12695
|
*/
|
|
12492
12696
|
fromFilename(filename) {
|
|
12493
|
-
var _a;
|
|
12494
12697
|
if (filename === "inline") {
|
|
12495
12698
|
return null;
|
|
12496
12699
|
}
|
|
12497
|
-
|
|
12498
|
-
|
|
12700
|
+
const cache = this.cache.get(filename);
|
|
12701
|
+
if (cache) {
|
|
12702
|
+
return cache;
|
|
12499
12703
|
}
|
|
12500
12704
|
let found = false;
|
|
12501
12705
|
let current = path__default["default"].resolve(path__default["default"].dirname(filename));
|