@samva/markup 0.7.0 → 0.8.0

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.
Files changed (73) hide show
  1. package/AGENTS.md +14 -0
  2. package/dist/brand-DcoVkh_S.d.ts +88 -0
  3. package/dist/{catalog-60eaYC7b.d.ts → catalog-CKuOooB8.d.ts} +13 -15
  4. package/dist/{checks-DBweSIPN.js → checks-D73Xizbw.js} +414 -2
  5. package/dist/checks.js +1 -1
  6. package/dist/classes.d.ts +2 -3
  7. package/dist/compiler.d.ts +62 -45
  8. package/dist/compiler.js +179 -857
  9. package/dist/{corpus-BjYMqOoY.d.ts → corpus-CPPfgAHN.d.ts} +14 -14
  10. package/dist/{css-COcFOT1y.d.ts → css-CS2jLID6.d.ts} +28 -2
  11. package/dist/email/assets.d.ts +40 -0
  12. package/dist/email/assets.js +63 -0
  13. package/dist/email/brand-footer.d.ts +34 -0
  14. package/dist/email/brand-footer.js +62 -0
  15. package/dist/email/brand.d.ts +2 -0
  16. package/dist/email/brand.js +148 -0
  17. package/dist/email/html.d.ts +2 -42
  18. package/dist/email/html.js +11 -0
  19. package/dist/email/render.d.ts +12 -5
  20. package/dist/email/render.js +2 -5
  21. package/dist/email/selection.d.ts +2 -91
  22. package/dist/email/stylesheets.d.ts +1 -1
  23. package/dist/email/stylesheets.js +3 -0
  24. package/dist/html-Bl9BAdqc.d.ts +42 -0
  25. package/dist/{ops-2aiiYO7m.js → ops-BvwqHl7V.js} +2 -2
  26. package/dist/ops.js +1 -1
  27. package/dist/registry-Dcp6QEK_.d.ts +73 -0
  28. package/dist/registry.d.ts +2 -42
  29. package/dist/{render-aaJBnsXK.js → render-vAv_4r9v.js} +174 -17
  30. package/dist/selection-CCArx9lU.d.ts +91 -0
  31. package/dist/{showcase-DGZAWn4Z.d.ts → showcase-egkusQBf.d.ts} +15 -56
  32. package/dist/{presets → starters}/category-mapping.d.ts +5 -5
  33. package/dist/{presets → starters}/category-mapping.js +5 -5
  34. package/dist/starters/corpus.d.ts +2 -0
  35. package/dist/{presets → starters}/corpus.js +15 -15
  36. package/dist/starters/library.d.ts +18 -0
  37. package/dist/starters/library.js +692 -0
  38. package/dist/{presets → starters}/public-gallery.d.ts +47 -47
  39. package/dist/{presets → starters}/public-gallery.js +92 -92
  40. package/dist/{presets → starters}/public-handoff.d.ts +14 -14
  41. package/dist/{presets → starters}/public-handoff.js +40 -40
  42. package/dist/{presets → starters}/public-preview-artifacts.d.ts +4 -4
  43. package/dist/{presets → starters}/public-preview-artifacts.js +15 -15
  44. package/dist/{presets → starters}/public-preview.d.ts +1 -1
  45. package/dist/{presets → starters}/public-preview.js +3 -3
  46. package/dist/starters/showcase.d.ts +2 -0
  47. package/dist/starters/showcase.js +335 -0
  48. package/dist/starters/sources.d.ts +45 -0
  49. package/dist/{presets → starters}/sources.js +1951 -272
  50. package/dist/starters/theme.d.ts +45 -0
  51. package/dist/starters/theme.js +77 -0
  52. package/dist/starters.d.ts +2 -0
  53. package/dist/{presets.js → starters.js} +4 -11
  54. package/dist/tailwind-BYq7Lo6u.js +727 -0
  55. package/dist/theme-css-hFqcPSar.js +587 -0
  56. package/dist/theme-css.d.ts +56 -15
  57. package/dist/theme-css.js +2 -509
  58. package/dist/tsx-tracer.js +1 -1
  59. package/dist/unsubscribe-link-CoesnQDU.js +26 -0
  60. package/package.json +53 -41
  61. package/samva-brand.d.ts +14 -0
  62. package/dist/footer.d.ts +0 -66
  63. package/dist/footer.js +0 -93
  64. package/dist/presets/corpus.d.ts +0 -2
  65. package/dist/presets/showcase.d.ts +0 -2
  66. package/dist/presets/showcase.js +0 -627
  67. package/dist/presets/sources.d.ts +0 -38
  68. package/dist/presets.d.ts +0 -2
  69. package/dist/theme-6ApjjiBo.d.ts +0 -116
  70. package/dist/theme.d.ts +0 -2
  71. package/dist/theme.js +0 -118
  72. package/dist/tokens.d.ts +0 -6
  73. package/dist/tokens.js +0 -417
