@samva/markup 0.7.0 → 0.9.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 (176) hide show
  1. package/AGENTS.md +104 -37
  2. package/README.md +188 -52
  3. package/dist/assets-C5duwTya.d.ts +40 -0
  4. package/dist/brand-B-0bN_Ir.d.ts +88 -0
  5. package/dist/{render-aaJBnsXK.js → cascade-t6kxlgEI.js} +130 -160
  6. package/dist/channel-edit.d.ts +121 -0
  7. package/dist/channel-edit.js +609 -0
  8. package/dist/channel-specs-B6M_HlaV.js +29 -0
  9. package/dist/channel-specs-C2feJwuP.d.ts +13 -0
  10. package/dist/compatibility-DbISoFIq.js +589 -0
  11. package/dist/compiler.d.ts +19 -249
  12. package/dist/compiler.js +10 -1408
  13. package/dist/{css-COcFOT1y.d.ts → css-BVQKICKC.d.ts} +30 -4
  14. package/dist/diagnostic-codes.d.ts +403 -0
  15. package/dist/diagnostic-codes.js +399 -0
  16. package/dist/{diagnostics-BMYtBCQk.d.ts → diagnostics-B-Gym34R.d.ts} +8 -4
  17. package/dist/diagnostics.d.ts +31 -2
  18. package/dist/diagnostics.js +1 -19
  19. package/dist/{elements-BzjjnoCS.d.ts → elements-cqldpaJt.d.ts} +3 -3
  20. package/dist/email/assets.d.ts +2 -0
  21. package/dist/email/assets.js +63 -0
  22. package/dist/email/brand-footer.d.ts +34 -0
  23. package/dist/email/brand-footer.js +63 -0
  24. package/dist/email/brand.d.ts +2 -0
  25. package/dist/email/brand.js +148 -0
  26. package/dist/email/components.d.ts +21 -10
  27. package/dist/email/components.js +46 -11
  28. package/dist/email/diagnostics.d.ts +2 -2
  29. package/dist/email/diagnostics.js +2 -1
  30. package/dist/email/elements.d.ts +1 -1
  31. package/dist/email/html.d.ts +2 -42
  32. package/dist/email/html.js +56 -2
  33. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  34. package/dist/email/jsx-runtime.d.ts +1 -1
  35. package/dist/email/render.d.ts +10 -13
  36. package/dist/email/render.js +163 -6
  37. package/dist/email/text.d.ts +1 -1
  38. package/dist/fmt.d.ts +32 -0
  39. package/dist/fmt.js +174 -0
  40. package/dist/html-BJepOwZ4.d.ts +56 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/ir-aaqNE987.d.ts +252 -0
  44. package/dist/ir.d.ts +2 -0
  45. package/dist/ir.js +7 -0
  46. package/dist/preview-Ekdg1dXG.d.ts +100 -0
  47. package/dist/preview.d.ts +2 -0
  48. package/dist/preview.js +160 -0
  49. package/dist/profile-BH-OtdDA.js +3670 -0
  50. package/dist/render-ir.d.ts +19 -0
  51. package/dist/render-ir.js +826 -0
  52. package/dist/sms-segments.d.ts +13 -2
  53. package/dist/sms.d.ts +16 -0
  54. package/dist/sms.js +12 -0
  55. package/dist/{email/source-edit.d.ts → source-edit.d.ts} +55 -6
  56. package/dist/source-edit.js +343 -0
  57. package/dist/source-locations-C8wCsiAy.d.ts +23 -0
  58. package/dist/source-locations.d.ts +2 -2
  59. package/dist/source-locations.js +3 -89
  60. package/dist/static.d.ts +57 -0
  61. package/dist/static.js +69 -0
  62. package/dist/tailwind-CMYpZf1v.js +614 -0
  63. package/dist/template-_QE_rQND.d.ts +65 -0
  64. package/dist/template.d.ts +2 -2
  65. package/dist/template.js +15 -10
  66. package/dist/theme-css-hFqcPSar.js +587 -0
  67. package/dist/theme-css.d.ts +57 -16
  68. package/dist/theme-css.js +2 -509
  69. package/dist/theme-imports-lvBw3AD1.js +109 -0
  70. package/dist/transform-Ct2OMkFN.d.ts +153 -0
  71. package/dist/unsubscribe-link-CoesnQDU.js +26 -0
  72. package/dist/whatsapp.d.ts +57 -0
  73. package/dist/whatsapp.js +22 -0
  74. package/generated/contract.json +1782 -0
  75. package/generated/reference.md +353 -0
  76. package/package.json +56 -154
  77. package/samva-brand.d.ts +14 -0
  78. package/skills/sml/SKILL.md +123 -0
  79. package/dist/ast-BrDtApwS.d.ts +0 -107
  80. package/dist/ast.d.ts +0 -2
  81. package/dist/ast.js +0 -124
  82. package/dist/candidate.d.ts +0 -29
  83. package/dist/candidate.js +0 -88
  84. package/dist/catalog-60eaYC7b.d.ts +0 -356
  85. package/dist/checks-DBweSIPN.js +0 -58954
  86. package/dist/checks.d.ts +0 -33
  87. package/dist/checks.js +0 -2
  88. package/dist/classes.d.ts +0 -27
  89. package/dist/classes.js +0 -184
  90. package/dist/compile-sms.d.ts +0 -28
  91. package/dist/compile-sms.js +0 -44
  92. package/dist/compile-whatsapp.d.ts +0 -56
  93. package/dist/compile-whatsapp.js +0 -186
  94. package/dist/compile.d.ts +0 -18
  95. package/dist/compile.js +0 -29
  96. package/dist/components.d.ts +0 -289
  97. package/dist/components.js +0 -106
  98. package/dist/conditionals.d.ts +0 -10
  99. package/dist/conditionals.js +0 -74
  100. package/dist/corpus-BjYMqOoY.d.ts +0 -106
  101. package/dist/diagnostics-OcQKQYSY.d.ts +0 -49
  102. package/dist/email/selection.d.ts +0 -91
  103. package/dist/email/selection.js +0 -91
  104. package/dist/email/source-edit.js +0 -185
  105. package/dist/email/stylesheets.d.ts +0 -15
  106. package/dist/email/stylesheets.js +0 -45
  107. package/dist/emit.d.ts +0 -51
  108. package/dist/emit.js +0 -151
  109. package/dist/footer.d.ts +0 -66
  110. package/dist/footer.js +0 -93
  111. package/dist/interpolate.d.ts +0 -24
  112. package/dist/interpolate.js +0 -43
  113. package/dist/jsx-dev-runtime.d.ts +0 -11
  114. package/dist/jsx-dev-runtime.js +0 -15
  115. package/dist/jsx-runtime-I-1nCtl2.d.ts +0 -57
  116. package/dist/jsx-runtime.d.ts +0 -2
  117. package/dist/jsx-runtime.js +0 -121
  118. package/dist/lexer.d.ts +0 -60
  119. package/dist/lexer.js +0 -281
  120. package/dist/model.d.ts +0 -24
  121. package/dist/model.js +0 -77
  122. package/dist/ops-2aiiYO7m.js +0 -630
  123. package/dist/ops.d.ts +0 -114
  124. package/dist/ops.js +0 -2
  125. package/dist/parser-t29cHLVL.d.ts +0 -396
  126. package/dist/parser.d.ts +0 -2
  127. package/dist/parser.js +0 -859
  128. package/dist/predicate-BI6uRRM-.js +0 -549
  129. package/dist/predicate-M-05Nd7R.d.ts +0 -56
  130. package/dist/predicate.d.ts +0 -2
  131. package/dist/predicate.js +0 -2
  132. package/dist/presets/category-mapping.d.ts +0 -13
  133. package/dist/presets/category-mapping.js +0 -49
  134. package/dist/presets/corpus.d.ts +0 -2
  135. package/dist/presets/corpus.js +0 -194
  136. package/dist/presets/public-gallery.d.ts +0 -266
  137. package/dist/presets/public-gallery.js +0 -916
  138. package/dist/presets/public-handoff.d.ts +0 -164
  139. package/dist/presets/public-handoff.js +0 -361
  140. package/dist/presets/public-preview-artifacts.d.ts +0 -43
  141. package/dist/presets/public-preview-artifacts.js +0 -104
  142. package/dist/presets/public-preview.d.ts +0 -61
  143. package/dist/presets/public-preview.js +0 -83
  144. package/dist/presets/showcase.d.ts +0 -2
  145. package/dist/presets/showcase.js +0 -627
  146. package/dist/presets/sources.d.ts +0 -38
  147. package/dist/presets/sources.js +0 -6083
  148. package/dist/presets.d.ts +0 -2
  149. package/dist/presets.js +0 -881
  150. package/dist/printer.d.ts +0 -6
  151. package/dist/printer.js +0 -122
  152. package/dist/registry.d.ts +0 -42
  153. package/dist/registry.js +0 -646
  154. package/dist/render-template.d.ts +0 -45
  155. package/dist/render-template.js +0 -76
  156. package/dist/showcase-DGZAWn4Z.d.ts +0 -456
  157. package/dist/sms-segments-y4FpPilJ.d.ts +0 -13
  158. package/dist/source-locations-DrCM82Ym.d.ts +0 -67
  159. package/dist/template-DWftqh4x.d.ts +0 -60
  160. package/dist/theme-6ApjjiBo.d.ts +0 -116
  161. package/dist/theme.d.ts +0 -2
  162. package/dist/theme.js +0 -118
  163. package/dist/tokens.d.ts +0 -6
  164. package/dist/tokens.js +0 -417
  165. package/dist/tsx-source-instrumentation.d.ts +0 -30
  166. package/dist/tsx-source-instrumentation.js +0 -115
  167. package/dist/tsx-tracer.d.ts +0 -112
  168. package/dist/tsx-tracer.js +0 -985
  169. package/dist/variables-J_L3qxRa.d.ts +0 -77
  170. package/dist/variables.d.ts +0 -2
  171. package/dist/variables.js +0 -141
  172. package/dist/version-BITr58kk.js +0 -96
  173. package/generated/sml-agent-contract.json +0 -709
  174. package/generated/sml-email-agent-contract.json +0 -615
  175. package/generated/sml-email-language-reference.md +0 -94
  176. package/generated/sml-language-reference.md +0 -195
