@samva/markup 0.18.0 → 0.19.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 (54) hide show
  1. package/AGENTS.md +11 -4
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +89 -5
  4. package/dist/assets-BSpgZOSR.js +61 -0
  5. package/dist/assets-k7_v3IOJ.d.ts +39 -0
  6. package/dist/{assets-CS5Xa_mr.d.ts → brand-Bxb6Sm7y.d.ts} +34 -41
  7. package/dist/brand.d.ts +2 -2
  8. package/dist/brand.js +3 -3
  9. package/dist/compatibility.d.ts +15 -5
  10. package/dist/compatibility.js +48 -5
  11. package/dist/compile-C1GWLQ5C.js +490 -0
  12. package/dist/compile-CIeuGmP-.d.ts +98 -0
  13. package/dist/{compiled-template-Bc4Dq9zy.d.ts → compiled-template-CPDHvFlt.d.ts} +157 -6
  14. package/dist/compiler.d.ts +4 -89
  15. package/dist/compiler.js +8 -486
  16. package/dist/components-DNBCz20_.js +3267 -0
  17. package/dist/{css-color-DLM12Osu.js → dark-scheme-BGDCCaUq.js} +12 -1
  18. package/dist/{diagnostic-codes-DZKb0Xcg.js → diagnostic-codes-BCwfUgnF.js} +33 -36
  19. package/dist/diagnostic-model-DJtqbETd.js +36 -0
  20. package/dist/{diagnostics-D4vSLQrO.d.ts → diagnostics-C3TSx6SU.d.ts} +22 -2
  21. package/dist/diagnostics.d.ts +33 -1
  22. package/dist/diagnostics.js +2 -1
  23. package/dist/edit.d.ts +15 -4
  24. package/dist/edit.js +67 -4
  25. package/dist/email/jsx-dev-runtime.d.ts +2 -2
  26. package/dist/email/jsx-runtime.d.ts +1 -1
  27. package/dist/email.d.ts +6 -2
  28. package/dist/email.js +6 -2
  29. package/dist/index.d.ts +1 -1
  30. package/dist/{jsx-runtime-DL8NVF1f.d.ts → jsx-runtime-HHfrVZEf.d.ts} +1 -2
  31. package/dist/markdown.d.ts +69 -0
  32. package/dist/markdown.js +95 -0
  33. package/dist/parse-Bex7QFqj.js +921 -0
  34. package/dist/{preview-rNoJBEIY.d.ts → preview-DRLWgmfg.d.ts} +3 -3
  35. package/dist/profile-DFk4gs7o.js +56 -0
  36. package/dist/{profile-JTvgCD6X.js → project-B6D64SZS.js} +497 -234
  37. package/dist/render.d.ts +8 -8
  38. package/dist/render.js +1 -2
  39. package/dist/{css-B_aVKrDo.js → selectors-CKGuc9MO.js} +158 -4
  40. package/dist/sms.d.ts +1 -1
  41. package/dist/{template-DmNcoH4Q.d.ts → template-4Dse7_OC.d.ts} +1 -1
  42. package/dist/{theme-css-P9F5S7Ic.js → theme-css-BujDuuEA.js} +1 -2
  43. package/dist/whatsapp.d.ts +1 -1
  44. package/generated/contract.json +41 -1
  45. package/generated/reference.md +97 -89
  46. package/package.json +15 -4
  47. package/dist/brand-CIiEqkU7.js +0 -193
  48. package/dist/brand-plugin-Dj9SMpfS.d.ts +0 -33
  49. package/dist/components-C5850-Qg.js +0 -409
  50. package/dist/dark-scheme-CbCwGDWH.js +0 -12
  51. package/dist/html-DJUK2YJF.js +0 -483
  52. package/dist/render-template-DwPDm3Hq.js +0 -1226
  53. package/dist/selectors-CYWCoxZr.js +0 -157
  54. package/dist/source-locations-C8wCsiAy.d.ts +0 -22
