@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
package/AGENTS.md CHANGED
@@ -1,35 +1,100 @@
1
1
  # @samva/markup agent guide
2
2
 
3
- `@samva/markup` is the authoring language and compiler for Samva templates. Email templates are
4
- TSX modules that default-export a `defineEmail` definition and render through a JSX runtime that
5
- lowers to email-safe HTML and plain text. SMS and WhatsApp templates use the structured SML runtime.
6
- The package is plain TypeScript with no application runtime; the local editor, project builds, and
7
- exports live in `@samva/vite`.
3
+ `@samva/markup` is the authoring language and compiler for Samva templates. A template is one TSX
4
+ file in a static profile that default-exports `defineTemplate`: ordinary JSX, HTML and Tailwind that
5
+ a compiler reads without running. The compiler produces a versioned JSON IR, and a pure renderer
6
+ turns the IR and an input into subject, preheader, HTML and text, so nothing executes at send time.
7
+ A body that would compute over its input is a diagnostic, not a second mode: compute the value in
8
+ the caller and send it in the payload. The package is plain TypeScript with no application runtime;
9
+ the local editor, project builds, and exports live in `@samva/vite`.
8
10
 
9
11
  ## Entry points
10
12
 
11
- - `@samva/markup/template` — `defineEmail`, `TemplateDefinition`, `TemplateInput`, and
13
+ - `@samva/markup` — `defineTemplate({ id, schema, fixtures, locale?, email?, sms?, whatsapp? })`,
14
+ `TemplateInput`, and `TEMPLATE_ID_PATTERN` (lowercase kebab-case, the rule the CLI and API slugs
15
+ share). `email` is `{ subject, preheader?, body }`; each is a function of the validated input.
16
+ - `@samva/markup/static` — `compileTemplate`, which reads a project's files and returns the IR,
17
+ the fixtures, the assets and the diagnostics; `checkTemplateCompatibility` checks each fixture
18
+ against the client matrix and locates findings at source. It needs the optional peers Babel,
19
+ Tailwind, css-tree and caniemail.
20
+ `checkStaticProfile(source | files, entry?)` is the synchronous structural check: it runs the
21
+ compiler's lowering without Tailwind, a brand or the files an import names, and returns only
22
+ findings the source itself owns. `introducedProfileErrors(before, after)` is what a visual edit
23
+ is refused on.
24
+ - `@samva/markup/source-edit` — the code transforms behind visual edits (`inspectSourceElement`,
25
+ `literalReplacement`, `expressionReplacement`, `classNameEdit`, `structuralReplacements`,
26
+ `applySourceReplacements` with a `ProfileGuard`). Browser-safe: Babel parser only.
27
+ - `@samva/markup/channel-edit` — form reads and exact source edits for the `sms` and `whatsapp`
28
+ channel bodies (`readSmsChannel`, `editSmsChannel`, `readWhatsAppChannel`,
29
+ `editWhatsAppChannel`). Text between a body's expressions is editable; an expression,
30
+ conditional or element is a locked segment that no edit touches. Each edit is a list of
31
+ `SourceReplacement`s for `applySourceReplacements`. Browser-safe: Babel parser only.
32
+ - `@samva/markup/render-ir` — `renderIr(ir, input, { locale, timeZone })`, plus `renderIrSms` and
33
+ `renderIrWhatsApp`. Dependency-free and the send-time security boundary. `@samva/markup/ir` holds
34
+ the IR types and `SML_IR_VERSION`.
35
+ - `@samva/markup/fmt` — `fmt.money`, `number`, `date`, `time`, `plural` and `list`: the only
36
+ functions a body may call.
37
+ - `@samva/markup/sms` and `@samva/markup/whatsapp` — the channel components (`Sms`; `WhatsApp` with
38
+ `WhatsApp.Header`, `Body`, `Footer`, `Buttons`, `UrlButton`, `QuickReplyButton`, `PhoneButton`,
39
+ `CopyCodeButton`). `whatsappTemplate` derives Meta's positional `{{n}}` text.
40
+ - `@samva/markup/template` — `defineTemplate`, `SmlTemplate`, `TemplateInput`, and
12
41
  `TEMPLATE_NAME_PATTERN`.
42
+ - `@samva/markup/preview` — `renderIrPreview(ir, input)`: the render an editor shows, with an
43
+ instance path (`INSTANCE_PATH_ATTRIBUTE`) stamped on every element and the `selections` that map
44
+ each one to the span of TSX that produced it.
13
45
  - `@samva/markup/email` — the JSX runtime. Set `jsxImportSource: "@samva/markup/email"` for email
14
46
  entries.