@@ -1,91 +1,2 @@
1
- import { i as JsxSourceLocation } from "../source-locations-DrCM82Ym.js";
2
- //#region src/email/selection.d.ts
3
- /**
4
- * The attribute a preview stamps so a click in a rendered document resolves to
5
- * exactly one rendered element. A delivered message never carries it.
6
- */
7
- declare const INSTANCE_PATH_ATTRIBUTE = "data-samva-instance";
8
- /** Where one element sits in the emitted document, with the authoring that produced it. */
9
- interface EmittedPosition {
10
- readonly start: number;
11
- readonly end: number;
12
- readonly origins: ReadonlyArray<JsxSourceLocation>;
13
- /**
14
- * Dot-separated element indices from the document root, assigned as the
15
- * document is written. It is the identity of one rendered instance.
16
- */
17
- readonly instancePath: string;
18
- readonly tag: string;
19
- /** False when a compiler primitive generated this element. */
20
- readonly authored: boolean;
21
- }
22
- /** One rendered element, and the authoring it came from. */
23
- interface EmailElementSelection {
24
- /**
25
- * Dot-separated element indices from the document root. It is the identity
26
- * of one rendered instance, which is what tells two iterations of the same
27
- * authored element apart: their origins are identical.
28
- */
29
- readonly instancePath: string;
30
- readonly tag: string;
31
- /**
32
- * The authored JSX element first, then each enclosing component call site,
33
- * innermost first. Empty when the compiler generated this element with no
34
- * authoring on the stack — the hidden preheader block, for instance.
35
- */
36
- readonly origins: ReadonlyArray<JsxSourceLocation>;
37
- /**
38
- * False when a compiler primitive generated this element. `origins[0]` is
39
- * then the authoring element that asked for it rather than this element
40
- * itself, so the wrapper selects the primitive the author wrote.
41
- */
42
- readonly authored: boolean;
43
- /** 1-based position among the rendered elements sharing this origin chain and tag. */
44
- readonly occurrence: number;
45
- /**
46
- * How many rendered elements share that chain and tag. Above one, one piece
47
- * of authoring produced several rendered elements — a loop over a component,
48
- * or a primitive that generates a repeated cell — so an edit there changes
49
- * all of them.
50
- */
51
- readonly occurrences: number;
52
- /** Absent for the document root. */
53
- readonly parentPath?: string;
54
- /** Offset range in the rendered HTML this selection was taken from. */
55
- readonly start: number;
56
- readonly end: number;
57
- }
58
- /**
59
- * Turn one render's emitted positions into selections, in document order.
60
- *
61
- * The serializer records an element once its content is written, so positions
62
- * arrive innermost-first; document order is what an outline and an ancestry
63
- * walk both need.
64
- */
65
- declare const emailSelections: (positions: readonly EmittedPosition[]) => readonly EmailElementSelection[];
66
- /** Look selections up by the instance path a rendered document carries. */
67
- declare const emailSelectionIndex: (selections: readonly EmailElementSelection[]) => ReadonlyMap<string, EmailElementSelection>;
68
- /**
69
- * The authoring element a selection edits. For an authored element that is the
70
- * element itself; for a generated wrapper it is the primitive that asked for
71
- * it. Undefined only where the compiler generated markup with no authoring on
72
- * the stack, which nothing can edit in place.
73
- */
74
- declare const emailAuthoringTarget: (selection: EmailElementSelection) => JsxSourceLocation | undefined;
75
- /** Root-first ancestry of a selection, itself last. */
76
- declare const emailSelectionAncestry: (index: ReadonlyMap<string, EmailElementSelection>, instancePath: string) => readonly EmailElementSelection[];
77
- /**
78
- * Every rendered instance produced through the same call site, in document
79
- * order. One entry means the call ran once; several mean it repeated.
80
- */
81
- declare const emailSelectionSiblings: (selections: readonly EmailElementSelection[], selection: EmailElementSelection) => readonly EmailElementSelection[];
82
- /**
83
- * Every rendered instance of the same authored element, across every call site
84
- * that reaches it. A shared component edited at its definition changes all of
85
- * these; edited at one call site it changes only `emailSelectionSiblings`. A
86
- * control has to say which of the two it is doing before it applies.
87
- */
88
- declare const emailDefinitionInstances: (selections: readonly EmailElementSelection[], selection: EmailElementSelection) => readonly EmailElementSelection[];
89
- //#endregion
90
- export { EmailElementSelection, EmittedPosition, INSTANCE_PATH_ATTRIBUTE, emailAuthoringTarget, emailDefinitionInstances, emailSelectionAncestry, emailSelectionIndex, emailSelectionSiblings, emailSelections };
91
- //# sourceMappingURL=selection.d.ts.map
1
+ import { a as emailDefinitionInstances, c as emailSelectionSiblings, i as emailAuthoringTarget, l as emailSelections, n as EmittedPosition, o as emailSelectionAncestry, r as INSTANCE_PATH_ATTRIBUTE, s as emailSelectionIndex, t as EmailElementSelection } from "../selection-CCArx9lU.js";
2
+ export { EmailElementSelection, EmittedPosition, INSTANCE_PATH_ATTRIBUTE, emailAuthoringTarget, emailDefinitionInstances, emailSelectionAncestry, emailSelectionIndex, emailSelectionSiblings, emailSelections };
@@ -1,4 +1,4 @@
1
- import { i as Stylesheet } from "../css-COcFOT1y.js";
1
+ import { i as Stylesheet } from "../css-CS2jLID6.js";
2
2
  //#region src/email/stylesheets.d.ts
