html-validate 7.6.0 → 7.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/es/core.js CHANGED
@@ -333,2222 +333,2537 @@ class SchemaValidationError extends UserError {
333
333
  }
334
334
 
335
335
  /**
336
- * Computes hash for given string.
336
+ * Helper function to assist IDE with completion and type-checking.
337
337
  *
338
- * @internal
338
+ * @public
339
339
  */
340
- function cyrb53(str) {
341
- const a = 2654435761;
342
- const b = 1597334677;
343
- const c = 2246822507;
344
- const d = 3266489909;
345
- const e = 4294967296;
346
- const f = 2097151;
347
- const seed = 0;
348
- let h1 = 0xdeadbeef ^ seed;
349
- let h2 = 0x41c6ce57 ^ seed;
350
- for (let i = 0, ch; i < str.length; i++) {
351
- ch = str.charCodeAt(i);
352
- h1 = Math.imul(h1 ^ ch, a);
353
- h2 = Math.imul(h2 ^ ch, b);
340
+ function defineMetadata(metatable) {
341
+ return metatable;
342
+ }
343
+
344
+ /**
345
+ * @public
346
+ */
347
+ class DynamicValue {
348
+ constructor(expr) {
349
+ this.expr = expr;
350
+ }
351
+ toString() {
352
+ return this.expr;
354
353
  }
355
- h1 = Math.imul(h1 ^ (h1 >>> 16), c) ^ Math.imul(h2 ^ (h2 >>> 13), d);
356
- h2 = Math.imul(h2 ^ (h2 >>> 16), c) ^ Math.imul(h1 ^ (h1 >>> 13), d);
357
- return e * (f & h2) + (h1 >>> 0);
358
354
  }
359
- const computeHash = cyrb53;
360
355
 
361
- const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../");
362
- const legacyRequire = createRequire(import.meta.url);
363
- const distFolder = path.resolve(projectRoot, "dist/es");
356
+ /**
357
+ * DOM Attribute.
358
+ *
359
+ * Represents a HTML attribute. Can contain either a fixed static value or a
360
+ * placeholder for dynamic values (e.g. interpolated).
361
+ */
362
+ class Attribute {
363
+ /**
364
+ * @param key - Attribute name.
365
+ * @param value - Attribute value. Set to `null` for boolean attributes.
366
+ * @param keyLocation - Source location of attribute name.
367
+ * @param valueLocation - Source location of attribute value.
368
+ * @param originalAttribute - If this attribute was dynamically added via a
369
+ * transformation (e.g. vuejs `:id` generating the `id` attribute) this
370
+ * parameter should be set to the attribute name of the source attribute (`:id`).
371
+ */
372
+ constructor(key, value, keyLocation, valueLocation, originalAttribute) {
373
+ this.key = key;
374
+ this.value = value;
375
+ this.keyLocation = keyLocation;
376
+ this.valueLocation = valueLocation;
377
+ this.originalAttribute = originalAttribute;
378
+ /* force undefined to null */
379
+ if (typeof this.value === "undefined") {
380
+ this.value = null;
381
+ }
382
+ }
383
+ /**
384
+ * Flag set to true if the attribute value is static.
385
+ */
386
+ get isStatic() {
387
+ return !this.isDynamic;
388
+ }
389
+ /**
390
+ * Flag set to true if the attribute value is dynamic.
391
+ */
392
+ get isDynamic() {
393
+ return this.value instanceof DynamicValue;
394
+ }
395
+ valueMatches(pattern, dynamicMatches = true) {
396
+ if (this.value === null) {
397
+ return false;
398
+ }
399
+ /* dynamic values matches everything */
400
+ if (this.value instanceof DynamicValue) {
401
+ return dynamicMatches;
402
+ }
403
+ /* test value against pattern */
404
+ if (pattern instanceof RegExp) {
405
+ return this.value.match(pattern) !== null;
406
+ }
407
+ else {
408
+ return this.value === pattern;
409
+ }
410
+ }
411
+ }
364
412
 
413
+ function getCSSDeclarations(value) {
414
+ return value
415
+ .trim()
416
+ .split(";")
417
+ .filter(Boolean)
418
+ .map((it) => {
419
+ const [property, value] = it.split(":", 2);
420
+ return [property.trim(), value ? value.trim() : ""];
421
+ });
422
+ }
365
423
  /**
366
- * Similar to `require(..)` but removes the cached copy first.
424
+ * @internal
367
425
  */
368
- function requireUncached(moduleId) {
369
- const filename = legacyRequire.resolve(moduleId);
370
- /* remove references from the parent module to prevent memory leak */
371
- const m = legacyRequire.cache[filename];
372
- if (m && m.parent) {
373
- const { parent } = m;
374
- for (let i = parent.children.length - 1; i >= 0; i--) {
375
- if (parent.children[i].id === filename) {
376
- parent.children.splice(i, 1);
426
+ function parseCssDeclaration(value) {
427
+ if (!value || value instanceof DynamicValue) {
428
+ return {};
429
+ }
430
+ const pairs = getCSSDeclarations(value);
431
+ return Object.fromEntries(pairs);
432
+ }
433
+
434
+ function sliceSize(size, begin, end) {
435
+ if (typeof size !== "number") {
436
+ return size;
437
+ }
438
+ if (typeof end !== "number") {
439
+ return size - begin;
440
+ }
441
+ if (end < 0) {
442
+ end = size + end;
443
+ }
444
+ return Math.min(size, end - begin);
445
+ }
446
+ function sliceLocation(location, begin, end, wrap) {
447
+ if (!location)
448
+ return null;
449
+ const size = sliceSize(location.size, begin, end);
450
+ const sliced = {
451
+ filename: location.filename,
452
+ offset: location.offset + begin,
453
+ line: location.line,
454
+ column: location.column + begin,
455
+ size,
456
+ };
457
+ /* if text content is provided try to find all newlines and modify line/column accordingly */
458
+ if (wrap) {
459
+ let index = -1;
460
+ const col = sliced.column;
461
+ do {
462
+ index = wrap.indexOf("\n", index + 1);
463
+ if (index >= 0 && index < begin) {
464
+ sliced.column = col - (index + 1);
465
+ sliced.line++;
377
466
  }
467
+ else {
468
+ break;
469
+ }
470
+ } while (true); // eslint-disable-line no-constant-condition
471
+ }
472
+ return sliced;
473
+ }
474
+
475
+ var State;
476
+ (function (State) {
477
+ State[State["INITIAL"] = 1] = "INITIAL";
478
+ State[State["DOCTYPE"] = 2] = "DOCTYPE";
479
+ State[State["TEXT"] = 3] = "TEXT";
480
+ State[State["TAG"] = 4] = "TAG";
481
+ State[State["ATTR"] = 5] = "ATTR";
482
+ State[State["CDATA"] = 6] = "CDATA";
483
+ State[State["SCRIPT"] = 7] = "SCRIPT";
484
+ State[State["STYLE"] = 8] = "STYLE";
485
+ })(State || (State = {}));
486
+
487
+ var ContentModel;
488
+ (function (ContentModel) {
489
+ ContentModel[ContentModel["TEXT"] = 1] = "TEXT";
490
+ ContentModel[ContentModel["SCRIPT"] = 2] = "SCRIPT";
491
+ ContentModel[ContentModel["STYLE"] = 3] = "STYLE";
492
+ })(ContentModel || (ContentModel = {}));
493
+ class Context {
494
+ constructor(source) {
495
+ var _a, _b, _c, _d;
496
+ this.state = State.INITIAL;
497
+ this.string = source.data;
498
+ this.filename = (_a = source.filename) !== null && _a !== void 0 ? _a : "";
499
+ this.offset = (_b = source.offset) !== null && _b !== void 0 ? _b : 0;
500
+ this.line = (_c = source.line) !== null && _c !== void 0 ? _c : 1;
501
+ this.column = (_d = source.column) !== null && _d !== void 0 ? _d : 1;
502
+ this.contentModel = ContentModel.TEXT;
503
+ }
504
+ getTruncatedLine(n = 13) {
505
+ return JSON.stringify(this.string.length > n ? `${this.string.slice(0, 10)}...` : this.string);
506
+ }
507
+ consume(n, state) {
508
+ /* if "n" is an regex match the first value is the full matched
509
+ * string so consume that many characters. */
510
+ if (typeof n !== "number") {
511
+ n = n[0].length; /* regex match */
512
+ }
513
+ /* poor mans line counter :( */
514
+ let consumed = this.string.slice(0, n);
515
+ let offset;
516
+ while ((offset = consumed.indexOf("\n")) >= 0) {
517
+ this.line++;
518
+ this.column = 1;
519
+ consumed = consumed.substr(offset + 1);
378
520
  }
521
+ this.column += consumed.length;
522
+ this.offset += n;
523
+ /* remove N chars */
524
+ this.string = this.string.substr(n);
525
+ /* change state */
526
+ this.state = state;
527
+ }
528
+ getLocation(size) {
529
+ return {
530
+ filename: this.filename,
531
+ offset: this.offset,
532
+ line: this.line,
533
+ column: this.column,
534
+ size,
535
+ };
379
536
  }
380
- /* remove old module from cache */
381
- delete legacyRequire.cache[filename];
382
- return legacyRequire(filename);
383
537
  }
384
538
 
385
- const $schema$1 = "http://json-schema.org/draft-06/schema#";
386
- const $id$1 = "https://html-validate.org/schemas/elements.json";
387
- const type$1 = "object";
388
- const properties$1 = {
389
- $schema: {
390
- type: "string"
391
- }
392
- };
393
- const patternProperties = {
394
- "^[^$].*$": {
395
- type: "object",
396
- properties: {
397
- inherit: {
398
- title: "Inherit from another element",
399
- description: "Most properties from the parent element will be copied onto this one",
400
- type: "string"
401
- },
402
- embedded: {
403
- title: "Mark this element as belonging in the embedded content category",
404
- $ref: "#/definitions/contentCategory"
405
- },
406
- flow: {
407
- title: "Mark this element as belonging in the flow content category",
408
- $ref: "#/definitions/contentCategory"
409
- },
410
- heading: {
411
- title: "Mark this element as belonging in the heading content category",
412
- $ref: "#/definitions/contentCategory"
413
- },
414
- interactive: {
415
- title: "Mark this element as belonging in the interactive content category",
416
- $ref: "#/definitions/contentCategory"
417
- },
418
- metadata: {
419
- title: "Mark this element as belonging in the metadata content category",
420
- $ref: "#/definitions/contentCategory"
421
- },
422
- phrasing: {
423
- title: "Mark this element as belonging in the phrasing content category",
424
- $ref: "#/definitions/contentCategory"
425
- },
426
- sectioning: {
427
- title: "Mark this element as belonging in the sectioning content category",
428
- $ref: "#/definitions/contentCategory"
429
- },
430
- deprecated: {
431
- title: "Mark element as deprecated",
432
- description: "Deprecated elements should not be used. If a message is provided it will be included in the error",
433
- anyOf: [
434
- {
435
- type: "boolean"
436
- },
437
- {
438
- type: "string"
439
- },
440
- {
441
- $ref: "#/definitions/deprecatedElement"
442
- }
443
- ]
444
- },
445
- foreign: {
446
- title: "Mark element as foreign",
447
- description: "Foreign elements are elements which have a start and end tag but is otherwize not parsed",
448
- type: "boolean"
449
- },
450
- "void": {
451
- title: "Mark element as void",
452
- description: "Void elements are elements which cannot have content and thus must not use an end tag",
453
- type: "boolean"
454
- },
455
- transparent: {
456
- title: "Mark element as transparent",
457
- description: "Transparent elements follows the same content model as its parent, i.e. the content must be allowed in the parent.",
458
- anyOf: [
459
- {
460
- type: "boolean"
461
- },
462
- {
463
- type: "array",
464
- items: {
465
- type: "string"
466
- }
467
- }
468
- ]
469
- },
470
- implicitClosed: {
471
- title: "List of elements which implicitly closes this element",
472
- description: "Some elements are automatically closed when another start tag occurs",
473
- type: "array",
474
- items: {
475
- type: "string"
476
- }
477
- },
478
- scriptSupporting: {
479
- title: "Mark element as script-supporting",
480
- description: "Script-supporting elements are elements which can be inserted where othersise not permitted to assist in templating",
481
- type: "boolean"
482
- },
483
- form: {
484
- title: "Mark element as a submittable form element",
485
- type: "boolean"
486
- },
487
- labelable: {
488
- title: "Mark this element as labelable",
489
- description: "This element may contain an associated label element.",
490
- anyOf: [
491
- {
492
- type: "boolean"
493
- },
494
- {
495
- $ref: "#/definitions/expression"
496
- }
497
- ]
498
- },
499
- deprecatedAttributes: {
500
- title: "List of deprecated attributes",
501
- type: "array",
502
- items: {
503
- type: "string"
504
- }
505
- },
506
- requiredAttributes: {
507
- title: "List of required attributes",
508
- type: "array",
509
- items: {
510
- type: "string"
511
- }
512
- },
513
- attributes: {
514
- title: "List of known attributes and allowed values",
515
- $ref: "#/definitions/PermittedAttribute"
516
- },
517
- permittedContent: {
518
- title: "List of elements or categories allowed as content in this element",
519
- $ref: "#/definitions/Permitted"
520
- },
521
- permittedDescendants: {
522
- title: "List of elements or categories allowed as descendants in this element",
523
- $ref: "#/definitions/Permitted"
524
- },
525
- permittedOrder: {
526
- title: "Required order of child elements",
527
- $ref: "#/definitions/PermittedOrder"
528
- },
529
- permittedParent: {
530
- title: "List of elements or categories allowed as parent to this element",
531
- $ref: "#/definitions/Permitted"
532
- },
533
- requiredAncestors: {
534
- title: "List of required ancestor elements",
535
- $ref: "#/definitions/RequiredAncestors"
536
- },
537
- requiredContent: {
538
- title: "List of required content elements",
539
- $ref: "#/definitions/RequiredContent"
540
- },
541
- textContent: {
542
- title: "Allow, disallow or require textual content",
543
- description: "This property controls whenever an element allows, disallows or requires text. Text from any descendant counts, not only direct children",
544
- "default": "default",
545
- type: "string",
546
- "enum": [
547
- "none",
548
- "default",
549
- "required",
550
- "accessible"
551
- ]
552
- }
553
- },
554
- additionalProperties: false
555
- }
556
- };
557
- const definitions = {
558
- contentCategory: {
559
- anyOf: [
560
- {
561
- type: "boolean"
562
- },
563
- {
564
- $ref: "#/definitions/expression"
565
- }
566
- ]
567
- },
568
- expression: {
569
- type: "array",
570
- minItems: 2,
571
- maxItems: 2,
572
- items: [
573
- {
574
- type: "string",
575
- "enum": [
576
- "isDescendant",
577
- "hasAttribute",
578
- "matchAttribute"
579
- ]
580
- },
581
- {
582
- anyOf: [
583
- {
584
- type: "string"
585
- },
586
- {
587
- $ref: "#/definitions/operation"
588
- }
589
- ]
590
- }
591
- ]
592
- },
593
- operation: {
594
- type: "array",
595
- minItems: 3,
596
- maxItems: 3,
597
- items: [
598
- {
599
- type: "string"
600
- },
601
- {
602
- type: "string",
603
- "enum": [
604
- "!=",
605
- "="
606
- ]
607
- },
608
- {
609
- type: "string"
610
- }
611
- ]
612
- },
613
- deprecatedElement: {
614
- type: "object",
615
- additionalProperties: false,
616
- properties: {
617
- message: {
618
- type: "string",
619
- title: "A short text message shown next to the regular error message."
620
- },
621
- documentation: {
622
- type: "string",
623
- title: "An extended markdown formatted message shown with the contextual rule documentation."
624
- },
625
- source: {
626
- type: "string",
627
- title: "Element source, e.g. what standard or library deprecated this element.",
628
- "default": "html5"
629
- }
630
- }
631
- },
632
- Permitted: {
633
- type: "array",
634
- items: {
635
- anyOf: [
636
- {
637
- type: "string"
638
- },
639
- {
640
- type: "array",
641
- items: {
642
- anyOf: [
643
- {
644
- type: "string"
645
- },
646
- {
647
- $ref: "#/definitions/PermittedGroup"
648
- }
649
- ]
650
- }
651
- },
652
- {
653
- $ref: "#/definitions/PermittedGroup"
654
- }
655
- ]
656
- }
657
- },
658
- PermittedAttribute: {
659
- type: "object",
660
- patternProperties: {
661
- "^.*$": {
662
- anyOf: [
663
- {
664
- type: "object",
665
- additionalProperties: false,
666
- properties: {
667
- boolean: {
668
- type: "boolean",
669
- title: "Set to true if this is a boolean attribute"
670
- },
671
- deprecated: {
672
- title: "Set to true or string if this attribute is deprecated",
673
- oneOf: [
674
- {
675
- type: "boolean"
676
- },
677
- {
678
- type: "string"
679
- }
680
- ]
681
- },
682
- list: {
683
- type: "boolean",
684
- title: "Set to true if this attribute is a list of space-separated tokens, each which must be valid by itself"
685
- },
686
- "enum": {
687
- type: "array",
688
- title: "Exhaustive list of values (string or regex) this attribute accepts",
689
- uniqueItems: true,
690
- items: {
691
- anyOf: [
692
- {
693
- type: "string"
694
- },
695
- {
696
- regexp: true
697
- }
698
- ]
699
- }
700
- },
701
- omit: {
702
- type: "boolean",
703
- title: "Set to true if this attribute can optionally omit its value"
704
- },
705
- required: {
706
- type: "boolean",
707
- title: "Set to true if this attribute is required"
708
- }
709
- }
710
- },
711
- {
712
- type: "array",
713
- uniqueItems: true,
714
- items: {
715
- type: "string"
716
- }
717
- }
718
- ]
719
- }
720
- }
721
- },
722
- PermittedGroup: {
723
- type: "object",
724
- additionalProperties: false,
725
- properties: {
726
- exclude: {
727
- anyOf: [
728
- {
729
- items: {
730
- type: "string"
731
- },
732
- type: "array"
733
- },
734
- {
735
- type: "string"
736
- }
737
- ]
738
- }
739
- }
740
- },
741
- PermittedOrder: {
742
- type: "array",
743
- items: {
744
- type: "string"
745
- }
746
- },
747
- RequiredAncestors: {
748
- type: "array",
749
- items: {
750
- type: "string"
751
- }
752
- },
753
- RequiredContent: {
754
- type: "array",
755
- items: {
756
- type: "string"
757
- }
758
- }
759
- };
760
- var schema = {
761
- $schema: $schema$1,
762
- $id: $id$1,
763
- type: type$1,
764
- properties: properties$1,
765
- patternProperties: patternProperties,
766
- definitions: definitions
767
- };
539
+ var TextContent$1;
540
+ (function (TextContent) {
541
+ /* forbid node to have text content, inter-element whitespace is ignored */
542
+ TextContent["NONE"] = "none";
543
+ /* node can have text but not required too */
544
+ TextContent["DEFAULT"] = "default";
545
+ /* node requires text-nodes to be present (direct or by descendant) */
546
+ TextContent["REQUIRED"] = "required";
547
+ /* node requires accessible text (hidden text is ignored, tries to get text from accessibility tree) */
548
+ TextContent["ACCESSIBLE"] = "accessible";
549
+ })(TextContent$1 || (TextContent$1 = {}));
550
+ /**
551
+ * Properties listed here can be copied (loaded) onto another element using
552
+ * [[HtmlElement.loadMeta]].
553
+ *
554
+ * @public
555
+ */
556
+ const MetaCopyableProperty = [
557
+ "metadata",
558
+ "flow",
559
+ "sectioning",
560
+ "heading",
561
+ "phrasing",
562
+ "embedded",
563
+ "interactive",
564
+ "transparent",
565
+ "form",
566
+ "labelable",
567
+ "attributes",
568
+ "permittedContent",
569
+ "permittedDescendants",
570
+ "permittedOrder",
571
+ "permittedParent",
572
+ "requiredAncestors",
573
+ "requiredContent",
574
+ ];
575
+ /**
576
+ * @internal
577
+ */
578
+ function setMetaProperty(dst, key, value) {
579
+ dst[key] = value;
580
+ }
581
+
582
+ var NodeType;
583
+ (function (NodeType) {
584
+ NodeType[NodeType["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
585
+ NodeType[NodeType["TEXT_NODE"] = 3] = "TEXT_NODE";
586
+ NodeType[NodeType["DOCUMENT_NODE"] = 9] = "DOCUMENT_NODE";
587
+ })(NodeType || (NodeType = {}));
588
+
589
+ const DOCUMENT_NODE_NAME = "#document";
590
+ const TEXT_CONTENT = Symbol("textContent");
591
+ let counter = 0;
592
+ class DOMNode {
593
+ /**
594
+ * Create a new DOMNode.
595
+ *
596
+ * @param nodeType - What node type to create.
597
+ * @param nodeName - What node name to use. For `HtmlElement` this corresponds
598
+ * to the tagName but other node types have specific predefined values.
599
+ * @param location - Source code location of this node.
600
+ */
601
+ constructor(nodeType, nodeName, location) {
602
+ this.nodeType = nodeType;
603
+ this.nodeName = nodeName !== null && nodeName !== void 0 ? nodeName : DOCUMENT_NODE_NAME;
604
+ this.location = location;
605
+ this.disabledRules = new Set();
606
+ this.childNodes = [];
607
+ this.unique = counter++;
608
+ this.cache = null;
609
+ }
610
+ /**
611
+ * Enable cache for this node.
612
+ *
613
+ * Should not be called before the node and all children are fully constructed.
614
+ */
615
+ cacheEnable() {
616
+ this.cache = new Map();
617
+ }
618
+ cacheGet(key) {
619
+ if (this.cache) {
620
+ return this.cache.get(key);
621
+ }
622
+ else {
623
+ return undefined;
624
+ }
625
+ }
626
+ cacheSet(key, value) {
627
+ if (this.cache) {
628
+ this.cache.set(key, value);
629
+ }
630
+ return value;
631
+ }
632
+ cacheRemove(key) {
633
+ if (this.cache) {
634
+ return this.cache.delete(key);
635
+ }
636
+ else {
637
+ return false;
638
+ }
639
+ }
640
+ cacheExists(key) {
641
+ return Boolean(this.cache && this.cache.has(key));
642
+ }
643
+ /**
644
+ * Get the text (recursive) from all child nodes.
645
+ */
646
+ get textContent() {
647
+ const cached = this.cacheGet(TEXT_CONTENT);
648
+ if (cached) {
649
+ return cached;
650
+ }
651
+ const text = this.childNodes.map((node) => node.textContent).join("");
652
+ this.cacheSet(TEXT_CONTENT, text);
653
+ return text;
654
+ }
655
+ append(node) {
656
+ this.childNodes.push(node);
657
+ }
658
+ isRootElement() {
659
+ return this.nodeType === NodeType.DOCUMENT_NODE;
660
+ }
661
+ /**
662
+ * Tests if two nodes are the same (references the same object).
663
+ */
664
+ isSameNode(otherNode) {
665
+ return this.unique === otherNode.unique;
666
+ }
667
+ /**
668
+ * Returns a DOMNode representing the first direct child node or `null` if the
669
+ * node has no children.
670
+ */
671
+ get firstChild() {
672
+ return this.childNodes[0] || null;
673
+ }
674
+ /**
675
+ * Returns a DOMNode representing the last direct child node or `null` if the
676
+ * node has no children.
677
+ */
678
+ get lastChild() {
679
+ return this.childNodes[this.childNodes.length - 1] || null;
680
+ }
681
+ /**
682
+ * Disable a rule for this node.
683
+ */
684
+ disableRule(ruleId) {
685
+ this.disabledRules.add(ruleId);
686
+ }
687
+ /**
688
+ * Disables multiple rules.
689
+ */
690
+ disableRules(rules) {
691
+ for (const rule of rules) {
692
+ this.disableRule(rule);
693
+ }
694
+ }
695
+ /**
696
+ * Enable a previously disabled rule for this node.
697
+ */
698
+ enableRule(ruleId) {
699
+ this.disabledRules.delete(ruleId);
700
+ }
701
+ /**
702
+ * Enables multiple rules.
703
+ */
704
+ enableRules(rules) {
705
+ for (const rule of rules) {
706
+ this.enableRule(rule);
707
+ }
708
+ }
709
+ /**
710
+ * Test if a rule is enabled for this node.
711
+ */
712
+ ruleEnabled(ruleId) {
713
+ return !this.disabledRules.has(ruleId);
714
+ }
715
+ generateSelector() {
716
+ return null;
717
+ }
718
+ }
719
+
720
+ function parse(text, baseLocation) {
721
+ const tokens = [];
722
+ const locations = baseLocation ? [] : null;
723
+ for (let begin = 0; begin < text.length;) {
724
+ let end = text.indexOf(" ", begin);
725
+ /* if the last space was found move the position to the last character
726
+ * in the string */
727
+ if (end === -1) {
728
+ end = text.length;
729
+ }
730
+ /* handle multiple spaces */
731
+ const size = end - begin;
732
+ if (size === 0) {
733
+ begin++;
734
+ continue;
735
+ }
736
+ /* extract token */
737
+ const token = text.substring(begin, end);
738
+ tokens.push(token);
739
+ /* extract location */
740
+ if (locations && baseLocation) {
741
+ const location = sliceLocation(baseLocation, begin, end);
742
+ locations.push(location);
743
+ }
744
+ /* advance position to the character after the current end position */
745
+ begin += size + 1;
746
+ }
747
+ return { tokens, locations };
748
+ }
749
+ class DOMTokenList extends Array {
750
+ constructor(value, location) {
751
+ if (value && typeof value === "string") {
752
+ /* replace all whitespace with a single space for easier parsing */
753
+ const normalized = value.replace(/[\t\r\n]/g, " ");
754
+ const { tokens, locations } = parse(normalized, location);
755
+ super(...tokens);
756
+ this.locations = locations;
757
+ }
758
+ else {
759
+ super(0);
760
+ this.locations = null;
761
+ }
762
+ if (value instanceof DynamicValue) {
763
+ this.value = value.expr;
764
+ }
765
+ else {
766
+ this.value = value || "";
767
+ }
768
+ }
769
+ item(n) {
770
+ return this[n];
771
+ }
772
+ location(n) {
773
+ if (this.locations) {
774
+ return this.locations[n];
775
+ }
776
+ else {
777
+ throw new Error("Trying to access DOMTokenList location when base location isn't set");
778
+ }
779
+ }
780
+ contains(token) {
781
+ return this.includes(token);
782
+ }
783
+ *iterator() {
784
+ for (let index = 0; index < this.length; index++) {
785
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
786
+ const item = this.item(index);
787
+ const location = this.location(index);
788
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
789
+ yield { index, item, location };
790
+ }
791
+ }
792
+ }
793
+
794
+ var Combinator;
795
+ (function (Combinator) {
796
+ Combinator[Combinator["DESCENDANT"] = 1] = "DESCENDANT";
797
+ Combinator[Combinator["CHILD"] = 2] = "CHILD";
798
+ Combinator[Combinator["ADJACENT_SIBLING"] = 3] = "ADJACENT_SIBLING";
799
+ Combinator[Combinator["GENERAL_SIBLING"] = 4] = "GENERAL_SIBLING";
800
+ /* special cases */
801
+ Combinator[Combinator["SCOPE"] = 5] = "SCOPE";
802
+ })(Combinator || (Combinator = {}));
803
+ function parseCombinator(combinator, pattern) {
804
+ /* special case, when pattern is :scope [[Selector]] will handle this
805
+ * "combinator" to match itself instead of descendants */
806
+ if (pattern === ":scope") {
807
+ return Combinator.SCOPE;
808
+ }
809
+ switch (combinator) {
810
+ case undefined:
811
+ case null:
812
+ case "":
813
+ return Combinator.DESCENDANT;
814
+ case ">":
815
+ return Combinator.CHILD;
816
+ case "+":
817
+ return Combinator.ADJACENT_SIBLING;
818
+ case "~":
819
+ return Combinator.GENERAL_SIBLING;
820
+ default:
821
+ throw new Error(`Unknown combinator "${combinator}"`);
822
+ }
823
+ }
768
824
 
769
- /**
770
- * AJV keyword "regexp" to validate the type to be a regular expression.
771
- * Injects errors with the "type" keyword to give the same output.
772
- */
773
- /* istanbul ignore next: manual testing */
774
- const ajvRegexpValidate = function (data, dataCxt) {
775
- const valid = data instanceof RegExp;
776
- if (!valid) {
777
- ajvRegexpValidate.errors = [
778
- {
779
- instancePath: dataCxt === null || dataCxt === void 0 ? void 0 : dataCxt.instancePath,
780
- schemaPath: undefined,
781
- keyword: "type",
782
- message: "should be a regular expression",
783
- params: {
784
- keyword: "type",
785
- },
786
- },
787
- ];
825
+ function firstChild(node) {
826
+ return node.previousSibling === null;
827
+ }
828
+
829
+ function lastChild(node) {
830
+ return node.nextSibling === null;
831
+ }
832
+
833
+ const cache = {};
834
+ function getNthChild(node) {
835
+ if (!node.parent) {
836
+ return -1;
788
837
  }
789
- return valid;
790
- };
791
- const ajvRegexpKeyword = {
792
- keyword: "regexp",
793
- schema: false,
794
- errors: true,
795
- validate: ajvRegexpValidate,
838
+ if (!cache[node.unique]) {
839
+ const parent = node.parent;
840
+ const index = parent.childElements.findIndex((cur) => {
841
+ return cur.unique === node.unique;
842
+ });
843
+ cache[node.unique] = index + 1; /* nthChild starts at 1 */
844
+ }
845
+ return cache[node.unique];
846
+ }
847
+ function nthChild(node, args) {
848
+ if (!args) {
849
+ throw new Error("Missing argument to nth-child");
850
+ }
851
+ const n = parseInt(args.trim(), 10);
852
+ const cur = getNthChild(node);
853
+ return cur === n;
854
+ }
855
+
856
+ function scope(node) {
857
+ return node.isSameNode(this.scope);
858
+ }
859
+
860
+ const table = {
861
+ "first-child": firstChild,
862
+ "last-child": lastChild,
863
+ "nth-child": nthChild,
864
+ scope: scope,
796
865
  };
866
+ function factory(name, context) {
867
+ const fn = table[name];
868
+ if (fn) {
869
+ return fn.bind(context);
870
+ }
871
+ else {
872
+ throw new Error(`Pseudo-class "${name}" is not implemented`);
873
+ }
874
+ }
797
875
 
798
- var TextContent$1;
799
- (function (TextContent) {
800
- /* forbid node to have text content, inter-element whitespace is ignored */
801
- TextContent["NONE"] = "none";
802
- /* node can have text but not required too */
803
- TextContent["DEFAULT"] = "default";
804
- /* node requires text-nodes to be present (direct or by descendant) */
805
- TextContent["REQUIRED"] = "required";
806
- /* node requires accessible text (hidden text is ignored, tries to get text from accessibility tree) */
807
- TextContent["ACCESSIBLE"] = "accessible";
808
- })(TextContent$1 || (TextContent$1 = {}));
809
876
  /**
810
- * Properties listed here can be copied (loaded) onto another element using
811
- * [[HtmlElement.loadMeta]].
877
+ * Homage to PHP: unescapes slashes.
812
878
  *
813
- * @public
879
+ * E.g. "foo\:bar" becomes "foo:bar"
814
880
  */
815
- const MetaCopyableProperty = [
816
- "metadata",
817
- "flow",
818
- "sectioning",
819
- "heading",
820
- "phrasing",
821
- "embedded",
822
- "interactive",
823
- "transparent",
824
- "form",
825
- "labelable",
826
- "attributes",
827
- "permittedContent",
828
- "permittedDescendants",
829
- "permittedOrder",
830
- "permittedParent",
831
- "requiredAncestors",
832
- "requiredContent",
833
- ];
881
+ function stripslashes(value) {
882
+ return value.replace(/\\(.)/g, "$1");
883
+ }
834
884
  /**
835
885
  * @internal
836
886
  */
837
- function setMetaProperty(dst, key, value) {
838
- dst[key] = value;
887
+ function escapeSelectorComponent(text) {
888
+ return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
839
889
  }
840
-
841
- function isSet(value) {
842
- return typeof value !== "undefined";
890
+ /**
891
+ * @internal
892
+ */
893
+ function generateIdSelector(id) {
894
+ const escaped = escapeSelectorComponent(id);
895
+ return escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
843
896
  }
844
- function flag(value) {
845
- return value ? true : undefined;
897
+ /**
898
+ * Returns true if the character is a delimiter for different kinds of selectors:
899
+ *
900
+ * - `.` - begins a class selector
901
+ * - `#` - begins an id selector
902
+ * - `[` - begins an attribute selector
903
+ * - `:` - begins a pseudo class or element selector
904
+ */
905
+ function isDelimiter(ch) {
906
+ return /[.#[:]/.test(ch);
846
907
  }
847
- function stripUndefined(src) {
848
- const entries = Object.entries(src).filter(([, value]) => isSet(value));
849
- return Object.fromEntries(entries);
908
+ /**
909
+ * Returns true if the character is a quotation mark.
910
+ */
911
+ function isQuotationMark(ch) {
912
+ return /['"]/.test(ch);
850
913
  }
851
- function migrateSingleAttribute(src, key) {
852
- var _a, _b;
853
- const result = {};
854
- result.deprecated = flag((_a = src.deprecatedAttributes) === null || _a === void 0 ? void 0 : _a.includes(key));
855
- result.required = flag((_b = src.requiredAttributes) === null || _b === void 0 ? void 0 : _b.includes(key));
856
- result.omit = undefined;
857
- const attr = src.attributes ? src.attributes[key] : undefined;
858
- if (typeof attr === "undefined") {
859
- return stripUndefined(result);
860
- }
861
- /* when the attribute is set to null we use a special property "delete" to
862
- * flag it, if it is still set during merge (inheritance, overwriting, etc) the attribute will be removed */
863
- if (attr === null) {
864
- result.delete = true;
865
- return stripUndefined(result);
914
+ function isPseudoElement(ch, buffer) {
915
+ return ch === ":" && buffer === ":";
916
+ }
917
+ /**
918
+ * @internal
919
+ */
920
+ function* splitPattern(pattern) {
921
+ if (pattern === "") {
922
+ return;
866
923
  }
867
- if (Array.isArray(attr)) {
868
- if (attr.length === 0) {
869
- result.boolean = true;
924
+ const end = pattern.length;
925
+ let begin = 0;
926
+ let cur = 1;
927
+ let quoted = false;
928
+ while (cur < end) {
929
+ const ch = pattern[cur];
930
+ const buffer = pattern.slice(begin, cur);
931
+ /* escaped character, ignore whatever is next */
932
+ if (ch === "\\") {
933
+ cur += 2;
934
+ continue;
870
935
  }
871
- else {
872
- result.enum = attr.filter((it) => it !== "");
873
- if (attr.includes("")) {
874
- result.omit = true;
936
+ /* if inside quoted string we only look for the end quotation mark */
937
+ if (quoted) {
938
+ if (ch === quoted) {
939
+ quoted = false;
875
940
  }
941
+ cur += 1;
942
+ continue;
876
943
  }
877
- return stripUndefined(result);
878
- }
879
- else {
880
- return stripUndefined({ ...result, ...attr });
944
+ /* if the character is a quotation mark we store the character and the above
945
+ * condition will look for a similar end quotation mark */
946
+ if (isQuotationMark(ch)) {
947
+ quoted = ch;
948
+ cur += 1;
949
+ continue;
950
+ }
951
+ /* special case when using :: pseudo element selector */
952
+ if (isPseudoElement(ch, buffer)) {
953
+ cur += 1;
954
+ continue;
955
+ }
956
+ /* if the character is a delimiter we yield the string and reset the
957
+ * position */
958
+ if (isDelimiter(ch)) {
959
+ begin = cur;
960
+ yield buffer;
961
+ }
962
+ cur += 1;
881
963
  }
964
+ /* yield the rest of the string */
965
+ const tail = pattern.slice(begin, cur);
966
+ yield tail;
882
967
  }
883
- function migrateAttributes(src) {
884
- var _a, _b, _c;
885
- const keys = [
886
- ...Object.keys((_a = src.attributes) !== null && _a !== void 0 ? _a : {}),
887
- ...((_b = src.requiredAttributes) !== null && _b !== void 0 ? _b : []),
888
- ...((_c = src.deprecatedAttributes) !== null && _c !== void 0 ? _c : []),
889
- ].sort();
890
- const entries = keys.map((key) => {
891
- return [key, migrateSingleAttribute(src, key)];
892
- });
893
- return Object.fromEntries(entries);
894
- }
895
- function migrateElement(src) {
896
- const result = {
897
- ...src,
898
- attributes: migrateAttributes(src),
899
- };
900
- /* removed properties */
901
- delete result.deprecatedAttributes;
902
- delete result.requiredAttributes;
903
- return result;
968
+ class Matcher {
904
969
  }
905
-
906
- /**
907
- * Returns true if given element is a descendant of given tagname.
908
- *
909
- * @internal
910
- */
911
- function isDescendant(node, tagName) {
912
- let cur = node.parent;
913
- while (cur && !cur.isRootElement()) {
914
- if (cur.is(tagName)) {
915
- return true;
916
- }
917
- cur = cur.parent;
970
+ class ClassMatcher extends Matcher {
971
+ constructor(classname) {
972
+ super();
973
+ this.classname = classname;
974
+ }
975
+ match(node) {
976
+ return node.classList.contains(this.classname);
918
977
  }
919
- return false;
920
978
  }
921
-
922
- /**
923
- * Returns true if given element has given attribute (no matter the value, null,
924
- * dynamic, etc).
925
- */
926
- function hasAttribute(node, attr) {
927
- return node.hasAttribute(attr);
979
+ class IdMatcher extends Matcher {
980
+ constructor(id) {
981
+ super();
982
+ this.id = stripslashes(id);
983
+ }
984
+ match(node) {
985
+ return node.id === this.id;
986
+ }
928
987
  }
929
-
930
- /**
931
- * Matches attribute against value.
932
- */
933
- function matchAttribute(node, key, op, value) {
934
- const nodeValue = (node.getAttributeValue(key) || "").toLowerCase();
935
- switch (op) {
936
- case "!=":
937
- return nodeValue !== value;
938
- case "=":
939
- return nodeValue === value;
988
+ class AttrMatcher extends Matcher {
989
+ constructor(attr) {
990
+ super();
991
+ const [, key, op, value] = attr.match(/^(.+?)(?:([~^$*|]?=)"([^"]+?)")?$/);
992
+ this.key = key;
993
+ this.op = op;
994
+ this.value = value;
995
+ }
996
+ match(node) {
997
+ const attr = node.getAttribute(this.key, true) || [];
998
+ return attr.some((cur) => {
999
+ switch (this.op) {
1000
+ case undefined:
1001
+ return true; /* attribute exists */
1002
+ case "=":
1003
+ return cur.value === this.value;
1004
+ default:
1005
+ throw new Error(`Attribute selector operator ${this.op} is not implemented yet`);
1006
+ }
1007
+ });
940
1008
  }
941
1009
  }
942
-
943
- const dynamicKeys = [
944
- "metadata",
945
- "flow",
946
- "sectioning",
947
- "heading",
948
- "phrasing",
949
- "embedded",
950
- "interactive",
951
- "labelable",
952
- ];
953
- const functionTable = {
954
- isDescendant: isDescendantFacade,
955
- hasAttribute: hasAttributeFacade,
956
- matchAttribute: matchAttributeFacade,
957
- };
958
- const schemaCache = new Map();
959
- function clone(src) {
960
- return JSON.parse(JSON.stringify(src));
1010
+ class PseudoClassMatcher extends Matcher {
1011
+ constructor(pseudoclass, context) {
1012
+ super();
1013
+ const match = pseudoclass.match(/^([^(]+)(?:\((.*)\))?$/);
1014
+ if (!match) {
1015
+ throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
1016
+ }
1017
+ const [, name, args] = match;
1018
+ this.name = name;
1019
+ this.args = args;
1020
+ }
1021
+ match(node, context) {
1022
+ const fn = factory(this.name, context);
1023
+ return fn(node, this.args);
1024
+ }
961
1025
  }
962
- function overwriteMerge$1(a, b) {
963
- return b;
1026
+ class Pattern {
1027
+ constructor(pattern) {
1028
+ const match = pattern.match(/^([~+\->]?)((?:[*]|[^.#[:]+)?)(.*)$/);
1029
+ match.shift(); /* remove full matched string */
1030
+ this.selector = pattern;
1031
+ this.combinator = parseCombinator(match.shift(), pattern);
1032
+ this.tagName = match.shift() || "*";
1033
+ this.pattern = Array.from(splitPattern(match[0]), (it) => this.createMatcher(it));
1034
+ }
1035
+ match(node, context) {
1036
+ return node.is(this.tagName) && this.pattern.every((cur) => cur.match(node, context));
1037
+ }
1038
+ createMatcher(pattern) {
1039
+ switch (pattern[0]) {
1040
+ case ".":
1041
+ return new ClassMatcher(pattern.slice(1));
1042
+ case "#":
1043
+ return new IdMatcher(pattern.slice(1));
1044
+ case "[":
1045
+ return new AttrMatcher(pattern.slice(1, -1));
1046
+ case ":":
1047
+ return new PseudoClassMatcher(pattern.slice(1), this.selector);
1048
+ default:
1049
+ /* istanbul ignore next: fallback solution, the switch cases should cover
1050
+ * everything and there is no known way to trigger this fallback */
1051
+ throw new Error(`Failed to create matcher for "${pattern}"`);
1052
+ }
1053
+ }
964
1054
  }
965
1055
  /**
966
- * @public
1056
+ * DOM Selector.
967
1057
  */
968
- class MetaTable {
969
- /**
970
- * @internal
971
- */
972
- constructor() {
973
- this.elements = {};
974
- this.schema = clone(schema);
975
- }
976
- /**
977
- * @internal
978
- */
979
- init() {
980
- this.resolveGlobal();
1058
+ class Selector {
1059
+ constructor(selector) {
1060
+ this.pattern = Selector.parse(selector);
981
1061
  }
982
1062
  /**
983
- * Extend validation schema.
1063
+ * Match this selector against a HtmlElement.
984
1064
  *
985
- * @internal
1065
+ * @param root - Element to match against.
1066
+ * @returns Iterator with matched elements.
986
1067
  */
987
- extendValidationSchema(patch) {
988
- if (patch.properties) {
989
- this.schema = deepmerge(this.schema, {
990
- patternProperties: {
991
- "^[^$].*$": {
992
- properties: patch.properties,
993
- },
994
- },
995
- });
996
- }
997
- if (patch.definitions) {
998
- this.schema = deepmerge(this.schema, {
999
- definitions: patch.definitions,
1000
- });
1001
- }
1068
+ *match(root) {
1069
+ const context = { scope: root };
1070
+ yield* this.matchInternal(root, 0, context);
1002
1071
  }
1003
- /**
1004
- * Load metadata table from object.
1005
- *
1006
- * @internal
1007
- * @param obj - Object with metadata to load
1008
- * @param filename - Optional filename used when presenting validation error
1009
- */
1010
- loadFromObject(obj, filename = null) {
1011
- var _a;
1012
- const validate = this.getSchemaValidator();
1013
- if (!validate(obj)) {
1014
- throw new SchemaValidationError(filename, `Element metadata is not valid`, obj, this.schema,
1015
- /* istanbul ignore next: AJV sets .errors when validate returns false */
1016
- (_a = validate.errors) !== null && _a !== void 0 ? _a : []);
1072
+ *matchInternal(root, level, context) {
1073
+ if (level >= this.pattern.length) {
1074
+ yield root;
1075
+ return;
1017
1076
  }
1018
- for (const [key, value] of Object.entries(obj)) {
1019
- if (key === "$schema")
1077
+ const pattern = this.pattern[level];
1078
+ const matches = Selector.findCandidates(root, pattern);
1079
+ for (const node of matches) {
1080
+ if (!pattern.match(node, context)) {
1020
1081
  continue;
1021
- this.addEntry(key, migrateElement(value));
1022
- }
1023
- }
1024
- /**
1025
- * Load metadata table from filename
1026
- *
1027
- * @internal
1028
- * @param filename - Filename to load
1029
- */
1030
- loadFromFile(filename) {
1031
- try {
1032
- /* load using require as it can process both js and json */
1033
- const data = requireUncached(filename);
1034
- this.loadFromObject(data, filename);
1035
- }
1036
- catch (err) {
1037
- if (err instanceof SchemaValidationError) {
1038
- throw err;
1039
1082
  }
1040
- throw new UserError(`Failed to load element metadata from "${filename}"`, ensureError(err));
1083
+ yield* this.matchInternal(node, level + 1, context);
1041
1084
  }
1042
1085
  }
1043
- /**
1044
- * Get [[MetaElement]] for the given tag. If no specific metadata is present
1045
- * the global metadata is returned or null if no global is present.
1046
- *
1047
- * @public
1048
- * @returns A shallow copy of metadata.
1049
- */
1050
- getMetaFor(tagName) {
1051
- /* try to locate by tagname */
1052
- tagName = tagName.toLowerCase();
1053
- if (this.elements[tagName]) {
1054
- return { ...this.elements[tagName] };
1055
- }
1056
- /* try to locate global element */
1057
- if (this.elements["*"]) {
1058
- return { ...this.elements["*"] };
1086
+ static parse(selector) {
1087
+ /* strip whitespace before combinators, "ul > li" becomes "ul >li", for
1088
+ * easier parsing */
1089
+ selector = selector.replace(/([+~>]) /g, "$1");
1090
+ const pattern = selector.split(/(?:(?<!\\) )+/);
1091
+ return pattern.map((part) => new Pattern(part));
1092
+ }
1093
+ static findCandidates(root, pattern) {
1094
+ switch (pattern.combinator) {
1095
+ case Combinator.DESCENDANT:
1096
+ return root.getElementsByTagName(pattern.tagName);
1097
+ case Combinator.CHILD:
1098
+ return root.childElements.filter((node) => node.is(pattern.tagName));
1099
+ case Combinator.ADJACENT_SIBLING:
1100
+ return Selector.findAdjacentSibling(root);
1101
+ case Combinator.GENERAL_SIBLING:
1102
+ return Selector.findGeneralSibling(root);
1103
+ case Combinator.SCOPE:
1104
+ return [root];
1059
1105
  }
1060
- return null;
1061
- }
1062
- /**
1063
- * Find all tags which has enabled given property.
1064
- *
1065
- * @public
1066
- */
1067
- getTagsWithProperty(propName) {
1068
- return Object.entries(this.elements)
1069
- .filter(([, entry]) => entry[propName])
1070
- .map(([tagName]) => tagName);
1106
+ /* istanbul ignore next: fallback solution, the switch cases should cover
1107
+ * everything and there is no known way to trigger this fallback */
1108
+ return [];
1071
1109
  }
1072
- /**
1073
- * Find tag matching tagName or inheriting from it.
1074
- *
1075
- * @public
1076
- */
1077
- getTagsDerivedFrom(tagName) {
1078
- return Object.entries(this.elements)
1079
- .filter(([key, entry]) => key === tagName || entry.inherit === tagName)
1080
- .map(([tagName]) => tagName);
1110
+ static findAdjacentSibling(node) {
1111
+ let adjacent = false;
1112
+ return node.siblings.filter((cur) => {
1113
+ if (adjacent) {
1114
+ adjacent = false;
1115
+ return true;
1116
+ }
1117
+ if (cur === node) {
1118
+ adjacent = true;
1119
+ }
1120
+ return false;
1121
+ });
1081
1122
  }
1082
- addEntry(tagName, entry) {
1083
- let parent = this.elements[tagName] || {};
1084
- /* handle inheritance */
1085
- if (entry.inherit) {
1086
- const name = entry.inherit;
1087
- parent = this.elements[name];
1088
- if (!parent) {
1089
- throw new UserError(`Element <${tagName}> cannot inherit from <${name}>: no such element`);
1123
+ static findGeneralSibling(node) {
1124
+ let after = false;
1125
+ return node.siblings.filter((cur) => {
1126
+ if (after) {
1127
+ return true;
1090
1128
  }
1091
- }
1092
- /* merge all sources together */
1093
- const expanded = this.mergeElement(parent, { ...entry, tagName });
1094
- expandRegex(expanded);
1095
- this.elements[tagName] = expanded;
1129
+ if (cur === node) {
1130
+ after = true;
1131
+ }
1132
+ return false;
1133
+ });
1096
1134
  }
1135
+ }
1136
+
1137
+ const TEXT_NODE_NAME = "#text";
1138
+ /**
1139
+ * Returns true if the node is a text node.
1140
+ *
1141
+ * @public
1142
+ */
1143
+ function isTextNode(node) {
1144
+ return Boolean(node && node.nodeType === NodeType.TEXT_NODE);
1145
+ }
1146
+ /**
1147
+ * Represents a text in the HTML document.
1148
+ *
1149
+ * Text nodes are appended as children of `HtmlElement` and cannot have childen
1150
+ * of its own.
1151
+ *
1152
+ * @public
1153
+ */
1154
+ class TextNode extends DOMNode {
1097
1155
  /**
1098
- * Construct a new AJV schema validator.
1156
+ * @param text - Text to add. When a `DynamicValue` is used the expression is
1157
+ * used as "text".
1158
+ * @param location - Source code location of this node.
1099
1159
  */
1100
- getSchemaValidator() {
1101
- const hash = computeHash(JSON.stringify(this.schema));
1102
- const cached = schemaCache.get(hash);
1103
- if (cached) {
1104
- return cached;
1105
- }
1106
- else {
1107
- const ajv = new Ajv({ strict: true, strictTuples: true, strictTypes: true });
1108
- ajv.addMetaSchema(ajvSchemaDraft);
1109
- ajv.addKeyword(ajvRegexpKeyword);
1110
- ajv.addKeyword({ keyword: "copyable" });
1111
- const validate = ajv.compile(this.schema);
1112
- schemaCache.set(hash, validate);
1113
- return validate;
1114
- }
1160
+ constructor(text, location) {
1161
+ super(NodeType.TEXT_NODE, TEXT_NODE_NAME, location);
1162
+ this.text = text;
1115
1163
  }
1116
1164
  /**
1117
- * @public
1165
+ * Get the text from node.
1118
1166
  */
1119
- getJSONSchema() {
1120
- return this.schema;
1167
+ get textContent() {
1168
+ return this.text.toString();
1121
1169
  }
1122
1170
  /**
1123
- * Finds the global element definition and merges each known element with the
1124
- * global, e.g. to assign global attributes.
1171
+ * Flag set to true if the attribute value is static.
1125
1172
  */
1126
- resolveGlobal() {
1127
- /* skip if there is no global elements */
1128
- if (!this.elements["*"])
1129
- return;
1130
- /* fetch and remove the global element, it should not be resolvable by
1131
- * itself */
1132
- const global = this.elements["*"];
1133
- delete this.elements["*"];
1134
- /* hack: unset default properties which global should not override */
1135
- delete global.tagName;
1136
- delete global.void;
1137
- /* merge elements */
1138
- for (const [tagName, entry] of Object.entries(this.elements)) {
1139
- this.elements[tagName] = this.mergeElement(global, entry);
1140
- }
1141
- }
1142
- mergeElement(a, b) {
1143
- const merged = deepmerge(a, b, { arrayMerge: overwriteMerge$1 });
1144
- /* special handling when removing attributes by setting them to null
1145
- * resulting in the deletion flag being set */
1146
- const filteredAttrs = Object.entries(merged.attributes).filter(([, attr]) => {
1147
- const val = !attr.delete;
1148
- delete attr.delete;
1149
- return val;
1150
- });
1151
- merged.attributes = Object.fromEntries(filteredAttrs);
1152
- return merged;
1173
+ get isStatic() {
1174
+ return !this.isDynamic;
1153
1175
  }
1154
1176
  /**
1155
- * @internal
1177
+ * Flag set to true if the attribute value is dynamic.
1156
1178
  */
1157
- resolve(node) {
1158
- if (node.meta) {
1159
- expandProperties(node, node.meta);
1160
- }
1161
- }
1162
- }
1163
- function expandProperties(node, entry) {
1164
- for (const key of dynamicKeys) {
1165
- const property = entry[key];
1166
- if (property && typeof property !== "boolean") {
1167
- setMetaProperty(entry, key, evaluateProperty(node, property));
1168
- }
1179
+ get isDynamic() {
1180
+ return this.text instanceof DynamicValue;
1169
1181
  }
1170
1182
  }
1183
+
1171
1184
  /**
1172
- * Given a string it returns either the string as-is or if the string is wrapped
1173
- * in /../ it creates and returns a regex instead.
1185
+ * @public
1174
1186
  */
1175
- function expandRegexValue(value) {
1176
- if (value instanceof RegExp) {
1177
- return value;
1178
- }
1179
- const match = value.match(/^\/\^?([^/$]*)\$?\/([i]*)$/);
1180
- if (match) {
1181
- const [, expr, flags] = match;
1182
- // eslint-disable-next-line security/detect-non-literal-regexp
1183
- return new RegExp(`^${expr}$`, flags);
1184
- }
1185
- else {
1186
- return value;
1187
- }
1188
- }
1187
+ var NodeClosed;
1188
+ (function (NodeClosed) {
1189
+ NodeClosed[NodeClosed["Open"] = 0] = "Open";
1190
+ NodeClosed[NodeClosed["EndTag"] = 1] = "EndTag";
1191
+ NodeClosed[NodeClosed["VoidOmitted"] = 2] = "VoidOmitted";
1192
+ NodeClosed[NodeClosed["VoidSelfClosed"] = 3] = "VoidSelfClosed";
1193
+ NodeClosed[NodeClosed["ImplicitClosed"] = 4] = "ImplicitClosed";
1194
+ })(NodeClosed || (NodeClosed = {}));
1189
1195
  /**
1190
- * Expand all regular expressions in strings ("/../"). This mutates the object.
1196
+ * Returns true if the node is an element node.
1197
+ *
1198
+ * @public
1191
1199
  */
1192
- function expandRegex(entry) {
1193
- for (const [name, values] of Object.entries(entry.attributes)) {
1194
- if (values.enum) {
1195
- entry.attributes[name].enum = values.enum.map(expandRegexValue);
1196
- }
1197
- }
1200
+ function isElementNode(node) {
1201
+ return Boolean(node && node.nodeType === NodeType.ELEMENT_NODE);
1198
1202
  }
1199
- function evaluateProperty(node, expr) {
1200
- const [func, options] = parseExpression(expr);
1201
- return func(node, options);
1203
+ function isValidTagName(tagName) {
1204
+ return Boolean(tagName !== "" && tagName !== "*");
1202
1205
  }
1203
- function parseExpression(expr) {
1204
- if (typeof expr === "string") {
1205
- return parseExpression([expr, {}]);
1206
- }
1207
- else {
1208
- const [funcName, options] = expr;
1209
- const func = functionTable[funcName];
1210
- if (!func) {
1211
- throw new Error(`Failed to find function "${funcName}" when evaluating property expression`);
1206
+ /**
1207
+ * @public
1208
+ */
1209
+ class HtmlElement extends DOMNode {
1210
+ constructor(tagName, parent, closed, meta, location) {
1211
+ const nodeType = tagName ? NodeType.ELEMENT_NODE : NodeType.DOCUMENT_NODE;
1212
+ super(nodeType, tagName, location);
1213
+ if (!isValidTagName(tagName)) {
1214
+ throw new Error(`The tag name provided ('${tagName || ""}') is not a valid name`);
1215
+ }
1216
+ this.tagName = tagName || "#document";
1217
+ this.parent = parent !== null && parent !== void 0 ? parent : null;
1218
+ this.attr = {};
1219
+ this.metaElement = meta !== null && meta !== void 0 ? meta : null;
1220
+ this.closed = closed;
1221
+ this.voidElement = meta ? Boolean(meta.void) : false;
1222
+ this.depth = 0;
1223
+ this.annotation = null;
1224
+ if (parent) {
1225
+ parent.childNodes.push(this);
1226
+ /* calculate depth in domtree */
1227
+ let cur = parent;
1228
+ while (cur.parent) {
1229
+ this.depth++;
1230
+ cur = cur.parent;
1231
+ }
1212
1232
  }
1213
- return [func, options];
1214
- }
1215
- }
1216
- function isDescendantFacade(node, tagName) {
1217
- if (typeof tagName !== "string") {
1218
- throw new Error(`Property expression "isDescendant" must take string argument when evaluating metadata for <${node.tagName}>`);
1219
- }
1220
- return isDescendant(node, tagName);
1221
- }
1222
- function hasAttributeFacade(node, attr) {
1223
- if (typeof attr !== "string") {
1224
- throw new Error(`Property expression "hasAttribute" must take string argument when evaluating metadata for <${node.tagName}>`);
1225
- }
1226
- return hasAttribute(node, attr);
1227
- }
1228
- function matchAttributeFacade(node, match) {
1229
- if (!Array.isArray(match) || match.length !== 3) {
1230
- throw new Error(`Property expression "matchAttribute" must take [key, op, value] array as argument when evaluating metadata for <${node.tagName}>`);
1231
- }
1232
- const [key, op, value] = match.map((x) => x.toLowerCase());
1233
- switch (op) {
1234
- case "!=":
1235
- case "=":
1236
- return matchAttribute(node, key, op, value);
1237
- default:
1238
- throw new Error(`Property expression "matchAttribute" has invalid operator "${op}" when evaluating metadata for <${node.tagName}>`);
1239
1233
  }
1240
- }
1241
-
1242
- /**
1243
- * @public
1244
- */
1245
- class DynamicValue {
1246
- constructor(expr) {
1247
- this.expr = expr;
1234
+ /**
1235
+ * @internal
1236
+ */
1237
+ static rootNode(location) {
1238
+ const root = new HtmlElement(undefined, null, NodeClosed.EndTag, null, location);
1239
+ root.setAnnotation("#document");
1240
+ return root;
1248
1241
  }
1249
- toString() {
1250
- return this.expr;
1242
+ /**
1243
+ * @internal
1244
+ *
1245
+ * @param namespace - If given it is appended to the tagName.
1246
+ */
1247
+ static fromTokens(startToken, endToken, parent, metaTable, namespace = "") {
1248
+ const name = startToken.data[2];
1249
+ const tagName = namespace ? `${namespace}:${name}` : name;
1250
+ if (!name) {
1251
+ throw new Error("tagName cannot be empty");
1252
+ }
1253
+ const meta = metaTable ? metaTable.getMetaFor(tagName) : null;
1254
+ const open = startToken.data[1] !== "/";
1255
+ const closed = isClosed(endToken, meta);
1256
+ /* location contains position of '<' so strip it out */
1257
+ const location = sliceLocation(startToken.location, 1);
1258
+ return new HtmlElement(tagName, open ? parent : null, closed, meta, location);
1251
1259
  }
1252
- }
1253
-
1254
- /**
1255
- * DOM Attribute.
1256
- *
1257
- * Represents a HTML attribute. Can contain either a fixed static value or a
1258
- * placeholder for dynamic values (e.g. interpolated).
1259
- */
1260
- class Attribute {
1261
1260
  /**
1262
- * @param key - Attribute name.
1263
- * @param value - Attribute value. Set to `null` for boolean attributes.
1264
- * @param keyLocation - Source location of attribute name.
1265
- * @param valueLocation - Source location of attribute value.
1266
- * @param originalAttribute - If this attribute was dynamically added via a
1267
- * transformation (e.g. vuejs `:id` generating the `id` attribute) this
1268
- * parameter should be set to the attribute name of the source attribute (`:id`).
1261
+ * Returns annotated name if set or defaults to `<tagName>`.
1262
+ *
1263
+ * E.g. `my-annotation` or `<div>`.
1269
1264
  */
1270
- constructor(key, value, keyLocation, valueLocation, originalAttribute) {
1271
- this.key = key;
1272
- this.value = value;
1273
- this.keyLocation = keyLocation;
1274
- this.valueLocation = valueLocation;
1275
- this.originalAttribute = originalAttribute;
1276
- /* force undefined to null */
1277
- if (typeof this.value === "undefined") {
1278
- this.value = null;
1265
+ get annotatedName() {
1266
+ if (this.annotation) {
1267
+ return this.annotation;
1268
+ }
1269
+ else {
1270
+ return `<${this.tagName}>`;
1279
1271
  }
1280
1272
  }
1281
1273
  /**
1282
- * Flag set to true if the attribute value is static.
1274
+ * Get list of IDs referenced by `aria-labelledby`.
1275
+ *
1276
+ * If the attribute is unset or empty this getter returns null.
1277
+ * If the attribute is dynamic the original {@link DynamicValue} is returned.
1278
+ *
1279
+ * @public
1283
1280
  */
1284
- get isStatic() {
1285
- return !this.isDynamic;
1281
+ get ariaLabelledby() {
1282
+ const attr = this.getAttribute("aria-labelledby");
1283
+ if (!attr || !attr.value) {
1284
+ return null;
1285
+ }
1286
+ if (attr.value instanceof DynamicValue) {
1287
+ return attr.value;
1288
+ }
1289
+ const list = new DOMTokenList(attr.value, attr.valueLocation);
1290
+ return list.length ? Array.from(list) : null;
1286
1291
  }
1287
1292
  /**
1288
- * Flag set to true if the attribute value is dynamic.
1293
+ * Similar to childNodes but only elements.
1289
1294
  */
1290
- get isDynamic() {
1291
- return this.value instanceof DynamicValue;
1295
+ get childElements() {
1296
+ return this.childNodes.filter(isElementNode);
1292
1297
  }
1293
- valueMatches(pattern, dynamicMatches = true) {
1294
- if (this.value === null) {
1295
- return false;
1298
+ /**
1299
+ * Find the first ancestor matching a selector.
1300
+ *
1301
+ * Implementation of DOM specification of Element.closest(selectors).
1302
+ */
1303
+ closest(selectors) {
1304
+ /* eslint-disable-next-line @typescript-eslint/no-this-alias */
1305
+ let node = this;
1306
+ while (node) {
1307
+ if (node.matches(selectors)) {
1308
+ return node;
1309
+ }
1310
+ node = node.parent;
1296
1311
  }
1297
- /* dynamic values matches everything */
1298
- if (this.value instanceof DynamicValue) {
1299
- return dynamicMatches;
1312
+ return null;
1313
+ }
1314
+ /**
1315
+ * Generate a DOM selector for this element. The returned selector will be
1316
+ * unique inside the current document.
1317
+ */
1318
+ generateSelector() {
1319
+ /* root element cannot have a selector as it isn't a proper element */
1320
+ if (this.isRootElement()) {
1321
+ return null;
1300
1322
  }
1301
- /* test value against pattern */
1302
- if (pattern instanceof RegExp) {
1303
- return this.value.match(pattern) !== null;
1323
+ const parts = [];
1324
+ let root;
1325
+ /* eslint-disable-next-line @typescript-eslint/no-this-alias */
1326
+ for (root = this; root.parent; root = root.parent) {
1327
+ /* .. */
1304
1328
  }
1305
- else {
1306
- return this.value === pattern;
1329
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
1330
+ for (let cur = this; cur.parent; cur = cur.parent) {
1331
+ /* if a unique id is present, use it and short-circuit */
1332
+ if (cur.id) {
1333
+ const selector = generateIdSelector(cur.id);
1334
+ const matches = root.querySelectorAll(selector);
1335
+ if (matches.length === 1) {
1336
+ parts.push(selector);
1337
+ break;
1338
+ }
1339
+ }
1340
+ const parent = cur.parent;
1341
+ const child = parent.childElements;
1342
+ const index = child.findIndex((it) => it.unique === cur.unique);
1343
+ const numOfType = child.filter((it) => it.is(cur.tagName)).length;
1344
+ const solo = numOfType === 1;
1345
+ /* if this is the only tagName in this level of siblings nth-child isn't needed */
1346
+ if (solo) {
1347
+ parts.push(cur.tagName.toLowerCase());
1348
+ continue;
1349
+ }
1350
+ /* this will generate the worst kind of selector but at least it will be accurate (optimizations welcome) */
1351
+ parts.push(`${cur.tagName.toLowerCase()}:nth-child(${index + 1})`);
1307
1352
  }
1353
+ return parts.reverse().join(" > ");
1308
1354
  }
1309
- }
1310
-
1311
- function getCSSDeclarations(value) {
1312
- return value
1313
- .trim()
1314
- .split(";")
1315
- .filter(Boolean)
1316
- .map((it) => {
1317
- const [property, value] = it.split(":", 2);
1318
- return [property.trim(), value ? value.trim() : ""];
1319
- });
1320
- }
1321
- /**
1322
- * @internal
1323
- */
1324
- function parseCssDeclaration(value) {
1325
- if (!value || value instanceof DynamicValue) {
1326
- return {};
1327
- }
1328
- const pairs = getCSSDeclarations(value);
1329
- return Object.fromEntries(pairs);
1330
- }
1331
-
1332
- function sliceSize(size, begin, end) {
1333
- if (typeof size !== "number") {
1334
- return size;
1335
- }
1336
- if (typeof end !== "number") {
1337
- return size - begin;
1338
- }
1339
- if (end < 0) {
1340
- end = size + end;
1355
+ /**
1356
+ * Tests if this element has given tagname.
1357
+ *
1358
+ * If passing "*" this test will pass if any tagname is set.
1359
+ */
1360
+ is(tagName) {
1361
+ return tagName === "*" || this.tagName.toLowerCase() === tagName.toLowerCase();
1341
1362
  }
1342
- return Math.min(size, end - begin);
1343
- }
1344
- function sliceLocation(location, begin, end, wrap) {
1345
- if (!location)
1346
- return null;
1347
- const size = sliceSize(location.size, begin, end);
1348
- const sliced = {
1349
- filename: location.filename,
1350
- offset: location.offset + begin,
1351
- line: location.line,
1352
- column: location.column + begin,
1353
- size,
1354
- };
1355
- /* if text content is provided try to find all newlines and modify line/column accordingly */
1356
- if (wrap) {
1357
- let index = -1;
1358
- const col = sliced.column;
1359
- do {
1360
- index = wrap.indexOf("\n", index + 1);
1361
- if (index >= 0 && index < begin) {
1362
- sliced.column = col - (index + 1);
1363
- sliced.line++;
1363
+ /**
1364
+ * Load new element metadata onto this element.
1365
+ *
1366
+ * Do note that semantics such as `void` cannot be changed (as the element has
1367
+ * already been created). In addition the element will still "be" the same
1368
+ * element, i.e. even if loading meta for a `<p>` tag upon a `<div>` tag it
1369
+ * will still be a `<div>` as far as the rest of the validator is concerned.
1370
+ *
1371
+ * In fact only certain properties will be copied onto the element:
1372
+ *
1373
+ * - content categories (flow, phrasing, etc)
1374
+ * - required attributes
1375
+ * - attribute allowed values
1376
+ * - permitted/required elements
1377
+ *
1378
+ * Properties *not* loaded:
1379
+ *
1380
+ * - inherit
1381
+ * - deprecated
1382
+ * - foreign
1383
+ * - void
1384
+ * - implicitClosed
1385
+ * - scriptSupporting
1386
+ * - deprecatedAttributes
1387
+ *
1388
+ * Changes to element metadata will only be visible after `element:ready` (and
1389
+ * the subsequent `dom:ready` event).
1390
+ */
1391
+ loadMeta(meta) {
1392
+ if (!this.metaElement) {
1393
+ this.metaElement = {};
1394
+ }
1395
+ for (const key of MetaCopyableProperty) {
1396
+ const value = meta[key];
1397
+ if (typeof value !== "undefined") {
1398
+ setMetaProperty(this.metaElement, key, value);
1364
1399
  }
1365
1400
  else {
1366
- break;
1401
+ delete this.metaElement[key];
1367
1402
  }
1368
- } while (true); // eslint-disable-line no-constant-condition
1369
- }
1370
- return sliced;
1371
- }
1372
-
1373
- var State;
1374
- (function (State) {
1375
- State[State["INITIAL"] = 1] = "INITIAL";
1376
- State[State["DOCTYPE"] = 2] = "DOCTYPE";
1377
- State[State["TEXT"] = 3] = "TEXT";
1378
- State[State["TAG"] = 4] = "TAG";
1379
- State[State["ATTR"] = 5] = "ATTR";
1380
- State[State["CDATA"] = 6] = "CDATA";
1381
- State[State["SCRIPT"] = 7] = "SCRIPT";
1382
- State[State["STYLE"] = 8] = "STYLE";
1383
- })(State || (State = {}));
1384
-
1385
- var ContentModel;
1386
- (function (ContentModel) {
1387
- ContentModel[ContentModel["TEXT"] = 1] = "TEXT";
1388
- ContentModel[ContentModel["SCRIPT"] = 2] = "SCRIPT";
1389
- ContentModel[ContentModel["STYLE"] = 3] = "STYLE";
1390
- })(ContentModel || (ContentModel = {}));
1391
- class Context {
1392
- constructor(source) {
1393
- var _a, _b, _c, _d;
1394
- this.state = State.INITIAL;
1395
- this.string = source.data;
1396
- this.filename = (_a = source.filename) !== null && _a !== void 0 ? _a : "";
1397
- this.offset = (_b = source.offset) !== null && _b !== void 0 ? _b : 0;
1398
- this.line = (_c = source.line) !== null && _c !== void 0 ? _c : 1;
1399
- this.column = (_d = source.column) !== null && _d !== void 0 ? _d : 1;
1400
- this.contentModel = ContentModel.TEXT;
1401
- }
1402
- getTruncatedLine(n = 13) {
1403
- return JSON.stringify(this.string.length > n ? `${this.string.slice(0, 10)}...` : this.string);
1403
+ }
1404
1404
  }
1405
- consume(n, state) {
1406
- /* if "n" is an regex match the first value is the full matched
1407
- * string so consume that many characters. */
1408
- if (typeof n !== "number") {
1409
- n = n[0].length; /* regex match */
1405
+ /**
1406
+ * Match this element against given selectors. Returns true if any selector
1407
+ * matches.
1408
+ *
1409
+ * Implementation of DOM specification of Element.matches(selectors).
1410
+ */
1411
+ matches(selector) {
1412
+ /* find root element */
1413
+ /* eslint-disable-next-line @typescript-eslint/no-this-alias */
1414
+ let root = this;
1415
+ while (root.parent) {
1416
+ root = root.parent;
1410
1417
  }
1411
- /* poor mans line counter :( */
1412
- let consumed = this.string.slice(0, n);
1413
- let offset;
1414
- while ((offset = consumed.indexOf("\n")) >= 0) {
1415
- this.line++;
1416
- this.column = 1;
1417
- consumed = consumed.substr(offset + 1);
1418
+ /* a bit slow implementation as it finds all candidates for the selector and
1419
+ * then tests if any of them are the current element. A better
1420
+ * implementation would be to walk the selector right-to-left and test
1421
+ * ancestors. */
1422
+ for (const match of root.querySelectorAll(selector)) {
1423
+ if (match.unique === this.unique) {
1424
+ return true;
1425
+ }
1418
1426
  }
1419
- this.column += consumed.length;
1420
- this.offset += n;
1421
- /* remove N chars */
1422
- this.string = this.string.substr(n);
1423
- /* change state */
1424
- this.state = state;
1427
+ return false;
1425
1428
  }
1426
- getLocation(size) {
1427
- return {
1428
- filename: this.filename,
1429
- offset: this.offset,
1430
- line: this.line,
1431
- column: this.column,
1432
- size,
1433
- };
1429
+ get meta() {
1430
+ return this.metaElement;
1434
1431
  }
1435
- }
1436
-
1437
- var NodeType;
1438
- (function (NodeType) {
1439
- NodeType[NodeType["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
1440
- NodeType[NodeType["TEXT_NODE"] = 3] = "TEXT_NODE";
1441
- NodeType[NodeType["DOCUMENT_NODE"] = 9] = "DOCUMENT_NODE";
1442
- })(NodeType || (NodeType = {}));
1443
-
1444
- const DOCUMENT_NODE_NAME = "#document";
1445
- const TEXT_CONTENT = Symbol("textContent");
1446
- let counter = 0;
1447
- class DOMNode {
1448
1432
  /**
1449
- * Create a new DOMNode.
1450
- *
1451
- * @param nodeType - What node type to create.
1452
- * @param nodeName - What node name to use. For `HtmlElement` this corresponds
1453
- * to the tagName but other node types have specific predefined values.
1454
- * @param location - Source code location of this node.
1433
+ * Set annotation for this element.
1455
1434
  */
1456
- constructor(nodeType, nodeName, location) {
1457
- this.nodeType = nodeType;
1458
- this.nodeName = nodeName !== null && nodeName !== void 0 ? nodeName : DOCUMENT_NODE_NAME;
1459
- this.location = location;
1460
- this.disabledRules = new Set();
1461
- this.childNodes = [];
1462
- this.unique = counter++;
1463
- this.cache = null;
1435
+ setAnnotation(text) {
1436
+ this.annotation = text;
1464
1437
  }
1465
1438
  /**
1466
- * Enable cache for this node.
1439
+ * Set attribute. Stores all attributes set even with the same name.
1467
1440
  *
1468
- * Should not be called before the node and all children are fully constructed.
1441
+ * @param key - Attribute name
1442
+ * @param value - Attribute value. Use `null` if no value is present.
1443
+ * @param keyLocation - Location of the attribute name.
1444
+ * @param valueLocation - Location of the attribute value (excluding quotation)
1445
+ * @param originalAttribute - If attribute is an alias for another attribute
1446
+ * (dynamic attributes) set this to the original attribute name.
1469
1447
  */
1470
- cacheEnable() {
1471
- this.cache = new Map();
1448
+ setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
1449
+ key = key.toLowerCase();
1450
+ if (!this.attr[key]) {
1451
+ this.attr[key] = [];
1452
+ }
1453
+ this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
1472
1454
  }
1473
- cacheGet(key) {
1474
- if (this.cache) {
1475
- return this.cache.get(key);
1455
+ /**
1456
+ * Get a list of all attributes on this node.
1457
+ */
1458
+ get attributes() {
1459
+ return Object.values(this.attr).reduce((result, cur) => {
1460
+ return result.concat(cur);
1461
+ }, []);
1462
+ }
1463
+ hasAttribute(key) {
1464
+ key = key.toLowerCase();
1465
+ return key in this.attr;
1466
+ }
1467
+ getAttribute(key, all = false) {
1468
+ key = key.toLowerCase();
1469
+ if (key in this.attr) {
1470
+ const matches = this.attr[key];
1471
+ return all ? matches : matches[0];
1476
1472
  }
1477
1473
  else {
1478
- return undefined;
1479
- }
1480
- }
1481
- cacheSet(key, value) {
1482
- if (this.cache) {
1483
- this.cache.set(key, value);
1474
+ return null;
1484
1475
  }
1485
- return value;
1486
1476
  }
1487
- cacheRemove(key) {
1488
- if (this.cache) {
1489
- return this.cache.delete(key);
1477
+ /**
1478
+ * Get attribute value.
1479
+ *
1480
+ * Returns the attribute value if present.
1481
+ *
1482
+ * - Missing attributes return `null`.
1483
+ * - Boolean attributes return `null`.
1484
+ * - `DynamicValue` returns attribute expression.
1485
+ *
1486
+ * @param key - Attribute name
1487
+ * @returns Attribute value or null.
1488
+ */
1489
+ getAttributeValue(key) {
1490
+ const attr = this.getAttribute(key);
1491
+ if (attr) {
1492
+ return attr.value !== null ? attr.value.toString() : null;
1490
1493
  }
1491
1494
  else {
1492
- return false;
1495
+ return null;
1493
1496
  }
1494
1497
  }
1495
- cacheExists(key) {
1496
- return Boolean(this.cache && this.cache.has(key));
1498
+ /**
1499
+ * Add text as a child node to this element.
1500
+ *
1501
+ * @param text - Text to add.
1502
+ * @param location - Source code location of this text.
1503
+ */
1504
+ appendText(text, location) {
1505
+ this.childNodes.push(new TextNode(text, location));
1497
1506
  }
1498
1507
  /**
1499
- * Get the text (recursive) from all child nodes.
1508
+ * Return a list of all known classes on the element. Dynamic values are
1509
+ * ignored.
1500
1510
  */
1501
- get textContent() {
1502
- const cached = this.cacheGet(TEXT_CONTENT);
1503
- if (cached) {
1504
- return cached;
1511
+ get classList() {
1512
+ if (!this.hasAttribute("class")) {
1513
+ return new DOMTokenList(null, null);
1505
1514
  }
1506
- const text = this.childNodes.map((node) => node.textContent).join("");
1507
- this.cacheSet(TEXT_CONTENT, text);
1508
- return text;
1515
+ const classes = this.getAttribute("class", true)
1516
+ .filter((attr) => attr.isStatic)
1517
+ .map((attr) => attr.value)
1518
+ .join(" ");
1519
+ return new DOMTokenList(classes, null);
1509
1520
  }
1510
- append(node) {
1511
- this.childNodes.push(node);
1521
+ /**
1522
+ * Get element ID if present.
1523
+ */
1524
+ get id() {
1525
+ return this.getAttributeValue("id");
1512
1526
  }
1513
- isRootElement() {
1514
- return this.nodeType === NodeType.DOCUMENT_NODE;
1527
+ get style() {
1528
+ const attr = this.getAttribute("style");
1529
+ return parseCssDeclaration(attr === null || attr === void 0 ? void 0 : attr.value);
1515
1530
  }
1516
1531
  /**
1517
- * Tests if two nodes are the same (references the same object).
1532
+ * Returns the first child element or null if there are no child elements.
1518
1533
  */
1519
- isSameNode(otherNode) {
1520
- return this.unique === otherNode.unique;
1534
+ get firstElementChild() {
1535
+ const children = this.childElements;
1536
+ return children.length > 0 ? children[0] : null;
1537
+ }
1538
+ /**
1539
+ * Returns the last child element or null if there are no child elements.
1540
+ */
1541
+ get lastElementChild() {
1542
+ const children = this.childElements;
1543
+ return children.length > 0 ? children[children.length - 1] : null;
1544
+ }
1545
+ get siblings() {
1546
+ return this.parent ? this.parent.childElements : [this];
1547
+ }
1548
+ get previousSibling() {
1549
+ const i = this.siblings.findIndex((node) => node.unique === this.unique);
1550
+ return i >= 1 ? this.siblings[i - 1] : null;
1551
+ }
1552
+ get nextSibling() {
1553
+ const i = this.siblings.findIndex((node) => node.unique === this.unique);
1554
+ return i <= this.siblings.length - 2 ? this.siblings[i + 1] : null;
1555
+ }
1556
+ getElementsByTagName(tagName) {
1557
+ return this.childElements.reduce((matches, node) => {
1558
+ return matches.concat(node.is(tagName) ? [node] : [], node.getElementsByTagName(tagName));
1559
+ }, []);
1521
1560
  }
1522
- /**
1523
- * Returns a DOMNode representing the first direct child node or `null` if the
1524
- * node has no children.
1525
- */
1526
- get firstChild() {
1527
- return this.childNodes[0] || null;
1561
+ querySelector(selector) {
1562
+ var _a;
1563
+ const it = this.querySelectorImpl(selector);
1564
+ return (_a = it.next().value) !== null && _a !== void 0 ? _a : null; // eslint-disable-line @typescript-eslint/no-unsafe-return
1528
1565
  }
1529
- /**
1530
- * Returns a DOMNode representing the last direct child node or `null` if the
1531
- * node has no children.
1532
- */
1533
- get lastChild() {
1534
- return this.childNodes[this.childNodes.length - 1] || null;
1566
+ querySelectorAll(selector) {
1567
+ const it = this.querySelectorImpl(selector);
1568
+ const unique = new Set(it);
1569
+ return Array.from(unique.values());
1535
1570
  }
1536
- /**
1537
- * Disable a rule for this node.
1538
- */
1539
- disableRule(ruleId) {
1540
- this.disabledRules.add(ruleId);
1571
+ *querySelectorImpl(selectorList) {
1572
+ if (!selectorList) {
1573
+ return;
1574
+ }
1575
+ for (const selector of selectorList.split(/,\s*/)) {
1576
+ const pattern = new Selector(selector);
1577
+ yield* pattern.match(this);
1578
+ }
1541
1579
  }
1542
1580
  /**
1543
- * Disables multiple rules.
1581
+ * Visit all nodes from this node and down. Depth first.
1582
+ *
1583
+ * @internal
1544
1584
  */
1545
- disableRules(rules) {
1546
- for (const rule of rules) {
1547
- this.disableRule(rule);
1585
+ visitDepthFirst(callback) {
1586
+ function visit(node) {
1587
+ node.childElements.forEach(visit);
1588
+ if (!node.isRootElement()) {
1589
+ callback(node);
1590
+ }
1548
1591
  }
1592
+ visit(this);
1549
1593
  }
1550
1594
  /**
1551
- * Enable a previously disabled rule for this node.
1595
+ * Evaluates callbackk on all descendants, returning true if any are true.
1596
+ *
1597
+ * @internal
1552
1598
  */
1553
- enableRule(ruleId) {
1554
- this.disabledRules.delete(ruleId);
1599
+ someChildren(callback) {
1600
+ return this.childElements.some(visit);
1601
+ function visit(node) {
1602
+ if (callback(node)) {
1603
+ return true;
1604
+ }
1605
+ else {
1606
+ return node.childElements.some(visit);
1607
+ }
1608
+ }
1555
1609
  }
1556
1610
  /**
1557
- * Enables multiple rules.
1611
+ * Evaluates callbackk on all descendants, returning true if all are true.
1612
+ *
1613
+ * @internal
1558
1614
  */
1559
- enableRules(rules) {
1560
- for (const rule of rules) {
1561
- this.enableRule(rule);
1615
+ everyChildren(callback) {
1616
+ return this.childElements.every(visit);
1617
+ function visit(node) {
1618
+ if (!callback(node)) {
1619
+ return false;
1620
+ }
1621
+ return node.childElements.every(visit);
1562
1622
  }
1563
1623
  }
1564
1624
  /**
1565
- * Test if a rule is enabled for this node.
1625
+ * Visit all nodes from this node and down. Breadth first.
1626
+ *
1627
+ * The first node for which the callback evaluates to true is returned.
1628
+ *
1629
+ * @internal
1566
1630
  */
1567
- ruleEnabled(ruleId) {
1568
- return !this.disabledRules.has(ruleId);
1569
- }
1570
- generateSelector() {
1571
- return null;
1572
- }
1573
- }
1574
-
1575
- function parse(text, baseLocation) {
1576
- const tokens = [];
1577
- const locations = baseLocation ? [] : null;
1578
- for (let begin = 0; begin < text.length;) {
1579
- let end = text.indexOf(" ", begin);
1580
- /* if the last space was found move the position to the last character
1581
- * in the string */
1582
- if (end === -1) {
1583
- end = text.length;
1584
- }
1585
- /* handle multiple spaces */
1586
- const size = end - begin;
1587
- if (size === 0) {
1588
- begin++;
1589
- continue;
1590
- }
1591
- /* extract token */
1592
- const token = text.substring(begin, end);
1593
- tokens.push(token);
1594
- /* extract location */
1595
- if (locations && baseLocation) {
1596
- const location = sliceLocation(baseLocation, begin, end);
1597
- locations.push(location);
1631
+ find(callback) {
1632
+ function visit(node) {
1633
+ if (callback(node)) {
1634
+ return node;
1635
+ }
1636
+ for (const child of node.childElements) {
1637
+ const match = child.find(callback);
1638
+ if (match) {
1639
+ return match;
1640
+ }
1641
+ }
1642
+ return null;
1598
1643
  }
1599
- /* advance position to the character after the current end position */
1600
- begin += size + 1;
1644
+ return visit(this);
1601
1645
  }
1602
- return { tokens, locations };
1603
1646
  }
1604
- class DOMTokenList extends Array {
1605
- constructor(value, location) {
1606
- if (value && typeof value === "string") {
1607
- /* replace all whitespace with a single space for easier parsing */
1608
- const normalized = value.replace(/[\t\r\n]/g, " ");
1609
- const { tokens, locations } = parse(normalized, location);
1610
- super(...tokens);
1611
- this.locations = locations;
1612
- }
1613
- else {
1614
- super(0);
1615
- this.locations = null;
1616
- }
1617
- if (value instanceof DynamicValue) {
1618
- this.value = value.expr;
1619
- }
1620
- else {
1621
- this.value = value || "";
1622
- }
1623
- }
1624
- item(n) {
1625
- return this[n];
1626
- }
1627
- location(n) {
1628
- if (this.locations) {
1629
- return this.locations[n];
1630
- }
1631
- else {
1632
- throw new Error("Trying to access DOMTokenList location when base location isn't set");
1633
- }
1634
- }
1635
- contains(token) {
1636
- return this.includes(token);
1647
+ function isClosed(endToken, meta) {
1648
+ let closed = NodeClosed.Open;
1649
+ if (meta && meta.void) {
1650
+ closed = NodeClosed.VoidOmitted;
1637
1651
  }
1638
- *iterator() {
1639
- for (let index = 0; index < this.length; index++) {
1640
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
1641
- const item = this.item(index);
1642
- const location = this.location(index);
1643
- /* eslint-enable @typescript-eslint/no-non-null-assertion */
1644
- yield { index, item, location };
1645
- }
1652
+ if (endToken.data[0] === "/>") {
1653
+ closed = NodeClosed.VoidSelfClosed;
1646
1654
  }
1655
+ return closed;
1647
1656
  }
1648
1657
 
1649
- var Combinator;
1650
- (function (Combinator) {
1651
- Combinator[Combinator["DESCENDANT"] = 1] = "DESCENDANT";
1652
- Combinator[Combinator["CHILD"] = 2] = "CHILD";
1653
- Combinator[Combinator["ADJACENT_SIBLING"] = 3] = "ADJACENT_SIBLING";
1654
- Combinator[Combinator["GENERAL_SIBLING"] = 4] = "GENERAL_SIBLING";
1655
- /* special cases */
1656
- Combinator[Combinator["SCOPE"] = 5] = "SCOPE";
1657
- })(Combinator || (Combinator = {}));
1658
- function parseCombinator(combinator, pattern) {
1659
- /* special case, when pattern is :scope [[Selector]] will handle this
1660
- * "combinator" to match itself instead of descendants */
1661
- if (pattern === ":scope") {
1662
- return Combinator.SCOPE;
1658
+ class DOMTree {
1659
+ constructor(location) {
1660
+ this.root = HtmlElement.rootNode(location);
1661
+ this.active = this.root;
1662
+ this.doctype = null;
1663
1663
  }
1664
- switch (combinator) {
1665
- case undefined:
1666
- case null:
1667
- case "":
1668
- return Combinator.DESCENDANT;
1669
- case ">":
1670
- return Combinator.CHILD;
1671
- case "+":
1672
- return Combinator.ADJACENT_SIBLING;
1673
- case "~":
1674
- return Combinator.GENERAL_SIBLING;
1675
- default:
1676
- throw new Error(`Unknown combinator "${combinator}"`);
1664
+ pushActive(node) {
1665
+ this.active = node;
1677
1666
  }
1678
- }
1679
-
1680
- function firstChild(node) {
1681
- return node.previousSibling === null;
1682
- }
1683
-
1684
- function lastChild(node) {
1685
- return node.nextSibling === null;
1686
- }
1687
-
1688
- const cache = {};
1689
- function getNthChild(node) {
1690
- if (!node.parent) {
1691
- return -1;
1667
+ popActive() {
1668
+ if (this.active.isRootElement()) {
1669
+ /* root element should never be popped, continue as if nothing happened */
1670
+ return;
1671
+ }
1672
+ this.active = this.active.parent || this.root;
1692
1673
  }
1693
- if (!cache[node.unique]) {
1694
- const parent = node.parent;
1695
- const index = parent.childElements.findIndex((cur) => {
1696
- return cur.unique === node.unique;
1697
- });
1698
- cache[node.unique] = index + 1; /* nthChild starts at 1 */
1674
+ getActive() {
1675
+ return this.active;
1699
1676
  }
1700
- return cache[node.unique];
1701
- }
1702
- function nthChild(node, args) {
1703
- if (!args) {
1704
- throw new Error("Missing argument to nth-child");
1677
+ /**
1678
+ * Resolve dynamic meta expressions.
1679
+ */
1680
+ resolveMeta(table) {
1681
+ this.visitDepthFirst((node) => table.resolve(node));
1682
+ }
1683
+ getElementsByTagName(tagName) {
1684
+ return this.root.getElementsByTagName(tagName);
1705
1685
  }
1706
- const n = parseInt(args.trim(), 10);
1707
- const cur = getNthChild(node);
1708
- return cur === n;
1709
- }
1710
-
1711
- function scope(node) {
1712
- return node.isSameNode(this.scope);
1713
- }
1714
-
1715
- const table = {
1716
- "first-child": firstChild,
1717
- "last-child": lastChild,
1718
- "nth-child": nthChild,
1719
- scope: scope,
1720
- };
1721
- function factory(name, context) {
1722
- const fn = table[name];
1723
- if (fn) {
1724
- return fn.bind(context);
1686
+ visitDepthFirst(callback) {
1687
+ this.root.visitDepthFirst(callback);
1725
1688
  }
1726
- else {
1727
- throw new Error(`Pseudo-class "${name}" is not implemented`);
1689
+ find(callback) {
1690
+ return this.root.find(callback);
1691
+ }
1692
+ querySelector(selector) {
1693
+ return this.root.querySelector(selector);
1694
+ }
1695
+ querySelectorAll(selector) {
1696
+ return this.root.querySelectorAll(selector);
1728
1697
  }
1729
1698
  }
1730
1699
 
1700
+ const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
1701
+ const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
1702
+ const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
1731
1703
  /**
1732
- * Homage to PHP: unescapes slashes.
1704
+ * Tests if this element is present in the accessibility tree.
1733
1705
  *
1734
- * E.g. "foo\:bar" becomes "foo:bar"
1735
- */
1736
- function stripslashes(value) {
1737
- return value.replace(/\\(.)/g, "$1");
1738
- }
1739
- /**
1740
- * @internal
1706
+ * In practice it tests whenever the element or its parents has
1707
+ * `role="presentation"` or `aria-hidden="false"`. Dynamic values counts as
1708
+ * visible since the element might be in the visibility tree sometimes.
1741
1709
  */
1742
- function escapeSelectorComponent(text) {
1743
- return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
1710
+ function inAccessibilityTree(node) {
1711
+ return !isAriaHidden(node) && !isPresentation(node);
1744
1712
  }
1745
- /**
1746
- * @internal
1747
- */
1748
- function generateIdSelector(id) {
1749
- const escaped = escapeSelectorComponent(id);
1750
- return escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
1713
+ function isAriaHiddenImpl(node) {
1714
+ const isHidden = (node) => {
1715
+ const ariaHidden = node.getAttribute("aria-hidden");
1716
+ return Boolean(ariaHidden && ariaHidden.value === "true");
1717
+ };
1718
+ return {
1719
+ byParent: node.parent ? isAriaHidden(node.parent) : false,
1720
+ bySelf: isHidden(node),
1721
+ };
1751
1722
  }
1752
- /**
1753
- * Returns true if the character is a delimiter for different kinds of selectors:
1754
- *
1755
- * - `.` - begins a class selector
1756
- * - `#` - begins an id selector
1757
- * - `[` - begins an attribute selector
1758
- * - `:` - begins a pseudo class or element selector
1759
- */
1760
- function isDelimiter(ch) {
1761
- return /[.#[:]/.test(ch);
1723
+ function isAriaHidden(node, details) {
1724
+ const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
1725
+ if (cached) {
1726
+ return details ? cached : cached.byParent || cached.bySelf;
1727
+ }
1728
+ const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
1729
+ return details ? result : result.byParent || result.bySelf;
1762
1730
  }
1763
- /**
1764
- * Returns true if the character is a quotation mark.
1765
- */
1766
- function isQuotationMark(ch) {
1767
- return /['"]/.test(ch);
1731
+ function isHTMLHiddenImpl(node) {
1732
+ const isHidden = (node) => {
1733
+ const hidden = node.getAttribute("hidden");
1734
+ return hidden !== null && hidden.isStatic;
1735
+ };
1736
+ return {
1737
+ byParent: node.parent ? isHTMLHidden(node.parent) : false,
1738
+ bySelf: isHidden(node),
1739
+ };
1768
1740
  }
1769
- function isPseudoElement(ch, buffer) {
1770
- return ch === ":" && buffer === ":";
1741
+ function isHTMLHidden(node, details) {
1742
+ const cached = node.cacheGet(HTML_HIDDEN_CACHE);
1743
+ if (cached) {
1744
+ return details ? cached : cached.byParent || cached.bySelf;
1745
+ }
1746
+ const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
1747
+ return details ? result : result.byParent || result.bySelf;
1771
1748
  }
1772
1749
  /**
1773
- * @internal
1750
+ * Tests if this element or a parent element has role="presentation".
1751
+ *
1752
+ * Dynamic values yields `false` just as if the attribute wasn't present.
1774
1753
  */
1775
- function* splitPattern(pattern) {
1776
- if (pattern === "") {
1777
- return;
1754
+ function isPresentation(node) {
1755
+ if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
1756
+ return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
1778
1757
  }
1779
- const end = pattern.length;
1780
- let begin = 0;
1781
- let cur = 1;
1782
- let quoted = false;
1783
- while (cur < end) {
1784
- const ch = pattern[cur];
1785
- const buffer = pattern.slice(begin, cur);
1786
- /* escaped character, ignore whatever is next */
1787
- if (ch === "\\") {
1788
- cur += 2;
1789
- continue;
1790
- }
1791
- /* if inside quoted string we only look for the end quotation mark */
1792
- if (quoted) {
1793
- if (ch === quoted) {
1794
- quoted = false;
1795
- }
1796
- cur += 1;
1797
- continue;
1798
- }
1799
- /* if the character is a quotation mark we store the character and the above
1800
- * condition will look for a similar end quotation mark */
1801
- if (isQuotationMark(ch)) {
1802
- quoted = ch;
1803
- cur += 1;
1804
- continue;
1805
- }
1806
- /* special case when using :: pseudo element selector */
1807
- if (isPseudoElement(ch, buffer)) {
1808
- cur += 1;
1809
- continue;
1758
+ let cur = node;
1759
+ do {
1760
+ const role = cur.getAttribute("role");
1761
+ /* role="presentation" */
1762
+ if (role && role.value === "presentation") {
1763
+ return cur.cacheSet(ROLE_PRESENTATION_CACHE, true);
1810
1764
  }
1811
- /* if the character is a delimiter we yield the string and reset the
1812
- * position */
1813
- if (isDelimiter(ch)) {
1814
- begin = cur;
1815
- yield buffer;
1765
+ /* sanity check: break if no parent is present, normally not an issue as the
1766
+ * root element should be found first */
1767
+ if (!cur.parent) {
1768
+ break;
1816
1769
  }
1817
- cur += 1;
1818
- }
1819
- /* yield the rest of the string */
1820
- const tail = pattern.slice(begin, cur);
1821
- yield tail;
1822
- }
1823
- class Matcher {
1824
- }
1825
- class ClassMatcher extends Matcher {
1826
- constructor(classname) {
1827
- super();
1828
- this.classname = classname;
1829
- }
1830
- match(node) {
1831
- return node.classList.contains(this.classname);
1832
- }
1833
- }
1834
- class IdMatcher extends Matcher {
1835
- constructor(id) {
1836
- super();
1837
- this.id = stripslashes(id);
1838
- }
1839
- match(node) {
1840
- return node.id === this.id;
1841
- }
1842
- }
1843
- class AttrMatcher extends Matcher {
1844
- constructor(attr) {
1845
- super();
1846
- const [, key, op, value] = attr.match(/^(.+?)(?:([~^$*|]?=)"([^"]+?)")?$/);
1847
- this.key = key;
1848
- this.op = op;
1849
- this.value = value;
1850
- }
1851
- match(node) {
1852
- const attr = node.getAttribute(this.key, true) || [];
1853
- return attr.some((cur) => {
1854
- switch (this.op) {
1855
- case undefined:
1856
- return true; /* attribute exists */
1857
- case "=":
1858
- return cur.value === this.value;
1859
- default:
1860
- throw new Error(`Attribute selector operator ${this.op} is not implemented yet`);
1861
- }
1862
- });
1863
- }
1770
+ /* check parents */
1771
+ cur = cur.parent;
1772
+ } while (!cur.isRootElement());
1773
+ return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
1864
1774
  }
1865
- class PseudoClassMatcher extends Matcher {
1866
- constructor(pseudoclass, context) {
1867
- super();
1868
- const match = pseudoclass.match(/^([^(]+)(?:\((.*)\))?$/);
1869
- if (!match) {
1870
- throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
1871
- }
1872
- const [, name, args] = match;
1873
- this.name = name;
1874
- this.args = args;
1875
- }
1876
- match(node, context) {
1877
- const fn = factory(this.name, context);
1878
- return fn(node, this.args);
1775
+
1776
+ const cachePrefix = classifyNodeText.name;
1777
+ const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
1778
+ const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
1779
+ const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
1780
+ const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
1781
+ /**
1782
+ * @public
1783
+ */
1784
+ var TextClassification;
1785
+ (function (TextClassification) {
1786
+ TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
1787
+ TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
1788
+ TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
1789
+ })(TextClassification || (TextClassification = {}));
1790
+ function getCachekey(options = {}) {
1791
+ const { accessible = false, ignoreHiddenRoot = false } = options;
1792
+ if (accessible && ignoreHiddenRoot) {
1793
+ return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
1879
1794
  }
1880
- }
1881
- class Pattern {
1882
- constructor(pattern) {
1883
- const match = pattern.match(/^([~+\->]?)((?:[*]|[^.#[:]+)?)(.*)$/);
1884
- match.shift(); /* remove full matched string */
1885
- this.selector = pattern;
1886
- this.combinator = parseCombinator(match.shift(), pattern);
1887
- this.tagName = match.shift() || "*";
1888
- this.pattern = Array.from(splitPattern(match[0]), (it) => this.createMatcher(it));
1795
+ else if (ignoreHiddenRoot) {
1796
+ return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
1889
1797
  }
1890
- match(node, context) {
1891
- return node.is(this.tagName) && this.pattern.every((cur) => cur.match(node, context));
1798
+ else if (accessible) {
1799
+ return A11Y_CACHE_KEY;
1892
1800
  }
1893
- createMatcher(pattern) {
1894
- switch (pattern[0]) {
1895
- case ".":
1896
- return new ClassMatcher(pattern.slice(1));
1897
- case "#":
1898
- return new IdMatcher(pattern.slice(1));
1899
- case "[":
1900
- return new AttrMatcher(pattern.slice(1, -1));
1901
- case ":":
1902
- return new PseudoClassMatcher(pattern.slice(1), this.selector);
1903
- default:
1904
- /* istanbul ignore next: fallback solution, the switch cases should cover
1905
- * everything and there is no known way to trigger this fallback */
1906
- throw new Error(`Failed to create matcher for "${pattern}"`);
1907
- }
1801
+ else {
1802
+ return HTML_CACHE_KEY;
1908
1803
  }
1909
1804
  }
1805
+ /* While I cannot find a reference about this in the standard the <select>
1806
+ * element kinda acts as if there is no text content, most particularly it
1807
+ * doesn't receive and accessible name. The `.textContent` property does
1808
+ * however include the <option> childrens text. But for the sake of the
1809
+ * validator it is probably best if the classification acts as if there is no
1810
+ * text as I think that is what is expected of the return values. Might have
1811
+ * to revisit this at some point or if someone could clarify what section of
1812
+ * the standard deals with this. */
1813
+ function isSpecialEmpty(node) {
1814
+ return node.is("select") || node.is("textarea");
1815
+ }
1910
1816
  /**
1911
- * DOM Selector.
1817
+ * Checks text content of an element.
1818
+ *
1819
+ * Any text is considered including text from descendant elements. Whitespace is
1820
+ * ignored.
1821
+ *
1822
+ * If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
1823
+ *
1824
+ * @public
1912
1825
  */
1913
- class Selector {
1914
- constructor(selector) {
1915
- this.pattern = Selector.parse(selector);
1826
+ function classifyNodeText(node, options = {}) {
1827
+ const { accessible = false, ignoreHiddenRoot = false } = options;
1828
+ const cacheKey = getCachekey(options);
1829
+ if (node.cacheExists(cacheKey)) {
1830
+ return node.cacheGet(cacheKey);
1916
1831
  }
1917
- /**
1918
- * Match this selector against a HtmlElement.
1919
- *
1920
- * @param root - Element to match against.
1921
- * @returns Iterator with matched elements.
1922
- */
1923
- *match(root) {
1924
- const context = { scope: root };
1925
- yield* this.matchInternal(root, 0, context);
1832
+ if (!ignoreHiddenRoot && isHTMLHidden(node)) {
1833
+ return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
1926
1834
  }
1927
- *matchInternal(root, level, context) {
1928
- if (level >= this.pattern.length) {
1929
- yield root;
1930
- return;
1931
- }
1932
- const pattern = this.pattern[level];
1933
- const matches = Selector.findCandidates(root, pattern);
1934
- for (const node of matches) {
1935
- if (!pattern.match(node, context)) {
1936
- continue;
1937
- }
1938
- yield* this.matchInternal(node, level + 1, context);
1939
- }
1835
+ if (!ignoreHiddenRoot && accessible && isAriaHidden(node)) {
1836
+ return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
1940
1837
  }
1941
- static parse(selector) {
1942
- /* strip whitespace before combinators, "ul > li" becomes "ul >li", for
1943
- * easier parsing */
1944
- selector = selector.replace(/([+~>]) /g, "$1");
1945
- const pattern = selector.split(/(?:(?<!\\) )+/);
1946
- return pattern.map((part) => new Pattern(part));
1838
+ if (isSpecialEmpty(node)) {
1839
+ return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
1947
1840
  }
1948
- static findCandidates(root, pattern) {
1949
- switch (pattern.combinator) {
1950
- case Combinator.DESCENDANT:
1951
- return root.getElementsByTagName(pattern.tagName);
1952
- case Combinator.CHILD:
1953
- return root.childElements.filter((node) => node.is(pattern.tagName));
1954
- case Combinator.ADJACENT_SIBLING:
1955
- return Selector.findAdjacentSibling(root);
1956
- case Combinator.GENERAL_SIBLING:
1957
- return Selector.findGeneralSibling(root);
1958
- case Combinator.SCOPE:
1959
- return [root];
1960
- }
1961
- /* istanbul ignore next: fallback solution, the switch cases should cover
1962
- * everything and there is no known way to trigger this fallback */
1963
- return [];
1841
+ const text = findTextNodes(node, {
1842
+ ...options,
1843
+ ignoreHiddenRoot: false,
1844
+ });
1845
+ /* if any text is dynamic classify as dynamic */
1846
+ if (text.some((cur) => cur.isDynamic)) {
1847
+ return node.cacheSet(cacheKey, TextClassification.DYNAMIC_TEXT);
1964
1848
  }
1965
- static findAdjacentSibling(node) {
1966
- let adjacent = false;
1967
- return node.siblings.filter((cur) => {
1968
- if (adjacent) {
1969
- adjacent = false;
1970
- return true;
1971
- }
1972
- if (cur === node) {
1973
- adjacent = true;
1974
- }
1975
- return false;
1976
- });
1849
+ /* if any text has non-whitespace character classify as static */
1850
+ if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
1851
+ return node.cacheSet(cacheKey, TextClassification.STATIC_TEXT);
1977
1852
  }
1978
- static findGeneralSibling(node) {
1979
- let after = false;
1980
- return node.siblings.filter((cur) => {
1981
- if (after) {
1982
- return true;
1853
+ /* default to empty */
1854
+ return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
1855
+ }
1856
+ function findTextNodes(node, options) {
1857
+ const { accessible = false } = options;
1858
+ let text = [];
1859
+ for (const child of node.childNodes) {
1860
+ if (isTextNode(child)) {
1861
+ text.push(child);
1862
+ }
1863
+ else if (isElementNode(child)) {
1864
+ if (isHTMLHidden(child, true).bySelf) {
1865
+ continue;
1983
1866
  }
1984
- if (cur === node) {
1985
- after = true;
1867
+ if (accessible && isAriaHidden(child, true).bySelf) {
1868
+ continue;
1986
1869
  }
1987
- return false;
1988
- });
1870
+ text = text.concat(findTextNodes(child, options));
1871
+ }
1989
1872
  }
1873
+ return text;
1874
+ }
1875
+
1876
+ function hasAltText(image) {
1877
+ const alt = image.getAttribute("alt");
1878
+ /* missing or boolean */
1879
+ if (alt === null || alt.value === null) {
1880
+ return false;
1881
+ }
1882
+ return alt.isDynamic || alt.value.toString() !== "";
1883
+ }
1884
+
1885
+ function hasAriaLabel(node) {
1886
+ const label = node.getAttribute("aria-label");
1887
+ /* missing or boolean */
1888
+ if (label === null || label.value === null) {
1889
+ return false;
1890
+ }
1891
+ return label.isDynamic || label.value.toString() !== "";
1990
1892
  }
1991
1893
 
1992
- const TEXT_NODE_NAME = "#text";
1993
1894
  /**
1994
- * Returns true if the node is a text node.
1895
+ * Joins a list of words into natural language.
1995
1896
  *
1996
- * @public
1897
+ * - `["foo"]` becomes `"foo"`
1898
+ * - `["foo", "bar"]` becomes `"foo or bar"`
1899
+ * - `["foo", "bar", "baz"]` becomes `"foo, bar or baz"`
1900
+ * - and so on...
1901
+ *
1902
+ * @internal
1903
+ * @param values - List of words to join
1904
+ * @param conjunction - Conjunction for the last element.
1905
+ * @returns String with the words naturally joined with a conjunction.
1997
1906
  */
1998
- function isTextNode(node) {
1999
- return Boolean(node && node.nodeType === NodeType.TEXT_NODE);
1907
+ function naturalJoin(values, conjunction = "or") {
1908
+ switch (values.length) {
1909
+ case 0:
1910
+ return "";
1911
+ case 1:
1912
+ return values[0];
1913
+ case 2:
1914
+ return `${values[0]} ${conjunction} ${values[1]}`;
1915
+ default:
1916
+ return `${values.slice(0, -1).join(", ")} ${conjunction} ${values.slice(-1)[0]}`;
1917
+ }
2000
1918
  }
1919
+
2001
1920
  /**
2002
- * Represents a text in the HTML document.
2003
- *
2004
- * Text nodes are appended as children of `HtmlElement` and cannot have childen
2005
- * of its own.
1921
+ * @internal
1922
+ */
1923
+ function allowedIfAttributeIsPresent(...attr) {
1924
+ return (node) => {
1925
+ if (attr.some((it) => node.hasAttribute(it))) {
1926
+ return null;
1927
+ }
1928
+ const expected = naturalJoin(attr.map((it) => `"${it}"`));
1929
+ return `requires ${expected} attribute to be present`;
1930
+ };
1931
+ }
1932
+ /**
1933
+ * @internal
1934
+ */
1935
+ function allowedIfAttributeIsAbsent(...attr) {
1936
+ return (node) => {
1937
+ const present = attr.filter((it) => node.hasAttribute(it));
1938
+ if (present.length === 0) {
1939
+ return null;
1940
+ }
1941
+ const expected = naturalJoin(present.map((it) => `"${it}"`));
1942
+ return `cannot be used at the same time as ${expected}`;
1943
+ };
1944
+ }
1945
+ /**
1946
+ * @internal
1947
+ */
1948
+ function allowedIfAttributeHasValue(key, expectedValue, { defaultValue } = {}) {
1949
+ return (node) => {
1950
+ const attr = node.getAttribute(key);
1951
+ if (attr === null || attr === void 0 ? void 0 : attr.isDynamic) {
1952
+ return null;
1953
+ }
1954
+ const actualValue = (attr === null || attr === void 0 ? void 0 : attr.value) ? attr.value.toString() : defaultValue;
1955
+ if (actualValue && expectedValue.includes(actualValue.toLocaleLowerCase())) {
1956
+ return null;
1957
+ }
1958
+ const expected = naturalJoin(expectedValue.map((it) => `"${it}"`));
1959
+ return `"${key}" attribute must be ${expected}`;
1960
+ };
1961
+ }
1962
+ const metadataHelper = {
1963
+ allowedIfAttributeIsPresent,
1964
+ allowedIfAttributeIsAbsent,
1965
+ allowedIfAttributeHasValue,
1966
+ };
1967
+
1968
+ /**
1969
+ * Computes hash for given string.
2006
1970
  *
2007
- * @public
1971
+ * @internal
2008
1972
  */
2009
- class TextNode extends DOMNode {
2010
- /**
2011
- * @param text - Text to add. When a `DynamicValue` is used the expression is
2012
- * used as "text".
2013
- * @param location - Source code location of this node.
2014
- */
2015
- constructor(text, location) {
2016
- super(NodeType.TEXT_NODE, TEXT_NODE_NAME, location);
2017
- this.text = text;
2018
- }
2019
- /**
2020
- * Get the text from node.
2021
- */
2022
- get textContent() {
2023
- return this.text.toString();
2024
- }
2025
- /**
2026
- * Flag set to true if the attribute value is static.
2027
- */
2028
- get isStatic() {
2029
- return !this.isDynamic;
1973
+ function cyrb53(str) {
1974
+ const a = 2654435761;
1975
+ const b = 1597334677;
1976
+ const c = 2246822507;
1977
+ const d = 3266489909;
1978
+ const e = 4294967296;
1979
+ const f = 2097151;
1980
+ const seed = 0;
1981
+ let h1 = 0xdeadbeef ^ seed;
1982
+ let h2 = 0x41c6ce57 ^ seed;
1983
+ for (let i = 0, ch; i < str.length; i++) {
1984
+ ch = str.charCodeAt(i);
1985
+ h1 = Math.imul(h1 ^ ch, a);
1986
+ h2 = Math.imul(h2 ^ ch, b);
2030
1987
  }
2031
- /**
2032
- * Flag set to true if the attribute value is dynamic.
2033
- */
2034
- get isDynamic() {
2035
- return this.text instanceof DynamicValue;
1988
+ h1 = Math.imul(h1 ^ (h1 >>> 16), c) ^ Math.imul(h2 ^ (h2 >>> 13), d);
1989
+ h2 = Math.imul(h2 ^ (h2 >>> 16), c) ^ Math.imul(h1 ^ (h1 >>> 13), d);
1990
+ return e * (f & h2) + (h1 >>> 0);
1991
+ }
1992
+ const computeHash = cyrb53;
1993
+
1994
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../");
1995
+ const legacyRequire = createRequire(import.meta.url);
1996
+ const distFolder = path.resolve(projectRoot, "dist/es");
1997
+
1998
+ /**
1999
+ * Similar to `require(..)` but removes the cached copy first.
2000
+ */
2001
+ function requireUncached(moduleId) {
2002
+ const filename = legacyRequire.resolve(moduleId);
2003
+ /* remove references from the parent module to prevent memory leak */
2004
+ const m = legacyRequire.cache[filename];
2005
+ if (m && m.parent) {
2006
+ const { parent } = m;
2007
+ for (let i = parent.children.length - 1; i >= 0; i--) {
2008
+ if (parent.children[i].id === filename) {
2009
+ parent.children.splice(i, 1);
2010
+ }
2011
+ }
2036
2012
  }
2013
+ /* remove old module from cache */
2014
+ delete legacyRequire.cache[filename];
2015
+ return legacyRequire(filename);
2037
2016
  }
2038
2017
 
2018
+ const $schema$1 = "http://json-schema.org/draft-06/schema#";
2019
+ const $id$1 = "https://html-validate.org/schemas/elements.json";
2020
+ const type$1 = "object";
2021
+ const properties$1 = {
2022
+ $schema: {
2023
+ type: "string"
2024
+ }
2025
+ };
2026
+ const patternProperties = {
2027
+ "^[^$].*$": {
2028
+ type: "object",
2029
+ properties: {
2030
+ inherit: {
2031
+ title: "Inherit from another element",
2032
+ description: "Most properties from the parent element will be copied onto this one",
2033
+ type: "string"
2034
+ },
2035
+ embedded: {
2036
+ title: "Mark this element as belonging in the embedded content category",
2037
+ $ref: "#/definitions/contentCategory"
2038
+ },
2039
+ flow: {
2040
+ title: "Mark this element as belonging in the flow content category",
2041
+ $ref: "#/definitions/contentCategory"
2042
+ },
2043
+ heading: {
2044
+ title: "Mark this element as belonging in the heading content category",
2045
+ $ref: "#/definitions/contentCategory"
2046
+ },
2047
+ interactive: {
2048
+ title: "Mark this element as belonging in the interactive content category",
2049
+ $ref: "#/definitions/contentCategory"
2050
+ },
2051
+ metadata: {
2052
+ title: "Mark this element as belonging in the metadata content category",
2053
+ $ref: "#/definitions/contentCategory"
2054
+ },
2055
+ phrasing: {
2056
+ title: "Mark this element as belonging in the phrasing content category",
2057
+ $ref: "#/definitions/contentCategory"
2058
+ },
2059
+ sectioning: {
2060
+ title: "Mark this element as belonging in the sectioning content category",
2061
+ $ref: "#/definitions/contentCategory"
2062
+ },
2063
+ deprecated: {
2064
+ title: "Mark element as deprecated",
2065
+ description: "Deprecated elements should not be used. If a message is provided it will be included in the error",
2066
+ anyOf: [
2067
+ {
2068
+ type: "boolean"
2069
+ },
2070
+ {
2071
+ type: "string"
2072
+ },
2073
+ {
2074
+ $ref: "#/definitions/deprecatedElement"
2075
+ }
2076
+ ]
2077
+ },
2078
+ foreign: {
2079
+ title: "Mark element as foreign",
2080
+ description: "Foreign elements are elements which have a start and end tag but is otherwize not parsed",
2081
+ type: "boolean"
2082
+ },
2083
+ "void": {
2084
+ title: "Mark element as void",
2085
+ description: "Void elements are elements which cannot have content and thus must not use an end tag",
2086
+ type: "boolean"
2087
+ },
2088
+ transparent: {
2089
+ title: "Mark element as transparent",
2090
+ description: "Transparent elements follows the same content model as its parent, i.e. the content must be allowed in the parent.",
2091
+ anyOf: [
2092
+ {
2093
+ type: "boolean"
2094
+ },
2095
+ {
2096
+ type: "array",
2097
+ items: {
2098
+ type: "string"
2099
+ }
2100
+ }
2101
+ ]
2102
+ },
2103
+ implicitClosed: {
2104
+ title: "List of elements which implicitly closes this element",
2105
+ description: "Some elements are automatically closed when another start tag occurs",
2106
+ type: "array",
2107
+ items: {
2108
+ type: "string"
2109
+ }
2110
+ },
2111
+ scriptSupporting: {
2112
+ title: "Mark element as script-supporting",
2113
+ description: "Script-supporting elements are elements which can be inserted where othersise not permitted to assist in templating",
2114
+ type: "boolean"
2115
+ },
2116
+ form: {
2117
+ title: "Mark element as a submittable form element",
2118
+ type: "boolean"
2119
+ },
2120
+ labelable: {
2121
+ title: "Mark this element as labelable",
2122
+ description: "This element may contain an associated label element.",
2123
+ anyOf: [
2124
+ {
2125
+ type: "boolean"
2126
+ },
2127
+ {
2128
+ $ref: "#/definitions/expression"
2129
+ }
2130
+ ]
2131
+ },
2132
+ deprecatedAttributes: {
2133
+ title: "List of deprecated attributes",
2134
+ type: "array",
2135
+ items: {
2136
+ type: "string"
2137
+ }
2138
+ },
2139
+ requiredAttributes: {
2140
+ title: "List of required attributes",
2141
+ type: "array",
2142
+ items: {
2143
+ type: "string"
2144
+ }
2145
+ },
2146
+ attributes: {
2147
+ title: "List of known attributes and allowed values",
2148
+ $ref: "#/definitions/PermittedAttribute"
2149
+ },
2150
+ permittedContent: {
2151
+ title: "List of elements or categories allowed as content in this element",
2152
+ $ref: "#/definitions/Permitted"
2153
+ },
2154
+ permittedDescendants: {
2155
+ title: "List of elements or categories allowed as descendants in this element",
2156
+ $ref: "#/definitions/Permitted"
2157
+ },
2158
+ permittedOrder: {
2159
+ title: "Required order of child elements",
2160
+ $ref: "#/definitions/PermittedOrder"
2161
+ },
2162
+ permittedParent: {
2163
+ title: "List of elements or categories allowed as parent to this element",
2164
+ $ref: "#/definitions/Permitted"
2165
+ },
2166
+ requiredAncestors: {
2167
+ title: "List of required ancestor elements",
2168
+ $ref: "#/definitions/RequiredAncestors"
2169
+ },
2170
+ requiredContent: {
2171
+ title: "List of required content elements",
2172
+ $ref: "#/definitions/RequiredContent"
2173
+ },
2174
+ textContent: {
2175
+ title: "Allow, disallow or require textual content",
2176
+ description: "This property controls whenever an element allows, disallows or requires text. Text from any descendant counts, not only direct children",
2177
+ "default": "default",
2178
+ type: "string",
2179
+ "enum": [
2180
+ "none",
2181
+ "default",
2182
+ "required",
2183
+ "accessible"
2184
+ ]
2185
+ }
2186
+ },
2187
+ additionalProperties: false
2188
+ }
2189
+ };
2190
+ const definitions = {
2191
+ contentCategory: {
2192
+ anyOf: [
2193
+ {
2194
+ type: "boolean"
2195
+ },
2196
+ {
2197
+ $ref: "#/definitions/expression"
2198
+ }
2199
+ ]
2200
+ },
2201
+ expression: {
2202
+ type: "array",
2203
+ minItems: 2,
2204
+ maxItems: 2,
2205
+ items: [
2206
+ {
2207
+ type: "string",
2208
+ "enum": [
2209
+ "isDescendant",
2210
+ "hasAttribute",
2211
+ "matchAttribute"
2212
+ ]
2213
+ },
2214
+ {
2215
+ anyOf: [
2216
+ {
2217
+ type: "string"
2218
+ },
2219
+ {
2220
+ $ref: "#/definitions/operation"
2221
+ }
2222
+ ]
2223
+ }
2224
+ ]
2225
+ },
2226
+ operation: {
2227
+ type: "array",
2228
+ minItems: 3,
2229
+ maxItems: 3,
2230
+ items: [
2231
+ {
2232
+ type: "string"
2233
+ },
2234
+ {
2235
+ type: "string",
2236
+ "enum": [
2237
+ "!=",
2238
+ "="
2239
+ ]
2240
+ },
2241
+ {
2242
+ type: "string"
2243
+ }
2244
+ ]
2245
+ },
2246
+ deprecatedElement: {
2247
+ type: "object",
2248
+ additionalProperties: false,
2249
+ properties: {
2250
+ message: {
2251
+ type: "string",
2252
+ title: "A short text message shown next to the regular error message."
2253
+ },
2254
+ documentation: {
2255
+ type: "string",
2256
+ title: "An extended markdown formatted message shown with the contextual rule documentation."
2257
+ },
2258
+ source: {
2259
+ type: "string",
2260
+ title: "Element source, e.g. what standard or library deprecated this element.",
2261
+ "default": "html5"
2262
+ }
2263
+ }
2264
+ },
2265
+ Permitted: {
2266
+ type: "array",
2267
+ items: {
2268
+ anyOf: [
2269
+ {
2270
+ type: "string"
2271
+ },
2272
+ {
2273
+ type: "array",
2274
+ items: {
2275
+ anyOf: [
2276
+ {
2277
+ type: "string"
2278
+ },
2279
+ {
2280
+ $ref: "#/definitions/PermittedGroup"
2281
+ }
2282
+ ]
2283
+ }
2284
+ },
2285
+ {
2286
+ $ref: "#/definitions/PermittedGroup"
2287
+ }
2288
+ ]
2289
+ }
2290
+ },
2291
+ PermittedAttribute: {
2292
+ type: "object",
2293
+ patternProperties: {
2294
+ "^.*$": {
2295
+ anyOf: [
2296
+ {
2297
+ type: "object",
2298
+ additionalProperties: false,
2299
+ properties: {
2300
+ allowed: {
2301
+ "function": true,
2302
+ title: "Set to a function to evaluate if this attribute is allowed in this context"
2303
+ },
2304
+ boolean: {
2305
+ type: "boolean",
2306
+ title: "Set to true if this is a boolean attribute"
2307
+ },
2308
+ deprecated: {
2309
+ title: "Set to true or string if this attribute is deprecated",
2310
+ oneOf: [
2311
+ {
2312
+ type: "boolean"
2313
+ },
2314
+ {
2315
+ type: "string"
2316
+ }
2317
+ ]
2318
+ },
2319
+ list: {
2320
+ type: "boolean",
2321
+ title: "Set to true if this attribute is a list of space-separated tokens, each which must be valid by itself"
2322
+ },
2323
+ "enum": {
2324
+ type: "array",
2325
+ title: "Exhaustive list of values (string or regex) this attribute accepts",
2326
+ uniqueItems: true,
2327
+ items: {
2328
+ anyOf: [
2329
+ {
2330
+ type: "string"
2331
+ },
2332
+ {
2333
+ regexp: true
2334
+ }
2335
+ ]
2336
+ }
2337
+ },
2338
+ omit: {
2339
+ type: "boolean",
2340
+ title: "Set to true if this attribute can optionally omit its value"
2341
+ },
2342
+ required: {
2343
+ type: "boolean",
2344
+ title: "Set to true if this attribute is required"
2345
+ }
2346
+ }
2347
+ },
2348
+ {
2349
+ type: "array",
2350
+ uniqueItems: true,
2351
+ items: {
2352
+ type: "string"
2353
+ }
2354
+ }
2355
+ ]
2356
+ }
2357
+ }
2358
+ },
2359
+ PermittedGroup: {
2360
+ type: "object",
2361
+ additionalProperties: false,
2362
+ properties: {
2363
+ exclude: {
2364
+ anyOf: [
2365
+ {
2366
+ items: {
2367
+ type: "string"
2368
+ },
2369
+ type: "array"
2370
+ },
2371
+ {
2372
+ type: "string"
2373
+ }
2374
+ ]
2375
+ }
2376
+ }
2377
+ },
2378
+ PermittedOrder: {
2379
+ type: "array",
2380
+ items: {
2381
+ type: "string"
2382
+ }
2383
+ },
2384
+ RequiredAncestors: {
2385
+ type: "array",
2386
+ items: {
2387
+ type: "string"
2388
+ }
2389
+ },
2390
+ RequiredContent: {
2391
+ type: "array",
2392
+ items: {
2393
+ type: "string"
2394
+ }
2395
+ }
2396
+ };
2397
+ var schema = {
2398
+ $schema: $schema$1,
2399
+ $id: $id$1,
2400
+ type: type$1,
2401
+ properties: properties$1,
2402
+ patternProperties: patternProperties,
2403
+ definitions: definitions
2404
+ };
2405
+
2039
2406
  /**
2040
- * @public
2041
- */
2042
- var NodeClosed;
2043
- (function (NodeClosed) {
2044
- NodeClosed[NodeClosed["Open"] = 0] = "Open";
2045
- NodeClosed[NodeClosed["EndTag"] = 1] = "EndTag";
2046
- NodeClosed[NodeClosed["VoidOmitted"] = 2] = "VoidOmitted";
2047
- NodeClosed[NodeClosed["VoidSelfClosed"] = 3] = "VoidSelfClosed";
2048
- NodeClosed[NodeClosed["ImplicitClosed"] = 4] = "ImplicitClosed";
2049
- })(NodeClosed || (NodeClosed = {}));
2050
- /**
2051
- * Returns true if the node is an element node.
2052
- *
2053
- * @public
2054
- */
2055
- function isElementNode(node) {
2056
- return Boolean(node && node.nodeType === NodeType.ELEMENT_NODE);
2057
- }
2058
- function isValidTagName(tagName) {
2059
- return Boolean(tagName !== "" && tagName !== "*");
2060
- }
2061
- /**
2062
- * @public
2407
+ * AJV keyword "regexp" to validate the type to be a regular expression.
2408
+ * Injects errors with the "type" keyword to give the same output.
2063
2409
  */
2064
- class HtmlElement extends DOMNode {
2065
- constructor(tagName, parent, closed, meta, location) {
2066
- const nodeType = tagName ? NodeType.ELEMENT_NODE : NodeType.DOCUMENT_NODE;
2067
- super(nodeType, tagName, location);
2068
- if (!isValidTagName(tagName)) {
2069
- throw new Error(`The tag name provided ('${tagName || ""}') is not a valid name`);
2070
- }
2071
- this.tagName = tagName || "#document";
2072
- this.parent = parent !== null && parent !== void 0 ? parent : null;
2073
- this.attr = {};
2074
- this.metaElement = meta !== null && meta !== void 0 ? meta : null;
2075
- this.closed = closed;
2076
- this.voidElement = meta ? Boolean(meta.void) : false;
2077
- this.depth = 0;
2078
- this.annotation = null;
2079
- if (parent) {
2080
- parent.childNodes.push(this);
2081
- /* calculate depth in domtree */
2082
- let cur = parent;
2083
- while (cur.parent) {
2084
- this.depth++;
2085
- cur = cur.parent;
2086
- }
2087
- }
2088
- }
2089
- /**
2090
- * @internal
2091
- */
2092
- static rootNode(location) {
2093
- const root = new HtmlElement(undefined, null, NodeClosed.EndTag, null, location);
2094
- root.setAnnotation("#document");
2095
- return root;
2096
- }
2097
- /**
2098
- * @internal
2099
- *
2100
- * @param namespace - If given it is appended to the tagName.
2101
- */
2102
- static fromTokens(startToken, endToken, parent, metaTable, namespace = "") {
2103
- const name = startToken.data[2];
2104
- const tagName = namespace ? `${namespace}:${name}` : name;
2105
- if (!name) {
2106
- throw new Error("tagName cannot be empty");
2107
- }
2108
- const meta = metaTable ? metaTable.getMetaFor(tagName) : null;
2109
- const open = startToken.data[1] !== "/";
2110
- const closed = isClosed(endToken, meta);
2111
- /* location contains position of '<' so strip it out */
2112
- const location = sliceLocation(startToken.location, 1);
2113
- return new HtmlElement(tagName, open ? parent : null, closed, meta, location);
2114
- }
2115
- /**
2116
- * Returns annotated name if set or defaults to `<tagName>`.
2117
- *
2118
- * E.g. `my-annotation` or `<div>`.
2119
- */
2120
- get annotatedName() {
2121
- if (this.annotation) {
2122
- return this.annotation;
2123
- }
2124
- else {
2125
- return `<${this.tagName}>`;
2126
- }
2410
+ /* istanbul ignore next: manual testing */
2411
+ const ajvRegexpValidate = function (data, dataCxt) {
2412
+ const valid = data instanceof RegExp;
2413
+ if (!valid) {
2414
+ ajvRegexpValidate.errors = [
2415
+ {
2416
+ instancePath: dataCxt === null || dataCxt === void 0 ? void 0 : dataCxt.instancePath,
2417
+ schemaPath: undefined,
2418
+ keyword: "type",
2419
+ message: "should be a regular expression",
2420
+ params: {
2421
+ keyword: "type",
2422
+ },
2423
+ },
2424
+ ];
2127
2425
  }
2128
- /**
2129
- * Get list of IDs referenced by `aria-labelledby`.
2130
- *
2131
- * If the attribute is unset or empty this getter returns null.
2132
- * If the attribute is dynamic the original {@link DynamicValue} is returned.
2133
- *
2134
- * @public
2135
- */
2136
- get ariaLabelledby() {
2137
- const attr = this.getAttribute("aria-labelledby");
2138
- if (!attr || !attr.value) {
2139
- return null;
2140
- }
2141
- if (attr.value instanceof DynamicValue) {
2142
- return attr.value;
2143
- }
2144
- const list = new DOMTokenList(attr.value, attr.valueLocation);
2145
- return list.length ? Array.from(list) : null;
2426
+ return valid;
2427
+ };
2428
+ const ajvRegexpKeyword = {
2429
+ keyword: "regexp",
2430
+ schema: false,
2431
+ errors: true,
2432
+ validate: ajvRegexpValidate,
2433
+ };
2434
+
2435
+ /**
2436
+ * AJV keyword "function" to validate the type to be a function. Injects errors
2437
+ * with the "type" keyword to give the same output.
2438
+ */
2439
+ const ajvFunctionValidate = function (data, dataCxt) {
2440
+ const valid = typeof data === "function";
2441
+ if (!valid) {
2442
+ ajvFunctionValidate.errors = [
2443
+ {
2444
+ instancePath: /* istanbul ignore next */ dataCxt === null || dataCxt === void 0 ? void 0 : dataCxt.instancePath,
2445
+ schemaPath: undefined,
2446
+ keyword: "type",
2447
+ message: "should be a function",
2448
+ params: {
2449
+ keyword: "type",
2450
+ },
2451
+ },
2452
+ ];
2146
2453
  }
2147
- /**
2148
- * Similar to childNodes but only elements.
2149
- */
2150
- get childElements() {
2151
- return this.childNodes.filter(isElementNode);
2454
+ return valid;
2455
+ };
2456
+ const ajvFunctionKeyword = {
2457
+ keyword: "function",
2458
+ schema: false,
2459
+ errors: true,
2460
+ validate: ajvFunctionValidate,
2461
+ };
2462
+
2463
+ function isSet(value) {
2464
+ return typeof value !== "undefined";
2465
+ }
2466
+ function flag(value) {
2467
+ return value ? true : undefined;
2468
+ }
2469
+ function stripUndefined(src) {
2470
+ const entries = Object.entries(src).filter(([, value]) => isSet(value));
2471
+ return Object.fromEntries(entries);
2472
+ }
2473
+ function migrateSingleAttribute(src, key) {
2474
+ var _a, _b;
2475
+ const result = {};
2476
+ result.deprecated = flag((_a = src.deprecatedAttributes) === null || _a === void 0 ? void 0 : _a.includes(key));
2477
+ result.required = flag((_b = src.requiredAttributes) === null || _b === void 0 ? void 0 : _b.includes(key));
2478
+ result.omit = undefined;
2479
+ const attr = src.attributes ? src.attributes[key] : undefined;
2480
+ if (typeof attr === "undefined") {
2481
+ return stripUndefined(result);
2152
2482
  }
2153
- /**
2154
- * Find the first ancestor matching a selector.
2155
- *
2156
- * Implementation of DOM specification of Element.closest(selectors).
2157
- */
2158
- closest(selectors) {
2159
- /* eslint-disable-next-line @typescript-eslint/no-this-alias */
2160
- let node = this;
2161
- while (node) {
2162
- if (node.matches(selectors)) {
2163
- return node;
2164
- }
2165
- node = node.parent;
2166
- }
2167
- return null;
2483
+ /* when the attribute is set to null we use a special property "delete" to
2484
+ * flag it, if it is still set during merge (inheritance, overwriting, etc) the attribute will be removed */
2485
+ if (attr === null) {
2486
+ result.delete = true;
2487
+ return stripUndefined(result);
2168
2488
  }
2169
- /**
2170
- * Generate a DOM selector for this element. The returned selector will be
2171
- * unique inside the current document.
2172
- */
2173
- generateSelector() {
2174
- /* root element cannot have a selector as it isn't a proper element */
2175
- if (this.isRootElement()) {
2176
- return null;
2177
- }
2178
- const parts = [];
2179
- let root;
2180
- /* eslint-disable-next-line @typescript-eslint/no-this-alias */
2181
- for (root = this; root.parent; root = root.parent) {
2182
- /* .. */
2489
+ if (Array.isArray(attr)) {
2490
+ if (attr.length === 0) {
2491
+ result.boolean = true;
2183
2492
  }
2184
- // eslint-disable-next-line @typescript-eslint/no-this-alias
2185
- for (let cur = this; cur.parent; cur = cur.parent) {
2186
- /* if a unique id is present, use it and short-circuit */
2187
- if (cur.id) {
2188
- const selector = generateIdSelector(cur.id);
2189
- const matches = root.querySelectorAll(selector);
2190
- if (matches.length === 1) {
2191
- parts.push(selector);
2192
- break;
2193
- }
2194
- }
2195
- const parent = cur.parent;
2196
- const child = parent.childElements;
2197
- const index = child.findIndex((it) => it.unique === cur.unique);
2198
- const numOfType = child.filter((it) => it.is(cur.tagName)).length;
2199
- const solo = numOfType === 1;
2200
- /* if this is the only tagName in this level of siblings nth-child isn't needed */
2201
- if (solo) {
2202
- parts.push(cur.tagName.toLowerCase());
2203
- continue;
2493
+ else {
2494
+ result.enum = attr.filter((it) => it !== "");
2495
+ if (attr.includes("")) {
2496
+ result.omit = true;
2204
2497
  }
2205
- /* this will generate the worst kind of selector but at least it will be accurate (optimizations welcome) */
2206
- parts.push(`${cur.tagName.toLowerCase()}:nth-child(${index + 1})`);
2207
2498
  }
2208
- return parts.reverse().join(" > ");
2499
+ return stripUndefined(result);
2209
2500
  }
2210
- /**
2211
- * Tests if this element has given tagname.
2212
- *
2213
- * If passing "*" this test will pass if any tagname is set.
2214
- */
2215
- is(tagName) {
2216
- return tagName === "*" || this.tagName.toLowerCase() === tagName.toLowerCase();
2501
+ else {
2502
+ return stripUndefined({ ...result, ...attr });
2217
2503
  }
2218
- /**
2219
- * Load new element metadata onto this element.
2220
- *
2221
- * Do note that semantics such as `void` cannot be changed (as the element has
2222
- * already been created). In addition the element will still "be" the same
2223
- * element, i.e. even if loading meta for a `<p>` tag upon a `<div>` tag it
2224
- * will still be a `<div>` as far as the rest of the validator is concerned.
2225
- *
2226
- * In fact only certain properties will be copied onto the element:
2227
- *
2228
- * - content categories (flow, phrasing, etc)
2229
- * - required attributes
2230
- * - attribute allowed values
2231
- * - permitted/required elements
2232
- *
2233
- * Properties *not* loaded:
2234
- *
2235
- * - inherit
2236
- * - deprecated
2237
- * - foreign
2238
- * - void
2239
- * - implicitClosed
2240
- * - scriptSupporting
2241
- * - deprecatedAttributes
2242
- *
2243
- * Changes to element metadata will only be visible after `element:ready` (and
2244
- * the subsequent `dom:ready` event).
2245
- */
2246
- loadMeta(meta) {
2247
- if (!this.metaElement) {
2248
- this.metaElement = {};
2249
- }
2250
- for (const key of MetaCopyableProperty) {
2251
- const value = meta[key];
2252
- if (typeof value !== "undefined") {
2253
- setMetaProperty(this.metaElement, key, value);
2254
- }
2255
- else {
2256
- delete this.metaElement[key];
2257
- }
2504
+ }
2505
+ function migrateAttributes(src) {
2506
+ var _a, _b, _c;
2507
+ const keys = [
2508
+ ...Object.keys((_a = src.attributes) !== null && _a !== void 0 ? _a : {}),
2509
+ ...((_b = src.requiredAttributes) !== null && _b !== void 0 ? _b : []),
2510
+ ...((_c = src.deprecatedAttributes) !== null && _c !== void 0 ? _c : []),
2511
+ ].sort();
2512
+ const entries = keys.map((key) => {
2513
+ return [key, migrateSingleAttribute(src, key)];
2514
+ });
2515
+ return Object.fromEntries(entries);
2516
+ }
2517
+ function migrateElement(src) {
2518
+ const result = {
2519
+ ...src,
2520
+ attributes: migrateAttributes(src),
2521
+ textContent: src.textContent,
2522
+ };
2523
+ /* removed properties */
2524
+ delete result.deprecatedAttributes;
2525
+ delete result.requiredAttributes;
2526
+ /* strip out undefined */
2527
+ if (!result.textContent) {
2528
+ delete result.textContent;
2529
+ }
2530
+ return result;
2531
+ }
2532
+
2533
+ /**
2534
+ * Returns true if given element is a descendant of given tagname.
2535
+ *
2536
+ * @internal
2537
+ */
2538
+ function isDescendant(node, tagName) {
2539
+ let cur = node.parent;
2540
+ while (cur && !cur.isRootElement()) {
2541
+ if (cur.is(tagName)) {
2542
+ return true;
2258
2543
  }
2544
+ cur = cur.parent;
2545
+ }
2546
+ return false;
2547
+ }
2548
+
2549
+ /**
2550
+ * Returns true if given element has given attribute (no matter the value, null,
2551
+ * dynamic, etc).
2552
+ */
2553
+ function hasAttribute(node, attr) {
2554
+ return node.hasAttribute(attr);
2555
+ }
2556
+
2557
+ /**
2558
+ * Matches attribute against value.
2559
+ */
2560
+ function matchAttribute(node, key, op, value) {
2561
+ const nodeValue = (node.getAttributeValue(key) || "").toLowerCase();
2562
+ switch (op) {
2563
+ case "!=":
2564
+ return nodeValue !== value;
2565
+ case "=":
2566
+ return nodeValue === value;
2259
2567
  }
2568
+ }
2569
+
2570
+ const dynamicKeys = [
2571
+ "metadata",
2572
+ "flow",
2573
+ "sectioning",
2574
+ "heading",
2575
+ "phrasing",
2576
+ "embedded",
2577
+ "interactive",
2578
+ "labelable",
2579
+ ];
2580
+ const functionTable = {
2581
+ isDescendant: isDescendantFacade,
2582
+ hasAttribute: hasAttributeFacade,
2583
+ matchAttribute: matchAttributeFacade,
2584
+ };
2585
+ const schemaCache = new Map();
2586
+ function clone(src) {
2587
+ return JSON.parse(JSON.stringify(src));
2588
+ }
2589
+ function overwriteMerge$1(a, b) {
2590
+ return b;
2591
+ }
2592
+ /**
2593
+ * @public
2594
+ */
2595
+ class MetaTable {
2260
2596
  /**
2261
- * Match this element against given selectors. Returns true if any selector
2262
- * matches.
2263
- *
2264
- * Implementation of DOM specification of Element.matches(selectors).
2597
+ * @internal
2265
2598
  */
2266
- matches(selector) {
2267
- /* find root element */
2268
- /* eslint-disable-next-line @typescript-eslint/no-this-alias */
2269
- let root = this;
2270
- while (root.parent) {
2271
- root = root.parent;
2272
- }
2273
- /* a bit slow implementation as it finds all candidates for the selector and
2274
- * then tests if any of them are the current element. A better
2275
- * implementation would be to walk the selector right-to-left and test
2276
- * ancestors. */
2277
- for (const match of root.querySelectorAll(selector)) {
2278
- if (match.unique === this.unique) {
2279
- return true;
2280
- }
2281
- }
2282
- return false;
2283
- }
2284
- get meta() {
2285
- return this.metaElement;
2599
+ constructor() {
2600
+ this.elements = {};
2601
+ this.schema = clone(schema);
2286
2602
  }
2287
2603
  /**
2288
- * Set annotation for this element.
2604
+ * @internal
2289
2605
  */
2290
- setAnnotation(text) {
2291
- this.annotation = text;
2606
+ init() {
2607
+ this.resolveGlobal();
2292
2608
  }
2293
2609
  /**
2294
- * Set attribute. Stores all attributes set even with the same name.
2610
+ * Extend validation schema.
2295
2611
  *
2296
- * @param key - Attribute name
2297
- * @param value - Attribute value. Use `null` if no value is present.
2298
- * @param keyLocation - Location of the attribute name.
2299
- * @param valueLocation - Location of the attribute value (excluding quotation)
2300
- * @param originalAttribute - If attribute is an alias for another attribute
2301
- * (dynamic attributes) set this to the original attribute name.
2612
+ * @internal
2302
2613
  */
2303
- setAttribute(key, value, keyLocation, valueLocation, originalAttribute) {
2304
- key = key.toLowerCase();
2305
- if (!this.attr[key]) {
2306
- this.attr[key] = [];
2614
+ extendValidationSchema(patch) {
2615
+ if (patch.properties) {
2616
+ this.schema = deepmerge(this.schema, {
2617
+ patternProperties: {
2618
+ "^[^$].*$": {
2619
+ properties: patch.properties,
2620
+ },
2621
+ },
2622
+ });
2623
+ }
2624
+ if (patch.definitions) {
2625
+ this.schema = deepmerge(this.schema, {
2626
+ definitions: patch.definitions,
2627
+ });
2307
2628
  }
2308
- this.attr[key].push(new Attribute(key, value, keyLocation, valueLocation, originalAttribute));
2309
2629
  }
2310
2630
  /**
2311
- * Get a list of all attributes on this node.
2631
+ * Load metadata table from object.
2632
+ *
2633
+ * @internal
2634
+ * @param obj - Object with metadata to load
2635
+ * @param filename - Optional filename used when presenting validation error
2312
2636
  */
2313
- get attributes() {
2314
- return Object.values(this.attr).reduce((result, cur) => {
2315
- return result.concat(cur);
2316
- }, []);
2317
- }
2318
- hasAttribute(key) {
2319
- key = key.toLowerCase();
2320
- return key in this.attr;
2321
- }
2322
- getAttribute(key, all = false) {
2323
- key = key.toLowerCase();
2324
- if (key in this.attr) {
2325
- const matches = this.attr[key];
2326
- return all ? matches : matches[0];
2637
+ loadFromObject(obj, filename = null) {
2638
+ var _a;
2639
+ const validate = this.getSchemaValidator();
2640
+ if (!validate(obj)) {
2641
+ throw new SchemaValidationError(filename, `Element metadata is not valid`, obj, this.schema,
2642
+ /* istanbul ignore next: AJV sets .errors when validate returns false */
2643
+ (_a = validate.errors) !== null && _a !== void 0 ? _a : []);
2327
2644
  }
2328
- else {
2329
- return null;
2645
+ for (const [key, value] of Object.entries(obj)) {
2646
+ if (key === "$schema")
2647
+ continue;
2648
+ this.addEntry(key, migrateElement(value));
2330
2649
  }
2331
2650
  }
2332
2651
  /**
2333
- * Get attribute value.
2334
- *
2335
- * Returns the attribute value if present.
2336
- *
2337
- * - Missing attributes return `null`.
2338
- * - Boolean attributes return `null`.
2339
- * - `DynamicValue` returns attribute expression.
2652
+ * Load metadata table from filename
2340
2653
  *
2341
- * @param key - Attribute name
2342
- * @returns Attribute value or null.
2654
+ * @internal
2655
+ * @param filename - Filename to load
2343
2656
  */
2344
- getAttributeValue(key) {
2345
- const attr = this.getAttribute(key);
2346
- if (attr) {
2347
- return attr.value !== null ? attr.value.toString() : null;
2657
+ loadFromFile(filename) {
2658
+ try {
2659
+ /* load using require as it can process both js and json */
2660
+ const data = requireUncached(filename);
2661
+ this.loadFromObject(data, filename);
2348
2662
  }
2349
- else {
2350
- return null;
2663
+ catch (err) {
2664
+ if (err instanceof SchemaValidationError) {
2665
+ throw err;
2666
+ }
2667
+ throw new UserError(`Failed to load element metadata from "${filename}"`, ensureError(err));
2351
2668
  }
2352
2669
  }
2353
2670
  /**
2354
- * Add text as a child node to this element.
2671
+ * Get [[MetaElement]] for the given tag. If no specific metadata is present
2672
+ * the global metadata is returned or null if no global is present.
2355
2673
  *
2356
- * @param text - Text to add.
2357
- * @param location - Source code location of this text.
2358
- */
2359
- appendText(text, location) {
2360
- this.childNodes.push(new TextNode(text, location));
2361
- }
2362
- /**
2363
- * Return a list of all known classes on the element. Dynamic values are
2364
- * ignored.
2674
+ * @public
2675
+ * @returns A shallow copy of metadata.
2365
2676
  */
2366
- get classList() {
2367
- if (!this.hasAttribute("class")) {
2368
- return new DOMTokenList(null, null);
2677
+ getMetaFor(tagName) {
2678
+ /* try to locate by tagname */
2679
+ tagName = tagName.toLowerCase();
2680
+ if (this.elements[tagName]) {
2681
+ return { ...this.elements[tagName] };
2369
2682
  }
2370
- const classes = this.getAttribute("class", true)
2371
- .filter((attr) => attr.isStatic)
2372
- .map((attr) => attr.value)
2373
- .join(" ");
2374
- return new DOMTokenList(classes, null);
2375
- }
2376
- /**
2377
- * Get element ID if present.
2378
- */
2379
- get id() {
2380
- return this.getAttributeValue("id");
2381
- }
2382
- get style() {
2383
- const attr = this.getAttribute("style");
2384
- return parseCssDeclaration(attr === null || attr === void 0 ? void 0 : attr.value);
2683
+ /* try to locate global element */
2684
+ if (this.elements["*"]) {
2685
+ return { ...this.elements["*"] };
2686
+ }
2687
+ return null;
2385
2688
  }
2386
2689
  /**
2387
- * Returns the first child element or null if there are no child elements.
2690
+ * Find all tags which has enabled given property.
2691
+ *
2692
+ * @public
2388
2693
  */
2389
- get firstElementChild() {
2390
- const children = this.childElements;
2391
- return children.length > 0 ? children[0] : null;
2694
+ getTagsWithProperty(propName) {
2695
+ return Object.entries(this.elements)
2696
+ .filter(([, entry]) => entry[propName])
2697
+ .map(([tagName]) => tagName);
2392
2698
  }
2393
2699
  /**
2394
- * Returns the last child element or null if there are no child elements.
2700
+ * Find tag matching tagName or inheriting from it.
2701
+ *
2702
+ * @public
2395
2703
  */
2396
- get lastElementChild() {
2397
- const children = this.childElements;
2398
- return children.length > 0 ? children[children.length - 1] : null;
2399
- }
2400
- get siblings() {
2401
- return this.parent ? this.parent.childElements : [this];
2402
- }
2403
- get previousSibling() {
2404
- const i = this.siblings.findIndex((node) => node.unique === this.unique);
2405
- return i >= 1 ? this.siblings[i - 1] : null;
2406
- }
2407
- get nextSibling() {
2408
- const i = this.siblings.findIndex((node) => node.unique === this.unique);
2409
- return i <= this.siblings.length - 2 ? this.siblings[i + 1] : null;
2410
- }
2411
- getElementsByTagName(tagName) {
2412
- return this.childElements.reduce((matches, node) => {
2413
- return matches.concat(node.is(tagName) ? [node] : [], node.getElementsByTagName(tagName));
2414
- }, []);
2415
- }
2416
- querySelector(selector) {
2417
- var _a;
2418
- const it = this.querySelectorImpl(selector);
2419
- return (_a = it.next().value) !== null && _a !== void 0 ? _a : null; // eslint-disable-line @typescript-eslint/no-unsafe-return
2420
- }
2421
- querySelectorAll(selector) {
2422
- const it = this.querySelectorImpl(selector);
2423
- const unique = new Set(it);
2424
- return Array.from(unique.values());
2704
+ getTagsDerivedFrom(tagName) {
2705
+ return Object.entries(this.elements)
2706
+ .filter(([key, entry]) => key === tagName || entry.inherit === tagName)
2707
+ .map(([tagName]) => tagName);
2425
2708
  }
2426
- *querySelectorImpl(selectorList) {
2427
- if (!selectorList) {
2428
- return;
2429
- }
2430
- for (const selector of selectorList.split(/,\s*/)) {
2431
- const pattern = new Selector(selector);
2432
- yield* pattern.match(this);
2709
+ addEntry(tagName, entry) {
2710
+ let parent = this.elements[tagName] || {};
2711
+ /* handle inheritance */
2712
+ if (entry.inherit) {
2713
+ const name = entry.inherit;
2714
+ parent = this.elements[name];
2715
+ if (!parent) {
2716
+ throw new UserError(`Element <${tagName}> cannot inherit from <${name}>: no such element`);
2717
+ }
2433
2718
  }
2719
+ /* merge all sources together */
2720
+ const expanded = this.mergeElement(parent, { ...entry, tagName });
2721
+ expandRegex(expanded);
2722
+ this.elements[tagName] = expanded;
2434
2723
  }
2435
2724
  /**
2436
- * Visit all nodes from this node and down. Depth first.
2437
- *
2438
- * @internal
2725
+ * Construct a new AJV schema validator.
2439
2726
  */
2440
- visitDepthFirst(callback) {
2441
- function visit(node) {
2442
- node.childElements.forEach(visit);
2443
- if (!node.isRootElement()) {
2444
- callback(node);
2445
- }
2727
+ getSchemaValidator() {
2728
+ const hash = computeHash(JSON.stringify(this.schema));
2729
+ const cached = schemaCache.get(hash);
2730
+ if (cached) {
2731
+ return cached;
2732
+ }
2733
+ else {
2734
+ const ajv = new Ajv({ strict: true, strictTuples: true, strictTypes: true });
2735
+ ajv.addMetaSchema(ajvSchemaDraft);
2736
+ ajv.addKeyword(ajvFunctionKeyword);
2737
+ ajv.addKeyword(ajvRegexpKeyword);
2738
+ ajv.addKeyword({ keyword: "copyable" });
2739
+ const validate = ajv.compile(this.schema);
2740
+ schemaCache.set(hash, validate);
2741
+ return validate;
2446
2742
  }
2447
- visit(this);
2448
2743
  }
2449
2744
  /**
2450
- * Evaluates callbackk on all descendants, returning true if any are true.
2451
- *
2452
- * @internal
2745
+ * @public
2453
2746
  */
2454
- someChildren(callback) {
2455
- return this.childElements.some(visit);
2456
- function visit(node) {
2457
- if (callback(node)) {
2458
- return true;
2459
- }
2460
- else {
2461
- return node.childElements.some(visit);
2462
- }
2463
- }
2747
+ getJSONSchema() {
2748
+ return this.schema;
2464
2749
  }
2465
2750
  /**
2466
- * Evaluates callbackk on all descendants, returning true if all are true.
2467
- *
2468
- * @internal
2751
+ * Finds the global element definition and merges each known element with the
2752
+ * global, e.g. to assign global attributes.
2469
2753
  */
2470
- everyChildren(callback) {
2471
- return this.childElements.every(visit);
2472
- function visit(node) {
2473
- if (!callback(node)) {
2474
- return false;
2475
- }
2476
- return node.childElements.every(visit);
2754
+ resolveGlobal() {
2755
+ /* skip if there is no global elements */
2756
+ if (!this.elements["*"])
2757
+ return;
2758
+ /* fetch and remove the global element, it should not be resolvable by
2759
+ * itself */
2760
+ const global = this.elements["*"];
2761
+ delete this.elements["*"];
2762
+ /* hack: unset default properties which global should not override */
2763
+ delete global.tagName;
2764
+ delete global.void;
2765
+ /* merge elements */
2766
+ for (const [tagName, entry] of Object.entries(this.elements)) {
2767
+ this.elements[tagName] = this.mergeElement(global, entry);
2477
2768
  }
2478
2769
  }
2770
+ mergeElement(a, b) {
2771
+ const merged = deepmerge(a, b, { arrayMerge: overwriteMerge$1 });
2772
+ /* special handling when removing attributes by setting them to null
2773
+ * resulting in the deletion flag being set */
2774
+ const filteredAttrs = Object.entries(merged.attributes).filter(([, attr]) => {
2775
+ const val = !attr.delete;
2776
+ delete attr.delete;
2777
+ return val;
2778
+ });
2779
+ merged.attributes = Object.fromEntries(filteredAttrs);
2780
+ return merged;
2781
+ }
2479
2782
  /**
2480
- * Visit all nodes from this node and down. Breadth first.
2481
- *
2482
- * The first node for which the callback evaluates to true is returned.
2483
- *
2484
2783
  * @internal
2485
2784
  */
2486
- find(callback) {
2487
- function visit(node) {
2488
- if (callback(node)) {
2489
- return node;
2490
- }
2491
- for (const child of node.childElements) {
2492
- const match = child.find(callback);
2493
- if (match) {
2494
- return match;
2495
- }
2496
- }
2497
- return null;
2785
+ resolve(node) {
2786
+ if (node.meta) {
2787
+ expandProperties(node, node.meta);
2498
2788
  }
2499
- return visit(this);
2500
2789
  }
2501
2790
  }
2502
- function isClosed(endToken, meta) {
2503
- let closed = NodeClosed.Open;
2504
- if (meta && meta.void) {
2505
- closed = NodeClosed.VoidOmitted;
2506
- }
2507
- if (endToken.data[0] === "/>") {
2508
- closed = NodeClosed.VoidSelfClosed;
2791
+ function expandProperties(node, entry) {
2792
+ for (const key of dynamicKeys) {
2793
+ const property = entry[key];
2794
+ if (property && typeof property !== "boolean") {
2795
+ setMetaProperty(entry, key, evaluateProperty(node, property));
2796
+ }
2509
2797
  }
2510
- return closed;
2511
2798
  }
2512
-
2513
- class DOMTree {
2514
- constructor(location) {
2515
- this.root = HtmlElement.rootNode(location);
2516
- this.active = this.root;
2517
- this.doctype = null;
2799
+ /**
2800
+ * Given a string it returns either the string as-is or if the string is wrapped
2801
+ * in /../ it creates and returns a regex instead.
2802
+ */
2803
+ function expandRegexValue(value) {
2804
+ if (value instanceof RegExp) {
2805
+ return value;
2518
2806
  }
2519
- pushActive(node) {
2520
- this.active = node;
2807
+ const match = value.match(/^\/\^?([^/$]*)\$?\/([i]*)$/);
2808
+ if (match) {
2809
+ const [, expr, flags] = match;
2810
+ // eslint-disable-next-line security/detect-non-literal-regexp
2811
+ return new RegExp(`^${expr}$`, flags);
2521
2812
  }
2522
- popActive() {
2523
- if (this.active.isRootElement()) {
2524
- /* root element should never be popped, continue as if nothing happened */
2525
- return;
2526
- }
2527
- this.active = this.active.parent || this.root;
2813
+ else {
2814
+ return value;
2528
2815
  }
2529
- getActive() {
2530
- return this.active;
2816
+ }
2817
+ /**
2818
+ * Expand all regular expressions in strings ("/../"). This mutates the object.
2819
+ */
2820
+ function expandRegex(entry) {
2821
+ for (const [name, values] of Object.entries(entry.attributes)) {
2822
+ if (values.enum) {
2823
+ entry.attributes[name].enum = values.enum.map(expandRegexValue);
2824
+ }
2531
2825
  }
2532
- /**
2533
- * Resolve dynamic meta expressions.
2534
- */
2535
- resolveMeta(table) {
2536
- this.visitDepthFirst((node) => table.resolve(node));
2826
+ }
2827
+ function evaluateProperty(node, expr) {
2828
+ const [func, options] = parseExpression(expr);
2829
+ return func(node, options);
2830
+ }
2831
+ function parseExpression(expr) {
2832
+ if (typeof expr === "string") {
2833
+ return parseExpression([expr, {}]);
2537
2834
  }
2538
- getElementsByTagName(tagName) {
2539
- return this.root.getElementsByTagName(tagName);
2835
+ else {
2836
+ const [funcName, options] = expr;
2837
+ const func = functionTable[funcName];
2838
+ if (!func) {
2839
+ throw new Error(`Failed to find function "${funcName}" when evaluating property expression`);
2840
+ }
2841
+ return [func, options];
2540
2842
  }
2541
- visitDepthFirst(callback) {
2542
- this.root.visitDepthFirst(callback);
2843
+ }
2844
+ function isDescendantFacade(node, tagName) {
2845
+ if (typeof tagName !== "string") {
2846
+ throw new Error(`Property expression "isDescendant" must take string argument when evaluating metadata for <${node.tagName}>`);
2543
2847
  }
2544
- find(callback) {
2545
- return this.root.find(callback);
2848
+ return isDescendant(node, tagName);
2849
+ }
2850
+ function hasAttributeFacade(node, attr) {
2851
+ if (typeof attr !== "string") {
2852
+ throw new Error(`Property expression "hasAttribute" must take string argument when evaluating metadata for <${node.tagName}>`);
2546
2853
  }
2547
- querySelector(selector) {
2548
- return this.root.querySelector(selector);
2854
+ return hasAttribute(node, attr);
2855
+ }
2856
+ function matchAttributeFacade(node, match) {
2857
+ if (!Array.isArray(match) || match.length !== 3) {
2858
+ throw new Error(`Property expression "matchAttribute" must take [key, op, value] array as argument when evaluating metadata for <${node.tagName}>`);
2549
2859
  }
2550
- querySelectorAll(selector) {
2551
- return this.root.querySelectorAll(selector);
2860
+ const [key, op, value] = match.map((x) => x.toLowerCase());
2861
+ switch (op) {
2862
+ case "!=":
2863
+ case "=":
2864
+ return matchAttribute(node, key, op, value);
2865
+ default:
2866
+ throw new Error(`Property expression "matchAttribute" has invalid operator "${op}" when evaluating metadata for <${node.tagName}>`);
2552
2867
  }
2553
2868
  }
2554
2869
 
@@ -3191,7 +3506,7 @@ var TRANSFORMER_API;
3191
3506
  /** @public */
3192
3507
  const name = "html-validate";
3193
3508
  /** @public */
3194
- const version = "7.6.0";
3509
+ const version = "7.7.1";
3195
3510
  /** @public */
3196
3511
  const homepage = "https://html-validate.org";
3197
3512
  /** @public */
@@ -3520,7 +3835,7 @@ function ruleDocumentationUrl(filename) {
3520
3835
  return `${homepage}/rules/${normalized}.html`;
3521
3836
  }
3522
3837
 
3523
- const defaults$s = {
3838
+ const defaults$t = {
3524
3839
  allowExternal: true,
3525
3840
  allowRelative: true,
3526
3841
  allowAbsolute: true,
@@ -3564,7 +3879,7 @@ function matchList(value, list) {
3564
3879
  }
3565
3880
  class AllowedLinks extends Rule {
3566
3881
  constructor(options) {
3567
- super({ ...defaults$s, ...options });
3882
+ super({ ...defaults$t, ...options });
3568
3883
  this.allowExternal = parseAllow(this.options.allowExternal);
3569
3884
  this.allowRelative = parseAllow(this.options.allowRelative);
3570
3885
  this.allowAbsolute = parseAllow(this.options.allowAbsolute);
@@ -3707,6 +4022,109 @@ class AllowedLinks extends Rule {
3707
4022
  }
3708
4023
  }
3709
4024
 
4025
+ var RuleContext$1;
4026
+ (function (RuleContext) {
4027
+ RuleContext["MISSING_ALT"] = "missing-alt";
4028
+ RuleContext["MISSING_HREF"] = "missing-href";
4029
+ })(RuleContext$1 || (RuleContext$1 = {}));
4030
+ const defaults$s = {
4031
+ accessible: true,
4032
+ };
4033
+ function findByTarget(target, siblings) {
4034
+ return siblings.filter((it) => it.getAttributeValue("href") === target);
4035
+ }
4036
+ function getAltText(node) {
4037
+ return node.getAttributeValue("alt");
4038
+ }
4039
+ function getDescription(context) {
4040
+ switch (context) {
4041
+ case RuleContext$1.MISSING_ALT:
4042
+ return [
4043
+ "The `alt` attribute must be set (and not empty) when the `href` attribute is present on an `<area>` element.",
4044
+ "",
4045
+ "The attribute is used to provide an alternative text description for the area of the image map.",
4046
+ "The text should describe the purpose of area and the resource referenced by the `href` attribute.",
4047
+ "",
4048
+ "Either add the `alt` attribute or remove the `href` attribute.",
4049
+ ];
4050
+ case RuleContext$1.MISSING_HREF:
4051
+ return [
4052
+ "The `alt` attribute must not be set when the `href` attribute is missing on an `<area>` element.",
4053
+ "",
4054
+ "Either add the `href` attribute or remove the `alt` attribute.",
4055
+ ];
4056
+ default:
4057
+ return [
4058
+ "The `alt` attribute must only be used together with the `href` attribute.",
4059
+ "It must be set if `href` is present and must be omitted if `href` is missing",
4060
+ "",
4061
+ "The attribute is used to provide an alternative text description for the area of the image map.",
4062
+ "The text should describe the purpose of area and the resource referenced by the `href` attribute.",
4063
+ ];
4064
+ }
4065
+ }
4066
+ class AreaAlt extends Rule {
4067
+ constructor(options) {
4068
+ super({ ...defaults$s, ...options });
4069
+ }
4070
+ static schema() {
4071
+ return {
4072
+ accessible: {
4073
+ type: "boolean",
4074
+ },
4075
+ };
4076
+ }
4077
+ documentation(context) {
4078
+ return {
4079
+ description: getDescription(context).join("\n"),
4080
+ url: ruleDocumentationUrl("@/rules/area-alt.ts"),
4081
+ };
4082
+ }
4083
+ setup() {
4084
+ this.on("element:ready", this.isRelevant, (event) => {
4085
+ const { target } = event;
4086
+ const siblings = target.querySelectorAll("area");
4087
+ for (const child of siblings) {
4088
+ this.validateArea(child, siblings);
4089
+ }
4090
+ });
4091
+ }
4092
+ validateArea(area, siblings) {
4093
+ const { accessible } = this.options;
4094
+ const href = area.getAttribute("href");
4095
+ const alt = area.getAttribute("alt");
4096
+ if (href) {
4097
+ if (alt && alt.isDynamic) {
4098
+ return;
4099
+ }
4100
+ const target = area.getAttributeValue("href");
4101
+ const altTexts = accessible
4102
+ ? [getAltText(area)]
4103
+ : findByTarget(target, siblings).map(getAltText);
4104
+ if (!altTexts.some(Boolean)) {
4105
+ this.report({
4106
+ node: area,
4107
+ message: `"alt" attribute must be set and non-empty when the "href" attribute is present`,
4108
+ location: alt ? alt.keyLocation : href.keyLocation,
4109
+ context: RuleContext$1.MISSING_ALT,
4110
+ });
4111
+ }
4112
+ }
4113
+ else if (alt) {
4114
+ this.report({
4115
+ node: area,
4116
+ message: `"alt" attribute cannot be used unless the "href" attribute is present`,
4117
+ location: alt.keyLocation,
4118
+ context: RuleContext$1.MISSING_HREF,
4119
+ });
4120
+ }
4121
+ }
4122
+ isRelevant(event) {
4123
+ const { target } = event;
4124
+ return target.is("map");
4125
+ }
4126
+ }
4127
+
3710
4128
  class AriaHiddenBody extends Rule {
3711
4129
  documentation() {
3712
4130
  return {
@@ -4463,6 +4881,16 @@ class AttrSpacing extends Rule {
4463
4881
  }
4464
4882
  }
4465
4883
 
4884
+ function pick(attr) {
4885
+ const result = {};
4886
+ if (typeof attr.enum !== "undefined") {
4887
+ result.enum = attr.enum;
4888
+ }
4889
+ if (typeof attr.boolean !== "undefined") {
4890
+ result.boolean = attr.boolean;
4891
+ }
4892
+ return result;
4893
+ }
4466
4894
  class AttributeAllowedValues extends Rule {
4467
4895
  documentation(context) {
4468
4896
  const docs = {
@@ -4513,7 +4941,7 @@ class AttributeAllowedValues extends Rule {
4513
4941
  element: node.tagName,
4514
4942
  attribute: attr.key,
4515
4943
  value,
4516
- allowed: meta.attributes[attr.key],
4944
+ allowed: pick(meta.attributes[attr.key]),
4517
4945
  };
4518
4946
  const message = this.getMessage(attr);
4519
4947
  const location = this.getLocation(attr);
@@ -4709,6 +5137,54 @@ function reportMessage(attr, style) {
4709
5137
  return "";
4710
5138
  }
4711
5139
 
5140
+ function ruleDescription(context) {
5141
+ if (context) {
5142
+ const { attr, details } = context;
5143
+ return `The "${attr}" attribute cannot be used in this context: ${details}`;
5144
+ }
5145
+ else {
5146
+ return "This attribute cannot be used in this context.";
5147
+ }
5148
+ }
5149
+ class AttributeMisuse extends Rule {
5150
+ documentation(context) {
5151
+ return {
5152
+ description: ruleDescription(context),
5153
+ url: ruleDocumentationUrl("@/rules/attribute-misuse.ts"),
5154
+ };
5155
+ }
5156
+ setup() {
5157
+ this.on("element:ready", (event) => {
5158
+ const { target } = event;
5159
+ const { meta } = target;
5160
+ if (!meta) {
5161
+ return;
5162
+ }
5163
+ for (const attr of target.attributes) {
5164
+ const key = attr.key.toLowerCase();
5165
+ this.validateAttr(target, attr, meta.attributes[key]);
5166
+ }
5167
+ });
5168
+ }
5169
+ validateAttr(node, attr, meta) {
5170
+ if (!meta || !meta.allowed) {
5171
+ return;
5172
+ }
5173
+ const details = meta.allowed(node);
5174
+ if (details) {
5175
+ this.report({
5176
+ node,
5177
+ message: `"{{ attr }}" attribute cannot be used in this context: {{ details }}`,
5178
+ location: attr.keyLocation,
5179
+ context: {
5180
+ attr: attr.key,
5181
+ details,
5182
+ },
5183
+ });
5184
+ }
5185
+ }
5186
+ }
5187
+
4712
5188
  function parsePattern(pattern) {
4713
5189
  switch (pattern) {
4714
5190
  case "kebabcase":
@@ -4995,7 +5471,7 @@ class DeprecatedRule extends Rule {
4995
5471
  }
4996
5472
  }
4997
5473
 
4998
- class NoStyleTag$1 extends Rule {
5474
+ let NoStyleTag$1 = class NoStyleTag extends Rule {
4999
5475
  documentation() {
5000
5476
  return {
5001
5477
  description: [
@@ -5016,7 +5492,7 @@ class NoStyleTag$1 extends Rule {
5016
5492
  }
5017
5493
  });
5018
5494
  }
5019
- }
5495
+ };
5020
5496
 
5021
5497
  const defaults$k = {
5022
5498
  style: "uppercase",
@@ -5414,226 +5890,6 @@ class ElementPermittedOrder extends Rule {
5414
5890
  }
5415
5891
  }
5416
5892
 
5417
- const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
5418
- const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
5419
- const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
5420
- /**
5421
- * Tests if this element is present in the accessibility tree.
5422
- *
5423
- * In practice it tests whenever the element or its parents has
5424
- * `role="presentation"` or `aria-hidden="false"`. Dynamic values counts as
5425
- * visible since the element might be in the visibility tree sometimes.
5426
- */
5427
- function inAccessibilityTree(node) {
5428
- return !isAriaHidden(node) && !isPresentation(node);
5429
- }
5430
- function isAriaHiddenImpl(node) {
5431
- const isHidden = (node) => {
5432
- const ariaHidden = node.getAttribute("aria-hidden");
5433
- return Boolean(ariaHidden && ariaHidden.value === "true");
5434
- };
5435
- return {
5436
- byParent: node.parent ? isAriaHidden(node.parent) : false,
5437
- bySelf: isHidden(node),
5438
- };
5439
- }
5440
- function isAriaHidden(node, details) {
5441
- const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
5442
- if (cached) {
5443
- return details ? cached : cached.byParent || cached.bySelf;
5444
- }
5445
- const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
5446
- return details ? result : result.byParent || result.bySelf;
5447
- }
5448
- function isHTMLHiddenImpl(node) {
5449
- const isHidden = (node) => {
5450
- const hidden = node.getAttribute("hidden");
5451
- return hidden !== null && hidden.isStatic;
5452
- };
5453
- return {
5454
- byParent: node.parent ? isHTMLHidden(node.parent) : false,
5455
- bySelf: isHidden(node),
5456
- };
5457
- }
5458
- function isHTMLHidden(node, details) {
5459
- const cached = node.cacheGet(HTML_HIDDEN_CACHE);
5460
- if (cached) {
5461
- return details ? cached : cached.byParent || cached.bySelf;
5462
- }
5463
- const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
5464
- return details ? result : result.byParent || result.bySelf;
5465
- }
5466
- /**
5467
- * Tests if this element or a parent element has role="presentation".
5468
- *
5469
- * Dynamic values yields `false` just as if the attribute wasn't present.
5470
- */
5471
- function isPresentation(node) {
5472
- if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
5473
- return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
5474
- }
5475
- let cur = node;
5476
- do {
5477
- const role = cur.getAttribute("role");
5478
- /* role="presentation" */
5479
- if (role && role.value === "presentation") {
5480
- return cur.cacheSet(ROLE_PRESENTATION_CACHE, true);
5481
- }
5482
- /* sanity check: break if no parent is present, normally not an issue as the
5483
- * root element should be found first */
5484
- if (!cur.parent) {
5485
- break;
5486
- }
5487
- /* check parents */
5488
- cur = cur.parent;
5489
- } while (!cur.isRootElement());
5490
- return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
5491
- }
5492
-
5493
- const cachePrefix = classifyNodeText.name;
5494
- const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
5495
- const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
5496
- const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
5497
- const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
5498
- /**
5499
- * @public
5500
- */
5501
- var TextClassification;
5502
- (function (TextClassification) {
5503
- TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
5504
- TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
5505
- TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
5506
- })(TextClassification || (TextClassification = {}));
5507
- function getCachekey(options = {}) {
5508
- const { accessible = false, ignoreHiddenRoot = false } = options;
5509
- if (accessible && ignoreHiddenRoot) {
5510
- return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
5511
- }
5512
- else if (ignoreHiddenRoot) {
5513
- return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
5514
- }
5515
- else if (accessible) {
5516
- return A11Y_CACHE_KEY;
5517
- }
5518
- else {
5519
- return HTML_CACHE_KEY;
5520
- }
5521
- }
5522
- /* While I cannot find a reference about this in the standard the <select>
5523
- * element kinda acts as if there is no text content, most particularly it
5524
- * doesn't receive and accessible name. The `.textContent` property does
5525
- * however include the <option> childrens text. But for the sake of the
5526
- * validator it is probably best if the classification acts as if there is no
5527
- * text as I think that is what is expected of the return values. Might have
5528
- * to revisit this at some point or if someone could clarify what section of
5529
- * the standard deals with this. */
5530
- function isSpecialEmpty(node) {
5531
- return node.is("select") || node.is("textarea");
5532
- }
5533
- /**
5534
- * Checks text content of an element.
5535
- *
5536
- * Any text is considered including text from descendant elements. Whitespace is
5537
- * ignored.
5538
- *
5539
- * If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
5540
- *
5541
- * @public
5542
- */
5543
- function classifyNodeText(node, options = {}) {
5544
- const { accessible = false, ignoreHiddenRoot = false } = options;
5545
- const cacheKey = getCachekey(options);
5546
- if (node.cacheExists(cacheKey)) {
5547
- return node.cacheGet(cacheKey);
5548
- }
5549
- if (!ignoreHiddenRoot && isHTMLHidden(node)) {
5550
- return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
5551
- }
5552
- if (!ignoreHiddenRoot && accessible && isAriaHidden(node)) {
5553
- return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
5554
- }
5555
- if (isSpecialEmpty(node)) {
5556
- return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
5557
- }
5558
- const text = findTextNodes(node, {
5559
- ...options,
5560
- ignoreHiddenRoot: false,
5561
- });
5562
- /* if any text is dynamic classify as dynamic */
5563
- if (text.some((cur) => cur.isDynamic)) {
5564
- return node.cacheSet(cacheKey, TextClassification.DYNAMIC_TEXT);
5565
- }
5566
- /* if any text has non-whitespace character classify as static */
5567
- if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
5568
- return node.cacheSet(cacheKey, TextClassification.STATIC_TEXT);
5569
- }
5570
- /* default to empty */
5571
- return node.cacheSet(cacheKey, TextClassification.EMPTY_TEXT);
5572
- }
5573
- function findTextNodes(node, options) {
5574
- const { accessible = false } = options;
5575
- let text = [];
5576
- for (const child of node.childNodes) {
5577
- if (isTextNode(child)) {
5578
- text.push(child);
5579
- }
5580
- else if (isElementNode(child)) {
5581
- if (isHTMLHidden(child, true).bySelf) {
5582
- continue;
5583
- }
5584
- if (accessible && isAriaHidden(child, true).bySelf) {
5585
- continue;
5586
- }
5587
- text = text.concat(findTextNodes(child, options));
5588
- }
5589
- }
5590
- return text;
5591
- }
5592
-
5593
- function hasAltText(image) {
5594
- const alt = image.getAttribute("alt");
5595
- /* missing or boolean */
5596
- if (alt === null || alt.value === null) {
5597
- return false;
5598
- }
5599
- return alt.isDynamic || alt.value.toString() !== "";
5600
- }
5601
-
5602
- function hasAriaLabel(node) {
5603
- const label = node.getAttribute("aria-label");
5604
- /* missing or boolean */
5605
- if (label === null || label.value === null) {
5606
- return false;
5607
- }
5608
- return label.isDynamic || label.value.toString() !== "";
5609
- }
5610
-
5611
- /**
5612
- * Joins a list of words into natural language.
5613
- *
5614
- * - `["foo"]` becomes `"foo"`
5615
- * - `["foo", "bar"]` becomes `"foo or bar"`
5616
- * - `["foo", "bar", "baz"]` becomes `"foo, bar or baz"`
5617
- * - and so on...
5618
- *
5619
- * @internal
5620
- * @param values - List of words to join
5621
- * @param conjunction - Conjunction for the last element.
5622
- * @returns String with the words naturally joined with a conjunction.
5623
- */
5624
- function naturalJoin(values, conjunction = "or") {
5625
- switch (values.length) {
5626
- case 0:
5627
- return "";
5628
- case 1:
5629
- return values[0];
5630
- case 2:
5631
- return `${values[0]} ${conjunction} ${values[1]}`;
5632
- default:
5633
- return `${values.slice(0, -1).join(", ")} ${conjunction} ${values.slice(-1)[0]}`;
5634
- }
5635
- }
5636
-
5637
5893
  function isCategoryOrTag(value) {
5638
5894
  return typeof value === "string";
5639
5895
  }
@@ -6200,11 +6456,6 @@ const restricted = new Map([
6200
6456
  ["capture", ["file"]],
6201
6457
  ["checked", ["checkbox", "radio"]],
6202
6458
  ["dirname", ["text", "search"]],
6203
- ["formaction", ["submit", "image"]],
6204
- ["formenctype", ["submit", "image"]],
6205
- ["formmethod", ["submit", "image"]],
6206
- ["formnovalidate", ["submit", "image"]],
6207
- ["formtarget", ["submit", "image"]],
6208
6459
  ["height", ["image"]],
6209
6460
  [
6210
6461
  "list",
@@ -10610,6 +10861,7 @@ const bundledRules$1 = {
10610
10861
 
10611
10862
  const bundledRules = {
10612
10863
  "allowed-links": AllowedLinks,
10864
+ "area-alt": AreaAlt,
10613
10865
  "aria-hidden-body": AriaHiddenBody,
10614
10866
  "aria-label-misuse": AriaLabelMisuse,
10615
10867
  "attr-case": AttrCase,
@@ -10620,6 +10872,7 @@ const bundledRules = {
10620
10872
  "attribute-allowed-values": AttributeAllowedValues,
10621
10873
  "attribute-boolean-style": AttributeBooleanStyle,
10622
10874
  "attribute-empty-style": AttributeEmptyStyle,
10875
+ "attribute-misuse": AttributeMisuse,
10623
10876
  "class-pattern": ClassPattern,
10624
10877
  "close-attr": CloseAttr,
10625
10878
  "close-order": CloseOrder,
@@ -10686,6 +10939,7 @@ var defaultConfig = {};
10686
10939
 
10687
10940
  const config$3 = {
10688
10941
  rules: {
10942
+ "area-alt": ["error", { accessible: true }],
10689
10943
  "aria-hidden-body": "error",
10690
10944
  "aria-label-misuse": "error",
10691
10945
  "deprecated-rule": "warn",
@@ -10721,6 +10975,7 @@ const config$2 = {
10721
10975
 
10722
10976
  const config$1 = {
10723
10977
  rules: {
10978
+ "area-alt": ["error", { accessible: true }],
10724
10979
  "aria-hidden-body": "error",
10725
10980
  "aria-label-misuse": "error",
10726
10981
  "attr-case": "error",
@@ -10730,6 +10985,7 @@ const config$1 = {
10730
10985
  "attribute-allowed-values": "error",
10731
10986
  "attribute-boolean-style": "error",
10732
10987
  "attribute-empty-style": "error",
10988
+ "attribute-misuse": "error",
10733
10989
  "close-attr": "error",
10734
10990
  "close-order": "error",
10735
10991
  deprecated: "error",
@@ -10790,8 +11046,10 @@ const config$1 = {
10790
11046
 
10791
11047
  const config = {
10792
11048
  rules: {
11049
+ "area-alt": ["error", { accessible: false }],
10793
11050
  "attr-spacing": "error",
10794
11051
  "attribute-allowed-values": "error",
11052
+ "attribute-misuse": "error",
10795
11053
  "close-attr": "error",
10796
11054
  "close-order": "error",
10797
11055
  deprecated: "error",
@@ -13327,5 +13585,5 @@ function getFormatter(name) {
13327
13585
  return (_a = availableFormatters[name]) !== null && _a !== void 0 ? _a : null;
13328
13586
  }
13329
13587
 
13330
- export { Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g, classifyNodeText as h, TextClassification as i, Reporter as j, TemplateExtractor as k, getFormatter as l, legacyRequire as m, ensureError as n, configDataFromFile as o, presets as p, compatibilityCheck as q, ruleExists as r, codeframe as s, name as t, bugs as u, version as v };
13588
+ export { Config as C, DynamicValue as D, EventHandler as E, FileSystemConfigLoader as F, HtmlValidate as H, MetaTable as M, NodeClosed as N, Parser as P, Rule as R, Severity as S, TextNode as T, UserError as U, WrappedError as W, ConfigError as a, ConfigLoader as b, StaticConfigLoader as c, HtmlElement as d, SchemaValidationError as e, NestedError as f, MetaCopyableProperty as g, defineMetadata as h, classifyNodeText as i, TextClassification as j, Reporter as k, TemplateExtractor as l, metadataHelper as m, getFormatter as n, legacyRequire as o, presets as p, ensureError as q, ruleExists as r, configDataFromFile as s, compatibilityCheck as t, codeframe as u, version as v, name as w, bugs as x };
13331
13589
  //# sourceMappingURL=core.js.map