html-validate 8.0.4 → 8.0.5

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