@samva/markup 0.10.0 → 0.11.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 CHANGED
@@ -30,7 +30,11 @@ Authoring entries are what a template file imports:
30
30
  mode) and `BrandFooter` (company, postal address, social links and the unsubscribe row the send
31
31
  path fills). The compiler binds them to the brand the build resolves; add
32
32
  `"types": ["@samva/markup/samva-brand"]` to the project's tsconfig for `tsc`. In `theme.css`,
33
- `@import "samva:brand";` layers the brand theme; `samva:brand/<slug>` names one brand.
33
+ `@import "samva:brand";` layers the brand theme; `samva:brand/<slug>` names one brand. These are
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` to `compileTemplate`,
36
+ `renderIr` and `renderBrandFooter` to rename the brand import, the footer markers, the
37
+ unsubscribe placeholder and the unsubscribe link's no-track marker.
34
38
 
35
39
  Tooling entries are what a host (Vite, the CLI, the editor, the API, the template agent) imports:
36
40
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## @samva/markup@0.11.0
2
+
3
+ ### Breaking: `@samva/markup` reads brand and footer names from a brand plugin
4
+
5
+ Templates do not change: `samva:brand`, `BrandLogo`, `BrandFooter` and the unsubscribe row work as
6
+ before, because Samva's brand plugin is the default.
7
+
8
+ A host that compiles or renders templates can pass its own `BrandPlugin` as `brandPlugin` to
9
+ `compileTemplate`, `transformProject`, `projectBrandSpecifier`, `parseBrandSpecifier`,
10
+ `checkStaticProfile`, `renderIr`, `compileEmail`, `renderBrandFooter`, `renderBrandSample`,
11
+ `brandComponents`, `renderedFooterMarkers` and `withoutUnsubscribeRow`. The plugin names the brand
12
+ import, the attributes that mark the footer and its unsubscribe row, the unsubscribe link's URL
13
+ placeholder, and the attribute that marks that link as untracked.
14
+
15
+ `BrandFooter`'s unsubscribe link now carries `data-samva-no-track` instead of SES's
16
+ `ses:no-track`. A host that delivers mail through SES turns the marker into `ses:no-track` before it
17
+ sends.
18
+
19
+ `@samva/markup/brand` exports `samvaBrandPlugin` and the `BrandPlugin` type in place of
20
+ `FOOTER_ATTRIBUTE`, `UNSUBSCRIBE_ROW_ATTRIBUTE` and `UNSUBSCRIBE_URL_PLACEHOLDER`; read
21
+ `samvaBrandPlugin.footerAttribute`, `.unsubscribeRowAttribute` and `.unsubscribeUrlPlaceholder`.
22
+ `@samva/markup/compiler` no longer exports `BRAND_SPECIFIER`; read `samvaBrandPlugin.specifier`.
23
+
24
+ ### Breaking: supply brand resolution to the authoring toolchain
25
+
26
+ `@samva/vite` accepts `brandResolver` and `brandPlugin` on the editor plugin and its dev, project,
27
+ build, render and export APIs. A brand import without a resolver fails with `brand-unavailable`.
28
+ Standalone callers supply a resolver; the package does not read Samva credentials, invoke the
29
+ Samva CLI or load Samva brand snapshots. The `offline` option and Samva snapshot helpers move to
30
+ the CLI.
31
+
32
+ `samva templates dev`, `check`, `build`, `render`, `snapshot` and `export` supply the Samva resolver
33
+ and brand plugin, preserving live lookup, cached snapshots and brand diagnostics.
34
+
1
35
  ## @samva/markup@0.10.0
2
36
 
3
37
  ### Breaking: `@samva/markup` publishes 17 entries in two tiers
@@ -1,3 +1,4 @@
1
+ import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
1
2
  import { r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
2
3
  import { r as EmailNode } from "./jsx-runtime-ylIOnaSf.js";
3
4
  //#region src/email/brand.d.ts
@@ -24,17 +25,13 @@ interface EmailBrandFooter {
24
25
  readonly socialLinks?: readonly EmailBrandSocialLink[] | undefined;
25
26
  }
26
27
  interface EmailBrand {
27
- /** The brand's slug; `samva:brand/<slug>` must name it. */
28
+ /** The brand's slug; a `<specifier>/<slug>` brand import must name it. */
28
29
  readonly slug: string;
29
30
  /** Brand theme CSS in the closed grammar `parseBrandCss` accepts. */
30
31
  readonly css: string;
31
32
  readonly assets?: EmailBrandAssets | undefined;
32
33
  readonly footer?: EmailBrandFooter | undefined;
33
34
  }
34
- /** Marks the block the send path recognizes as the template's own footer. */
35
- declare const FOOTER_ATTRIBUTE = "data-samva-footer";
36
- /** Marks the footer row that carries the unsubscribe link. */
37
- declare const UNSUBSCRIBE_ROW_ATTRIBUTE = "data-samva-unsubscribe";
38
35
  /** Every class the brand components use, compiled for any project that imports them. */
39
36
  declare const BRAND_COMPONENT_CLASSES: readonly ["dark:hidden!", "hidden", "dark:block!", "text-xs", "text-gray-500", "text-center", "font-semibold", "underline"];
40
37
  interface BrandLogoProps {
@@ -54,8 +51,8 @@ interface BrandComponents {
54
51
  readonly BrandLogo: (props: BrandLogoProps) => EmailNode | null;
55
52
  readonly BrandFooter: (props: BrandFooterProps) => EmailNode | null;
56
53
  }
57
- /** Bind `BrandLogo` and `BrandFooter` to one brand. */
58
- declare const brandComponents: (brand: EmailBrand) => BrandComponents;
54
+ /** Bind `BrandLogo` and `BrandFooter` to one brand, marked as the plugin names. */
55
+ declare const brandComponents: (brand: EmailBrand, plugin?: BrandPlugin) => BrandComponents;
59
56
  /** What a rendered message says about the footer it carries. */
60
57
  interface RenderedFooterMarkers {
61
58
  /** The message renders `BrandFooter` (or claims to, with the marker). */
@@ -63,8 +60,8 @@ interface RenderedFooterMarkers {
63
60
  /** The message carries the unsubscribe placeholder link only `BrandFooter` can write. */
64
61
  readonly unsubscribeLink: boolean;
65
62
  }
66
- /** Read the footer markers from rendered HTML. */
67
- declare const renderedFooterMarkers: (html: string) => RenderedFooterMarkers;
63
+ /** Read the plugin's footer markers from rendered HTML. */
64
+ declare const renderedFooterMarkers: (html: string, plugin?: BrandPlugin) => RenderedFooterMarkers;
68
65
  /**
69
66
  * Drop the unsubscribe row from a rendered message, for a send that resolved
70
67
  * no unsubscribe group: there is no link to put there, so the row goes.
@@ -72,7 +69,7 @@ declare const renderedFooterMarkers: (html: string) => RenderedFooterMarkers;
72
69
  declare const withoutUnsubscribeRow: (rendered: {
73
70
  readonly html?: string | undefined;
74
71
  readonly text?: string | undefined;
75
- }) => {
72
+ }, plugin?: BrandPlugin) => {
76
73
  readonly html: string | undefined;
77
74
  readonly text: string | undefined;
78
75
  };
@@ -114,5 +111,5 @@ declare const isExternalAsset: (reference: string) => boolean;
114
111
  */
115
112
  declare const checkAssetBase: (base: string) => readonly EmailDiagnostic[];
116
113
  //#endregion
117
- export { withoutUnsubscribeRow as C, renderedFooterMarkers as S, EmailBrandSocialLink as _, assetUrl as a, UNSUBSCRIBE_ROW_ATTRIBUTE 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, FOOTER_ATTRIBUTE as v, brandComponents as x, RenderedFooterMarkers as y };
118
- //# sourceMappingURL=assets-BIsyMQ92.d.ts.map
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 };
115
+ //# sourceMappingURL=assets-B0yseju_.d.ts.map
@@ -1,4 +1,4 @@
1
- import { i as UNSUBSCRIBE_URL_PLACEHOLDER, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "./unsubscribe-link-CoesnQDU.js";
1
+ import { D as samvaBrandPlugin, O as BRAND_FOOTER_MARKER, k as UNSUBSCRIBE_LINK, r as escapeAttribute } from "./html-DjPxwjLi.js";
2
2
  import { a as jsx, n as Fragment } from "./jsx-runtime-CBlexoeR.js";
3
3
  //#region src/email/assets.ts
4
4
  /** Files a template may import, and what they are served as. */
@@ -61,10 +61,6 @@ const checkAssetBase = (base) => /^(https:\/\/|\.{1,2}\/|\/)/i.test(base) ? [] :
61
61
  }];
62
62
  //#endregion
63
63
  //#region src/email/brand.ts
64
- /** Marks the block the send path recognizes as the template's own footer. */
65
- const FOOTER_ATTRIBUTE = "data-samva-footer";
66
- /** Marks the footer row that carries the unsubscribe link. */
67
- const UNSUBSCRIBE_ROW_ATTRIBUTE = "data-samva-unsubscribe";
68
64
  /** Every class the brand components use, compiled for any project that imports them. */
69
65
  const BRAND_COMPONENT_CLASSES = [
70
66
  "dark:hidden!",
@@ -97,8 +93,8 @@ const row = (children, props = {}) => jsx("tr", {
97
93
  children
98
94
  })
99
95
  });
100
- /** Bind `BrandLogo` and `BrandFooter` to one brand. */
101
- const brandComponents = (brand) => {
96
+ /** Bind `BrandLogo` and `BrandFooter` to one brand, marked as the plugin names. */
97
+ const brandComponents = (brand, plugin = samvaBrandPlugin) => {
102
98
  const assets = brand.assets ?? {};
103
99
  const footer = brand.footer ?? {};
104
100
  const BrandLogo = ({ width = 120, alt, align, className }) => {
@@ -145,8 +141,8 @@ const brandComponents = (brand) => {
145
141
  className: "text-gray-500 underline",
146
142
  children: link.label
147
143
  })])));
148
- rows.push(row(unsubscribeLink(unsubscribeLabel), {
149
- [UNSUBSCRIBE_ROW_ATTRIBUTE]: "",
144
+ rows.push(row(unsubscribeLink(unsubscribeLabel, plugin), {
145
+ [plugin.unsubscribeRowAttribute]: "",
150
146
  [BRAND_FOOTER_MARKER]: true
151
147
  }));
152
148
  return jsx("table", {
@@ -155,7 +151,7 @@ const brandComponents = (brand) => {
155
151
  border: 0,
156
152
  cellpadding: 0,
157
153
  cellspacing: 0,
158
- [FOOTER_ATTRIBUTE]: "",
154
+ [plugin.footerAttribute]: "",
159
155
  [BRAND_FOOTER_MARKER]: true,
160
156
  children: jsx("tbody", { children: jsx("tr", { children: jsx("td", {
161
157
  align: "center",
@@ -178,31 +174,29 @@ const brandComponents = (brand) => {
178
174
  };
179
175
  /**
180
176
  * The unsubscribe link. Its href is the placeholder, which no author-written
181
- * link may carry: the serializer accepts it only on this node, and marks it so
182
- * click tracking never rewrites it.
177
+ * link may carry: the serializer accepts it only on this node, and writes the
178
+ * plugin's no-track marker on it so click tracking never rewrites it.
183
179
  */
184
- const unsubscribeLink = (label) => jsx("a", {
185
- href: UNSUBSCRIBE_URL_PLACEHOLDER,
180
+ const unsubscribeLink = (label, plugin) => jsx("a", {
181
+ href: plugin.unsubscribeUrlPlaceholder,
186
182
  className: "text-gray-500 underline",
187
183
  [UNSUBSCRIBE_LINK]: true,
188
184
  children: label
189
185
  });
190
- const UNSUBSCRIBE_ROW = new RegExp(`<tr ${UNSUBSCRIBE_ROW_ATTRIBUTE}=""[^>]*>[\\s\\S]*?</tr>`, "g");
191
- const UNSUBSCRIBE_HREF = `href="${UNSUBSCRIBE_URL_PLACEHOLDER}"`;
192
- /** Read the footer markers from rendered HTML. */
193
- const renderedFooterMarkers = (html) => ({
194
- footer: html.includes(`${FOOTER_ATTRIBUTE}=""`),
195
- unsubscribeLink: html.includes(UNSUBSCRIBE_HREF)
186
+ /** Read the plugin's footer markers from rendered HTML. */
187
+ const renderedFooterMarkers = (html, plugin = samvaBrandPlugin) => ({
188
+ footer: html.includes(`${plugin.footerAttribute}=""`),
189
+ unsubscribeLink: html.includes(`href="${escapeAttribute(plugin.unsubscribeUrlPlaceholder)}"`)
196
190
  });
197
191
  /**
198
192
  * Drop the unsubscribe row from a rendered message, for a send that resolved
199
193
  * no unsubscribe group: there is no link to put there, so the row goes.
200
194
  */
201
- const withoutUnsubscribeRow = (rendered) => ({
202
- html: rendered.html?.replace(UNSUBSCRIBE_ROW, ""),
203
- text: rendered.text?.split("\n").filter((line) => !line.includes(UNSUBSCRIBE_URL_PLACEHOLDER)).join("\n")
195
+ const withoutUnsubscribeRow = (rendered, plugin = samvaBrandPlugin) => ({
196
+ html: rendered.html?.replace(new RegExp(`<tr ${plugin.unsubscribeRowAttribute}=""[^>]*>[\\s\\S]*?</tr>`, "g"), ""),
197
+ text: rendered.text?.split("\n").filter((line) => !line.includes(plugin.unsubscribeUrlPlaceholder)).join("\n")
204
198
  });
205
199
  //#endregion
206
- export { renderedFooterMarkers as a, assetEntry as c, isAssetPath as d, isExternalAsset as f, brandComponents as i, assetUrl as l, FOOTER_ATTRIBUTE as n, withoutUnsubscribeRow as o, UNSUBSCRIBE_ROW_ATTRIBUTE as r, ASSET_CONTENT_TYPES as s, BRAND_COMPONENT_CLASSES as t, checkAssetBase as u };
200
+ 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 };
207
201
 
208
- //# sourceMappingURL=brand-C0MKTwmX.js.map
202
+ //# sourceMappingURL=brand-DTctcDmf.js.map
@@ -0,0 +1,32 @@
1
+ //#region src/email/brand-plugin.d.ts
2
+ /**
3
+ * How a template reaches its brand and how the footer `BrandFooter` renders is
4
+ * marked for the send path that delivers it. The compiler and the IR renderer
5
+ * read every brand import, footer marker and unsubscribe link from the plugin
6
+ * a build passes; `samvaBrandPlugin` is the default.
7
+ */
8
+ interface BrandPlugin {
9
+ /**
10
+ * The module and stylesheet specifier a template imports its brand from.
11
+ * `<specifier>/<slug>` names one brand; the bare specifier names the default.
12
+ */
13
+ readonly specifier: string;
14
+ /** Marks the block `BrandFooter` renders, so the send path finds a template's own footer. */
15
+ readonly footerAttribute: `data-${string}`;
16
+ /** Marks the footer row that carries the unsubscribe link, so a send can drop it. */
17
+ readonly unsubscribeRowAttribute: `data-${string}`;
18
+ /** The unsubscribe link's href; the send path substitutes each delivery's URL for it. */
19
+ readonly unsubscribeUrlPlaceholder: string;
20
+ /**
21
+ * Written on the unsubscribe link, so the send path delivers it without
22
+ * click tracking: a rewritten link would carry another recipient's token.
23
+ */
24
+ readonly noTrackAttribute: `data-${string}`;
25
+ }
26
+ /** The brand a specifier names: `undefined` slug means the default brand; `undefined` when it names none. */
27
+ declare const parseBrandSpecifier: (specifier: string, plugin?: BrandPlugin) => {
28
+ readonly slug: string | undefined;
29
+ } | undefined;
30
+ //#endregion
31
+ export { parseBrandSpecifier as n, BrandPlugin as t };
32
+ //# sourceMappingURL=brand-plugin-CHhcei-j.d.ts.map
package/dist/brand.d.ts CHANGED
@@ -1,13 +1,15 @@
1
- import { C as withoutUnsubscribeRow, S as renderedFooterMarkers, _ as EmailBrandSocialLink, a as assetUrl, b as UNSUBSCRIBE_ROW_ATTRIBUTE, 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 FOOTER_ATTRIBUTE, x as brandComponents, y as RenderedFooterMarkers } from "./assets-BIsyMQ92.js";
1
+ import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.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-B0yseju_.js";
2
3
  import { t as Diagnostic } from "./diagnostic-model-DxthUZBT.js";
3
4
  import { Result } from "better-result";
4
- //#region src/internal/unsubscribe-link.d.ts
5
+ //#region src/email/samva-brand-plugin.d.ts
5
6
  /**
6
- * The per-recipient unsubscribe URL is not known at render: the send path
7
- * substitutes each delivery's link for this placeholder, or removes the
8
- * unsubscribe row when the send resolved no unsubscribe group.
7
+ * Samva's brand plugin, the compiler's default. A template imports its
8
+ * organization's brand from `samva:brand`, and Samva's send path fills
9
+ * `%samva:unsubscribe-url%` per delivery and turns `data-samva-no-track` into
10
+ * its email provider's own no-track marker.
9
11
  */
10
- declare const UNSUBSCRIBE_URL_PLACEHOLDER = "%samva:unsubscribe-url%";
12
+ declare const samvaBrandPlugin: BrandPlugin;
11
13
  //#endregion
12
14
  //#region src/theme-css.d.ts
13
15
  /** One `@font-face` a brand declares. `src` is the declaration value as authored. */
@@ -67,8 +69,8 @@ declare const brandForegroundDefaults: (brand: BrandCss) => {
67
69
  * build compiles the same default and the stored CSS never carries it.
68
70
  */
69
71
  declare const brandThemeVariables: (brand: BrandCss) => Readonly<Record<string, string>>;
70
- /** The stylesheet Tailwind compiles for `@import "samva:brand"`: `brandThemeVariables` and the font faces. */
72
+ /** The stylesheet Tailwind compiles for the theme's brand import: `brandThemeVariables` and the font faces. */
71
73
  declare const brandTailwindCss: (brand: BrandCss) => string;
72
74
  //#endregion
73
- export { ASSET_CONTENT_TYPES, AssetEntry, AssetManifest, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, BrandComponents, BrandCss, BrandFontFace, BrandFooterProps, BrandLogoProps, EmailBrand, EmailBrandAddress, EmailBrandAssets, EmailBrandFooter, EmailBrandSocialLink, FOOTER_ATTRIBUTE, RenderedFooterMarkers, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, withoutUnsubscribeRow };
75
+ export { ASSET_CONTENT_TYPES, AssetEntry, AssetManifest, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, BrandComponents, BrandCss, BrandFontFace, BrandFooterProps, BrandLogoProps, type BrandPlugin, EmailBrand, EmailBrandAddress, EmailBrandAssets, EmailBrandFooter, EmailBrandSocialLink, RenderedFooterMarkers, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
74
76
  //# sourceMappingURL=brand.d.ts.map
package/dist/brand.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as UNSUBSCRIBE_URL_PLACEHOLDER } from "./unsubscribe-link-CoesnQDU.js";
2
- import { a as brandThemeVariables, c as parseBrandCss, i as brandTailwindCss, l as printBrandCss, n as BRAND_FOREGROUND_VARIABLE, o as contrastForeground, r as brandForegroundDefaults, s as darkVariableName, t as BRAND_COLOR_VARIABLE } from "./theme-css-JtRCm1fb.js";
3
- import { a as renderedFooterMarkers, c as assetEntry, d as isAssetPath, f as isExternalAsset, i as brandComponents, l as assetUrl, n as FOOTER_ATTRIBUTE, o as withoutUnsubscribeRow, r as UNSUBSCRIBE_ROW_ATTRIBUTE, s as ASSET_CONTENT_TYPES, t as BRAND_COMPONENT_CLASSES, u as checkAssetBase } from "./brand-C0MKTwmX.js";
4
- export { ASSET_CONTENT_TYPES, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, FOOTER_ATTRIBUTE, UNSUBSCRIBE_ROW_ATTRIBUTE, UNSUBSCRIBE_URL_PLACEHOLDER, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, withoutUnsubscribeRow };
1
+ import { D as samvaBrandPlugin } from "./html-DjPxwjLi.js";
2
+ import { a as brandThemeVariables, c as parseBrandCss, i as brandTailwindCss, l as printBrandCss, n as BRAND_FOREGROUND_VARIABLE, o as contrastForeground, r as brandForegroundDefaults, s as darkVariableName, t as BRAND_COLOR_VARIABLE } from "./theme-css-CEUrDpqq.js";
3
+ 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-DTctcDmf.js";
4
+ export { ASSET_CONTENT_TYPES, BRAND_COLOR_VARIABLE, BRAND_COMPONENT_CLASSES, BRAND_FOREGROUND_VARIABLE, assetEntry, assetUrl, brandComponents, brandForegroundDefaults, brandTailwindCss, brandThemeVariables, checkAssetBase, contrastForeground, darkVariableName, isAssetPath, isExternalAsset, parseBrandCss, printBrandCss, renderedFooterMarkers, samvaBrandPlugin, withoutUnsubscribeRow };
@@ -1,7 +1,8 @@
1
+ import { n as parseBrandSpecifier, t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
1
2
  import { a as TemplateDiagnostic, r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
2
3
  import { r as EmailNode } from "./jsx-runtime-ylIOnaSf.js";
3
- import { n as AssetEntry, p as EmailBrand, u as BrandComponents } from "./assets-BIsyMQ92.js";
4
- import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-BPonDpCl.js";
4
+ import { n as AssetEntry, p as EmailBrand, u as BrandComponents } from "./assets-B0yseju_.js";
5
+ import { C as Compound, D as EmittedPosition, S as CompiledSelector, T as matchesSelector, _ as Specificity, b as parseStylesheet, ct as TemplateIr, g as CssRule, h as CssDeclaration, r as RenderedEmail, v as Stylesheet, w as classListOf, x as Ancestry, y as compileSelector } from "./render-DQKz4Aco.js";
5
6
  import { a as TEMPLATE_NAME_PATTERN, n as EmailContent } from "./template-DMPwxMSq.js";
6
7
  import { caniemail } from "caniemail";
7
8
  //#region src/email/compatibility.d.ts
@@ -156,12 +157,14 @@ interface TailwindOutput {
156
157
  }
157
158
  /**
158
159
  * What the project theme's `@import`s resolve to, already checked by
159
- * `resolveThemeImports`: project stylesheets by path, and the brand theme
160
- * `samva:brand` stands for.
160
+ * `resolveThemeImports`: project stylesheets by path, and the brand theme the
161
+ * brand plugin's specifier stands for.
161
162
  */
162
163
  interface TailwindImports {
163
164
  readonly stylesheets: ReadonlyMap<string, string>;
164
165
  readonly brandCss?: string | undefined;
166
+ /** Names the brand import; `samvaBrandPlugin` when omitted. */
167
+ readonly brandPlugin?: BrandPlugin | undefined;
165
168
  }
166
169
  /** Compile the discovered class set into CSS with the pinned Tailwind release. */
167
170
  declare const compileTailwind: (candidates: Iterable<string>, options?: TailwindOptions, imports?: TailwindImports) => Promise<TailwindOutput>;
@@ -182,10 +185,12 @@ interface TransformOptions {
182
185
  /** `false` compiles no Tailwind at all. */
183
186
  readonly tailwind?: TailwindOptions | false | undefined;
184
187
  /**
185
- * The brand `samva:brand` resolves to, supplied by the host. A project that
186
- * imports `samva:brand` with no brand here fails to compile.
188
+ * The brand the brand import resolves to, supplied by the host. A project
189
+ * that imports a brand with no brand here fails to compile.
187
190
  */
188
191
  readonly brand?: EmailBrand | undefined;
192
+ /** Names the brand import and marks the footer `BrandFooter` renders; `samvaBrandPlugin` when omitted. */
193
+ readonly brandPlugin?: BrandPlugin | undefined;
189
194
  }
190
195
  interface ProjectTransform {
191
196
  readonly assets: readonly AssetEntry[];
@@ -196,23 +201,15 @@ interface ProjectTransform {
196
201
  readonly diagnostics: readonly EmailDiagnostic[];
197
202
  }
198
203
  /**
199
- * The `samva:brand` specifier a project imports — from its theme's `@import`s
200
- * or its TSX — so a host can fetch that brand before it compiles. `undefined`
201
- * when the project imports no brand. A project that names two brands gets the
204
+ * The brand specifier a project imports — from its theme's `@import`s or its
205
+ * TSX — so a host can fetch that brand before it compiles. `undefined` when
206
+ * the project imports no brand. A project that names two brands gets the
202
207
  * first here and a refusal from the compile.
203
208
  */
204
- declare const projectBrandSpecifier: (files: Readonly<Record<string, string | Uint8Array>>, themePath?: string) => string | undefined;
209
+ declare const projectBrandSpecifier: (files: Readonly<Record<string, string | Uint8Array>>, themePath?: string, brandPlugin?: BrandPlugin) => string | undefined;
205
210
  /** Read a whole project, then compile the classes its scripts used. */
206
211
  declare const transformProject: (files: Readonly<Record<string, string | Uint8Array>>, options: TransformOptions) => Promise<ProjectTransform>;
207
212
  //#endregion
208
- //#region src/email/theme-imports.d.ts
209
- /** The CSS and TSX specifier a template uses to reach its organization's brand. */
210
- declare const BRAND_SPECIFIER = "samva:brand";
211
- /** The brand a `samva:brand` specifier names: `undefined` slug means the default brand. */
212
- declare const parseBrandSpecifier: (specifier: string) => {
213
- readonly slug: string | undefined;
214
- } | undefined;
215
- //#endregion
216
213
  //#region src/email/brand-footer.d.ts
217
214
  /** A footer block for the send path to append, and its plain-text tail. */
218
215
  interface RenderedBrandFooter {
@@ -223,15 +220,16 @@ interface RenderedBrandFooter {
223
220
  * Render `BrandFooter` against a brand on its own, for the send path to
224
221
  * append to a message whose template renders no footer of its own. It
225
222
  * compiles the same component, styled by the same brand theme, a template
226
- * would get from `samva:brand`. Without `unsubscribe` the unsubscribe row is
223
+ * would get from its brand import. Without `unsubscribe` the unsubscribe row is
227
224
  * left out.
228
225
  */
229
226
  declare const renderBrandFooter: (brand: EmailBrand, options: {
230
227
  readonly unsubscribe: boolean;
228
+ readonly brandPlugin?: BrandPlugin | undefined;
231
229
  }) => Promise<RenderedBrandFooter>;
232
230
  /**
233
- * Render content against a brand as a project whose `theme.css` is
234
- * `@import "samva:brand";` renders it: Tailwind compiles the brand theme for
231
+ * Render content against a brand as a project whose `theme.css` imports only
232
+ * the brand renders it: Tailwind compiles the brand theme for
235
233
  * the classes the content names, `BrandLogo` and `BrandFooter` are bound to the
236
234
  * brand, and the render inlines the result. A brand preview renders through
237
235
  * this, so it shows what a template importing the brand compiles to.
@@ -240,7 +238,7 @@ declare const renderBrandSample: (brand: EmailBrand, sample: {
240
238
  /** Every class the content names; nothing is discovered from source here. */
241
239
  readonly classes: readonly string[];
242
240
  readonly render: (components: BrandComponents) => EmailContent;
243
- }) => Promise<RenderedEmail>;
241
+ }, brandPlugin?: BrandPlugin) => Promise<RenderedEmail>;
244
242
  //#endregion
245
243
  //#region src/sml/lower-project.d.ts
246
244
  interface CompiledTemplate {
@@ -285,12 +283,12 @@ declare const checkTemplateCompatibility: (ir: TemplateIr, fixtures: Readonly<Re
285
283
  * and the entry's path. Imports of files that are absent are trusted to exist, and the brand,
286
284
  * Tailwind and theme are not resolved, so the result holds only findings the source itself owns.
287
285
  */
288
- declare const checkStaticProfile: (files: string | Readonly<Record<string, string | Uint8Array>>, entry?: string) => readonly TemplateDiagnostic[];
286
+ declare const checkStaticProfile: (files: string | Readonly<Record<string, string | Uint8Array>>, entry?: string, brandPlugin?: BrandPlugin) => readonly TemplateDiagnostic[];
289
287
  /**
290
288
  * The errors in `after` that `before` did not already have, compared by code and message so a
291
289
  * finding that only moved does not count as new.
292
290
  */
293
291
  declare const introducedProfileErrors: (before: readonly TemplateDiagnostic[], after: readonly TemplateDiagnostic[]) => readonly TemplateDiagnostic[];
294
292
  //#endregion
295
- export { type Ancestry, type AppliedStyles, BRAND_SPECIFIER, COMPATIBILITY_PROVENANCE, type ClassDiscovery, type CompatibilityInput, type CompatibilityProvenance, type CompileTemplateOptions, type CompiledSelector, type CompiledTemplate, type Compound, type CssDeclaration, type CssRule, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, type EmailClientTarget, type EmailContent, type ProjectTransform, type RenderedBrandFooter, type Specificity, type Stylesheet, TAILWIND_VERSION, TEMPLATE_NAME_PATTERN, type TailwindLimits, type TailwindOptions, type TailwindOutput, type TransformOptions, applyStylesheet, checkEmailCompatibility, checkStaticProfile, checkTemplateCompatibility, classListOf, compileSelector, compileTailwind, compileTemplate, discoverClassCandidates, downlevelColors, downlevelMediaQuery, finalizeValue, foldCalc, formatColor, hasBlockingCompatibility, introducedProfileErrors, matchesSelector, parseBrandSpecifier, parseColor, parseStylesheet, projectBrandSpecifier, renderBrandFooter, renderBrandSample, renderHeadCss, resolveVariables, transformProject, unknownCoverage };
293
+ export { type Ancestry, type AppliedStyles, COMPATIBILITY_PROVENANCE, type ClassDiscovery, type CompatibilityInput, type CompatibilityProvenance, type CompileTemplateOptions, type CompiledSelector, type CompiledTemplate, type Compound, type CssDeclaration, type CssRule, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, type EmailClientTarget, type EmailContent, type ProjectTransform, type RenderedBrandFooter, type Specificity, type Stylesheet, TAILWIND_VERSION, TEMPLATE_NAME_PATTERN, type TailwindLimits, type TailwindOptions, type TailwindOutput, type TransformOptions, applyStylesheet, checkEmailCompatibility, checkStaticProfile, checkTemplateCompatibility, classListOf, compileSelector, compileTailwind, compileTemplate, discoverClassCandidates, downlevelColors, downlevelMediaQuery, finalizeValue, foldCalc, formatColor, hasBlockingCompatibility, introducedProfileErrors, matchesSelector, parseBrandSpecifier, parseColor, parseStylesheet, projectBrandSpecifier, renderBrandFooter, renderBrandSample, renderHeadCss, resolveVariables, transformProject, unknownCoverage };
296
294
  //# sourceMappingURL=compiler.d.ts.map
package/dist/compiler.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { n as TEMPLATE_NAME_PATTERN } from "./template-zLUjcwfF.js";
3
- import { k as renderIr } from "./components-BZCqegS8.js";
4
- import { c as parseBrandCss, d as formatColor, f as parseColor, i as brandTailwindCss, u as downlevelColors } from "./theme-css-JtRCm1fb.js";
5
- import { a as downlevelMediaQuery, c as renderHeadCss, i as matchesSelector, l as resolveVariables, n as applyStylesheet, o as finalizeValue, r as classListOf, s as foldCalc, u as unquote } from "./ir-CLhlklKV.js";
3
+ import { D as samvaBrandPlugin, T as parseBrandSpecifier, _ as renderIr, w as brandSpecifierPattern } from "./html-DjPxwjLi.js";
4
+ import { c as parseBrandCss, d as formatColor, f as parseColor, i as brandTailwindCss, u as downlevelColors } from "./theme-css-CEUrDpqq.js";
5
+ import { a as downlevelMediaQuery, c as renderHeadCss, i as matchesSelector, l as resolveVariables, n as applyStylesheet, o as finalizeValue, r as classListOf, s as foldCalc, u as unquote } from "./ir-De6pTH51.js";
6
6
  import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
7
- import { a as lowerProject, d as parseBrandSpecifier, f as projectDirectory, i as completed, l as Findings, m as resolveThemeImports, n as introducedProfileErrors, p as resolveFromDirectory, r as loadProject, t as checkStaticProfile, u as BRAND_SPECIFIER } from "./profile-pWGtWZXF.js";
8
- import { c as assetEntry, d as isAssetPath, i as brandComponents, l as assetUrl, o as withoutUnsubscribeRow, t as BRAND_COMPONENT_CLASSES, u as checkAssetBase } from "./brand-C0MKTwmX.js";
7
+ import { a as lowerProject, d as resolveFromDirectory, f as resolveThemeImports, i as completed, l as Findings, n as introducedProfileErrors, r as loadProject, t as checkStaticProfile, u as projectDirectory } from "./profile-PUfi3GPH.js";
8
+ import { c as checkAssetBase, i as withoutUnsubscribeRow, l as isAssetPath, n as brandComponents, o as assetEntry, s as assetUrl, t as BRAND_COMPONENT_CLASSES } from "./brand-DTctcDmf.js";
9
9
  import { t as EmailCompileError } from "./diagnostics-CnKFefKz.js";
10
- import { n as compileEmail, r as renderEmailContent } from "./render-BQmfh94k.js";
10
+ import { n as compileEmail, r as renderEmailContent } from "./render-D2ht57ZZ.js";
11
11
  import { caniemail, parseHtml, rawData } from "caniemail";
12
12
  import { generate, ident, parse, walk } from "css-tree";
13
13
  import { parse as parse$1 } from "@babel/parser";
@@ -869,7 +869,7 @@ const compileTailwind = async (candidates, options = {}, imports = { stylesheets
869
869
  base,
870
870
  content: builtIn
871
871
  });
872
- if (/^samva:brand(?:\/|$)/.test(id)) {
872
+ if (parseBrandSpecifier(id, imports.brandPlugin) !== void 0) {
873
873
  if (imports.brandCss === void 0) throw new TypeError(`${id} has no brand in this build`);
874
874
  return Promise.resolve({
875
875
  path: id,
@@ -1047,12 +1047,12 @@ const readFile = async (id, source, options, assets) => {
1047
1047
  };
1048
1048
  /** The Tailwind sheet's stylesheet origin. */
1049
1049
  const TAILWIND_ORIGIN = ".samva-tailwind.css";
1050
- /** `samva:brand` / `samva:brand/<slug>` as a module specifier in an import or export. */
1051
- const BRAND_MODULE_SPECIFIER = /(\b(?:from|import)\s*\(?\s*)(["'])(samva:brand(?:\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)?)\2/g;
1052
- /** The `samva:brand` specifiers a script imports, with where it asks for each. */
1053
- const scriptBrandImports = (path, source) => {
1050
+ /** The brand specifier, bare or with a slug, as a module specifier in an import or export. */
1051
+ const brandModuleSpecifier = (plugin) => new RegExp(`(\\b(?:from|import)\\s*\\(?\\s*)(["'])(${brandSpecifierPattern(plugin)})\\2`, "g");
1052
+ /** The brand specifiers a script imports, with where it asks for each. */
1053
+ const scriptBrandImports = (path, source, plugin) => {
1054
1054
  const lines = buildLineMap(source);
1055
- return [...source.matchAll(BRAND_MODULE_SPECIFIER)].map((match) => {
1055
+ return [...source.matchAll(brandModuleSpecifier(plugin))].map((match) => {
1056
1056
  const at = lineColumn(lines, match.index + match[1].length);
1057
1057
  return {
1058
1058
  specifier: match[3],
@@ -1065,15 +1065,15 @@ const scriptBrandImports = (path, source) => {
1065
1065
  });
1066
1066
  };
1067
1067
  /**
1068
- * The `samva:brand` specifier a project imports — from its theme's `@import`s
1069
- * or its TSX — so a host can fetch that brand before it compiles. `undefined`
1070
- * when the project imports no brand. A project that names two brands gets the
1068
+ * The brand specifier a project imports — from its theme's `@import`s or its
1069
+ * TSX — so a host can fetch that brand before it compiles. `undefined` when
1070
+ * the project imports no brand. A project that names two brands gets the
1071
1071
  * first here and a refusal from the compile.
1072
1072
  */
1073
- const projectBrandSpecifier = (files, themePath = "theme.css") => {
1073
+ const projectBrandSpecifier = (files, themePath = "theme.css", brandPlugin = samvaBrandPlugin) => {
1074
1074
  const theme = files[themePath];
1075
- const fromTheme = typeof theme === "string" ? resolveThemeImports(themePath, theme, files).brandSpecifiers : [];
1076
- const fromScripts = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path])).map((request) => request.specifier);
1075
+ const fromTheme = typeof theme === "string" ? resolveThemeImports(themePath, theme, files, brandPlugin).brandSpecifiers : [];
1076
+ const fromScripts = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path], brandPlugin)).map((request) => request.specifier);
1077
1077
  return [...fromTheme, ...fromScripts][0];
1078
1078
  };
1079
1079
  /**
@@ -1083,11 +1083,11 @@ const projectBrandSpecifier = (files, themePath = "theme.css") => {
1083
1083
  * outside its grammar refuses the whole compile: a theme that silently lost a
1084
1084
  * layer would ship the wrong look.
1085
1085
  */
1086
- const resolveTheme = (files, tailwind, brand) => {
1086
+ const resolveTheme = (files, tailwind, brand, brandPlugin) => {
1087
1087
  const refusals = [];
1088
1088
  const css = tailwind?.css;
1089
1089
  const themePath = tailwind?.cssPath ?? "theme.css";
1090
- const imports = css === void 0 ? void 0 : resolveThemeImports(themePath, css, files);
1090
+ const imports = css === void 0 ? void 0 : resolveThemeImports(themePath, css, files, brandPlugin);
1091
1091
  refusals.push(...imports?.diagnostics ?? []);
1092
1092
  const themeOrigin = {
1093
1093
  fileName: themePath,
@@ -1098,7 +1098,7 @@ const resolveTheme = (files, tailwind, brand) => {
1098
1098
  specifier,
1099
1099
  origin: themeOrigin
1100
1100
  }));
1101
- const scriptRequests = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path]));
1101
+ const scriptRequests = Object.keys(files).sort().filter((path) => SCRIPT.test(path) && typeof files[path] === "string").flatMap((path) => scriptBrandImports(path, files[path], brandPlugin));
1102
1102
  const requests = [...cssRequests, ...scriptRequests];
