@samva/markup 0.15.1 → 0.16.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.
@@ -1,38 +1,3 @@
1
- //#region src/diagnostic-model.ts
2
- /** Newline-offset line map. Build once per source, query with `lineColumn`. */
3
- const buildLineMap = (source) => {
4
- const lineStarts = [0];
5
- for (let index = 0; index < source.length; index++) {
6
- const code = source.charCodeAt(index);
7
- if (code === 10 || code === 13 && source.charCodeAt(index + 1) !== 10 || code === 8232 || code === 8233) lineStarts.push(index + 1);
8
- }
9
- return lineStarts;
10
- };
11
- /** 1-based line/column for an absolute offset, via binary search. */
12
- const lineColumn = (lineMap, offset) => {
13
- let low = 0;
14
- let high = lineMap.length - 1;
15
- while (low < high) {
16
- const middle = low + high + 1 >> 1;
17
- if (lineMap[middle] <= offset) low = middle;
18
- else high = middle - 1;
19
- }
20
- return {
21
- line: low + 1,
22
- column: offset - lineMap[low] + 1
23
- };
24
- };
25
- /** Build one formatter for a batch of diagnostics against the same source. */
26
- const createDiagnosticFormatter = (source) => {
27
- const lineMap = buildLineMap(source);
28
- return (diagnostic) => {
29
- const { line, column } = lineColumn(lineMap, diagnostic.span.start);
30
- return `line ${line}:${column} ${diagnostic.message}`;
31
- };
32
- };
33
- /** "line 12:8 unknown class ..." — the shape agents iterate on. */
34
- const formatDiagnostic = (source, diagnostic) => createDiagnosticFormatter(source)(diagnostic);
35
- //#endregion
36
1
  //#region src/diagnostic-codes.ts
37
2
  const codes = (table) => table;
