@samva/markup 0.1.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 +8 -0
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/ast-DAhnAvQN.d.ts +120 -0
- package/dist/ast.d.ts +2 -0
- package/dist/ast.js +159 -0
- package/dist/ast.js.map +1 -0
- package/dist/candidate.d.ts +29 -0
- package/dist/candidate.js +88 -0
- package/dist/candidate.js.map +1 -0
- package/dist/checks-CW0nFj5n.js +58922 -0
- package/dist/checks-CW0nFj5n.js.map +1 -0
- package/dist/checks.d.ts +26 -0
- package/dist/checks.js +2 -0
- package/dist/classes.d.ts +27 -0
- package/dist/classes.js +184 -0
- package/dist/classes.js.map +1 -0
- package/dist/compile-sms.d.ts +28 -0
- package/dist/compile-sms.js +44 -0
- package/dist/compile-sms.js.map +1 -0
- package/dist/compile-whatsapp.d.ts +56 -0
- package/dist/compile-whatsapp.js +186 -0
- package/dist/compile-whatsapp.js.map +1 -0
- package/dist/compile.d.ts +93 -0
- package/dist/compile.js +938 -0
- package/dist/compile.js.map +1 -0
- package/dist/components.d.ts +289 -0
- package/dist/components.js +106 -0
- package/dist/components.js.map +1 -0
- package/dist/conditionals.d.ts +10 -0
- package/dist/conditionals.js +42 -0
- package/dist/conditionals.js.map +1 -0
- package/dist/diagnostics-oveE-laV.d.ts +79 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +62 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/emit.d.ts +50 -0
- package/dist/emit.js +151 -0
- package/dist/emit.js.map +1 -0
- package/dist/footer.d.ts +66 -0
- package/dist/footer.js +90 -0
- package/dist/footer.js.map +1 -0
- package/dist/interpolate.d.ts +27 -0
- package/dist/interpolate.js +61 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/jsx-dev-runtime.d.ts +10 -0
- package/dist/jsx-dev-runtime.js +7 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime-fSxcvlx4.d.ts +57 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +102 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/lexer.d.ts +60 -0
- package/dist/lexer.js +269 -0
- package/dist/lexer.js.map +1 -0
- package/dist/model.d.ts +39 -0
- package/dist/model.js +90 -0
- package/dist/model.js.map +1 -0
- package/dist/ops.d.ts +208 -0
- package/dist/ops.js +546 -0
- package/dist/ops.js.map +1 -0
- package/dist/parser-yzjUOGhc.d.ts +387 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +821 -0
- package/dist/parser.js.map +1 -0
- package/dist/predicate-DTtVtLK7.d.ts +49 -0
- package/dist/predicate.d.ts +2 -0
- package/dist/predicate.js +417 -0
- package/dist/predicate.js.map +1 -0
- package/dist/presets/category-mapping.d.ts +13 -0
- package/dist/presets/category-mapping.js +49 -0
- package/dist/presets/category-mapping.js.map +1 -0
- package/dist/presets/public-gallery.d.ts +490 -0
- package/dist/presets/public-gallery.js +563 -0
- package/dist/presets/public-gallery.js.map +1 -0
- package/dist/presets/public-handoff.d.ts +68 -0
- package/dist/presets/public-handoff.js +105 -0
- package/dist/presets/public-handoff.js.map +1 -0
- package/dist/presets/public-preview-artifacts.d.ts +41 -0
- package/dist/presets/public-preview-artifacts.js +109 -0
- package/dist/presets/public-preview-artifacts.js.map +1 -0
- package/dist/presets/public-preview.d.ts +1168 -0
- package/dist/presets/public-preview.js +1158 -0
- package/dist/presets/public-preview.js.map +1 -0
- package/dist/presets/sources.d.ts +7 -0
- package/dist/presets/sources.js +54 -0
- package/dist/presets/sources.js.map +1 -0
- package/dist/presets.d.ts +350 -0
- package/dist/presets.js +1251 -0
- package/dist/presets.js.map +1 -0
- package/dist/printer.d.ts +6 -0
- package/dist/printer.js +122 -0
- package/dist/printer.js.map +1 -0
- package/dist/registry.d.ts +42 -0
- package/dist/registry.js +646 -0
- package/dist/registry.js.map +1 -0
- package/dist/render-template.d.ts +42 -0
- package/dist/render-template.js +74 -0
- package/dist/render-template.js.map +1 -0
- package/dist/sms-segments-y4FpPilJ.d.ts +13 -0
- package/dist/sms-segments.d.ts +2 -0
- package/dist/sms-segments.js +46 -0
- package/dist/sms-segments.js.map +1 -0
- package/dist/theme-6ApjjiBo.d.ts +116 -0
- package/dist/theme-css.d.ts +24 -0
- package/dist/theme-css.js +509 -0
- package/dist/theme-css.js.map +1 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +95 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.d.ts +6 -0
- package/dist/tokens.js +417 -0
- package/dist/tokens.js.map +1 -0
- package/dist/variables-3NGLpNTB.d.ts +77 -0
- package/dist/variables.d.ts +2 -0
- package/dist/variables.js +141 -0
- package/dist/variables.js.map +1 -0
- package/dist/version-BITr58kk.js +96 -0
- package/dist/version-BITr58kk.js.map +1 -0
- package/generated/sml-agent-contract.json +917 -0
- package/generated/sml-email-agent-contract.json +790 -0
- package/generated/sml-email-language-reference.md +160 -0
- package/generated/sml-language-reference.md +220 -0
- package/package.json +203 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { evaluatePredicate, parsePredicate } from "./predicate.js";
|
|
2
|
+
import { Result } from "effect";
|
|
3
|
+
//#region src/conditionals.ts
|
|
4
|
+
const IF_MARKER = "<!--sml:if:";
|
|
5
|
+
const MARKER_CLOSE = "-->";
|
|
6
|
+
const resolve = (content, variables) => {
|
|
7
|
+
const start = content.indexOf(IF_MARKER);
|
|
8
|
+
if (start === -1) return content;
|
|
9
|
+
const headEnd = content.indexOf(MARKER_CLOSE, start);
|
|
10
|
+
if (headEnd === -1) return content;
|
|
11
|
+
const header = content.slice(start + 11, headEnd);
|
|
12
|
+
const spaceIndex = header.indexOf(" ");
|
|
13
|
+
const id = spaceIndex === -1 ? header : header.slice(0, spaceIndex);
|
|
14
|
+
const predicateSource = spaceIndex === -1 ? "" : header.slice(spaceIndex + 1);
|
|
15
|
+
const elseMarker = `<!--sml:else:${id}-->`;
|
|
16
|
+
const endMarker = `<!--sml:endif:${id}-->`;
|
|
17
|
+
const bodyStart = headEnd + 3;
|
|
18
|
+
const endIndex = content.indexOf(endMarker, bodyStart);
|
|
19
|
+
if (endIndex === -1) return content;
|
|
20
|
+
const elseCandidate = content.indexOf(elseMarker, bodyStart);
|
|
21
|
+
const elseIndex = elseCandidate !== -1 && elseCandidate < endIndex ? elseCandidate : -1;
|
|
22
|
+
const parsed = parsePredicate(predicateSource);
|
|
23
|
+
const condition = Result.isSuccess(parsed) && evaluatePredicate(parsed.success, variables);
|
|
24
|
+
const branchA = content.slice(bodyStart, elseIndex === -1 ? endIndex : elseIndex);
|
|
25
|
+
const branchB = elseIndex === -1 ? "" : content.slice(elseIndex + elseMarker.length, endIndex);
|
|
26
|
+
const selected = resolve(condition ? branchA : branchB, variables).trim();
|
|
27
|
+
const rest = resolve(content.slice(endIndex + endMarker.length), variables);
|
|
28
|
+
return content.slice(0, start) + selected + rest;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Selects <If> branches in a compiled artifact (html or plain-text alt) for
|
|
32
|
+
* one recipient's variables and strips all sml: markers. Marker-free content
|
|
33
|
+
* passes through unchanged.
|
|
34
|
+
*/
|
|
35
|
+
const resolveArtifactConditionals = (content, variables) => {
|
|
36
|
+
if (!content.includes(IF_MARKER)) return content;
|
|
37
|
+
return resolve(content, variables).replace(/\n{3,}/g, "\n\n");
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { resolveArtifactConditionals };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=conditionals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditionals.js","names":[],"sources":["../src/conditionals.ts"],"sourcesContent":["import { Result } from \"effect\";\n\nimport { evaluatePredicate, parsePredicate } from \"./predicate\";\n\n// Send-time / preview branch selection over compiled artifacts. The compiler\n// leaves both branches of every <If> in the artifact behind comment markers:\n//\n// <!--sml:if:n4 plan == 'pro'--> A <!--sml:else:n4--> B <!--sml:endif:n4-->\n//\n// (else segment omitted when the <If> has no <Else>). Node ids are unique per\n// document, so nested regions resolve by exact-id matching — no depth\n// counting. The same markers appear in the plain-text alt, so this runs\n// identically on both artifacts. It must run BEFORE {{}} interpolation: sent\n// mail never contains markers.\n\nconst IF_MARKER = \"<!--sml:if:\";\nconst MARKER_CLOSE = \"-->\";\n\nconst resolve = (content: string, variables: Record<string, unknown>): string => {\n const start = content.indexOf(IF_MARKER);\n if (start === -1) return content;\n const headEnd = content.indexOf(MARKER_CLOSE, start);\n if (headEnd === -1) {\n // Unterminated marker — impossible for compiler-produced artifacts. Leave\n // the region visible rather than guessing at a repair.\n return content;\n }\n const header = content.slice(start + IF_MARKER.length, headEnd);\n const spaceIndex = header.indexOf(\" \");\n const id = spaceIndex === -1 ? header : header.slice(0, spaceIndex);\n const predicateSource = spaceIndex === -1 ? \"\" : header.slice(spaceIndex + 1);\n\n const elseMarker = `<!--sml:else:${id}-->`;\n const endMarker = `<!--sml:endif:${id}-->`;\n const bodyStart = headEnd + MARKER_CLOSE.length;\n const endIndex = content.indexOf(endMarker, bodyStart);\n if (endIndex === -1) {\n // Missing endif — same corrupt-input posture as above.\n return content;\n }\n const elseCandidate = content.indexOf(elseMarker, bodyStart);\n const elseIndex = elseCandidate !== -1 && elseCandidate < endIndex ? elseCandidate : -1;\n\n // An unparsable predicate cannot occur in compiler output (the parser\n // validates and canonicalizes it); resolve it like a missing variable: false.\n const parsed = parsePredicate(predicateSource);\n const condition = Result.isSuccess(parsed) && evaluatePredicate(parsed.success, variables);\n\n const branchA = content.slice(bodyStart, elseIndex === -1 ? endIndex : elseIndex);\n const branchB = elseIndex === -1 ? \"\" : content.slice(elseIndex + elseMarker.length, endIndex);\n // Trim after nested resolution: dropped inner regions leave edge whitespace\n // of their own. Branch boundaries are block-level, so trimming is safe.\n const selected = resolve(condition ? branchA : branchB, variables).trim();\n const rest = resolve(content.slice(endIndex + endMarker.length), variables);\n return content.slice(0, start) + selected + rest;\n};\n\n/**\n * Selects <If> branches in a compiled artifact (html or plain-text alt) for\n * one recipient's variables and strips all sml: markers. Marker-free content\n * passes through unchanged.\n */\nexport const resolveArtifactConditionals = (\n content: string,\n variables: Record<string, unknown>,\n): string => {\n if (!content.includes(IF_MARKER)) return content;\n // Dropped branches leave blank runs behind; both artifacts use at most one\n // blank line legitimately, so deeper runs are always resolution residue.\n return resolve(content, variables).replace(/\\n{3,}/g, \"\\n\\n\");\n};\n"],"mappings":";;;AAeA,MAAM,YAAY;AAClB,MAAM,eAAe;AAErB,MAAM,WAAW,SAAiB,cAA+C;CAC/E,MAAM,QAAQ,QAAQ,QAAQ,SAAS;CACvC,IAAI,UAAU,IAAI,OAAO;CACzB,MAAM,UAAU,QAAQ,QAAQ,cAAc,KAAK;CACnD,IAAI,YAAY,IAGd,OAAO;CAET,MAAM,SAAS,QAAQ,MAAM,QAAQ,IAAkB,OAAO;CAC9D,MAAM,aAAa,OAAO,QAAQ,GAAG;CACrC,MAAM,KAAK,eAAe,KAAK,SAAS,OAAO,MAAM,GAAG,UAAU;CAClE,MAAM,kBAAkB,eAAe,KAAK,KAAK,OAAO,MAAM,aAAa,CAAC;CAE5E,MAAM,aAAa,gBAAgB,GAAG;CACtC,MAAM,YAAY,iBAAiB,GAAG;CACtC,MAAM,YAAY,UAAU;CAC5B,MAAM,WAAW,QAAQ,QAAQ,WAAW,SAAS;CACrD,IAAI,aAAa,IAEf,OAAO;CAET,MAAM,gBAAgB,QAAQ,QAAQ,YAAY,SAAS;CAC3D,MAAM,YAAY,kBAAkB,MAAM,gBAAgB,WAAW,gBAAgB;CAIrF,MAAM,SAAS,eAAe,eAAe;CAC7C,MAAM,YAAY,OAAO,UAAU,MAAM,KAAK,kBAAkB,OAAO,SAAS,SAAS;CAEzF,MAAM,UAAU,QAAQ,MAAM,WAAW,cAAc,KAAK,WAAW,SAAS;CAChF,MAAM,UAAU,cAAc,KAAK,KAAK,QAAQ,MAAM,YAAY,WAAW,QAAQ,QAAQ;CAG7F,MAAM,WAAW,QAAQ,YAAY,UAAU,SAAS,SAAS,CAAC,CAAC,KAAK;CACxE,MAAM,OAAO,QAAQ,QAAQ,MAAM,WAAW,UAAU,MAAM,GAAG,SAAS;CAC1E,OAAO,QAAQ,MAAM,GAAG,KAAK,IAAI,WAAW;AAC9C;;;;;;AAOA,MAAa,+BACX,SACA,cACW;CACX,IAAI,CAAC,QAAQ,SAAS,SAAS,GAAG,OAAO;CAGzC,OAAO,QAAQ,SAAS,SAAS,CAAC,CAAC,QAAQ,WAAW,MAAM;AAC9D"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/diagnostics.d.ts
|
|
3
|
+
declare const SpanSchema: Schema.Struct<{
|
|
4
|
+
readonly start: Schema.Finite;
|
|
5
|
+
readonly end: Schema.Finite;
|
|
6
|
+
}>;
|
|
7
|
+
interface Span extends Schema.Schema.Type<typeof SpanSchema> {}
|
|
8
|
+
declare const DiagnosticRepairSchema: Schema.Struct<{
|
|
9
|
+
readonly kind: Schema.Literals<readonly ["replace-class", "remove-class", "rewrite-node", "use-primitive"]>;
|
|
10
|
+
readonly raw: Schema.optional<Schema.String>;
|
|
11
|
+
readonly suggestions: Schema.$Array<Schema.String>;
|
|
12
|
+
readonly explanation: Schema.String;
|
|
13
|
+
}>;
|
|
14
|
+
interface DiagnosticRepair extends Schema.Schema.Type<typeof DiagnosticRepairSchema> {}
|
|
15
|
+
declare const DiagnosticSchema: Schema.Struct<{
|
|
16
|
+
readonly code: Schema.String;
|
|
17
|
+
readonly message: Schema.String;
|
|
18
|
+
readonly span: Schema.Struct<{
|
|
19
|
+
readonly start: Schema.Finite;
|
|
20
|
+
readonly end: Schema.Finite;
|
|
21
|
+
}>;
|
|
22
|
+
readonly repair: Schema.optional<Schema.Struct<{
|
|
23
|
+
readonly kind: Schema.Literals<readonly ["replace-class", "remove-class", "rewrite-node", "use-primitive"]>;
|
|
24
|
+
readonly raw: Schema.optional<Schema.String>;
|
|
25
|
+
readonly suggestions: Schema.$Array<Schema.String>;
|
|
26
|
+
readonly explanation: Schema.String;
|
|
27
|
+
}>>;
|
|
28
|
+
}>;
|
|
29
|
+
interface Diagnostic extends Schema.Schema.Type<typeof DiagnosticSchema> {}
|
|
30
|
+
declare const SmlParseError_base: Schema.Class<SmlParseError, Schema.TaggedStruct<"SmlParseError", {
|
|
31
|
+
readonly diagnostics: Schema.$Array<Schema.Struct<{
|
|
32
|
+
readonly code: Schema.String;
|
|
33
|
+
readonly message: Schema.String;
|
|
34
|
+
readonly span: Schema.Struct<{
|
|
35
|
+
readonly start: Schema.Finite;
|
|
36
|
+
readonly end: Schema.Finite;
|
|
37
|
+
}>;
|
|
38
|
+
readonly repair: Schema.optional<Schema.Struct<{
|
|
39
|
+
readonly kind: Schema.Literals<readonly ["replace-class", "remove-class", "rewrite-node", "use-primitive"]>;
|
|
40
|
+
readonly raw: Schema.optional<Schema.String>;
|
|
41
|
+
readonly suggestions: Schema.$Array<Schema.String>;
|
|
42
|
+
readonly explanation: Schema.String;
|
|
43
|
+
}>>;
|
|
44
|
+
}>>;
|
|
45
|
+
}>, import("effect/Cause").YieldableError>;
|
|
46
|
+
declare class SmlParseError extends SmlParseError_base {
|
|
47
|
+
get message(): string;
|
|
48
|
+
}
|
|
49
|
+
declare const SmlCompileError_base: Schema.Class<SmlCompileError, Schema.TaggedStruct<"SmlCompileError", {
|
|
50
|
+
readonly diagnostics: Schema.$Array<Schema.Struct<{
|
|
51
|
+
readonly code: Schema.String;
|
|
52
|
+
readonly message: Schema.String;
|
|
53
|
+
readonly span: Schema.Struct<{
|
|
54
|
+
readonly start: Schema.Finite;
|
|
55
|
+
readonly end: Schema.Finite;
|
|
56
|
+
}>;
|
|
57
|
+
readonly repair: Schema.optional<Schema.Struct<{
|
|
58
|
+
readonly kind: Schema.Literals<readonly ["replace-class", "remove-class", "rewrite-node", "use-primitive"]>;
|
|
59
|
+
readonly raw: Schema.optional<Schema.String>;
|
|
60
|
+
readonly suggestions: Schema.$Array<Schema.String>;
|
|
61
|
+
readonly explanation: Schema.String;
|
|
62
|
+
}>>;
|
|
63
|
+
}>>;
|
|
64
|
+
}>, import("effect/Cause").YieldableError>;
|
|
65
|
+
declare class SmlCompileError extends SmlCompileError_base {
|
|
66
|
+
get message(): string;
|
|
67
|
+
}
|
|
68
|
+
/** Newline-offset line map. Build once per source, query with `lineColumn`. */
|
|
69
|
+
declare const buildLineMap: (source: string) => ReadonlyArray<number>;
|
|
70
|
+
/** 1-based line/column for an absolute offset, via binary search. */
|
|
71
|
+
declare const lineColumn: (lineMap: ReadonlyArray<number>, offset: number) => {
|
|
72
|
+
readonly line: number;
|
|
73
|
+
readonly column: number;
|
|
74
|
+
};
|
|
75
|
+
/** "line 12:8 unknown class ..." — the shape agents iterate on. */
|
|
76
|
+
declare const formatDiagnostic: (source: string, diagnostic: Diagnostic) => string;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { SmlCompileError as a, SpanSchema as c, lineColumn as d, DiagnosticSchema as i, buildLineMap as l, DiagnosticRepair as n, SmlParseError as o, DiagnosticRepairSchema as r, Span as s, Diagnostic as t, formatDiagnostic as u };
|
|
79
|
+
//# sourceMappingURL=diagnostics-oveE-laV.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as SmlCompileError, c as SpanSchema, d as lineColumn, i as DiagnosticSchema, l as buildLineMap, n as DiagnosticRepair, o as SmlParseError, r as DiagnosticRepairSchema, s as Span, t as Diagnostic, u as formatDiagnostic } from "./diagnostics-oveE-laV.js";
|
|
2
|
+
export { Diagnostic, DiagnosticRepair, DiagnosticRepairSchema, DiagnosticSchema, SmlCompileError, SmlParseError, Span, SpanSchema, buildLineMap, formatDiagnostic, lineColumn };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/diagnostics.ts
|
|
3
|
+
const SpanSchema = Schema.Struct({
|
|
4
|
+
start: Schema.Finite,
|
|
5
|
+
end: Schema.Finite
|
|
6
|
+
});
|
|
7
|
+
const DiagnosticRepairSchema = Schema.Struct({
|
|
8
|
+
kind: Schema.Literals([
|
|
9
|
+
"replace-class",
|
|
10
|
+
"remove-class",
|
|
11
|
+
"rewrite-node",
|
|
12
|
+
"use-primitive"
|
|
13
|
+
]),
|
|
14
|
+
raw: Schema.optional(Schema.String),
|
|
15
|
+
suggestions: Schema.Array(Schema.String),
|
|
16
|
+
explanation: Schema.String
|
|
17
|
+
});
|
|
18
|
+
const DiagnosticSchema = Schema.Struct({
|
|
19
|
+
code: Schema.String,
|
|
20
|
+
message: Schema.String,
|
|
21
|
+
span: SpanSchema,
|
|
22
|
+
repair: Schema.optional(DiagnosticRepairSchema)
|
|
23
|
+
});
|
|
24
|
+
var SmlParseError = class extends Schema.TaggedErrorClass()("SmlParseError", { diagnostics: Schema.Array(DiagnosticSchema) }) {
|
|
25
|
+
get message() {
|
|
26
|
+
return this.diagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var SmlCompileError = class extends Schema.TaggedErrorClass()("SmlCompileError", { diagnostics: Schema.Array(DiagnosticSchema) }) {
|
|
30
|
+
get message() {
|
|
31
|
+
return this.diagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** Newline-offset line map. Build once per source, query with `lineColumn`. */
|
|
35
|
+
const buildLineMap = (source) => {
|
|
36
|
+
const lineStarts = [0];
|
|
37
|
+
for (let index = 0; index < source.length; index++) if (source.charCodeAt(index) === 10) lineStarts.push(index + 1);
|
|
38
|
+
return lineStarts;
|
|
39
|
+
};
|
|
40
|
+
/** 1-based line/column for an absolute offset, via binary search. */
|
|
41
|
+
const lineColumn = (lineMap, offset) => {
|
|
42
|
+
let low = 0;
|
|
43
|
+
let high = lineMap.length - 1;
|
|
44
|
+
while (low < high) {
|
|
45
|
+
const middle = low + high + 1 >> 1;
|
|
46
|
+
if (lineMap[middle] <= offset) low = middle;
|
|
47
|
+
else high = middle - 1;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
line: low + 1,
|
|
51
|
+
column: offset - lineMap[low] + 1
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** "line 12:8 unknown class ..." — the shape agents iterate on. */
|
|
55
|
+
const formatDiagnostic = (source, diagnostic) => {
|
|
56
|
+
const { line, column } = lineColumn(buildLineMap(source), diagnostic.span.start);
|
|
57
|
+
return `line ${line}:${column} ${diagnostic.message}`;
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { DiagnosticRepairSchema, DiagnosticSchema, SmlCompileError, SmlParseError, SpanSchema, buildLineMap, formatDiagnostic, lineColumn };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","names":[],"sources":["../src/diagnostics.ts"],"sourcesContent":["import { Schema } from \"effect\";\n\n// Every SML diagnostic carries a source span and a message that names the fix.\n// Spans are absolute character offsets into the source; line/column rendering\n// is derived on demand via a newline-offset line map + binary search (the same\n// model as the TypeScript scanner), so tokens and nodes stay (start, end) pairs.\n\nexport const SpanSchema = Schema.Struct({\n start: Schema.Finite,\n end: Schema.Finite,\n});\n\nexport interface Span extends Schema.Schema.Type<typeof SpanSchema> {}\n\nexport const DiagnosticRepairSchema = Schema.Struct({\n kind: Schema.Literals([\"replace-class\", \"remove-class\", \"rewrite-node\", \"use-primitive\"]),\n raw: Schema.optional(Schema.String),\n suggestions: Schema.Array(Schema.String),\n explanation: Schema.String,\n});\n\nexport interface DiagnosticRepair extends Schema.Schema.Type<typeof DiagnosticRepairSchema> {}\n\nexport const DiagnosticSchema = Schema.Struct({\n code: Schema.String,\n message: Schema.String,\n span: SpanSchema,\n repair: Schema.optional(DiagnosticRepairSchema),\n});\n\nexport interface Diagnostic extends Schema.Schema.Type<typeof DiagnosticSchema> {}\n\nexport class SmlParseError extends Schema.TaggedErrorClass<SmlParseError>()(\"SmlParseError\", {\n diagnostics: Schema.Array(DiagnosticSchema),\n}) {\n override get message() {\n return this.diagnostics.map((diagnostic) => diagnostic.message).join(\"\\n\");\n }\n}\n\nexport class SmlCompileError extends Schema.TaggedErrorClass<SmlCompileError>()(\"SmlCompileError\", {\n diagnostics: Schema.Array(DiagnosticSchema),\n}) {\n override get message() {\n return this.diagnostics.map((diagnostic) => diagnostic.message).join(\"\\n\");\n }\n}\n\n/** Newline-offset line map. Build once per source, query with `lineColumn`. */\nexport const buildLineMap = (source: string): ReadonlyArray<number> => {\n const lineStarts = [0];\n for (let index = 0; index < source.length; index++) {\n if (source.charCodeAt(index) === 10 /* \\n */) lineStarts.push(index + 1);\n }\n return lineStarts;\n};\n\n/** 1-based line/column for an absolute offset, via binary search. */\nexport const lineColumn = (\n lineMap: ReadonlyArray<number>,\n offset: number,\n): { readonly line: number; readonly column: number } => {\n let low = 0;\n let high = lineMap.length - 1;\n while (low < high) {\n const middle = (low + high + 1) >> 1;\n if (lineMap[middle]! <= offset) low = middle;\n else high = middle - 1;\n }\n return { line: low + 1, column: offset - lineMap[low]! + 1 };\n};\n\n/** \"line 12:8 unknown class ...\" — the shape agents iterate on. */\nexport const formatDiagnostic = (source: string, diagnostic: Diagnostic): string => {\n const { line, column } = lineColumn(buildLineMap(source), diagnostic.span.start);\n return `line ${line}:${column} ${diagnostic.message}`;\n};\n"],"mappings":";;AAOA,MAAa,aAAa,OAAO,OAAO;CACtC,OAAO,OAAO;CACd,KAAK,OAAO;AACd,CAAC;AAID,MAAa,yBAAyB,OAAO,OAAO;CAClD,MAAM,OAAO,SAAS;EAAC;EAAiB;EAAgB;EAAgB;CAAe,CAAC;CACxF,KAAK,OAAO,SAAS,OAAO,MAAM;CAClC,aAAa,OAAO,MAAM,OAAO,MAAM;CACvC,aAAa,OAAO;AACtB,CAAC;AAID,MAAa,mBAAmB,OAAO,OAAO;CAC5C,MAAM,OAAO;CACb,SAAS,OAAO;CAChB,MAAM;CACN,QAAQ,OAAO,SAAS,sBAAsB;AAChD,CAAC;AAID,IAAa,gBAAb,cAAmC,OAAO,iBAAgC,CAAC,CAAC,iBAAiB,EAC3F,aAAa,OAAO,MAAM,gBAAgB,EAC5C,CAAC,CAAC,CAAC;CACD,IAAa,UAAU;EACrB,OAAO,KAAK,YAAY,KAAK,eAAe,WAAW,OAAO,CAAC,CAAC,KAAK,IAAI;CAC3E;AACF;AAEA,IAAa,kBAAb,cAAqC,OAAO,iBAAkC,CAAC,CAAC,mBAAmB,EACjG,aAAa,OAAO,MAAM,gBAAgB,EAC5C,CAAC,CAAC,CAAC;CACD,IAAa,UAAU;EACrB,OAAO,KAAK,YAAY,KAAK,eAAe,WAAW,OAAO,CAAC,CAAC,KAAK,IAAI;CAC3E;AACF;;AAGA,MAAa,gBAAgB,WAA0C;CACrE,MAAM,aAAa,CAAC,CAAC;CACrB,KAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SACzC,IAAI,OAAO,WAAW,KAAK,MAAM,IAAa,WAAW,KAAK,QAAQ,CAAC;CAEzE,OAAO;AACT;;AAGA,MAAa,cACX,SACA,WACuD;CACvD,IAAI,MAAM;CACV,IAAI,OAAO,QAAQ,SAAS;CAC5B,OAAO,MAAM,MAAM;EACjB,MAAM,SAAU,MAAM,OAAO,KAAM;EACnC,IAAI,QAAQ,WAAY,QAAQ,MAAM;OACjC,OAAO,SAAS;CACvB;CACA,OAAO;EAAE,MAAM,MAAM;EAAG,QAAQ,SAAS,QAAQ,OAAQ;CAAE;AAC7D;;AAGA,MAAa,oBAAoB,QAAgB,eAAmC;CAClF,MAAM,EAAE,MAAM,WAAW,WAAW,aAAa,MAAM,GAAG,WAAW,KAAK,KAAK;CAC/E,OAAO,QAAQ,KAAK,GAAG,OAAO,GAAG,WAAW;AAC9C"}
|
package/dist/emit.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { t as Diagnostic } from "./diagnostics-oveE-laV.js";
|
|
2
|
+
import { TemplateVariable } from "./render-template.js";
|
|
3
|
+
//#region src/emit.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* One template's emission outcome. `ok` is true only when the template rendered
|
|
6
|
+
* cleanly with zero diagnostics (valid grammar AND stable emission). `source` is
|
|
7
|
+
* the first render's canonical SML whenever the render itself succeeded — it is
|
|
8
|
+
* present even for an unstable template (so the build can show what it produced),
|
|
9
|
+
* and `undefined` only when the print → parse validation failed outright.
|
|
10
|
+
*/
|
|
11
|
+
type EmittedTemplate = {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly ok: true;
|
|
14
|
+
readonly source: string;
|
|
15
|
+
readonly variables: ReadonlyArray<TemplateVariable>;
|
|
16
|
+
readonly diagnostics: readonly [];
|
|
17
|
+
} | {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly source: string | undefined;
|
|
21
|
+
readonly variables: ReadonlyArray<TemplateVariable>;
|
|
22
|
+
readonly diagnostics: ReadonlyArray<Diagnostic>;
|
|
23
|
+
};
|
|
24
|
+
/** The aggregate emit result for a module. */
|
|
25
|
+
interface EmittedModule {
|
|
26
|
+
/** True when at least one template was found and every found template emitted cleanly. */
|
|
27
|
+
readonly ok: boolean;
|
|
28
|
+
readonly templates: ReadonlyArray<EmittedTemplate>;
|
|
29
|
+
/**
|
|
30
|
+
* Module-level diagnostics not tied to a single template — currently only the
|
|
31
|
+
* zero-templates-found case. Per-template failures live on each entry.
|
|
32
|
+
*/
|
|
33
|
+
readonly diagnostics: ReadonlyArray<Diagnostic>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Discovers template functions in an already-evaluated module object, renders
|
|
37
|
+
* each to canonical SML with a determinism guard, and returns per-template
|
|
38
|
+
* results plus an aggregate ok/fail. `moduleExports` is a plain record of the
|
|
39
|
+
* module's exports (the vite plugin imports the TSX; this stays bundler-agnostic
|
|
40
|
+
* and pure).
|
|
41
|
+
*
|
|
42
|
+
* Discovery is exhaustive and order-preserving over `Object.entries`: every
|
|
43
|
+
* function export is probed, non-templates (helpers, constants, types-erased
|
|
44
|
+
* exports) are ignored silently, and a module with zero templates yields a
|
|
45
|
+
* single `no-templates` diagnostic with `ok: false`.
|
|
46
|
+
*/
|
|
47
|
+
declare const emitModule: (moduleExports: Record<string, unknown>) => EmittedModule;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { EmittedModule, EmittedTemplate, emitModule };
|
|
50
|
+
//# sourceMappingURL=emit.d.ts.map
|
package/dist/emit.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { isRootTag } from "./ast.js";
|
|
2
|
+
import { createVariablesProxy } from "./variables.js";
|
|
3
|
+
import { renderTemplate } from "./render-template.js";
|
|
4
|
+
import { Result } from "effect";
|
|
5
|
+
//#region src/emit.ts
|
|
6
|
+
/**
|
|
7
|
+
* emitModule — the module-level emit boundary for the TSX authoring layer
|
|
8
|
+
* (design brief §7b). The vite plugin (M12) does the importing: TSX runs at the
|
|
9
|
+
* USER's build time and this receives the already-evaluated module object (a
|
|
10
|
+
* record of its exports). emitModule stays bundler-agnostic — it never touches
|
|
11
|
+
* the filesystem, esbuild, or vite. It discovers the named template functions,
|
|
12
|
+
* runs each through renderTemplate (print → parse validation), and additionally
|
|
13
|
+
* renders each one TWICE to guard emission determinism (a build-time clock or
|
|
14
|
+
* RNG produces a different second emission and is reported as unstable).
|
|
15
|
+
*
|
|
16
|
+
* Non-function and helper exports are ignored silently; a module with zero
|
|
17
|
+
* templates is a distinct diagnostic (the common "forgot to export the function"
|
|
18
|
+
* / "imported the wrong file" mistake).
|
|
19
|
+
*
|
|
20
|
+
* Template discovery
|
|
21
|
+
*
|
|
22
|
+
* Discovery is by PROBING, not by an explicit `isTemplate` marker. A module
|
|
23
|
+
* export is a template iff it is a function that, when invoked with a fresh
|
|
24
|
+
* recording proxy, returns a channel root element (JsxResult with
|
|
25
|
+
* `kind: "element"` and an <Email>/<Sms>/<WhatsApp> tag). Rationale for
|
|
26
|
+
* probing over a marker:
|
|
27
|
+
*
|
|
28
|
+
* • The canonical template shape (brief §7b) is a plain named exported
|
|
29
|
+
* function of `Variables<T>` — requiring a `Foo.isTemplate = true` tag or a
|
|
30
|
+
* `defineTemplate()` wrapper would add ceremony the brief explicitly avoids
|
|
31
|
+
* ("named exported function is the canonical template shape").
|
|
32
|
+
* • The proxy makes the probe cheap and side-effect-free for well-formed
|
|
33
|
+
* templates: property accesses record variables and return refs; no network,
|
|
34
|
+
* no I/O. Composition helpers that assemble sub-trees also just build AST.
|
|
35
|
+
*
|
|
36
|
+
* Soundness boundary (documented tradeoff): probing invokes the function, so a
|
|
37
|
+
* helper export with arbitrary arity or side effects is executed once during
|
|
38
|
+
* discovery. We contain the two failure shapes:
|
|
39
|
+
*
|
|
40
|
+
* • A helper that THROWS when called with a proxy (wrong argument shape, a
|
|
41
|
+
* coercion-guard trip, an invalid-child assembly) is classified as a
|
|
42
|
+
* non-template and skipped — the throw is swallowed only for the probe. This
|
|
43
|
+
* is the one place we catch, and it is confined to classification.
|
|
44
|
+
* • A helper that RETURNS a non-<Email> node (a bare <Container>, a Fragment,
|
|
45
|
+
* a string) is likewise skipped.
|
|
46
|
+
*
|
|
47
|
+
* The corollary: a genuine template whose body throws (a real authoring bug —
|
|
48
|
+
* coercing a variable, a malformed child) is misclassified as a helper and
|
|
49
|
+
* silently skipped rather than surfacing its error. That is acceptable because
|
|
50
|
+
* the same bug breaks the template's real emit regardless, and the zero-template
|
|
51
|
+
* diagnostic fires when it was the module's only template. The probe re-runs the
|
|
52
|
+
* function; renderTemplate runs it again on the confirmed templates, so a
|
|
53
|
+
* template function must be a pure builder of its AST (no external side effects)
|
|
54
|
+
* — which the recording-proxy model already requires.
|
|
55
|
+
*/
|
|
56
|
+
/** Probe one export: is it a function that yields a channel root document? */
|
|
57
|
+
const isTemplateFunction = (value) => {
|
|
58
|
+
if (typeof value !== "function") return false;
|
|
59
|
+
const { proxy } = createVariablesProxy();
|
|
60
|
+
let result;
|
|
61
|
+
try {
|
|
62
|
+
result = value(proxy);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return result !== null && typeof result === "object" && result.kind === "element" && isRootTag(result.tag);
|
|
67
|
+
};
|
|
68
|
+
const ZERO_TEMPLATES = {
|
|
69
|
+
code: "no-templates",
|
|
70
|
+
message: "no template exports found — a template is a named exported function returning a channel root element (e.g. `export function WelcomeEmail(v: Variables<{…}>) { return <Email>…</Email>; }`; <Sms> and <WhatsApp> roots work the same way)",
|
|
71
|
+
span: {
|
|
72
|
+
start: 0,
|
|
73
|
+
end: 0
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const unstableDiagnostic = (name) => ({
|
|
77
|
+
code: "unstable-emission",
|
|
78
|
+
message: `template "${name}" emits different source on each render — its output depends on non-deterministic build-time data (a clock or random source); templates must be pure functions of their variables`,
|
|
79
|
+
span: {
|
|
80
|
+
start: 0,
|
|
81
|
+
end: 0
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* Discovers template functions in an already-evaluated module object, renders
|
|
86
|
+
* each to canonical SML with a determinism guard, and returns per-template
|
|
87
|
+
* results plus an aggregate ok/fail. `moduleExports` is a plain record of the
|
|
88
|
+
* module's exports (the vite plugin imports the TSX; this stays bundler-agnostic
|
|
89
|
+
* and pure).
|
|
90
|
+
*
|
|
91
|
+
* Discovery is exhaustive and order-preserving over `Object.entries`: every
|
|
92
|
+
* function export is probed, non-templates (helpers, constants, types-erased
|
|
93
|
+
* exports) are ignored silently, and a module with zero templates yields a
|
|
94
|
+
* single `no-templates` diagnostic with `ok: false`.
|
|
95
|
+
*/
|
|
96
|
+
const emitModule = (moduleExports) => {
|
|
97
|
+
const templates = [];
|
|
98
|
+
for (const [name, value] of Object.entries(moduleExports)) {
|
|
99
|
+
if (!isTemplateFunction(value)) continue;
|
|
100
|
+
templates.push(emitOne(name, value));
|
|
101
|
+
}
|
|
102
|
+
if (templates.length === 0) return {
|
|
103
|
+
ok: false,
|
|
104
|
+
templates: [],
|
|
105
|
+
diagnostics: [ZERO_TEMPLATES]
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
ok: templates.every((template) => template.ok),
|
|
109
|
+
templates,
|
|
110
|
+
diagnostics: []
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Renders one confirmed template with the determinism guard. The first render is
|
|
115
|
+
* the authoritative emission; the second exists only to detect instability
|
|
116
|
+
* (a build-time clock or random source). Both renders run at BUILD time only
|
|
117
|
+
* (the plugin) —
|
|
118
|
+
* runtime send-path rendering never double-renders. For a deterministic template
|
|
119
|
+
* the second render is pure redundant work at build time; that is the accepted
|
|
120
|
+
* cost of the guard, paid once per template per build.
|
|
121
|
+
*/
|
|
122
|
+
const emitOne = (name, template) => {
|
|
123
|
+
const first = renderTemplate(template);
|
|
124
|
+
if (Result.isFailure(first)) return {
|
|
125
|
+
name,
|
|
126
|
+
ok: false,
|
|
127
|
+
source: void 0,
|
|
128
|
+
variables: [],
|
|
129
|
+
diagnostics: first.failure.diagnostics
|
|
130
|
+
};
|
|
131
|
+
const rendered = first.success;
|
|
132
|
+
const second = renderTemplate(template);
|
|
133
|
+
if (Result.isSuccess(second) && second.success.source === rendered.source) return {
|
|
134
|
+
name,
|
|
135
|
+
ok: true,
|
|
136
|
+
source: rendered.source,
|
|
137
|
+
variables: rendered.variables,
|
|
138
|
+
diagnostics: []
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
name,
|
|
142
|
+
ok: false,
|
|
143
|
+
source: rendered.source,
|
|
144
|
+
variables: rendered.variables,
|
|
145
|
+
diagnostics: [unstableDiagnostic(name)]
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
export { emitModule };
|
|
150
|
+
|
|
151
|
+
//# sourceMappingURL=emit.js.map
|
package/dist/emit.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit.js","names":[],"sources":["../src/emit.ts"],"sourcesContent":["import { Result } from \"effect\";\n\nimport { isRootTag } from \"./ast\";\nimport type { Diagnostic } from \"./diagnostics\";\nimport type { JsxResult } from \"./jsx-runtime\";\nimport { renderTemplate, type Template, type TemplateVariable } from \"./render-template\";\nimport { createVariablesProxy } from \"./variables\";\n\n/**\n * emitModule — the module-level emit boundary for the TSX authoring layer\n * (design brief §7b). The vite plugin (M12) does the importing: TSX runs at the\n * USER's build time and this receives the already-evaluated module object (a\n * record of its exports). emitModule stays bundler-agnostic — it never touches\n * the filesystem, esbuild, or vite. It discovers the named template functions,\n * runs each through renderTemplate (print → parse validation), and additionally\n * renders each one TWICE to guard emission determinism (a build-time clock or\n * RNG produces a different second emission and is reported as unstable).\n *\n * Non-function and helper exports are ignored silently; a module with zero\n * templates is a distinct diagnostic (the common \"forgot to export the function\"\n * / \"imported the wrong file\" mistake).\n *\n * Template discovery\n *\n * Discovery is by PROBING, not by an explicit `isTemplate` marker. A module\n * export is a template iff it is a function that, when invoked with a fresh\n * recording proxy, returns a channel root element (JsxResult with\n * `kind: \"element\"` and an <Email>/<Sms>/<WhatsApp> tag). Rationale for\n * probing over a marker:\n *\n * • The canonical template shape (brief §7b) is a plain named exported\n * function of `Variables<T>` — requiring a `Foo.isTemplate = true` tag or a\n * `defineTemplate()` wrapper would add ceremony the brief explicitly avoids\n * (\"named exported function is the canonical template shape\").\n * • The proxy makes the probe cheap and side-effect-free for well-formed\n * templates: property accesses record variables and return refs; no network,\n * no I/O. Composition helpers that assemble sub-trees also just build AST.\n *\n * Soundness boundary (documented tradeoff): probing invokes the function, so a\n * helper export with arbitrary arity or side effects is executed once during\n * discovery. We contain the two failure shapes:\n *\n * • A helper that THROWS when called with a proxy (wrong argument shape, a\n * coercion-guard trip, an invalid-child assembly) is classified as a\n * non-template and skipped — the throw is swallowed only for the probe. This\n * is the one place we catch, and it is confined to classification.\n * • A helper that RETURNS a non-<Email> node (a bare <Container>, a Fragment,\n * a string) is likewise skipped.\n *\n * The corollary: a genuine template whose body throws (a real authoring bug —\n * coercing a variable, a malformed child) is misclassified as a helper and\n * silently skipped rather than surfacing its error. That is acceptable because\n * the same bug breaks the template's real emit regardless, and the zero-template\n * diagnostic fires when it was the module's only template. The probe re-runs the\n * function; renderTemplate runs it again on the confirmed templates, so a\n * template function must be a pure builder of its AST (no external side effects)\n * — which the recording-proxy model already requires.\n */\n\n/** Probe one export: is it a function that yields a channel root document? */\nconst isTemplateFunction = (value: unknown): value is Template<Record<string, unknown>> => {\n if (typeof value !== \"function\") return false;\n const { proxy } = createVariablesProxy<Record<string, unknown>>();\n let result: JsxResult;\n // oxlint-disable-next-line samva/no-try-catch-or-throw -- Discovery probe: a helper export that throws when invoked with a recording proxy is not a template. This catch is confined to classification (brief §7b probing decision); confirmed templates are rendered by renderTemplate, which lets authoring errors propagate.\n try {\n result = (value as Template<Record<string, unknown>>)(proxy);\n } catch {\n return false;\n }\n return (\n // oxlint-disable-next-line samva/no-hand-rolled-object-guard -- This narrows the already-typed JsxResult union; Predicate.isObject erases its discriminant and breaks the kind/tag narrowing below.\n result !== null &&\n typeof result === \"object\" &&\n result.kind === \"element\" &&\n isRootTag(result.tag)\n );\n};\n\n// ── Result shapes ────────────────────────────────────────────────────────────────\n\n/**\n * One template's emission outcome. `ok` is true only when the template rendered\n * cleanly with zero diagnostics (valid grammar AND stable emission). `source` is\n * the first render's canonical SML whenever the render itself succeeded — it is\n * present even for an unstable template (so the build can show what it produced),\n * and `undefined` only when the print → parse validation failed outright.\n */\nexport type EmittedTemplate =\n | {\n readonly name: string;\n readonly ok: true;\n readonly source: string;\n readonly variables: ReadonlyArray<TemplateVariable>;\n readonly diagnostics: readonly [];\n }\n | {\n readonly name: string;\n readonly ok: false;\n readonly source: string | undefined;\n readonly variables: ReadonlyArray<TemplateVariable>;\n readonly diagnostics: ReadonlyArray<Diagnostic>;\n };\n\n/** The aggregate emit result for a module. */\nexport interface EmittedModule {\n /** True when at least one template was found and every found template emitted cleanly. */\n readonly ok: boolean;\n readonly templates: ReadonlyArray<EmittedTemplate>;\n /**\n * Module-level diagnostics not tied to a single template — currently only the\n * zero-templates-found case. Per-template failures live on each entry.\n */\n readonly diagnostics: ReadonlyArray<Diagnostic>;\n}\n\nconst ZERO_TEMPLATES: Diagnostic = {\n code: \"no-templates\",\n message:\n \"no template exports found — a template is a named exported function returning a channel root element (e.g. `export function WelcomeEmail(v: Variables<{…}>) { return <Email>…</Email>; }`; <Sms> and <WhatsApp> roots work the same way)\",\n span: { start: 0, end: 0 },\n};\n\nconst unstableDiagnostic = (name: string): Diagnostic => ({\n code: \"unstable-emission\",\n message: `template \"${name}\" emits different source on each render — its output depends on non-deterministic build-time data (a clock or random source); templates must be pure functions of their variables`,\n span: { start: 0, end: 0 },\n});\n\n// ── The module emitter ────────────────────────────────────────────────────────────\n\n/**\n * Discovers template functions in an already-evaluated module object, renders\n * each to canonical SML with a determinism guard, and returns per-template\n * results plus an aggregate ok/fail. `moduleExports` is a plain record of the\n * module's exports (the vite plugin imports the TSX; this stays bundler-agnostic\n * and pure).\n *\n * Discovery is exhaustive and order-preserving over `Object.entries`: every\n * function export is probed, non-templates (helpers, constants, types-erased\n * exports) are ignored silently, and a module with zero templates yields a\n * single `no-templates` diagnostic with `ok: false`.\n */\nexport const emitModule = (moduleExports: Record<string, unknown>): EmittedModule => {\n const templates: Array<EmittedTemplate> = [];\n\n for (const [name, value] of Object.entries(moduleExports)) {\n if (!isTemplateFunction(value)) continue;\n templates.push(emitOne(name, value));\n }\n\n if (templates.length === 0) {\n return { ok: false, templates: [], diagnostics: [ZERO_TEMPLATES] };\n }\n return {\n ok: templates.every((template) => template.ok),\n templates,\n diagnostics: [],\n };\n};\n\n/**\n * Renders one confirmed template with the determinism guard. The first render is\n * the authoritative emission; the second exists only to detect instability\n * (a build-time clock or random source). Both renders run at BUILD time only\n * (the plugin) —\n * runtime send-path rendering never double-renders. For a deterministic template\n * the second render is pure redundant work at build time; that is the accepted\n * cost of the guard, paid once per template per build.\n */\nconst emitOne = (name: string, template: Template<Record<string, unknown>>): EmittedTemplate => {\n const first = renderTemplate(template);\n if (Result.isFailure(first)) {\n return {\n name,\n ok: false,\n source: undefined,\n variables: [],\n diagnostics: first.failure.diagnostics,\n };\n }\n const rendered = first.success;\n\n // Determinism guard: a second render must produce byte-identical source. A\n // grammar failure on the second pass would itself be an instability, so treat\n // any non-matching second emission (failure or differing source) as unstable.\n const second = renderTemplate(template);\n const stable = Result.isSuccess(second) && second.success.source === rendered.source;\n if (stable) {\n return {\n name,\n ok: true,\n source: rendered.source,\n variables: rendered.variables,\n diagnostics: [],\n };\n }\n\n // Unstable: keep the first render's source (for the build to display) but mark\n // the template not-ok so the module aggregate fails.\n return {\n name,\n ok: false,\n source: rendered.source,\n variables: rendered.variables,\n diagnostics: [unstableDiagnostic(name)],\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,MAAM,sBAAsB,UAA+D;CACzF,IAAI,OAAO,UAAU,YAAY,OAAO;CACxC,MAAM,EAAE,UAAU,qBAA8C;CAChE,IAAI;CAEJ,IAAI;EACF,SAAU,MAA4C,KAAK;CAC7D,QAAQ;EACN,OAAO;CACT;CACA,OAEE,WAAW,QACX,OAAO,WAAW,YAClB,OAAO,SAAS,aAChB,UAAU,OAAO,GAAG;AAExB;AAuCA,MAAM,iBAA6B;CACjC,MAAM;CACN,SACE;CACF,MAAM;EAAE,OAAO;EAAG,KAAK;CAAE;AAC3B;AAEA,MAAM,sBAAsB,UAA8B;CACxD,MAAM;CACN,SAAS,aAAa,KAAK;CAC3B,MAAM;EAAE,OAAO;EAAG,KAAK;CAAE;AAC3B;;;;;;;;;;;;;AAgBA,MAAa,cAAc,kBAA0D;CACnF,MAAM,YAAoC,CAAC;CAE3C,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,aAAa,GAAG;EACzD,IAAI,CAAC,mBAAmB,KAAK,GAAG;EAChC,UAAU,KAAK,QAAQ,MAAM,KAAK,CAAC;CACrC;CAEA,IAAI,UAAU,WAAW,GACvB,OAAO;EAAE,IAAI;EAAO,WAAW,CAAC;EAAG,aAAa,CAAC,cAAc;CAAE;CAEnE,OAAO;EACL,IAAI,UAAU,OAAO,aAAa,SAAS,EAAE;EAC7C;EACA,aAAa,CAAC;CAChB;AACF;;;;;;;;;;AAWA,MAAM,WAAW,MAAc,aAAiE;CAC9F,MAAM,QAAQ,eAAe,QAAQ;CACrC,IAAI,OAAO,UAAU,KAAK,GACxB,OAAO;EACL;EACA,IAAI;EACJ,QAAQ,KAAA;EACR,WAAW,CAAC;EACZ,aAAa,MAAM,QAAQ;CAC7B;CAEF,MAAM,WAAW,MAAM;CAKvB,MAAM,SAAS,eAAe,QAAQ;CAEtC,IADe,OAAO,UAAU,MAAM,KAAK,OAAO,QAAQ,WAAW,SAAS,QAE5E,OAAO;EACL;EACA,IAAI;EACJ,QAAQ,SAAS;EACjB,WAAW,SAAS;EACpB,aAAa,CAAC;CAChB;CAKF,OAAO;EACL;EACA,IAAI;EACJ,QAAQ,SAAS;EACjB,WAAW,SAAS;EACpB,aAAa,CAAC,mBAAmB,IAAI,CAAC;CACxC;AACF"}
|
package/dist/footer.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { s as TokenTable } from "./theme-6ApjjiBo.js";
|
|
2
|
+
//#region src/footer.d.ts
|
|
3
|
+
/** The per-recipient unsubscribe URL is not known at accept time (the body is
|
|
4
|
+
* shared across a send's deliveries), so the footer is composed with this
|
|
5
|
+
* placeholder in the unsubscribe href and the plain-text tail. The email queue
|
|
6
|
+
* processor substitutes each delivery's minted token URL at dispatch. The link
|
|
7
|
+
* carries `ses:no-track` so click tracking never rewrites (and thereby breaks)
|
|
8
|
+
* the placeholder. */
|
|
9
|
+
declare const FOOTER_UNSUBSCRIBE_PLACEHOLDER = "%samva:unsubscribe-url%";
|
|
10
|
+
/** One labelled social link. Structurally mirrors `DesignSocialLink`. */
|
|
11
|
+
interface FooterSocialLink {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly url: string;
|
|
14
|
+
}
|
|
15
|
+
/** A structured postal address. Structurally mirrors `DesignAddress`. */
|
|
16
|
+
interface FooterAddress {
|
|
17
|
+
readonly street1: string;
|
|
18
|
+
readonly street2?: string | undefined;
|
|
19
|
+
readonly city: string;
|
|
20
|
+
readonly region: string;
|
|
21
|
+
readonly postalCode: string;
|
|
22
|
+
readonly country: string;
|
|
23
|
+
}
|
|
24
|
+
/** The effective footer content. Structurally mirrors `DesignFooter`. */
|
|
25
|
+
interface FooterContent {
|
|
26
|
+
readonly companyName?: string | undefined;
|
|
27
|
+
readonly address?: FooterAddress | undefined;
|
|
28
|
+
readonly socialLinks?: ReadonlyArray<FooterSocialLink> | undefined;
|
|
29
|
+
readonly customHtml?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Which compliance-relevant footer fields the effective content is missing,
|
|
32
|
+
* surfaced on the design read path for M6's dashboard nudge. */
|
|
33
|
+
interface FooterCompleteness {
|
|
34
|
+
/** No physical mailing address is set anywhere in the cascade (CAN-SPAM). */
|
|
35
|
+
readonly missingAddress: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface RenderedFooter {
|
|
38
|
+
/** Table-based footer block, ready to inject before the body's `</body>`. */
|
|
39
|
+
readonly html: string;
|
|
40
|
+
/** Plain-text footer tail, appended to the text alternative. */
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly completeness: FooterCompleteness;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Which compliance-relevant fields the footer content lacks — the signal M6's
|
|
46
|
+
* dashboard nudge reads. Derivable from `design.footer` alone (no token table),
|
|
47
|
+
* so the design read path can report it without composing the block. A
|
|
48
|
+
* `customHtml` footer is treated as complete (the org owns its own content).
|
|
49
|
+
*/
|
|
50
|
+
declare const footerCompleteness: (footer: FooterContent | undefined) => FooterCompleteness;
|
|
51
|
+
/**
|
|
52
|
+
* Compose the injected footer for one send. `unsubscribeUrl` is the placeholder
|
|
53
|
+
* (`FOOTER_UNSUBSCRIBE_PLACEHOLDER`) when a group resolved and dispatch will
|
|
54
|
+
* mint the per-recipient link, or `null` to omit the unsubscribe row entirely
|
|
55
|
+
* (no group resolved). Never throws and never blocks: whatever content exists
|
|
56
|
+
* renders, and the missing pieces are reported via `completeness`.
|
|
57
|
+
*/
|
|
58
|
+
declare const renderFooterBlock: (input: {
|
|
59
|
+
readonly footer: FooterContent;
|
|
60
|
+
readonly tokens: TokenTable;
|
|
61
|
+
readonly unsubscribeUrl: string | null;
|
|
62
|
+
readonly unsubscribeLabel?: string | undefined;
|
|
63
|
+
}) => RenderedFooter;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { FOOTER_UNSUBSCRIBE_PLACEHOLDER, FooterAddress, FooterCompleteness, FooterContent, FooterSocialLink, RenderedFooter, footerCompleteness, renderFooterBlock };
|
|
66
|
+
//# sourceMappingURL=footer.d.ts.map
|
package/dist/footer.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//#region src/footer.ts
|
|
2
|
+
/** The per-recipient unsubscribe URL is not known at accept time (the body is
|
|
3
|
+
* shared across a send's deliveries), so the footer is composed with this
|
|
4
|
+
* placeholder in the unsubscribe href and the plain-text tail. The email queue
|
|
5
|
+
* processor substitutes each delivery's minted token URL at dispatch. The link
|
|
6
|
+
* carries `ses:no-track` so click tracking never rewrites (and thereby breaks)
|
|
7
|
+
* the placeholder. */
|
|
8
|
+
const FOOTER_UNSUBSCRIBE_PLACEHOLDER = "%samva:unsubscribe-url%";
|
|
9
|
+
const escapeText = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
10
|
+
const escapeAttribute = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll("\"", """);
|
|
11
|
+
const isSafeUrl = (url) => /^(https?:|mailto:)/i.test(url.trim());
|
|
12
|
+
const addressLines = (address) => {
|
|
13
|
+
const region = [
|
|
14
|
+
address.city,
|
|
15
|
+
address.region,
|
|
16
|
+
address.postalCode
|
|
17
|
+
].filter(Boolean).join(", ");
|
|
18
|
+
return [
|
|
19
|
+
address.street1,
|
|
20
|
+
address.street2,
|
|
21
|
+
region,
|
|
22
|
+
address.country
|
|
23
|
+
].filter((line) => typeof line === "string" && line.trim().length > 0);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Which compliance-relevant fields the footer content lacks — the signal M6's
|
|
27
|
+
* dashboard nudge reads. Derivable from `design.footer` alone (no token table),
|
|
28
|
+
* so the design read path can report it without composing the block. A
|
|
29
|
+
* `customHtml` footer is treated as complete (the org owns its own content).
|
|
30
|
+
*/
|
|
31
|
+
const footerCompleteness = (footer) => ({ missingAddress: footer?.customHtml === void 0 && (footer?.address === void 0 || addressLines(footer.address).length === 0) });
|
|
32
|
+
/**
|
|
33
|
+
* Compose the injected footer for one send. `unsubscribeUrl` is the placeholder
|
|
34
|
+
* (`FOOTER_UNSUBSCRIBE_PLACEHOLDER`) when a group resolved and dispatch will
|
|
35
|
+
* mint the per-recipient link, or `null` to omit the unsubscribe row entirely
|
|
36
|
+
* (no group resolved). Never throws and never blocks: whatever content exists
|
|
37
|
+
* renders, and the missing pieces are reported via `completeness`.
|
|
38
|
+
*/
|
|
39
|
+
const renderFooterBlock = (input) => {
|
|
40
|
+
const { footer, tokens, unsubscribeUrl } = input;
|
|
41
|
+
const unsubscribeLabel = input.unsubscribeLabel ?? "Unsubscribe";
|
|
42
|
+
const fontFamily = tokens.fontFamilies["body"] ?? tokens.fontFamilies["sans"] ?? "sans-serif";
|
|
43
|
+
const mutedColor = tokens.colors["gray-500"] ?? tokens.colors["neutral-500"] ?? "#6a7282";
|
|
44
|
+
const linkColor = tokens.colors["brand"] ?? mutedColor;
|
|
45
|
+
const containerPx = tokens.containerPx["xl"] ?? 600;
|
|
46
|
+
const fontSizePx = tokens.text["xs"]?.sizePx ?? 12;
|
|
47
|
+
const lineHeightPx = tokens.text["xs"]?.lineHeightPx ?? 18;
|
|
48
|
+
const rows = [];
|
|
49
|
+
const textLines = [];
|
|
50
|
+
if (typeof footer.customHtml === "string" && footer.customHtml.trim().length > 0) rows.push(`<tr><td style="padding:0 0 12px 0;">${footer.customHtml}</td></tr>`);
|
|
51
|
+
else {
|
|
52
|
+
if (typeof footer.companyName === "string" && footer.companyName.trim().length > 0) {
|
|
53
|
+
rows.push(`<tr><td style="padding:0 0 4px 0;font-weight:600;">${escapeText(footer.companyName)}</td></tr>`);
|
|
54
|
+
textLines.push(footer.companyName);
|
|
55
|
+
}
|
|
56
|
+
if (footer.address !== void 0) {
|
|
57
|
+
const lines = addressLines(footer.address);
|
|
58
|
+
if (lines.length > 0) {
|
|
59
|
+
rows.push(`<tr><td style="padding:0 0 8px 0;">${lines.map(escapeText).join("<br />")}</td></tr>`);
|
|
60
|
+
textLines.push(lines.join(", "));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (footer.socialLinks !== void 0 && footer.socialLinks.length > 0) {
|
|
64
|
+
const links = footer.socialLinks.map((link) => isSafeUrl(link.url) ? `<a href="${escapeAttribute(link.url.trim())}" style="color:${linkColor};text-decoration:underline;" ses:no-track>${escapeText(link.label)}</a>` : escapeText(link.label));
|
|
65
|
+
rows.push(`<tr><td style="padding:0 0 8px 0;">${links.join("<span style=\"padding:0 6px;\">·</span>")}</td></tr>`);
|
|
66
|
+
textLines.push(footer.socialLinks.map((link) => `${link.label}: ${link.url}`).join(" "));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (unsubscribeUrl !== null) {
|
|
70
|
+
rows.push(`<tr><td style="padding:0;"><a href="${escapeAttribute(unsubscribeUrl)}" style="color:${mutedColor};text-decoration:underline;" ses:no-track>${escapeText(unsubscribeLabel)}</a></td></tr>`);
|
|
71
|
+
textLines.push(`${unsubscribeLabel}: ${unsubscribeUrl}`);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
html: rows.length === 0 ? "" : [
|
|
75
|
+
`<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="width:100%;border-collapse:collapse;">`,
|
|
76
|
+
`<tr><td align="center" style="padding:32px 16px;">`,
|
|
77
|
+
`<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="${containerPx}" style="width:100%;max-width:${containerPx}px;border-collapse:collapse;font-family:${fontFamily};font-size:${fontSizePx}px;line-height:${lineHeightPx}px;color:${mutedColor};text-align:center;">`,
|
|
78
|
+
...rows,
|
|
79
|
+
`</table>`,
|
|
80
|
+
`</td></tr>`,
|
|
81
|
+
`</table>`
|
|
82
|
+
].join(""),
|
|
83
|
+
text: textLines.length === 0 ? "" : `\n\n---\n${textLines.join("\n")}`,
|
|
84
|
+
completeness: footerCompleteness(footer)
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { FOOTER_UNSUBSCRIBE_PLACEHOLDER, footerCompleteness, renderFooterBlock };
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=footer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"footer.js","names":[],"sources":["../src/footer.ts"],"sourcesContent":["import type { TokenTable } from \"./theme.js\";\n\n// The injected email footer (settings cascade M5). A send whose resolved\n// `email.footer.enabled` is true appends this block beneath the body: the\n// sender's company identity (name, CAN-SPAM physical address, social links)\n// plus the per-recipient unsubscribe link. It is composed from the send's\n// effective `design.footer` content and the resolved theme token table, so the\n// footer inherits the org/domain/template brand (font + brand color) without a\n// separate styling system. Callers map their effective footer settings onto\n// `FooterContent`.\n\n/** The per-recipient unsubscribe URL is not known at accept time (the body is\n * shared across a send's deliveries), so the footer is composed with this\n * placeholder in the unsubscribe href and the plain-text tail. The email queue\n * processor substitutes each delivery's minted token URL at dispatch. The link\n * carries `ses:no-track` so click tracking never rewrites (and thereby breaks)\n * the placeholder. */\nexport const FOOTER_UNSUBSCRIBE_PLACEHOLDER = \"%samva:unsubscribe-url%\";\n\n/** One labelled social link. Structurally mirrors `DesignSocialLink`. */\nexport interface FooterSocialLink {\n readonly label: string;\n readonly url: string;\n}\n\n/** A structured postal address. Structurally mirrors `DesignAddress`. */\nexport interface FooterAddress {\n readonly street1: string;\n readonly street2?: string | undefined;\n readonly city: string;\n readonly region: string;\n readonly postalCode: string;\n readonly country: string;\n}\n\n/** The effective footer content. Structurally mirrors `DesignFooter`. */\nexport interface FooterContent {\n readonly companyName?: string | undefined;\n readonly address?: FooterAddress | undefined;\n readonly socialLinks?: ReadonlyArray<FooterSocialLink> | undefined;\n readonly customHtml?: string | undefined;\n}\n\n/** Which compliance-relevant footer fields the effective content is missing,\n * surfaced on the design read path for M6's dashboard nudge. */\nexport interface FooterCompleteness {\n /** No physical mailing address is set anywhere in the cascade (CAN-SPAM). */\n readonly missingAddress: boolean;\n}\n\nexport interface RenderedFooter {\n /** Table-based footer block, ready to inject before the body's `</body>`. */\n readonly html: string;\n /** Plain-text footer tail, appended to the text alternative. */\n readonly text: string;\n readonly completeness: FooterCompleteness;\n}\n\nconst escapeText = (value: string): string =>\n value.replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\");\n\nconst escapeAttribute = (value: string): string =>\n value.replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll('\"', \""\");\n\n// Only http(s) and mailto links are emitted as anchors; anything else (e.g. a\n// `javascript:` scheme in org-authored social data) renders as inert text so a\n// footer can never smuggle an active link into delivered mail.\nconst isSafeUrl = (url: string): boolean => /^(https?:|mailto:)/i.test(url.trim());\n\nconst addressLines = (address: FooterAddress): ReadonlyArray<string> => {\n const region = [address.city, address.region, address.postalCode].filter(Boolean).join(\", \");\n return [address.street1, address.street2, region, address.country].filter(\n (line): line is string => typeof line === \"string\" && line.trim().length > 0,\n );\n};\n\n/**\n * Which compliance-relevant fields the footer content lacks — the signal M6's\n * dashboard nudge reads. Derivable from `design.footer` alone (no token table),\n * so the design read path can report it without composing the block. A\n * `customHtml` footer is treated as complete (the org owns its own content).\n */\nexport const footerCompleteness = (footer: FooterContent | undefined): FooterCompleteness => ({\n missingAddress:\n footer?.customHtml === undefined &&\n (footer?.address === undefined || addressLines(footer.address).length === 0),\n});\n\n/**\n * Compose the injected footer for one send. `unsubscribeUrl` is the placeholder\n * (`FOOTER_UNSUBSCRIBE_PLACEHOLDER`) when a group resolved and dispatch will\n * mint the per-recipient link, or `null` to omit the unsubscribe row entirely\n * (no group resolved). Never throws and never blocks: whatever content exists\n * renders, and the missing pieces are reported via `completeness`.\n */\nexport const renderFooterBlock = (input: {\n readonly footer: FooterContent;\n readonly tokens: TokenTable;\n readonly unsubscribeUrl: string | null;\n readonly unsubscribeLabel?: string | undefined;\n}): RenderedFooter => {\n const { footer, tokens, unsubscribeUrl } = input;\n const unsubscribeLabel = input.unsubscribeLabel ?? \"Unsubscribe\";\n\n // Muted-on-surface footer type, inheriting the brand body font and brand link\n // color from the resolved token table so an org theme carries into the footer.\n const fontFamily = tokens.fontFamilies[\"body\"] ?? tokens.fontFamilies[\"sans\"] ?? \"sans-serif\";\n const mutedColor = tokens.colors[\"gray-500\"] ?? tokens.colors[\"neutral-500\"] ?? \"#6a7282\";\n const linkColor = tokens.colors[\"brand\"] ?? mutedColor;\n const containerPx = tokens.containerPx[\"xl\"] ?? 600;\n const fontSizePx = tokens.text[\"xs\"]?.sizePx ?? 12;\n const lineHeightPx = tokens.text[\"xs\"]?.lineHeightPx ?? 18;\n\n const rows: string[] = [];\n const textLines: string[] = [];\n\n if (typeof footer.customHtml === \"string\" && footer.customHtml.trim().length > 0) {\n // Escape hatch: org-authored footer HTML replaces the structured block. The\n // compliance unsubscribe row below is still appended — its link is the\n // per-recipient token the org cannot author into static HTML.\n rows.push(`<tr><td style=\"padding:0 0 12px 0;\">${footer.customHtml}</td></tr>`);\n } else {\n if (typeof footer.companyName === \"string\" && footer.companyName.trim().length > 0) {\n rows.push(\n `<tr><td style=\"padding:0 0 4px 0;font-weight:600;\">${escapeText(footer.companyName)}</td></tr>`,\n );\n textLines.push(footer.companyName);\n }\n if (footer.address !== undefined) {\n const lines = addressLines(footer.address);\n if (lines.length > 0) {\n rows.push(\n `<tr><td style=\"padding:0 0 8px 0;\">${lines.map(escapeText).join(\"<br />\")}</td></tr>`,\n );\n textLines.push(lines.join(\", \"));\n }\n }\n if (footer.socialLinks !== undefined && footer.socialLinks.length > 0) {\n const links = footer.socialLinks.map((link) =>\n isSafeUrl(link.url)\n ? `<a href=\"${escapeAttribute(link.url.trim())}\" style=\"color:${linkColor};text-decoration:underline;\" ses:no-track>${escapeText(link.label)}</a>`\n : escapeText(link.label),\n );\n rows.push(\n `<tr><td style=\"padding:0 0 8px 0;\">${links.join('<span style=\"padding:0 6px;\">·</span>')}</td></tr>`,\n );\n textLines.push(footer.socialLinks.map((link) => `${link.label}: ${link.url}`).join(\" \"));\n }\n }\n\n if (unsubscribeUrl !== null) {\n rows.push(\n `<tr><td style=\"padding:0;\"><a href=\"${escapeAttribute(unsubscribeUrl)}\" style=\"color:${mutedColor};text-decoration:underline;\" ses:no-track>${escapeText(unsubscribeLabel)}</a></td></tr>`,\n );\n textLines.push(`${unsubscribeLabel}: ${unsubscribeUrl}`);\n }\n\n const html =\n rows.length === 0\n ? \"\"\n : [\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"width:100%;border-collapse:collapse;\">`,\n `<tr><td align=\"center\" style=\"padding:32px 16px;\">`,\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"${containerPx}\" style=\"width:100%;max-width:${containerPx}px;border-collapse:collapse;font-family:${fontFamily};font-size:${fontSizePx}px;line-height:${lineHeightPx}px;color:${mutedColor};text-align:center;\">`,\n ...rows,\n `</table>`,\n `</td></tr>`,\n `</table>`,\n ].join(\"\");\n\n return {\n html,\n text: textLines.length === 0 ? \"\" : `\\n\\n---\\n${textLines.join(\"\\n\")}`,\n completeness: footerCompleteness(footer),\n };\n};\n"],"mappings":";;;;;;;AAiBA,MAAa,iCAAiC;AAyC9C,MAAM,cAAc,UAClB,MAAM,WAAW,KAAK,OAAO,CAAC,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,WAAW,KAAK,MAAM;AAE/E,MAAM,mBAAmB,UACvB,MAAM,WAAW,KAAK,OAAO,CAAC,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,WAAW,MAAK,QAAQ;AAKjF,MAAM,aAAa,QAAyB,sBAAsB,KAAK,IAAI,KAAK,CAAC;AAEjF,MAAM,gBAAgB,YAAkD;CACtE,MAAM,SAAS;EAAC,QAAQ;EAAM,QAAQ;EAAQ,QAAQ;CAAU,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,IAAI;CAC3F,OAAO;EAAC,QAAQ;EAAS,QAAQ;EAAS;EAAQ,QAAQ;CAAO,CAAC,CAAC,QAChE,SAAyB,OAAO,SAAS,YAAY,KAAK,KAAK,CAAC,CAAC,SAAS,CAC7E;AACF;;;;;;;AAQA,MAAa,sBAAsB,YAA2D,EAC5F,gBACE,QAAQ,eAAe,KAAA,MACtB,QAAQ,YAAY,KAAA,KAAa,aAAa,OAAO,OAAO,CAAC,CAAC,WAAW,GAC9E;;;;;;;;AASA,MAAa,qBAAqB,UAKZ;CACpB,MAAM,EAAE,QAAQ,QAAQ,mBAAmB;CAC3C,MAAM,mBAAmB,MAAM,oBAAoB;CAInD,MAAM,aAAa,OAAO,aAAa,WAAW,OAAO,aAAa,WAAW;CACjF,MAAM,aAAa,OAAO,OAAO,eAAe,OAAO,OAAO,kBAAkB;CAChF,MAAM,YAAY,OAAO,OAAO,YAAY;CAC5C,MAAM,cAAc,OAAO,YAAY,SAAS;CAChD,MAAM,aAAa,OAAO,KAAK,KAAK,EAAE,UAAU;CAChD,MAAM,eAAe,OAAO,KAAK,KAAK,EAAE,gBAAgB;CAExD,MAAM,OAAiB,CAAC;CACxB,MAAM,YAAsB,CAAC;CAE7B,IAAI,OAAO,OAAO,eAAe,YAAY,OAAO,WAAW,KAAK,CAAC,CAAC,SAAS,GAI7E,KAAK,KAAK,uCAAuC,OAAO,WAAW,WAAW;MACzE;EACL,IAAI,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,CAAC,CAAC,SAAS,GAAG;GAClF,KAAK,KACH,sDAAsD,WAAW,OAAO,WAAW,EAAE,WACvF;GACA,UAAU,KAAK,OAAO,WAAW;EACnC;EACA,IAAI,OAAO,YAAY,KAAA,GAAW;GAChC,MAAM,QAAQ,aAAa,OAAO,OAAO;GACzC,IAAI,MAAM,SAAS,GAAG;IACpB,KAAK,KACH,sCAAsC,MAAM,IAAI,UAAU,CAAC,CAAC,KAAK,QAAQ,EAAE,WAC7E;IACA,UAAU,KAAK,MAAM,KAAK,IAAI,CAAC;GACjC;EACF;EACA,IAAI,OAAO,gBAAgB,KAAA,KAAa,OAAO,YAAY,SAAS,GAAG;GACrE,MAAM,QAAQ,OAAO,YAAY,KAAK,SACpC,UAAU,KAAK,GAAG,IACd,YAAY,gBAAgB,KAAK,IAAI,KAAK,CAAC,EAAE,iBAAiB,UAAU,4CAA4C,WAAW,KAAK,KAAK,EAAE,QAC3I,WAAW,KAAK,KAAK,CAC3B;GACA,KAAK,KACH,sCAAsC,MAAM,KAAK,yCAAuC,EAAE,WAC5F;GACA,UAAU,KAAK,OAAO,YAAY,KAAK,SAAS,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC;EAC1F;CACF;CAEA,IAAI,mBAAmB,MAAM;EAC3B,KAAK,KACH,uCAAuC,gBAAgB,cAAc,EAAE,iBAAiB,WAAW,4CAA4C,WAAW,gBAAgB,EAAE,eAC9K;EACA,UAAU,KAAK,GAAG,iBAAiB,IAAI,gBAAgB;CACzD;CAeA,OAAO;EACL,MAbA,KAAK,WAAW,IACZ,KACA;GACE;GACA;GACA,gFAAgF,YAAY,gCAAgC,YAAY,0CAA0C,WAAW,aAAa,WAAW,iBAAiB,aAAa,WAAW,WAAW;GACzQ,GAAG;GACH;GACA;GACA;EACF,CAAC,CAAC,KAAK,EAAE;EAIb,MAAM,UAAU,WAAW,IAAI,KAAK,YAAY,UAAU,KAAK,IAAI;EACnE,cAAc,mBAAmB,MAAM;CACzC;AACF"}
|