@reckona/mreact-server 0.0.138 → 0.0.139

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.
@@ -184,7 +184,12 @@ function appendReactNodeList(
184
184
 
185
185
  for (const node of nodes) {
186
186
  if (chain !== undefined) {
187
- chain = chain.then(() => appendReactNode(sink, node, state));
187
+ const buffered = renderReactNodeToBufferedString(node, state);
188
+ chain = chain.then(async () => {
189
+ await buffered.result;
190
+ await buffered.sink.drain();
191
+ sink.append(buffered.sink.toString());
192
+ });
188
193
  continue;
189
194
  }
190
195
 
@@ -198,6 +203,18 @@ function appendReactNodeList(
198
203
  return chain;
199
204
  }
200
205
 
206
+ function renderReactNodeToBufferedString(
207
+ node: unknown,
208
+ state: HtmlRenderState,
209
+ ): { result: void | PromiseLike<void>; sink: ReturnType<typeof createStringSink> } {
210
+ const sink = createStringSink();
211
+
212
+ return {
213
+ result: appendReactNode(sink, node, state),
214
+ sink,
215
+ };
216
+ }
217
+
201
218
  function appendReactElement(
202
219
  sink: HtmlSink,
203
220
  element: ReactCompatElement,
@@ -294,7 +311,12 @@ function appendRawTextNodeList(
294
311
 
295
312
  for (const node of nodes) {
296
313
  if (chain !== undefined) {
297
- chain = chain.then(() => appendRawTextNode(sink, node));
314
+ const buffered = renderRawTextNodeToBufferedString(node);
315
+ chain = chain.then(async () => {
316
+ await buffered.result;
317
+ await buffered.sink.drain();
318
+ sink.append(buffered.sink.toString());
319
+ });
298
320
  continue;
299
321
  }
300
322
 
@@ -308,6 +330,17 @@ function appendRawTextNodeList(
308
330
  return chain;
309
331
  }
310
332
 
333
+ function renderRawTextNodeToBufferedString(
334
+ node: unknown,
335
+ ): { result: void | PromiseLike<void>; sink: ReturnType<typeof createStringSink> } {
336
+ const sink = createStringSink();
337
+
338
+ return {
339
+ result: appendRawTextNode(sink, node),
340
+ sink,
341
+ };
342
+ }
343
+
311
344
  function appendSuspenseElement(
312
345
  sink: HtmlSink,
313
346
  element: ReactCompatElement,
@@ -410,7 +443,8 @@ function renderHtmlAttribute(name: string, value: unknown): string {
410
443
  name === "ref" ||
411
444
  value === null ||
412
445
  value === undefined ||
413
- typeof value === "function"
446
+ typeof value === "function" ||
447
+ /^on/i.test(name)
414
448
  ) {
415
449
  return "";
416
450
  }
@@ -421,6 +455,10 @@ function renderHtmlAttribute(name: string, value: unknown): string {
421
455
  return "";
422
456
  }
423
457
 
458
+ if (/^on/i.test(attributeName)) {
459
+ return "";
460
+ }
461
+
424
462
  if (value === false && !isBooleanishStringAttribute(attributeName)) {
425
463
  return "";
426
464
  }
@@ -488,11 +526,25 @@ const HTML_ATTRIBUTE_ALIASES: Record<string, string> = {
488
526
  };
489
527
 
490
528
  function isBooleanishStringAttribute(name: string): boolean {
491
- const attributeName = toHtmlAttributeName(name).toLowerCase();
492
- return (
529
+ const attributeName = name;
530
+
531
+ if (
493
532
  attributeName.startsWith("aria-") ||
494
533
  attributeName.startsWith("data-") ||
495
534
  BOOLEANISH_STRING_ATTRIBUTES.has(attributeName)
535
+ ) {
536
+ return true;
537
+ }
538
+
539
+ if (!hasAsciiUppercase(attributeName)) {
540
+ return false;
541
+ }
542
+
543
+ const lowerAttributeName = attributeName.toLowerCase();
544
+ return (
545
+ lowerAttributeName.startsWith("aria-") ||
546
+ lowerAttributeName.startsWith("data-") ||
547
+ BOOLEANISH_STRING_ATTRIBUTES.has(lowerAttributeName)
496
548
  );
497
549
  }
498
550
 
@@ -502,6 +554,17 @@ const BOOLEANISH_STRING_ATTRIBUTES = new Set<string>([
502
554
  "spellcheck",
503
555
  ]);
504
556
 
557
+ function hasAsciiUppercase(value: string): boolean {
558
+ for (let index = 0; index < value.length; index += 1) {
559
+ const code = value.charCodeAt(index);
560
+ if (code >= 65 && code <= 90) {
561
+ return true;
562
+ }
563
+ }
564
+
565
+ return false;
566
+ }
567
+
505
568
  function isPromiseLikeNode(value: unknown): value is PromiseLike<unknown> {
506
569
  return isPromiseLikeUnknown(value);
507
570
  }
@@ -1,16 +1,17 @@
1
+ const attributeEscapePattern = /["&<>]/;
2
+ const scriptJsonEscapePattern = /[<\u2028\u2029]/;
3
+
1
4
  export function escapeAttribute(value: string): string {
2
- return value
3
- .replaceAll("&", "&amp;")
4
- .replaceAll('"', "&quot;")
5
- .replaceAll("<", "&lt;")
6
- .replaceAll(">", "&gt;");
5
+ return escapeString(value, attributeEscapePattern, attributeReplacement);
7
6
  }
8
7
 
9
8
  export function serializeScriptJson(value: unknown): string {
10
- return JSON.stringify(value)
11
- .replaceAll("<", "\\u003c")
12
- .replaceAll("\u2028", "\\u2028")
13
- .replaceAll("\u2029", "\\u2029");
9
+ const json = JSON.stringify(value);
10
+ if (json === undefined) {
11
+ throw new TypeError("Cannot serialize undefined as script JSON");
12
+ }
13
+
14
+ return escapeString(json, scriptJsonEscapePattern, scriptJsonReplacement);
14
15
  }
15
16
 
16
17
  export function renderNonceAttribute(nonce: string | undefined): string {
@@ -25,3 +26,64 @@ export function isPromiseLikeUnknown(value: unknown): value is PromiseLike<unkno
25
26
  typeof (value as { then?: unknown }).then === "function"
26
27
  );
27
28
  }
29
+
30
+ function escapeString(
31
+ value: string,
32
+ pattern: RegExp,
33
+ replacementForCode: (code: number) => string | undefined,
34
+ ): string {
35
+ const match = pattern.exec(value);
36
+
37
+ if (match === null) {
38
+ return value;
39
+ }
40
+
41
+ let escaped = "";
42
+ let lastIndex = 0;
43
+ let index = match.index;
44
+
45
+ for (; index < value.length; index += 1) {
46
+ const replacement = replacementForCode(value.charCodeAt(index));
47
+
48
+ if (replacement === undefined) {
49
+ continue;
50
+ }
51
+
52
+ if (lastIndex !== index) {
53
+ escaped += value.substring(lastIndex, index);
54
+ }
55
+
56
+ escaped += replacement;
57
+ lastIndex = index + 1;
58
+ }
59
+
60
+ return lastIndex === index ? escaped : escaped + value.substring(lastIndex, index);
61
+ }
62
+
63
+ function attributeReplacement(code: number): string | undefined {
64
+ switch (code) {
65
+ case 34:
66
+ return "&quot;";
67
+ case 38:
68
+ return "&amp;";
69
+ case 60:
70
+ return "&lt;";
71
+ case 62:
72
+ return "&gt;";
73
+ default:
74
+ return undefined;
75
+ }
76
+ }
77
+
78
+ function scriptJsonReplacement(code: number): string | undefined {
79
+ switch (code) {
80
+ case 60:
81
+ return "\\u003c";
82
+ case 8232:
83
+ return "\\u2028";
84
+ case 8233:
85
+ return "\\u2029";
86
+ default:
87
+ return undefined;
88
+ }
89
+ }