@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.
@@ -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 html_to_text = require("html-to-text");
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
- function toPlainText(html, options) {
111
- return (0, html_to_text.convert)(html, {
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: [...plainTextSelectors, ...options?.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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
273
+ }
274
+ function escapeAttr(str) {
275
+ return str.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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
- options;
260
- contentSlots;
261
- attrSlots;
689
+ htmlToTextOptions;
690
+ slotLookup;
262
691
  markerRegex;
263
- constructor(html, options) {
692
+ plainTextTemplate;
693
+ constructor(html, options = {}) {
264
694
  this.html = html;
265
- this.options = options;
266
- const lookup = buildSlotLookup(html);
267
- this.contentSlots = lookup.content;
268
- this.attrSlots = lookup.attr;
269
- this.markerRegex = this.buildMarkerRegex();
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
- let result = this.html;
273
- result = await this.replaceSlots(result, data);
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 ((options ?? this.options)?.pretty) throw new Error("renderSync does not support pretty output; use render.");
278
- let result = this.html;
279
- result = this.replaceSlotsSync(result, data);
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
- validateSlots(data) {
290
- const allSlotNames = /* @__PURE__ */ new Set([...this.contentSlots.keys(), ...this.attrSlots.keys()]);
291
- for (const name of allSlotNames) if (data[name] === void 0) {
292
- 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.`);
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
- async replaceSlots(result, data) {
296
- return this.replaceSlotsInFragment(result, data, true);
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 replaceSlotsInFragment(result, data, validate) {
299
- if (validate) this.validateSlots(data);
300
- const replacements = /* @__PURE__ */ new Map();
301
- for (const [name, occurrences] of this.contentSlots) {
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
- if (replacements.size === 0) return result;
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
- replaceSlotsInFragmentSync(result, data, validate) {
322
- if (validate) this.validateSlots(data);
323
- const replacements = /* @__PURE__ */ new Map();
324
- for (const [name, occurrences] of this.contentSlots) {
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
- if (replacements.size === 0) return result;
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
367
- }
368
- function escapeAttr(str) {
369
- return str.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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
  }