3
3
  /** Called by a compiler-generated stylesheet module at import time. */
4
4
  declare const registerStylesheet: (sheet: Stylesheet) => void;
@@ -23,14 +23,17 @@ const collectStylesheets = () => {
23
23
  })));
24
24
  const variables = {};
25
25
  const headAtRules = [];
26
+ const fontFaces = /* @__PURE__ */ new Map();
26
27
  const diagnostics = registry.flatMap(({ sheet }) => sheet.diagnostics);
27
28
  for (const { sheet } of registry) {
28
29
  Object.assign(variables, sheet.variables);
29
30
  headAtRules.push(...sheet.headAtRules);
31
+ for (const face of sheet.fontFaces) fontFaces.set(face.css, face);
30
32
  }
31
33
  return {
32
34
  rules,
33
35
  headAtRules,
36
+ fontFaces: [...fontFaces.values()],
34
37
  variables,
35
38
  diagnostics
36
39
  };
@@ -0,0 +1,42 @@
1
+ import { r as EmailDiagnostic } from "./diagnostics-BMYtBCQk.js";
2
+ import { m as EmailNode } from "./elements-BzjjnoCS.js";
3
+ import { n as EmittedPosition } from "./selection-CCArx9lU.js";
4
+ //#region src/email/html.d.ts
5
+ /** Escape a value for element content. */
6
+ declare const escapeText: (value: string) => string;
7
+ /** Escape a value for a double-quoted attribute. */
8
+ declare const escapeAttribute: (value: string) => string;
9
+ /** camelCase to kebab-case; already-kebab names pass through unchanged. */
10
+ declare const kebab: (property: string) => string;
11
+ /** A bare number is pixels for every property that takes a length. */
12
+ declare const declarationValue: (property: string, value: string | number) => string;
13
+ interface EmailHtmlOutput {
14
+ readonly html: string;
15
+ readonly diagnostics: readonly EmailDiagnostic[];
16
+ /**
17
+ * Each emitted element's offset range and origins. A compatibility finding
18
+ * carries a position in the emitted document, and this is what turns that
19
+ * back into the authoring element the author can edit.
20
+ */
21
+ readonly positions: readonly EmittedPosition[];
22
+ }
23
+ /** How this document will be consumed. Delivery is the default; export is opt-in. */
24
+ interface SerializeEmailOptions {
25
+ /**
26
+ * Accept relative references. A local export serves assets from a relative
27
+ * base beside the file; a delivered message has no directory to resolve one
28
+ * against, so this stays off for anything that will be sent.
29
+ */
30
+ readonly relativeUrls?: boolean | undefined;
31
+ /**
32
+ * Stamp every element with its instance path, so an editor preview can
33
+ * resolve a click in the rendered document to exactly one rendered element.
34
+ * A delivered message never carries it.
35
+ */
36
+ readonly instancePaths?: boolean | undefined;
37
+ }
38
+ /** Lower the derived tree to email HTML, reporting every unsupported construct with its origin. */
39
+ declare const serializeEmailHtml: (node: EmailNode, options?: SerializeEmailOptions) => EmailHtmlOutput;
40
+ //#endregion
41
+ export { escapeText as a, escapeAttribute as i, SerializeEmailOptions as n, kebab as o, declarationValue as r, serializeEmailHtml as s, EmailHtmlOutput as t };
42
+ //# sourceMappingURL=html-Bl9BAdqc.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { documentChannel } from "./ast.js";
2
2
  import { CHANNEL_TAG_SPECS, allowedChildren, parseSml } from "./parser.js";
3
3
  import { compileSml } from "./compile.js";
4
- import { a as isBreakingCompatibilityCheck, r as checkSmlCompatibility } from "./checks-DBweSIPN.js";
4
+ import { a as isBreakingCompatibilityCheck, r as checkSmlCompatibility } from "./checks-D73Xizbw.js";
5
5
  import { printSml } from "./printer.js";
6
6
  import { Result, TaggedError } from "better-result";
7
7
  //#region src/internal/sha256.ts
@@ -627,4 +627,4 @@ const applyOp = (source, op, options) => applyOps(source, [op], options);
627
627
  //#endregion
628
628
  export { sourceRevision as a, normalizeSml as i, applyOp as n, applyOps as r, SmlOpError as t };
629
629
 
