@templatical/quality 0.8.1 → 0.8.3

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.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { Block } from '../../types/src/index.ts';
2
- import { SectionBlock } from '../../types/src/index.ts';
3
- import { TemplateContent } from '../../../types/src/index.ts';
4
- import { TemplateContent as TemplateContent_2 } from '../../types/src/index.ts';
5
- import { TemplateSettings } from '../../types/src/index.ts';
1
+ import type { Block } from '@templatical/types';
2
+ import type { SectionBlock } from '@templatical/types';
3
+ import type { TemplateContent } from '@templatical/types';
4
+ import type { TemplateSettings } from '@templatical/types';
6
5
 
7
6
  export declare const ACCESSIBILITY_RULES: Rule[];
8
7
 
@@ -64,6 +63,7 @@ declare const en_2: {
64
63
  "a11y.link-vague-text": string;
65
64
  "a11y.link-href-empty": string;
66
65
  "a11y.link-target-blank-no-rel": string;
66
+ "a11y.link-nested-anchor": string;
67
67
  "a11y.text-all-caps": string;
68
68
  "a11y.text-low-contrast": string;
69
69
  "a11y.text-too-small": string;
@@ -105,6 +105,15 @@ declare const en_4: {
105
105
  * Extract every anchor from a TipTap-style HTML fragment. Used by
106
106
  * link-* rules. Doesn't try to be a full DOM — only the data the rules
107
107
  * need.
108
+ *
109
+ * Nested `<a>` is invalid HTML; htmlparser2 follows the HTML5 spec and
110
+ * emits an implicit `</a>` when a second `<a>` opens, so anchors are
111
+ * effectively flat siblings. We mirror that with a single in-flight
112
+ * anchor (no stack); a defensive finalize-on-reopen handles the
113
+ * theoretical case where the parser ever stops emitting the implicit
114
+ * close. Detecting nested-anchor markup as its own concern lives in
115
+ * the `a11y.link-nested-anchor` rule, which inspects the raw input
116
+ * before this normalization.
108
117
  */
109
118
  export declare function extractAnchors(html: string): AnchorInfo[];
110
119
 
@@ -154,6 +163,20 @@ export declare function getMessages(locale: string): MessageMap;
154
163
 
155
164
  export declare function getStructureMessages(locale: string): StructureMessageMap;
156
165
 
166
+ /**
167
+ * Whether the raw HTML contains an `<a>` opened inside another open
168
+ * `<a>` — invalid markup that htmlparser2 silently normalizes by
169
+ * emitting an implicit `</a>` before the inner open. `extractAnchors`
170
+ * runs against the normalized parse and therefore can't distinguish
171
+ * nested-from-sibling input; this helper inspects the raw text so the
172
+ * `a11y.link-nested-anchor` rule can flag the structural problem.
173
+ *
174
+ * Tokenization here ignores anchor-like tokens inside HTML comments,
175
+ * which is enough for TipTap email-template HTML. CDATA, `<script>`,
176
+ * and attribute-value occurrences aren't expected in this surface.
177
+ */
178
+ export declare function hasNestedAnchors(html: string): boolean;
179
+
157
180
  /**
158
181
  * `true` when no linter would run for the given options — either the
159
182
  * global `disabled` flag is set, or every per-tool key is `false`.
@@ -273,7 +296,7 @@ export declare interface Rule {
273
296
  /** Block-level rule. Returns a hit or null. */
274
297
  block?: (block: Block, ctx: WalkContext, opts: ResolvedOptions) => RuleHit | null;
275
298
  /** Template-level rule. Runs once after the walk. */
276
- template?: (content: TemplateContent_2, opts: ResolvedOptions) => RuleHit[];
299
+ template?: (content: TemplateContent, opts: ResolvedOptions) => RuleHit[];
277
300
  }
278
301
 
279
302
  /**
@@ -346,7 +369,7 @@ export declare type Visitor = (block: Block, ctx: WalkContext) => void;
346
369
  * descend into a section that lives inside a column. Custom blocks are
347
370
  * visited but not descended into.
348
371
  */
349
- export declare function walkBlocks(content: TemplateContent_2, visit: Visitor): void;
372
+ export declare function walkBlocks(content: TemplateContent, visit: Visitor): void;
350
373
 
351
374
  export declare interface WalkContext {
352
375
  parent: Block | null;
@@ -374,6 +397,6 @@ export declare interface WalkContext {
374
397
  *
375
398
  * Each rule iterates this list once and decides per occurrence.
376
399
  */
377
- export declare function walkUrls(content: TemplateContent_2): UrlOccurrence[];
400
+ export declare function walkUrls(content: TemplateContent): UrlOccurrence[];
378
401
 
379
402
  export { }
package/dist/index.js CHANGED
@@ -175,6 +175,7 @@ var le = /* @__PURE__ */ d({ default: () => ue }), ue = {
175
175
  "a11y.link-vague-text": "Link-Text „{text}“ ist unspezifisch. Beschreibe stattdessen das Ziel.",
176
176
  "a11y.link-href-empty": "Ein Link in diesem Block hat ein leeres oder „#“-href.",
177
177
  "a11y.link-target-blank-no-rel": "Link öffnet in neuem Tab, aber rel=\"noopener\" fehlt – ergänze es, damit das Ziel nicht auf window.opener zugreifen kann.",
178
+ "a11y.link-nested-anchor": "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.",
178
179
  "a11y.text-all-caps": "Längere Texte in Großbuchstaben sind schwerer lesbar. Verwende Groß- und Kleinschreibung.",
179
180
  "a11y.text-low-contrast": "Überschriftskontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.",
180
181
  "a11y.text-too-small": "Text ist {size}px; mindestens {min}px verwenden.",
@@ -195,6 +196,7 @@ var le = /* @__PURE__ */ d({ default: () => ue }), ue = {
195
196
  "a11y.link-vague-text": "Link text \"{text}\" is vague. Describe the destination instead.",
196
197
  "a11y.link-href-empty": "A link in this block has an empty or '#' href.",
197
198
  "a11y.link-target-blank-no-rel": "Link opens in a new tab but is missing rel=\"noopener\" — add it to prevent the destination from accessing window.opener.",
199
+ "a11y.link-nested-anchor": "A link is nested inside another link. Nested anchors are invalid HTML and clients render them inconsistently — flatten to a single anchor.",
198
200
  "a11y.text-all-caps": "Long all-caps text is harder to read for everyone. Use sentence case.",
199
201
  "a11y.text-low-contrast": "Heading contrast is {ratio}:1; WCAG AA requires at least {required}:1.",
200
202
  "a11y.text-too-small": "Text is {size}px; aim for at least {min}px.",
@@ -406,37 +408,44 @@ var Te = {
406
408
  //#endregion
407
409
  //#region src/html-utils.ts
408
410
  function M(e) {
409
- let t = [], n = [], r = new l({
411
+ let t = [], n = null, r = "", i = () => {
412
+ n !== null && (n.text = r.trim(), t.push(n), n = null, r = "");
413
+ }, a = new l({
410
414
  onopentag(e, t) {
411
415
  if (e === "a") {
412
- let e = {
416
+ i(), n = {
413
417
  href: t.href ?? "",
414
418
  text: "",
415
419
  target: t.target ?? null,
416
420
  rel: t.rel ?? null,
417
421
  hasImageWithAlt: !1
418
422
  };
419
- n.push({
420
- anchor: e,
421
- buffer: ""
422
- });
423
423
  return;
424
424
  }
425
- e === "img" && n.length > 0 && (t.alt ?? "").trim() !== "" && (n[n.length - 1].anchor.hasImageWithAlt = !0);
425
+ e === "img" && n !== null && (t.alt ?? "").trim() !== "" && (n.hasImageWithAlt = !0);
426
426
  },
427
427
  ontext(e) {
428
- for (let t of n) t.buffer += e;
428
+ n !== null && (r += e);
429
429
  },
430
430
  onclosetag(e) {
431
- if (e === "a" && n.length > 0) {
432
- let e = n.pop();
433
- e.anchor.text = e.buffer.trim(), t.push(e.anchor);
434
- }
431
+ e === "a" && i();
435
432
  }
436
433
  });
437
- return r.write(e), r.end(), t;
434
+ return a.write(e), a.end(), i(), t;
438
435
  }
439
436
  function N(e) {
437
+ let t = e.replace(/<!--[\s\S]*?-->/g, "").matchAll(/<\/?a\b[^>]*>/gi), n = 0;
438
+ for (let e of t) {
439
+ if (e[0].startsWith("</")) {
440
+ n > 0 && n--;
441
+ continue;
442
+ }
443
+ if (n > 0) return !0;
444
+ n++;
445
+ }
446
+ return !1;
447
+ }
448
+ function P(e) {
440
449
  let t = "", n = new l({ ontext(e) {
441
450
  t += e;
442
451
  } });
@@ -448,26 +457,26 @@ var Ee = {
448
457
  severity: "error"
449
458
  },
450
459
  block(e) {
451
- return !c(e) || N(e.content ?? "") !== "" ? null : { blockId: e.id };
460
+ return !c(e) || P(e.content ?? "") !== "" ? null : { blockId: e.id };
452
461
  }
453
462
  }, De = {
454
463
  id: "a11y.heading-skip-level",
455
464
  severity: "error"
456
465
  };
457
- function P(e, t) {
466
+ function F(e, t) {
458
467
  for (let n of e) {
459
468
  if (c(n)) {
460
469
  t.push(n);
461
470
  continue;
462
471
  }
463
- if (o(n)) for (let e of n.children) P(e, t);
472
+ if (o(n)) for (let e of n.children) F(e, t);
464
473
  }
465
474
  }
466
475
  var Oe = {
467
476
  meta: De,
468
477
  template(e) {
469
478
  let t = [];
470
- P(e.blocks, t);
479
+ F(e.blocks, t);
471
480
  let n = [], r = 0;
472
481
  for (let e of t) r !== 0 && e.level > r + 1 && n.push({
473
482
  blockId: e.id,
@@ -482,20 +491,20 @@ var Oe = {
482
491
  id: "a11y.heading-multiple-h1",
483
492
  severity: "warning"
484
493
  };
485
- function F(e, t) {
494
+ function I(e, t) {
486
495
  for (let n of e) {
487
496
  if (c(n)) {
488
497
  t.push(n);
489
498
  continue;
490
499
  }
491
- if (o(n)) for (let e of n.children) F(e, t);
500
+ if (o(n)) for (let e of n.children) I(e, t);
492
501
  }
493
502
  }
494
503
  var Ae = {
495
504
  meta: ke,
496
505
  template(e) {
497
506
  let t = [];
498
- F(e.blocks, t);
507
+ I(e.blocks, t);
499
508
  let n = t.filter((e) => e.level === 1);
500
509
  return n.length <= 1 ? [] : n.slice(1).map((e) => ({ blockId: e.id }));
501
510
  }
@@ -577,9 +586,9 @@ var Ue = {
577
586
  }
578
587
  };
579
588
  }
580
- }, I = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
589
+ }, L = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
581
590
  function We(e) {
582
- let t = [], n = new RegExp(I.source, I.flags), r;
591
+ let t = [], n = new RegExp(L.source, L.flags), r;
583
592
  for (; (r = n.exec(e)) !== null;) {
584
593
  let e = r[2] ?? r[3] ?? r[4] ?? null;
585
594
  t.push({
@@ -608,17 +617,32 @@ function Ke(e) {
608
617
  return `<a${t} rel="noopener">`;
609
618
  });
610
619
  }
620
+ //#endregion
621
+ //#region src/accessibility/rules/link-nested-anchor.ts
611
622
  var qe = {
623
+ id: "a11y.link-nested-anchor",
624
+ severity: "error"
625
+ };
626
+ function Je(e) {
627
+ return a(e) || c(e) ? e.content : null;
628
+ }
629
+ var Ye = {
630
+ meta: qe,
631
+ block(e) {
632
+ let t = Je(e);
633
+ return t === null || !N(t) ? null : { blockId: e.id };
634
+ }
635
+ }, Xe = {
612
636
  meta: {
613
637
  id: "a11y.text-all-caps",
614
638
  severity: "warning"
615
639
  },
616
640
  block(e, t, n) {
617
641
  if (!a(e) && !c(e)) return null;
618
- let r = N(e.content ?? "").replace(/[^\p{L}]/gu, "");
642
+ let r = P(e.content ?? "").replace(/[^\p{L}]/gu, "");
619
643
  return r.length < n.thresholds.allCapsMinLength || r !== r.toLocaleUpperCase() ? null : { blockId: e.id };
620
644
  }
621
- }, Je = {
645
+ }, Ze = {
622
646
  meta: {
623
647
  id: "a11y.text-low-contrast",
624
648
  severity: "error"
@@ -634,16 +658,16 @@ var qe = {
634
658
  }
635
659
  };
636
660
  }
637
- }, Ye = {
661
+ }, Qe = {
638
662
  id: "a11y.text-too-small",
639
663
  severity: "warning"
640
664
  };
641
- function Xe(e) {
665
+ function $e(e) {
642
666
  return i(e) || ee(e) ? e.fontSize : null;
643
667
  }
644
668
  //#endregion
645
669
  //#region src/accessibility/index.ts
646
- var L = [
670
+ var R = [
647
671
  pe,
648
672
  ge,
649
673
  _e,
@@ -656,12 +680,13 @@ var L = [
656
680
  Ie,
657
681
  ze,
658
682
  Ue,
659
- qe,
660
- Je,
683
+ Ye,
684
+ Xe,
685
+ Ze,
661
686
  {
662
- meta: Ye,
687
+ meta: Qe,
663
688
  block(e, t, n) {
664
- let r = Xe(e);
689
+ let r = $e(e);
665
690
  return r === null || r >= n.thresholds.minFontSize ? null : {
666
691
  blockId: e.id,
667
692
  params: {
@@ -733,41 +758,41 @@ var L = [
733
758
  }
734
759
  }
735
760
  ];
736
- function Ze(e, t = {}) {
761
+ function et(e, t = {}) {
737
762
  if (t.disabled === !0 || t.accessibility === !1) return [];
738
763
  let n = t.accessibility ?? {};
739
- return b(e, L, oe(t.locale, n, L), (e, t, n) => E(e, t, n));
764
+ return b(e, R, oe(t.locale, n, R), (e, t, n) => E(e, t, n));
740
765
  }
741
766
  //#endregion
742
767
  //#region src/structure/messages/de.ts
743
- var Qe = /* @__PURE__ */ d({ default: () => $e }), $e = {
768
+ var tt = /* @__PURE__ */ d({ default: () => nt }), nt = {
744
769
  "structure.duplicate-block-id": "Block-ID erscheint {count}-mal im Baum. Jeder Block muss eine eindeutige ID haben.",
745
770
  "structure.section-column-mismatch": "Sektion verwendet Layout „{layout}\" (erwartet {expected} Spalten), hat aber {actual}. Deutet auf beschädigten Zustand hin.",
746
771
  "structure.nested-section": "Sektion ist in einer anderen Sektion verschachtelt. Sektionen können nicht verschachtelt werden – der Renderer wird sich falsch verhalten.",
747
772
  "structure.empty-section": "Sektion enthält keine Blöcke. Entferne sie oder füge Inhalt hinzu.",
748
773
  "structure.empty-column": "Spalte {columnIndex} dieser Sektion ist leer. Füge Inhalt hinzu oder reduziere die Spaltenanzahl."
749
- }, et = /* @__PURE__ */ d({ default: () => R }), R = {
774
+ }, rt = /* @__PURE__ */ d({ default: () => z }), z = {
750
775
  "structure.duplicate-block-id": "Block id appears {count} times in the tree. Each block must have a unique id.",
751
776
  "structure.section-column-mismatch": "Section uses layout \"{layout}\" (expects {expected} columns) but has {actual}. Indicates corrupted state.",
752
777
  "structure.nested-section": "Section is nested inside another section. Sections cannot nest — the renderer will misbehave.",
753
778
  "structure.empty-section": "Section has no blocks. Remove it or add content.",
754
779
  "structure.empty-column": "Column {columnIndex} of this section is empty. Add content or reduce the column count."
755
- }, z = /* @__PURE__ */ Object.assign({
756
- "./de.ts": Qe,
757
- "./en.ts": et
758
- }), B = {};
759
- for (let e in z) {
780
+ }, B = /* @__PURE__ */ Object.assign({
781
+ "./de.ts": tt,
782
+ "./en.ts": rt
783
+ }), V = {};
784
+ for (let e in B) {
760
785
  let t = /\.\/([^/]+)\.ts$/.exec(e);
761
786
  if (!t) continue;
762
787
  let n = t[1];
763
- n !== "index" && (B[n] = z[e].default);
788
+ n !== "index" && (V[n] = B[e].default);
764
789
  }
765
- var tt = Object.keys(B);
766
- function V(e) {
767
- return B[e.split("-")[0]?.toLowerCase() ?? "en"] ?? B.en ?? R;
790
+ var it = Object.keys(V);
791
+ function H(e) {
792
+ return V[e.split("-")[0]?.toLowerCase() ?? "en"] ?? V.en ?? z;
768
793
  }
769
- function H(e, t, n) {
770
- let r = V(e)[t] ?? R[t];
794
+ function U(e, t, n) {
795
+ let r = H(e)[t] ?? z[t];
771
796
  return n ? r.replace(/\{(\w+)\}/g, (e, t) => {
772
797
  let r = n[t];
773
798
  return r === void 0 ? `{${t}}` : String(r);
@@ -775,18 +800,18 @@ function H(e, t, n) {
775
800
  }
776
801
  //#endregion
777
802
  //#region src/structure/rules/duplicate-block-id.ts
778
- var nt = {
803
+ var at = {
779
804
  id: "structure.duplicate-block-id",
780
805
  severity: "error"
781
806
  };
782
- function U(e, t) {
783
- 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);
807
+ function W(e, t) {
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) W(e, t);
784
809
  }
785
- var rt = {
786
- meta: nt,
810
+ var ot = {
811
+ meta: at,
787
812
  template(e) {
788
813
  let t = /* @__PURE__ */ new Map();
789
- U(e.blocks, t);
814
+ W(e.blocks, t);
790
815
  let n = [];
791
816
  for (let [e, r] of t) r > 1 && n.push({
792
817
  blockId: e,
@@ -794,11 +819,11 @@ var rt = {
794
819
  });
795
820
  return n;
796
821
  }
797
- }, it = {
822
+ }, st = {
798
823
  id: "structure.empty-column",
799
824
  severity: "warning"
800
825
  };
801
- function W(e, t) {
826
+ function G(e, t) {
802
827
  for (let n of e) {
803
828
  if (!o(n)) continue;
804
829
  let e = n;
@@ -808,28 +833,28 @@ function W(e, t) {
808
833
  params: { columnIndex: r + 1 }
809
834
  });
810
835
  });
811
- for (let n of e.children) W(n, t);
836
+ for (let n of e.children) G(n, t);
812
837
  }
813
838
  }
814
- var at = {
815
- meta: it,
839
+ var ct = {
840
+ meta: st,
816
841
  template(e) {
817
842
  let t = [];
818
- return W(e.blocks, t), t;
843
+ return G(e.blocks, t), t;
819
844
  }
820
- }, ot = {
845
+ }, lt = {
821
846
  id: "structure.empty-section",
822
847
  severity: "warning"
823
848
  };
824
- function st(e) {
849
+ function ut(e) {
825
850
  return e.children.length === 0 ? !0 : e.children.every((e) => e.length === 0);
826
851
  }
827
- var ct = {
828
- meta: ot,
852
+ var dt = {
853
+ meta: lt,
829
854
  block(e) {
830
855
  if (!o(e)) return null;
831
856
  let t = e;
832
- return st(t) ? {
857
+ return ut(t) ? {
833
858
  blockId: t.id,
834
859
  fix: {
835
860
  description: "Remove the empty section",
@@ -839,7 +864,7 @@ var ct = {
839
864
  }
840
865
  } : null;
841
866
  }
842
- }, lt = {
867
+ }, ft = {
843
868
  meta: {
844
869
  id: "structure.nested-section",
845
870
  severity: "error"
@@ -852,25 +877,25 @@ var ct = {
852
877
  params: { parentId: n.id }
853
878
  };
854
879
  }
855
- }, ut = {
880
+ }, pt = {
856
881
  id: "structure.section-column-mismatch",
857
882
  severity: "error"
858
883
  };
859
- function dt(e) {
884
+ function mt(e) {
860
885
  return e === "1" ? 1 : e === "3" ? 3 : 2;
861
886
  }
862
887
  //#endregion
863
888
  //#region src/structure/index.ts
864
- var G = [
865
- rt,
889
+ var K = [
890
+ ot,
891
+ dt,
866
892
  ct,
867
- at,
868
- lt,
893
+ ft,
869
894
  {
870
- meta: ut,
895
+ meta: pt,
871
896
  block(e) {
872
897
  if (!o(e)) return null;
873
- let t = e, n = dt(t.columns), r = t.children.length;
898
+ let t = e, n = mt(t.columns), r = t.children.length;
874
899
  return r === n ? null : {
875
900
  blockId: t.id,
876
901
  params: {
@@ -882,41 +907,41 @@ var G = [
882
907
  }
883
908
  }
884
909
  ];
885
- function ft(e, t = {}) {
910
+ function ht(e, t = {}) {
886
911
  if (t.disabled === !0 || t.structure === !1) return [];
887
912
  let n = t.structure ?? {};
888
- return b(e, G, se(t.locale, n, G), (e, t, n) => H(e, t, n));
913
+ return b(e, K, se(t.locale, n, K), (e, t, n) => U(e, t, n));
889
914
  }
890
915
  //#endregion
891
916
  //#region src/links/messages/de.ts
892
- var pt = /* @__PURE__ */ d({ default: () => mt }), mt = {
917
+ var gt = /* @__PURE__ */ d({ default: () => _t }), _t = {
893
918
  "link.javascript-protocol": "Die URL verwendet das „javascript:\"-Protokoll, das aus Sicherheitsgründen beim Rendern entfernt wird. Ersetze sie durch eine echte URL oder entferne sie.",
894
919
  "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.",
895
920
  "link.malformed-mailto": "Der mailto:-Link ist fehlerhaft. Erwartet wird eine einzelne Empfängeradresse vor einer eventuellen Querystring (z. B. mailto:hallo@example.com).",
896
921
  "link.malformed-tel": "Der tel:-Link enthält Zeichen, die keine Ziffern, +, Leerzeichen, Bindestriche, Klammern oder Punkte sind.",
897
922
  "link.localhost-or-staging": "Der URL-Host „{host}\" entspricht einem Nicht-Produktionsmuster. Ersetze ihn vor dem Versand durch die Produktions-URL."
898
- }, ht = /* @__PURE__ */ d({ default: () => K }), K = {
923
+ }, vt = /* @__PURE__ */ d({ default: () => q }), q = {
899
924
  "link.javascript-protocol": "URL uses the \"javascript:\" protocol, which is stripped at render time for safety. Replace it with a real link or remove the URL.",
900
925
  "link.unsupported-protocol": "URL uses the \"{protocol}\" protocol, which most email clients do not support. Use http, https, mailto, tel, or sms.",
901
926
  "link.malformed-mailto": "mailto: link is malformed. Expected a single recipient address before any query string (e.g. mailto:hello@example.com).",
902
927
  "link.malformed-tel": "tel: link contains characters that are not digits, +, spaces, dashes, parentheses, or dots.",
903
928
  "link.localhost-or-staging": "URL host \"{host}\" matches a non-production pattern. Replace with the production URL before sending."
904
- }, q = /* @__PURE__ */ Object.assign({
905
- "./de.ts": pt,
906
- "./en.ts": ht
907
- }), J = {};
908
- for (let e in q) {
929
+ }, J = /* @__PURE__ */ Object.assign({
930
+ "./de.ts": gt,
931
+ "./en.ts": vt
932
+ }), Y = {};
933
+ for (let e in J) {
909
934
  let t = /\.\/([^/]+)\.ts$/.exec(e);
910
935
  if (!t) continue;
911
936
  let n = t[1];
912
- n !== "index" && (J[n] = q[e].default);
937
+ n !== "index" && (Y[n] = J[e].default);
913
938
  }
914
- var gt = Object.keys(J);
915
- function Y(e) {
916
- return J[e.split("-")[0]?.toLowerCase() ?? "en"] ?? J.en ?? K;
939
+ var yt = Object.keys(Y);
940
+ function X(e) {
941
+ return Y[e.split("-")[0]?.toLowerCase() ?? "en"] ?? Y.en ?? q;
917
942
  }
918
- function X(e, t, n) {
919
- let r = Y(e)[t] ?? K[t];
943
+ function Z(e, t, n) {
944
+ let r = X(e)[t] ?? q[t];
920
945
  return n ? r.replace(/\{(\w+)\}/g, (e, t) => {
921
946
  let r = n[t];
922
947
  return r === void 0 ? `{${t}}` : String(r);
@@ -924,7 +949,7 @@ function X(e, t, n) {
924
949
  }
925
950
  //#endregion
926
951
  //#region src/url-walker.ts
927
- function Z(e) {
952
+ function Q(e) {
928
953
  let o = [];
929
954
  return y(e, (e) => {
930
955
  if (c(e) || a(e) || n(e)) {
@@ -985,55 +1010,55 @@ function Z(e) {
985
1010
  }
986
1011
  //#endregion
987
1012
  //#region src/links/rules/javascript-protocol.ts
988
- var _t = {
1013
+ var bt = {
989
1014
  id: "link.javascript-protocol",
990
1015
  severity: "error"
991
1016
  };
992
- function vt(e) {
1017
+ function xt(e) {
993
1018
  if (!e) return !1;
994
1019
  let t = e.replace(/\s+/g, "");
995
1020
  return /^javascript:/i.test(t);
996
1021
  }
997
- var yt = {
998
- meta: _t,
1022
+ var St = {
1023
+ meta: bt,
999
1024
  template(e) {
1000
1025
  let t = [];
1001
- for (let n of Z(e)) vt(n.url) && t.push({ blockId: n.blockId });
1026
+ for (let n of Q(e)) xt(n.url) && t.push({ blockId: n.blockId });
1002
1027
  return t;
1003
1028
  }
1004
- }, bt = {
1029
+ }, Ct = {
1005
1030
  id: "link.unsupported-protocol",
1006
1031
  severity: "warning"
1007
- }, xt = new Set([
1032
+ }, wt = new Set([
1008
1033
  "http",
1009
1034
  "https",
1010
1035
  "mailto",
1011
1036
  "tel",
1012
1037
  "sms"
1013
1038
  ]);
1014
- function St(e) {
1039
+ function Tt(e) {
1015
1040
  if (!e) return null;
1016
1041
  let t = e.trim(), n = /^([a-z][a-z0-9+\-.]*):/i.exec(t);
1017
1042
  return n ? n[1].toLowerCase() : null;
1018
1043
  }
1019
- var Ct = {
1020
- meta: bt,
1044
+ var Et = {
1045
+ meta: Ct,
1021
1046
  template(e) {
1022
1047
  let t = [];
1023
- for (let n of Z(e)) {
1024
- let e = St(n.url);
1025
- e !== null && e !== "javascript" && (xt.has(e) || t.push({
1048
+ for (let n of Q(e)) {
1049
+ let e = Tt(n.url);
1050
+ e !== null && e !== "javascript" && (wt.has(e) || t.push({
1026
1051
  blockId: n.blockId,
1027
1052
  params: { protocol: e }
1028
1053
  }));
1029
1054
  }
1030
1055
  return t;
1031
1056
  }
1032
- }, wt = {
1057
+ }, Dt = {
1033
1058
  id: "link.malformed-mailto",
1034
1059
  severity: "warning"
1035
1060
  };
1036
- function Tt(e) {
1061
+ function Ot(e) {
1037
1062
  let t = e.trim();
1038
1063
  if (!/^mailto:/i.test(t)) return !1;
1039
1064
  let [n] = t.slice(7).split("?", 2);
@@ -1048,41 +1073,41 @@ function Tt(e) {
1048
1073
  }
1049
1074
  return !1;
1050
1075
  }
1051
- var Et = {
1052
- meta: wt,
1076
+ var kt = {
1077
+ meta: Dt,
1053
1078
  template(e) {
1054
1079
  let t = [];
1055
- for (let n of Z(e)) Tt(n.url) && t.push({ blockId: n.blockId });
1080
+ for (let n of Q(e)) Ot(n.url) && t.push({ blockId: n.blockId });
1056
1081
  return t;
1057
1082
  }
1058
- }, Dt = {
1083
+ }, At = {
1059
1084
  id: "link.malformed-tel",
1060
1085
  severity: "warning"
1061
- }, Q = /^[+0-9\s().\-]+$/, Ot = /^[A-Za-z0-9-]+(=[^;]+)?$/;
1062
- function kt(e) {
1086
+ }, jt = /^[+0-9\s().\-]+$/, Mt = /^[A-Za-z0-9-]+(=[^;]+)?$/;
1087
+ function Nt(e) {
1063
1088
  let t = e.trim();
1064
1089
  if (!/^tel:/i.test(t)) return !1;
1065
1090
  let n = t.slice(4).trim();
1066
1091
  if (n === "") return !0;
1067
1092
  let [r, ...i] = n.split(";");
1068
- return Q.test(r) ? i.some((e) => !Ot.test(e)) : !0;
1093
+ return jt.test(r) ? i.some((e) => !Mt.test(e)) : !0;
1069
1094
  }
1070
- var At = {
1071
- meta: Dt,
1095
+ var Pt = {
1096
+ meta: At,
1072
1097
  template(e) {
1073
1098
  let t = [];
1074
- for (let n of Z(e)) kt(n.url) && t.push({ blockId: n.blockId });
1099
+ for (let n of Q(e)) Nt(n.url) && t.push({ blockId: n.blockId });
1075
1100
  return t;
1076
1101
  }
1077
- }, jt = {
1102
+ }, Ft = {
1078
1103
  id: "link.localhost-or-staging",
1079
1104
  severity: "warning"
1080
1105
  };
1081
- function Mt(e) {
1106
+ function It(e) {
1082
1107
  let t = e.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1083
1108
  return RegExp(`^${t}$`, "i");
1084
1109
  }
1085
- function Nt(e) {
1110
+ function Lt(e) {
1086
1111
  if (!e) return null;
1087
1112
  let t = e.trim();
1088
1113
  if (!/^(https?|ftps?):\/\//i.test(t)) return null;
@@ -1095,18 +1120,18 @@ function Nt(e) {
1095
1120
  //#endregion
1096
1121
  //#region src/links/index.ts
1097
1122
  var $ = [
1098
- yt,
1099
- Ct,
1123
+ St,
1100
1124
  Et,
1101
- At,
1125
+ kt,
1126
+ Pt,
1102
1127
  {
1103
- meta: jt,
1128
+ meta: Ft,
1104
1129
  template(e, t) {
1105
1130
  let n = t.links.nonProductionHosts;
1106
1131
  if (n.length === 0) return [];
1107
- let r = n.map(Mt), i = [];
1108
- for (let t of Z(e)) {
1109
- let e = Nt(t.url);
1132
+ let r = n.map(It), i = [];
1133
+ for (let t of Q(e)) {
1134
+ let e = Lt(t.url);
1110
1135
  e !== null && r.some((t) => t.test(e)) && i.push({
1111
1136
  blockId: t.blockId,
1112
1137
  params: { host: e }
@@ -1116,17 +1141,17 @@ var $ = [
1116
1141
  }
1117
1142
  }
1118
1143
  ];
1119
- function Pt(e, t = {}) {
1144
+ function Rt(e, t = {}) {
1120
1145
  if (t.disabled === !0 || t.links === !1) return [];
1121
1146
  let n = t.links ?? {};
1122
- return b(e, $, ce(t.locale, n, $), (e, t, n) => X(e, t, n));
1147
+ return b(e, $, ce(t.locale, n, $), (e, t, n) => Z(e, t, n));
1123
1148
  }
1124
1149
  //#endregion
1125
1150
  //#region src/util.ts
1126
- function Ft(e) {
1151
+ function zt(e) {
1127
1152
  return e ? e.disabled === !0 ? !0 : e.accessibility === !1 && e.structure === !1 && e.links === !1 : !1;
1128
1153
  }
1129
1154
  //#endregion
1130
- 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, gt as SUPPORTED_LINK_MESSAGE_LOCALES, fe as SUPPORTED_MESSAGE_LOCALES, tt 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, Ft as isLintFullyDisabled, g as isOpaqueHex, Ze as lintAccessibility, Pt as lintLinks, ft as lintStructure, h as parseHex, y as walkBlocks, Z as walkUrls };
1155
+ export { R as ACCESSIBILITY_RULES, f as DEFAULT_A11Y_THRESHOLDS, p as DEFAULT_NON_PRODUCTION_HOSTS, $ as LINK_RULES, K as STRUCTURE_RULES, we as SUPPORTED_DICTIONARY_LOCALES, yt as SUPPORTED_LINK_MESSAGE_LOCALES, fe as SUPPORTED_MESSAGE_LOCALES, it as SUPPORTED_STRUCTURE_MESSAGE_LOCALES, M as extractAnchors, P as extractText, Z as formatLinkMessage, E as formatMessage, U as formatStructureMessage, m as getContrastRatio, k as getDictionary, X as getLinkMessages, T as getMessages, H as getStructureMessages, N as hasNestedAnchors, zt as isLintFullyDisabled, g as isOpaqueHex, et as lintAccessibility, Rt as lintLinks, ht as lintStructure, h as parseHex, y as walkBlocks, Q as walkUrls };
1131
1156
 
1132
1157
  //# 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/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.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.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 */\nexport function extractAnchors(html: string): AnchorInfo[] {\n const anchors: AnchorInfo[] = [];\n // Each open anchor owns its own text buffer so a nested `<a>` (invalid\n // HTML but parsed permissively) doesn't truncate the outer anchor's text.\n const stack: { anchor: AnchorInfo; buffer: string }[] = [];\n\n const parser = new Parser({\n onopentag(name, attribs) {\n if (name === \"a\") {\n const anchor: AnchorInfo = {\n href: attribs.href ?? \"\",\n text: \"\",\n target: attribs.target ?? null,\n rel: attribs.rel ?? null,\n hasImageWithAlt: false,\n };\n stack.push({ anchor, buffer: \"\" });\n return;\n }\n\n if (name === \"img\" && stack.length > 0) {\n const alt = (attribs.alt ?? \"\").trim();\n if (alt !== \"\") {\n stack[stack.length - 1].anchor.hasImageWithAlt = true;\n }\n }\n },\n ontext(text) {\n for (const frame of stack) {\n frame.buffer += text;\n }\n },\n onclosetag(name) {\n if (name === \"a\" && stack.length > 0) {\n const frame = stack.pop()!;\n frame.anchor.text = frame.buffer.trim();\n anchors.push(frame.anchor);\n }\n },\n });\n\n parser.write(html);\n parser.end();\n\n return anchors;\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 { 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 { 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 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 = 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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"}
package/package.json CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "name": "@templatical/quality",
3
3
  "description": "Accessibility linter for Templatical email templates",
4
- "version": "0.8.1",
4
+ "version": "0.8.3",
5
5
  "bugs": "https://github.com/templatical/sdk/issues",
6
6
  "dependencies": {
7
- "htmlparser2": "^9.1.0",
8
- "@templatical/types": "0.8.1"
7
+ "htmlparser2": "^12.0.0",
8
+ "@templatical/types": "0.8.3"
9
9
  },
10
10
  "devDependencies": {
11
11
  "typescript": "^6.0.3",
12
- "vite": "^8.0.10",
13
- "vite-plugin-dts": "^4.5.4",
14
- "vitest": "^4.1.5"
12
+ "vite": "^8.0.13",
13
+ "vitest": "^4.1.6"
15
14
  },
16
15
  "exports": {
17
16
  ".": {
@@ -44,7 +43,7 @@
44
43
  "type": "module",
45
44
  "types": "./dist/index.d.ts",
46
45
  "scripts": {
47
- "build": "vite build",
46
+ "build": "vite build && tsc -p tsconfig.build.json && api-extractor run --local && rm -rf dist/types",
48
47
  "test": "vitest run --config vitest.config.ts",
49
48
  "typecheck": "tsc --noEmit"
50
49
  }