@templatical/quality 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +196 -170
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -433,8 +433,8 @@ function M(e) {
|
|
|
433
433
|
});
|
|
434
434
|
return a.write(e), a.end(), i(), t;
|
|
435
435
|
}
|
|
436
|
-
function
|
|
437
|
-
let t = e
|
|
436
|
+
function Ee(e) {
|
|
437
|
+
let t = De(e).matchAll(/<\/?a\b[^<>]*>/gi), n = 0;
|
|
438
438
|
for (let e of t) {
|
|
439
439
|
if (e[0].startsWith("</")) {
|
|
440
440
|
n > 0 && n--;
|
|
@@ -445,38 +445,53 @@ function N(e) {
|
|
|
445
445
|
}
|
|
446
446
|
return !1;
|
|
447
447
|
}
|
|
448
|
-
function
|
|
448
|
+
function De(e) {
|
|
449
|
+
let t = "", n = 0;
|
|
450
|
+
for (; n < e.length;) {
|
|
451
|
+
let r = e.indexOf("<!--", n);
|
|
452
|
+
if (r === -1) {
|
|
453
|
+
t += e.substring(n);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
t += e.substring(n, r);
|
|
457
|
+
let i = e.indexOf("-->", r + 4);
|
|
458
|
+
if (i === -1) break;
|
|
459
|
+
n = i + 3;
|
|
460
|
+
}
|
|
461
|
+
return t;
|
|
462
|
+
}
|
|
463
|
+
function N(e) {
|
|
449
464
|
let t = "", n = new l({ ontext(e) {
|
|
450
465
|
t += e;
|
|
451
466
|
} });
|
|
452
467
|
return n.write(e), n.end(), t.trim();
|
|
453
468
|
}
|
|
454
|
-
var
|
|
469
|
+
var Oe = {
|
|
455
470
|
meta: {
|
|
456
471
|
id: "a11y.heading-empty",
|
|
457
472
|
severity: "error"
|
|
458
473
|
},
|
|
459
474
|
block(e) {
|
|
460
|
-
return !c(e) ||
|
|
475
|
+
return !c(e) || N(e.content ?? "") !== "" ? null : { blockId: e.id };
|
|
461
476
|
}
|
|
462
|
-
},
|
|
477
|
+
}, ke = {
|
|
463
478
|
id: "a11y.heading-skip-level",
|
|
464
479
|
severity: "error"
|
|
465
480
|
};
|
|
466
|
-
function
|
|
481
|
+
function P(e, t) {
|
|
467
482
|
for (let n of e) {
|
|
468
483
|
if (c(n)) {
|
|
469
484
|
t.push(n);
|
|
470
485
|
continue;
|
|
471
486
|
}
|
|
472
|
-
if (o(n)) for (let e of n.children)
|
|
487
|
+
if (o(n)) for (let e of n.children) P(e, t);
|
|
473
488
|
}
|
|
474
489
|
}
|
|
475
|
-
var
|
|
476
|
-
meta:
|
|
490
|
+
var Ae = {
|
|
491
|
+
meta: ke,
|
|
477
492
|
template(e) {
|
|
478
493
|
let t = [];
|
|
479
|
-
|
|
494
|
+
P(e.blocks, t);
|
|
480
495
|
let n = [], r = 0;
|
|
481
496
|
for (let e of t) r !== 0 && e.level > r + 1 && n.push({
|
|
482
497
|
blockId: e.id,
|
|
@@ -487,51 +502,51 @@ var Oe = {
|
|
|
487
502
|
}), r = e.level;
|
|
488
503
|
return n;
|
|
489
504
|
}
|
|
490
|
-
},
|
|
505
|
+
}, je = {
|
|
491
506
|
id: "a11y.heading-multiple-h1",
|
|
492
507
|
severity: "warning"
|
|
493
508
|
};
|
|
494
|
-
function
|
|
509
|
+
function F(e, t) {
|
|
495
510
|
for (let n of e) {
|
|
496
511
|
if (c(n)) {
|
|
497
512
|
t.push(n);
|
|
498
513
|
continue;
|
|
499
514
|
}
|
|
500
|
-
if (o(n)) for (let e of n.children)
|
|
515
|
+
if (o(n)) for (let e of n.children) F(e, t);
|
|
501
516
|
}
|
|
502
517
|
}
|
|
503
|
-
var
|
|
504
|
-
meta:
|
|
518
|
+
var Me = {
|
|
519
|
+
meta: je,
|
|
505
520
|
template(e) {
|
|
506
521
|
let t = [];
|
|
507
|
-
|
|
522
|
+
F(e.blocks, t);
|
|
508
523
|
let n = t.filter((e) => e.level === 1);
|
|
509
524
|
return n.length <= 1 ? [] : n.slice(1).map((e) => ({ blockId: e.id }));
|
|
510
525
|
}
|
|
511
|
-
},
|
|
526
|
+
}, Ne = {
|
|
512
527
|
id: "a11y.link-empty",
|
|
513
528
|
severity: "error"
|
|
514
529
|
};
|
|
515
|
-
function
|
|
530
|
+
function Pe(e) {
|
|
516
531
|
return a(e) || c(e) ? e.content : null;
|
|
517
532
|
}
|
|
518
|
-
var
|
|
519
|
-
meta:
|
|
533
|
+
var Fe = {
|
|
534
|
+
meta: Ne,
|
|
520
535
|
block(e) {
|
|
521
|
-
let t =
|
|
536
|
+
let t = Pe(e);
|
|
522
537
|
return t === null || !M(t).find((e) => e.text === "" && !e.hasImageWithAlt) ? null : { blockId: e.id };
|
|
523
538
|
}
|
|
524
|
-
},
|
|
539
|
+
}, Ie = {
|
|
525
540
|
id: "a11y.link-vague-text",
|
|
526
541
|
severity: "warning"
|
|
527
542
|
};
|
|
528
|
-
function
|
|
543
|
+
function Le(e) {
|
|
529
544
|
return a(e) || c(e) ? e.content : null;
|
|
530
545
|
}
|
|
531
|
-
var
|
|
532
|
-
meta:
|
|
546
|
+
var Re = {
|
|
547
|
+
meta: Ie,
|
|
533
548
|
block(e, t, n) {
|
|
534
|
-
let r =
|
|
549
|
+
let r = Le(e);
|
|
535
550
|
if (r === null) return null;
|
|
536
551
|
let i = k(n.locale).vagueLinkText, a = M(r).find((e) => {
|
|
537
552
|
let t = j(e.text);
|
|
@@ -542,53 +557,53 @@ var Ie = {
|
|
|
542
557
|
params: { text: a.text }
|
|
543
558
|
} : null;
|
|
544
559
|
}
|
|
545
|
-
},
|
|
560
|
+
}, ze = {
|
|
546
561
|
id: "a11y.link-href-empty",
|
|
547
562
|
severity: "error"
|
|
548
563
|
};
|
|
549
|
-
function
|
|
564
|
+
function Be(e) {
|
|
550
565
|
return a(e) || c(e) ? e.content : null;
|
|
551
566
|
}
|
|
552
|
-
var
|
|
553
|
-
meta:
|
|
567
|
+
var Ve = {
|
|
568
|
+
meta: ze,
|
|
554
569
|
block(e) {
|
|
555
|
-
let t =
|
|
570
|
+
let t = Be(e);
|
|
556
571
|
return t === null || !M(t).find((e) => {
|
|
557
572
|
let t = e.href.trim();
|
|
558
573
|
return t === "" || t === "#";
|
|
559
574
|
}) ? null : { blockId: e.id };
|
|
560
575
|
}
|
|
561
|
-
},
|
|
576
|
+
}, He = {
|
|
562
577
|
id: "a11y.link-target-blank-no-rel",
|
|
563
578
|
severity: "warning"
|
|
564
579
|
};
|
|
565
|
-
function
|
|
580
|
+
function Ue(e) {
|
|
566
581
|
return a(e) || c(e) ? e.content : null;
|
|
567
582
|
}
|
|
568
|
-
function
|
|
583
|
+
function We(e) {
|
|
569
584
|
if (e === null) return !1;
|
|
570
585
|
let t = e.toLowerCase().split(/\s+/);
|
|
571
586
|
return t.includes("noopener") || t.includes("noreferrer");
|
|
572
587
|
}
|
|
573
|
-
var
|
|
574
|
-
meta:
|
|
588
|
+
var Ge = {
|
|
589
|
+
meta: He,
|
|
575
590
|
block(e) {
|
|
576
|
-
let t =
|
|
577
|
-
return t === null || !M(t).find((e) => e.target === "_blank" && !
|
|
591
|
+
let t = Ue(e);
|
|
592
|
+
return t === null || !M(t).find((e) => e.target === "_blank" && !We(e.rel)) ? null : {
|
|
578
593
|
blockId: e.id,
|
|
579
594
|
fix: {
|
|
580
595
|
description: "Add rel=\"noopener\"",
|
|
581
596
|
apply: (t) => {
|
|
582
597
|
if (!a(e) && !c(e)) return;
|
|
583
|
-
let n =
|
|
598
|
+
let n = Je(e.content ?? "");
|
|
584
599
|
t.updateBlock(e.id, { content: n });
|
|
585
600
|
}
|
|
586
601
|
}
|
|
587
602
|
};
|
|
588
603
|
}
|
|
589
|
-
},
|
|
590
|
-
function
|
|
591
|
-
let t = [], n = new RegExp(
|
|
604
|
+
}, I = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
605
|
+
function Ke(e) {
|
|
606
|
+
let t = [], n = new RegExp(I.source, I.flags), r;
|
|
592
607
|
for (; (r = n.exec(e)) !== null;) {
|
|
593
608
|
let e = r[2] ?? r[3] ?? r[4] ?? null;
|
|
594
609
|
t.push({
|
|
@@ -600,13 +615,13 @@ function We(e) {
|
|
|
600
615
|
}
|
|
601
616
|
return t;
|
|
602
617
|
}
|
|
603
|
-
function
|
|
618
|
+
function qe(e) {
|
|
604
619
|
return e.some((e) => e.name.toLowerCase() === "target" && e.value !== null && e.value.toLowerCase() === "_blank");
|
|
605
620
|
}
|
|
606
|
-
function
|
|
621
|
+
function Je(e) {
|
|
607
622
|
return e.replace(/<a\b([^>]*)>/gi, (e, t) => {
|
|
608
|
-
let n =
|
|
609
|
-
if (!
|
|
623
|
+
let n = Ke(t);
|
|
624
|
+
if (!qe(n)) return e;
|
|
610
625
|
let r = n.find((e) => e.name.toLowerCase() === "rel");
|
|
611
626
|
if (r) {
|
|
612
627
|
let n = (r.value ?? "").toLowerCase().split(/\s+/);
|
|
@@ -619,30 +634,30 @@ function Ke(e) {
|
|
|
619
634
|
}
|
|
620
635
|
//#endregion
|
|
621
636
|
//#region src/accessibility/rules/link-nested-anchor.ts
|
|
622
|
-
var
|
|
637
|
+
var Ye = {
|
|
623
638
|
id: "a11y.link-nested-anchor",
|
|
624
639
|
severity: "error"
|
|
625
640
|
};
|
|
626
|
-
function
|
|
641
|
+
function Xe(e) {
|
|
627
642
|
return a(e) || c(e) ? e.content : null;
|
|
628
643
|
}
|
|
629
|
-
var
|
|
630
|
-
meta:
|
|
644
|
+
var Ze = {
|
|
645
|
+
meta: Ye,
|
|
631
646
|
block(e) {
|
|
632
|
-
let t =
|
|
633
|
-
return t === null || !
|
|
647
|
+
let t = Xe(e);
|
|
648
|
+
return t === null || !Ee(t) ? null : { blockId: e.id };
|
|
634
649
|
}
|
|
635
|
-
},
|
|
650
|
+
}, Qe = {
|
|
636
651
|
meta: {
|
|
637
652
|
id: "a11y.text-all-caps",
|
|
638
653
|
severity: "warning"
|
|
639
654
|
},
|
|
640
655
|
block(e, t, n) {
|
|
641
656
|
if (!a(e) && !c(e)) return null;
|
|
642
|
-
let r =
|
|
657
|
+
let r = N(e.content ?? "").replace(/[^\p{L}]/gu, "");
|
|
643
658
|
return r.length < n.thresholds.allCapsMinLength || r !== r.toLocaleUpperCase() ? null : { blockId: e.id };
|
|
644
659
|
}
|
|
645
|
-
},
|
|
660
|
+
}, $e = {
|
|
646
661
|
meta: {
|
|
647
662
|
id: "a11y.text-low-contrast",
|
|
648
663
|
severity: "error"
|
|
@@ -658,35 +673,35 @@ var Ye = {
|
|
|
658
673
|
}
|
|
659
674
|
};
|
|
660
675
|
}
|
|
661
|
-
},
|
|
676
|
+
}, et = {
|
|
662
677
|
id: "a11y.text-too-small",
|
|
663
678
|
severity: "warning"
|
|
664
679
|
};
|
|
665
|
-
function
|
|
680
|
+
function tt(e) {
|
|
666
681
|
return i(e) || ee(e) ? e.fontSize : null;
|
|
667
682
|
}
|
|
668
683
|
//#endregion
|
|
669
684
|
//#region src/accessibility/index.ts
|
|
670
|
-
var
|
|
685
|
+
var L = [
|
|
671
686
|
pe,
|
|
672
687
|
ge,
|
|
673
688
|
_e,
|
|
674
689
|
ve,
|
|
675
690
|
Te,
|
|
676
|
-
Ee,
|
|
677
691
|
Oe,
|
|
678
692
|
Ae,
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
Xe,
|
|
693
|
+
Me,
|
|
694
|
+
Fe,
|
|
695
|
+
Re,
|
|
696
|
+
Ve,
|
|
697
|
+
Ge,
|
|
685
698
|
Ze,
|
|
699
|
+
Qe,
|
|
700
|
+
$e,
|
|
686
701
|
{
|
|
687
|
-
meta:
|
|
702
|
+
meta: et,
|
|
688
703
|
block(e, t, n) {
|
|
689
|
-
let r =
|
|
704
|
+
let r = tt(e);
|
|
690
705
|
return r === null || r >= n.thresholds.minFontSize ? null : {
|
|
691
706
|
blockId: e.id,
|
|
692
707
|
params: {
|
|
@@ -758,41 +773,41 @@ var R = [
|
|
|
758
773
|
}
|
|
759
774
|
}
|
|
760
775
|
];
|
|
761
|
-
function
|
|
776
|
+
function nt(e, t = {}) {
|
|
762
777
|
if (t.disabled === !0 || t.accessibility === !1) return [];
|
|
763
778
|
let n = t.accessibility ?? {};
|
|
764
|
-
return b(e,
|
|
779
|
+
return b(e, L, oe(t.locale, n, L), (e, t, n) => E(e, t, n));
|
|
765
780
|
}
|
|
766
781
|
//#endregion
|
|
767
782
|
//#region src/structure/messages/de.ts
|
|
768
|
-
var
|
|
783
|
+
var rt = /* @__PURE__ */ d({ default: () => it }), it = {
|
|
769
784
|
"structure.duplicate-block-id": "Block-ID erscheint {count}-mal im Baum. Jeder Block muss eine eindeutige ID haben.",
|
|
770
785
|
"structure.section-column-mismatch": "Sektion verwendet Layout „{layout}\" (erwartet {expected} Spalten), hat aber {actual}. Deutet auf beschädigten Zustand hin.",
|
|
771
786
|
"structure.nested-section": "Sektion ist in einer anderen Sektion verschachtelt. Sektionen können nicht verschachtelt werden – der Renderer wird sich falsch verhalten.",
|
|
772
787
|
"structure.empty-section": "Sektion enthält keine Blöcke. Entferne sie oder füge Inhalt hinzu.",
|
|
773
788
|
"structure.empty-column": "Spalte {columnIndex} dieser Sektion ist leer. Füge Inhalt hinzu oder reduziere die Spaltenanzahl."
|
|
774
|
-
},
|
|
789
|
+
}, at = /* @__PURE__ */ d({ default: () => R }), R = {
|
|
775
790
|
"structure.duplicate-block-id": "Block id appears {count} times in the tree. Each block must have a unique id.",
|
|
776
791
|
"structure.section-column-mismatch": "Section uses layout \"{layout}\" (expects {expected} columns) but has {actual}. Indicates corrupted state.",
|
|
777
792
|
"structure.nested-section": "Section is nested inside another section. Sections cannot nest — the renderer will misbehave.",
|
|
778
793
|
"structure.empty-section": "Section has no blocks. Remove it or add content.",
|
|
779
794
|
"structure.empty-column": "Column {columnIndex} of this section is empty. Add content or reduce the column count."
|
|
780
|
-
},
|
|
781
|
-
"./de.ts":
|
|
782
|
-
"./en.ts":
|
|
783
|
-
}),
|
|
784
|
-
for (let e in
|
|
795
|
+
}, z = /* @__PURE__ */ Object.assign({
|
|
796
|
+
"./de.ts": rt,
|
|
797
|
+
"./en.ts": at
|
|
798
|
+
}), B = {};
|
|
799
|
+
for (let e in z) {
|
|
785
800
|
let t = /\.\/([^/]+)\.ts$/.exec(e);
|
|
786
801
|
if (!t) continue;
|
|
787
802
|
let n = t[1];
|
|
788
|
-
n !== "index" && (
|
|
803
|
+
n !== "index" && (B[n] = z[e].default);
|
|
789
804
|
}
|
|
790
|
-
var
|
|
791
|
-
function
|
|
792
|
-
return
|
|
805
|
+
var ot = Object.keys(B);
|
|
806
|
+
function V(e) {
|
|
807
|
+
return B[e.split("-")[0]?.toLowerCase() ?? "en"] ?? B.en ?? R;
|
|
793
808
|
}
|
|
794
|
-
function
|
|
795
|
-
let r =
|
|
809
|
+
function H(e, t, n) {
|
|
810
|
+
let r = V(e)[t] ?? R[t];
|
|
796
811
|
return n ? r.replace(/\{(\w+)\}/g, (e, t) => {
|
|
797
812
|
let r = n[t];
|
|
798
813
|
return r === void 0 ? `{${t}}` : String(r);
|
|
@@ -800,18 +815,18 @@ function U(e, t, n) {
|
|
|
800
815
|
}
|
|
801
816
|
//#endregion
|
|
802
817
|
//#region src/structure/rules/duplicate-block-id.ts
|
|
803
|
-
var
|
|
818
|
+
var st = {
|
|
804
819
|
id: "structure.duplicate-block-id",
|
|
805
820
|
severity: "error"
|
|
806
821
|
};
|
|
807
|
-
function
|
|
808
|
-
for (let n of e) if (t.set(n.id, (t.get(n.id) ?? 0) + 1), o(n)) for (let e of n.children)
|
|
822
|
+
function U(e, t) {
|
|
823
|
+
for (let n of e) if (t.set(n.id, (t.get(n.id) ?? 0) + 1), o(n)) for (let e of n.children) U(e, t);
|
|
809
824
|
}
|
|
810
|
-
var
|
|
811
|
-
meta:
|
|
825
|
+
var ct = {
|
|
826
|
+
meta: st,
|
|
812
827
|
template(e) {
|
|
813
828
|
let t = /* @__PURE__ */ new Map();
|
|
814
|
-
|
|
829
|
+
U(e.blocks, t);
|
|
815
830
|
let n = [];
|
|
816
831
|
for (let [e, r] of t) r > 1 && n.push({
|
|
817
832
|
blockId: e,
|
|
@@ -819,11 +834,11 @@ var ot = {
|
|
|
819
834
|
});
|
|
820
835
|
return n;
|
|
821
836
|
}
|
|
822
|
-
},
|
|
837
|
+
}, lt = {
|
|
823
838
|
id: "structure.empty-column",
|
|
824
839
|
severity: "warning"
|
|
825
840
|
};
|
|
826
|
-
function
|
|
841
|
+
function W(e, t) {
|
|
827
842
|
for (let n of e) {
|
|
828
843
|
if (!o(n)) continue;
|
|
829
844
|
let e = n;
|
|
@@ -833,28 +848,28 @@ function G(e, t) {
|
|
|
833
848
|
params: { columnIndex: r + 1 }
|
|
834
849
|
});
|
|
835
850
|
});
|
|
836
|
-
for (let n of e.children)
|
|
851
|
+
for (let n of e.children) W(n, t);
|
|
837
852
|
}
|
|
838
853
|
}
|
|
839
|
-
var
|
|
840
|
-
meta:
|
|
854
|
+
var ut = {
|
|
855
|
+
meta: lt,
|
|
841
856
|
template(e) {
|
|
842
857
|
let t = [];
|
|
843
|
-
return
|
|
858
|
+
return W(e.blocks, t), t;
|
|
844
859
|
}
|
|
845
|
-
},
|
|
860
|
+
}, dt = {
|
|
846
861
|
id: "structure.empty-section",
|
|
847
862
|
severity: "warning"
|
|
848
863
|
};
|
|
849
|
-
function
|
|
864
|
+
function ft(e) {
|
|
850
865
|
return e.children.length === 0 ? !0 : e.children.every((e) => e.length === 0);
|
|
851
866
|
}
|
|
852
|
-
var
|
|
853
|
-
meta:
|
|
867
|
+
var pt = {
|
|
868
|
+
meta: dt,
|
|
854
869
|
block(e) {
|
|
855
870
|
if (!o(e)) return null;
|
|
856
871
|
let t = e;
|
|
857
|
-
return
|
|
872
|
+
return ft(t) ? {
|
|
858
873
|
blockId: t.id,
|
|
859
874
|
fix: {
|
|
860
875
|
description: "Remove the empty section",
|
|
@@ -864,7 +879,7 @@ var dt = {
|
|
|
864
879
|
}
|
|
865
880
|
} : null;
|
|
866
881
|
}
|
|
867
|
-
},
|
|
882
|
+
}, mt = {
|
|
868
883
|
meta: {
|
|
869
884
|
id: "structure.nested-section",
|
|
870
885
|
severity: "error"
|
|
@@ -877,25 +892,25 @@ var dt = {
|
|
|
877
892
|
params: { parentId: n.id }
|
|
878
893
|
};
|
|
879
894
|
}
|
|
880
|
-
},
|
|
895
|
+
}, ht = {
|
|
881
896
|
id: "structure.section-column-mismatch",
|
|
882
897
|
severity: "error"
|
|
883
898
|
};
|
|
884
|
-
function
|
|
899
|
+
function gt(e) {
|
|
885
900
|
return e === "1" ? 1 : e === "3" ? 3 : 2;
|
|
886
901
|
}
|
|
887
902
|
//#endregion
|
|
888
903
|
//#region src/structure/index.ts
|
|
889
|
-
var
|
|
890
|
-
ot,
|
|
891
|
-
dt,
|
|
904
|
+
var G = [
|
|
892
905
|
ct,
|
|
893
|
-
|
|
906
|
+
pt,
|
|
907
|
+
ut,
|
|
908
|
+
mt,
|
|
894
909
|
{
|
|
895
|
-
meta:
|
|
910
|
+
meta: ht,
|
|
896
911
|
block(e) {
|
|
897
912
|
if (!o(e)) return null;
|
|
898
|
-
let t = e, n =
|
|
913
|
+
let t = e, n = gt(t.columns), r = t.children.length;
|
|
899
914
|
return r === n ? null : {
|
|
900
915
|
blockId: t.id,
|
|
901
916
|
params: {
|
|
@@ -907,41 +922,41 @@ var K = [
|
|
|
907
922
|
}
|
|
908
923
|
}
|
|
909
924
|
];
|
|
910
|
-
function
|
|
925
|
+
function _t(e, t = {}) {
|
|
911
926
|
if (t.disabled === !0 || t.structure === !1) return [];
|
|
912
927
|
let n = t.structure ?? {};
|
|
913
|
-
return b(e,
|
|
928
|
+
return b(e, G, se(t.locale, n, G), (e, t, n) => H(e, t, n));
|
|
914
929
|
}
|
|
915
930
|
//#endregion
|
|
916
931
|
//#region src/links/messages/de.ts
|
|
917
|
-
var
|
|
918
|
-
"link.javascript-protocol": "Die URL verwendet das „
|
|
932
|
+
var vt = /* @__PURE__ */ d({ default: () => yt }), yt = {
|
|
933
|
+
"link.javascript-protocol": "Die URL verwendet das Protokoll „{protocol}:\", das beliebigen Skriptcode ausführen kann und aus Sicherheitsgründen beim Rendern entfernt wird. Ersetze sie durch eine echte URL oder entferne sie.",
|
|
919
934
|
"link.unsupported-protocol": "Die URL verwendet das Protokoll „{protocol}\", das von den meisten E-Mail-Clients nicht unterstützt wird. Verwende http, https, mailto, tel oder sms.",
|
|
920
935
|
"link.malformed-mailto": "Der mailto:-Link ist fehlerhaft. Erwartet wird eine einzelne Empfängeradresse vor einer eventuellen Querystring (z. B. mailto:hallo@example.com).",
|
|
921
936
|
"link.malformed-tel": "Der tel:-Link enthält Zeichen, die keine Ziffern, +, Leerzeichen, Bindestriche, Klammern oder Punkte sind.",
|
|
922
937
|
"link.localhost-or-staging": "Der URL-Host „{host}\" entspricht einem Nicht-Produktionsmuster. Ersetze ihn vor dem Versand durch die Produktions-URL."
|
|
923
|
-
},
|
|
924
|
-
"link.javascript-protocol": "URL uses the \"
|
|
938
|
+
}, bt = /* @__PURE__ */ d({ default: () => K }), K = {
|
|
939
|
+
"link.javascript-protocol": "URL uses the \"{protocol}:\" protocol, which can execute arbitrary script and is stripped at render time for safety. Replace it with a real link or remove the URL.",
|
|
925
940
|
"link.unsupported-protocol": "URL uses the \"{protocol}\" protocol, which most email clients do not support. Use http, https, mailto, tel, or sms.",
|
|
926
941
|
"link.malformed-mailto": "mailto: link is malformed. Expected a single recipient address before any query string (e.g. mailto:hello@example.com).",
|
|
927
942
|
"link.malformed-tel": "tel: link contains characters that are not digits, +, spaces, dashes, parentheses, or dots.",
|
|
928
943
|
"link.localhost-or-staging": "URL host \"{host}\" matches a non-production pattern. Replace with the production URL before sending."
|
|
929
|
-
},
|
|
930
|
-
"./de.ts":
|
|
931
|
-
"./en.ts":
|
|
932
|
-
}),
|
|
933
|
-
for (let e in
|
|
944
|
+
}, q = /* @__PURE__ */ Object.assign({
|
|
945
|
+
"./de.ts": vt,
|
|
946
|
+
"./en.ts": bt
|
|
947
|
+
}), J = {};
|
|
948
|
+
for (let e in q) {
|
|
934
949
|
let t = /\.\/([^/]+)\.ts$/.exec(e);
|
|
935
950
|
if (!t) continue;
|
|
936
951
|
let n = t[1];
|
|
937
|
-
n !== "index" && (
|
|
952
|
+
n !== "index" && (J[n] = q[e].default);
|
|
938
953
|
}
|
|
939
|
-
var
|
|
940
|
-
function
|
|
941
|
-
return
|
|
954
|
+
var xt = Object.keys(J);
|
|
955
|
+
function Y(e) {
|
|
956
|
+
return J[e.split("-")[0]?.toLowerCase() ?? "en"] ?? J.en ?? K;
|
|
942
957
|
}
|
|
943
|
-
function
|
|
944
|
-
let r =
|
|
958
|
+
function X(e, t, n) {
|
|
959
|
+
let r = Y(e)[t] ?? K[t];
|
|
945
960
|
return n ? r.replace(/\{(\w+)\}/g, (e, t) => {
|
|
946
961
|
let r = n[t];
|
|
947
962
|
return r === void 0 ? `{${t}}` : String(r);
|
|
@@ -949,7 +964,7 @@ function Z(e, t, n) {
|
|
|
949
964
|
}
|
|
950
965
|
//#endregion
|
|
951
966
|
//#region src/url-walker.ts
|
|
952
|
-
function
|
|
967
|
+
function Z(e) {
|
|
953
968
|
let o = [];
|
|
954
969
|
return y(e, (e) => {
|
|
955
970
|
if (c(e) || a(e) || n(e)) {
|
|
@@ -1010,55 +1025,66 @@ function Q(e) {
|
|
|
1010
1025
|
}
|
|
1011
1026
|
//#endregion
|
|
1012
1027
|
//#region src/links/rules/javascript-protocol.ts
|
|
1013
|
-
var
|
|
1028
|
+
var St = {
|
|
1014
1029
|
id: "link.javascript-protocol",
|
|
1015
1030
|
severity: "error"
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1031
|
+
}, Q = [
|
|
1032
|
+
"javascript",
|
|
1033
|
+
"data",
|
|
1034
|
+
"vbscript"
|
|
1035
|
+
];
|
|
1036
|
+
function Ct(e) {
|
|
1037
|
+
if (!e) return null;
|
|
1019
1038
|
let t = e.replace(/\s+/g, "");
|
|
1020
|
-
|
|
1039
|
+
for (let e of Q) if (RegExp(`^${e}:`, "i").test(t)) return e;
|
|
1040
|
+
return null;
|
|
1021
1041
|
}
|
|
1022
|
-
var
|
|
1023
|
-
meta:
|
|
1042
|
+
var wt = {
|
|
1043
|
+
meta: St,
|
|
1024
1044
|
template(e) {
|
|
1025
1045
|
let t = [];
|
|
1026
|
-
for (let n of
|
|
1046
|
+
for (let n of Z(e)) {
|
|
1047
|
+
let e = Ct(n.url);
|
|
1048
|
+
e !== null && t.push({
|
|
1049
|
+
blockId: n.blockId,
|
|
1050
|
+
params: { protocol: e }
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1027
1053
|
return t;
|
|
1028
1054
|
}
|
|
1029
|
-
},
|
|
1055
|
+
}, Tt = {
|
|
1030
1056
|
id: "link.unsupported-protocol",
|
|
1031
1057
|
severity: "warning"
|
|
1032
|
-
},
|
|
1058
|
+
}, Et = new Set([
|
|
1033
1059
|
"http",
|
|
1034
1060
|
"https",
|
|
1035
1061
|
"mailto",
|
|
1036
1062
|
"tel",
|
|
1037
1063
|
"sms"
|
|
1038
|
-
]);
|
|
1039
|
-
function
|
|
1064
|
+
]), Dt = new Set(Q);
|
|
1065
|
+
function Ot(e) {
|
|
1040
1066
|
if (!e) return null;
|
|
1041
1067
|
let t = e.trim(), n = /^([a-z][a-z0-9+\-.]*):/i.exec(t);
|
|
1042
1068
|
return n ? n[1].toLowerCase() : null;
|
|
1043
1069
|
}
|
|
1044
|
-
var
|
|
1045
|
-
meta:
|
|
1070
|
+
var kt = {
|
|
1071
|
+
meta: Tt,
|
|
1046
1072
|
template(e) {
|
|
1047
1073
|
let t = [];
|
|
1048
|
-
for (let n of
|
|
1049
|
-
let e =
|
|
1050
|
-
e !== null && e
|
|
1074
|
+
for (let n of Z(e)) {
|
|
1075
|
+
let e = Ot(n.url);
|
|
1076
|
+
e !== null && (Dt.has(e) || Et.has(e) || t.push({
|
|
1051
1077
|
blockId: n.blockId,
|
|
1052
1078
|
params: { protocol: e }
|
|
1053
1079
|
}));
|
|
1054
1080
|
}
|
|
1055
1081
|
return t;
|
|
1056
1082
|
}
|
|
1057
|
-
},
|
|
1083
|
+
}, At = {
|
|
1058
1084
|
id: "link.malformed-mailto",
|
|
1059
1085
|
severity: "warning"
|
|
1060
1086
|
};
|
|
1061
|
-
function
|
|
1087
|
+
function jt(e) {
|
|
1062
1088
|
let t = e.trim();
|
|
1063
1089
|
if (!/^mailto:/i.test(t)) return !1;
|
|
1064
1090
|
let [n] = t.slice(7).split("?", 2);
|
|
@@ -1073,41 +1099,41 @@ function Ot(e) {
|
|
|
1073
1099
|
}
|
|
1074
1100
|
return !1;
|
|
1075
1101
|
}
|
|
1076
|
-
var
|
|
1077
|
-
meta:
|
|
1102
|
+
var Mt = {
|
|
1103
|
+
meta: At,
|
|
1078
1104
|
template(e) {
|
|
1079
1105
|
let t = [];
|
|
1080
|
-
for (let n of
|
|
1106
|
+
for (let n of Z(e)) jt(n.url) && t.push({ blockId: n.blockId });
|
|
1081
1107
|
return t;
|
|
1082
1108
|
}
|
|
1083
|
-
},
|
|
1109
|
+
}, Nt = {
|
|
1084
1110
|
id: "link.malformed-tel",
|
|
1085
1111
|
severity: "warning"
|
|
1086
|
-
},
|
|
1087
|
-
function
|
|
1112
|
+
}, Pt = /^[+0-9\s().\-]+$/, Ft = /^[A-Za-z0-9-]+(=[^;]+)?$/;
|
|
1113
|
+
function It(e) {
|
|
1088
1114
|
let t = e.trim();
|
|
1089
1115
|
if (!/^tel:/i.test(t)) return !1;
|
|
1090
1116
|
let n = t.slice(4).trim();
|
|
1091
1117
|
if (n === "") return !0;
|
|
1092
1118
|
let [r, ...i] = n.split(";");
|
|
1093
|
-
return
|
|
1119
|
+
return Pt.test(r) ? i.some((e) => !Ft.test(e)) : !0;
|
|
1094
1120
|
}
|
|
1095
|
-
var
|
|
1096
|
-
meta:
|
|
1121
|
+
var Lt = {
|
|
1122
|
+
meta: Nt,
|
|
1097
1123
|
template(e) {
|
|
1098
1124
|
let t = [];
|
|
1099
|
-
for (let n of
|
|
1125
|
+
for (let n of Z(e)) It(n.url) && t.push({ blockId: n.blockId });
|
|
1100
1126
|
return t;
|
|
1101
1127
|
}
|
|
1102
|
-
},
|
|
1128
|
+
}, Rt = {
|
|
1103
1129
|
id: "link.localhost-or-staging",
|
|
1104
1130
|
severity: "warning"
|
|
1105
1131
|
};
|
|
1106
|
-
function
|
|
1132
|
+
function zt(e) {
|
|
1107
1133
|
let t = e.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1108
1134
|
return RegExp(`^${t}$`, "i");
|
|
1109
1135
|
}
|
|
1110
|
-
function
|
|
1136
|
+
function Bt(e) {
|
|
1111
1137
|
if (!e) return null;
|
|
1112
1138
|
let t = e.trim();
|
|
1113
1139
|
if (!/^(https?|ftps?):\/\//i.test(t)) return null;
|
|
@@ -1120,18 +1146,18 @@ function Lt(e) {
|
|
|
1120
1146
|
//#endregion
|
|
1121
1147
|
//#region src/links/index.ts
|
|
1122
1148
|
var $ = [
|
|
1123
|
-
|
|
1124
|
-
Et,
|
|
1149
|
+
wt,
|
|
1125
1150
|
kt,
|
|
1126
|
-
|
|
1151
|
+
Mt,
|
|
1152
|
+
Lt,
|
|
1127
1153
|
{
|
|
1128
|
-
meta:
|
|
1154
|
+
meta: Rt,
|
|
1129
1155
|
template(e, t) {
|
|
1130
1156
|
let n = t.links.nonProductionHosts;
|
|
1131
1157
|
if (n.length === 0) return [];
|
|
1132
|
-
let r = n.map(
|
|
1133
|
-
for (let t of
|
|
1134
|
-
let e =
|
|
1158
|
+
let r = n.map(zt), i = [];
|
|
1159
|
+
for (let t of Z(e)) {
|
|
1160
|
+
let e = Bt(t.url);
|
|
1135
1161
|
e !== null && r.some((t) => t.test(e)) && i.push({
|
|
1136
1162
|
blockId: t.blockId,
|
|
1137
1163
|
params: { host: e }
|
|
@@ -1141,17 +1167,17 @@ var $ = [
|
|
|
1141
1167
|
}
|
|
1142
1168
|
}
|
|
1143
1169
|
];
|
|
1144
|
-
function
|
|
1170
|
+
function Vt(e, t = {}) {
|
|
1145
1171
|
if (t.disabled === !0 || t.links === !1) return [];
|
|
1146
1172
|
let n = t.links ?? {};
|
|
1147
|
-
return b(e, $, ce(t.locale, n, $), (e, t, n) =>
|
|
1173
|
+
return b(e, $, ce(t.locale, n, $), (e, t, n) => X(e, t, n));
|
|
1148
1174
|
}
|
|
1149
1175
|
//#endregion
|
|
1150
1176
|
//#region src/util.ts
|
|
1151
|
-
function
|
|
1177
|
+
function Ht(e) {
|
|
1152
1178
|
return e ? e.disabled === !0 ? !0 : e.accessibility === !1 && e.structure === !1 && e.links === !1 : !1;
|
|
1153
1179
|
}
|
|
1154
1180
|
//#endregion
|
|
1155
|
-
export {
|
|
1181
|
+
export { L as ACCESSIBILITY_RULES, f as DEFAULT_A11Y_THRESHOLDS, p as DEFAULT_NON_PRODUCTION_HOSTS, $ as LINK_RULES, G as STRUCTURE_RULES, we as SUPPORTED_DICTIONARY_LOCALES, xt as SUPPORTED_LINK_MESSAGE_LOCALES, fe as SUPPORTED_MESSAGE_LOCALES, ot as SUPPORTED_STRUCTURE_MESSAGE_LOCALES, M as extractAnchors, N as extractText, X as formatLinkMessage, E as formatMessage, H as formatStructureMessage, m as getContrastRatio, k as getDictionary, Y as getLinkMessages, T as getMessages, V as getStructureMessages, Ee as hasNestedAnchors, Ht as isLintFullyDisabled, g as isOpaqueHex, nt as lintAccessibility, Vt as lintLinks, _t as lintStructure, h as parseHex, y as walkBlocks, Z as walkUrls };
|
|
1156
1182
|
|
|
1157
1183
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/types.ts","../src/contrast.ts","../src/walk.ts","../src/run-rules.ts","../src/accessibility/messages/de.ts","../src/accessibility/messages/en.ts","../src/accessibility/messages/index.ts","../src/accessibility/rules/img-missing-alt.ts","../src/accessibility/rules/img-alt-is-filename.ts","../src/accessibility/rules/img-alt-too-long.ts","../src/accessibility/rules/img-decorative-needs-empty-alt.ts","../src/accessibility/dictionaries/de.ts","../src/accessibility/dictionaries/en.ts","../src/accessibility/dictionaries/index.ts","../src/accessibility/rules/img-linked-no-context.ts","../src/html-utils.ts","../src/accessibility/rules/heading-empty.ts","../src/accessibility/rules/heading-skip-level.ts","../src/accessibility/rules/heading-multiple-h1.ts","../src/accessibility/rules/link-empty.ts","../src/accessibility/rules/link-vague-text.ts","../src/accessibility/rules/link-href-empty.ts","../src/accessibility/rules/link-target-blank-no-rel.ts","../src/accessibility/rules/link-nested-anchor.ts","../src/accessibility/rules/text-all-caps.ts","../src/accessibility/rules/text-low-contrast.ts","../src/accessibility/rules/text-too-small.ts","../src/accessibility/rules/button-vague-label.ts","../src/accessibility/rules/button-touch-target.ts","../src/accessibility/rules/button-low-contrast.ts","../src/accessibility/rules/missing-preheader.ts","../src/accessibility/index.ts","../src/structure/messages/de.ts","../src/structure/messages/en.ts","../src/structure/messages/index.ts","../src/structure/rules/duplicate-block-id.ts","../src/structure/rules/empty-column.ts","../src/structure/rules/empty-section.ts","../src/structure/rules/nested-section.ts","../src/structure/rules/section-column-mismatch.ts","../src/structure/index.ts","../src/links/messages/de.ts","../src/links/messages/en.ts","../src/links/messages/index.ts","../src/url-walker.ts","../src/links/rules/javascript-protocol.ts","../src/links/rules/unsupported-protocol.ts","../src/links/rules/malformed-mailto.ts","../src/links/rules/malformed-tel.ts","../src/links/rules/localhost-or-staging.ts","../src/links/index.ts","../src/util.ts"],"sourcesContent":["import type {\n Block,\n SectionBlock,\n TemplateContent,\n TemplateSettings,\n} from \"@templatical/types\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\" | \"off\";\n\nexport interface LintIssue {\n /** Block id, or null for template-level issues. */\n blockId: string | null;\n ruleId: string;\n severity: Exclude<Severity, \"off\">;\n message: string;\n fix?: LintPatch;\n}\n\nexport interface LintPatchContext {\n updateBlock: (blockId: string, patch: Partial<Block>) => void;\n updateSettings: (patch: Partial<TemplateSettings>) => void;\n removeBlock: (blockId: string) => void;\n}\n\nexport interface LintPatch {\n description: string;\n apply: (ctx: LintPatchContext) => void;\n}\n\nexport interface LintThresholds {\n altMaxLength: number;\n minFontSize: number;\n allCapsMinLength: number;\n minTouchTargetPx: number;\n}\n\n/**\n * Per-rule severity override. Set a rule to `'off'` to disable it.\n * Keys are the full prefixed rule IDs (`a11y.*`, `structure.*`, `link.*`)\n * so a value copied from `LintIssue.ruleId` pastes straight in.\n */\nexport type RuleOverrides = Record<string, Severity>;\n\n/** Options consumed only by the accessibility linter. */\nexport interface AccessibilityLintOptions {\n rules?: RuleOverrides;\n thresholds?: Partial<LintThresholds>;\n}\n\n/** Options consumed only by the structure linter. */\nexport interface StructureLintOptions {\n rules?: RuleOverrides;\n}\n\n/** Options consumed only by the links linter. */\nexport interface LinksLintOptions {\n rules?: RuleOverrides;\n /**\n * Host patterns that should flag as \"staging / non-production\".\n * Each entry is a glob-style pattern matched against the URL host.\n * `*` matches any run of characters (including `.`), so `*.staging.*`\n * matches `app.staging.example.com`.\n *\n * Default: ['localhost', '127.0.0.1', '0.0.0.0', '*.local',\n * '*.staging.*', '*.dev.*']\n */\n nonProductionHosts?: string[];\n}\n\nexport interface LintOptions {\n /**\n * Fully disable linting. When true, the editor skips lazy-loading the\n * package, hides the sidebar tab, and suppresses inline badges.\n */\n disabled?: boolean;\n /** Locale for vague-text dictionaries and message text. Falls back to `en`. */\n locale?: string;\n /**\n * Accessibility linter config. Set to `false` to disable the whole\n * `lintAccessibility` linter without enumerating its rules.\n */\n accessibility?: false | AccessibilityLintOptions;\n /**\n * Structure linter config. Set to `false` to disable the whole\n * `lintStructure` linter without enumerating its rules.\n */\n structure?: false | StructureLintOptions;\n /**\n * Links linter config. Set to `false` to disable the whole `lintLinks`\n * linter without enumerating its rules.\n */\n links?: false | LinksLintOptions;\n}\n\nexport interface ResolvedLinksOptions {\n nonProductionHosts: string[];\n}\n\nexport interface ResolvedOptions {\n locale: string;\n rules: RuleOverrides;\n thresholds: LintThresholds;\n links: ResolvedLinksOptions;\n /** Returns the effective severity for a rule (override or default). */\n severity: (ruleId: string) => Severity;\n}\n\nexport interface WalkContext {\n parent: Block | null;\n section: SectionBlock | null;\n columnIndex: number | null;\n depth: number;\n /**\n * Nearest opaque ancestor background, or template settings background.\n * Hex string, lowercased.\n */\n resolvedBackgroundColor: string;\n}\n\nexport interface RuleMeta {\n /** Stable identifier — used for severity overrides and message lookup. */\n id: string;\n /** Default severity when no override is supplied. */\n severity: Exclude<Severity, \"off\">;\n}\n\n/**\n * What a rule emits per match. The orchestrator combines this with the\n * rule's `meta` (for `ruleId` + default severity) and resolves the\n * localized message via the active locale's message map.\n */\nexport interface RuleHit {\n blockId: string | null;\n /** Interpolation values for the rule's localized message template. */\n params?: Record<string, string | number>;\n fix?: LintPatch;\n}\n\nexport interface Rule {\n meta: RuleMeta;\n /** Block-level rule. Returns a hit or null. */\n block?: (\n block: Block,\n ctx: WalkContext,\n opts: ResolvedOptions,\n ) => RuleHit | null;\n /** Template-level rule. Runs once after the walk. */\n template?: (content: TemplateContent, opts: ResolvedOptions) => RuleHit[];\n}\n\nexport const DEFAULT_A11Y_THRESHOLDS: LintThresholds = {\n altMaxLength: 125,\n minFontSize: 14,\n allCapsMinLength: 20,\n minTouchTargetPx: 44,\n};\n\nexport const DEFAULT_NON_PRODUCTION_HOSTS: string[] = [\n \"localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \"*.local\",\n \"*.staging.*\",\n \"*.dev.*\",\n];\n","/**\n * WCAG 2.1 sRGB relative-luminance contrast.\n *\n * Inputs are hex strings (`#rgb`, `#rrggbb`, optional leading `#`).\n * Returns the contrast ratio (1–21) per WCAG, or `NaN` if either input\n * cannot be parsed as an opaque solid hex color.\n *\n * The codebase uses OKLch for design tokens, but contrast math is\n * sRGB-defined; mixing the two gives incorrect results.\n */\nexport function getContrastRatio(fg: string, bg: string): number {\n const fgRgb = parseHex(fg);\n const bgRgb = parseHex(bg);\n\n if (!fgRgb || !bgRgb) {\n return Number.NaN;\n }\n\n const l1 = relativeLuminance(fgRgb);\n const l2 = relativeLuminance(bgRgb);\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n\n return (lighter + 0.05) / (darker + 0.05);\n}\n\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nconst HEX3 = /^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i;\nconst HEX6 = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;\nconst HEX8 = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;\n\nexport function parseHex(input: string | undefined | null): Rgb | null {\n if (typeof input !== \"string\") {\n return null;\n }\n\n const trimmed = input.trim();\n\n const match8 = HEX8.exec(trimmed);\n if (match8) {\n // Only treat fully-opaque (alpha = ff) as a valid RGB color; partial\n // alpha can't be flattened without knowing the underlay.\n if (match8[4].toLowerCase() !== \"ff\") return null;\n return {\n r: parseInt(match8[1], 16),\n g: parseInt(match8[2], 16),\n b: parseInt(match8[3], 16),\n };\n }\n\n const match6 = HEX6.exec(trimmed);\n if (match6) {\n return {\n r: parseInt(match6[1], 16),\n g: parseInt(match6[2], 16),\n b: parseInt(match6[3], 16),\n };\n }\n\n const match3 = HEX3.exec(trimmed);\n if (match3) {\n return {\n r: parseInt(match3[1] + match3[1], 16),\n g: parseInt(match3[2] + match3[2], 16),\n b: parseInt(match3[3] + match3[3], 16),\n };\n }\n\n return null;\n}\n\nexport function isOpaqueHex(input: string | undefined | null): boolean {\n return parseHex(input ?? \"\") !== null;\n}\n\nfunction relativeLuminance({ r, g, b }: Rgb): number {\n const rs = channel(r / 255);\n const gs = channel(g / 255);\n const bs = channel(b / 255);\n return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;\n}\n\nfunction channel(c: number): number {\n return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n}\n","import type { Block, TemplateContent } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { WalkContext } from \"./types\";\nimport { isOpaqueHex } from \"./contrast\";\n\nexport type Visitor = (block: Block, ctx: WalkContext) => void;\n\nconst DEFAULT_BG = \"#ffffff\";\n\n/**\n * Pure traversal of the block tree. Calls `visit` once per block in\n * document order, providing a `WalkContext` that includes the resolved\n * background color (nearest opaque ancestor) and structural refs.\n *\n * Sections cannot nest (renderer enforces this), so the walker doesn't\n * descend into a section that lives inside a column. Custom blocks are\n * visited but not descended into.\n */\nexport function walkBlocks(content: TemplateContent, visit: Visitor): void {\n const rootBg = isOpaqueHex(content.settings.backgroundColor)\n ? content.settings.backgroundColor.toLowerCase()\n : DEFAULT_BG;\n\n const walk = (block: Block, ctx: WalkContext): void => {\n // A block's own opaque backgroundColor is what's behind its content —\n // visit it with that resolved bg so contrast rules compare against the\n // right surface. Falls back to the inherited section/template bg.\n const ownBg = block.styles?.backgroundColor;\n const effectiveBg = isOpaqueHex(ownBg)\n ? (ownBg as string).toLowerCase()\n : ctx.resolvedBackgroundColor;\n const blockCtx: WalkContext =\n effectiveBg === ctx.resolvedBackgroundColor\n ? ctx\n : { ...ctx, resolvedBackgroundColor: effectiveBg };\n\n visit(block, blockCtx);\n\n if (!isSection(block)) {\n return;\n }\n\n block.children.forEach((column, columnIndex) => {\n column.forEach((child) =>\n walk(child, {\n parent: block,\n section: block,\n columnIndex,\n depth: ctx.depth + 1,\n resolvedBackgroundColor: effectiveBg,\n }),\n );\n });\n };\n\n for (const block of content.blocks) {\n walk(block, {\n parent: null,\n section: null,\n columnIndex: null,\n depth: 0,\n resolvedBackgroundColor: rootBg,\n });\n }\n}\n","import type { TemplateContent } from \"@templatical/types\";\nimport type {\n AccessibilityLintOptions,\n LinksLintOptions,\n LintIssue,\n ResolvedOptions,\n Rule,\n RuleHit,\n RuleOverrides,\n Severity,\n StructureLintOptions,\n} from \"./types\";\nimport { DEFAULT_A11Y_THRESHOLDS, DEFAULT_NON_PRODUCTION_HOSTS } from \"./types\";\nimport { walkBlocks } from \"./walk\";\n\nexport type MessageFormatter = (\n locale: string,\n ruleId: string,\n params?: Record<string, string | number>,\n) => string;\n\n/**\n * Walk the tree once, dispatch every block-level rule, then run every\n * template-level rule. Each tool (lintAccessibility, lintStructure, …)\n * wraps this with its own rule list + message formatter and a pre-built\n * `ResolvedOptions` containing that tool's overrides and tool-scoped config.\n */\nexport function runRules(\n content: TemplateContent,\n rules: Rule[],\n opts: ResolvedOptions,\n formatMessage: MessageFormatter,\n): LintIssue[] {\n const issues: LintIssue[] = [];\n\n function buildIssue(\n ruleId: string,\n severity: Exclude<Severity, \"off\">,\n hit: RuleHit,\n ): LintIssue {\n return {\n blockId: hit.blockId,\n ruleId,\n severity,\n message: formatMessage(opts.locale, ruleId, hit.params),\n fix: hit.fix,\n };\n }\n\n walkBlocks(content, (block, ctx) => {\n for (const rule of rules) {\n const sev = opts.severity(rule.meta.id);\n if (sev === \"off\" || !rule.block) continue;\n const hit = rule.block(block, ctx, opts);\n if (hit !== null) {\n issues.push(buildIssue(rule.meta.id, sev, hit));\n }\n }\n });\n\n for (const rule of rules) {\n const sev = opts.severity(rule.meta.id);\n if (sev === \"off\" || !rule.template) continue;\n const hits = rule.template(content, opts);\n for (const hit of hits) {\n issues.push(buildIssue(rule.meta.id, sev, hit));\n }\n }\n\n return issues;\n}\n\n/**\n * Build a `ResolvedOptions` for a given tool. Each tool wrapper passes its\n * own per-tool bag; fields not relevant to the tool fall back to defaults\n * (e.g. `lintStructure` still gets `thresholds` populated, but no structure\n * rule reads them).\n */\nexport function resolveOptions(args: {\n locale: string | undefined;\n rules: Rule[];\n overrides: RuleOverrides | undefined;\n thresholds: Partial<import(\"./types\").LintThresholds> | undefined;\n nonProductionHosts: string[] | undefined;\n}): ResolvedOptions {\n const overrides = args.overrides ?? {};\n const thresholds = {\n ...DEFAULT_A11Y_THRESHOLDS,\n ...(args.thresholds ?? {}),\n };\n const links = {\n nonProductionHosts: args.nonProductionHosts ?? DEFAULT_NON_PRODUCTION_HOSTS,\n };\n const locale = args.locale ?? \"en\";\n const rules = args.rules;\n\n return {\n locale,\n rules: overrides,\n thresholds,\n links,\n severity: (ruleId: string): Severity => {\n const override = overrides[ruleId];\n if (override !== undefined) {\n return override;\n }\n const rule = rules.find((r) => r.meta.id === ruleId);\n return rule?.meta.severity ?? \"warning\";\n },\n };\n}\n\n/**\n * Resolver for the accessibility linter — reads `options.accessibility`.\n */\nexport function resolveAccessibilityOptions(\n locale: string | undefined,\n tool: AccessibilityLintOptions,\n rules: Rule[],\n): ResolvedOptions {\n return resolveOptions({\n locale,\n rules,\n overrides: tool.rules,\n thresholds: tool.thresholds,\n nonProductionHosts: undefined,\n });\n}\n\n/**\n * Resolver for the structure linter — reads `options.structure`.\n */\nexport function resolveStructureOptions(\n locale: string | undefined,\n tool: StructureLintOptions,\n rules: Rule[],\n): ResolvedOptions {\n return resolveOptions({\n locale,\n rules,\n overrides: tool.rules,\n thresholds: undefined,\n nonProductionHosts: undefined,\n });\n}\n\n/**\n * Resolver for the links linter — reads `options.links`.\n */\nexport function resolveLinksOptions(\n locale: string | undefined,\n tool: LinksLintOptions,\n rules: Rule[],\n): ResolvedOptions {\n return resolveOptions({\n locale,\n rules,\n overrides: tool.rules,\n thresholds: undefined,\n nonProductionHosts: tool.nonProductionHosts,\n });\n}\n","import type en from \"./en\";\n\nconst de: typeof en = {\n \"a11y.img-missing-alt\":\n \"Bild ohne Alt-Text. Füge eine kurze Beschreibung hinzu oder markiere das Bild als dekorativ.\",\n \"a11y.img-alt-is-filename\":\n 'Alt-Text sieht wie ein Dateiname aus (\"{alt}\"). Beschreibe stattdessen kurz, was das Bild zeigt.',\n \"a11y.img-alt-too-long\":\n \"Alt-Text ist {length} Zeichen lang; bleibe unter {max}.\",\n \"a11y.img-decorative-needs-empty-alt\":\n \"Dekoratives Bild hat Alt-Text. Entferne den Alt-Text oder hebe die Markierung als dekorativ auf.\",\n \"a11y.img-linked-no-context\":\n \"Verlinktes Bild beschreibt nur das Motiv, nicht das Linkziel. Nenne das Ziel (z. B. „Frühlingssale ansehen“).\",\n \"a11y.heading-empty\":\n \"Überschrift ist leer. Füge Text hinzu oder entferne den Block.\",\n \"a11y.heading-skip-level\":\n \"Überschrift springt von H{from} auf H{to}. Eine Ebene pro Schritt.\",\n \"a11y.heading-multiple-h1\":\n \"E-Mail enthält mehr als eine H1. Verwende H1 nur einmal für die Hauptüberschrift.\",\n \"a11y.link-empty\":\n \"Ein Link in diesem Block hat keinen Text und kein beschriebenes Bild.\",\n \"a11y.link-vague-text\":\n \"Link-Text „{text}“ ist unspezifisch. Beschreibe stattdessen das Ziel.\",\n \"a11y.link-href-empty\":\n \"Ein Link in diesem Block hat ein leeres oder „#“-href.\",\n \"a11y.link-target-blank-no-rel\":\n 'Link öffnet in neuem Tab, aber rel=\"noopener\" fehlt – ergänze es, damit das Ziel nicht auf window.opener zugreifen kann.',\n \"a11y.link-nested-anchor\":\n \"Ein Link liegt innerhalb eines anderen Links. Verschachtelte Anker sind ungültiges HTML und werden von E-Mail-Clients unterschiedlich gerendert – flache einen einzigen Anker daraus.\",\n \"a11y.text-all-caps\":\n \"Längere Texte in Großbuchstaben sind schwerer lesbar. Verwende Groß- und Kleinschreibung.\",\n \"a11y.text-low-contrast\":\n \"Überschriftskontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"a11y.text-too-small\": \"Text ist {size}px; mindestens {min}px verwenden.\",\n \"a11y.button-vague-label\":\n \"Button-Beschriftung „{text}“ ist unspezifisch. Beschreibe die Aktion.\",\n \"a11y.button-touch-target\":\n \"Button ist etwa {height}px hoch; mindestens {min}px verwenden, um Fehltipper auf Mobilgeräten zu vermeiden.\",\n \"a11y.button-low-contrast\":\n \"Buttontextkontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"a11y.missing-preheader\":\n \"Kein Preheader-Text gesetzt. Postfächer zeigen sonst Bruchstücke des ersten Blocks an.\",\n};\n\nexport default de;\n","/**\n * English rule messages. The source of truth — other locales annotate\n * themselves `typeof en` so missing or extra keys fail typecheck.\n *\n * Templates use `{name}` placeholders, interpolated by `formatMessage`.\n */\nconst en = {\n \"a11y.img-missing-alt\":\n \"Image is missing alt text. Add a short description, or mark the image as decorative.\",\n \"a11y.img-alt-is-filename\":\n 'Alt text looks like a filename (\"{alt}\"). Replace with a short description of what the image conveys.',\n \"a11y.img-alt-too-long\":\n \"Alt text is {length} characters; aim for under {max}.\",\n \"a11y.img-decorative-needs-empty-alt\":\n \"Decorative image has alt text. Either clear the alt text or unmark the image as decorative.\",\n \"a11y.img-linked-no-context\":\n \"Linked image alt describes the image but not the link destination. Include where the link goes (e.g. 'Buy spring sale').\",\n \"a11y.heading-empty\": \"Heading is empty. Add text or remove the block.\",\n \"a11y.heading-skip-level\":\n \"Heading jumps from H{from} to H{to}. Step one level at a time.\",\n \"a11y.heading-multiple-h1\":\n \"Email has more than one H1. Use H1 once for the main heading.\",\n \"a11y.link-empty\": \"A link in this block has no text and no described image.\",\n \"a11y.link-vague-text\":\n 'Link text \"{text}\" is vague. Describe the destination instead.',\n \"a11y.link-href-empty\": \"A link in this block has an empty or '#' href.\",\n \"a11y.link-target-blank-no-rel\":\n 'Link opens in a new tab but is missing rel=\"noopener\" — add it to prevent the destination from accessing window.opener.',\n \"a11y.link-nested-anchor\":\n \"A link is nested inside another link. Nested anchors are invalid HTML and clients render them inconsistently — flatten to a single anchor.\",\n \"a11y.text-all-caps\":\n \"Long all-caps text is harder to read for everyone. Use sentence case.\",\n \"a11y.text-low-contrast\":\n \"Heading contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"a11y.text-too-small\": \"Text is {size}px; aim for at least {min}px.\",\n \"a11y.button-vague-label\":\n 'Button label \"{text}\" is vague. Describe the action.',\n \"a11y.button-touch-target\":\n \"Button is roughly {height}px tall; aim for at least {min}px to avoid mis-taps on mobile.\",\n \"a11y.button-low-contrast\":\n \"Button text contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"a11y.missing-preheader\":\n \"No preheader text set. Inboxes will fall back to fragments of the first block.\",\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type MessageMap = typeof en;\nexport type RuleMessageId = keyof MessageMap;\n\n/**\n * Auto-discovered locale registry. Drop a `messages/<lang>.ts` file and\n * it's bundled automatically — same pattern as the editor's i18n.\n *\n * Eager glob: synchronous, all locales bundled into the package. Tiny\n * (a few hundred bytes per locale) so the cost is negligible compared\n * to the lazy-loading overhead.\n */\nconst modules = import.meta.glob<{ default: MessageMap }>(\"./*.ts\", {\n eager: true,\n});\n\nconst MESSAGES: Record<string, MessageMap> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n MESSAGES[locale] = modules[path].default;\n}\n\nexport const SUPPORTED_MESSAGE_LOCALES = Object.keys(MESSAGES);\n\nexport function getMessages(locale: string): MessageMap {\n const base = locale.split(\"-\")[0]?.toLowerCase() ?? \"en\";\n return MESSAGES[base] ?? MESSAGES.en ?? en;\n}\n\n/**\n * Resolve a localized message for a rule. `params` interpolate `{name}`\n * placeholders. Falls back to English when the locale doesn't ship the\n * key (shouldn't happen — the parity test enforces it).\n */\nexport function formatMessage(\n locale: string,\n ruleId: RuleMessageId,\n params?: Record<string, string | number>,\n): string {\n const map = getMessages(locale);\n const template = map[ruleId] ?? en[ruleId];\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, key: string) => {\n const value = params[key];\n return value === undefined ? `{${key}}` : String(value);\n });\n}\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-missing-alt\",\n severity: \"error\",\n};\n\nexport const imgMissingAlt: Rule = {\n meta,\n block(block) {\n if (!isImage(block)) return null;\n if (block.decorative === true) return null;\n const alt = block.alt?.trim() ?? \"\";\n if (alt !== \"\") return null;\n if ((block.src ?? \"\").trim() === \"\") return null;\n return { blockId: block.id };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-alt-is-filename\",\n severity: \"warning\",\n};\n\nconst FILENAME_PATTERNS: RegExp[] = [\n /\\.(jpe?g|png|gif|webp|svg)$/i,\n /^IMG[_-]?\\d+/i,\n /^Untitled/i,\n /^Screen[\\s_-]?Shot/i,\n /^DSC[_-]?\\d+/i,\n];\n\nexport const imgAltIsFilename: Rule = {\n meta,\n block(block) {\n if (!isImage(block) || block.decorative === true) return null;\n const alt = block.alt?.trim() ?? \"\";\n if (alt === \"\") return null;\n if (!FILENAME_PATTERNS.some((re) => re.test(alt))) return null;\n\n return {\n blockId: block.id,\n params: { alt },\n };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-alt-too-long\",\n severity: \"warning\",\n};\n\nexport const imgAltTooLong: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isImage(block) || block.decorative === true) return null;\n const alt = block.alt ?? \"\";\n if (alt.length <= opts.thresholds.altMaxLength) return null;\n return {\n blockId: block.id,\n params: { length: alt.length, max: opts.thresholds.altMaxLength },\n };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-decorative-needs-empty-alt\",\n severity: \"info\",\n};\n\nexport const imgDecorativeNeedsEmptyAlt: Rule = {\n meta,\n block(block) {\n if (!isImage(block)) return null;\n if (block.decorative !== true) return null;\n if ((block.alt ?? \"\").trim() === \"\") return null;\n return {\n blockId: block.id,\n fix: {\n description: \"Clear alt text\",\n apply: (ctx) => ctx.updateBlock(block.id, { alt: \"\" }),\n },\n };\n },\n};\n","import type en from \"./en\";\n\nconst de: typeof en = {\n vagueLinkText: [\n \"hier klicken\",\n \"hier\",\n \"mehr lesen\",\n \"mehr\",\n \"weiter\",\n \"weiterlesen\",\n \"siehe mehr\",\n \"dies\",\n \"dieser link\",\n \"link\",\n \"klick\",\n ],\n vagueButtonLabels: [\n \"hier klicken\",\n \"klicken\",\n \"senden\",\n \"los\",\n \"ok\",\n \"okay\",\n \"ja\",\n \"nein\",\n ],\n linkedImageActionHints: [\n \"kaufen\",\n \"shoppen\",\n \"ansehen\",\n \"lesen\",\n \"lernen\",\n \"öffnen\",\n \"los\",\n \"sehen\",\n \"entdecken\",\n \"erkunden\",\n \"stöbern\",\n \"herunterladen\",\n \"holen\",\n \"abholen\",\n \"einlösen\",\n \"anschauen\",\n \"jetzt\",\n ],\n};\n\nexport default de;\n","/**\n * English vague-text dictionaries. Treated as the source of truth — other\n * locales annotate themselves `typeof en` so missing/extra phrases fail\n * typecheck.\n *\n * Phrases are matched case-insensitively against trimmed text content.\n */\nconst en = {\n vagueLinkText: [\n \"click here\",\n \"here\",\n \"read more\",\n \"more\",\n \"learn more\",\n \"see more\",\n \"this\",\n \"this link\",\n \"link\",\n \"click\",\n ],\n vagueButtonLabels: [\n \"click here\",\n \"click\",\n \"submit\",\n \"go\",\n \"ok\",\n \"okay\",\n \"yes\",\n \"no\",\n ],\n /**\n * Action verbs that signal a linked image's alt describes the link\n * destination, not just the visual subject. Used by `img-linked-no-context`.\n * Stored lowercase; tokenized matching is case-insensitive.\n */\n linkedImageActionHints: [\n \"buy\",\n \"shop\",\n \"view\",\n \"read\",\n \"learn\",\n \"open\",\n \"go\",\n \"see\",\n \"explore\",\n \"discover\",\n \"browse\",\n \"download\",\n \"get\",\n \"claim\",\n \"redeem\",\n \"watch\",\n ],\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type Dictionary = typeof en;\n\n/**\n * Auto-discovered locale registry. Drop a `dictionaries/<lang>.ts` file\n * and it's bundled automatically — same pattern as the editor's i18n\n * and the sibling `messages/` registry.\n *\n * Eager glob: synchronous, all locales bundled into the package. Tiny\n * (a few hundred bytes per locale) so the cost is negligible.\n */\nconst modules = import.meta.glob<{ default: Dictionary }>(\"./*.ts\", {\n eager: true,\n});\n\nconst DICTIONARIES: Record<string, Dictionary> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n DICTIONARIES[locale] = modules[path].default;\n}\n\n/**\n * Returns a dictionary that unions every registered locale. Vague phrases\n * are universally vague — a German-locale email with an English \"Click here\"\n * CTA, or an English email with a French \"cliquez ici\", is still a vague\n * CTA, so the rule must detect across languages regardless of editor locale.\n *\n * The `locale` argument is accepted for API symmetry and future use (e.g.\n * weighted matching) but currently doesn't change the returned set.\n */\nexport function getDictionary(_locale: string): Dictionary {\n return UNIONED_DICTIONARY;\n}\n\nfunction unionAll(pick: (d: Dictionary) => readonly string[]): string[] {\n const set = new Set<string>();\n for (const dict of Object.values(DICTIONARIES)) {\n for (const phrase of pick(dict)) set.add(phrase);\n }\n return Array.from(set);\n}\n\nconst UNIONED_DICTIONARY: Dictionary = {\n vagueLinkText: unionAll((d) => d.vagueLinkText),\n vagueButtonLabels: unionAll((d) => d.vagueButtonLabels),\n linkedImageActionHints: unionAll((d) => d.linkedImageActionHints),\n};\n\nexport const SUPPORTED_DICTIONARY_LOCALES = Object.keys(DICTIONARIES);\n\n/**\n * Normalize text for dictionary matching: lowercase, collapse whitespace,\n * strip leading/trailing non-alphanumeric characters (punctuation, arrows,\n * emoji, decorative symbols). \"Click here!\", \"→ click here\", \"click here?\"\n * all collapse to \"click here\" so the dictionary's plain phrase matches.\n */\nexport function normalizeForMatch(input: string): string {\n return input\n .toLowerCase()\n .replace(/\\s+/g, \" \")\n .replace(/^[^\\p{L}\\p{N}]+|[^\\p{L}\\p{N}]+$/gu, \"\")\n .trim();\n}\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getDictionary } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-linked-no-context\",\n severity: \"warning\",\n};\n\nexport const imgLinkedNoContext: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isImage(block) || block.decorative === true) return null;\n if (!block.linkUrl || block.linkUrl.trim() === \"\") return null;\n const alt = (block.alt ?? \"\").trim();\n if (alt === \"\") return null;\n const tokens = alt\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n const hints = getDictionary(opts.locale).linkedImageActionHints;\n if (tokens.some((token) => hints.includes(token))) return null;\n return { blockId: block.id };\n },\n};\n","import { Parser } from \"htmlparser2\";\n\nexport interface AnchorInfo {\n href: string;\n text: string;\n target: string | null;\n rel: string | null;\n /** True if the anchor wraps an image with non-empty alt. */\n hasImageWithAlt: boolean;\n}\n\n/**\n * Extract every anchor from a TipTap-style HTML fragment. Used by\n * link-* rules. Doesn't try to be a full DOM — only the data the rules\n * need.\n *\n * Nested `<a>` is invalid HTML; htmlparser2 follows the HTML5 spec and\n * emits an implicit `</a>` when a second `<a>` opens, so anchors are\n * effectively flat siblings. We mirror that with a single in-flight\n * anchor (no stack); a defensive finalize-on-reopen handles the\n * theoretical case where the parser ever stops emitting the implicit\n * close. Detecting nested-anchor markup as its own concern lives in\n * the `a11y.link-nested-anchor` rule, which inspects the raw input\n * before this normalization.\n */\nexport function extractAnchors(html: string): AnchorInfo[] {\n const anchors: AnchorInfo[] = [];\n let current: AnchorInfo | null = null;\n let buffer = \"\";\n\n const finalize = () => {\n if (current === null) return;\n current.text = buffer.trim();\n anchors.push(current);\n current = null;\n buffer = \"\";\n };\n\n const parser = new Parser({\n onopentag(name, attribs) {\n if (name === \"a\") {\n finalize();\n current = {\n href: attribs.href ?? \"\",\n text: \"\",\n target: attribs.target ?? null,\n rel: attribs.rel ?? null,\n hasImageWithAlt: false,\n };\n return;\n }\n\n if (name === \"img\" && current !== null) {\n const alt = (attribs.alt ?? \"\").trim();\n if (alt !== \"\") {\n current.hasImageWithAlt = true;\n }\n }\n },\n ontext(text) {\n if (current !== null) {\n buffer += text;\n }\n },\n onclosetag(name) {\n if (name === \"a\") {\n finalize();\n }\n },\n });\n\n parser.write(html);\n parser.end();\n finalize();\n\n return anchors;\n}\n\n/**\n * Whether the raw HTML contains an `<a>` opened inside another open\n * `<a>` — invalid markup that htmlparser2 silently normalizes by\n * emitting an implicit `</a>` before the inner open. `extractAnchors`\n * runs against the normalized parse and therefore can't distinguish\n * nested-from-sibling input; this helper inspects the raw text so the\n * `a11y.link-nested-anchor` rule can flag the structural problem.\n *\n * Tokenization here ignores anchor-like tokens inside HTML comments,\n * which is enough for TipTap email-template HTML. CDATA, `<script>`,\n * and attribute-value occurrences aren't expected in this surface.\n */\nexport function hasNestedAnchors(html: string): boolean {\n const stripped = html.replace(/<!--[\\s\\S]*?-->/g, \"\");\n const tokens = stripped.matchAll(/<\\/?a\\b[^>]*>/gi);\n let depth = 0;\n for (const match of tokens) {\n if (match[0].startsWith(\"</\")) {\n if (depth > 0) depth--;\n continue;\n }\n if (depth > 0) return true;\n depth++;\n }\n return false;\n}\n\n/**\n * Strip tags and return the visible text content of an HTML fragment.\n * Used by heading-empty and other text-presence rules.\n */\nexport function extractText(html: string): string {\n let text = \"\";\n const parser = new Parser({\n ontext(chunk) {\n text += chunk;\n },\n });\n parser.write(html);\n parser.end();\n return text.trim();\n}\n","import { isTitle } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractText } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.heading-empty\",\n severity: \"error\",\n};\n\nexport const headingEmpty: Rule = {\n meta,\n block(block) {\n if (!isTitle(block)) return null;\n const text = extractText(block.content ?? \"\");\n if (text !== \"\") return null;\n return { blockId: block.id };\n },\n};\n","import { isTitle, isSection } from \"@templatical/types\";\nimport type { Block, TitleBlock, TemplateContent } from \"@templatical/types\";\nimport type { Rule, RuleHit, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.heading-skip-level\",\n severity: \"error\",\n};\n\nfunction collectTitles(blocks: Block[], out: TitleBlock[]): void {\n for (const block of blocks) {\n if (isTitle(block)) {\n out.push(block);\n continue;\n }\n if (isSection(block)) {\n for (const column of block.children) {\n collectTitles(column, out);\n }\n }\n }\n}\n\nexport const headingSkipLevel: Rule = {\n meta,\n template(content: TemplateContent) {\n const titles: TitleBlock[] = [];\n collectTitles(content.blocks, titles);\n\n const hits: RuleHit[] = [];\n let lastLevel = 0;\n\n for (const title of titles) {\n if (lastLevel !== 0 && title.level > lastLevel + 1) {\n hits.push({\n blockId: title.id,\n params: { from: lastLevel, to: title.level },\n });\n }\n lastLevel = title.level;\n }\n\n return hits;\n },\n};\n","import { isTitle, isSection } from \"@templatical/types\";\nimport type { Block, TitleBlock, TemplateContent } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.heading-multiple-h1\",\n severity: \"warning\",\n};\n\nfunction collectTitles(blocks: Block[], out: TitleBlock[]): void {\n for (const block of blocks) {\n if (isTitle(block)) {\n out.push(block);\n continue;\n }\n if (isSection(block)) {\n for (const column of block.children) {\n collectTitles(column, out);\n }\n }\n }\n}\n\nexport const headingMultipleH1: Rule = {\n meta,\n template(content: TemplateContent) {\n const titles: TitleBlock[] = [];\n collectTitles(content.blocks, titles);\n const h1s = titles.filter((t) => t.level === 1);\n if (h1s.length <= 1) return [];\n return h1s.slice(1).map((title) => ({ blockId: title.id }));\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-empty\",\n severity: \"error\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkEmpty: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n\n const anchors = extractAnchors(html);\n const offender = anchors.find(\n (anchor) => anchor.text === \"\" && !anchor.hasImageWithAlt,\n );\n if (!offender) return null;\n\n return { blockId: block.id };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\nimport { getDictionary, normalizeForMatch } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-vague-text\",\n severity: \"warning\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkVagueText: Rule = {\n meta,\n block(block, _ctx, opts) {\n const html = getHtml(block);\n if (html === null) return null;\n\n const phrases = getDictionary(opts.locale).vagueLinkText;\n const anchors = extractAnchors(html);\n const offender = anchors.find((a) => {\n const text = normalizeForMatch(a.text);\n return text !== \"\" && phrases.includes(text);\n });\n if (!offender) return null;\n\n return { blockId: block.id, params: { text: offender.text } };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-href-empty\",\n severity: \"error\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkHrefEmpty: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n const anchors = extractAnchors(html);\n const offender = anchors.find((a) => {\n const href = a.href.trim();\n return href === \"\" || href === \"#\";\n });\n if (!offender) return null;\n return { blockId: block.id };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-target-blank-no-rel\",\n severity: \"warning\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nfunction hasSafeRel(rel: string | null): boolean {\n if (rel === null) return false;\n const tokens = rel.toLowerCase().split(/\\s+/);\n return tokens.includes(\"noopener\") || tokens.includes(\"noreferrer\");\n}\n\nexport const linkTargetBlankNoRel: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n const anchors = extractAnchors(html);\n const offender = anchors.find(\n (a) => a.target === \"_blank\" && !hasSafeRel(a.rel),\n );\n if (!offender) return null;\n\n return {\n blockId: block.id,\n fix: {\n description: 'Add rel=\"noopener\"',\n apply: (ctx) => {\n if (!isParagraph(block) && !isTitle(block)) return;\n const updated = addNoopenerToTargetBlank(block.content ?? \"\");\n ctx.updateBlock(block.id, { content: updated } as Partial<Block>);\n },\n },\n };\n },\n};\n\ninterface ParsedAttr {\n raw: string;\n name: string;\n value: string | null;\n /** Start offset of `raw` within the parent attrs string. */\n start: number;\n}\n\nconst ATTR_RE =\n /([^\\s\"'>/=]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g;\n\nfunction parseAttrs(attrs: string): ParsedAttr[] {\n const parsed: ParsedAttr[] = [];\n const re = new RegExp(ATTR_RE.source, ATTR_RE.flags);\n let match: RegExpExecArray | null;\n while ((match = re.exec(attrs)) !== null) {\n const value = match[2] ?? match[3] ?? match[4] ?? null;\n parsed.push({\n raw: match[0],\n name: match[1],\n value,\n start: match.index,\n });\n }\n return parsed;\n}\n\nfunction hasUnsafeTargetBlank(parsed: ParsedAttr[]): boolean {\n return parsed.some(\n (a) =>\n a.name.toLowerCase() === \"target\" &&\n a.value !== null &&\n a.value.toLowerCase() === \"_blank\",\n );\n}\n\nfunction addNoopenerToTargetBlank(html: string): string {\n return html.replace(/<a\\b([^>]*)>/gi, (match, attrs: string) => {\n const parsed = parseAttrs(attrs);\n if (!hasUnsafeTargetBlank(parsed)) return match;\n\n const relAttr = parsed.find((a) => a.name.toLowerCase() === \"rel\");\n if (relAttr) {\n const tokens = (relAttr.value ?? \"\").toLowerCase().split(/\\s+/);\n if (tokens.includes(\"noopener\") || tokens.includes(\"noreferrer\")) {\n return match;\n }\n const newRel = `${relAttr.value ?? \"\"} noopener`.trim();\n const before = attrs.slice(0, relAttr.start);\n const after = attrs.slice(relAttr.start + relAttr.raw.length);\n return `<a${before}rel=\"${newRel}\"${after}>`;\n }\n return `<a${attrs} rel=\"noopener\">`;\n });\n}\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { hasNestedAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-nested-anchor\",\n severity: \"error\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkNestedAnchor: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n if (!hasNestedAnchors(html)) return null;\n return { blockId: block.id };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractText } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.text-all-caps\",\n severity: \"warning\",\n};\n\nexport const textAllCaps: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isParagraph(block) && !isTitle(block)) return null;\n const text = extractText(block.content ?? \"\");\n const letters = text.replace(/[^\\p{L}]/gu, \"\");\n if (letters.length < opts.thresholds.allCapsMinLength) return null;\n if (letters !== letters.toLocaleUpperCase()) return null;\n return { blockId: block.id };\n },\n};\n","import { isTitle } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getContrastRatio, isOpaqueHex } from \"../../contrast\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.text-low-contrast\",\n severity: \"error\",\n};\n\nexport const textLowContrast: Rule = {\n meta,\n block(block, ctx) {\n if (!isTitle(block)) return null;\n if (\n !isOpaqueHex(block.color) ||\n !isOpaqueHex(ctx.resolvedBackgroundColor)\n ) {\n return null;\n }\n const fontSize = HEADING_LEVEL_FONT_SIZE[block.level];\n // WCAG large text = 18pt (~24px). Headings have no structured bold\n // flag in this codebase (TipTap stores it inline), so we conservatively\n // skip the 14pt-bold (~18.66px) relaxation and apply the px threshold.\n const required = fontSize >= 24 ? 3 : 4.5;\n const ratio = getContrastRatio(block.color, ctx.resolvedBackgroundColor);\n if (Number.isNaN(ratio) || ratio >= required) return null;\n return {\n blockId: block.id,\n params: { ratio: ratio.toFixed(2), required },\n };\n },\n};\n","import { isMenu, isTable } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.text-too-small\",\n severity: \"warning\",\n};\n\nfunction getFontSize(block: Block): number | null {\n if (isMenu(block) || isTable(block)) return block.fontSize;\n return null;\n}\n\nexport const textTooSmall: Rule = {\n meta,\n block(block, _ctx, opts) {\n const fontSize = getFontSize(block);\n if (fontSize === null) return null;\n if (fontSize >= opts.thresholds.minFontSize) return null;\n return {\n blockId: block.id,\n params: { size: fontSize, min: opts.thresholds.minFontSize },\n };\n },\n};\n","import { isButton } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getDictionary, normalizeForMatch } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.button-vague-label\",\n severity: \"warning\",\n};\n\nexport const buttonVagueLabel: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isButton(block)) return null;\n const text = normalizeForMatch(block.text ?? \"\");\n if (text === \"\") return null;\n const phrases = getDictionary(opts.locale).vagueButtonLabels;\n if (!phrases.includes(text)) return null;\n return { blockId: block.id, params: { text: block.text } };\n },\n};\n","import { isButton } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.button-touch-target\",\n severity: \"warning\",\n};\n\nexport const buttonTouchTarget: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isButton(block)) return null;\n const padding = block.buttonPadding;\n if (!padding) return null;\n const estimatedHeight = block.fontSize * 1.4 + padding.top + padding.bottom;\n if (estimatedHeight >= opts.thresholds.minTouchTargetPx) return null;\n return {\n blockId: block.id,\n params: {\n height: Math.round(estimatedHeight),\n min: opts.thresholds.minTouchTargetPx,\n },\n };\n },\n};\n","import { isButton } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getContrastRatio } from \"../../contrast\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.button-low-contrast\",\n severity: \"error\",\n};\n\nexport const buttonLowContrast: Rule = {\n meta,\n block(block) {\n if (!isButton(block)) return null;\n const ratio = getContrastRatio(block.textColor, block.backgroundColor);\n if (Number.isNaN(ratio)) return null;\n // WCAG large text = 18pt (~24px). Mirrors the heading rule's threshold.\n const required = block.fontSize >= 24 ? 3 : 4.5;\n if (ratio >= required) return null;\n return {\n blockId: block.id,\n params: { ratio: ratio.toFixed(2), required },\n };\n },\n};\n","import type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.missing-preheader\",\n severity: \"info\",\n};\n\nexport const missingPreheader: Rule = {\n meta,\n template(content) {\n const text = content.settings.preheaderText?.trim() ?? \"\";\n if (text !== \"\") return [];\n return [{ blockId: null }];\n },\n};\n","import type { TemplateContent } from \"@templatical/types\";\nimport type { LintIssue, LintOptions, Rule } from \"../types\";\nimport { resolveAccessibilityOptions, runRules } from \"../run-rules\";\nimport { formatMessage, type RuleMessageId } from \"./messages\";\nimport { imgMissingAlt } from \"./rules/img-missing-alt\";\nimport { imgAltIsFilename } from \"./rules/img-alt-is-filename\";\nimport { imgAltTooLong } from \"./rules/img-alt-too-long\";\nimport { imgDecorativeNeedsEmptyAlt } from \"./rules/img-decorative-needs-empty-alt\";\nimport { imgLinkedNoContext } from \"./rules/img-linked-no-context\";\nimport { headingEmpty } from \"./rules/heading-empty\";\nimport { headingSkipLevel } from \"./rules/heading-skip-level\";\nimport { headingMultipleH1 } from \"./rules/heading-multiple-h1\";\nimport { linkEmpty } from \"./rules/link-empty\";\nimport { linkVagueText } from \"./rules/link-vague-text\";\nimport { linkHrefEmpty } from \"./rules/link-href-empty\";\nimport { linkTargetBlankNoRel } from \"./rules/link-target-blank-no-rel\";\nimport { linkNestedAnchor } from \"./rules/link-nested-anchor\";\nimport { textAllCaps } from \"./rules/text-all-caps\";\nimport { textLowContrast } from \"./rules/text-low-contrast\";\nimport { textTooSmall } from \"./rules/text-too-small\";\nimport { buttonVagueLabel } from \"./rules/button-vague-label\";\nimport { buttonTouchTarget } from \"./rules/button-touch-target\";\nimport { buttonLowContrast } from \"./rules/button-low-contrast\";\nimport { missingPreheader } from \"./rules/missing-preheader\";\n\nexport const ACCESSIBILITY_RULES: Rule[] = [\n imgMissingAlt,\n imgAltIsFilename,\n imgAltTooLong,\n imgDecorativeNeedsEmptyAlt,\n imgLinkedNoContext,\n headingEmpty,\n headingSkipLevel,\n headingMultipleH1,\n linkEmpty,\n linkVagueText,\n linkHrefEmpty,\n linkTargetBlankNoRel,\n linkNestedAnchor,\n textAllCaps,\n textLowContrast,\n textTooSmall,\n buttonVagueLabel,\n buttonTouchTarget,\n buttonLowContrast,\n missingPreheader,\n];\n\nexport function lintAccessibility(\n content: TemplateContent,\n options: LintOptions = {},\n): LintIssue[] {\n if (options.disabled === true || options.accessibility === false) return [];\n const tool = options.accessibility ?? {};\n const resolved = resolveAccessibilityOptions(\n options.locale,\n tool,\n ACCESSIBILITY_RULES,\n );\n return runRules(\n content,\n ACCESSIBILITY_RULES,\n resolved,\n (locale, id, params) => formatMessage(locale, id as RuleMessageId, params),\n );\n}\n","import type en from \"./en\";\n\nconst de: typeof en = {\n \"structure.duplicate-block-id\":\n \"Block-ID erscheint {count}-mal im Baum. Jeder Block muss eine eindeutige ID haben.\",\n \"structure.section-column-mismatch\":\n 'Sektion verwendet Layout „{layout}\" (erwartet {expected} Spalten), hat aber {actual}. Deutet auf beschädigten Zustand hin.',\n \"structure.nested-section\":\n \"Sektion ist in einer anderen Sektion verschachtelt. Sektionen können nicht verschachtelt werden – der Renderer wird sich falsch verhalten.\",\n \"structure.empty-section\":\n \"Sektion enthält keine Blöcke. Entferne sie oder füge Inhalt hinzu.\",\n \"structure.empty-column\":\n \"Spalte {columnIndex} dieser Sektion ist leer. Füge Inhalt hinzu oder reduziere die Spaltenanzahl.\",\n};\n\nexport default de;\n","/**\n * English structure-rule messages. The source of truth — other locales\n * annotate themselves `typeof en` so missing or extra keys fail typecheck.\n *\n * Templates use `{name}` placeholders, interpolated by `formatMessage`.\n */\nconst en = {\n \"structure.duplicate-block-id\":\n \"Block id appears {count} times in the tree. Each block must have a unique id.\",\n \"structure.section-column-mismatch\":\n 'Section uses layout \"{layout}\" (expects {expected} columns) but has {actual}. Indicates corrupted state.',\n \"structure.nested-section\":\n \"Section is nested inside another section. Sections cannot nest — the renderer will misbehave.\",\n \"structure.empty-section\": \"Section has no blocks. Remove it or add content.\",\n \"structure.empty-column\":\n \"Column {columnIndex} of this section is empty. Add content or reduce the column count.\",\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type StructureMessageMap = typeof en;\nexport type StructureRuleMessageId = keyof StructureMessageMap;\n\nconst modules = import.meta.glob<{ default: StructureMessageMap }>(\"./*.ts\", {\n eager: true,\n});\n\nconst MESSAGES: Record<string, StructureMessageMap> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n MESSAGES[locale] = modules[path].default;\n}\n\nexport const SUPPORTED_STRUCTURE_MESSAGE_LOCALES = Object.keys(MESSAGES);\n\nexport function getStructureMessages(locale: string): StructureMessageMap {\n const base = locale.split(\"-\")[0]?.toLowerCase() ?? \"en\";\n return MESSAGES[base] ?? MESSAGES.en ?? en;\n}\n\nexport function formatStructureMessage(\n locale: string,\n ruleId: StructureRuleMessageId,\n params?: Record<string, string | number>,\n): string {\n const map = getStructureMessages(locale);\n const template = map[ruleId] ?? en[ruleId];\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, key: string) => {\n const value = params[key];\n return value === undefined ? `{${key}}` : String(value);\n });\n}\n","import type { Block, SectionBlock, TemplateContent } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleHit, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.duplicate-block-id\",\n severity: \"error\",\n};\n\nfunction collectIds(blocks: Block[], counts: Map<string, number>): void {\n for (const block of blocks) {\n counts.set(block.id, (counts.get(block.id) ?? 0) + 1);\n if (isSection(block)) {\n for (const column of (block as SectionBlock).children) {\n collectIds(column, counts);\n }\n }\n }\n}\n\nexport const duplicateBlockId: Rule = {\n meta,\n template(content: TemplateContent): RuleHit[] {\n const counts = new Map<string, number>();\n collectIds(content.blocks, counts);\n\n const hits: RuleHit[] = [];\n for (const [id, count] of counts) {\n if (count > 1) {\n hits.push({ blockId: id, params: { count } });\n }\n }\n return hits;\n },\n};\n","import type { Block, SectionBlock, TemplateContent } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleHit, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.empty-column\",\n severity: \"warning\",\n};\n\nfunction findEmptyColumns(blocks: Block[], hits: RuleHit[]): void {\n for (const block of blocks) {\n if (!isSection(block)) continue;\n const section = block as SectionBlock;\n if (section.children.length > 1) {\n section.children.forEach((column, columnIndex) => {\n if (column.length === 0) {\n hits.push({\n blockId: section.id,\n params: { columnIndex: columnIndex + 1 },\n });\n }\n });\n }\n for (const column of section.children) {\n findEmptyColumns(column, hits);\n }\n }\n}\n\nexport const emptyColumn: Rule = {\n meta,\n template(content: TemplateContent): RuleHit[] {\n const hits: RuleHit[] = [];\n findEmptyColumns(content.blocks, hits);\n return hits;\n },\n};\n","import type { SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.empty-section\",\n severity: \"warning\",\n};\n\nfunction isSectionEmpty(section: SectionBlock): boolean {\n if (section.children.length === 0) return true;\n return section.children.every((column) => column.length === 0);\n}\n\nexport const emptySection: Rule = {\n meta,\n block(block) {\n if (!isSection(block)) return null;\n const section = block as SectionBlock;\n if (!isSectionEmpty(section)) return null;\n return {\n blockId: section.id,\n fix: {\n description: \"Remove the empty section\",\n apply: (ctx) => {\n ctx.removeBlock(section.id);\n },\n },\n };\n },\n};\n","import type { SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleMeta, WalkContext } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.nested-section\",\n severity: \"error\",\n};\n\nexport const nestedSection: Rule = {\n meta,\n block(block, ctx: WalkContext) {\n if (!isSection(block)) return null;\n if (ctx.section === null) return null;\n const parentSection = ctx.section as SectionBlock;\n return {\n blockId: block.id,\n params: { parentId: parentSection.id },\n };\n },\n};\n","import type { ColumnLayout, SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.section-column-mismatch\",\n severity: \"error\",\n};\n\nfunction expectedColumnCount(layout: ColumnLayout): number {\n if (layout === \"1\") return 1;\n if (layout === \"3\") return 3;\n return 2;\n}\n\nexport const sectionColumnMismatch: Rule = {\n meta,\n block(block) {\n if (!isSection(block)) return null;\n const section = block as SectionBlock;\n const expected = expectedColumnCount(section.columns);\n const actual = section.children.length;\n if (actual === expected) return null;\n return {\n blockId: section.id,\n params: { layout: section.columns, expected, actual },\n };\n },\n};\n","import type { TemplateContent } from \"@templatical/types\";\nimport type { LintIssue, LintOptions, Rule } from \"../types\";\nimport { resolveStructureOptions, runRules } from \"../run-rules\";\nimport {\n formatStructureMessage,\n type StructureRuleMessageId,\n} from \"./messages\";\nimport { duplicateBlockId } from \"./rules/duplicate-block-id\";\nimport { emptyColumn } from \"./rules/empty-column\";\nimport { emptySection } from \"./rules/empty-section\";\nimport { nestedSection } from \"./rules/nested-section\";\nimport { sectionColumnMismatch } from \"./rules/section-column-mismatch\";\n\nexport const STRUCTURE_RULES: Rule[] = [\n duplicateBlockId,\n emptySection,\n emptyColumn,\n nestedSection,\n sectionColumnMismatch,\n];\n\nexport function lintStructure(\n content: TemplateContent,\n options: LintOptions = {},\n): LintIssue[] {\n if (options.disabled === true || options.structure === false) return [];\n const tool = options.structure ?? {};\n const resolved = resolveStructureOptions(\n options.locale,\n tool,\n STRUCTURE_RULES,\n );\n return runRules(content, STRUCTURE_RULES, resolved, (locale, id, params) =>\n formatStructureMessage(locale, id as StructureRuleMessageId, params),\n );\n}\n","import type en from \"./en\";\n\nconst de: typeof en = {\n \"link.javascript-protocol\":\n 'Die URL verwendet das „javascript:\"-Protokoll, das aus Sicherheitsgründen beim Rendern entfernt wird. Ersetze sie durch eine echte URL oder entferne sie.',\n \"link.unsupported-protocol\":\n 'Die URL verwendet das Protokoll „{protocol}\", das von den meisten E-Mail-Clients nicht unterstützt wird. Verwende http, https, mailto, tel oder sms.',\n \"link.malformed-mailto\":\n \"Der mailto:-Link ist fehlerhaft. Erwartet wird eine einzelne Empfängeradresse vor einer eventuellen Querystring (z. B. mailto:hallo@example.com).\",\n \"link.malformed-tel\":\n \"Der tel:-Link enthält Zeichen, die keine Ziffern, +, Leerzeichen, Bindestriche, Klammern oder Punkte sind.\",\n \"link.localhost-or-staging\":\n 'Der URL-Host „{host}\" entspricht einem Nicht-Produktionsmuster. Ersetze ihn vor dem Versand durch die Produktions-URL.',\n};\n\nexport default de;\n","/**\n * English link-rule messages. The source of truth — other locales annotate\n * themselves `typeof en` so missing or extra keys fail typecheck.\n *\n * Templates use `{name}` placeholders, interpolated by `formatLinkMessage`.\n */\nconst en = {\n \"link.javascript-protocol\":\n 'URL uses the \"javascript:\" protocol, which is stripped at render time for safety. Replace it with a real link or remove the URL.',\n \"link.unsupported-protocol\":\n 'URL uses the \"{protocol}\" protocol, which most email clients do not support. Use http, https, mailto, tel, or sms.',\n \"link.malformed-mailto\":\n \"mailto: link is malformed. Expected a single recipient address before any query string (e.g. mailto:hello@example.com).\",\n \"link.malformed-tel\":\n \"tel: link contains characters that are not digits, +, spaces, dashes, parentheses, or dots.\",\n \"link.localhost-or-staging\":\n 'URL host \"{host}\" matches a non-production pattern. Replace with the production URL before sending.',\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type LinkMessageMap = typeof en;\nexport type LinkRuleMessageId = keyof LinkMessageMap;\n\nconst modules = import.meta.glob<{ default: LinkMessageMap }>(\"./*.ts\", {\n eager: true,\n});\n\nconst MESSAGES: Record<string, LinkMessageMap> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n MESSAGES[locale] = modules[path].default;\n}\n\nexport const SUPPORTED_LINK_MESSAGE_LOCALES = Object.keys(MESSAGES);\n\nexport function getLinkMessages(locale: string): LinkMessageMap {\n const base = locale.split(\"-\")[0]?.toLowerCase() ?? \"en\";\n return MESSAGES[base] ?? MESSAGES.en ?? en;\n}\n\nexport function formatLinkMessage(\n locale: string,\n ruleId: LinkRuleMessageId,\n params?: Record<string, string | number>,\n): string {\n const map = getLinkMessages(locale);\n const template = map[ruleId] ?? en[ruleId];\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, key: string) => {\n const value = params[key];\n return value === undefined ? `{${key}}` : String(value);\n });\n}\n","import type { TemplateContent } from \"@templatical/types\";\nimport {\n isButton,\n isHtml,\n isImage,\n isMenu,\n isParagraph,\n isSocialIcons,\n isTitle,\n isVideo,\n} from \"@templatical/types\";\nimport { walkBlocks } from \"./walk\";\nimport { extractAnchors } from \"./html-utils\";\n\nexport type UrlSource =\n | \"anchor\"\n | \"button\"\n | \"image-link\"\n | \"video\"\n | \"menu-item\"\n | \"social-icon\";\n\nexport interface UrlOccurrence {\n url: string;\n blockId: string;\n source: UrlSource;\n /** Anchor text or block-derived label, if applicable. */\n label?: string;\n}\n\n/**\n * Visit every URL-bearing field in the template tree.\n *\n * Sources covered:\n * - anchor — `<a href>` inside `title.content`, `paragraph.content`,\n * `html.content` (parsed via extractAnchors)\n * - button — `button.url`\n * - image-link — `image.linkUrl` (only when present + non-empty)\n * - video — `video.url`\n * - menu-item — `menu.items[i].url`\n * - social-icon — `social.icons[i].url`\n *\n * Each rule iterates this list once and decides per occurrence.\n */\nexport function walkUrls(content: TemplateContent): UrlOccurrence[] {\n const occurrences: UrlOccurrence[] = [];\n\n walkBlocks(content, (block) => {\n if (isTitle(block) || isParagraph(block) || isHtml(block)) {\n for (const anchor of extractAnchors(block.content)) {\n occurrences.push({\n url: anchor.href,\n blockId: block.id,\n source: \"anchor\",\n label: anchor.text,\n });\n }\n return;\n }\n\n if (isButton(block)) {\n occurrences.push({\n url: block.url,\n blockId: block.id,\n source: \"button\",\n label: block.text,\n });\n return;\n }\n\n if (isImage(block)) {\n if (block.linkUrl && block.linkUrl !== \"\") {\n occurrences.push({\n url: block.linkUrl,\n blockId: block.id,\n source: \"image-link\",\n label: block.alt || undefined,\n });\n }\n return;\n }\n\n if (isVideo(block)) {\n occurrences.push({\n url: block.url,\n blockId: block.id,\n source: \"video\",\n label: block.alt || undefined,\n });\n return;\n }\n\n if (isMenu(block)) {\n for (const item of block.items) {\n occurrences.push({\n url: item.url,\n blockId: block.id,\n source: \"menu-item\",\n label: item.text,\n });\n }\n return;\n }\n\n if (isSocialIcons(block)) {\n for (const icon of block.icons) {\n occurrences.push({\n url: icon.url,\n blockId: block.id,\n source: \"social-icon\",\n label: icon.platform,\n });\n }\n return;\n }\n });\n\n return occurrences;\n}\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.javascript-protocol\",\n severity: \"error\",\n};\n\n/**\n * Match `javascript:` even when the value is whitespace-padded or mixed-case.\n * Mirrors what HTML attribute parsers see at insert time — leading whitespace\n * (spaces, tabs, newlines) is stripped before scheme parsing.\n */\nfunction isJavascriptProtocol(url: string): boolean {\n if (!url) return false;\n const stripped = url.replace(/\\s+/g, \"\");\n return /^javascript:/i.test(stripped);\n}\n\nexport const javascriptProtocol: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n if (isJavascriptProtocol(occ.url)) {\n hits.push({ blockId: occ.blockId });\n }\n }\n return hits;\n },\n};\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.unsupported-protocol\",\n severity: \"warning\",\n};\n\nconst SUPPORTED = new Set([\"http\", \"https\", \"mailto\", \"tel\", \"sms\"]);\n\n/**\n * Treat `javascript:` (covered by its own rule) and bare/relative URLs as\n * \"not unsupported\" — this rule fires only for explicitly named schemes that\n * email clients typically refuse.\n */\nfunction getProtocol(url: string): string | null {\n if (!url) return null;\n const trimmed = url.trim();\n const match = /^([a-z][a-z0-9+\\-.]*):/i.exec(trimmed);\n if (!match) return null;\n return match[1].toLowerCase();\n}\n\nexport const unsupportedProtocol: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n const protocol = getProtocol(occ.url);\n if (protocol === null) continue;\n if (protocol === \"javascript\") continue;\n if (SUPPORTED.has(protocol)) continue;\n hits.push({ blockId: occ.blockId, params: { protocol } });\n }\n return hits;\n },\n};\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.malformed-mailto\",\n severity: \"warning\",\n};\n\n/**\n * Pragmatic RFC-5321-ish sanity check, not a full validator. Splits on `?`,\n * requires the left side to contain exactly one `@` with a non-empty local\n * part and a domain that includes at least one dot.\n *\n * Multi-recipient `mailto:a@x.com,b@y.com` is accepted (commas pass through;\n * each recipient is validated individually).\n */\nfunction isMalformedMailto(url: string): boolean {\n const trimmed = url.trim();\n if (!/^mailto:/i.test(trimmed)) return false;\n const value = trimmed.slice(\"mailto:\".length);\n const [recipients] = value.split(\"?\", 2);\n if (recipients.trim() === \"\") return true;\n\n const list = recipients.split(\",\").map((r) => r.trim());\n for (const recipient of list) {\n if (recipient === \"\") return true;\n const at = recipient.split(\"@\");\n if (at.length !== 2) return true;\n const [local, domain] = at;\n if (local === \"\" || domain === \"\") return true;\n if (!domain.includes(\".\")) return true;\n }\n return false;\n}\n\nexport const malformedMailto: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n if (isMalformedMailto(occ.url)) {\n hits.push({ blockId: occ.blockId });\n }\n }\n return hits;\n },\n};\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.malformed-tel\",\n severity: \"warning\",\n};\n\nconst VALID_SUBSCRIBER_CHARS = /^[+0-9\\s().\\-]+$/;\n// RFC 3966 par = `;` pname [ \"=\" pvalue ]. pname is alphanum/`-`, pvalue is\n// 1+ paramchar. We accept anything non-empty on the right of `=` since email\n// clients don't validate it.\nconst VALID_PARAM = /^[A-Za-z0-9-]+(=[^;]+)?$/;\n\nfunction isMalformedTel(url: string): boolean {\n const trimmed = url.trim();\n if (!/^tel:/i.test(trimmed)) return false;\n const value = trimmed.slice(\"tel:\".length).trim();\n if (value === \"\") return true;\n const [subscriber, ...params] = value.split(\";\");\n if (!VALID_SUBSCRIBER_CHARS.test(subscriber)) return true;\n return params.some((p) => !VALID_PARAM.test(p));\n}\n\nexport const malformedTel: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n if (isMalformedTel(occ.url)) {\n hits.push({ blockId: occ.blockId });\n }\n }\n return hits;\n },\n};\n","import type { ResolvedOptions, Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.localhost-or-staging\",\n severity: \"warning\",\n};\n\n/**\n * Glob → RegExp for the `nonProductionHosts` pattern set. `*` is a wildcard\n * that matches any run of characters (including `.`) so `*.staging.*`\n * matches `app.staging.example.com` and `*.local` matches both `acme.local`\n * and `a.b.c.local`. Case-insensitive.\n */\nfunction globToRegex(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const expanded = escaped.replace(/\\*/g, \".*\");\n return new RegExp(`^${expanded}$`, \"i\");\n}\n\nfunction extractHost(url: string): string | null {\n if (!url) return null;\n const trimmed = url.trim();\n // mailto/tel/sms have no host concept worth matching.\n if (!/^(https?|ftps?):\\/\\//i.test(trimmed)) return null;\n try {\n return new URL(trimmed).hostname.toLowerCase();\n } catch {\n return null;\n }\n}\n\nexport const localhostOrStaging: Rule = {\n meta,\n template(content, opts: ResolvedOptions): RuleHit[] {\n const patterns = opts.links.nonProductionHosts;\n if (patterns.length === 0) return [];\n const regexes = patterns.map(globToRegex);\n const hits: RuleHit[] = [];\n\n for (const occ of walkUrls(content)) {\n const host = extractHost(occ.url);\n if (host === null) continue;\n if (regexes.some((re) => re.test(host))) {\n hits.push({ blockId: occ.blockId, params: { host } });\n }\n }\n return hits;\n },\n};\n","import type { TemplateContent } from \"@templatical/types\";\nimport type { LintIssue, LintOptions, Rule } from \"../types\";\nimport { resolveLinksOptions, runRules } from \"../run-rules\";\nimport { formatLinkMessage, type LinkRuleMessageId } from \"./messages\";\nimport { javascriptProtocol } from \"./rules/javascript-protocol\";\nimport { unsupportedProtocol } from \"./rules/unsupported-protocol\";\nimport { malformedMailto } from \"./rules/malformed-mailto\";\nimport { malformedTel } from \"./rules/malformed-tel\";\nimport { localhostOrStaging } from \"./rules/localhost-or-staging\";\n\nexport const LINK_RULES: Rule[] = [\n javascriptProtocol,\n unsupportedProtocol,\n malformedMailto,\n malformedTel,\n localhostOrStaging,\n];\n\nexport function lintLinks(\n content: TemplateContent,\n options: LintOptions = {},\n): LintIssue[] {\n if (options.disabled === true || options.links === false) return [];\n const tool = options.links ?? {};\n const resolved = resolveLinksOptions(options.locale, tool, LINK_RULES);\n return runRules(content, LINK_RULES, resolved, (locale, id, params) =>\n formatLinkMessage(locale, id as LinkRuleMessageId, params),\n );\n}\n","import type { LintOptions } from \"./types\";\n\n/**\n * `true` when no linter would run for the given options — either the\n * global `disabled` flag is set, or every per-tool key is `false`.\n *\n * The editor uses this to skip lazy-loading `@templatical/quality`, hide\n * the Issues sidebar tab, and suppress inline canvas badges. Headless\n * callers can use it to short-circuit before any linter call.\n */\nexport function isLintFullyDisabled(options: LintOptions | undefined): boolean {\n if (!options) return false;\n if (options.disabled === true) return true;\n return (\n options.accessibility === false &&\n options.structure === false &&\n options.links === false\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/types.ts","../src/contrast.ts","../src/walk.ts","../src/run-rules.ts","../src/accessibility/messages/de.ts","../src/accessibility/messages/en.ts","../src/accessibility/messages/index.ts","../src/accessibility/rules/img-missing-alt.ts","../src/accessibility/rules/img-alt-is-filename.ts","../src/accessibility/rules/img-alt-too-long.ts","../src/accessibility/rules/img-decorative-needs-empty-alt.ts","../src/accessibility/dictionaries/de.ts","../src/accessibility/dictionaries/en.ts","../src/accessibility/dictionaries/index.ts","../src/accessibility/rules/img-linked-no-context.ts","../src/html-utils.ts","../src/accessibility/rules/heading-empty.ts","../src/accessibility/rules/heading-skip-level.ts","../src/accessibility/rules/heading-multiple-h1.ts","../src/accessibility/rules/link-empty.ts","../src/accessibility/rules/link-vague-text.ts","../src/accessibility/rules/link-href-empty.ts","../src/accessibility/rules/link-target-blank-no-rel.ts","../src/accessibility/rules/link-nested-anchor.ts","../src/accessibility/rules/text-all-caps.ts","../src/accessibility/rules/text-low-contrast.ts","../src/accessibility/rules/text-too-small.ts","../src/accessibility/rules/button-vague-label.ts","../src/accessibility/rules/button-touch-target.ts","../src/accessibility/rules/button-low-contrast.ts","../src/accessibility/rules/missing-preheader.ts","../src/accessibility/index.ts","../src/structure/messages/de.ts","../src/structure/messages/en.ts","../src/structure/messages/index.ts","../src/structure/rules/duplicate-block-id.ts","../src/structure/rules/empty-column.ts","../src/structure/rules/empty-section.ts","../src/structure/rules/nested-section.ts","../src/structure/rules/section-column-mismatch.ts","../src/structure/index.ts","../src/links/messages/de.ts","../src/links/messages/en.ts","../src/links/messages/index.ts","../src/url-walker.ts","../src/links/rules/javascript-protocol.ts","../src/links/rules/unsupported-protocol.ts","../src/links/rules/malformed-mailto.ts","../src/links/rules/malformed-tel.ts","../src/links/rules/localhost-or-staging.ts","../src/links/index.ts","../src/util.ts"],"sourcesContent":["import type {\n Block,\n SectionBlock,\n TemplateContent,\n TemplateSettings,\n} from \"@templatical/types\";\n\nexport type Severity = \"error\" | \"warning\" | \"info\" | \"off\";\n\nexport interface LintIssue {\n /** Block id, or null for template-level issues. */\n blockId: string | null;\n ruleId: string;\n severity: Exclude<Severity, \"off\">;\n message: string;\n fix?: LintPatch;\n}\n\nexport interface LintPatchContext {\n updateBlock: (blockId: string, patch: Partial<Block>) => void;\n updateSettings: (patch: Partial<TemplateSettings>) => void;\n removeBlock: (blockId: string) => void;\n}\n\nexport interface LintPatch {\n description: string;\n apply: (ctx: LintPatchContext) => void;\n}\n\nexport interface LintThresholds {\n altMaxLength: number;\n minFontSize: number;\n allCapsMinLength: number;\n minTouchTargetPx: number;\n}\n\n/**\n * Per-rule severity override. Set a rule to `'off'` to disable it.\n * Keys are the full prefixed rule IDs (`a11y.*`, `structure.*`, `link.*`)\n * so a value copied from `LintIssue.ruleId` pastes straight in.\n */\nexport type RuleOverrides = Record<string, Severity>;\n\n/** Options consumed only by the accessibility linter. */\nexport interface AccessibilityLintOptions {\n rules?: RuleOverrides;\n thresholds?: Partial<LintThresholds>;\n}\n\n/** Options consumed only by the structure linter. */\nexport interface StructureLintOptions {\n rules?: RuleOverrides;\n}\n\n/** Options consumed only by the links linter. */\nexport interface LinksLintOptions {\n rules?: RuleOverrides;\n /**\n * Host patterns that should flag as \"staging / non-production\".\n * Each entry is a glob-style pattern matched against the URL host.\n * `*` matches any run of characters (including `.`), so `*.staging.*`\n * matches `app.staging.example.com`.\n *\n * Default: ['localhost', '127.0.0.1', '0.0.0.0', '*.local',\n * '*.staging.*', '*.dev.*']\n */\n nonProductionHosts?: string[];\n}\n\nexport interface LintOptions {\n /**\n * Fully disable linting. When true, the editor skips lazy-loading the\n * package, hides the sidebar tab, and suppresses inline badges.\n */\n disabled?: boolean;\n /** Locale for vague-text dictionaries and message text. Falls back to `en`. */\n locale?: string;\n /**\n * Accessibility linter config. Set to `false` to disable the whole\n * `lintAccessibility` linter without enumerating its rules.\n */\n accessibility?: false | AccessibilityLintOptions;\n /**\n * Structure linter config. Set to `false` to disable the whole\n * `lintStructure` linter without enumerating its rules.\n */\n structure?: false | StructureLintOptions;\n /**\n * Links linter config. Set to `false` to disable the whole `lintLinks`\n * linter without enumerating its rules.\n */\n links?: false | LinksLintOptions;\n}\n\nexport interface ResolvedLinksOptions {\n nonProductionHosts: string[];\n}\n\nexport interface ResolvedOptions {\n locale: string;\n rules: RuleOverrides;\n thresholds: LintThresholds;\n links: ResolvedLinksOptions;\n /** Returns the effective severity for a rule (override or default). */\n severity: (ruleId: string) => Severity;\n}\n\nexport interface WalkContext {\n parent: Block | null;\n section: SectionBlock | null;\n columnIndex: number | null;\n depth: number;\n /**\n * Nearest opaque ancestor background, or template settings background.\n * Hex string, lowercased.\n */\n resolvedBackgroundColor: string;\n}\n\nexport interface RuleMeta {\n /** Stable identifier — used for severity overrides and message lookup. */\n id: string;\n /** Default severity when no override is supplied. */\n severity: Exclude<Severity, \"off\">;\n}\n\n/**\n * What a rule emits per match. The orchestrator combines this with the\n * rule's `meta` (for `ruleId` + default severity) and resolves the\n * localized message via the active locale's message map.\n */\nexport interface RuleHit {\n blockId: string | null;\n /** Interpolation values for the rule's localized message template. */\n params?: Record<string, string | number>;\n fix?: LintPatch;\n}\n\nexport interface Rule {\n meta: RuleMeta;\n /** Block-level rule. Returns a hit or null. */\n block?: (\n block: Block,\n ctx: WalkContext,\n opts: ResolvedOptions,\n ) => RuleHit | null;\n /** Template-level rule. Runs once after the walk. */\n template?: (content: TemplateContent, opts: ResolvedOptions) => RuleHit[];\n}\n\nexport const DEFAULT_A11Y_THRESHOLDS: LintThresholds = {\n altMaxLength: 125,\n minFontSize: 14,\n allCapsMinLength: 20,\n minTouchTargetPx: 44,\n};\n\nexport const DEFAULT_NON_PRODUCTION_HOSTS: string[] = [\n \"localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \"*.local\",\n \"*.staging.*\",\n \"*.dev.*\",\n];\n","/**\n * WCAG 2.1 sRGB relative-luminance contrast.\n *\n * Inputs are hex strings (`#rgb`, `#rrggbb`, optional leading `#`).\n * Returns the contrast ratio (1–21) per WCAG, or `NaN` if either input\n * cannot be parsed as an opaque solid hex color.\n *\n * The codebase uses OKLch for design tokens, but contrast math is\n * sRGB-defined; mixing the two gives incorrect results.\n */\nexport function getContrastRatio(fg: string, bg: string): number {\n const fgRgb = parseHex(fg);\n const bgRgb = parseHex(bg);\n\n if (!fgRgb || !bgRgb) {\n return Number.NaN;\n }\n\n const l1 = relativeLuminance(fgRgb);\n const l2 = relativeLuminance(bgRgb);\n const lighter = Math.max(l1, l2);\n const darker = Math.min(l1, l2);\n\n return (lighter + 0.05) / (darker + 0.05);\n}\n\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\nconst HEX3 = /^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i;\nconst HEX6 = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;\nconst HEX8 = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;\n\nexport function parseHex(input: string | undefined | null): Rgb | null {\n if (typeof input !== \"string\") {\n return null;\n }\n\n const trimmed = input.trim();\n\n const match8 = HEX8.exec(trimmed);\n if (match8) {\n // Only treat fully-opaque (alpha = ff) as a valid RGB color; partial\n // alpha can't be flattened without knowing the underlay.\n if (match8[4].toLowerCase() !== \"ff\") return null;\n return {\n r: parseInt(match8[1], 16),\n g: parseInt(match8[2], 16),\n b: parseInt(match8[3], 16),\n };\n }\n\n const match6 = HEX6.exec(trimmed);\n if (match6) {\n return {\n r: parseInt(match6[1], 16),\n g: parseInt(match6[2], 16),\n b: parseInt(match6[3], 16),\n };\n }\n\n const match3 = HEX3.exec(trimmed);\n if (match3) {\n return {\n r: parseInt(match3[1] + match3[1], 16),\n g: parseInt(match3[2] + match3[2], 16),\n b: parseInt(match3[3] + match3[3], 16),\n };\n }\n\n return null;\n}\n\nexport function isOpaqueHex(input: string | undefined | null): boolean {\n return parseHex(input ?? \"\") !== null;\n}\n\nfunction relativeLuminance({ r, g, b }: Rgb): number {\n const rs = channel(r / 255);\n const gs = channel(g / 255);\n const bs = channel(b / 255);\n return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;\n}\n\nfunction channel(c: number): number {\n return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n}\n","import type { Block, TemplateContent } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { WalkContext } from \"./types\";\nimport { isOpaqueHex } from \"./contrast\";\n\nexport type Visitor = (block: Block, ctx: WalkContext) => void;\n\nconst DEFAULT_BG = \"#ffffff\";\n\n/**\n * Pure traversal of the block tree. Calls `visit` once per block in\n * document order, providing a `WalkContext` that includes the resolved\n * background color (nearest opaque ancestor) and structural refs.\n *\n * Sections cannot nest (renderer enforces this), so the walker doesn't\n * descend into a section that lives inside a column. Custom blocks are\n * visited but not descended into.\n */\nexport function walkBlocks(content: TemplateContent, visit: Visitor): void {\n const rootBg = isOpaqueHex(content.settings.backgroundColor)\n ? content.settings.backgroundColor.toLowerCase()\n : DEFAULT_BG;\n\n const walk = (block: Block, ctx: WalkContext): void => {\n // A block's own opaque backgroundColor is what's behind its content —\n // visit it with that resolved bg so contrast rules compare against the\n // right surface. Falls back to the inherited section/template bg.\n const ownBg = block.styles?.backgroundColor;\n const effectiveBg = isOpaqueHex(ownBg)\n ? (ownBg as string).toLowerCase()\n : ctx.resolvedBackgroundColor;\n const blockCtx: WalkContext =\n effectiveBg === ctx.resolvedBackgroundColor\n ? ctx\n : { ...ctx, resolvedBackgroundColor: effectiveBg };\n\n visit(block, blockCtx);\n\n if (!isSection(block)) {\n return;\n }\n\n block.children.forEach((column, columnIndex) => {\n column.forEach((child) =>\n walk(child, {\n parent: block,\n section: block,\n columnIndex,\n depth: ctx.depth + 1,\n resolvedBackgroundColor: effectiveBg,\n }),\n );\n });\n };\n\n for (const block of content.blocks) {\n walk(block, {\n parent: null,\n section: null,\n columnIndex: null,\n depth: 0,\n resolvedBackgroundColor: rootBg,\n });\n }\n}\n","import type { TemplateContent } from \"@templatical/types\";\nimport type {\n AccessibilityLintOptions,\n LinksLintOptions,\n LintIssue,\n ResolvedOptions,\n Rule,\n RuleHit,\n RuleOverrides,\n Severity,\n StructureLintOptions,\n} from \"./types\";\nimport { DEFAULT_A11Y_THRESHOLDS, DEFAULT_NON_PRODUCTION_HOSTS } from \"./types\";\nimport { walkBlocks } from \"./walk\";\n\nexport type MessageFormatter = (\n locale: string,\n ruleId: string,\n params?: Record<string, string | number>,\n) => string;\n\n/**\n * Walk the tree once, dispatch every block-level rule, then run every\n * template-level rule. Each tool (lintAccessibility, lintStructure, …)\n * wraps this with its own rule list + message formatter and a pre-built\n * `ResolvedOptions` containing that tool's overrides and tool-scoped config.\n */\nexport function runRules(\n content: TemplateContent,\n rules: Rule[],\n opts: ResolvedOptions,\n formatMessage: MessageFormatter,\n): LintIssue[] {\n const issues: LintIssue[] = [];\n\n function buildIssue(\n ruleId: string,\n severity: Exclude<Severity, \"off\">,\n hit: RuleHit,\n ): LintIssue {\n return {\n blockId: hit.blockId,\n ruleId,\n severity,\n message: formatMessage(opts.locale, ruleId, hit.params),\n fix: hit.fix,\n };\n }\n\n walkBlocks(content, (block, ctx) => {\n for (const rule of rules) {\n const sev = opts.severity(rule.meta.id);\n if (sev === \"off\" || !rule.block) continue;\n const hit = rule.block(block, ctx, opts);\n if (hit !== null) {\n issues.push(buildIssue(rule.meta.id, sev, hit));\n }\n }\n });\n\n for (const rule of rules) {\n const sev = opts.severity(rule.meta.id);\n if (sev === \"off\" || !rule.template) continue;\n const hits = rule.template(content, opts);\n for (const hit of hits) {\n issues.push(buildIssue(rule.meta.id, sev, hit));\n }\n }\n\n return issues;\n}\n\n/**\n * Build a `ResolvedOptions` for a given tool. Each tool wrapper passes its\n * own per-tool bag; fields not relevant to the tool fall back to defaults\n * (e.g. `lintStructure` still gets `thresholds` populated, but no structure\n * rule reads them).\n */\nexport function resolveOptions(args: {\n locale: string | undefined;\n rules: Rule[];\n overrides: RuleOverrides | undefined;\n thresholds: Partial<import(\"./types\").LintThresholds> | undefined;\n nonProductionHosts: string[] | undefined;\n}): ResolvedOptions {\n const overrides = args.overrides ?? {};\n const thresholds = {\n ...DEFAULT_A11Y_THRESHOLDS,\n ...(args.thresholds ?? {}),\n };\n const links = {\n nonProductionHosts: args.nonProductionHosts ?? DEFAULT_NON_PRODUCTION_HOSTS,\n };\n const locale = args.locale ?? \"en\";\n const rules = args.rules;\n\n return {\n locale,\n rules: overrides,\n thresholds,\n links,\n severity: (ruleId: string): Severity => {\n const override = overrides[ruleId];\n if (override !== undefined) {\n return override;\n }\n const rule = rules.find((r) => r.meta.id === ruleId);\n return rule?.meta.severity ?? \"warning\";\n },\n };\n}\n\n/**\n * Resolver for the accessibility linter — reads `options.accessibility`.\n */\nexport function resolveAccessibilityOptions(\n locale: string | undefined,\n tool: AccessibilityLintOptions,\n rules: Rule[],\n): ResolvedOptions {\n return resolveOptions({\n locale,\n rules,\n overrides: tool.rules,\n thresholds: tool.thresholds,\n nonProductionHosts: undefined,\n });\n}\n\n/**\n * Resolver for the structure linter — reads `options.structure`.\n */\nexport function resolveStructureOptions(\n locale: string | undefined,\n tool: StructureLintOptions,\n rules: Rule[],\n): ResolvedOptions {\n return resolveOptions({\n locale,\n rules,\n overrides: tool.rules,\n thresholds: undefined,\n nonProductionHosts: undefined,\n });\n}\n\n/**\n * Resolver for the links linter — reads `options.links`.\n */\nexport function resolveLinksOptions(\n locale: string | undefined,\n tool: LinksLintOptions,\n rules: Rule[],\n): ResolvedOptions {\n return resolveOptions({\n locale,\n rules,\n overrides: tool.rules,\n thresholds: undefined,\n nonProductionHosts: tool.nonProductionHosts,\n });\n}\n","import type en from \"./en\";\n\nconst de: typeof en = {\n \"a11y.img-missing-alt\":\n \"Bild ohne Alt-Text. Füge eine kurze Beschreibung hinzu oder markiere das Bild als dekorativ.\",\n \"a11y.img-alt-is-filename\":\n 'Alt-Text sieht wie ein Dateiname aus (\"{alt}\"). Beschreibe stattdessen kurz, was das Bild zeigt.',\n \"a11y.img-alt-too-long\":\n \"Alt-Text ist {length} Zeichen lang; bleibe unter {max}.\",\n \"a11y.img-decorative-needs-empty-alt\":\n \"Dekoratives Bild hat Alt-Text. Entferne den Alt-Text oder hebe die Markierung als dekorativ auf.\",\n \"a11y.img-linked-no-context\":\n \"Verlinktes Bild beschreibt nur das Motiv, nicht das Linkziel. Nenne das Ziel (z. B. „Frühlingssale ansehen“).\",\n \"a11y.heading-empty\":\n \"Überschrift ist leer. Füge Text hinzu oder entferne den Block.\",\n \"a11y.heading-skip-level\":\n \"Überschrift springt von H{from} auf H{to}. Eine Ebene pro Schritt.\",\n \"a11y.heading-multiple-h1\":\n \"E-Mail enthält mehr als eine H1. Verwende H1 nur einmal für die Hauptüberschrift.\",\n \"a11y.link-empty\":\n \"Ein Link in diesem Block hat keinen Text und kein beschriebenes Bild.\",\n \"a11y.link-vague-text\":\n \"Link-Text „{text}“ ist unspezifisch. Beschreibe stattdessen das Ziel.\",\n \"a11y.link-href-empty\":\n \"Ein Link in diesem Block hat ein leeres oder „#“-href.\",\n \"a11y.link-target-blank-no-rel\":\n 'Link öffnet in neuem Tab, aber rel=\"noopener\" fehlt – ergänze es, damit das Ziel nicht auf window.opener zugreifen kann.',\n \"a11y.link-nested-anchor\":\n \"Ein Link liegt innerhalb eines anderen Links. Verschachtelte Anker sind ungültiges HTML und werden von E-Mail-Clients unterschiedlich gerendert – flache einen einzigen Anker daraus.\",\n \"a11y.text-all-caps\":\n \"Längere Texte in Großbuchstaben sind schwerer lesbar. Verwende Groß- und Kleinschreibung.\",\n \"a11y.text-low-contrast\":\n \"Überschriftskontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"a11y.text-too-small\": \"Text ist {size}px; mindestens {min}px verwenden.\",\n \"a11y.button-vague-label\":\n \"Button-Beschriftung „{text}“ ist unspezifisch. Beschreibe die Aktion.\",\n \"a11y.button-touch-target\":\n \"Button ist etwa {height}px hoch; mindestens {min}px verwenden, um Fehltipper auf Mobilgeräten zu vermeiden.\",\n \"a11y.button-low-contrast\":\n \"Buttontextkontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"a11y.missing-preheader\":\n \"Kein Preheader-Text gesetzt. Postfächer zeigen sonst Bruchstücke des ersten Blocks an.\",\n};\n\nexport default de;\n","/**\n * English rule messages. The source of truth — other locales annotate\n * themselves `typeof en` so missing or extra keys fail typecheck.\n *\n * Templates use `{name}` placeholders, interpolated by `formatMessage`.\n */\nconst en = {\n \"a11y.img-missing-alt\":\n \"Image is missing alt text. Add a short description, or mark the image as decorative.\",\n \"a11y.img-alt-is-filename\":\n 'Alt text looks like a filename (\"{alt}\"). Replace with a short description of what the image conveys.',\n \"a11y.img-alt-too-long\":\n \"Alt text is {length} characters; aim for under {max}.\",\n \"a11y.img-decorative-needs-empty-alt\":\n \"Decorative image has alt text. Either clear the alt text or unmark the image as decorative.\",\n \"a11y.img-linked-no-context\":\n \"Linked image alt describes the image but not the link destination. Include where the link goes (e.g. 'Buy spring sale').\",\n \"a11y.heading-empty\": \"Heading is empty. Add text or remove the block.\",\n \"a11y.heading-skip-level\":\n \"Heading jumps from H{from} to H{to}. Step one level at a time.\",\n \"a11y.heading-multiple-h1\":\n \"Email has more than one H1. Use H1 once for the main heading.\",\n \"a11y.link-empty\": \"A link in this block has no text and no described image.\",\n \"a11y.link-vague-text\":\n 'Link text \"{text}\" is vague. Describe the destination instead.',\n \"a11y.link-href-empty\": \"A link in this block has an empty or '#' href.\",\n \"a11y.link-target-blank-no-rel\":\n 'Link opens in a new tab but is missing rel=\"noopener\" — add it to prevent the destination from accessing window.opener.',\n \"a11y.link-nested-anchor\":\n \"A link is nested inside another link. Nested anchors are invalid HTML and clients render them inconsistently — flatten to a single anchor.\",\n \"a11y.text-all-caps\":\n \"Long all-caps text is harder to read for everyone. Use sentence case.\",\n \"a11y.text-low-contrast\":\n \"Heading contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"a11y.text-too-small\": \"Text is {size}px; aim for at least {min}px.\",\n \"a11y.button-vague-label\":\n 'Button label \"{text}\" is vague. Describe the action.',\n \"a11y.button-touch-target\":\n \"Button is roughly {height}px tall; aim for at least {min}px to avoid mis-taps on mobile.\",\n \"a11y.button-low-contrast\":\n \"Button text contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"a11y.missing-preheader\":\n \"No preheader text set. Inboxes will fall back to fragments of the first block.\",\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type MessageMap = typeof en;\nexport type RuleMessageId = keyof MessageMap;\n\n/**\n * Auto-discovered locale registry. Drop a `messages/<lang>.ts` file and\n * it's bundled automatically — same pattern as the editor's i18n.\n *\n * Eager glob: synchronous, all locales bundled into the package. Tiny\n * (a few hundred bytes per locale) so the cost is negligible compared\n * to the lazy-loading overhead.\n */\nconst modules = import.meta.glob<{ default: MessageMap }>(\"./*.ts\", {\n eager: true,\n});\n\nconst MESSAGES: Record<string, MessageMap> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n MESSAGES[locale] = modules[path].default;\n}\n\nexport const SUPPORTED_MESSAGE_LOCALES = Object.keys(MESSAGES);\n\nexport function getMessages(locale: string): MessageMap {\n const base = locale.split(\"-\")[0]?.toLowerCase() ?? \"en\";\n return MESSAGES[base] ?? MESSAGES.en ?? en;\n}\n\n/**\n * Resolve a localized message for a rule. `params` interpolate `{name}`\n * placeholders. Falls back to English when the locale doesn't ship the\n * key (shouldn't happen — the parity test enforces it).\n */\nexport function formatMessage(\n locale: string,\n ruleId: RuleMessageId,\n params?: Record<string, string | number>,\n): string {\n const map = getMessages(locale);\n const template = map[ruleId] ?? en[ruleId];\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, key: string) => {\n const value = params[key];\n return value === undefined ? `{${key}}` : String(value);\n });\n}\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-missing-alt\",\n severity: \"error\",\n};\n\nexport const imgMissingAlt: Rule = {\n meta,\n block(block) {\n if (!isImage(block)) return null;\n if (block.decorative === true) return null;\n const alt = block.alt?.trim() ?? \"\";\n if (alt !== \"\") return null;\n if ((block.src ?? \"\").trim() === \"\") return null;\n return { blockId: block.id };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-alt-is-filename\",\n severity: \"warning\",\n};\n\nconst FILENAME_PATTERNS: RegExp[] = [\n /\\.(jpe?g|png|gif|webp|svg)$/i,\n /^IMG[_-]?\\d+/i,\n /^Untitled/i,\n /^Screen[\\s_-]?Shot/i,\n /^DSC[_-]?\\d+/i,\n];\n\nexport const imgAltIsFilename: Rule = {\n meta,\n block(block) {\n if (!isImage(block) || block.decorative === true) return null;\n const alt = block.alt?.trim() ?? \"\";\n if (alt === \"\") return null;\n if (!FILENAME_PATTERNS.some((re) => re.test(alt))) return null;\n\n return {\n blockId: block.id,\n params: { alt },\n };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-alt-too-long\",\n severity: \"warning\",\n};\n\nexport const imgAltTooLong: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isImage(block) || block.decorative === true) return null;\n const alt = block.alt ?? \"\";\n if (alt.length <= opts.thresholds.altMaxLength) return null;\n return {\n blockId: block.id,\n params: { length: alt.length, max: opts.thresholds.altMaxLength },\n };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-decorative-needs-empty-alt\",\n severity: \"info\",\n};\n\nexport const imgDecorativeNeedsEmptyAlt: Rule = {\n meta,\n block(block) {\n if (!isImage(block)) return null;\n if (block.decorative !== true) return null;\n if ((block.alt ?? \"\").trim() === \"\") return null;\n return {\n blockId: block.id,\n fix: {\n description: \"Clear alt text\",\n apply: (ctx) => ctx.updateBlock(block.id, { alt: \"\" }),\n },\n };\n },\n};\n","import type en from \"./en\";\n\nconst de: typeof en = {\n vagueLinkText: [\n \"hier klicken\",\n \"hier\",\n \"mehr lesen\",\n \"mehr\",\n \"weiter\",\n \"weiterlesen\",\n \"siehe mehr\",\n \"dies\",\n \"dieser link\",\n \"link\",\n \"klick\",\n ],\n vagueButtonLabels: [\n \"hier klicken\",\n \"klicken\",\n \"senden\",\n \"los\",\n \"ok\",\n \"okay\",\n \"ja\",\n \"nein\",\n ],\n linkedImageActionHints: [\n \"kaufen\",\n \"shoppen\",\n \"ansehen\",\n \"lesen\",\n \"lernen\",\n \"öffnen\",\n \"los\",\n \"sehen\",\n \"entdecken\",\n \"erkunden\",\n \"stöbern\",\n \"herunterladen\",\n \"holen\",\n \"abholen\",\n \"einlösen\",\n \"anschauen\",\n \"jetzt\",\n ],\n};\n\nexport default de;\n","/**\n * English vague-text dictionaries. Treated as the source of truth — other\n * locales annotate themselves `typeof en` so missing/extra phrases fail\n * typecheck.\n *\n * Phrases are matched case-insensitively against trimmed text content.\n */\nconst en = {\n vagueLinkText: [\n \"click here\",\n \"here\",\n \"read more\",\n \"more\",\n \"learn more\",\n \"see more\",\n \"this\",\n \"this link\",\n \"link\",\n \"click\",\n ],\n vagueButtonLabels: [\n \"click here\",\n \"click\",\n \"submit\",\n \"go\",\n \"ok\",\n \"okay\",\n \"yes\",\n \"no\",\n ],\n /**\n * Action verbs that signal a linked image's alt describes the link\n * destination, not just the visual subject. Used by `img-linked-no-context`.\n * Stored lowercase; tokenized matching is case-insensitive.\n */\n linkedImageActionHints: [\n \"buy\",\n \"shop\",\n \"view\",\n \"read\",\n \"learn\",\n \"open\",\n \"go\",\n \"see\",\n \"explore\",\n \"discover\",\n \"browse\",\n \"download\",\n \"get\",\n \"claim\",\n \"redeem\",\n \"watch\",\n ],\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type Dictionary = typeof en;\n\n/**\n * Auto-discovered locale registry. Drop a `dictionaries/<lang>.ts` file\n * and it's bundled automatically — same pattern as the editor's i18n\n * and the sibling `messages/` registry.\n *\n * Eager glob: synchronous, all locales bundled into the package. Tiny\n * (a few hundred bytes per locale) so the cost is negligible.\n */\nconst modules = import.meta.glob<{ default: Dictionary }>(\"./*.ts\", {\n eager: true,\n});\n\nconst DICTIONARIES: Record<string, Dictionary> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n DICTIONARIES[locale] = modules[path].default;\n}\n\n/**\n * Returns a dictionary that unions every registered locale. Vague phrases\n * are universally vague — a German-locale email with an English \"Click here\"\n * CTA, or an English email with a French \"cliquez ici\", is still a vague\n * CTA, so the rule must detect across languages regardless of editor locale.\n *\n * The `locale` argument is accepted for API symmetry and future use (e.g.\n * weighted matching) but currently doesn't change the returned set.\n */\nexport function getDictionary(_locale: string): Dictionary {\n return UNIONED_DICTIONARY;\n}\n\nfunction unionAll(pick: (d: Dictionary) => readonly string[]): string[] {\n const set = new Set<string>();\n for (const dict of Object.values(DICTIONARIES)) {\n for (const phrase of pick(dict)) set.add(phrase);\n }\n return Array.from(set);\n}\n\nconst UNIONED_DICTIONARY: Dictionary = {\n vagueLinkText: unionAll((d) => d.vagueLinkText),\n vagueButtonLabels: unionAll((d) => d.vagueButtonLabels),\n linkedImageActionHints: unionAll((d) => d.linkedImageActionHints),\n};\n\nexport const SUPPORTED_DICTIONARY_LOCALES = Object.keys(DICTIONARIES);\n\n/**\n * Normalize text for dictionary matching: lowercase, collapse whitespace,\n * strip leading/trailing non-alphanumeric characters (punctuation, arrows,\n * emoji, decorative symbols). \"Click here!\", \"→ click here\", \"click here?\"\n * all collapse to \"click here\" so the dictionary's plain phrase matches.\n */\nexport function normalizeForMatch(input: string): string {\n return input\n .toLowerCase()\n .replace(/\\s+/g, \" \")\n .replace(/^[^\\p{L}\\p{N}]+|[^\\p{L}\\p{N}]+$/gu, \"\")\n .trim();\n}\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getDictionary } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.img-linked-no-context\",\n severity: \"warning\",\n};\n\nexport const imgLinkedNoContext: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isImage(block) || block.decorative === true) return null;\n if (!block.linkUrl || block.linkUrl.trim() === \"\") return null;\n const alt = (block.alt ?? \"\").trim();\n if (alt === \"\") return null;\n const tokens = alt\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n const hints = getDictionary(opts.locale).linkedImageActionHints;\n if (tokens.some((token) => hints.includes(token))) return null;\n return { blockId: block.id };\n },\n};\n","import { Parser } from \"htmlparser2\";\n\nexport interface AnchorInfo {\n href: string;\n text: string;\n target: string | null;\n rel: string | null;\n /** True if the anchor wraps an image with non-empty alt. */\n hasImageWithAlt: boolean;\n}\n\n/**\n * Extract every anchor from a TipTap-style HTML fragment. Used by\n * link-* rules. Doesn't try to be a full DOM — only the data the rules\n * need.\n *\n * Nested `<a>` is invalid HTML; htmlparser2 follows the HTML5 spec and\n * emits an implicit `</a>` when a second `<a>` opens, so anchors are\n * effectively flat siblings. We mirror that with a single in-flight\n * anchor (no stack); a defensive finalize-on-reopen handles the\n * theoretical case where the parser ever stops emitting the implicit\n * close. Detecting nested-anchor markup as its own concern lives in\n * the `a11y.link-nested-anchor` rule, which inspects the raw input\n * before this normalization.\n */\nexport function extractAnchors(html: string): AnchorInfo[] {\n const anchors: AnchorInfo[] = [];\n let current: AnchorInfo | null = null;\n let buffer = \"\";\n\n const finalize = () => {\n if (current === null) return;\n current.text = buffer.trim();\n anchors.push(current);\n current = null;\n buffer = \"\";\n };\n\n const parser = new Parser({\n onopentag(name, attribs) {\n if (name === \"a\") {\n finalize();\n current = {\n href: attribs.href ?? \"\",\n text: \"\",\n target: attribs.target ?? null,\n rel: attribs.rel ?? null,\n hasImageWithAlt: false,\n };\n return;\n }\n\n if (name === \"img\" && current !== null) {\n const alt = (attribs.alt ?? \"\").trim();\n if (alt !== \"\") {\n current.hasImageWithAlt = true;\n }\n }\n },\n ontext(text) {\n if (current !== null) {\n buffer += text;\n }\n },\n onclosetag(name) {\n if (name === \"a\") {\n finalize();\n }\n },\n });\n\n parser.write(html);\n parser.end();\n finalize();\n\n return anchors;\n}\n\n/**\n * Whether the raw HTML contains an `<a>` opened inside another open\n * `<a>` — invalid markup that htmlparser2 silently normalizes by\n * emitting an implicit `</a>` before the inner open. `extractAnchors`\n * runs against the normalized parse and therefore can't distinguish\n * nested-from-sibling input; this helper inspects the raw text so the\n * `a11y.link-nested-anchor` rule can flag the structural problem.\n *\n * Tokenization here ignores anchor-like tokens inside HTML comments,\n * which is enough for TipTap email-template HTML. CDATA, `<script>`,\n * and attribute-value occurrences aren't expected in this surface.\n */\nexport function hasNestedAnchors(html: string): boolean {\n const stripped = stripHtmlComments(html);\n const tokens = stripped.matchAll(/<\\/?a\\b[^<>]*>/gi);\n let depth = 0;\n for (const match of tokens) {\n if (match[0].startsWith(\"</\")) {\n if (depth > 0) depth--;\n continue;\n }\n if (depth > 0) return true;\n depth++;\n }\n return false;\n}\n\n/**\n * Linear-time `<!-- … -->` stripper. Replaces the original\n * `replace(/<!--[\\s\\S]*?-->/g, \"\")`, which had two related problems:\n *\n * - Polynomial ReDoS: each `<!--` start re-scanned the rest of the\n * input for `-->`, costing O(n²) over inputs like `<!--<!--<!--…`.\n * - Incomplete sanitization: an unterminated `<!--` left the literal\n * `<!--` in the output, so downstream rendering could still parse it\n * as an open HTML comment and swallow the rest of the document.\n *\n * The scanner discards an unterminated comment entirely — there is no\n * way to recover meaningful content after a stray `<!--`, and leaving\n * it in would re-introduce the sanitizer gap.\n */\nfunction stripHtmlComments(html: string): string {\n let out = \"\";\n let i = 0;\n while (i < html.length) {\n const start = html.indexOf(\"<!--\", i);\n if (start === -1) {\n out += html.substring(i);\n break;\n }\n out += html.substring(i, start);\n const end = html.indexOf(\"-->\", start + 4);\n if (end === -1) {\n // Unterminated comment — drop everything from this point onward.\n break;\n }\n i = end + 3;\n }\n return out;\n}\n\n/**\n * Strip tags and return the visible text content of an HTML fragment.\n * Used by heading-empty and other text-presence rules.\n */\nexport function extractText(html: string): string {\n let text = \"\";\n const parser = new Parser({\n ontext(chunk) {\n text += chunk;\n },\n });\n parser.write(html);\n parser.end();\n return text.trim();\n}\n","import { isTitle } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractText } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.heading-empty\",\n severity: \"error\",\n};\n\nexport const headingEmpty: Rule = {\n meta,\n block(block) {\n if (!isTitle(block)) return null;\n const text = extractText(block.content ?? \"\");\n if (text !== \"\") return null;\n return { blockId: block.id };\n },\n};\n","import { isTitle, isSection } from \"@templatical/types\";\nimport type { Block, TitleBlock, TemplateContent } from \"@templatical/types\";\nimport type { Rule, RuleHit, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.heading-skip-level\",\n severity: \"error\",\n};\n\nfunction collectTitles(blocks: Block[], out: TitleBlock[]): void {\n for (const block of blocks) {\n if (isTitle(block)) {\n out.push(block);\n continue;\n }\n if (isSection(block)) {\n for (const column of block.children) {\n collectTitles(column, out);\n }\n }\n }\n}\n\nexport const headingSkipLevel: Rule = {\n meta,\n template(content: TemplateContent) {\n const titles: TitleBlock[] = [];\n collectTitles(content.blocks, titles);\n\n const hits: RuleHit[] = [];\n let lastLevel = 0;\n\n for (const title of titles) {\n if (lastLevel !== 0 && title.level > lastLevel + 1) {\n hits.push({\n blockId: title.id,\n params: { from: lastLevel, to: title.level },\n });\n }\n lastLevel = title.level;\n }\n\n return hits;\n },\n};\n","import { isTitle, isSection } from \"@templatical/types\";\nimport type { Block, TitleBlock, TemplateContent } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.heading-multiple-h1\",\n severity: \"warning\",\n};\n\nfunction collectTitles(blocks: Block[], out: TitleBlock[]): void {\n for (const block of blocks) {\n if (isTitle(block)) {\n out.push(block);\n continue;\n }\n if (isSection(block)) {\n for (const column of block.children) {\n collectTitles(column, out);\n }\n }\n }\n}\n\nexport const headingMultipleH1: Rule = {\n meta,\n template(content: TemplateContent) {\n const titles: TitleBlock[] = [];\n collectTitles(content.blocks, titles);\n const h1s = titles.filter((t) => t.level === 1);\n if (h1s.length <= 1) return [];\n return h1s.slice(1).map((title) => ({ blockId: title.id }));\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-empty\",\n severity: \"error\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkEmpty: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n\n const anchors = extractAnchors(html);\n const offender = anchors.find(\n (anchor) => anchor.text === \"\" && !anchor.hasImageWithAlt,\n );\n if (!offender) return null;\n\n return { blockId: block.id };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\nimport { getDictionary, normalizeForMatch } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-vague-text\",\n severity: \"warning\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkVagueText: Rule = {\n meta,\n block(block, _ctx, opts) {\n const html = getHtml(block);\n if (html === null) return null;\n\n const phrases = getDictionary(opts.locale).vagueLinkText;\n const anchors = extractAnchors(html);\n const offender = anchors.find((a) => {\n const text = normalizeForMatch(a.text);\n return text !== \"\" && phrases.includes(text);\n });\n if (!offender) return null;\n\n return { blockId: block.id, params: { text: offender.text } };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-href-empty\",\n severity: \"error\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkHrefEmpty: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n const anchors = extractAnchors(html);\n const offender = anchors.find((a) => {\n const href = a.href.trim();\n return href === \"\" || href === \"#\";\n });\n if (!offender) return null;\n return { blockId: block.id };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-target-blank-no-rel\",\n severity: \"warning\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nfunction hasSafeRel(rel: string | null): boolean {\n if (rel === null) return false;\n const tokens = rel.toLowerCase().split(/\\s+/);\n return tokens.includes(\"noopener\") || tokens.includes(\"noreferrer\");\n}\n\nexport const linkTargetBlankNoRel: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n const anchors = extractAnchors(html);\n const offender = anchors.find(\n (a) => a.target === \"_blank\" && !hasSafeRel(a.rel),\n );\n if (!offender) return null;\n\n return {\n blockId: block.id,\n fix: {\n description: 'Add rel=\"noopener\"',\n apply: (ctx) => {\n if (!isParagraph(block) && !isTitle(block)) return;\n const updated = addNoopenerToTargetBlank(block.content ?? \"\");\n ctx.updateBlock(block.id, { content: updated } as Partial<Block>);\n },\n },\n };\n },\n};\n\ninterface ParsedAttr {\n raw: string;\n name: string;\n value: string | null;\n /** Start offset of `raw` within the parent attrs string. */\n start: number;\n}\n\nconst ATTR_RE =\n /([^\\s\"'>/=]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g;\n\nfunction parseAttrs(attrs: string): ParsedAttr[] {\n const parsed: ParsedAttr[] = [];\n const re = new RegExp(ATTR_RE.source, ATTR_RE.flags);\n let match: RegExpExecArray | null;\n while ((match = re.exec(attrs)) !== null) {\n const value = match[2] ?? match[3] ?? match[4] ?? null;\n parsed.push({\n raw: match[0],\n name: match[1],\n value,\n start: match.index,\n });\n }\n return parsed;\n}\n\nfunction hasUnsafeTargetBlank(parsed: ParsedAttr[]): boolean {\n return parsed.some(\n (a) =>\n a.name.toLowerCase() === \"target\" &&\n a.value !== null &&\n a.value.toLowerCase() === \"_blank\",\n );\n}\n\nfunction addNoopenerToTargetBlank(html: string): string {\n return html.replace(/<a\\b([^>]*)>/gi, (match, attrs: string) => {\n const parsed = parseAttrs(attrs);\n if (!hasUnsafeTargetBlank(parsed)) return match;\n\n const relAttr = parsed.find((a) => a.name.toLowerCase() === \"rel\");\n if (relAttr) {\n const tokens = (relAttr.value ?? \"\").toLowerCase().split(/\\s+/);\n if (tokens.includes(\"noopener\") || tokens.includes(\"noreferrer\")) {\n return match;\n }\n const newRel = `${relAttr.value ?? \"\"} noopener`.trim();\n const before = attrs.slice(0, relAttr.start);\n const after = attrs.slice(relAttr.start + relAttr.raw.length);\n return `<a${before}rel=\"${newRel}\"${after}>`;\n }\n return `<a${attrs} rel=\"noopener\">`;\n });\n}\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { hasNestedAnchors } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.link-nested-anchor\",\n severity: \"error\",\n};\n\nfunction getHtml(block: Block): string | null {\n if (isParagraph(block) || isTitle(block)) return block.content;\n return null;\n}\n\nexport const linkNestedAnchor: Rule = {\n meta,\n block(block) {\n const html = getHtml(block);\n if (html === null) return null;\n if (!hasNestedAnchors(html)) return null;\n return { blockId: block.id };\n },\n};\n","import { isParagraph, isTitle } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { extractText } from \"../../html-utils\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.text-all-caps\",\n severity: \"warning\",\n};\n\nexport const textAllCaps: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isParagraph(block) && !isTitle(block)) return null;\n const text = extractText(block.content ?? \"\");\n const letters = text.replace(/[^\\p{L}]/gu, \"\");\n if (letters.length < opts.thresholds.allCapsMinLength) return null;\n if (letters !== letters.toLocaleUpperCase()) return null;\n return { blockId: block.id };\n },\n};\n","import { isTitle } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getContrastRatio, isOpaqueHex } from \"../../contrast\";\nimport { HEADING_LEVEL_FONT_SIZE } from \"@templatical/types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.text-low-contrast\",\n severity: \"error\",\n};\n\nexport const textLowContrast: Rule = {\n meta,\n block(block, ctx) {\n if (!isTitle(block)) return null;\n if (\n !isOpaqueHex(block.color) ||\n !isOpaqueHex(ctx.resolvedBackgroundColor)\n ) {\n return null;\n }\n const fontSize = HEADING_LEVEL_FONT_SIZE[block.level];\n // WCAG large text = 18pt (~24px). Headings have no structured bold\n // flag in this codebase (TipTap stores it inline), so we conservatively\n // skip the 14pt-bold (~18.66px) relaxation and apply the px threshold.\n const required = fontSize >= 24 ? 3 : 4.5;\n const ratio = getContrastRatio(block.color, ctx.resolvedBackgroundColor);\n if (Number.isNaN(ratio) || ratio >= required) return null;\n return {\n blockId: block.id,\n params: { ratio: ratio.toFixed(2), required },\n };\n },\n};\n","import { isMenu, isTable } from \"@templatical/types\";\nimport type { Block } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.text-too-small\",\n severity: \"warning\",\n};\n\nfunction getFontSize(block: Block): number | null {\n if (isMenu(block) || isTable(block)) return block.fontSize;\n return null;\n}\n\nexport const textTooSmall: Rule = {\n meta,\n block(block, _ctx, opts) {\n const fontSize = getFontSize(block);\n if (fontSize === null) return null;\n if (fontSize >= opts.thresholds.minFontSize) return null;\n return {\n blockId: block.id,\n params: { size: fontSize, min: opts.thresholds.minFontSize },\n };\n },\n};\n","import { isButton } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getDictionary, normalizeForMatch } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.button-vague-label\",\n severity: \"warning\",\n};\n\nexport const buttonVagueLabel: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isButton(block)) return null;\n const text = normalizeForMatch(block.text ?? \"\");\n if (text === \"\") return null;\n const phrases = getDictionary(opts.locale).vagueButtonLabels;\n if (!phrases.includes(text)) return null;\n return { blockId: block.id, params: { text: block.text } };\n },\n};\n","import { isButton } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.button-touch-target\",\n severity: \"warning\",\n};\n\nexport const buttonTouchTarget: Rule = {\n meta,\n block(block, _ctx, opts) {\n if (!isButton(block)) return null;\n const padding = block.buttonPadding;\n if (!padding) return null;\n const estimatedHeight = block.fontSize * 1.4 + padding.top + padding.bottom;\n if (estimatedHeight >= opts.thresholds.minTouchTargetPx) return null;\n return {\n blockId: block.id,\n params: {\n height: Math.round(estimatedHeight),\n min: opts.thresholds.minTouchTargetPx,\n },\n };\n },\n};\n","import { isButton } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\nimport { getContrastRatio } from \"../../contrast\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.button-low-contrast\",\n severity: \"error\",\n};\n\nexport const buttonLowContrast: Rule = {\n meta,\n block(block) {\n if (!isButton(block)) return null;\n const ratio = getContrastRatio(block.textColor, block.backgroundColor);\n if (Number.isNaN(ratio)) return null;\n // WCAG large text = 18pt (~24px). Mirrors the heading rule's threshold.\n const required = block.fontSize >= 24 ? 3 : 4.5;\n if (ratio >= required) return null;\n return {\n blockId: block.id,\n params: { ratio: ratio.toFixed(2), required },\n };\n },\n};\n","import type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"a11y.missing-preheader\",\n severity: \"info\",\n};\n\nexport const missingPreheader: Rule = {\n meta,\n template(content) {\n const text = content.settings.preheaderText?.trim() ?? \"\";\n if (text !== \"\") return [];\n return [{ blockId: null }];\n },\n};\n","import type { TemplateContent } from \"@templatical/types\";\nimport type { LintIssue, LintOptions, Rule } from \"../types\";\nimport { resolveAccessibilityOptions, runRules } from \"../run-rules\";\nimport { formatMessage, type RuleMessageId } from \"./messages\";\nimport { imgMissingAlt } from \"./rules/img-missing-alt\";\nimport { imgAltIsFilename } from \"./rules/img-alt-is-filename\";\nimport { imgAltTooLong } from \"./rules/img-alt-too-long\";\nimport { imgDecorativeNeedsEmptyAlt } from \"./rules/img-decorative-needs-empty-alt\";\nimport { imgLinkedNoContext } from \"./rules/img-linked-no-context\";\nimport { headingEmpty } from \"./rules/heading-empty\";\nimport { headingSkipLevel } from \"./rules/heading-skip-level\";\nimport { headingMultipleH1 } from \"./rules/heading-multiple-h1\";\nimport { linkEmpty } from \"./rules/link-empty\";\nimport { linkVagueText } from \"./rules/link-vague-text\";\nimport { linkHrefEmpty } from \"./rules/link-href-empty\";\nimport { linkTargetBlankNoRel } from \"./rules/link-target-blank-no-rel\";\nimport { linkNestedAnchor } from \"./rules/link-nested-anchor\";\nimport { textAllCaps } from \"./rules/text-all-caps\";\nimport { textLowContrast } from \"./rules/text-low-contrast\";\nimport { textTooSmall } from \"./rules/text-too-small\";\nimport { buttonVagueLabel } from \"./rules/button-vague-label\";\nimport { buttonTouchTarget } from \"./rules/button-touch-target\";\nimport { buttonLowContrast } from \"./rules/button-low-contrast\";\nimport { missingPreheader } from \"./rules/missing-preheader\";\n\nexport const ACCESSIBILITY_RULES: Rule[] = [\n imgMissingAlt,\n imgAltIsFilename,\n imgAltTooLong,\n imgDecorativeNeedsEmptyAlt,\n imgLinkedNoContext,\n headingEmpty,\n headingSkipLevel,\n headingMultipleH1,\n linkEmpty,\n linkVagueText,\n linkHrefEmpty,\n linkTargetBlankNoRel,\n linkNestedAnchor,\n textAllCaps,\n textLowContrast,\n textTooSmall,\n buttonVagueLabel,\n buttonTouchTarget,\n buttonLowContrast,\n missingPreheader,\n];\n\nexport function lintAccessibility(\n content: TemplateContent,\n options: LintOptions = {},\n): LintIssue[] {\n if (options.disabled === true || options.accessibility === false) return [];\n const tool = options.accessibility ?? {};\n const resolved = resolveAccessibilityOptions(\n options.locale,\n tool,\n ACCESSIBILITY_RULES,\n );\n return runRules(\n content,\n ACCESSIBILITY_RULES,\n resolved,\n (locale, id, params) => formatMessage(locale, id as RuleMessageId, params),\n );\n}\n","import type en from \"./en\";\n\nconst de: typeof en = {\n \"structure.duplicate-block-id\":\n \"Block-ID erscheint {count}-mal im Baum. Jeder Block muss eine eindeutige ID haben.\",\n \"structure.section-column-mismatch\":\n 'Sektion verwendet Layout „{layout}\" (erwartet {expected} Spalten), hat aber {actual}. Deutet auf beschädigten Zustand hin.',\n \"structure.nested-section\":\n \"Sektion ist in einer anderen Sektion verschachtelt. Sektionen können nicht verschachtelt werden – der Renderer wird sich falsch verhalten.\",\n \"structure.empty-section\":\n \"Sektion enthält keine Blöcke. Entferne sie oder füge Inhalt hinzu.\",\n \"structure.empty-column\":\n \"Spalte {columnIndex} dieser Sektion ist leer. Füge Inhalt hinzu oder reduziere die Spaltenanzahl.\",\n};\n\nexport default de;\n","/**\n * English structure-rule messages. The source of truth — other locales\n * annotate themselves `typeof en` so missing or extra keys fail typecheck.\n *\n * Templates use `{name}` placeholders, interpolated by `formatMessage`.\n */\nconst en = {\n \"structure.duplicate-block-id\":\n \"Block id appears {count} times in the tree. Each block must have a unique id.\",\n \"structure.section-column-mismatch\":\n 'Section uses layout \"{layout}\" (expects {expected} columns) but has {actual}. Indicates corrupted state.',\n \"structure.nested-section\":\n \"Section is nested inside another section. Sections cannot nest — the renderer will misbehave.\",\n \"structure.empty-section\": \"Section has no blocks. Remove it or add content.\",\n \"structure.empty-column\":\n \"Column {columnIndex} of this section is empty. Add content or reduce the column count.\",\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type StructureMessageMap = typeof en;\nexport type StructureRuleMessageId = keyof StructureMessageMap;\n\nconst modules = import.meta.glob<{ default: StructureMessageMap }>(\"./*.ts\", {\n eager: true,\n});\n\nconst MESSAGES: Record<string, StructureMessageMap> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n MESSAGES[locale] = modules[path].default;\n}\n\nexport const SUPPORTED_STRUCTURE_MESSAGE_LOCALES = Object.keys(MESSAGES);\n\nexport function getStructureMessages(locale: string): StructureMessageMap {\n const base = locale.split(\"-\")[0]?.toLowerCase() ?? \"en\";\n return MESSAGES[base] ?? MESSAGES.en ?? en;\n}\n\nexport function formatStructureMessage(\n locale: string,\n ruleId: StructureRuleMessageId,\n params?: Record<string, string | number>,\n): string {\n const map = getStructureMessages(locale);\n const template = map[ruleId] ?? en[ruleId];\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, key: string) => {\n const value = params[key];\n return value === undefined ? `{${key}}` : String(value);\n });\n}\n","import type { Block, SectionBlock, TemplateContent } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleHit, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.duplicate-block-id\",\n severity: \"error\",\n};\n\nfunction collectIds(blocks: Block[], counts: Map<string, number>): void {\n for (const block of blocks) {\n counts.set(block.id, (counts.get(block.id) ?? 0) + 1);\n if (isSection(block)) {\n for (const column of (block as SectionBlock).children) {\n collectIds(column, counts);\n }\n }\n }\n}\n\nexport const duplicateBlockId: Rule = {\n meta,\n template(content: TemplateContent): RuleHit[] {\n const counts = new Map<string, number>();\n collectIds(content.blocks, counts);\n\n const hits: RuleHit[] = [];\n for (const [id, count] of counts) {\n if (count > 1) {\n hits.push({ blockId: id, params: { count } });\n }\n }\n return hits;\n },\n};\n","import type { Block, SectionBlock, TemplateContent } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleHit, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.empty-column\",\n severity: \"warning\",\n};\n\nfunction findEmptyColumns(blocks: Block[], hits: RuleHit[]): void {\n for (const block of blocks) {\n if (!isSection(block)) continue;\n const section = block as SectionBlock;\n if (section.children.length > 1) {\n section.children.forEach((column, columnIndex) => {\n if (column.length === 0) {\n hits.push({\n blockId: section.id,\n params: { columnIndex: columnIndex + 1 },\n });\n }\n });\n }\n for (const column of section.children) {\n findEmptyColumns(column, hits);\n }\n }\n}\n\nexport const emptyColumn: Rule = {\n meta,\n template(content: TemplateContent): RuleHit[] {\n const hits: RuleHit[] = [];\n findEmptyColumns(content.blocks, hits);\n return hits;\n },\n};\n","import type { SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.empty-section\",\n severity: \"warning\",\n};\n\nfunction isSectionEmpty(section: SectionBlock): boolean {\n if (section.children.length === 0) return true;\n return section.children.every((column) => column.length === 0);\n}\n\nexport const emptySection: Rule = {\n meta,\n block(block) {\n if (!isSection(block)) return null;\n const section = block as SectionBlock;\n if (!isSectionEmpty(section)) return null;\n return {\n blockId: section.id,\n fix: {\n description: \"Remove the empty section\",\n apply: (ctx) => {\n ctx.removeBlock(section.id);\n },\n },\n };\n },\n};\n","import type { SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleMeta, WalkContext } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.nested-section\",\n severity: \"error\",\n};\n\nexport const nestedSection: Rule = {\n meta,\n block(block, ctx: WalkContext) {\n if (!isSection(block)) return null;\n if (ctx.section === null) return null;\n const parentSection = ctx.section as SectionBlock;\n return {\n blockId: block.id,\n params: { parentId: parentSection.id },\n };\n },\n};\n","import type { ColumnLayout, SectionBlock } from \"@templatical/types\";\nimport { isSection } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"structure.section-column-mismatch\",\n severity: \"error\",\n};\n\nfunction expectedColumnCount(layout: ColumnLayout): number {\n if (layout === \"1\") return 1;\n if (layout === \"3\") return 3;\n return 2;\n}\n\nexport const sectionColumnMismatch: Rule = {\n meta,\n block(block) {\n if (!isSection(block)) return null;\n const section = block as SectionBlock;\n const expected = expectedColumnCount(section.columns);\n const actual = section.children.length;\n if (actual === expected) return null;\n return {\n blockId: section.id,\n params: { layout: section.columns, expected, actual },\n };\n },\n};\n","import type { TemplateContent } from \"@templatical/types\";\nimport type { LintIssue, LintOptions, Rule } from \"../types\";\nimport { resolveStructureOptions, runRules } from \"../run-rules\";\nimport {\n formatStructureMessage,\n type StructureRuleMessageId,\n} from \"./messages\";\nimport { duplicateBlockId } from \"./rules/duplicate-block-id\";\nimport { emptyColumn } from \"./rules/empty-column\";\nimport { emptySection } from \"./rules/empty-section\";\nimport { nestedSection } from \"./rules/nested-section\";\nimport { sectionColumnMismatch } from \"./rules/section-column-mismatch\";\n\nexport const STRUCTURE_RULES: Rule[] = [\n duplicateBlockId,\n emptySection,\n emptyColumn,\n nestedSection,\n sectionColumnMismatch,\n];\n\nexport function lintStructure(\n content: TemplateContent,\n options: LintOptions = {},\n): LintIssue[] {\n if (options.disabled === true || options.structure === false) return [];\n const tool = options.structure ?? {};\n const resolved = resolveStructureOptions(\n options.locale,\n tool,\n STRUCTURE_RULES,\n );\n return runRules(content, STRUCTURE_RULES, resolved, (locale, id, params) =>\n formatStructureMessage(locale, id as StructureRuleMessageId, params),\n );\n}\n","import type en from \"./en\";\n\nconst de: typeof en = {\n \"link.javascript-protocol\":\n 'Die URL verwendet das Protokoll „{protocol}:\", das beliebigen Skriptcode ausführen kann und aus Sicherheitsgründen beim Rendern entfernt wird. Ersetze sie durch eine echte URL oder entferne sie.',\n \"link.unsupported-protocol\":\n 'Die URL verwendet das Protokoll „{protocol}\", das von den meisten E-Mail-Clients nicht unterstützt wird. Verwende http, https, mailto, tel oder sms.',\n \"link.malformed-mailto\":\n \"Der mailto:-Link ist fehlerhaft. Erwartet wird eine einzelne Empfängeradresse vor einer eventuellen Querystring (z. B. mailto:hallo@example.com).\",\n \"link.malformed-tel\":\n \"Der tel:-Link enthält Zeichen, die keine Ziffern, +, Leerzeichen, Bindestriche, Klammern oder Punkte sind.\",\n \"link.localhost-or-staging\":\n 'Der URL-Host „{host}\" entspricht einem Nicht-Produktionsmuster. Ersetze ihn vor dem Versand durch die Produktions-URL.',\n};\n\nexport default de;\n","/**\n * English link-rule messages. The source of truth — other locales annotate\n * themselves `typeof en` so missing or extra keys fail typecheck.\n *\n * Templates use `{name}` placeholders, interpolated by `formatLinkMessage`.\n */\nconst en = {\n \"link.javascript-protocol\":\n 'URL uses the \"{protocol}:\" protocol, which can execute arbitrary script and is stripped at render time for safety. Replace it with a real link or remove the URL.',\n \"link.unsupported-protocol\":\n 'URL uses the \"{protocol}\" protocol, which most email clients do not support. Use http, https, mailto, tel, or sms.',\n \"link.malformed-mailto\":\n \"mailto: link is malformed. Expected a single recipient address before any query string (e.g. mailto:hello@example.com).\",\n \"link.malformed-tel\":\n \"tel: link contains characters that are not digits, +, spaces, dashes, parentheses, or dots.\",\n \"link.localhost-or-staging\":\n 'URL host \"{host}\" matches a non-production pattern. Replace with the production URL before sending.',\n};\n\nexport default en;\n","import en from \"./en\";\n\nexport type LinkMessageMap = typeof en;\nexport type LinkRuleMessageId = keyof LinkMessageMap;\n\nconst modules = import.meta.glob<{ default: LinkMessageMap }>(\"./*.ts\", {\n eager: true,\n});\n\nconst MESSAGES: Record<string, LinkMessageMap> = {};\nfor (const path in modules) {\n const match = /\\.\\/([^/]+)\\.ts$/.exec(path);\n if (!match) continue;\n const locale = match[1];\n if (locale === \"index\") continue;\n MESSAGES[locale] = modules[path].default;\n}\n\nexport const SUPPORTED_LINK_MESSAGE_LOCALES = Object.keys(MESSAGES);\n\nexport function getLinkMessages(locale: string): LinkMessageMap {\n const base = locale.split(\"-\")[0]?.toLowerCase() ?? \"en\";\n return MESSAGES[base] ?? MESSAGES.en ?? en;\n}\n\nexport function formatLinkMessage(\n locale: string,\n ruleId: LinkRuleMessageId,\n params?: Record<string, string | number>,\n): string {\n const map = getLinkMessages(locale);\n const template = map[ruleId] ?? en[ruleId];\n if (!params) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, key: string) => {\n const value = params[key];\n return value === undefined ? `{${key}}` : String(value);\n });\n}\n","import type { TemplateContent } from \"@templatical/types\";\nimport {\n isButton,\n isHtml,\n isImage,\n isMenu,\n isParagraph,\n isSocialIcons,\n isTitle,\n isVideo,\n} from \"@templatical/types\";\nimport { walkBlocks } from \"./walk\";\nimport { extractAnchors } from \"./html-utils\";\n\nexport type UrlSource =\n | \"anchor\"\n | \"button\"\n | \"image-link\"\n | \"video\"\n | \"menu-item\"\n | \"social-icon\";\n\nexport interface UrlOccurrence {\n url: string;\n blockId: string;\n source: UrlSource;\n /** Anchor text or block-derived label, if applicable. */\n label?: string;\n}\n\n/**\n * Visit every URL-bearing field in the template tree.\n *\n * Sources covered:\n * - anchor — `<a href>` inside `title.content`, `paragraph.content`,\n * `html.content` (parsed via extractAnchors)\n * - button — `button.url`\n * - image-link — `image.linkUrl` (only when present + non-empty)\n * - video — `video.url`\n * - menu-item — `menu.items[i].url`\n * - social-icon — `social.icons[i].url`\n *\n * Each rule iterates this list once and decides per occurrence.\n */\nexport function walkUrls(content: TemplateContent): UrlOccurrence[] {\n const occurrences: UrlOccurrence[] = [];\n\n walkBlocks(content, (block) => {\n if (isTitle(block) || isParagraph(block) || isHtml(block)) {\n for (const anchor of extractAnchors(block.content)) {\n occurrences.push({\n url: anchor.href,\n blockId: block.id,\n source: \"anchor\",\n label: anchor.text,\n });\n }\n return;\n }\n\n if (isButton(block)) {\n occurrences.push({\n url: block.url,\n blockId: block.id,\n source: \"button\",\n label: block.text,\n });\n return;\n }\n\n if (isImage(block)) {\n if (block.linkUrl && block.linkUrl !== \"\") {\n occurrences.push({\n url: block.linkUrl,\n blockId: block.id,\n source: \"image-link\",\n label: block.alt || undefined,\n });\n }\n return;\n }\n\n if (isVideo(block)) {\n occurrences.push({\n url: block.url,\n blockId: block.id,\n source: \"video\",\n label: block.alt || undefined,\n });\n return;\n }\n\n if (isMenu(block)) {\n for (const item of block.items) {\n occurrences.push({\n url: item.url,\n blockId: block.id,\n source: \"menu-item\",\n label: item.text,\n });\n }\n return;\n }\n\n if (isSocialIcons(block)) {\n for (const icon of block.icons) {\n occurrences.push({\n url: icon.url,\n blockId: block.id,\n source: \"social-icon\",\n label: icon.platform,\n });\n }\n return;\n }\n });\n\n return occurrences;\n}\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.javascript-protocol\",\n severity: \"error\",\n};\n\n/**\n * The set of URL schemes that can encode executable script or arbitrary\n * payloads inside an `href`/`src` value. All three are in scope for this\n * rule because a sanitizer that strips `javascript:` but leaves `data:` or\n * `vbscript:` through is the textbook \"incomplete URL scheme check\" bug.\n *\n * Rule ID stays `link.javascript-protocol` for back-compat with consumer\n * severity overrides; the message is parameterized so users see the actual\n * matched protocol.\n */\nexport const DANGEROUS_SCRIPT_PROTOCOLS = [\n \"javascript\",\n \"data\",\n \"vbscript\",\n] as const;\n\ntype DangerousProtocol = (typeof DANGEROUS_SCRIPT_PROTOCOLS)[number];\n\n/**\n * Match dangerous-script schemes even when the value is whitespace-padded or\n * mixed-case. Mirrors what HTML attribute parsers see at insert time —\n * leading whitespace (spaces, tabs, newlines) is stripped before scheme\n * parsing, and unicode-percent-encoded whitespace inside the scheme is\n * rejected by browsers, so a literal-strip is sufficient.\n */\nfunction matchDangerousProtocol(url: string): DangerousProtocol | null {\n if (!url) return null;\n const stripped = url.replace(/\\s+/g, \"\");\n for (const proto of DANGEROUS_SCRIPT_PROTOCOLS) {\n if (new RegExp(`^${proto}:`, \"i\").test(stripped)) return proto;\n }\n return null;\n}\n\nexport const javascriptProtocol: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n const protocol = matchDangerousProtocol(occ.url);\n if (protocol !== null) {\n hits.push({ blockId: occ.blockId, params: { protocol } });\n }\n }\n return hits;\n },\n};\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\nimport { DANGEROUS_SCRIPT_PROTOCOLS } from \"./javascript-protocol\";\n\nexport const meta: RuleMeta = {\n id: \"link.unsupported-protocol\",\n severity: \"warning\",\n};\n\nconst SUPPORTED = new Set([\"http\", \"https\", \"mailto\", \"tel\", \"sms\"]);\nconst DANGEROUS = new Set<string>(DANGEROUS_SCRIPT_PROTOCOLS);\n\n/**\n * Treat dangerous-script schemes (covered by `link.javascript-protocol`) and\n * bare/relative URLs as \"not unsupported\" — this rule fires only for\n * explicitly named schemes that email clients typically refuse.\n */\nfunction getProtocol(url: string): string | null {\n if (!url) return null;\n const trimmed = url.trim();\n const match = /^([a-z][a-z0-9+\\-.]*):/i.exec(trimmed);\n if (!match) return null;\n return match[1].toLowerCase();\n}\n\nexport const unsupportedProtocol: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n const protocol = getProtocol(occ.url);\n if (protocol === null) continue;\n if (DANGEROUS.has(protocol)) continue;\n if (SUPPORTED.has(protocol)) continue;\n hits.push({ blockId: occ.blockId, params: { protocol } });\n }\n return hits;\n },\n};\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.malformed-mailto\",\n severity: \"warning\",\n};\n\n/**\n * Pragmatic RFC-5321-ish sanity check, not a full validator. Splits on `?`,\n * requires the left side to contain exactly one `@` with a non-empty local\n * part and a domain that includes at least one dot.\n *\n * Multi-recipient `mailto:a@x.com,b@y.com` is accepted (commas pass through;\n * each recipient is validated individually).\n */\nfunction isMalformedMailto(url: string): boolean {\n const trimmed = url.trim();\n if (!/^mailto:/i.test(trimmed)) return false;\n const value = trimmed.slice(\"mailto:\".length);\n const [recipients] = value.split(\"?\", 2);\n if (recipients.trim() === \"\") return true;\n\n const list = recipients.split(\",\").map((r) => r.trim());\n for (const recipient of list) {\n if (recipient === \"\") return true;\n const at = recipient.split(\"@\");\n if (at.length !== 2) return true;\n const [local, domain] = at;\n if (local === \"\" || domain === \"\") return true;\n if (!domain.includes(\".\")) return true;\n }\n return false;\n}\n\nexport const malformedMailto: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n if (isMalformedMailto(occ.url)) {\n hits.push({ blockId: occ.blockId });\n }\n }\n return hits;\n },\n};\n","import type { Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.malformed-tel\",\n severity: \"warning\",\n};\n\nconst VALID_SUBSCRIBER_CHARS = /^[+0-9\\s().\\-]+$/;\n// RFC 3966 par = `;` pname [ \"=\" pvalue ]. pname is alphanum/`-`, pvalue is\n// 1+ paramchar. We accept anything non-empty on the right of `=` since email\n// clients don't validate it.\nconst VALID_PARAM = /^[A-Za-z0-9-]+(=[^;]+)?$/;\n\nfunction isMalformedTel(url: string): boolean {\n const trimmed = url.trim();\n if (!/^tel:/i.test(trimmed)) return false;\n const value = trimmed.slice(\"tel:\".length).trim();\n if (value === \"\") return true;\n const [subscriber, ...params] = value.split(\";\");\n if (!VALID_SUBSCRIBER_CHARS.test(subscriber)) return true;\n return params.some((p) => !VALID_PARAM.test(p));\n}\n\nexport const malformedTel: Rule = {\n meta,\n template(content): RuleHit[] {\n const hits: RuleHit[] = [];\n for (const occ of walkUrls(content)) {\n if (isMalformedTel(occ.url)) {\n hits.push({ blockId: occ.blockId });\n }\n }\n return hits;\n },\n};\n","import type { ResolvedOptions, Rule, RuleHit, RuleMeta } from \"../../types\";\nimport { walkUrls } from \"../../url-walker\";\n\nexport const meta: RuleMeta = {\n id: \"link.localhost-or-staging\",\n severity: \"warning\",\n};\n\n/**\n * Glob → RegExp for the `nonProductionHosts` pattern set. `*` is a wildcard\n * that matches any run of characters (including `.`) so `*.staging.*`\n * matches `app.staging.example.com` and `*.local` matches both `acme.local`\n * and `a.b.c.local`. Case-insensitive.\n */\nfunction globToRegex(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const expanded = escaped.replace(/\\*/g, \".*\");\n return new RegExp(`^${expanded}$`, \"i\");\n}\n\nfunction extractHost(url: string): string | null {\n if (!url) return null;\n const trimmed = url.trim();\n // mailto/tel/sms have no host concept worth matching.\n if (!/^(https?|ftps?):\\/\\//i.test(trimmed)) return null;\n try {\n return new URL(trimmed).hostname.toLowerCase();\n } catch {\n return null;\n }\n}\n\nexport const localhostOrStaging: Rule = {\n meta,\n template(content, opts: ResolvedOptions): RuleHit[] {\n const patterns = opts.links.nonProductionHosts;\n if (patterns.length === 0) return [];\n const regexes = patterns.map(globToRegex);\n const hits: RuleHit[] = [];\n\n for (const occ of walkUrls(content)) {\n const host = extractHost(occ.url);\n if (host === null) continue;\n if (regexes.some((re) => re.test(host))) {\n hits.push({ blockId: occ.blockId, params: { host } });\n }\n }\n return hits;\n },\n};\n","import type { TemplateContent } from \"@templatical/types\";\nimport type { LintIssue, LintOptions, Rule } from \"../types\";\nimport { resolveLinksOptions, runRules } from \"../run-rules\";\nimport { formatLinkMessage, type LinkRuleMessageId } from \"./messages\";\nimport { javascriptProtocol } from \"./rules/javascript-protocol\";\nimport { unsupportedProtocol } from \"./rules/unsupported-protocol\";\nimport { malformedMailto } from \"./rules/malformed-mailto\";\nimport { malformedTel } from \"./rules/malformed-tel\";\nimport { localhostOrStaging } from \"./rules/localhost-or-staging\";\n\nexport const LINK_RULES: Rule[] = [\n javascriptProtocol,\n unsupportedProtocol,\n malformedMailto,\n malformedTel,\n localhostOrStaging,\n];\n\nexport function lintLinks(\n content: TemplateContent,\n options: LintOptions = {},\n): LintIssue[] {\n if (options.disabled === true || options.links === false) return [];\n const tool = options.links ?? {};\n const resolved = resolveLinksOptions(options.locale, tool, LINK_RULES);\n return runRules(content, LINK_RULES, resolved, (locale, id, params) =>\n formatLinkMessage(locale, id as LinkRuleMessageId, params),\n );\n}\n","import type { LintOptions } from \"./types\";\n\n/**\n * `true` when no linter would run for the given options — either the\n * global `disabled` flag is set, or every per-tool key is `false`.\n *\n * The editor uses this to skip lazy-loading `@templatical/quality`, hide\n * the Issues sidebar tab, and suppress inline canvas badges. Headless\n * callers can use it to short-circuit before any linter call.\n */\nexport function isLintFullyDisabled(options: LintOptions | undefined): boolean {\n if (!options) return false;\n if (options.disabled === true) return true;\n return (\n options.accessibility === false &&\n options.structure === false &&\n options.links === false\n );\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@templatical/quality",
|
|
3
3
|
"description": "Accessibility linter for Templatical email templates",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.5",
|
|
5
5
|
"bugs": "https://github.com/templatical/sdk/issues",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"htmlparser2": "^12.0.0",
|
|
8
|
-
"@templatical/types": "0.8.
|
|
8
|
+
"@templatical/types": "0.8.5"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"typescript": "^6.0.3",
|