630
- //# sourceMappingURL=ops-2aiiYO7m.js.map
630
+ //# sourceMappingURL=ops-BvwqHl7V.js.map
package/dist/ops.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as sourceRevision, i as normalizeSml, n as applyOp, r as applyOps, t as SmlOpError } from "./ops-2aiiYO7m.js";
1
+ import { a as sourceRevision, i as normalizeSml, n as applyOp, r as applyOps, t as SmlOpError } from "./ops-BvwqHl7V.js";
2
2
  export { SmlOpError, applyOp, applyOps, normalizeSml, sourceRevision };
@@ -0,0 +1,73 @@
1
+ //#region src/theme.d.ts
2
+ interface TextToken {
3
+ readonly sizePx: number;
4
+ readonly lineHeightPx: number;
5
+ }
6
+ interface UtilityScale {
7
+ /** Flattened color names ("red-500", "white", "brand") → 6-digit lowercase hex. */
8
+ readonly colors: Readonly<Record<string, string>>;
9
+ /**
10
+ * Dark counterparts, colors namespace only (dark radius/fonts are not a
11
+ * thing). Only tokens with a dark pair appear; a color-family utility that
12
+ * resolves a paired token also emits the dark head rule automatically.
13
+ */
14
+ readonly colorsDark: Readonly<Record<string, string>>;
15
+ /** px per spacing unit (`p-4` → 4 × spacingPx). */
16
+ readonly spacingPx: number;
17
+ /** text-{size} → font-size / line-height px pair. */
18
+ readonly text: Readonly<Record<string, TextToken>>;
19
+ /** leading-{name} → unitless line-height ratio. */
20
+ readonly leading: Readonly<Record<string, number>>;
21
+ /** tracking-{name} → letter-spacing em. */
22
+ readonly trackingEm: Readonly<Record<string, number>>;
23
+ /** rounded-{name} → border-radius px. */
24
+ readonly radiusPx: Readonly<Record<string, number>>;
25
+ /** font-{name} → font-weight number. */
26
+ readonly fontWeights: Readonly<Record<string, number>>;
27
+ /** font-{name} → the inline font-family stack, ending in a generic family. */
28
+ readonly fontFamilies: Readonly<Record<string, string>>;
29
+ /** max-w-{name} → px. */
30
+ readonly containerPx: Readonly<Record<string, number>>;
31
+ }
32
+ //#endregion
33
+ //#region src/registry.d.ts
34
+ interface Declaration {
35
+ readonly property: string;
36
+ readonly value: string;
37
+ }
38
+ interface UtilityFamily {
39
+ readonly root: string;
40
+ /** Accepts `root` with no value ("border", "rounded", "block"). */
41
+ readonly bare?: ReadonlyArray<Declaration> | undefined;
42
+ /** Resolve a named value; null = this family does not accept it. */
43
+ readonly named?: ((value: string, theme: UtilityScale) => ReadonlyArray<Declaration> | null) | undefined;
44
+ /** Resolve an arbitrary `[...]` value; null = malformed for this family. */
45
+ readonly arbitrary?: ((value: string, theme: UtilityScale) => ReadonlyArray<Declaration> | null) | undefined;
46
+ /** dark: variant is only legal on color families. */
47
+ readonly colorFamily?: boolean | undefined;
48
+ /**
49
+ * Base-variant only: sm:/dark: are rejected. Set on the gradient family —
50
+ * its declarations are compiler-internal placeholders that must be combined
51
+ * per node, which head rules cannot express.
52
+ */
53
+ readonly baseOnly?: boolean | undefined;
54
+ /** "warn" families compile but surface a lint warning. */
55
+ readonly warn?: string | undefined;
56
+ readonly description: string;
57
+ readonly caniemailSlugs: ReadonlyArray<string>;
58
+ }
59
+ interface ForbiddenUtility {
60
+ /** Exact class or `prefix-` match against the candidate base. */
61
+ readonly match: string;
62
+ readonly message: string;
63
+ }
64
+ declare const GRADIENT_DIRECTION_PROPERTY = "-sml-gradient-direction";
65
+ declare const GRADIENT_FROM_PROPERTY = "-sml-gradient-from";
66
+ declare const GRADIENT_TO_PROPERTY = "-sml-gradient-to";
67
+ declare const utilityRegistry: ReadonlyArray<UtilityFamily>;
68
+ declare const forbiddenUtilities: ReadonlyArray<ForbiddenUtility>;
69
+ /** Every root the resolver may try to split on, longest-match first (candidate.ts). */
70
+ declare const registryRoots: ReadonlySet<string>;
71
+ //#endregion
72
+ export { GRADIENT_TO_PROPERTY as a, registryRoots as c, GRADIENT_FROM_PROPERTY as i, utilityRegistry as l, ForbiddenUtility as n, UtilityFamily as o, GRADIENT_DIRECTION_PROPERTY as r, forbiddenUtilities as s, Declaration as t, UtilityScale as u };
73
+ //# sourceMappingURL=registry-Dcp6QEK_.d.ts.map
@@ -1,42 +1,2 @@
1
- import { s as TokenTable } from "./theme-6ApjjiBo.js";
2
- //#region src/registry.d.ts
3
- interface Declaration {
4
- readonly property: string;
5
- readonly value: string;
6
- }
7
- interface UtilityFamily {
8
- readonly root: string;
9
- /** Accepts `root` with no value ("border", "rounded", "block"). */
10
- readonly bare?: ReadonlyArray<Declaration> | undefined;
11
- /** Resolve a named value; null = this family does not accept it. */
12
- readonly named?: ((value: string, theme: TokenTable) => ReadonlyArray<Declaration> | null) | undefined;
13
- /** Resolve an arbitrary `[...]` value; null = malformed for this family. */
14
- readonly arbitrary?: ((value: string, theme: TokenTable) => ReadonlyArray<Declaration> | null) | undefined;
15
- /** dark: variant is only legal on color families. */
16
- readonly colorFamily?: boolean | undefined;
17
- /**
18
- * Base-variant only: sm:/dark: are rejected. Set on the gradient family —
19
- * its declarations are compiler-internal placeholders that must be combined
20
- * per node, which head rules cannot express.
21
- */
22
- readonly baseOnly?: boolean | undefined;
23
- /** "warn" families compile but surface a lint warning. */
24
- readonly warn?: string | undefined;
25
- readonly description: string;
26
- readonly caniemailSlugs: ReadonlyArray<string>;
27
- }
28
- interface ForbiddenUtility {
29
- /** Exact class or `prefix-` match against the candidate base. */
30
- readonly match: string;
31
- readonly message: string;
32
- }
33
- declare const GRADIENT_DIRECTION_PROPERTY = "-sml-gradient-direction";
34
- declare const GRADIENT_FROM_PROPERTY = "-sml-gradient-from";
35
- declare const GRADIENT_TO_PROPERTY = "-sml-gradient-to";
36
- declare const utilityRegistry: ReadonlyArray<UtilityFamily>;
37
- declare const forbiddenUtilities: ReadonlyArray<ForbiddenUtility>;
38
- /** Every root the resolver may try to split on, longest-match first (candidate.ts). */
39
- declare const registryRoots: ReadonlySet<string>;
40
- //#endregion
41
- export { Declaration, ForbiddenUtility, GRADIENT_DIRECTION_PROPERTY, GRADIENT_FROM_PROPERTY, GRADIENT_TO_PROPERTY, UtilityFamily, forbiddenUtilities, registryRoots, utilityRegistry };
42
- //# sourceMappingURL=registry.d.ts.map
1
+ import { a as GRADIENT_TO_PROPERTY, c as registryRoots, i as GRADIENT_FROM_PROPERTY, l as utilityRegistry, n as ForbiddenUtility, o as UtilityFamily, r as GRADIENT_DIRECTION_PROPERTY, s as forbiddenUtilities, t as Declaration } from "./registry-Dcp6QEK_.js";
2
+ export { Declaration, ForbiddenUtility, GRADIENT_DIRECTION_PROPERTY, GRADIENT_FROM_PROPERTY, GRADIENT_TO_PROPERTY, UtilityFamily, forbiddenUtilities, registryRoots, utilityRegistry };
@@ -1,6 +1,6 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { EmailNode, drainEmailDiagnostics } from "./email/jsx-runtime.js";
3
- import { declarationValue, kebab, serializeEmailHtml } from "./email/html.js";
3
+ import { declarationValue, escapeAttribute, kebab, serializeEmailHtml } from "./email/html.js";
4
4
  import { PREHEADER_ATTRIBUTE, preheaderNode } from "./email/components.js";
