unhead 1.0.22 → 1.1.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 +369 -179
- package/dist/index.d.ts +107 -13
- package/dist/index.mjs +366 -186
- package/package.json +6 -8
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const hookable = require('hookable');
|
|
4
|
-
const shared = require('@unhead/shared');
|
|
5
4
|
const dom = require('@unhead/dom');
|
|
5
|
+
const shared = require('@unhead/shared');
|
|
6
|
+
const packrup = require('packrup');
|
|
6
7
|
|
|
7
8
|
const TAG_WEIGHTS = {
|
|
8
9
|
// aliases
|
|
@@ -66,20 +67,20 @@ const TitleTemplatePlugin = () => {
|
|
|
66
67
|
const titleIdx = tags.findIndex((i) => i.tag === "title");
|
|
67
68
|
if (titleIdx !== -1 && titleTemplateIdx !== -1) {
|
|
68
69
|
const newTitle = renderTitleTemplate(
|
|
69
|
-
tags[titleTemplateIdx].
|
|
70
|
-
tags[titleIdx].
|
|
70
|
+
tags[titleTemplateIdx].textContent,
|
|
71
|
+
tags[titleIdx].textContent
|
|
71
72
|
);
|
|
72
73
|
if (newTitle !== null) {
|
|
73
|
-
tags[titleIdx].
|
|
74
|
+
tags[titleIdx].textContent = newTitle || tags[titleIdx].textContent;
|
|
74
75
|
} else {
|
|
75
76
|
delete tags[titleIdx];
|
|
76
77
|
}
|
|
77
78
|
} else if (titleTemplateIdx !== -1) {
|
|
78
79
|
const newTitle = renderTitleTemplate(
|
|
79
|
-
tags[titleTemplateIdx].
|
|
80
|
+
tags[titleTemplateIdx].textContent
|
|
80
81
|
);
|
|
81
82
|
if (newTitle !== null) {
|
|
82
|
-
tags[titleTemplateIdx].
|
|
83
|
+
tags[titleTemplateIdx].textContent = newTitle;
|
|
83
84
|
tags[titleTemplateIdx].tag = "title";
|
|
84
85
|
titleTemplateIdx = -1;
|
|
85
86
|
}
|
|
@@ -112,15 +113,18 @@ const ProvideTagHashPlugin = () => {
|
|
|
112
113
|
return shared.defineHeadPlugin({
|
|
113
114
|
hooks: {
|
|
114
115
|
"tag:normalise": (ctx) => {
|
|
115
|
-
const { tag, entry } = ctx;
|
|
116
|
+
const { tag, entry, resolvedOptions } = ctx;
|
|
117
|
+
if (resolvedOptions.experimentalHashHydration === true) {
|
|
118
|
+
tag._h = shared.hashTag(tag);
|
|
119
|
+
}
|
|
116
120
|
const isDynamic = typeof tag.props._dynamic !== "undefined";
|
|
117
121
|
if (!shared.HasElementTags.includes(tag.tag) || !tag.key)
|
|
118
122
|
return;
|
|
119
|
-
tag._hash = shared.hashCode(JSON.stringify({ tag: tag.tag, key: tag.key }));
|
|
120
123
|
if (IsBrowser || getActiveHead()?.resolvedOptions?.document)
|
|
121
124
|
return;
|
|
122
125
|
if (entry._m === "server" || isDynamic) {
|
|
123
|
-
tag.
|
|
126
|
+
tag._h = tag._h || shared.hashTag(tag);
|
|
127
|
+
tag.props[`data-h-${tag._h}`] = "";
|
|
124
128
|
}
|
|
125
129
|
},
|
|
126
130
|
"tags:resolve": (ctx) => {
|
|
@@ -184,7 +188,8 @@ const EventHandlersPlugin = () => {
|
|
|
184
188
|
const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
|
|
185
189
|
const eventName = k.slice(2).toLowerCase();
|
|
186
190
|
const eventDedupeKey = `data-h-${eventName}`;
|
|
187
|
-
|
|
191
|
+
ctx.markSideEffect(sdeKey, () => {
|
|
192
|
+
});
|
|
188
193
|
if ($el.hasAttribute(eventDedupeKey))
|
|
189
194
|
return;
|
|
190
195
|
const handler = value;
|
|
@@ -205,6 +210,119 @@ const EventHandlersPlugin = () => {
|
|
|
205
210
|
});
|
|
206
211
|
};
|
|
207
212
|
|
|
213
|
+
const UsesMergeStrategy = ["templateParams", "htmlAttrs", "bodyAttrs"];
|
|
214
|
+
const DedupesTagsPlugin = (options) => {
|
|
215
|
+
options = options || {};
|
|
216
|
+
const dedupeKeys = options.dedupeKeys || ["hid", "vmid", "key"];
|
|
217
|
+
return shared.defineHeadPlugin({
|
|
218
|
+
hooks: {
|
|
219
|
+
"tag:normalise": function({ tag }) {
|
|
220
|
+
dedupeKeys.forEach((key) => {
|
|
221
|
+
if (tag.props[key]) {
|
|
222
|
+
tag.key = tag.props[key];
|
|
223
|
+
delete tag.props[key];
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
const dedupe = tag.key ? `${tag.tag}:${tag.key}` : shared.tagDedupeKey(tag);
|
|
227
|
+
if (dedupe)
|
|
228
|
+
tag._d = dedupe;
|
|
229
|
+
},
|
|
230
|
+
"tags:resolve": function(ctx) {
|
|
231
|
+
const deduping = {};
|
|
232
|
+
ctx.tags.forEach((tag) => {
|
|
233
|
+
const dedupeKey = tag._d || tag._p;
|
|
234
|
+
const dupedTag = deduping[dedupeKey];
|
|
235
|
+
if (dupedTag) {
|
|
236
|
+
let strategy = tag?.tagDuplicateStrategy;
|
|
237
|
+
if (!strategy && UsesMergeStrategy.includes(tag.tag))
|
|
238
|
+
strategy = "merge";
|
|
239
|
+
if (strategy === "merge") {
|
|
240
|
+
const oldProps = dupedTag.props;
|
|
241
|
+
["class", "style"].forEach((key) => {
|
|
242
|
+
if (tag.props[key] && oldProps[key]) {
|
|
243
|
+
if (key === "style" && !oldProps[key].endsWith(";"))
|
|
244
|
+
oldProps[key] += ";";
|
|
245
|
+
tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
deduping[dedupeKey].props = {
|
|
249
|
+
...oldProps,
|
|
250
|
+
...tag.props
|
|
251
|
+
};
|
|
252
|
+
return;
|
|
253
|
+
} else if (tag._e === dupedTag._e) {
|
|
254
|
+
dupedTag._duped = dupedTag._duped || [];
|
|
255
|
+
tag._d = `${dupedTag._d}:${dupedTag._duped.length + 1}`;
|
|
256
|
+
dupedTag._duped.push(tag);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const propCount = Object.keys(tag.props).length;
|
|
260
|
+
if ((propCount === 0 || propCount === 1 && typeof tag.props["data-h-key"] !== "undefined") && !tag.innerHTML && !tag.textContent) {
|
|
261
|
+
delete deduping[dedupeKey];
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
deduping[dedupeKey] = tag;
|
|
266
|
+
});
|
|
267
|
+
const newTags = [];
|
|
268
|
+
Object.values(deduping).forEach((tag) => {
|
|
269
|
+
const dupes = tag._duped;
|
|
270
|
+
delete tag._duped;
|
|
271
|
+
newTags.push(tag);
|
|
272
|
+
if (dupes)
|
|
273
|
+
newTags.push(...dupes);
|
|
274
|
+
});
|
|
275
|
+
ctx.tags = newTags;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
function processTemplateParams(s, config) {
|
|
282
|
+
const replacer = (preserveToken) => (_, token) => {
|
|
283
|
+
if (token === "pageTitle" || token === "s")
|
|
284
|
+
return "%s";
|
|
285
|
+
let val;
|
|
286
|
+
if (token.includes("."))
|
|
287
|
+
val = token.split(".").reduce((acc, key) => acc[key] || {}, config);
|
|
288
|
+
else
|
|
289
|
+
val = config[token];
|
|
290
|
+
return val || (preserveToken ? token : "");
|
|
291
|
+
};
|
|
292
|
+
let template = s.replace(/%(\w+\.?\w+)%/g, replacer()).replace(/%(\w+\.?\w+)/g, replacer(true)).trim();
|
|
293
|
+
if (config.separator) {
|
|
294
|
+
if (template.endsWith(config.separator))
|
|
295
|
+
template = template.slice(0, -config.separator.length).trim();
|
|
296
|
+
if (template.startsWith(config.separator))
|
|
297
|
+
template = template.slice(config.separator.length).trim();
|
|
298
|
+
template = template.replace(new RegExp(`\\${config.separator}\\s*\\${config.separator}`, "g"), config.separator);
|
|
299
|
+
}
|
|
300
|
+
return template;
|
|
301
|
+
}
|
|
302
|
+
function TemplateParamsPlugin() {
|
|
303
|
+
return shared.defineHeadPlugin({
|
|
304
|
+
hooks: {
|
|
305
|
+
"tags:resolve": (ctx) => {
|
|
306
|
+
const { tags } = ctx;
|
|
307
|
+
const templateParamsIdx = tags.findIndex((tag) => tag.tag === "templateParams");
|
|
308
|
+
if (templateParamsIdx !== -1) {
|
|
309
|
+
const templateParams = tags[templateParamsIdx].textContent;
|
|
310
|
+
delete tags[templateParamsIdx];
|
|
311
|
+
for (const tag of tags) {
|
|
312
|
+
if (tag) {
|
|
313
|
+
if (["titleTemplate", "title"].includes(tag.tag) && typeof tag.textContent === "string")
|
|
314
|
+
tag.textContent = processTemplateParams(tag.textContent, templateParams);
|
|
315
|
+
if (tag.tag === "meta" && typeof tag.props.content === "string")
|
|
316
|
+
tag.props.content = processTemplateParams(tag.props.content, templateParams);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
ctx.tags = tags.filter(Boolean);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
208
326
|
exports.activeHead = void 0;
|
|
209
327
|
const setActiveHead = (head) => exports.activeHead = head;
|
|
210
328
|
const getActiveHead = () => exports.activeHead;
|
|
@@ -218,6 +336,38 @@ function useHead(input, options = {}) {
|
|
|
218
336
|
return head.push(input, options);
|
|
219
337
|
}
|
|
220
338
|
}
|
|
339
|
+
|
|
340
|
+
function useHeadSafe(input, options = {}) {
|
|
341
|
+
return useHead(input, {
|
|
342
|
+
...options || {},
|
|
343
|
+
transform: whitelistSafeInput
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function useServerHead(input, options = {}) {
|
|
348
|
+
return useHead(input, { ...options, mode: "server" });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function useServerHeadSafe(input, options = {}) {
|
|
352
|
+
return useHeadSafe(input, { ...options, mode: "server" });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function useSeoMeta(input, options) {
|
|
356
|
+
const { title, titleTemplate, ...meta } = input;
|
|
357
|
+
return useHead({
|
|
358
|
+
title,
|
|
359
|
+
titleTemplate,
|
|
360
|
+
meta: unpackMeta(meta)
|
|
361
|
+
}, options);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function useServerSeoMeta(input, options) {
|
|
365
|
+
return useSeoMeta(input, {
|
|
366
|
+
...options || {},
|
|
367
|
+
mode: "server"
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
221
371
|
const useTagTitle = (title) => useHead({ title });
|
|
222
372
|
const useTagBase = (base) => useHead({ base });
|
|
223
373
|
const useTagMeta = (meta) => useHead({ meta: shared.asArray(meta) });
|
|
@@ -229,10 +379,6 @@ const useTagNoscript = (noscript) => useHead({ noscript: shared.asArray(noscript
|
|
|
229
379
|
const useHtmlAttrs = (attrs) => useHead({ htmlAttrs: attrs });
|
|
230
380
|
const useBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
|
|
231
381
|
const useTitleTemplate = (titleTemplate) => useHead({ titleTemplate });
|
|
232
|
-
|
|
233
|
-
function useServerHead(input, options = {}) {
|
|
234
|
-
return useHead(input, { ...options, mode: "server" });
|
|
235
|
-
}
|
|
236
382
|
const useServerTagTitle = (title) => useServerHead({ title });
|
|
237
383
|
const useServerTagBase = (base) => useServerHead({ base });
|
|
238
384
|
const useServerTagMeta = (meta) => useServerHead({ meta: shared.asArray(meta) });
|
|
@@ -245,111 +391,6 @@ const useServerHtmlAttrs = (attrs) => useServerHead({ htmlAttrs: attrs });
|
|
|
245
391
|
const useServerBodyAttrs = (attrs) => useServerHead({ bodyAttrs: attrs });
|
|
246
392
|
const useServerTitleTemplate = (titleTemplate) => useServerHead({ titleTemplate });
|
|
247
393
|
|
|
248
|
-
const useSeoMeta = (input) => {
|
|
249
|
-
const { title, titleTemplate, ...meta } = input;
|
|
250
|
-
return useHead({
|
|
251
|
-
title,
|
|
252
|
-
titleTemplate,
|
|
253
|
-
meta: unpackMeta(meta)
|
|
254
|
-
});
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
function asArray(input) {
|
|
258
|
-
return Array.isArray(input) ? input : [input];
|
|
259
|
-
}
|
|
260
|
-
const InternalKeySymbol = "_$key";
|
|
261
|
-
function packObject(input, options) {
|
|
262
|
-
const keys = Object.keys(input);
|
|
263
|
-
let [k, v] = keys;
|
|
264
|
-
options = options || {};
|
|
265
|
-
options.key = options.key || k;
|
|
266
|
-
options.value = options.value || v;
|
|
267
|
-
options.resolveKey = options.resolveKey || ((k2) => k2);
|
|
268
|
-
const resolveKey = (index) => {
|
|
269
|
-
const arr = asArray(options?.[index]);
|
|
270
|
-
return arr.find((k2) => {
|
|
271
|
-
if (typeof k2 === "string" && k2.includes(".")) {
|
|
272
|
-
return k2;
|
|
273
|
-
}
|
|
274
|
-
return k2 && keys.includes(k2);
|
|
275
|
-
});
|
|
276
|
-
};
|
|
277
|
-
const resolveValue = (k2, input2) => {
|
|
278
|
-
if (k2.includes(".")) {
|
|
279
|
-
const paths = k2.split(".");
|
|
280
|
-
let val = input2;
|
|
281
|
-
for (const path of paths)
|
|
282
|
-
val = val[path];
|
|
283
|
-
return val;
|
|
284
|
-
}
|
|
285
|
-
return input2[k2];
|
|
286
|
-
};
|
|
287
|
-
k = resolveKey("key") || k;
|
|
288
|
-
v = resolveKey("value") || v;
|
|
289
|
-
const dedupeKeyPrefix = input.key ? `${InternalKeySymbol}${input.key}-` : "";
|
|
290
|
-
let keyValue = resolveValue(k, input);
|
|
291
|
-
keyValue = options.resolveKey(keyValue);
|
|
292
|
-
return {
|
|
293
|
-
[`${dedupeKeyPrefix}${keyValue}`]: resolveValue(v, input)
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function packArray(input, options) {
|
|
298
|
-
const packed = {};
|
|
299
|
-
for (const i of input) {
|
|
300
|
-
const packedObj = packObject(i, options);
|
|
301
|
-
const pKey = Object.keys(packedObj)[0];
|
|
302
|
-
const isDedupeKey = pKey.startsWith(InternalKeySymbol);
|
|
303
|
-
if (!isDedupeKey && packed[pKey]) {
|
|
304
|
-
packed[pKey] = Array.isArray(packed[pKey]) ? packed[pKey] : [packed[pKey]];
|
|
305
|
-
packed[pKey].push(Object.values(packedObj)[0]);
|
|
306
|
-
} else {
|
|
307
|
-
packed[isDedupeKey ? pKey.split("-").slice(1).join("-") || pKey : pKey] = packedObj[pKey];
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return packed;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function unpackToArray(input, options) {
|
|
314
|
-
const unpacked = [];
|
|
315
|
-
const kFn = options.resolveKeyData || ((ctx) => ctx.key);
|
|
316
|
-
const vFn = options.resolveValueData || ((ctx) => ctx.value);
|
|
317
|
-
for (const [k, v] of Object.entries(input)) {
|
|
318
|
-
unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
|
|
319
|
-
const ctx = { key: k, value: i };
|
|
320
|
-
const val = vFn(ctx);
|
|
321
|
-
if (typeof val === "object")
|
|
322
|
-
return unpackToArray(val, options);
|
|
323
|
-
if (Array.isArray(val))
|
|
324
|
-
return val;
|
|
325
|
-
return {
|
|
326
|
-
[typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
|
|
327
|
-
[typeof options.value === "function" ? options.value(ctx) : options.value]: val
|
|
328
|
-
};
|
|
329
|
-
}).flat());
|
|
330
|
-
}
|
|
331
|
-
return unpacked;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function unpackToString(value, options) {
|
|
335
|
-
return Object.entries(value).map(([key, value2]) => {
|
|
336
|
-
if (typeof value2 === "object")
|
|
337
|
-
value2 = unpackToString(value2, options);
|
|
338
|
-
if (options.resolve) {
|
|
339
|
-
const resolved = options.resolve({ key, value: value2 });
|
|
340
|
-
if (resolved)
|
|
341
|
-
return resolved;
|
|
342
|
-
}
|
|
343
|
-
if (typeof value2 === "number")
|
|
344
|
-
value2 = value2.toString();
|
|
345
|
-
if (typeof value2 === "string" && options.wrapValue) {
|
|
346
|
-
value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
|
|
347
|
-
value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
|
|
348
|
-
}
|
|
349
|
-
return `${key}${options.keyValueSeparator || ""}${value2}`;
|
|
350
|
-
}).join(options.entrySeparator || "");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
394
|
const MetaPackingSchema = {
|
|
354
395
|
robots: {
|
|
355
396
|
unpack: {
|
|
@@ -405,7 +446,7 @@ function resolveMetaKeyType(key) {
|
|
|
405
446
|
|
|
406
447
|
function packMeta(inputs) {
|
|
407
448
|
const mappedPackingSchema = Object.entries(MetaPackingSchema).map(([key, value]) => [key, value.keyValue]);
|
|
408
|
-
return packArray(inputs, {
|
|
449
|
+
return packrup.packArray(inputs, {
|
|
409
450
|
key: ["name", "property", "httpEquiv", "http-equiv", "charset"],
|
|
410
451
|
value: ["content", "charset"],
|
|
411
452
|
resolveKey(k) {
|
|
@@ -428,7 +469,7 @@ function unpackMeta(input) {
|
|
|
428
469
|
(Array.isArray(val) ? val : [val]).forEach((entry) => {
|
|
429
470
|
if (!entry)
|
|
430
471
|
return;
|
|
431
|
-
const unpackedEntry = unpackToArray(entry, {
|
|
472
|
+
const unpackedEntry = packrup.unpackToArray(entry, {
|
|
432
473
|
key: "property",
|
|
433
474
|
value: "content",
|
|
434
475
|
resolveKeyData({ key: key2 }) {
|
|
@@ -445,7 +486,7 @@ function unpackMeta(input) {
|
|
|
445
486
|
delete input[inputKey];
|
|
446
487
|
}
|
|
447
488
|
});
|
|
448
|
-
const meta = unpackToArray(input, {
|
|
489
|
+
const meta = packrup.unpackToArray(input, {
|
|
449
490
|
key({ key }) {
|
|
450
491
|
return resolveMetaKeyType(key);
|
|
451
492
|
},
|
|
@@ -458,74 +499,98 @@ function unpackMeta(input) {
|
|
|
458
499
|
resolveValueData({ value, key }) {
|
|
459
500
|
if (value === null)
|
|
460
501
|
return "_null";
|
|
461
|
-
if (typeof value === "object")
|
|
462
|
-
|
|
463
|
-
if (key === "refresh")
|
|
464
|
-
return `${value.seconds};url=${value.url}`;
|
|
465
|
-
return unpackToString(
|
|
466
|
-
changeKeyCasingDeep(value),
|
|
467
|
-
{
|
|
468
|
-
entrySeparator: ", ",
|
|
469
|
-
keyValueSeparator: "=",
|
|
470
|
-
resolve({ value: value2, key: key2 }) {
|
|
471
|
-
if (value2 === null)
|
|
472
|
-
return "";
|
|
473
|
-
if (typeof value2 === "boolean")
|
|
474
|
-
return `${key2}`;
|
|
475
|
-
},
|
|
476
|
-
...definition?.unpack
|
|
477
|
-
}
|
|
478
|
-
);
|
|
479
|
-
}
|
|
502
|
+
if (typeof value === "object")
|
|
503
|
+
return resolvePackedMetaObjectValue(value, key);
|
|
480
504
|
return typeof value === "number" ? value.toString() : value;
|
|
481
505
|
}
|
|
482
506
|
});
|
|
483
507
|
return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
|
|
484
508
|
}
|
|
509
|
+
function resolvePackedMetaObjectValue(value, key) {
|
|
510
|
+
const definition = MetaPackingSchema[key];
|
|
511
|
+
if (key === "refresh")
|
|
512
|
+
return `${value.seconds};url=${value.url}`;
|
|
513
|
+
return packrup.unpackToString(
|
|
514
|
+
changeKeyCasingDeep(value),
|
|
515
|
+
{
|
|
516
|
+
entrySeparator: ", ",
|
|
517
|
+
keyValueSeparator: "=",
|
|
518
|
+
resolve({ value: value2, key: key2 }) {
|
|
519
|
+
if (value2 === null)
|
|
520
|
+
return "";
|
|
521
|
+
if (typeof value2 === "boolean")
|
|
522
|
+
return `${key2}`;
|
|
523
|
+
},
|
|
524
|
+
...definition?.unpack
|
|
525
|
+
}
|
|
526
|
+
);
|
|
527
|
+
}
|
|
485
528
|
|
|
486
529
|
async function normaliseTag(tagName, input) {
|
|
487
530
|
const tag = { tag: tagName, props: {} };
|
|
488
|
-
if (
|
|
489
|
-
tag.
|
|
531
|
+
if (["title", "titleTemplate", "templateParams"].includes(tagName)) {
|
|
532
|
+
tag.textContent = input instanceof Promise ? await input : input;
|
|
490
533
|
return tag;
|
|
491
534
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
535
|
+
if (typeof input === "string") {
|
|
536
|
+
if (!["script", "noscript", "style"].includes(tagName))
|
|
537
|
+
return false;
|
|
538
|
+
if (tagName === "script" && (/^(https?:)?\/\//.test(input) || input.startsWith("/"))) {
|
|
539
|
+
tag.props.src = input;
|
|
540
|
+
} else {
|
|
541
|
+
tag.innerHTML = input;
|
|
542
|
+
tag.key = shared.hashCode(input);
|
|
499
543
|
}
|
|
500
|
-
|
|
544
|
+
return tag;
|
|
545
|
+
}
|
|
546
|
+
tag.props = await normaliseProps(tagName, { ...input });
|
|
547
|
+
if (tag.props.children) {
|
|
548
|
+
tag.props.innerHTML = tag.props.children;
|
|
549
|
+
}
|
|
550
|
+
delete tag.props.children;
|
|
501
551
|
Object.keys(tag.props).filter((k) => shared.TagConfigKeys.includes(k)).forEach((k) => {
|
|
502
|
-
|
|
552
|
+
if (!["innerHTML", "textContent"].includes(k) || shared.TagsWithInnerContent.includes(tag.tag)) {
|
|
553
|
+
tag[k] = tag.props[k];
|
|
554
|
+
}
|
|
503
555
|
delete tag.props[k];
|
|
504
556
|
});
|
|
505
|
-
|
|
506
|
-
tag.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
557
|
+
["innerHTML", "textContent"].forEach((k) => {
|
|
558
|
+
if (tag.tag === "script" && tag[k] && ["application/ld+json", "application/json"].includes(tag.props.type)) {
|
|
559
|
+
try {
|
|
560
|
+
tag[k] = JSON.parse(tag[k]);
|
|
561
|
+
} catch (e) {
|
|
562
|
+
tag[k] = "";
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (typeof tag[k] === "object")
|
|
566
|
+
tag[k] = JSON.stringify(tag[k]);
|
|
567
|
+
});
|
|
568
|
+
if (tag.props.class)
|
|
569
|
+
tag.props.class = normaliseClassProp(tag.props.class);
|
|
570
|
+
if (tag.props.content && Array.isArray(tag.props.content))
|
|
571
|
+
return tag.props.content.map((v) => ({ ...tag, props: { ...tag.props, content: v } }));
|
|
518
572
|
return tag;
|
|
519
573
|
}
|
|
520
|
-
|
|
574
|
+
function normaliseClassProp(v) {
|
|
575
|
+
if (typeof v === "object" && !Array.isArray(v)) {
|
|
576
|
+
v = Object.keys(v).filter((k) => v[k]);
|
|
577
|
+
}
|
|
578
|
+
return (Array.isArray(v) ? v.join(" ") : v).split(" ").filter((c) => c.trim()).filter(Boolean).join(" ");
|
|
579
|
+
}
|
|
580
|
+
async function normaliseProps(tagName, props) {
|
|
521
581
|
for (const k of Object.keys(props)) {
|
|
582
|
+
const isDataKey = k.startsWith("data-");
|
|
522
583
|
if (props[k] instanceof Promise) {
|
|
523
584
|
props[k] = await props[k];
|
|
524
585
|
}
|
|
525
586
|
if (String(props[k]) === "true") {
|
|
526
|
-
props[k] = "";
|
|
587
|
+
props[k] = isDataKey ? "true" : "";
|
|
527
588
|
} else if (String(props[k]) === "false") {
|
|
528
|
-
|
|
589
|
+
if (isDataKey) {
|
|
590
|
+
props[k] = "false";
|
|
591
|
+
} else {
|
|
592
|
+
delete props[k];
|
|
593
|
+
}
|
|
529
594
|
}
|
|
530
595
|
}
|
|
531
596
|
return props;
|
|
@@ -533,11 +598,11 @@ async function normaliseProps(props) {
|
|
|
533
598
|
const TagEntityBits = 10;
|
|
534
599
|
async function normaliseEntryTags(e) {
|
|
535
600
|
const tagPromises = [];
|
|
536
|
-
Object.entries(e.resolvedInput
|
|
601
|
+
Object.entries(e.resolvedInput).filter(([k, v]) => typeof v !== "undefined" && shared.ValidHeadTags.includes(k)).forEach(([k, value]) => {
|
|
537
602
|
const v = shared.asArray(value);
|
|
538
603
|
tagPromises.push(...v.map((props) => normaliseTag(k, props)).flat());
|
|
539
604
|
});
|
|
540
|
-
return (await Promise.all(tagPromises)).flat().map((t, i) => {
|
|
605
|
+
return (await Promise.all(tagPromises)).flat().filter(Boolean).map((t, i) => {
|
|
541
606
|
t._e = e._i;
|
|
542
607
|
t._p = (e._i << TagEntityBits) + i;
|
|
543
608
|
return t;
|
|
@@ -565,14 +630,119 @@ function changeKeyCasingDeep(input) {
|
|
|
565
630
|
return output;
|
|
566
631
|
}
|
|
567
632
|
|
|
633
|
+
const WhitelistAttributes = {
|
|
634
|
+
htmlAttrs: ["id", "class", "lang", "dir"],
|
|
635
|
+
bodyAttrs: ["id", "class"],
|
|
636
|
+
meta: ["id", "name", "property", "charset", "content"],
|
|
637
|
+
noscript: ["id", "textContent"],
|
|
638
|
+
script: ["id", "type", "textContent"],
|
|
639
|
+
link: ["id", "color", "crossorigin", "fetchpriority", "href", "hreflang", "imagesrcset", "imagesizes", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"]
|
|
640
|
+
};
|
|
641
|
+
function whitelistSafeInput(input) {
|
|
642
|
+
const filtered = {};
|
|
643
|
+
Object.keys(input).forEach((key) => {
|
|
644
|
+
const tagValue = input[key];
|
|
645
|
+
if (!tagValue)
|
|
646
|
+
return;
|
|
647
|
+
switch (key) {
|
|
648
|
+
case "title":
|
|
649
|
+
case "titleTemplate":
|
|
650
|
+
case "templateParams":
|
|
651
|
+
filtered[key] = tagValue;
|
|
652
|
+
break;
|
|
653
|
+
case "htmlAttrs":
|
|
654
|
+
case "bodyAttrs":
|
|
655
|
+
filtered[key] = {};
|
|
656
|
+
WhitelistAttributes[key].forEach((a) => {
|
|
657
|
+
if (tagValue[a])
|
|
658
|
+
filtered[key][a] = tagValue[a];
|
|
659
|
+
});
|
|
660
|
+
Object.keys(tagValue || {}).filter((a) => a.startsWith("data-")).forEach((a) => {
|
|
661
|
+
filtered[key][a] = tagValue[a];
|
|
662
|
+
});
|
|
663
|
+
break;
|
|
664
|
+
case "meta":
|
|
665
|
+
if (Array.isArray(tagValue)) {
|
|
666
|
+
filtered[key] = tagValue.map((meta) => {
|
|
667
|
+
const safeMeta = {};
|
|
668
|
+
WhitelistAttributes.meta.forEach((key2) => {
|
|
669
|
+
if (meta[key2] || key2.startsWith("data-"))
|
|
670
|
+
safeMeta[key2] = meta[key2];
|
|
671
|
+
});
|
|
672
|
+
return safeMeta;
|
|
673
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
674
|
+
}
|
|
675
|
+
break;
|
|
676
|
+
case "link":
|
|
677
|
+
if (Array.isArray(tagValue)) {
|
|
678
|
+
filtered[key] = tagValue.map((meta) => {
|
|
679
|
+
const link = {};
|
|
680
|
+
WhitelistAttributes.link.forEach((key2) => {
|
|
681
|
+
if (key2 === "rel" && ["stylesheet", "canonical", "modulepreload", "prerender", "preload", "prefetch"].includes(meta[key2]))
|
|
682
|
+
return;
|
|
683
|
+
if (key2 === "href") {
|
|
684
|
+
try {
|
|
685
|
+
const url = new URL(meta[key2]);
|
|
686
|
+
if (["javascript:", "data:"].includes(url.protocol))
|
|
687
|
+
return;
|
|
688
|
+
link[key2] = meta[key2];
|
|
689
|
+
} catch (e) {
|
|
690
|
+
}
|
|
691
|
+
} else if (meta[key2] || key2.startsWith("data-")) {
|
|
692
|
+
link[key2] = meta[key2];
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
return link;
|
|
696
|
+
}).filter((link) => Object.keys(link).length > 1 && !!link.rel);
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
case "noscript":
|
|
700
|
+
if (Array.isArray(tagValue)) {
|
|
701
|
+
filtered[key] = tagValue.map((meta) => {
|
|
702
|
+
const noscript = {};
|
|
703
|
+
WhitelistAttributes.noscript.forEach((key2) => {
|
|
704
|
+
if (meta[key2] || key2.startsWith("data-"))
|
|
705
|
+
noscript[key2] = meta[key2];
|
|
706
|
+
});
|
|
707
|
+
return noscript;
|
|
708
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
case "script":
|
|
712
|
+
if (Array.isArray(tagValue)) {
|
|
713
|
+
filtered[key] = tagValue.map((script) => {
|
|
714
|
+
const safeScript = {};
|
|
715
|
+
WhitelistAttributes.script.forEach((s) => {
|
|
716
|
+
if (script[s] || s.startsWith("data-")) {
|
|
717
|
+
if (s === "textContent") {
|
|
718
|
+
try {
|
|
719
|
+
const jsonVal = typeof script[s] === "string" ? JSON.parse(script[s]) : script[s];
|
|
720
|
+
safeScript[s] = JSON.stringify(jsonVal, null, 0);
|
|
721
|
+
} catch (e) {
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
safeScript[s] = script[s];
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
return safeScript;
|
|
729
|
+
}).filter((meta) => Object.keys(meta).length > 0);
|
|
730
|
+
}
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
return filtered;
|
|
735
|
+
}
|
|
736
|
+
|
|
568
737
|
const CorePlugins = () => [
|
|
569
738
|
// dedupe needs to come first
|
|
570
|
-
|
|
739
|
+
DedupesTagsPlugin(),
|
|
571
740
|
SortTagsPlugin(),
|
|
572
741
|
TitleTemplatePlugin(),
|
|
573
742
|
ProvideTagHashPlugin(),
|
|
574
743
|
EventHandlersPlugin(),
|
|
575
|
-
DeprecatedTagAttrPlugin()
|
|
744
|
+
DeprecatedTagAttrPlugin(),
|
|
745
|
+
TemplateParamsPlugin()
|
|
576
746
|
];
|
|
577
747
|
const DOMPlugins = (options = {}) => [
|
|
578
748
|
dom.PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
|
|
@@ -582,6 +752,8 @@ function createHead(options = {}) {
|
|
|
582
752
|
...options,
|
|
583
753
|
plugins: [...DOMPlugins(options), ...options?.plugins || []]
|
|
584
754
|
});
|
|
755
|
+
if (options.experimentalHashHydration && head.resolvedOptions.document)
|
|
756
|
+
head._hash = dom.maybeGetSSRHash(head.resolvedOptions.document);
|
|
585
757
|
setActiveHead(head);
|
|
586
758
|
return head;
|
|
587
759
|
}
|
|
@@ -597,6 +769,7 @@ function createHeadCore(options = {}) {
|
|
|
597
769
|
...options?.plugins || []
|
|
598
770
|
];
|
|
599
771
|
options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
|
|
772
|
+
options.document = options.document || (IsBrowser ? document : void 0);
|
|
600
773
|
const updated = () => hooks.callHook("entries:updated", head);
|
|
601
774
|
const head = {
|
|
602
775
|
resolvedOptions: options,
|
|
@@ -618,6 +791,9 @@ function createHeadCore(options = {}) {
|
|
|
618
791
|
};
|
|
619
792
|
if (options2?.mode)
|
|
620
793
|
activeEntry._m = options2?.mode;
|
|
794
|
+
if (options2?.transform) {
|
|
795
|
+
activeEntry._t = options2?.transform;
|
|
796
|
+
}
|
|
621
797
|
entries.push(activeEntry);
|
|
622
798
|
updated();
|
|
623
799
|
return {
|
|
@@ -647,8 +823,10 @@ function createHeadCore(options = {}) {
|
|
|
647
823
|
const resolveCtx = { tags: [], entries: [...entries] };
|
|
648
824
|
await hooks.callHook("entries:resolve", resolveCtx);
|
|
649
825
|
for (const entry of resolveCtx.entries) {
|
|
826
|
+
const transformer = entry._t || ((i) => i);
|
|
827
|
+
entry.resolvedInput = transformer(entry.resolvedInput || entry.input);
|
|
650
828
|
for (const tag of await normaliseEntryTags(entry)) {
|
|
651
|
-
const tagCtx = { tag, entry };
|
|
829
|
+
const tagCtx = { tag, entry, resolvedOptions: head.resolvedOptions };
|
|
652
830
|
await hooks.callHook("tag:normalise", tagCtx);
|
|
653
831
|
resolveCtx.tags.push(tagCtx.tag);
|
|
654
832
|
}
|
|
@@ -656,12 +834,12 @@ function createHeadCore(options = {}) {
|
|
|
656
834
|
await hooks.callHook("tags:resolve", resolveCtx);
|
|
657
835
|
return resolveCtx.tags;
|
|
658
836
|
},
|
|
659
|
-
_elMap: {},
|
|
660
837
|
_popSideEffectQueue() {
|
|
661
838
|
const sde = { ..._sde };
|
|
662
839
|
_sde = {};
|
|
663
840
|
return sde;
|
|
664
|
-
}
|
|
841
|
+
},
|
|
842
|
+
_elMap: {}
|
|
665
843
|
};
|
|
666
844
|
head.hooks.callHook("init", head);
|
|
667
845
|
return head;
|
|
@@ -672,12 +850,16 @@ const coreComposableNames = [
|
|
|
672
850
|
];
|
|
673
851
|
const composableNames = [
|
|
674
852
|
"useHead",
|
|
853
|
+
"useSeoMeta",
|
|
854
|
+
"useHeadSafe",
|
|
855
|
+
"useServerHead",
|
|
856
|
+
"useServerSeoMeta",
|
|
857
|
+
"useServerHeadSafe",
|
|
858
|
+
// deprecated
|
|
675
859
|
"useTagTitle",
|
|
676
860
|
"useTagBase",
|
|
677
861
|
"useTagMeta",
|
|
678
862
|
"useTagMetaFlat",
|
|
679
|
-
// alias
|
|
680
|
-
"useSeoMeta",
|
|
681
863
|
"useTagLink",
|
|
682
864
|
"useTagScript",
|
|
683
865
|
"useTagStyle",
|
|
@@ -685,8 +867,6 @@ const composableNames = [
|
|
|
685
867
|
"useHtmlAttrs",
|
|
686
868
|
"useBodyAttrs",
|
|
687
869
|
"useTitleTemplate",
|
|
688
|
-
// server only composables
|
|
689
|
-
"useServerHead",
|
|
690
870
|
"useServerTagTitle",
|
|
691
871
|
"useServerTagBase",
|
|
692
872
|
"useServerTagMeta",
|
|
@@ -709,14 +889,17 @@ const unheadComposablesImports = [
|
|
|
709
889
|
exports.ColonPrefixKeys = ColonPrefixKeys;
|
|
710
890
|
exports.CorePlugins = CorePlugins;
|
|
711
891
|
exports.DOMPlugins = DOMPlugins;
|
|
892
|
+
exports.DedupesTagsPlugin = DedupesTagsPlugin;
|
|
712
893
|
exports.DeprecatedTagAttrPlugin = DeprecatedTagAttrPlugin;
|
|
713
894
|
exports.EventHandlersPlugin = EventHandlersPlugin;
|
|
895
|
+
exports.MetaPackingSchema = MetaPackingSchema;
|
|
714
896
|
exports.PropertyPrefixKeys = PropertyPrefixKeys;
|
|
715
897
|
exports.ProvideTagHashPlugin = ProvideTagHashPlugin;
|
|
716
898
|
exports.SortModifiers = SortModifiers;
|
|
717
899
|
exports.SortTagsPlugin = SortTagsPlugin;
|
|
718
900
|
exports.TAG_WEIGHTS = TAG_WEIGHTS;
|
|
719
901
|
exports.TagEntityBits = TagEntityBits;
|
|
902
|
+
exports.TemplateParamsPlugin = TemplateParamsPlugin;
|
|
720
903
|
exports.TitleTemplatePlugin = TitleTemplatePlugin;
|
|
721
904
|
exports.changeKeyCasingDeep = changeKeyCasingDeep;
|
|
722
905
|
exports.composableNames = composableNames;
|
|
@@ -724,22 +907,28 @@ exports.createHead = createHead;
|
|
|
724
907
|
exports.createHeadCore = createHeadCore;
|
|
725
908
|
exports.fixKeyCase = fixKeyCase;
|
|
726
909
|
exports.getActiveHead = getActiveHead;
|
|
910
|
+
exports.normaliseClassProp = normaliseClassProp;
|
|
727
911
|
exports.normaliseEntryTags = normaliseEntryTags;
|
|
728
912
|
exports.normaliseProps = normaliseProps;
|
|
729
913
|
exports.normaliseTag = normaliseTag;
|
|
730
914
|
exports.packMeta = packMeta;
|
|
731
915
|
exports.renderTitleTemplate = renderTitleTemplate;
|
|
916
|
+
exports.resolveMetaKeyType = resolveMetaKeyType;
|
|
917
|
+
exports.resolvePackedMetaObjectValue = resolvePackedMetaObjectValue;
|
|
732
918
|
exports.setActiveHead = setActiveHead;
|
|
733
919
|
exports.tagWeight = tagWeight;
|
|
734
920
|
exports.unheadComposablesImports = unheadComposablesImports;
|
|
735
921
|
exports.unpackMeta = unpackMeta;
|
|
736
922
|
exports.useBodyAttrs = useBodyAttrs;
|
|
737
923
|
exports.useHead = useHead;
|
|
924
|
+
exports.useHeadSafe = useHeadSafe;
|
|
738
925
|
exports.useHtmlAttrs = useHtmlAttrs;
|
|
739
926
|
exports.useSeoMeta = useSeoMeta;
|
|
740
927
|
exports.useServerBodyAttrs = useServerBodyAttrs;
|
|
741
928
|
exports.useServerHead = useServerHead;
|
|
929
|
+
exports.useServerHeadSafe = useServerHeadSafe;
|
|
742
930
|
exports.useServerHtmlAttrs = useServerHtmlAttrs;
|
|
931
|
+
exports.useServerSeoMeta = useServerSeoMeta;
|
|
743
932
|
exports.useServerTagBase = useServerTagBase;
|
|
744
933
|
exports.useServerTagLink = useServerTagLink;
|
|
745
934
|
exports.useServerTagMeta = useServerTagMeta;
|
|
@@ -758,6 +947,7 @@ exports.useTagScript = useTagScript;
|
|
|
758
947
|
exports.useTagStyle = useTagStyle;
|
|
759
948
|
exports.useTagTitle = useTagTitle;
|
|
760
949
|
exports.useTitleTemplate = useTitleTemplate;
|
|
950
|
+
exports.whitelistSafeInput = whitelistSafeInput;
|
|
761
951
|
Object.keys(shared).forEach(function (k) {
|
|
762
952
|
if (k !== 'default' && !exports.hasOwnProperty(k)) exports[k] = shared[k];
|
|
763
953
|
});
|