@templatical/quality 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -66,6 +66,12 @@ export declare type Dictionary = typeof en;
66
66
  declare const en: {
67
67
  vagueLinkText: string[];
68
68
  vagueButtonLabels: string[];
69
+ /**
70
+ * Action verbs that signal a linked image's alt describes the link
71
+ * destination, not just the visual subject. Used by `img-linked-no-context`.
72
+ * Stored lowercase; tokenized matching is case-insensitive.
73
+ */
74
+ linkedImageActionHints: string[];
69
75
  };
70
76
 
71
77
  /**
package/dist/index.js CHANGED
@@ -136,7 +136,7 @@ function k(e, t, n) {
136
136
  return r === void 0 ? `{${t}}` : String(r);
137
137
  }) : r;
138
138
  }
139
- var ee = {
139
+ var A = {
140
140
  meta: {
141
141
  id: "img-missing-alt",
142
142
  severity: "error"
@@ -144,21 +144,21 @@ var ee = {
144
144
  block(e) {
145
145
  return !n(e) || e.decorative === !0 || (e.alt?.trim() ?? "") !== "" || (e.src ?? "").trim() === "" ? null : { blockId: e.id };
146
146
  }
147
- }, te = {
147
+ }, ee = {
148
148
  id: "img-alt-is-filename",
149
149
  severity: "warning"
150
- }, ne = [
150
+ }, te = [
151
151
  /\.(jpe?g|png|gif|webp|svg)$/i,
152
152
  /^IMG[_-]?\d+/i,
153
153
  /^Untitled/i,
154
154
  /^Screen[\s_-]?Shot/i,
155
- /^DSC\d+/i
156
- ], A = {
157
- meta: te,
155
+ /^DSC[_-]?\d+/i
156
+ ], ne = {
157
+ meta: ee,
158
158
  block(e) {
159
159
  if (!n(e) || e.decorative === !0) return null;
160
160
  let t = e.alt?.trim() ?? "";
161
- return t === "" || !ne.some((e) => e.test(t)) ? null : {
161
+ return t === "" || !te.some((e) => e.test(t)) ? null : {
162
162
  blockId: e.id,
163
163
  params: { alt: t },
164
164
  fix: {
@@ -167,7 +167,7 @@ var ee = {
167
167
  }
168
168
  };
169
169
  }
170
- }, j = {
170
+ }, re = {
171
171
  meta: {
172
172
  id: "img-alt-too-long",
173
173
  severity: "warning"
@@ -183,7 +183,7 @@ var ee = {
183
183
  }
184
184
  };
185
185
  }
186
- }, M = {
186
+ }, ie = {
187
187
  meta: {
188
188
  id: "img-decorative-needs-empty-alt",
189
189
  severity: "info"
@@ -197,40 +197,133 @@ var ee = {
197
197
  }
198
198
  };
199
199
  }
200
- }, N = {
201
- id: "img-linked-no-context",
202
- severity: "warning"
203
- }, P = [
204
- "buy",
205
- "shop",
206
- "view",
207
- "read",
208
- "learn",
209
- "open",
210
- "go",
211
- "see",
212
- "explore",
213
- "discover",
214
- "browse",
215
- "download",
216
- "get",
217
- "claim",
218
- "redeem",
219
- "watch"
220
- ], F = {
221
- meta: N,
222
- block(e) {
200
+ }, j = /* @__PURE__ */ u({ default: () => M }), M = {
201
+ vagueLinkText: [
202
+ "hier klicken",
203
+ "hier",
204
+ "mehr lesen",
205
+ "mehr",
206
+ "weiter",
207
+ "weiterlesen",
208
+ "siehe mehr",
209
+ "dies",
210
+ "dieser link",
211
+ "link",
212
+ "klick"
213
+ ],
214
+ vagueButtonLabels: [
215
+ "hier klicken",
216
+ "klicken",
217
+ "senden",
218
+ "los",
219
+ "ok",
220
+ "okay",
221
+ "ja",
222
+ "nein"
223
+ ],
224
+ linkedImageActionHints: [
225
+ "kaufen",
226
+ "shoppen",
227
+ "ansehen",
228
+ "lesen",
229
+ "lernen",
230
+ "öffnen",
231
+ "los",
232
+ "sehen",
233
+ "entdecken",
234
+ "erkunden",
235
+ "stöbern",
236
+ "herunterladen",
237
+ "holen",
238
+ "abholen",
239
+ "einlösen",
240
+ "anschauen",
241
+ "jetzt"
242
+ ]
243
+ }, N = /* @__PURE__ */ u({ default: () => P }), P = {
244
+ vagueLinkText: [
245
+ "click here",
246
+ "here",
247
+ "read more",
248
+ "more",
249
+ "learn more",
250
+ "see more",
251
+ "this",
252
+ "this link",
253
+ "link",
254
+ "click"
255
+ ],
256
+ vagueButtonLabels: [
257
+ "click here",
258
+ "click",
259
+ "submit",
260
+ "go",
261
+ "ok",
262
+ "okay",
263
+ "yes",
264
+ "no"
265
+ ],
266
+ linkedImageActionHints: [
267
+ "buy",
268
+ "shop",
269
+ "view",
270
+ "read",
271
+ "learn",
272
+ "open",
273
+ "go",
274
+ "see",
275
+ "explore",
276
+ "discover",
277
+ "browse",
278
+ "download",
279
+ "get",
280
+ "claim",
281
+ "redeem",
282
+ "watch"
283
+ ]
284
+ }, F = /* @__PURE__ */ Object.assign({
285
+ "./de.ts": j,
286
+ "./en.ts": N
287
+ }), I = {};
288
+ for (let e in F) {
289
+ let t = /\.\/([^/]+)\.ts$/.exec(e);
290
+ if (!t) continue;
291
+ let n = t[1];
292
+ n !== "index" && (I[n] = F[e].default);
293
+ }
294
+ function L(e) {
295
+ return z;
296
+ }
297
+ function R(e) {
298
+ let t = /* @__PURE__ */ new Set();
299
+ for (let n of Object.values(I)) for (let r of e(n)) t.add(r);
300
+ return Array.from(t);
301
+ }
302
+ var z = {
303
+ vagueLinkText: R((e) => e.vagueLinkText),
304
+ vagueButtonLabels: R((e) => e.vagueButtonLabels),
305
+ linkedImageActionHints: R((e) => e.linkedImageActionHints)
306
+ }, B = Object.keys(I);
307
+ function V(e) {
308
+ return e.toLowerCase().replace(/\s+/g, " ").replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "").trim();
309
+ }
310
+ var H = {
311
+ meta: {
312
+ id: "img-linked-no-context",
313
+ severity: "warning"
314
+ },
315
+ block(e, t, r) {
223
316
  if (!n(e) || e.decorative === !0 || !e.linkUrl || e.linkUrl.trim() === "") return null;
224
- let t = (e.alt ?? "").trim();
225
- if (t === "") return null;
226
- let r = t.toLowerCase();
227
- return P.some((e) => r.includes(e)) ? null : { blockId: e.id };
317
+ let i = (e.alt ?? "").trim();
318
+ if (i === "") return null;
319
+ let a = i.toLocaleLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean), o = L(r.locale).linkedImageActionHints;
320
+ return a.some((e) => o.includes(e)) ? null : { blockId: e.id };
228
321
  }