15
47
  - `@samva/markup/email/components` — email primitives (`Email`, `Section`, `Columns`, `Button`,
16
48
  and the rest of its exports). They generate table layout and classic-Outlook fallbacks.
17
- - `@samva/markup/email/render` — `renderEmail(template, data)`, which validates input and returns
18
- subject, preheader, HTML, text, and the derived tree.
49
+ - `@samva/markup/email/render` — `compileEmail(tree)` and `renderEmailContent`, which lower a
50
+ derived email tree to HTML and text.
19
51
  - `@samva/markup/email/diagnostics` — `EmailDiagnostic` and `EmailCompileError`.
52
+ - `@samva/markup/email/assets` — `isAssetPath` and `ASSET_CONTENT_TYPES`: which imported files
53
+ become content-addressed assets, without loading the compiler.
20
54
  - `@samva/markup/input-schema` — `inputSchema`, which turns any Standard JSON Schema into the
21
55
  portable JSON Schema and validator a template's input is checked against.
22
- - `@samva/markup/compiler` — `compileEmailProject`, the bundler-agnostic compile step. It imports no
23
- Vite, esbuild, or Node builtins, so it runs in any JavaScript host.
24
- - `jsxImportSource: "@samva/markup"` and `@samva/markup/components` — the structured SMS and
25
- WhatsApp runtime and components. `parseSml` (`@samva/markup/parser`) and `compileSml` /
26
- `compileSource` (`@samva/markup/compile`) work on SML source.
56
+ - `@samva/markup/compiler` — the compiler's building blocks (`transformProject`, Tailwind, the
57
+ cascade, diagnostics). It imports no Vite, esbuild, or Node builtins, so it runs in any
58
+ JavaScript host.
59
+ - `samva:brand` — `BrandLogo` (light logo, swapped for the dark one where the client honors dark
60
+ mode) and `BrandFooter` (company, postal address, social links and the unsubscribe row the send
61
+ path fills). The compiler binds them to the brand the build resolves; add
62
+ `"types": ["@samva/markup/samva-brand"]` to the project's tsconfig for `tsc`. In `theme.css`,
63
+ `@import "samva:brand";` layers the brand theme; `samva:brand/<slug>` names one brand.
27
64
 
28
- The package's `generated/` directory ships the SML and email language references and agent
29
- contracts; read those before authoring. Import only subpaths declared in `package.json` `exports`.
65
+ The package ships what an agent needs to author without this repository:
66
+
67
+ - `@samva/markup/contract.json` (`generated/contract.json`) — components with their props, the
68
+ static-profile forms and the forms it refuses, formatters, HTML elements, channel specs and every
69
+ diagnostic code with its fix. `generated/reference.md` renders the same contract for reading.
70
+ `bun run codegen:contract` writes both from the modules that own each fact (component and `Fmt`
71
+ interfaces, `PROFILE_FORMS`, `DIAGNOSTIC_CODES`, the element table), and a test fails when they
72
+ drift.
73
+ - `skills/sml/SKILL.md` — the `sml` skill: the write, check, render, look, repair loop, the static
74
+ profile and the data rules, for an agent that edits a template project.
75
+
76
+ Read the contract before authoring. Import only subpaths declared in `package.json` `exports`.
30
77
 
31
78
  ## How you hurt yourself here
32
79
 
80
+ - **Computing in a body.** A body is read, not run. A call other than `fmt.*` and `.map`, a
81
+ statement (`const`, `if`, a loop), an import outside `@samva/markup` and the project, a hook, an
82
+ event handler and `dangerouslySetInnerHTML` are each a diagnostic with a code, a location and a
83
+ fix. Compute the value in the caller and send it in the payload. There is no escape hatch.
84
+ - **Chunking or counting in a body.** `items.reduce`, index arithmetic and `filter` helpers are
85
+ calls the profile refuses. Use `<Columns each={list} per={n}>{(item) => <Column … />}</Columns>`
86
+ for a grid and `.filter(condition)` before `.map` or `.length`; the condition is a `&&` / `?:`
87
+ condition over the item.
88
+ - **Building a class name at run time.** Tailwind reads literal classes. `className={`text-${size}`}`
89
+ is `dynamic-class`; choose between literal lists instead: `input.vip ? "bg-amber-100" : "bg-white"`.
90
+ - **Reading an optional field unguarded.** Binding `input.trackingUrl` outside
91
+ `input.trackingUrl && …` is `unguarded-optional`; a misspelled field is `unknown-field` with the
92
+ nearest match; a fixture that stopped matching the schema is `fixture-invalid`.
93
+ - **Reporting a finding without a registry entry.** `Findings.add` and the lowerer accept only
94
+ codes in `DIAGNOSTIC_CODES` (`src/diagnostic-codes.ts`), which holds each code's meaning and fix.
95
+ Add the entry with the code; the contract and the docs read it.
96
+ - **Putting the subject or preheader on `<Email>`.** They belong to the channel (`email.subject`,
97
+ `email.preheader`); `<Email>` has no `preheader` prop.
33
98
  - **Passing a string where you want markup.** Nothing an author passes as a string becomes markup;