@@ -0,0 +1,63 @@
1
+ import { EmailCompileError } from "./diagnostics.js";
2
+ import { c as parseBrandCss, i as brandTailwindCss } from "../theme-css-hFqcPSar.js";
3
+ import { BRAND_COMPONENT_CLASSES, brandComponents, withoutUnsubscribeRow } from "./brand.js";
4
+ import { t as applyStylesheet } from "../cascade-t6kxlgEI.js";
5
+ import { a as parseStylesheet, t as compileTailwind } from "../tailwind-CMYpZf1v.js";
6
+ import { compileEmail, renderEmailContent } from "./render.js";
7
+ //#region src/email/brand-footer.ts
8
+ /** The brand theme as Tailwind compiles it; a theme outside the grammar is a compile refusal. */
9
+ const brandTheme = (brand) => {
10
+ const parsed = parseBrandCss(brand.css);
11
+ if (parsed.isErr()) throw new EmailCompileError({ diagnostics: parsed.error.map((finding) => ({
12
+ code: finding.code,
13
+ severity: "error",
14
+ message: `samva:brand: ${finding.message}`,
15
+ origins: []
16
+ })) });
17
+ return brandTailwindCss(parsed.value);
18
+ };
19
+ /**
20
+ * Render `BrandFooter` against a brand on its own, for the send path to
21
+ * append to a message whose template renders no footer of its own. It
22
+ * compiles the same component, styled by the same brand theme, a template
23
+ * would get from `samva:brand`. Without `unsubscribe` the unsubscribe row is
24
+ * left out.
25
+ */
26
+ const renderBrandFooter = async (brand, options) => {
27
+ const tailwind = await compileTailwind(BRAND_COMPONENT_CLASSES, { css: brandTheme(brand) });
28
+ const node = brandComponents(brand).BrandFooter({});
29
+ if (node === null) return {
30
+ html: "",
31
+ text: ""
32
+ };
33
+ const styled = applyStylesheet(node, parseStylesheet(tailwind.css, { origin: "samva:brand" }));
34
+ const compiled = compileEmail(styled.tree);
35
+ const rendered = options.unsubscribe ? compiled : withoutUnsubscribeRow({
36
+ html: compiled.html,
37
+ text: compiled.text
38
+ });
39
+ const text = (rendered.text ?? "").trim();
40
+ return {
41
+ html: rendered.html ?? "",
42
+ text: text === "" ? "" : `\n\n---\n${text}`
43
+ };
44
+ };
45
+ /**
46
+ * Render content against a brand as a project whose `theme.css` is
47
+ * `@import "samva:brand";` renders it: Tailwind compiles the brand theme for
48
+ * the classes the content names, `BrandLogo` and `BrandFooter` are bound to the
49
+ * brand, and the render inlines the result. A brand preview renders through
50
+ * this, so it shows what a template importing the brand compiles to.
51
+ */
52
+ const renderBrandSample = async (brand, sample) => {
53
+ const tailwind = await compileTailwind([...BRAND_COMPONENT_CLASSES, ...sample.classes], { css: "@import \"samva:brand\";" }, {
54
+ stylesheets: /* @__PURE__ */ new Map(),
55
+ brandCss: brandTheme(brand)
56
+ });
57
+ const components = brandComponents(brand);
58
+ return renderEmailContent(parseStylesheet(tailwind.css, { origin: "samva:brand" }), () => sample.render(components));
59
+ };
60
+ //#endregion
61
+ export { renderBrandFooter, renderBrandSample };
62
+
63
+ //# sourceMappingURL=brand-footer.js.map
@@ -0,0 +1,2 @@
1
+ import { a as EmailBrand, c as EmailBrandFooter, d as RenderedFooterMarkers, f as UNSUBSCRIBE_ROW_ATTRIBUTE, g as UNSUBSCRIBE_URL_PLACEHOLDER, h as withoutUnsubscribeRow, i as BrandLogoProps, l as EmailBrandSocialLink, m as renderedFooterMarkers, n as BrandComponents, o as EmailBrandAddress, p as brandComponents, r as BrandFooterProps, s as EmailBrandAssets, t as BRAND_COMPONENT_CLASSES, u as FOOTER_ATTRIBUTE } from "../brand-B-0bN_Ir.js";
2
+ export { BRAND_COMPONENT_CLASSES, BrandComponents, BrandFooterProps, BrandLogoProps, EmailBrand, EmailBrandAddress, EmailBrandAssets, EmailBrandFooter, EmailBrandSocialLink, FOOTER_ATTRIBUTE, RenderedFooterMarkers, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, brandComponents, renderedFooterMarkers, withoutUnsubscribeRow };
@@ -0,0 +1,148 @@
1
+ import { i as UNSUBSCRIBE_URL_PLACEHOLDER, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "../unsubscribe-link-CoesnQDU.js";
2
+ import { Fragment, jsx } from "./jsx-runtime.js";
3
+ //#region src/email/brand.ts
4
+ /** Marks the block the send path recognizes as the template's own footer. */
5
+ const FOOTER_ATTRIBUTE = "data-samva-footer";
6
+ /** Marks the footer row that carries the unsubscribe link. */
7
+ const UNSUBSCRIBE_ROW_ATTRIBUTE = "data-samva-unsubscribe";
8
+ /** Every class the brand components use, compiled for any project that imports them. */
9
+ const BRAND_COMPONENT_CLASSES = [
10
+ "dark:hidden!",
11
+ "hidden",
12
+ "dark:block!",
13
+ "text-xs",
14
+ "text-gray-500",
15
+ "text-center",
16
+ "font-semibold",
17
+ "underline"
18
+ ];
19
+ const addressLines = (address) => [
20
+ address.street1,
21
+ address.street2,
22
+ [
23
+ address.city,
24
+ address.region,
25
+ address.postalCode
26
+ ].filter((part) => part.trim() !== "").join(", "),
27
+ address.country
28
+ ].filter((line) => line !== void 0 && line.trim() !== "");
29
+ const classes = (...names) => {
30
+ const joined = names.filter((name) => name !== void 0 && name !== "").join(" ");
31
+ return joined === "" ? void 0 : joined;
32
+ };
33
+ const row = (children, props = {}) => jsx("tr", {
34
+ ...props,
35
+ children: jsx("td", {
36
+ style: { padding: "0 0 8px 0" },
37
+ children
38
+ })
39
+ });
40
+ /** Bind `BrandLogo` and `BrandFooter` to one brand. */
41
+ const brandComponents = (brand) => {
42
+ const assets = brand.assets ?? {};
43
+ const footer = brand.footer ?? {};
44
+ const BrandLogo = ({ width = 120, alt, align, className }) => {
45
+ if (assets.logoUrl === void 0) return null;
46
+ const label = alt ?? footer.companyName ?? "";
47
+ const image = (src, imageClass, imageAlt) => jsx("img", {
48
+ src,
49
+ alt: imageAlt,
50
+ width,
51
+ border: 0,
52
+ ...align === void 0 ? {} : { align },
53
+ className: classes(imageClass, className),
54
+ style: {
55
+ display: "block",
56
+ outline: "none",
57
+ border: "none",
58
+ height: "auto"
59
+ }
60
+ });
61
+ if (assets.logoDarkUrl === void 0) return image(assets.logoUrl, void 0, label);
62
+ return jsx(Fragment, { children: [image(assets.logoUrl, "dark:hidden!", label), jsx("div", {
63
+ className: "hidden dark:block!",
64
+ style: {
65
+ msoHide: "all",
66
+ maxHeight: 0,
67
+ overflow: "hidden"
68
+ },
69
+ children: image(assets.logoDarkUrl, void 0, "")
70
+ })] });
71
+ };
72
+ const BrandFooter = ({ unsubscribeLabel = "Unsubscribe", className }) => {
73
+ const rows = [];
74
+ if (footer.companyName !== void 0 && footer.companyName.trim() !== "") rows.push(row(jsx("span", {
75
+ className: "font-semibold",
76
+ children: footer.companyName
77
+ })));
78
+ if (footer.address !== void 0) {
79
+ const lines = addressLines(footer.address);
80
+ if (lines.length > 0) rows.push(row(lines.flatMap((line, index) => index === 0 ? [line] : [jsx("br", {}), line])));
81
+ }
82
+ const links = footer.socialLinks ?? [];
83
+ if (links.length > 0) rows.push(row(links.flatMap((link, index) => [...index === 0 ? [] : [" · "], jsx("a", {
84
+ href: link.url,
85
+ className: "text-gray-500 underline",
86
+ children: link.label
87
+ })])));
88
+ rows.push(row(unsubscribeLink(unsubscribeLabel), {
89
+ [UNSUBSCRIBE_ROW_ATTRIBUTE]: "",
90
+ [BRAND_FOOTER_MARKER]: true
91
+ }));
92
+ return jsx("table", {
93
+ role: "presentation",
94
+ width: "100%",
95
+ border: 0,
96
+ cellpadding: 0,
97
+ cellspacing: 0,
98
+ [FOOTER_ATTRIBUTE]: "",
99
+ [BRAND_FOOTER_MARKER]: true,
100
+ children: jsx("tbody", { children: jsx("tr", { children: jsx("td", {
101
+ align: "center",
102
+ className: classes("text-xs text-gray-500 text-center", className),
103
+ style: { padding: "32px 16px" },
104
+ children: jsx("table", {
105
+ role: "presentation",
106
+ border: 0,
107
+ cellpadding: 0,
108
+ cellspacing: 0,
109
+ children: jsx("tbody", { children: rows })
110
+ })
111
+ }) }) })
112
+ });
113
+ };
114
+ return {
115
+ BrandLogo,
116
+ BrandFooter
117
+ };
118
+ };
119
+ /**
120
+ * The unsubscribe link. Its href is the placeholder, which no author-written
121
+ * link may carry: the serializer accepts it only on this node, and marks it so
122
+ * click tracking never rewrites it.
123
+ */
124
+ const unsubscribeLink = (label) => jsx("a", {
125
+ href: UNSUBSCRIBE_URL_PLACEHOLDER,
126
+ className: "text-gray-500 underline",
127
+ [UNSUBSCRIBE_LINK]: true,
128
+ children: label
129
+ });
130
+ const UNSUBSCRIBE_ROW = new RegExp(`<tr ${UNSUBSCRIBE_ROW_ATTRIBUTE}=""[^>]*>[\\s\\S]*?</tr>`, "g");
131
+ const UNSUBSCRIBE_HREF = `href="${UNSUBSCRIBE_URL_PLACEHOLDER}"`;
132
+ /** Read the footer markers from rendered HTML. */
133
+ const renderedFooterMarkers = (html) => ({
134
+ footer: html.includes(`${FOOTER_ATTRIBUTE}=""`),
135
+ unsubscribeLink: html.includes(UNSUBSCRIBE_HREF)
136
+ });
137
+ /**
138
+ * Drop the unsubscribe row from a rendered message, for a send that resolved
139
+ * no unsubscribe group: there is no link to put there, so the row goes.
140
+ */
141
+ const withoutUnsubscribeRow = (rendered) => ({
142
+ html: rendered.html?.replace(UNSUBSCRIBE_ROW, ""),
143
+ text: rendered.text?.split("\n").filter((line) => !line.includes(UNSUBSCRIBE_URL_PLACEHOLDER)).join("\n")
144
+ });
145
+ //#endregion
146
+ export { BRAND_COMPONENT_CLASSES, FOOTER_ATTRIBUTE, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, brandComponents, renderedFooterMarkers, withoutUnsubscribeRow };
147
+
148
+ //# sourceMappingURL=brand.js.map
@@ -1,4 +1,4 @@
1
- import { c as EmailStyle, f as EmailChild, m as EmailNode } from "../elements-BzjjnoCS.js";
1
+ import { c as EmailStyle, f as EmailChild, m as EmailNode } from "../elements-cqldpaJt.js";
2
2
  //#region src/email/components.d.ts
3
3
  /** Marks the hidden preview block so the renderer can read it back and the text lane can keep it. */
4
4
  declare const PREHEADER_ATTRIBUTE = "data-samva-preheader";
@@ -9,8 +9,6 @@ interface EmailProps {
9
9
  readonly dir?: "ltr" | "rtl" | undefined;
10
10
  /** Document title; several clients use it as the fallback preview line. */
11
11
  readonly title?: string | undefined;
12
- /** Hidden preview text shown in the inbox list next to the subject. */
13
- readonly preheader?: string | undefined;
14
12
  readonly backgroundColor?: string | undefined;
15
13
  /** Applied to the content cell, where Yahoo and AOL keep body styling. */
16
14
  readonly style?: EmailStyle | undefined;
@@ -19,7 +17,7 @@ interface EmailProps {
19
17
  /** The hidden inbox preview block. Padding characters stop the client borrowing body copy. */
20
18
  declare const preheaderNode: (text: string) => EmailNode;
21
19
  /** Document shell: the head every target client needs, plus the body content cell. */
22
- declare const Email: ({ className, lang, dir, title, preheader, backgroundColor, style, children }: EmailProps) => EmailNode;
20
+ declare const Email: ({ className, lang, dir, title, backgroundColor, style, children }: EmailProps) => EmailNode;
23
21
  interface SectionProps {
24
22
  /** CSS classes applied to the primitive's content element. */
25
23
  readonly className?: string | undefined;
@@ -33,17 +31,30 @@ interface SectionProps {
33
31
  }
34
32
  /** A full-width band of content, as the single-cell table every client agrees on. */
35
33
  declare const Section: ({ className, align, width, bgcolor, style, tableStyle, children }: SectionProps) => EmailNode;
36
- interface ColumnsProps {
34
+ interface ColumnsProps<Item = never> {
37
35
  /** CSS classes applied to the primitive's content element. */
38
36
  readonly className?: string | undefined;
39
37
  readonly align?: "left" | "center" | "right" | undefined;
40
38
  readonly width?: number | string | undefined;
41
39
  readonly style?: EmailStyle | undefined;
42
- /** `Column` elements; anything else is a content-model error. */
43
- readonly children?: EmailChild;
40
+ /**
41
+ * Repeat the cells for each item: `children` is then a function from an item (and its index in
42
+ * the row) to a `Column`. Without `per` all items share one row.
43
+ */
44
+ readonly each?: readonly Item[] | undefined;
45
+ /** With `each`, the cells in a row; each chunk of items is its own `Columns` table. */
46
+ readonly per?: number | undefined;
47
+ /** `Column` elements, or with `each` a function that returns one. */
48
+ readonly children?: EmailChild | ((item: Item, index: number) => EmailChild);
44
49
  }
45
- /** A row of side-by-side `Column`s. No flex or grid conversion happens here. */
46
- declare const Columns: ({ className, align, width, style, children }: ColumnsProps) => EmailNode;
50
+ /** The equal share of a row that each of `count` cells takes, whole percent rounded down. */
51
+ declare const equalColumnWidth: (count: number) => string;
52
+ /**
53
+ * A row of side-by-side `Column`s. No flex or grid conversion happens here. With `each` it repeats
54
+ * its cells over a list, `per` to a row, and splits the row equally between cells that state no
55
+ * width.
56
+ */
57
+ declare const Columns: <Item = never>({ className, align, width, style, each, per, children }: ColumnsProps<Item>) => EmailNode;
47
58
  interface ColumnProps {
48
59
  /** CSS classes applied to the primitive's content element. */
49
60
  readonly className?: string | undefined;
@@ -121,5 +132,5 @@ interface LinkProps {
121
132
  /** A text link that opens outside the client and cannot reach back at the opener. */
122
133
  declare const Link: ({ className, href, target, rel, style, children }: LinkProps) => EmailNode;
123
134
  //#endregion
124
- export { Button, ButtonProps, Column, ColumnProps, Columns, ColumnsProps, Divider, DividerProps, Email, EmailProps, Image, ImageProps, Link, LinkProps, PREHEADER_ATTRIBUTE, Section, SectionProps, Spacer, SpacerProps, preheaderNode };
135
+ export { Button, ButtonProps, Column, ColumnProps, Columns, ColumnsProps, Divider, DividerProps, Email, EmailProps, Image, ImageProps, Link, LinkProps, PREHEADER_ATTRIBUTE, Section, SectionProps, Spacer, SpacerProps, equalColumnWidth, preheaderNode };
125
136
  //# sourceMappingURL=components.d.ts.map
@@ -1,4 +1,4 @@
1
- import { jsx, rawMarkup, reportEmailDiagnostic } from "./jsx-runtime.js";
1
+ import { EmailNode, Fragment, jsx, normalizeChildren, rawMarkup, reportEmailDiagnostic } from "./jsx-runtime.js";
2
2
  import { escapeAttribute, escapeText } from "./html.js";
3
3
  //#region src/email/components.ts
4
4
  /** Marks the hidden preview block so the renderer can read it back and the text lane can keep it. */
@@ -34,7 +34,7 @@ const preheaderNode = (text) => jsx("div", {
34
34
  })]
35
35
  });
36
36
  /** Document shell: the head every target client needs, plus the body content cell. */
37
- const Email = ({ className, lang = "en", dir = "ltr", title, preheader, backgroundColor, style, children }) => jsx("html", {
37
+ const Email = ({ className, lang = "en", dir = "ltr", title, backgroundColor, style, children }) => jsx("html", {
38
38
  lang,
39
39
  dir,
40
40
  xmlns: "http://www.w3.org/1999/xhtml",
@@ -66,7 +66,7 @@ const Email = ({ className, lang = "en", dir = "ltr", title, preheader, backgrou
66
66
  padding: 0,
67
67
  ...backgroundColor === void 0 ? {} : { backgroundColor }
68
68
  },
69
- children: [preheader === void 0 ? null : preheaderNode(preheader), presentation({
69
+ children: [presentation({
70
70
  align: "center",
71
71
  width: "100%",
72
72
  ...backgroundColor === void 0 ? {} : { bgcolor: backgroundColor }
@@ -88,13 +88,48 @@ const Section = ({ className, align = "center", width = "100%", bgcolor, style,
88
88
  ...style === void 0 ? {} : { style },
89
89
  children
90
90
  }));
91
- /** A row of side-by-side `Column`s. No flex or grid conversion happens here. */
92
- const Columns = ({ className, align = "center", width = "100%", style, children }) => presentation({
93
- className,
94
- align,
95
- width,
96
- ...style === void 0 ? {} : { style }
97
- }, children, { style: { width: "100%" } });
91
+ /** The equal share of a row that each of `count` cells takes, whole percent rounded down. */
92
+ const equalColumnWidth = (count) => `${Math.floor(100 / count)}%`;
93
+ /** Give a `Column` that states no width its equal share, keeping the primitive's attribute order. */
94
+ const withColumnWidth = (node, width) => {
95
+ if (node.type === "Fragment") return EmailNode.Fragment({ children: node.children.map((child) => withColumnWidth(child, width)) });
96
+ if (node.type !== "Element" || node.tag !== "td" || node.props.width !== void 0) return node;
97
+ const props = {};
98
+ for (const [key, value] of Object.entries(node.props)) {
99
+ if (key === "style") props.width = width;
100
+ props[key] = value;
101
+ }
102
+ if (props.width === void 0) props.width = width;
103
+ return EmailNode.Element({
104
+ ...node,
105
+ props
106
+ });
107
+ };
108
+ /**
109
+ * A row of side-by-side `Column`s. No flex or grid conversion happens here. With `each` it repeats
110
+ * its cells over a list, `per` to a row, and splits the row equally between cells that state no
111
+ * width.
112
+ */
113
+ const Columns = ({ className, align = "center", width = "100%", style, each, per, children }) => {
114
+ const table = {
115
+ className,
116
+ align,
117
+ width,
118
+ ...style === void 0 ? {} : { style }
119
+ };
120
+ if (each === void 0 || typeof children !== "function") return presentation(table, children, { style: { width: "100%" } });
121
+ if (per !== void 0 && (!Number.isInteger(per) || per < 1)) reportEmailDiagnostic({
122
+ code: "invalid-columns-per",
123
+ severity: "error",
124
+ message: "<Columns per> must be a positive integer."
125
+ });
126
+ const size = per !== void 0 && Number.isInteger(per) && per >= 1 ? per : void 0;
127
+ const chunks = [];
128
+ if (size === void 0) chunks.push(each);
129
+ else for (let at = 0; at < each.length; at += size) chunks.push(each.slice(at, at + size));
130
+ const share = equalColumnWidth(size ?? Math.max(each.length, 1));
131
+ return jsx(Fragment, { children: chunks.map((chunk) => presentation(table, chunk.flatMap((item, index) => normalizeChildren(children(item, index)).map((cell) => withColumnWidth(cell, share))), { style: { width: "100%" } })) });
132
+ };
98
133
  /** One cell of a `Columns` row. */
99
134
  const Column = ({ className, align, valign, width, style, children }) => jsx("td", {
100
135
  className,
@@ -235,6 +270,6 @@ const Link = ({ className, href, target = "_blank", rel = "noopener noreferrer",
235
270
  children
236
271
  });
237
272
  //#endregion
238
- export { Button, Column, Columns, Divider, Email, Image, Link, PREHEADER_ATTRIBUTE, Section, Spacer, preheaderNode };
273
+ export { Button, Column, Columns, Divider, Email, Image, Link, PREHEADER_ATTRIBUTE, Section, Spacer, equalColumnWidth, preheaderNode };
239
274
 
240
275
  //# sourceMappingURL=components.js.map
@@ -1,2 +1,2 @@
1
- import { a as formatEmailDiagnostic, i as EmailDiagnosticSeverity, n as EmailCompileError, o as hasBlockingDiagnostic, r as EmailDiagnostic, t as EmailCompatibilityAssessment } from "../diagnostics-BMYtBCQk.js";
2
- export { EmailCompatibilityAssessment, EmailCompileError, EmailDiagnostic, EmailDiagnosticSeverity, formatEmailDiagnostic, hasBlockingDiagnostic };
1
+ import { a as TemplateDiagnostic, i as EmailDiagnosticSeverity, n as EmailCompileError, o as formatEmailDiagnostic, r as EmailDiagnostic, s as hasBlockingDiagnostic, t as EmailCompatibilityAssessment } from "../diagnostics-B-Gym34R.js";
2
+ export { EmailCompatibilityAssessment, EmailCompileError, EmailDiagnostic, EmailDiagnosticSeverity, TemplateDiagnostic, formatEmailDiagnostic, hasBlockingDiagnostic };
@@ -11,7 +11,8 @@ var EmailCompileError = class extends TaggedError("EmailCompileError") {
11
11
  /** `emails/welcome.tsx:12:3 unsupported-element: …` — the shape agents iterate on. */
12
12
  const formatEmailDiagnostic = (diagnostic) => {
13
13
  const origin = diagnostic.origins[0];
14
- return `${origin === void 0 ? "<unknown source>" : `${origin.fileName}:${origin.lineNumber}:${origin.columnNumber}`} ${diagnostic.code}: ${diagnostic.message}`;
14
+ const head = `${origin === void 0 ? "<unknown source>" : `${origin.fileName}:${origin.lineNumber}:${origin.columnNumber}`} ${diagnostic.code}: ${diagnostic.message}`;
15
+ return diagnostic.fix === void 0 ? head : `${head}\n Fix: ${diagnostic.fix}`;
15
16
  };
16
17
  /** True when a set of findings must stop the compile or refuse a publication. */
17
18
  const hasBlockingDiagnostic = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.severity === "error");
@@ -1,2 +1,2 @@
1
- import { a as EmailGlobalProps, c as EmailStyle, d as isUrlAttribute, i as EmailElementSpec, l as EmailTag, n as EMAIL_GLOBAL_ATTRIBUTES, o as EmailIntrinsicElements, r as EmailAttributeSpec, s as EmailLayout, t as EMAIL_ELEMENTS, u as emailElement } from "../elements-BzjjnoCS.js";
1
+ import { a as EmailGlobalProps, c as EmailStyle, d as isUrlAttribute, i as EmailElementSpec, l as EmailTag, n as EMAIL_GLOBAL_ATTRIBUTES, o as EmailIntrinsicElements, r as EmailAttributeSpec, s as EmailLayout, t as EMAIL_ELEMENTS, u as emailElement } from "../elements-cqldpaJt.js";
2
2
  export { EMAIL_ELEMENTS, EMAIL_GLOBAL_ATTRIBUTES, EmailAttributeSpec, EmailElementSpec, EmailGlobalProps, EmailIntrinsicElements, EmailLayout, EmailStyle, EmailTag, emailElement, isUrlAttribute };
@@ -1,42 +1,2 @@
1
- import { r as EmailDiagnostic } from "../diagnostics-BMYtBCQk.js";
2
- import { m as EmailNode } from "../elements-BzjjnoCS.js";
3
- import { EmittedPosition } from "./selection.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 { EmailHtmlOutput, SerializeEmailOptions, declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml };
42
- //# sourceMappingURL=html.d.ts.map
1
+ import { a as escapeAttribute, c as serializeEmailHtml, i as declarationValue, l as serializeStyle, n as SerializeEmailOptions, o as escapeText, r as checkAttribute, s as kebab, t as EmailHtmlOutput } from "../html-BJepOwZ4.js";
2
+ export { EmailHtmlOutput, SerializeEmailOptions, checkAttribute, declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml, serializeStyle };
@@ -1,6 +1,7 @@
1
1
  import { n as isObject } from "../guards-eV1qLbf2.js";
2
+ import { i as UNSUBSCRIBE_URL_PLACEHOLDER, n as RESERVED_FOOTER_ATTRIBUTES, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "../unsubscribe-link-CoesnQDU.js";
2
3
  import { EMAIL_ELEMENTS, EMAIL_GLOBAL_ATTRIBUTES, emailElement } from "./elements.js";
3
- import { INSTANCE_PATH_ATTRIBUTE } from "./selection.js";
4
+ import { INSTANCE_PATH_ATTRIBUTE } from "../preview.js";
4
5
  //#region src/email/html.ts
5
6
  /** Escape a value for element content. */
6
7
  const escapeText = (value) => value.replace(/[&<>]/g, (character) => ({
@@ -117,9 +118,15 @@ const attributes = (sink, tag, spec, props, origins) => {
117
118
  const emitted = [];
118
119
  const present = /* @__PURE__ */ new Set();
119
120
  let classes;
121
+ const unsubscribeLink = tag === "a" && props[UNSUBSCRIBE_LINK] === true;
120
122
  for (const [key, value] of Object.entries(props)) {
121
123
  if (key === "key") continue;
122
124
  const name = key === "className" ? "class" : key;
125
+ if (unsubscribeLink && name === "href" && value === "%samva:unsubscribe-url%") {
126
+ present.add(name);
127
+ emitted.push(` href="${UNSUBSCRIBE_URL_PLACEHOLDER}" ses:no-track`);
128
+ continue;
129
+ }
123
130
  if (name === "class") {
124
131
  if (value === void 0 || value === null) continue;
125
132
  if (typeof value !== "string") {
@@ -148,6 +155,10 @@ const attributes = (sink, tag, spec, props, origins) => {
148
155
  report(sink, "event-handler", `<${tag} ${name}> is an event handler; email clients strip scripting and Samva refuses it.`, origins);
149
156
  continue;
150
157
  }
158
+ if (RESERVED_FOOTER_ATTRIBUTES.has(name) && props[BRAND_FOOTER_MARKER] !== true) {
159
+ report(sink, "reserved-attribute", `<${tag} ${name}> is written only by BrandFooter from samva:brand; render BrandFooter instead of marking an element as the footer.`, origins);
160
+ continue;
161
+ }
151
162
  if (/^(data|aria)-[a-z0-9-]+$/.test(name)) {
152
163
  const text = attributeValue(sink, tag, name, { type: "string" }, value, origins);
153
164
  if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
@@ -261,7 +272,50 @@ const serializeEmailHtml = (node, options = {}) => {
261
272
  positions: sink.positions
262
273
  };
263
274
  };
275
+ /** An authored style object as a declaration list, checked by the same rules as a serialized tree. */
276
+ const serializeStyle = (style, origins) => {
277
+ const sink = {
278
+ diagnostics: [],
279
+ positions: [],
280
+ written: 0,
281
+ relativeUrls: false,
282
+ instancePaths: false
283
+ };
284
+ return {
285
+ css: declarations(sink, style, origins),
286
+ diagnostics: sink.diagnostics
287
+ };
288
+ };
289
+ /**
290
+ * Check one static attribute value against its element's spec, as serialization does. Returns
291
+ * the text that would be emitted, or `undefined` when the attribute is dropped or invalid.
292
+ */
293
+ const checkAttribute = (tag, name, value, origins) => {
294
+ const sink = {
295
+ diagnostics: [],
296
+ positions: [],
297
+ written: 0,
298
+ relativeUrls: false,
299
+ instancePaths: false
300
+ };
301
+ const element = emailElement(tag);
302
+ const globals = EMAIL_GLOBAL_ATTRIBUTES;
303
+ const attributeSpec = () => {
304
+ if (/^(data|aria)-[a-z0-9-]+$/.test(name)) return { type: "string" };
305
+ if (Object.hasOwn(globals, name)) return globals[name];
306
+ if (element !== void 0 && Object.hasOwn(element.attributes, name)) return element.attributes[name];
307
+ };
308
+ const spec = attributeSpec();
309
+ if (spec === void 0) return {
310
+ text: void 0,
311
+ diagnostics: sink.diagnostics
312
+ };
313
+ return {
314
+ text: attributeValue(sink, tag, name, spec, value, origins),
315
+ diagnostics: sink.diagnostics
316
+ };
317
+ };
264
318
  //#endregion
265
- export { declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml };
319
+ export { checkAttribute, declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml, serializeStyle };
266
320
 
267
321
  //# sourceMappingURL=html.js.map
@@ -1,5 +1,5 @@
1
- import { r as JsxSource } from "../source-locations-DrCM82Ym.js";
2
- import { g as JSX, h as Fragment, m as EmailNode, p as EmailComponent } from "../elements-BzjjnoCS.js";
1
+ import { n as JsxSource } from "../source-locations-C8wCsiAy.js";
2
+ import { g as JSX, h as Fragment, m as EmailNode, p as EmailComponent } from "../elements-cqldpaJt.js";
3
3
  //#region src/email/jsx-dev-runtime.d.ts
4
4
  /** Development JSX retains compiler-provided origins directly on the semantic tree. */
5
5
  declare const jsxDEV: (type: string | EmailComponent<Record<string, unknown>> | typeof Fragment, props: Record<string, unknown> | null, _key?: unknown, _staticChildren?: boolean, source?: JsxSource) => EmailNode;
@@ -1,2 +1,2 @@
1
- import { C as reportEmailDiagnostic, S as rawMarkup, _ as currentEmailOrigins, b as jsxs, f as EmailChild, g as JSX, h as Fragment, m as EmailNode, p as EmailComponent, v as drainEmailDiagnostics, x as normalizeChildren, y as jsx } from "../elements-BzjjnoCS.js";
1
+ import { C as reportEmailDiagnostic, S as rawMarkup, _ as currentEmailOrigins, b as jsxs, f as EmailChild, g as JSX, h as Fragment, m as EmailNode, p as EmailComponent, v as drainEmailDiagnostics, x as normalizeChildren, y as jsx } from "../elements-cqldpaJt.js";
2
2
  export { EmailChild, EmailComponent, EmailNode, Fragment, JSX, currentEmailOrigins, drainEmailDiagnostics, jsx, jsxs, normalizeChildren, rawMarkup, reportEmailDiagnostic };
@@ -1,9 +1,9 @@
1
- import { r as EmailDiagnostic } from "../diagnostics-BMYtBCQk.js";
2
- import { m as EmailNode } from "../elements-BzjjnoCS.js";
3
- import { EmittedPosition } from "./selection.js";
4
- import { SerializeEmailOptions } from "./html.js";
5
- import { r as TemplateDefinition } from "../template-DWftqh4x.js";
6
- import { i as Stylesheet } from "../css-COcFOT1y.js";
1
+ import { r as EmailDiagnostic } from "../diagnostics-B-Gym34R.js";
2
+ import { m as EmailNode } from "../elements-cqldpaJt.js";
3
+ import { n as EmittedPosition } from "../preview-Ekdg1dXG.js";
4
+ import { n as SerializeEmailOptions } from "../html-BJepOwZ4.js";
5
+ import { n as EmailContent } from "../template-_QE_rQND.js";
6
+ import { i as Stylesheet } from "../css-BVQKICKC.js";
7
7
  //#region src/email/render.d.ts
8
8
  /** Classic Outlook lays a document out correctly only under a transitional doctype. */
9
9
  declare const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
@@ -30,14 +30,11 @@ interface RenderedEmail {
30
30
  readonly diagnostics: readonly EmailDiagnostic[];
31
31
  readonly positions: readonly EmittedPosition[];
32
32
  }
33
- /** Render one validated input through the template's only content function. Host isolation is required for untrusted source. */
34
- declare const renderEmail: <Input>(template: TemplateDefinition<Input>, data: unknown, options?: SerializeEmailOptions) => RenderedEmail;
35
33
  /**
36
- * A render bound to the sheets registered so far. A compiled project's wrapper
37
- * calls this once its own stylesheet imports have run, so a later project
38
- * loaded into the same process cannot restyle an entry that is rendered again.
34
+ * Render authored content against a stylesheet: inline what a client honors,
35
+ * keep the rest for `<head>`, and lower to HTML and text.
39
36
  */
40
- declare const bindStylesheets: (stylesheet?: Stylesheet | undefined) => typeof renderEmail;
37
+ declare const renderEmailContent: (stylesheet: Stylesheet | undefined, render: () => EmailContent, options?: SerializeEmailOptions) => RenderedEmail;
41
38
  //#endregion
42
- export { EMAIL_DOCTYPE, EmailCompileOutput, RenderedEmail, bindStylesheets, compileEmail, renderEmail };
39
+ export { EMAIL_DOCTYPE, EmailCompileOutput, RenderedEmail, compileEmail, renderEmailContent };
43
40
  //# sourceMappingURL=render.d.ts.map