5
5
  import { EmailCompileError, hasBlockingDiagnostic } from "./email/diagnostics.js";
6
6
  import { snapshotStylesheets } from "./email/stylesheets.js";
@@ -192,6 +192,95 @@ const renderHeadCss = (rules, headAtRules, variables) => {
192
192
  return blocks.join("");
193
193
  };
194
194
  //#endregion
195
+ //#region src/email/font-stacks.ts
196
+ /** Generic families every email client understands as the last resort in a stack. */
197
+ const GENERIC_FAMILIES = /* @__PURE__ */ new Set([
198
+ "serif",
199
+ "sans-serif",
200
+ "monospace",
201
+ "cursive",
202
+ "fantasy"
203
+ ]);
204
+ /** One family name as written, without its quotes and with runs of spaces collapsed. */
205
+ const unquote = (value) => {
206
+ const trimmed = value.trim();
207
+ const quote = trimmed[0];
208
+ return (quote === "\"" || quote === "'") && trimmed.endsWith(quote) && trimmed.length >= 2 ? trimmed.slice(1, -1) : trimmed.replace(/\s+/g, " ");
209
+ };
210
+ /** The families of a `font-family` value, unquoted, in stack order. */
211
+ const fontStackFamilies = (value) => {
212
+ const families = [];
213
+ let start = 0;
214
+ let quote;
215
+ for (let index = 0; index < value.length; index++) {
216
+ const character = value[index];
217
+ if (quote !== void 0) {
218
+ if (character === quote) quote = void 0;
219
+ } else if (character === "'" || character === "\"") quote = character;
220
+ else if (character === ",") {
221
+ families.push(unquote(value.slice(start, index)));
222
+ start = index + 1;
223
+ }
224
+ }
225
+ families.push(unquote(value.slice(start)));
226
+ return families.filter((family) => family !== "");
227
+ };
228
+ const FONT_SIZE = /^(?:xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|larger|smaller|[+-]?(?:\d+\.?\d*|\.\d+)(?:[a-z]+|%)|(?:calc|clamp|min|max|var)\(.*\))$/i;
229
+ /** Whitespace-separated tokens at the top level, keeping parentheses and quotes whole. */
230
+ const shorthandTokens = (value) => {
231
+ const tokens = [];
232
+ let current = "";
233
+ let depth = 0;
234
+ let quote;
235
+ for (const character of value.trim()) {
236
+ if (quote !== void 0) {
237
+ if (character === quote) quote = void 0;
238
+ } else if (character === "'" || character === "\"") quote = character;
239
+ else if (character === "(") depth++;
240
+ else if (character === ")") depth--;
241
+ else if (/\s/.test(character) && depth === 0) {
242
+ if (current !== "") tokens.push(current);
243
+ current = "";
244
+ continue;
245
+ }
246
+ current += character;
247
+ }
248
+ if (current !== "") tokens.push(current);
249
+ return tokens;
250
+ };
251
+ /**
252
+ * The family list of a `font` shorthand: everything after the size (and its
253
+ * optional line height), or `undefined` for a system-font keyword that names
254
+ * no family.
255
+ */
256
+ const fontShorthandFamilies = (value) => {
257
+ const tokens = shorthandTokens(value);
258
+ const size = tokens.findIndex((token) => FONT_SIZE.test(token.replace(/\/.*$/, "")));
259
+ if (size < 0) return void 0;
260
+ let next = size + 1;
261
+ const sizeToken = tokens[size];
262
+ if (sizeToken.endsWith("/")) next += 1;
263
+ else if (!sizeToken.includes("/") && tokens[next]?.startsWith("/")) next += tokens[next] === "/" ? 2 : 1;
264
+ const family = tokens.slice(next).join(" ");
265
+ return family === "" ? void 0 : family;
266
+ };
267
+ /**
268
+ * True when a stack names a declared project family without ending in a
269
+ * generic. `declared` holds lowercased family names. A quoted `"sans-serif"`
270
+ * names a font called sans-serif, not the generic, so it does not count.
271
+ */
272
+ const lacksGenericFallback = (value, declared) => {
273
+ const stack = value.replace(/\s*!important\s*$/i, "");
274
+ if (!fontStackFamilies(stack).some((family) => declared.has(family.toLowerCase()))) return false;
275
+ const last = stack.slice(stack.lastIndexOf(",") + 1).trim();
276
+ return !GENERIC_FAMILIES.has(last.toLowerCase());
277
+ };
278
+ const fontFallbackDiagnostic = (value) => ({
279
+ code: "email-font-fallback",
280
+ severity: "error",
281
+ message: `font-family ${JSON.stringify(value)} uses a project font without a generic fallback. Clients that drop webfonts (Gmail, classic Outlook) render this stack as written, so end it with sans-serif, serif or monospace.`
282
+ });
283
+ //#endregion
195
284
  //#region src/email/selectors.ts