34
99
  strings render as escaped text. There is no raw-HTML escape hatch: compatibility markup comes only
35
100
  from the email primitives.
@@ -38,15 +103,15 @@ contracts; read those before authoring. Import only subpaths declared in `packag
38
103
  unsupported element or attribute fails at typecheck and again at compile with a source location.
39
104
  - **Naming a template or fixture like a path.** A template id and a fixture key are both written
40
105
  into file paths, so each must match `TEMPLATE_NAME_PATTERN`, one plain path segment.
41
- `defineEmail` throws a `TypeError` where the name is declared. Code that builds a path from these
106
+ `defineTemplate` throws a `TypeError` where the name is declared. Code that builds a path from these
42
107
  names imports the pattern instead of restating it.
43
- - **Reading the clock or randomness in a template.** Samva's hosted render has no ambient clock and
44
- no entropy source: reading the current instant or a random value there throws unless the
45
- definition declares the fixed value in its `runtime` (`TemplateRuntime`). A local render does not
46
- enforce this, so a template that reads `Date.now()` can pass locally and fail once published. Take
47
- times from validated input, or declare them.
48
- - **Treating `renderEmail` as a sandbox.** It executes the template's TypeScript in the calling
49
- process. Run untrusted template source in an isolated host.
108
+ - **Declaring a webfont as if every client loads it.** A project font is an ordinary `@font-face`
109
+ in the project's CSS (usually `theme.css`) whose `src` is a `.woff2` file in the project, named
110
+ by a path relative to that stylesheet; the compiler serves it as a content-addressed asset and
111
+ hides the rule from classic Outlook. Gmail and Outlook never load it, so a `font-family` stack
112
+ that names a project font must end in a generic (`sans-serif`, `serif`, `monospace`) or the
113
+ render fails with `email-font-fallback`. Only WOFF2 is accepted; an `https:` `src` stays an
114
+ external reference.
50
115
  - **Expecting the schema library to do the validation.** A template's input schema is converted to
51
116
  a portable JSON Schema and validated by that document, not by the library that produced it. A
52
117
  refinement that the converter cannot export is refused, and defaults are never inserted or values
@@ -54,27 +119,29 @@ contracts; read those before authoring. Import only subpaths declared in `packag
54
119
 
55
120
  ## Rules
56
121
 
57
- - **Email entries default-export a `defineEmail` definition** from a `.tsx` file. The definition
58
- owns a stable, project-unique `id`, an input schema, named fixtures, and a synchronous `render`
59
- that receives the validated JSON input unchanged. Helper modules are ordinary imports, not
60
- catalog entries.
61
- - **Email templates have no textual SML form.** The derived semantic tree lowers directly to HTML
62
- and text through `@samva/markup/email/render`; SML is the SMS and WhatsApp language.
63
- - **Structured SML is a closed grammar.** `parseSml` and `compileSml` reject anything outside it
64
- rather than passing it through.
65
- - **Failures are typed.** The SML surface returns a better-result `Result`; branch with
66
- `Result.isOk` / `Result.isErr` and discriminate the `TaggedError` (`SmlParseError`,
67
- `SmlCompileError`) by `_tag`. Email compilation reports `EmailDiagnostic` values: errors throw
68
- `EmailCompileError`, warnings ride back on the render result. A declaration the compiler cannot
122
+ - **Email entries default-export `defineTemplate`** from a `.tsx` file. The definition owns a
123
+ stable, project-unique kebab-case `id`, an input schema, at least one fixture, and one body per
124
+ channel. Helper modules are ordinary imports, not catalog entries; a partial is a function from
125
+ props (and `children`) to JSX that the compiler inlines. A fixture is data: literals, constants
126
+ and imported assets.
127
+ - **The IR is the artifact.** Rendering is a pure function of the IR and the input. The renderer
128
+ escapes per context, applies the serializer's URL and style rules to every bound value, and
129
+ rejects unsafe input rather than sanitizing it.
130
+ - **Failures are typed.** Compilation reports diagnostic values with a code, a location and a fix:
131
+ `compileTemplate` returns them beside the IR, and an error finding leaves no IR. A declaration the compiler cannot
69
132
  accept at all, such as an invalid name or a nonportable input schema, throws a `TypeError`.
70
133
  - **Data models are plain types.** Markup's data models are TypeScript types, not schema
71
134
  declarations, so there is no schema library to adopt to consume them. Unions discriminate on
