50
};
51
}
52
53
-// Constants for the namespace we use. We don't actually provide the namespace but conditionally
54
-// use different segment parents based on namespace. Therefore we use constants instead of the string.
55
-const ROOT_NAMESPACE = 0; // At the root we don't need to know which namespace it is. We just need to know that it's already the right one.
56
-const HTML_NAMESPACE = 1;
57
-const SVG_NAMESPACE = 2;
58
-const MATHML_NAMESPACE = 3;
59
-
60
-type NamespaceFlag = 0 | 1 | 2 | 3;
53
+// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
54
+// modes. We only include the variants as they matter for the sake of our purposes.
55
+// We don't actually provide the namespace therefore we use constants instead of the string.
56
+const HTML_MODE = 0;
57
+const SVG_MODE = 1;
58
+const MATHML_MODE = 2;
59
+const HTML_TABLE_MODE = 4;
60
+const HTML_TABLE_BODY_MODE = 5;
61
+const HTML_TABLE_ROW_MODE = 6;
62
+const HTML_COLGROUP_MODE = 7;
63
+// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
64
+// still makes sense
65
+
66
+type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
67
68
// Lets us keep track of contextual state and pick it back up after suspending.
69
export type FormatContext = {
64
- namespace: NamespaceFlag, // root/svg/html/mathml
70
+ insertionMode: InsertionMode, // root/svg/html/mathml/table
71
selectedValue: null | string, // the selected value(s) inside a <select>, or null outside <select>
72
};
73
74
function createFormatContext(
69
- namespace: NamespaceFlag,
75
+ insertionMode: InsertionMode,
76
selectedValue: null | string,
77
): FormatContext {
78
return {
73
- namespace,
79
+ insertionMode,
80
selectedValue,
81
};
82
}
83
78
-export function createRootFormatContext(): FormatContext {
79
- return createFormatContext(ROOT_NAMESPACE, null);
84
+export function createRootFormatContext(namespaceURI?: string): FormatContext {
85
+ const insertionMode =
86
+ namespaceURI === 'http://www.w3.org/2000/svg'
87
+ ? SVG_MODE
88
+ : namespaceURI === 'http://www.w3.org/1998/Math/MathML'
89
+ ? MATHML_MODE
90
+ : HTML_MODE;
91
+ return createFormatContext(insertionMode, null);
92
}
93
94
export function getChildFormatContext(
99
switch (type) {
100
case 'select':
101
return createFormatContext(
90
- parentContext.namespace,
102
+ HTML_MODE,
103
props.value != null ? props.value : props.defaultValue,
104
);
105
case 'svg':
94
- return createFormatContext(SVG_NAMESPACE, null);
106
+ return createFormatContext(SVG_MODE, null);
107
case 'math':
96
- return createFormatContext(MATHML_NAMESPACE, null);
108
+ return createFormatContext(MATHML_MODE, null);
109
case 'foreignObject':
98
- return createFormatContext(HTML_NAMESPACE, null);
110
+ return createFormatContext(HTML_MODE, null);
111
+ // Table parents are special in that their children can only be created at all if they're
112
+ // wrapped in a table parent. So we need to encode that we're entering this mode.
113
+ case 'table':
114
+ return createFormatContext(HTML_TABLE_MODE, null);
115
+ case 'thead':
116
+ case 'tbody':
117
+ case 'tfoot':
118
+ return createFormatContext(HTML_TABLE_BODY_MODE, null);
119
+ case 'colgroup':
120
+ return createFormatContext(HTML_COLGROUP_MODE, null);
121
+ case 'tr':
122
+ return createFormatContext(HTML_TABLE_ROW_MODE, null);
123
+ }
124
+ if (parentContext.insertionMode >= HTML_TABLE_MODE) {
125
+ // Whatever tag this was, it wasn't a table parent or other special parent, so we must have
126
+ // entered plain HTML again.
127
+ return createFormatContext(HTML_MODE, null);
128
}
129
return parentContext;
130
}
161
));
162
}
163
135
-const dummyNode1 = stringToPrecomputedChunk('<span hidden id="');
136
-const dummyNode2 = stringToPrecomputedChunk('"></span>');
164
+const dummyNode1 = stringToPrecomputedChunk('<template id="');
165
+const dummyNode2 = stringToPrecomputedChunk('"></template>');
166
167
function pushDummyNodeWithID(
168
target: Array<Chunk | PrecomputedChunk>,
235
startTag2,
236
);
237
} else {
209
- target.push(startTag1, stringToChunk(type), startTag2);
238
+ target.push(startTag1, stringToChunk(type));
239
+ if (props.className) {
240
+ target.push(
241
+ stringToChunk(
242
+ ' class="' + encodeHTMLIDAttribute(props.className) + '"',
243
+ ),
244
+ );
245
+ }
246
+ if (props.id) {
247
+ target.push(
248
+ stringToChunk(' id="' + encodeHTMLIDAttribute(props.id) + '"'),
249
+ );
250
+ }
251
+ target.push(startTag2);
252
}
253
}
254
267
// Structural Nodes
268
269
// A placeholder is a node inside a hidden partial tree that can be filled in later, but before
228
-// display. It's never visible to users.
229
-const placeholder1 = stringToPrecomputedChunk('<span id="');
230
-const placeholder2 = stringToPrecomputedChunk('"></span>');
270
+// display. It's never visible to users. We use the template tag because it can be used in every
271
+// type of parent. <script> tags also work in every other tag except <colgroup>.
272
+const placeholder1 = stringToPrecomputedChunk('<template id="');
273
+const placeholder2 = stringToPrecomputedChunk('"></template>');
274
export function writePlaceholder(
275
destination: Destination,
276
responseState: ResponseState,
277
id: number,
278
): boolean {
236
- // TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like
237
- // <select> or <tbody>. E.g. suspending a component that renders a table row.
279
writeChunk(destination, placeholder1);
280
writeChunk(destination, responseState.placeholderPrefix);
281
const formattedID = stringToChunk(id.toString(16));
313
return writeChunk(destination, endSuspenseBoundary);
314
}
315
275
-const startSegment = stringToPrecomputedChunk('<div hidden id="');
276
-const startSegment2 = stringToPrecomputedChunk('">');
277
-const endSegment = stringToPrecomputedChunk('</div>');
316
+const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
317
+const startSegmentHTML2 = stringToPrecomputedChunk('">');
318
+const endSegmentHTML = stringToPrecomputedChunk('</div>');
319
+
320
+const startSegmentSVG = stringToPrecomputedChunk(
321
+ '<svg aria-hidden="true" style="display:none" id="',
322
+);
323
+const startSegmentSVG2 = stringToPrecomputedChunk('">');
324
+const endSegmentSVG = stringToPrecomputedChunk('</svg>');
325
+
326
+const startSegmentMathML = stringToPrecomputedChunk(
327
+ '<math aria-hidden="true" style="display:none" id="',
328
+);
329
+const startSegmentMathML2 = stringToPrecomputedChunk('">');
330
+const endSegmentMathML = stringToPrecomputedChunk('</math>');
331
+
332
+const startSegmentTable = stringToPrecomputedChunk('<table hidden id="');
333
+const startSegmentTable2 = stringToPrecomputedChunk('">');
334
+const endSegmentTable = stringToPrecomputedChunk('</table>');
335
+
336
+const startSegmentTableBody = stringToPrecomputedChunk(
337
+ '<table hidden><tbody id="',
338
+);
339
+const startSegmentTableBody2 = stringToPrecomputedChunk('">');
340
+const endSegmentTableBody = stringToPrecomputedChunk('</tbody></table>');
341
+
342
+const startSegmentTableRow = stringToPrecomputedChunk('<table hidden><tr id="');
343
+const startSegmentTableRow2 = stringToPrecomputedChunk('">');
344
+const endSegmentTableRow = stringToPrecomputedChunk('</tr></table>');
345
+
346
+const startSegmentColGroup = stringToPrecomputedChunk(
347
+ '<table hidden><colgroup id="',
348
+);
349
+const startSegmentColGroup2 = stringToPrecomputedChunk('">');
350
+const endSegmentColGroup = stringToPrecomputedChunk('</colgroup></table>');
351
+
352
export function writeStartSegment(
353
destination: Destination,
354
responseState: ResponseState,
355
+ formatContext: FormatContext,
356
id: number,
357
): boolean {
283
- // TODO: What happens with special children like <tr> if they're inserted in a div? Maybe needs contextually aware containers.
284
- writeChunk(destination, startSegment);
285
- writeChunk(destination, responseState.segmentPrefix);
286
- const formattedID = stringToChunk(id.toString(16));
287
- writeChunk(destination, formattedID);
288
- return writeChunk(destination, startSegment2);
358
+ switch (formatContext.insertionMode) {
359
+ case HTML_MODE: {
360
+ writeChunk(destination, startSegmentHTML);
361
+ writeChunk(destination, responseState.segmentPrefix);
362
+ writeChunk(destination, stringToChunk(id.toString(16)));
363
+ return writeChunk(destination, startSegmentHTML2);
364
+ }
365
+ case SVG_MODE: {
366
+ writeChunk(destination, startSegmentSVG);
367
+ writeChunk(destination, responseState.segmentPrefix);
368
+ writeChunk(destination, stringToChunk(id.toString(16)));
369
+ return writeChunk(destination, startSegmentSVG2);
370
+ }
371
+ case MATHML_MODE: {
372
+ writeChunk(destination, startSegmentMathML);
373
+ writeChunk(destination, responseState.segmentPrefix);
374
+ writeChunk(destination, stringToChunk(id.toString(16)));
375
+ return writeChunk(destination, startSegmentMathML2);
376
+ }
377
+ case HTML_TABLE_MODE: {
378
+ writeChunk(destination, startSegmentTable);
379
+ writeChunk(destination, responseState.segmentPrefix);
380
+ writeChunk(destination, stringToChunk(id.toString(16)));
381
+ return writeChunk(destination, startSegmentTable2);
382
+ }
383
+ // TODO: For the rest of these, there will be extra wrapper nodes that never
384
+ // get deleted from the document. We need to delete the table too as part
385
+ // of the injected scripts. They are invisible though so it's not too terrible
386
+ // and it's kind of an edge case to suspend in a table. Totally supported though.
387
+ case HTML_TABLE_BODY_MODE: {
388
+ writeChunk(destination, startSegmentTableBody);
389
+ writeChunk(destination, responseState.segmentPrefix);
390
+ writeChunk(destination, stringToChunk(id.toString(16)));
391
+ return writeChunk(destination, startSegmentTableBody2);
392
+ }
393
+ case HTML_TABLE_ROW_MODE: {
394
+ writeChunk(destination, startSegmentTableRow);
395
+ writeChunk(destination, responseState.segmentPrefix);
396
+ writeChunk(destination, stringToChunk(id.toString(16)));
397
+ return writeChunk(destination, startSegmentTableRow2);
398
+ }
399
+ case HTML_COLGROUP_MODE: {
400
+ writeChunk(destination, startSegmentColGroup);
401
+ writeChunk(destination, responseState.segmentPrefix);
402
+ writeChunk(destination, stringToChunk(id.toString(16)));
403
+ return writeChunk(destination, startSegmentColGroup2);
404
+ }
405
+ default: {
406
+ invariant(false, 'Unknown insertion mode. This is a bug in React.');
407
+ }
408
+ }
409
}
290
-export function writeEndSegment(destination: Destination): boolean {
291
- return writeChunk(destination, endSegment);
410
+export function writeEndSegment(
411
+ destination: Destination,
412
+ formatContext: FormatContext,
413
+): boolean {
414
+ switch (formatContext.insertionMode) {
415
+ case HTML_MODE: {
416
+ return writeChunk(destination, endSegmentHTML);
417
+ }
418
+ case SVG_MODE: {
419
+ return writeChunk(destination, endSegmentSVG);
420
+ }
421
+ case MATHML_MODE: {
422
+ return writeChunk(destination, endSegmentMathML);
423
+ }
424
+ case HTML_TABLE_MODE: {
425
+ return writeChunk(destination, endSegmentTable);
426
+ }
427
+ case HTML_TABLE_BODY_MODE: {
428
+ return writeChunk(destination, endSegmentTableBody);
429
+ }
430
+ case HTML_TABLE_ROW_MODE: {
431
+ return writeChunk(destination, endSegmentTableRow);
432
+ }
433
+ case HTML_COLGROUP_MODE: {
434
+ return writeChunk(destination, endSegmentColGroup);
435
+ }
436
+ default: {
437
+ invariant(false, 'Unknown insertion mode. This is a bug in React.');
438
+ }
439
+ }
440
}
441
442
// Instruction Set