@samva/markup 0.15.1 → 0.17.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.
- package/AGENTS.md +42 -35
- package/CHANGELOG.md +92 -0
- package/README.md +10 -9
- package/dist/{assets-tISHaQNF.d.ts → assets-CS5Xa_mr.d.ts} +3 -3
- package/dist/{brand-nreeKWxF.js → brand-CIiEqkU7.js} +24 -31
- package/dist/{brand-plugin-CHhcei-j.d.ts → brand-plugin-Dj9SMpfS.d.ts} +3 -1
- package/dist/brand.d.ts +2 -2
- package/dist/brand.js +3 -3
- package/dist/compatibility.d.ts +76 -0
- package/dist/compatibility.js +807 -0
- package/dist/compiled-template-Bc4Dq9zy.d.ts +268 -0
- package/dist/compiler.d.ts +58 -225
- package/dist/compiler.js +261 -1297
- package/dist/{components-3XHjYD3i.js → components-C5850-Qg.js} +37 -29
- package/dist/css-B_aVKrDo.js +436 -0
- package/dist/css-color-DLM12Osu.js +315 -0
- package/dist/{diagnostic-codes-UXwOO3A-.js → diagnostic-codes-DZKb0Xcg.js} +4 -0
- package/dist/{diagnostics-CUYlQ_6x.d.ts → diagnostics-D4vSLQrO.d.ts} +1 -10
- package/dist/diagnostics.d.ts +6 -2
- package/dist/diagnostics.js +3 -3
- package/dist/edit.d.ts +10 -9
- package/dist/edit.js +12 -11
- package/dist/email/jsx-dev-runtime.d.ts +1 -1
- package/dist/email/jsx-dev-runtime.js +1 -1
- package/dist/email/jsx-runtime.d.ts +2 -2
- package/dist/email/jsx-runtime.js +2 -2
- package/dist/email.d.ts +1 -1
- package/dist/email.js +1 -1
- package/dist/html-DJUK2YJF.js +483 -0
- package/dist/index.d.ts +1 -1
- package/dist/{jsx-runtime-DC7xG0aA.d.ts → jsx-runtime-DL8NVF1f.d.ts} +12 -6
- package/dist/{jsx-runtime-CBlexoeR.js → jsx-runtime-qL19u3Fy.js} +17 -10
- package/dist/preview-rNoJBEIY.d.ts +104 -0
- package/dist/{profile-AatO_SvP.js → profile-JTvgCD6X.js} +614 -24
- package/dist/{html-BvRFepeO.js → render-template-DwPDm3Hq.js} +155 -572
- package/dist/render.d.ts +40 -28
- package/dist/render.js +3 -47
- package/dist/selectors-CYWCoxZr.js +157 -0
- package/dist/sms.d.ts +1 -1
- package/dist/sms.js +1 -1
- package/dist/{template-B-UDXK8a.d.ts → template-DmNcoH4Q.d.ts} +2 -9
- package/dist/{theme-css-s9QwD5Ko.js → theme-css-P9F5S7Ic.js} +3 -316
- package/dist/whatsapp.d.ts +1 -1
- package/dist/whatsapp.js +1 -1
- package/generated/contract.json +7 -2
- package/generated/reference.md +3 -2
- package/package.json +5 -1
- package/skills/sml/SKILL.md +2 -2
- package/dist/ir-DNL6H7jA.js +0 -705
- package/dist/render-BdvewoQt.d.ts +0 -564
- package/dist/render-DpYouYYG.js +0 -258
package/AGENTS.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`@samva/markup` is the authoring language and compiler for Samva templates. A template is one TSX
|
|
4
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
|
|
6
|
-
turns
|
|
5
|
+
a compiler reads without running. The compiler produces a versioned JSON compiled template, and a
|
|
6
|
+
pure renderer turns it and an input into a message, so nothing executes at send time.
|
|
7
7
|
A body that would compute over its input is a diagnostic, not a second mode: compute the value in
|
|
8
8
|
the caller and send it in the payload. The package is plain TypeScript with no application runtime;
|
|
9
9
|
the local editor, project builds, and exports live in `@samva/vite`.
|
|
@@ -32,38 +32,45 @@ Authoring entries are what a template file imports:
|
|
|
32
32
|
`"types": ["@samva/markup/samva-brand"]` to the project's tsconfig for `tsc`. In `theme.css`,
|
|
33
33
|
`@import "samva:brand";` layers the brand theme over the default theme (`DEFAULT_THEME`); `samva:brand/<slug>` names one brand. These are
|
|
34
34
|
Samva's brand plugin (`samvaBrandPlugin` from `@samva/markup/brand`), the default; a host that
|
|
35
|
-
delivers mail itself passes its own `BrandPlugin` as `brandPlugin`
|
|
36
|
-
`
|
|
37
|
-
|
|
35
|
+
delivers mail itself passes its own `BrandPlugin` as the `brandPlugin` option of `compileTemplate`,
|
|
36
|
+
`render` and `compileFooter` to rename the brand import, the footer markers, the unsubscribe
|
|
37
|
+
placeholder and the unsubscribe link's no-track marker.
|
|
38
38
|
|
|
39
39
|
Tooling entries are what a host (Vite, the CLI, the editor, the API, the template agent) imports:
|
|
40
40
|
|
|
41
|
-
- `@samva/markup/compiler` — `compileTemplate`, which reads a project's files and returns
|
|
42
|
-
the fixtures, the assets and the diagnostics
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
- `@samva/markup/compiler` — `compileTemplate`, which reads a project's files and returns a
|
|
42
|
+
`CompileResult`: the compiled `template`, the fixtures, the assets and the diagnostics.
|
|
43
|
+
`checkTemplate(source | files, { entry?, brandPlugin? })` is the synchronous structural check: it
|
|
44
|
+
runs the compiler's lowering without Tailwind, a brand or the files an import names, and returns
|
|
45
|
+
only findings the source itself owns. `introducedErrors(before, after)` is what a visual edit is
|
|
46
|
+
refused on. It also holds the brand compiles: `compileFooter`, the footer a send appends (with
|
|
47
|
+
its unsubscribe row, which the send drops with `withoutUnsubscribeRow`; a brand theme outside the
|
|
48
|
+
grammar comes back as diagnostics), and `previewBrand`, a sample email in an unsaved brand
|
|
49
|
+
compiled and rendered as a template that imports the brand. It imports no
|
|
50
|
+
Vite, esbuild, Node builtins or caniemail, so it runs in Node, Bun, Workers and the browser
|
|
51
|
+
(`tests/browser-entries.test.ts` loads it without `process`).
|
|
52
|
+
- `@samva/markup/compatibility` — `checkCompatibility`, which checks each fixture against
|
|
53
|
+
the client matrix and locates findings at source; `checkHtmlCompatibility` for HTML a host
|
|
54
|
+
already has; `hasBlockingCompatibility` and the matrix. It is the only entry that loads
|
|
55
|
+
caniemail, whose glob dependency reads `process` when it loads.
|
|
50
56
|
- `@samva/markup/diagnostics` — source spans and line maps, the `DIAGNOSTIC_CODES` registry with
|
|
51
|
-
each code's fix and upgrade recipe, and `EmailDiagnostic
|
|
52
|
-
|
|
53
|
-
- `@samva/markup/render` — `
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
what the API bundles.
|
|
57
|
+
each code's fix and upgrade recipe, and `EmailDiagnostic`. It does not load the compiler, so a
|
|
58
|
+
host that only reports imports it.
|
|
59
|
+
- `@samva/markup/render` — `render(template, input, { channel, locale?, timeZone?, brandPlugin? })`,
|
|
60
|
+
which returns the `channel`'s message (`RenderedEmail`, `RenderedSms` with its segment counts, or
|
|
61
|
+
`RenderedWhatsApp`) and throws only `RenderError`, including for a template compiled by another
|
|
62
|
+
`COMPILED_TEMPLATE_VERSION`; the `Compiled*` types; the email element table and `escapeText` /
|
|
63
|
+
`escapeAttribute`; `smsSegmentInfo`. Its import closure excludes the compiler's libraries
|
|
64
|
+
(`tests/entry-closure.test.ts`): it is the send-time security boundary and what the API bundles.
|
|
60
65
|
- `@samva/markup/edit` — the code transforms behind visual edits (`inspectSourceElement`,
|
|
61
66
|
`literalReplacement`, `expressionReplacement`, `classNameEdit`, `structuralReplacements`,
|
|
62
67
|
`applySourceReplacements` with a `ProfileGuard`); form reads and exact edits for the `sms` and
|
|
63
68
|
`whatsapp` channel bodies (`readSmsChannel`, `editSmsChannel`, `readWhatsAppChannel`,
|
|
64
69
|
`editWhatsAppChannel`), where text between expressions is editable and every expression,
|
|
65
|
-
conditional or element is a locked segment;
|
|
66
|
-
|
|
70
|
+
conditional or element is a locked segment; JSX source locations; and `previewEmail(template,
|
|
71
|
+
input)`, the render an editor shows, with an instance path (`INSTANCE_PATH_ATTRIBUTE`) on every
|
|
72
|
+
element and the `selections` that map each one to its TSX span. Browser-safe: its closure holds
|
|
73
|
+
Babel's parser and css-tree only (`tests/entry-closure.test.ts`).
|
|
67
74
|
- `@samva/markup/brand` — brand records and their CSS model (`parseBrandCss`, `printBrandCss`), the
|
|
68
75
|
theme vocabulary (`THEME_COLOR_ROLES`, `THEME_TOKENS`, `DEFAULT_THEME`, `foregroundDefaults`), the
|
|
69
76
|
footer markers a send reads (`renderedFooterMarkers`, `withoutUnsubscribeRow`), and project
|
|
@@ -124,7 +131,7 @@ Read the contract before authoring. Import only subpaths declared in `package.js
|
|
|
124
131
|
generated `sml-dark-*` class, so `bg-card` switches by itself. A `--color-card-dark` token is just
|
|
125
132
|
another color, and `dark:` only picks a different token. A block styled with palette colors
|
|
126
133
|
(`bg-indigo-600`) or raw hex ignores the theme; style with the vocabulary in `THEME_COLOR_ROLES`.
|
|
127
|
-
- **Reading contrast off the light render.** `
|
|
134
|
+
- **Reading contrast off the light render.** `checkHtmlCompatibility` includes `email-contrast`,
|
|
128
135
|
which judges each fixture in light and dark mode (`CONTRAST_THRESHOLDS`); a color a dark block
|
|
129
136
|
leaves unpaired is an error there, not a visual nit.
|
|
130
137
|
- **Declaring a webfont as if every client loads it.** A project font is an ordinary `@font-face`
|
|
@@ -146,12 +153,12 @@ Read the contract before authoring. Import only subpaths declared in `package.js
|
|
|
146
153
|
channel. Helper modules are ordinary imports, not catalog entries; a partial is a function from
|
|
147
154
|
props (and `children`) to JSX that the compiler inlines. A fixture is data: literals, constants
|
|
148
155
|
and imported assets.
|
|
149
|
-
- **The
|
|
150
|
-
escapes per context, applies the serializer's URL and style rules to every
|
|
151
|
-
rejects unsafe input rather than sanitizing it.
|
|
156
|
+
- **The compiled template is the artifact.** Rendering is a pure function of the template and the
|
|
157
|
+
input. The renderer escapes per context, applies the serializer's URL and style rules to every
|
|
158
|
+
bound value, and rejects unsafe input rather than sanitizing it.
|
|
152
159
|
- **Failures are typed.** Compilation reports diagnostic values with a code, a location and a fix:
|
|
153
|
-
`compileTemplate` returns them beside the
|
|
154
|
-
accept at all, such as an invalid name or a nonportable input schema, throws a `TypeError`.
|
|
160
|
+
`compileTemplate` returns them beside the template, and an error finding leaves no template. A
|
|
161
|
+
declaration the compiler cannot accept at all, such as an invalid name or a nonportable input schema, throws a `TypeError`.
|
|
155
162
|
- **Data models are plain types.** Markup's data models are TypeScript types, not schema
|
|
156
163
|
declarations, so there is no schema library to adopt to consume them. Unions discriminate on
|
|
157
164
|
`type`; only error classes carry `_tag`. The input schema is the one
|
|
@@ -161,9 +168,9 @@ Read the contract before authoring. Import only subpaths declared in `package.js
|
|
|
161
168
|
|
|
162
169
|
- `tsc --noEmit` over the template project catches unsupported elements, attributes, and props.
|
|
163
170
|
- `compileTemplate` reports every static-profile, binding and fixture finding with a source
|
|
164
|
-
location, and `
|
|
165
|
-
template.
|
|
171
|
+
location, and `render(compiled.template, fixture, { channel: "email" })` proves one fixture's
|
|
172
|
+
output without executing the template.
|
|
166
173
|
- `samva templates check` (from `@samva/cli`) compiles every entry and renders every fixture,
|
|
167
174
|
reporting findings with source locations.
|
|
168
|
-
- `
|
|
169
|
-
selects by.
|
|
175
|
+
- `previewEmail(compiled.template, fixture)` returns the same document with the source spans an
|
|
176
|
+
editor selects by.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
+
## @samva/markup@0.17.0
|
|
2
|
+
|
|
3
|
+
### One compiler for every host, one render, one error model
|
|
4
|
+
|
|
5
|
+
Breaking. Update imports and calls as follows:
|
|
6
|
+
|
|
7
|
+
| Before | After |
|
|
8
|
+
| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `renderIr(ir, input, options)` | `render(template, input, { channel: "email", ...options })` |
|
|
10
|
+
| `renderIrSms(ir, input)` (a string) | `render(template, input, { channel: "sms" })` (`{ text, segments }`) |
|
|
11
|
+
| `renderIrWhatsApp(ir, input)` | `render(template, input, { channel: "whatsapp" })` |
|
|
12
|
+
| `renderIrPreview` from `./render` | `previewEmail` from `./edit` |
|
|
13
|
+
| `TemplateIr`, `SML_IR_VERSION` | `CompiledTemplate`, `COMPILED_TEMPLATE_VERSION` |
|
|
14
|
+
| `IrRenderError` | `RenderError`, a tagged error |
|
|
15
|
+
| `compileTemplate(...).ir` | `compileTemplate(...).template` |
|
|
16
|
+
| `checkStaticProfile(files, entry, plugin)` | `checkTemplate(files, { entry, brandPlugin })` |
|
|
17
|
+
| `introducedProfileErrors` | `introducedErrors` |
|
|
18
|
+
| `checkTemplateCompatibility`, `checkEmailCompatibility` from `./compiler` | `checkCompatibility`, `checkHtmlCompatibility` from `./compatibility` |
|
|
19
|
+
| `renderBrandFooter(brand, { unsubscribe })` | `compileFooter(brand)`, which returns `{ footer, diagnostics }`; strip the unsubscribe row with `withoutUnsubscribeRow` |
|
|
20
|
+
| `renderBrandSample` from `./brand-sample` | `previewBrand(brand)` from `./compiler` |
|
|
21
|
+
| `parseBrandSpecifier(s, plugin)`, `projectBrandSpecifier(files, path, plugin)` | `parseBrandSpecifier(s, { brandPlugin })`, `projectBrandSpecifier(files, { themePath, brandPlugin })` |
|
|
22
|
+
| `@samva/vite` `TemplateBuild.ir`, `CompiledEntry.ir` | `.compiled` |
|
|
23
|
+
|
|
24
|
+
- `./compiler` now loads in a browser. caniemail, whose glob dependency reads Node's `process`, is
|
|
25
|
+
only in `./compatibility`.
|
|
26
|
+
- `./compiler` exports only `compileTemplate`, `checkTemplate`, `introducedErrors`,
|
|
27
|
+
`compileFooter`, `previewBrand` and the brand specifiers. The cascade, CSS, Tailwind and
|
|
28
|
+
serializer helpers are no longer public, and `./brand-sample` is removed.
|
|
29
|
+
- Compile and check functions return diagnostics and do not throw for bad input; `render` throws
|
|
30
|
+
only `RenderError`, including for a compiled template version it does not know. Stored compiled
|
|
31
|
+
templates keep their `sml` key and stay readable.
|
|
32
|
+
- `readSmsChannel` and `readWhatsAppChannel` return `undefined` instead of `null`.
|
|
33
|
+
|
|
34
|
+
### Design mode and pins
|
|
35
|
+
|
|
36
|
+
The preview bar has a **Design** switch. In Design, a click on the email selects it with a
|
|
37
|
+
toolbar that steps out to its container, moves, duplicates or deletes it, and beside a docked
|
|
38
|
+
assistant the element's properties sit next to the email. Edits from the toolbar, the properties
|
|
39
|
+
and the fixture data are staged instead of saved one field at a time: the preview shows them, a
|
|
40
|
+
dock counts them, and undo, redo, reset (after a confirm) and a locked before-and-after compare
|
|
41
|
+
work over them. **Apply** saves them as one version named "Design: N edits". Leaving Design waits
|
|
42
|
+
for the edits to be applied or reset, and Design waits while an assistant turn is running.
|
|
43
|
+
|
|
44
|
+
The Design tab speaks the author's language. Layers name the blocks and elements the template
|
|
45
|
+
wrote (`Section`, `Button`, `Heading`, `Text`, `Image`) and leave out the compiler's tables and
|
|
46
|
+
cells; a loop's copies are one layer. The inspector names the element and the blocks around it,
|
|
47
|
+
with no internal paths, and edits it with plain controls: text with data bindings shown as chips,
|
|
48
|
+
image description and size, component settings, theme colors as named swatches, type size and
|
|
49
|
+
weight, alignment, and spacing. Each change is staged like any Design edit. The raw props and
|
|
50
|
+
classes are under **Advanced**, and the whole template is in Source.
|
|
51
|
+
|
|
52
|
+
Beside a docked assistant, the **Pin** switch drops numbered pins on parts of the email. A tray
|
|
53
|
+
lists them with a note each, removes one at a time, and clears all after a confirm.
|
|
54
|
+
|
|
55
|
+
For hosts:
|
|
56
|
+
|
|
57
|
+
- `EditorProvider` takes `onPinsChange`, called with every `EditorPin` in tray order whenever the
|
|
58
|
+
pins change. The `rail.assistant` contribution reads `workspace.pins` and gets `removePin` and
|
|
59
|
+
`clearPins`, so a chat can attach pins to its next message and clear them once sent.
|
|
60
|
+
- `workspace.design` reports `{ on, pending, applying, waiting }`. Hold the chat while `waiting`
|
|
61
|
+
is true.
|
|
62
|
+
- `versions.save(revision, options)` receives `options.message`, the name the editor asks for.
|
|
63
|
+
The parameter is optional, so an existing host keeps working and names versions itself.
|
|
64
|
+
|
|
65
|
+
### A warning for hotlinked images
|
|
66
|
+
|
|
67
|
+
`email-external-asset` warns when an email's image points at an absolute URL outside the project's
|
|
68
|
+
asset base. A hotlinked image can disappear, be blocked, track recipients or slow the message; the
|
|
69
|
+
fix is to add the image to the project and import it. It is a warning, so every template that
|
|
70
|
+
compiled before still compiles, and compiled output is unchanged: the email parity digests do not
|
|
71
|
+
move.
|
|
72
|
+
|
|
73
|
+
## @samva/markup@0.16.0
|
|
74
|
+
|
|
75
|
+
### The brand footer is one line
|
|
76
|
+
|
|
77
|
+
`BrandFooter` renders the company name, social links and the unsubscribe link on one line,
|
|
78
|
+
separated by `·`, with the postal address on the line below. It follows a `text-left` or
|
|
79
|
+
`text-right` `className` instead of always centering. Sends without an unsubscribe group drop only
|
|
80
|
+
the unsubscribe link and its separator, and still strip the row of footers compiled before this
|
|
81
|
+
release.
|
|
82
|
+
|
|
83
|
+
This is an intended change to published output: the email parity digests
|
|
84
|
+
(`packages/markup/tests/support/email-parity.ts`) move with the new footer markup, and Samva's hosted
|
|
85
|
+
runtime must take the same values when it repins to this release.
|
|
86
|
+
|
|
87
|
+
### Render a brand sample in the browser
|
|
88
|
+
|
|
89
|
+
`@samva/markup/brand-sample` exports `renderBrandSample` without caniemail, whose glob dependency
|
|
90
|
+
reads Node's `process` when it loads. Import it to preview a brand in a browser; `./compiler`
|
|
91
|
+
still exports `renderBrandSample` for Node, Bun and Workers.
|
|
92
|
+
|
|
1
93
|
## @samva/markup@0.15.0
|
|
2
94
|
|
|
3
95
|
### WhatsApp template FLOW buttons
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ directly for TSX templates or together with `@samva/vite` for the visual editor
|
|
|
7
7
|
|
|
8
8
|
- **SML** is the name of this toolkit, not a second syntax. An email is a TSX file that
|
|
9
9
|
default-exports `defineTemplate`, written in the static profile (ordinary JSX, HTML, and Tailwind
|
|
10
|
-
classes read without running). Compiling it produces the **
|
|
10
|
+
classes read without running). Compiling it produces the **compiled template**, the JSON description that
|
|
11
11
|
previews and sends render.
|
|
12
12
|
- The package ships the `sml` skill at `skills/sml/SKILL.md`; it routes you through write, check,
|
|
13
13
|
render, look, and repair. `AGENTS.md` and `contract.json` ship alongside it.
|
|
@@ -107,13 +107,13 @@ export default defineTemplate({
|
|
|
107
107
|
|
|
108
108
|
A template is read, not run. `@samva/markup/compiler` parses the file and the project files it
|
|
109
109
|
imports without evaluating them, checks every binding against the schema, resolves Tailwind and CSS
|
|
110
|
-
against the markup, and produces the template
|
|
110
|
+
against the markup, and produces the compiled template: JSON, versioned (`"sml": 1`), carrying the
|
|
111
111
|
markup, the bindings, the conditions, the loops and the formatters. `@samva/markup/render`
|
|
112
|
-
renders that
|
|
112
|
+
renders that template against an input. It is a pure function with no dependencies, so nothing executes
|
|
113
113
|
at send time.
|
|
114
114
|
|
|
115
115
|
```ts
|
|
116
|
-
import {
|
|
116
|
+
import { render } from "@samva/markup/render";
|
|
117
117
|
import { compileTemplate } from "@samva/markup/compiler";
|
|
118
118
|
|
|
119
119
|
const compiled = await compileTemplate({
|
|
@@ -122,9 +122,10 @@ const compiled = await compileTemplate({
|
|
|
122
122
|
assetBase: "https://assets.example.com",
|
|
123
123
|
tailwind: { css: files["theme.css"] },
|
|
124
124
|
});
|
|
125
|
-
if (compiled.
|
|
125
|
+
if (compiled.template === undefined) throw new Error(compiled.diagnostics.map(String).join("\n"));
|
|
126
126
|
|
|
127
|
-
const { subject, preheader, html, text } =
|
|
127
|
+
const { subject, preheader, html, text } = render(compiled.template, compiled.fixtures.default, {
|
|
128
|
+
channel: "email",
|
|
128
129
|
locale: "en-US",
|
|
129
130
|
timeZone: "UTC",
|
|
130
131
|
});
|
|
@@ -157,7 +158,7 @@ chains before `.map(...)` or `.length`. `<Columns each per>` lays a list out in
|
|
|
157
158
|
(one row without `per`): the child is a function from an item, and its index within the row, to a
|
|
158
159
|
`Column`. A `Column` that states no `width` takes an equal share of the row, whole percent rounded
|
|
159
160
|
down, and a last row with fewer items keeps that share, so it leaves empty space instead of
|
|
160
|
-
stretching. Both lower to a loop in the
|
|
161
|
+
stretching. Both lower to a loop in the compiled template, so nothing is computed at send time.
|
|
161
162
|
|
|
162
163
|
Rejected, each with its own diagnostic: calls other than `fmt.*` and `.map` (`dynamic-expression`),
|
|
163
164
|
statements in a body (`statement-in-body`), imports other than `@samva/markup` and project files
|
|
@@ -234,7 +235,7 @@ is checked for these vendors, but a converter-supplied JSON Schema constraint re
|
|
|
234
235
|
### Authoring with an agent
|
|
235
236
|
|
|
236
237
|
The package ships what an agent needs without any other source. `AGENTS.md` opens with the model:
|
|
237
|
-
a template is TSX in the static profile that compiles to
|
|
238
|
+
a template is TSX in the static profile that compiles to a compiled template, and nothing runs at send time.
|
|
238
239
|
`@samva/markup/contract.json` lists the components with their props, the profile's forms, the
|
|
239
240
|
formatters, the HTML elements and every diagnostic code with its fix; it is generated from the
|
|
240
241
|
compiler, so it matches the version you installed. `skills/sml/SKILL.md` is the `sml` skill: it
|
|
@@ -243,7 +244,7 @@ for diagnostics an agent can apply.
|
|
|
243
244
|
|
|
244
245
|
### Previewing a render
|
|
245
246
|
|
|
246
|
-
`
|
|
247
|
+
`previewEmail(template, input)` from `@samva/markup/edit` renders a fixture the way `render` does
|
|
247
248
|
and stamps `data-samva-instance` on every element. Its `selections` map each stamped element to the
|
|
248
249
|
line and column of the JSX that produced it, so an editor can select an element on the canvas and
|
|
249
250
|
edit the source at that span. A delivered message never carries the attribute.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as BrandPlugin } from "./brand-plugin-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { t as BrandPlugin } from "./brand-plugin-Dj9SMpfS.js";
|
|
2
|
+
import { n as EmailDiagnostic } from "./diagnostics-D4vSLQrO.js";
|
|
3
|
+
import { i as EmailNode } from "./jsx-runtime-DL8NVF1f.js";
|
|
4
4
|
//#region src/email/brand.d.ts
|
|
5
5
|
interface EmailBrandAddress {
|
|
6
6
|
readonly street1: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { _ as UNSUBSCRIBE_LINK, g as BRAND_FOOTER_MARKER, r as escapeAttribute } from "./html-DJUK2YJF.js";
|
|
2
|
+
import { f as samvaBrandPlugin } from "./render-template-DwPDm3Hq.js";
|
|
3
|
+
import { a as jsx, c as rawMarkup, n as Fragment } from "./jsx-runtime-qL19u3Fy.js";
|
|
3
4
|
//#region src/email/assets.ts
|
|
4
5
|
/** Files a template may import, and what they are served as. */
|
|
5
6
|
const ASSET_CONTENT_TYPES = {
|
|
@@ -86,13 +87,12 @@ const classes = (...names) => {
|
|
|
86
87
|
const joined = names.filter((name) => name !== void 0 && name !== "").join(" ");
|
|
87
88
|
return joined === "" ? void 0 : joined;
|
|
88
89
|
};
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
children
|
|
92
|
-
style: { padding: "0 0 8px 0" },
|
|
93
|
-
children
|
|
94
|
-
})
|
|
90
|
+
const line = (children, last) => jsx("p", {
|
|
91
|
+
style: { margin: last ? "0" : "0 0 8px 0" },
|
|
92
|
+
children
|
|
95
93
|
});
|
|
94
|
+
/** The alignment a footer `className` asks for; the footer centers when it names none. */
|
|
95
|
+
const footerAlignment = (className) => /(?:^|\s)text-(left|center|right)(?:\s|$)/.exec(className ?? "")?.[1] ?? "center";
|
|
96
96
|
/** Bind `BrandLogo` and `BrandFooter` to one brand, marked as the plugin names. */
|
|
97
97
|
const brandComponents = (brand, plugin = samvaBrandPlugin) => {
|
|
98
98
|
const assets = brand.assets ?? {};
|
|
@@ -126,25 +126,24 @@ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
|
|
|
126
126
|
})] });
|
|
127
127
|
};
|
|
128
128
|
const BrandFooter = ({ unsubscribeLabel = "Unsubscribe", className }) => {
|
|
129
|
-
const
|
|
130
|
-
|
|
129
|
+
const items = [];
|
|
130
|
+
const separator = () => items.length === 0 ? [] : [" · "];
|
|
131
|
+
if (footer.companyName !== void 0 && footer.companyName.trim() !== "") items.push(jsx("span", {
|
|
131
132
|
className: "font-semibold",
|
|
132
133
|
children: footer.companyName
|
|
133
|
-
}))
|
|
134
|
-
|
|
135
|
-
const lines = addressLines(footer.address);
|
|
136
|
-
if (lines.length > 0) rows.push(row(lines.flatMap((line, index) => index === 0 ? [line] : [jsx("br", {}), line])));
|
|
137
|
-
}
|
|
138
|
-
const links = footer.socialLinks ?? [];
|
|
139
|
-
if (links.length > 0) rows.push(row(links.flatMap((link, index) => [...index === 0 ? [] : [" · "], jsx("a", {
|
|
134
|
+
}));
|
|
135
|
+
for (const link of footer.socialLinks ?? []) items.push(...separator(), jsx("a", {
|
|
140
136
|
href: link.url,
|
|
141
137
|
className: "text-muted-foreground underline",
|
|
142
138
|
children: link.label
|
|
143
|
-
})
|
|
144
|
-
|
|
139
|
+
}));
|
|
140
|
+
items.push(jsx("span", {
|
|
145
141
|
[plugin.unsubscribeRowAttribute]: "",
|
|
146
|
-
[BRAND_FOOTER_MARKER]: true
|
|
142
|
+
[BRAND_FOOTER_MARKER]: true,
|
|
143
|
+
children: [...items.length === 0 ? [] : [rawMarkup(" · ", "\n")], unsubscribeLink(unsubscribeLabel, plugin)]
|
|
147
144
|
}));
|
|
145
|
+
const address = footer.address === void 0 ? [] : addressLines(footer.address);
|
|
146
|
+
const alignment = footerAlignment(className);
|
|
148
147
|
return jsx("table", {
|
|
149
148
|
role: "presentation",
|
|
150
149
|
width: "100%",
|
|
@@ -154,16 +153,10 @@ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
|
|
|
154
153
|
[plugin.footerAttribute]: "",
|
|
155
154
|
[BRAND_FOOTER_MARKER]: true,
|
|
156
155
|
children: jsx("tbody", { children: jsx("tr", { children: jsx("td", {
|
|
157
|
-
align:
|
|
158
|
-
className: classes("text-xs text-muted-foreground text-center", className),
|
|
156
|
+
align: alignment,
|
|
157
|
+
className: classes("text-xs text-muted-foreground", alignment === "center" ? "text-center" : void 0, className),
|
|
159
158
|
style: { padding: "32px 16px" },
|
|
160
|
-
children:
|
|
161
|
-
role: "presentation",
|
|
162
|
-
border: 0,
|
|
163
|
-
cellpadding: 0,
|
|
164
|
-
cellspacing: 0,
|
|
165
|
-
children: jsx("tbody", { children: rows })
|
|
166
|
-
})
|
|
159
|
+
children: [line(items, address.length === 0), ...address.length === 0 ? [] : [line([address.join(", ")], true)]]
|
|
167
160
|
}) }) })
|
|
168
161
|
});
|
|
169
162
|
};
|
|
@@ -193,8 +186,8 @@ const renderedFooterMarkers = (html, plugin = samvaBrandPlugin) => ({
|
|
|
193
186
|
* no unsubscribe group: there is no link to put there, so the row goes.
|
|
194
187
|
*/
|
|
195
188
|
const withoutUnsubscribeRow = (rendered, plugin = samvaBrandPlugin) => ({
|
|
196
|
-
html: rendered.html?.replace(new RegExp(`<tr ${plugin.unsubscribeRowAttribute}=""[^>]*>[\\s\\S]
|
|
197
|
-
text: rendered.text?.split("\n").filter((
|
|
189
|
+
html: rendered.html?.replace(new RegExp(`<(span|tr) ${plugin.unsubscribeRowAttribute}=""[^>]*>[\\s\\S]*?</\\1>`, "g"), ""),
|
|
190
|
+
text: rendered.text?.split("\n").filter((textLine) => !textLine.includes(plugin.unsubscribeUrlPlaceholder)).join("\n")
|
|
198
191
|
});
|
|
199
192
|
//#endregion
|
|
200
193
|
export { ASSET_CONTENT_TYPES as a, checkAssetBase as c, withoutUnsubscribeRow as i, isAssetPath as l, brandComponents as n, assetEntry as o, renderedFooterMarkers as r, assetUrl as s, BRAND_COMPONENT_CLASSES as t, isExternalAsset as u };
|
|
@@ -24,7 +24,9 @@ interface BrandPlugin {
|
|
|
24
24
|
readonly noTrackAttribute: `data-${string}`;
|
|
25
25
|
}
|
|
26
26
|
/** The brand a specifier names: `undefined` slug means the default brand; `undefined` when it names none. */
|
|
27
|
-
declare const parseBrandSpecifier: (specifier: string,
|
|
27
|
+
declare const parseBrandSpecifier: (specifier: string, options?: {
|
|
28
|
+
readonly brandPlugin?: BrandPlugin | undefined;
|
|
29
|
+
}) => {
|
|
28
30
|
readonly slug: string | undefined;
|
|
29
31
|
} | undefined;
|
|
30
32
|
//#endregion
|
package/dist/brand.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as BrandPlugin } from "./brand-plugin-
|
|
2
|
-
import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-
|
|
1
|
+
import { t as BrandPlugin } from "./brand-plugin-Dj9SMpfS.js";
|
|
2
|
+
import { _ as EmailBrandSocialLink, a as assetUrl, b as renderedFooterMarkers, c as isExternalAsset, d as BrandFooterProps, f as BrandLogoProps, g as EmailBrandFooter, h as EmailBrandAssets, i as assetEntry, l as BRAND_COMPONENT_CLASSES, m as EmailBrandAddress, n as AssetEntry, o as checkAssetBase, p as EmailBrand, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES, u as BrandComponents, v as RenderedFooterMarkers, x as withoutUnsubscribeRow, y as brandComponents } from "./assets-CS5Xa_mr.js";
|
|
3
3
|
import { t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
|
|
4
4
|
import { Result } from "better-result";
|
|
5
5
|
//#region src/email/samva-brand-plugin.d.ts
|
package/dist/brand.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import { a as
|
|
1
|
+
import { f as samvaBrandPlugin } from "./render-template-DwPDm3Hq.js";
|
|
2
|
+
import { a as ASSET_CONTENT_TYPES, c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, r as renderedFooterMarkers, s as assetUrl, t as BRAND_COMPONENT_CLASSES, u as isExternalAsset } from "./brand-CIiEqkU7.js";
|
|
3
|
+
import { a as THEME_COLOR_ROLES, c as foregroundDefaults, i as THEME_COLORS, l as parseBrandCss, n as DEFAULT_THEME_CSS, o as THEME_TOKENS, r as FILL_COLORS, s as contrastForeground, t as DEFAULT_THEME, u as printBrandCss } from "./theme-css-P9F5S7Ic.js";
|
|
4
4
|
export { ASSET_CONTENT_TYPES, BRAND_COMPONENT_CLASSES, DEFAULT_THEME, DEFAULT_THEME_CSS, FILL_COLORS, THEME_COLORS, THEME_COLOR_ROLES, THEME_TOKENS, assetEntry, assetUrl, brandComponents, checkAssetBase, contrastForeground, foregroundDefaults, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { t as BrandPlugin } from "./brand-plugin-Dj9SMpfS.js";
|
|
2
|
+
import { n as EmailDiagnostic } from "./diagnostics-D4vSLQrO.js";
|
|
3
|
+
import { v as CompiledTemplate } from "./compiled-template-Bc4Dq9zy.js";
|
|
4
|
+
import { r as EmittedPosition } from "./preview-rNoJBEIY.js";
|
|
5
|
+
import { caniemail } from "caniemail";
|
|
6
|
+
//#region src/email/compatibility.d.ts
|
|
7
|
+
type CaniemailClient = Parameters<typeof caniemail>[0]["clients"][number];
|
|
8
|
+
/** One client this compiler makes claims about. */
|
|
9
|
+
interface EmailClientTarget {
|
|
10
|
+
readonly id: CaniemailClient;
|
|
11
|
+
readonly label: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The pinned matrix. Every finding is reported per client, and a client absent
|
|
15
|
+
* from this list is a client Samva makes no claim about.
|
|
16
|
+
*/
|
|
17
|
+
declare const EMAIL_CLIENT_MATRIX: readonly EmailClientTarget[];
|
|
18
|
+
interface CompatibilityProvenance {
|
|
19
|
+
/** The exact rules package a finding came from. */
|
|
20
|
+
readonly rules: string;
|
|
21
|
+
readonly apiVersion: string;
|
|
22
|
+
/** The day caniemail last changed the data this build was checked against. */
|
|
23
|
+
readonly lastUpdate: string;
|
|
24
|
+
}
|
|
25
|
+
declare const COMPATIBILITY_PROVENANCE: CompatibilityProvenance;
|
|
26
|
+
/**
|
|
27
|
+
* The constructs whose loss actually breaks a message: the recipient reads
|
|
28
|
+
* something different, in a different order, follows a link that does not work,
|
|
29
|
+
* or sees a layout that has collapsed. A target client dropping one of these
|
|
30
|
+
* blocks publication.
|
|
31
|
+
*
|
|
32
|
+
* The list is deliberately short and enumerated, because the contract is to
|
|
33
|
+
* block *known* failures. Nonessential differences require an assessment of
|
|
34
|
+
* emitted values: a concrete risk warns, expected differences and unverified
|
|
35
|
+
* behavior remain compatibility observations.
|
|
36
|
+
*/
|
|
37
|
+
declare const ESSENTIAL_CONSTRUCTS: ReadonlySet<string>;
|
|
38
|
+
interface CompatibilityInput {
|
|
39
|
+
/** The document the recipient receives. */
|
|
40
|
+
readonly html: string;
|
|
41
|
+
/** The declared fixture checked by this invocation. */
|
|
42
|
+
readonly fixture?: string | undefined;
|
|
43
|
+
/** Retained head rules, when they are not already inside the HTML. */
|
|
44
|
+
readonly css?: string | undefined;
|
|
45
|
+
/** Emitted-offset ranges with the origins of the node that produced them. */
|
|
46
|
+
readonly positions?: readonly EmittedPosition[] | undefined;
|
|
47
|
+
readonly clients?: readonly EmailClientTarget[] | undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check the emitted document against the pinned matrix.
|
|
51
|
+
*
|
|
52
|
+
* Findings are grouped by construct rather than repeated per client, so one
|
|
53
|
+
* unsupported property is one diagnostic naming every client that drops it.
|
|
54
|
+
*/
|
|
55
|
+
declare const checkHtmlCompatibility: (input: CompatibilityInput) => readonly EmailDiagnostic[];
|
|
56
|
+
/**
|
|
57
|
+
* Constructs the pinned data says nothing about for a target client. These are
|
|
58
|
+
* reported so "we did not check that" is never mistaken for "that works".
|
|
59
|
+
*/
|
|
60
|
+
declare const unknownCoverage: (titles: Iterable<string>) => readonly EmailDiagnostic[];
|
|
61
|
+
/** A publication carrying one of these is refused; the message would arrive broken. */
|
|
62
|
+
declare const hasBlockingCompatibility: (findings: readonly EmailDiagnostic[]) => boolean;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/sml/compatibility.d.ts
|
|
65
|
+
/**
|
|
66
|
+
* Check what each declared fixture delivers against the pinned client matrix. The rules run on the
|
|
67
|
+
* rendered document, because what a client drops is what the compiler produced, and every finding
|
|
68
|
+
* is located at the source of the element that produced it. A fixture the renderer refuses throws
|
|
69
|
+
* its `RenderError`: validate fixtures before asking what they deliver.
|
|
70
|
+
*/
|
|
71
|
+
declare const checkCompatibility: (template: CompiledTemplate, fixtures: Readonly<Record<string, unknown>>, options?: {
|
|
72
|
+
readonly clients?: readonly EmailClientTarget[] | undefined;
|
|
73
|
+
readonly brandPlugin?: BrandPlugin | undefined;
|
|
74
|
+
}) => readonly EmailDiagnostic[];
|
|
75
|
+
//#endregion
|
|
76
|
+
export { COMPATIBILITY_PROVENANCE, type CompatibilityInput, type CompatibilityProvenance, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, type EmailClientTarget, checkCompatibility, checkHtmlCompatibility, hasBlockingCompatibility, unknownCoverage };
|