72
- `type` (an `EmailNode`, an `SmlOp`); only error classes carry `_tag`. The input schema is the one
135
+ `type`; only error classes carry `_tag`. The input schema is the one
73
136
  place a Standard Schema is accepted, and which library produces it is yours to choose.
74
137
 
75
138
  ## Proving your setup
76
139
 
77
140
  - `tsc --noEmit` over the template project catches unsupported elements, attributes, and props.
141
+ - `compileTemplate` reports every static-profile, binding and fixture finding with a source
142
+ location, and `renderIr(compiled.ir, fixture)` proves one fixture's output without executing the
143
+ template.
78
144
  - `samva templates check` (from `@samva/cli`) compiles every entry and renders every fixture,
79
145
  reporting findings with source locations.
80
- - `renderEmail(template, fixture)` in a unit test proves one template's output without a build.
146
+ - `renderIrPreview(compiled.ir, fixture)` returns the same document with the source spans an editor
147
+ selects by.
package/README.md CHANGED
@@ -6,59 +6,195 @@ directly for TSX templates or together with `@samva/vite` for the visual editor
6
6
  ## Install
7
7
 
8
8
  ```sh
9
- npm install --save-dev @samva/markup @samva/vite vite
9
+ npm install --save-dev @samva/markup
10
10
  ```
11
11
 
12
- Configure TypeScript with `jsx: "react-jsx"` and
13
- `jsxImportSource: "@samva/markup/email"`. Install `effect` for this schema example
14
- (or use a Zod 4 schema). Email entries use `.tsx` and default-export a definition:
12
+ Configure TypeScript with `jsx: "react-jsx"` and `jsxImportSource: "@samva/markup/email"`. A
13
+ template is one `.tsx` file that default-exports `defineTemplate`:
15
14
 
16
15
  ```tsx
17
- /** @jsxImportSource @samva/markup/email */
18
- import { Schema } from "effect";
19
- import { Email, Section } from "@samva/markup/email/components";
20
- import { defineEmail } from "@samva/markup/template";
21
-
22
- export default defineEmail({
23
- id: "welcome",
24
- schema: Schema.toStandardJSONSchemaV1(
25
- Schema.Struct({ firstName: Schema.String, items: Schema.Array(Schema.String) }),
26
- ),
27
- fixtures: { basic: { firstName: "Ada", items: ["Your first project"] } },
28
- render: (input) => ({
29
- subject: `Welcome, ${input.firstName}`,
30
- preheader: "Your account is ready",
31
- body: (
16
+ import { defineTemplate } from "@samva/markup";
17
+ import { Button, Email, Section } from "@samva/markup/email/components";
18
+ import { fmt } from "@samva/markup/fmt";
19
+ import { jsonSchema } from "@samva/markup/input-schema";
20
+
21
+ import { OrderRow } from "../partials/order-row";
22
+
23
+ export default defineTemplate({
24
+ id: "order-shipped",
25
+ schema: jsonSchema<{
26
+ name: string;
27
+ orderId: string;
28
+ currency: string;
29
+ total: number;
30
+ items: { title: string; qty: number; price: number }[];
31
+ trackingUrl?: string;
32
+ }>({
33
+ type: "object",
34
+ properties: {
35
+ name: { type: "string" },
36
+ orderId: { type: "string" },
37
+ currency: { type: "string" },
38
+ total: { type: "number" },
39
+ items: {
40
+ type: "array",
41
+ items: {
42
+ type: "object",
43
+ properties: {
44
+ title: { type: "string" },
45
+ qty: { type: "number" },
46
+ price: { type: "number" },
47
+ },
48
+ required: ["title", "qty", "price"],
49
+ additionalProperties: false,
50
+ },
51
+ },
52
+ trackingUrl: { type: "string" },
53
+ },
54
+ required: ["name", "orderId", "currency", "total", "items"],
55
+ additionalProperties: false,
56
+ }),
57
+ fixtures: {
58
+ default: {
59
+ name: "Ada",
60
+ orderId: "A-1042",
61
+ currency: "USD",
62
+ total: 84,
63
+ items: [{ title: "Linen shirt", qty: 2, price: 42 }],
64
+ trackingUrl: "https://track.example.com/A-1042",
65
+ },
66
+ "no-tracking": {
67
+ name: "Ada",
68
+ orderId: "A-1042",
69
+ currency: "USD",
70
+ total: 84,
71
+ items: [{ title: "Linen shirt", qty: 2, price: 42 }],
72
+ },
73
+ },
74
+ email: {
75
+ subject: (input) => `Order ${input.orderId} is on its way`,
76
+ preheader: (input) => `${input.items.length} items, ${fmt.money(input.total, input.currency)}`,
77
+ body: (input) => (
32
78
  <Email>
33
- <Section>
34
- <h1>Welcome, {input.firstName}</h1>
35
- {input.items.length > 0 && (
36
- <ul>
37
- {input.items.map((item) => (
38
- <li key={item}>{item}</li>
39
- ))}
40
- </ul>
79
+ <Section className="px-8 py-6">
80
+ <h1 className="text-2xl font-semibold">Hi {input.name}, it shipped.</h1>
81
+ {input.items.map((item) => (
82
+ <OrderRow item={item} currency={input.currency} />
83
+ ))}
84
+ <p className="mt-4 font-medium">Total {fmt.money(input.total, input.currency)}</p>
85
+ {input.trackingUrl && (
86
+ <Button href={input.trackingUrl} width={200} height={44}>
87
+ Track package
88
+ </Button>
41
89
  )}
42
90
  </Section>
43
91
  </Email>
44
92
  ),
45
- }),
93
+ },
94
+ });
95
+ ```
96
+
97
+ A template is read, not run. `@samva/markup/static` parses the file and the project files it
98
+ imports without evaluating them, checks every binding against the schema, resolves Tailwind and CSS
99
+ against the markup, and produces the template's IR: JSON, versioned (`"sml": 1`), carrying the
100
+ markup, the bindings, the conditions, the loops and the formatters. `@samva/markup/render-ir`
101
+ renders that IR against an input. It is a pure function with no dependencies, so nothing executes
102
+ at send time.
103
+
104
+ ```ts
105
+ import { renderIr } from "@samva/markup/render-ir";
106
+ import { compileTemplate } from "@samva/markup/static";
107
+
108
+ const compiled = await compileTemplate({
109
+ files, // the project's files by path; binary files become content-addressed assets
110
+ entry: "templates/order-shipped.tsx",
111
+ assetBase: "https://assets.example.com",
112
+ tailwind: { css: files["theme.css"] },
113
+ });
114
+ if (compiled.ir === undefined) throw new Error(compiled.diagnostics.map(String).join("\n"));
115
+
116
+ const { subject, preheader, html, text } = renderIr(compiled.ir, compiled.fixtures.default, {
117
+ locale: "en-US",
118
+ timeZone: "UTC",
46
119
  });
47
120
  ```