package/AGENTS.md CHANGED
@@ -49,6 +49,13 @@ Tooling entries are what a host (Vite, the CLI, the editor, the API, the templat
49
49
  compiled and rendered as a template that imports the brand. It imports no
50
50
  Vite, esbuild, Node builtins or caniemail, so it runs in Node, Bun, Workers and the browser
51
51
  (`tests/browser-entries.test.ts` loads it without `process`).
52
+ - `@samva/markup/markdown` — `parseIssue(source, file)` reads typed YAML frontmatter and static
53
+ Markdown/SML component tags into semantic nodes with exact source ranges; `compileIssue` inserts
54
+ them into an email-only TSX layout (default `layout.tsx`) at one `IssueContent` directive imported
55
+ from `@samva/markup/email`, before CSS is applied. The issue slug/title/preheader set its compiled
56
+ identity/subject/preview; the layout supplies schema and fixtures. `inspectMarkdownNode` in
57
+ `./edit` resolves its origins; `applySourceReplacements` takes `{ markdown: { file } }` to validate
58
+ Markdown edits. Expressions, raw HTML, unknown tags and invalid metadata are diagnostics.
52
59
  - `@samva/markup/compatibility` — `checkCompatibility`, which checks each fixture against
53
60
  the client matrix and locates findings at source; `checkHtmlCompatibility` for HTML a host
54
61
  already has; `hasBlockingCompatibility` and the matrix. It is the only entry that loads
@@ -56,9 +63,9 @@ Tooling entries are what a host (Vite, the CLI, the editor, the API, the templat
56
63
  - `@samva/markup/diagnostics` — source spans and line maps, the `DIAGNOSTIC_CODES` registry with
57
64
  each code's fix and upgrade recipe, and `EmailDiagnostic`. It does not load the compiler, so a
58
65
  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
66
+ - `@samva/markup/render` — `render(template, input, { target, locale?, timeZone?, brandPlugin? })`,
67
+ which returns the target's output (`RenderedEmail`, `RenderedPage` with body HTML and head hints,
68
+ `RenderedSms` with its segment counts, or `RenderedWhatsApp`) and throws only `RenderError`, including for a template compiled by another
62
69
  `COMPILED_TEMPLATE_VERSION`; the `Compiled*` types; the email element table and `escapeText` /
63
70
  `escapeAttribute`; `smsSegmentInfo`. Its import closure excludes the compiler's libraries
64
71
  (`tests/entry-closure.test.ts`): it is the send-time security boundary and what the API bundles.
@@ -168,7 +175,7 @@ Read the contract before authoring. Import only subpaths declared in `package.js
168
175
 
169
176
  - `tsc --noEmit` over the template project catches unsupported elements, attributes, and props.
170
177
  - `compileTemplate` reports every static-profile, binding and fixture finding with a source
171
- location, and `render(compiled.template, fixture, { channel: "email" })` proves one fixture's
178
+ location, and `render(compiled.template, fixture, { target: "email" })` proves one fixture's
172
179
  output without executing the template.
173
180
  - `samva templates check` (from `@samva/cli`) compiles every entry and renders every fixture,
174
181
  reporting findings with source locations.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,39 @@
1
+ ## @samva/markup@0.19.0
2
+
3
+ ### Author newsletter issues in SML Markdown
4
+
5
+ Parse `issues/*.md` with typed YAML frontmatter and static SML component tags through
6
+ `@samva/markup/markdown`. Unknown components, raw HTML, expressions, and invalid metadata return
7
+ located diagnostics. Prose and component attributes retain exact editable source spans. Prose
8
+ edits preserve ordinary punctuation and protect only syntax-sensitive delimiters and whitespace.
9
+
10
+ Compile each issue into an email-only TSX layout containing one `<IssueContent />` from
11
+ `@samva/markup/email`, then render the shared document to email or web. Vite discovers issues
12
+ separately from template entries and includes them in project build, render, and export workflows.
13
+ Existing template email output and golden digests are unchanged.
14
+
15
+ ### Render email templates as web pages
16
+
17
+ **Breaking:** replace `render(template, input, { channel })` with
18
+ `render(template, input, { target })`. Use `target: "email"`, `"sms"`, or `"whatsapp"` for message
19
+ delivery, and `target: "web"` for the semantic HTML body and head hints of an email template.
20
+ Recompile stored templates to compiled document version `sml: 2` before rendering them with this
21
+ version. Compilation digests change with this document format.
22
+
23
+ Email and web share the template's components, bindings, conditions, loops, and source positions.
24
+ Email preserves the existing rendered HTML, subject, preheader, and text byte for byte, including
25
+ Outlook fallbacks. The web target emits semantic HTML with styles supplied separately to the page
26
+ shell. `templateEmailDegradations` locates component fidelity declarations in source, and
27
+ `checkCompatibility` reports content losses alongside client HTML findings.
28
+
29
+ ### An important declaration keeps `!important` when it is inlined
30
+
31
+ A stylesheet `!important` that wins the cascade is written onto the element's `style` with `!important`. The flag used to decide the winner and then disappear, so a later important rule could override the inlined value. An author's own important inline style is kept the same way.
32
+
33
+ A logical padding shorthand keeps that flag on every physical side. `padding-inline: 3px 4px !important` becomes `padding-left: 3px !important` and `padding-right: 4px !important`.
34
+
35
+ A property the generated dark-mode class restates is inlined without `!important`, including a declaration that only overlaps the color that changed. That class also uses `!important`, and an important inline style would keep the light color. A weaker rule kept in the head, such as a hover, is copied onto a more specific selector so it does not take that color; the copy inside the dark-mode query keeps the theme switch. A stronger rule is copied again after that guard. A shorthand in that copy also restates any locked longhand that still outranks it, so the shorthand does not reset a stronger inline color. The dark-mode copy resolves that shorthand and the longhand at their dark values. A pseudo-element keeps the color it sets for itself.
36
+
1
37
  ## @samva/markup@0.17.0
2
38
 
3
39
  ### One compiler for every host, one render, one error model
package/README.md CHANGED
@@ -107,10 +107,10 @@ 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 compiled template: JSON, versioned (`"sml": 1`), carrying the
111
- markup, the bindings, the conditions, the loops and the formatters. `@samva/markup/render`
112
- renders that template against an input. It is a pure function with no dependencies, so nothing executes
113
- at send time.
110
+ against the markup, and produces a semantic document: JSON, versioned (`"sml": 2`), carrying
111
+ components, block and inline content, bindings, conditions, loops, formatters, and source positions.
112
+ `@samva/markup/render` lowers that document to the requested target against an input. Rendering is
113
+ pure, with no authored code or external I/O at send time.
114
114
 
115
115
  ```ts
116
116
  import { render } from "@samva/markup/render";
@@ -125,12 +125,28 @@ const compiled = await compileTemplate({
125
125
  if (compiled.template === undefined) throw new Error(compiled.diagnostics.map(String).join("\n"));
126
126
 
127
127
  const { subject, preheader, html, text } = render(compiled.template, compiled.fixtures.default, {
128
- channel: "email",
128
+ target: "email",
129
129
  locale: "en-US",
130
130
  timeZone: "UTC",
131
131
  });
132
132
  ```
133
133
 
134
+ Every email template also has a web target. It returns the page body and hints for the host's page
135
+ shell; the host owns the document head, canonical URL, indexing, and access control.
136
+
137
+ ```ts
138
+ const page = render(compiled.template, compiled.fixtures.default, { target: "web" });
139
+ // page.html is semantic HTML; page.headHints carries the styles and document hints.
140
+ ```
141
+
142
+ Email lowering preserves email layout, Outlook fallbacks, subject, preheader, and plain text.
143
+ Web lowering uses semantic elements and supplies styles separately from the HTML body. Both read
144
+ the same authored components and source positions. `COMPONENT_DEGRADATIONS` from
145
+ `@samva/markup/compatibility` declares what each component loses in email.
146
+ `templateEmailDegradations(compiled.template)` locates those declarations in the authored source;
147
+ `checkCompatibility` reports content degradations alongside client HTML findings. A `same`
148
+ declaration records that content is preserved and adds no finding.
149
+
134
150
  ### The static profile
135
151
 
136
152
  The profile is the subset of TSX the compiler reads without running it: the forms an agent already
@@ -253,3 +269,71 @@ Portable validation uses `@cfworker/json-schema` without runtime code generation
253
269
  2020-12 meta-schema is checked at every supported schema position, and the explicit profile rejects
254
270
  keywords outside its contract before interpretation. The AJV dependency supplies static meta-schema
255
271
  JSON; its code generator is not invoked in a loaded template.
272
+
273
+ ## Markdown newsletter issues
274
+
275
+ `@samva/markup/markdown` parses newsletter prose into the same semantic document as TSX. Files
276
+ carry YAML frontmatter and CommonMark prose, with static tags from SML's email component set:
277
+
278
+ ```md
279
+ ---
280
+ title: October product news
281
+ slug: october-product-news
282
+ topic: product
283
+ preheader: What shipped this month
284
+ seo:
285
+ description: October updates from the team
286
+ image: https://example.com/october.png
287
+ gate:
288
+ leadIn: 2
289
+ ---
290
+
291
+ # What shipped
292
+
293
+ Read the **latest updates**.
294
+
295
+ <Button href="https://example.com/updates">See every update</Button>
296
+ ```
297
+
298
+ `title`, `slug`, and `topic` are required nonempty strings. The slug is lowercase kebab-case.
299
+ `preheader` is optional text; `seo` accepts optional `title`, `description`, and an absolute HTTPS
300
+ `image`; `gate.leadIn` counts leading top-level content blocks before a host-owned subscription
301
+ wall. SML preserves this metadata; the host owns SEO tags, gating, and publication. Unknown fields,
302
+ invalid YAML, raw HTML, unknown components, expressions, spreads, and nonliteral props are errors.
303
+ Use explicit `[label](url)` links; angle autolinks are outside this grammar.
304
+ Numeric and boolean props use quoted values (`height="24"`, `stack="false"`); bare boolean flags
305
+ mean true. Fenced code is literal content, including any HTML or braces inside it.
306
+
307
+ A layout is an ordinary email-only `defineTemplate` entry containing one static `<IssueContent />`
308
+ imported from `@samva/markup/email`. The directive takes no props or children and must sit outside
309
+ conditions and loops. It disappears during compilation before styles are applied.
310
+
311
+ ```ts
312
+ import { compileIssue, parseIssue } from "@samva/markup/markdown";
313
+ import { render } from "@samva/markup/render";
314
+
315
+ const parsed = parseIssue(files["issues/october.md"], "issues/october.md");
316
+ const compiled = await compileIssue({
317
+ files,
318
+ entry: "issues/october.md",
319
+ layout: "layout.tsx",
320
+ assetBase: "https://assets.example.com",
321
+ });
322
+ if (compiled.template) {
323
+ const page = render(compiled.template, compiled.fixtures.default, { target: "web" });
324
+ }
325
+ ```
326
+
327
+ `compileIssue` takes its schema, fixtures, theme and body shell from the layout; the issue slug
328
+ becomes the compiled identity, its title becomes the subject, and its preheader becomes the inbox
329
+ preview. Frontmatter is returned separately and is not injected into layout inputs. Layout and issue
330
+ nodes retain their own source files. `parseIssue` returns no usable nodes when it reports an error.
331
+
332
+ `inspectMarkdownNode` from `@samva/markup/edit` resolves a rendered origin to editable prose and
333
+ literal attributes. Pass `{ tag: "a" }` as its fourth argument to distinguish a link from a
334
+ paragraph sharing its source origin; without a tag it prefers the editable element. Feed its
335
+ property to `literalReplacement`, then call `applySourceReplacements`
336
+ with `{ markdown: { file: "issues/october.md" } }` to validate the rewritten issue. Offsets are
337
+ UTF-16, end-exclusive, and stale spans are refused. Prose edits retain ordinary punctuation;
338
+ syntax delimiters are escaped only where they could change the authored structure, and literal
339
+ line breaks or edge whitespace are encoded as character references.
@@ -0,0 +1,61 @@
1
+ //#region src/email/assets.ts
2
+ /** Files a template may import, and what they are served as. */
3
+ const ASSET_CONTENT_TYPES = {
4
+ ".png": "image/png",
5
+ ".jpg": "image/jpeg",
6
+ ".jpeg": "image/jpeg",
7
+ ".gif": "image/gif",
8
+ ".webp": "image/webp",
9
+ ".avif": "image/avif",
10
+ ".svg": "image/svg+xml",
11
+ ".ico": "image/x-icon",
12
+ ".woff": "font/woff",
13
+ ".woff2": "font/woff2",
14
+ ".ttf": "font/ttf",
15
+ ".otf": "font/otf",
16
+ ".pdf": "application/pdf",
17
+ ".txt": "text/plain"
18
+ };
19
+ const extensionOf = (path) => {
20
+ const dot = path.lastIndexOf(".");
21
+ return dot < 0 ? "" : path.slice(dot).toLowerCase();
22
+ };
23
+ /** True when the compiler turns this import into a content-addressed asset. */
24
+ const isAssetPath = (path) => Object.hasOwn(ASSET_CONTENT_TYPES, extensionOf(path));
25
+ const hex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
26
+ /** Address one file by its content. */
27
+ const assetEntry = async (path, bytes) => {
28
+ const extension = extensionOf(path);
29
+ const view = new Uint8Array(bytes.byteLength);
30
+ view.set(bytes);
31
+ const digest = hex(await crypto.subtle.digest("SHA-256", view));
32
+ return {
33
+ path,
34
+ digest,
35
+ bytes: bytes.byteLength,
36
+ contentType: ASSET_CONTENT_TYPES[extension] ?? "application/octet-stream",
37
+ fileName: `${digest}${extension}`
38
+ };
39
+ };
40
+ /**
41
+ * The URL an asset has under a base. The base is a build input: publication
42
+ * passes its hosted origin, and a local export passes whatever it will serve
43
+ * the exported files from.
44
+ */
45
+ const assetUrl = (entry, base) => `${base.replace(/\/+$/, "")}/${entry.fileName}`;
46
+ const EXTERNAL_IMAGE = /^https:\/\//i;
47
+ /** Record, but never fetch, a URL the template points at. */
48
+ const isExternalAsset = (reference) => EXTERNAL_IMAGE.test(reference);
49
+ /**
50
+ * Reject an asset base that would produce URLs a client will not load. The
51
+ * relative forms are the ones a page on disk resolves against its own
52
+ * directory, which is what a local export writes.
53
+ */
54
+ const checkAssetBase = (base) => /^(https:\/\/|\.{1,2}\/|\/)/i.test(base) ? [] : [{
55
+ code: "invalid-asset-base",
56
+ severity: "error",
57
+ message: `Asset base ${JSON.stringify(base)} must be an https URL, an absolute path, or a relative path for a local export.`,
58
+ origins: []
59
+ }];
60
+ //#endregion
61
+ export { isAssetPath as a, checkAssetBase as i, assetEntry as n, isExternalAsset as o, assetUrl as r, ASSET_CONTENT_TYPES as t };
@@ -0,0 +1,39 @@
1
+ import { n as EmailDiagnostic } from "./diagnostics-C3TSx6SU.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 };
@@ -1,6 +1,36 @@
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";
1
+ import { i as EmailNode } from "./jsx-runtime-HHfrVZEf.js";
2
+ //#region src/email/brand-plugin.d.ts
3
+ /**
4
+ * How a template reaches its brand and how the footer `BrandFooter` renders is
5
+ * marked for the send path that delivers it. The compiler and the IR renderer
6
+ * read every brand import, footer marker and unsubscribe link from the plugin
7
+ * a build passes; `samvaBrandPlugin` is the default.
8
+ */
9
+ interface BrandPlugin {
10
+ /**
11
+ * The module and stylesheet specifier a template imports its brand from.
12
+ * `<specifier>/<slug>` names one brand; the bare specifier names the default.
13
+ */
14
+ readonly specifier: string;
15
+ /** Marks the block `BrandFooter` renders, so the send path finds a template's own footer. */
16
+ readonly footerAttribute: `data-${string}`;
17
+ /** Marks the footer row that carries the unsubscribe link, so a send can drop it. */
18
+ readonly unsubscribeRowAttribute: `data-${string}`;
19
+ /** The unsubscribe link's href; the send path substitutes each delivery's URL for it. */
20
+ readonly unsubscribeUrlPlaceholder: string;
21
+ /**
22
+ * Written on the unsubscribe link, so the send path delivers it without
23
+ * click tracking: a rewritten link would carry another recipient's token.
24
+ */
25
+ readonly noTrackAttribute: `data-${string}`;
26
+ }
27
+ /** The brand a specifier names: `undefined` slug means the default brand; `undefined` when it names none. */
28
+ declare const parseBrandSpecifier: (specifier: string, options?: {
29
+ readonly brandPlugin?: BrandPlugin | undefined;
30
+ }) => {
31
+ readonly slug: string | undefined;
32
+ } | undefined;
33
+ //#endregion
4
34
  //#region src/email/brand.d.ts
5
35
  interface EmailBrandAddress {
6
36
  readonly street1: string;
@@ -74,41 +104,4 @@ declare const withoutUnsubscribeRow: (rendered: {
74
104
  readonly text: string | undefined;
75
105
  };
76
106
  //#endregion
77
- //#region src/email/assets.d.ts
78
- /** Files a template may import, and what they are served as. */
79
- declare const ASSET_CONTENT_TYPES: Readonly<Record<string, string>>;
80
- interface AssetEntry {
81
- /** Project-relative path the template imported. */
82
- readonly path: string;
83
- /** SHA-256 of the exact bytes, lowercase hex. */
84
- readonly digest: string;
85
- readonly bytes: number;
86
- readonly contentType: string;
87
- /** `<digest><ext>` — the name the asset has under any base. */
88
- readonly fileName: string;
89
- }
90
- interface AssetManifest {
91
- /** Where these assets are served from in this build. */
92
- readonly base: string;
93
- readonly assets: readonly AssetEntry[];
94
- }
95
- /** True when the compiler turns this import into a content-addressed asset. */
96
- declare const isAssetPath: (path: string) => boolean;
97
- /** Address one file by its content. */
98
- declare const assetEntry: (path: string, bytes: Uint8Array) => Promise<AssetEntry>;
99
- /**
100
- * The URL an asset has under a base. The base is a build input: publication
101
- * passes its hosted origin, and a local export passes whatever it will serve
102
- * the exported files from.
103
- */
104
- declare const assetUrl: (entry: AssetEntry, base: string) => string;
105
- /** Record, but never fetch, a URL the template points at. */
106
- declare const isExternalAsset: (reference: string) => boolean;
107
- /**
108
- * Reject an asset base that would produce URLs a client will not load. The
109
- * relative forms are the ones a page on disk resolves against its own
110
- * directory, which is what a local export writes.
111
- */
112
- declare const checkAssetBase: (base: string) => readonly EmailDiagnostic[];
113
- //#endregion
114
- export { EmailBrandSocialLink as _, assetUrl as a, renderedFooterMarkers as b, isExternalAsset as c, BrandFooterProps as d, BrandLogoProps as f, EmailBrandFooter as g, EmailBrandAssets as h, assetEntry as i, BRAND_COMPONENT_CLASSES as l, EmailBrandAddress as m, AssetEntry as n, checkAssetBase as o, EmailBrand as p, AssetManifest as r, isAssetPath as s, ASSET_CONTENT_TYPES as t, BrandComponents as u, RenderedFooterMarkers as v, withoutUnsubscribeRow as x, brandComponents as y };
107
+ export { EmailBrand as a, EmailBrandFooter as c, brandComponents as d, renderedFooterMarkers as f, parseBrandSpecifier as h, BrandLogoProps as i, EmailBrandSocialLink as l, BrandPlugin as m, BrandComponents as n, EmailBrandAddress as o, withoutUnsubscribeRow as p, BrandFooterProps as r, EmailBrandAssets as s, BRAND_COMPONENT_CLASSES as t, RenderedFooterMarkers as u };
package/dist/brand.d.ts CHANGED
@@ -1,5 +1,5 @@
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";
1
+ import { a as EmailBrand, c as EmailBrandFooter, d as brandComponents, f as renderedFooterMarkers, i as BrandLogoProps, l as EmailBrandSocialLink, m as BrandPlugin, n as BrandComponents, o as EmailBrandAddress, p as withoutUnsubscribeRow, r as BrandFooterProps, s as EmailBrandAssets, t as BRAND_COMPONENT_CLASSES, u as RenderedFooterMarkers } from "./brand-Bxb6Sm7y.js";
2
+ import { a as assetUrl, c as isExternalAsset, i as assetEntry, n as AssetEntry, o as checkAssetBase, r as AssetManifest, s as isAssetPath, t as ASSET_CONTENT_TYPES } from "./assets-k7_v3IOJ.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 { 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";
1
+ import { B as brandComponents, H as withoutUnsubscribeRow, V as renderedFooterMarkers, et as samvaBrandPlugin, z as BRAND_COMPONENT_CLASSES } from "./components-DNBCz20_.js";
2
+ import { a as isAssetPath, i as checkAssetBase, n as assetEntry, o as isExternalAsset, r as assetUrl, t as ASSET_CONTENT_TYPES } from "./assets-BSpgZOSR.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-BujDuuEA.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 };
@@ -1,7 +1,7 @@
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";
1
+ import { m as BrandPlugin } from "./brand-Bxb6Sm7y.js";
2
+ import { n as EmailDiagnostic } from "./diagnostics-C3TSx6SU.js";
3
+ import { G as EmailDegradation, K as SemanticComponentName, W as COMPONENT_DEGRADATIONS, q as componentDegradation, v as CompiledTemplate } from "./compiled-template-CPDHvFlt.js";
4
+ import { r as EmittedPosition } from "./preview-DRLWgmfg.js";
5
5
  import { caniemail } from "caniemail";
6
6
  //#region src/email/compatibility.d.ts
7
7
  type CaniemailClient = Parameters<typeof caniemail>[0]["clients"][number];
@@ -62,6 +62,16 @@ declare const unknownCoverage: (titles: Iterable<string>) => readonly EmailDiagn
62
62
  declare const hasBlockingCompatibility: (findings: readonly EmailDiagnostic[]) => boolean;
63
63
  //#endregion
64
64
  //#region src/sml/compatibility.d.ts
65
+ /** One authored component's content fidelity in email, independent of client presentation. */
66
+ interface ComponentEmailDegradation extends EmailDegradation {
67
+ readonly component: SemanticComponentName;
68
+ readonly origins: EmailDiagnostic["origins"];
69
+ }
70
+ /**
71
+ * The document's declarations, including conditional branches. A declaration describes the
72
+ * component when used; fixture-specific client observations still come from the rendered HTML.
73
+ */
74
+ declare const templateEmailDegradations: (template: CompiledTemplate) => readonly ComponentEmailDegradation[];
65
75
  /**
66
76
  * Check what each declared fixture delivers against the pinned client matrix. The rules run on the
67
77
  * rendered document, because what a client drops is what the compiler produced, and every finding
@@ -73,4 +83,4 @@ declare const checkCompatibility: (template: CompiledTemplate, fixtures: Readonl
73
83
  readonly brandPlugin?: BrandPlugin | undefined;
74
84
  }) => readonly EmailDiagnostic[];
75
85
  //#endregion
76
- export { COMPATIBILITY_PROVENANCE, type CompatibilityInput, type CompatibilityProvenance, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, type EmailClientTarget, checkCompatibility, checkHtmlCompatibility, hasBlockingCompatibility, unknownCoverage };
86
+ export { COMPATIBILITY_PROVENANCE, COMPONENT_DEGRADATIONS, type CompatibilityInput, type CompatibilityProvenance, type ComponentEmailDegradation, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, type EmailClientTarget, type EmailDegradation, type SemanticComponentName, checkCompatibility, checkHtmlCompatibility, componentDegradation, hasBlockingCompatibility, templateEmailDegradations, unknownCoverage };
@@ -1,7 +1,7 @@
1
- import { r as renderEmail } from "./render-template-DwPDm3Hq.js";
2
- import { n as parseColor } from "./css-color-DLM12Osu.js";
3
- import { n as matchesSelector } from "./selectors-CYWCoxZr.js";
4
- import { t as parseStylesheet } from "./css-B_aVKrDo.js";
1
+ import { A as renderEmail, K as isValueNode, U as COMPONENT_DEGRADATIONS, W as componentDegradation } from "./components-DNBCz20_.js";
2
+ import { o as parseColor } from "./dark-scheme-BGDCCaUq.js";
3
+ import { n as matchesSelector, r as parseStylesheet } from "./selectors-CKGuc9MO.js";
4
+ import { t as DIAGNOSTIC_CODES } from "./diagnostic-codes-BCwfUgnF.js";
5
5
  import { generate, parse, walk } from "css-tree";
6
6
  import { caniemail, parseHtml, rawData } from "caniemail";
7
7
  //#region src/email/compatibility-assessment.ts
@@ -768,6 +768,48 @@ const hasBlockingCompatibility = (findings) => findings.some((finding) => findin
768
768
  //#endregion
769
769
  //#region src/sml/compatibility.ts
770
770
  /**
771
+ * The document's declarations, including conditional branches. A declaration describes the
772
+ * component when used; fixture-specific client observations still come from the rendered HTML.
773
+ */
774
+ const templateEmailDegradations = (template) => {
775
+ if (template.email === void 0) return [];
776
+ const declarations = [];
777
+ const pending = [template.email.body];
778
+ const seen = /* @__PURE__ */ new Set();
779
+ while (pending.length > 0) {
780
+ const node = pending.pop();
781
+ if (isValueNode(node) || seen.has(node)) continue;
782
+ seen.add(node);
783
+ if ("type" in node && node.type === "component") {
784
+ const src = node.src;
785
+ declarations.push({
786
+ component: node.name,
787
+ ...componentDegradation(node.name),
788
+ origins: src === void 0 ? [] : [{
789
+ fileName: template.sources?.[src[2] ?? 0] ?? "",
790
+ lineNumber: src[0],
791
+ columnNumber: src[1]
792
+ }]
793
+ });
794
+ }
795
+ if ("if" in node) pending.push(...[...node.then, ...node.else ?? []].reverse());
796
+ else if ("children" in node) pending.push(...(node.children ?? []).toReversed());
797
+ }
798
+ return declarations;
799
+ };
800
+ /** Translate content losses into the same located report as HTML compatibility findings. */
801
+ const degradationFindings = (declarations) => declarations.flatMap((declaration) => declaration.kind === "same" ? [] : [{
802
+ code: "email-component-degradation",
803
+ severity: "warning",
804
+ message: `<${declaration.component}>: ${declaration.note}`,
805
+ origins: declaration.origins,
806
+ notes: DIAGNOSTIC_CODES["email-component-degradation"].fix,
807
+ compatibility: {
808
+ assessment: "degradation",
809
+ title: declaration.note
810
+ }
811
+ }]);
812
+ /**
771
813
  * Check what each declared fixture delivers against the pinned client matrix. The rules run on the
772
814
  * rendered document, because what a client drops is what the compiler produced, and every finding
773
815
  * is located at the source of the element that produced it. A fixture the renderer refuses throws
@@ -801,7 +843,8 @@ const checkCompatibility = (template, fixtures, options = {}) => {
801
843
  ...options.clients === void 0 ? {} : { clients: options.clients }
802
844
  }));
803
845
  }
846
+ findings.push(...degradationFindings(templateEmailDegradations(template)));
804
847
  return findings;
805
848
  };
806
849
  //#endregion
807
- export { COMPATIBILITY_PROVENANCE, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, checkCompatibility, checkHtmlCompatibility, hasBlockingCompatibility, unknownCoverage };
850
+ export { COMPATIBILITY_PROVENANCE, COMPONENT_DEGRADATIONS, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, checkCompatibility, checkHtmlCompatibility, componentDegradation, hasBlockingCompatibility, templateEmailDegradations, unknownCoverage };