@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.
@@ -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 { convert } from "html-to-text";
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
- function toPlainText(html, options) {
87
- return convert(html, {
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: [...plainTextSelectors, ...options?.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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
249
+ }
250
+ function escapeAttr(str) {
251
+ return str.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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
- options;
236
- contentSlots;
237
- attrSlots;
665
+ htmlToTextOptions;
666
+ slotLookup;
238
667
  markerRegex;
239
- constructor(html, options) {
668
+ plainTextTemplate;
669
+ constructor(html, options = {}) {
240
670
  this.html = html;
241
- this.options = options;
242
- const lookup = buildSlotLookup(html);
243
- this.contentSlots = lookup.content;
244
- this.attrSlots = lookup.attr;
245
- this.markerRegex = this.buildMarkerRegex();
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
- let result = this.html;
249
- result = await this.replaceSlots(result, data);
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 ((options ?? this.options)?.pretty) throw new Error("renderSync does not support pretty output; use render.");
254
- let result = this.html;
255
- result = this.replaceSlotsSync(result, data);
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
- validateSlots(data) {
266
- const allSlotNames = /* @__PURE__ */ new Set([...this.contentSlots.keys(), ...this.attrSlots.keys()]);
267
- for (const name of allSlotNames) if (data[name] === void 0) {
268
- if (!(this.contentSlots.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.`);
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
- async replaceSlots(result, data) {
272
- return this.replaceSlotsInFragment(result, data, true);
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 replaceSlotsInFragment(result, data, validate) {
275
- if (validate) this.validateSlots(data);
276
- const replacements = /* @__PURE__ */ new Map();
277
- for (const [name, occurrences] of this.contentSlots) {
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
- if (replacements.size === 0) return result;
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
- replaceSlotsInFragmentSync(result, data, validate) {
298
- if (validate) this.validateSlots(data);
299
- const replacements = /* @__PURE__ */ new Map();
300
- for (const [name, occurrences] of this.contentSlots) {
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
- if (replacements.size === 0) return result;
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
343
- }
344
- function escapeAttr(str) {
345
- return str.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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
  }