48
121
 
49
- The definition owns a stable, project-unique `id`, a portable JSON input schema, named fixtures,
50
- and a synchronous render function. Imported helpers are ordinary modules, not catalog entries.
51
- Email executes real TypeScript against validated input and lowers a semantic tree to HTML and text.
52
- SMS and WhatsApp keep their structured channel compiler and `@samva/markup` JSX runtime.
122
+ ### The static profile
123
+
124
+ The profile is the subset of TSX the compiler reads without running it: the forms an agent already
125
+ writes for React. Anything else is a compile error that names the fix.
126
+
127
+ | Form | Example |
128
+ | ------------------- | ------------------------------------------------------------------------ |
129
+ | Text binding | `{input.name}` |
130
+ | Attribute binding | `href={input.trackingUrl}` |
131
+ | Template string | `` `Order ${input.orderId}` `` |
132
+ | Conditional | `{input.trackingUrl && <Button … />}` |
133
+ | Either/or | `{input.vip ? <Gold /> : <Standard />}` |
134
+ | Conditions | `===`, `!==`, `<`, `>`, `<=`, `>=`, `!`, `&&`, `\|\|`, `.length` |
135
+ | Loop | `{input.items.map((item, i) => <Row … />)}` |
136
+ | Filter | `input.steps.filter((step) => step.done).map(…)`, `.filter(…).length` |
137
+ | Grid | `<Columns each={input.deals} per={2}>{(deal) => <Column … />}</Columns>` |
138
+ | Formatter | `fmt.money(input.total, input.currency)` |
139
+ | Partial | `<OrderRow item={item} />`, with props and `children` |
140
+ | Conditional class | `className={input.vip ? "bg-amber-100" : "bg-white"}` |
141
+ | Destructured params | `body: ({ name, items }) => …` |
142
+ | Arithmetic | `item.price * item.qty`, inside a formatter argument or a condition |
143
+
144
+ A `.filter` callback takes the item and one condition from the same grammar as `&&` and `?:`; it
145
+ chains before `.map(...)` or `.length`. `<Columns each per>` lays a list out in rows of `per` cells
146
+ (one row without `per`): the child is a function from an item, and its index within the row, to a
147
+ `Column`. A `Column` that states no `width` takes an equal share of the row, whole percent rounded
148
+ down, and a last row with fewer items keeps that share, so it leaves empty space instead of
149
+ stretching. Both lower to a loop in the IR, so nothing is computed at send time.
150
+
151
+ Rejected, each with its own diagnostic: calls other than `fmt.*` and `.map` (`dynamic-expression`),
152
+ statements in a body (`statement-in-body`), imports other than `@samva/markup` and project files
153
+ (`non-project-import`), event handlers, hooks and `dangerouslySetInnerHTML`, class names built at
154
+ run time (`dynamic-class`), and recursion (`recursive-partial`). There is no escape hatch: logic the
155
+ profile cannot express is computed by the caller and sent in the payload.
156
+
157
+ Each diagnostic has a stable `code`, the exact `file:line:column`, a one-sentence `message` and a
158
+ `fix`. `unknown-field` names the field and suggests the nearest one, `unguarded-optional` marks an
159
+ optional field read outside a guard, and `fixture-invalid` names the fixture that no longer matches
160
+ the schema.
161
+
162
+ Only `@samva/markup` and files inside the project can be imported, so a project has no other
163
+ dependency to install. A fixture is data: a literal, or constants and imported assets assembled from
164
+ literals.
165
+
166
+ ### Formatters
167
+
168
+ `@samva/markup/fmt` is the only set of functions a body may call. Each one is deterministic `Intl`
169
+ with the locale and time zone the render supplies; the template's `locale` is the default and an
170
+ explicit argument overrides it.
171
+
172
+ | Formatter | Example | Output |
173
+ | -------------------------------------- | -------------------------------------------------- | ------------- |
174
+ | `fmt.money(amount, currency, locale?)` | `fmt.money(84, "EUR", "de")` | 84,00 € |
175
+ | `fmt.number(n, options?, locale?)` | `fmt.number(1234.5, { maximumFractionDigits: 0 })` | 1,235 |
176
+ | `fmt.date(iso, style?, locale?)` | `fmt.date(input.shippedAt, "medium")` | Sep 29, 2026 |
177
+ | `fmt.time(iso, style?, locale?)` | `fmt.time(input.slot, "short")` | 3:30 PM |
178
+ | `fmt.plural(n, forms, locale?)` | `fmt.plural(n, { one: "item", other: "items" })` | items |
179
+ | `fmt.list(values, locale?)` | `fmt.list(["Ada", "Grace"])` | Ada and Grace |
180
+
181
+ ### Channels
182
+
183
+ One template carries one schema and a body for each channel it supports. `@samva/markup/sms` and
184
+ `@samva/markup/whatsapp` export the channel components (`Sms`; `WhatsApp` with `WhatsApp.Header`,
185
+ `.Body`, `.Footer`, `.Buttons`, `.UrlButton`, `.QuickReplyButton`, `.PhoneButton` and
186
+ `.CopyCodeButton`). Their names are distinct per channel, so a wrong prop fails in the type checker.
187
+ WhatsApp bindings compile to Meta's positional `{{1}}` placeholders (`whatsappTemplate`), and a
188
+ conditional in a WhatsApp text part is refused because Meta templates cannot branch. The platform
189
+ gates these channels.
53
190
 
