html-validate 8.0.3 → 8.0.5
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/README.md +14 -6
- package/dist/cjs/browser.js +7 -7
- package/dist/cjs/cli.js +0 -1
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core.js +2464 -2000
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/elements.js.map +1 -1
- package/dist/cjs/html-validate.js +1 -1
- package/dist/cjs/html5.js +10 -0
- package/dist/cjs/html5.js.map +1 -0
- package/dist/cjs/index.js +7 -7
- package/dist/cjs/jest.js +1 -1
- package/dist/cjs/meta-helper.js +4 -4
- package/dist/cjs/meta-helper.js.map +1 -1
- package/dist/cjs/nodejs.js +1 -22
- package/dist/cjs/nodejs.js.map +1 -1
- package/dist/cjs/utils/natural-join.js +30 -0
- package/dist/cjs/utils/natural-join.js.map +1 -0
- package/dist/es/browser.js +2 -2
- package/dist/es/cli.js +1 -2
- package/dist/es/cli.js.map +1 -1
- package/dist/es/core.js +2397 -1933
- package/dist/es/core.js.map +1 -1
- package/dist/es/elements.js.map +1 -1
- package/dist/es/html-validate.js +2 -2
- package/dist/es/html5.js +8 -0
- package/dist/es/html5.js.map +1 -0
- package/dist/es/index.js +2 -2
- package/dist/es/jest-lib.js +1 -1
- package/dist/es/jest.js +1 -1
- package/dist/es/meta-helper.js +1 -1
- package/dist/es/nodejs.js +1 -22
- package/dist/es/nodejs.js.map +1 -1
- package/dist/es/utils/natural-join.js +28 -0
- package/dist/es/utils/natural-join.js.map +1 -0
- package/node.d.ts +1 -0
- package/node.js +1 -0
- package/package.json +14 -3
- package/dist/cjs/rules-helper.js +0 -486
- package/dist/cjs/rules-helper.js.map +0 -1
- package/dist/es/rules-helper.js +0 -473
- package/dist/es/rules-helper.js.map +0 -1
package/dist/cjs/core.js
CHANGED
|
@@ -6,8 +6,8 @@ var elements = require('./elements.js');
|
|
|
6
6
|
var fs = require('fs');
|
|
7
7
|
var semver = require('semver');
|
|
8
8
|
var kleur = require('kleur');
|
|
9
|
-
var rulesHelper = require('./rules-helper.js');
|
|
10
9
|
var betterAjvErrors = require('@sidvind/better-ajv-errors');
|
|
10
|
+
var utils_naturalJoin = require('./utils/natural-join.js');
|
|
11
11
|
var codeFrame = require('@babel/code-frame');
|
|
12
12
|
var stylish$2 = require('@html-validate/stylish');
|
|
13
13
|
|
|
@@ -251,1593 +251,166 @@ var ajvSchemaDraft = {
|
|
|
251
251
|
}
|
|
252
252
|
};
|
|
253
253
|
|
|
254
|
+
function stringify(value) {
|
|
255
|
+
if (typeof value === "string") {
|
|
256
|
+
return String(value);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
return JSON.stringify(value);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
254
262
|
/**
|
|
263
|
+
* Represents an `Error` created from arbitrary values.
|
|
264
|
+
*
|
|
255
265
|
* @public
|
|
256
266
|
*/
|
|
257
|
-
class
|
|
258
|
-
constructor(
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
toString() {
|
|
262
|
-
return this.expr;
|
|
267
|
+
class WrappedError extends Error {
|
|
268
|
+
constructor(message) {
|
|
269
|
+
super(stringify(message));
|
|
263
270
|
}
|
|
264
271
|
}
|
|
265
272
|
|
|
266
273
|
/**
|
|
267
|
-
*
|
|
274
|
+
* Ensures the value is an Error.
|
|
268
275
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
276
|
+
* If the passed value is not an `Error` instance a [[WrappedError]] is
|
|
277
|
+
* constructed with the stringified value.
|
|
271
278
|
*
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
function ensureError(value) {
|
|
282
|
+
if (value instanceof Error) {
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
return new WrappedError(value);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
272
291
|
* @public
|
|
273
292
|
*/
|
|
274
|
-
class
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
* transformation (e.g. vuejs `:id` generating the `id` attribute) this
|
|
282
|
-
* parameter should be set to the attribute name of the source attribute (`:id`).
|
|
283
|
-
*/
|
|
284
|
-
constructor(key, value, keyLocation, valueLocation, originalAttribute) {
|
|
285
|
-
this.key = key;
|
|
286
|
-
this.value = value;
|
|
287
|
-
this.keyLocation = keyLocation;
|
|
288
|
-
this.valueLocation = valueLocation;
|
|
289
|
-
this.originalAttribute = originalAttribute;
|
|
290
|
-
/* force undefined to null */
|
|
291
|
-
if (typeof this.value === "undefined") {
|
|
292
|
-
this.value = null;
|
|
293
|
+
class NestedError extends Error {
|
|
294
|
+
constructor(message, nested) {
|
|
295
|
+
super(message);
|
|
296
|
+
Error.captureStackTrace(this, NestedError);
|
|
297
|
+
this.name = NestedError.name;
|
|
298
|
+
if (nested && nested.stack) {
|
|
299
|
+
this.stack += `\nCaused by: ${nested.stack}`;
|
|
293
300
|
}
|
|
294
301
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @public
|
|
306
|
+
*/
|
|
307
|
+
class UserError extends NestedError {
|
|
308
|
+
constructor(message, nested) {
|
|
309
|
+
super(message, nested);
|
|
310
|
+
Error.captureStackTrace(this, UserError);
|
|
311
|
+
this.name = UserError.name;
|
|
300
312
|
}
|
|
301
313
|
/**
|
|
302
|
-
*
|
|
314
|
+
* @public
|
|
303
315
|
*/
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
valueMatches(pattern, dynamicMatches = true) {
|
|
308
|
-
if (this.value === null) {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
/* dynamic values matches everything */
|
|
312
|
-
if (this.value instanceof DynamicValue) {
|
|
313
|
-
return dynamicMatches;
|
|
314
|
-
}
|
|
315
|
-
/* test against an array of keywords */
|
|
316
|
-
if (Array.isArray(pattern)) {
|
|
317
|
-
return pattern.includes(this.value);
|
|
318
|
-
}
|
|
319
|
-
/* test value against pattern */
|
|
320
|
-
if (pattern instanceof RegExp) {
|
|
321
|
-
return this.value.match(pattern) !== null;
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
return this.value === pattern;
|
|
325
|
-
}
|
|
316
|
+
/* istanbul ignore next: default implementation */
|
|
317
|
+
prettyFormat() {
|
|
318
|
+
return undefined;
|
|
326
319
|
}
|
|
327
320
|
}
|
|
328
321
|
|
|
329
|
-
function getCSSDeclarations(value) {
|
|
330
|
-
return value
|
|
331
|
-
.trim()
|
|
332
|
-
.split(";")
|
|
333
|
-
.filter(Boolean)
|
|
334
|
-
.map((it) => {
|
|
335
|
-
const [property, value] = it.split(":", 2);
|
|
336
|
-
return [property.trim(), value ? value.trim() : ""];
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
322
|
/**
|
|
340
323
|
* @internal
|
|
341
324
|
*/
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
325
|
+
class InheritError extends UserError {
|
|
326
|
+
constructor({ tagName, inherit }) {
|
|
327
|
+
const message = `Element <${tagName}> cannot inherit from <${inherit}>: no such element`;
|
|
328
|
+
super(message);
|
|
329
|
+
Error.captureStackTrace(this, InheritError);
|
|
330
|
+
this.name = InheritError.name;
|
|
331
|
+
this.tagName = tagName;
|
|
332
|
+
this.inherit = inherit;
|
|
333
|
+
this.filename = null;
|
|
334
|
+
}
|
|
335
|
+
prettyFormat() {
|
|
336
|
+
const { message, tagName, inherit } = this;
|
|
337
|
+
const source = this.filename
|
|
338
|
+
? ["", "This error occurred when loading element metadata from:", `"${this.filename}"`, ""]
|
|
339
|
+
: [""];
|
|
340
|
+
return [
|
|
341
|
+
message,
|
|
342
|
+
...source,
|
|
343
|
+
"This usually occurs when the elements are defined in the wrong order, try one of the following:",
|
|
344
|
+
"",
|
|
345
|
+
` - Ensure the spelling of "${inherit}" is correct.`,
|
|
346
|
+
` - Ensure the file containing "${inherit}" is loaded before the file containing "${tagName}".`,
|
|
347
|
+
` - Move the definition of "${inherit}" above the definition for "${tagName}".`,
|
|
348
|
+
].join("\n");
|
|
345
349
|
}
|
|
346
|
-
const pairs = getCSSDeclarations(value);
|
|
347
|
-
return Object.fromEntries(pairs);
|
|
348
350
|
}
|
|
349
351
|
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
function getSummary(schema, obj, errors) {
|
|
353
|
+
const output = betterAjvErrors__default.default(schema, obj, errors, {
|
|
354
|
+
format: "js",
|
|
355
|
+
});
|
|
356
|
+
// istanbul ignore next: for safety only
|
|
357
|
+
return output.length > 0 ? output[0].error : "unknown validation error";
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* @public
|
|
361
|
+
*/
|
|
362
|
+
class SchemaValidationError extends UserError {
|
|
363
|
+
constructor(filename, message, obj, schema, errors) {
|
|
364
|
+
const summary = getSummary(schema, obj, errors);
|
|
365
|
+
super(`${message}: ${summary}`);
|
|
366
|
+
this.filename = filename;
|
|
367
|
+
this.obj = obj;
|
|
368
|
+
this.schema = schema;
|
|
369
|
+
this.errors = errors;
|
|
353
370
|
}
|
|
354
|
-
|
|
355
|
-
|
|
371
|
+
prettyError() {
|
|
372
|
+
const json = this.getRawJSON();
|
|
373
|
+
return betterAjvErrors__default.default(this.schema, this.obj, this.errors, {
|
|
374
|
+
format: "cli",
|
|
375
|
+
indent: 2,
|
|
376
|
+
json,
|
|
377
|
+
});
|
|
356
378
|
}
|
|
357
|
-
|
|
358
|
-
|
|
379
|
+
getRawJSON() {
|
|
380
|
+
if (this.filename && fs__default.default.existsSync(this.filename)) {
|
|
381
|
+
return fs__default.default.readFileSync(this.filename, "utf-8");
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
359
386
|
}
|
|
360
|
-
return Math.min(size, end - begin);
|
|
361
387
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
} while (true); // eslint-disable-line no-constant-condition -- it will break out
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Computes hash for given string.
|
|
391
|
+
*
|
|
392
|
+
* @internal
|
|
393
|
+
*/
|
|
394
|
+
function cyrb53(str) {
|
|
395
|
+
const a = 2654435761;
|
|
396
|
+
const b = 1597334677;
|
|
397
|
+
const c = 2246822507;
|
|
398
|
+
const d = 3266489909;
|
|
399
|
+
const e = 4294967296;
|
|
400
|
+
const f = 2097151;
|
|
401
|
+
const seed = 0;
|
|
402
|
+
let h1 = 0xdeadbeef ^ seed;
|
|
403
|
+
let h2 = 0x41c6ce57 ^ seed;
|
|
404
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
405
|
+
ch = str.charCodeAt(i);
|
|
406
|
+
h1 = Math.imul(h1 ^ ch, a);
|
|
407
|
+
h2 = Math.imul(h2 ^ ch, b);
|
|
387
408
|
}
|
|
388
|
-
|
|
409
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), c) ^ Math.imul(h2 ^ (h2 >>> 13), d);
|
|
410
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), c) ^ Math.imul(h1 ^ (h1 >>> 13), d);
|
|
411
|
+
return e * (f & h2) + (h1 >>> 0);
|
|
389
412
|
}
|
|
390
|
-
|
|
391
|
-
var State;
|
|
392
|
-
(function (State) {
|
|
393
|
-
State[State["INITIAL"] = 1] = "INITIAL";
|
|
394
|
-
State[State["DOCTYPE"] = 2] = "DOCTYPE";
|
|
395
|
-
State[State["TEXT"] = 3] = "TEXT";
|
|
396
|
-
State[State["TAG"] = 4] = "TAG";
|
|
397
|
-
State[State["ATTR"] = 5] = "ATTR";
|
|
398
|
-
State[State["CDATA"] = 6] = "CDATA";
|
|
399
|
-
State[State["SCRIPT"] = 7] = "SCRIPT";
|
|
400
|
-
State[State["STYLE"] = 8] = "STYLE";
|
|
401
|
-
})(State || (State = {}));
|
|
402
|
-
|
|
403
|
-
var ContentModel;
|
|
404
|
-
(function (ContentModel) {
|
|
405
|
-
ContentModel[ContentModel["TEXT"] = 1] = "TEXT";
|
|
406
|
-
ContentModel[ContentModel["SCRIPT"] = 2] = "SCRIPT";
|
|
407
|
-
ContentModel[ContentModel["STYLE"] = 3] = "STYLE";
|
|
408
|
-
})(ContentModel || (ContentModel = {}));
|
|
409
|
-
class Context {
|
|
410
|
-
constructor(source) {
|
|
411
|
-
var _a, _b, _c, _d;
|
|
412
|
-
this.state = State.INITIAL;
|
|
413
|
-
this.string = source.data;
|
|
414
|
-
this.filename = (_a = source.filename) !== null && _a !== void 0 ? _a : "";
|
|
415
|
-
this.offset = (_b = source.offset) !== null && _b !== void 0 ? _b : 0;
|
|
416
|
-
this.line = (_c = source.line) !== null && _c !== void 0 ? _c : 1;
|
|
417
|
-
this.column = (_d = source.column) !== null && _d !== void 0 ? _d : 1;
|
|
418
|
-
this.contentModel = ContentModel.TEXT;
|
|
419
|
-
}
|
|
420
|
-
getTruncatedLine(n = 13) {
|
|
421
|
-
return JSON.stringify(this.string.length > n ? `${this.string.slice(0, 10)}...` : this.string);
|
|
422
|
-
}
|
|
423
|
-
consume(n, state) {
|
|
424
|
-
/* if "n" is an regex match the first value is the full matched
|
|
425
|
-
* string so consume that many characters. */
|
|
426
|
-
if (typeof n !== "number") {
|
|
427
|
-
n = n[0].length; /* regex match */
|
|
428
|
-
}
|
|
429
|
-
/* poor mans line counter :( */
|
|
430
|
-
let consumed = this.string.slice(0, n);
|
|
431
|
-
let offset;
|
|
432
|
-
while ((offset = consumed.indexOf("\n")) >= 0) {
|
|
433
|
-
this.line++;
|
|
434
|
-
this.column = 1;
|
|
435
|
-
consumed = consumed.substr(offset + 1);
|
|
436
|
-
}
|
|
437
|
-
this.column += consumed.length;
|
|
438
|
-
this.offset += n;
|
|
439
|
-
/* remove N chars */
|
|
440
|
-
this.string = this.string.substr(n);
|
|
441
|
-
/* change state */
|
|
442
|
-
this.state = state;
|
|
443
|
-
}
|
|
444
|
-
getLocation(size) {
|
|
445
|
-
return {
|
|
446
|
-
filename: this.filename,
|
|
447
|
-
offset: this.offset,
|
|
448
|
-
line: this.line,
|
|
449
|
-
column: this.column,
|
|
450
|
-
size,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* @public
|
|
457
|
-
*/
|
|
458
|
-
exports.TextContent = void 0;
|
|
459
|
-
(function (TextContent) {
|
|
460
|
-
/* forbid node to have text content, inter-element whitespace is ignored */
|
|
461
|
-
TextContent["NONE"] = "none";
|
|
462
|
-
/* node can have text but not required too */
|
|
463
|
-
TextContent["DEFAULT"] = "default";
|
|
464
|
-
/* node requires text-nodes to be present (direct or by descendant) */
|
|
465
|
-
TextContent["REQUIRED"] = "required";
|
|
466
|
-
/* node requires accessible text (hidden text is ignored, tries to get text from accessibility tree) */
|
|
467
|
-
TextContent["ACCESSIBLE"] = "accessible";
|
|
468
|
-
})(exports.TextContent || (exports.TextContent = {}));
|
|
469
|
-
/**
|
|
470
|
-
* Properties listed here can be copied (loaded) onto another element using
|
|
471
|
-
* [[HtmlElement.loadMeta]].
|
|
472
|
-
*
|
|
473
|
-
* @public
|
|
474
|
-
*/
|
|
475
|
-
const MetaCopyableProperty = [
|
|
476
|
-
"metadata",
|
|
477
|
-
"flow",
|
|
478
|
-
"sectioning",
|
|
479
|
-
"heading",
|
|
480
|
-
"phrasing",
|
|
481
|
-
"embedded",
|
|
482
|
-
"interactive",
|
|
483
|
-
"transparent",
|
|
484
|
-
"form",
|
|
485
|
-
"formAssociated",
|
|
486
|
-
"labelable",
|
|
487
|
-
"attributes",
|
|
488
|
-
"permittedContent",
|
|
489
|
-
"permittedDescendants",
|
|
490
|
-
"permittedOrder",
|
|
491
|
-
"permittedParent",
|
|
492
|
-
"requiredAncestors",
|
|
493
|
-
"requiredContent",
|
|
494
|
-
];
|
|
495
|
-
/**
|
|
496
|
-
* @internal
|
|
497
|
-
*/
|
|
498
|
-
function setMetaProperty(dst, key, value) {
|
|
499
|
-
dst[key] = value;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* @public
|
|
504
|
-
*/
|
|
505
|
-
exports.NodeType = void 0;
|
|
506
|
-
(function (NodeType) {
|
|
507
|
-
NodeType[NodeType["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
|
|
508
|
-
NodeType[NodeType["TEXT_NODE"] = 3] = "TEXT_NODE";
|
|
509
|
-
NodeType[NodeType["DOCUMENT_NODE"] = 9] = "DOCUMENT_NODE";
|
|
510
|
-
})(exports.NodeType || (exports.NodeType = {}));
|
|
511
|
-
|
|
512
|
-
const DOCUMENT_NODE_NAME = "#document";
|
|
513
|
-
const TEXT_CONTENT = Symbol("textContent");
|
|
514
|
-
let counter = 0;
|
|
515
|
-
/**
|
|
516
|
-
* @public
|
|
517
|
-
*/
|
|
518
|
-
class DOMNode {
|
|
519
|
-
/**
|
|
520
|
-
* Create a new DOMNode.
|
|
521
|
-
*
|
|
522
|
-
* @param nodeType - What node type to create.
|
|
523
|
-
* @param nodeName - What node name to use. For `HtmlElement` this corresponds
|
|
524
|
-
* to the tagName but other node types have specific predefined values.
|
|
525
|
-
* @param location - Source code location of this node.
|
|
526
|
-
*/
|
|
527
|
-
constructor(nodeType, nodeName, location) {
|
|
528
|
-
this.nodeType = nodeType;
|
|
529
|
-
this.nodeName = nodeName !== null && nodeName !== void 0 ? nodeName : DOCUMENT_NODE_NAME;
|
|
530
|
-
this.location = location;
|
|
531
|
-
this.disabledRules = new Set();
|
|
532
|
-
this.blockedRules = new Map();
|
|
533
|
-
this.childNodes = [];
|
|
534
|
-
this.unique = counter++;
|
|
535
|
-
this.cache = null;
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Enable cache for this node.
|
|
539
|
-
*
|
|
540
|
-
* Should not be called before the node and all children are fully constructed.
|
|
541
|
-
*
|
|
542
|
-
* @internal
|
|
543
|
-
*/
|
|
544
|
-
cacheEnable() {
|
|
545
|
-
this.cache = new Map();
|
|
546
|
-
}
|
|
547
|
-
cacheGet(key) {
|
|
548
|
-
if (this.cache) {
|
|
549
|
-
return this.cache.get(key);
|
|
550
|
-
}
|
|
551
|
-
else {
|
|
552
|
-
return undefined;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
cacheSet(key, value) {
|
|
556
|
-
if (this.cache) {
|
|
557
|
-
this.cache.set(key, value);
|
|
558
|
-
}
|
|
559
|
-
return value;
|
|
560
|
-
}
|
|
561
|
-
cacheRemove(key) {
|
|
562
|
-
if (this.cache) {
|
|
563
|
-
return this.cache.delete(key);
|
|
564
|
-
}
|
|
565
|
-
else {
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
cacheExists(key) {
|
|
570
|
-
return Boolean(this.cache && this.cache.has(key));
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* Get the text (recursive) from all child nodes.
|
|
574
|
-
*/
|
|
575
|
-
get textContent() {
|
|
576
|
-
const cached = this.cacheGet(TEXT_CONTENT);
|
|
577
|
-
if (cached) {
|
|
578
|
-
return cached;
|
|
579
|
-
}
|
|
580
|
-
const text = this.childNodes.map((node) => node.textContent).join("");
|
|
581
|
-
this.cacheSet(TEXT_CONTENT, text);
|
|
582
|
-
return text;
|
|
583
|
-
}
|
|
584
|
-
append(node) {
|
|
585
|
-
this.childNodes.push(node);
|
|
586
|
-
}
|
|
587
|
-
isRootElement() {
|
|
588
|
-
return this.nodeType === exports.NodeType.DOCUMENT_NODE;
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Tests if two nodes are the same (references the same object).
|
|
592
|
-
*
|
|
593
|
-
* @since v4.11.0
|
|
594
|
-
*/
|
|
595
|
-
isSameNode(otherNode) {
|
|
596
|
-
return this.unique === otherNode.unique;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Returns a DOMNode representing the first direct child node or `null` if the
|
|
600
|
-
* node has no children.
|
|
601
|
-
*/
|
|
602
|
-
get firstChild() {
|
|
603
|
-
return this.childNodes[0] || null;
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Returns a DOMNode representing the last direct child node or `null` if the
|
|
607
|
-
* node has no children.
|
|
608
|
-
*/
|
|
609
|
-
get lastChild() {
|
|
610
|
-
return this.childNodes[this.childNodes.length - 1] || null;
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Block a rule for this node.
|
|
614
|
-
*
|
|
615
|
-
* @internal
|
|
616
|
-
*/
|
|
617
|
-
blockRule(ruleId, blocker) {
|
|
618
|
-
const current = this.blockedRules.get(ruleId);
|
|
619
|
-
if (current) {
|
|
620
|
-
current.push(blocker);
|
|
621
|
-
}
|
|
622
|
-
else {
|
|
623
|
-
this.blockedRules.set(ruleId, [blocker]);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Blocks multiple rules.
|
|
628
|
-
*
|
|
629
|
-
* @internal
|
|
630
|
-
*/
|
|
631
|
-
blockRules(rules, blocker) {
|
|
632
|
-
for (const rule of rules) {
|
|
633
|
-
this.blockRule(rule, blocker);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
/**
|
|
637
|
-
* Disable a rule for this node.
|
|
638
|
-
*
|
|
639
|
-
* @internal
|
|
640
|
-
*/
|
|
641
|
-
disableRule(ruleId) {
|
|
642
|
-
this.disabledRules.add(ruleId);
|
|
643
|
-
}
|
|
644
|
-
/**
|
|
645
|
-
* Disables multiple rules.
|
|
646
|
-
*
|
|
647
|
-
* @internal
|
|
648
|
-
*/
|
|
649
|
-
disableRules(rules) {
|
|
650
|
-
for (const rule of rules) {
|
|
651
|
-
this.disableRule(rule);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Enable a previously disabled rule for this node.
|
|
656
|
-
*/
|
|
657
|
-
enableRule(ruleId) {
|
|
658
|
-
this.disabledRules.delete(ruleId);
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Enables multiple rules.
|
|
662
|
-
*/
|
|
663
|
-
enableRules(rules) {
|
|
664
|
-
for (const rule of rules) {
|
|
665
|
-
this.enableRule(rule);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Test if a rule is enabled for this node.
|
|
670
|
-
*
|
|
671
|
-
* @internal
|
|
672
|
-
*/
|
|
673
|
-
ruleEnabled(ruleId) {
|
|
674
|
-
return !this.disabledRules.has(ruleId);
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Test if a rule is blocked for this node.
|
|
678
|
-
*
|
|
679
|
-
* @internal
|
|
680
|
-
*/
|
|
681
|
-
ruleBlockers(ruleId) {
|
|
682
|
-
var _a;
|
|
683
|
-
return (_a = this.blockedRules.get(ruleId)) !== null && _a !== void 0 ? _a : [];
|
|
684
|
-
}
|
|
685
|
-
generateSelector() {
|
|
686
|
-
return null;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function parse(text, baseLocation) {
|
|
691
|
-
const tokens = [];
|
|
692
|
-
const locations = baseLocation ? [] : null;
|
|
693
|
-
for (let begin = 0; begin < text.length;) {
|
|
694
|
-
let end = text.indexOf(" ", begin);
|
|
695
|
-
/* if the last space was found move the position to the last character
|
|
696
|
-
* in the string */
|
|
697
|
-
if (end === -1) {
|
|
698
|
-
end = text.length;
|
|
699
|
-
}
|
|
700
|
-
/* handle multiple spaces */
|
|
701
|
-
const size = end - begin;
|
|
702
|
-
if (size === 0) {
|
|
703
|
-
begin++;
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
/* extract token */
|
|
707
|
-
const token = text.substring(begin, end);
|
|
708
|
-
tokens.push(token);
|
|
709
|
-
/* extract location */
|
|
710
|
-
if (locations && baseLocation) {
|
|
711
|
-
const location = sliceLocation(baseLocation, begin, end);
|
|
712
|
-
locations.push(location);
|
|
713
|
-
}
|
|
714
|
-
/* advance position to the character after the current end position */
|
|
715
|
-
begin += size + 1;
|
|
716
|
-
}
|
|
717
|
-
return { tokens, locations };
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* @public
|
|
721
|
-
*/
|
|
722
|
-
class DOMTokenList extends Array {
|
|
723
|
-
constructor(value, location) {
|
|
724
|
-
if (value && typeof value === "string") {
|
|
725
|
-
/* replace all whitespace with a single space for easier parsing */
|
|
726
|
-
const normalized = value.replace(/[\t\r\n]/g, " ");
|
|
727
|
-
const { tokens, locations } = parse(normalized, location);
|
|
728
|
-
super(...tokens);
|
|
729
|
-
this.locations = locations;
|
|
730
|
-
}
|
|
731
|
-
else {
|
|
732
|
-
super(0);
|
|
733
|
-
this.locations = null;
|
|
734
|
-
}
|
|
735
|
-
if (value instanceof DynamicValue) {
|
|
736
|
-
this.value = value.expr;
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
this.value = value || "";
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
item(n) {
|
|
743
|
-
return this[n];
|
|
744
|
-
}
|
|
745
|
-
location(n) {
|
|
746
|
-
if (this.locations) {
|
|
747
|
-
return this.locations[n];
|
|
748
|
-
}
|
|
749
|
-
else {
|
|
750
|
-
throw new Error("Trying to access DOMTokenList location when base location isn't set");
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
contains(token) {
|
|
754
|
-
return this.includes(token);
|
|
755
|
-
}
|
|
756
|
-
*iterator() {
|
|
757
|
-
for (let index = 0; index < this.length; index++) {
|
|
758
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion -- as we loop over length this should always be set */
|
|
759
|
-
const item = this.item(index);
|
|
760
|
-
const location = this.location(index);
|
|
761
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
762
|
-
yield { index, item, location };
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
var Combinator;
|
|
768
|
-
(function (Combinator) {
|
|
769
|
-
Combinator[Combinator["DESCENDANT"] = 1] = "DESCENDANT";
|
|
770
|
-
Combinator[Combinator["CHILD"] = 2] = "CHILD";
|
|
771
|
-
Combinator[Combinator["ADJACENT_SIBLING"] = 3] = "ADJACENT_SIBLING";
|
|
772
|
-
Combinator[Combinator["GENERAL_SIBLING"] = 4] = "GENERAL_SIBLING";
|
|
773
|
-
/* special cases */
|
|
774
|
-
Combinator[Combinator["SCOPE"] = 5] = "SCOPE";
|
|
775
|
-
})(Combinator || (Combinator = {}));
|
|
776
|
-
function parseCombinator(combinator, pattern) {
|
|
777
|
-
/* special case, when pattern is :scope [[Selector]] will handle this
|
|
778
|
-
* "combinator" to match itself instead of descendants */
|
|
779
|
-
if (pattern === ":scope") {
|
|
780
|
-
return Combinator.SCOPE;
|
|
781
|
-
}
|
|
782
|
-
switch (combinator) {
|
|
783
|
-
case undefined:
|
|
784
|
-
case null:
|
|
785
|
-
case "":
|
|
786
|
-
return Combinator.DESCENDANT;
|
|
787
|
-
case ">":
|
|
788
|
-
return Combinator.CHILD;
|
|
789
|
-
case "+":
|
|
790
|
-
return Combinator.ADJACENT_SIBLING;
|
|
791
|
-
case "~":
|
|
792
|
-
return Combinator.GENERAL_SIBLING;
|
|
793
|
-
default:
|
|
794
|
-
throw new Error(`Unknown combinator "${combinator}"`);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function firstChild(node) {
|
|
799
|
-
return node.previousSibling === null;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function lastChild(node) {
|
|
803
|
-
return node.nextSibling === null;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const cache = {};
|
|
807
|
-
function getNthChild(node) {
|
|
808
|
-
if (!node.parent) {
|
|
809
|
-
return -1;
|
|
810
|
-
}
|
|
811
|
-
if (!cache[node.unique]) {
|
|
812
|
-
const parent = node.parent;
|
|
813
|
-
const index = parent.childElements.findIndex((cur) => {
|
|
814
|
-
return cur.unique === node.unique;
|
|
815
|
-
});
|
|
816
|
-
cache[node.unique] = index + 1; /* nthChild starts at 1 */
|
|
817
|
-
}
|
|
818
|
-
return cache[node.unique];
|
|
819
|
-
}
|
|
820
|
-
function nthChild(node, args) {
|
|
821
|
-
if (!args) {
|
|
822
|
-
throw new Error("Missing argument to nth-child");
|
|
823
|
-
}
|
|
824
|
-
const n = parseInt(args.trim(), 10);
|
|
825
|
-
const cur = getNthChild(node);
|
|
826
|
-
return cur === n;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function scope(node) {
|
|
830
|
-
return node.isSameNode(this.scope);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const table = {
|
|
834
|
-
"first-child": firstChild,
|
|
835
|
-
"last-child": lastChild,
|
|
836
|
-
"nth-child": nthChild,
|
|
837
|
-
scope: scope,
|
|
838
|
-
};
|
|
839
|
-
function factory(name, context) {
|
|
840
|
-
const fn = table[name];
|
|
841
|
-
if (fn) {
|
|
842
|
-
return fn.bind(context);
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
throw new Error(`Pseudo-class "${name}" is not implemented`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Homage to PHP: unescapes slashes.
|
|
851
|
-
*
|
|
852
|
-
* E.g. "foo\:bar" becomes "foo:bar"
|
|
853
|
-
*/
|
|
854
|
-
function stripslashes(value) {
|
|
855
|
-
return value.replace(/\\(.)/g, "$1");
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* @internal
|
|
859
|
-
*/
|
|
860
|
-
function escapeSelectorComponent(text) {
|
|
861
|
-
return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* @internal
|
|
865
|
-
*/
|
|
866
|
-
function generateIdSelector(id) {
|
|
867
|
-
const escaped = escapeSelectorComponent(id);
|
|
868
|
-
return escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Returns true if the character is a delimiter for different kinds of selectors:
|
|
872
|
-
*
|
|
873
|
-
* - `.` - begins a class selector
|
|
874
|
-
* - `#` - begins an id selector
|
|
875
|
-
* - `[` - begins an attribute selector
|
|
876
|
-
* - `:` - begins a pseudo class or element selector
|
|
877
|
-
*/
|
|
878
|
-
function isDelimiter(ch) {
|
|
879
|
-
return /[.#[:]/.test(ch);
|
|
880
|
-
}
|
|
881
|
-
/**
|
|
882
|
-
* Returns true if the character is a quotation mark.
|
|
883
|
-
*/
|
|
884
|
-
function isQuotationMark(ch) {
|
|
885
|
-
return /['"]/.test(ch);
|
|
886
|
-
}
|
|
887
|
-
function isPseudoElement(ch, buffer) {
|
|
888
|
-
return ch === ":" && buffer === ":";
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* @internal
|
|
892
|
-
*/
|
|
893
|
-
function* splitPattern(pattern) {
|
|
894
|
-
if (pattern === "") {
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
const end = pattern.length;
|
|
898
|
-
let begin = 0;
|
|
899
|
-
let cur = 1;
|
|
900
|
-
let quoted = false;
|
|
901
|
-
while (cur < end) {
|
|
902
|
-
const ch = pattern[cur];
|
|
903
|
-
const buffer = pattern.slice(begin, cur);
|
|
904
|
-
/* escaped character, ignore whatever is next */
|
|
905
|
-
if (ch === "\\") {
|
|
906
|
-
cur += 2;
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
/* if inside quoted string we only look for the end quotation mark */
|
|
910
|
-
if (quoted) {
|
|
911
|
-
if (ch === quoted) {
|
|
912
|
-
quoted = false;
|
|
913
|
-
}
|
|
914
|
-
cur += 1;
|
|
915
|
-
continue;
|
|
916
|
-
}
|
|
917
|
-
/* if the character is a quotation mark we store the character and the above
|
|
918
|
-
* condition will look for a similar end quotation mark */
|
|
919
|
-
if (isQuotationMark(ch)) {
|
|
920
|
-
quoted = ch;
|
|
921
|
-
cur += 1;
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
/* special case when using :: pseudo element selector */
|
|
925
|
-
if (isPseudoElement(ch, buffer)) {
|
|
926
|
-
cur += 1;
|
|
927
|
-
continue;
|
|
928
|
-
}
|
|
929
|
-
/* if the character is a delimiter we yield the string and reset the
|
|
930
|
-
* position */
|
|
931
|
-
if (isDelimiter(ch)) {
|
|
932
|
-
begin = cur;
|
|
933
|
-
yield buffer;
|
|
934
|
-
}
|
|
935
|
-
cur += 1;
|
|
936
|
-
}
|
|
937
|
-
/* yield the rest of the string */
|
|
938
|
-
const tail = pattern.slice(begin, cur);
|
|
939
|
-
yield tail;
|
|
940
|
-
}
|
|
941
|
-
class Matcher {
|
|
942
|
-
}
|
|
943
|
-
class ClassMatcher extends Matcher {
|
|
944
|
-
constructor(classname) {
|
|
945
|
-
super();
|
|
946
|
-
this.classname = classname;
|
|
947
|
-
}
|
|
948
|
-
match(node) {
|
|
949
|
-
return node.classList.contains(this.classname);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
class IdMatcher extends Matcher {
|
|
953
|
-
constructor(id) {
|
|
954
|
-
super();
|
|
955
|
-
this.id = stripslashes(id);
|
|
956
|
-
}
|
|
957
|
-
match(node) {
|
|
958
|
-
return node.id === this.id;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
class AttrMatcher extends Matcher {
|
|
962
|
-
constructor(attr) {
|
|
963
|
-
super();
|
|
964
|
-
const [, key, op, value] = attr.match(/^(.+?)(?:([~^$*|]?=)"([^"]+?)")?$/);
|
|
965
|
-
this.key = key;
|
|
966
|
-
this.op = op;
|
|
967
|
-
this.value = value;
|
|
968
|
-
}
|
|
969
|
-
match(node) {
|
|
970
|
-
const attr = node.getAttribute(this.key, true) || [];
|
|
971
|
-
return attr.some((cur) => {
|
|
972
|
-
switch (this.op) {
|
|
973
|
-
case undefined:
|
|
974
|
-
return true; /* attribute exists */
|
|
975
|
-
case "=":
|
|
976
|
-
return cur.value === this.value;
|
|
977
|
-
default:
|
|
978
|
-
throw new Error(`Attribute selector operator ${this.op} is not implemented yet`);
|
|
979
|
-
}
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
class PseudoClassMatcher extends Matcher {
|
|
984
|
-
constructor(pseudoclass, context) {
|
|
985
|
-
super();
|
|
986
|
-
const match = pseudoclass.match(/^([^(]+)(?:\((.*)\))?$/);
|
|
987
|
-
if (!match) {
|
|
988
|
-
throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
|
|
989
|
-
}
|
|
990
|
-
const [, name, args] = match;
|
|
991
|
-
this.name = name;
|
|
992
|
-
this.args = args;
|
|
993
|
-
}
|
|
994
|
-
match(node, context) {
|
|
995
|
-
const fn = factory(this.name, context);
|
|
996
|
-
return fn(node, this.args);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
class Pattern {
|
|
1000
|
-
constructor(pattern) {
|
|
1001
|
-
const match = pattern.match(/^([~+\->]?)((?:[*]|[^.#[:]+)?)(.*)$/);
|
|
1002
|
-
match.shift(); /* remove full matched string */
|
|
1003
|
-
this.selector = pattern;
|
|
1004
|
-
this.combinator = parseCombinator(match.shift(), pattern);
|
|
1005
|
-
this.tagName = match.shift() || "*";
|
|
1006
|
-
this.pattern = Array.from(splitPattern(match[0]), (it) => this.createMatcher(it));
|
|
1007
|
-
}
|
|
1008
|
-
match(node, context) {
|
|
1009
|
-
return node.is(this.tagName) && this.pattern.every((cur) => cur.match(node, context));
|
|
1010
|
-
}
|
|
1011
|
-
createMatcher(pattern) {
|
|
1012
|
-
switch (pattern[0]) {
|
|
1013
|
-
case ".":
|
|
1014
|
-
return new ClassMatcher(pattern.slice(1));
|
|
1015
|
-
case "#":
|
|
1016
|
-
return new IdMatcher(pattern.slice(1));
|
|
1017
|
-
case "[":
|
|
1018
|
-
return new AttrMatcher(pattern.slice(1, -1));
|
|
1019
|
-
case ":":
|
|
1020
|
-
return new PseudoClassMatcher(pattern.slice(1), this.selector);
|
|
1021
|
-
default:
|
|
1022
|
-
/* istanbul ignore next: fallback solution, the switch cases should cover
|
|
1023
|
-
* everything and there is no known way to trigger this fallback */
|
|
1024
|
-
throw new Error(`Failed to create matcher for "${pattern}"`);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* DOM Selector.
|
|
1030
|
-
*/
|
|
1031
|
-
class Selector {
|
|
1032
|
-
constructor(selector) {
|
|
1033
|
-
this.pattern = Selector.parse(selector);
|
|
1034
|
-
}
|
|
1035
|
-
/**
|
|
1036
|
-
* Match this selector against a HtmlElement.
|
|
1037
|
-
*
|
|
1038
|
-
* @param root - Element to match against.
|
|
1039
|
-
* @returns Iterator with matched elements.
|
|
1040
|
-
*/
|
|
1041
|
-
*match(root) {
|
|
1042
|
-
const context = { scope: root };
|
|
1043
|
-
yield* this.matchInternal(root, 0, context);
|
|
1044
|
-
}
|
|
1045
|
-
*matchInternal(root, level, context) {
|
|
1046
|
-
if (level >= this.pattern.length) {
|
|
1047
|
-
yield root;
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
const pattern = this.pattern[level];
|
|
1051
|
-
const matches = Selector.findCandidates(root, pattern);
|
|
1052
|
-
for (const node of matches) {
|
|
1053
|
-
if (!pattern.match(node, context)) {
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
yield* this.matchInternal(node, level + 1, context);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
static parse(selector) {
|
|
1060
|
-
/* strip whitespace before combinators, "ul > li" becomes "ul >li", for
|
|
1061
|
-
* easier parsing */
|
|
1062
|
-
selector = selector.replace(/([+~>]) /g, "$1");
|
|
1063
|
-
const pattern = selector.split(/(?:(?<!\\) )+/);
|
|
1064
|
-
return pattern.map((part) => new Pattern(part));
|
|
1065
|
-
}
|
|
1066
|
-
static findCandidates(root, pattern) {
|
|
1067
|
-
switch (pattern.combinator) {
|
|
1068
|
-
case Combinator.DESCENDANT:
|
|
1069
|
-
return root.getElementsByTagName(pattern.tagName);
|
|
1070
|
-
case Combinator.CHILD:
|
|
1071
|
-
return root.childElements.filter((node) => node.is(pattern.tagName));
|
|
1072
|
-
case Combinator.ADJACENT_SIBLING:
|
|
1073
|
-
return Selector.findAdjacentSibling(root);
|
|
1074
|
-
case Combinator.GENERAL_SIBLING:
|
|
1075
|
-
return Selector.findGeneralSibling(root);
|
|
1076
|
-
case Combinator.SCOPE:
|
|
1077
|
-
return [root];
|
|
1078
|
-
}
|
|
1079
|
-
/* istanbul ignore next: fallback solution, the switch cases should cover
|
|
1080
|
-
* everything and there is no known way to trigger this fallback */
|
|
1081
|
-
return [];
|
|
1082
|
-
}
|
|
1083
|
-
static findAdjacentSibling(node) {
|
|
1084
|
-
let adjacent = false;
|
|
1085
|
-
return node.siblings.filter((cur) => {
|
|
1086
|
-
if (adjacent) {
|
|
1087
|
-
adjacent = false;
|
|
1088
|
-
return true;
|
|
1089
|
-
}
|
|
1090
|
-
if (cur === node) {
|
|
1091
|
-
adjacent = true;
|
|
1092
|
-
}
|
|
1093
|
-
return false;
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
static findGeneralSibling(node) {
|
|
1097
|
-
let after = false;
|
|
1098
|
-
return node.siblings.filter((cur) => {
|
|
1099
|
-
if (after) {
|
|
1100
|
-
return true;
|
|
1101
|
-
}
|
|
1102
|
-
if (cur === node) {
|
|
1103
|
-
after = true;
|
|
1104
|
-
}
|
|
1105
|
-
return false;
|
|
1106
|
-
});
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
const TEXT_NODE_NAME = "#text";
|
|
1111
|
-
/**
|
|
1112
|
-
* Returns true if the node is a text node.
|
|
1113
|
-
*
|
|
1114
|
-
* @public
|
|
1115
|
-
*/
|
|
1116
|
-
function isTextNode(node) {
|
|
1117
|
-
return Boolean(node && node.nodeType === exports.NodeType.TEXT_NODE);
|
|
1118
|
-
}
|
|
1119
|
-
/**
|
|
1120
|
-
* Represents a text in the HTML document.
|
|
1121
|
-
*
|
|
1122
|
-
* Text nodes are appended as children of `HtmlElement` and cannot have childen
|
|
1123
|
-
* of its own.
|
|
1124
|
-
*
|
|
1125
|
-
* @public
|
|
1126
|
-
*/
|
|
1127
|
-
class TextNode extends DOMNode {
|
|
1128
|
-
/**
|
|
1129
|
-
* @param text - Text to add. When a `DynamicValue` is used the expression is
|
|
1130
|
-
* used as "text".
|
|
1131
|
-
* @param location - Source code location of this node.
|
|
1132
|
-
*/
|
|
1133
|
-
constructor(text, location) {
|
|
1134
|
-
super(exports.NodeType.TEXT_NODE, TEXT_NODE_NAME, location);
|
|
1135
|
-
this.text = text;
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Get the text from node.
|
|
1139
|
-
*/
|
|
1140
|
-
get textContent() {
|
|
1141
|
-
return this.text.toString();
|
|
1142
|
-
}
|
|
1143
|
-
/**
|
|
1144
|
-
* Flag set to true if the attribute value is static.
|
|
1145
|
-
*/
|
|
1146
|
-
get isStatic() {
|
|
1147
|
-
return !this.isDynamic;
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* Flag set to true if the attribute value is dynamic.
|
|
1151
|
-
*/
|
|
1152
|
-
get isDynamic() {
|
|
1153
|
-
return this.text instanceof DynamicValue;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
/**
|
|
1158
|
-
* @public
|
|
1159
|
-
*/
|
|
1160
|
-
exports.NodeClosed = void 0;
|
|
1161
|
-
(function (NodeClosed) {
|
|
1162
|
-
NodeClosed[NodeClosed["Open"] = 0] = "Open";
|
|
1163
|
-
NodeClosed[NodeClosed["EndTag"] = 1] = "EndTag";
|
|
1164
|
-
NodeClosed[NodeClosed["VoidOmitted"] = 2] = "VoidOmitted";
|
|
1165
|
-
NodeClosed[NodeClosed["VoidSelfClosed"] = 3] = "VoidSelfClosed";
|
|
1166
|
-
NodeClosed[NodeClosed["ImplicitClosed"] = 4] = "ImplicitClosed";
|
|
1167
|
-
})(exports.NodeClosed || (exports.NodeClosed = {}));
|
|
1168
|
-
/**
|
|
1169
|
-
* Returns true if the node is an element node.
|
|
1170
|
-
*
|
|
1171
|
-
* @public
|
|
1172
|
-
*/
|
|
1173
|
-
function isElementNode(node) {
|
|
1174
|
-
return Boolean(node && node.nodeType === exports.NodeType.ELEMENT_NODE);
|
|
1175
|
-
}
|
|
1176
|
-
function isValidTagName(tagName) {
|
|
1177
|
-
return Boolean(tagName !== "" && tagName !== "*");
|
|
1178
|
-
}
|
|
1179
|
-
/**
|
|
1180
|
-
* @public
|
|
1181
|
-
*/
|
|
1182
|
-
class HtmlElement extends DOMNode {
|
|
1183
|
-
constructor(tagName, parent, closed, meta, location) {
|
|
1184
|
-
const nodeType = tagName ? exports.NodeType.ELEMENT_NODE : exports.NodeType.DOCUMENT_NODE;
|
|
1185
|
-
super(nodeType, tagName, location);
|
|
1186
|
-
if (!isValidTagName(tagName)) {
|
|
1187
|
-
throw new Error(`The tag name provided ('${tagName || ""}') is not a valid name`);
|
|
1188
|
-
}
|
|
1189
|
-
this.tagName = tagName || "#document";
|
|
1190
|
-
this.parent = parent !== null && parent !== void 0 ? parent : null;
|
|
1191
|
-
this.attr = {};
|
|
1192
|
-
this.metaElement = meta !== null && meta !== void 0 ? meta : null;
|
|
1193
|
-
this.closed = closed;
|
|
1194
|
-
this.voidElement = meta ? Boolean(meta.void) : false;
|
|
1195
|
-
this.depth = 0;
|
|
1196
|
-
this.annotation = null;
|
|
1197
|
-
if (parent) {
|
|
1198
|
-
parent.childNodes.push(this);
|
|
1199
|
-
/* calculate depth in domtree */
|
|
1200
|
-
let cur = parent;
|
|
1201
|
-
while (cur.parent) {
|
|
1202
|
-
this.depth++;
|
|
1203
|
-
cur = cur.parent;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
/**
|
|
1208
|
-
* @internal
|
|
1209
|
-
*/
|
|
1210
|
-
static rootNode(location) {
|
|
1211
|
-
const root = new HtmlElement(undefined, null, exports.NodeClosed.EndTag, null, location);
|
|
1212
|
-
root.setAnnotation("#document");
|
|
1213
|
-
return root;
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* @internal
|
|
1217
|
-
*
|
|
1218
|
-
* @param namespace - If given it is appended to the tagName.
|
|
1219
|
-
*/
|
|
1220
|
-
static fromTokens(startToken, endToken, parent, metaTable, namespace = "") {
|
|
1221
|
-
const name = startToken.data[2];
|
|
1222
|
-
const tagName = namespace ? `${namespace}:${name}` : name;
|
|
1223
|
-
if (!name) {
|
|
1224
|
-
throw new Error("tagName cannot be empty");
|
|
1225
|
-
}
|
|
1226
|
-
const meta = metaTable ? metaTable.getMetaFor(tagName) : null;
|
|
1227
|
-
const open = startToken.data[1] !== "/";
|
|
1228
|
-
const closed = isClosed(endToken, meta);
|
|
1229
|
-
/* location contains position of '<' so strip it out */
|
|
1230
|
-
const location = sliceLocation(startToken.location, 1);
|
|
1231
|
-
return new HtmlElement(tagName, open ? parent : null, closed, meta, location);
|
|
1232
|
-
}
|
|
1233
|
-
/**
|
|
1234
|
-
* Returns annotated name if set or defaults to `<tagName>`.
|
|
1235
|
-
*
|
|
1236
|
-
* E.g. `my-annotation` or `<div>`.
|
|
1237
|
-
*/
|
|
1238
|
-
get annotatedName() {
|
|
1239
|
-
if (this.annotation) {
|
|
1240
|
-
return this.annotation;
|
|
1241
|
-
}
|
|
1242
|
-
else {
|
|
1243
|
-
return `<${this.tagName}>`;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* Get list of IDs referenced by `aria-labelledby`.
|
|
1248
|
-
*
|
|
1249
|
-
* If the attribute is unset or empty this getter returns null.
|
|
1250
|
-
* If the attribute is dynamic the original {@link DynamicValue} is returned.
|
|
1251
|
-
*
|
|
1252
|
-
* @public
|
|
1253
|
-
*/
|
|
1254
|
-
get ariaLabelledby() {
|
|
1255
|
-
const attr = this.getAttribute("aria-labelledby");
|
|
1256
|
-
if (!attr || !attr.value) {
|
|
1257
|
-
return null;
|
|
1258
|
-
}
|
|
1259
|
-
if (attr.value instanceof DynamicValue) {
|
|
1260
|
-
return attr.value;
|
|
1261
|
-
}
|
|
1262
|
-
const list = new DOMTokenList(attr.value, attr.valueLocation);
|
|
1263
|
-
return list.length ? Array.from(list) : null;
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Similar to childNodes but only elements.
|
|
1267
|
-
*/
|
|
1268
|
-
get childElements() {
|
|
1269
|
-
return this.childNodes.filter(isElementNode);
|
|
1270
|
-
}
|
|
1271
|
-
/**
|
|
1272
|
-
* Find the first ancestor matching a selector.
|
|
1273
|
-
*
|
|
1274
|
-
* Implementation of DOM specification of Element.closest(selectors).
|
|
1275
|
-
*/
|
|
1276
|
-
closest(selectors) {
|
|
1277
|
-
/* eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive*/
|
|
1278
|
-
let node = this;
|
|
1279
|
-
while (node) {
|
|
1280
|
-
if (node.matches(selectors)) {
|
|
1281
|
-
return node;
|
|
1282
|
-
}
|
|
1283
|
-
node = node.parent;
|
|
1284
|
-
}
|
|
1285
|
-
return null;
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Generate a DOM selector for this element. The returned selector will be
|
|
1289
|
-
* unique inside the current document.
|
|
1290
|
-
*/
|
|
1291
|
-
generateSelector() {
|
|
1292
|
-
/* root element cannot have a selector as it isn't a proper element */
|
|
1293
|
-
if (this.isRootElement()) {
|
|
1294
|
-
return null;
|
|
1295
|
-
}
|
|
1296
|
-
const parts = [];
|
|
1297
|
-
let root;
|
|
1298
|
-
/* eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive */
|
|
1299
|
-
for (root = this; root.parent; root = root.parent) {
|
|
1300
|
-
/* .. */
|
|
1301
|
-
}
|
|
1302
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive
|
|
1303
|
-
for (let cur = this; cur.parent; cur = cur.parent) {
|
|
1304
|
-
/* if a unique id is present, use it and short-circuit */
|
|
1305
|
-
if (cur.id) {
|
|
1306
|
-
const selector = generateIdSelector(cur.id);
|
|
1307
|
-
const matches = root.querySelectorAll(selector);
|
|
1308
|
-
if (matches.length === 1) {
|
|
1309
|
-
parts.push(selector);
|
|
1310
|
-
break;
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
const parent = cur.parent;
|
|
1314
|
-
const child = parent.childElements;
|
|
1315
|
-
const index = child.findIndex((it) => it.unique === cur.unique);
|
|
1316
|
-
const numOfType = child.filter((it) => it.is(cur.tagName)).length;
|
|
1317
|
-
const solo = numOfType === 1;
|
|
1318
|
-
/* if this is the only tagName in this level of siblings nth-child isn't needed */
|
|
1319
|
-
if (solo) {
|
|
1320
|
-
parts.push(cur.tagName.toLowerCase());
|
|
1321
|
-
continue;
|
|
1322
|
-
}
|
|
1323
|
-
/* this will generate the worst kind of selector but at least it will be accurate (optimizations welcome) */
|
|
1324
|
-
parts.push(`${cur.tagName.toLowerCase()}:nth-child(${index + 1})`);
|
|
1325
|
-
}
|
|
1326
|
-
return parts.reverse().join(" > ");
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* Tests if this element has given tagname.
|
|
1330
|
-
*
|
|
1331
|
-
* If passing "*" this test will pass if any tagname is set.
|
|
1332
|
-
*/
|
|
1333
|
-
is(tagName) {
|
|
1334
|
-
return tagName === "*" || this.tagName.toLowerCase() === tagName.toLowerCase();
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Load new element metadata onto this element.
|
|
1338
|
-
*
|
|
1339
|
-
* Do note that semantics such as `void` cannot be changed (as the element has
|
|
1340
|
-
* already been created). In addition the element will still "be" the same
|
|
1341
|
-
* element, i.e. even if loading meta for a `<p>` tag upon a `<div>` tag it
|
|
1342
|
-
* will still be a `<div>` as far as the rest of the validator is concerned.
|
|
1343
|
-
*
|
|
1344
|
-
* In fact only certain properties will be copied onto the element:
|
|
1345
|
-
*
|
|
1346
|
-
* - content categories (flow, phrasing, etc)
|
|
1347
|
-
* - required attributes
|
|
1348
|
-
* - attribute allowed values
|
|
1349
|
-
* - permitted/required elements
|
|
1350
|
-
*
|
|
1351
|
-
* Properties *not* loaded:
|
|
1352
|
-
*
|
|
1353
|
-
* - inherit
|
|
1354
|
-
* - deprecated
|
|
1355
|
-
* - foreign
|
|
1356
|
-
* - void
|
|
1357
|
-
* - implicitClosed
|
|
1358
|
-
* - scriptSupporting
|
|
1359
|
-
* - deprecatedAttributes
|
|
1360
|
-
*
|
|
1361
|
-
* Changes to element metadata will only be visible after `element:ready` (and
|
|
1362
|
-
* the subsequent `dom:ready` event).
|
|
1363
|
-
*/
|
|
1364
|
-
loadMeta(meta) {
|
|
1365
|
-
if (!this.metaElement) {
|
|
1366
|
-
this.metaElement = {};
|
|
1367
|
-
}
|
|
1368
|
-
for (const key of MetaCopyableProperty) {
|
|
1369
|
-
const value = meta[key];
|
|
1370
|
-
if (typeof value !== "undefined") {
|
|
1371
|
-
setMetaProperty(this.metaElement, key, value);
|
|
1372
|
-
}
|
|
1373
|
-
else {
|
|
1374
|
-
delete this.metaElement[key];
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
/**
|
|
1379
|
-
* Match this element against given selectors. Returns true if any selector
|
|
1380
|
-
* matches.
|
|
1381
|
-
*
|
|
1382
|
-
* Implementation of DOM specification of Element.matches(selectors).
|
|
1383
|
-
*/
|
|
1384
|
-
matches(selector) {
|
|
1385
|
-
/* find root element */
|
|
1386
|
-
/* eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive */
|
|
1387
|
-
let root = this;
|
|
1388
|
-
while (root.parent) {
|
|
1389
|
-
root = root.parent;
|
|
1390
|
-
}
|
|
1391
|
-
/* a bit slow implementation as it finds all candidates for the selector and
|
|
1392
|
-
* then tests if any of them are the current element. A better
|
|
1393
|
-
* implementation would be to walk the selector right-to-left and test
|
|
1394
|
-
* ancestors. */
|
|
1395
|
-
for (const match of root.querySelectorAll(selector)) {
|
|
1396
|
-
if (match.unique === this.unique) {
|
|
1397
|
-
return true;
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
return false;
|
|
1401
|
-
}
|
|
1402
|
-
get meta() {
|
|
1403
|
-
return this.metaElement;
|
|
1404
|
-
}
|
|
1405
|
-
/**
|
|
1406
|
-
* Set annotation for this element.
|
|
1407
|
-
*/
|
|
1408
|
-
setAnnotation(text) {
|
|
1409
|
-
this.annotation = text;
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Set attribute. Stores all attributes set even with the same name.
|
|
1413
|
-
*
|
|
1414
|
-
* @param key - Attribute name
|
|
1415
|
-
* @param value - Attribute value. Use `null` if no value is present.
|
|
1416
|
-
* @param keyLocation - Location of the attribute name.
|
|
1417
|
-
* @param valueLocation - Location of the attribute value (excluding quotation)
|
|
1418
|
-
* @param originalAttribute - If attribute is an alias for another attribute
|
|
1419
|
-
* (dynamic attributes) set this to the original attribute name.
|
|
1420
|
-
*/
|
|
1421
|
-
setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
|
|
1422
|
-
key = key.toLowerCase();
|
|
1423
|
-
if (!this.attr[key]) {
|
|
1424
|
-
this.attr[key] = [];
|
|
1425
|
-
}
|
|
1426
|
-
this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
|
|
1427
|
-
}
|
|
1428
|
-
/**
|
|
1429
|
-
* Get a list of all attributes on this node.
|
|
1430
|
-
*/
|
|
1431
|
-
get attributes() {
|
|
1432
|
-
return Object.values(this.attr).reduce((result, cur) => {
|
|
1433
|
-
return result.concat(cur);
|
|
1434
|
-
}, []);
|
|
1435
|
-
}
|
|
1436
|
-
hasAttribute(key) {
|
|
1437
|
-
key = key.toLowerCase();
|
|
1438
|
-
return key in this.attr;
|
|
1439
|
-
}
|
|
1440
|
-
getAttribute(key, all = false) {
|
|
1441
|
-
key = key.toLowerCase();
|
|
1442
|
-
if (key in this.attr) {
|
|
1443
|
-
const matches = this.attr[key];
|
|
1444
|
-
return all ? matches : matches[0];
|
|
1445
|
-
}
|
|
1446
|
-
else {
|
|
1447
|
-
return null;
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
/**
|
|
1451
|
-
* Get attribute value.
|
|
1452
|
-
*
|
|
1453
|
-
* Returns the attribute value if present.
|
|
1454
|
-
*
|
|
1455
|
-
* - Missing attributes return `null`.
|
|
1456
|
-
* - Boolean attributes return `null`.
|
|
1457
|
-
* - `DynamicValue` returns attribute expression.
|
|
1458
|
-
*
|
|
1459
|
-
* @param key - Attribute name
|
|
1460
|
-
* @returns Attribute value or null.
|
|
1461
|
-
*/
|
|
1462
|
-
getAttributeValue(key) {
|
|
1463
|
-
const attr = this.getAttribute(key);
|
|
1464
|
-
if (attr) {
|
|
1465
|
-
return attr.value !== null ? attr.value.toString() : null;
|
|
1466
|
-
}
|
|
1467
|
-
else {
|
|
1468
|
-
return null;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
|
-
* Add text as a child node to this element.
|
|
1473
|
-
*
|
|
1474
|
-
* @param text - Text to add.
|
|
1475
|
-
* @param location - Source code location of this text.
|
|
1476
|
-
*/
|
|
1477
|
-
appendText(text, location) {
|
|
1478
|
-
this.childNodes.push(new TextNode(text, location));
|
|
1479
|
-
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Return a list of all known classes on the element. Dynamic values are
|
|
1482
|
-
* ignored.
|
|
1483
|
-
*/
|
|
1484
|
-
get classList() {
|
|
1485
|
-
if (!this.hasAttribute("class")) {
|
|
1486
|
-
return new DOMTokenList(null, null);
|
|
1487
|
-
}
|
|
1488
|
-
const classes = this.getAttribute("class", true)
|
|
1489
|
-
.filter((attr) => attr.isStatic)
|
|
1490
|
-
.map((attr) => attr.value)
|
|
1491
|
-
.join(" ");
|
|
1492
|
-
return new DOMTokenList(classes, null);
|
|
1493
|
-
}
|
|
1494
|
-
/**
|
|
1495
|
-
* Get element ID if present.
|
|
1496
|
-
*/
|
|
1497
|
-
get id() {
|
|
1498
|
-
return this.getAttributeValue("id");
|
|
1499
|
-
}
|
|
1500
|
-
get style() {
|
|
1501
|
-
const attr = this.getAttribute("style");
|
|
1502
|
-
return parseCssDeclaration(attr === null || attr === void 0 ? void 0 : attr.value);
|
|
1503
|
-
}
|
|
1504
|
-
/**
|
|
1505
|
-
* Returns the first child element or null if there are no child elements.
|
|
1506
|
-
*/
|
|
1507
|
-
get firstElementChild() {
|
|
1508
|
-
const children = this.childElements;
|
|
1509
|
-
return children.length > 0 ? children[0] : null;
|
|
1510
|
-
}
|
|
1511
|
-
/**
|
|
1512
|
-
* Returns the last child element or null if there are no child elements.
|
|
1513
|
-
*/
|
|
1514
|
-
get lastElementChild() {
|
|
1515
|
-
const children = this.childElements;
|
|
1516
|
-
return children.length > 0 ? children[children.length - 1] : null;
|
|
1517
|
-
}
|
|
1518
|
-
get siblings() {
|
|
1519
|
-
return this.parent ? this.parent.childElements : [this];
|
|
1520
|
-
}
|
|
1521
|
-
get previousSibling() {
|
|
1522
|
-
const i = this.siblings.findIndex((node) => node.unique === this.unique);
|
|
1523
|
-
return i >= 1 ? this.siblings[i - 1] : null;
|
|
1524
|
-
}
|
|
1525
|
-
get nextSibling() {
|
|
1526
|
-
const i = this.siblings.findIndex((node) => node.unique === this.unique);
|
|
1527
|
-
return i <= this.siblings.length - 2 ? this.siblings[i + 1] : null;
|
|
1528
|
-
}
|
|
1529
|
-
getElementsByTagName(tagName) {
|
|
1530
|
-
return this.childElements.reduce((matches, node) => {
|
|
1531
|
-
return matches.concat(node.is(tagName) ? [node] : [], node.getElementsByTagName(tagName));
|
|
1532
|
-
}, []);
|
|
1533
|
-
}
|
|
1534
|
-
querySelector(selector) {
|
|
1535
|
-
const it = this.querySelectorImpl(selector);
|
|
1536
|
-
const next = it.next();
|
|
1537
|
-
if (next.done) {
|
|
1538
|
-
return null;
|
|
1539
|
-
}
|
|
1540
|
-
else {
|
|
1541
|
-
return next.value;
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
querySelectorAll(selector) {
|
|
1545
|
-
const it = this.querySelectorImpl(selector);
|
|
1546
|
-
const unique = new Set(it);
|
|
1547
|
-
return Array.from(unique.values());
|
|
1548
|
-
}
|
|
1549
|
-
*querySelectorImpl(selectorList) {
|
|
1550
|
-
if (!selectorList) {
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
for (const selector of selectorList.split(/,\s*/)) {
|
|
1554
|
-
const pattern = new Selector(selector);
|
|
1555
|
-
yield* pattern.match(this);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Visit all nodes from this node and down. Depth first.
|
|
1560
|
-
*
|
|
1561
|
-
* @internal
|
|
1562
|
-
*/
|
|
1563
|
-
visitDepthFirst(callback) {
|
|
1564
|
-
function visit(node) {
|
|
1565
|
-
node.childElements.forEach(visit);
|
|
1566
|
-
if (!node.isRootElement()) {
|
|
1567
|
-
callback(node);
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
visit(this);
|
|
1571
|
-
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Evaluates callbackk on all descendants, returning true if any are true.
|
|
1574
|
-
*
|
|
1575
|
-
* @internal
|
|
1576
|
-
*/
|
|
1577
|
-
someChildren(callback) {
|
|
1578
|
-
return this.childElements.some(visit);
|
|
1579
|
-
function visit(node) {
|
|
1580
|
-
if (callback(node)) {
|
|
1581
|
-
return true;
|
|
1582
|
-
}
|
|
1583
|
-
else {
|
|
1584
|
-
return node.childElements.some(visit);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
/**
|
|
1589
|
-
* Evaluates callbackk on all descendants, returning true if all are true.
|
|
1590
|
-
*
|
|
1591
|
-
* @internal
|
|
1592
|
-
*/
|
|
1593
|
-
everyChildren(callback) {
|
|
1594
|
-
return this.childElements.every(visit);
|
|
1595
|
-
function visit(node) {
|
|
1596
|
-
if (!callback(node)) {
|
|
1597
|
-
return false;
|
|
1598
|
-
}
|
|
1599
|
-
return node.childElements.every(visit);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
/**
|
|
1603
|
-
* Visit all nodes from this node and down. Breadth first.
|
|
1604
|
-
*
|
|
1605
|
-
* The first node for which the callback evaluates to true is returned.
|
|
1606
|
-
*
|
|
1607
|
-
* @internal
|
|
1608
|
-
*/
|
|
1609
|
-
find(callback) {
|
|
1610
|
-
function visit(node) {
|
|
1611
|
-
if (callback(node)) {
|
|
1612
|
-
return node;
|
|
1613
|
-
}
|
|
1614
|
-
for (const child of node.childElements) {
|
|
1615
|
-
const match = child.find(callback);
|
|
1616
|
-
if (match) {
|
|
1617
|
-
return match;
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
return null;
|
|
1621
|
-
}
|
|
1622
|
-
return visit(this);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
function isClosed(endToken, meta) {
|
|
1626
|
-
let closed = exports.NodeClosed.Open;
|
|
1627
|
-
if (meta && meta.void) {
|
|
1628
|
-
closed = exports.NodeClosed.VoidOmitted;
|
|
1629
|
-
}
|
|
1630
|
-
if (endToken.data[0] === "/>") {
|
|
1631
|
-
closed = exports.NodeClosed.VoidSelfClosed;
|
|
1632
|
-
}
|
|
1633
|
-
return closed;
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
/**
|
|
1637
|
-
* @public
|
|
1638
|
-
*/
|
|
1639
|
-
class DOMTree {
|
|
1640
|
-
constructor(location) {
|
|
1641
|
-
this.root = HtmlElement.rootNode(location);
|
|
1642
|
-
this.active = this.root;
|
|
1643
|
-
this.doctype = null;
|
|
1644
|
-
}
|
|
1645
|
-
pushActive(node) {
|
|
1646
|
-
this.active = node;
|
|
1647
|
-
}
|
|
1648
|
-
popActive() {
|
|
1649
|
-
if (this.active.isRootElement()) {
|
|
1650
|
-
/* root element should never be popped, continue as if nothing happened */
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
this.active = this.active.parent || this.root;
|
|
1654
|
-
}
|
|
1655
|
-
getActive() {
|
|
1656
|
-
return this.active;
|
|
1657
|
-
}
|
|
1658
|
-
/**
|
|
1659
|
-
* Resolve dynamic meta expressions.
|
|
1660
|
-
*/
|
|
1661
|
-
resolveMeta(table) {
|
|
1662
|
-
this.visitDepthFirst((node) => table.resolve(node));
|
|
1663
|
-
}
|
|
1664
|
-
getElementsByTagName(tagName) {
|
|
1665
|
-
return this.root.getElementsByTagName(tagName);
|
|
1666
|
-
}
|
|
1667
|
-
visitDepthFirst(callback) {
|
|
1668
|
-
this.root.visitDepthFirst(callback);
|
|
1669
|
-
}
|
|
1670
|
-
find(callback) {
|
|
1671
|
-
return this.root.find(callback);
|
|
1672
|
-
}
|
|
1673
|
-
querySelector(selector) {
|
|
1674
|
-
return this.root.querySelector(selector);
|
|
1675
|
-
}
|
|
1676
|
-
querySelectorAll(selector) {
|
|
1677
|
-
return this.root.querySelectorAll(selector);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
function stringify(value) {
|
|
1682
|
-
if (typeof value === "string") {
|
|
1683
|
-
return String(value);
|
|
1684
|
-
}
|
|
1685
|
-
else {
|
|
1686
|
-
return JSON.stringify(value);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
/**
|
|
1690
|
-
* Represents an `Error` created from arbitrary values.
|
|
1691
|
-
*
|
|
1692
|
-
* @public
|
|
1693
|
-
*/
|
|
1694
|
-
class WrappedError extends Error {
|
|
1695
|
-
constructor(message) {
|
|
1696
|
-
super(stringify(message));
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
/**
|
|
1701
|
-
* Ensures the value is an Error.
|
|
1702
|
-
*
|
|
1703
|
-
* If the passed value is not an `Error` instance a [[WrappedError]] is
|
|
1704
|
-
* constructed with the stringified value.
|
|
1705
|
-
*
|
|
1706
|
-
* @internal
|
|
1707
|
-
*/
|
|
1708
|
-
function ensureError(value) {
|
|
1709
|
-
if (value instanceof Error) {
|
|
1710
|
-
return value;
|
|
1711
|
-
}
|
|
1712
|
-
else {
|
|
1713
|
-
return new WrappedError(value);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
/**
|
|
1718
|
-
* @public
|
|
1719
|
-
*/
|
|
1720
|
-
class NestedError extends Error {
|
|
1721
|
-
constructor(message, nested) {
|
|
1722
|
-
super(message);
|
|
1723
|
-
Error.captureStackTrace(this, NestedError);
|
|
1724
|
-
this.name = NestedError.name;
|
|
1725
|
-
if (nested && nested.stack) {
|
|
1726
|
-
this.stack += `\nCaused by: ${nested.stack}`;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
/**
|
|
1732
|
-
* @public
|
|
1733
|
-
*/
|
|
1734
|
-
class UserError extends NestedError {
|
|
1735
|
-
constructor(message, nested) {
|
|
1736
|
-
super(message, nested);
|
|
1737
|
-
Error.captureStackTrace(this, UserError);
|
|
1738
|
-
this.name = UserError.name;
|
|
1739
|
-
}
|
|
1740
|
-
/**
|
|
1741
|
-
* @public
|
|
1742
|
-
*/
|
|
1743
|
-
/* istanbul ignore next: default implementation */
|
|
1744
|
-
prettyFormat() {
|
|
1745
|
-
return undefined;
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
/**
|
|
1750
|
-
* @internal
|
|
1751
|
-
*/
|
|
1752
|
-
class InheritError extends UserError {
|
|
1753
|
-
constructor({ tagName, inherit }) {
|
|
1754
|
-
const message = `Element <${tagName}> cannot inherit from <${inherit}>: no such element`;
|
|
1755
|
-
super(message);
|
|
1756
|
-
Error.captureStackTrace(this, InheritError);
|
|
1757
|
-
this.name = InheritError.name;
|
|
1758
|
-
this.tagName = tagName;
|
|
1759
|
-
this.inherit = inherit;
|
|
1760
|
-
this.filename = null;
|
|
1761
|
-
}
|
|
1762
|
-
prettyFormat() {
|
|
1763
|
-
const { message, tagName, inherit } = this;
|
|
1764
|
-
const source = this.filename
|
|
1765
|
-
? ["", "This error occurred when loading element metadata from:", `"${this.filename}"`, ""]
|
|
1766
|
-
: [""];
|
|
1767
|
-
return [
|
|
1768
|
-
message,
|
|
1769
|
-
...source,
|
|
1770
|
-
"This usually occurs when the elements are defined in the wrong order, try one of the following:",
|
|
1771
|
-
"",
|
|
1772
|
-
` - Ensure the spelling of "${inherit}" is correct.`,
|
|
1773
|
-
` - Ensure the file containing "${inherit}" is loaded before the file containing "${tagName}".`,
|
|
1774
|
-
` - Move the definition of "${inherit}" above the definition for "${tagName}".`,
|
|
1775
|
-
].join("\n");
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
function getSummary(schema, obj, errors) {
|
|
1780
|
-
const output = betterAjvErrors__default.default(schema, obj, errors, {
|
|
1781
|
-
format: "js",
|
|
1782
|
-
});
|
|
1783
|
-
// istanbul ignore next: for safety only
|
|
1784
|
-
return output.length > 0 ? output[0].error : "unknown validation error";
|
|
1785
|
-
}
|
|
1786
|
-
/**
|
|
1787
|
-
* @public
|
|
1788
|
-
*/
|
|
1789
|
-
class SchemaValidationError extends UserError {
|
|
1790
|
-
constructor(filename, message, obj, schema, errors) {
|
|
1791
|
-
const summary = getSummary(schema, obj, errors);
|
|
1792
|
-
super(`${message}: ${summary}`);
|
|
1793
|
-
this.filename = filename;
|
|
1794
|
-
this.obj = obj;
|
|
1795
|
-
this.schema = schema;
|
|
1796
|
-
this.errors = errors;
|
|
1797
|
-
}
|
|
1798
|
-
prettyError() {
|
|
1799
|
-
const json = this.getRawJSON();
|
|
1800
|
-
return betterAjvErrors__default.default(this.schema, this.obj, this.errors, {
|
|
1801
|
-
format: "cli",
|
|
1802
|
-
indent: 2,
|
|
1803
|
-
json,
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
getRawJSON() {
|
|
1807
|
-
if (this.filename && fs__default.default.existsSync(this.filename)) {
|
|
1808
|
-
return fs__default.default.readFileSync(this.filename, "utf-8");
|
|
1809
|
-
}
|
|
1810
|
-
else {
|
|
1811
|
-
return null;
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
/**
|
|
1817
|
-
* Computes hash for given string.
|
|
1818
|
-
*
|
|
1819
|
-
* @internal
|
|
1820
|
-
*/
|
|
1821
|
-
function cyrb53(str) {
|
|
1822
|
-
const a = 2654435761;
|
|
1823
|
-
const b = 1597334677;
|
|
1824
|
-
const c = 2246822507;
|
|
1825
|
-
const d = 3266489909;
|
|
1826
|
-
const e = 4294967296;
|
|
1827
|
-
const f = 2097151;
|
|
1828
|
-
const seed = 0;
|
|
1829
|
-
let h1 = 0xdeadbeef ^ seed;
|
|
1830
|
-
let h2 = 0x41c6ce57 ^ seed;
|
|
1831
|
-
for (let i = 0, ch; i < str.length; i++) {
|
|
1832
|
-
ch = str.charCodeAt(i);
|
|
1833
|
-
h1 = Math.imul(h1 ^ ch, a);
|
|
1834
|
-
h2 = Math.imul(h2 ^ ch, b);
|
|
1835
|
-
}
|
|
1836
|
-
h1 = Math.imul(h1 ^ (h1 >>> 16), c) ^ Math.imul(h2 ^ (h2 >>> 13), d);
|
|
1837
|
-
h2 = Math.imul(h2 ^ (h2 >>> 16), c) ^ Math.imul(h1 ^ (h1 >>> 13), d);
|
|
1838
|
-
return e * (f & h2) + (h1 >>> 0);
|
|
1839
|
-
}
|
|
1840
|
-
const computeHash = cyrb53;
|
|
413
|
+
const computeHash = cyrb53;
|
|
1841
414
|
|
|
1842
415
|
const $schema$1 = "http://json-schema.org/draft-06/schema#";
|
|
1843
416
|
const $id$1 = "https://html-validate.org/schemas/elements.json";
|
|
@@ -2235,487 +808,1914 @@ const definitions = {
|
|
|
2235
808
|
}
|
|
2236
809
|
}
|
|
2237
810
|
};
|
|
2238
|
-
var schema = {
|
|
2239
|
-
$schema: $schema$1,
|
|
2240
|
-
$id: $id$1,
|
|
2241
|
-
type: type$1,
|
|
2242
|
-
properties: properties$1,
|
|
2243
|
-
patternProperties: patternProperties,
|
|
2244
|
-
definitions: definitions
|
|
811
|
+
var schema = {
|
|
812
|
+
$schema: $schema$1,
|
|
813
|
+
$id: $id$1,
|
|
814
|
+
type: type$1,
|
|
815
|
+
properties: properties$1,
|
|
816
|
+
patternProperties: patternProperties,
|
|
817
|
+
definitions: definitions
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* AJV keyword "regexp" to validate the type to be a regular expression.
|
|
822
|
+
* Injects errors with the "type" keyword to give the same output.
|
|
823
|
+
*/
|
|
824
|
+
/* istanbul ignore next: manual testing */
|
|
825
|
+
const ajvRegexpValidate = function (data, dataCxt) {
|
|
826
|
+
const valid = data instanceof RegExp;
|
|
827
|
+
if (!valid) {
|
|
828
|
+
ajvRegexpValidate.errors = [
|
|
829
|
+
{
|
|
830
|
+
instancePath: dataCxt === null || dataCxt === void 0 ? void 0 : dataCxt.instancePath,
|
|
831
|
+
schemaPath: undefined,
|
|
832
|
+
keyword: "type",
|
|
833
|
+
message: "should be a regular expression",
|
|
834
|
+
params: {
|
|
835
|
+
keyword: "type",
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
];
|
|
839
|
+
}
|
|
840
|
+
return valid;
|
|
841
|
+
};
|
|
842
|
+
const ajvRegexpKeyword = {
|
|
843
|
+
keyword: "regexp",
|
|
844
|
+
schema: false,
|
|
845
|
+
errors: true,
|
|
846
|
+
validate: ajvRegexpValidate,
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* AJV keyword "function" to validate the type to be a function. Injects errors
|
|
851
|
+
* with the "type" keyword to give the same output.
|
|
852
|
+
*/
|
|
853
|
+
const ajvFunctionValidate = function (data, dataCxt) {
|
|
854
|
+
const valid = typeof data === "function";
|
|
855
|
+
if (!valid) {
|
|
856
|
+
ajvFunctionValidate.errors = [
|
|
857
|
+
{
|
|
858
|
+
instancePath: /* istanbul ignore next */ dataCxt === null || dataCxt === void 0 ? void 0 : dataCxt.instancePath,
|
|
859
|
+
schemaPath: undefined,
|
|
860
|
+
keyword: "type",
|
|
861
|
+
message: "should be a function",
|
|
862
|
+
params: {
|
|
863
|
+
keyword: "type",
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
];
|
|
867
|
+
}
|
|
868
|
+
return valid;
|
|
869
|
+
};
|
|
870
|
+
const ajvFunctionKeyword = {
|
|
871
|
+
keyword: "function",
|
|
872
|
+
schema: false,
|
|
873
|
+
errors: true,
|
|
874
|
+
validate: ajvFunctionValidate,
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* @public
|
|
879
|
+
*/
|
|
880
|
+
exports.TextContent = void 0;
|
|
881
|
+
(function (TextContent) {
|
|
882
|
+
/* forbid node to have text content, inter-element whitespace is ignored */
|
|
883
|
+
TextContent["NONE"] = "none";
|
|
884
|
+
/* node can have text but not required too */
|
|
885
|
+
TextContent["DEFAULT"] = "default";
|
|
886
|
+
/* node requires text-nodes to be present (direct or by descendant) */
|
|
887
|
+
TextContent["REQUIRED"] = "required";
|
|
888
|
+
/* node requires accessible text (hidden text is ignored, tries to get text from accessibility tree) */
|
|
889
|
+
TextContent["ACCESSIBLE"] = "accessible";
|
|
890
|
+
})(exports.TextContent || (exports.TextContent = {}));
|
|
891
|
+
/**
|
|
892
|
+
* Properties listed here can be copied (loaded) onto another element using
|
|
893
|
+
* [[HtmlElement.loadMeta]].
|
|
894
|
+
*
|
|
895
|
+
* @public
|
|
896
|
+
*/
|
|
897
|
+
const MetaCopyableProperty = [
|
|
898
|
+
"metadata",
|
|
899
|
+
"flow",
|
|
900
|
+
"sectioning",
|
|
901
|
+
"heading",
|
|
902
|
+
"phrasing",
|
|
903
|
+
"embedded",
|
|
904
|
+
"interactive",
|
|
905
|
+
"transparent",
|
|
906
|
+
"form",
|
|
907
|
+
"formAssociated",
|
|
908
|
+
"labelable",
|
|
909
|
+
"attributes",
|
|
910
|
+
"permittedContent",
|
|
911
|
+
"permittedDescendants",
|
|
912
|
+
"permittedOrder",
|
|
913
|
+
"permittedParent",
|
|
914
|
+
"requiredAncestors",
|
|
915
|
+
"requiredContent",
|
|
916
|
+
];
|
|
917
|
+
/**
|
|
918
|
+
* @internal
|
|
919
|
+
*/
|
|
920
|
+
function setMetaProperty(dst, key, value) {
|
|
921
|
+
dst[key] = value;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function isSet(value) {
|
|
925
|
+
return typeof value !== "undefined";
|
|
926
|
+
}
|
|
927
|
+
function flag(value) {
|
|
928
|
+
return value ? true : undefined;
|
|
929
|
+
}
|
|
930
|
+
function stripUndefined(src) {
|
|
931
|
+
const entries = Object.entries(src).filter(([, value]) => isSet(value));
|
|
932
|
+
return Object.fromEntries(entries);
|
|
933
|
+
}
|
|
934
|
+
function migrateSingleAttribute(src, key) {
|
|
935
|
+
var _a, _b;
|
|
936
|
+
const result = {};
|
|
937
|
+
result.deprecated = flag((_a = src.deprecatedAttributes) === null || _a === void 0 ? void 0 : _a.includes(key));
|
|
938
|
+
result.required = flag((_b = src.requiredAttributes) === null || _b === void 0 ? void 0 : _b.includes(key));
|
|
939
|
+
result.omit = undefined;
|
|
940
|
+
const attr = src.attributes ? src.attributes[key] : undefined;
|
|
941
|
+
if (typeof attr === "undefined") {
|
|
942
|
+
return stripUndefined(result);
|
|
943
|
+
}
|
|
944
|
+
/* when the attribute is set to null we use a special property "delete" to
|
|
945
|
+
* flag it, if it is still set during merge (inheritance, overwriting, etc) the attribute will be removed */
|
|
946
|
+
if (attr === null) {
|
|
947
|
+
result.delete = true;
|
|
948
|
+
return stripUndefined(result);
|
|
949
|
+
}
|
|
950
|
+
if (Array.isArray(attr)) {
|
|
951
|
+
if (attr.length === 0) {
|
|
952
|
+
result.boolean = true;
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
result.enum = attr.filter((it) => it !== "");
|
|
956
|
+
if (attr.includes("")) {
|
|
957
|
+
result.omit = true;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return stripUndefined(result);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
return stripUndefined({ ...result, ...attr });
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function migrateAttributes(src) {
|
|
967
|
+
var _a, _b, _c;
|
|
968
|
+
const keys = [
|
|
969
|
+
...Object.keys((_a = src.attributes) !== null && _a !== void 0 ? _a : {}),
|
|
970
|
+
...((_b = src.requiredAttributes) !== null && _b !== void 0 ? _b : []),
|
|
971
|
+
...((_c = src.deprecatedAttributes) !== null && _c !== void 0 ? _c : []),
|
|
972
|
+
].sort();
|
|
973
|
+
const entries = keys.map((key) => {
|
|
974
|
+
return [key, migrateSingleAttribute(src, key)];
|
|
975
|
+
});
|
|
976
|
+
return Object.fromEntries(entries);
|
|
977
|
+
}
|
|
978
|
+
function migrateElement(src) {
|
|
979
|
+
const result = {
|
|
980
|
+
...src,
|
|
981
|
+
...{
|
|
982
|
+
formAssociated: undefined,
|
|
983
|
+
},
|
|
984
|
+
attributes: migrateAttributes(src),
|
|
985
|
+
textContent: src.textContent,
|
|
986
|
+
};
|
|
987
|
+
/* removed properties */
|
|
988
|
+
delete result.deprecatedAttributes;
|
|
989
|
+
delete result.requiredAttributes;
|
|
990
|
+
/* strip out undefined */
|
|
991
|
+
if (!result.textContent) {
|
|
992
|
+
delete result.textContent;
|
|
993
|
+
}
|
|
994
|
+
if (src.formAssociated) {
|
|
995
|
+
result.formAssociated = {
|
|
996
|
+
listed: Boolean(src.formAssociated.listed),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
delete result.formAssociated;
|
|
1001
|
+
}
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Returns true if given element is a descendant of given tagname.
|
|
1007
|
+
*
|
|
1008
|
+
* @internal
|
|
1009
|
+
*/
|
|
1010
|
+
function isDescendant(node, tagName) {
|
|
1011
|
+
let cur = node.parent;
|
|
1012
|
+
while (cur && !cur.isRootElement()) {
|
|
1013
|
+
if (cur.is(tagName)) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
cur = cur.parent;
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Returns true if given element has given attribute (no matter the value, null,
|
|
1023
|
+
* dynamic, etc).
|
|
1024
|
+
*/
|
|
1025
|
+
function hasAttribute(node, attr) {
|
|
1026
|
+
return node.hasAttribute(attr);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Matches attribute against value.
|
|
1031
|
+
*/
|
|
1032
|
+
function matchAttribute(node, key, op, value) {
|
|
1033
|
+
const nodeValue = (node.getAttributeValue(key) || "").toLowerCase();
|
|
1034
|
+
switch (op) {
|
|
1035
|
+
case "!=":
|
|
1036
|
+
return nodeValue !== value;
|
|
1037
|
+
case "=":
|
|
1038
|
+
return nodeValue === value;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const dynamicKeys = [
|
|
1043
|
+
"metadata",
|
|
1044
|
+
"flow",
|
|
1045
|
+
"sectioning",
|
|
1046
|
+
"heading",
|
|
1047
|
+
"phrasing",
|
|
1048
|
+
"embedded",
|
|
1049
|
+
"interactive",
|
|
1050
|
+
"labelable",
|
|
1051
|
+
];
|
|
1052
|
+
const functionTable = {
|
|
1053
|
+
isDescendant: isDescendantFacade,
|
|
1054
|
+
hasAttribute: hasAttributeFacade,
|
|
1055
|
+
matchAttribute: matchAttributeFacade,
|
|
1056
|
+
};
|
|
1057
|
+
const schemaCache = new Map();
|
|
1058
|
+
function clone(src) {
|
|
1059
|
+
return JSON.parse(JSON.stringify(src));
|
|
1060
|
+
}
|
|
1061
|
+
function overwriteMerge$1(a, b) {
|
|
1062
|
+
return b;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* @public
|
|
1066
|
+
*/
|
|
1067
|
+
class MetaTable {
|
|
1068
|
+
/**
|
|
1069
|
+
* @internal
|
|
1070
|
+
*/
|
|
1071
|
+
constructor() {
|
|
1072
|
+
this.elements = {};
|
|
1073
|
+
this.schema = clone(schema);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* @internal
|
|
1077
|
+
*/
|
|
1078
|
+
init() {
|
|
1079
|
+
this.resolveGlobal();
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Extend validation schema.
|
|
1083
|
+
*
|
|
1084
|
+
* @public
|
|
1085
|
+
*/
|
|
1086
|
+
extendValidationSchema(patch) {
|
|
1087
|
+
if (patch.properties) {
|
|
1088
|
+
this.schema = deepmerge__default.default(this.schema, {
|
|
1089
|
+
patternProperties: {
|
|
1090
|
+
"^[^$].*$": {
|
|
1091
|
+
properties: patch.properties,
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
if (patch.definitions) {
|
|
1097
|
+
this.schema = deepmerge__default.default(this.schema, {
|
|
1098
|
+
definitions: patch.definitions,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Load metadata table from object.
|
|
1104
|
+
*
|
|
1105
|
+
* @public
|
|
1106
|
+
* @param obj - Object with metadata to load
|
|
1107
|
+
* @param filename - Optional filename used when presenting validation error
|
|
1108
|
+
*/
|
|
1109
|
+
loadFromObject(obj, filename = null) {
|
|
1110
|
+
var _a;
|
|
1111
|
+
try {
|
|
1112
|
+
const validate = this.getSchemaValidator();
|
|
1113
|
+
if (!validate(obj)) {
|
|
1114
|
+
throw new SchemaValidationError(filename, `Element metadata is not valid`, obj, this.schema,
|
|
1115
|
+
/* istanbul ignore next: AJV sets .errors when validate returns false */
|
|
1116
|
+
(_a = validate.errors) !== null && _a !== void 0 ? _a : []);
|
|
1117
|
+
}
|
|
1118
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1119
|
+
if (key === "$schema")
|
|
1120
|
+
continue;
|
|
1121
|
+
this.addEntry(key, migrateElement(value));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
if (err instanceof InheritError) {
|
|
1126
|
+
err.filename = filename;
|
|
1127
|
+
throw err;
|
|
1128
|
+
}
|
|
1129
|
+
if (err instanceof SchemaValidationError) {
|
|
1130
|
+
throw err;
|
|
1131
|
+
}
|
|
1132
|
+
if (!filename) {
|
|
1133
|
+
throw err;
|
|
1134
|
+
}
|
|
1135
|
+
throw new UserError(`Failed to load element metadata from "${filename}"`, ensureError(err));
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Get [[MetaElement]] for the given tag. If no specific metadata is present
|
|
1140
|
+
* the global metadata is returned or null if no global is present.
|
|
1141
|
+
*
|
|
1142
|
+
* @public
|
|
1143
|
+
* @returns A shallow copy of metadata.
|
|
1144
|
+
*/
|
|
1145
|
+
getMetaFor(tagName) {
|
|
1146
|
+
/* try to locate by tagname */
|
|
1147
|
+
tagName = tagName.toLowerCase();
|
|
1148
|
+
if (this.elements[tagName]) {
|
|
1149
|
+
return { ...this.elements[tagName] };
|
|
1150
|
+
}
|
|
1151
|
+
/* try to locate global element */
|
|
1152
|
+
if (this.elements["*"]) {
|
|
1153
|
+
return { ...this.elements["*"] };
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Find all tags which has enabled given property.
|
|
1159
|
+
*
|
|
1160
|
+
* @public
|
|
1161
|
+
*/
|
|
1162
|
+
getTagsWithProperty(propName) {
|
|
1163
|
+
return Object.entries(this.elements)
|
|
1164
|
+
.filter(([, entry]) => entry[propName])
|
|
1165
|
+
.map(([tagName]) => tagName);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Find tag matching tagName or inheriting from it.
|
|
1169
|
+
*
|
|
1170
|
+
* @public
|
|
1171
|
+
*/
|
|
1172
|
+
getTagsDerivedFrom(tagName) {
|
|
1173
|
+
return Object.entries(this.elements)
|
|
1174
|
+
.filter(([key, entry]) => key === tagName || entry.inherit === tagName)
|
|
1175
|
+
.map(([tagName]) => tagName);
|
|
1176
|
+
}
|
|
1177
|
+
addEntry(tagName, entry) {
|
|
1178
|
+
let parent = this.elements[tagName] || {};
|
|
1179
|
+
/* handle inheritance */
|
|
1180
|
+
if (entry.inherit) {
|
|
1181
|
+
const name = entry.inherit;
|
|
1182
|
+
parent = this.elements[name];
|
|
1183
|
+
if (!parent) {
|
|
1184
|
+
throw new InheritError({
|
|
1185
|
+
tagName,
|
|
1186
|
+
inherit: name,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
/* merge all sources together */
|
|
1191
|
+
const expanded = this.mergeElement(parent, { ...entry, tagName });
|
|
1192
|
+
expandRegex(expanded);
|
|
1193
|
+
this.elements[tagName] = expanded;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Construct a new AJV schema validator.
|
|
1197
|
+
*/
|
|
1198
|
+
getSchemaValidator() {
|
|
1199
|
+
const hash = computeHash(JSON.stringify(this.schema));
|
|
1200
|
+
const cached = schemaCache.get(hash);
|
|
1201
|
+
if (cached) {
|
|
1202
|
+
return cached;
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
const ajv = new Ajv__default.default({ strict: true, strictTuples: true, strictTypes: true });
|
|
1206
|
+
ajv.addMetaSchema(ajvSchemaDraft);
|
|
1207
|
+
ajv.addKeyword(ajvFunctionKeyword);
|
|
1208
|
+
ajv.addKeyword(ajvRegexpKeyword);
|
|
1209
|
+
ajv.addKeyword({ keyword: "copyable" });
|
|
1210
|
+
const validate = ajv.compile(this.schema);
|
|
1211
|
+
schemaCache.set(hash, validate);
|
|
1212
|
+
return validate;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* @public
|
|
1217
|
+
*/
|
|
1218
|
+
getJSONSchema() {
|
|
1219
|
+
return this.schema;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Finds the global element definition and merges each known element with the
|
|
1223
|
+
* global, e.g. to assign global attributes.
|
|
1224
|
+
*/
|
|
1225
|
+
resolveGlobal() {
|
|
1226
|
+
/* skip if there is no global elements */
|
|
1227
|
+
if (!this.elements["*"])
|
|
1228
|
+
return;
|
|
1229
|
+
/* fetch and remove the global element, it should not be resolvable by
|
|
1230
|
+
* itself */
|
|
1231
|
+
const global = this.elements["*"];
|
|
1232
|
+
delete this.elements["*"];
|
|
1233
|
+
/* hack: unset default properties which global should not override */
|
|
1234
|
+
delete global.tagName;
|
|
1235
|
+
delete global.void;
|
|
1236
|
+
/* merge elements */
|
|
1237
|
+
for (const [tagName, entry] of Object.entries(this.elements)) {
|
|
1238
|
+
this.elements[tagName] = this.mergeElement(global, entry);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
mergeElement(a, b) {
|
|
1242
|
+
const merged = deepmerge__default.default(a, b, { arrayMerge: overwriteMerge$1 });
|
|
1243
|
+
/* special handling when removing attributes by setting them to null
|
|
1244
|
+
* resulting in the deletion flag being set */
|
|
1245
|
+
const filteredAttrs = Object.entries(merged.attributes).filter(([, attr]) => {
|
|
1246
|
+
const val = !attr.delete;
|
|
1247
|
+
delete attr.delete;
|
|
1248
|
+
return val;
|
|
1249
|
+
});
|
|
1250
|
+
merged.attributes = Object.fromEntries(filteredAttrs);
|
|
1251
|
+
return merged;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* @internal
|
|
1255
|
+
*/
|
|
1256
|
+
resolve(node) {
|
|
1257
|
+
if (node.meta) {
|
|
1258
|
+
expandProperties(node, node.meta);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function expandProperties(node, entry) {
|
|
1263
|
+
for (const key of dynamicKeys) {
|
|
1264
|
+
const property = entry[key];
|
|
1265
|
+
if (property && typeof property !== "boolean") {
|
|
1266
|
+
setMetaProperty(entry, key, evaluateProperty(node, property));
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Given a string it returns either the string as-is or if the string is wrapped
|
|
1272
|
+
* in /../ it creates and returns a regex instead.
|
|
1273
|
+
*/
|
|
1274
|
+
function expandRegexValue(value) {
|
|
1275
|
+
if (value instanceof RegExp) {
|
|
1276
|
+
return value;
|
|
1277
|
+
}
|
|
1278
|
+
const match = value.match(/^\/\^?([^/$]*)\$?\/([i]*)$/);
|
|
1279
|
+
if (match) {
|
|
1280
|
+
const [, expr, flags] = match;
|
|
1281
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- expected to be regexp
|
|
1282
|
+
return new RegExp(`^${expr}$`, flags);
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
return value;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Expand all regular expressions in strings ("/../"). This mutates the object.
|
|
1290
|
+
*/
|
|
1291
|
+
function expandRegex(entry) {
|
|
1292
|
+
for (const [name, values] of Object.entries(entry.attributes)) {
|
|
1293
|
+
if (values.enum) {
|
|
1294
|
+
entry.attributes[name].enum = values.enum.map(expandRegexValue);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function evaluateProperty(node, expr) {
|
|
1299
|
+
const [func, options] = parseExpression(expr);
|
|
1300
|
+
return func(node, options);
|
|
1301
|
+
}
|
|
1302
|
+
function parseExpression(expr) {
|
|
1303
|
+
if (typeof expr === "string") {
|
|
1304
|
+
return parseExpression([expr, {}]);
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- old style expressions should be replaced with typesafe functions */
|
|
1308
|
+
const [funcName, options] = expr;
|
|
1309
|
+
const func = functionTable[funcName];
|
|
1310
|
+
if (!func) {
|
|
1311
|
+
throw new Error(`Failed to find function "${funcName}" when evaluating property expression`);
|
|
1312
|
+
}
|
|
1313
|
+
return [func, options];
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function isDescendantFacade(node, tagName) {
|
|
1317
|
+
if (typeof tagName !== "string") {
|
|
1318
|
+
throw new Error(`Property expression "isDescendant" must take string argument when evaluating metadata for <${node.tagName}>`);
|
|
1319
|
+
}
|
|
1320
|
+
return isDescendant(node, tagName);
|
|
1321
|
+
}
|
|
1322
|
+
function hasAttributeFacade(node, attr) {
|
|
1323
|
+
if (typeof attr !== "string") {
|
|
1324
|
+
throw new Error(`Property expression "hasAttribute" must take string argument when evaluating metadata for <${node.tagName}>`);
|
|
1325
|
+
}
|
|
1326
|
+
return hasAttribute(node, attr);
|
|
1327
|
+
}
|
|
1328
|
+
function matchAttributeFacade(node, match) {
|
|
1329
|
+
if (!Array.isArray(match) || match.length !== 3) {
|
|
1330
|
+
throw new Error(`Property expression "matchAttribute" must take [key, op, value] array as argument when evaluating metadata for <${node.tagName}>`);
|
|
1331
|
+
}
|
|
1332
|
+
const [key, op, value] = match.map((x) => x.toLowerCase());
|
|
1333
|
+
switch (op) {
|
|
1334
|
+
case "!=":
|
|
1335
|
+
case "=":
|
|
1336
|
+
return matchAttribute(node, key, op, value);
|
|
1337
|
+
default:
|
|
1338
|
+
throw new Error(`Property expression "matchAttribute" has invalid operator "${op}" when evaluating metadata for <${node.tagName}>`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* @public
|
|
1344
|
+
*/
|
|
1345
|
+
class DynamicValue {
|
|
1346
|
+
constructor(expr) {
|
|
1347
|
+
this.expr = expr;
|
|
1348
|
+
}
|
|
1349
|
+
toString() {
|
|
1350
|
+
return this.expr;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* DOM Attribute.
|
|
1356
|
+
*
|
|
1357
|
+
* Represents a HTML attribute. Can contain either a fixed static value or a
|
|
1358
|
+
* placeholder for dynamic values (e.g. interpolated).
|
|
1359
|
+
*
|
|
1360
|
+
* @public
|
|
1361
|
+
*/
|
|
1362
|
+
class Attribute {
|
|
1363
|
+
/**
|
|
1364
|
+
* @param key - Attribute name.
|
|
1365
|
+
* @param value - Attribute value. Set to `null` for boolean attributes.
|
|
1366
|
+
* @param keyLocation - Source location of attribute name.
|
|
1367
|
+
* @param valueLocation - Source location of attribute value.
|
|
1368
|
+
* @param originalAttribute - If this attribute was dynamically added via a
|
|
1369
|
+
* transformation (e.g. vuejs `:id` generating the `id` attribute) this
|
|
1370
|
+
* parameter should be set to the attribute name of the source attribute (`:id`).
|
|
1371
|
+
*/
|
|
1372
|
+
constructor(key, value, keyLocation, valueLocation, originalAttribute) {
|
|
1373
|
+
this.key = key;
|
|
1374
|
+
this.value = value;
|
|
1375
|
+
this.keyLocation = keyLocation;
|
|
1376
|
+
this.valueLocation = valueLocation;
|
|
1377
|
+
this.originalAttribute = originalAttribute;
|
|
1378
|
+
/* force undefined to null */
|
|
1379
|
+
if (typeof this.value === "undefined") {
|
|
1380
|
+
this.value = null;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Flag set to true if the attribute value is static.
|
|
1385
|
+
*/
|
|
1386
|
+
get isStatic() {
|
|
1387
|
+
return !this.isDynamic;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Flag set to true if the attribute value is dynamic.
|
|
1391
|
+
*/
|
|
1392
|
+
get isDynamic() {
|
|
1393
|
+
return this.value instanceof DynamicValue;
|
|
1394
|
+
}
|
|
1395
|
+
valueMatches(pattern, dynamicMatches = true) {
|
|
1396
|
+
if (this.value === null) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
/* dynamic values matches everything */
|
|
1400
|
+
if (this.value instanceof DynamicValue) {
|
|
1401
|
+
return dynamicMatches;
|
|
1402
|
+
}
|
|
1403
|
+
/* test against an array of keywords */
|
|
1404
|
+
if (Array.isArray(pattern)) {
|
|
1405
|
+
return pattern.includes(this.value);
|
|
1406
|
+
}
|
|
1407
|
+
/* test value against pattern */
|
|
1408
|
+
if (pattern instanceof RegExp) {
|
|
1409
|
+
return this.value.match(pattern) !== null;
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
return this.value === pattern;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function getCSSDeclarations(value) {
|
|
1418
|
+
return value
|
|
1419
|
+
.trim()
|
|
1420
|
+
.split(";")
|
|
1421
|
+
.filter(Boolean)
|
|
1422
|
+
.map((it) => {
|
|
1423
|
+
const [property, value] = it.split(":", 2);
|
|
1424
|
+
return [property.trim(), value ? value.trim() : ""];
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* @internal
|
|
1429
|
+
*/
|
|
1430
|
+
function parseCssDeclaration(value) {
|
|
1431
|
+
if (!value || value instanceof DynamicValue) {
|
|
1432
|
+
return {};
|
|
1433
|
+
}
|
|
1434
|
+
const pairs = getCSSDeclarations(value);
|
|
1435
|
+
return Object.fromEntries(pairs);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function sliceSize(size, begin, end) {
|
|
1439
|
+
if (typeof size !== "number") {
|
|
1440
|
+
return size;
|
|
1441
|
+
}
|
|
1442
|
+
if (typeof end !== "number") {
|
|
1443
|
+
return size - begin;
|
|
1444
|
+
}
|
|
1445
|
+
if (end < 0) {
|
|
1446
|
+
end = size + end;
|
|
1447
|
+
}
|
|
1448
|
+
return Math.min(size, end - begin);
|
|
1449
|
+
}
|
|
1450
|
+
function sliceLocation(location, begin, end, wrap) {
|
|
1451
|
+
if (!location)
|
|
1452
|
+
return null;
|
|
1453
|
+
const size = sliceSize(location.size, begin, end);
|
|
1454
|
+
const sliced = {
|
|
1455
|
+
filename: location.filename,
|
|
1456
|
+
offset: location.offset + begin,
|
|
1457
|
+
line: location.line,
|
|
1458
|
+
column: location.column + begin,
|
|
1459
|
+
size,
|
|
1460
|
+
};
|
|
1461
|
+
/* if text content is provided try to find all newlines and modify line/column accordingly */
|
|
1462
|
+
if (wrap) {
|
|
1463
|
+
let index = -1;
|
|
1464
|
+
const col = sliced.column;
|
|
1465
|
+
do {
|
|
1466
|
+
index = wrap.indexOf("\n", index + 1);
|
|
1467
|
+
if (index >= 0 && index < begin) {
|
|
1468
|
+
sliced.column = col - (index + 1);
|
|
1469
|
+
sliced.line++;
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
break;
|
|
1473
|
+
}
|
|
1474
|
+
} while (true); // eslint-disable-line no-constant-condition -- it will break out
|
|
1475
|
+
}
|
|
1476
|
+
return sliced;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
var State;
|
|
1480
|
+
(function (State) {
|
|
1481
|
+
State[State["INITIAL"] = 1] = "INITIAL";
|
|
1482
|
+
State[State["DOCTYPE"] = 2] = "DOCTYPE";
|
|
1483
|
+
State[State["TEXT"] = 3] = "TEXT";
|
|
1484
|
+
State[State["TAG"] = 4] = "TAG";
|
|
1485
|
+
State[State["ATTR"] = 5] = "ATTR";
|
|
1486
|
+
State[State["CDATA"] = 6] = "CDATA";
|
|
1487
|
+
State[State["SCRIPT"] = 7] = "SCRIPT";
|
|
1488
|
+
State[State["STYLE"] = 8] = "STYLE";
|
|
1489
|
+
})(State || (State = {}));
|
|
1490
|
+
|
|
1491
|
+
var ContentModel;
|
|
1492
|
+
(function (ContentModel) {
|
|
1493
|
+
ContentModel[ContentModel["TEXT"] = 1] = "TEXT";
|
|
1494
|
+
ContentModel[ContentModel["SCRIPT"] = 2] = "SCRIPT";
|
|
1495
|
+
ContentModel[ContentModel["STYLE"] = 3] = "STYLE";
|
|
1496
|
+
})(ContentModel || (ContentModel = {}));
|
|
1497
|
+
class Context {
|
|
1498
|
+
constructor(source) {
|
|
1499
|
+
var _a, _b, _c, _d;
|
|
1500
|
+
this.state = State.INITIAL;
|
|
1501
|
+
this.string = source.data;
|
|
1502
|
+
this.filename = (_a = source.filename) !== null && _a !== void 0 ? _a : "";
|
|
1503
|
+
this.offset = (_b = source.offset) !== null && _b !== void 0 ? _b : 0;
|
|
1504
|
+
this.line = (_c = source.line) !== null && _c !== void 0 ? _c : 1;
|
|
1505
|
+
this.column = (_d = source.column) !== null && _d !== void 0 ? _d : 1;
|
|
1506
|
+
this.contentModel = ContentModel.TEXT;
|
|
1507
|
+
}
|
|
1508
|
+
getTruncatedLine(n = 13) {
|
|
1509
|
+
return JSON.stringify(this.string.length > n ? `${this.string.slice(0, 10)}...` : this.string);
|
|
1510
|
+
}
|
|
1511
|
+
consume(n, state) {
|
|
1512
|
+
/* if "n" is an regex match the first value is the full matched
|
|
1513
|
+
* string so consume that many characters. */
|
|
1514
|
+
if (typeof n !== "number") {
|
|
1515
|
+
n = n[0].length; /* regex match */
|
|
1516
|
+
}
|
|
1517
|
+
/* poor mans line counter :( */
|
|
1518
|
+
let consumed = this.string.slice(0, n);
|
|
1519
|
+
let offset;
|
|
1520
|
+
while ((offset = consumed.indexOf("\n")) >= 0) {
|
|
1521
|
+
this.line++;
|
|
1522
|
+
this.column = 1;
|
|
1523
|
+
consumed = consumed.substr(offset + 1);
|
|
1524
|
+
}
|
|
1525
|
+
this.column += consumed.length;
|
|
1526
|
+
this.offset += n;
|
|
1527
|
+
/* remove N chars */
|
|
1528
|
+
this.string = this.string.substr(n);
|
|
1529
|
+
/* change state */
|
|
1530
|
+
this.state = state;
|
|
1531
|
+
}
|
|
1532
|
+
getLocation(size) {
|
|
1533
|
+
return {
|
|
1534
|
+
filename: this.filename,
|
|
1535
|
+
offset: this.offset,
|
|
1536
|
+
line: this.line,
|
|
1537
|
+
column: this.column,
|
|
1538
|
+
size,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/**
|
|
1544
|
+
* @public
|
|
1545
|
+
*/
|
|
1546
|
+
exports.NodeType = void 0;
|
|
1547
|
+
(function (NodeType) {
|
|
1548
|
+
NodeType[NodeType["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
|
|
1549
|
+
NodeType[NodeType["TEXT_NODE"] = 3] = "TEXT_NODE";
|
|
1550
|
+
NodeType[NodeType["DOCUMENT_NODE"] = 9] = "DOCUMENT_NODE";
|
|
1551
|
+
})(exports.NodeType || (exports.NodeType = {}));
|
|
1552
|
+
|
|
1553
|
+
const DOCUMENT_NODE_NAME = "#document";
|
|
1554
|
+
const TEXT_CONTENT = Symbol("textContent");
|
|
1555
|
+
let counter = 0;
|
|
1556
|
+
/**
|
|
1557
|
+
* @public
|
|
1558
|
+
*/
|
|
1559
|
+
class DOMNode {
|
|
1560
|
+
/**
|
|
1561
|
+
* Create a new DOMNode.
|
|
1562
|
+
*
|
|
1563
|
+
* @param nodeType - What node type to create.
|
|
1564
|
+
* @param nodeName - What node name to use. For `HtmlElement` this corresponds
|
|
1565
|
+
* to the tagName but other node types have specific predefined values.
|
|
1566
|
+
* @param location - Source code location of this node.
|
|
1567
|
+
*/
|
|
1568
|
+
constructor(nodeType, nodeName, location) {
|
|
1569
|
+
this.nodeType = nodeType;
|
|
1570
|
+
this.nodeName = nodeName !== null && nodeName !== void 0 ? nodeName : DOCUMENT_NODE_NAME;
|
|
1571
|
+
this.location = location;
|
|
1572
|
+
this.disabledRules = new Set();
|
|
1573
|
+
this.blockedRules = new Map();
|
|
1574
|
+
this.childNodes = [];
|
|
1575
|
+
this.unique = counter++;
|
|
1576
|
+
this.cache = null;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Enable cache for this node.
|
|
1580
|
+
*
|
|
1581
|
+
* Should not be called before the node and all children are fully constructed.
|
|
1582
|
+
*
|
|
1583
|
+
* @internal
|
|
1584
|
+
*/
|
|
1585
|
+
cacheEnable() {
|
|
1586
|
+
this.cache = new Map();
|
|
1587
|
+
}
|
|
1588
|
+
cacheGet(key) {
|
|
1589
|
+
if (this.cache) {
|
|
1590
|
+
return this.cache.get(key);
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
cacheSet(key, value) {
|
|
1597
|
+
if (this.cache) {
|
|
1598
|
+
this.cache.set(key, value);
|
|
1599
|
+
}
|
|
1600
|
+
return value;
|
|
1601
|
+
}
|
|
1602
|
+
cacheRemove(key) {
|
|
1603
|
+
if (this.cache) {
|
|
1604
|
+
return this.cache.delete(key);
|
|
1605
|
+
}
|
|
1606
|
+
else {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
cacheExists(key) {
|
|
1611
|
+
return Boolean(this.cache && this.cache.has(key));
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Get the text (recursive) from all child nodes.
|
|
1615
|
+
*/
|
|
1616
|
+
get textContent() {
|
|
1617
|
+
const cached = this.cacheGet(TEXT_CONTENT);
|
|
1618
|
+
if (cached) {
|
|
1619
|
+
return cached;
|
|
1620
|
+
}
|
|
1621
|
+
const text = this.childNodes.map((node) => node.textContent).join("");
|
|
1622
|
+
this.cacheSet(TEXT_CONTENT, text);
|
|
1623
|
+
return text;
|
|
1624
|
+
}
|
|
1625
|
+
append(node) {
|
|
1626
|
+
this.childNodes.push(node);
|
|
1627
|
+
}
|
|
1628
|
+
isRootElement() {
|
|
1629
|
+
return this.nodeType === exports.NodeType.DOCUMENT_NODE;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Tests if two nodes are the same (references the same object).
|
|
1633
|
+
*
|
|
1634
|
+
* @since v4.11.0
|
|
1635
|
+
*/
|
|
1636
|
+
isSameNode(otherNode) {
|
|
1637
|
+
return this.unique === otherNode.unique;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Returns a DOMNode representing the first direct child node or `null` if the
|
|
1641
|
+
* node has no children.
|
|
1642
|
+
*/
|
|
1643
|
+
get firstChild() {
|
|
1644
|
+
return this.childNodes[0] || null;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Returns a DOMNode representing the last direct child node or `null` if the
|
|
1648
|
+
* node has no children.
|
|
1649
|
+
*/
|
|
1650
|
+
get lastChild() {
|
|
1651
|
+
return this.childNodes[this.childNodes.length - 1] || null;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Block a rule for this node.
|
|
1655
|
+
*
|
|
1656
|
+
* @internal
|
|
1657
|
+
*/
|
|
1658
|
+
blockRule(ruleId, blocker) {
|
|
1659
|
+
const current = this.blockedRules.get(ruleId);
|
|
1660
|
+
if (current) {
|
|
1661
|
+
current.push(blocker);
|
|
1662
|
+
}
|
|
1663
|
+
else {
|
|
1664
|
+
this.blockedRules.set(ruleId, [blocker]);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Blocks multiple rules.
|
|
1669
|
+
*
|
|
1670
|
+
* @internal
|
|
1671
|
+
*/
|
|
1672
|
+
blockRules(rules, blocker) {
|
|
1673
|
+
for (const rule of rules) {
|
|
1674
|
+
this.blockRule(rule, blocker);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Disable a rule for this node.
|
|
1679
|
+
*
|
|
1680
|
+
* @internal
|
|
1681
|
+
*/
|
|
1682
|
+
disableRule(ruleId) {
|
|
1683
|
+
this.disabledRules.add(ruleId);
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Disables multiple rules.
|
|
1687
|
+
*
|
|
1688
|
+
* @internal
|
|
1689
|
+
*/
|
|
1690
|
+
disableRules(rules) {
|
|
1691
|
+
for (const rule of rules) {
|
|
1692
|
+
this.disableRule(rule);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Enable a previously disabled rule for this node.
|
|
1697
|
+
*/
|
|
1698
|
+
enableRule(ruleId) {
|
|
1699
|
+
this.disabledRules.delete(ruleId);
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Enables multiple rules.
|
|
1703
|
+
*/
|
|
1704
|
+
enableRules(rules) {
|
|
1705
|
+
for (const rule of rules) {
|
|
1706
|
+
this.enableRule(rule);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Test if a rule is enabled for this node.
|
|
1711
|
+
*
|
|
1712
|
+
* @internal
|
|
1713
|
+
*/
|
|
1714
|
+
ruleEnabled(ruleId) {
|
|
1715
|
+
return !this.disabledRules.has(ruleId);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Test if a rule is blocked for this node.
|
|
1719
|
+
*
|
|
1720
|
+
* @internal
|
|
1721
|
+
*/
|
|
1722
|
+
ruleBlockers(ruleId) {
|
|
1723
|
+
var _a;
|
|
1724
|
+
return (_a = this.blockedRules.get(ruleId)) !== null && _a !== void 0 ? _a : [];
|
|
1725
|
+
}
|
|
1726
|
+
generateSelector() {
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function parse(text, baseLocation) {
|
|
1732
|
+
const tokens = [];
|
|
1733
|
+
const locations = baseLocation ? [] : null;
|
|
1734
|
+
for (let begin = 0; begin < text.length;) {
|
|
1735
|
+
let end = text.indexOf(" ", begin);
|
|
1736
|
+
/* if the last space was found move the position to the last character
|
|
1737
|
+
* in the string */
|
|
1738
|
+
if (end === -1) {
|
|
1739
|
+
end = text.length;
|
|
1740
|
+
}
|
|
1741
|
+
/* handle multiple spaces */
|
|
1742
|
+
const size = end - begin;
|
|
1743
|
+
if (size === 0) {
|
|
1744
|
+
begin++;
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
/* extract token */
|
|
1748
|
+
const token = text.substring(begin, end);
|
|
1749
|
+
tokens.push(token);
|
|
1750
|
+
/* extract location */
|
|
1751
|
+
if (locations && baseLocation) {
|
|
1752
|
+
const location = sliceLocation(baseLocation, begin, end);
|
|
1753
|
+
locations.push(location);
|
|
1754
|
+
}
|
|
1755
|
+
/* advance position to the character after the current end position */
|
|
1756
|
+
begin += size + 1;
|
|
1757
|
+
}
|
|
1758
|
+
return { tokens, locations };
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* @public
|
|
1762
|
+
*/
|
|
1763
|
+
class DOMTokenList extends Array {
|
|
1764
|
+
constructor(value, location) {
|
|
1765
|
+
if (value && typeof value === "string") {
|
|
1766
|
+
/* replace all whitespace with a single space for easier parsing */
|
|
1767
|
+
const normalized = value.replace(/[\t\r\n]/g, " ");
|
|
1768
|
+
const { tokens, locations } = parse(normalized, location);
|
|
1769
|
+
super(...tokens);
|
|
1770
|
+
this.locations = locations;
|
|
1771
|
+
}
|
|
1772
|
+
else {
|
|
1773
|
+
super(0);
|
|
1774
|
+
this.locations = null;
|
|
1775
|
+
}
|
|
1776
|
+
if (value instanceof DynamicValue) {
|
|
1777
|
+
this.value = value.expr;
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
this.value = value || "";
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
item(n) {
|
|
1784
|
+
return this[n];
|
|
1785
|
+
}
|
|
1786
|
+
location(n) {
|
|
1787
|
+
if (this.locations) {
|
|
1788
|
+
return this.locations[n];
|
|
1789
|
+
}
|
|
1790
|
+
else {
|
|
1791
|
+
throw new Error("Trying to access DOMTokenList location when base location isn't set");
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
contains(token) {
|
|
1795
|
+
return this.includes(token);
|
|
1796
|
+
}
|
|
1797
|
+
*iterator() {
|
|
1798
|
+
for (let index = 0; index < this.length; index++) {
|
|
1799
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion -- as we loop over length this should always be set */
|
|
1800
|
+
const item = this.item(index);
|
|
1801
|
+
const location = this.location(index);
|
|
1802
|
+
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
1803
|
+
yield { index, item, location };
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
var Combinator;
|
|
1809
|
+
(function (Combinator) {
|
|
1810
|
+
Combinator[Combinator["DESCENDANT"] = 1] = "DESCENDANT";
|
|
1811
|
+
Combinator[Combinator["CHILD"] = 2] = "CHILD";
|
|
1812
|
+
Combinator[Combinator["ADJACENT_SIBLING"] = 3] = "ADJACENT_SIBLING";
|
|
1813
|
+
Combinator[Combinator["GENERAL_SIBLING"] = 4] = "GENERAL_SIBLING";
|
|
1814
|
+
/* special cases */
|
|
1815
|
+
Combinator[Combinator["SCOPE"] = 5] = "SCOPE";
|
|
1816
|
+
})(Combinator || (Combinator = {}));
|
|
1817
|
+
function parseCombinator(combinator, pattern) {
|
|
1818
|
+
/* special case, when pattern is :scope [[Selector]] will handle this
|
|
1819
|
+
* "combinator" to match itself instead of descendants */
|
|
1820
|
+
if (pattern === ":scope") {
|
|
1821
|
+
return Combinator.SCOPE;
|
|
1822
|
+
}
|
|
1823
|
+
switch (combinator) {
|
|
1824
|
+
case undefined:
|
|
1825
|
+
case null:
|
|
1826
|
+
case "":
|
|
1827
|
+
return Combinator.DESCENDANT;
|
|
1828
|
+
case ">":
|
|
1829
|
+
return Combinator.CHILD;
|
|
1830
|
+
case "+":
|
|
1831
|
+
return Combinator.ADJACENT_SIBLING;
|
|
1832
|
+
case "~":
|
|
1833
|
+
return Combinator.GENERAL_SIBLING;
|
|
1834
|
+
default:
|
|
1835
|
+
throw new Error(`Unknown combinator "${combinator}"`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function firstChild(node) {
|
|
1840
|
+
return node.previousSibling === null;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function lastChild(node) {
|
|
1844
|
+
return node.nextSibling === null;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const cache = {};
|
|
1848
|
+
function getNthChild(node) {
|
|
1849
|
+
if (!node.parent) {
|
|
1850
|
+
return -1;
|
|
1851
|
+
}
|
|
1852
|
+
if (!cache[node.unique]) {
|
|
1853
|
+
const parent = node.parent;
|
|
1854
|
+
const index = parent.childElements.findIndex((cur) => {
|
|
1855
|
+
return cur.unique === node.unique;
|
|
1856
|
+
});
|
|
1857
|
+
cache[node.unique] = index + 1; /* nthChild starts at 1 */
|
|
1858
|
+
}
|
|
1859
|
+
return cache[node.unique];
|
|
1860
|
+
}
|
|
1861
|
+
function nthChild(node, args) {
|
|
1862
|
+
if (!args) {
|
|
1863
|
+
throw new Error("Missing argument to nth-child");
|
|
1864
|
+
}
|
|
1865
|
+
const n = parseInt(args.trim(), 10);
|
|
1866
|
+
const cur = getNthChild(node);
|
|
1867
|
+
return cur === n;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function scope(node) {
|
|
1871
|
+
return node.isSameNode(this.scope);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
const table = {
|
|
1875
|
+
"first-child": firstChild,
|
|
1876
|
+
"last-child": lastChild,
|
|
1877
|
+
"nth-child": nthChild,
|
|
1878
|
+
scope: scope,
|
|
2245
1879
|
};
|
|
1880
|
+
function factory(name, context) {
|
|
1881
|
+
const fn = table[name];
|
|
1882
|
+
if (fn) {
|
|
1883
|
+
return fn.bind(context);
|
|
1884
|
+
}
|
|
1885
|
+
else {
|
|
1886
|
+
throw new Error(`Pseudo-class "${name}" is not implemented`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* Homage to PHP: unescapes slashes.
|
|
1892
|
+
*
|
|
1893
|
+
* E.g. "foo\:bar" becomes "foo:bar"
|
|
1894
|
+
*/
|
|
1895
|
+
function stripslashes(value) {
|
|
1896
|
+
return value.replace(/\\(.)/g, "$1");
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* @internal
|
|
1900
|
+
*/
|
|
1901
|
+
function escapeSelectorComponent(text) {
|
|
1902
|
+
return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* @internal
|
|
1906
|
+
*/
|
|
1907
|
+
function generateIdSelector(id) {
|
|
1908
|
+
const escaped = escapeSelectorComponent(id);
|
|
1909
|
+
return escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Returns true if the character is a delimiter for different kinds of selectors:
|
|
1913
|
+
*
|
|
1914
|
+
* - `.` - begins a class selector
|
|
1915
|
+
* - `#` - begins an id selector
|
|
1916
|
+
* - `[` - begins an attribute selector
|
|
1917
|
+
* - `:` - begins a pseudo class or element selector
|
|
1918
|
+
*/
|
|
1919
|
+
function isDelimiter(ch) {
|
|
1920
|
+
return /[.#[:]/.test(ch);
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Returns true if the character is a quotation mark.
|
|
1924
|
+
*/
|
|
1925
|
+
function isQuotationMark(ch) {
|
|
1926
|
+
return /['"]/.test(ch);
|
|
1927
|
+
}
|
|
1928
|
+
function isPseudoElement(ch, buffer) {
|
|
1929
|
+
return ch === ":" && buffer === ":";
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* @internal
|
|
1933
|
+
*/
|
|
1934
|
+
function* splitPattern(pattern) {
|
|
1935
|
+
if (pattern === "") {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const end = pattern.length;
|
|
1939
|
+
let begin = 0;
|
|
1940
|
+
let cur = 1;
|
|
1941
|
+
let quoted = false;
|
|
1942
|
+
while (cur < end) {
|
|
1943
|
+
const ch = pattern[cur];
|
|
1944
|
+
const buffer = pattern.slice(begin, cur);
|
|
1945
|
+
/* escaped character, ignore whatever is next */
|
|
1946
|
+
if (ch === "\\") {
|
|
1947
|
+
cur += 2;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
/* if inside quoted string we only look for the end quotation mark */
|
|
1951
|
+
if (quoted) {
|
|
1952
|
+
if (ch === quoted) {
|
|
1953
|
+
quoted = false;
|
|
1954
|
+
}
|
|
1955
|
+
cur += 1;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
/* if the character is a quotation mark we store the character and the above
|
|
1959
|
+
* condition will look for a similar end quotation mark */
|
|
1960
|
+
if (isQuotationMark(ch)) {
|
|
1961
|
+
quoted = ch;
|
|
1962
|
+
cur += 1;
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
/* special case when using :: pseudo element selector */
|
|
1966
|
+
if (isPseudoElement(ch, buffer)) {
|
|
1967
|
+
cur += 1;
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
/* if the character is a delimiter we yield the string and reset the
|
|
1971
|
+
* position */
|
|
1972
|
+
if (isDelimiter(ch)) {
|
|
1973
|
+
begin = cur;
|
|
1974
|
+
yield buffer;
|
|
1975
|
+
}
|
|
1976
|
+
cur += 1;
|
|
1977
|
+
}
|
|
1978
|
+
/* yield the rest of the string */
|
|
1979
|
+
const tail = pattern.slice(begin, cur);
|
|
1980
|
+
yield tail;
|
|
1981
|
+
}
|
|
1982
|
+
class Matcher {
|
|
1983
|
+
}
|
|
1984
|
+
class ClassMatcher extends Matcher {
|
|
1985
|
+
constructor(classname) {
|
|
1986
|
+
super();
|
|
1987
|
+
this.classname = classname;
|
|
1988
|
+
}
|
|
1989
|
+
match(node) {
|
|
1990
|
+
return node.classList.contains(this.classname);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
class IdMatcher extends Matcher {
|
|
1994
|
+
constructor(id) {
|
|
1995
|
+
super();
|
|
1996
|
+
this.id = stripslashes(id);
|
|
1997
|
+
}
|
|
1998
|
+
match(node) {
|
|
1999
|
+
return node.id === this.id;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
class AttrMatcher extends Matcher {
|
|
2003
|
+
constructor(attr) {
|
|
2004
|
+
super();
|
|
2005
|
+
const [, key, op, value] = attr.match(/^(.+?)(?:([~^$*|]?=)"([^"]+?)")?$/);
|
|
2006
|
+
this.key = key;
|
|
2007
|
+
this.op = op;
|
|
2008
|
+
this.value = value;
|
|
2009
|
+
}
|
|
2010
|
+
match(node) {
|
|
2011
|
+
const attr = node.getAttribute(this.key, true) || [];
|
|
2012
|
+
return attr.some((cur) => {
|
|
2013
|
+
switch (this.op) {
|
|
2014
|
+
case undefined:
|
|
2015
|
+
return true; /* attribute exists */
|
|
2016
|
+
case "=":
|
|
2017
|
+
return cur.value === this.value;
|
|
2018
|
+
default:
|
|
2019
|
+
throw new Error(`Attribute selector operator ${this.op} is not implemented yet`);
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
class PseudoClassMatcher extends Matcher {
|
|
2025
|
+
constructor(pseudoclass, context) {
|
|
2026
|
+
super();
|
|
2027
|
+
const match = pseudoclass.match(/^([^(]+)(?:\((.*)\))?$/);
|
|
2028
|
+
if (!match) {
|
|
2029
|
+
throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
|
|
2030
|
+
}
|
|
2031
|
+
const [, name, args] = match;
|
|
2032
|
+
this.name = name;
|
|
2033
|
+
this.args = args;
|
|
2034
|
+
}
|
|
2035
|
+
match(node, context) {
|
|
2036
|
+
const fn = factory(this.name, context);
|
|
2037
|
+
return fn(node, this.args);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
class Pattern {
|
|
2041
|
+
constructor(pattern) {
|
|
2042
|
+
const match = pattern.match(/^([~+\->]?)((?:[*]|[^.#[:]+)?)(.*)$/);
|
|
2043
|
+
match.shift(); /* remove full matched string */
|
|
2044
|
+
this.selector = pattern;
|
|
2045
|
+
this.combinator = parseCombinator(match.shift(), pattern);
|
|
2046
|
+
this.tagName = match.shift() || "*";
|
|
2047
|
+
this.pattern = Array.from(splitPattern(match[0]), (it) => this.createMatcher(it));
|
|
2048
|
+
}
|
|
2049
|
+
match(node, context) {
|
|
2050
|
+
return node.is(this.tagName) && this.pattern.every((cur) => cur.match(node, context));
|
|
2051
|
+
}
|
|
2052
|
+
createMatcher(pattern) {
|
|
2053
|
+
switch (pattern[0]) {
|
|
2054
|
+
case ".":
|
|
2055
|
+
return new ClassMatcher(pattern.slice(1));
|
|
2056
|
+
case "#":
|
|
2057
|
+
return new IdMatcher(pattern.slice(1));
|
|
2058
|
+
case "[":
|
|
2059
|
+
return new AttrMatcher(pattern.slice(1, -1));
|
|
2060
|
+
case ":":
|
|
2061
|
+
return new PseudoClassMatcher(pattern.slice(1), this.selector);
|
|
2062
|
+
default:
|
|
2063
|
+
/* istanbul ignore next: fallback solution, the switch cases should cover
|
|
2064
|
+
* everything and there is no known way to trigger this fallback */
|
|
2065
|
+
throw new Error(`Failed to create matcher for "${pattern}"`);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* DOM Selector.
|
|
2071
|
+
*/
|
|
2072
|
+
class Selector {
|
|
2073
|
+
constructor(selector) {
|
|
2074
|
+
this.pattern = Selector.parse(selector);
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Match this selector against a HtmlElement.
|
|
2078
|
+
*
|
|
2079
|
+
* @param root - Element to match against.
|
|
2080
|
+
* @returns Iterator with matched elements.
|
|
2081
|
+
*/
|
|
2082
|
+
*match(root) {
|
|
2083
|
+
const context = { scope: root };
|
|
2084
|
+
yield* this.matchInternal(root, 0, context);
|
|
2085
|
+
}
|
|
2086
|
+
*matchInternal(root, level, context) {
|
|
2087
|
+
if (level >= this.pattern.length) {
|
|
2088
|
+
yield root;
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const pattern = this.pattern[level];
|
|
2092
|
+
const matches = Selector.findCandidates(root, pattern);
|
|
2093
|
+
for (const node of matches) {
|
|
2094
|
+
if (!pattern.match(node, context)) {
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
yield* this.matchInternal(node, level + 1, context);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
static parse(selector) {
|
|
2101
|
+
/* strip whitespace before combinators, "ul > li" becomes "ul >li", for
|
|
2102
|
+
* easier parsing */
|
|
2103
|
+
selector = selector.replace(/([+~>]) /g, "$1");
|
|
2104
|
+
const pattern = selector.split(/(?:(?<!\\) )+/);
|
|
2105
|
+
return pattern.map((part) => new Pattern(part));
|
|
2106
|
+
}
|
|
2107
|
+
static findCandidates(root, pattern) {
|
|
2108
|
+
switch (pattern.combinator) {
|
|
2109
|
+
case Combinator.DESCENDANT:
|
|
2110
|
+
return root.getElementsByTagName(pattern.tagName);
|
|
2111
|
+
case Combinator.CHILD:
|
|
2112
|
+
return root.childElements.filter((node) => node.is(pattern.tagName));
|
|
2113
|
+
case Combinator.ADJACENT_SIBLING:
|
|
2114
|
+
return Selector.findAdjacentSibling(root);
|
|
2115
|
+
case Combinator.GENERAL_SIBLING:
|
|
2116
|
+
return Selector.findGeneralSibling(root);
|
|
2117
|
+
case Combinator.SCOPE:
|
|
2118
|
+
return [root];
|
|
2119
|
+
}
|
|
2120
|
+
/* istanbul ignore next: fallback solution, the switch cases should cover
|
|
2121
|
+
* everything and there is no known way to trigger this fallback */
|
|
2122
|
+
return [];
|
|
2123
|
+
}
|
|
2124
|
+
static findAdjacentSibling(node) {
|
|
2125
|
+
let adjacent = false;
|
|
2126
|
+
return node.siblings.filter((cur) => {
|
|
2127
|
+
if (adjacent) {
|
|
2128
|
+
adjacent = false;
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
if (cur === node) {
|
|
2132
|
+
adjacent = true;
|
|
2133
|
+
}
|
|
2134
|
+
return false;
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
static findGeneralSibling(node) {
|
|
2138
|
+
let after = false;
|
|
2139
|
+
return node.siblings.filter((cur) => {
|
|
2140
|
+
if (after) {
|
|
2141
|
+
return true;
|
|
2142
|
+
}
|
|
2143
|
+
if (cur === node) {
|
|
2144
|
+
after = true;
|
|
2145
|
+
}
|
|
2146
|
+
return false;
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
const TEXT_NODE_NAME = "#text";
|
|
2152
|
+
/**
|
|
2153
|
+
* Returns true if the node is a text node.
|
|
2154
|
+
*
|
|
2155
|
+
* @public
|
|
2156
|
+
*/
|
|
2157
|
+
function isTextNode(node) {
|
|
2158
|
+
return Boolean(node && node.nodeType === exports.NodeType.TEXT_NODE);
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Represents a text in the HTML document.
|
|
2162
|
+
*
|
|
2163
|
+
* Text nodes are appended as children of `HtmlElement` and cannot have childen
|
|
2164
|
+
* of its own.
|
|
2165
|
+
*
|
|
2166
|
+
* @public
|
|
2167
|
+
*/
|
|
2168
|
+
class TextNode extends DOMNode {
|
|
2169
|
+
/**
|
|
2170
|
+
* @param text - Text to add. When a `DynamicValue` is used the expression is
|
|
2171
|
+
* used as "text".
|
|
2172
|
+
* @param location - Source code location of this node.
|
|
2173
|
+
*/
|
|
2174
|
+
constructor(text, location) {
|
|
2175
|
+
super(exports.NodeType.TEXT_NODE, TEXT_NODE_NAME, location);
|
|
2176
|
+
this.text = text;
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Get the text from node.
|
|
2180
|
+
*/
|
|
2181
|
+
get textContent() {
|
|
2182
|
+
return this.text.toString();
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Flag set to true if the attribute value is static.
|
|
2186
|
+
*/
|
|
2187
|
+
get isStatic() {
|
|
2188
|
+
return !this.isDynamic;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Flag set to true if the attribute value is dynamic.
|
|
2192
|
+
*/
|
|
2193
|
+
get isDynamic() {
|
|
2194
|
+
return this.text instanceof DynamicValue;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2246
2197
|
|
|
2247
2198
|
/**
|
|
2248
|
-
*
|
|
2249
|
-
* Injects errors with the "type" keyword to give the same output.
|
|
2199
|
+
* @public
|
|
2250
2200
|
*/
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
keyword: "type",
|
|
2260
|
-
message: "should be a regular expression",
|
|
2261
|
-
params: {
|
|
2262
|
-
keyword: "type",
|
|
2263
|
-
},
|
|
2264
|
-
},
|
|
2265
|
-
];
|
|
2266
|
-
}
|
|
2267
|
-
return valid;
|
|
2268
|
-
};
|
|
2269
|
-
const ajvRegexpKeyword = {
|
|
2270
|
-
keyword: "regexp",
|
|
2271
|
-
schema: false,
|
|
2272
|
-
errors: true,
|
|
2273
|
-
validate: ajvRegexpValidate,
|
|
2274
|
-
};
|
|
2275
|
-
|
|
2201
|
+
exports.NodeClosed = void 0;
|
|
2202
|
+
(function (NodeClosed) {
|
|
2203
|
+
NodeClosed[NodeClosed["Open"] = 0] = "Open";
|
|
2204
|
+
NodeClosed[NodeClosed["EndTag"] = 1] = "EndTag";
|
|
2205
|
+
NodeClosed[NodeClosed["VoidOmitted"] = 2] = "VoidOmitted";
|
|
2206
|
+
NodeClosed[NodeClosed["VoidSelfClosed"] = 3] = "VoidSelfClosed";
|
|
2207
|
+
NodeClosed[NodeClosed["ImplicitClosed"] = 4] = "ImplicitClosed";
|
|
2208
|
+
})(exports.NodeClosed || (exports.NodeClosed = {}));
|
|
2276
2209
|
/**
|
|
2277
|
-
*
|
|
2278
|
-
*
|
|
2210
|
+
* Returns true if the node is an element node.
|
|
2211
|
+
*
|
|
2212
|
+
* @public
|
|
2279
2213
|
*/
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
if (!valid) {
|
|
2283
|
-
ajvFunctionValidate.errors = [
|
|
2284
|
-
{
|
|
2285
|
-
instancePath: /* istanbul ignore next */ dataCxt === null || dataCxt === void 0 ? void 0 : dataCxt.instancePath,
|
|
2286
|
-
schemaPath: undefined,
|
|
2287
|
-
keyword: "type",
|
|
2288
|
-
message: "should be a function",
|
|
2289
|
-
params: {
|
|
2290
|
-
keyword: "type",
|
|
2291
|
-
},
|
|
2292
|
-
},
|
|
2293
|
-
];
|
|
2294
|
-
}
|
|
2295
|
-
return valid;
|
|
2296
|
-
};
|
|
2297
|
-
const ajvFunctionKeyword = {
|
|
2298
|
-
keyword: "function",
|
|
2299
|
-
schema: false,
|
|
2300
|
-
errors: true,
|
|
2301
|
-
validate: ajvFunctionValidate,
|
|
2302
|
-
};
|
|
2303
|
-
|
|
2304
|
-
function isSet(value) {
|
|
2305
|
-
return typeof value !== "undefined";
|
|
2306
|
-
}
|
|
2307
|
-
function flag(value) {
|
|
2308
|
-
return value ? true : undefined;
|
|
2214
|
+
function isElementNode(node) {
|
|
2215
|
+
return Boolean(node && node.nodeType === exports.NodeType.ELEMENT_NODE);
|
|
2309
2216
|
}
|
|
2310
|
-
function
|
|
2311
|
-
|
|
2312
|
-
return Object.fromEntries(entries);
|
|
2217
|
+
function isValidTagName(tagName) {
|
|
2218
|
+
return Boolean(tagName !== "" && tagName !== "*");
|
|
2313
2219
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2220
|
+
/**
|
|
2221
|
+
* @public
|
|
2222
|
+
*/
|
|
2223
|
+
class HtmlElement extends DOMNode {
|
|
2224
|
+
constructor(tagName, parent, closed, meta, location) {
|
|
2225
|
+
const nodeType = tagName ? exports.NodeType.ELEMENT_NODE : exports.NodeType.DOCUMENT_NODE;
|
|
2226
|
+
super(nodeType, tagName, location);
|
|
2227
|
+
if (!isValidTagName(tagName)) {
|
|
2228
|
+
throw new Error(`The tag name provided ('${tagName || ""}') is not a valid name`);
|
|
2229
|
+
}
|
|
2230
|
+
this.tagName = tagName || "#document";
|
|
2231
|
+
this.parent = parent !== null && parent !== void 0 ? parent : null;
|
|
2232
|
+
this.attr = {};
|
|
2233
|
+
this.metaElement = meta !== null && meta !== void 0 ? meta : null;
|
|
2234
|
+
this.closed = closed;
|
|
2235
|
+
this.voidElement = meta ? Boolean(meta.void) : false;
|
|
2236
|
+
this.depth = 0;
|
|
2237
|
+
this.annotation = null;
|
|
2238
|
+
if (parent) {
|
|
2239
|
+
parent.childNodes.push(this);
|
|
2240
|
+
/* calculate depth in domtree */
|
|
2241
|
+
let cur = parent;
|
|
2242
|
+
while (cur.parent) {
|
|
2243
|
+
this.depth++;
|
|
2244
|
+
cur = cur.parent;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2323
2247
|
}
|
|
2324
|
-
|
|
2325
|
-
*
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2248
|
+
/**
|
|
2249
|
+
* @internal
|
|
2250
|
+
*/
|
|
2251
|
+
static rootNode(location) {
|
|
2252
|
+
const root = new HtmlElement(undefined, null, exports.NodeClosed.EndTag, null, location);
|
|
2253
|
+
root.setAnnotation("#document");
|
|
2254
|
+
return root;
|
|
2329
2255
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2256
|
+
/**
|
|
2257
|
+
* @internal
|
|
2258
|
+
*
|
|
2259
|
+
* @param namespace - If given it is appended to the tagName.
|
|
2260
|
+
*/
|
|
2261
|
+
static fromTokens(startToken, endToken, parent, metaTable, namespace = "") {
|
|
2262
|
+
const name = startToken.data[2];
|
|
2263
|
+
const tagName = namespace ? `${namespace}:${name}` : name;
|
|
2264
|
+
if (!name) {
|
|
2265
|
+
throw new Error("tagName cannot be empty");
|
|
2266
|
+
}
|
|
2267
|
+
const meta = metaTable ? metaTable.getMetaFor(tagName) : null;
|
|
2268
|
+
const open = startToken.data[1] !== "/";
|
|
2269
|
+
const closed = isClosed(endToken, meta);
|
|
2270
|
+
/* location contains position of '<' so strip it out */
|
|
2271
|
+
const location = sliceLocation(startToken.location, 1);
|
|
2272
|
+
return new HtmlElement(tagName, open ? parent : null, closed, meta, location);
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Returns annotated name if set or defaults to `<tagName>`.
|
|
2276
|
+
*
|
|
2277
|
+
* E.g. `my-annotation` or `<div>`.
|
|
2278
|
+
*/
|
|
2279
|
+
get annotatedName() {
|
|
2280
|
+
if (this.annotation) {
|
|
2281
|
+
return this.annotation;
|
|
2282
|
+
}
|
|
2283
|
+
else {
|
|
2284
|
+
return `<${this.tagName}>`;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Get list of IDs referenced by `aria-labelledby`.
|
|
2289
|
+
*
|
|
2290
|
+
* If the attribute is unset or empty this getter returns null.
|
|
2291
|
+
* If the attribute is dynamic the original {@link DynamicValue} is returned.
|
|
2292
|
+
*
|
|
2293
|
+
* @public
|
|
2294
|
+
*/
|
|
2295
|
+
get ariaLabelledby() {
|
|
2296
|
+
const attr = this.getAttribute("aria-labelledby");
|
|
2297
|
+
if (!attr || !attr.value) {
|
|
2298
|
+
return null;
|
|
2299
|
+
}
|
|
2300
|
+
if (attr.value instanceof DynamicValue) {
|
|
2301
|
+
return attr.value;
|
|
2302
|
+
}
|
|
2303
|
+
const list = new DOMTokenList(attr.value, attr.valueLocation);
|
|
2304
|
+
return list.length ? Array.from(list) : null;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Similar to childNodes but only elements.
|
|
2308
|
+
*/
|
|
2309
|
+
get childElements() {
|
|
2310
|
+
return this.childNodes.filter(isElementNode);
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Find the first ancestor matching a selector.
|
|
2314
|
+
*
|
|
2315
|
+
* Implementation of DOM specification of Element.closest(selectors).
|
|
2316
|
+
*/
|
|
2317
|
+
closest(selectors) {
|
|
2318
|
+
/* eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive*/
|
|
2319
|
+
let node = this;
|
|
2320
|
+
while (node) {
|
|
2321
|
+
if (node.matches(selectors)) {
|
|
2322
|
+
return node;
|
|
2323
|
+
}
|
|
2324
|
+
node = node.parent;
|
|
2325
|
+
}
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Generate a DOM selector for this element. The returned selector will be
|
|
2330
|
+
* unique inside the current document.
|
|
2331
|
+
*/
|
|
2332
|
+
generateSelector() {
|
|
2333
|
+
/* root element cannot have a selector as it isn't a proper element */
|
|
2334
|
+
if (this.isRootElement()) {
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
const parts = [];
|
|
2338
|
+
let root;
|
|
2339
|
+
/* eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive */
|
|
2340
|
+
for (root = this; root.parent; root = root.parent) {
|
|
2341
|
+
/* .. */
|
|
2342
|
+
}
|
|
2343
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive
|
|
2344
|
+
for (let cur = this; cur.parent; cur = cur.parent) {
|
|
2345
|
+
/* if a unique id is present, use it and short-circuit */
|
|
2346
|
+
if (cur.id) {
|
|
2347
|
+
const selector = generateIdSelector(cur.id);
|
|
2348
|
+
const matches = root.querySelectorAll(selector);
|
|
2349
|
+
if (matches.length === 1) {
|
|
2350
|
+
parts.push(selector);
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
const parent = cur.parent;
|
|
2355
|
+
const child = parent.childElements;
|
|
2356
|
+
const index = child.findIndex((it) => it.unique === cur.unique);
|
|
2357
|
+
const numOfType = child.filter((it) => it.is(cur.tagName)).length;
|
|
2358
|
+
const solo = numOfType === 1;
|
|
2359
|
+
/* if this is the only tagName in this level of siblings nth-child isn't needed */
|
|
2360
|
+
if (solo) {
|
|
2361
|
+
parts.push(cur.tagName.toLowerCase());
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
/* this will generate the worst kind of selector but at least it will be accurate (optimizations welcome) */
|
|
2365
|
+
parts.push(`${cur.tagName.toLowerCase()}:nth-child(${index + 1})`);
|
|
2366
|
+
}
|
|
2367
|
+
return parts.reverse().join(" > ");
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Tests if this element has given tagname.
|
|
2371
|
+
*
|
|
2372
|
+
* If passing "*" this test will pass if any tagname is set.
|
|
2373
|
+
*/
|
|
2374
|
+
is(tagName) {
|
|
2375
|
+
return tagName === "*" || this.tagName.toLowerCase() === tagName.toLowerCase();
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Load new element metadata onto this element.
|
|
2379
|
+
*
|
|
2380
|
+
* Do note that semantics such as `void` cannot be changed (as the element has
|
|
2381
|
+
* already been created). In addition the element will still "be" the same
|
|
2382
|
+
* element, i.e. even if loading meta for a `<p>` tag upon a `<div>` tag it
|
|
2383
|
+
* will still be a `<div>` as far as the rest of the validator is concerned.
|
|
2384
|
+
*
|
|
2385
|
+
* In fact only certain properties will be copied onto the element:
|
|
2386
|
+
*
|
|
2387
|
+
* - content categories (flow, phrasing, etc)
|
|
2388
|
+
* - required attributes
|
|
2389
|
+
* - attribute allowed values
|
|
2390
|
+
* - permitted/required elements
|
|
2391
|
+
*
|
|
2392
|
+
* Properties *not* loaded:
|
|
2393
|
+
*
|
|
2394
|
+
* - inherit
|
|
2395
|
+
* - deprecated
|
|
2396
|
+
* - foreign
|
|
2397
|
+
* - void
|
|
2398
|
+
* - implicitClosed
|
|
2399
|
+
* - scriptSupporting
|
|
2400
|
+
* - deprecatedAttributes
|
|
2401
|
+
*
|
|
2402
|
+
* Changes to element metadata will only be visible after `element:ready` (and
|
|
2403
|
+
* the subsequent `dom:ready` event).
|
|
2404
|
+
*/
|
|
2405
|
+
loadMeta(meta) {
|
|
2406
|
+
if (!this.metaElement) {
|
|
2407
|
+
this.metaElement = {};
|
|
2333
2408
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
if (
|
|
2337
|
-
|
|
2409
|
+
for (const key of MetaCopyableProperty) {
|
|
2410
|
+
const value = meta[key];
|
|
2411
|
+
if (typeof value !== "undefined") {
|
|
2412
|
+
setMetaProperty(this.metaElement, key, value);
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
delete this.metaElement[key];
|
|
2338
2416
|
}
|
|
2339
2417
|
}
|
|
2340
|
-
return stripUndefined(result);
|
|
2341
|
-
}
|
|
2342
|
-
else {
|
|
2343
|
-
return stripUndefined({ ...result, ...attr });
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
function migrateAttributes(src) {
|
|
2347
|
-
var _a, _b, _c;
|
|
2348
|
-
const keys = [
|
|
2349
|
-
...Object.keys((_a = src.attributes) !== null && _a !== void 0 ? _a : {}),
|
|
2350
|
-
...((_b = src.requiredAttributes) !== null && _b !== void 0 ? _b : []),
|
|
2351
|
-
...((_c = src.deprecatedAttributes) !== null && _c !== void 0 ? _c : []),
|
|
2352
|
-
].sort();
|
|
2353
|
-
const entries = keys.map((key) => {
|
|
2354
|
-
return [key, migrateSingleAttribute(src, key)];
|
|
2355
|
-
});
|
|
2356
|
-
return Object.fromEntries(entries);
|
|
2357
|
-
}
|
|
2358
|
-
function migrateElement(src) {
|
|
2359
|
-
const result = {
|
|
2360
|
-
...src,
|
|
2361
|
-
...{
|
|
2362
|
-
formAssociated: undefined,
|
|
2363
|
-
},
|
|
2364
|
-
attributes: migrateAttributes(src),
|
|
2365
|
-
textContent: src.textContent,
|
|
2366
|
-
};
|
|
2367
|
-
/* removed properties */
|
|
2368
|
-
delete result.deprecatedAttributes;
|
|
2369
|
-
delete result.requiredAttributes;
|
|
2370
|
-
/* strip out undefined */
|
|
2371
|
-
if (!result.textContent) {
|
|
2372
|
-
delete result.textContent;
|
|
2373
|
-
}
|
|
2374
|
-
if (src.formAssociated) {
|
|
2375
|
-
result.formAssociated = {
|
|
2376
|
-
listed: Boolean(src.formAssociated.listed),
|
|
2377
|
-
};
|
|
2378
|
-
}
|
|
2379
|
-
else {
|
|
2380
|
-
delete result.formAssociated;
|
|
2381
2418
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
*/
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
return true;
|
|
2419
|
+
/**
|
|
2420
|
+
* Match this element against given selectors. Returns true if any selector
|
|
2421
|
+
* matches.
|
|
2422
|
+
*
|
|
2423
|
+
* Implementation of DOM specification of Element.matches(selectors).
|
|
2424
|
+
*/
|
|
2425
|
+
matches(selector) {
|
|
2426
|
+
/* find root element */
|
|
2427
|
+
/* eslint-disable-next-line @typescript-eslint/no-this-alias -- false positive */
|
|
2428
|
+
let root = this;
|
|
2429
|
+
while (root.parent) {
|
|
2430
|
+
root = root.parent;
|
|
2395
2431
|
}
|
|
2396
|
-
|
|
2432
|
+
/* a bit slow implementation as it finds all candidates for the selector and
|
|
2433
|
+
* then tests if any of them are the current element. A better
|
|
2434
|
+
* implementation would be to walk the selector right-to-left and test
|
|
2435
|
+
* ancestors. */
|
|
2436
|
+
for (const match of root.querySelectorAll(selector)) {
|
|
2437
|
+
if (match.unique === this.unique) {
|
|
2438
|
+
return true;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return false;
|
|
2397
2442
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
/**
|
|
2402
|
-
* Returns true if given element has given attribute (no matter the value, null,
|
|
2403
|
-
* dynamic, etc).
|
|
2404
|
-
*/
|
|
2405
|
-
function hasAttribute(node, attr) {
|
|
2406
|
-
return node.hasAttribute(attr);
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
/**
|
|
2410
|
-
* Matches attribute against value.
|
|
2411
|
-
*/
|
|
2412
|
-
function matchAttribute(node, key, op, value) {
|
|
2413
|
-
const nodeValue = (node.getAttributeValue(key) || "").toLowerCase();
|
|
2414
|
-
switch (op) {
|
|
2415
|
-
case "!=":
|
|
2416
|
-
return nodeValue !== value;
|
|
2417
|
-
case "=":
|
|
2418
|
-
return nodeValue === value;
|
|
2443
|
+
get meta() {
|
|
2444
|
+
return this.metaElement;
|
|
2419
2445
|
}
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
const dynamicKeys = [
|
|
2423
|
-
"metadata",
|
|
2424
|
-
"flow",
|
|
2425
|
-
"sectioning",
|
|
2426
|
-
"heading",
|
|
2427
|
-
"phrasing",
|
|
2428
|
-
"embedded",
|
|
2429
|
-
"interactive",
|
|
2430
|
-
"labelable",
|
|
2431
|
-
];
|
|
2432
|
-
const functionTable = {
|
|
2433
|
-
isDescendant: isDescendantFacade,
|
|
2434
|
-
hasAttribute: hasAttributeFacade,
|
|
2435
|
-
matchAttribute: matchAttributeFacade,
|
|
2436
|
-
};
|
|
2437
|
-
const schemaCache = new Map();
|
|
2438
|
-
function clone(src) {
|
|
2439
|
-
return JSON.parse(JSON.stringify(src));
|
|
2440
|
-
}
|
|
2441
|
-
function overwriteMerge$1(a, b) {
|
|
2442
|
-
return b;
|
|
2443
|
-
}
|
|
2444
|
-
/**
|
|
2445
|
-
* @public
|
|
2446
|
-
*/
|
|
2447
|
-
class MetaTable {
|
|
2448
2446
|
/**
|
|
2449
|
-
*
|
|
2447
|
+
* Set annotation for this element.
|
|
2450
2448
|
*/
|
|
2451
|
-
|
|
2452
|
-
this.
|
|
2453
|
-
this.schema = clone(schema);
|
|
2449
|
+
setAnnotation(text) {
|
|
2450
|
+
this.annotation = text;
|
|
2454
2451
|
}
|
|
2455
2452
|
/**
|
|
2456
|
-
*
|
|
2453
|
+
* Set attribute. Stores all attributes set even with the same name.
|
|
2454
|
+
*
|
|
2455
|
+
* @param key - Attribute name
|
|
2456
|
+
* @param value - Attribute value. Use `null` if no value is present.
|
|
2457
|
+
* @param keyLocation - Location of the attribute name.
|
|
2458
|
+
* @param valueLocation - Location of the attribute value (excluding quotation)
|
|
2459
|
+
* @param originalAttribute - If attribute is an alias for another attribute
|
|
2460
|
+
* (dynamic attributes) set this to the original attribute name.
|
|
2457
2461
|
*/
|
|
2458
|
-
|
|
2459
|
-
|
|
2462
|
+
setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
|
|
2463
|
+
key = key.toLowerCase();
|
|
2464
|
+
if (!this.attr[key]) {
|
|
2465
|
+
this.attr[key] = [];
|
|
2466
|
+
}
|
|
2467
|
+
this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
|
|
2460
2468
|
}
|
|
2461
2469
|
/**
|
|
2462
|
-
*
|
|
2463
|
-
*
|
|
2464
|
-
* @public
|
|
2470
|
+
* Get a list of all attributes on this node.
|
|
2465
2471
|
*/
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2472
|
+
get attributes() {
|
|
2473
|
+
return Object.values(this.attr).reduce((result, cur) => {
|
|
2474
|
+
return result.concat(cur);
|
|
2475
|
+
}, []);
|
|
2476
|
+
}
|
|
2477
|
+
hasAttribute(key) {
|
|
2478
|
+
key = key.toLowerCase();
|
|
2479
|
+
return key in this.attr;
|
|
2480
|
+
}
|
|
2481
|
+
getAttribute(key, all = false) {
|
|
2482
|
+
key = key.toLowerCase();
|
|
2483
|
+
if (key in this.attr) {
|
|
2484
|
+
const matches = this.attr[key];
|
|
2485
|
+
return all ? matches : matches[0];
|
|
2475
2486
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
definitions: patch.definitions,
|
|
2479
|
-
});
|
|
2487
|
+
else {
|
|
2488
|
+
return null;
|
|
2480
2489
|
}
|
|
2481
2490
|
}
|
|
2482
2491
|
/**
|
|
2483
|
-
*
|
|
2492
|
+
* Get attribute value.
|
|
2484
2493
|
*
|
|
2485
|
-
*
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2494
|
+
* Returns the attribute value if present.
|
|
2495
|
+
*
|
|
2496
|
+
* - Missing attributes return `null`.
|
|
2497
|
+
* - Boolean attributes return `null`.
|
|
2498
|
+
* - `DynamicValue` returns attribute expression.
|
|
2499
|
+
*
|
|
2500
|
+
* @param key - Attribute name
|
|
2501
|
+
* @returns Attribute value or null.
|
|
2488
2502
|
*/
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
if (!validate(obj)) {
|
|
2494
|
-
throw new SchemaValidationError(filename, `Element metadata is not valid`, obj, this.schema,
|
|
2495
|
-
/* istanbul ignore next: AJV sets .errors when validate returns false */
|
|
2496
|
-
(_a = validate.errors) !== null && _a !== void 0 ? _a : []);
|
|
2497
|
-
}
|
|
2498
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
2499
|
-
if (key === "$schema")
|
|
2500
|
-
continue;
|
|
2501
|
-
this.addEntry(key, migrateElement(value));
|
|
2502
|
-
}
|
|
2503
|
+
getAttributeValue(key) {
|
|
2504
|
+
const attr = this.getAttribute(key);
|
|
2505
|
+
if (attr) {
|
|
2506
|
+
return attr.value !== null ? attr.value.toString() : null;
|
|
2503
2507
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
err.filename = filename;
|
|
2507
|
-
throw err;
|
|
2508
|
-
}
|
|
2509
|
-
if (err instanceof SchemaValidationError) {
|
|
2510
|
-
throw err;
|
|
2511
|
-
}
|
|
2512
|
-
if (!filename) {
|
|
2513
|
-
throw err;
|
|
2514
|
-
}
|
|
2515
|
-
throw new UserError(`Failed to load element metadata from "${filename}"`, ensureError(err));
|
|
2508
|
+
else {
|
|
2509
|
+
return null;
|
|
2516
2510
|
}
|
|
2517
2511
|
}
|
|
2518
2512
|
/**
|
|
2519
|
-
*
|
|
2520
|
-
* the global metadata is returned or null if no global is present.
|
|
2513
|
+
* Add text as a child node to this element.
|
|
2521
2514
|
*
|
|
2522
|
-
* @
|
|
2523
|
-
* @
|
|
2515
|
+
* @param text - Text to add.
|
|
2516
|
+
* @param location - Source code location of this text.
|
|
2524
2517
|
*/
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
tagName = tagName.toLowerCase();
|
|
2528
|
-
if (this.elements[tagName]) {
|
|
2529
|
-
return { ...this.elements[tagName] };
|
|
2530
|
-
}
|
|
2531
|
-
/* try to locate global element */
|
|
2532
|
-
if (this.elements["*"]) {
|
|
2533
|
-
return { ...this.elements["*"] };
|
|
2534
|
-
}
|
|
2535
|
-
return null;
|
|
2518
|
+
appendText(text, location) {
|
|
2519
|
+
this.childNodes.push(new TextNode(text, location));
|
|
2536
2520
|
}
|
|
2537
2521
|
/**
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
* @public
|
|
2522
|
+
* Return a list of all known classes on the element. Dynamic values are
|
|
2523
|
+
* ignored.
|
|
2541
2524
|
*/
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2525
|
+
get classList() {
|
|
2526
|
+
if (!this.hasAttribute("class")) {
|
|
2527
|
+
return new DOMTokenList(null, null);
|
|
2528
|
+
}
|
|
2529
|
+
const classes = this.getAttribute("class", true)
|
|
2530
|
+
.filter((attr) => attr.isStatic)
|
|
2531
|
+
.map((attr) => attr.value)
|
|
2532
|
+
.join(" ");
|
|
2533
|
+
return new DOMTokenList(classes, null);
|
|
2546
2534
|
}
|
|
2547
2535
|
/**
|
|
2548
|
-
*
|
|
2549
|
-
*
|
|
2550
|
-
* @public
|
|
2536
|
+
* Get element ID if present.
|
|
2551
2537
|
*/
|
|
2552
|
-
|
|
2553
|
-
return
|
|
2554
|
-
.filter(([key, entry]) => key === tagName || entry.inherit === tagName)
|
|
2555
|
-
.map(([tagName]) => tagName);
|
|
2538
|
+
get id() {
|
|
2539
|
+
return this.getAttributeValue("id");
|
|
2556
2540
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
if (entry.inherit) {
|
|
2561
|
-
const name = entry.inherit;
|
|
2562
|
-
parent = this.elements[name];
|
|
2563
|
-
if (!parent) {
|
|
2564
|
-
throw new InheritError({
|
|
2565
|
-
tagName,
|
|
2566
|
-
inherit: name,
|
|
2567
|
-
});
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
/* merge all sources together */
|
|
2571
|
-
const expanded = this.mergeElement(parent, { ...entry, tagName });
|
|
2572
|
-
expandRegex(expanded);
|
|
2573
|
-
this.elements[tagName] = expanded;
|
|
2541
|
+
get style() {
|
|
2542
|
+
const attr = this.getAttribute("style");
|
|
2543
|
+
return parseCssDeclaration(attr === null || attr === void 0 ? void 0 : attr.value);
|
|
2574
2544
|
}
|
|
2575
2545
|
/**
|
|
2576
|
-
*
|
|
2546
|
+
* Returns the first child element or null if there are no child elements.
|
|
2577
2547
|
*/
|
|
2578
|
-
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2548
|
+
get firstElementChild() {
|
|
2549
|
+
const children = this.childElements;
|
|
2550
|
+
return children.length > 0 ? children[0] : null;
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Returns the last child element or null if there are no child elements.
|
|
2554
|
+
*/
|
|
2555
|
+
get lastElementChild() {
|
|
2556
|
+
const children = this.childElements;
|
|
2557
|
+
return children.length > 0 ? children[children.length - 1] : null;
|
|
2558
|
+
}
|
|
2559
|
+
get siblings() {
|
|
2560
|
+
return this.parent ? this.parent.childElements : [this];
|
|
2561
|
+
}
|
|
2562
|
+
get previousSibling() {
|
|
2563
|
+
const i = this.siblings.findIndex((node) => node.unique === this.unique);
|
|
2564
|
+
return i >= 1 ? this.siblings[i - 1] : null;
|
|
2565
|
+
}
|
|
2566
|
+
get nextSibling() {
|
|
2567
|
+
const i = this.siblings.findIndex((node) => node.unique === this.unique);
|
|
2568
|
+
return i <= this.siblings.length - 2 ? this.siblings[i + 1] : null;
|
|
2569
|
+
}
|
|
2570
|
+
getElementsByTagName(tagName) {
|
|
2571
|
+
return this.childElements.reduce((matches, node) => {
|
|
2572
|
+
return matches.concat(node.is(tagName) ? [node] : [], node.getElementsByTagName(tagName));
|
|
2573
|
+
}, []);
|
|
2574
|
+
}
|
|
2575
|
+
querySelector(selector) {
|
|
2576
|
+
const it = this.querySelectorImpl(selector);
|
|
2577
|
+
const next = it.next();
|
|
2578
|
+
if (next.done) {
|
|
2579
|
+
return null;
|
|
2583
2580
|
}
|
|
2584
2581
|
else {
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2582
|
+
return next.value;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
querySelectorAll(selector) {
|
|
2586
|
+
const it = this.querySelectorImpl(selector);
|
|
2587
|
+
const unique = new Set(it);
|
|
2588
|
+
return Array.from(unique.values());
|
|
2589
|
+
}
|
|
2590
|
+
*querySelectorImpl(selectorList) {
|
|
2591
|
+
if (!selectorList) {
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
for (const selector of selectorList.split(/,\s*/)) {
|
|
2595
|
+
const pattern = new Selector(selector);
|
|
2596
|
+
yield* pattern.match(this);
|
|
2593
2597
|
}
|
|
2594
2598
|
}
|
|
2595
2599
|
/**
|
|
2596
|
-
*
|
|
2600
|
+
* Visit all nodes from this node and down. Depth first.
|
|
2601
|
+
*
|
|
2602
|
+
* @internal
|
|
2597
2603
|
*/
|
|
2598
|
-
|
|
2599
|
-
|
|
2604
|
+
visitDepthFirst(callback) {
|
|
2605
|
+
function visit(node) {
|
|
2606
|
+
node.childElements.forEach(visit);
|
|
2607
|
+
if (!node.isRootElement()) {
|
|
2608
|
+
callback(node);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
visit(this);
|
|
2600
2612
|
}
|
|
2601
2613
|
/**
|
|
2602
|
-
*
|
|
2603
|
-
*
|
|
2614
|
+
* Evaluates callbackk on all descendants, returning true if any are true.
|
|
2615
|
+
*
|
|
2616
|
+
* @internal
|
|
2604
2617
|
*/
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
delete global.tagName;
|
|
2615
|
-
delete global.void;
|
|
2616
|
-
/* merge elements */
|
|
2617
|
-
for (const [tagName, entry] of Object.entries(this.elements)) {
|
|
2618
|
-
this.elements[tagName] = this.mergeElement(global, entry);
|
|
2618
|
+
someChildren(callback) {
|
|
2619
|
+
return this.childElements.some(visit);
|
|
2620
|
+
function visit(node) {
|
|
2621
|
+
if (callback(node)) {
|
|
2622
|
+
return true;
|
|
2623
|
+
}
|
|
2624
|
+
else {
|
|
2625
|
+
return node.childElements.some(visit);
|
|
2626
|
+
}
|
|
2619
2627
|
}
|
|
2620
2628
|
}
|
|
2621
|
-
mergeElement(a, b) {
|
|
2622
|
-
const merged = deepmerge__default.default(a, b, { arrayMerge: overwriteMerge$1 });
|
|
2623
|
-
/* special handling when removing attributes by setting them to null
|
|
2624
|
-
* resulting in the deletion flag being set */
|
|
2625
|
-
const filteredAttrs = Object.entries(merged.attributes).filter(([, attr]) => {
|
|
2626
|
-
const val = !attr.delete;
|
|
2627
|
-
delete attr.delete;
|
|
2628
|
-
return val;
|
|
2629
|
-
});
|
|
2630
|
-
merged.attributes = Object.fromEntries(filteredAttrs);
|
|
2631
|
-
return merged;
|
|
2632
|
-
}
|
|
2633
2629
|
/**
|
|
2630
|
+
* Evaluates callbackk on all descendants, returning true if all are true.
|
|
2631
|
+
*
|
|
2634
2632
|
* @internal
|
|
2635
2633
|
*/
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2634
|
+
everyChildren(callback) {
|
|
2635
|
+
return this.childElements.every(visit);
|
|
2636
|
+
function visit(node) {
|
|
2637
|
+
if (!callback(node)) {
|
|
2638
|
+
return false;
|
|
2639
|
+
}
|
|
2640
|
+
return node.childElements.every(visit);
|
|
2639
2641
|
}
|
|
2640
2642
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2643
|
+
/**
|
|
2644
|
+
* Visit all nodes from this node and down. Breadth first.
|
|
2645
|
+
*
|
|
2646
|
+
* The first node for which the callback evaluates to true is returned.
|
|
2647
|
+
*
|
|
2648
|
+
* @internal
|
|
2649
|
+
*/
|
|
2650
|
+
find(callback) {
|
|
2651
|
+
function visit(node) {
|
|
2652
|
+
if (callback(node)) {
|
|
2653
|
+
return node;
|
|
2654
|
+
}
|
|
2655
|
+
for (const child of node.childElements) {
|
|
2656
|
+
const match = child.find(callback);
|
|
2657
|
+
if (match) {
|
|
2658
|
+
return match;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return null;
|
|
2647
2662
|
}
|
|
2663
|
+
return visit(this);
|
|
2648
2664
|
}
|
|
2649
2665
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
function expandRegexValue(value) {
|
|
2655
|
-
if (value instanceof RegExp) {
|
|
2656
|
-
return value;
|
|
2657
|
-
}
|
|
2658
|
-
const match = value.match(/^\/\^?([^/$]*)\$?\/([i]*)$/);
|
|
2659
|
-
if (match) {
|
|
2660
|
-
const [, expr, flags] = match;
|
|
2661
|
-
// eslint-disable-next-line security/detect-non-literal-regexp -- expected to be regexp
|
|
2662
|
-
return new RegExp(`^${expr}$`, flags);
|
|
2663
|
-
}
|
|
2664
|
-
else {
|
|
2665
|
-
return value;
|
|
2666
|
+
function isClosed(endToken, meta) {
|
|
2667
|
+
let closed = exports.NodeClosed.Open;
|
|
2668
|
+
if (meta && meta.void) {
|
|
2669
|
+
closed = exports.NodeClosed.VoidOmitted;
|
|
2666
2670
|
}
|
|
2671
|
+
if (endToken.data[0] === "/>") {
|
|
2672
|
+
closed = exports.NodeClosed.VoidSelfClosed;
|
|
2673
|
+
}
|
|
2674
|
+
return closed;
|
|
2667
2675
|
}
|
|
2676
|
+
|
|
2668
2677
|
/**
|
|
2669
|
-
*
|
|
2678
|
+
* @public
|
|
2670
2679
|
*/
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2680
|
+
class DOMTree {
|
|
2681
|
+
constructor(location) {
|
|
2682
|
+
this.root = HtmlElement.rootNode(location);
|
|
2683
|
+
this.active = this.root;
|
|
2684
|
+
this.doctype = null;
|
|
2676
2685
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
const [func, options] = parseExpression(expr);
|
|
2680
|
-
return func(node, options);
|
|
2681
|
-
}
|
|
2682
|
-
function parseExpression(expr) {
|
|
2683
|
-
if (typeof expr === "string") {
|
|
2684
|
-
return parseExpression([expr, {}]);
|
|
2686
|
+
pushActive(node) {
|
|
2687
|
+
this.active = node;
|
|
2685
2688
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
if (!func) {
|
|
2691
|
-
throw new Error(`Failed to find function "${funcName}" when evaluating property expression`);
|
|
2689
|
+
popActive() {
|
|
2690
|
+
if (this.active.isRootElement()) {
|
|
2691
|
+
/* root element should never be popped, continue as if nothing happened */
|
|
2692
|
+
return;
|
|
2692
2693
|
}
|
|
2693
|
-
|
|
2694
|
+
this.active = this.active.parent || this.root;
|
|
2694
2695
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
if (typeof tagName !== "string") {
|
|
2698
|
-
throw new Error(`Property expression "isDescendant" must take string argument when evaluating metadata for <${node.tagName}>`);
|
|
2696
|
+
getActive() {
|
|
2697
|
+
return this.active;
|
|
2699
2698
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2699
|
+
/**
|
|
2700
|
+
* Resolve dynamic meta expressions.
|
|
2701
|
+
*/
|
|
2702
|
+
resolveMeta(table) {
|
|
2703
|
+
this.visitDepthFirst((node) => table.resolve(node));
|
|
2705
2704
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
function matchAttributeFacade(node, match) {
|
|
2709
|
-
if (!Array.isArray(match) || match.length !== 3) {
|
|
2710
|
-
throw new Error(`Property expression "matchAttribute" must take [key, op, value] array as argument when evaluating metadata for <${node.tagName}>`);
|
|
2705
|
+
getElementsByTagName(tagName) {
|
|
2706
|
+
return this.root.getElementsByTagName(tagName);
|
|
2711
2707
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2708
|
+
visitDepthFirst(callback) {
|
|
2709
|
+
this.root.visitDepthFirst(callback);
|
|
2710
|
+
}
|
|
2711
|
+
find(callback) {
|
|
2712
|
+
return this.root.find(callback);
|
|
2713
|
+
}
|
|
2714
|
+
querySelector(selector) {
|
|
2715
|
+
return this.root.querySelector(selector);
|
|
2716
|
+
}
|
|
2717
|
+
querySelectorAll(selector) {
|
|
2718
|
+
return this.root.querySelectorAll(selector);
|
|
2719
2719
|
}
|
|
2720
2720
|
}
|
|
2721
2721
|
|
|
@@ -3233,6 +3233,275 @@ function interpolate(text, data) {
|
|
|
3233
3233
|
});
|
|
3234
3234
|
}
|
|
3235
3235
|
|
|
3236
|
+
const patternCache = new Map();
|
|
3237
|
+
function compileStringPattern(pattern) {
|
|
3238
|
+
const regexp = pattern.replace(/[*]+/g, ".+");
|
|
3239
|
+
/* eslint-disable-next-line security/detect-non-literal-regexp -- technical debt, should do input sanitation and precompilation */
|
|
3240
|
+
return new RegExp(`^${regexp}$`);
|
|
3241
|
+
}
|
|
3242
|
+
function compileRegExpPattern(pattern) {
|
|
3243
|
+
/* eslint-disable-next-line security/detect-non-literal-regexp -- technical debt, should do input sanitation and precompilation */
|
|
3244
|
+
return new RegExp(`^${pattern}$`);
|
|
3245
|
+
}
|
|
3246
|
+
function compilePattern(pattern) {
|
|
3247
|
+
const cached = patternCache.get(pattern);
|
|
3248
|
+
if (cached) {
|
|
3249
|
+
return cached;
|
|
3250
|
+
}
|
|
3251
|
+
const match = pattern.match(/^\/(.*)\/$/);
|
|
3252
|
+
const regexp = match ? compileRegExpPattern(match[1]) : compileStringPattern(pattern);
|
|
3253
|
+
patternCache.set(pattern, regexp);
|
|
3254
|
+
return regexp;
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* @internal
|
|
3258
|
+
*/
|
|
3259
|
+
function keywordPatternMatcher(list, keyword) {
|
|
3260
|
+
for (const pattern of list) {
|
|
3261
|
+
const regexp = compilePattern(pattern);
|
|
3262
|
+
if (regexp.test(keyword)) {
|
|
3263
|
+
return true;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return false;
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* @internal
|
|
3270
|
+
*/
|
|
3271
|
+
function isKeywordIgnored(options, keyword, matcher = (list, it) => list.includes(it)) {
|
|
3272
|
+
const { include, exclude } = options;
|
|
3273
|
+
/* ignore keyword if not present in "include" */
|
|
3274
|
+
if (include && !matcher(include, keyword)) {
|
|
3275
|
+
return true;
|
|
3276
|
+
}
|
|
3277
|
+
/* ignore keyword if present in "excludes" */
|
|
3278
|
+
if (exclude && matcher(exclude, keyword)) {
|
|
3279
|
+
return true;
|
|
3280
|
+
}
|
|
3281
|
+
return false;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
3285
|
+
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
3286
|
+
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
3287
|
+
/**
|
|
3288
|
+
* Tests if this element is present in the accessibility tree.
|
|
3289
|
+
*
|
|
3290
|
+
* In practice it tests whenever the element or its parents has
|
|
3291
|
+
* `role="presentation"` or `aria-hidden="false"`. Dynamic values counts as
|
|
3292
|
+
* visible since the element might be in the visibility tree sometimes.
|
|
3293
|
+
*/
|
|
3294
|
+
function inAccessibilityTree(node) {
|
|
3295
|
+
return !isAriaHidden(node) && !isPresentation(node);
|
|
3296
|
+
}
|
|
3297
|
+
function isAriaHiddenImpl(node) {
|
|
3298
|
+
const isHidden = (node) => {
|
|
3299
|
+
const ariaHidden = node.getAttribute("aria-hidden");
|
|
3300
|
+
return Boolean(ariaHidden && ariaHidden.value === "true");
|
|
3301
|
+
};
|
|
3302
|
+
return {
|
|
3303
|
+
byParent: node.parent ? isAriaHidden(node.parent) : false,
|
|
3304
|
+
bySelf: isHidden(node),
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
function isAriaHidden(node, details) {
|
|
3308
|
+
const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
|
|
3309
|
+
if (cached) {
|
|
3310
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
3311
|
+
}
|
|
3312
|
+
const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
|
|
3313
|
+
return details ? result : result.byParent || result.bySelf;
|
|
3314
|
+
}
|
|
3315
|
+
function isHTMLHiddenImpl(node) {
|
|
3316
|
+
const isHidden = (node) => {
|
|
3317
|
+
const hidden = node.getAttribute("hidden");
|
|
3318
|
+
return hidden !== null && hidden.isStatic;
|
|
3319
|
+
};
|
|
3320
|
+
return {
|
|
3321
|
+
byParent: node.parent ? isHTMLHidden(node.parent) : false,
|
|
3322
|
+
bySelf: isHidden(node),
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
function isHTMLHidden(node, details) {
|
|
3326
|
+
const cached = node.cacheGet(HTML_HIDDEN_CACHE);
|
|
3327
|
+
if (cached) {
|
|
3328
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
3329
|
+
}
|
|
3330
|
+
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
3331
|
+
return details ? result : result.byParent || result.bySelf;
|
|
3332
|
+
}
|
|
3333
|
+
/**
|
|
3334
|
+
* Tests if this element or a parent element has role="presentation".
|
|
3335
|
+
*
|
|
3336
|
+
* Dynamic values yields `false` just as if the attribute wasn't present.
|
|
3337
|
+
*/
|
|
3338
|
+
function isPresentation(node) {
|
|
3339
|
+
if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
|
|
3340
|
+
return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
|
|
3341
|
+
}
|
|
3342
|
+
let cur = node;
|
|
3343
|
+
do {
|
|
3344
|
+
const role = cur.getAttribute("role");
|
|
3345
|
+
/* role="presentation" */
|
|
3346
|
+
if (role && role.value === "presentation") {
|
|
3347
|
+
return cur.cacheSet(ROLE_PRESENTATION_CACHE, true);
|
|
3348
|
+
}
|
|
3349
|
+
/* sanity check: break if no parent is present, normally not an issue as the
|
|
3350
|
+
* root element should be found first */
|
|
3351
|
+
if (!cur.parent) {
|
|
3352
|
+
break;
|
|
3353
|
+
}
|
|
3354
|
+
/* check parents */
|
|
3355
|
+
cur = cur.parent;
|
|
3356
|
+
} while (!cur.isRootElement());
|
|
3357
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
const cachePrefix = classifyNodeText.name;
|
|
3361
|
+
const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
|
|
3362
|
+
const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
|
|
3363
|
+
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
3364
|
+
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
3365
|
+
/**
|
|
3366
|
+
* @public
|
|
3367
|
+
*/
|
|
3368
|
+
exports.TextClassification = void 0;
|
|
3369
|
+
(function (TextClassification) {
|
|
3370
|
+
TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
3371
|
+
TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
3372
|
+
TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
3373
|
+
})(exports.TextClassification || (exports.TextClassification = {}));
|
|
3374
|
+
/**
|
|
3375
|
+
* @internal
|
|
3376
|
+
*/
|
|
3377
|
+
function getCachekey(options) {
|
|
3378
|
+
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
3379
|
+
if (accessible && ignoreHiddenRoot) {
|
|
3380
|
+
return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
|
|
3381
|
+
}
|
|
3382
|
+
else if (ignoreHiddenRoot) {
|
|
3383
|
+
return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
|
|
3384
|
+
}
|
|
3385
|
+
else if (accessible) {
|
|
3386
|
+
return A11Y_CACHE_KEY;
|
|
3387
|
+
}
|
|
3388
|
+
else {
|
|
3389
|
+
return HTML_CACHE_KEY;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
/* While I cannot find a reference about this in the standard the <select>
|
|
3393
|
+
* element kinda acts as if there is no text content, most particularly it
|
|
3394
|
+
* doesn't receive and accessible name. The `.textContent` property does
|
|
3395
|
+
* however include the <option> childrens text. But for the sake of the
|
|
3396
|
+
* validator it is probably best if the classification acts as if there is no
|
|
3397
|
+
* text as I think that is what is expected of the return values. Might have
|
|
3398
|
+
* to revisit this at some point or if someone could clarify what section of
|
|
3399
|
+
* the standard deals with this. */
|
|
3400
|
+
function isSpecialEmpty(node) {
|
|
3401
|
+
return node.is("select") || node.is("textarea");
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Checks text content of an element.
|
|
3405
|
+
*
|
|
3406
|
+
* Any text is considered including text from descendant elements. Whitespace is
|
|
3407
|
+
* ignored.
|
|
3408
|
+
*
|
|
3409
|
+
* If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
|
|
3410
|
+
*
|
|
3411
|
+
* @public
|
|
3412
|
+
*/
|
|
3413
|
+
function classifyNodeText(node, options = {}) {
|
|
3414
|
+
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
3415
|
+
const cacheKey = getCachekey(options);
|
|
3416
|
+
if (node.cacheExists(cacheKey)) {
|
|
3417
|
+
return node.cacheGet(cacheKey);
|
|
3418
|
+
}
|
|
3419
|
+
if (!ignoreHiddenRoot && isHTMLHidden(node)) {
|
|
3420
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
3421
|
+
}
|
|
3422
|
+
if (!ignoreHiddenRoot && accessible && isAriaHidden(node)) {
|
|
3423
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
3424
|
+
}
|
|
3425
|
+
if (isSpecialEmpty(node)) {
|
|
3426
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
3427
|
+
}
|
|
3428
|
+
const text = findTextNodes(node, {
|
|
3429
|
+
...options,
|
|
3430
|
+
ignoreHiddenRoot: false,
|
|
3431
|
+
});
|
|
3432
|
+
/* if any text is dynamic classify as dynamic */
|
|
3433
|
+
if (text.some((cur) => cur.isDynamic)) {
|
|
3434
|
+
return node.cacheSet(cacheKey, exports.TextClassification.DYNAMIC_TEXT);
|
|
3435
|
+
}
|
|
3436
|
+
/* if any text has non-whitespace character classify as static */
|
|
3437
|
+
if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
|
|
3438
|
+
return node.cacheSet(cacheKey, exports.TextClassification.STATIC_TEXT);
|
|
3439
|
+
}
|
|
3440
|
+
/* default to empty */
|
|
3441
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
3442
|
+
}
|
|
3443
|
+
function findTextNodes(node, options) {
|
|
3444
|
+
const { accessible = false } = options;
|
|
3445
|
+
let text = [];
|
|
3446
|
+
for (const child of node.childNodes) {
|
|
3447
|
+
if (isTextNode(child)) {
|
|
3448
|
+
text.push(child);
|
|
3449
|
+
}
|
|
3450
|
+
else if (isElementNode(child)) {
|
|
3451
|
+
if (isHTMLHidden(child, true).bySelf) {
|
|
3452
|
+
continue;
|
|
3453
|
+
}
|
|
3454
|
+
if (accessible && isAriaHidden(child, true).bySelf) {
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
text = text.concat(findTextNodes(child, options));
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
return text;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function hasAltText(image) {
|
|
3464
|
+
const alt = image.getAttribute("alt");
|
|
3465
|
+
/* missing or boolean */
|
|
3466
|
+
if (alt === null || alt.value === null) {
|
|
3467
|
+
return false;
|
|
3468
|
+
}
|
|
3469
|
+
return alt.isDynamic || alt.value.toString() !== "";
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
function hasAriaLabel(node) {
|
|
3473
|
+
const label = node.getAttribute("aria-label");
|
|
3474
|
+
/* missing or boolean */
|
|
3475
|
+
if (label === null || label.value === null) {
|
|
3476
|
+
return false;
|
|
3477
|
+
}
|
|
3478
|
+
return label.isDynamic || label.value.toString() !== "";
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
/**
|
|
3482
|
+
* Partition an array to two new lists based on the result of a
|
|
3483
|
+
* predicate. Similar to `Array.filter` but returns both matching and
|
|
3484
|
+
* non-matching in the same call.
|
|
3485
|
+
*
|
|
3486
|
+
* Elements matching the predicate is placed in the first array and elements not
|
|
3487
|
+
* matching is placed in the second.
|
|
3488
|
+
*
|
|
3489
|
+
* @public
|
|
3490
|
+
* @param values - The array of values to partition.
|
|
3491
|
+
* @param predicate - A predicate function taking a single element and returning
|
|
3492
|
+
* a boolean.
|
|
3493
|
+
* @returns - Two arrays where the first contains all elements where the
|
|
3494
|
+
* predicate matched and second contains the rest of the elements.
|
|
3495
|
+
*/
|
|
3496
|
+
function partition(values, predicate) {
|
|
3497
|
+
const initial = [[], []];
|
|
3498
|
+
return values.reduce((accumulator, value, index) => {
|
|
3499
|
+
const match = predicate(value, index, values);
|
|
3500
|
+
accumulator[match ? 0 : 1].push(value);
|
|
3501
|
+
return accumulator;
|
|
3502
|
+
}, initial);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3236
3505
|
const remapEvents = {
|
|
3237
3506
|
"tag:open": "tag:start",
|
|
3238
3507
|
"tag:close": "tag:end",
|
|
@@ -3386,7 +3655,7 @@ class Rule {
|
|
|
3386
3655
|
* `exclude`.
|
|
3387
3656
|
*/
|
|
3388
3657
|
isKeywordIgnored(keyword, matcher = (list, it) => list.includes(it)) {
|
|
3389
|
-
return
|
|
3658
|
+
return isKeywordIgnored(this.options, keyword, matcher);
|
|
3390
3659
|
}
|
|
3391
3660
|
/**
|
|
3392
3661
|
* Get [[MetaElement]] for the given tag. If no specific metadata is present
|
|
@@ -3950,6 +4219,60 @@ class ConfigError extends UserError {
|
|
|
3950
4219
|
}
|
|
3951
4220
|
}
|
|
3952
4221
|
|
|
4222
|
+
/**
|
|
4223
|
+
* Represents casing for a name, e.g. lowercase, uppercase, etc.
|
|
4224
|
+
*/
|
|
4225
|
+
class CaseStyle {
|
|
4226
|
+
/**
|
|
4227
|
+
* @param style - Name of a valid case style.
|
|
4228
|
+
*/
|
|
4229
|
+
constructor(style, ruleId) {
|
|
4230
|
+
if (!Array.isArray(style)) {
|
|
4231
|
+
style = [style];
|
|
4232
|
+
}
|
|
4233
|
+
if (style.length === 0) {
|
|
4234
|
+
throw new ConfigError(`Missing style for ${ruleId} rule`);
|
|
4235
|
+
}
|
|
4236
|
+
this.styles = this.parseStyle(style, ruleId);
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* Test if a text matches this case style.
|
|
4240
|
+
*/
|
|
4241
|
+
match(text) {
|
|
4242
|
+
return this.styles.some((style) => text.match(style.pattern));
|
|
4243
|
+
}
|
|
4244
|
+
get name() {
|
|
4245
|
+
const names = this.styles.map((style) => style.name);
|
|
4246
|
+
switch (this.styles.length) {
|
|
4247
|
+
case 1:
|
|
4248
|
+
return names[0];
|
|
4249
|
+
case 2:
|
|
4250
|
+
return names.join(" or ");
|
|
4251
|
+
default: {
|
|
4252
|
+
const last = names.slice(-1);
|
|
4253
|
+
const rest = names.slice(0, -1);
|
|
4254
|
+
return `${rest.join(", ")} or ${last[0]}`;
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
parseStyle(style, ruleId) {
|
|
4259
|
+
return style.map((cur) => {
|
|
4260
|
+
switch (cur.toLowerCase()) {
|
|
4261
|
+
case "lowercase":
|
|
4262
|
+
return { pattern: /^[a-z]*$/, name: "lowercase" };
|
|
4263
|
+
case "uppercase":
|
|
4264
|
+
return { pattern: /^[A-Z]*$/, name: "uppercase" };
|
|
4265
|
+
case "pascalcase":
|
|
4266
|
+
return { pattern: /^[A-Z][A-Za-z]*$/, name: "PascalCase" };
|
|
4267
|
+
case "camelcase":
|
|
4268
|
+
return { pattern: /^[a-z][A-Za-z]*$/, name: "camelCase" };
|
|
4269
|
+
default:
|
|
4270
|
+
throw new ConfigError(`Invalid style "${cur}" for ${ruleId} rule`);
|
|
4271
|
+
}
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
|
|
3953
4276
|
const defaults$t = {
|
|
3954
4277
|
style: "lowercase",
|
|
3955
4278
|
ignoreForeign: true,
|
|
@@ -3957,7 +4280,7 @@ const defaults$t = {
|
|
|
3957
4280
|
class AttrCase extends Rule {
|
|
3958
4281
|
constructor(options) {
|
|
3959
4282
|
super({ ...defaults$t, ...options });
|
|
3960
|
-
this.style = new
|
|
4283
|
+
this.style = new CaseStyle(this.options.style, "attr-case");
|
|
3961
4284
|
}
|
|
3962
4285
|
static schema() {
|
|
3963
4286
|
const styleEnum = ["lowercase", "uppercase", "pascalcase", "camelcase"];
|
|
@@ -5207,7 +5530,7 @@ const defaults$l = {
|
|
|
5207
5530
|
class ElementCase extends Rule {
|
|
5208
5531
|
constructor(options) {
|
|
5209
5532
|
super({ ...defaults$l, ...options });
|
|
5210
|
-
this.style = new
|
|
5533
|
+
this.style = new CaseStyle(this.options.style, "element-case");
|
|
5211
5534
|
}
|
|
5212
5535
|
static schema() {
|
|
5213
5536
|
const styleEnum = ["lowercase", "uppercase", "pascalcase", "camelcase"];
|
|
@@ -5591,7 +5914,7 @@ function formatMessage$1(node, parent, rules) {
|
|
|
5591
5914
|
if (!isFormattable(rules)) {
|
|
5592
5915
|
return `${nodeName} element cannot have ${parentName} element as parent`;
|
|
5593
5916
|
}
|
|
5594
|
-
const allowed =
|
|
5917
|
+
const allowed = utils_naturalJoin.naturalJoin(rules.filter(isCategoryOrTag).map(formatCategoryOrTag));
|
|
5595
5918
|
return `${nodeName} element requires a ${allowed} element as parent`;
|
|
5596
5919
|
}
|
|
5597
5920
|
class ElementPermittedParent extends Rule {
|
|
@@ -5655,7 +5978,7 @@ function getRuleDescription(context) {
|
|
|
5655
5978
|
];
|
|
5656
5979
|
}
|
|
5657
5980
|
const escaped = context.ancestor.map((it) => `\`${it}\``);
|
|
5658
|
-
return [`The \`${context.child}\` element requires a ${
|
|
5981
|
+
return [`The \`${context.child}\` element requires a ${utils_naturalJoin.naturalJoin(escaped)} ancestor.`];
|
|
5659
5982
|
}
|
|
5660
5983
|
class ElementRequiredAncestor extends Rule {
|
|
5661
5984
|
documentation(context) {
|
|
@@ -5691,7 +6014,7 @@ class ElementRequiredAncestor extends Rule {
|
|
|
5691
6014
|
}
|
|
5692
6015
|
const ancestor = rules.map((it) => (isTagnameOnly(it) ? `<${it}>` : `"${it}"`));
|
|
5693
6016
|
const child = `<${node.tagName}>`;
|
|
5694
|
-
const message = `<${node.tagName}> element requires a ${
|
|
6017
|
+
const message = `<${node.tagName}> element requires a ${utils_naturalJoin.naturalJoin(ancestor)} ancestor`;
|
|
5695
6018
|
const context = {
|
|
5696
6019
|
ancestor,
|
|
5697
6020
|
child,
|
|
@@ -5782,9 +6105,9 @@ class ElementRequiredContent extends Rule {
|
|
|
5782
6105
|
}
|
|
5783
6106
|
|
|
5784
6107
|
const selector = ["h1", "h2", "h3", "h4", "h5", "h6"].join(",");
|
|
5785
|
-
function hasImgAltText(node) {
|
|
6108
|
+
function hasImgAltText$1(node) {
|
|
5786
6109
|
if (node.is("img")) {
|
|
5787
|
-
return
|
|
6110
|
+
return hasAltText(node);
|
|
5788
6111
|
}
|
|
5789
6112
|
else if (node.is("svg")) {
|
|
5790
6113
|
return node.textContent.trim() !== "";
|
|
@@ -5811,16 +6134,16 @@ class EmptyHeading extends Rule {
|
|
|
5811
6134
|
validateHeading(heading) {
|
|
5812
6135
|
const images = heading.querySelectorAll("img, svg");
|
|
5813
6136
|
for (const child of images) {
|
|
5814
|
-
if (hasImgAltText(child)) {
|
|
6137
|
+
if (hasImgAltText$1(child)) {
|
|
5815
6138
|
return;
|
|
5816
6139
|
}
|
|
5817
6140
|
}
|
|
5818
|
-
switch (
|
|
5819
|
-
case
|
|
5820
|
-
case
|
|
6141
|
+
switch (classifyNodeText(heading, { ignoreHiddenRoot: true })) {
|
|
6142
|
+
case exports.TextClassification.DYNAMIC_TEXT:
|
|
6143
|
+
case exports.TextClassification.STATIC_TEXT:
|
|
5821
6144
|
/* have some text content, consider ok */
|
|
5822
6145
|
break;
|
|
5823
|
-
case
|
|
6146
|
+
case exports.TextClassification.EMPTY_TEXT:
|
|
5824
6147
|
/* no content or whitespace only */
|
|
5825
6148
|
this.report(heading, `<${heading.tagName}> cannot be empty, must have text content`);
|
|
5826
6149
|
break;
|
|
@@ -5847,12 +6170,12 @@ class EmptyTitle extends Rule {
|
|
|
5847
6170
|
const node = event.previous;
|
|
5848
6171
|
if (node.tagName !== "title")
|
|
5849
6172
|
return;
|
|
5850
|
-
switch (
|
|
5851
|
-
case
|
|
5852
|
-
case
|
|
6173
|
+
switch (classifyNodeText(node)) {
|
|
6174
|
+
case exports.TextClassification.DYNAMIC_TEXT:
|
|
6175
|
+
case exports.TextClassification.STATIC_TEXT:
|
|
5853
6176
|
/* have some text content, consider ok */
|
|
5854
6177
|
break;
|
|
5855
|
-
case
|
|
6178
|
+
case exports.TextClassification.EMPTY_TEXT:
|
|
5856
6179
|
/* no content or whitespace only */
|
|
5857
6180
|
{
|
|
5858
6181
|
const message = `<${node.tagName}> cannot be empty, must have text content`;
|
|
@@ -5925,7 +6248,7 @@ class FormDupName extends Rule {
|
|
|
5925
6248
|
var _a, _b;
|
|
5926
6249
|
const { document } = event;
|
|
5927
6250
|
const controls = document.querySelectorAll(selector);
|
|
5928
|
-
const [sharedControls, uniqueControls] =
|
|
6251
|
+
const [sharedControls, uniqueControls] = partition(controls, (it) => {
|
|
5929
6252
|
return allowSharedName(it, shared);
|
|
5930
6253
|
});
|
|
5931
6254
|
/* validate all form controls which require unique elements first so each
|
|
@@ -6434,6 +6757,126 @@ class InputAttributes extends Rule {
|
|
|
6434
6757
|
}
|
|
6435
6758
|
}
|
|
6436
6759
|
|
|
6760
|
+
const HAS_ACCESSIBLE_TEXT_CACHE = Symbol(hasAccessibleName.name);
|
|
6761
|
+
function isHidden(node, context) {
|
|
6762
|
+
const { reference } = context;
|
|
6763
|
+
if (reference && reference.isSameNode(node)) {
|
|
6764
|
+
return false;
|
|
6765
|
+
}
|
|
6766
|
+
else {
|
|
6767
|
+
return isHTMLHidden(node) || !inAccessibilityTree(node);
|
|
6768
|
+
}
|
|
6769
|
+
}
|
|
6770
|
+
function hasImgAltText(node, context) {
|
|
6771
|
+
if (node.is("img")) {
|
|
6772
|
+
return hasAltText(node);
|
|
6773
|
+
}
|
|
6774
|
+
else if (node.is("svg")) {
|
|
6775
|
+
return node.textContent.trim() !== "";
|
|
6776
|
+
}
|
|
6777
|
+
else {
|
|
6778
|
+
for (const img of node.querySelectorAll("img, svg")) {
|
|
6779
|
+
const hasName = hasAccessibleNameImpl(img, context);
|
|
6780
|
+
if (hasName) {
|
|
6781
|
+
return true;
|
|
6782
|
+
}
|
|
6783
|
+
}
|
|
6784
|
+
return false;
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
function hasLabel(node) {
|
|
6788
|
+
var _a;
|
|
6789
|
+
const value = (_a = node.getAttributeValue("aria-label")) !== null && _a !== void 0 ? _a : "";
|
|
6790
|
+
return Boolean(value.trim());
|
|
6791
|
+
}
|
|
6792
|
+
function isLabelledby(node, context) {
|
|
6793
|
+
const { document, reference } = context;
|
|
6794
|
+
/* if we already have resolved one level of reference we don't resolve another
|
|
6795
|
+
* level (as per accname step 2B) */
|
|
6796
|
+
if (reference) {
|
|
6797
|
+
return false;
|
|
6798
|
+
}
|
|
6799
|
+
const ariaLabelledby = node.ariaLabelledby;
|
|
6800
|
+
/* consider dynamic aria-labelledby as having a name as we cannot resolve it
|
|
6801
|
+
* so no way to prove correctness */
|
|
6802
|
+
if (ariaLabelledby instanceof DynamicValue) {
|
|
6803
|
+
return true;
|
|
6804
|
+
}
|
|
6805
|
+
/* ignore elements without aria-labelledby */
|
|
6806
|
+
if (ariaLabelledby === null) {
|
|
6807
|
+
return false;
|
|
6808
|
+
}
|
|
6809
|
+
return ariaLabelledby.some((id) => {
|
|
6810
|
+
const selector = generateIdSelector(id);
|
|
6811
|
+
return document.querySelectorAll(selector).some((child) => {
|
|
6812
|
+
return hasAccessibleNameImpl(child, {
|
|
6813
|
+
document,
|
|
6814
|
+
reference: child,
|
|
6815
|
+
});
|
|
6816
|
+
});
|
|
6817
|
+
});
|
|
6818
|
+
}
|
|
6819
|
+
/**
|
|
6820
|
+
* This algorithm is based on ["Accessible Name and Description Computation
|
|
6821
|
+
* 1.2"][accname] with some exceptions:
|
|
6822
|
+
*
|
|
6823
|
+
* It doesn't compute the actual name but only the presence of one, e.g. if a
|
|
6824
|
+
* non-empty flat string is present the algorithm terminates with a positive
|
|
6825
|
+
* result.
|
|
6826
|
+
*
|
|
6827
|
+
* It takes some optimization shortcuts such as starting with step F as it
|
|
6828
|
+
* would be more common usage and as there is no actual name being computed
|
|
6829
|
+
* the order wont matter.
|
|
6830
|
+
*
|
|
6831
|
+
* [accname]: https://w3c.github.io/accname
|
|
6832
|
+
*/
|
|
6833
|
+
function hasAccessibleNameImpl(current, context) {
|
|
6834
|
+
const { reference } = context;
|
|
6835
|
+
/* if this element is hidden (see function for exceptions) it does not have an accessible name */
|
|
6836
|
+
if (isHidden(current, context)) {
|
|
6837
|
+
return false;
|
|
6838
|
+
}
|
|
6839
|
+
/* special case: when this element is directly referenced by aria-labelledby
|
|
6840
|
+
* we ignore `hidden` */
|
|
6841
|
+
const ignoreHiddenRoot = Boolean(reference && reference.isSameNode(current));
|
|
6842
|
+
const text = classifyNodeText(current, { accessible: true, ignoreHiddenRoot });
|
|
6843
|
+
if (text !== exports.TextClassification.EMPTY_TEXT) {
|
|
6844
|
+
return true;
|
|
6845
|
+
}
|
|
6846
|
+
if (hasImgAltText(current, context)) {
|
|
6847
|
+
return true;
|
|
6848
|
+
}
|
|
6849
|
+
if (hasLabel(current)) {
|
|
6850
|
+
return true;
|
|
6851
|
+
}
|
|
6852
|
+
if (isLabelledby(current, context)) {
|
|
6853
|
+
return true;
|
|
6854
|
+
}
|
|
6855
|
+
return false;
|
|
6856
|
+
}
|
|
6857
|
+
/**
|
|
6858
|
+
* Returns `true` if the element has an accessible name.
|
|
6859
|
+
*
|
|
6860
|
+
* It does not yet consider if the elements role prohibits naming, e.g. a `<p>`
|
|
6861
|
+
* element will still show up as having an accessible name.
|
|
6862
|
+
*
|
|
6863
|
+
* @public
|
|
6864
|
+
* @param document - Document element.
|
|
6865
|
+
* @param current - The element to get accessible name for
|
|
6866
|
+
* @returns `true` if the element has an accessible name.
|
|
6867
|
+
*/
|
|
6868
|
+
function hasAccessibleName(document, current) {
|
|
6869
|
+
/* istanbul ignore next: we're not testing cache */
|
|
6870
|
+
if (current.cacheExists(HAS_ACCESSIBLE_TEXT_CACHE)) {
|
|
6871
|
+
return Boolean(current.cacheGet(HAS_ACCESSIBLE_TEXT_CACHE));
|
|
6872
|
+
}
|
|
6873
|
+
const result = hasAccessibleNameImpl(current, {
|
|
6874
|
+
document,
|
|
6875
|
+
reference: null,
|
|
6876
|
+
});
|
|
6877
|
+
return current.cacheSet(HAS_ACCESSIBLE_TEXT_CACHE, result);
|
|
6878
|
+
}
|
|
6879
|
+
|
|
6437
6880
|
function isIgnored(node) {
|
|
6438
6881
|
var _a;
|
|
6439
6882
|
if (node.is("input")) {
|
|
@@ -6468,14 +6911,14 @@ class InputMissingLabel extends Rule {
|
|
|
6468
6911
|
});
|
|
6469
6912
|
}
|
|
6470
6913
|
validateInput(root, elem) {
|
|
6471
|
-
if (
|
|
6914
|
+
if (isHTMLHidden(elem) || isAriaHidden(elem)) {
|
|
6472
6915
|
return;
|
|
6473
6916
|
}
|
|
6474
6917
|
/* hidden, submit, reset or button should not have label */
|
|
6475
6918
|
if (isIgnored(elem)) {
|
|
6476
6919
|
return;
|
|
6477
6920
|
}
|
|
6478
|
-
if (
|
|
6921
|
+
if (hasAccessibleName(root, elem)) {
|
|
6479
6922
|
return;
|
|
6480
6923
|
}
|
|
6481
6924
|
let label = [];
|
|
@@ -6508,13 +6951,13 @@ class InputMissingLabel extends Rule {
|
|
|
6508
6951
|
this.report(elem, `<${elem.tagName}> element has <label> but <label> element is hidden`);
|
|
6509
6952
|
return;
|
|
6510
6953
|
}
|
|
6511
|
-
if (!labels.some((label) =>
|
|
6954
|
+
if (!labels.some((label) => hasAccessibleName(root, label))) {
|
|
6512
6955
|
this.report(elem, `<${elem.tagName}> element has <label> but <label> has no text`);
|
|
6513
6956
|
}
|
|
6514
6957
|
}
|
|
6515
6958
|
}
|
|
6516
6959
|
function isVisible(elem) {
|
|
6517
|
-
const hidden =
|
|
6960
|
+
const hidden = isHTMLHidden(elem) || isAriaHidden(elem);
|
|
6518
6961
|
return !hidden;
|
|
6519
6962
|
}
|
|
6520
6963
|
function findLabelById(root, id) {
|
|
@@ -7561,7 +8004,7 @@ class NoUnknownElements extends Rule {
|
|
|
7561
8004
|
if (node.meta) {
|
|
7562
8005
|
return;
|
|
7563
8006
|
}
|
|
7564
|
-
if (this.isKeywordIgnored(node.tagName,
|
|
8007
|
+
if (this.isKeywordIgnored(node.tagName, keywordPatternMatcher)) {
|
|
7565
8008
|
return;
|
|
7566
8009
|
}
|
|
7567
8010
|
this.report(node, `Unknown element <${node.tagName}>`, null, node.tagName);
|
|
@@ -8299,7 +8742,7 @@ function isNonEmptyText(node) {
|
|
|
8299
8742
|
* - Elements with default text
|
|
8300
8743
|
*/
|
|
8301
8744
|
function haveAccessibleText(node) {
|
|
8302
|
-
if (!
|
|
8745
|
+
if (!inAccessibilityTree(node)) {
|
|
8303
8746
|
return false;
|
|
8304
8747
|
}
|
|
8305
8748
|
/* check direct descendants for non-empty or dynamic text */
|
|
@@ -8378,7 +8821,7 @@ class TextContent extends Rule {
|
|
|
8378
8821
|
* Validate element has empty text (inter-element whitespace is not considered text)
|
|
8379
8822
|
*/
|
|
8380
8823
|
validateNone(node) {
|
|
8381
|
-
if (
|
|
8824
|
+
if (classifyNodeText(node) === exports.TextClassification.EMPTY_TEXT) {
|
|
8382
8825
|
return;
|
|
8383
8826
|
}
|
|
8384
8827
|
this.reportError(node, node.meta, `${node.annotatedName} must not have text content`);
|
|
@@ -8387,7 +8830,7 @@ class TextContent extends Rule {
|
|
|
8387
8830
|
* Validate element has any text (inter-element whitespace is not considered text)
|
|
8388
8831
|
*/
|
|
8389
8832
|
validateRequired(node) {
|
|
8390
|
-
if (
|
|
8833
|
+
if (classifyNodeText(node) !== exports.TextClassification.EMPTY_TEXT) {
|
|
8391
8834
|
return;
|
|
8392
8835
|
}
|
|
8393
8836
|
this.reportError(node, node.meta, `${node.annotatedName} must have text content`);
|
|
@@ -8398,7 +8841,7 @@ class TextContent extends Rule {
|
|
|
8398
8841
|
*/
|
|
8399
8842
|
validateAccessible(node) {
|
|
8400
8843
|
/* skip this element if the element isn't present in accessibility tree */
|
|
8401
|
-
if (!
|
|
8844
|
+
if (!inAccessibilityTree(node)) {
|
|
8402
8845
|
return;
|
|
8403
8846
|
}
|
|
8404
8847
|
/* if the element or a child has aria-label, alt or default text, etc the
|
|
@@ -8786,22 +9229,22 @@ class H30 extends Rule {
|
|
|
8786
9229
|
const links = event.document.getElementsByTagName("a");
|
|
8787
9230
|
for (const link of links) {
|
|
8788
9231
|
/* ignore links with aria-hidden="true" */
|
|
8789
|
-
if (!
|
|
9232
|
+
if (!inAccessibilityTree(link)) {
|
|
8790
9233
|
continue;
|
|
8791
9234
|
}
|
|
8792
9235
|
/* check if text content is present (or dynamic) */
|
|
8793
|
-
const textClassification =
|
|
8794
|
-
if (textClassification !==
|
|
9236
|
+
const textClassification = classifyNodeText(link, { ignoreHiddenRoot: true });
|
|
9237
|
+
if (textClassification !== exports.TextClassification.EMPTY_TEXT) {
|
|
8795
9238
|
continue;
|
|
8796
9239
|
}
|
|
8797
9240
|
/* check if image with alt-text is present */
|
|
8798
9241
|
const images = link.querySelectorAll("img");
|
|
8799
|
-
if (images.some((image) =>
|
|
9242
|
+
if (images.some((image) => hasAltText(image))) {
|
|
8800
9243
|
continue;
|
|
8801
9244
|
}
|
|
8802
9245
|
/* check if aria-label is present on either the <a> element or a descendant */
|
|
8803
9246
|
const labels = link.querySelectorAll("[aria-label]");
|
|
8804
|
-
if (
|
|
9247
|
+
if (hasAriaLabel(link) || labels.some((cur) => hasAriaLabel(cur))) {
|
|
8805
9248
|
continue;
|
|
8806
9249
|
}
|
|
8807
9250
|
this.report(link, "Anchor link must have a text describing its purpose");
|
|
@@ -8884,7 +9327,7 @@ class H36 extends Rule {
|
|
|
8884
9327
|
if (node.getAttributeValue("type") !== "image") {
|
|
8885
9328
|
return;
|
|
8886
9329
|
}
|
|
8887
|
-
if (!
|
|
9330
|
+
if (!hasAltText(node)) {
|
|
8888
9331
|
this.report(node, "image used as submit button must have alt text");
|
|
8889
9332
|
}
|
|
8890
9333
|
});
|
|
@@ -8952,7 +9395,7 @@ class H37 extends Rule {
|
|
|
8952
9395
|
return;
|
|
8953
9396
|
}
|
|
8954
9397
|
/* ignore images with aria-hidden="true" or role="presentation" */
|
|
8955
|
-
if (!
|
|
9398
|
+
if (!inAccessibilityTree(node)) {
|
|
8956
9399
|
return;
|
|
8957
9400
|
}
|
|
8958
9401
|
/* validate plain alt-attribute */
|
|
@@ -8981,7 +9424,7 @@ var _a;
|
|
|
8981
9424
|
/* istanbul ignore next: this will always be present for the <th>
|
|
8982
9425
|
* attribute (or the tests would fail) */
|
|
8983
9426
|
const { enum: validScopes } = (_a = elements.html5.th.attributes) === null || _a === void 0 ? void 0 : _a.scope;
|
|
8984
|
-
const joinedScopes =
|
|
9427
|
+
const joinedScopes = utils_naturalJoin.naturalJoin(validScopes);
|
|
8985
9428
|
class H63 extends Rule {
|
|
8986
9429
|
documentation() {
|
|
8987
9430
|
return {
|
|
@@ -11701,7 +12144,7 @@ class HtmlValidate {
|
|
|
11701
12144
|
/** @public */
|
|
11702
12145
|
const name = "html-validate";
|
|
11703
12146
|
/** @public */
|
|
11704
|
-
const version = "8.0.
|
|
12147
|
+
const version = "8.0.5";
|
|
11705
12148
|
/** @public */
|
|
11706
12149
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
11707
12150
|
|
|
@@ -11748,6 +12191,27 @@ function compatibilityCheck(name, declared, options) {
|
|
|
11748
12191
|
return false;
|
|
11749
12192
|
}
|
|
11750
12193
|
|
|
12194
|
+
/**
|
|
12195
|
+
* Similar to `require(..)` but removes the cached copy first.
|
|
12196
|
+
*/
|
|
12197
|
+
function requireUncached(require, moduleId) {
|
|
12198
|
+
const filename = require.resolve(moduleId);
|
|
12199
|
+
/* remove references from the parent module to prevent memory leak */
|
|
12200
|
+
const m = require.cache[filename];
|
|
12201
|
+
if (m && m.parent) {
|
|
12202
|
+
const { parent } = m;
|
|
12203
|
+
for (let i = parent.children.length - 1; i >= 0; i--) {
|
|
12204
|
+
if (parent.children[i].id === filename) {
|
|
12205
|
+
parent.children.splice(i, 1);
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
/* remove old module from cache */
|
|
12210
|
+
delete require.cache[filename];
|
|
12211
|
+
/* eslint-disable-next-line import/no-dynamic-require, security/detect-non-literal-require -- as expected but should be moved to upcoming resolver class */
|
|
12212
|
+
return require(filename);
|
|
12213
|
+
}
|
|
12214
|
+
|
|
11751
12215
|
const ruleIds = new Set(Object.keys(rules));
|
|
11752
12216
|
/**
|
|
11753
12217
|
* Returns true if given ruleId is an existing builtin rule. It does not handle
|
|
@@ -12040,15 +12504,15 @@ exports.UserError = UserError;
|
|
|
12040
12504
|
exports.Validator = Validator;
|
|
12041
12505
|
exports.WrappedError = WrappedError;
|
|
12042
12506
|
exports.bugs = bugs;
|
|
12507
|
+
exports.classifyNodeText = classifyNodeText;
|
|
12043
12508
|
exports.codeframe = codeframe;
|
|
12044
12509
|
exports.compatibilityCheck = compatibilityCheck;
|
|
12045
12510
|
exports.definePlugin = definePlugin;
|
|
12046
12511
|
exports.ensureError = ensureError;
|
|
12047
|
-
exports.generateIdSelector = generateIdSelector;
|
|
12048
12512
|
exports.getFormatter = getFormatter;
|
|
12049
|
-
exports.
|
|
12050
|
-
exports.isTextNode = isTextNode;
|
|
12513
|
+
exports.keywordPatternMatcher = keywordPatternMatcher;
|
|
12051
12514
|
exports.name = name;
|
|
12515
|
+
exports.requireUncached = requireUncached;
|
|
12052
12516
|
exports.ruleExists = ruleExists;
|
|
12053
12517
|
exports.sliceLocation = sliceLocation;
|
|
12054
12518
|
exports.staticResolver = staticResolver;
|