@@ -107,7 +107,10 @@ type MetaProps = {
107
107
} ;
108
108
export type MetaResource = {
109
109
type : 'meta' ,
110
- matcher : string ,
110
+ key : string ,
111
+ matchValue : string ,
112
+ matchAttr : 'charset' | 'http-equiv' | 'name' | 'itemprop' | 'property' ,
113
+ parentResource : ?MetaResource ,
111
114
props : MetaProps ,
112
115
113
116
count : number ,
@@ -123,6 +126,7 @@ export type RootResources = {
123
126
styles : Map < string , StyleResource> ,
124
127
scripts : Map < string , ScriptResource> ,
125
128
head : Map < string , HeadResource> ,
129
+ lastStructuredMeta : Map < string , MetaResource> ,
126
130
} ;
127
131
128
132
// Brief on purpose due to insertion by script when streaming late boundaries
@@ -424,41 +428,70 @@ export function getResource(
424
428
}
425
429
switch ( type ) {
426
430
case 'meta' : {
427
- let matcher ;
428
- if ( typeof pendingProps . charSet === 'string' ) {
429
- // We don't support charSet metas on the client since they are for describing the encoding
430
- // of the streamed in html and don't make sense to set from the client
431
- } else if ( typeof pendingProps . httpEquiv === 'string' ) {
432
- matcher = `[http-equiv="${ escapeSelectorAttributeValueInsideDoubleQuotes (
433
- pendingProps . httpEquiv ,
434
- ) } "]`;
435
- } else if ( typeof pendingProps . name === 'string' ) {
436
- matcher = `[name="${ escapeSelectorAttributeValueInsideDoubleQuotes (
437
- pendingProps . name ,
438
- ) } "]`;
439
- } else if ( typeof pendingProps . itemProp === 'string' ) {
440
- matcher = `[itemprop="${ escapeSelectorAttributeValueInsideDoubleQuotes (
441
- pendingProps . itemProp ,
442
- ) } "]`;
443
- } else if ( typeof pendingProps . property === 'string' ) {
444
- matcher = `[property="${ escapeSelectorAttributeValueInsideDoubleQuotes (
445
- pendingProps . property ,
446
- ) } "]`;
431
+ let key , matchAttr, matchValue, propertyPath, parentResource ;
432
+ const {
433
+ charSet,
434
+ content,
435
+ httpEquiv,
436
+ name,
437
+ itemProp,
438
+ property,
439
+ } = pendingProps ;
440
+ const headRoot : Document = getDocumentFromRoot ( resourceRoot ) ;
441
+ const { head : headResources , lastStructuredMeta} = getResourcesFromRoot (
442
+ headRoot ,
443
+ ) ;
444
+ if ( typeof charSet === 'string' ) {
445
+ matchValue = charSet ;
446
+ key = matchAttr = 'charset' ;
447
+ } else if ( typeof content === 'string' ) {
448
+ const contentKey = '::' + content ;
449
+ if ( typeof httpEquiv === 'string' ) {
450
+ matchValue = httpEquiv ;
451
+ matchAttr = 'http-equiv' ;
452
+ key = matchAttr + '::' + httpEquiv + contentKey ;
453
+ } else if ( typeof property === 'string' ) {
454
+ matchValue = property ;
455
+ matchAttr = 'property' ;
456
+ key = matchAttr + '::' + property + contentKey ;
457
+ propertyPath = property ;
458
+ const parentPropertyPath = property
459
+ . split ( ':' )
460
+ . slice ( 0 , - 1 )
461
+ . join ( ':' ) ;
462
+ parentResource = lastStructuredMeta . get ( parentPropertyPath ) ;
463
+ if ( parentResource ) {
464
+ key = parentResource . key + '::child::' + key ;
465
+ }
466
+ } else if ( typeof name === 'string' ) {
467
+ matchValue = name ;
468
+ matchAttr = 'name' ;
469
+ key = matchAttr + '::' + name + contentKey ;
470
+ } else if ( typeof itemProp === 'string' ) {
471
+ matchValue = itemProp ;
472
+ matchAttr = 'itemprop' ;
473
+ key = matchAttr + '::' + itemProp + contentKey ;
474
+ }
447
475
}
448
- if ( matcher ) {
449
- const headRoot : Document = getDocumentFromRoot ( resourceRoot ) ;
450
- const headResources = getResourcesFromRoot ( headRoot ) . head ;
451
- let resource = headResources . get ( matcher ) ;
476
+ if ( key && matchAttr ) {
477
+ let resource = headResources . get ( key ) ;
452
478
if ( ! resource ) {
453
479
resource = {
454
480
type : 'meta' ,
455
- matcher,
481
+ matchValue : ( ( matchValue : any ) : string ) ,
482
+ matchAttr,
483
+ key,
484
+ parentResource,
456
485
props : Object . assign ( { } , pendingProps ) ,
457
486
count : 0 ,
458
487
instance : null ,
459
488
root : headRoot ,
460
489
} ;
461
- headResources . set ( matcher , resource ) ;
490
+ headResources . set ( key , resource ) ;
491
+ }
492
+ if ( propertyPath ) {
493
+ // We cast because flow doesn't know that this resource must be a Meta resource
494
+ lastStructuredMeta. set ( propertyPath , ( resource : any ) ) ;
462
495
}
463
496
return resource ;
464
497
}
@@ -960,22 +993,64 @@ function acquireHeadResource(resource: HeadResource): Instance {
960
993
break ;
961
994
}
962
995
case 'meta' : {
963
- instance = resource . instance = createResourceInstance (
964
- type ,
965
- props ,
966
- root ,
967
- ) ;
996
+ let existingEl = null ;
997
+ let insertBefore = null ;
998
+
968
999
const metaResource : MetaResource = ( resource : any ) ;
969
- const metas = root . querySelectorAll ( 'meta' + metaResource . matcher ) ;
970
- // For keymatched metas we simply remove them all because this new meta
971
- // will supercede it. It's possibly, likely even that they may match props
972
- // and another approach is to check if the props are equal and just use the node
973
- // however for simplicity we will purge any server rendered metas before
974
- // constructing a new one here on the client
975
- insertResourceInstanceBefore ( root , instance , metas . item ( 0 ) ) ;
976
- for ( let i = 0 ; i < metas . length ; i ++ ) {
977
- const meta = metas [ i ] ;
978
- ( meta . parentNode : any ) . removeChild ( meta ) ;
1000
+ const { matchAttr, matchValue, parentResource} = metaResource ;
1001
+
1002
+ if ( matchAttr === 'charset' ) {
1003
+ existingEl = root . querySelector ( 'meta[charset]' ) ;
1004
+ } else {
1005
+ let scope : Document | Element = root ;
1006
+ let parent = null ;
1007
+ let parentProperty = null ;
1008
+ if ( matchAttr === 'property' && parentResource ) {
1009
+ const parentInstance = parentResource . instance ;
1010
+ if ( parentInstance && parentInstance . parentElement ) {
1011
+ parent = parentInstance ;
1012
+ insertBefore = parent . nextSibling ;
1013
+ parentProperty = parentResource . matchValue ;
1014
+ scope = parentInstance . parentElement ;
1015
+ }
1016
+ }
1017
+ const metas = scope . querySelectorAll ( 'meta' ) ;
1018
+ for ( let i = 0 ; i < metas . length ; i ++ ) {
1019
+ const meta = metas [ i ] ;
1020
+ if ( parent ) {
1021
+ if ( meta === parent ) {
1022
+ parent = null ;
1023
+ }
1024
+ continue ;
1025
+ } else if (
1026
+ meta . getAttribute ( 'content' ) === props . content &&
1027
+ meta . getAttribute ( matchAttr ) === matchValue
1028
+ ) {
1029
+ existingEl = meta ;
1030
+ break ;
1031
+ } else {
1032
+ const metaProperty = meta . getAttribute ( 'property' ) ;
1033
+ if (
1034
+ parentProperty &&
1035
+ metaProperty &&
1036
+ parentProperty . startsWith ( metaProperty )
1037
+ ) {
1038
+ // We have found aother matching parent meta and need to stop
1039
+ break ;
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ if ( existingEl ) {
1045
+ instance = resource . instance = existingEl ;
1046
+ markNodeAsResource ( instance ) ;
1047
+ } else {
1048
+ instance = resource . instance = createResourceInstance (
1049
+ type ,
1050
+ props ,
1051
+ root ,
1052
+ ) ;
1053
+ insertResourceInstanceBefore ( root , instance , insertBefore ) ;
979
1054
}
980
1055
break ;
981
1056
}
@@ -1166,7 +1241,7 @@ function insertStyleInstance(
1166
1241
function insertResourceInstanceBefore (
1167
1242
ownerDocument : Document ,
1168
1243
instance : Instance ,
1169
- before : null | Node ,
1244
+ before : ? Node ,
1170
1245
) : void {
1171
1246
if ( __DEV__ ) {
1172
1247
if ( instance . tagName === 'LINK' && ( instance : any ) . rel === 'stylesheet' ) {
0 commit comments