229
322
  };
230
323
  //#endregion
231
324
  //#region src/html-utils.ts
232
- function I(e) {
233
- let t = [], n = [], r = "", i = new c({
325
+ function U(e) {
326
+ let t = [], n = [], r = new c({
234
327
  onopentag(e, t) {
235
328
  if (e === "a") {
236
329
  let e = {
@@ -240,55 +333,58 @@ function I(e) {
240
333
  rel: t.rel ?? null,
241
334
  hasImageWithAlt: !1
242
335
  };
243
- n.push(e), r = "";
336
+ n.push({
337
+ anchor: e,
338
+ buffer: ""
339
+ });
244
340
  return;
245
341
  }
246
- e === "img" && n.length > 0 && (t.alt ?? "").trim() !== "" && (n[n.length - 1].hasImageWithAlt = !0);
342
+ e === "img" && n.length > 0 && (t.alt ?? "").trim() !== "" && (n[n.length - 1].anchor.hasImageWithAlt = !0);
247
343
  },
248
344
  ontext(e) {
249
- n.length > 0 && (r += e);
345
+ for (let t of n) t.buffer += e;
250
346
  },
251
347
  onclosetag(e) {
252
348
  if (e === "a" && n.length > 0) {
253
349
  let e = n.pop();
254
- e.text = r.trim(), t.push(e), r = "";
350
+ e.anchor.text = e.buffer.trim(), t.push(e.anchor);
255
351
  }
256
352
  }
257
353
  });
258
- return i.write(e), i.end(), t;
354
+ return r.write(e), r.end(), t;
259
355
  }
260
- function L(e) {
356
+ function W(e) {
261
357
  let t = "", n = new c({ ontext(e) {
262
358
  t += e;
263
359
  } });
264
360
  return n.write(e), n.end(), t.trim();
265
361
  }
266
- var R = {
362
+ var G = {
267
363
  meta: {
268
364
  id: "heading-empty",
269
365
  severity: "error"
270
366
  },
271
367
  block(e) {
272
- return !s(e) || L(e.content ?? "") !== "" ? null : { blockId: e.id };
368
+ return !s(e) || W(e.content ?? "") !== "" ? null : { blockId: e.id };
273
369
  }
274
- }, z = {
370
+ }, K = {
275
371
  id: "heading-skip-level",
276
372
  severity: "error"
277
373
  };
278
- function B(e, t) {
374
+ function q(e, t) {
279
375
  for (let n of e) {
280
376
  if (s(n)) {
281
377
  t.push(n);
282
378
  continue;
283
379
  }
284
- if (a(n)) for (let e of n.children) B(e, t);
380
+ if (a(n)) for (let e of n.children) q(e, t);
285
381
  }
286
382
  }
287
- var V = {
288
- meta: z,
383
+ var J = {
384
+ meta: K,
289
385
  template(e) {
290
386
  let t = [];
291
- B(e.blocks, t);
387
+ q(e.blocks, t);
292
388
  let n = [], r = 0;
293
389
  for (let e of t) r !== 0 && e.level > r + 1 && n.push({
294
390
  blockId: e.id,
@@ -299,116 +395,54 @@ var V = {
299
395
  }), r = e.level;
300
396
  return n;
301
397
  }
302
- }, H = {
398
+ }, Y = {
303
399
  id: "heading-multiple-h1",
304
400
  severity: "warning"
305
401
  };
306
- function U(e, t) {
402
+ function X(e, t) {
307
403
  for (let n of e) {
308
404
  if (s(n)) {
309
405
  t.push(n);
310
406
  continue;
311
407
  }
312
- if (a(n)) for (let e of n.children) U(e, t);
408
+ if (a(n)) for (let e of n.children) X(e, t);
313
409
  }
314
410
  }
315
- var W = {
316
- meta: H,
411
+ var ae = {
412
+ meta: Y,
317
413
  template(e) {
318
414
  let t = [];
319
- U(e.blocks, t);
415
+ X(e.blocks, t);
320
416
  let n = t.filter((e) => e.level === 1);
321
417
  return n.length <= 1 ? [] : n.slice(1).map((e) => ({ blockId: e.id }));
322
418
  }
323
- }, G = {
419
+ }, oe = {
324
420
  id: "link-empty",
325
421
  severity: "error"
326
422
  };
327
- function K(e) {
423
+ function se(e) {
328
424
  return i(e) || s(e) ? e.content : null;
329
425
  }
330
- var q = {
331
- meta: G,
426
+ var ce = {
427
+ meta: oe,
332
428
  block(e) {
333
- let t = K(e);
334
- return t === null || !I(t).find((e) => e.text === "" && !e.hasImageWithAlt) ? null : { blockId: e.id };
335
- }
336
- }, J = {
337
- en: {
338
- vagueLinkText: [
339
- "click here",
340
- "here",
341
- "read more",
342
- "more",
343
- "learn more",
344
- "see more",
345
- "this",
346
- "this link",
347
- "link",
348
- "click"
349
- ],
350
- vagueButtonLabels: [
351
- "click here",
352
- "click",
353
- "submit",
354
- "go",
355
- "ok",
356
- "okay",
357
- "yes",
358
- "no"
359
- ]
360
- },
361
- de: {
362
- vagueLinkText: [
363
- "hier klicken",
364
- "hier",
365
- "mehr lesen",
366
- "mehr",
367
- "weiter",
368
- "weiterlesen",
369
- "siehe mehr",
370
- "dies",
371
- "dieser link",
372
- "link",
373
- "klick"
374
- ],
375
- vagueButtonLabels: [
376
- "hier klicken",
377
- "klicken",
378
- "senden",
379
- "los",
380
- "ok",
381
- "okay",
382
- "ja",
383
- "nein"
384
- ]
429
+ let t = se(e);
430
+ return t === null || !U(t).find((e) => e.text === "" && !e.hasImageWithAlt) ? null : { blockId: e.id };
385
431
  }
386
- };
387
- function Y(e) {
388
- return Z;
389
- }
390
- function X(e) {
391
- let t = /* @__PURE__ */ new Set();
392
- for (let n of Object.values(J)) for (let r of e(n)) t.add(r);
393
- return Array.from(t);
394
- }
395
- var Z = {
396
- vagueLinkText: X((e) => e.vagueLinkText),
397
- vagueButtonLabels: X((e) => e.vagueButtonLabels)
398
- }, re = Object.keys(J), ie = {
432
+ }, le = {
399
433
  id: "link-vague-text",
400
434
  severity: "warning"
401
435
  };
402
- function ae(e) {
436
+ function ue(e) {
403
437
  return i(e) || s(e) ? e.content : null;
404
438
  }
405
- var oe = {
406
- meta: ie,
439
+ var de = {
440
+ meta: le,
407
441
  block(e, t, n) {
408
- let r = ae(e);
442
+ let r = ue(e);
409
443
  if (r === null) return null;
410
- let i = Y(n.locale).vagueLinkText, a = I(r).find((e) => {
411
- let t = e.text.toLowerCase().replace(/\s+/g, " ").trim();
444
+ let i = L(n.locale).vagueLinkText, a = U(r).find((e) => {
445
+ let t = V(e.text);
412
446
  return t !== "" && i.includes(t);
413
447
  });
414
448
  return a ? {
@@ -416,82 +450,99 @@ var oe = {
416
450
  params: { text: a.text }
417
451
  } : null;
418
452
  }
419
- }, se = {
453
+ }, fe = {
420
454
  id: "link-href-empty",
421
455
  severity: "error"
422
456
  };
423
- function ce(e) {
457
+ function pe(e) {
424
458
  return i(e) || s(e) ? e.content : null;
425
459
  }
426
- var le = {
427
- meta: se,
460
+ var me = {
461
+ meta: fe,
428
462
  block(e) {
429
- let t = ce(e);
430
- return t === null || !I(t).find((e) => {
463
+ let t = pe(e);
464
+ return t === null || !U(t).find((e) => {
431
465
  let t = e.href.trim();
432
466
  return t === "" || t === "#";
433
467
  }) ? null : { blockId: e.id };
434
468
  }
435
- }, ue = {
469
+ }, he = {
436
470
  id: "link-target-blank-no-rel",
437
471
  severity: "warning"
438
472
  };
439
- function de(e) {
473
+ function ge(e) {
440
474
  return i(e) || s(e) ? e.content : null;
441
475
  }
442
- function fe(e) {
476
+ function _e(e) {
443
477
  if (e === null) return !1;
444
478
  let t = e.toLowerCase().split(/\s+/);
445
479
  return t.includes("noopener") || t.includes("noreferrer");
446
480
  }
447
- var pe = {
448
- meta: ue,
481
+ var ve = {
482
+ meta: he,
449
483
  block(e) {
450
- let t = de(e);
451
- return t === null || !I(t).find((e) => e.target === "_blank" && !fe(e.rel)) ? null : {
484
+ let t = ge(e);
485
+ return t === null || !U(t).find((e) => e.target === "_blank" && !_e(e.rel)) ? null : {
452
486
  blockId: e.id,
453
487
  fix: {
454
488
  description: "Add rel=\"noopener\"",
455
489
  apply: (t) => {
456
490
  if (!i(e) && !s(e)) return;
457
- let n = me(e.content ?? "");
491
+ let n = xe(e.content ?? "");
458
492
  t.updateBlock(e.id, { content: n });
459
493
  }
460
494
  }
461
495
  };
462
496
  }
463
- };
464
- function me(e) {
497
+ }, Z = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
498
+ function ye(e) {
499
+ let t = [], n = new RegExp(Z.source, Z.flags), r;
500
+ for (; (r = n.exec(e)) !== null;) {
501
+ let e = r[2] ?? r[3] ?? r[4] ?? null;
502
+ t.push({
503
+ raw: r[0],
504
+ name: r[1],
505
+ value: e,
506
+ start: r.index
507
+ });
508
+ }
509
+ return t;
510
+ }
511
+ function be(e) {
512
+ return e.some((e) => e.name.toLowerCase() === "target" && e.value !== null && e.value.toLowerCase() === "_blank");
513
+ }
514
+ function xe(e) {
465
515
  return e.replace(/<a\b([^>]*)>/gi, (e, t) => {
466
- if (!/\btarget\s*=\s*["']_blank["']/i.test(t)) return e;
467
- let n = /\brel\s*=\s*["']([^"']*)["']/i.exec(t);
468
- if (n) {
469
- let r = n[1].toLowerCase().split(/\s+/);
470
- if (r.includes("noopener") || r.includes("noreferrer")) return e;
471
- let i = `${n[1]} noopener`.trim();
472
- return `<a${t.replace(n[0], `rel="${i}"`)}>`;
516
+ let n = ye(t);
517
+ if (!be(n)) return e;
518
+ let r = n.find((e) => e.name.toLowerCase() === "rel");
519
+ if (r) {
520
+ let n = (r.value ?? "").toLowerCase().split(/\s+/);
521
+ if (n.includes("noopener") || n.includes("noreferrer")) return e;
522
+ let i = `${r.value ?? ""} noopener`.trim();
523
+ return `<a${t.slice(0, r.start)}rel="${i}"${t.slice(r.start + r.raw.length)}>`;
473
524
  }
474
525
  return `<a${t} rel="noopener">`;
475
526
  });
476
527
  }
477
- var he = {
528
+ var Se = {
478
529
  meta: {
479
530
  id: "text-all-caps",
480
531
  severity: "warning"
481
532
  },
482
533
  block(e, t, n) {
483
534
  if (!i(e) && !s(e)) return null;
484
- let r = L(e.content ?? "").replace(/[^A-Za-zÀ-ɏ]/g, "");
485
- return r.length < n.thresholds.allCapsMinLength || r !== r.toUpperCase() ? null : { blockId: e.id };
535
+ let r = W(e.content ?? "").replace(/[^\p{L}]/gu, "");
536
+ return r.length < n.thresholds.allCapsMinLength || r !== r.toLocaleUpperCase() ? null : { blockId: e.id };
486
537
  }
487
- }, ge = {
538
+ }, Ce = {
488
539
  meta: {
489
540
  id: "text-low-contrast",
490
541
  severity: "error"
491
542
  },
492
543
  block(t, n) {
493
544
  if (!s(t) || !g(t.color) || !g(n.resolvedBackgroundColor)) return null;
494
- let r = e[t.level] >= 18 ? 3 : 4.5, i = f(t.color, n.resolvedBackgroundColor);
545
+ let r = e[t.level] >= 24 ? 3 : 4.5, i = f(t.color, n.resolvedBackgroundColor);
495
546
  return Number.isNaN(i) || i >= r ? null : {
496
547
  blockId: t.id,
497
548
  params: {
@@ -500,87 +551,91 @@ var he = {
500
551
  }
501
552
  };
502
553
  }
503
- }, _e = {
554
+ }, we = {
504
555
  id: "text-too-small",
505
556
  severity: "warning"
506
557
  };
507
- function ve(e) {
558
+ function Q(e) {
508
559
  return r(e) || o(e) ? e.fontSize : null;
509
560
  }
510
- var ye = {
511
- meta: _e,
512
- block(e, t, n) {
513
- let r = ve(e);
514
- return r === null || r >= n.thresholds.minFontSize ? null : {
515
- blockId: e.id,
516
- params: {
517
- size: r,
518
- min: n.thresholds.minFontSize
519
- }
520
- };
521
- }
522
- }, be = {
523
- meta: {
524
- id: "button-vague-label",
525
- severity: "warning"
561
+ //#endregion
562
+ //#region src/accessibility/index.ts
563
+ var $ = [
564
+ A,
565
+ ne,
566
+ re,
567
+ ie,
568
+ H,
569
+ G,
570
+ J,
571
+ ae,
572
+ ce,
573
+ de,
574
+ me,
575
+ ve,
576
+ Se,
577
+ Ce,
578
+ {
579
+ meta: we,
580
+ block(e, t, n) {
581
+ let r = Q(e);
582
+ return r === null || r >= n.thresholds.minFontSize ? null : {
583
+ blockId: e.id,
584
+ params: {
585
+ size: r,
586
+ min: n.thresholds.minFontSize
587
+ }
588
+ };
589
+ }
526
590
  },
527
- block(e, n, r) {
528
- if (!t(e)) return null;
529
- let i = (e.text ?? "").toLowerCase().replace(/\s+/g, " ").trim();
530
- return i === "" || !Y(r.locale).vagueButtonLabels.includes(i) ? null : {
531
- blockId: e.id,
532
- params: { text: e.text }
533
- };
534
- }
535
- }, xe = {
536
- meta: {
537
- id: "button-touch-target",
538
- severity: "warning"
591
+ {
592
+ meta: {
593
+ id: "button-vague-label",
594
+ severity: "warning"
595
+ },
596
+ block(e, n, r) {
597
+ if (!t(e)) return null;
598
+ let i = V(e.text ?? "");
599
+ return i === "" || !L(r.locale).vagueButtonLabels.includes(i) ? null : {
600
+ blockId: e.id,
601
+ params: { text: e.text }
602
+ };
603
+ }
539
604
  },
540
- block(e, n, r) {
541
- if (!t(e)) return null;
542
- let i = e.buttonPadding;
543
- if (!i) return null;
544
- let a = e.fontSize * 1.4 + i.top + i.bottom;
545
- return a >= r.thresholds.minTouchTargetPx ? null : {
546
- blockId: e.id,
547
- params: {
548
- height: Math.round(a),
549
- min: r.thresholds.minTouchTargetPx
550
- }
551
- };
552
- }
553
- }, Se = {
554
- id: "button-low-contrast",
555
- severity: "error"
556
- }, Q = 4.5, $ = [
557
- ee,
558
- A,
559
- j,
560
- M,
561
- F,
562
- R,
563
- V,
564
- W,
565
- q,
566
- oe,
567
- le,
568
- pe,
569
- he,
570
- ge,
571
- ye,
572
- be,
573
- xe,
574
605
  {
575
- meta: Se,
606
+ meta: {
607
+ id: "button-touch-target",
608
+ severity: "warning"
609
+ },
610
+ block(e, n, r) {
611
+ if (!t(e)) return null;
612
+ let i = e.buttonPadding;
613
+ if (!i) return null;
614
+ let a = e.fontSize * 1.4 + i.top + i.bottom;
615
+ return a >= r.thresholds.minTouchTargetPx ? null : {
616
+ blockId: e.id,
617
+ params: {
618
+ height: Math.round(a),
619
+ min: r.thresholds.minTouchTargetPx
620
+ }
621
+ };
622
+ }
623
+ },
624
+ {
625
+ meta: {
626
+ id: "button-low-contrast",
627
+ severity: "error"
628
+ },
576
629
  block(e) {
577
630
  if (!t(e)) return null;
578
631
  let n = f(e.textColor, e.backgroundColor);
579
- return Number.isNaN(n) || n >= Q ? null : {
632
+ if (Number.isNaN(n)) return null;
633
+ let r = e.fontSize >= 24 ? 3 : 4.5;
634
+ return n >= r ? null : {
580
635
  blockId: e.id,
581
636
  params: {
582
637
  ratio: n.toFixed(2),
583
- required: Q
638
+ required: r
584
639
  }
585
640
  };
586
641
  }
@@ -595,9 +650,9 @@ var ye = {
595
650
  }
596
651
  }
597
652
  ];
598
- function Ce(e, t = {}) {
653
+ function Te(e, t = {}) {
599
654
  if (t.disabled === !0) return [];
600
- let n = we(t), r = [];
655
+ let n = Ee(t), r = [];
601
656
  function i(e, t, r) {
602
657
  return {
603
658
  blockId: r.blockId,
@@ -623,7 +678,7 @@ function Ce(e, t = {}) {
623
678
  }
624
679
  return r;
625
680
  }
626
- function we(e) {
681
+ function Ee(e) {
627
682
  let t = e.rules ?? {}, n = {
628
683
  ...d,
629
684
  ...e.thresholds ?? {}
@@ -639,6 +694,6 @@ function we(e) {
639
694
  };
640
695
  }
641
696
  //#endregion
642
- export { d as DEFAULT_THRESHOLDS, $ as RULES, re as SUPPORTED_DICTIONARY_LOCALES, D as SUPPORTED_MESSAGE_LOCALES, I as extractAnchors, L as extractText, k as formatMessage, f as getContrastRatio, Y as getDictionary, O as getMessages, g as isOpaqueHex, Ce as lintAccessibility, h as parseHex, b as walkBlocks };
697
+ export { d as DEFAULT_THRESHOLDS, $ as RULES, B as SUPPORTED_DICTIONARY_LOCALES, D as SUPPORTED_MESSAGE_LOCALES, U as extractAnchors, W as extractText, k as formatMessage, f as getContrastRatio, L as getDictionary, O as getMessages, g as isOpaqueHex, Te as lintAccessibility, h as parseHex, b as walkBlocks };
643
698
 
644
699
  //# 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/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/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/dictionaries/en.ts","../src/accessibility/dictionaries/de.ts","../src/accessibility/dictionaries/index.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"],"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 A11yIssue {\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?: A11yPatch;\n}\n\nexport interface A11yPatchContext {\n updateBlock: (blockId: string, patch: Partial<Block>) => void;\n updateSettings: (patch: Partial<TemplateSettings>) => void;\n}\n\nexport interface A11yPatch {\n description: string;\n apply: (ctx: A11yPatchContext) => void;\n}\n\nexport interface A11yThresholds {\n altMaxLength: number;\n minFontSize: number;\n allCapsMinLength: number;\n minTouchTargetPx: number;\n}\n\nexport interface A11yOptions {\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 /** Per-rule severity override. Set to `'off'` to disable a specific rule. */\n rules?: Record<string, Severity>;\n thresholds?: Partial<A11yThresholds>;\n}\n\nexport interface ResolvedOptions {\n locale: string;\n rules: Record<string, Severity>;\n thresholds: A11yThresholds;\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?: A11yPatch;\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_THRESHOLDS: A11yThresholds = {\n altMaxLength: 125,\n minFontSize: 14,\n allCapsMinLength: 20,\n minTouchTargetPx: 44,\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;\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 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 visit(block, ctx);\n\n if (!isSection(block)) {\n return;\n }\n\n const sectionBg = block.styles?.backgroundColor;\n const childBg = isOpaqueHex(sectionBg)\n ? (sectionBg as string).toLowerCase()\n : ctx.resolvedBackgroundColor;\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: childBg,\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 en from \"./en\";\n\nconst de: typeof en = {\n \"img-missing-alt\":\n \"Bild ohne Alt-Text. Füge eine kurze Beschreibung hinzu oder markiere das Bild als dekorativ.\",\n \"img-alt-is-filename\":\n 'Alt-Text sieht wie ein Dateiname aus (\"{alt}\"). Beschreibe stattdessen kurz, was das Bild zeigt.',\n \"img-alt-too-long\": \"Alt-Text ist {length} Zeichen lang; bleibe unter {max}.\",\n \"img-decorative-needs-empty-alt\":\n \"Dekoratives Bild hat Alt-Text. Entferne den Alt-Text oder hebe die Markierung als dekorativ auf.\",\n \"img-linked-no-context\":\n \"Verlinktes Bild beschreibt nur das Motiv, nicht das Linkziel. Nenne das Ziel (z. B. „Frühlingssale ansehen“).\",\n \"heading-empty\":\n \"Überschrift ist leer. Füge Text hinzu oder entferne den Block.\",\n \"heading-skip-level\":\n \"Überschrift springt von H{from} auf H{to}. Eine Ebene pro Schritt.\",\n \"heading-multiple-h1\":\n \"E-Mail enthält mehr als eine H1. Verwende H1 nur einmal für die Hauptüberschrift.\",\n \"link-empty\":\n \"Ein Link in diesem Block hat keinen Text und kein beschriebenes Bild.\",\n \"link-vague-text\":\n \"Link-Text „{text}“ ist unspezifisch. Beschreibe stattdessen das Ziel.\",\n \"link-href-empty\": \"Ein Link in diesem Block hat ein leeres oder „#“-href.\",\n \"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 \"text-all-caps\":\n \"Längere Texte in Großbuchstaben sind schwerer lesbar. Verwende Groß- und Kleinschreibung.\",\n \"text-low-contrast\":\n \"Überschriftskontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"text-too-small\": \"Text ist {size}px; mindestens {min}px verwenden.\",\n \"button-vague-label\":\n \"Button-Beschriftung „{text}“ ist unspezifisch. Beschreibe die Aktion.\",\n \"button-touch-target\":\n \"Button ist etwa {height}px hoch; mindestens {min}px verwenden, um Fehltipper auf Mobilgeräten zu vermeiden.\",\n \"button-low-contrast\":\n \"Buttontextkontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"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 \"img-missing-alt\":\n \"Image is missing alt text. Add a short description, or mark the image as decorative.\",\n \"img-alt-is-filename\":\n 'Alt text looks like a filename (\"{alt}\"). Replace with a short description of what the image conveys.',\n \"img-alt-too-long\": \"Alt text is {length} characters; aim for under {max}.\",\n \"img-decorative-needs-empty-alt\":\n \"Decorative image has alt text. Either clear the alt text or unmark the image as decorative.\",\n \"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 \"heading-empty\": \"Heading is empty. Add text or remove the block.\",\n \"heading-skip-level\":\n \"Heading jumps from H{from} to H{to}. Step one level at a time.\",\n \"heading-multiple-h1\":\n \"Email has more than one H1. Use H1 once for the main heading.\",\n \"link-empty\": \"A link in this block has no text and no described image.\",\n \"link-vague-text\":\n 'Link text \"{text}\" is vague. Describe the destination instead.',\n \"link-href-empty\": \"A link in this block has an empty or '#' href.\",\n \"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 \"text-all-caps\":\n \"Long all-caps text is harder to read for everyone. Use sentence case.\",\n \"text-low-contrast\":\n \"Heading contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"text-too-small\": \"Text is {size}px; aim for at least {min}px.\",\n \"button-vague-label\": 'Button label \"{text}\" is vague. Describe the action.',\n \"button-touch-target\":\n \"Button is roughly {height}px tall; aim for at least {min}px to avoid mis-taps on mobile.\",\n \"button-low-contrast\":\n \"Button text contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"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: \"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: \"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 fix: {\n description: \"Clear alt text\",\n apply: (ctx) => ctx.updateBlock(block.id, { alt: \"\" }),\n },\n };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"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: \"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 ?? \"\") === \"\") 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 { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"img-linked-no-context\",\n severity: \"warning\",\n};\n\nconst ACTION_HINTS = [\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\nexport const imgLinkedNoContext: Rule = {\n meta,\n block(block) {\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 lower = alt.toLowerCase();\n if (ACTION_HINTS.some((hint) => lower.includes(hint))) 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 const stack: AnchorInfo[] = [];\n let textBuffer = \"\";\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);\n textBuffer = \"\";\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].hasImageWithAlt = true;\n }\n }\n },\n ontext(text) {\n if (stack.length > 0) {\n textBuffer += text;\n }\n },\n onclosetag(name) {\n if (name === \"a\" && stack.length > 0) {\n const anchor = stack.pop()!;\n anchor.text = textBuffer.trim();\n anchors.push(anchor);\n textBuffer = \"\";\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: \"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: \"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: \"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: \"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","/**\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\nexport default en;\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};\n\nexport default de;\n","import en from \"./en\";\nimport de from \"./de\";\n\nexport type Dictionary = typeof en;\n\nconst DICTIONARIES: Record<string, Dictionary> = {\n en,\n de,\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};\n\nexport const SUPPORTED_DICTIONARY_LOCALES = Object.keys(DICTIONARIES);\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 } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"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 = a.text.toLowerCase().replace(/\\s+/g, \" \").trim();\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: \"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: \"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\nfunction addNoopenerToTargetBlank(html: string): string {\n return html.replace(/<a\\b([^>]*)>/gi, (match, attrs: string) => {\n const hasTargetBlank = /\\btarget\\s*=\\s*[\"']_blank[\"']/i.test(attrs);\n if (!hasTargetBlank) return match;\n const relMatch = /\\brel\\s*=\\s*[\"']([^\"']*)[\"']/i.exec(attrs);\n if (relMatch) {\n const tokens = relMatch[1].toLowerCase().split(/\\s+/);\n if (tokens.includes(\"noopener\") || tokens.includes(\"noreferrer\")) {\n return match;\n }\n const newRel = `${relMatch[1]} noopener`.trim();\n const newAttrs = attrs.replace(relMatch[0], `rel=\"${newRel}\"`);\n return `<a${newAttrs}>`;\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: \"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(/[^A-Za-zÀ-ɏ]/g, \"\");\n if (letters.length < opts.thresholds.allCapsMinLength) return null;\n if (letters !== letters.toUpperCase()) 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: \"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 const required = fontSize >= 18 ? 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: \"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 } from \"../dictionaries\";\n\nexport const meta: RuleMeta = {\n id: \"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 = (block.text ?? \"\").toLowerCase().replace(/\\s+/g, \" \").trim();\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: \"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: \"button-low-contrast\",\n severity: \"error\",\n};\n\nconst MIN_RATIO = 4.5;\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) || ratio >= MIN_RATIO) return null;\n return {\n blockId: block.id,\n params: { ratio: ratio.toFixed(2), required: MIN_RATIO },\n };\n },\n};\n","import type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"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 {\n A11yIssue,\n A11yOptions,\n ResolvedOptions,\n Rule,\n RuleHit,\n Severity,\n} from \"../types\";\nimport { DEFAULT_THRESHOLDS } from \"../types\";\nimport { walkBlocks } from \"../walk\";\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 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: A11yOptions = {},\n): A11yIssue[] {\n if (options.disabled === true) {\n return [];\n }\n\n const opts = resolveOptions(options);\n const issues: A11yIssue[] = [];\n\n function buildIssue(\n ruleId: string,\n severity: Exclude<Severity, \"off\">,\n hit: RuleHit,\n ): A11yIssue {\n return {\n blockId: hit.blockId,\n ruleId,\n severity,\n message: formatMessage(opts.locale, ruleId as RuleMessageId, 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\nexport function resolveOptions(options: A11yOptions): ResolvedOptions {\n const overrides = options.rules ?? {};\n const thresholds = { ...DEFAULT_THRESHOLDS, ...(options.thresholds ?? {}) };\n const locale = options.locale ?? \"en\";\n\n return {\n locale,\n rules: overrides,\n thresholds,\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"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/types.ts","../src/contrast.ts","../src/walk.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"],"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 A11yIssue {\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?: A11yPatch;\n}\n\nexport interface A11yPatchContext {\n updateBlock: (blockId: string, patch: Partial<Block>) => void;\n updateSettings: (patch: Partial<TemplateSettings>) => void;\n}\n\nexport interface A11yPatch {\n description: string;\n apply: (ctx: A11yPatchContext) => void;\n}\n\nexport interface A11yThresholds {\n altMaxLength: number;\n minFontSize: number;\n allCapsMinLength: number;\n minTouchTargetPx: number;\n}\n\nexport interface A11yOptions {\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 /** Per-rule severity override. Set to `'off'` to disable a specific rule. */\n rules?: Record<string, Severity>;\n thresholds?: Partial<A11yThresholds>;\n}\n\nexport interface ResolvedOptions {\n locale: string;\n rules: Record<string, Severity>;\n thresholds: A11yThresholds;\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?: A11yPatch;\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_THRESHOLDS: A11yThresholds = {\n altMaxLength: 125,\n minFontSize: 14,\n allCapsMinLength: 20,\n minTouchTargetPx: 44,\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;\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 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 visit(block, ctx);\n\n if (!isSection(block)) {\n return;\n }\n\n const sectionBg = block.styles?.backgroundColor;\n const childBg = isOpaqueHex(sectionBg)\n ? (sectionBg as string).toLowerCase()\n : ctx.resolvedBackgroundColor;\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: childBg,\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 en from \"./en\";\n\nconst de: typeof en = {\n \"img-missing-alt\":\n \"Bild ohne Alt-Text. Füge eine kurze Beschreibung hinzu oder markiere das Bild als dekorativ.\",\n \"img-alt-is-filename\":\n 'Alt-Text sieht wie ein Dateiname aus (\"{alt}\"). Beschreibe stattdessen kurz, was das Bild zeigt.',\n \"img-alt-too-long\": \"Alt-Text ist {length} Zeichen lang; bleibe unter {max}.\",\n \"img-decorative-needs-empty-alt\":\n \"Dekoratives Bild hat Alt-Text. Entferne den Alt-Text oder hebe die Markierung als dekorativ auf.\",\n \"img-linked-no-context\":\n \"Verlinktes Bild beschreibt nur das Motiv, nicht das Linkziel. Nenne das Ziel (z. B. „Frühlingssale ansehen“).\",\n \"heading-empty\":\n \"Überschrift ist leer. Füge Text hinzu oder entferne den Block.\",\n \"heading-skip-level\":\n \"Überschrift springt von H{from} auf H{to}. Eine Ebene pro Schritt.\",\n \"heading-multiple-h1\":\n \"E-Mail enthält mehr als eine H1. Verwende H1 nur einmal für die Hauptüberschrift.\",\n \"link-empty\":\n \"Ein Link in diesem Block hat keinen Text und kein beschriebenes Bild.\",\n \"link-vague-text\":\n \"Link-Text „{text}“ ist unspezifisch. Beschreibe stattdessen das Ziel.\",\n \"link-href-empty\": \"Ein Link in diesem Block hat ein leeres oder „#“-href.\",\n \"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 \"text-all-caps\":\n \"Längere Texte in Großbuchstaben sind schwerer lesbar. Verwende Groß- und Kleinschreibung.\",\n \"text-low-contrast\":\n \"Überschriftskontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"text-too-small\": \"Text ist {size}px; mindestens {min}px verwenden.\",\n \"button-vague-label\":\n \"Button-Beschriftung „{text}“ ist unspezifisch. Beschreibe die Aktion.\",\n \"button-touch-target\":\n \"Button ist etwa {height}px hoch; mindestens {min}px verwenden, um Fehltipper auf Mobilgeräten zu vermeiden.\",\n \"button-low-contrast\":\n \"Buttontextkontrast beträgt {ratio}:1; WCAG AA verlangt mindestens {required}:1.\",\n \"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 \"img-missing-alt\":\n \"Image is missing alt text. Add a short description, or mark the image as decorative.\",\n \"img-alt-is-filename\":\n 'Alt text looks like a filename (\"{alt}\"). Replace with a short description of what the image conveys.',\n \"img-alt-too-long\": \"Alt text is {length} characters; aim for under {max}.\",\n \"img-decorative-needs-empty-alt\":\n \"Decorative image has alt text. Either clear the alt text or unmark the image as decorative.\",\n \"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 \"heading-empty\": \"Heading is empty. Add text or remove the block.\",\n \"heading-skip-level\":\n \"Heading jumps from H{from} to H{to}. Step one level at a time.\",\n \"heading-multiple-h1\":\n \"Email has more than one H1. Use H1 once for the main heading.\",\n \"link-empty\": \"A link in this block has no text and no described image.\",\n \"link-vague-text\":\n 'Link text \"{text}\" is vague. Describe the destination instead.',\n \"link-href-empty\": \"A link in this block has an empty or '#' href.\",\n \"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 \"text-all-caps\":\n \"Long all-caps text is harder to read for everyone. Use sentence case.\",\n \"text-low-contrast\":\n \"Heading contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"text-too-small\": \"Text is {size}px; aim for at least {min}px.\",\n \"button-vague-label\": 'Button label \"{text}\" is vague. Describe the action.',\n \"button-touch-target\":\n \"Button is roughly {height}px tall; aim for at least {min}px to avoid mis-taps on mobile.\",\n \"button-low-contrast\":\n \"Button text contrast is {ratio}:1; WCAG AA requires at least {required}:1.\",\n \"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: \"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: \"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 fix: {\n description: \"Clear alt text\",\n apply: (ctx) => ctx.updateBlock(block.id, { alt: \"\" }),\n },\n };\n },\n};\n","import { isImage } from \"@templatical/types\";\nimport type { Rule, RuleMeta } from \"../../types\";\n\nexport const meta: RuleMeta = {\n id: \"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: \"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 ?? \"\") === \"\") 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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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: \"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 {\n A11yIssue,\n A11yOptions,\n ResolvedOptions,\n Rule,\n RuleHit,\n Severity,\n} from \"../types\";\nimport { DEFAULT_THRESHOLDS } from \"../types\";\nimport { walkBlocks } from \"../walk\";\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 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: A11yOptions = {},\n): A11yIssue[] {\n if (options.disabled === true) {\n return [];\n }\n\n const opts = resolveOptions(options);\n const issues: A11yIssue[] = [];\n\n function buildIssue(\n ruleId: string,\n severity: Exclude<Severity, \"off\">,\n hit: RuleHit,\n ): A11yIssue {\n return {\n blockId: hit.blockId,\n ruleId,\n severity,\n message: formatMessage(opts.locale, ruleId as RuleMessageId, 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\nexport function resolveOptions(options: A11yOptions): ResolvedOptions {\n const overrides = options.rules ?? {};\n const thresholds = { ...DEFAULT_THRESHOLDS, ...(options.thresholds ?? {}) };\n const locale = options.locale ?? \"en\";\n\n return {\n locale,\n rules: overrides,\n thresholds,\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"],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@templatical/quality",
3
3
  "description": "Accessibility linter for Templatical email templates",
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
5
5
  "bugs": "https://github.com/templatical/sdk/issues",
6
6
  "dependencies": {
7
7
  "htmlparser2": "^9.1.0",
8
- "@templatical/types": "0.6.0"
8
+ "@templatical/types": "0.6.1"
9
9
  },
10
10
  "devDependencies": {
11
11
  "typescript": "^6.0.3",