@samva/markup 0.15.0 → 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.
- package/AGENTS.md +7 -2
- package/CHANGELOG.md +20 -0
- package/dist/assets-BSpgZOSR.js +61 -0
- package/dist/assets-BSuNT9gr.d.ts +39 -0
- package/dist/brand-Ci05p37c.js +132 -0
- package/dist/brand-footer-ClRtWnIc.d.ts +40 -0
- package/dist/brand-footer-DXVGsKE5.js +746 -0
- package/dist/{assets-tISHaQNF.d.ts → brand-jFV6YtSV.d.ts} +2 -40
- package/dist/brand-sample.d.ts +4 -0
- package/dist/brand-sample.js +2 -0
- package/dist/brand.d.ts +2 -1
- package/dist/brand.js +2 -1
- package/dist/{ir-DNL6H7jA.js → cascade-DoLWZ66g.js} +1 -5
- package/dist/compiler.d.ts +5 -38
- package/dist/compiler.js +9 -743
- package/dist/{diagnostic-codes-UXwOO3A-.js → diagnostic-codes-8XDLR9ef.js} +1 -36
- package/dist/diagnostic-model-DJtqbETd.js +36 -0
- package/dist/diagnostics.js +2 -1
- package/dist/edit.js +2 -2
- package/dist/ir-KJLFW08u.js +5 -0
- package/dist/{profile-AatO_SvP.js → profile-BsY4dkan.js} +10 -105
- package/dist/{render-DpYouYYG.js → render-CVnXZMyK.js} +1 -1
- package/dist/{render-BdvewoQt.d.ts → render-CuZej25A.d.ts} +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +2 -2
- package/dist/theme-imports-u_P0ygvQ.js +102 -0
- package/package.json +5 -1
- package/dist/brand-nreeKWxF.js +0 -200
|
@@ -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 {
|
|
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 };
|
package/dist/diagnostics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
4
|
-
import { c as WHATSAPP_HEADER_TYPES, n as introducedProfileErrors, o as SMS_CATEGORIES, s as WHATSAPP_CATEGORIES, t as checkStaticProfile } from "./profile-
|
|
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";
|
|
@@ -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 {
|
|
9
|
-
import { i as
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
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-
|
|
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
|
|
3
|
-
import {
|
|
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.
|
|
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
|
},
|
package/dist/brand-nreeKWxF.js
DELETED
|
@@ -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 };
|