1
- import type { TransformValueOptions } from 'packrup'
2
- import type { BaseMeta , Head , MetaFlatInput } from 'unhead/types'
3
- import { packArray , unpackToArray , unpackToString } from 'packrup'
1
+ import type { BaseMeta , Head , MetaFlatInput } from '../types'
2
+
3
+ export interface TransformValueOptions {
4
+ entrySeparator ?: string
5
+ keyValueSeparator ?: string
6
+ resolve ?: ( ctx : { key : string , value : unknown } ) => string | void
7
+ }
8
+
9
+ function unpackToString < T extends Record < keyof T , unknown > > ( value : T , options : TransformValueOptions ) : string {
10
+ return Object . entries ( value )
11
+ . map ( ( [ key , value ] ) => {
12
+ if ( typeof value === 'object' )
13
+ value = unpackToString ( value as Record < keyof T , any > , options )
14
+ if ( options . resolve ) {
15
+ const resolved = options . resolve ( { key, value } )
16
+ if ( typeof resolved !== 'undefined' )
17
+ return resolved
18
+ }
19
+ return `${ fixKeyCase ( key ) } ${ options . keyValueSeparator || '' } ${ String ( value ) } `
20
+ } )
21
+ . filter ( Boolean )
22
+ . join ( options . entrySeparator || '' )
23
+ }
24
+
25
+ interface Context { key : string , value : any }
26
+ type ResolveFn = ( ctx : Context ) => string
27
+
28
+ export interface UnpackArrayOptions {
29
+ key : string | ResolveFn
30
+ value : string | ResolveFn
31
+ resolveKeyData ?: ResolveFn
32
+ resolveValueData ?: ResolveFn
33
+ }
34
+
35
+ export function unpackToArray ( input : Record < string , any > , options : UnpackArrayOptions ) : Record < string , any > [ ] {
36
+ const unpacked : any [ ] = [ ]
37
+ const kFn = options . resolveKeyData || ( ( ctx : Context ) => ctx . key )
38
+ const vFn = options . resolveValueData || ( ( ctx : Context ) => ctx . value )
39
+
40
+ for ( const [ k , v ] of Object . entries ( input ) ) {
41
+ unpacked . push ( ...( Array . isArray ( v ) ? v : [ v ] ) . flatMap ( ( i ) => {
42
+ if ( String ( i ) === 'false' ) {
43
+ return false
44
+ }
45
+ const ctx = { key : k , value : i }
46
+ const val = vFn ( ctx )
47
+ // handle nested objects
48
+ if ( typeof val === 'object' )
49
+ return unpackToArray ( val ! , options )
50
+
51
+ if ( Array . isArray ( val ) )
52
+ return val
53
+
54
+ return {
55
+ [ typeof options . key === 'function' ? options . key ( ctx ) : options . key ] : kFn ( ctx ) ,
56
+ [ typeof options . value === 'function' ? options . value ( ctx ) : options . value ] : val ,
57
+ }
58
+ } ) )
59
+ }
60
+ return unpacked . filter ( Boolean )
61
+ }
4
62
5
63
interface PackingDefinition {
6
64
metaKey ?: keyof BaseMeta
@@ -11,52 +69,47 @@ interface PackingDefinition {
11
69
const p = ( p : string ) => ( { keyValue : p , metaKey : 'property' } ) as PackingDefinition
12
70
const k = ( p : string ) => ( { keyValue : p } ) as PackingDefinition
13
71
14
- const MetaPackingSchema : Record < string , PackingDefinition > = {
15
- appleItunesApp : {
72
+ // @ts -expect-error untyped
73
+ export const MetaPackingSchema = new Map < string , PackingDefinition > ( [
74
+ [ 'appleItunesApp' , {
16
75
unpack : {
17
76
entrySeparator : ', ' ,
18
77
resolve ( { key, value } ) {
19
78
return `${ fixKeyCase ( key ) } =${ value } `
20
79
} ,
21
80
} ,
22
- } ,
23
- articleExpirationTime : p ( 'article:expiration_time' ) ,
24
- articleModifiedTime : p ( 'article:modified_time' ) ,
25
- articlePublishedTime : p ( 'article:published_time' ) ,
26
- bookReleaseDate : p ( 'book:release_date' ) ,
27
- charset : {
28
- metaKey : 'charset' ,
29
- } ,
30
- contentSecurityPolicy : {
81
+ } ] ,
82
+ [ 'articleExpirationTime' , p ( 'article:expiration_time' ) ] ,
83
+ [ 'articleModifiedTime' , p ( 'article:modified_time' ) ] ,
84
+ [ 'articlePublishedTime' , p ( 'article:published_time' ) ] ,
85
+ [ 'bookReleaseDate' , p ( 'book:release_date' ) ] ,
86
+ [ 'charset' , { metaKey : 'charset' } ] ,
87
+ [ 'contentSecurityPolicy' , {
31
88
unpack : {
32
89
entrySeparator : '; ' ,
33
90
resolve ( { key, value } ) {
34
91
return `${ fixKeyCase ( key ) } ${ value } `
35
92
} ,
36
93
} ,
37
94
metaKey : 'http-equiv' ,
38
- } ,
39
- contentType : {
40
- metaKey : 'http-equiv' ,
41
- } ,
42
- defaultStyle : {
43
- metaKey : 'http-equiv' ,
44
- } ,
45
- fbAppId : p ( 'fb:app_id' ) ,
46
- msapplicationConfig : k ( 'msapplication-Config' ) ,
47
- msapplicationTileColor : k ( 'msapplication-TileColor' ) ,
48
- msapplicationTileImage : k ( 'msapplication-TileImage' ) ,
49
- ogAudioSecureUrl : p ( 'og:audio:secure_url' ) ,
50
- ogAudioUrl : p ( 'og:audio' ) ,
51
- ogImageSecureUrl : p ( 'og:image:secure_url' ) ,
52
- ogImageUrl : p ( 'og:image' ) ,
53
- ogSiteName : p ( 'og:site_name' ) ,
54
- ogVideoSecureUrl : p ( 'og:video:secure_url' ) ,
55
- ogVideoUrl : p ( 'og:video' ) ,
56
- profileFirstName : p ( 'profile:first_name' ) ,
57
- profileLastName : p ( 'profile:last_name' ) ,
58
- profileUsername : p ( 'profile:username' ) ,
59
- refresh : {
95
+ } ] ,
96
+ [ 'contentType' , { metaKey : 'http-equiv' } ] ,
97
+ [ 'defaultStyle' , { metaKey : 'http-equiv' } ] ,
98
+ [ 'fbAppId' , p ( 'fb:app_id' ) ] ,
99
+ [ 'msapplicationConfig' , k ( 'msapplication-Config' ) ] ,
100
+ [ 'msapplicationTileColor' , k ( 'msapplication-TileColor' ) ] ,
101
+ [ 'msapplicationTileImage' , k ( 'msapplication-TileImage' ) ] ,
102
+ [ 'ogAudioSecureUrl' , p ( 'og:audio:secure_url' ) ] ,
103
+ [ 'ogAudioUrl' , p ( 'og:audio' ) ] ,
104
+ [ 'ogImageSecureUrl' , p ( 'og:image:secure_url' ) ] ,
105
+ [ 'ogImageUrl' , p ( 'og:image' ) ] ,
106
+ [ 'ogSiteName' , p ( 'og:site_name' ) ] ,
107
+ [ 'ogVideoSecureUrl' , p ( 'og:video:secure_url' ) ] ,
108
+ [ 'ogVideoUrl' , p ( 'og:video' ) ] ,
109
+ [ 'profileFirstName' , p ( 'profile:first_name' ) ] ,
110
+ [ 'profileLastName' , p ( 'profile:last_name' ) ] ,
111
+ [ 'profileUsername' , p ( 'profile:username' ) ] ,
112
+ [ 'refresh' , {
60
113
metaKey : 'http-equiv' ,
61
114
unpack : {
62
115
entrySeparator : ';' ,
@@ -65,22 +118,22 @@ const MetaPackingSchema: Record<string, PackingDefinition> = {
65
118
return `${ value } `
66
119
} ,
67
120
} ,
68
- } ,
69
- robots : {
121
+ } ] ,
122
+ [ ' robots' , {
70
123
unpack : {
71
124
entrySeparator : ', ' ,
72
125
resolve ( { key, value } ) {
126
+ if ( ! value ) {
127
+ return false
128
+ }
73
129
if ( typeof value === 'boolean' )
74
130
return `${ fixKeyCase ( key ) } `
75
- else
76
- return `${ fixKeyCase ( key ) } :${ value } `
131
+ return `${ fixKeyCase ( key ) } :${ value } `
77
132
} ,
78
133
} ,
79
- } ,
80
- xUaCompatible : {
81
- metaKey : 'http-equiv' ,
82
- } ,
83
- } as const
134
+ } ] ,
135
+ [ 'xUaCompatible' , { metaKey : 'http-equiv' } ] ,
136
+ ] )
84
137
85
138
const openGraphNamespaces = new Set ( [
86
139
'og' ,
@@ -94,49 +147,32 @@ export function resolveMetaKeyType(key: string): keyof BaseMeta {
94
147
const prefixIndex = fKey . indexOf ( ':' )
95
148
if ( openGraphNamespaces . has ( fKey . substring ( 0 , prefixIndex ) ) )
96
149
return 'property'
97
- return MetaPackingSchema [ key ] ?. metaKey || 'name'
150
+ return MetaPackingSchema . get ( key ) ?. metaKey || 'name'
98
151
}
99
152
100
153
export function resolveMetaKeyValue ( key : string ) : string {
101
- return MetaPackingSchema [ key ] ?. keyValue || fixKeyCase ( key )
154
+ return MetaPackingSchema . get ( key ) ?. keyValue || fixKeyCase ( key )
102
155
}
103
156
157
+ const UPPERCASE_PATTERN = / ( [ A - Z ] ) / g
158
+
104
159
function fixKeyCase ( key : string ) {
105
- const updated = key . replace ( / ( [ A - Z ] ) / g , '-$1' ) . toLowerCase ( )
160
+ const updated = key . replace ( UPPERCASE_PATTERN , '-$1' ) . toLowerCase ( )
106
161
const prefixIndex = updated . indexOf ( '-' )
107
162
const fKey = updated . substring ( 0 , prefixIndex )
108
163
if ( fKey === 'twitter' || openGraphNamespaces . has ( fKey ) )
109
- return key . replace ( / ( [ A - Z ] ) / g , ':$1' ) . toLowerCase ( )
164
+ return key . replace ( UPPERCASE_PATTERN , ':$1' ) . toLowerCase ( )
110
165
return updated
111
166
}
112
167
113
- function changeKeyCasingDeep ( input : any ) : any {
114
- if ( Array . isArray ( input ) ) {
115
- return input . map ( entry => changeKeyCasingDeep ( entry ) )
116
- }
117
- if ( typeof input !== 'object' || Array . isArray ( input ) )
118
- return input
119
-
120
- const output : Record < string , any > = { }
121
- for ( const key in input ) {
122
- if ( ! Object . prototype . hasOwnProperty . call ( input , key ) ) {
123
- continue
124
- }
125
- output [ fixKeyCase ( key ) ] = changeKeyCasingDeep ( input [ key ] )
126
- }
127
-
128
- return output
129
- }
130
-
131
168
export function resolvePackedMetaObjectValue ( value : string , key : string ) : string {
132
- const definition = MetaPackingSchema [ key ]
133
-
169
+ const definition = MetaPackingSchema . get ( key )
134
170
// refresh is weird...
135
171
if ( key === 'refresh' )
136
172
// @ts -expect-error untyped
137
173
return `${ value . seconds } ;url=${ value . url } `
138
174
return unpackToString (
139
- changeKeyCasingDeep ( value ) ,
175
+ value ,
140
176
{
141
177
keyValueSeparator : '=' ,
142
178
entrySeparator : ', ' ,
@@ -153,41 +189,29 @@ export function resolvePackedMetaObjectValue(value: string, key: string): string
153
189
154
190
const ObjectArrayEntries = new Set ( [ 'og:image' , 'og:video' , 'og:audio' , 'twitter:image' ] )
155
191
156
- function sanitize ( input : Record < string , any > ) {
157
- const out : Record < string , any > = { }
158
- for ( const k in input ) {
159
- if ( ! Object . prototype . hasOwnProperty . call ( input , k ) ) {
160
- continue
161
- }
162
- const v = input [ k ]
163
- if ( String ( v ) !== 'false' && k )
164
- out [ k ] = v
165
- }
166
- return out
167
- }
168
-
169
192
function handleObjectEntry ( key : string , v : Record < string , any > ) {
170
193
// filter out falsy values
171
- const value : Record < string , any > = sanitize ( v )
172
194
const fKey = fixKeyCase ( key )
173
195
const attr = resolveMetaKeyType ( fKey )
174
196
if ( ObjectArrayEntries . has ( fKey as keyof MetaFlatInput ) ) {
175
197
const input : MetaFlatInput = { }
176
- for ( const k in value ) {
177
- if ( ! Object . prototype . hasOwnProperty . call ( value , k ) ) {
198
+ for ( const k in v ) {
199
+ if ( ! Object . prototype . hasOwnProperty . call ( v , k ) ) {
178
200
continue
179
201
}
180
202
181
203
// we need to prefix the keys with og:
182
- // @ts -expect-error untyped
183
- input [ `${ key } ${ k === 'url' ? '' : `${ k [ 0 ] . toUpperCase ( ) } ${ k . slice ( 1 ) } ` } ` ] = value [ k ]
204
+ if ( String ( v [ k ] ) !== 'false' ) {
205
+ // @ts -expect-error untyped
206
+ input [ `${ key } ${ k === 'url' ? '' : `${ k [ 0 ] . toUpperCase ( ) } ${ k . slice ( 1 ) } ` } ` ] = v [ k ]
207
+ }
184
208
}
185
209
return unpackMeta ( input )
186
210
// sort by property name
187
211
// @ts -expect-error untyped
188
212
. sort ( ( a , b ) => ( a [ attr ] ?. length || 0 ) - ( b [ attr ] ?. length || 0 ) ) as BaseMeta [ ]
189
213
}
190
- return [ { [ attr ] : fKey , ...value } ] as BaseMeta [ ]
214
+ return [ { [ attr ] : fKey , ...v } ] as BaseMeta [ ]
191
215
}
192
216
193
217
/**
@@ -211,11 +235,8 @@ export function unpackMeta<T extends MetaFlatInput>(input: T): Required<Head>['m
211
235
extras . push ( ...handleObjectEntry ( key , value ) )
212
236
continue
213
237
}
214
- primitives [ key ] = sanitize ( value )
215
- }
216
- else {
217
- primitives [ key ] = value
218
238
}
239
+ primitives [ key ] = value
219
240
continue
220
241
}
221
242
for ( const v of value ) {
@@ -250,27 +271,3 @@ export function unpackMeta<T extends MetaFlatInput>(input: T): Required<Head>['m
250
271
return m
251
272
} ) as unknown as Required < Head > [ 'meta' ]
252
273
}
253
-
254
- /**
255
- * Convert an array of meta entries to a flat object.
256
- * @param inputs
257
- */
258
- export function packMeta < T extends Required < Head > [ 'meta' ] > ( inputs : T ) : MetaFlatInput {
259
- const mappedPackingSchema = Object . entries ( MetaPackingSchema )
260
- . map ( ( [ key , value ] ) => [ key , value . keyValue ] )
261
-
262
- return packArray ( inputs , {
263
- key : [ 'name' , 'property' , 'httpEquiv' , 'http-equiv' , 'charset' ] ,
264
- value : [ 'content' , 'charset' ] ,
265
- resolveKey ( k ) {
266
- let key = ( mappedPackingSchema . filter ( sk => sk [ 1 ] === k ) ?. [ 0 ] ?. [ 0 ] || k ) as string
267
- // turn : into a capital letter
268
- // @ts -expect-error untyped
269
- const replacer = ( _ , letter ) => letter ?. toUpperCase ( )
270
- key = key
271
- . replace ( / : ( [ a - z ] ) / g, replacer )
272
- . replace ( / - ( [ a - z ] ) / g, replacer )
273
- return key as string
274
- } ,
275
- } )
276
- }
0 commit comments