@depup/react-email__render 2.0.6-depup.0 → 2.1.0-depup.4
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 +6 -4
- package/changes.json +11 -3
- package/dist/browser/index.cjs +326 -21
- package/dist/browser/index.d.cts +10 -0
- package/dist/browser/index.d.cts.map +1 -1
- package/dist/browser/index.d.mts +10 -0
- package/dist/browser/index.d.mts.map +1 -1
- package/dist/browser/index.mjs +322 -12
- package/dist/browser/index.mjs.map +1 -1
- package/dist/edge/index.cjs +326 -22
- package/dist/edge/index.d.cts +10 -0
- package/dist/edge/index.d.cts.map +1 -1
- package/dist/edge/index.d.mts +10 -0
- package/dist/edge/index.d.mts.map +1 -1
- package/dist/edge/index.mjs +322 -13
- package/dist/edge/index.mjs.map +1 -1
- package/dist/node/index.cjs +330 -21
- package/dist/node/index.d.cts +14 -1
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.mts +14 -1
- package/dist/node/index.d.mts.map +1 -1
- package/dist/node/index.mjs +327 -14
- package/dist/node/index.mjs.map +1 -1
- package/package.json +38 -17
package/dist/browser/index.mjs
CHANGED
|
@@ -2,8 +2,9 @@ import * as html from "prettier/plugins/html";
|
|
|
2
2
|
import { format } from "prettier/standalone";
|
|
3
3
|
import { convert } from "html-to-text";
|
|
4
4
|
import React, { Suspense } from "react";
|
|
5
|
+
import { decodeHTML, decodeHTMLAttribute } from "entities/lib/decode.js";
|
|
6
|
+
import { SyntaxKind, parse } from "html5parser";
|
|
5
7
|
import { Fragment, jsx } from "react/jsx-runtime";
|
|
6
|
-
|
|
7
8
|
//#region src/shared/utils/pretty.ts
|
|
8
9
|
function getHtmlNode(path) {
|
|
9
10
|
const topNode = path.node;
|
|
@@ -64,7 +65,6 @@ const pretty = (str, options = {}) => {
|
|
|
64
65
|
...options
|
|
65
66
|
});
|
|
66
67
|
};
|
|
67
|
-
|
|
68
68
|
//#endregion
|
|
69
69
|
//#region src/shared/utils/to-plain-text.ts
|
|
70
70
|
const plainTextSelectors = [
|
|
@@ -82,16 +82,291 @@ const plainTextSelectors = [
|
|
|
82
82
|
linkBrackets: false,
|
|
83
83
|
hideLinkHrefIfSameAsText: true
|
|
84
84
|
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
selector: "[data-text-format=\"dataTable\"]",
|
|
88
|
+
format: "dataTable"
|
|
85
89
|
}
|
|
86
90
|
];
|
|
87
|
-
function toPlainText(html
|
|
88
|
-
return convert(html
|
|
91
|
+
function toPlainText(html, options) {
|
|
92
|
+
return convert(html, {
|
|
89
93
|
wordwrap: false,
|
|
90
94
|
...options,
|
|
91
95
|
selectors: [...plainTextSelectors, ...options?.selectors ?? []]
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
|
-
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/shared/utils/unstable-to-plain-text.ts
|
|
100
|
+
const SKIPPED_TAGS = new Set([
|
|
101
|
+
"img",
|
|
102
|
+
"noscript",
|
|
103
|
+
"script",
|
|
104
|
+
"style",
|
|
105
|
+
"template"
|
|
106
|
+
]);
|
|
107
|
+
const WHITESPACE_RUN = /([ \t\n\r\f\u200b]+)/;
|
|
108
|
+
const TAG_BLOCKS = {
|
|
109
|
+
article: {
|
|
110
|
+
open: 2,
|
|
111
|
+
close: 2
|
|
112
|
+
},
|
|
113
|
+
aside: {
|
|
114
|
+
open: 2,
|
|
115
|
+
close: 2
|
|
116
|
+
},
|
|
117
|
+
blockquote: {
|
|
118
|
+
open: 2,
|
|
119
|
+
close: 2,
|
|
120
|
+
prefix: {
|
|
121
|
+
first: "> ",
|
|
122
|
+
rest: "> "
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
div: {
|
|
126
|
+
open: 2,
|
|
127
|
+
close: 2
|
|
128
|
+
},
|
|
129
|
+
footer: {
|
|
130
|
+
open: 2,
|
|
131
|
+
close: 2
|
|
132
|
+
},
|
|
133
|
+
form: {
|
|
134
|
+
open: 2,
|
|
135
|
+
close: 2
|
|
136
|
+
},
|
|
137
|
+
h1: {
|
|
138
|
+
open: 3,
|
|
139
|
+
close: 2
|
|
140
|
+
},
|
|
141
|
+
h2: {
|
|
142
|
+
open: 3,
|
|
143
|
+
close: 2
|
|
144
|
+
},
|
|
145
|
+
h3: {
|
|
146
|
+
open: 3,
|
|
147
|
+
close: 2
|
|
148
|
+
},
|
|
149
|
+
h4: {
|
|
150
|
+
open: 3,
|
|
151
|
+
close: 2
|
|
152
|
+
},
|
|
153
|
+
h5: {
|
|
154
|
+
open: 3,
|
|
155
|
+
close: 2
|
|
156
|
+
},
|
|
157
|
+
h6: {
|
|
158
|
+
open: 3,
|
|
159
|
+
close: 2
|
|
160
|
+
},
|
|
161
|
+
header: {
|
|
162
|
+
open: 2,
|
|
163
|
+
close: 2
|
|
164
|
+
},
|
|
165
|
+
hr: {
|
|
166
|
+
open: 2,
|
|
167
|
+
close: 2
|
|
168
|
+
},
|
|
169
|
+
main: {
|
|
170
|
+
open: 2,
|
|
171
|
+
close: 2
|
|
172
|
+
},
|
|
173
|
+
nav: {
|
|
174
|
+
open: 2,
|
|
175
|
+
close: 2
|
|
176
|
+
},
|
|
177
|
+
p: {
|
|
178
|
+
open: 2,
|
|
179
|
+
close: 2
|
|
180
|
+
},
|
|
181
|
+
pre: {
|
|
182
|
+
open: 2,
|
|
183
|
+
close: 2
|
|
184
|
+
},
|
|
185
|
+
section: {
|
|
186
|
+
open: 2,
|
|
187
|
+
close: 2
|
|
188
|
+
},
|
|
189
|
+
table: {
|
|
190
|
+
open: 2,
|
|
191
|
+
close: 2
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
function unstableToPlainText(html) {
|
|
195
|
+
const tree = parse(html, { setAttributeMap: true });
|
|
196
|
+
const body = findBody(tree);
|
|
197
|
+
const blocks = [{
|
|
198
|
+
block: {
|
|
199
|
+
open: 0,
|
|
200
|
+
close: 0
|
|
201
|
+
},
|
|
202
|
+
text: [],
|
|
203
|
+
leading: 0,
|
|
204
|
+
stash: 0,
|
|
205
|
+
space: false
|
|
206
|
+
}];
|
|
207
|
+
const stack = [{
|
|
208
|
+
parent: body,
|
|
209
|
+
children: body?.body ?? tree,
|
|
210
|
+
index: 0,
|
|
211
|
+
pre: false,
|
|
212
|
+
opened: false,
|
|
213
|
+
textFrom: 0,
|
|
214
|
+
orderedList: void 0
|
|
215
|
+
}];
|
|
216
|
+
while (stack.length > 0) {
|
|
217
|
+
const frame = stack[stack.length - 1];
|
|
218
|
+
const node = frame.children[frame.index];
|
|
219
|
+
if (node === void 0) {
|
|
220
|
+
stack.pop();
|
|
221
|
+
exitElement(frame.parent, frame);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
frame.index += 1;
|
|
225
|
+
enterNode(node, frame);
|
|
226
|
+
}
|
|
227
|
+
function top() {
|
|
228
|
+
return blocks[blocks.length - 1];
|
|
229
|
+
}
|
|
230
|
+
function writeWord(value) {
|
|
231
|
+
const block = top();
|
|
232
|
+
if (block.stash > 0) block.text.push("\n".repeat(block.stash));
|
|
233
|
+
else if (block.space && block.text.length > 0) block.text.push(" ");
|
|
234
|
+
block.stash = 0;
|
|
235
|
+
block.space = false;
|
|
236
|
+
block.text.push(value);
|
|
237
|
+
}
|
|
238
|
+
function enterNode(node, frame) {
|
|
239
|
+
if (node.type === SyntaxKind.Text) {
|
|
240
|
+
const value = decodeHTML(node.value);
|
|
241
|
+
if (frame.pre) {
|
|
242
|
+
if (value.length > 0) writeWord(value);
|
|
243
|
+
} else {
|
|
244
|
+
const segments = value.split(WHITESPACE_RUN);
|
|
245
|
+
for (let i = 0; i < segments.length; i++) {
|
|
246
|
+
const segment = segments[i];
|
|
247
|
+
if (segment.length === 0) continue;
|
|
248
|
+
if (i % 2 === 1) top().space = true;
|
|
249
|
+
else writeWord(segment);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (SKIPPED_TAGS.has(node.name) || decodeHTMLAttribute(node.attributeMap?.["data-skip-in-text"]?.value?.value ?? "") === "true") return;
|
|
255
|
+
const parentTag = frame.parent?.name;
|
|
256
|
+
let block = TAG_BLOCKS[node.name];
|
|
257
|
+
let orderedList;
|
|
258
|
+
if (node.name === "ul") {
|
|
259
|
+
const breaks = parentTag === "li" ? 1 : 2;
|
|
260
|
+
block = {
|
|
261
|
+
open: breaks,
|
|
262
|
+
close: breaks
|
|
263
|
+
};
|
|
264
|
+
} else if (node.name === "li" && parentTag === "ul") block = {
|
|
265
|
+
open: 1,
|
|
266
|
+
close: 1,
|
|
267
|
+
prefix: (stack[stack.length - 2]?.parent)?.name === "li" ? {
|
|
268
|
+
first: "* ",
|
|
269
|
+
rest: " "
|
|
270
|
+
} : {
|
|
271
|
+
first: " * ",
|
|
272
|
+
rest: " "
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
else if (node.name === "ol") {
|
|
276
|
+
const nested = parentTag === "li";
|
|
277
|
+
const parsedStart = Number.parseInt(decodeHTMLAttribute(node.attributeMap?.start?.value?.value ?? "1"), 10);
|
|
278
|
+
const start = Number.isNaN(parsedStart) ? 1 : parsedStart;
|
|
279
|
+
const itemCount = node.body?.filter((child) => child.type === SyntaxKind.Tag && child.name === "li").length ?? 0;
|
|
280
|
+
let prefixLength = 0;
|
|
281
|
+
for (let index = start; index < start + itemCount; index++) {
|
|
282
|
+
const prefix = `${nested ? "" : " "}${index}. `;
|
|
283
|
+
prefixLength = Math.max(prefixLength, prefix.length);
|
|
284
|
+
}
|
|
285
|
+
const breaks = nested ? 1 : 2;
|
|
286
|
+
block = {
|
|
287
|
+
open: breaks,
|
|
288
|
+
close: breaks
|
|
289
|
+
};
|
|
290
|
+
orderedList = {
|
|
291
|
+
next: start,
|
|
292
|
+
prefixLength,
|
|
293
|
+
nested
|
|
294
|
+
};
|
|
295
|
+
} else if (node.name === "li" && parentTag === "ol" && frame.orderedList) {
|
|
296
|
+
const list = frame.orderedList;
|
|
297
|
+
block = {
|
|
298
|
+
open: 1,
|
|
299
|
+
close: 1,
|
|
300
|
+
prefix: {
|
|
301
|
+
first: `${list.nested ? "" : " "}${list.next++}. `.padEnd(list.prefixLength),
|
|
302
|
+
rest: " ".repeat(list.prefixLength)
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
if (block) blocks.push({
|
|
307
|
+
block,
|
|
308
|
+
text: [],
|
|
309
|
+
leading: block.open,
|
|
310
|
+
stash: 0,
|
|
311
|
+
space: false
|
|
312
|
+
});
|
|
313
|
+
if (node.name === "hr") writeWord("-".repeat(40));
|
|
314
|
+
else if (node.name === "br") {
|
|
315
|
+
top().space = false;
|
|
316
|
+
top().text.push("\n");
|
|
317
|
+
}
|
|
318
|
+
stack.push({
|
|
319
|
+
parent: node,
|
|
320
|
+
children: node.body ?? [],
|
|
321
|
+
index: 0,
|
|
322
|
+
pre: frame.pre || node.name === "pre",
|
|
323
|
+
opened: block !== void 0,
|
|
324
|
+
textFrom: top().text.length,
|
|
325
|
+
orderedList
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function exitElement(element, frame) {
|
|
329
|
+
if (element === void 0) return;
|
|
330
|
+
if (element.name === "a") {
|
|
331
|
+
const href = decodeHTMLAttribute(element.attributeMap?.href?.value?.value ?? "").replace(/^mailto:/, "");
|
|
332
|
+
if (href.length > 0 && !href.startsWith("#")) {
|
|
333
|
+
const anchorText = top().text.slice(frame.textFrom).join("");
|
|
334
|
+
if (anchorText !== href) {
|
|
335
|
+
if (anchorText.length > 0) top().space = true;
|
|
336
|
+
writeWord(href);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (frame.opened) closeBlock();
|
|
341
|
+
}
|
|
342
|
+
function closeBlock() {
|
|
343
|
+
const child = blocks.pop();
|
|
344
|
+
if (child === void 0) return;
|
|
345
|
+
const parent = blocks[blocks.length - 1];
|
|
346
|
+
let content = child.text.join("");
|
|
347
|
+
const prefix = child.block.prefix;
|
|
348
|
+
if (prefix !== void 0) {
|
|
349
|
+
const trimmed = content.replace(/^\n+|\n+$/g, "");
|
|
350
|
+
content = prefix.first + trimmed.replaceAll("\n", `\n${prefix.rest}`);
|
|
351
|
+
}
|
|
352
|
+
const breaks = Math.max(parent.stash, child.leading);
|
|
353
|
+
if (parent.text.length > 0) {
|
|
354
|
+
parent.text.push("\n".repeat(breaks));
|
|
355
|
+
if (content.length > 0) parent.text.push(content);
|
|
356
|
+
} else {
|
|
357
|
+
if (content.length > 0) parent.text.push(content);
|
|
358
|
+
parent.leading = breaks;
|
|
359
|
+
}
|
|
360
|
+
parent.stash = Math.max(child.stash, child.block.close);
|
|
361
|
+
}
|
|
362
|
+
return blocks[0].text.join("");
|
|
363
|
+
}
|
|
364
|
+
function findBody(tree) {
|
|
365
|
+
for (const child of tree) {
|
|
366
|
+
if (child.type !== SyntaxKind.Tag || child.name !== "html") continue;
|
|
367
|
+
for (const inner of child.body ?? []) if (inner.type === SyntaxKind.Tag && inner.name === "body") return inner;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
95
370
|
//#endregion
|
|
96
371
|
//#region src/shared/error-boundary.tsx
|
|
97
372
|
function createErrorBoundary(reject) {
|
|
@@ -105,7 +380,43 @@ function createErrorBoundary(reject) {
|
|
|
105
380
|
}
|
|
106
381
|
};
|
|
107
382
|
}
|
|
108
|
-
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/shared/utils/strip-image-preload-links.ts
|
|
385
|
+
/**
|
|
386
|
+
* React injects `<link rel="preload" as="image" />` resource hints into the
|
|
387
|
+
* document `<head>` for every `<img>` it renders during server-side rendering.
|
|
388
|
+
*
|
|
389
|
+
* These hints are meant for browsers loading a web page, where they can speed
|
|
390
|
+
* up the initial paint. In an email they are dead weight: email clients ignore
|
|
391
|
+
* `<link rel="preload">`, and the tags only add noise to the rendered HTML.
|
|
392
|
+
*
|
|
393
|
+
* @see https://github.com/resend/react-email/issues/3034
|
|
394
|
+
*
|
|
395
|
+
* This removes only those auto-injected image preload links, leaving every
|
|
396
|
+
* other `<link>` (stylesheets, fonts, user-authored non-image preloads, ...)
|
|
397
|
+
* untouched. It parses each `<link>` tag's attributes instead of relying on a
|
|
398
|
+
* fixed string match, so it is not affected by attribute order or spacing.
|
|
399
|
+
*/
|
|
400
|
+
const stripImagePreloadLinks = (html) => {
|
|
401
|
+
return html.replace(/<link\b[^>]*\/?>/gi, (tag) => isImagePreloadLink(tag) ? "" : tag);
|
|
402
|
+
};
|
|
403
|
+
const isImagePreloadLink = (tag) => {
|
|
404
|
+
const attributes = parseAttributes(tag);
|
|
405
|
+
return attributes.rel === "preload" && attributes.as === "image";
|
|
406
|
+
};
|
|
407
|
+
const ATTRIBUTE_PATTERN = /([a-z][a-z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/gi;
|
|
408
|
+
/**
|
|
409
|
+
* Parses the attributes of a single, already-isolated HTML tag string (e.g.
|
|
410
|
+
* `<link rel="preload" as="image" href="..." />`) into a name → value map.
|
|
411
|
+
* Attribute names are lower-cased; values are read from double, single, or
|
|
412
|
+
* unquoted forms.
|
|
413
|
+
*/
|
|
414
|
+
const parseAttributes = (tag) => {
|
|
415
|
+
const attributeSection = tag.replace(/^<[a-z][a-z0-9-]*/i, "");
|
|
416
|
+
const attributes = {};
|
|
417
|
+
for (const [, name, doubleQuoted, singleQuoted, unquoted] of attributeSection.matchAll(ATTRIBUTE_PATTERN)) attributes[name.toLowerCase()] = doubleQuoted ?? singleQuoted ?? unquoted ?? "";
|
|
418
|
+
return attributes;
|
|
419
|
+
};
|
|
109
420
|
//#endregion
|
|
110
421
|
//#region src/shared/read-stream.browser.ts
|
|
111
422
|
const decoder = new TextDecoder("utf-8");
|
|
@@ -132,7 +443,6 @@ const readStream = async (stream) => {
|
|
|
132
443
|
});
|
|
133
444
|
return decoder.decode(mergedChunks);
|
|
134
445
|
};
|
|
135
|
-
|
|
136
446
|
//#endregion
|
|
137
447
|
//#region src/browser/render.tsx
|
|
138
448
|
const render = async (node, options) => {
|
|
@@ -140,7 +450,7 @@ const render = async (node, options) => {
|
|
|
140
450
|
if ("default" in m) return m.default;
|
|
141
451
|
return m;
|
|
142
452
|
});
|
|
143
|
-
const html
|
|
453
|
+
const html = await new Promise((resolve, reject) => {
|
|
144
454
|
const ErrorBoundary = createErrorBoundary(reject);
|
|
145
455
|
reactDOMServer.renderToReadableStream(/* @__PURE__ */ jsx(Suspense, { children: /* @__PURE__ */ jsx(ErrorBoundary, { children: node }) }), {
|
|
146
456
|
onError(error) {
|
|
@@ -150,14 +460,14 @@ const render = async (node, options) => {
|
|
|
150
460
|
}).then(async (stream) => {
|
|
151
461
|
await stream.allReady;
|
|
152
462
|
return readStream(stream);
|
|
153
|
-
}).then(resolve).catch(reject);
|
|
463
|
+
}).then((result) => resolve(stripImagePreloadLinks(result))).catch(reject);
|
|
154
464
|
});
|
|
155
|
-
if (options?.plainText) return toPlainText(html
|
|
156
|
-
const document = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">${html
|
|
465
|
+
if (options?.plainText) return options.unstableTextConversion ? unstableToPlainText(html) : toPlainText(html, options.htmlToTextOptions);
|
|
466
|
+
const document = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">${html.replace(/<!DOCTYPE.*?>/, "")}`;
|
|
157
467
|
if (options?.pretty) return pretty(document);
|
|
158
468
|
return document;
|
|
159
469
|
};
|
|
160
|
-
|
|
161
470
|
//#endregion
|
|
162
471
|
export { plainTextSelectors, pretty, render, toPlainText };
|
|
472
|
+
|
|
163
473
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["html","html"],"sources":["../../src/shared/utils/pretty.ts","../../src/shared/utils/to-plain-text.ts","../../src/shared/error-boundary.tsx","../../src/shared/read-stream.browser.ts","../../src/browser/render.tsx"],"sourcesContent":["import type { Options, Plugin } from 'prettier';\nimport type { builders } from 'prettier/doc';\nimport * as html from 'prettier/plugins/html';\nimport { format } from 'prettier/standalone';\n\ninterface HtmlNode {\n type?: 'element' | 'text' | 'ieConditionalComment';\n kind?: 'element' | 'text' | 'ieConditionalComment' | 'root';\n name?: string;\n sourceSpan: {\n start: { file: unknown[]; offset: number; line: number; col: number };\n end: { file: unknown[]; offset: number; line: number; col: number };\n details: null;\n };\n parent?: HtmlNode;\n}\n\nfunction getHtmlNode(path: {\n node?: HtmlNode;\n stack?: Array<Record<string, unknown>>;\n}) {\n const topNode = path.node;\n if (topNode) {\n return topNode;\n }\n\n return path.stack?.[path.stack.length - 1] as HtmlNode;\n}\n\nfunction recursivelyMapDoc(\n doc: builders.Doc,\n callback: (innerDoc: string | builders.DocCommand) => builders.Doc,\n): builders.Doc {\n if (Array.isArray(doc)) {\n return doc.map((innerDoc) => recursivelyMapDoc(innerDoc, callback));\n }\n\n if (typeof doc === 'object') {\n if (doc.type === 'line') {\n return callback(doc.soft ? '' : ' ');\n }\n\n if (doc.type === 'group') {\n return {\n ...doc,\n contents: recursivelyMapDoc(doc.contents, callback),\n expandedStates: recursivelyMapDoc(\n doc.expandedStates,\n callback,\n ) as builders.Doc[],\n };\n }\n\n if ('contents' in doc) {\n return {\n ...doc,\n contents: recursivelyMapDoc(doc.contents, callback),\n };\n }\n\n if ('parts' in doc) {\n return {\n ...doc,\n parts: recursivelyMapDoc(doc.parts, callback) as builders.Doc[],\n };\n }\n\n if (doc.type === 'if-break') {\n return {\n ...doc,\n breakContents: recursivelyMapDoc(doc.breakContents, callback),\n flatContents: recursivelyMapDoc(doc.flatContents, callback),\n };\n }\n\n const nextDoc = { ...doc } as Record<string, unknown>;\n for (const [key, value] of Object.entries(nextDoc)) {\n if (value && typeof value === 'object') {\n nextDoc[key] = recursivelyMapDoc(value as builders.Doc, callback);\n }\n }\n\n return nextDoc as builders.Doc;\n }\n\n return callback(doc);\n}\n\nconst modifiedHtml = { ...html } as Plugin;\nif (modifiedHtml.printers) {\n const previousPrint = modifiedHtml.printers.html.print;\n modifiedHtml.printers.html.print = (path, options, print, args) => {\n const node = getHtmlNode(\n path as Parameters<Plugin['printers']['html']['print']>[0],\n );\n\n const rawPrintingResult = previousPrint(path, options, print, args);\n\n if (\n node?.type === 'ieConditionalComment' ||\n node?.kind === 'ieConditionalComment'\n ) {\n const printingResult = recursivelyMapDoc(rawPrintingResult, (doc) => {\n if (typeof doc === 'object' && doc.type === 'line') {\n return doc.soft ? '' : ' ';\n }\n\n return doc;\n });\n\n return printingResult;\n }\n\n return rawPrintingResult;\n };\n}\n\nconst defaults: Options = {\n endOfLine: 'lf',\n tabWidth: 2,\n plugins: [modifiedHtml],\n bracketSameLine: true,\n parser: 'html',\n};\n\nexport const pretty = (str: string, options: Options = {}) => {\n return format(str.replaceAll('\\0', ''), {\n ...defaults,\n ...options,\n });\n};\n","import {\n convert,\n type HtmlToTextOptions,\n type SelectorDefinition,\n} from 'html-to-text';\n\nexport const plainTextSelectors: SelectorDefinition[] = [\n { selector: 'img', format: 'skip' },\n { selector: '[data-skip-in-text=true]', format: 'skip' },\n {\n selector: 'a',\n options: { linkBrackets: false, hideLinkHrefIfSameAsText: true },\n },\n];\n\nexport function toPlainText(html: string, options?: HtmlToTextOptions) {\n return convert(html, {\n wordwrap: false,\n ...options,\n selectors: [...plainTextSelectors, ...(options?.selectors ?? [])],\n });\n}\n","import React from 'react';\n\nexport function createErrorBoundary(reject: (error: unknown) => void) {\n // React Server Components don't support React.Component, so it's just not defined here\n if (!React.Component) {\n return (props: { children?: React.ReactNode }) => <>{props.children}</>;\n }\n\n return class ErrorBoundary extends React.Component<{\n children: React.ReactNode;\n }> {\n componentDidCatch(error: unknown) {\n reject(error);\n }\n render() {\n return this.props.children;\n }\n };\n}\n","import type { ReactDOMServerReadableStream } from 'react-dom/server.browser';\n\nconst decoder = new TextDecoder('utf-8');\n\nexport const readStream = async (stream: ReactDOMServerReadableStream) => {\n const chunks: Uint8Array[] = [];\n\n const writableStream = new WritableStream({\n write(chunk: Uint8Array) {\n chunks.push(chunk);\n },\n abort(reason) {\n throw new Error('Stream aborted', {\n cause: {\n reason,\n },\n });\n },\n });\n await stream.pipeTo(writableStream);\n\n let length = 0;\n chunks.forEach((item) => {\n length += item.length;\n });\n const mergedChunks = new Uint8Array(length);\n let offset = 0;\n chunks.forEach((item) => {\n mergedChunks.set(item, offset);\n offset += item.length;\n });\n\n return decoder.decode(mergedChunks);\n};\n","import { Suspense } from 'react';\nimport { pretty, toPlainText } from '../node';\nimport { createErrorBoundary } from '../shared/error-boundary';\nimport type { Options } from '../shared/options';\nimport { readStream } from '../shared/read-stream.browser';\n\nexport const render = async (node: React.ReactNode, options?: Options) => {\n const reactDOMServer = await import('react-dom/server').then((m) => {\n if ('default' in m) {\n return m.default;\n }\n return m;\n });\n\n const html = await new Promise<string>((resolve, reject) => {\n const ErrorBoundary = createErrorBoundary(reject);\n reactDOMServer\n .renderToReadableStream(\n <Suspense>\n <ErrorBoundary>{node}</ErrorBoundary>\n </Suspense>,\n {\n onError(error: unknown) {\n reject(error);\n },\n progressiveChunkSize: Number.POSITIVE_INFINITY,\n },\n )\n .then(async (stream) => {\n await stream.allReady;\n return readStream(stream);\n })\n .then(resolve)\n .catch(reject);\n });\n\n if (options?.plainText) {\n return toPlainText(html, options.htmlToTextOptions);\n }\n\n const doctype =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\n const document = `${doctype}${html.replace(/<!DOCTYPE.*?>/, '')}`;\n\n if (options?.pretty) {\n return pretty(document);\n }\n\n return document;\n};\n"],"mappings":";;;;;;;AAiBA,SAAS,YAAY,MAGlB;CACD,MAAM,UAAU,KAAK;AACrB,KAAI,QACF,QAAO;AAGT,QAAO,KAAK,QAAQ,KAAK,MAAM,SAAS;;AAG1C,SAAS,kBACP,KACA,UACc;AACd,KAAI,MAAM,QAAQ,IAAI,CACpB,QAAO,IAAI,KAAK,aAAa,kBAAkB,UAAU,SAAS,CAAC;AAGrE,KAAI,OAAO,QAAQ,UAAU;AAC3B,MAAI,IAAI,SAAS,OACf,QAAO,SAAS,IAAI,OAAO,KAAK,IAAI;AAGtC,MAAI,IAAI,SAAS,QACf,QAAO;GACL,GAAG;GACH,UAAU,kBAAkB,IAAI,UAAU,SAAS;GACnD,gBAAgB,kBACd,IAAI,gBACJ,SACD;GACF;AAGH,MAAI,cAAc,IAChB,QAAO;GACL,GAAG;GACH,UAAU,kBAAkB,IAAI,UAAU,SAAS;GACpD;AAGH,MAAI,WAAW,IACb,QAAO;GACL,GAAG;GACH,OAAO,kBAAkB,IAAI,OAAO,SAAS;GAC9C;AAGH,MAAI,IAAI,SAAS,WACf,QAAO;GACL,GAAG;GACH,eAAe,kBAAkB,IAAI,eAAe,SAAS;GAC7D,cAAc,kBAAkB,IAAI,cAAc,SAAS;GAC5D;EAGH,MAAM,UAAU,EAAE,GAAG,KAAK;AAC1B,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,CAChD,KAAI,SAAS,OAAO,UAAU,SAC5B,SAAQ,OAAO,kBAAkB,OAAuB,SAAS;AAIrE,SAAO;;AAGT,QAAO,SAAS,IAAI;;AAGtB,MAAM,eAAe,EAAE,GAAG,MAAM;AAChC,IAAI,aAAa,UAAU;CACzB,MAAM,gBAAgB,aAAa,SAAS,KAAK;AACjD,cAAa,SAAS,KAAK,SAAS,MAAM,SAAS,OAAO,SAAS;EACjE,MAAM,OAAO,YACX,KACD;EAED,MAAM,oBAAoB,cAAc,MAAM,SAAS,OAAO,KAAK;AAEnE,MACE,MAAM,SAAS,0BACf,MAAM,SAAS,uBAUf,QARuB,kBAAkB,oBAAoB,QAAQ;AACnE,OAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,OAC1C,QAAO,IAAI,OAAO,KAAK;AAGzB,UAAO;IACP;AAKJ,SAAO;;;AAIX,MAAM,WAAoB;CACxB,WAAW;CACX,UAAU;CACV,SAAS,CAAC,aAAa;CACvB,iBAAiB;CACjB,QAAQ;CACT;AAED,MAAa,UAAU,KAAa,UAAmB,EAAE,KAAK;AAC5D,QAAO,OAAO,IAAI,WAAW,MAAM,GAAG,EAAE;EACtC,GAAG;EACH,GAAG;EACJ,CAAC;;;;;AC3HJ,MAAa,qBAA2C;CACtD;EAAE,UAAU;EAAO,QAAQ;EAAQ;CACnC;EAAE,UAAU;EAA4B,QAAQ;EAAQ;CACxD;EACE,UAAU;EACV,SAAS;GAAE,cAAc;GAAO,0BAA0B;GAAM;EACjE;CACF;AAED,SAAgB,YAAY,QAAc,SAA6B;AACrE,QAAO,QAAQA,QAAM;EACnB,UAAU;EACV,GAAG;EACH,WAAW,CAAC,GAAG,oBAAoB,GAAI,SAAS,aAAa,EAAE,CAAE;EAClE,CAAC;;;;;AClBJ,SAAgB,oBAAoB,QAAkC;AAEpE,KAAI,CAAC,MAAM,UACT,SAAQ,UAA0C,0CAAG,MAAM,WAAY;AAGzE,QAAO,MAAM,sBAAsB,MAAM,UAEtC;EACD,kBAAkB,OAAgB;AAChC,UAAO,MAAM;;EAEf,SAAS;AACP,UAAO,KAAK,MAAM;;;;;;;ACbxB,MAAM,UAAU,IAAI,YAAY,QAAQ;AAExC,MAAa,aAAa,OAAO,WAAyC;CACxE,MAAM,SAAuB,EAAE;CAE/B,MAAM,iBAAiB,IAAI,eAAe;EACxC,MAAM,OAAmB;AACvB,UAAO,KAAK,MAAM;;EAEpB,MAAM,QAAQ;AACZ,SAAM,IAAI,MAAM,kBAAkB,EAChC,OAAO,EACL,QACD,EACF,CAAC;;EAEL,CAAC;AACF,OAAM,OAAO,OAAO,eAAe;CAEnC,IAAI,SAAS;AACb,QAAO,SAAS,SAAS;AACvB,YAAU,KAAK;GACf;CACF,MAAM,eAAe,IAAI,WAAW,OAAO;CAC3C,IAAI,SAAS;AACb,QAAO,SAAS,SAAS;AACvB,eAAa,IAAI,MAAM,OAAO;AAC9B,YAAU,KAAK;GACf;AAEF,QAAO,QAAQ,OAAO,aAAa;;;;;AC1BrC,MAAa,SAAS,OAAO,MAAuB,YAAsB;CACxE,MAAM,iBAAiB,MAAM,OAAO,oBAAoB,MAAM,MAAM;AAClE,MAAI,aAAa,EACf,QAAO,EAAE;AAEX,SAAO;GACP;CAEF,MAAMC,SAAO,MAAM,IAAI,SAAiB,SAAS,WAAW;EAC1D,MAAM,gBAAgB,oBAAoB,OAAO;AACjD,iBACG,uBACC,oBAAC,sBACC,oBAAC,2BAAe,OAAqB,GAC5B,EACX;GACE,QAAQ,OAAgB;AACtB,WAAO,MAAM;;GAEf,sBAAsB,OAAO;GAC9B,CACF,CACA,KAAK,OAAO,WAAW;AACtB,SAAM,OAAO;AACb,UAAO,WAAW,OAAO;IACzB,CACD,KAAK,QAAQ,CACb,MAAM,OAAO;GAChB;AAEF,KAAI,SAAS,UACX,QAAO,YAAYA,QAAM,QAAQ,kBAAkB;CAMrD,MAAM,WAAW,4HAAaA,OAAK,QAAQ,iBAAiB,GAAG;AAE/D,KAAI,SAAS,OACX,QAAO,OAAO,SAAS;AAGzB,QAAO"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/shared/utils/pretty.ts","../../src/shared/utils/to-plain-text.ts","../../src/shared/utils/unstable-to-plain-text.ts","../../src/shared/error-boundary.tsx","../../src/shared/utils/strip-image-preload-links.ts","../../src/shared/read-stream.browser.ts","../../src/browser/render.tsx"],"sourcesContent":["import type { Options, Plugin } from 'prettier';\nimport type { builders } from 'prettier/doc';\nimport * as html from 'prettier/plugins/html';\nimport { format } from 'prettier/standalone';\n\ninterface HtmlNode {\n type?: 'element' | 'text' | 'ieConditionalComment';\n kind?: 'element' | 'text' | 'ieConditionalComment' | 'root';\n name?: string;\n sourceSpan: {\n start: { file: unknown[]; offset: number; line: number; col: number };\n end: { file: unknown[]; offset: number; line: number; col: number };\n details: null;\n };\n parent?: HtmlNode;\n}\n\nfunction getHtmlNode(path: {\n node?: HtmlNode;\n stack?: Array<Record<string, unknown>>;\n}) {\n const topNode = path.node;\n if (topNode) {\n return topNode;\n }\n\n return path.stack?.[path.stack.length - 1] as unknown as HtmlNode;\n}\n\nfunction recursivelyMapDoc(\n doc: builders.Doc,\n callback: (innerDoc: string | builders.DocCommand) => builders.Doc,\n): builders.Doc {\n if (Array.isArray(doc)) {\n return doc.map((innerDoc) => recursivelyMapDoc(innerDoc, callback));\n }\n\n if (typeof doc === 'object') {\n if (doc.type === 'line') {\n return callback(doc.soft ? '' : ' ');\n }\n\n if (doc.type === 'group') {\n return {\n ...doc,\n contents: recursivelyMapDoc(doc.contents, callback),\n expandedStates: recursivelyMapDoc(\n doc.expandedStates,\n callback,\n ) as builders.Doc[],\n };\n }\n\n if ('contents' in doc) {\n return {\n ...doc,\n contents: recursivelyMapDoc(doc.contents, callback),\n };\n }\n\n if ('parts' in doc) {\n return {\n ...doc,\n parts: recursivelyMapDoc(doc.parts, callback) as builders.Doc[],\n };\n }\n\n if (doc.type === 'if-break') {\n return {\n ...doc,\n breakContents: recursivelyMapDoc(doc.breakContents, callback),\n flatContents: recursivelyMapDoc(doc.flatContents, callback),\n };\n }\n\n const nextDoc = { ...doc } as Record<string, unknown>;\n for (const [key, value] of Object.entries(nextDoc)) {\n if (value && typeof value === 'object') {\n nextDoc[key] = recursivelyMapDoc(value as builders.Doc, callback);\n }\n }\n\n return nextDoc as unknown as builders.Doc;\n }\n\n return callback(doc);\n}\n\nconst modifiedHtml = { ...html } as Plugin;\nif (modifiedHtml.printers) {\n const previousPrint = modifiedHtml.printers.html.print;\n modifiedHtml.printers.html.print = (path, options, print, args) => {\n const node = getHtmlNode(\n path as Parameters<NonNullable<Plugin['printers']>['html']['print']>[0],\n );\n\n const rawPrintingResult = previousPrint(path, options, print, args);\n\n if (\n node?.type === 'ieConditionalComment' ||\n node?.kind === 'ieConditionalComment'\n ) {\n const printingResult = recursivelyMapDoc(rawPrintingResult, (doc) => {\n if (typeof doc === 'object' && doc.type === 'line') {\n return doc.soft ? '' : ' ';\n }\n\n return doc;\n });\n\n return printingResult;\n }\n\n return rawPrintingResult;\n };\n}\n\nconst defaults: Options = {\n endOfLine: 'lf',\n tabWidth: 2,\n plugins: [modifiedHtml],\n bracketSameLine: true,\n parser: 'html',\n};\n\nexport const pretty = (str: string, options: Options = {}) => {\n return format(str.replaceAll('\\0', ''), {\n ...defaults,\n ...options,\n });\n};\n","import {\n convert,\n type HtmlToTextOptions,\n type SelectorDefinition,\n} from 'html-to-text';\n\nexport const plainTextSelectors: SelectorDefinition[] = [\n { selector: 'img', format: 'skip' },\n { selector: '[data-skip-in-text=true]', format: 'skip' },\n {\n selector: 'a',\n options: { linkBrackets: false, hideLinkHrefIfSameAsText: true },\n },\n { selector: '[data-text-format=\"dataTable\"]', format: 'dataTable' },\n];\n\nexport function toPlainText(html: string, options?: HtmlToTextOptions) {\n return convert(html, {\n wordwrap: false,\n ...options,\n selectors: [...plainTextSelectors, ...(options?.selectors ?? [])],\n });\n}\n","import { decodeHTML, decodeHTMLAttribute } from 'entities/lib/decode.js';\nimport { type INode, type ITag, parse, SyntaxKind } from 'html5parser';\n\nconst SKIPPED_TAGS = new Set([\n 'img',\n 'noscript',\n 'script',\n 'style',\n 'template',\n]);\n\nconst WHITESPACE_RUN = /([ \\t\\n\\r\\f\\u200b]+)/;\n\n// A block prefix puts `first` before the block's content and `rest` at the\n// start of every following line: blockquote marks every line (\"> \"), a list\n// item marks the first (\" * \") and indents the rest to align nested content.\ntype BlockPrefix = { first: string; rest: string };\n\n// Everything tag-specific about how a block separates from its surroundings.\ntype Block = { open: number; close: number; prefix?: BlockPrefix };\n\n// html-to-text's default separation for the block tags implemented so far.\n// Tags absent here (and not handled in `enterNode`) are treated as inline\n// and contribute no separation — notably td/tr, which is why table cells run\n// together, matching `toPlainText` today. Only extend this alongside a spec\n// case verifying the behavior against `toPlainText`.\nconst TAG_BLOCKS: Record<string, Block> = {\n article: { open: 2, close: 2 },\n aside: { open: 2, close: 2 },\n blockquote: { open: 2, close: 2, prefix: { first: '> ', rest: '> ' } },\n div: { open: 2, close: 2 },\n footer: { open: 2, close: 2 },\n form: { open: 2, close: 2 },\n h1: { open: 3, close: 2 },\n h2: { open: 3, close: 2 },\n h3: { open: 3, close: 2 },\n h4: { open: 3, close: 2 },\n h5: { open: 3, close: 2 },\n h6: { open: 3, close: 2 },\n header: { open: 2, close: 2 },\n hr: { open: 2, close: 2 },\n main: { open: 2, close: 2 },\n nav: { open: 2, close: 2 },\n p: { open: 2, close: 2 },\n pre: { open: 2, close: 2 },\n section: { open: 2, close: 2 },\n table: { open: 2, close: 2 },\n};\n\ninterface OpenBlock {\n block: Block;\n text: string[];\n leading: number;\n stash: number;\n space: boolean;\n}\n\ninterface OrderedList {\n next: number;\n prefixLength: number;\n nested: boolean;\n}\n\ninterface WalkFrame {\n // the element whose children this frame is iterating, or undefined at root\n parent: ITag | undefined;\n children: INode[];\n index: number;\n pre: boolean;\n // whether this element opened a block, to be closed back on exit\n opened: boolean;\n // content length of the enclosing block when this element was entered;\n // exit of an <a> reads back the text written since\n textFrom: number;\n orderedList: OrderedList | undefined;\n}\n\nexport function unstableToPlainText(html: string): string {\n const tree = parse(html, { setAttributeMap: true });\n const body = findBody(tree);\n\n const blocks: OpenBlock[] = [\n {\n block: { open: 0, close: 0 },\n text: [],\n leading: 0,\n stash: 0,\n space: false,\n },\n ];\n const stack: WalkFrame[] = [\n {\n parent: body,\n children: body?.body ?? tree,\n index: 0,\n pre: false,\n opened: false,\n textFrom: 0,\n orderedList: undefined,\n },\n ];\n\n while (stack.length > 0) {\n const frame = stack[stack.length - 1];\n const node = frame.children[frame.index];\n if (node === undefined) {\n // children exhausted: the element ends here, the only moment its\n // exit effects can go out — they must follow the subtree's content\n stack.pop();\n exitElement(frame.parent, frame);\n continue;\n }\n frame.index += 1;\n enterNode(node, frame);\n }\n\n function top(): OpenBlock {\n return blocks[blocks.length - 1];\n }\n\n function writeWord(value: string) {\n const block = top();\n if (block.stash > 0) block.text.push('\\n'.repeat(block.stash));\n else if (block.space && block.text.length > 0) block.text.push(' ');\n block.stash = 0;\n block.space = false;\n block.text.push(value);\n }\n\n // enterNode descends into an element by pushing its frame; skipped\n // subtrees are skipped by simply not pushing\n function enterNode(node: INode, frame: WalkFrame) {\n if (node.type === SyntaxKind.Text) {\n const value = decodeHTML(node.value);\n if (frame.pre) {\n // whitespace is significant inside <pre>: one verbatim word,\n // newlines and all, so it never goes through collapsing\n if (value.length > 0) {\n writeWord(value);\n }\n } else {\n // splitting on a capturing group alternates strictly: even indices\n // are the words, odd indices are the whitespace runs between them —\n // so parity alone says which, no need to re-match each segment\n const segments = value.split(WHITESPACE_RUN);\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n if (segment.length === 0) continue;\n if (i % 2 === 1) {\n top().space = true;\n } else {\n writeWord(segment);\n }\n }\n }\n return;\n }\n if (\n SKIPPED_TAGS.has(node.name) ||\n decodeHTMLAttribute(\n node.attributeMap?.['data-skip-in-text']?.value?.value ?? '',\n ) === 'true'\n ) {\n return;\n }\n\n const parentTag = frame.parent?.name;\n\n let block: Block | undefined = TAG_BLOCKS[node.name];\n let orderedList: OrderedList | undefined;\n if (node.name === 'ul') {\n // a list directly inside an <li> sits closer to its parent item:\n // single line breaks both ways (html-to-text's isNestedList)\n const breaks = parentTag === 'li' ? 1 : 2;\n block = { open: breaks, close: breaks };\n } else if (node.name === 'li' && parentTag === 'ul') {\n // whether the enclosing list is itself nested decides the item's\n // prefix; `frame` belongs to that list, so the list's own parent is\n // one frame below the top of the stack\n const grandparent = stack[stack.length - 2]?.parent;\n const inNestedList = grandparent?.name === 'li';\n block = {\n open: 1,\n close: 1,\n prefix: inNestedList\n ? { first: '* ', rest: ' ' }\n : { first: ' * ', rest: ' ' },\n };\n } else if (node.name === 'ol') {\n const nested = parentTag === 'li';\n const parsedStart = Number.parseInt(\n decodeHTMLAttribute(node.attributeMap?.start?.value?.value ?? '1'),\n 10,\n );\n const start = Number.isNaN(parsedStart) ? 1 : parsedStart;\n const itemCount =\n node.body?.filter(\n (child) => child.type === SyntaxKind.Tag && child.name === 'li',\n ).length ?? 0;\n let prefixLength = 0;\n for (let index = start; index < start + itemCount; index++) {\n const prefix = `${nested ? '' : ' '}${index}. `;\n prefixLength = Math.max(prefixLength, prefix.length);\n }\n const breaks = nested ? 1 : 2;\n block = { open: breaks, close: breaks };\n orderedList = { next: start, prefixLength, nested };\n } else if (node.name === 'li' && parentTag === 'ol' && frame.orderedList) {\n const list = frame.orderedList;\n const prefix = `${list.nested ? '' : ' '}${list.next++}. `.padEnd(\n list.prefixLength,\n );\n block = {\n open: 1,\n close: 1,\n prefix: { first: prefix, rest: ' '.repeat(list.prefixLength) },\n };\n }\n\n if (block) {\n blocks.push({\n block,\n text: [],\n leading: block.open,\n stash: 0,\n space: false,\n });\n }\n\n if (node.name === 'hr') {\n writeWord('-'.repeat(40));\n } else if (node.name === 'br') {\n top().space = false;\n top().text.push('\\n');\n }\n\n stack.push({\n parent: node,\n children: node.body ?? [],\n index: 0,\n pre: frame.pre || node.name === 'pre',\n opened: block !== undefined,\n textFrom: top().text.length,\n orderedList,\n });\n }\n\n function exitElement(element: ITag | undefined, frame: WalkFrame) {\n if (element === undefined) return;\n\n if (element.name === 'a') {\n const href = decodeHTMLAttribute(\n element.attributeMap?.href?.value?.value ?? '',\n ).replace(/^mailto:/, '');\n // fragment-only hrefs are suppressed (html-to-text's noAnchorUrl)\n if (href.length > 0 && !href.startsWith('#')) {\n // the enclosing block's content doubles as the buffer of the\n // anchor's text: what was written since the anchor opened is\n // compared against the href to decide whether to append it\n // (html-to-text's hideLinkHrefIfSameAsText, which `toPlainText`\n // enables)\n const anchorText = top().text.slice(frame.textFrom).join('');\n if (anchorText !== href) {\n if (anchorText.length > 0) top().space = true;\n writeWord(href);\n }\n }\n }\n\n if (frame.opened) {\n closeBlock();\n }\n }\n\n function closeBlock() {\n const child = blocks.pop();\n if (child === undefined) return;\n const parent = blocks[blocks.length - 1];\n\n let content = child.text.join('');\n const prefix = child.block.prefix;\n if (prefix !== undefined) {\n const trimmed = content.replace(/^\\n+|\\n+$/g, '');\n content = prefix.first + trimmed.replaceAll('\\n', `\\n${prefix.rest}`);\n }\n\n const breaks = Math.max(parent.stash, child.leading);\n if (parent.text.length > 0) {\n parent.text.push('\\n'.repeat(breaks));\n if (content.length > 0) parent.text.push(content);\n } else {\n // nothing to separate from yet: the separation propagates upward\n // instead of materializing — this is edge suppression, and also how\n // empty blocks nested in empty blocks still commit their breaks\n if (content.length > 0) parent.text.push(content);\n parent.leading = breaks;\n }\n parent.stash = Math.max(child.stash, child.block.close);\n }\n\n return blocks[0].text.join('');\n}\n\nfunction findBody(tree: INode[]): ITag | undefined {\n for (const child of tree) {\n if (child.type !== SyntaxKind.Tag || child.name !== 'html') continue;\n for (const inner of child.body ?? []) {\n if (inner.type === SyntaxKind.Tag && inner.name === 'body') return inner;\n }\n }\n return undefined;\n}\n","import React from 'react';\n\nexport function createErrorBoundary(reject: (error: unknown) => void) {\n // React Server Components don't support React.Component, so it's just not defined here\n if (!React.Component) {\n return (props: { children?: React.ReactNode }) => <>{props.children}</>;\n }\n\n return class ErrorBoundary extends React.Component<{\n children: React.ReactNode;\n }> {\n componentDidCatch(error: unknown) {\n reject(error);\n }\n render() {\n return this.props.children;\n }\n };\n}\n","/**\n * React injects `<link rel=\"preload\" as=\"image\" />` resource hints into the\n * document `<head>` for every `<img>` it renders during server-side rendering.\n *\n * These hints are meant for browsers loading a web page, where they can speed\n * up the initial paint. In an email they are dead weight: email clients ignore\n * `<link rel=\"preload\">`, and the tags only add noise to the rendered HTML.\n *\n * @see https://github.com/resend/react-email/issues/3034\n *\n * This removes only those auto-injected image preload links, leaving every\n * other `<link>` (stylesheets, fonts, user-authored non-image preloads, ...)\n * untouched. It parses each `<link>` tag's attributes instead of relying on a\n * fixed string match, so it is not affected by attribute order or spacing.\n */\nexport const stripImagePreloadLinks = (html: string): string => {\n return html.replace(/<link\\b[^>]*\\/?>/gi, (tag) =>\n isImagePreloadLink(tag) ? '' : tag,\n );\n};\n\nconst isImagePreloadLink = (tag: string): boolean => {\n const attributes = parseAttributes(tag);\n return attributes.rel === 'preload' && attributes.as === 'image';\n};\n\nconst ATTRIBUTE_PATTERN =\n /([a-z][a-z0-9-]*)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'>]+)))?/gi;\n\n/**\n * Parses the attributes of a single, already-isolated HTML tag string (e.g.\n * `<link rel=\"preload\" as=\"image\" href=\"...\" />`) into a name → value map.\n * Attribute names are lower-cased; values are read from double, single, or\n * unquoted forms.\n */\nconst parseAttributes = (tag: string): Record<string, string> => {\n // Skip the tag name (`<link`) so it is not read as an attribute.\n const attributeSection = tag.replace(/^<[a-z][a-z0-9-]*/i, '');\n\n const attributes: Record<string, string> = {};\n for (const [\n ,\n name,\n doubleQuoted,\n singleQuoted,\n unquoted,\n ] of attributeSection.matchAll(ATTRIBUTE_PATTERN)) {\n attributes[name.toLowerCase()] =\n doubleQuoted ?? singleQuoted ?? unquoted ?? '';\n }\n\n return attributes;\n};\n","import type { ReactDOMServerReadableStream } from 'react-dom/server.browser';\n\nconst decoder = new TextDecoder('utf-8');\n\nexport const readStream = async (stream: ReactDOMServerReadableStream) => {\n const chunks: Uint8Array[] = [];\n\n const writableStream = new WritableStream({\n write(chunk: Uint8Array) {\n chunks.push(chunk);\n },\n abort(reason) {\n throw new Error('Stream aborted', {\n cause: {\n reason,\n },\n });\n },\n });\n await stream.pipeTo(writableStream);\n\n let length = 0;\n chunks.forEach((item) => {\n length += item.length;\n });\n const mergedChunks = new Uint8Array(length);\n let offset = 0;\n chunks.forEach((item) => {\n mergedChunks.set(item, offset);\n offset += item.length;\n });\n\n return decoder.decode(mergedChunks);\n};\n","import { Suspense } from 'react';\nimport { pretty, toPlainText, unstableToPlainText } from '../node';\nimport { createErrorBoundary } from '../shared/error-boundary';\nimport type { Options } from '../shared/options';\nimport { readStream } from '../shared/read-stream.browser';\nimport { stripImagePreloadLinks } from '../shared/utils/strip-image-preload-links';\n\nexport const render = async (node: React.ReactNode, options?: Options) => {\n const reactDOMServer = await import('react-dom/server').then((m) => {\n if ('default' in m) {\n return m.default;\n }\n return m;\n });\n\n const html = await new Promise<string>((resolve, reject) => {\n const ErrorBoundary = createErrorBoundary(reject);\n reactDOMServer\n .renderToReadableStream(\n <Suspense>\n <ErrorBoundary>{node}</ErrorBoundary>\n </Suspense>,\n {\n onError(error: unknown) {\n reject(error);\n },\n progressiveChunkSize: Number.POSITIVE_INFINITY,\n },\n )\n .then(async (stream) => {\n await stream.allReady;\n return readStream(stream);\n })\n .then((result) => resolve(stripImagePreloadLinks(result)))\n .catch(reject);\n });\n\n if (options?.plainText) {\n return options.unstableTextConversion\n ? unstableToPlainText(html)\n : toPlainText(html, options.htmlToTextOptions);\n }\n\n const doctype =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\n const document = `${doctype}${html.replace(/<!DOCTYPE.*?>/, '')}`;\n\n if (options?.pretty) {\n return pretty(document);\n }\n\n return document;\n};\n"],"mappings":"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"}
|