@samva/markup 0.1.1 → 0.2.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 +25 -5
- package/README.md +92 -26
- package/dist/{ast-DAhnAvQN.d.ts → ast-63I-VLQx.d.ts} +15 -2
- package/dist/ast.d.ts +2 -2
- package/dist/ast.js +15 -2
- package/dist/{checks-CW0nFj5n.js → checks-DXuERO4B.js} +37 -3
- package/dist/checks.d.ts +8 -1
- package/dist/checks.js +2 -2
- package/dist/compile-sms.d.ts +2 -2
- package/dist/compile-whatsapp.d.ts +2 -2
- package/dist/compile.d.ts +9 -84
- package/dist/compile.js +11 -920
- package/dist/compiler.d.ts +301 -0
- package/dist/compiler.js +1389 -0
- package/dist/components.d.ts +4 -4
- package/dist/conditionals.js +1 -1
- package/dist/corpus-Dz5irJ9x.d.ts +161 -0
- package/dist/diagnostics-Djd9ckLg.d.ts +90 -0
- package/dist/diagnostics.js +6 -3
- package/dist/elements-DtwKlFVH.d.ts +820 -0
- package/dist/email/components.d.ts +119 -0
- package/dist/email/components.js +207 -0
- package/dist/email/diagnostics.d.ts +2 -0
- package/dist/email/diagnostics.js +56 -0
- package/dist/email/elements.d.ts +2 -0
- package/dist/email/elements.js +299 -0
- package/dist/email/html.d.ts +42 -0
- package/dist/email/html.js +266 -0
- package/dist/email/jsx-dev-runtime.d.ts +26 -0
- package/dist/email/jsx-dev-runtime.js +12 -0
- package/dist/email/jsx-runtime.d.ts +2 -0
- package/dist/email/jsx-runtime.js +60 -0
- package/dist/email/render.d.ts +36 -0
- package/dist/email/render.js +6 -0
- package/dist/email/selection.d.ts +102 -0
- package/dist/email/selection.js +143 -0
- package/dist/email/source-edit.d.ts +67 -0
- package/dist/email/source-edit.js +185 -0
- package/dist/email/stylesheets.d.ts +2 -0
- package/dist/email/stylesheets.js +43 -0
- package/dist/email/text.d.ts +7 -0
- package/dist/email/text.js +92 -0
- package/dist/emit.d.ts +13 -12
- package/dist/emit.js +63 -63
- package/dist/input-schema-BIfzI63B.d.ts +23 -0
- package/dist/input-schema.d.ts +2 -0
- package/dist/input-schema.js +170 -0
- package/dist/jsx-dev-runtime.d.ts +4 -3
- package/dist/jsx-dev-runtime.js +9 -1
- package/dist/{jsx-runtime-fSxcvlx4.d.ts → jsx-runtime-C5k_Zq0A.d.ts} +4 -4
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +20 -1
- package/dist/model.d.ts +1 -1
- package/dist/ops.d.ts +1 -1
- package/dist/ops.js +2 -2
- package/dist/{parser-yzjUOGhc.d.ts → parser-CGBS73Gz.d.ts} +12 -3
- package/dist/parser.d.ts +2 -2
- package/dist/parser.js +48 -9
- package/dist/predicate-CvkzT-a2.js +539 -0
- package/dist/{predicate-DTtVtLK7.d.ts → predicate-G2SeSvoN.d.ts} +9 -2
- package/dist/predicate.d.ts +2 -2
- package/dist/predicate.js +2 -417
- package/dist/presets/corpus.d.ts +2 -0
- package/dist/presets/corpus.js +186 -0
- package/dist/presets/public-gallery.d.ts +241 -459
- package/dist/presets/public-gallery.js +305 -5
- package/dist/presets/public-handoff.d.ts +75 -3
- package/dist/presets/public-handoff.js +220 -37
- package/dist/presets/public-preview-artifacts.js +20 -26
- package/dist/presets/public-preview.d.ts +40 -1152
- package/dist/presets/public-preview.js +388 -1152
- package/dist/presets/sources.d.ts +17 -4
- package/dist/presets/sources.js +1212 -43
- package/dist/presets.d.ts +2 -2
- package/dist/printer.d.ts +1 -1
- package/dist/render-Cddsy_XF.js +585 -0
- package/dist/render-template.d.ts +6 -3
- package/dist/render-template.js +4 -2
- package/dist/source-locations-BCJQnIfU.d.ts +89 -0
- package/dist/source-locations.d.ts +2 -0
- package/dist/source-locations.js +125 -0
- package/dist/stylesheets-oDRAnYA3.d.ts +94 -0
- package/dist/template-Dy5yOPyp.d.ts +60 -0
- package/dist/template.d.ts +2 -0
- package/dist/template.js +37 -0
- package/dist/tsx-source-instrumentation.d.ts +23 -0
- package/dist/tsx-source-instrumentation.js +97 -0
- package/dist/tsx-tracer.d.ts +116 -0
- package/dist/tsx-tracer.js +985 -0
- package/dist/{variables-3NGLpNTB.d.ts → variables-if1VrV34.d.ts} +2 -2
- package/dist/variables.d.ts +1 -1
- package/dist/variables.js +1 -1
- package/generated/sml-agent-contract.json +574 -782
- package/generated/sml-email-agent-contract.json +502 -677
- package/generated/sml-email-language-reference.md +94 -160
- package/generated/sml-language-reference.md +195 -220
- package/package.json +111 -8
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { r as EmailDiagnostic } from "../diagnostics-Djd9ckLg.js";
|
|
2
|
+
import { m as EmailNode } from "../elements-DtwKlFVH.js";
|
|
3
|
+
import { EmittedPosition } from "./selection.js";
|
|
4
|
+
//#region src/email/html.d.ts
|
|
5
|
+
/** Escape a value for element content. */
|
|
6
|
+
declare const escapeText: (value: string) => string;
|
|
7
|
+
/** Escape a value for a double-quoted attribute. */
|
|
8
|
+
declare const escapeAttribute: (value: string) => string;
|
|
9
|
+
/** camelCase to kebab-case; already-kebab names pass through unchanged. */
|
|
10
|
+
declare const kebab: (property: string) => string;
|
|
11
|
+
/** A bare number is pixels for every property that takes a length. */
|
|
12
|
+
declare const declarationValue: (property: string, value: string | number) => string;
|
|
13
|
+
interface EmailHtmlOutput {
|
|
14
|
+
readonly html: string;
|
|
15
|
+
readonly diagnostics: readonly EmailDiagnostic[];
|
|
16
|
+
/**
|
|
17
|
+
* Each emitted element's offset range and origins. A compatibility finding
|
|
18
|
+
* carries a position in the emitted document, and this is what turns that
|
|
19
|
+
* back into the authoring element the author can edit.
|
|
20
|
+
*/
|
|
21
|
+
readonly positions: readonly EmittedPosition[];
|
|
22
|
+
}
|
|
23
|
+
/** How this document will be consumed. Delivery is the default; export is opt-in. */
|
|
24
|
+
interface SerializeEmailOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Accept relative references. A local export serves assets from a relative
|
|
27
|
+
* base beside the file; a delivered message has no directory to resolve one
|
|
28
|
+
* against, so this stays off for anything that will be sent.
|
|
29
|
+
*/
|
|
30
|
+
readonly relativeUrls?: boolean | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Stamp every element with its instance path, so an editor preview can
|
|
33
|
+
* resolve a click in the rendered document to exactly one rendered element.
|
|
34
|
+
* A delivered message never carries it.
|
|
35
|
+
*/
|
|
36
|
+
readonly instancePaths?: boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Lower the derived tree to email HTML, reporting every unsupported construct with its origin. */
|
|
39
|
+
declare const serializeEmailHtml: (node: EmailNode, options?: SerializeEmailOptions) => EmailHtmlOutput;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { EmailHtmlOutput, SerializeEmailOptions, declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml };
|
|
42
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { EMAIL_ELEMENTS, EMAIL_GLOBAL_ATTRIBUTES, emailElement } from "./elements.js";
|
|
2
|
+
import { INSTANCE_PATH_ATTRIBUTE } from "./selection.js";
|
|
3
|
+
import { Predicate } from "effect";
|
|
4
|
+
//#region src/email/html.ts
|
|
5
|
+
/** Escape a value for element content. */
|
|
6
|
+
const escapeText = (value) => value.replace(/[&<>]/g, (character) => ({
|
|
7
|
+
"&": "&",
|
|
8
|
+
"<": "<",
|
|
9
|
+
">": ">"
|
|
10
|
+
})[character] ?? character);
|
|
11
|
+
/** Escape a value for a double-quoted attribute. */
|
|
12
|
+
const escapeAttribute = (value) => value.replace(/[&<>"']/g, (character) => ({
|
|
13
|
+
"&": "&",
|
|
14
|
+
"<": "<",
|
|
15
|
+
">": ">",
|
|
16
|
+
"\"": """,
|
|
17
|
+
"'": "'"
|
|
18
|
+
})[character] ?? character);
|
|
19
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
20
|
+
const SAFE_URL_SCHEME = /^(https:\/\/|mailto:|tel:|cid:|#)/i;
|
|
21
|
+
const RELATIVE_URL = /^(\.{1,2}\/|\/(?!\/))/;
|
|
22
|
+
/**
|
|
23
|
+
* Only references an email client will follow safely; no `javascript:`, no
|
|
24
|
+
* `data:`. A relative reference is legal only for an export: a file on disk can
|
|
25
|
+
* resolve one against its own directory, and a delivered message cannot resolve
|
|
26
|
+
* anything but an absolute URL.
|
|
27
|
+
*/
|
|
28
|
+
const isSafeUrl = (value, relative) => !/^[\u0000-\u0020]/.test(value) && !CONTROL_CHARACTERS.test(value) && !value.includes("\\") && (SAFE_URL_SCHEME.test(value) || relative && RELATIVE_URL.test(value));
|
|
29
|
+
const CSS_PROPERTY = /^(--[a-zA-Z0-9-]+|[a-zA-Z][a-zA-Z0-9-]*)$/;
|
|
30
|
+
const CSS_UNSAFE = /[;{}<>\\]|expression\s*\(|@import|\/\*/i;
|
|
31
|
+
const CSS_URL = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
|
|
32
|
+
/** camelCase to kebab-case; already-kebab names pass through unchanged. */
|
|
33
|
+
const kebab = (property) => property.startsWith("--") ? property : property.replace(/[A-Z]/g, (l) => `-${l.toLowerCase()}`);
|
|
34
|
+
const UNITLESS = /* @__PURE__ */ new Set([
|
|
35
|
+
"opacity",
|
|
36
|
+
"zIndex",
|
|
37
|
+
"fontWeight",
|
|
38
|
+
"lineHeight",
|
|
39
|
+
"order",
|
|
40
|
+
"flexGrow",
|
|
41
|
+
"flexShrink",
|
|
42
|
+
"columnCount",
|
|
43
|
+
"orphans",
|
|
44
|
+
"widows",
|
|
45
|
+
"zoom",
|
|
46
|
+
"fillOpacity",
|
|
47
|
+
"strokeOpacity"
|
|
48
|
+
]);
|
|
49
|
+
/** A bare number is pixels for every property that takes a length. */
|
|
50
|
+
const declarationValue = (property, value) => typeof value === "number" && value !== 0 && !UNITLESS.has(property) && !property.startsWith("--") ? `${value}px` : String(value);
|
|
51
|
+
const report = (sink, code, message, origins, severity = "error") => {
|
|
52
|
+
sink.diagnostics.push({
|
|
53
|
+
code,
|
|
54
|
+
severity,
|
|
55
|
+
message,
|
|
56
|
+
origins
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const declarations = (sink, style, origins) => {
|
|
60
|
+
const parts = [];
|
|
61
|
+
for (const [property, value] of Object.entries(style)) {
|
|
62
|
+
if (value === void 0 || value === null) continue;
|
|
63
|
+
if (!CSS_PROPERTY.test(property)) {
|
|
64
|
+
report(sink, "unsupported-style-property", `Style property ${JSON.stringify(property)} is not a CSS property name; write it in camelCase (backgroundColor) or as a custom property (--brand).`, origins);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
68
|
+
report(sink, "invalid-style-value", `Style ${property} must be a string or number, not ${typeof value}.`, origins);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const text = declarationValue(property, value);
|
|
72
|
+
if (CSS_UNSAFE.test(text) || CONTROL_CHARACTERS.test(text)) {
|
|
73
|
+
report(sink, "unsafe-style-value", `Style ${property} contains CSS that could break out of the declaration; remove ${JSON.stringify(text)}.`, origins);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let unsafeUrl;
|
|
77
|
+
for (const match of text.matchAll(CSS_URL)) if (!isSafeUrl(match[2] ?? "", sink.relativeUrls)) unsafeUrl = match[2] ?? "";
|
|
78
|
+
if (unsafeUrl !== void 0) {
|
|
79
|
+
report(sink, "unsafe-url", `Style ${property} points at ${JSON.stringify(unsafeUrl)}; email URLs must use https:, mailto:, tel:, cid: or a fragment${sink.relativeUrls ? ", or a relative path" : ""}.`, origins);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
parts.push(`${kebab(property)}:${text}`);
|
|
83
|
+
}
|
|
84
|
+
return parts.join(";");
|
|
85
|
+
};
|
|
86
|
+
const attributeValue = (sink, tag, name, spec, value, origins) => {
|
|
87
|
+
if (value === void 0 || value === null || value === false) return void 0;
|
|
88
|
+
if (value === true) return "";
|
|
89
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
90
|
+
report(sink, "invalid-attribute-value", `<${tag} ${name}> must be a string or number, not ${typeof value}.`, origins);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const text = String(value);
|
|
94
|
+
if (CONTROL_CHARACTERS.test(text)) {
|
|
95
|
+
report(sink, "invalid-attribute-value", `<${tag} ${name}> contains control characters.`, origins);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (spec.values !== void 0 && !spec.values.includes(text)) {
|
|
99
|
+
report(sink, "invalid-attribute-value", `<${tag} ${name}> accepts ${spec.values.join(", ")}; received ${JSON.stringify(text)}.`, origins);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (spec.type === "number" && !/^-?\d+$/.test(text)) {
|
|
103
|
+
report(sink, "invalid-attribute-value", `<${tag} ${name}> must be an integer.`, origins);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (spec.type === "measure" && !/^\d+(\.\d+)?%?$/.test(text)) {
|
|
107
|
+
report(sink, "invalid-attribute-value", `<${tag} ${name}> must be a pixel count or a percentage.`, origins);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (spec.type === "url" && !isSafeUrl(text, sink.relativeUrls)) {
|
|
111
|
+
report(sink, "unsafe-url", sink.relativeUrls ? `<${tag} ${name}> must use https:, mailto:, tel:, cid:, a fragment, or a relative path; received ${JSON.stringify(text)}.` : `<${tag} ${name}> must use https:, mailto:, tel:, cid: or a fragment; received ${JSON.stringify(text)}. A relative path resolves only in an export, never in a delivered message.`, origins);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
return text;
|
|
115
|
+
};
|
|
116
|
+
const attributes = (sink, tag, spec, props, origins) => {
|
|
117
|
+
const emitted = [];
|
|
118
|
+
const present = /* @__PURE__ */ new Set();
|
|
119
|
+
let classes;
|
|
120
|
+
for (const [key, value] of Object.entries(props)) {
|
|
121
|
+
if (key === "key") continue;
|
|
122
|
+
const name = key === "className" ? "class" : key;
|
|
123
|
+
if (name === "class") {
|
|
124
|
+
if (value === void 0 || value === null) continue;
|
|
125
|
+
if (typeof value !== "string") {
|
|
126
|
+
report(sink, "invalid-attribute-value", `<${tag} class> must be a string.`, origins);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (classes !== void 0) {
|
|
130
|
+
report(sink, "duplicate-attribute", `<${tag}> sets both class and className; keep one.`, origins);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
classes = value;
|
|
134
|
+
emitted.push(` class="${escapeAttribute(value)}"`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (name === "style") {
|
|
138
|
+
if (value === void 0 || value === null) continue;
|
|
139
|
+
if (!Predicate.isObject(value) || Array.isArray(value)) {
|
|
140
|
+
report(sink, "invalid-style", `<${tag} style> must be a property object, for example style={{ backgroundColor: "#fff" }}.`, origins);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const css = declarations(sink, value, origins);
|
|
144
|
+
if (css !== "") emitted.push(` style="${escapeAttribute(css)}"`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (/^on/i.test(name)) {
|
|
148
|
+
report(sink, "event-handler", `<${tag} ${name}> is an event handler; email clients strip scripting and Samva refuses it.`, origins);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (/^(data|aria)-[a-z0-9-]+$/.test(name)) {
|
|
152
|
+
const text = attributeValue(sink, tag, name, { type: "string" }, value, origins);
|
|
153
|
+
if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const attribute = EMAIL_GLOBAL_ATTRIBUTES[name] ?? spec.attributes[name];
|
|
157
|
+
if (attribute === void 0) {
|
|
158
|
+
const supported = [
|
|
159
|
+
...Object.keys(EMAIL_GLOBAL_ATTRIBUTES),
|
|
160
|
+
...Object.keys(spec.attributes),
|
|
161
|
+
"class",
|
|
162
|
+
"style"
|
|
163
|
+
].sort().join(", ");
|
|
164
|
+
report(sink, "unsupported-attribute", `<${tag}> does not support the ${name} attribute. Supported: ${supported}, plus data-* and aria-*.`, origins);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
present.add(name);
|
|
168
|
+
const text = attributeValue(sink, tag, name, attribute, value, origins);
|
|
169
|
+
if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
|
|
170
|
+
}
|
|
171
|
+
for (const [name, attribute] of Object.entries(spec.attributes)) if (attribute.required === true && !present.has(name)) report(sink, "missing-attribute", `<${tag}> requires the ${name} attribute.`, origins);
|
|
172
|
+
return emitted.join("");
|
|
173
|
+
};
|
|
174
|
+
/** Fragments are transparent to a content model; raw compatibility markup is not content. */
|
|
175
|
+
const contentChildren = (children) => children.flatMap((child) => child._tag === "Fragment" ? contentChildren(child.children) : [child]);
|
|
176
|
+
const checkContentModel = (sink, tag, spec, children, origins) => {
|
|
177
|
+
if (spec.content === "void") {
|
|
178
|
+
if (contentChildren(children).length > 0) report(sink, "void-element-children", `<${tag}> cannot have children.`, origins);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (spec.content === "text" || spec.content === "raw") {
|
|
182
|
+
for (const child of contentChildren(children)) if (child._tag !== "Text") report(sink, "invalid-content", `<${tag}> accepts text only.`, child._tag === "Element" ? child.origins : origins);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (spec.childElements === void 0) return;
|
|
186
|
+
for (const child of contentChildren(children)) {
|
|
187
|
+
if (child._tag === "Raw") continue;
|
|
188
|
+
if (child._tag === "Text") {
|
|
189
|
+
if (child.value.trim() !== "") report(sink, "invalid-content", `<${tag}> cannot contain text directly; put it inside <${spec.childElements[0]}>.`, origins);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (child._tag === "Element" && !spec.childElements.includes(child.tag)) report(sink, "invalid-content", `<${tag}> accepts only ${spec.childElements.map((name) => `<${name}>`).join(", ")}; found <${child.tag}>.`, child.origins);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/** Every write goes through here so `sink.written` stays the emitted offset. */
|
|
196
|
+
const emit = (sink, text) => {
|
|
197
|
+
sink.written += text.length;
|
|
198
|
+
return text;
|
|
199
|
+
};
|
|
200
|
+
const serialize = (sink, node, parentPath, counter) => {
|
|
201
|
+
switch (node._tag) {
|
|
202
|
+
case "Text": return emit(sink, escapeText(node.value));
|
|
203
|
+
case "Raw": return emit(sink, node.html);
|
|
204
|
+
case "Fragment": return node.children.map((child) => serialize(sink, child, parentPath, counter)).join("");
|
|
205
|
+
case "Element": {
|
|
206
|
+
const start = sink.written;
|
|
207
|
+
const index = counter.next++;
|
|
208
|
+
const path = parentPath === "" ? String(index) : `${parentPath}.${index}`;
|
|
209
|
+
const own = { next: 0 };
|
|
210
|
+
const stamp = sink.instancePaths ? ` ${INSTANCE_PATH_ATTRIBUTE}="${path}"` : "";
|
|
211
|
+
const spec = emailElement(node.tag);
|
|
212
|
+
if (spec === void 0) {
|
|
213
|
+
report(sink, "unsupported-element", `<${node.tag}> is not part of the supported email vocabulary. Supported elements: ${Object.keys(EMAIL_ELEMENTS).join(", ")}.`, node.origins);
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
checkContentModel(sink, node.tag, spec, node.children, node.origins);
|
|
217
|
+
if (spec.content === "raw") {
|
|
218
|
+
const css = node.children.map((child) => child._tag === "Text" ? child.value : "").join("");
|
|
219
|
+
if (/<\/|<!--/i.test(css)) {
|
|
220
|
+
report(sink, "unsafe-raw-text", `<${node.tag}> content cannot contain markup.`, node.origins);
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
const rawAttrs = attributes(sink, node.tag, spec, node.props, node.origins);
|
|
224
|
+
const raw = css === "" ? "" : `<${node.tag}${rawAttrs}${stamp}>${css}</${node.tag}>`;
|
|
225
|
+
return record(sink, start, node, path, emit(sink, raw));
|
|
226
|
+
}
|
|
227
|
+
const attrs = attributes(sink, node.tag, spec, node.props, node.origins);
|
|
228
|
+
if (spec.content === "void") return record(sink, start, node, path, emit(sink, `<${node.tag}${attrs}${stamp} />`));
|
|
229
|
+
const open = emit(sink, `<${node.tag}${attrs}${stamp}>`);
|
|
230
|
+
const children = node.children.map((child) => serialize(sink, child, path, own)).join("");
|
|
231
|
+
const close = emit(sink, `</${node.tag}>`);
|
|
232
|
+
return record(sink, start, node, path, `${open}${children}${close}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
/** Record an element's emitted range once its content is written. */
|
|
237
|
+
const record = (sink, start, node, instancePath, text) => {
|
|
238
|
+
sink.positions.push({
|
|
239
|
+
start,
|
|
240
|
+
end: sink.written,
|
|
241
|
+
origins: node.origins,
|
|
242
|
+
instancePath,
|
|
243
|
+
tag: node.tag,
|
|
244
|
+
authored: node.authored
|
|
245
|
+
});
|
|
246
|
+
return text;
|
|
247
|
+
};
|
|
248
|
+
/** Lower the derived tree to email HTML, reporting every unsupported construct with its origin. */
|
|
249
|
+
const serializeEmailHtml = (node, options = {}) => {
|
|
250
|
+
const sink = {
|
|
251
|
+
diagnostics: [],
|
|
252
|
+
positions: [],
|
|
253
|
+
written: 0,
|
|
254
|
+
relativeUrls: options.relativeUrls === true,
|
|
255
|
+
instancePaths: options.instancePaths === true
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
html: serialize(sink, node, "", { next: 0 }),
|
|
259
|
+
diagnostics: sink.diagnostics,
|
|
260
|
+
positions: sink.positions
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
//#endregion
|
|
264
|
+
export { declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml };
|
|
265
|
+
|
|
266
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { i as JsxSourceLocation, r as JsxSource } from "../source-locations-BCJQnIfU.js";
|
|
2
|
+
import { g as JSX, h as Fragment, m as EmailNode, p as EmailComponent } from "../elements-DtwKlFVH.js";
|
|
3
|
+
//#region src/email/jsx-dev-runtime.d.ts
|
|
4
|
+
/** Development JSX retains compiler-provided origins directly on the semantic tree. */
|
|
5
|
+
declare const jsxDEV: (type: string | EmailComponent<Record<string, unknown>> | typeof Fragment, props: Record<string, unknown> | null, _key?: unknown, _staticChildren?: boolean, source?: JsxSource) => {
|
|
6
|
+
readonly _tag: "Element";
|
|
7
|
+
readonly tag: string;
|
|
8
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
9
|
+
readonly children: readonly EmailNode[];
|
|
10
|
+
readonly origins: readonly JsxSourceLocation[];
|
|
11
|
+
readonly authored: boolean;
|
|
12
|
+
} | {
|
|
13
|
+
readonly _tag: "Fragment";
|
|
14
|
+
readonly children: readonly EmailNode[];
|
|
15
|
+
} | {
|
|
16
|
+
readonly _tag: "Raw";
|
|
17
|
+
readonly html: string;
|
|
18
|
+
readonly text: string;
|
|
19
|
+
readonly origins: readonly JsxSourceLocation[];
|
|
20
|
+
} | {
|
|
21
|
+
readonly _tag: "Text";
|
|
22
|
+
readonly value: string;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { Fragment, type JSX, jsxDEV };
|
|
26
|
+
//# sourceMappingURL=jsx-dev-runtime.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { JSX_SOURCE_LOCATION_PROP } from "../source-locations.js";
|
|
2
|
+
import { Fragment, jsx } from "./jsx-runtime.js";
|
|
3
|
+
//#region src/email/jsx-dev-runtime.ts
|
|
4
|
+
/** Development JSX retains compiler-provided origins directly on the semantic tree. */
|
|
5
|
+
const jsxDEV = (type, props, _key, _staticChildren, source) => jsx(type, {
|
|
6
|
+
...props,
|
|
7
|
+
[JSX_SOURCE_LOCATION_PROP]: props?.["__samva_source_location"] ?? source
|
|
8
|
+
});
|
|
9
|
+
//#endregion
|
|
10
|
+
export { Fragment, jsxDEV };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=jsx-dev-runtime.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { C as reportEmailDiagnostic, S as rawMarkup, _ as currentEmailOrigins, b as jsxs, f as EmailChild, g as JSX, h as Fragment, m as EmailNode, p as EmailComponent, v as drainEmailDiagnostics, x as normalizeChildren, y as jsx } from "../elements-DtwKlFVH.js";
|
|
2
|
+
export { EmailChild, EmailComponent, EmailNode, Fragment, JSX, currentEmailOrigins, drainEmailDiagnostics, jsx, jsxs, normalizeChildren, rawMarkup, reportEmailDiagnostic };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { JSX_SOURCE_LOCATION_PROP, completeJsxSourceLocation } from "../source-locations.js";
|
|
2
|
+
import { Data, Predicate } from "effect";
|
|
3
|
+
//#region src/email/jsx-runtime.ts
|
|
4
|
+
const EmailNode = Data.taggedEnum();
|
|
5
|
+
const Fragment = Symbol.for("@samva/markup/email/Fragment");
|
|
6
|
+
const callSites = [];
|
|
7
|
+
/** Call sites enclosing the element being built, innermost first. */
|
|
8
|
+
const currentEmailOrigins = () => [...callSites].reverse();
|
|
9
|
+
const reported = [];
|
|
10
|
+
/** Record a finding from inside a compiler primitive, located at the authoring call site. */
|
|
11
|
+
const reportEmailDiagnostic = (diagnostic) => {
|
|
12
|
+
reported.push({
|
|
13
|
+
...diagnostic,
|
|
14
|
+
origins: currentEmailOrigins()
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
/** Take and clear the findings primitives reported during a render. */
|
|
18
|
+
const drainEmailDiagnostics = () => reported.splice(0);
|
|
19
|
+
/** Normalize actual values without interpolation or recording proxies. */
|
|
20
|
+
const normalizeChildren = (input) => {
|
|
21
|
+
if (input === null || input === void 0 || typeof input === "boolean") return [];
|
|
22
|
+
if (typeof input === "string") return [EmailNode.Text({ value: input })];
|
|
23
|
+
if (typeof input === "number" && Number.isFinite(input)) return [EmailNode.Text({ value: String(input) })];
|
|
24
|
+
if (Array.isArray(input)) return input.flatMap(normalizeChildren);
|
|
25
|
+
if (Predicate.isTagged(input, "Text") || Predicate.isTagged(input, "Element") || Predicate.isTagged(input, "Fragment") || Predicate.isTagged(input, "Raw")) return [input];
|
|
26
|
+
throw new TypeError("Email children must be synchronous text, finite numbers, elements, or arrays");
|
|
27
|
+
};
|
|
28
|
+
/** Compatibility markup emitted by a compiler primitive, with the text it stands for. */
|
|
29
|
+
const rawMarkup = (html, text = "") => EmailNode.Raw({
|
|
30
|
+
html,
|
|
31
|
+
text,
|
|
32
|
+
origins: currentEmailOrigins()
|
|
33
|
+
});
|
|
34
|
+
/** Automatic JSX factory for ordinary HTML and synchronous imported components. */
|
|
35
|
+
const jsx = (type, props) => {
|
|
36
|
+
const { children, [JSX_SOURCE_LOCATION_PROP]: source, ...attributes } = props ?? {};
|
|
37
|
+
const origin = completeJsxSourceLocation(source);
|
|
38
|
+
if (type === Fragment) return EmailNode.Fragment({ children: normalizeChildren(children) });
|
|
39
|
+
if (typeof type === "string") return EmailNode.Element({
|
|
40
|
+
tag: type,
|
|
41
|
+
props: attributes,
|
|
42
|
+
children: normalizeChildren(children),
|
|
43
|
+
origins: origin === void 0 ? currentEmailOrigins() : [origin, ...currentEmailOrigins()],
|
|
44
|
+
authored: origin !== void 0
|
|
45
|
+
});
|
|
46
|
+
if (origin !== void 0) callSites.push(origin);
|
|
47
|
+
try {
|
|
48
|
+
return EmailNode.Fragment({ children: normalizeChildren(type({
|
|
49
|
+
...attributes,
|
|
50
|
+
children
|
|
51
|
+
})) });
|
|
52
|
+
} finally {
|
|
53
|
+
if (origin !== void 0) callSites.pop();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const jsxs = jsx;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { EmailNode, Fragment, currentEmailOrigins, drainEmailDiagnostics, jsx, jsxs, normalizeChildren, rawMarkup, reportEmailDiagnostic };
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=jsx-runtime.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { r as EmailDiagnostic } from "../diagnostics-Djd9ckLg.js";
|
|
2
|
+
import { m as EmailNode } from "../elements-DtwKlFVH.js";
|
|
3
|
+
import { EmittedPosition } from "./selection.js";
|
|
4
|
+
import { SerializeEmailOptions } from "./html.js";
|
|
5
|
+
import { r as TemplateDefinition } from "../template-Dy5yOPyp.js";
|
|
6
|
+
//#region src/email/render.d.ts
|
|
7
|
+
/** Classic Outlook lays a document out correctly only under a transitional doctype. */
|
|
8
|
+
declare const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
9
|
+
interface EmailCompileOutput {
|
|
10
|
+
readonly html: string;
|
|
11
|
+
readonly text: string;
|
|
12
|
+
/** Findings that did not block the compile; blocking findings throw. */
|
|
13
|
+
readonly diagnostics: readonly EmailDiagnostic[];
|
|
14
|
+
/**
|
|
15
|
+
* Each emitted element's offset range and origins. Compatibility checks run
|
|
16
|
+
* in the host over the emitted document, and this is what lets a finding name
|
|
17
|
+
* the authoring element rather than a character offset.
|
|
18
|
+
*/
|
|
19
|
+
readonly positions: readonly EmittedPosition[];
|
|
20
|
+
}
|
|
21
|
+
/** Lower the derived tree to HTML and its plain-text alternative. */
|
|
22
|
+
declare const compileEmail: (node: EmailNode, options?: SerializeEmailOptions) => EmailCompileOutput;
|
|
23
|
+
interface RenderedEmail {
|
|
24
|
+
readonly subject: string;
|
|
25
|
+
readonly preheader: string | undefined;
|
|
26
|
+
readonly html: string;
|
|
27
|
+
readonly text: string;
|
|
28
|
+
readonly tree: EmailNode;
|
|
29
|
+
readonly diagnostics: readonly EmailDiagnostic[];
|
|
30
|
+
readonly positions: readonly EmittedPosition[];
|
|
31
|
+
}
|
|
32
|
+
/** Render one validated input through the template's only content function. Host isolation is required for untrusted source. */
|
|
33
|
+
declare const renderEmail: <Input>(template: TemplateDefinition<Input>, data: unknown, options?: SerializeEmailOptions) => RenderedEmail;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { EMAIL_DOCTYPE, EmailCompileOutput, RenderedEmail, compileEmail, renderEmail };
|
|
36
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { i as JsxSourceLocation } from "../source-locations-BCJQnIfU.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
//#region src/email/selection.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The attribute a preview stamps so a click in a rendered document resolves to
|
|
6
|
+
* exactly one rendered element. A delivered message never carries it.
|
|
7
|
+
*/
|
|
8
|
+
declare const INSTANCE_PATH_ATTRIBUTE = "data-samva-instance";
|
|
9
|
+
/** Where one element sits in the emitted document, with the authoring that produced it. */
|
|
10
|
+
declare const EmittedPosition: Schema.Struct<{
|
|
11
|
+
readonly start: Schema.Int;
|
|
12
|
+
readonly end: Schema.Int;
|
|
13
|
+
readonly origins: Schema.$Array<Schema.Struct<{
|
|
14
|
+
readonly fileName: Schema.String;
|
|
15
|
+
readonly lineNumber: Schema.Int;
|
|
16
|
+
readonly columnNumber: Schema.Int;
|
|
17
|
+
}>>;
|
|
18
|
+
/**
|
|
19
|
+
* Dot-separated element indices from the document root, assigned as the
|
|
20
|
+
* document is written. It is the identity of one rendered instance.
|
|
21
|
+
*/
|
|
22
|
+
readonly instancePath: Schema.String;
|
|
23
|
+
readonly tag: Schema.String;
|
|
24
|
+
/** False when a compiler primitive generated this element. */
|
|
25
|
+
readonly authored: Schema.Boolean;
|
|
26
|
+
}>;
|
|
27
|
+
interface EmittedPosition extends Schema.Schema.Type<typeof EmittedPosition> {}
|
|
28
|
+
/** One rendered element, and the authoring it came from. */
|
|
29
|
+
declare const EmailElementSelection: Schema.Struct<{
|
|
30
|
+
/**
|
|
31
|
+
* Dot-separated element indices from the document root. It is the identity
|
|
32
|
+
* of one rendered instance, which is what tells two iterations of the same
|
|
33
|
+
* authored element apart: their origins are identical.
|
|
34
|
+
*/
|
|
35
|
+
readonly instancePath: Schema.String;
|
|
36
|
+
readonly tag: Schema.String;
|
|
37
|
+
/**
|
|
38
|
+
* The authored JSX element first, then each enclosing component call site,
|
|
39
|
+
* innermost first. Empty when the compiler generated this element with no
|
|
40
|
+
* authoring on the stack — the hidden preheader block, for instance.
|
|
41
|
+
*/
|
|
42
|
+
readonly origins: Schema.$Array<Schema.Struct<{
|
|
43
|
+
readonly fileName: Schema.String;
|
|
44
|
+
readonly lineNumber: Schema.Int;
|
|
45
|
+
readonly columnNumber: Schema.Int;
|
|
46
|
+
}>>;
|
|
47
|
+
/**
|
|
48
|
+
* False when a compiler primitive generated this element. `origins[0]` is
|
|
49
|
+
* then the authoring element that asked for it rather than this element
|
|
50
|
+
* itself, so the wrapper selects the primitive the author wrote.
|
|
51
|
+
*/
|
|
52
|
+
readonly authored: Schema.Boolean;
|
|
53
|
+
/** 1-based position among the rendered elements sharing this origin chain and tag. */
|
|
54
|
+
readonly occurrence: Schema.Int;
|
|
55
|
+
/**
|
|
56
|
+
* How many rendered elements share that chain and tag. Above one, one piece
|
|
57
|
+
* of authoring produced several rendered elements — a loop over a component,
|
|
58
|
+
* or a primitive that generates a repeated cell — so an edit there changes
|
|
59
|
+
* all of them.
|
|
60
|
+
*/
|
|
61
|
+
readonly occurrences: Schema.Int;
|
|
62
|
+
/** Absent for the document root. */
|
|
63
|
+
readonly parentPath: Schema.optionalKey<Schema.String>;
|
|
64
|
+
/** Offset range in the rendered HTML this selection was taken from. */
|
|
65
|
+
readonly start: Schema.Int;
|
|
66
|
+
readonly end: Schema.Int;
|
|
67
|
+
}>;
|
|
68
|
+
interface EmailElementSelection extends Schema.Schema.Type<typeof EmailElementSelection> {}
|
|
69
|
+
/**
|
|
70
|
+
* Turn one render's emitted positions into selections, in document order.
|
|
71
|
+
*
|
|
72
|
+
* The serializer records an element once its content is written, so positions
|
|
73
|
+
* arrive innermost-first; document order is what an outline and an ancestry
|
|
74
|
+
* walk both need.
|
|
75
|
+
*/
|
|
76
|
+
declare const emailSelections: (positions: readonly EmittedPosition[]) => readonly EmailElementSelection[];
|
|
77
|
+
/** Look selections up by the instance path a rendered document carries. */
|
|
78
|
+
declare const emailSelectionIndex: (selections: readonly EmailElementSelection[]) => ReadonlyMap<string, EmailElementSelection>;
|
|
79
|
+
/**
|
|
80
|
+
* The authoring element a selection edits. For an authored element that is the
|
|
81
|
+
* element itself; for a generated wrapper it is the primitive that asked for
|
|
82
|
+
* it. Undefined only where the compiler generated markup with no authoring on
|
|
83
|
+
* the stack, which nothing can edit in place.
|
|
84
|
+
*/
|
|
85
|
+
declare const emailAuthoringTarget: (selection: EmailElementSelection) => JsxSourceLocation | undefined;
|
|
86
|
+
/** Root-first ancestry of a selection, itself last. */
|
|
87
|
+
declare const emailSelectionAncestry: (index: ReadonlyMap<string, EmailElementSelection>, instancePath: string) => readonly EmailElementSelection[];
|
|
88
|
+
/**
|
|
89
|
+
* Every rendered instance produced through the same call site, in document
|
|
90
|
+
* order. One entry means the call ran once; several mean it repeated.
|
|
91
|
+
*/
|
|
92
|
+
declare const emailSelectionSiblings: (selections: readonly EmailElementSelection[], selection: EmailElementSelection) => readonly EmailElementSelection[];
|
|
93
|
+
/**
|
|
94
|
+
* Every rendered instance of the same authored element, across every call site
|
|
95
|
+
* that reaches it. A shared component edited at its definition changes all of
|
|
96
|
+
* these; edited at one call site it changes only `emailSelectionSiblings`. A
|
|
97
|
+
* control has to say which of the two it is doing before it applies.
|
|
98
|
+
*/
|
|
99
|
+
declare const emailDefinitionInstances: (selections: readonly EmailElementSelection[], selection: EmailElementSelection) => readonly EmailElementSelection[];
|
|
100
|
+
//#endregion
|
|
101
|
+
export { EmailElementSelection, EmittedPosition, INSTANCE_PATH_ATTRIBUTE, emailAuthoringTarget, emailDefinitionInstances, emailSelectionAncestry, emailSelectionIndex, emailSelectionSiblings, emailSelections };
|
|
102
|
+
//# sourceMappingURL=selection.d.ts.map
|