54
191
  ### Explicit template inputs
55
192
 
56
- `@samva/markup/template` exports `defineEmail({ id, schema, fixtures, render })`, the email
57
- channel's authoring helper; `TemplateDefinition` and `TemplateInput` are channel-neutral.
193
+ `@samva/markup` exports `defineTemplate` and `TemplateInput`.
58
194
  The schema must implement Standard **JSON Schema** v1; a validator-only Standard Schema is
59
195
  insufficient. `Schema.toStandardJSONSchemaV1` (Effect) and Zod 4 schemas support caller-input
60
- inference, including imported render functions and named fixtures. `TemplateInput<typeof template>`
61
- extracts the caller type. Rendering receives input unchanged.
196
+ inference, including named fixtures. `TemplateInput<typeof template>` extracts the caller type.
197
+ Rendering receives input unchanged.
62
198
 
63
199
  The portable profile uses JSON Schema 2020-12. Supported validation keywords are `type`, `enum`,
64
200
  `const`, `required`, numeric bounds and `multipleOf`, string lengths and `pattern`, object/array
@@ -84,22 +220,22 @@ The pinned Zod/Effect adapter rejects lazy/suspended schema nodes before executi
84
220
  author recursive contracts directly using local JSON Schema references. Custom predicate metadata
85
221
  is checked for these vendors, but a converter-supplied JSON Schema constraint remains its contract.
86
222
 