1103
1103
  const specifiers = [...new Set(requests.map((request) => request.specifier))];
1104
1104
  if (specifiers.length > 1) refusals.push({
@@ -1109,7 +1109,7 @@ const resolveTheme = (files, tailwind, brand) => {
1109
1109
  });
1110
1110
  else if (specifiers.length === 1) {
1111
1111
  const specifier = specifiers[0];
1112
- const named = parseBrandSpecifier(specifier)?.slug;
1112
+ const named = parseBrandSpecifier(specifier, brandPlugin)?.slug;
1113
1113
  const origins = [requests[0].origin];
1114
1114
  if (brand === void 0) refusals.push({
1115
1115
  code: "brand-unavailable",
@@ -1150,6 +1150,7 @@ const resolveTheme = (files, tailwind, brand) => {
1150
1150
  return {
1151
1151
  stylesheets: imports?.stylesheets ?? /* @__PURE__ */ new Map(),
1152
1152
  brandCss,
1153
+ brandPlugin,
1153
1154
  brandModule: scriptRequests.length > 0 ? brand : void 0
1154
1155
  };
1155
1156
  };
@@ -1160,7 +1161,7 @@ const transformProject = async (files, options) => {
1160
1161
  const stylesheets = /* @__PURE__ */ new Map();
1161
1162
  const classes = /* @__PURE__ */ new Set();
1162
1163
  const wanted = options.tailwind === false ? void 0 : options.tailwind ?? {};
1163
- const theme = resolveTheme(files, wanted, options.brand);
1164
+ const theme = resolveTheme(files, wanted, options.brand, options.brandPlugin ?? samvaBrandPlugin);
1164
1165
  if (theme.brandModule !== void 0) for (const name of BRAND_COMPONENT_CLASSES) classes.add(name);
1165
1166
  const paths = Object.keys(files).sort();
1166
1167
  const binaries = new Map(await Promise.all(paths.filter(isAssetPath).map(async (path) => [path, await readFile(path, files[path], options)])));
@@ -1194,12 +1195,12 @@ const transformProject = async (files, options) => {
1194
1195
  //#endregion
1195
1196
  //#region src/email/brand-footer.ts
1196
1197
  /** The brand theme as Tailwind compiles it; a theme outside the grammar is a compile refusal. */
1197
- const brandTheme = (brand) => {
1198
+ const brandTheme = (brand, plugin) => {
1198
1199
  const parsed = parseBrandCss(brand.css);
1199
1200
  if (parsed.isErr()) throw new EmailCompileError({ diagnostics: parsed.error.map((finding) => ({
1200
1201
  code: finding.code,
1201
1202
  severity: "error",
1202
- message: `samva:brand: ${finding.message}`,
1203
+ message: `${plugin.specifier}: ${finding.message}`,
1203
1204
  origins: []
1204
1205
  })) });
1205
1206
  return brandTailwindCss(parsed.value);
@@ -1208,22 +1209,23 @@ const brandTheme = (brand) => {
1208
1209
  * Render `BrandFooter` against a brand on its own, for the send path to
1209
1210
  * append to a message whose template renders no footer of its own. It
1210
1211
  * compiles the same component, styled by the same brand theme, a template
1211
- * would get from `samva:brand`. Without `unsubscribe` the unsubscribe row is
1212
+ * would get from its brand import. Without `unsubscribe` the unsubscribe row is
1212
1213
  * left out.
1213
1214
  */
1214
1215
  const renderBrandFooter = async (brand, options) => {
1215
- const tailwind = await compileTailwind(BRAND_COMPONENT_CLASSES, { css: brandTheme(brand) });
1216
- const node = brandComponents(brand).BrandFooter({});
1216
+ const plugin = options.brandPlugin ?? samvaBrandPlugin;
1217
+ const tailwind = await compileTailwind(BRAND_COMPONENT_CLASSES, { css: brandTheme(brand, plugin) });
1218
+ const node = brandComponents(brand, plugin).BrandFooter({});
1217
1219
  if (node === null) return {
1218
1220
  html: "",
1219
1221
  text: ""
1220
1222
  };
1221
- const styled = applyStylesheet(node, parseStylesheet(tailwind.css, { origin: "samva:brand" }));
1222
- const compiled = compileEmail(styled.tree);
1223
+ const styled = applyStylesheet(node, parseStylesheet(tailwind.css, { origin: plugin.specifier }));
1224
+ const compiled = compileEmail(styled.tree, { brandPlugin: plugin });
1223
1225
  const rendered = options.unsubscribe ? compiled : withoutUnsubscribeRow({
1224
1226
  html: compiled.html,
1225
1227
  text: compiled.text
1226
- });
1228
+ }, plugin);
1227
1229
  const text = (rendered.text ?? "").trim();
1228
1230
  return {
1229
1231
  html: rendered.html ?? "",
@@ -1231,26 +1233,27 @@ const renderBrandFooter = async (brand, options) => {
1231
1233
  };
1232
1234
  };
1233
1235
  /**
1234
- * Render content against a brand as a project whose `theme.css` is
1235
- * `@import "samva:brand";` renders it: Tailwind compiles the brand theme for
1236
+ * Render content against a brand as a project whose `theme.css` imports only
1237
+ * the brand renders it: Tailwind compiles the brand theme for
1236
1238
  * the classes the content names, `BrandLogo` and `BrandFooter` are bound to the
1237
1239
  * brand, and the render inlines the result. A brand preview renders through
1238
1240
  * this, so it shows what a template importing the brand compiles to.
1239
1241
  */
1240
- const renderBrandSample = async (brand, sample) => {
1241
- const tailwind = await compileTailwind([...BRAND_COMPONENT_CLASSES, ...sample.classes], { css: "@import \"samva:brand\";" }, {
1242
+ const renderBrandSample = async (brand, sample, brandPlugin = samvaBrandPlugin) => {
1243
+ const tailwind = await compileTailwind([...BRAND_COMPONENT_CLASSES, ...sample.classes], { css: `@import ${JSON.stringify(brandPlugin.specifier)};` }, {
1242
1244
  stylesheets: /* @__PURE__ */ new Map(),
1243
- brandCss: brandTheme(brand)
1245
+ brandCss: brandTheme(brand, brandPlugin),
1246
+ brandPlugin
1244
1247
  });
1245
- const components = brandComponents(brand);
1246
- return renderEmailContent(parseStylesheet(tailwind.css, { origin: "samva:brand" }), () => sample.render(components));
1248
+ const components = brandComponents(brand, brandPlugin);
1249
+ return renderEmailContent(parseStylesheet(tailwind.css, { origin: brandPlugin.specifier }), () => sample.render(components), { brandPlugin });
1247
1250
  };
1248
1251
  //#endregion
1249
1252
  //#region src/sml/compile.ts
1250
1253
  /** Compile a project's entry template to IR without evaluating any of it. */
1251
1254
  const compileTemplate = async (options) => {
1252
1255
  const findings = new Findings();
1253
- const project = loadProject(options.files, options.entry, findings);
1256
+ const project = loadProject(options.files, options.entry, findings, { brandPlugin: options.brandPlugin ?? samvaBrandPlugin });
1254
1257
  if (project === void 0) return completed(findings, void 0);
1255
1258
  let transformed;
1256
1259
  try {
@@ -1307,6 +1310,6 @@ const checkTemplateCompatibility = (ir, fixtures, options = {}) => {
1307
1310
  return findings;
1308
1311
  };
1309
1312
  //#endregion
1310
- export { BRAND_SPECIFIER, COMPATIBILITY_PROVENANCE, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, TAILWIND_VERSION, TEMPLATE_NAME_PATTERN, applyStylesheet, checkEmailCompatibility, checkStaticProfile, checkTemplateCompatibility, classListOf, compileSelector, compileTailwind, compileTemplate, discoverClassCandidates, downlevelColors, downlevelMediaQuery, finalizeValue, foldCalc, formatColor, hasBlockingCompatibility, introducedProfileErrors, matchesSelector, parseBrandSpecifier, parseColor, parseStylesheet, projectBrandSpecifier, renderBrandFooter, renderBrandSample, renderHeadCss, resolveVariables, transformProject, unknownCoverage };
1313
+ export { COMPATIBILITY_PROVENANCE, EMAIL_CLIENT_MATRIX, ESSENTIAL_CONSTRUCTS, TAILWIND_VERSION, TEMPLATE_NAME_PATTERN, applyStylesheet, checkEmailCompatibility, checkStaticProfile, checkTemplateCompatibility, classListOf, compileSelector, compileTailwind, compileTemplate, discoverClassCandidates, downlevelColors, downlevelMediaQuery, finalizeValue, foldCalc, formatColor, hasBlockingCompatibility, introducedProfileErrors, matchesSelector, parseBrandSpecifier, parseColor, parseStylesheet, projectBrandSpecifier, renderBrandFooter, renderBrandSample, renderHeadCss, resolveVariables, transformProject, unknownCoverage };
1311
1314
 
1312
1315
  //# sourceMappingURL=compiler.js.map