@unhead/shared 1.2.2 → 1.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +568 -14
- package/dist/index.d.ts +44 -4
- package/dist/index.mjs +550 -12
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
function asArray(value) {
|
|
3
|
+
function asArray$1(value) {
|
|
4
4
|
return Array.isArray(value) ? value : [value];
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -29,7 +29,16 @@ const ValidHeadTags = [
|
|
|
29
29
|
];
|
|
30
30
|
const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs", "templateParams"];
|
|
31
31
|
const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy", "innerHTML", "textContent"];
|
|
32
|
-
const
|
|
32
|
+
const IsBrowser = typeof window !== "undefined";
|
|
33
|
+
const composableNames = [
|
|
34
|
+
"getActiveHead",
|
|
35
|
+
"useHead",
|
|
36
|
+
"useSeoMeta",
|
|
37
|
+
"useHeadSafe",
|
|
38
|
+
"useServerHead",
|
|
39
|
+
"useServerSeoMeta",
|
|
40
|
+
"useServerHeadSafe"
|
|
41
|
+
];
|
|
33
42
|
|
|
34
43
|
function defineHeadPlugin(plugin) {
|
|
35
44
|
return plugin;
|
|
@@ -42,15 +51,7 @@ function hashCode(s) {
|
|
|
42
51
|
return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
|
|
43
52
|
}
|
|
44
53
|
function hashTag(tag) {
|
|
45
|
-
return hashCode(`${tag.tag}:${tag.textContent || tag.innerHTML || ""}:${Object.entries(tag.props).map(([key, value]) => `${key}:${String(value)}`).join(",")}`);
|
|
46
|
-
}
|
|
47
|
-
function computeHashes(hashes) {
|
|
48
|
-
let h = 9;
|
|
49
|
-
for (const s of hashes) {
|
|
50
|
-
for (let i = 0; i < s.length; )
|
|
51
|
-
h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
|
|
52
|
-
}
|
|
53
|
-
return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
|
|
54
|
+
return tag._h || hashCode(tag._d ? tag._d : `${tag.tag}:${tag.textContent || tag.innerHTML || ""}:${Object.entries(tag.props).map(([key, value]) => `${key}:${String(value)}`).join(",")}`);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
function tagDedupeKey(tag, fn) {
|
|
@@ -83,17 +84,570 @@ function resolveTitleTemplate(template, title) {
|
|
|
83
84
|
return template;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
function asArray(input) {
|
|
88
|
+
return Array.isArray(input) ? input : [input];
|
|
89
|
+
}
|
|
90
|
+
const InternalKeySymbol = "_$key";
|
|
91
|
+
function packObject(input, options) {
|
|
92
|
+
const keys = Object.keys(input);
|
|
93
|
+
let [k, v] = keys;
|
|
94
|
+
options = options || {};
|
|
95
|
+
options.key = options.key || k;
|
|
96
|
+
options.value = options.value || v;
|
|
97
|
+
options.resolveKey = options.resolveKey || ((k2) => k2);
|
|
98
|
+
const resolveKey = (index) => {
|
|
99
|
+
const arr = asArray(options?.[index]);
|
|
100
|
+
return arr.find((k2) => {
|
|
101
|
+
if (typeof k2 === "string" && k2.includes(".")) {
|
|
102
|
+
return k2;
|
|
103
|
+
}
|
|
104
|
+
return k2 && keys.includes(k2);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
const resolveValue = (k2, input2) => {
|
|
108
|
+
if (k2.includes(".")) {
|
|
109
|
+
const paths = k2.split(".");
|
|
110
|
+
let val = input2;
|
|
111
|
+
for (const path of paths)
|
|
112
|
+
val = val[path];
|
|
113
|
+
return val;
|
|
114
|
+
}
|
|
115
|
+
return input2[k2];
|
|
116
|
+
};
|
|
117
|
+
k = resolveKey("key") || k;
|
|
118
|
+
v = resolveKey("value") || v;
|
|
119
|
+
const dedupeKeyPrefix = input.key ? `${InternalKeySymbol}${input.key}-` : "";
|
|
120
|
+
let keyValue = resolveValue(k, input);
|
|
121
|
+
keyValue = options.resolveKey(keyValue);
|
|
122
|
+
return {
|
|
123
|
+
[`${dedupeKeyPrefix}${keyValue}`]: resolveValue(v, input)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function packArray(input, options) {
|
|
128
|
+
const packed = {};
|
|
129
|
+
for (const i of input) {
|
|
130
|
+
const packedObj = packObject(i, options);
|
|
131
|
+
const pKey = Object.keys(packedObj)[0];
|
|
132
|
+
const isDedupeKey = pKey.startsWith(InternalKeySymbol);
|
|
133
|
+
if (!isDedupeKey && packed[pKey]) {
|
|
134
|
+
packed[pKey] = Array.isArray(packed[pKey]) ? packed[pKey] : [packed[pKey]];
|
|
135
|
+
packed[pKey].push(Object.values(packedObj)[0]);
|
|
136
|
+
} else {
|
|
137
|
+
packed[isDedupeKey ? pKey.split("-").slice(1).join("-") || pKey : pKey] = packedObj[pKey];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return packed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function unpackToArray(input, options) {
|
|
144
|
+
const unpacked = [];
|
|
145
|
+
const kFn = options.resolveKeyData || ((ctx) => ctx.key);
|
|
146
|
+
const vFn = options.resolveValueData || ((ctx) => ctx.value);
|
|
147
|
+
for (const [k, v] of Object.entries(input)) {
|
|
148
|
+
unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
|
|
149
|
+
const ctx = { key: k, value: i };
|
|
150
|
+
const val = vFn(ctx);
|
|
151
|
+
if (typeof val === "object")
|
|
152
|
+
return unpackToArray(val, options);
|
|
153
|
+
if (Array.isArray(val))
|
|
154
|
+
return val;
|
|
155
|
+
return {
|
|
156
|
+
[typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
|
|
157
|
+
[typeof options.value === "function" ? options.value(ctx) : options.value]: val
|
|
158
|
+
};
|
|
159
|
+
}).flat());
|
|
160
|
+
}
|
|
161
|
+
return unpacked;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function unpackToString(value, options) {
|
|
165
|
+
return Object.entries(value).map(([key, value2]) => {
|
|
166
|
+
if (typeof value2 === "object")
|
|
167
|
+
value2 = unpackToString(value2, options);
|
|
168
|
+
if (options.resolve) {
|
|
169
|
+
const resolved = options.resolve({ key, value: value2 });
|
|
170
|
+
if (resolved)
|
|
171
|
+
return resolved;
|
|
172
|
+
}
|
|
173
|
+
if (typeof value2 === "number")
|
|
174
|
+
value2 = value2.toString();
|
|
175
|
+
if (typeof value2 === "string" && options.wrapValue) {
|
|
176
|
+
value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
|
|
177
|
+
value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
|
|
178
|
+
}
|
|
179
|
+
return `${key}${options.keyValueSeparator || ""}${value2}`;
|
|
180
|
+
}).join(options.entrySeparator || "");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const MetaPackingSchema = {
|
|
184
|
+
robots: {
|
|
185
|
+
unpack: {
|
|
186
|
+
keyValueSeparator: ":"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
// Pragma directives
|
|
190
|
+
contentSecurityPolicy: {
|
|
191
|
+
unpack: {
|
|
192
|
+
keyValueSeparator: " ",
|
|
193
|
+
entrySeparator: "; "
|
|
194
|
+
},
|
|
195
|
+
metaKey: "http-equiv"
|
|
196
|
+
},
|
|
197
|
+
fbAppId: {
|
|
198
|
+
keyValue: "fb:app_id",
|
|
199
|
+
metaKey: "property"
|
|
200
|
+
},
|
|
201
|
+
ogSiteName: {
|
|
202
|
+
keyValue: "og:site_name"
|
|
203
|
+
},
|
|
204
|
+
msapplicationTileImage: {
|
|
205
|
+
keyValue: "msapplication-TileImage"
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* Tile colour for windows
|
|
209
|
+
*/
|
|
210
|
+
msapplicationTileColor: {
|
|
211
|
+
keyValue: "msapplication-TileColor"
|
|
212
|
+
},
|
|
213
|
+
/**
|
|
214
|
+
* URL of a config for windows tile.
|
|
215
|
+
*/
|
|
216
|
+
msapplicationConfig: {
|
|
217
|
+
keyValue: "msapplication-Config"
|
|
218
|
+
},
|
|
219
|
+
charset: {
|
|
220
|
+
metaKey: "charset"
|
|
221
|
+
},
|
|
222
|
+
contentType: {
|
|
223
|
+
metaKey: "http-equiv"
|
|
224
|
+
},
|
|
225
|
+
defaultStyle: {
|
|
226
|
+
metaKey: "http-equiv"
|
|
227
|
+
},
|
|
228
|
+
xUaCompatible: {
|
|
229
|
+
metaKey: "http-equiv"
|
|
230
|
+
},
|
|
231
|
+
refresh: {
|
|
232
|
+
metaKey: "http-equiv"
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const ColonPrefixKeys = /^(og|twitter|fb)/;
|
|
236
|
+
const PropertyPrefixKeys = /^(og|fb)/;
|
|
237
|
+
function resolveMetaKeyType(key) {
|
|
238
|
+
return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
|
|
239
|
+
}
|
|
240
|
+
function resolveMetaKeyValue(key) {
|
|
241
|
+
return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
|
|
242
|
+
}
|
|
243
|
+
function fixKeyCase(key) {
|
|
244
|
+
key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
245
|
+
if (ColonPrefixKeys.test(key)) {
|
|
246
|
+
key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
|
|
247
|
+
}
|
|
248
|
+
return key;
|
|
249
|
+
}
|
|
250
|
+
function changeKeyCasingDeep(input) {
|
|
251
|
+
if (Array.isArray(input)) {
|
|
252
|
+
return input.map((entry) => changeKeyCasingDeep(entry));
|
|
253
|
+
}
|
|
254
|
+
if (typeof input !== "object" || Array.isArray(input))
|
|
255
|
+
return input;
|
|
256
|
+
const output = {};
|
|
257
|
+
for (const [key, value] of Object.entries(input))
|
|
258
|
+
output[fixKeyCase(key)] = changeKeyCasingDeep(value);
|
|
259
|
+
return output;
|
|
260
|
+
}
|
|
261
|
+
function resolvePackedMetaObjectValue(value, key) {
|
|
262
|
+
const definition = MetaPackingSchema[key];
|
|
263
|
+
if (key === "refresh")
|
|
264
|
+
return `${value.seconds};url=${value.url}`;
|
|
265
|
+
return unpackToString(
|
|
266
|
+
changeKeyCasingDeep(value),
|
|
267
|
+
{
|
|
268
|
+
entrySeparator: ", ",
|
|
269
|
+
keyValueSeparator: "=",
|
|
270
|
+
resolve({ value: value2, key: key2 }) {
|
|
271
|
+
if (value2 === null)
|
|
272
|
+
return "";
|
|
273
|
+
if (typeof value2 === "boolean")
|
|
274
|
+
return `${key2}`;
|
|
275
|
+
},
|
|
276
|
+
...definition?.unpack
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const OpenGraphInputs = ["og:Image", "og:Video", "og:Audio", "twitter:Image"];
|
|
281
|
+
const SimpleArrayUnpackMetas = ["themeColor"];
|
|
282
|
+
function unpackMeta(input) {
|
|
283
|
+
const extras = [];
|
|
284
|
+
OpenGraphInputs.forEach((key) => {
|
|
285
|
+
const propKey = key.toLowerCase();
|
|
286
|
+
const inputKey = `${key.replace(":", "")}`;
|
|
287
|
+
const val = input[inputKey];
|
|
288
|
+
if (typeof val === "object") {
|
|
289
|
+
(Array.isArray(val) ? val : [val]).forEach((entry) => {
|
|
290
|
+
if (!entry)
|
|
291
|
+
return;
|
|
292
|
+
const unpackedEntry = unpackToArray(entry, {
|
|
293
|
+
key: key.startsWith("og") ? "property" : "name",
|
|
294
|
+
value: "content",
|
|
295
|
+
resolveKeyData({ key: key2 }) {
|
|
296
|
+
return fixKeyCase(`${propKey}${key2 !== "url" ? `:${key2}` : ""}`);
|
|
297
|
+
},
|
|
298
|
+
resolveValueData({ value }) {
|
|
299
|
+
return typeof value === "number" ? value.toString() : value;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
extras.push(
|
|
303
|
+
...unpackedEntry.sort((a, b) => a.property === propKey ? -1 : b.property === propKey ? 1 : 0)
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
delete input[inputKey];
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
SimpleArrayUnpackMetas.forEach((meta2) => {
|
|
310
|
+
if (input[meta2] && typeof input[meta2] !== "string") {
|
|
311
|
+
const val = Array.isArray(input[meta2]) ? input[meta2] : [input[meta2]];
|
|
312
|
+
delete input[meta2];
|
|
313
|
+
val.forEach((entry) => {
|
|
314
|
+
extras.push({
|
|
315
|
+
name: fixKeyCase(meta2),
|
|
316
|
+
...entry
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
const meta = unpackToArray(input, {
|
|
322
|
+
key({ key }) {
|
|
323
|
+
return resolveMetaKeyType(key);
|
|
324
|
+
},
|
|
325
|
+
value({ key }) {
|
|
326
|
+
return key === "charset" ? "charset" : "content";
|
|
327
|
+
},
|
|
328
|
+
resolveKeyData({ key }) {
|
|
329
|
+
return resolveMetaKeyValue(key);
|
|
330
|
+
},
|
|
331
|
+
resolveValueData({ value, key }) {
|
|
332
|
+
if (value === null)
|
|
333
|
+
return "_null";
|
|
334
|
+
if (typeof value === "object")
|
|
335
|
+
return resolvePackedMetaObjectValue(value, key);
|
|
336
|
+
return typeof value === "number" ? value.toString() : value;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
|
|
340
|
+
}
|
|
341
|
+
function packMeta(inputs) {
|
|
342
|
+
const mappedPackingSchema = Object.entries(MetaPackingSchema).map(([key, value]) => [key, value.keyValue]);
|
|
343
|
+
return packArray(inputs, {
|
|
344
|
+
key: ["name", "property", "httpEquiv", "http-equiv", "charset"],
|
|
345
|
+
value: ["content", "charset"],
|
|
346
|
+
resolveKey(k) {
|
|
347
|
+
let key = mappedPackingSchema.filter((sk) => sk[1] === k)?.[0]?.[0] || k;
|
|
348
|
+
const replacer = (_, letter) => letter?.toUpperCase();
|
|
349
|
+
key = key.replace(/:([a-z])/g, replacer).replace(/-([a-z])/g, replacer);
|
|
350
|
+
return key;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const WhitelistAttributes = {
|
|
356
|
+
htmlAttrs: ["id", "class", "lang", "dir"],
|
|
357
|
+
bodyAttrs: ["id", "class"],
|
|
358
|
+
meta: ["id", "name", "property", "charset", "content"],
|
|
359
|
+
noscript: ["id", "textContent"],
|
|
360
|
+
script: ["id", "type", "textContent"],
|
|
361
|
+
link: ["id", "color", "crossorigin", "fetchpriority", "href", "hreflang", "imagesrcset", "imagesizes", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"]
|
|
362
|
+
};
|
|
363
|
+
function whitelistSafeInput(input) {
|
|
364
|
+
const filtered = {};
|
|
365
|
+
Object.keys(input).forEach((key) => {
|
|
366
|
+
const tagValue = input[key];
|
|
367
|
+
if (!tagValue)
|
|
368
|
+
return;
|
|
369
|
+
switch (key) {
|
|
370
|
+
case "title":
|
|
371
|
+
case "titleTemplate":
|
|
372
|
+
case "templateParams":
|
|
373
|
+
filtered[key] = tagValue;
|
|
374
|
+
break;
|
|
375
|
+
case "htmlAttrs":
|
|
376
|
+
case "bodyAttrs":
|
|
377
|
+
filtered[key] = {};
|
|
378
|
+
WhitelistAttributes[key].forEach((a) => {
|
|
379
|
+
if (tagValue[a])
|
|
380
|
+
filtered[key][a] = tagValue[a];
|
|
381
|
+
});
|
|
382
|
+
Object.keys(tagValue || {}).filter((a) => a.startsWith("data-")).forEach((a) => {
|
|
383
|
+
filtered[key][a] = tagValue[a];
|
|
384
|
+
});
|
|
385
|
+
break;
|
|
386
|
+
case "meta":
|
|
387
|
+
if (Array.isArray(tagValue)) {
|
|
388
|
+
filtered[key] = tagValue.map((meta) => {
|
|
389
|
+
const safeMeta = {};
|
|
390
|
+
WhitelistAttributes.meta.forEach((key2) => {
|
|
391
|
+
if (meta[key2] || key2.startsWith("data-"))
|
|
392
|
+
safeMeta[key2] = meta[key2];
|
|
393
|
+
});
|
|
394
|
+
return safeMeta;
|
|
395
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
case "link":
|
|
399
|
+
if (Array.isArray(tagValue)) {
|
|
400
|
+
filtered[key] = tagValue.map((meta) => {
|
|
401
|
+
const link = {};
|
|
402
|
+
WhitelistAttributes.link.forEach((key2) => {
|
|
403
|
+
const val = meta[key2];
|
|
404
|
+
if (key2 === "rel" && ["stylesheet", "canonical", "modulepreload", "prerender", "preload", "prefetch"].includes(val))
|
|
405
|
+
return;
|
|
406
|
+
if (key2 === "href") {
|
|
407
|
+
if (val.includes("javascript:") || val.includes("data:"))
|
|
408
|
+
return;
|
|
409
|
+
link[key2] = val;
|
|
410
|
+
} else if (val || key2.startsWith("data-")) {
|
|
411
|
+
link[key2] = val;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
return link;
|
|
415
|
+
}).filter((link) => Object.keys(link).length > 1 && !!link.rel);
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
case "noscript":
|
|
419
|
+
if (Array.isArray(tagValue)) {
|
|
420
|
+
filtered[key] = tagValue.map((meta) => {
|
|
421
|
+
const noscript = {};
|
|
422
|
+
WhitelistAttributes.noscript.forEach((key2) => {
|
|
423
|
+
if (meta[key2] || key2.startsWith("data-"))
|
|
424
|
+
noscript[key2] = meta[key2];
|
|
425
|
+
});
|
|
426
|
+
return noscript;
|
|
427
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
case "script":
|
|
431
|
+
if (Array.isArray(tagValue)) {
|
|
432
|
+
filtered[key] = tagValue.map((script) => {
|
|
433
|
+
const safeScript = {};
|
|
434
|
+
WhitelistAttributes.script.forEach((s) => {
|
|
435
|
+
if (script[s] || s.startsWith("data-")) {
|
|
436
|
+
if (s === "textContent") {
|
|
437
|
+
try {
|
|
438
|
+
const jsonVal = typeof script[s] === "string" ? JSON.parse(script[s]) : script[s];
|
|
439
|
+
safeScript[s] = JSON.stringify(jsonVal, null, 0);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
safeScript[s] = script[s];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return safeScript;
|
|
448
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return filtered;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function normaliseTag(tagName, input, e) {
|
|
457
|
+
const tag = { tag: tagName, props: {} };
|
|
458
|
+
if (input instanceof Promise)
|
|
459
|
+
input = await input;
|
|
460
|
+
if (tagName === "templateParams") {
|
|
461
|
+
tag.props = input;
|
|
462
|
+
return tag;
|
|
463
|
+
}
|
|
464
|
+
if (["title", "titleTemplate"].includes(tagName)) {
|
|
465
|
+
if (input && typeof input === "object") {
|
|
466
|
+
tag.textContent = input.textContent;
|
|
467
|
+
if (input.tagPriority)
|
|
468
|
+
tag.tagPriority = input.tagPriority;
|
|
469
|
+
} else {
|
|
470
|
+
tag.textContent = input;
|
|
471
|
+
}
|
|
472
|
+
return tag;
|
|
473
|
+
}
|
|
474
|
+
if (typeof input === "string") {
|
|
475
|
+
if (!["script", "noscript", "style"].includes(tagName))
|
|
476
|
+
return false;
|
|
477
|
+
if (tagName === "script" && (/^(https?:)?\/\//.test(input) || input.startsWith("/")))
|
|
478
|
+
tag.props.src = input;
|
|
479
|
+
else
|
|
480
|
+
tag.innerHTML = input;
|
|
481
|
+
return tag;
|
|
482
|
+
}
|
|
483
|
+
tag.props = await normaliseProps(tagName, { ...input });
|
|
484
|
+
if (tag.props.children) {
|
|
485
|
+
tag.props.innerHTML = tag.props.children;
|
|
486
|
+
}
|
|
487
|
+
delete tag.props.children;
|
|
488
|
+
Object.keys(tag.props).filter((k) => TagConfigKeys.includes(k)).forEach((k) => {
|
|
489
|
+
if (!["innerHTML", "textContent"].includes(k) || TagsWithInnerContent.includes(tag.tag)) {
|
|
490
|
+
tag[k] = tag.props[k];
|
|
491
|
+
}
|
|
492
|
+
delete tag.props[k];
|
|
493
|
+
});
|
|
494
|
+
TagConfigKeys.forEach((k) => {
|
|
495
|
+
if (!tag[k] && e[k]) {
|
|
496
|
+
tag[k] = e[k];
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
["innerHTML", "textContent"].forEach((k) => {
|
|
500
|
+
if (tag.tag === "script" && typeof tag[k] === "string" && ["application/ld+json", "application/json"].includes(tag.props.type)) {
|
|
501
|
+
try {
|
|
502
|
+
tag[k] = JSON.parse(tag[k]);
|
|
503
|
+
} catch (e2) {
|
|
504
|
+
tag[k] = "";
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (typeof tag[k] === "object")
|
|
508
|
+
tag[k] = JSON.stringify(tag[k]);
|
|
509
|
+
});
|
|
510
|
+
if (tag.props.class)
|
|
511
|
+
tag.props.class = normaliseClassProp(tag.props.class);
|
|
512
|
+
if (tag.props.content && Array.isArray(tag.props.content))
|
|
513
|
+
return tag.props.content.map((v) => ({ ...tag, props: { ...tag.props, content: v } }));
|
|
514
|
+
return tag;
|
|
515
|
+
}
|
|
516
|
+
function normaliseClassProp(v) {
|
|
517
|
+
if (typeof v === "object" && !Array.isArray(v)) {
|
|
518
|
+
v = Object.keys(v).filter((k) => v[k]);
|
|
519
|
+
}
|
|
520
|
+
return (Array.isArray(v) ? v.join(" ") : v).split(" ").filter((c) => c.trim()).filter(Boolean).join(" ");
|
|
521
|
+
}
|
|
522
|
+
async function normaliseProps(tagName, props) {
|
|
523
|
+
for (const k of Object.keys(props)) {
|
|
524
|
+
const isDataKey = k.startsWith("data-");
|
|
525
|
+
if (props[k] instanceof Promise) {
|
|
526
|
+
props[k] = await props[k];
|
|
527
|
+
}
|
|
528
|
+
if (String(props[k]) === "true") {
|
|
529
|
+
props[k] = isDataKey ? "true" : "";
|
|
530
|
+
} else if (String(props[k]) === "false") {
|
|
531
|
+
if (isDataKey) {
|
|
532
|
+
props[k] = "false";
|
|
533
|
+
} else {
|
|
534
|
+
delete props[k];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return props;
|
|
539
|
+
}
|
|
540
|
+
const TagEntityBits = 10;
|
|
541
|
+
async function normaliseEntryTags(e) {
|
|
542
|
+
const tagPromises = [];
|
|
543
|
+
Object.entries(e.resolvedInput).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).forEach(([k, value]) => {
|
|
544
|
+
const v = asArray$1(value);
|
|
545
|
+
tagPromises.push(...v.map((props) => normaliseTag(k, props, e)).flat());
|
|
546
|
+
});
|
|
547
|
+
return (await Promise.all(tagPromises)).flat().filter(Boolean).map((t, i) => {
|
|
548
|
+
t._e = e._i;
|
|
549
|
+
t._p = (e._i << TagEntityBits) + i;
|
|
550
|
+
return t;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const TAG_WEIGHTS = {
|
|
555
|
+
// tags
|
|
556
|
+
base: -1,
|
|
557
|
+
title: 1
|
|
558
|
+
};
|
|
559
|
+
const TAG_ALIASES = {
|
|
560
|
+
// relative scores to their default values
|
|
561
|
+
critical: -8,
|
|
562
|
+
high: -1,
|
|
563
|
+
low: 2
|
|
564
|
+
};
|
|
565
|
+
function tagWeight(tag) {
|
|
566
|
+
let weight = 10;
|
|
567
|
+
const priority = tag.tagPriority;
|
|
568
|
+
if (typeof priority === "number")
|
|
569
|
+
return priority;
|
|
570
|
+
if (tag.tag === "meta") {
|
|
571
|
+
if (tag.props.charset)
|
|
572
|
+
weight = -2;
|
|
573
|
+
if (tag.props["http-equiv"] === "content-security-policy")
|
|
574
|
+
weight = 0;
|
|
575
|
+
} else if (tag.tag === "link" && tag.props.rel === "preconnect") {
|
|
576
|
+
weight = 2;
|
|
577
|
+
} else if (tag.tag in TAG_WEIGHTS) {
|
|
578
|
+
weight = TAG_WEIGHTS[tag.tag];
|
|
579
|
+
}
|
|
580
|
+
if (typeof priority === "string" && priority in TAG_ALIASES) {
|
|
581
|
+
return weight + TAG_ALIASES[priority];
|
|
582
|
+
}
|
|
583
|
+
return weight;
|
|
584
|
+
}
|
|
585
|
+
const SortModifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
|
|
586
|
+
|
|
587
|
+
function processTemplateParams(s, p) {
|
|
588
|
+
if (typeof s !== "string")
|
|
589
|
+
return s;
|
|
590
|
+
function sub(token) {
|
|
591
|
+
if (["s", "pageTitle"].includes(token))
|
|
592
|
+
return p.pageTitle;
|
|
593
|
+
let val;
|
|
594
|
+
if (token.includes(".")) {
|
|
595
|
+
val = token.split(".").reduce((acc, key) => acc ? acc[key] || void 0 : void 0, p);
|
|
596
|
+
} else {
|
|
597
|
+
val = p[token];
|
|
598
|
+
}
|
|
599
|
+
return typeof val !== "undefined" ? val || "" : false;
|
|
600
|
+
}
|
|
601
|
+
let decoded = s;
|
|
602
|
+
try {
|
|
603
|
+
decoded = decodeURI(s);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
const tokens = (decoded.match(/%(\w+\.+\w+)|%(\w+)/g) || []).sort().reverse();
|
|
607
|
+
tokens.forEach((token) => {
|
|
608
|
+
const re = sub(token.slice(1));
|
|
609
|
+
if (typeof re === "string") {
|
|
610
|
+
s = s.replace(new RegExp(`\\${token}(\\W|$)`, "g"), (_, args) => `${re}${args}`).trim();
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
const sep = p.separator;
|
|
614
|
+
if (s.includes(sep)) {
|
|
615
|
+
if (s.endsWith(sep))
|
|
616
|
+
s = s.slice(0, -sep.length).trim();
|
|
617
|
+
if (s.startsWith(sep))
|
|
618
|
+
s = s.slice(sep.length).trim();
|
|
619
|
+
s = s.replace(new RegExp(`\\${sep}\\s*\\${sep}`, "g"), sep);
|
|
620
|
+
}
|
|
621
|
+
return s;
|
|
622
|
+
}
|
|
623
|
+
|
|
87
624
|
exports.HasElementTags = HasElementTags;
|
|
625
|
+
exports.IsBrowser = IsBrowser;
|
|
88
626
|
exports.SelfClosingTags = SelfClosingTags;
|
|
627
|
+
exports.SortModifiers = SortModifiers;
|
|
628
|
+
exports.TAG_ALIASES = TAG_ALIASES;
|
|
629
|
+
exports.TAG_WEIGHTS = TAG_WEIGHTS;
|
|
89
630
|
exports.TagConfigKeys = TagConfigKeys;
|
|
631
|
+
exports.TagEntityBits = TagEntityBits;
|
|
90
632
|
exports.TagsWithInnerContent = TagsWithInnerContent;
|
|
91
633
|
exports.UniqueTags = UniqueTags;
|
|
92
634
|
exports.ValidHeadTags = ValidHeadTags;
|
|
93
|
-
exports.asArray = asArray;
|
|
94
|
-
exports.
|
|
635
|
+
exports.asArray = asArray$1;
|
|
636
|
+
exports.composableNames = composableNames;
|
|
95
637
|
exports.defineHeadPlugin = defineHeadPlugin;
|
|
96
638
|
exports.hashCode = hashCode;
|
|
97
639
|
exports.hashTag = hashTag;
|
|
640
|
+
exports.normaliseClassProp = normaliseClassProp;
|
|
641
|
+
exports.normaliseEntryTags = normaliseEntryTags;
|
|
642
|
+
exports.normaliseProps = normaliseProps;
|
|
643
|
+
exports.normaliseTag = normaliseTag;
|
|
644
|
+
exports.packMeta = packMeta;
|
|
645
|
+
exports.processTemplateParams = processTemplateParams;
|
|
646
|
+
exports.resolveMetaKeyType = resolveMetaKeyType;
|
|
647
|
+
exports.resolveMetaKeyValue = resolveMetaKeyValue;
|
|
648
|
+
exports.resolvePackedMetaObjectValue = resolvePackedMetaObjectValue;
|
|
98
649
|
exports.resolveTitleTemplate = resolveTitleTemplate;
|
|
99
650
|
exports.tagDedupeKey = tagDedupeKey;
|
|
651
|
+
exports.tagWeight = tagWeight;
|
|
652
|
+
exports.unpackMeta = unpackMeta;
|
|
653
|
+
exports.whitelistSafeInput = whitelistSafeInput;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HeadPlugin, HeadTag } from '@unhead/schema';
|
|
1
|
+
import { HeadPlugin, HeadTag, MetaFlatInput, Head, MaybeArray, HeadSafe, HeadEntry, TemplateParams } from '@unhead/schema';
|
|
2
2
|
|
|
3
3
|
type Arrayable<T> = T | Array<T>;
|
|
4
4
|
declare function asArray<T>(value: Arrayable<T>): T[];
|
|
@@ -9,16 +9,56 @@ declare const HasElementTags: string[];
|
|
|
9
9
|
declare const ValidHeadTags: string[];
|
|
10
10
|
declare const UniqueTags: string[];
|
|
11
11
|
declare const TagConfigKeys: string[];
|
|
12
|
-
declare const
|
|
12
|
+
declare const IsBrowser: boolean;
|
|
13
|
+
declare const composableNames: string[];
|
|
13
14
|
|
|
14
15
|
declare function defineHeadPlugin(plugin: HeadPlugin): HeadPlugin;
|
|
15
16
|
|
|
16
17
|
declare function hashCode(s: string): string;
|
|
17
18
|
declare function hashTag(tag: HeadTag): string;
|
|
18
|
-
declare function computeHashes(hashes: string[]): string;
|
|
19
19
|
|
|
20
20
|
declare function tagDedupeKey<T extends HeadTag>(tag: T, fn?: (key: string) => boolean): string | false;
|
|
21
21
|
|
|
22
22
|
declare function resolveTitleTemplate(template: string | ((title?: string) => string | null) | null, title?: string): string | null;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
type ValidMetaType = 'name' | 'http-equiv' | 'property' | 'charset';
|
|
25
|
+
declare function resolveMetaKeyType(key: string): string;
|
|
26
|
+
declare function resolveMetaKeyValue(key: string): string;
|
|
27
|
+
declare function resolvePackedMetaObjectValue(value: string, key: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Converts a flat meta object into an array of meta entries.
|
|
30
|
+
* @param input
|
|
31
|
+
*/
|
|
32
|
+
declare function unpackMeta<T extends MetaFlatInput>(input: T): Required<Head>['meta'];
|
|
33
|
+
/**
|
|
34
|
+
* Convert an array of meta entries to a flat object.
|
|
35
|
+
* @param inputs
|
|
36
|
+
*/
|
|
37
|
+
declare function packMeta<T extends Required<Head>['meta']>(inputs: T): MetaFlatInput;
|
|
38
|
+
|
|
39
|
+
declare function whitelistSafeInput(input: Record<string, MaybeArray<Record<string, string>>>): HeadSafe;
|
|
40
|
+
|
|
41
|
+
declare function normaliseTag<T extends HeadTag>(tagName: T['tag'], input: HeadTag['props'] | string, e: HeadEntry<T>): Promise<T | T[] | false>;
|
|
42
|
+
declare function normaliseClassProp(v: Required<Required<Head>['htmlAttrs']['class']>): string;
|
|
43
|
+
declare function normaliseProps<T extends HeadTag>(tagName: T['tag'], props: T['props']): Promise<T['props']>;
|
|
44
|
+
declare const TagEntityBits = 10;
|
|
45
|
+
declare function normaliseEntryTags<T extends {} = Head>(e: HeadEntry<T>): Promise<HeadTag[]>;
|
|
46
|
+
|
|
47
|
+
declare const TAG_WEIGHTS: {
|
|
48
|
+
readonly base: -1;
|
|
49
|
+
readonly title: 1;
|
|
50
|
+
};
|
|
51
|
+
declare const TAG_ALIASES: {
|
|
52
|
+
readonly critical: -8;
|
|
53
|
+
readonly high: -1;
|
|
54
|
+
readonly low: 2;
|
|
55
|
+
};
|
|
56
|
+
declare function tagWeight<T extends HeadTag>(tag: T): any;
|
|
57
|
+
declare const SortModifiers: {
|
|
58
|
+
prefix: string;
|
|
59
|
+
offset: number;
|
|
60
|
+
}[];
|
|
61
|
+
|
|
62
|
+
declare function processTemplateParams(s: string, p: TemplateParams): string;
|
|
63
|
+
|
|
64
|
+
export { Arrayable, HasElementTags, IsBrowser, SelfClosingTags, SortModifiers, TAG_ALIASES, TAG_WEIGHTS, TagConfigKeys, TagEntityBits, TagsWithInnerContent, UniqueTags, ValidHeadTags, ValidMetaType, asArray, composableNames, defineHeadPlugin, hashCode, hashTag, normaliseClassProp, normaliseEntryTags, normaliseProps, normaliseTag, packMeta, processTemplateParams, resolveMetaKeyType, resolveMetaKeyValue, resolvePackedMetaObjectValue, resolveTitleTemplate, tagDedupeKey, tagWeight, unpackMeta, whitelistSafeInput };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function asArray(value) {
|
|
1
|
+
function asArray$1(value) {
|
|
2
2
|
return Array.isArray(value) ? value : [value];
|
|
3
3
|
}
|
|
4
4
|
|
|
@@ -27,7 +27,16 @@ const ValidHeadTags = [
|
|
|
27
27
|
];
|
|
28
28
|
const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs", "templateParams"];
|
|
29
29
|
const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy", "innerHTML", "textContent"];
|
|
30
|
-
const
|
|
30
|
+
const IsBrowser = typeof window !== "undefined";
|
|
31
|
+
const composableNames = [
|
|
32
|
+
"getActiveHead",
|
|
33
|
+
"useHead",
|
|
34
|
+
"useSeoMeta",
|
|
35
|
+
"useHeadSafe",
|
|
36
|
+
"useServerHead",
|
|
37
|
+
"useServerSeoMeta",
|
|
38
|
+
"useServerHeadSafe"
|
|
39
|
+
];
|
|
31
40
|
|
|
32
41
|
function defineHeadPlugin(plugin) {
|
|
33
42
|
return plugin;
|
|
@@ -40,15 +49,7 @@ function hashCode(s) {
|
|
|
40
49
|
return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
|
|
41
50
|
}
|
|
42
51
|
function hashTag(tag) {
|
|
43
|
-
return hashCode(`${tag.tag}:${tag.textContent || tag.innerHTML || ""}:${Object.entries(tag.props).map(([key, value]) => `${key}:${String(value)}`).join(",")}`);
|
|
44
|
-
}
|
|
45
|
-
function computeHashes(hashes) {
|
|
46
|
-
let h = 9;
|
|
47
|
-
for (const s of hashes) {
|
|
48
|
-
for (let i = 0; i < s.length; )
|
|
49
|
-
h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
|
|
50
|
-
}
|
|
51
|
-
return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
|
|
52
|
+
return tag._h || hashCode(tag._d ? tag._d : `${tag.tag}:${tag.textContent || tag.innerHTML || ""}:${Object.entries(tag.props).map(([key, value]) => `${key}:${String(value)}`).join(",")}`);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function tagDedupeKey(tag, fn) {
|
|
@@ -81,4 +82,541 @@ function resolveTitleTemplate(template, title) {
|
|
|
81
82
|
return template;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
function asArray(input) {
|
|
86
|
+
return Array.isArray(input) ? input : [input];
|
|
87
|
+
}
|
|
88
|
+
const InternalKeySymbol = "_$key";
|
|
89
|
+
function packObject(input, options) {
|
|
90
|
+
const keys = Object.keys(input);
|
|
91
|
+
let [k, v] = keys;
|
|
92
|
+
options = options || {};
|
|
93
|
+
options.key = options.key || k;
|
|
94
|
+
options.value = options.value || v;
|
|
95
|
+
options.resolveKey = options.resolveKey || ((k2) => k2);
|
|
96
|
+
const resolveKey = (index) => {
|
|
97
|
+
const arr = asArray(options?.[index]);
|
|
98
|
+
return arr.find((k2) => {
|
|
99
|
+
if (typeof k2 === "string" && k2.includes(".")) {
|
|
100
|
+
return k2;
|
|
101
|
+
}
|
|
102
|
+
return k2 && keys.includes(k2);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const resolveValue = (k2, input2) => {
|
|
106
|
+
if (k2.includes(".")) {
|
|
107
|
+
const paths = k2.split(".");
|
|
108
|
+
let val = input2;
|
|
109
|
+
for (const path of paths)
|
|
110
|
+
val = val[path];
|
|
111
|
+
return val;
|
|
112
|
+
}
|
|
113
|
+
return input2[k2];
|
|
114
|
+
};
|
|
115
|
+
k = resolveKey("key") || k;
|
|
116
|
+
v = resolveKey("value") || v;
|
|
117
|
+
const dedupeKeyPrefix = input.key ? `${InternalKeySymbol}${input.key}-` : "";
|
|
118
|
+
let keyValue = resolveValue(k, input);
|
|
119
|
+
keyValue = options.resolveKey(keyValue);
|
|
120
|
+
return {
|
|
121
|
+
[`${dedupeKeyPrefix}${keyValue}`]: resolveValue(v, input)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function packArray(input, options) {
|
|
126
|
+
const packed = {};
|
|
127
|
+
for (const i of input) {
|
|
128
|
+
const packedObj = packObject(i, options);
|
|
129
|
+
const pKey = Object.keys(packedObj)[0];
|
|
130
|
+
const isDedupeKey = pKey.startsWith(InternalKeySymbol);
|
|
131
|
+
if (!isDedupeKey && packed[pKey]) {
|
|
132
|
+
packed[pKey] = Array.isArray(packed[pKey]) ? packed[pKey] : [packed[pKey]];
|
|
133
|
+
packed[pKey].push(Object.values(packedObj)[0]);
|
|
134
|
+
} else {
|
|
135
|
+
packed[isDedupeKey ? pKey.split("-").slice(1).join("-") || pKey : pKey] = packedObj[pKey];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return packed;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function unpackToArray(input, options) {
|
|
142
|
+
const unpacked = [];
|
|
143
|
+
const kFn = options.resolveKeyData || ((ctx) => ctx.key);
|
|
144
|
+
const vFn = options.resolveValueData || ((ctx) => ctx.value);
|
|
145
|
+
for (const [k, v] of Object.entries(input)) {
|
|
146
|
+
unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
|
|
147
|
+
const ctx = { key: k, value: i };
|
|
148
|
+
const val = vFn(ctx);
|
|
149
|
+
if (typeof val === "object")
|
|
150
|
+
return unpackToArray(val, options);
|
|
151
|
+
if (Array.isArray(val))
|
|
152
|
+
return val;
|
|
153
|
+
return {
|
|
154
|
+
[typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
|
|
155
|
+
[typeof options.value === "function" ? options.value(ctx) : options.value]: val
|
|
156
|
+
};
|
|
157
|
+
}).flat());
|
|
158
|
+
}
|
|
159
|
+
return unpacked;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function unpackToString(value, options) {
|
|
163
|
+
return Object.entries(value).map(([key, value2]) => {
|
|
164
|
+
if (typeof value2 === "object")
|
|
165
|
+
value2 = unpackToString(value2, options);
|
|
166
|
+
if (options.resolve) {
|
|
167
|
+
const resolved = options.resolve({ key, value: value2 });
|
|
168
|
+
if (resolved)
|
|
169
|
+
return resolved;
|
|
170
|
+
}
|
|
171
|
+
if (typeof value2 === "number")
|
|
172
|
+
value2 = value2.toString();
|
|
173
|
+
if (typeof value2 === "string" && options.wrapValue) {
|
|
174
|
+
value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
|
|
175
|
+
value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
|
|
176
|
+
}
|
|
177
|
+
return `${key}${options.keyValueSeparator || ""}${value2}`;
|
|
178
|
+
}).join(options.entrySeparator || "");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const MetaPackingSchema = {
|
|
182
|
+
robots: {
|
|
183
|
+
unpack: {
|
|
184
|
+
keyValueSeparator: ":"
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
// Pragma directives
|
|
188
|
+
contentSecurityPolicy: {
|
|
189
|
+
unpack: {
|
|
190
|
+
keyValueSeparator: " ",
|
|
191
|
+
entrySeparator: "; "
|
|
192
|
+
},
|
|
193
|
+
metaKey: "http-equiv"
|
|
194
|
+
},
|
|
195
|
+
fbAppId: {
|
|
196
|
+
keyValue: "fb:app_id",
|
|
197
|
+
metaKey: "property"
|
|
198
|
+
},
|
|
199
|
+
ogSiteName: {
|
|
200
|
+
keyValue: "og:site_name"
|
|
201
|
+
},
|
|
202
|
+
msapplicationTileImage: {
|
|
203
|
+
keyValue: "msapplication-TileImage"
|
|
204
|
+
},
|
|
205
|
+
/**
|
|
206
|
+
* Tile colour for windows
|
|
207
|
+
*/
|
|
208
|
+
msapplicationTileColor: {
|
|
209
|
+
keyValue: "msapplication-TileColor"
|
|
210
|
+
},
|
|
211
|
+
/**
|
|
212
|
+
* URL of a config for windows tile.
|
|
213
|
+
*/
|
|
214
|
+
msapplicationConfig: {
|
|
215
|
+
keyValue: "msapplication-Config"
|
|
216
|
+
},
|
|
217
|
+
charset: {
|
|
218
|
+
metaKey: "charset"
|
|
219
|
+
},
|
|
220
|
+
contentType: {
|
|
221
|
+
metaKey: "http-equiv"
|
|
222
|
+
},
|
|
223
|
+
defaultStyle: {
|
|
224
|
+
metaKey: "http-equiv"
|
|
225
|
+
},
|
|
226
|
+
xUaCompatible: {
|
|
227
|
+
metaKey: "http-equiv"
|
|
228
|
+
},
|
|
229
|
+
refresh: {
|
|
230
|
+
metaKey: "http-equiv"
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const ColonPrefixKeys = /^(og|twitter|fb)/;
|
|
234
|
+
const PropertyPrefixKeys = /^(og|fb)/;
|
|
235
|
+
function resolveMetaKeyType(key) {
|
|
236
|
+
return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
|
|
237
|
+
}
|
|
238
|
+
function resolveMetaKeyValue(key) {
|
|
239
|
+
return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
|
|
240
|
+
}
|
|
241
|
+
function fixKeyCase(key) {
|
|
242
|
+
key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
243
|
+
if (ColonPrefixKeys.test(key)) {
|
|
244
|
+
key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
|
|
245
|
+
}
|
|
246
|
+
return key;
|
|
247
|
+
}
|
|
248
|
+
function changeKeyCasingDeep(input) {
|
|
249
|
+
if (Array.isArray(input)) {
|
|
250
|
+
return input.map((entry) => changeKeyCasingDeep(entry));
|
|
251
|
+
}
|
|
252
|
+
if (typeof input !== "object" || Array.isArray(input))
|
|
253
|
+
return input;
|
|
254
|
+
const output = {};
|
|
255
|
+
for (const [key, value] of Object.entries(input))
|
|
256
|
+
output[fixKeyCase(key)] = changeKeyCasingDeep(value);
|
|
257
|
+
return output;
|
|
258
|
+
}
|
|
259
|
+
function resolvePackedMetaObjectValue(value, key) {
|
|
260
|
+
const definition = MetaPackingSchema[key];
|
|
261
|
+
if (key === "refresh")
|
|
262
|
+
return `${value.seconds};url=${value.url}`;
|
|
263
|
+
return unpackToString(
|
|
264
|
+
changeKeyCasingDeep(value),
|
|
265
|
+
{
|
|
266
|
+
entrySeparator: ", ",
|
|
267
|
+
keyValueSeparator: "=",
|
|
268
|
+
resolve({ value: value2, key: key2 }) {
|
|
269
|
+
if (value2 === null)
|
|
270
|
+
return "";
|
|
271
|
+
if (typeof value2 === "boolean")
|
|
272
|
+
return `${key2}`;
|
|
273
|
+
},
|
|
274
|
+
...definition?.unpack
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const OpenGraphInputs = ["og:Image", "og:Video", "og:Audio", "twitter:Image"];
|
|
279
|
+
const SimpleArrayUnpackMetas = ["themeColor"];
|
|
280
|
+
function unpackMeta(input) {
|
|
281
|
+
const extras = [];
|
|
282
|
+
OpenGraphInputs.forEach((key) => {
|
|
283
|
+
const propKey = key.toLowerCase();
|
|
284
|
+
const inputKey = `${key.replace(":", "")}`;
|
|
285
|
+
const val = input[inputKey];
|
|
286
|
+
if (typeof val === "object") {
|
|
287
|
+
(Array.isArray(val) ? val : [val]).forEach((entry) => {
|
|
288
|
+
if (!entry)
|
|
289
|
+
return;
|
|
290
|
+
const unpackedEntry = unpackToArray(entry, {
|
|
291
|
+
key: key.startsWith("og") ? "property" : "name",
|
|
292
|
+
value: "content",
|
|
293
|
+
resolveKeyData({ key: key2 }) {
|
|
294
|
+
return fixKeyCase(`${propKey}${key2 !== "url" ? `:${key2}` : ""}`);
|
|
295
|
+
},
|
|
296
|
+
resolveValueData({ value }) {
|
|
297
|
+
return typeof value === "number" ? value.toString() : value;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
extras.push(
|
|
301
|
+
...unpackedEntry.sort((a, b) => a.property === propKey ? -1 : b.property === propKey ? 1 : 0)
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
delete input[inputKey];
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
SimpleArrayUnpackMetas.forEach((meta2) => {
|
|
308
|
+
if (input[meta2] && typeof input[meta2] !== "string") {
|
|
309
|
+
const val = Array.isArray(input[meta2]) ? input[meta2] : [input[meta2]];
|
|
310
|
+
delete input[meta2];
|
|
311
|
+
val.forEach((entry) => {
|
|
312
|
+
extras.push({
|
|
313
|
+
name: fixKeyCase(meta2),
|
|
314
|
+
...entry
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
const meta = unpackToArray(input, {
|
|
320
|
+
key({ key }) {
|
|
321
|
+
return resolveMetaKeyType(key);
|
|
322
|
+
},
|
|
323
|
+
value({ key }) {
|
|
324
|
+
return key === "charset" ? "charset" : "content";
|
|
325
|
+
},
|
|
326
|
+
resolveKeyData({ key }) {
|
|
327
|
+
return resolveMetaKeyValue(key);
|
|
328
|
+
},
|
|
329
|
+
resolveValueData({ value, key }) {
|
|
330
|
+
if (value === null)
|
|
331
|
+
return "_null";
|
|
332
|
+
if (typeof value === "object")
|
|
333
|
+
return resolvePackedMetaObjectValue(value, key);
|
|
334
|
+
return typeof value === "number" ? value.toString() : value;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
|
|
338
|
+
}
|
|
339
|
+
function packMeta(inputs) {
|
|
340
|
+
const mappedPackingSchema = Object.entries(MetaPackingSchema).map(([key, value]) => [key, value.keyValue]);
|
|
341
|
+
return packArray(inputs, {
|
|
342
|
+
key: ["name", "property", "httpEquiv", "http-equiv", "charset"],
|
|
343
|
+
value: ["content", "charset"],
|
|
344
|
+
resolveKey(k) {
|
|
345
|
+
let key = mappedPackingSchema.filter((sk) => sk[1] === k)?.[0]?.[0] || k;
|
|
346
|
+
const replacer = (_, letter) => letter?.toUpperCase();
|
|
347
|
+
key = key.replace(/:([a-z])/g, replacer).replace(/-([a-z])/g, replacer);
|
|
348
|
+
return key;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const WhitelistAttributes = {
|
|
354
|
+
htmlAttrs: ["id", "class", "lang", "dir"],
|
|
355
|
+
bodyAttrs: ["id", "class"],
|
|
356
|
+
meta: ["id", "name", "property", "charset", "content"],
|
|
357
|
+
noscript: ["id", "textContent"],
|
|
358
|
+
script: ["id", "type", "textContent"],
|
|
359
|
+
link: ["id", "color", "crossorigin", "fetchpriority", "href", "hreflang", "imagesrcset", "imagesizes", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"]
|
|
360
|
+
};
|
|
361
|
+
function whitelistSafeInput(input) {
|
|
362
|
+
const filtered = {};
|
|
363
|
+
Object.keys(input).forEach((key) => {
|
|
364
|
+
const tagValue = input[key];
|
|
365
|
+
if (!tagValue)
|
|
366
|
+
return;
|
|
367
|
+
switch (key) {
|
|
368
|
+
case "title":
|
|
369
|
+
case "titleTemplate":
|
|
370
|
+
case "templateParams":
|
|
371
|
+
filtered[key] = tagValue;
|
|
372
|
+
break;
|
|
373
|
+
case "htmlAttrs":
|
|
374
|
+
case "bodyAttrs":
|
|
375
|
+
filtered[key] = {};
|
|
376
|
+
WhitelistAttributes[key].forEach((a) => {
|
|
377
|
+
if (tagValue[a])
|
|
378
|
+
filtered[key][a] = tagValue[a];
|
|
379
|
+
});
|
|
380
|
+
Object.keys(tagValue || {}).filter((a) => a.startsWith("data-")).forEach((a) => {
|
|
381
|
+
filtered[key][a] = tagValue[a];
|
|
382
|
+
});
|
|
383
|
+
break;
|
|
384
|
+
case "meta":
|
|
385
|
+
if (Array.isArray(tagValue)) {
|
|
386
|
+
filtered[key] = tagValue.map((meta) => {
|
|
387
|
+
const safeMeta = {};
|
|
388
|
+
WhitelistAttributes.meta.forEach((key2) => {
|
|
389
|
+
if (meta[key2] || key2.startsWith("data-"))
|
|
390
|
+
safeMeta[key2] = meta[key2];
|
|
391
|
+
});
|
|
392
|
+
return safeMeta;
|
|
393
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
case "link":
|
|
397
|
+
if (Array.isArray(tagValue)) {
|
|
398
|
+
filtered[key] = tagValue.map((meta) => {
|
|
399
|
+
const link = {};
|
|
400
|
+
WhitelistAttributes.link.forEach((key2) => {
|
|
401
|
+
const val = meta[key2];
|
|
402
|
+
if (key2 === "rel" && ["stylesheet", "canonical", "modulepreload", "prerender", "preload", "prefetch"].includes(val))
|
|
403
|
+
return;
|
|
404
|
+
if (key2 === "href") {
|
|
405
|
+
if (val.includes("javascript:") || val.includes("data:"))
|
|
406
|
+
return;
|
|
407
|
+
link[key2] = val;
|
|
408
|
+
} else if (val || key2.startsWith("data-")) {
|
|
409
|
+
link[key2] = val;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
return link;
|
|
413
|
+
}).filter((link) => Object.keys(link).length > 1 && !!link.rel);
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
case "noscript":
|
|
417
|
+
if (Array.isArray(tagValue)) {
|
|
418
|
+
filtered[key] = tagValue.map((meta) => {
|
|
419
|
+
const noscript = {};
|
|
420
|
+
WhitelistAttributes.noscript.forEach((key2) => {
|
|
421
|
+
if (meta[key2] || key2.startsWith("data-"))
|
|
422
|
+
noscript[key2] = meta[key2];
|
|
423
|
+
});
|
|
424
|
+
return noscript;
|
|
425
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
case "script":
|
|
429
|
+
if (Array.isArray(tagValue)) {
|
|
430
|
+
filtered[key] = tagValue.map((script) => {
|
|
431
|
+
const safeScript = {};
|
|
432
|
+
WhitelistAttributes.script.forEach((s) => {
|
|
433
|
+
if (script[s] || s.startsWith("data-")) {
|
|
434
|
+
if (s === "textContent") {
|
|
435
|
+
try {
|
|
436
|
+
const jsonVal = typeof script[s] === "string" ? JSON.parse(script[s]) : script[s];
|
|
437
|
+
safeScript[s] = JSON.stringify(jsonVal, null, 0);
|
|
438
|
+
} catch (e) {
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
safeScript[s] = script[s];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
return safeScript;
|
|
446
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
return filtered;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function normaliseTag(tagName, input, e) {
|
|
455
|
+
const tag = { tag: tagName, props: {} };
|
|
456
|
+
if (input instanceof Promise)
|
|
457
|
+
input = await input;
|
|
458
|
+
if (tagName === "templateParams") {
|
|
459
|
+
tag.props = input;
|
|
460
|
+
return tag;
|
|
461
|
+
}
|
|
462
|
+
if (["title", "titleTemplate"].includes(tagName)) {
|
|
463
|
+
if (input && typeof input === "object") {
|
|
464
|
+
tag.textContent = input.textContent;
|
|
465
|
+
if (input.tagPriority)
|
|
466
|
+
tag.tagPriority = input.tagPriority;
|
|
467
|
+
} else {
|
|
468
|
+
tag.textContent = input;
|
|
469
|
+
}
|
|
470
|
+
return tag;
|
|
471
|
+
}
|
|
472
|
+
if (typeof input === "string") {
|
|
473
|
+
if (!["script", "noscript", "style"].includes(tagName))
|
|
474
|
+
return false;
|
|
475
|
+
if (tagName === "script" && (/^(https?:)?\/\//.test(input) || input.startsWith("/")))
|
|
476
|
+
tag.props.src = input;
|
|
477
|
+
else
|
|
478
|
+
tag.innerHTML = input;
|
|
479
|
+
return tag;
|
|
480
|
+
}
|
|
481
|
+
tag.props = await normaliseProps(tagName, { ...input });
|
|
482
|
+
if (tag.props.children) {
|
|
483
|
+
tag.props.innerHTML = tag.props.children;
|
|
484
|
+
}
|
|
485
|
+
delete tag.props.children;
|
|
486
|
+
Object.keys(tag.props).filter((k) => TagConfigKeys.includes(k)).forEach((k) => {
|
|
487
|
+
if (!["innerHTML", "textContent"].includes(k) || TagsWithInnerContent.includes(tag.tag)) {
|
|
488
|
+
tag[k] = tag.props[k];
|
|
489
|
+
}
|
|
490
|
+
delete tag.props[k];
|
|
491
|
+
});
|
|
492
|
+
TagConfigKeys.forEach((k) => {
|
|
493
|
+
if (!tag[k] && e[k]) {
|
|
494
|
+
tag[k] = e[k];
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
["innerHTML", "textContent"].forEach((k) => {
|
|
498
|
+
if (tag.tag === "script" && typeof tag[k] === "string" && ["application/ld+json", "application/json"].includes(tag.props.type)) {
|
|
499
|
+
try {
|
|
500
|
+
tag[k] = JSON.parse(tag[k]);
|
|
501
|
+
} catch (e2) {
|
|
502
|
+
tag[k] = "";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (typeof tag[k] === "object")
|
|
506
|
+
tag[k] = JSON.stringify(tag[k]);
|
|
507
|
+
});
|
|
508
|
+
if (tag.props.class)
|
|
509
|
+
tag.props.class = normaliseClassProp(tag.props.class);
|
|
510
|
+
if (tag.props.content && Array.isArray(tag.props.content))
|
|
511
|
+
return tag.props.content.map((v) => ({ ...tag, props: { ...tag.props, content: v } }));
|
|
512
|
+
return tag;
|
|
513
|
+
}
|
|
514
|
+
function normaliseClassProp(v) {
|
|
515
|
+
if (typeof v === "object" && !Array.isArray(v)) {
|
|
516
|
+
v = Object.keys(v).filter((k) => v[k]);
|
|
517
|
+
}
|
|
518
|
+
return (Array.isArray(v) ? v.join(" ") : v).split(" ").filter((c) => c.trim()).filter(Boolean).join(" ");
|
|
519
|
+
}
|
|
520
|
+
async function normaliseProps(tagName, props) {
|
|
521
|
+
for (const k of Object.keys(props)) {
|
|
522
|
+
const isDataKey = k.startsWith("data-");
|
|
523
|
+
if (props[k] instanceof Promise) {
|
|
524
|
+
props[k] = await props[k];
|
|
525
|
+
}
|
|
526
|
+
if (String(props[k]) === "true") {
|
|
527
|
+
props[k] = isDataKey ? "true" : "";
|
|
528
|
+
} else if (String(props[k]) === "false") {
|
|
529
|
+
if (isDataKey) {
|
|
530
|
+
props[k] = "false";
|
|
531
|
+
} else {
|
|
532
|
+
delete props[k];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return props;
|
|
537
|
+
}
|
|
538
|
+
const TagEntityBits = 10;
|
|
539
|
+
async function normaliseEntryTags(e) {
|
|
540
|
+
const tagPromises = [];
|
|
541
|
+
Object.entries(e.resolvedInput).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).forEach(([k, value]) => {
|
|
542
|
+
const v = asArray$1(value);
|
|
543
|
+
tagPromises.push(...v.map((props) => normaliseTag(k, props, e)).flat());
|
|
544
|
+
});
|
|
545
|
+
return (await Promise.all(tagPromises)).flat().filter(Boolean).map((t, i) => {
|
|
546
|
+
t._e = e._i;
|
|
547
|
+
t._p = (e._i << TagEntityBits) + i;
|
|
548
|
+
return t;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const TAG_WEIGHTS = {
|
|
553
|
+
// tags
|
|
554
|
+
base: -1,
|
|
555
|
+
title: 1
|
|
556
|
+
};
|
|
557
|
+
const TAG_ALIASES = {
|
|
558
|
+
// relative scores to their default values
|
|
559
|
+
critical: -8,
|
|
560
|
+
high: -1,
|
|
561
|
+
low: 2
|
|
562
|
+
};
|
|
563
|
+
function tagWeight(tag) {
|
|
564
|
+
let weight = 10;
|
|
565
|
+
const priority = tag.tagPriority;
|
|
566
|
+
if (typeof priority === "number")
|
|
567
|
+
return priority;
|
|
568
|
+
if (tag.tag === "meta") {
|
|
569
|
+
if (tag.props.charset)
|
|
570
|
+
weight = -2;
|
|
571
|
+
if (tag.props["http-equiv"] === "content-security-policy")
|
|
572
|
+
weight = 0;
|
|
573
|
+
} else if (tag.tag === "link" && tag.props.rel === "preconnect") {
|
|
574
|
+
weight = 2;
|
|
575
|
+
} else if (tag.tag in TAG_WEIGHTS) {
|
|
576
|
+
weight = TAG_WEIGHTS[tag.tag];
|
|
577
|
+
}
|
|
578
|
+
if (typeof priority === "string" && priority in TAG_ALIASES) {
|
|
579
|
+
return weight + TAG_ALIASES[priority];
|
|
580
|
+
}
|
|
581
|
+
return weight;
|
|
582
|
+
}
|
|
583
|
+
const SortModifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
|
|
584
|
+
|
|
585
|
+
function processTemplateParams(s, p) {
|
|
586
|
+
if (typeof s !== "string")
|
|
587
|
+
return s;
|
|
588
|
+
function sub(token) {
|
|
589
|
+
if (["s", "pageTitle"].includes(token))
|
|
590
|
+
return p.pageTitle;
|
|
591
|
+
let val;
|
|
592
|
+
if (token.includes(".")) {
|
|
593
|
+
val = token.split(".").reduce((acc, key) => acc ? acc[key] || void 0 : void 0, p);
|
|
594
|
+
} else {
|
|
595
|
+
val = p[token];
|
|
596
|
+
}
|
|
597
|
+
return typeof val !== "undefined" ? val || "" : false;
|
|
598
|
+
}
|
|
599
|
+
let decoded = s;
|
|
600
|
+
try {
|
|
601
|
+
decoded = decodeURI(s);
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
const tokens = (decoded.match(/%(\w+\.+\w+)|%(\w+)/g) || []).sort().reverse();
|
|
605
|
+
tokens.forEach((token) => {
|
|
606
|
+
const re = sub(token.slice(1));
|
|
607
|
+
if (typeof re === "string") {
|
|
608
|
+
s = s.replace(new RegExp(`\\${token}(\\W|$)`, "g"), (_, args) => `${re}${args}`).trim();
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
const sep = p.separator;
|
|
612
|
+
if (s.includes(sep)) {
|
|
613
|
+
if (s.endsWith(sep))
|
|
614
|
+
s = s.slice(0, -sep.length).trim();
|
|
615
|
+
if (s.startsWith(sep))
|
|
616
|
+
s = s.slice(sep.length).trim();
|
|
617
|
+
s = s.replace(new RegExp(`\\${sep}\\s*\\${sep}`, "g"), sep);
|
|
618
|
+
}
|
|
619
|
+
return s;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export { HasElementTags, IsBrowser, SelfClosingTags, SortModifiers, TAG_ALIASES, TAG_WEIGHTS, TagConfigKeys, TagEntityBits, TagsWithInnerContent, UniqueTags, ValidHeadTags, asArray$1 as asArray, composableNames, defineHeadPlugin, hashCode, hashTag, normaliseClassProp, normaliseEntryTags, normaliseProps, normaliseTag, packMeta, processTemplateParams, resolveMetaKeyType, resolveMetaKeyValue, resolvePackedMetaObjectValue, resolveTitleTemplate, tagDedupeKey, tagWeight, unpackMeta, whitelistSafeInput };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unhead/shared",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0-beta.0",
|
|
5
5
|
"author": "Harlan Wilton <harlan@harlanzw.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": "https://github.com/sponsors/harlan-zw",
|
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
"dist"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@unhead/schema": "1.
|
|
37
|
+
"@unhead/schema": "1.3.0-beta.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"packrup": "^0.1.0"
|
|
38
41
|
},
|
|
39
42
|
"scripts": {
|
|
40
43
|
"build": "unbuild .",
|