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