38
3
  const DIAGNOSTIC_CODES = codes({
@@ -466,4 +431,4 @@ const DIAGNOSTIC_CODE_FAMILIES = { "caniemail/": {
466
431
  fix: "Use the alternative the message names, such as an email component instead of display: flex."
467
432
  } };
468
433
  //#endregion
469
- export { createDiagnosticFormatter as a, buildLineMap as i, DIAGNOSTIC_CODE_FAMILIES as n, formatDiagnostic as o, hasUpgradeRecipe as r, lineColumn as s, DIAGNOSTIC_CODES as t };
434
+ export { DIAGNOSTIC_CODE_FAMILIES as n, hasUpgradeRecipe as r, DIAGNOSTIC_CODES as t };
@@ -0,0 +1,36 @@
1
+ //#region src/diagnostic-model.ts
2
+ /** Newline-offset line map. Build once per source, query with `lineColumn`. */
3
+ const buildLineMap = (source) => {
4
+ const lineStarts = [0];
5
+ for (let index = 0; index < source.length; index++) {
6
+ const code = source.charCodeAt(index);
7
+ if (code === 10 || code === 13 && source.charCodeAt(index + 1) !== 10 || code === 8232 || code === 8233) lineStarts.push(index + 1);
8
+ }
9
+ return lineStarts;
10
+ };
11
+ /** 1-based line/column for an absolute offset, via binary search. */
12
+ const lineColumn = (lineMap, offset) => {
13
+ let low = 0;
14
+ let high = lineMap.length - 1;
15
+ while (low < high) {
16
+ const middle = low + high + 1 >> 1;
17
+ if (lineMap[middle] <= offset) low = middle;
18
+ else high = middle - 1;
19
+ }
20
+ return {
21
+ line: low + 1,
22
+ column: offset - lineMap[low] + 1
23
+ };
24
+ };
25
+ /** Build one formatter for a batch of diagnostics against the same source. */
26
+ const createDiagnosticFormatter = (source) => {
27
+ const lineMap = buildLineMap(source);
28
+ return (diagnostic) => {
29
+ const { line, column } = lineColumn(lineMap, diagnostic.span.start);
30
+ return `line ${line}:${column} ${diagnostic.message}`;
31
+ };
32
+ };
33
+ /** "line 12:8 unknown class ..." — the shape agents iterate on. */
34
+ const formatDiagnostic = (source, diagnostic) => createDiagnosticFormatter(source)(diagnostic);
35
+ //#endregion
36
+ export { lineColumn as i, createDiagnosticFormatter as n, formatDiagnostic as r, buildLineMap as t };
@@ -1,3 +1,4 @@
1
- import { a as createDiagnosticFormatter, i as buildLineMap, n as DIAGNOSTIC_CODE_FAMILIES, o as formatDiagnostic, r as hasUpgradeRecipe, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-UXwOO3A-.js";
1
+ import { i as lineColumn, n as createDiagnosticFormatter, r as formatDiagnostic, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
2
2
  import { n as formatEmailDiagnostic, r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
3
+ import { n as DIAGNOSTIC_CODE_FAMILIES, r as hasUpgradeRecipe, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-8XDLR9ef.js";
3
4
  export { DIAGNOSTIC_CODES, DIAGNOSTIC_CODE_FAMILIES, EmailCompileError, buildLineMap, createDiagnosticFormatter, formatDiagnostic, formatEmailDiagnostic, hasBlockingDiagnostic, hasUpgradeRecipe, lineColumn };
package/dist/edit.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as isObject } from "./guards-eV1qLbf2.js";
2
2
  import { d as completeJsxSourceLocation, u as JSX_SOURCE_LOCATION_PROP } from "./jsx-runtime-CBlexoeR.js";
3
- import { i as buildLineMap } from "./diagnostic-codes-UXwOO3A-.js";
4
- import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-AatO_SvP.js";
3
+ import { t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
4
+ import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-BsY4dkan.js";
5
5
  import { parse } from "@babel/parser";
6
6
  //#region src/source-edit.ts
7
7
  const node = (value) => isObject(value) && "type" in value && typeof value.type === "string" && "start" in value && typeof value.start === "number" && "end" in value && typeof value.end === "number";
@@ -0,0 +1,5 @@
1
+ //#region src/ir.ts
2
+ /** The IR version this package writes. A publication pins the version it was built with. */
3
+ const SML_IR_VERSION = 1;
4
+ //#endregion
5
+ export { SML_IR_VERSION as t };
@@ -5,109 +5,14 @@ import { D as samvaBrandPlugin, E as reservedBrandAttributes, O as BRAND_FOOTER_
5
5
  import { FORMATTER_NAMES } from "./fmt.js";
6
6
  import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
7
7
  import { a as Email, d as equalColumnWidth, i as Divider, l as Section, n as Column, o as Image, p as PRIMITIVE_CLASSES, r as Columns, s as Link, t as Button, u as Spacer } from "./components-3XHjYD3i.js";
8
- import { i as renderDarkRules, n as DARK_CLASS_PREFIX, r as applyStylesheet } from "./ir-DNL6H7jA.js";
9
- import { i as buildLineMap, s as lineColumn, t as DIAGNOSTIC_CODES } from "./diagnostic-codes-UXwOO3A-.js";
10
- import { l as isAssetPath, n as brandComponents } from "./brand-nreeKWxF.js";
11
- import { parse } from "css-tree";
12
- import { parse as parse$1 } from "@babel/parser";
13
- //#region src/email/theme-imports.ts
14
- /** The directory part of a project path, with no trailing slash (`""` at the root). */
15
- const projectDirectory = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
16
- /** A relative or root-absolute stylesheet reference, resolved from a project directory. */
17
- const resolveFromDirectory = (directory, reference) => {
18
- const segments = reference.startsWith("/") || directory === "" ? [] : directory.split("/");
19
- for (const segment of reference.split("/")) {
20
- if (segment === "" || segment === ".") continue;
21
- if (segment === "..") {
22
- if (segments.length === 0) return void 0;
23
- segments.pop();
24
- } else segments.push(segment);
25
- }
26
- return segments.length === 0 ? void 0 : segments.join("/");
27
- };
28
- const importTarget = (node) => {
29
- if (node.type !== "Atrule" || node.prelude === null || node.prelude.type !== "AtrulePrelude") return { extra: true };
30
- const parts = [...node.prelude.children].filter((part) => part.type !== "WhiteSpace");
31
- const first = parts[0];
32
- const target = first?.type === "String" ? first.value : first?.type === "Url" ? first.value : void 0;
33
- return target === void 0 ? { extra: true } : {
34
- target,
35
- extra: parts.length > 1
36
- };
37
- };
38
- /**
39
- * Walk the theme's `@import` graph. Project `.css` files resolve from the
40
- * importing file's directory; the plugin's brand specifier, bare or with a
41
- * slug, is recorded for the brand the build supplies. Anything else is an
42
- * error.
43
- */
44
- const resolveThemeImports = (themePath, themeCss, files, brandPlugin) => {
45
- const brand = JSON.stringify(brandPlugin.specifier);
46
- const stylesheets = /* @__PURE__ */ new Map([[themePath, themeCss]]);
47
- const brandSpecifiers = /* @__PURE__ */ new Set();
48
- const diagnostics = [];
49
- const visit = (path, source, chain) => {
50
- const lineMap = buildLineMap(source);
51
- const ast = parse(source, {
52
- positions: true,
53
- parseValue: false,
54
- parseRulePrelude: false
55
- });
56
- if (ast.type !== "StyleSheet") return;
57
- for (const node of ast.children) {
58
- if (node.type !== "Atrule" || node.name.toLowerCase() !== "import") continue;
59
- const position = lineColumn(lineMap, node.loc?.start.offset ?? 0);
60
- const origins = [{
61
- fileName: path,
62
- lineNumber: position.line,
63
- columnNumber: position.column
64
- }];
65
- const refuse = (message) => diagnostics.push({
66
- code: "css-import-unresolved",
67
- severity: "error",
68
- message,
69
- origins
70
- });
71
- const { target, extra } = importTarget(node);
72
- if (target === void 0) {
73
- refuse(`${path}: @import needs a quoted stylesheet path or ${brand}.`);
74
- continue;
75
- }
76
- if (extra) {
77
- refuse(`${path}: @import ${JSON.stringify(target)} takes no layer, supports or media conditions; the import order is the layering.`);
78
- continue;
79
- }
80
- if (parseBrandSpecifier(target, brandPlugin) !== void 0) {
81
- brandSpecifiers.add(target);
82
- continue;
83
- }
84
- if (!/^\.{0,2}\//.test(target) || !/\.css$/i.test(target.replace(/[?#].*$/, ""))) {
85
- refuse(`${path}: @import ${JSON.stringify(target)} is not a project stylesheet or ${brand}. The compiler supplies Tailwind itself; import project .css files by relative path.`);
86
- continue;
87
- }
88
- const resolved = resolveFromDirectory(projectDirectory(path), target.replace(/[?#].*$/, ""));
89
- const content = resolved === void 0 ? void 0 : files[resolved];
90
- if (resolved === void 0 || typeof content !== "string") {
91
- refuse(`${path}: @import ${JSON.stringify(target)} names no stylesheet in the project.`);
92
- continue;
93
- }
94
- if (chain.includes(resolved)) {
95
- refuse(`${path}: @import ${JSON.stringify(target)} imports itself through ${chain.join(" → ")}.`);
96
- continue;
97
- }
98
- if (stylesheets.has(resolved)) continue;
99
- stylesheets.set(resolved, content);
100
- visit(resolved, content, [...chain, resolved]);
101
- }
102
- };
103
- visit(themePath, themeCss, [themePath]);
104
- return {
105
- stylesheets,
106
- brandSpecifiers: [...brandSpecifiers].sort(),
107
- diagnostics
108
- };
109
- };
110
- //#endregion
8
+ import { n as applyStylesheet, r as renderDarkRules, t as DARK_CLASS_PREFIX } from "./cascade-DoLWZ66g.js";
9
+ import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
10
+ import { n as resolveFromDirectory, t as projectDirectory } from "./theme-imports-u_P0ygvQ.js";
11
+ import { a as isAssetPath } from "./assets-BSpgZOSR.js";
12
+ import { n as brandComponents } from "./brand-Ci05p37c.js";
13
+ import { t as DIAGNOSTIC_CODES } from "./diagnostic-codes-8XDLR9ef.js";
14
+ import "./ir-KJLFW08u.js";
15
+ import { parse } from "@babel/parser";
111
16
  //#region src/sml/diagnostics.ts
112
17
  /** Collects the findings of one compile. Every finding carries a code, a location and a fix. */
113
18
  var Findings = class {
@@ -229,7 +134,7 @@ const parseSource = (path, source) => {
229
134
  const position = (offset) => lineColumn(lines, offset);
230
135
  let program;
231
136
  try {
232
- program = parse$1(source, {
137
+ program = parse(source, {
233
138
  plugins: ["typescript", "jsx"],
234
139
  sourceType: "module"
235
140
  }).program;
@@ -3933,4 +3838,4 @@ const introducedProfileErrors = (before, after) => {
3933
3838
  return added;
3934
3839
  };
3935
3840
  //#endregion
3936
- export { lowerProject as a, WHATSAPP_HEADER_TYPES as c, resolveFromDirectory as d, resolveThemeImports as f, completed as i, Findings as l, introducedProfileErrors as n, SMS_CATEGORIES as o, loadProject as r, WHATSAPP_CATEGORIES as s, checkStaticProfile as t, projectDirectory as u };
3841
+ export { lowerProject as a, WHATSAPP_HEADER_TYPES as c, completed as i, Findings as l, introducedProfileErrors as n, SMS_CATEGORIES as o, loadProject as r, WHATSAPP_CATEGORIES as s, checkStaticProfile as t };
@@ -1,7 +1,7 @@
1
1
  import { S as emailElement, o as serializeEmailHtml } from "./html-BvRFepeO.js";
2
2
  import { i as drainEmailDiagnostics, t as EmailNode } from "./jsx-runtime-CBlexoeR.js";
3
3
  import { f as preheaderNode } from "./components-3XHjYD3i.js";
4
- import { i as renderDarkRules, r as applyStylesheet } from "./ir-DNL6H7jA.js";
4
+ import { n as applyStylesheet, r as renderDarkRules } from "./cascade-DoLWZ66g.js";
5
5
  import { r as hasBlockingDiagnostic, t as EmailCompileError } from "./diagnostics-C0Tr8-eQ.js";
6
6
  //#region src/email/text.ts
7
7
  const PARAGRAPH = "\n\n";
@@ -1,8 +1,8 @@
1
- import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
2
1
  import { r as JsxSourceLocation } from "./source-locations-C8wCsiAy.js";
3
2
  import { r as EmailDiagnostic } from "./diagnostics-CUYlQ_6x.js";
4
3
  import { r as EmailNode } from "./jsx-runtime-DC7xG0aA.js";
5
4
  import { n as EmailContent } from "./template-B-UDXK8a.js";
5
+ import { t as BrandPlugin } from "./brand-plugin-CHhcei-j.js";
6
6
  import "css-tree";
7
7
  //#region src/ir.d.ts
8
8
  /** The IR version this package writes. A publication pins the version it was built with. */
package/dist/render.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as isUrlAttribute, S as emailElement, _ as EmailGlobalProps, b as EmailStyle, g as EmailElementSpec, h as EmailAttributeSpec, m as EMAIL_GLOBAL_ATTRIBUTES, p as EMAIL_ELEMENTS, r as EmailNode, v as EmailIntrinsicElements, x as EmailTag, y as EmailLayout } from "./jsx-runtime-DC7xG0aA.js";
2
- import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-BdvewoQt.js";
2
+ import { $ as IrWhatsApp, A as emailAuthoringTarget, B as IrElement, D as EmittedPosition, E as EmailElementSelection, F as emailSelections, G as IrPredicate, H as IrFormatter, I as renderIrPreview, J as IrRawPart, K as IrPreheader, L as IrArithmetic, M as emailSelectionAncestry, N as emailSelectionIndex, O as INSTANCE_PATH_ATTRIBUTE, P as emailSelectionSiblings, Q as IrValue, R as IrComparison, U as IrIf, V as IrEmail, W as IrNode, X as IrSource, Y as IrSms, Z as IrStyle, a as renderEmailContent, at as RenderedPosition, c as checkAttribute, ct as TemplateIr, d as escapeText, et as IrWhatsAppButton, f as kebab, i as compileEmail, it as RenderedIrWhatsApp, j as emailDefinitionInstances, k as IrPreview, l as declarationValue, m as serializeStyle, n as EmailCompileOutput, nt as RenderOptions, o as EmailHtmlOutput, ot as RenderedWhatsAppButton, p as serializeEmailHtml, q as IrRaw, r as RenderedEmail, rt as RenderedIrEmail, s as SerializeEmailOptions, st as SML_IR_VERSION, t as EMAIL_DOCTYPE, tt as IrWhatsAppButtonNode, u as escapeAttribute, z as IrEach } from "./render-CuZej25A.js";
3
3
  //#region src/render-ir.d.ts
4
4
  interface IrRenderDiagnostic {
5
5
  readonly code: string;
package/dist/render.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { C as isUrlAttribute, S as emailElement, _ as renderIr, a as kebab, b as EMAIL_ELEMENTS, c as INSTANCE_PATH_ATTRIBUTE, d as emailSelectionAncestry, f as emailSelectionIndex, g as IrRenderError, h as renderIrPreview, i as escapeText, l as emailAuthoringTarget, m as emailSelections, n as declarationValue, o as serializeEmailHtml, p as emailSelectionSiblings, r as escapeAttribute, s as serializeStyle, t as checkAttribute, u as emailDefinitionInstances, v as renderIrSms, x as EMAIL_GLOBAL_ATTRIBUTES, y as renderIrWhatsApp } from "./html-BvRFepeO.js";
2
- import { t as SML_IR_VERSION } from "./ir-DNL6H7jA.js";
3
- import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-DpYouYYG.js";
2
+ import { i as deriveEmailText, n as compileEmail, r as renderEmailContent, t as EMAIL_DOCTYPE } from "./render-CVnXZMyK.js";
3
+ import { t as SML_IR_VERSION } from "./ir-KJLFW08u.js";
4
4
  //#region src/sms-segments.ts
5
5
  const GSM7_EXTENDED = /* @__PURE__ */ new Set([
6
6
  "[",
@@ -0,0 +1,102 @@
1
+ import { T as parseBrandSpecifier } from "./html-BvRFepeO.js";
2
+ import { i as lineColumn, t as buildLineMap } from "./diagnostic-model-DJtqbETd.js";
3
+ import { parse } from "css-tree";
4
+ //#region src/email/theme-imports.ts
5
+ /** The directory part of a project path, with no trailing slash (`""` at the root). */
6
+ const projectDirectory = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
7
+ /** A relative or root-absolute stylesheet reference, resolved from a project directory. */
8
+ const resolveFromDirectory = (directory, reference) => {
9
+ const segments = reference.startsWith("/") || directory === "" ? [] : directory.split("/");
10
+ for (const segment of reference.split("/")) {
11
+ if (segment === "" || segment === ".") continue;
12
+ if (segment === "..") {
13
+ if (segments.length === 0) return void 0;
14
+ segments.pop();
15
+ } else segments.push(segment);
16
+ }
17
+ return segments.length === 0 ? void 0 : segments.join("/");
18
+ };
19
+ const importTarget = (node) => {
20
+ if (node.type !== "Atrule" || node.prelude === null || node.prelude.type !== "AtrulePrelude") return { extra: true };
21
+ const parts = [...node.prelude.children].filter((part) => part.type !== "WhiteSpace");
22
+ const first = parts[0];
23
+ const target = first?.type === "String" ? first.value : first?.type === "Url" ? first.value : void 0;
24
+ return target === void 0 ? { extra: true } : {
25
+ target,
26
+ extra: parts.length > 1
27
+ };
28
+ };
29
+ /**
30
+ * Walk the theme's `@import` graph. Project `.css` files resolve from the
31
+ * importing file's directory; the plugin's brand specifier, bare or with a
32
+ * slug, is recorded for the brand the build supplies. Anything else is an
33
+ * error.
34
+ */
35
+ const resolveThemeImports = (themePath, themeCss, files, brandPlugin) => {
36
+ const brand = JSON.stringify(brandPlugin.specifier);
37
+ const stylesheets = /* @__PURE__ */ new Map([[themePath, themeCss]]);
38
+ const brandSpecifiers = /* @__PURE__ */ new Set();
39
+ const diagnostics = [];
40
+ const visit = (path, source, chain) => {
41
+ const lineMap = buildLineMap(source);
42
+ const ast = parse(source, {
43
+ positions: true,
44
+ parseValue: false,
45
+ parseRulePrelude: false
46
+ });
47
+ if (ast.type !== "StyleSheet") return;
48
+ for (const node of ast.children) {
49
+ if (node.type !== "Atrule" || node.name.toLowerCase() !== "import") continue;
50
+ const position = lineColumn(lineMap, node.loc?.start.offset ?? 0);
51
+ const origins = [{
52
+ fileName: path,
53
+ lineNumber: position.line,
54
+ columnNumber: position.column
55
+ }];
56
+ const refuse = (message) => diagnostics.push({
57
+ code: "css-import-unresolved",
58
+ severity: "error",
59
+ message,
60
+ origins
61
+ });
62
+ const { target, extra } = importTarget(node);
63
+ if (target === void 0) {
64
+ refuse(`${path}: @import needs a quoted stylesheet path or ${brand}.`);
65
+ continue;
66
+ }
67
+ if (extra) {
68
+ refuse(`${path}: @import ${JSON.stringify(target)} takes no layer, supports or media conditions; the import order is the layering.`);
69
+ continue;
70
+ }
71
+ if (parseBrandSpecifier(target, brandPlugin) !== void 0) {
72
+ brandSpecifiers.add(target);
73
+ continue;
74
+ }
75
+ if (!/^\.{0,2}\//.test(target) || !/\.css$/i.test(target.replace(/[?#].*$/, ""))) {
76
+ refuse(`${path}: @import ${JSON.stringify(target)} is not a project stylesheet or ${brand}. The compiler supplies Tailwind itself; import project .css files by relative path.`);
77
+ continue;
78
+ }
79
+ const resolved = resolveFromDirectory(projectDirectory(path), target.replace(/[?#].*$/, ""));
80
+ const content = resolved === void 0 ? void 0 : files[resolved];
81
+ if (resolved === void 0 || typeof content !== "string") {
82
+ refuse(`${path}: @import ${JSON.stringify(target)} names no stylesheet in the project.`);
83
+ continue;
84
+ }
85
+ if (chain.includes(resolved)) {
86
+ refuse(`${path}: @import ${JSON.stringify(target)} imports itself through ${chain.join(" → ")}.`);
87
+ continue;
88
+ }
89
+ if (stylesheets.has(resolved)) continue;
90
+ stylesheets.set(resolved, content);
91
+ visit(resolved, content, [...chain, resolved]);
92
+ }
93
+ };
94
+ visit(themePath, themeCss, [themePath]);
95
+ return {
96
+ stylesheets,
97
+ brandSpecifiers: [...brandSpecifiers].sort(),
98
+ diagnostics
99
+ };
100
+ };
101
+ //#endregion
102
+ export { resolveFromDirectory as n, resolveThemeImports as r, projectDirectory as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samva/markup",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Type-safe authoring and compilation primitives for Samva templates",
5
5
  "keywords": [
6
6
  "email",
@@ -89,6 +89,10 @@
89
89
  "types": "./dist/brand.d.ts",
90
90
  "import": "./dist/brand.js"
91
91
  },
92
+ "./brand-sample": {
93
+ "types": "./dist/brand-sample.d.ts",
94
+ "import": "./dist/brand-sample.js"
95
+ },
92
96
  "./samva-brand": {
93
97
  "types": "./samva-brand.d.ts"
94
98
  },
@@ -1,200 +0,0 @@
1
- import { D as samvaBrandPlugin, O as BRAND_FOOTER_MARKER, k as UNSUBSCRIBE_LINK, r as escapeAttribute } from "./html-BvRFepeO.js";
2
- import { a as jsx, n as Fragment } from "./jsx-runtime-CBlexoeR.js";
3
- //#region src/email/assets.ts
4
- /** Files a template may import, and what they are served as. */
5
- const ASSET_CONTENT_TYPES = {
6
- ".png": "image/png",
7
- ".jpg": "image/jpeg",
8
- ".jpeg": "image/jpeg",
9
- ".gif": "image/gif",
10
- ".webp": "image/webp",
11
- ".avif": "image/avif",
12
- ".svg": "image/svg+xml",
13
- ".ico": "image/x-icon",
14
- ".woff": "font/woff",
15
- ".woff2": "font/woff2",
16
- ".ttf": "font/ttf",
17
- ".otf": "font/otf",
18
- ".pdf": "application/pdf",
19
- ".txt": "text/plain"
20
- };
21
- const extensionOf = (path) => {
22
- const dot = path.lastIndexOf(".");
23
- return dot < 0 ? "" : path.slice(dot).toLowerCase();
24
- };
25
- /** True when the compiler turns this import into a content-addressed asset. */
26
- const isAssetPath = (path) => Object.hasOwn(ASSET_CONTENT_TYPES, extensionOf(path));
27
- const hex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
28
- /** Address one file by its content. */
29
- const assetEntry = async (path, bytes) => {
30
- const extension = extensionOf(path);
31
- const view = new Uint8Array(bytes.byteLength);
32
- view.set(bytes);
33
- const digest = hex(await crypto.subtle.digest("SHA-256", view));
34
- return {
35
- path,
36
- digest,
37
- bytes: bytes.byteLength,
38
- contentType: ASSET_CONTENT_TYPES[extension] ?? "application/octet-stream",
39
- fileName: `${digest}${extension}`
40
- };
41
- };
42
- /**
43
- * The URL an asset has under a base. The base is a build input: publication
44
- * passes its hosted origin, and a local export passes whatever it will serve
45
- * the exported files from.
46
- */
47
- const assetUrl = (entry, base) => `${base.replace(/\/+$/, "")}/${entry.fileName}`;
48
- const EXTERNAL_IMAGE = /^https:\/\//i;
49
- /** Record, but never fetch, a URL the template points at. */
50
- const isExternalAsset = (reference) => EXTERNAL_IMAGE.test(reference);
51
- /**
52
- * Reject an asset base that would produce URLs a client will not load. The
53
- * relative forms are the ones a page on disk resolves against its own
54
- * directory, which is what a local export writes.
55
- */
56
- const checkAssetBase = (base) => /^(https:\/\/|\.{1,2}\/|\/)/i.test(base) ? [] : [{
57
- code: "invalid-asset-base",
58
- severity: "error",
59
- message: `Asset base ${JSON.stringify(base)} must be an https URL, an absolute path, or a relative path for a local export.`,
60
- origins: []
61
- }];
62
- //#endregion
63
- //#region src/email/brand.ts
64
- /** Every class the brand components use, compiled for any project that imports them. */
65
- const BRAND_COMPONENT_CLASSES = [
66
- "dark:hidden!",
67
- "hidden",
68
- "dark:block!",
69
- "text-xs",
70
- "text-muted-foreground",
71
- "text-center",
72
- "font-semibold",
73
- "underline"
74
- ];
75
- const addressLines = (address) => [
76
- address.street1,
77
- address.street2,
78
- [
79
- address.city,
80
- address.region,
81
- address.postalCode
82
- ].filter((part) => part.trim() !== "").join(", "),
83
- address.country
84
- ].filter((line) => line !== void 0 && line.trim() !== "");
85
- const classes = (...names) => {
86
- const joined = names.filter((name) => name !== void 0 && name !== "").join(" ");
87
- return joined === "" ? void 0 : joined;
88
- };
89
- const row = (children, props = {}) => jsx("tr", {
90
- ...props,
91
- children: jsx("td", {
92
- style: { padding: "0 0 8px 0" },
93
- children
94
- })
95
- });
96
- /** Bind `BrandLogo` and `BrandFooter` to one brand, marked as the plugin names. */
97
- const brandComponents = (brand, plugin = samvaBrandPlugin) => {
98
- const assets = brand.assets ?? {};
99
- const footer = brand.footer ?? {};
100
- const BrandLogo = ({ width = 120, alt, align, className }) => {
101
- if (assets.logoUrl === void 0) return null;
102
- const label = alt ?? footer.companyName ?? "";
103
- const image = (src, imageClass, imageAlt) => jsx("img", {
104
- src,
105
- alt: imageAlt,
106
- width,
107
- border: 0,
108
- ...align === void 0 ? {} : { align },
109
- className: classes(imageClass, className),
110
- style: {
111
- display: "block",
112
- outline: "none",
113
- border: "none",
114
- height: "auto"
115
- }
116
- });
117
- if (assets.logoDarkUrl === void 0) return image(assets.logoUrl, void 0, label);
118
- return jsx(Fragment, { children: [image(assets.logoUrl, "dark:hidden!", label), jsx("div", {
119
- className: "hidden dark:block!",
120
- style: {
121
- msoHide: "all",
122
- maxHeight: 0,
123
- overflow: "hidden"
124
- },
125
- children: image(assets.logoDarkUrl, void 0, "")
126
- })] });
127
- };
128
- const BrandFooter = ({ unsubscribeLabel = "Unsubscribe", className }) => {
129
- const rows = [];
130
- if (footer.companyName !== void 0 && footer.companyName.trim() !== "") rows.push(row(jsx("span", {
131
- className: "font-semibold",
132
- children: footer.companyName
133
- })));
134
- if (footer.address !== void 0) {
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", {
140
- href: link.url,
141
- className: "text-muted-foreground underline",
142
- children: link.label
143
- })])));
144
- rows.push(row(unsubscribeLink(unsubscribeLabel, plugin), {
145
- [plugin.unsubscribeRowAttribute]: "",
146
- [BRAND_FOOTER_MARKER]: true
147
- }));
148
- return jsx("table", {
149
- role: "presentation",
150
- width: "100%",
151
- border: 0,
152
- cellpadding: 0,
153
- cellspacing: 0,
154
- [plugin.footerAttribute]: "",
155
- [BRAND_FOOTER_MARKER]: true,
156
- children: jsx("tbody", { children: jsx("tr", { children: jsx("td", {
157
- align: "center",
158
- className: classes("text-xs text-muted-foreground text-center", className),
159
- style: { padding: "32px 16px" },
160
- children: jsx("table", {
161
- role: "presentation",
162
- border: 0,
163
- cellpadding: 0,
164
- cellspacing: 0,
165
- children: jsx("tbody", { children: rows })
166
- })
167
- }) }) })
168
- });
169
- };
170
- return {
171
- BrandLogo,
172
- BrandFooter
173
- };
174
- };
175
- /**
176
- * The unsubscribe link. Its href is the placeholder, which no author-written
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.
179
- */
180
- const unsubscribeLink = (label, plugin) => jsx("a", {
181
- href: plugin.unsubscribeUrlPlaceholder,
182
- className: "text-muted-foreground underline",
183
- [UNSUBSCRIBE_LINK]: true,
184
- children: label
185
- });
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)}"`)
190
- });
191
- /**
192
- * Drop the unsubscribe row from a rendered message, for a send that resolved
193
- * no unsubscribe group: there is no link to put there, so the row goes.
194
- */
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")
198
- });
199
- //#endregion
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 };