80
81
let didWarnControlledToUncontrolled = false;
82
let didWarnUncontrolledToControlled = false;
83
-let didWarnInvalidHydration = false;
83
let didWarnFormActionType = false;
84
let didWarnFormActionName = false;
85
let didWarnFormActionTarget = false;
226
propName: string,
227
serverValue: mixed,
228
clientValue: mixed,
230
-) {
229
+ serverDifferences: {[propName: string]: mixed},
230
+): void {
231
if (__DEV__) {
232
- if (didWarnInvalidHydration) {
233
- return;
234
- }
232
if (serverValue === clientValue) {
233
return;
234
}
239
if (normalizedServerValue === normalizedClientValue) {
240
return;
241
}
245
- didWarnInvalidHydration = true;
246
- console.error(
247
- 'Prop `%s` did not match. Server: %s Client: %s',
248
- propName,
249
- JSON.stringify(normalizedServerValue),
250
- JSON.stringify(normalizedClientValue),
251
- );
242
+
243
+ serverDifferences[propName] = serverValue;
244
}
245
}
246
255
-function warnForExtraAttributes(attributeNames: Set<string>) {
247
+function warnForExtraAttributes(
248
+ domElement: Element,
249
+ attributeNames: Set<string>,
250
+ serverDifferences: {[propName: string]: mixed},
251
+) {
252
if (__DEV__) {
257
- if (didWarnInvalidHydration) {
258
- return;
259
- }
260
- didWarnInvalidHydration = true;
261
- const names = [];
262
- attributeNames.forEach(function (name) {
263
- names.push(name);
253
+ attributeNames.forEach(function (attributeName) {
254
+ serverDifferences[attributeName] =
255
+ attributeName === 'style'
256
+ ? getStylesObjectFromElement(domElement)
257
+ : domElement.getAttribute(attributeName);
258
});
265
- console.error('Extra attributes from the server: %s', names);
259
}
260
}
261
319
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
320
}
321
329
-export function checkForUnmatchedText(
322
+function checkForUnmatchedText(
323
serverText: string,
324
clientText: string | number | bigint,
332
- shouldWarnDev: boolean,
325
) {
326
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
327
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
328
if (normalizedServerText === normalizedClientText) {
337
- return;
338
- }
339
-
340
- if (shouldWarnDev) {
341
- if (__DEV__) {
342
- if (!didWarnInvalidHydration) {
343
- didWarnInvalidHydration = true;
344
- console.error(
345
- 'Text content did not match. Server: "%s" Client: "%s"',
346
- normalizedServerText,
347
- normalizedClientText,
348
- );
349
- }
350
- }
329
+ return true;
330
}
352
-
353
- // In concurrent roots, we throw when there's a text mismatch and revert to
354
- // client rendering, up to the nearest Suspense boundary.
355
- throw new Error('Text content does not match server-rendered HTML.');
331
+ return false;
332
}
333
334
function noop() {}
1829
return null;
1830
}
1831
1856
-function diffHydratedStyles(domElement: Element, value: mixed) {
1832
+export function getPropsFromElement(domElement: Element): Object {
1833
+ const serverDifferences: {[propName: string]: mixed} = {};
1834
+ const attributes = domElement.attributes;
1835
+ for (let i = 0; i < attributes.length; i++) {
1836
+ const attr = attributes[i];
1837
+ serverDifferences[attr.name] =
1838
+ attr.name.toLowerCase() === 'style'
1839
+ ? getStylesObjectFromElement(domElement)
1840
+ : attr.value;
1841
+ }
1842
+ return serverDifferences;
1843
+}
1844
+
1845
+function getStylesObjectFromElement(domElement: Element): {
1846
+ [styleName: string]: string,
1847
+} {
1848
+ const serverValueInObjectForm: {[prop: string]: string} = {};
1849
+ const style = ((domElement: any): HTMLElement).style;
1850
+ for (let i = 0; i < style.length; i++) {
1851
+ const styleName: string = style[i];
1852
+ // TODO: We should use the original prop value here if it is equivalent.
1853
+ // TODO: We could use the original client capitalization if the equivalent
1854
+ // other capitalization exists in the DOM.
1855
+ serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
1856
+ }
1857
+ return serverValueInObjectForm;
1858
+}
1859
+
1860
+function diffHydratedStyles(
1861
+ domElement: Element,
1862
+ value: mixed,
1863
+ serverDifferences: {[propName: string]: mixed},
1864
+): void {
1865
if (value != null && typeof value !== 'object') {
1858
- throw new Error(
1859
- 'The `style` prop expects a mapping from style properties to values, ' +
1860
- "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
1861
- 'using JSX.',
1862
- );
1866
+ if (__DEV__) {
1867
+ console.error(
1868
+ 'The `style` prop expects a mapping from style properties to values, ' +
1869
+ "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
1870
+ 'using JSX.',
1871
+ );
1872
+ }
1873
+ return;
1874
}
1875
if (canDiffStyleForHydrationWarning) {
1865
- const expectedStyle = createDangerousStringForStyles(value);
1876
+ // First we compare the string form and see if it's equivalent.
1877
+ // This lets us bail out on anything that used to pass in this form.
1878
+ // It also lets us compare anything that's not parsed by this browser.
1879
+ const clientValue = createDangerousStringForStyles(value);
1880
const serverValue = domElement.getAttribute('style');
1867
- warnForPropDifference('style', serverValue, expectedStyle);
1881
+
1882
+ if (serverValue === clientValue) {
1883
+ return;
1884
+ }
1885
+ const normalizedClientValue =
1886
+ normalizeMarkupForTextOrAttribute(clientValue);
1887
+ const normalizedServerValue =
1888
+ normalizeMarkupForTextOrAttribute(serverValue);
1889
+ if (normalizedServerValue === normalizedClientValue) {
1890
+ return;
1891
+ }
1892
+
1893
+ // Otherwise, we create the object from the DOM for the diff view.
1894
+ serverDifferences.style = getStylesObjectFromElement(domElement);
1895
}
1896
}
1897
1901
attributeName: string,
1902
value: any,
1903
extraAttributes: Set<string>,
1904
+ serverDifferences: {[propName: string]: mixed},
1905
): void {
1906
extraAttributes.delete(attributeName);
1907
const serverValue = domElement.getAttribute(attributeName);
1934
}
1935
}
1936
}
1909
- warnForPropDifference(propKey, serverValue, value);
1937
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
1938
}
1939
1940
function hydrateBooleanAttribute(
1943
attributeName: string,
1944
value: any,
1945
extraAttributes: Set<string>,
1946
+ serverDifferences: {[propName: string]: mixed},
1947
): void {
1948
extraAttributes.delete(attributeName);
1949
const serverValue = domElement.getAttribute(attributeName);
1971
}
1972
}
1973
}
1945
- warnForPropDifference(propKey, serverValue, value);
1974
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
1975
}
1976
1977
function hydrateOverloadedBooleanAttribute(
1980
attributeName: string,
1981
value: any,
1982
extraAttributes: Set<string>,
1983
+ serverDifferences: {[propName: string]: mixed},
1984
): void {
1985
extraAttributes.delete(attributeName);
1986
const serverValue = domElement.getAttribute(attributeName);
2020
}
2021
}
2022
}
1993
- warnForPropDifference(propKey, serverValue, value);
2023
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2024
}
2025
2026
function hydrateBooleanishAttribute(
2029
attributeName: string,
2030
value: any,
2031
extraAttributes: Set<string>,
2032
+ serverDifferences: {[propName: string]: mixed},
2033
): void {
2034
extraAttributes.delete(attributeName);
2035
const serverValue = domElement.getAttribute(attributeName);
2060
}
2061
}
2062
}
2032
- warnForPropDifference(propKey, serverValue, value);
2063
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2064
}
2065
2066
function hydrateNumericAttribute(
2069
attributeName: string,
2070
value: any,
2071
extraAttributes: Set<string>,
2072
+ serverDifferences: {[propName: string]: mixed},
2073
): void {
2074
extraAttributes.delete(attributeName);
2075
const serverValue = domElement.getAttribute(attributeName);
2111
}
2112
}
2113
}
2082
- warnForPropDifference(propKey, serverValue, value);
2114
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2115
}
2116
2117
function hydratePositiveNumericAttribute(
2120
attributeName: string,
2121
value: any,
2122
extraAttributes: Set<string>,
2123
+ serverDifferences: {[propName: string]: mixed},
2124
): void {
2125
extraAttributes.delete(attributeName);
2126
const serverValue = domElement.getAttribute(attributeName);
2162
}
2163
}
2164
}
2132
- warnForPropDifference(propKey, serverValue, value);
2165
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2166
}
2167
2168
function hydrateSanitizedAttribute(
2171
attributeName: string,
2172
value: any,
2173
extraAttributes: Set<string>,
2174
+ serverDifferences: {[propName: string]: mixed},
2175
): void {
2176
extraAttributes.delete(attributeName);
2177
const serverValue = domElement.getAttribute(attributeName);
2205
}
2206
}
2207
}
2174
- warnForPropDifference(propKey, serverValue, value);
2208
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2209
}
2210
2211
function diffHydratedCustomComponent(
2214
props: Object,
2215
hostContext: HostContext,
2216
extraAttributes: Set<string>,
2217
+ serverDifferences: {[propName: string]: mixed},
2218
) {
2219
for (const propKey in props) {
2220
if (!props.hasOwnProperty(propKey)) {
2236
}
2237
// Validate that the properties correspond to their expected values.
2238
switch (propKey) {
2204
- case 'children': // Checked above already
2239
+ case 'children': {
2240
+ if (typeof value === 'string' || typeof value === 'number') {
2241
+ warnForPropDifference(
2242
+ 'children',
2243
+ domElement.textContent,
2244
+ value,
2245
+ serverDifferences,
2246
+ );
2247
+ }
2248
+ continue;
2249
+ }
2250
+ // Checked above already
2251
case 'suppressContentEditableWarning':
2252
case 'suppressHydrationWarning':
2253
case 'defaultValue':
2261
const nextHtml = value ? value.__html : undefined;
2262
if (nextHtml != null) {
2263
const expectedHTML = normalizeHTML(domElement, nextHtml);
2218
- warnForPropDifference(propKey, serverHTML, expectedHTML);
2264
+ warnForPropDifference(
2265
+ propKey,
2266
+ serverHTML,
2267
+ expectedHTML,
2268
+ serverDifferences,
2269
+ );
2270
}
2271
continue;
2272
case 'style':
2273
extraAttributes.delete(propKey);
2223
- diffHydratedStyles(domElement, value);
2274
+ diffHydratedStyles(domElement, value, serverDifferences);
2275
continue;
2276
case 'offsetParent':
2277
case 'offsetTop':
2301
'class',
2302
value,
2303
);
2253
- warnForPropDifference('className', serverValue, value);
2304
+ warnForPropDifference(
2305
+ 'className',
2306
+ serverValue,
2307
+ value,
2308
+ serverDifferences,
2309
+ );
2310
continue;
2311
}
2312
// Fall through
2328
propKey,
2329
value,
2330
);
2275
- warnForPropDifference(propKey, serverValue, value);
2331
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2332
}
2333
}
2334
}
2347
props: Object,
2348
hostContext: HostContext,
2349
extraAttributes: Set<string>,
2350
+ serverDifferences: {[propName: string]: mixed},
2351
) {
2352
for (const propKey in props) {
2353
if (!props.hasOwnProperty(propKey)) {
2369
}
2370
// Validate that the properties correspond to their expected values.
2371
switch (propKey) {
2315
- case 'children': // Checked above already
2372
+ case 'children': {
2373
+ if (typeof value === 'string' || typeof value === 'number') {
2374
+ warnForPropDifference(
2375
+ 'children',
2376
+ domElement.textContent,
2377
+ value,
2378
+ serverDifferences,
2379
+ );
2380
+ }
2381
+ continue;
2382
+ }
2383
+ // Checked above already
2384
case 'suppressContentEditableWarning':
2385
case 'suppressHydrationWarning':
2386
case 'value': // Controlled attributes are not validated
2397
const nextHtml = value ? value.__html : undefined;
2398
if (nextHtml != null) {
2399
const expectedHTML = normalizeHTML(domElement, nextHtml);
2332
- warnForPropDifference(propKey, serverHTML, expectedHTML);
2400
+ if (serverHTML !== expectedHTML) {
2401
+ serverDifferences[propKey] = {
2402
+ __html: serverHTML,
2403
+ };
2404
+ }
2405
}
2406
continue;
2407
case 'className':
2336
- hydrateAttribute(domElement, propKey, 'class', value, extraAttributes);
2408
+ hydrateAttribute(
2409
+ domElement,
2410
+ propKey,
2411
+ 'class',
2412
+ value,
2413
+ extraAttributes,
2414
+ serverDifferences,
2415
+ );
2416
continue;
2417
case 'tabIndex':
2418
hydrateAttribute(
2421
'tabindex',
2422
value,
2423
extraAttributes,
2424
+ serverDifferences,
2425
);
2426
continue;
2427
case 'style':
2428
extraAttributes.delete(propKey);
2349
- diffHydratedStyles(domElement, value);
2429
+ diffHydratedStyles(domElement, value, serverDifferences);
2430
continue;
2431
case 'multiple': {
2432
extraAttributes.delete(propKey);
2433
const serverValue = (domElement: any).multiple;
2354
- warnForPropDifference(propKey, serverValue, value);
2434
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2435
continue;
2436
}
2437
case 'muted': {
2438
extraAttributes.delete(propKey);
2439
const serverValue = (domElement: any).muted;
2360
- warnForPropDifference(propKey, serverValue, value);
2440
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2441
continue;
2442
}
2443
case 'autoFocus': {
2444
extraAttributes.delete('autofocus');
2445
const serverValue = (domElement: any).autofocus;
2366
- warnForPropDifference(propKey, serverValue, value);
2446
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2447
continue;
2448
}
2449
case 'src':
2480
propKey,
2481
null,
2482
extraAttributes,
2483
+ serverDifferences,
2484
);
2485
continue;
2486
}
2491
propKey,
2492
value,
2493
extraAttributes,
2494
+ serverDifferences,
2495
);
2496
continue;
2497
case 'action':
2520
continue;
2521
} else if (serverValue === EXPECTED_FORM_ACTION_URL) {
2522
extraAttributes.delete(propKey.toLowerCase());
2441
- warnForPropDifference(propKey, 'function', value);
2523
+ warnForPropDifference(propKey, 'function', value, serverDifferences);
2524
continue;
2525
}
2526
hydrateSanitizedAttribute(
2529
propKey.toLowerCase(),
2530
value,
2531
extraAttributes,
2532
+ serverDifferences,
2533
);
2534
continue;
2535
}
2540
'xlink:href',
2541
value,
2542
extraAttributes,
2543
+ serverDifferences,
2544
);
2545
continue;
2546
case 'contentEditable': {
2551
'contenteditable',
2552
value,
2553
extraAttributes,
2554
+ serverDifferences,
2555
);
2556
continue;
2557
}
2563
'spellcheck',
2564
value,
2565
extraAttributes,
2566
+ serverDifferences,
2567
);
2568
continue;
2569
}
2579
propKey,
2580
value,
2581
extraAttributes,
2582
+ serverDifferences,
2583
);
2584
continue;
2585
}
2612
propKey.toLowerCase(),
2613
value,
2614
extraAttributes,
2615
+ serverDifferences,
2616
);
2617
continue;
2618
}
2624
propKey,
2625
value,
2626
extraAttributes,
2627
+ serverDifferences,
2628
);
2629
continue;
2630
}
2638
propKey,
2639
value,
2640
extraAttributes,
2641
+ serverDifferences,
2642
);
2643
continue;
2644
}
2649
'rowspan',
2650
value,
2651
extraAttributes,
2652
+ serverDifferences,
2653
);
2654
continue;
2655
}
2660
propKey,
2661
value,
2662
extraAttributes,
2663
+ serverDifferences,
2664
);
2665
continue;
2666
}
2671
'x-height',
2672
value,
2673
extraAttributes,
2674
+ serverDifferences,
2675
);
2676
continue;
2677
case 'xlinkActuate':
2681
'xlink:actuate',
2682
value,
2683
extraAttributes,
2684
+ serverDifferences,
2685
);
2686
continue;
2687
case 'xlinkArcrole':
2691
'xlink:arcrole',
2692
value,
2693
extraAttributes,
2694
+ serverDifferences,
2695
);
2696
continue;
2697
case 'xlinkRole':
2701
'xlink:role',
2702
value,
2703
extraAttributes,
2704
+ serverDifferences,
2705
);
2706
continue;
2707
case 'xlinkShow':
2711
'xlink:show',
2712
value,
2713
extraAttributes,
2714
+ serverDifferences,
2715
);
2716
continue;
2717
case 'xlinkTitle':
2721
'xlink:title',
2722
value,
2723
extraAttributes,
2724
+ serverDifferences,
2725
);
2726
continue;
2727
case 'xlinkType':
2731
'xlink:type',
2732
value,
2733
extraAttributes,
2734
+ serverDifferences,
2735
);
2736
continue;
2737
case 'xmlBase':
2741
'xml:base',
2742
value,
2743
extraAttributes,
2744
+ serverDifferences,
2745
);
2746
continue;
2747
case 'xmlLang':
2751
'xml:lang',
2752
value,
2753
extraAttributes,
2754
+ serverDifferences,
2755
);
2756
continue;
2757
case 'xmlSpace':
2761
'xml:space',
2762
value,
2763
extraAttributes,
2764
+ serverDifferences,
2765
);
2766
continue;
2767
case 'inert':
2787
propKey,
2788
value,
2789
extraAttributes,
2790
+ serverDifferences,
2791
);
2792
continue;
2793
}
2834
value,
2835
);
2836
if (!isMismatchDueToBadCasing) {
2734
- warnForPropDifference(propKey, serverValue, value);
2837
+ warnForPropDifference(propKey, serverValue, value, serverDifferences);
2838
}
2839
}
2840
}
2841
}
2842
}
2843
2741
-export function diffHydratedProperties(
2844
+export function hydrateProperties(
2845
domElement: Element,
2846
tag: string,
2847
props: Object,
2745
- shouldWarnDev: boolean,
2848
hostContext: HostContext,
2747
-): void {
2849
+): boolean {
2850
if (__DEV__) {
2851
validatePropertiesInDevelopment(tag, props);
2852
}
2959
typeof children === 'number' ||
2960
(enableBigIntSupport && typeof children === 'bigint')
2961
) {
2860
- // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
2861
- if (domElement.textContent !== '' + children) {
2862
- if (props.suppressHydrationWarning !== true) {
2863
- checkForUnmatchedText(domElement.textContent, children, shouldWarnDev);
2864
- }
2962
+ if (
2963
+ // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
2964
+ domElement.textContent !== '' + children &&
2965
+ props.suppressHydrationWarning !== true &&
2966
+ !checkForUnmatchedText(domElement.textContent, children)
2967
+ ) {
2968
+ return false;
2969
}
2970
}
2971
2982
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
2983
}
2984
2881
- if (__DEV__ && shouldWarnDev) {
2985
+ return true;
2986
+}
2987
+
2988
+export function diffHydratedProperties(
2989
+ domElement: Element,
2990
+ tag: string,
2991
+ props: Object,
2992
+ hostContext: HostContext,
2993
+): null | Object {
2994
+ const serverDifferences: {[propName: string]: mixed} = {};
2995
+ if (__DEV__) {
2996
const extraAttributes: Set<string> = new Set();
2997
const attributes = domElement.attributes;
2998
for (let i = 0; i < attributes.length; i++) {
3019
props,
3020
hostContext,
3021
extraAttributes,
3022
+ serverDifferences,
3023
);
3024
} else {
3025
diffHydratedGenericElement(
3028
props,
3029
hostContext,
3030
extraAttributes,
3031
+ serverDifferences,
3032
);
3033
}
3034
if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) {
2919
- warnForExtraAttributes(extraAttributes);
3035
+ warnForExtraAttributes(domElement, extraAttributes, serverDifferences);
3036
}
3037
}
2922
-}
2923
-
2924
-export function diffHydratedText(textNode: Text, text: string): boolean {
2925
- const isDifferent = textNode.nodeValue !== text;
2926
- return isDifferent;
2927
-}
2928
-
2929
-export function warnForDeletedHydratableElement(
2930
- parentNode: Element | Document | DocumentFragment,
2931
- child: Element,
2932
-) {
2933
- if (__DEV__) {
2934
- if (didWarnInvalidHydration) {
2935
- return;
2936
- }
2937
- didWarnInvalidHydration = true;
2938
- console.error(
2939
- 'Did not expect server HTML to contain a <%s> in <%s>.',
2940
- child.nodeName.toLowerCase(),
2941
- parentNode.nodeName.toLowerCase(),
2942
- );
3038
+ if (Object.keys(serverDifferences).length === 0) {
3039
+ return null;
3040
}
3041
+ return serverDifferences;
3042
}
3043
2946
-export function warnForDeletedHydratableText(
2947
- parentNode: Element | Document | DocumentFragment,
2948
- child: Text,
2949
-) {
2950
- if (__DEV__) {
2951
- if (didWarnInvalidHydration) {
2952
- return;
2953
- }
2954
- didWarnInvalidHydration = true;
2955
- console.error(
2956
- 'Did not expect server HTML to contain the text node "%s" in <%s>.',
2957
- child.nodeValue,
2958
- parentNode.nodeName.toLowerCase(),
2959
- );
3044
+export function hydrateText(
3045
+ textNode: Text,
3046
+ text: string,
3047
+ parentProps: null | Object,
3048
+): boolean {
3049
+ const isDifferent = textNode.nodeValue !== text;
3050
+ if (
3051
+ isDifferent &&
3052
+ (parentProps === null || parentProps.suppressHydrationWarning !== true) &&
3053
+ !checkForUnmatchedText(textNode.nodeValue, text)
3054
+ ) {
3055
+ return false;
3056
}
3057
+ return true;
3058
}
3059
2963
-export function warnForInsertedHydratedElement(
2964
- parentNode: Element | Document | DocumentFragment,
2965
- tag: string,
2966
- props: Object,
2967
-) {
2968
- if (__DEV__) {
2969
- if (didWarnInvalidHydration) {
2970
- return;
2971
- }
2972
- didWarnInvalidHydration = true;
2973
- console.error(
2974
- 'Expected server HTML to contain a matching <%s> in <%s>.',
2975
- tag,
2976
- parentNode.nodeName.toLowerCase(),
2977
- );
3060
+export function diffHydratedText(textNode: Text, text: string): null | string {
3061
+ if (textNode.nodeValue === text) {
3062
+ return null;
3063
}
2979
-}
2980
-
2981
-export function warnForInsertedHydratedText(
2982
- parentNode: Element | Document | DocumentFragment,
2983
- text: string,
2984
-) {
2985
- if (__DEV__) {
2986
- if (didWarnInvalidHydration) {
2987
- return;
2988
- }
2989
- didWarnInvalidHydration = true;
2990
- console.error(
2991
- 'Expected server HTML to contain a matching text node for "%s" in <%s>.',
2992
- text,
2993
- parentNode.nodeName.toLowerCase(),
2994
- );
3064
+ const normalizedClientText = normalizeMarkupForTextOrAttribute(text);
3065
+ const normalizedServerText = normalizeMarkupForTextOrAttribute(
3066
+ textNode.nodeValue,
3067
+ );
3068
+ if (normalizedServerText === normalizedClientText) {
3069
+ return null;
3070
}
3071
+ return textNode.nodeValue;
3072
}
3073
3074
export function restoreControlledState(