@samva/markup 0.9.0 → 0.10.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 +48 -40
- package/CHANGELOG.md +345 -0
- package/README.md +6 -6
- package/dist/{brand-B-0bN_Ir.d.ts → assets-BIsyMQ92.d.ts} +41 -11
- package/dist/{email/brand.js → brand-C0MKTwmX.js} +64 -4
- package/dist/{theme-css.d.ts → brand.d.ts} +12 -3
- package/dist/brand.js +4 -0
- package/dist/compiler.d.ts +231 -11
- package/dist/compiler.js +1312 -13
- package/dist/components-BZCqegS8.js +1864 -0
- package/dist/diagnostic-model-DJtqbETd.js +38 -0
- package/dist/diagnostic-model-DxthUZBT.d.ts +31 -0
- package/dist/{email/diagnostics.js → diagnostics-CnKFefKz.js} +2 -2
- package/dist/diagnostics.d.ts +406 -27
- package/dist/diagnostics.js +402 -34
- package/dist/edit.d.ts +234 -0
- package/dist/{channel-edit.js → edit.js} +341 -3
- package/dist/email/jsx-dev-runtime.d.ts +1 -1
- package/dist/email/jsx-dev-runtime.js +1 -2
- package/dist/email/jsx-runtime.d.ts +1 -1
- package/dist/email/jsx-runtime.js +1 -85
- package/dist/{email/components.d.ts → email.d.ts} +2 -2
- package/dist/email.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{cascade-t6kxlgEI.js → ir-CLhlklKV.js} +8 -4
- package/dist/jsx-runtime-CBlexoeR.js +98 -0
- package/dist/{elements-cqldpaJt.d.ts → jsx-runtime-ylIOnaSf.d.ts} +74 -74
- package/dist/{profile-BH-OtdDA.js → profile-pWGtWZXF.js} +153 -20
- package/dist/render-BPonDpCl.d.ts +539 -0
- package/dist/{email/render.js → render-BQmfh94k.js} +102 -8
- package/dist/render.d.ts +35 -0
- package/dist/render.js +49 -0
- package/dist/sms.d.ts +1 -1
- package/dist/sms.js +1 -1
- package/dist/{template-_QE_rQND.d.ts → template-DMPwxMSq.d.ts} +2 -2
- package/dist/{template.js → template-zLUjcwfF.js} +2 -2
- package/dist/{theme-css-hFqcPSar.js → theme-css-JtRCm1fb.js} +2 -2
- package/dist/whatsapp.d.ts +1 -1
- package/dist/whatsapp.js +1 -1
- package/generated/contract.json +16 -10
- package/generated/reference.md +17 -10
- package/package.json +36 -119
- package/samva-brand.d.ts +4 -4
- package/skills/sml/SKILL.md +2 -2
- package/dist/assets-C5duwTya.d.ts +0 -40
- package/dist/channel-edit.d.ts +0 -121
- package/dist/channel-specs-B6M_HlaV.js +0 -29
- package/dist/compatibility-DbISoFIq.js +0 -589
- package/dist/css-BVQKICKC.d.ts +0 -109
- package/dist/diagnostic-codes.d.ts +0 -403
- package/dist/diagnostic-codes.js +0 -399
- package/dist/email/assets.d.ts +0 -2
- package/dist/email/assets.js +0 -63
- package/dist/email/brand-footer.d.ts +0 -34
- package/dist/email/brand-footer.js +0 -63
- package/dist/email/brand.d.ts +0 -2
- package/dist/email/components.js +0 -275
- package/dist/email/diagnostics.d.ts +0 -2
- package/dist/email/elements.d.ts +0 -2
- package/dist/email/elements.js +0 -299
- package/dist/email/html.d.ts +0 -2
- package/dist/email/html.js +0 -321
- package/dist/email/render.d.ts +0 -40
- package/dist/email/text.d.ts +0 -7
- package/dist/email/text.js +0 -100
- package/dist/html-BJepOwZ4.d.ts +0 -56
- package/dist/ir-aaqNE987.d.ts +0 -252
- package/dist/ir.d.ts +0 -2
- package/dist/ir.js +0 -7
- package/dist/preview-Ekdg1dXG.d.ts +0 -100
- package/dist/preview.d.ts +0 -2
- package/dist/preview.js +0 -160
- package/dist/render-ir.d.ts +0 -19
- package/dist/render-ir.js +0 -826
- package/dist/sms-segments.d.ts +0 -13
- package/dist/sms-segments.js +0 -46
- package/dist/source-edit.d.ts +0 -116
- package/dist/source-edit.js +0 -343
- package/dist/source-locations.d.ts +0 -2
- package/dist/source-locations.js +0 -16
- package/dist/static.d.ts +0 -57
- package/dist/static.js +0 -69
- package/dist/tailwind-CMYpZf1v.js +0 -614
- package/dist/template.d.ts +0 -2
- package/dist/theme-css.js +0 -2
- package/dist/theme-imports-lvBw3AD1.js +0 -109
- package/dist/transform-Ct2OMkFN.d.ts +0 -153
package/dist/email/html.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
import { n as isObject } from "../guards-eV1qLbf2.js";
|
|
2
|
-
import { i as UNSUBSCRIBE_URL_PLACEHOLDER, n as RESERVED_FOOTER_ATTRIBUTES, r as UNSUBSCRIBE_LINK, t as BRAND_FOOTER_MARKER } from "../unsubscribe-link-CoesnQDU.js";
|
|
3
|
-
import { EMAIL_ELEMENTS, EMAIL_GLOBAL_ATTRIBUTES, emailElement } from "./elements.js";
|
|
4
|
-
import { INSTANCE_PATH_ATTRIBUTE } from "../preview.js";
|
|
5
|
-
//#region src/email/html.ts
|
|
6
|
-
/** Escape a value for element content. */
|
|
7
|
-
const escapeText = (value) => value.replace(/[&<>]/g, (character) => ({
|
|
8
|
-
"&": "&",
|
|
9
|
-
"<": "<",
|
|
10
|
-
">": ">"
|
|
11
|
-
})[character] ?? character);
|
|
12
|
-
/** Escape a value for a double-quoted attribute. */
|
|
13
|
-
const escapeAttribute = (value) => value.replace(/[&<>"']/g, (character) => ({
|
|
14
|
-
"&": "&",
|
|
15
|
-
"<": "<",
|
|
16
|
-
">": ">",
|
|
17
|
-
"\"": """,
|
|
18
|
-
"'": "'"
|
|
19
|
-
})[character] ?? character);
|
|
20
|
-
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
21
|
-
const SAFE_URL_SCHEME = /^(https:\/\/|mailto:|tel:|cid:|#)/i;
|
|
22
|
-
const RELATIVE_URL = /^(\.{1,2}\/|\/(?!\/))/;
|
|
23
|
-
/**
|
|
24
|
-
* Only references an email client will follow safely; no `javascript:`, no
|
|
25
|
-
* `data:`. A relative reference is legal only for an export: a file on disk can
|
|
26
|
-
* resolve one against its own directory, and a delivered message cannot resolve
|
|
27
|
-
* anything but an absolute URL.
|
|
28
|
-
*/
|
|
29
|
-
const isSafeUrl = (value, relative) => !/^[\u0000-\u0020]/.test(value) && !CONTROL_CHARACTERS.test(value) && !value.includes("\\") && (SAFE_URL_SCHEME.test(value) || relative && RELATIVE_URL.test(value));
|
|
30
|
-
const CSS_PROPERTY = /^(--[a-zA-Z0-9-]+|[a-zA-Z][a-zA-Z0-9-]*)$/;
|
|
31
|
-
const CSS_UNSAFE = /[;{}<>\\]|expression\s*\(|@import|\/\*/i;
|
|
32
|
-
const CSS_URL = /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi;
|
|
33
|
-
/** camelCase to kebab-case; already-kebab names pass through unchanged. */
|
|
34
|
-
const kebab = (property) => property.startsWith("--") ? property : property.replace(/[A-Z]/g, (l) => `-${l.toLowerCase()}`);
|
|
35
|
-
const UNITLESS = /* @__PURE__ */ new Set([
|
|
36
|
-
"opacity",
|
|
37
|
-
"zIndex",
|
|
38
|
-
"fontWeight",
|
|
39
|
-
"lineHeight",
|
|
40
|
-
"order",
|
|
41
|
-
"flexGrow",
|
|
42
|
-
"flexShrink",
|
|
43
|
-
"columnCount",
|
|
44
|
-
"orphans",
|
|
45
|
-
"widows",
|
|
46
|
-
"zoom",
|
|
47
|
-
"fillOpacity",
|
|
48
|
-
"strokeOpacity"
|
|
49
|
-
]);
|
|
50
|
-
/** A bare number is pixels for every property that takes a length. */
|
|
51
|
-
const declarationValue = (property, value) => typeof value === "number" && value !== 0 && !UNITLESS.has(property) && !property.startsWith("--") ? `${value}px` : String(value);
|
|
52
|
-
const report = (sink, code, message, origins, severity = "error") => {
|
|
53
|
-
sink.diagnostics.push({
|
|
54
|
-
code,
|
|
55
|
-
severity,
|
|
56
|
-
message,
|
|
57
|
-
origins
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
const declarations = (sink, style, origins) => {
|
|
61
|
-
const parts = [];
|
|
62
|
-
for (const [property, value] of Object.entries(style)) {
|
|
63
|
-
if (value === void 0 || value === null) continue;
|
|
64
|
-
if (!CSS_PROPERTY.test(property)) {
|
|
65
|
-
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);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (typeof value !== "string" && typeof value !== "number") {
|
|
69
|
-
report(sink, "invalid-style-value", `Style ${property} must be a string or number, not ${typeof value}.`, origins);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const text = declarationValue(property, value);
|
|
73
|
-
if (CSS_UNSAFE.test(text) || CONTROL_CHARACTERS.test(text)) {
|
|
74
|
-
report(sink, "unsafe-style-value", `Style ${property} contains CSS that could break out of the declaration; remove ${JSON.stringify(text)}.`, origins);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
let unsafeUrl;
|
|
78
|
-
for (const match of text.matchAll(CSS_URL)) if (!isSafeUrl(match[2] ?? "", sink.relativeUrls)) unsafeUrl = match[2] ?? "";
|
|
79
|
-
if (unsafeUrl !== void 0) {
|
|
80
|
-
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);
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
parts.push(`${kebab(property)}:${text}`);
|
|
84
|
-
}
|
|
85
|
-
return parts.join(";");
|
|
86
|
-
};
|
|
87
|
-
const attributeValue = (sink, tag, name, spec, value, origins) => {
|
|
88
|
-
if (value === void 0 || value === null || value === false) return void 0;
|
|
89
|
-
if (value === true) return "";
|
|
90
|
-
if (typeof value !== "string" && typeof value !== "number") {
|
|
91
|
-
report(sink, "invalid-attribute-value", `<${tag} ${name}> must be a string or number, not ${typeof value}.`, origins);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const text = String(value);
|
|
95
|
-
if (CONTROL_CHARACTERS.test(text)) {
|
|
96
|
-
report(sink, "invalid-attribute-value", `<${tag} ${name}> contains control characters.`, origins);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (spec.values !== void 0 && !spec.values.includes(text)) {
|
|
100
|
-
report(sink, "invalid-attribute-value", `<${tag} ${name}> accepts ${spec.values.join(", ")}; received ${JSON.stringify(text)}.`, origins);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (spec.type === "number" && !/^-?\d+$/.test(text)) {
|
|
104
|
-
report(sink, "invalid-attribute-value", `<${tag} ${name}> must be an integer.`, origins);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (spec.type === "measure" && !/^\d+(\.\d+)?%?$/.test(text)) {
|
|
108
|
-
report(sink, "invalid-attribute-value", `<${tag} ${name}> must be a pixel count or a percentage.`, origins);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (spec.type === "url" && !isSafeUrl(text, sink.relativeUrls)) {
|
|
112
|
-
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);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
return text;
|
|
116
|
-
};
|
|
117
|
-
const attributes = (sink, tag, spec, props, origins) => {
|
|
118
|
-
const emitted = [];
|
|
119
|
-
const present = /* @__PURE__ */ new Set();
|
|
120
|
-
let classes;
|
|
121
|
-
const unsubscribeLink = tag === "a" && props[UNSUBSCRIBE_LINK] === true;
|
|
122
|
-
for (const [key, value] of Object.entries(props)) {
|
|
123
|
-
if (key === "key") continue;
|
|
124
|
-
const name = key === "className" ? "class" : key;
|
|
125
|
-
if (unsubscribeLink && name === "href" && value === "%samva:unsubscribe-url%") {
|
|
126
|
-
present.add(name);
|
|
127
|
-
emitted.push(` href="${UNSUBSCRIBE_URL_PLACEHOLDER}" ses:no-track`);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (name === "class") {
|
|
131
|
-
if (value === void 0 || value === null) continue;
|
|
132
|
-
if (typeof value !== "string") {
|
|
133
|
-
report(sink, "invalid-attribute-value", `<${tag} class> must be a string.`, origins);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (classes !== void 0) {
|
|
137
|
-
report(sink, "duplicate-attribute", `<${tag}> sets both class and className; keep one.`, origins);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
classes = value;
|
|
141
|
-
emitted.push(` class="${escapeAttribute(value)}"`);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (name === "style") {
|
|
145
|
-
if (value === void 0 || value === null) continue;
|
|
146
|
-
if (!isObject(value) || Array.isArray(value)) {
|
|
147
|
-
report(sink, "invalid-style", `<${tag} style> must be a property object, for example style={{ backgroundColor: "#fff" }}.`, origins);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const css = declarations(sink, value, origins);
|
|
151
|
-
if (css !== "") emitted.push(` style="${escapeAttribute(css)}"`);
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (/^on/i.test(name)) {
|
|
155
|
-
report(sink, "event-handler", `<${tag} ${name}> is an event handler; email clients strip scripting and Samva refuses it.`, origins);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (RESERVED_FOOTER_ATTRIBUTES.has(name) && props[BRAND_FOOTER_MARKER] !== true) {
|
|
159
|
-
report(sink, "reserved-attribute", `<${tag} ${name}> is written only by BrandFooter from samva:brand; render BrandFooter instead of marking an element as the footer.`, origins);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
if (/^(data|aria)-[a-z0-9-]+$/.test(name)) {
|
|
163
|
-
const text = attributeValue(sink, tag, name, { type: "string" }, value, origins);
|
|
164
|
-
if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const globalAttributes = EMAIL_GLOBAL_ATTRIBUTES;
|
|
168
|
-
const attribute = Object.hasOwn(globalAttributes, name) ? globalAttributes[name] : Object.hasOwn(spec.attributes, name) ? spec.attributes[name] : void 0;
|
|
169
|
-
if (attribute === void 0) {
|
|
170
|
-
const supported = [
|
|
171
|
-
...Object.keys(EMAIL_GLOBAL_ATTRIBUTES),
|
|
172
|
-
...Object.keys(spec.attributes),
|
|
173
|
-
"class",
|
|
174
|
-
"style"
|
|
175
|
-
].sort().join(", ");
|
|
176
|
-
report(sink, "unsupported-attribute", `<${tag}> does not support the ${name} attribute. Supported: ${supported}, plus data-* and aria-*.`, origins);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
present.add(name);
|
|
180
|
-
const text = attributeValue(sink, tag, name, attribute, value, origins);
|
|
181
|
-
if (text !== void 0) emitted.push(` ${name}="${escapeAttribute(text)}"`);
|
|
182
|
-
}
|
|
183
|
-
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);
|
|
184
|
-
return emitted.join("");
|
|
185
|
-
};
|
|
186
|
-
/** Fragments are transparent to a content model; raw compatibility markup is not content. */
|
|
187
|
-
const contentChildren = (children) => children.flatMap((child) => child.type === "Fragment" ? contentChildren(child.children) : [child]);
|
|
188
|
-
const checkContentModel = (sink, tag, spec, children, origins) => {
|
|
189
|
-
if (spec.content === "void") {
|
|
190
|
-
if (contentChildren(children).length > 0) report(sink, "void-element-children", `<${tag}> cannot have children.`, origins);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (spec.content === "text" || spec.content === "raw") {
|
|
194
|
-
for (const child of contentChildren(children)) if (child.type !== "Text") report(sink, "invalid-content", `<${tag}> accepts text only.`, child.type === "Element" ? child.origins : origins);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (spec.childElements === void 0) return;
|
|
198
|
-
for (const child of contentChildren(children)) {
|
|
199
|
-
if (child.type === "Raw") continue;
|
|
200
|
-
if (child.type === "Text") {
|
|
201
|
-
if (child.value.trim() !== "") report(sink, "invalid-content", `<${tag}> cannot contain text directly; put it inside <${spec.childElements[0]}>.`, origins);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (child.type === "Element" && !spec.childElements.includes(child.tag)) report(sink, "invalid-content", `<${tag}> accepts only ${spec.childElements.map((name) => `<${name}>`).join(", ")}; found <${child.tag}>.`, child.origins);
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
/** Every write goes through here so `sink.written` stays the emitted offset. */
|
|
208
|
-
const emit = (sink, text) => {
|
|
209
|
-
sink.written += text.length;
|
|
210
|
-
return text;
|
|
211
|
-
};
|
|
212
|
-
const serialize = (sink, node, parentPath, counter) => {
|
|
213
|
-
switch (node.type) {
|
|
214
|
-
case "Text": return emit(sink, escapeText(node.value));
|
|
215
|
-
case "Raw": return emit(sink, node.html);
|
|
216
|
-
case "Fragment": return node.children.map((child) => serialize(sink, child, parentPath, counter)).join("");
|
|
217
|
-
case "Element": {
|
|
218
|
-
const start = sink.written;
|
|
219
|
-
const index = counter.next++;
|
|
220
|
-
const path = parentPath === "" ? String(index) : `${parentPath}.${index}`;
|
|
221
|
-
const own = { next: 0 };
|
|
222
|
-
const stamp = sink.instancePaths ? ` ${INSTANCE_PATH_ATTRIBUTE}="${path}"` : "";
|
|
223
|
-
const spec = emailElement(node.tag);
|
|
224
|
-
if (spec === void 0) {
|
|
225
|
-
report(sink, "unsupported-element", `<${node.tag}> is not part of the supported email vocabulary. Supported elements: ${Object.keys(EMAIL_ELEMENTS).join(", ")}.`, node.origins);
|
|
226
|
-
return "";
|
|
227
|
-
}
|
|
228
|
-
checkContentModel(sink, node.tag, spec, node.children, node.origins);
|
|
229
|
-
if (spec.content === "raw") {
|
|
230
|
-
const css = node.children.map((child) => child.type === "Text" ? child.value : "").join("");
|
|
231
|
-
if (/<\/|<!--/i.test(css)) {
|
|
232
|
-
report(sink, "unsafe-raw-text", `<${node.tag}> content cannot contain markup.`, node.origins);
|
|
233
|
-
return "";
|
|
234
|
-
}
|
|
235
|
-
const rawAttrs = attributes(sink, node.tag, spec, node.props, node.origins);
|
|
236
|
-
const raw = css === "" ? "" : `<${node.tag}${rawAttrs}${stamp}>${css}</${node.tag}>`;
|
|
237
|
-
return record(sink, start, node, path, emit(sink, raw));
|
|
238
|
-
}
|
|
239
|
-
const attrs = attributes(sink, node.tag, spec, node.props, node.origins);
|
|
240
|
-
if (spec.content === "void") return record(sink, start, node, path, emit(sink, `<${node.tag}${attrs}${stamp} />`));
|
|
241
|
-
const open = emit(sink, `<${node.tag}${attrs}${stamp}>`);
|
|
242
|
-
const children = node.children.map((child) => serialize(sink, child, path, own)).join("");
|
|
243
|
-
const close = emit(sink, `</${node.tag}>`);
|
|
244
|
-
return record(sink, start, node, path, `${open}${children}${close}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
/** Record an element's emitted range once its content is written. */
|
|
249
|
-
const record = (sink, start, node, instancePath, text) => {
|
|
250
|
-
sink.positions.push({
|
|
251
|
-
start,
|
|
252
|
-
end: sink.written,
|
|
253
|
-
origins: node.origins,
|
|
254
|
-
instancePath,
|
|
255
|
-
tag: node.tag,
|
|
256
|
-
authored: node.authored
|
|
257
|
-
});
|
|
258
|
-
return text;
|
|
259
|
-
};
|
|
260
|
-
/** Lower the derived tree to email HTML, reporting every unsupported construct with its origin. */
|
|
261
|
-
const serializeEmailHtml = (node, options = {}) => {
|
|
262
|
-
const sink = {
|
|
263
|
-
diagnostics: [],
|
|
264
|
-
positions: [],
|
|
265
|
-
written: 0,
|
|
266
|
-
relativeUrls: options.relativeUrls === true,
|
|
267
|
-
instancePaths: options.instancePaths === true
|
|
268
|
-
};
|
|
269
|
-
return {
|
|
270
|
-
html: serialize(sink, node, "", { next: 0 }),
|
|
271
|
-
diagnostics: sink.diagnostics,
|
|
272
|
-
positions: sink.positions
|
|
273
|
-
};
|
|
274
|
-
};
|
|
275
|
-
/** An authored style object as a declaration list, checked by the same rules as a serialized tree. */
|
|
276
|
-
const serializeStyle = (style, origins) => {
|
|
277
|
-
const sink = {
|
|
278
|
-
diagnostics: [],
|
|
279
|
-
positions: [],
|
|
280
|
-
written: 0,
|
|
281
|
-
relativeUrls: false,
|
|
282
|
-
instancePaths: false
|
|
283
|
-
};
|
|
284
|
-
return {
|
|
285
|
-
css: declarations(sink, style, origins),
|
|
286
|
-
diagnostics: sink.diagnostics
|
|
287
|
-
};
|
|
288
|
-
};
|
|
289
|
-
/**
|
|
290
|
-
* Check one static attribute value against its element's spec, as serialization does. Returns
|
|
291
|
-
* the text that would be emitted, or `undefined` when the attribute is dropped or invalid.
|
|
292
|
-
*/
|
|
293
|
-
const checkAttribute = (tag, name, value, origins) => {
|
|
294
|
-
const sink = {
|
|
295
|
-
diagnostics: [],
|
|
296
|
-
positions: [],
|
|
297
|
-
written: 0,
|
|
298
|
-
relativeUrls: false,
|
|
299
|
-
instancePaths: false
|
|
300
|
-
};
|
|
301
|
-
const element = emailElement(tag);
|
|
302
|
-
const globals = EMAIL_GLOBAL_ATTRIBUTES;
|
|
303
|
-
const attributeSpec = () => {
|
|
304
|
-
if (/^(data|aria)-[a-z0-9-]+$/.test(name)) return { type: "string" };
|
|
305
|
-
if (Object.hasOwn(globals, name)) return globals[name];
|
|
306
|
-
if (element !== void 0 && Object.hasOwn(element.attributes, name)) return element.attributes[name];
|
|
307
|
-
};
|
|
308
|
-
const spec = attributeSpec();
|
|
309
|
-
if (spec === void 0) return {
|
|
310
|
-
text: void 0,
|
|
311
|
-
diagnostics: sink.diagnostics
|
|
312
|
-
};
|
|
313
|
-
return {
|
|
314
|
-
text: attributeValue(sink, tag, name, spec, value, origins),
|
|
315
|
-
diagnostics: sink.diagnostics
|
|
316
|
-
};
|
|
317
|
-
};
|
|
318
|
-
//#endregion
|
|
319
|
-
export { checkAttribute, declarationValue, escapeAttribute, escapeText, kebab, serializeEmailHtml, serializeStyle };
|
|
320
|
-
|
|
321
|
-
//# sourceMappingURL=html.js.map
|
package/dist/email/render.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { r as EmailDiagnostic } from "../diagnostics-B-Gym34R.js";
|
|
2
|
-
import { m as EmailNode } from "../elements-cqldpaJt.js";
|
|
3
|
-
import { n as EmittedPosition } from "../preview-Ekdg1dXG.js";
|
|
4
|
-
import { n as SerializeEmailOptions } from "../html-BJepOwZ4.js";
|
|
5
|
-
import { n as EmailContent } from "../template-_QE_rQND.js";
|
|
6
|
-
import { i as Stylesheet } from "../css-BVQKICKC.js";
|
|
7
|
-
//#region src/email/render.d.ts
|
|
8
|
-
/** Classic Outlook lays a document out correctly only under a transitional doctype. */
|
|
9
|
-
declare const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
10
|
-
interface EmailCompileOutput {
|
|
11
|
-
readonly html: string;
|
|
12
|
-
readonly text: string;
|
|
13
|
-
/** Findings that did not block the compile; blocking findings throw. */
|
|
14
|
-
readonly diagnostics: readonly EmailDiagnostic[];
|
|
15
|
-
/**
|
|
16
|
-
* Each emitted element's offset range and origins. Compatibility checks run
|
|
17
|
-
* in the host over the emitted document, and this is what lets a finding name
|
|
18
|
-
* the authoring element rather than a character offset.
|
|
19
|
-
*/
|
|
20
|
-
readonly positions: readonly EmittedPosition[];
|
|
21
|
-
}
|
|
22
|
-
/** Lower the derived tree to HTML and its plain-text alternative. */
|
|
23
|
-
declare const compileEmail: (node: EmailNode, options?: SerializeEmailOptions) => EmailCompileOutput;
|
|
24
|
-
interface RenderedEmail {
|
|
25
|
-
readonly subject: string;
|
|
26
|
-
readonly preheader: string | undefined;
|
|
27
|
-
readonly html: string;
|
|
28
|
-
readonly text: string;
|
|
29
|
-
readonly tree: EmailNode;
|
|
30
|
-
readonly diagnostics: readonly EmailDiagnostic[];
|
|
31
|
-
readonly positions: readonly EmittedPosition[];
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Render authored content against a stylesheet: inline what a client honors,
|
|
35
|
-
* keep the rest for `<head>`, and lower to HTML and text.
|
|
36
|
-
*/
|
|
37
|
-
declare const renderEmailContent: (stylesheet: Stylesheet | undefined, render: () => EmailContent, options?: SerializeEmailOptions) => RenderedEmail;
|
|
38
|
-
//#endregion
|
|
39
|
-
export { EMAIL_DOCTYPE, EmailCompileOutput, RenderedEmail, compileEmail, renderEmailContent };
|
|
40
|
-
//# sourceMappingURL=render.d.ts.map
|
package/dist/email/text.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { m as EmailNode } from "../elements-cqldpaJt.js";
|
|
2
|
-
//#region src/email/text.d.ts
|
|
3
|
-
/** Derive the plain-text alternative from the semantic tree in reading order. */
|
|
4
|
-
declare const deriveEmailText: (node: EmailNode) => string;
|
|
5
|
-
//#endregion
|
|
6
|
-
export { deriveEmailText };
|
|
7
|
-
//# sourceMappingURL=text.d.ts.map
|
package/dist/email/text.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { emailElement } from "./elements.js";
|
|
2
|
-
//#region src/email/text.ts
|
|
3
|
-
const PARAGRAPH = "\n\n";
|
|
4
|
-
const LINE = "\n";
|
|
5
|
-
const INVISIBLE = /\u200b|\u200c|\u200d|\u200e|\u200f|\u2060|\ufeff/g;
|
|
6
|
-
const isLayoutTable = (props) => props.role === "presentation" || props.role === "none";
|
|
7
|
-
/**
|
|
8
|
-
* A data-table cell that reads as nothing and holds only imagery with empty alt text carries no
|
|
9
|
-
* column of its own; an empty cell without such an image is a positional placeholder.
|
|
10
|
-
*/
|
|
11
|
-
const isDecorativeCell = (cell, context) => {
|
|
12
|
-
const images = collect(cell.children, (tag) => tag === "img", () => true);
|
|
13
|
-
return images.length > 0 && images.every((image) => image.props.alt === "") && childText(cell, context).trim() === "";
|
|
14
|
-
};
|
|
15
|
-
/** Wrap a block's content in separators; empty blocks contribute nothing. */
|
|
16
|
-
const block = (inner, separator) => {
|
|
17
|
-
const trimmed = inner.replace(/^\s+/, "").replace(/\s+$/, "");
|
|
18
|
-
return trimmed === "" ? "" : `${separator}${trimmed}${separator}`;
|
|
19
|
-
};
|
|
20
|
-
const collect = (nodes, keep, descend) => nodes.flatMap((child) => {
|
|
21
|
-
if (child.type === "Fragment") return collect(child.children, keep, descend);
|
|
22
|
-
if (child.type !== "Element") return [];
|
|
23
|
-
if (keep(child.tag)) return [child];
|
|
24
|
-
return descend(child.tag) ? collect(child.children, keep, descend) : [];
|
|
25
|
-
});
|
|
26
|
-
const TABLE_SECTIONS = /* @__PURE__ */ new Set([
|
|
27
|
-
"thead",
|
|
28
|
-
"tbody",
|
|
29
|
-
"tfoot"
|
|
30
|
-
]);
|
|
31
|
-
const childText = (node, context) => node.children.map((child) => derive(child, context)).join("");
|
|
32
|
-
const derive = (node, context) => {
|
|
33
|
-
switch (node.type) {
|
|
34
|
-
case "Text": return context.preformatted ? node.value : node.value.replace(INVISIBLE, "").replace(/[^\S\n]+/g, " ");
|
|
35
|
-
case "Raw": return node.text;
|
|
36
|
-
case "Fragment": return node.children.map((child) => derive(child, context)).join("");
|
|
37
|
-
case "Element": {
|
|
38
|
-
const spec = emailElement(node.tag);
|
|
39
|
-
if (spec === void 0) return childText(node, context);
|
|
40
|
-
switch (spec.layout) {
|
|
41
|
-
case "metadata": return "";
|
|
42
|
-
case "document":
|
|
43
|
-
case "inline":
|
|
44
|
-
case "list-item": return childText(node, context);
|
|
45
|
-
case "break": return LINE;
|
|
46
|
-
case "rule": return `${PARAGRAPH}---${PARAGRAPH}`;
|
|
47
|
-
case "image": {
|
|
48
|
-
const alt = typeof node.props.alt === "string" ? node.props.alt.trim() : "";
|
|
49
|
-
return alt === "" ? "" : `[${alt}]`;
|
|
50
|
-
}
|
|
51
|
-
case "link": {
|
|
52
|
-
const label = childText(node, context).replace(/\s+/g, " ").trim();
|
|
53
|
-
const href = typeof node.props.href === "string" ? node.props.href : "";
|
|
54
|
-
if (href === "" || href === label) return label;
|
|
55
|
-
return label === "" ? href : `${label} (${href})`;
|
|
56
|
-
}
|
|
57
|
-
case "list": {
|
|
58
|
-
const indent = " ".repeat(context.listDepth);
|
|
59
|
-
const start = Number(node.props.start ?? 1);
|
|
60
|
-
const first = Number.isFinite(start) ? start : 1;
|
|
61
|
-
const nested = {
|
|
62
|
-
...context,
|
|
63
|
-
listDepth: context.listDepth + 1
|
|
64
|
-
};
|
|
65
|
-
const lines = collect(node.children, (tag) => tag === "li", () => false).map((item, index) => {
|
|
66
|
-
const marker = node.tag === "ol" ? `${first + index}.` : "-";
|
|
67
|
-
const continuation = `${indent}${" ".repeat(marker.length + 1)}`;
|
|
68
|
-
const [head = "", ...rest] = derive(item, nested).replace(/^\s+/, "").replace(/\s+$/, "").replace(/\n{2,}/g, LINE).split("\n");
|
|
69
|
-
return [`${indent}${marker} ${head}`, ...rest.map((line) => line.trim() === "" ? "" : /^\s/.test(line) ? line : `${continuation}${line}`)].join(LINE);
|
|
70
|
-
});
|
|
71
|
-
return block(lines.join(LINE), PARAGRAPH);
|
|
72
|
-
}
|
|
73
|
-
case "table": {
|
|
74
|
-
if (isLayoutTable(node.props)) return childText(node, context);
|
|
75
|
-
const rows = collect(node.children, (tag) => tag === "tr", (tag) => TABLE_SECTIONS.has(tag)).map((row) => collect(row.children, (tag) => tag === "td" || tag === "th", () => false).filter((cell) => !isDecorativeCell(cell, context)).map((cell) => childText(cell, context).replace(/\s+/g, " ").trim()).join(" | "));
|
|
76
|
-
return block(rows.join(LINE), PARAGRAPH);
|
|
77
|
-
}
|
|
78
|
-
case "row": return block(collect(node.children, (tag) => tag === "td" || tag === "th", () => false).map((cell) => childText(cell, context).replace(/^\s+|\s+$/g, "")).filter((cell) => cell !== "").join(LINE), LINE);
|
|
79
|
-
case "table-section":
|
|
80
|
-
case "cell": return block(childText(node, context), LINE);
|
|
81
|
-
case "preformatted": return block(childText(node, {
|
|
82
|
-
...context,
|
|
83
|
-
preformatted: true
|
|
84
|
-
}), PARAGRAPH);
|
|
85
|
-
case "block": return block(childText(node, context), PARAGRAPH);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
/** Collapse the separators the walk emits into readable plain text. */
|
|
91
|
-
const normalize = (value) => value.replace(/\r\n?/g, "\n").split("\n").map((line) => line.replace(/[^\S\n]+$/, "")).join("\n").replace(/\n{3,}/g, PARAGRAPH).trim();
|
|
92
|
-
/** Derive the plain-text alternative from the semantic tree in reading order. */
|
|
93
|
-
const deriveEmailText = (node) => normalize(derive(node, {
|
|
94
|
-
preformatted: false,
|
|
95
|
-
listDepth: 0
|
|
96
|
-
}));
|
|
97
|
-
//#endregion
|
|
98
|
-
export { deriveEmailText };
|
|
99
|
-
|
|
100
|
-
//# sourceMappingURL=text.js.map
|
package/dist/html-BJepOwZ4.d.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { r as JsxSourceLocation } from "./source-locations-C8wCsiAy.js";
|
|
2
|
-
import { r as EmailDiagnostic } from "./diagnostics-B-Gym34R.js";
|
|
3
|
-
import { m as EmailNode } from "./elements-cqldpaJt.js";
|
|
4
|
-
import { n as EmittedPosition } from "./preview-Ekdg1dXG.js";
|
|
5
|
-
//#region src/email/html.d.ts
|
|
6
|
-
/** Escape a value for element content. */
|
|
7
|
-
declare const escapeText: (value: string) => string;
|
|
8
|
-
/** Escape a value for a double-quoted attribute. */
|
|
9
|
-
declare const escapeAttribute: (value: string) => string;
|
|
10
|
-
/** camelCase to kebab-case; already-kebab names pass through unchanged. */
|
|
11
|
-
declare const kebab: (property: string) => string;
|
|
12
|
-
/** A bare number is pixels for every property that takes a length. */
|
|
13
|
-
declare const declarationValue: (property: string, value: string | number) => string;
|
|
14
|
-
interface EmailHtmlOutput {
|
|
15
|
-
readonly html: string;
|
|
16
|
-
readonly diagnostics: readonly EmailDiagnostic[];
|
|
17
|
-
/**
|
|
18
|
-
* Each emitted element's offset range and origins. A compatibility finding
|
|
19
|
-
* carries a position in the emitted document, and this is what turns that
|
|
20
|
-
* back into the authoring element the author can edit.
|
|
21
|
-
*/
|
|
22
|
-
readonly positions: readonly EmittedPosition[];
|
|
23
|
-
}
|
|
24
|
-
/** How this document will be consumed. Delivery is the default; export is opt-in. */
|
|
25
|
-
interface SerializeEmailOptions {
|
|
26
|
-
/**
|
|
27
|
-
* Accept relative references. A local export serves assets from a relative
|
|
28
|
-
* base beside the file; a delivered message has no directory to resolve one
|
|
29
|
-
* against, so this stays off for anything that will be sent.
|
|
30
|
-
*/
|
|
31
|
-
readonly relativeUrls?: boolean | undefined;
|
|
32
|
-
/**
|
|
33
|
-
* Stamp every element with its instance path, so an editor preview can
|
|
34
|
-
* resolve a click in the rendered document to exactly one rendered element.
|
|
35
|
-
* A delivered message never carries it.
|
|
36
|
-
*/
|
|
37
|
-
readonly instancePaths?: boolean | undefined;
|
|
38
|
-
}
|
|
39
|
-
/** Lower the derived tree to email HTML, reporting every unsupported construct with its origin. */
|
|
40
|
-
declare const serializeEmailHtml: (node: EmailNode, options?: SerializeEmailOptions) => EmailHtmlOutput;
|
|
41
|
-
/** An authored style object as a declaration list, checked by the same rules as a serialized tree. */
|
|
42
|
-
declare const serializeStyle: (style: Readonly<Record<string, unknown>>, origins: readonly JsxSourceLocation[]) => {
|
|
43
|
-
readonly css: string;
|
|
44
|
-
readonly diagnostics: readonly EmailDiagnostic[];
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Check one static attribute value against its element's spec, as serialization does. Returns
|
|
48
|
-
* the text that would be emitted, or `undefined` when the attribute is dropped or invalid.
|
|
49
|
-
*/
|
|
50
|
-
declare const checkAttribute: (tag: string, name: string, value: unknown, origins: readonly JsxSourceLocation[]) => {
|
|
51
|
-
readonly text: string | undefined;
|
|
52
|
-
readonly diagnostics: readonly EmailDiagnostic[];
|
|
53
|
-
};
|
|
54
|
-
//#endregion
|
|
55
|
-
export { escapeAttribute as a, serializeEmailHtml as c, declarationValue as i, serializeStyle as l, SerializeEmailOptions as n, escapeText as o, checkAttribute as r, kebab as s, EmailHtmlOutput as t };
|
|
56
|
-
//# sourceMappingURL=html-BJepOwZ4.d.ts.map
|