87
- ### Actual-input email JSX
88
-
89
- Use `jsxImportSource: "@samva/markup/email"` for object template bodies. Ordinary HTML JSX,
90
- imported synchronous components, nested JSON, conditionals and mapped arrays lower directly to a
91
- semantic tree. `renderEmail(template, data)` validates unchanged input and returns subject,
92
- preheader, escaped HTML, explicit or derived text, and the derived tree. Development JSX preserves
93
- source origins and component call sites. This synchronous library call requires an isolated host
94
- before executing untrusted source; it is not itself a sandbox.
95
-
96
- The rendering host has no ambient clock and no entropy source. Reading the current instant or a
97
- random value throws unless the template declares the matching fixed value in `runtime`, which takes
98
- an optional `now` (epoch milliseconds) and `randomSeed` (32-bit unsigned integer). A declaration is
99
- recorded on the publication and applies to every render, including module initializers that capture
100
- a value at import. `new Date(value)` with an explicit argument always works, so a message either
101
- takes its time from validated input or states it once as configuration. Locale (`en-US`) and time
102
- zone (UTC) are publication-wide and back every `Intl` constructor and `toLocale*` call.
223
+ ### Authoring with an agent
224
+
225
+ The package ships what an agent needs without any other source. `AGENTS.md` opens with the model:
226
+ a template is TSX in the static profile that compiles to IR, and nothing runs at send time.
227
+ `@samva/markup/contract.json` lists the components with their props, the profile's forms, the
228
+ formatters, the HTML elements and every diagnostic code with its fix; it is generated from the
229
+ compiler, so it matches the version you installed. `skills/sml/SKILL.md` is the `sml` skill: it
230
+ routes an agent through write, check, render, look and repair. Run `samva templates check --json`
231
+ for diagnostics an agent can apply.
232
+
233
+ ### Previewing a render
234
+
235
+ `renderIrPreview(ir, input)` from `@samva/markup/preview` renders a fixture the way `renderIr` does
236
+ and stamps `data-samva-instance` on every element. Its `selections` map each stamped element to the
237
+ line and column of the JSX that produced it, so an editor can select an element on the canvas and
238
+ edit the source at that span. A delivered message never carries the attribute.
103
239
 
104
240
  Portable validation uses `@cfworker/json-schema` without runtime code generation. The
105
241
  2020-12 meta-schema is checked at every supported schema position, and the explicit profile rejects
