html-validate 8.0.3 → 8.0.5

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