@@ -88,13 +88,29 @@ export type ScriptResource = {
88
88
root : FloatRoot ,
89
89
} ;
90
90
91
- type HeadProps = {
91
+ export type HeadResource = TitleResource | MetaResource ;
92
+
93
+ type TitleProps = {
94
+ [ string ] : mixed ,
95
+ } ;
96
+ export type TitleResource = {
97
+ type : 'title' ,
98
+ props : TitleProps ,
99
+
100
+ count : number ,
101
+ instance : ?Element ,
102
+ root : Document ,
103
+ } ;
104
+
105
+ type MetaProps = {
92
106
[ string ] : mixed ,
93
107
} ;
94
- export type HeadResource = {
95
- type : 'head' ,
96
- instanceType : string ,
97
- props : HeadProps ,
108
+ export type MetaResource = {
109
+ type : 'meta' ,
110
+ matcher : string ,
111
+ property : ?string ,
112
+ parentResource : ?MetaResource ,
113
+ props : MetaProps ,
98
114
99
115
count : number ,
100
116
instance : ?Element ,
@@ -109,6 +125,7 @@ export type RootResources = {
109
125
styles : Map < string , StyleResource> ,
110
126
scripts : Map < string , ScriptResource> ,
111
127
head : Map < string , HeadResource> ,
128
+ lastStructuredMeta : Map < string , MetaResource> ,
112
129
} ;
113
130
114
131
// Brief on purpose due to insertion by script when streaming late boundaries
@@ -409,6 +426,84 @@ export function getResource(
409
426
) ;
410
427
}
411
428
switch ( type ) {
429
+ case 'meta' : {
430
+ let matcher, propertyString, parentResource ;
431
+ const {
432
+ charSet,
433
+ content,
434
+ httpEquiv,
435
+ name,
436
+ itemProp,
437
+ property,
438
+ } = pendingProps ;
439
+ const headRoot : Document = getDocumentFromRoot ( resourceRoot ) ;
440
+ const { head : headResources , lastStructuredMeta} = getResourcesFromRoot (
441
+ headRoot ,
442
+ ) ;
443
+ if ( typeof charSet === 'string' ) {
444
+ matcher = 'meta[charset]' ;
445
+ } else if ( typeof content === 'string' ) {
446
+ if ( typeof httpEquiv === 'string' ) {
447
+ matcher = `meta[http-equiv="${ escapeSelectorAttributeValueInsideDoubleQuotes (
448
+ httpEquiv ,
449
+ ) } "][content="${ escapeSelectorAttributeValueInsideDoubleQuotes (
450
+ content ,
451
+ ) } "]`;
452
+ } else if ( typeof property === 'string' ) {
453
+ propertyString = property ;
454
+ matcher = `meta[property="${ escapeSelectorAttributeValueInsideDoubleQuotes (
455
+ property ,
456
+ ) } "][content="${ escapeSelectorAttributeValueInsideDoubleQuotes (
457
+ content ,
458
+ ) } "]`;
459
+
460
+ const parentPropertyPath = property
461
+ . split ( ':' )
462
+ . slice ( 0 , - 1 )
463
+ . join ( ':' ) ;
464
+ parentResource = lastStructuredMeta . get ( parentPropertyPath ) ;
465
+ if ( parentResource ) {
466
+ // When using parentResource the matcher is not functional for locating
467
+ // the instance in the DOM but it still serves as a unique key.
468
+ matcher = parentResource . matcher + matcher ;
469
+ }
470
+ } else if ( typeof name === 'string' ) {
471
+ matcher = `meta[name="${ escapeSelectorAttributeValueInsideDoubleQuotes (
472
+ name ,
473
+ ) } "][content="${ escapeSelectorAttributeValueInsideDoubleQuotes (
474
+ content ,
475
+ ) } "]`;
476
+ } else if ( typeof itemProp === 'string' ) {
477
+ matcher = `meta[itemprop="${ escapeSelectorAttributeValueInsideDoubleQuotes (
478
+ itemProp ,
479
+ ) } "][content="${ escapeSelectorAttributeValueInsideDoubleQuotes (
480
+ content ,
481
+ ) } "]`;
482
+ }
483
+ }
484
+ if ( matcher ) {
485
+ let resource = headResources . get ( matcher ) ;
486
+ if ( ! resource ) {
487
+ resource = {
488
+ type : 'meta' ,
489
+ matcher,
490
+ property : propertyString ,
491
+ parentResource,
492
+ props : Object . assign ( { } , pendingProps ) ,
493
+ count : 0 ,
494
+ instance : null ,
495
+ root : headRoot ,
496
+ } ;
497
+ headResources . set ( matcher , resource ) ;
498
+ }
499
+ if ( typeof resource . property === 'string' ) {
500
+ // We cast because flow doesn't know that this resource must be a Meta resource
501
+ lastStructuredMeta. set ( resource . property , ( resource : any ) ) ;
502
+ }
503
+ return resource ;
504
+ }
505
+ return null ;
506
+ }
412
507
case 'title ': {
413
508
let child = pendingProps . children ;
414
509
if ( Array . isArray ( child ) && child . length === 1 ) {
@@ -421,13 +516,14 @@ export function getResource(
421
516
let resource = headResources . get ( key ) ;
422
517
if ( ! resource ) {
423
518
const titleProps = titlePropsFromRawProps ( child , pendingProps ) ;
424
- resource = createHeadResource (
425
- headResources ,
426
- headRoot ,
427
- 'title' ,
428
- key ,
429
- titleProps ,
430
- ) ;
519
+ resource = {
520
+ type : 'title ',
521
+ props : titleProps ,
522
+ count : 0 ,
523
+ instance : null ,
524
+ root : headRoot ,
525
+ } ;
526
+ headResources . set ( key , resource) ;
431
527
}
432
528
return resource ;
433
529
}
@@ -588,8 +684,8 @@ function preloadPropsFromRawProps(
588
684
function titlePropsFromRawProps (
589
685
child : string | number ,
590
686
rawProps : Props ,
591
- ) : HeadProps {
592
- const props : HeadProps = Object . assign ( { } , rawProps ) ;
687
+ ) : TitleProps {
688
+ const props : TitleProps = Object . assign ( { } , rawProps ) ;
593
689
props . children = child ;
594
690
return props ;
595
691
}
@@ -613,7 +709,8 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
613
709
614
710
export function acquireResource ( resource : Resource ) : Instance {
615
711
switch ( resource . type ) {
616
- case 'head' : {
712
+ case 'title' :
713
+ case 'meta' : {
617
714
return acquireHeadResource ( resource ) ;
618
715
}
619
716
case 'style' : {
@@ -635,7 +732,8 @@ export function acquireResource(resource: Resource): Instance {
635
732
636
733
export function releaseResource ( resource : Resource ) : void {
637
734
switch ( resource . type ) {
638
- case 'head ': {
735
+ case 'title ':
736
+ case 'meta': {
639
737
return releaseHeadResource ( resource ) ;
640
738
}
641
739
case 'style ': {
@@ -668,35 +766,6 @@ function createResourceInstance(
668
766
return element ;
669
767
}
670
768
671
- function createHeadResource (
672
- headResources : Map < string , HeadResource > ,
673
- root : Document ,
674
- instanceType : string ,
675
- key : string ,
676
- props : HeadProps ,
677
- ) : HeadResource {
678
- if ( __DEV__ ) {
679
- if ( headResources . has ( key ) ) {
680
- console . error (
681
- 'createHeadResource was called when a head Resource matching the same key already exists. This is a bug in React.' ,
682
- ) ;
683
- }
684
- }
685
-
686
- const resource : HeadResource = {
687
- type : 'head' ,
688
- instanceType,
689
- props,
690
-
691
- count : 0 ,
692
- instance : null ,
693
- root,
694
- } ;
695
-
696
- headResources . set ( key , resource ) ;
697
- return resource ;
698
- }
699
-
700
769
function createStyleResource (
701
770
styleResources : Map < string , StyleResource > ,
702
771
root : FloatRoot ,
@@ -894,7 +963,7 @@ function createPreloadResource(
894
963
) ;
895
964
if ( ! element ) {
896
965
element = createResourceInstance ( 'link' , props , ownerDocument ) ;
897
- appendResourceInstance ( element , ownerDocument ) ;
966
+ insertResourceInstanceBefore ( ownerDocument , element , null ) ;
898
967
} else {
899
968
markNodeAsResource ( element ) ;
900
969
}
@@ -911,8 +980,8 @@ function acquireHeadResource(resource: HeadResource): Instance {
911
980
resource . count ++ ;
912
981
let instance = resource . instance ;
913
982
if ( ! instance ) {
914
- const { props, root, instanceType } = resource ;
915
- switch ( instanceType ) {
983
+ const { props, root, type } = resource ;
984
+ switch ( type ) {
916
985
case 'title' : {
917
986
const titles = root . querySelectorAll ( 'title' ) ;
918
987
for ( let i = 0 ; i < titles . length ; i ++ ) {
@@ -922,18 +991,70 @@ function acquireHeadResource(resource: HeadResource): Instance {
922
991
return instance ;
923
992
}
924
993
}
994
+ instance = resource . instance = createResourceInstance (
995
+ type ,
996
+ props ,
997
+ root ,
998
+ ) ;
999
+ insertResourceInstanceBefore ( root , instance , titles . item ( 0 ) ) ;
1000
+ break ;
1001
+ }
1002
+ case 'meta' : {
1003
+ let insertBefore = null ;
1004
+
1005
+ const metaResource : MetaResource = ( resource : any ) ;
1006
+ const { matcher, property, parentResource} = metaResource ;
1007
+
1008
+ if ( parentResource && typeof property === 'string' ) {
1009
+ // This resoruce is a structured meta type with a parent.
1010
+ // Instead of using the matcher we just traverse forward
1011
+ // siblings of the parent instance until we find a match
1012
+ // or exhaust.
1013
+ const parent = parentResource . instance ;
1014
+ if ( parent ) {
1015
+ let node = null ;
1016
+ let nextNode = ( insertBefore = parent . nextSibling ) ;
1017
+ while ( ( node = nextNode ) ) {
1018
+ nextNode = node . nextSibling ;
1019
+ if ( node . nodeName === 'META' ) {
1020
+ const meta : Element = ( node : any ) ;
1021
+ const propertyAttr = meta . getAttribute ( 'property' ) ;
1022
+ if ( typeof propertyAttr !== 'string' ) {
1023
+ continue ;
1024
+ } else if (
1025
+ propertyAttr === property &&
1026
+ meta . getAttribute ( 'content' ) === props . content
1027
+ ) {
1028
+ resource . instance = meta ;
1029
+ markNodeAsResource ( meta ) ;
1030
+ return meta ;
1031
+ } else if ( property . startsWith ( propertyAttr + ':' ) ) {
1032
+ // This meta starts a new instance of a parent structure for this meta type
1033
+ // We need to halt our search here because even if we find a later match it
1034
+ // is for a different parent element
1035
+ break ;
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ } else if ( ( instance = root . querySelector ( matcher ) ) ) {
1041
+ resource . instance = instance ;
1042
+ markNodeAsResource ( instance ) ;
1043
+ return instance ;
1044
+ }
1045
+ instance = resource . instance = createResourceInstance (
1046
+ type ,
1047
+ props ,
1048
+ root ,
1049
+ ) ;
1050
+ insertResourceInstanceBefore ( root , instance , insertBefore ) ;
1051
+ break ;
1052
+ }
1053
+ default : {
1054
+ throw new Error (
1055
+ `acquireHeadResource encountered a resource type it did not expect: "${ type } ". This is a bug in React.` ,
1056
+ ) ;
925
1057
}
926
- }
927
- instance = resource . instance = createResourceInstance (
928
- instanceType ,
929
- props ,
930
- root ,
931
- ) ;
932
-
933
- if ( instanceType === 'title' ) {
934
- prependResourceInstance ( instance , root ) ;
935
- } else {
936
- appendResourceInstance ( instance , root ) ;
937
1058
}
938
1059
}
939
1060
return instance ;
@@ -1010,7 +1131,7 @@ function acquireScriptResource(resource: ScriptResource): Instance {
1010
1131
getDocumentFromRoot ( root ) ,
1011
1132
) ;
1012
1133
1013
- appendResourceInstance ( instance , getDocumentFromRoot ( root ) ) ;
1134
+ insertResourceInstanceBefore ( getDocumentFromRoot ( root ) , instance , null ) ;
1014
1135
}
1015
1136
}
1016
1137
return instance ;
@@ -1113,45 +1234,22 @@ function insertStyleInstance(
1113
1234
}
1114
1235
}
1115
1236
1116
- function prependResourceInstance (
1117
- instance : Instance ,
1237
+ function insertResourceInstanceBefore (
1118
1238
ownerDocument : Document ,
1119
- ) : void {
1120
- if ( __DEV__ ) {
1121
- if ( instance . tagName === 'LINK' && ( instance : any ) . rel === 'stylesheet' ) {
1122
- console . error (
1123
- 'prependResourceInstance was called with a stylesheet. Stylesheets must be' +
1124
- ' inserted with insertStyleInstance instead. This is a bug in React.' ,
1125
- ) ;
1126
- }
1127
- }
1128
-
1129
- const parent = ownerDocument . head ;
1130
- if ( parent ) {
1131
- parent . insertBefore ( instance , parent . firstChild ) ;
1132
- } else {
1133
- throw new Error (
1134
- 'While attempting to insert a Resource, React expected the Document to contain' +
1135
- ' a head element but it was not found.' ,
1136
- ) ;
1137
- }
1138
- }
1139
-
1140
- function appendResourceInstance (
1141
1239
instance : Instance ,
1142
- ownerDocument : Document ,
1240
+ before : ? Node ,
1143
1241
) : void {
1144
1242
if ( __DEV__ ) {
1145
1243
if ( instance . tagName === 'LINK' && ( instance : any ) . rel === 'stylesheet' ) {
1146
1244
console . error (
1147
- 'appendResourceInstance was called with a stylesheet. Stylesheets must be' +
1245
+ 'insertResourceInstanceBefore was called with a stylesheet. Stylesheets must be' +
1148
1246
' inserted with insertStyleInstance instead. This is a bug in React.' ,
1149
1247
) ;
1150
1248
}
1151
1249
}
1152
- const parent = ownerDocument . head ;
1250
+ const parent = ( before && before . parentNode ) || ownerDocument . head ;
1153
1251
if ( parent ) {
1154
- parent . appendChild ( instance ) ;
1252
+ parent . insertBefore ( instance , before ) ;
1155
1253
} else {
1156
1254
throw new Error (
1157
1255
'While attempting to insert a Resource, React expected the Document to contain' +
@@ -1162,6 +1260,7 @@ function appendResourceInstance(
1162
1260
1163
1261
export function isHostResourceType ( type : string , props : Props ) : boolean {
1164
1262
switch ( type ) {
1263
+ case 'meta':
1165
1264
case 'title ': {
1166
1265
return true ;
1167
1266
}
0 commit comments