@@ -0,0 +1,40 @@
1
+ import { r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
2
+ //#region src/email/assets.d.ts
3
+ /** Files a template may import, and what they are served as. */
4
+ declare const ASSET_CONTENT_TYPES: Readonly<Record<string, string>>;
5
+ interface AssetEntry {
6
+ /** Project-relative path the template imported. */
7
+ readonly path: string;
8
+ /** SHA-256 of the exact bytes, lowercase hex. */
9
+ readonly digest: string;
10
+ readonly bytes: number;
11
+ readonly contentType: string;
12
+ /** `<digest><ext>` — the name the asset has under any base. */
13
+ readonly fileName: string;
14
+ }
15
+ interface AssetManifest {
16
+ /** Where these assets are served from in this build. */
17
+ readonly base: string;
18
+ readonly assets: readonly AssetEntry[];
19
+ }
20
+ /** True when the compiler turns this import into a content-addressed asset. */
21
+ declare const isAssetPath: (path: string) => boolean;
22
+ /** Address one file by its content. */
23
+ declare const assetEntry: (path: string, bytes: Uint8Array) => Promise<AssetEntry>;
24
+ /**
25
+ * The URL an asset has under a base. The base is a build input: publication
26
+ * passes its hosted origin, and a local export passes whatever it will serve
27
+ * the exported files from.
28
+ */
29
+ declare const assetUrl: (entry: AssetEntry, base: string) => string;
30
+ /** Record, but never fetch, a URL the template points at. */
31
+ declare const isExternalAsset: (reference: string) => boolean;
32
+ /**
33
+ * Reject an asset base that would produce URLs a client will not load. The
34
+ * relative forms are the ones a page on disk resolves against its own
35
+ * directory, which is what a local export writes.
36
+ */
37
+ declare const checkAssetBase: (base: string) => readonly EmailDiagnostic[];
38
+ //#endregion
39
+ export { assetUrl as a, isExternalAsset as c, assetEntry as i, AssetEntry as n, checkAssetBase as o, AssetManifest as r, isAssetPath as s, ASSET_CONTENT_TYPES as t };
40
+ //# sourceMappingURL=assets-C5duwTya.d.ts.map
@@ -0,0 +1,88 @@
1
+ import { m as EmailNode } from "./elements-cqldpaJt.js";
2
+ //#region src/internal/unsubscribe-link.d.ts
3
+ /**
4
+ * The per-recipient unsubscribe URL is not known at render: the send path
5
+ * substitutes each delivery's link for this placeholder, or removes the
6
+ * unsubscribe row when the send resolved no unsubscribe group.
7
+ */
8
+ declare const UNSUBSCRIBE_URL_PLACEHOLDER = "%samva:unsubscribe-url%";
9
+ //#endregion
10
+ //#region src/email/brand.d.ts
11
+ interface EmailBrandAddress {
12
+ readonly street1: string;
13
+ readonly street2?: string | undefined;
14
+ readonly city: string;
15
+ readonly region: string;
16
+ readonly postalCode: string;
17
+ readonly country: string;
18
+ }
19
+ interface EmailBrandSocialLink {
20
+ readonly label: string;
21
+ readonly url: string;
22
+ }
23
+ interface EmailBrandAssets {
24
+ readonly logoUrl?: string | undefined;
25
+ readonly logoDarkUrl?: string | undefined;
26
+ readonly iconUrl?: string | undefined;
27
+ }
28
+ interface EmailBrandFooter {
29
+ readonly companyName?: string | undefined;
30
+ readonly address?: EmailBrandAddress | undefined;
31
+ readonly socialLinks?: readonly EmailBrandSocialLink[] | undefined;
32
+ }
33
+ interface EmailBrand {
34
+ /** The brand's slug; `samva:brand/<slug>` must name it. */
35
+ readonly slug: string;
36
+ /** Brand theme CSS in the closed grammar `parseBrandCss` accepts. */
37
+ readonly css: string;
38
+ readonly assets?: EmailBrandAssets | undefined;
39
+ readonly footer?: EmailBrandFooter | undefined;
40
+ }
41
+ /** Marks the block the send path recognizes as the template's own footer. */
42
+ declare const FOOTER_ATTRIBUTE = "data-samva-footer";
43
+ /** Marks the footer row that carries the unsubscribe link. */
44
+ declare const UNSUBSCRIBE_ROW_ATTRIBUTE = "data-samva-unsubscribe";
45
+ /** Every class the brand components use, compiled for any project that imports them. */
46
+ declare const BRAND_COMPONENT_CLASSES: readonly ["dark:hidden!", "hidden", "dark:block!", "text-xs", "text-gray-500", "text-center", "font-semibold", "underline"];
47
+ interface BrandLogoProps {
48
+ /** Rendered width in pixels; the height follows the image. */
49
+ readonly width?: number | undefined;
50
+ /** Defaults to the brand footer's company name. */
51
+ readonly alt?: string | undefined;
52
+ readonly align?: "left" | "center" | "right" | undefined;
53
+ readonly className?: string | undefined;
54
+ }
55
+ interface BrandFooterProps {
56
+ /** Text of the unsubscribe link. */
57
+ readonly unsubscribeLabel?: string | undefined;
58
+ readonly className?: string | undefined;
59
+ }
60
+ interface BrandComponents {
61
+ readonly BrandLogo: (props: BrandLogoProps) => EmailNode | null;
62
+ readonly BrandFooter: (props: BrandFooterProps) => EmailNode | null;
63
+ }
64
+ /** Bind `BrandLogo` and `BrandFooter` to one brand. */
65
+ declare const brandComponents: (brand: EmailBrand) => BrandComponents;
66
+ /** What a rendered message says about the footer it carries. */
67
+ interface RenderedFooterMarkers {
68
+ /** The message renders `BrandFooter` (or claims to, with the marker). */
69
+ readonly footer: boolean;
70
+ /** The message carries the unsubscribe placeholder link only `BrandFooter` can write. */
71
+ readonly unsubscribeLink: boolean;
72
+ }
73
+ /** Read the footer markers from rendered HTML. */
74
+ declare const renderedFooterMarkers: (html: string) => RenderedFooterMarkers;
75
+ /**
76
+ * Drop the unsubscribe row from a rendered message, for a send that resolved
77
+ * no unsubscribe group: there is no link to put there, so the row goes.
78
+ */
79
+ declare const withoutUnsubscribeRow: (rendered: {
80
+ readonly html?: string | undefined;
81
+ readonly text?: string | undefined;
82
+ }) => {
83
+ readonly html: string | undefined;
84
+ readonly text: string | undefined;
85
+ };
86
+ //#endregion
87
+ export { EmailBrand as a, EmailBrandFooter as c, RenderedFooterMarkers as d, UNSUBSCRIBE_ROW_ATTRIBUTE as f, UNSUBSCRIBE_URL_PLACEHOLDER as g, withoutUnsubscribeRow as h, BrandLogoProps as i, EmailBrandSocialLink as l, renderedFooterMarkers as m, BrandComponents as n, EmailBrandAddress as o, brandComponents as p, BrandFooterProps as r, EmailBrandAssets as s, BRAND_COMPONENT_CLASSES as t, FOOTER_ATTRIBUTE as u };
88
+ //# sourceMappingURL=brand-B-0bN_Ir.d.ts.map