196
285
  const classListOf = (element) => {
197
286
  const value = element.props.class ?? element.props.className;
@@ -260,6 +349,17 @@ const matchesFrom = (selector, index, ancestry) => {
260
349
  const matchesSelector = (selector, ancestry) => selector === void 0 || selector.parts.length === 0 ? false : matchesFrom(selector, selector.parts.length - 1, ancestry);
261
350
  //#endregion
262
351
  //#region src/email/cascade.ts
352
+ const VARIABLE_REFERENCE = /var\(\s*--[\w-]+\s*(?:,[^()]*)?\)/g;
353
+ /**
354
+ * Compatibility markup a primitive emits (a Button's VML) and a presentational
355
+ * `bgcolor` attribute are not CSS, so no client resolves `var()` in them. Their
356
+ * theme variables resolve here, in the scope of the element they sit in, as a
357
+ * declaration on that element would.
358
+ */
359
+ const resolveMarkupVariables = (markup, variables) => markup.replace(VARIABLE_REFERENCE, (reference) => {
360
+ const value = finalizeValue(reference, variables);
361
+ return value === reference ? reference : escapeAttribute(value);
362
+ });
263
363
  /**
264
364
  * Cascade rank as the tuple CSS actually sorts by: importance first, then
265
365
  * whether the declaration is attached to the element itself, then layer, then
@@ -353,6 +453,14 @@ const applyStylesheet = (root, sheet, options = {}) => {
353
453
  }
354
454
  const conditionalWritingRules = retained.filter((rule) => rule.declarations.some((declaration) => declaration.property === "direction" || declaration.property === "writing-mode" || declaration.property === "all"));
355
455
  const unmatched = /* @__PURE__ */ new Map();
456
+ const declaredFamilies = new Set(sheet.fontFaces.map((face) => face.family.toLowerCase()));
457
+ const fallbackFindings = /* @__PURE__ */ new Map();
458
+ const checkFallback = (property, value, origins) => {
459
+ if (declaredFamilies.size === 0) return;
460
+ const stack = property === "font-family" ? value : property === "font" ? fontShorthandFamilies(value) : void 0;
461
+ if (stack === void 0 || fallbackFindings.has(stack)) return;
462
+ if (lacksGenericFallback(stack, declaredFamilies)) fallbackFindings.set(stack, origins);
463
+ };
356
464
  /**
357
465
  * Children are visited with their own preceding-sibling list so `+`/`~` work.
358
466
  * A fragment is not an element in the serialized document, so its children
@@ -371,6 +479,10 @@ const applyStylesheet = (root, sheet, options = {}) => {
371
479
  return result;
372
480
  }
373
481
  if (child.type === "Fragment") return EmailNode.Fragment({ children: visit(child.children) });
482
+ if (child.type === "Raw" && child.html.includes("var(")) return EmailNode.Raw({
483
+ ...child,
484
+ html: resolveMarkupVariables(child.html, variables)
485
+ });
374
486
  return child;
375
487
  });
376
488
  return visit(children);
@@ -422,15 +534,21 @@ const applyStylesheet = (root, sheet, options = {}) => {
422
534
  writingMode: winners.has("all") ? "unknown" : inheritedValue("writing-mode", inheritedWriting.writingMode, "horizontal-tb")
423
535
  };
424
536
  const emitted = [...winners].filter(([property]) => !property.startsWith("--")).sort(([, left], [, right]) => compareRank(left.key, right.key)).map(([property, entry]) => [property, finalizeValue(entry.value, variables)]);
537
+ for (const [property, value] of emitted) checkFallback(property, value, element.origins.slice(0, 1));
425
538
  const physical = /* @__PURE__ */ new Map();
426
539
  for (const [property, value] of emitted) for (const [name, lowered] of conditionalWriting ? [[property, value]] : physicalPadding(property, value, writing)) {
427
540
  physical.delete(name);
428
541
  physical.set(name, lowered);
429
542
  }
543
+ const bgcolor = element.props.bgcolor;
544
+ const props = typeof bgcolor === "string" && bgcolor.includes("var(") ? {
545
+ ...element.props,
546
+ bgcolor: finalizeValue(bgcolor, variables)
547
+ } : element.props;
430
548
  return EmailNode.Element({
431
549
  ...element,
432
- props: emitted.length === 0 ? element.props : {
433
- ...element.props,
550
+ props: emitted.length === 0 ? props : {
551
+ ...props,
434
552
  style: Object.fromEntries(physical)
435
553
  },
436
554
  children: visitChildren(element.children, ancestry, variables, writing)
@@ -441,6 +559,11 @@ const applyStylesheet = (root, sheet, options = {}) => {
441
559
  parent: void 0,
442
560
  previous: []
443
561
  }, sheet.variables, horizontalWriting) : root.type === "Fragment" ? EmailNode.Fragment({ children: visitChildren(root.children, void 0, sheet.variables, horizontalWriting) }) : root;
562
+ for (const rule of retained) for (const declaration of rule.declarations) checkFallback(declaration.property, finalizeValue(declaration.value, sheet.variables), []);
563
+ for (const [value, origins] of fallbackFindings) diagnostics.push({
564
+ ...fontFallbackDiagnostic(value),
565
+ origins
566
+ });
444
567
  for (const [name, origins] of unmatched) diagnostics.push({
445
568
  code: "unmatched-class",
446
569
  severity: "warning",
@@ -450,6 +573,7 @@ const applyStylesheet = (root, sheet, options = {}) => {
450
573
  return {
451
574
  tree,
452
575
  headCss: renderHeadCss(retained, sheet.headAtRules, sheet.variables),
576
+ fontFaceCss: sheet.fontFaces.map((face) => face.css).join(""),
453
577
  diagnostics
454
578
  };
455
579
  };
@@ -510,8 +634,21 @@ const withPreheader = (node, text) => {
510
634
  };
511
635
  return insert(node) ?? EmailNode.Fragment({ children: [preheaderNode(text), node] });
512
636
  };
513
- /** Put the retained rules in a `<style>` at the end of `<head>`. */
514
- const withHeadCss = (node, css) => {
637
+ const headStyle = (css, origins) => EmailNode.Element({
638
+ tag: "style",
639
+ props: {},
640
+ children: [EmailNode.Text({ value: css })],
641
+ origins,
642
+ authored: false
643
+ });
644
+ /**
645
+ * Put the retained rules in a `<style>` at the end of `<head>`, and project
646
+ * `@font-face` rules in a sibling classic Outlook cannot see. Outlook for
647
+ * Windows registers a face it cannot load and then renders the family in Times
648
+ * New Roman, ignoring the stack; hidden, the family is merely missing and the
649
+ * stack's fallback applies.
650
+ */
651
+ const withHeadCss = (node, css, fontFaceCss) => {
515
652
  const insert = (current) => {
516
653
  if (current.type === "Fragment") {
517
654
  for (const [index, child] of current.children.entries()) {
@@ -523,13 +660,23 @@ const withHeadCss = (node, css) => {
523
660
  if (current.type !== "Element") return void 0;
524
661
  if (current.tag === "head") return EmailNode.Element({
525
662
  ...current,
526
- children: [...current.children, EmailNode.Element({
527
- tag: "style",
528
- props: {},
529
- children: [EmailNode.Text({ value: css })],
530
- origins: current.origins,
531
- authored: false
532
- })]
663
+ children: [
664
+ ...current.children,
665
+ ...css === "" ? [] : [headStyle(css, current.origins)],
666
+ ...fontFaceCss === "" ? [] : [
667
+ EmailNode.Raw({
668
+ html: "<!--[if !mso]><!-->",
669
+ text: "",
670
+ origins: []
671
+ }),
672
+ headStyle(fontFaceCss, current.origins),
673
+ EmailNode.Raw({
674
+ html: "<!--<![endif]-->",
675
+ text: "",
676
+ origins: []
677
+ })
678
+ ]
679
+ ]
533
680
  });
534
681
  for (const [index, child] of current.children.entries()) {
535
682
  const replaced = insert(child);
@@ -551,8 +698,17 @@ const renderEmail = (template, data, options = {}) => renderEmailWith(snapshotSt
551
698
  const bindStylesheets = (stylesheet = snapshotStylesheets()) => (template, data, options = {}) => renderEmailWith(stylesheet, template, data, options);
552
699
  const renderEmailWith = (stylesheet, template, data, options) => {
553
700
  if (!template.schema.validate(data)) throw new TypeError(`Invalid template input: ${JSON.stringify(template.schema.validate.errors)}`);
701
+ return renderEmailContent(stylesheet, () => template.render(data), options);
702
+ };
703
+ /**
704
+ * Render authored content against a stylesheet: inline what a client honors,
705
+ * keep the rest for `<head>`, and lower to HTML and text. `renderEmail` runs a
706
+ * validated template through it; a host that composes content without a
707
+ * template definition calls it directly.
708
+ */
709
+ const renderEmailContent = (stylesheet, render, options = {}) => {
554
710
  drainEmailDiagnostics();
555
- const content = template.render(data);
711
+ const content = render();
556
712
  const invalid = [];
557
713
  const fail = (code, message) => invalid.push({
558
714
  code,
@@ -570,12 +726,13 @@ const renderEmailWith = (stylesheet, template, data, options) => {
570
726
  const styled = stylesheet === void 0 ? {
571
727
  tree: base,
572
728
  headCss: "",
729
+ fontFaceCss: "",
573
730
  diagnostics: []
574
731
  } : applyStylesheet(base, stylesheet);
575
732
  const style = [...styled.diagnostics];
576
733
  let tree = styled.tree;
577
- if (styled.headCss !== "") {
578
- const placed = withHeadCss(tree, styled.headCss);
734
+ if (styled.headCss !== "" || styled.fontFaceCss !== "") {
735
+ const placed = withHeadCss(tree, styled.headCss, styled.fontFaceCss);
579
736
  if (placed === void 0) style.push({
580
737
  code: "missing-head",
581
738
  severity: "error",
@@ -604,6 +761,6 @@ const renderEmailWith = (stylesheet, template, data, options) => {
604
761
  };
605
762
  };
606
763
  //#endregion
607
- export { applyStylesheet as a, downlevelMediaQuery as c, renderHeadCss as d, resolveVariables as f, renderEmail as i, finalizeValue as l, bindStylesheets as n, classListOf as o, compileEmail as r, matchesSelector as s, EMAIL_DOCTYPE as t, foldCalc as u };
764
+ export { renderEmailContent as a, matchesSelector as c, finalizeValue as d, foldCalc as f, renderEmail as i, unquote as l, resolveVariables as m, bindStylesheets as n, applyStylesheet as o, renderHeadCss as p, compileEmail as r, classListOf as s, EMAIL_DOCTYPE as t, downlevelMediaQuery as u };
608
765
 
609
- //# sourceMappingURL=render-aaJBnsXK.js.map
766
+ //# sourceMappingURL=render-vAv_4r9v.js.map