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