@solid-email/render 0.1.2 → 0.1.3
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/README.md +82 -0
- package/dist/browser/index.cjs +489 -116
- package/dist/browser/index.d.cts +27 -16
- package/dist/browser/index.d.cts.map +1 -1
- package/dist/browser/index.d.mts +27 -16
- package/dist/browser/index.d.mts.map +1 -1
- package/dist/browser/index.mjs +489 -116
- package/dist/browser/index.mjs.map +1 -1
- package/dist/edge/index.cjs +489 -116
- package/dist/edge/index.d.cts +27 -16
- package/dist/edge/index.d.cts.map +1 -1
- package/dist/edge/index.d.mts +27 -16
- package/dist/edge/index.d.mts.map +1 -1
- package/dist/edge/index.mjs +489 -116
- package/dist/edge/index.mjs.map +1 -1
- package/dist/node/index.cjs +489 -116
- package/dist/node/index.d.cts +27 -16
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.mts +27 -16
- package/dist/node/index.d.mts.map +1 -1
- package/dist/node/index.mjs +489 -116
- package/dist/node/index.mjs.map +1 -1
- package/package.json +6 -5
package/dist/node/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { renderToString, renderToStringAsync } from "solid-js/web/dist/server.js";
|
|
2
2
|
import * as html from "prettier/plugins/html";
|
|
3
3
|
import { format } from "prettier/standalone";
|
|
4
|
-
import {
|
|
4
|
+
import { compile as compile$1 } from "@solid-email/html-to-text";
|
|
5
5
|
import { ssr } from "solid-js/web";
|
|
6
6
|
//#region src/shared/utils/pretty.ts
|
|
7
7
|
function getHtmlNode(path) {
|
|
@@ -83,12 +83,74 @@ const plainTextSelectors = [
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
];
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
let defaultConverter;
|
|
87
|
+
let defaultSelectorSnapshot;
|
|
88
|
+
const OPTION_SNAPSHOT_KEYS = [
|
|
89
|
+
"baseElements",
|
|
90
|
+
"decodeEntities",
|
|
91
|
+
"encodeCharacters",
|
|
92
|
+
"formatters",
|
|
93
|
+
"limits",
|
|
94
|
+
"longWordSplit",
|
|
95
|
+
"preserveNewlines",
|
|
96
|
+
"selectors",
|
|
97
|
+
"whitespaceCharacters",
|
|
98
|
+
"wordwrap"
|
|
99
|
+
];
|
|
100
|
+
const customConverterCache = /* @__PURE__ */ new WeakMap();
|
|
101
|
+
function plainSelectorsChanged(snapshot) {
|
|
102
|
+
return snapshot.length !== plainTextSelectors.length || snapshot.some((selector, index) => selector !== plainTextSelectors[index]);
|
|
103
|
+
}
|
|
104
|
+
function getDefaultConverter() {
|
|
105
|
+
const changed = !defaultSelectorSnapshot || plainSelectorsChanged(defaultSelectorSnapshot);
|
|
106
|
+
if (!defaultConverter || changed) {
|
|
107
|
+
defaultSelectorSnapshot = [...plainTextSelectors];
|
|
108
|
+
defaultConverter = compile$1({
|
|
109
|
+
wordwrap: false,
|
|
110
|
+
selectors: defaultSelectorSnapshot
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return defaultConverter;
|
|
114
|
+
}
|
|
115
|
+
function createOptionsSnapshot(options, plainSelectorSnapshot, customSelectorSnapshot) {
|
|
116
|
+
return {
|
|
117
|
+
baseElements: options.baseElements,
|
|
118
|
+
decodeEntities: options.decodeEntities,
|
|
119
|
+
encodeCharacters: options.encodeCharacters,
|
|
120
|
+
formatters: options.formatters,
|
|
121
|
+
limits: options.limits,
|
|
122
|
+
longWordSplit: options.longWordSplit,
|
|
123
|
+
preserveNewlines: options.preserveNewlines,
|
|
124
|
+
customSelectorSnapshot,
|
|
125
|
+
plainSelectorSnapshot,
|
|
126
|
+
selectors: options.selectors,
|
|
127
|
+
whitespaceCharacters: options.whitespaceCharacters,
|
|
128
|
+
wordwrap: options.wordwrap
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function customOptionsChanged(options, snapshot) {
|
|
132
|
+
return plainSelectorsChanged(snapshot.plainSelectorSnapshot) || OPTION_SNAPSHOT_KEYS.some((key) => options[key] !== snapshot[key]) || options.selectors?.length !== snapshot.customSelectorSnapshot?.length || (options.selectors?.some((selector, index) => selector !== snapshot.customSelectorSnapshot?.[index]) ?? false);
|
|
133
|
+
}
|
|
134
|
+
function getCustomConverter(options) {
|
|
135
|
+
const cached = customConverterCache.get(options);
|
|
136
|
+
if (cached && !customOptionsChanged(options, cached.snapshot)) return cached.converter;
|
|
137
|
+
const plainSelectorSnapshot = [...plainTextSelectors];
|
|
138
|
+
const customSelectorSnapshot = options.selectors ? [...options.selectors] : void 0;
|
|
139
|
+
const selectors = [...plainSelectorSnapshot, ...customSelectorSnapshot ?? []];
|
|
140
|
+
const converter = compile$1({
|
|
88
141
|
wordwrap: false,
|
|
89
142
|
...options,
|
|
90
|
-
selectors
|
|
143
|
+
selectors
|
|
144
|
+
});
|
|
145
|
+
customConverterCache.set(options, {
|
|
146
|
+
converter,
|
|
147
|
+
snapshot: createOptionsSnapshot(options, plainSelectorSnapshot, customSelectorSnapshot)
|
|
91
148
|
});
|
|
149
|
+
return converter;
|
|
150
|
+
}
|
|
151
|
+
function toPlainText(html, options) {
|
|
152
|
+
if (!options) return getDefaultConverter()(html);
|
|
153
|
+
return getCustomConverter(options)(html);
|
|
92
154
|
}
|
|
93
155
|
//#endregion
|
|
94
156
|
//#region src/shared/render.ts
|
|
@@ -131,11 +193,139 @@ function renderSync(node, options) {
|
|
|
131
193
|
return renderSyncOutput(removeSolidResourceScripts(renderToString(normalizeRenderable(node))), options);
|
|
132
194
|
}
|
|
133
195
|
//#endregion
|
|
196
|
+
//#region src/shared/slot-values.ts
|
|
197
|
+
async function renderSlotValueAsync(value) {
|
|
198
|
+
if (value == null) return "";
|
|
199
|
+
if (Array.isArray(value)) return Promise.all(value.map(renderSlotValueAsync)).then((results) => results.join(""));
|
|
200
|
+
if (typeof value === "boolean") return value ? "true" : "";
|
|
201
|
+
if (typeof value === "string") return escapeHtml(value);
|
|
202
|
+
if (typeof value === "number") return String(value);
|
|
203
|
+
return removeSolidResourceScripts(await renderToStringAsync(() => value));
|
|
204
|
+
}
|
|
205
|
+
function renderSlotValueSync(value) {
|
|
206
|
+
if (value == null) return "";
|
|
207
|
+
if (Array.isArray(value)) return value.map(renderSlotValueSync).join("");
|
|
208
|
+
if (typeof value === "boolean") return value ? "true" : "";
|
|
209
|
+
if (typeof value === "string") return escapeHtml(value);
|
|
210
|
+
if (typeof value === "number") return String(value);
|
|
211
|
+
return removeSolidResourceScripts(renderToString(() => value));
|
|
212
|
+
}
|
|
213
|
+
async function renderSlotValueTextAsync(value, options) {
|
|
214
|
+
if (value == null) return "";
|
|
215
|
+
if (Array.isArray(value)) return (await Promise.all(value.map((item) => renderSlotValueTextAsync(item, options)))).join("");
|
|
216
|
+
if (typeof value === "boolean") return value ? "true" : "";
|
|
217
|
+
if (typeof value === "string") return value;
|
|
218
|
+
if (typeof value === "number") return String(value);
|
|
219
|
+
return toPlainText(removeSolidResourceScripts(await renderToStringAsync(() => value)), options);
|
|
220
|
+
}
|
|
221
|
+
function renderSlotValueTextSync(value, options) {
|
|
222
|
+
if (value == null) return "";
|
|
223
|
+
if (Array.isArray(value)) return value.map((item) => renderSlotValueTextSync(item, options)).join("");
|
|
224
|
+
if (typeof value === "boolean") return value ? "true" : "";
|
|
225
|
+
if (typeof value === "string") return value;
|
|
226
|
+
if (typeof value === "number") return String(value);
|
|
227
|
+
return toPlainText(removeSolidResourceScripts(renderToString(() => value)), options);
|
|
228
|
+
}
|
|
229
|
+
function renderAttrValue(name, value) {
|
|
230
|
+
if (value == null) return "";
|
|
231
|
+
if (typeof value === "boolean") return value ? "true" : "";
|
|
232
|
+
if (typeof value === "string") return escapeAttr(value);
|
|
233
|
+
if (typeof value === "number") return String(value);
|
|
234
|
+
throw new TypeError(`Attribute slot "${name}" only accepts string, number, boolean, null, or undefined. Use <Slot name="${name}" /> for JSX/content values.`);
|
|
235
|
+
}
|
|
236
|
+
function renderTextAttrValue(name, value) {
|
|
237
|
+
if (value == null) return "";
|
|
238
|
+
if (typeof value === "boolean") return value ? "true" : "";
|
|
239
|
+
if (typeof value === "string") return value;
|
|
240
|
+
if (typeof value === "number") return String(value);
|
|
241
|
+
throw new TypeError(`Attribute slot "${name}" only accepts string, number, boolean, null, or undefined. Use <Slot name="${name}" /> for JSX/content values.`);
|
|
242
|
+
}
|
|
243
|
+
function renderTextLinkHrefValue(name, value) {
|
|
244
|
+
const href = renderTextAttrValue(name, value).replace(/^mailto:/, "");
|
|
245
|
+
return href.startsWith("#") ? "" : href;
|
|
246
|
+
}
|
|
247
|
+
function escapeHtml(str) {
|
|
248
|
+
return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
249
|
+
}
|
|
250
|
+
function escapeAttr(str) {
|
|
251
|
+
return str.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/shared/slot-replacer.ts
|
|
255
|
+
function buildMarkerRegex(lookup) {
|
|
256
|
+
const markers = /* @__PURE__ */ new Set();
|
|
257
|
+
for (const occurrences of lookup.content.values()) for (const occ of occurrences) markers.add(occ.full);
|
|
258
|
+
for (const attrMarkers of lookup.attr.values()) for (const marker of attrMarkers) markers.add(marker);
|
|
259
|
+
if (markers.size === 0) return /$^/g;
|
|
260
|
+
return new RegExp(Array.from(markers).sort((a, b) => b.length - a.length).map(escapeRegex$2).join("|"), "g");
|
|
261
|
+
}
|
|
262
|
+
function validateSlots(data, lookup) {
|
|
263
|
+
const allSlotNames = /* @__PURE__ */ new Set([...lookup.content.keys(), ...lookup.attr.keys()]);
|
|
264
|
+
for (const name of allSlotNames) if (data[name] === void 0) {
|
|
265
|
+
if (!(lookup.content.get(name)?.some((occ) => occ.hasDefault) ?? false)) console.warn(`[solid-email] Slot "${name}" has no default and was not provided in render data. It will render as empty.`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function replaceSlots({ result, data, lookup, markerRegex, validate }) {
|
|
269
|
+
if (validate) validateSlots(data, lookup);
|
|
270
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const [name, occurrences] of lookup.content) {
|
|
272
|
+
const value = data[name];
|
|
273
|
+
const rendered = value !== void 0 ? await renderSlotValueAsync(value) : void 0;
|
|
274
|
+
for (const occ of occurrences) {
|
|
275
|
+
if (!result.includes(occ.full)) continue;
|
|
276
|
+
const replacement = rendered ?? await replaceSlots({
|
|
277
|
+
result: occ.defaultValue,
|
|
278
|
+
data,
|
|
279
|
+
lookup,
|
|
280
|
+
markerRegex,
|
|
281
|
+
validate: false
|
|
282
|
+
});
|
|
283
|
+
replacements.set(occ.full, replacement);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const [name, markers] of lookup.attr) {
|
|
287
|
+
const value = data[name];
|
|
288
|
+
const replacement = renderAttrValue(name, value);
|
|
289
|
+
for (const marker of markers) replacements.set(marker, replacement);
|
|
290
|
+
}
|
|
291
|
+
if (replacements.size === 0) return result;
|
|
292
|
+
return result.replace(markerRegex, (marker) => replacements.get(marker) ?? marker);
|
|
293
|
+
}
|
|
294
|
+
function replaceSlotsSync({ result, data, lookup, markerRegex, validate }) {
|
|
295
|
+
if (validate) validateSlots(data, lookup);
|
|
296
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
297
|
+
for (const [name, occurrences] of lookup.content) {
|
|
298
|
+
const value = data[name];
|
|
299
|
+
const rendered = value !== void 0 ? renderSlotValueSync(value) : void 0;
|
|
300
|
+
for (const occ of occurrences) {
|
|
301
|
+
if (!result.includes(occ.full)) continue;
|
|
302
|
+
const replacement = rendered ?? replaceSlotsSync({
|
|
303
|
+
result: occ.defaultValue,
|
|
304
|
+
data,
|
|
305
|
+
lookup,
|
|
306
|
+
markerRegex,
|
|
307
|
+
validate: false
|
|
308
|
+
});
|
|
309
|
+
replacements.set(occ.full, replacement);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const [name, markers] of lookup.attr) {
|
|
313
|
+
const value = data[name];
|
|
314
|
+
const replacement = renderAttrValue(name, value);
|
|
315
|
+
for (const marker of markers) replacements.set(marker, replacement);
|
|
316
|
+
}
|
|
317
|
+
if (replacements.size === 0) return result;
|
|
318
|
+
return result.replace(markerRegex, (marker) => replacements.get(marker) ?? marker);
|
|
319
|
+
}
|
|
320
|
+
function escapeRegex$2(str) {
|
|
321
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
134
324
|
//#region src/shared/slots.ts
|
|
135
325
|
const MARKER_PREFIX = "__SM_";
|
|
136
|
-
const CONTENT_START = `${MARKER_PREFIX}CNT_`;
|
|
137
|
-
const CONTENT_END = `${MARKER_PREFIX}CNE_`;
|
|
138
|
-
const ATTR_PREFIX = `${MARKER_PREFIX}ATR_`;
|
|
326
|
+
const CONTENT_START$1 = `${MARKER_PREFIX}CNT_`;
|
|
327
|
+
const CONTENT_END$1 = `${MARKER_PREFIX}CNE_`;
|
|
328
|
+
const ATTR_PREFIX$1 = `${MARKER_PREFIX}ATR_`;
|
|
139
329
|
function encodeName(name) {
|
|
140
330
|
return encodeURIComponent(name).replace(/[!'()*_]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
141
331
|
}
|
|
@@ -144,12 +334,12 @@ function decodeName(encoded) {
|
|
|
144
334
|
}
|
|
145
335
|
function makeContentMarker(name, defaultValue) {
|
|
146
336
|
const encoded = encodeName(name);
|
|
147
|
-
const start = `${CONTENT_START}${encoded}__`;
|
|
148
|
-
if (defaultValue !== void 0) return `${start}${defaultValue}${CONTENT_END}${encoded}__`;
|
|
149
|
-
return `${start}${CONTENT_END}${encoded}__`;
|
|
337
|
+
const start = `${CONTENT_START$1}${encoded}__`;
|
|
338
|
+
if (defaultValue !== void 0) return `${start}${defaultValue}${CONTENT_END$1}${encoded}__`;
|
|
339
|
+
return `${start}${CONTENT_END$1}${encoded}__`;
|
|
150
340
|
}
|
|
151
341
|
function makeAttrMarker(name) {
|
|
152
|
-
return `${ATTR_PREFIX}${encodeName(name)}__`;
|
|
342
|
+
return `${ATTR_PREFIX$1}${encodeName(name)}__`;
|
|
153
343
|
}
|
|
154
344
|
function escapeRegex$1(str) {
|
|
155
345
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -158,7 +348,7 @@ function buildSlotLookup(html) {
|
|
|
158
348
|
const contentSlots = /* @__PURE__ */ new Map();
|
|
159
349
|
const attrSlots = /* @__PURE__ */ new Map();
|
|
160
350
|
const nameChars = "(?:[A-Za-z0-9.~-]|%[0-9A-Fa-f]{2})+";
|
|
161
|
-
const tokenRegex = new RegExp(`${escapeRegex$1(CONTENT_START)}(${nameChars})__|${escapeRegex$1(CONTENT_END)}(${nameChars})__`, "g");
|
|
351
|
+
const tokenRegex = new RegExp(`${escapeRegex$1(CONTENT_START$1)}(${nameChars})__|${escapeRegex$1(CONTENT_END$1)}(${nameChars})__`, "g");
|
|
162
352
|
const stack = [];
|
|
163
353
|
let match;
|
|
164
354
|
while (true) {
|
|
@@ -194,7 +384,7 @@ function buildSlotLookup(html) {
|
|
|
194
384
|
if (existing) existing.push(occurrence);
|
|
195
385
|
else contentSlots.set(open.name, [occurrence]);
|
|
196
386
|
}
|
|
197
|
-
const attrRegex = new RegExp(`${escapeRegex$1(ATTR_PREFIX)}(${nameChars})__`, "g");
|
|
387
|
+
const attrRegex = new RegExp(`${escapeRegex$1(ATTR_PREFIX$1)}(${nameChars})__`, "g");
|
|
198
388
|
while (true) {
|
|
199
389
|
match = attrRegex.exec(html);
|
|
200
390
|
if (match === null) break;
|
|
@@ -210,8 +400,8 @@ function buildSlotLookup(html) {
|
|
|
210
400
|
}
|
|
211
401
|
function Slot(props) {
|
|
212
402
|
const encoded = encodeName(props.name);
|
|
213
|
-
const start = ssr(`${CONTENT_START}${encoded}__`);
|
|
214
|
-
const end = ssr(`${CONTENT_END}${encoded}__`);
|
|
403
|
+
const start = ssr(`${CONTENT_START$1}${encoded}__`);
|
|
404
|
+
const end = ssr(`${CONTENT_END$1}${encoded}__`);
|
|
215
405
|
if (props.children !== void 0 && props.children !== null) return [
|
|
216
406
|
start,
|
|
217
407
|
props.children,
|
|
@@ -229,124 +419,307 @@ function defineSlots() {
|
|
|
229
419
|
};
|
|
230
420
|
}
|
|
231
421
|
//#endregion
|
|
422
|
+
//#region src/shared/text-template.ts
|
|
423
|
+
const MARKER_NAME_CHARS = "(?:[A-Za-z0-9.~-]|%[0-9A-Fa-f]{2})+";
|
|
424
|
+
const CONTENT_START = "__SM_CNT_";
|
|
425
|
+
const CONTENT_END = "__SM_CNE_";
|
|
426
|
+
const ATTR_PREFIX = "__SM_ATR_";
|
|
427
|
+
const TEXT_SKIP_START = "__SM_TXS_";
|
|
428
|
+
const TEXT_SKIP_END = "__SM_TXE_";
|
|
429
|
+
const LINK_HREF_PREFIX = "__SM_LNK_";
|
|
430
|
+
function createPlainTextTemplate({ html, options, contentSlots, attrSlots }) {
|
|
431
|
+
const marked = markTextTemplateSlots(html, options);
|
|
432
|
+
const parsed = parseTextTemplate(toPlainText(marked.html, options));
|
|
433
|
+
return {
|
|
434
|
+
nodes: parsed.nodes,
|
|
435
|
+
usable: canUsePlainTextTemplate({
|
|
436
|
+
parsed,
|
|
437
|
+
supportedAttrMarkerCounts: marked.supportedAttrMarkerCounts,
|
|
438
|
+
expectedConditionalCount: marked.conditionalCount,
|
|
439
|
+
expectedLinkMarkerCounts: marked.expectedLinkMarkerCounts,
|
|
440
|
+
contentSlots,
|
|
441
|
+
attrSlots
|
|
442
|
+
})
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function renderTextTemplate(nodes, data, options) {
|
|
446
|
+
const chunks = [];
|
|
447
|
+
for (const node of nodes) switch (node.type) {
|
|
448
|
+
case "text":
|
|
449
|
+
chunks.push(node.value);
|
|
450
|
+
break;
|
|
451
|
+
case "contentSlot": {
|
|
452
|
+
const value = data[node.name];
|
|
453
|
+
chunks.push(value !== void 0 ? await renderSlotValueTextAsync(value, options) : await renderTextTemplate(node.fallback, data, options));
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case "attrSlot": {
|
|
457
|
+
const value = data[node.name];
|
|
458
|
+
chunks.push(renderTextAttrValue(node.name, value));
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case "linkHrefSlot": {
|
|
462
|
+
const value = data[node.name];
|
|
463
|
+
chunks.push(renderTextLinkHrefValue(node.name, value));
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case "conditionalSkip": {
|
|
467
|
+
const value = data[node.slotName];
|
|
468
|
+
if (renderTextAttrValue(node.slotName, value) !== "true") chunks.push(await renderTextTemplate(node.children, data, options));
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return chunks.join("");
|
|
473
|
+
}
|
|
474
|
+
function renderTextTemplateSync(nodes, data, options) {
|
|
475
|
+
const chunks = [];
|
|
476
|
+
for (const node of nodes) switch (node.type) {
|
|
477
|
+
case "text":
|
|
478
|
+
chunks.push(node.value);
|
|
479
|
+
break;
|
|
480
|
+
case "contentSlot": {
|
|
481
|
+
const value = data[node.name];
|
|
482
|
+
chunks.push(value !== void 0 ? renderSlotValueTextSync(value, options) : renderTextTemplateSync(node.fallback, data, options));
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case "attrSlot": {
|
|
486
|
+
const value = data[node.name];
|
|
487
|
+
chunks.push(renderTextAttrValue(node.name, value));
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case "linkHrefSlot": {
|
|
491
|
+
const value = data[node.name];
|
|
492
|
+
chunks.push(renderTextLinkHrefValue(node.name, value));
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case "conditionalSkip": {
|
|
496
|
+
const value = data[node.slotName];
|
|
497
|
+
if (renderTextAttrValue(node.slotName, value) !== "true") chunks.push(renderTextTemplateSync(node.children, data, options));
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return chunks.join("");
|
|
502
|
+
}
|
|
503
|
+
function canUsePlainTextTemplate({ parsed, supportedAttrMarkerCounts, expectedConditionalCount, expectedLinkMarkerCounts, contentSlots, attrSlots }) {
|
|
504
|
+
if (!parsed.valid) return false;
|
|
505
|
+
const remainingSupportedAttrMarkers = new Map(supportedAttrMarkerCounts);
|
|
506
|
+
for (const attrMarkers of attrSlots.values()) for (const marker of attrMarkers) {
|
|
507
|
+
const remaining = remainingSupportedAttrMarkers.get(marker) ?? 0;
|
|
508
|
+
if (remaining < 1) return false;
|
|
509
|
+
remainingSupportedAttrMarkers.set(marker, remaining - 1);
|
|
510
|
+
}
|
|
511
|
+
if (parsed.conditionalCount !== expectedConditionalCount) return false;
|
|
512
|
+
if (parsed.attrMarkerCounts.size > 0) return false;
|
|
513
|
+
for (const [marker, count] of expectedLinkMarkerCounts) if ((parsed.linkMarkerCounts.get(marker) ?? 0) < count) return false;
|
|
514
|
+
for (const [name, occurrences] of contentSlots) if ((parsed.contentSlotCounts.get(name) ?? 0) < occurrences.length) return false;
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
function markTextTemplateSlots(html, options) {
|
|
518
|
+
const supportedAttrMarkerCounts = /* @__PURE__ */ new Map();
|
|
519
|
+
const expectedLinkMarkerCounts = /* @__PURE__ */ new Map();
|
|
520
|
+
let conditionalCount = 0;
|
|
521
|
+
const incrementSupported = (marker) => {
|
|
522
|
+
incrementCount(supportedAttrMarkerCounts, marker);
|
|
523
|
+
};
|
|
524
|
+
const dataSkipRegex = new RegExp(`<([A-Za-z][\\w:-]*)([^>]*)\\sdata-skip-in-text=(["'])(__SM_ATR_(${MARKER_NAME_CHARS})__)\\3([^>]*)>([\\s\\S]*?)</\\1>`, "g");
|
|
525
|
+
let markedHtml = html.replace(dataSkipRegex, (_match, tagName, beforeAttrs, _quote, marker, encodedName, afterAttrs, children) => {
|
|
526
|
+
incrementSupported(marker);
|
|
527
|
+
conditionalCount += 1;
|
|
528
|
+
return `<${tagName}${beforeAttrs}${afterAttrs}>${TEXT_SKIP_START}${encodedName}__${children}${TEXT_SKIP_END}${encodedName}__</${tagName}>`;
|
|
529
|
+
});
|
|
530
|
+
if (!options) {
|
|
531
|
+
const linkHrefRegex = new RegExp(`<a([^>]*)\\shref=(["'])(__SM_ATR_(${MARKER_NAME_CHARS})__)\\2([^>]*)>([\\s\\S]*?)</a>`, "g");
|
|
532
|
+
markedHtml = markedHtml.replace(linkHrefRegex, (match, beforeAttrs, quote, marker, encodedName, afterAttrs, children) => {
|
|
533
|
+
const sameContentMarker = `${CONTENT_START}${encodedName}__`;
|
|
534
|
+
if (children.includes(sameContentMarker)) return match;
|
|
535
|
+
incrementSupported(marker);
|
|
536
|
+
incrementCount(expectedLinkMarkerCounts, marker);
|
|
537
|
+
return `<a${beforeAttrs} href=${quote}${LINK_HREF_PREFIX}${encodedName}__${quote}${afterAttrs}>${children}</a>`;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
html: markedHtml,
|
|
542
|
+
supportedAttrMarkerCounts,
|
|
543
|
+
expectedLinkMarkerCounts,
|
|
544
|
+
conditionalCount
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function parseTextTemplate(text) {
|
|
548
|
+
const tokenRegex = new RegExp(`${escapeRegex(CONTENT_START)}(${MARKER_NAME_CHARS})__|${escapeRegex(CONTENT_END)}(${MARKER_NAME_CHARS})__|${escapeRegex(ATTR_PREFIX)}(${MARKER_NAME_CHARS})__|${escapeRegex(LINK_HREF_PREFIX)}(${MARKER_NAME_CHARS})__|${escapeRegex(TEXT_SKIP_START)}(${MARKER_NAME_CHARS})__|${escapeRegex(TEXT_SKIP_END)}(${MARKER_NAME_CHARS})__`, "g");
|
|
549
|
+
const root = {
|
|
550
|
+
kind: "root",
|
|
551
|
+
nodes: []
|
|
552
|
+
};
|
|
553
|
+
const stack = [root];
|
|
554
|
+
const contentSlotCounts = /* @__PURE__ */ new Map();
|
|
555
|
+
const attrMarkerCounts = /* @__PURE__ */ new Map();
|
|
556
|
+
const linkMarkerCounts = /* @__PURE__ */ new Map();
|
|
557
|
+
let conditionalCount = 0;
|
|
558
|
+
let valid = true;
|
|
559
|
+
let offset = 0;
|
|
560
|
+
let match;
|
|
561
|
+
const currentNodes = () => stack[stack.length - 1]?.nodes ?? root.nodes;
|
|
562
|
+
const addText = (value) => {
|
|
563
|
+
if (!value) return;
|
|
564
|
+
currentNodes().push({
|
|
565
|
+
type: "text",
|
|
566
|
+
value
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
while (true) {
|
|
570
|
+
match = tokenRegex.exec(text);
|
|
571
|
+
if (match === null) break;
|
|
572
|
+
addText(text.slice(offset, match.index));
|
|
573
|
+
offset = tokenRegex.lastIndex;
|
|
574
|
+
const [token, contentStart, contentEnd, attrName, linkName, conditionalStart, conditionalEnd] = match;
|
|
575
|
+
if (contentStart !== void 0) {
|
|
576
|
+
stack.push({
|
|
577
|
+
kind: "contentSlot",
|
|
578
|
+
encodedName: contentStart,
|
|
579
|
+
name: decodeName(contentStart),
|
|
580
|
+
nodes: []
|
|
581
|
+
});
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (contentEnd !== void 0) {
|
|
585
|
+
const frame = stack[stack.length - 1];
|
|
586
|
+
if (frame?.kind !== "contentSlot" || frame.encodedName !== contentEnd || !frame.name) {
|
|
587
|
+
valid = false;
|
|
588
|
+
addText(token);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
stack.pop();
|
|
592
|
+
currentNodes().push({
|
|
593
|
+
type: "contentSlot",
|
|
594
|
+
name: frame.name,
|
|
595
|
+
fallback: frame.nodes
|
|
596
|
+
});
|
|
597
|
+
incrementCount(contentSlotCounts, frame.name);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (attrName !== void 0) {
|
|
601
|
+
const marker = `${ATTR_PREFIX}${attrName}__`;
|
|
602
|
+
currentNodes().push({
|
|
603
|
+
type: "attrSlot",
|
|
604
|
+
name: decodeName(attrName),
|
|
605
|
+
marker
|
|
606
|
+
});
|
|
607
|
+
incrementCount(attrMarkerCounts, marker);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (linkName !== void 0) {
|
|
611
|
+
const marker = `${ATTR_PREFIX}${linkName}__`;
|
|
612
|
+
currentNodes().push({
|
|
613
|
+
type: "linkHrefSlot",
|
|
614
|
+
name: decodeName(linkName),
|
|
615
|
+
marker
|
|
616
|
+
});
|
|
617
|
+
incrementCount(linkMarkerCounts, marker);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (conditionalStart !== void 0) {
|
|
621
|
+
stack.push({
|
|
622
|
+
kind: "conditionalSkip",
|
|
623
|
+
encodedName: conditionalStart,
|
|
624
|
+
name: decodeName(conditionalStart),
|
|
625
|
+
nodes: []
|
|
626
|
+
});
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (conditionalEnd !== void 0) {
|
|
630
|
+
const frame = stack[stack.length - 1];
|
|
631
|
+
if (frame?.kind !== "conditionalSkip" || frame.encodedName !== conditionalEnd || !frame.name) {
|
|
632
|
+
valid = false;
|
|
633
|
+
addText(token);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
stack.pop();
|
|
637
|
+
currentNodes().push({
|
|
638
|
+
type: "conditionalSkip",
|
|
639
|
+
slotName: frame.name,
|
|
640
|
+
children: frame.nodes
|
|
641
|
+
});
|
|
642
|
+
conditionalCount += 1;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
addText(text.slice(offset));
|
|
646
|
+
return {
|
|
647
|
+
nodes: root.nodes,
|
|
648
|
+
contentSlotCounts,
|
|
649
|
+
attrMarkerCounts,
|
|
650
|
+
linkMarkerCounts,
|
|
651
|
+
conditionalCount,
|
|
652
|
+
valid: valid && stack.length === 1
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function incrementCount(map, key) {
|
|
656
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
657
|
+
}
|
|
658
|
+
function escapeRegex(str) {
|
|
659
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
660
|
+
}
|
|
661
|
+
//#endregion
|
|
232
662
|
//#region src/shared/compile.ts
|
|
233
663
|
var CompiledTemplate = class {
|
|
234
664
|
html;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
attrSlots;
|
|
665
|
+
htmlToTextOptions;
|
|
666
|
+
slotLookup;
|
|
238
667
|
markerRegex;
|
|
239
|
-
|
|
668
|
+
plainTextTemplate;
|
|
669
|
+
constructor(html, options = {}) {
|
|
240
670
|
this.html = html;
|
|
241
|
-
this.
|
|
242
|
-
|
|
243
|
-
this.
|
|
244
|
-
this.
|
|
245
|
-
|
|
671
|
+
this.htmlToTextOptions = options.htmlToTextOptions;
|
|
672
|
+
this.slotLookup = buildSlotLookup(html);
|
|
673
|
+
this.markerRegex = buildMarkerRegex(this.slotLookup);
|
|
674
|
+
if (options.withPlainText) this.plainTextTemplate = createPlainTextTemplate({
|
|
675
|
+
html,
|
|
676
|
+
options: options.htmlToTextOptions,
|
|
677
|
+
contentSlots: this.slotLookup.content,
|
|
678
|
+
attrSlots: this.slotLookup.attr
|
|
679
|
+
});
|
|
246
680
|
}
|
|
247
681
|
async render(data, options) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return renderOutput(result, options ?? this.options);
|
|
682
|
+
if (options?.plainText) return this.renderPlainText(data);
|
|
683
|
+
return renderOutput(await this.replaceHtmlSlots(data), options?.pretty ? { pretty: true } : void 0);
|
|
251
684
|
}
|
|
252
685
|
renderSync(data, options) {
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return renderSyncOutput(result, options ?? this.options);
|
|
257
|
-
}
|
|
258
|
-
buildMarkerRegex() {
|
|
259
|
-
const markers = /* @__PURE__ */ new Set();
|
|
260
|
-
for (const occurrences of this.contentSlots.values()) for (const occ of occurrences) markers.add(occ.full);
|
|
261
|
-
for (const attrMarkers of this.attrSlots.values()) for (const marker of attrMarkers) markers.add(marker);
|
|
262
|
-
if (markers.size === 0) return /$^/g;
|
|
263
|
-
return new RegExp(Array.from(markers).sort((a, b) => b.length - a.length).map(escapeRegex).join("|"), "g");
|
|
686
|
+
if (options?.pretty) throw new Error("renderSync does not support pretty output; use render.");
|
|
687
|
+
if (options?.plainText) return this.renderPlainTextSync(data);
|
|
688
|
+
return renderSyncOutput(this.replaceHtmlSlotsSync(data));
|
|
264
689
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
690
|
+
async replaceHtmlSlots(data) {
|
|
691
|
+
return replaceSlots({
|
|
692
|
+
result: this.html,
|
|
693
|
+
data,
|
|
694
|
+
lookup: this.slotLookup,
|
|
695
|
+
markerRegex: this.markerRegex,
|
|
696
|
+
validate: true
|
|
697
|
+
});
|
|
270
698
|
}
|
|
271
|
-
|
|
272
|
-
return
|
|
699
|
+
replaceHtmlSlotsSync(data) {
|
|
700
|
+
return replaceSlotsSync({
|
|
701
|
+
result: this.html,
|
|
702
|
+
data,
|
|
703
|
+
lookup: this.slotLookup,
|
|
704
|
+
markerRegex: this.markerRegex,
|
|
705
|
+
validate: true
|
|
706
|
+
});
|
|
273
707
|
}
|
|
274
|
-
async
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const value = data[name];
|
|
279
|
-
const rendered = value !== void 0 ? await renderSlotValueAsync(value) : void 0;
|
|
280
|
-
for (const occ of occurrences) {
|
|
281
|
-
if (!result.includes(occ.full)) continue;
|
|
282
|
-
const replacement = rendered ?? await this.replaceSlotsInFragment(occ.defaultValue, data, false);
|
|
283
|
-
replacements.set(occ.full, replacement);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
for (const [name, markers] of this.attrSlots) {
|
|
287
|
-
const value = data[name];
|
|
288
|
-
const replacement = renderAttrValue(name, value);
|
|
289
|
-
for (const marker of markers) replacements.set(marker, replacement);
|
|
708
|
+
async renderPlainText(data) {
|
|
709
|
+
if (this.plainTextTemplate?.usable) {
|
|
710
|
+
validateSlots(data, this.slotLookup);
|
|
711
|
+
return renderTextTemplate(this.plainTextTemplate.nodes, data, this.htmlToTextOptions);
|
|
290
712
|
}
|
|
291
|
-
|
|
292
|
-
return result.replace(this.markerRegex, (marker) => replacements.get(marker) ?? marker);
|
|
293
|
-
}
|
|
294
|
-
replaceSlotsSync(result, data) {
|
|
295
|
-
return this.replaceSlotsInFragmentSync(result, data, true);
|
|
713
|
+
return toPlainText(await this.replaceHtmlSlots(data), this.htmlToTextOptions);
|
|
296
714
|
}
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const value = data[name];
|
|
302
|
-
const rendered = value !== void 0 ? renderSlotValueSync(value) : void 0;
|
|
303
|
-
for (const occ of occurrences) {
|
|
304
|
-
if (!result.includes(occ.full)) continue;
|
|
305
|
-
const replacement = rendered ?? this.replaceSlotsInFragmentSync(occ.defaultValue, data, false);
|
|
306
|
-
replacements.set(occ.full, replacement);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
for (const [name, markers] of this.attrSlots) {
|
|
310
|
-
const value = data[name];
|
|
311
|
-
const replacement = renderAttrValue(name, value);
|
|
312
|
-
for (const marker of markers) replacements.set(marker, replacement);
|
|
715
|
+
renderPlainTextSync(data) {
|
|
716
|
+
if (this.plainTextTemplate?.usable) {
|
|
717
|
+
validateSlots(data, this.slotLookup);
|
|
718
|
+
return renderTextTemplateSync(this.plainTextTemplate.nodes, data, this.htmlToTextOptions);
|
|
313
719
|
}
|
|
314
|
-
|
|
315
|
-
return result.replace(this.markerRegex, (marker) => replacements.get(marker) ?? marker);
|
|
720
|
+
return toPlainText(this.replaceHtmlSlotsSync(data), this.htmlToTextOptions);
|
|
316
721
|
}
|
|
317
722
|
};
|
|
318
|
-
async function renderSlotValueAsync(value) {
|
|
319
|
-
if (value == null) return "";
|
|
320
|
-
if (Array.isArray(value)) return Promise.all(value.map(renderSlotValueAsync)).then((results) => results.join(""));
|
|
321
|
-
if (typeof value === "boolean") return value ? "true" : "";
|
|
322
|
-
if (typeof value === "string") return escapeHtml(value);
|
|
323
|
-
if (typeof value === "number") return String(value);
|
|
324
|
-
return removeSolidResourceScripts(await renderToStringAsync(() => value));
|
|
325
|
-
}
|
|
326
|
-
function renderSlotValueSync(value) {
|
|
327
|
-
if (value == null) return "";
|
|
328
|
-
if (Array.isArray(value)) return value.map(renderSlotValueSync).join("");
|
|
329
|
-
if (typeof value === "boolean") return value ? "true" : "";
|
|
330
|
-
if (typeof value === "string") return escapeHtml(value);
|
|
331
|
-
if (typeof value === "number") return String(value);
|
|
332
|
-
return removeSolidResourceScripts(renderToString(() => value));
|
|
333
|
-
}
|
|
334
|
-
function renderAttrValue(name, value) {
|
|
335
|
-
if (value == null) return "";
|
|
336
|
-
if (typeof value === "boolean") return value ? "true" : "";
|
|
337
|
-
if (typeof value === "string") return escapeAttr(value);
|
|
338
|
-
if (typeof value === "number") return String(value);
|
|
339
|
-
throw new TypeError(`Attribute slot "${name}" only accepts string, number, boolean, null, or undefined. Use <Slot name="${name}" /> for JSX/content values.`);
|
|
340
|
-
}
|
|
341
|
-
function escapeHtml(str) {
|
|
342
|
-
return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
343
|
-
}
|
|
344
|
-
function escapeAttr(str) {
|
|
345
|
-
return str.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
346
|
-
}
|
|
347
|
-
function escapeRegex(str) {
|
|
348
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
349
|
-
}
|
|
350
723
|
async function compile(node, options) {
|
|
351
724
|
return new CompiledTemplate(removeSolidResourceScripts(await renderToStringAsync(normalizeRenderable(node))), options);
|
|
352
725
|
}
|