@unhead/vue 1.0.21 → 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.mjs CHANGED
@@ -1,738 +1,7 @@
1
- import { createHooks } from 'hookable';
1
+ import { getActiveHead, createHead as createHead$1, whitelistSafeInput, unpackMeta, composableNames } from 'unhead';
2
+ export { createHeadCore } from 'unhead';
2
3
  import { unref, isRef, version, getCurrentInstance, inject, nextTick, ref, watchEffect, watch, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
3
-
4
- const TagsWithInnerContent = ["script", "style", "noscript"];
5
- const HasElementTags$1 = [
6
- "base",
7
- "meta",
8
- "link",
9
- "style",
10
- "script",
11
- "noscript"
12
- ];
13
-
14
- const UniqueTags$1 = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
15
- function tagDedupeKey$1(tag, fn) {
16
- const { props, tag: tagName } = tag;
17
- if (UniqueTags$1.includes(tagName))
18
- return tagName;
19
- if (tagName === "link" && props.rel === "canonical")
20
- return "canonical";
21
- if (props.charset)
22
- return "charset";
23
- const name = ["id"];
24
- if (tagName === "meta")
25
- name.push(...["name", "property", "http-equiv"]);
26
- for (const n of name) {
27
- if (typeof props[n] !== "undefined") {
28
- const val = String(props[n]);
29
- if (fn && !fn(val))
30
- return false;
31
- return `${tagName}:${n}:${val}`;
32
- }
33
- }
34
- return false;
35
- }
36
-
37
- const setAttrs = (ctx, markSideEffect) => {
38
- const { tag, $el } = ctx;
39
- if (!$el)
40
- return;
41
- Object.entries(tag.props).forEach(([k, value]) => {
42
- value = String(value);
43
- const attrSdeKey = `attr:${k}`;
44
- if (k === "class") {
45
- if (!value)
46
- return;
47
- for (const c of value.split(" ")) {
48
- const classSdeKey = `${attrSdeKey}:${c}`;
49
- if (markSideEffect)
50
- markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
51
- if (!$el.classList.contains(c))
52
- $el.classList.add(c);
53
- }
54
- return;
55
- }
56
- if (markSideEffect && !k.startsWith("data-h-"))
57
- markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
58
- if ($el.getAttribute(k) !== value)
59
- $el.setAttribute(k, value);
60
- });
61
- if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
62
- $el.innerHTML = tag.children || "";
63
- };
64
-
65
- function hashCode(s) {
66
- let h = 9;
67
- for (let i = 0; i < s.length; )
68
- h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
69
- return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
70
- }
71
-
72
- async function renderDOMHead(head, options = {}) {
73
- const ctx = { shouldRender: true };
74
- await head.hooks.callHook("dom:beforeRender", ctx);
75
- if (!ctx.shouldRender)
76
- return;
77
- const dom = options.document || window.document;
78
- const staleSideEffects = head._popSideEffectQueue();
79
- head.headEntries().map((entry) => entry._sde).forEach((sde) => {
80
- Object.entries(sde).forEach(([key, fn]) => {
81
- staleSideEffects[key] = fn;
82
- });
83
- });
84
- const preRenderTag = async (tag) => {
85
- const entry = head.headEntries().find((e) => e._i === tag._e);
86
- const renderCtx = {
87
- renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
88
- $el: null,
89
- shouldRender: true,
90
- tag,
91
- entry,
92
- staleSideEffects
93
- };
94
- await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
95
- return renderCtx;
96
- };
97
- const renders = [];
98
- const pendingRenders = {
99
- body: [],
100
- head: []
101
- };
102
- const markSideEffect = (ctx2, key, fn) => {
103
- key = `${ctx2.renderId}:${key}`;
104
- if (ctx2.entry)
105
- ctx2.entry._sde[key] = fn;
106
- delete staleSideEffects[key];
107
- };
108
- const markEl = (ctx2) => {
109
- head._elMap[ctx2.renderId] = ctx2.$el;
110
- renders.push(ctx2);
111
- markSideEffect(ctx2, "el", () => {
112
- ctx2.$el?.remove();
113
- delete head._elMap[ctx2.renderId];
114
- });
115
- };
116
- for (const t of await head.resolveTags()) {
117
- const ctx2 = await preRenderTag(t);
118
- if (!ctx2.shouldRender)
119
- continue;
120
- const { tag } = ctx2;
121
- if (tag.tag === "title") {
122
- dom.title = tag.children || "";
123
- renders.push(ctx2);
124
- continue;
125
- }
126
- if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
127
- ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
128
- setAttrs(ctx2, markSideEffect);
129
- renders.push(ctx2);
130
- continue;
131
- }
132
- ctx2.$el = head._elMap[ctx2.renderId];
133
- if (!ctx2.$el && tag._hash) {
134
- ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
135
- }
136
- if (ctx2.$el) {
137
- if (ctx2.tag._d)
138
- setAttrs(ctx2);
139
- markEl(ctx2);
140
- continue;
141
- }
142
- ctx2.$el = dom.createElement(tag.tag);
143
- setAttrs(ctx2);
144
- pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
145
- }
146
- Object.entries(pendingRenders).forEach(([pos, queue]) => {
147
- if (!queue.length)
148
- return;
149
- const children = dom?.[pos]?.children;
150
- if (!children)
151
- return;
152
- for (const $el of [...children].reverse()) {
153
- const elTag = $el.tagName.toLowerCase();
154
- if (!HasElementTags$1.includes(elTag))
155
- continue;
156
- const dedupeKey = tagDedupeKey$1({
157
- tag: elTag,
158
- // convert attributes to object
159
- props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
160
- });
161
- const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode?.(ctx2.$el)));
162
- if (matchIdx !== -1) {
163
- const ctx2 = queue[matchIdx];
164
- ctx2.$el = $el;
165
- setAttrs(ctx2);
166
- markEl(ctx2);
167
- delete queue[matchIdx];
168
- }
169
- }
170
- queue.forEach((ctx2) => {
171
- if (!ctx2.$el)
172
- return;
173
- switch (ctx2.tag.tagPosition) {
174
- case "bodyClose":
175
- dom.body.appendChild(ctx2.$el);
176
- break;
177
- case "bodyOpen":
178
- dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
179
- break;
180
- case "head":
181
- default:
182
- dom.head.appendChild(ctx2.$el);
183
- break;
184
- }
185
- markEl(ctx2);
186
- });
187
- });
188
- for (const ctx2 of renders)
189
- await head.hooks.callHook("dom:renderTag", ctx2);
190
- Object.values(staleSideEffects).forEach((fn) => fn());
191
- }
192
- let domUpdatePromise = null;
193
- async function debouncedRenderDOMHead(head, options = {}) {
194
- function doDomUpdate() {
195
- domUpdatePromise = null;
196
- return renderDOMHead(head, options);
197
- }
198
- const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
199
- return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
200
- }
201
-
202
- const index = {
203
- __proto__: null,
204
- debouncedRenderDOMHead: debouncedRenderDOMHead,
205
- get domUpdatePromise () { return domUpdatePromise; },
206
- hashCode: hashCode,
207
- renderDOMHead: renderDOMHead
208
- };
209
-
210
- const ValidHeadTags = [
211
- "title",
212
- "titleTemplate",
213
- "base",
214
- "htmlAttrs",
215
- "bodyAttrs",
216
- "meta",
217
- "link",
218
- "style",
219
- "script",
220
- "noscript"
221
- ];
222
- const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy"];
223
-
224
- async function normaliseTag(tagName, input) {
225
- const tag = { tag: tagName, props: {} };
226
- if (tagName === "title" || tagName === "titleTemplate") {
227
- tag.children = input instanceof Promise ? await input : input;
228
- return tag;
229
- }
230
- tag.props = await normaliseProps({ ...input });
231
- ["children", "innerHtml", "innerHTML"].forEach((key) => {
232
- if (typeof tag.props[key] !== "undefined") {
233
- tag.children = tag.props[key];
234
- if (typeof tag.children === "object")
235
- tag.children = JSON.stringify(tag.children);
236
- delete tag.props[key];
237
- }
238
- });
239
- Object.keys(tag.props).filter((k) => TagConfigKeys.includes(k)).forEach((k) => {
240
- tag[k] = tag.props[k];
241
- delete tag.props[k];
242
- });
243
- if (typeof tag.props.class === "object" && !Array.isArray(tag.props.class)) {
244
- tag.props.class = Object.keys(tag.props.class).filter((k) => tag.props.class[k]);
245
- }
246
- if (Array.isArray(tag.props.class))
247
- tag.props.class = tag.props.class.join(" ");
248
- if (tag.props.content && Array.isArray(tag.props.content)) {
249
- return tag.props.content.map((v, i) => {
250
- const newTag = { ...tag, props: { ...tag.props } };
251
- newTag.props.content = v;
252
- newTag.key = `${tag.props.name || tag.props.property}:${i}`;
253
- return newTag;
254
- });
255
- }
256
- return tag;
257
- }
258
- async function normaliseProps(props) {
259
- for (const k of Object.keys(props)) {
260
- if (props[k] instanceof Promise) {
261
- props[k] = await props[k];
262
- }
263
- if (String(props[k]) === "true") {
264
- props[k] = "";
265
- } else if (String(props[k]) === "false") {
266
- delete props[k];
267
- }
268
- }
269
- return props;
270
- }
271
-
272
- const tagWeight = (tag) => {
273
- if (typeof tag.tagPriority === "number")
274
- return tag.tagPriority;
275
- switch (tag.tagPriority) {
276
- case "critical":
277
- return 2;
278
- case "high":
279
- return 9;
280
- case "low":
281
- return 12;
282
- }
283
- switch (tag.tag) {
284
- case "base":
285
- return -1;
286
- case "title":
287
- return 1;
288
- case "meta":
289
- if (tag.props.charset)
290
- return -2;
291
- if (tag.props["http-equiv"] === "content-security-policy")
292
- return 0;
293
- return 10;
294
- default:
295
- return 10;
296
- }
297
- };
298
- const sortTags = (aTag, bTag) => {
299
- return tagWeight(aTag) - tagWeight(bTag);
300
- };
301
-
302
- const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
303
- function tagDedupeKey(tag, fn) {
304
- const { props, tag: tagName } = tag;
305
- if (UniqueTags.includes(tagName))
306
- return tagName;
307
- if (tagName === "link" && props.rel === "canonical")
308
- return "canonical";
309
- if (props.charset)
310
- return "charset";
311
- const name = ["id"];
312
- if (tagName === "meta")
313
- name.push(...["name", "property", "http-equiv"]);
314
- for (const n of name) {
315
- if (typeof props[n] !== "undefined") {
316
- const val = String(props[n]);
317
- if (fn && !fn(val))
318
- return false;
319
- return `${tagName}:${n}:${val}`;
320
- }
321
- }
322
- return false;
323
- }
324
-
325
- const renderTitleTemplate = (template, title) => {
326
- if (template == null)
327
- return title || null;
328
- if (typeof template === "function")
329
- return template(title);
330
- return template.replace("%s", title ?? "");
331
- };
332
- function resolveTitleTemplateFromTags(tags) {
333
- let titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
334
- const titleIdx = tags.findIndex((i) => i.tag === "title");
335
- if (titleIdx !== -1 && titleTemplateIdx !== -1) {
336
- const newTitle = renderTitleTemplate(
337
- tags[titleTemplateIdx].children,
338
- tags[titleIdx].children
339
- );
340
- if (newTitle !== null) {
341
- tags[titleIdx].children = newTitle || tags[titleIdx].children;
342
- } else {
343
- delete tags[titleIdx];
344
- }
345
- } else if (titleTemplateIdx !== -1) {
346
- const newTitle = renderTitleTemplate(
347
- tags[titleTemplateIdx].children
348
- );
349
- if (newTitle !== null) {
350
- tags[titleTemplateIdx].children = newTitle;
351
- tags[titleTemplateIdx].tag = "title";
352
- titleTemplateIdx = -1;
353
- }
354
- }
355
- if (titleTemplateIdx !== -1) {
356
- delete tags[titleTemplateIdx];
357
- }
358
- return tags.filter(Boolean);
359
- }
360
-
361
- const DedupesTagsPlugin = (options) => {
362
- options = options || {};
363
- const dedupeKeys = options.dedupeKeys || ["hid", "vmid", "key"];
364
- return defineHeadPlugin({
365
- hooks: {
366
- "tag:normalise": function({ tag }) {
367
- dedupeKeys.forEach((key) => {
368
- if (tag.props[key]) {
369
- tag.key = tag.props[key];
370
- delete tag.props[key];
371
- }
372
- });
373
- const dedupe = tag.key ? `${tag.tag}:${tag.key}` : tagDedupeKey(tag);
374
- if (dedupe)
375
- tag._d = dedupe;
376
- },
377
- "tags:resolve": function(ctx) {
378
- const deduping = {};
379
- ctx.tags.forEach((tag) => {
380
- let dedupeKey = tag._d || tag._p;
381
- const dupedTag = deduping[dedupeKey];
382
- if (dupedTag) {
383
- let strategy = tag?.tagDuplicateStrategy;
384
- if (!strategy && (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs"))
385
- strategy = "merge";
386
- if (strategy === "merge") {
387
- const oldProps = dupedTag.props;
388
- ["class", "style"].forEach((key) => {
389
- if (tag.props[key] && oldProps[key]) {
390
- if (key === "style" && !oldProps[key].endsWith(";"))
391
- oldProps[key] += ";";
392
- tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
393
- }
394
- });
395
- deduping[dedupeKey].props = {
396
- ...oldProps,
397
- ...tag.props
398
- };
399
- return;
400
- } else if (tag._e === dupedTag._e) {
401
- dedupeKey = tag._d = `${dedupeKey}:${tag._p}`;
402
- }
403
- const propCount = Object.keys(tag.props).length;
404
- if ((propCount === 0 || propCount === 1 && typeof tag.props["data-h-key"] !== "undefined") && !tag.children) {
405
- delete deduping[dedupeKey];
406
- return;
407
- }
408
- }
409
- deduping[dedupeKey] = tag;
410
- });
411
- ctx.tags = Object.values(deduping);
412
- }
413
- }
414
- });
415
- };
416
-
417
- const SortTagsPlugin = () => {
418
- return defineHeadPlugin({
419
- hooks: {
420
- "tags:resolve": (ctx) => {
421
- const tagIndexForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
422
- for (const tag of ctx.tags) {
423
- if (!tag.tagPriority || typeof tag.tagPriority === "number")
424
- continue;
425
- const modifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
426
- for (const { prefix, offset } of modifiers) {
427
- if (tag.tagPriority.startsWith(prefix)) {
428
- const key = tag.tagPriority.replace(prefix, "");
429
- const index = tagIndexForKey(key);
430
- if (typeof index !== "undefined")
431
- tag._p = index + offset;
432
- }
433
- }
434
- }
435
- ctx.tags.sort((a, b) => a._p - b._p).sort(sortTags);
436
- }
437
- }
438
- });
439
- };
440
-
441
- const TitleTemplatePlugin = () => {
442
- return defineHeadPlugin({
443
- hooks: {
444
- "tags:resolve": (ctx) => {
445
- ctx.tags = resolveTitleTemplateFromTags(ctx.tags);
446
- }
447
- }
448
- });
449
- };
450
-
451
- const DeprecatedTagAttrPlugin = () => {
452
- return defineHeadPlugin({
453
- hooks: {
454
- "tag:normalise": function({ tag }) {
455
- if (typeof tag.props.body !== "undefined") {
456
- tag.tagPosition = "bodyClose";
457
- delete tag.props.body;
458
- }
459
- }
460
- }
461
- });
462
- };
463
-
464
- const IsBrowser$1 = typeof window !== "undefined";
465
-
466
- const ProvideTagHashPlugin = () => {
467
- return defineHeadPlugin({
468
- hooks: {
469
- "tag:normalise": (ctx) => {
470
- const { tag, entry } = ctx;
471
- const isDynamic = typeof tag.props._dynamic !== "undefined";
472
- if (!HasElementTags.includes(tag.tag) || !tag.key)
473
- return;
474
- tag._hash = hashCode(JSON.stringify({ tag: tag.tag, key: tag.key }));
475
- if (IsBrowser$1 || getActiveHead()?.resolvedOptions?.document)
476
- return;
477
- if (entry._m === "server" || isDynamic) {
478
- tag.props[`data-h-${tag._hash}`] = "";
479
- }
480
- },
481
- "tags:resolve": (ctx) => {
482
- ctx.tags = ctx.tags.map((t) => {
483
- delete t.props._dynamic;
484
- return t;
485
- });
486
- }
487
- }
488
- });
489
- };
490
-
491
- const PatchDomOnEntryUpdatesPlugin = (options) => {
492
- return defineHeadPlugin({
493
- hooks: {
494
- "entries:updated": function(head) {
495
- if (typeof options?.document === "undefined" && typeof window === "undefined")
496
- return;
497
- let delayFn = options?.delayFn;
498
- if (!delayFn && typeof requestAnimationFrame !== "undefined")
499
- delayFn = requestAnimationFrame;
500
- Promise.resolve().then(function () { return index; }).then(({ debouncedRenderDOMHead }) => {
501
- debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
502
- });
503
- }
504
- }
505
- });
506
- };
507
-
508
- const EventHandlersPlugin = () => {
509
- const stripEventHandlers = (mode, tag) => {
510
- const props = {};
511
- const eventHandlers = {};
512
- Object.entries(tag.props).forEach(([key, value]) => {
513
- if (key.startsWith("on") && typeof value === "function")
514
- eventHandlers[key] = value;
515
- else
516
- props[key] = value;
517
- });
518
- let delayedSrc;
519
- if (mode === "dom" && tag.tag === "script" && typeof props.src === "string" && typeof eventHandlers.onload !== "undefined") {
520
- delayedSrc = props.src;
521
- delete props.src;
522
- }
523
- return { props, eventHandlers, delayedSrc };
524
- };
525
- return defineHeadPlugin({
526
- hooks: {
527
- "ssr:render": function(ctx) {
528
- ctx.tags = ctx.tags.map((tag) => {
529
- tag.props = stripEventHandlers("ssr", tag).props;
530
- return tag;
531
- });
532
- },
533
- "dom:beforeRenderTag": function(ctx) {
534
- const { props, eventHandlers, delayedSrc } = stripEventHandlers("dom", ctx.tag);
535
- if (!Object.keys(eventHandlers).length)
536
- return;
537
- ctx.tag.props = props;
538
- ctx.tag._eventHandlers = eventHandlers;
539
- ctx.tag._delayedSrc = delayedSrc;
540
- },
541
- "dom:renderTag": function(ctx) {
542
- const $el = ctx.$el;
543
- if (!ctx.tag._eventHandlers || !$el)
544
- return;
545
- const $eventListenerTarget = ctx.tag.tag === "bodyAttrs" && typeof window !== "undefined" ? window : $el;
546
- Object.entries(ctx.tag._eventHandlers).forEach(([k, value]) => {
547
- const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
548
- const eventName = k.slice(2).toLowerCase();
549
- const eventDedupeKey = `data-h-${eventName}`;
550
- delete ctx.staleSideEffects[sdeKey];
551
- if ($el.hasAttribute(eventDedupeKey))
552
- return;
553
- const handler = value;
554
- $el.setAttribute(eventDedupeKey, "");
555
- $eventListenerTarget.addEventListener(eventName, handler);
556
- if (ctx.entry) {
557
- ctx.entry._sde[sdeKey] = () => {
558
- $eventListenerTarget.removeEventListener(eventName, handler);
559
- $el.removeAttribute(eventDedupeKey);
560
- };
561
- }
562
- });
563
- if (ctx.tag._delayedSrc) {
564
- $el.setAttribute("src", ctx.tag._delayedSrc);
565
- }
566
- }
567
- }
568
- });
569
- };
570
-
571
- function asArray$1(value) {
572
- return Array.isArray(value) ? value : [value];
573
- }
574
- const HasElementTags = [
575
- "base",
576
- "meta",
577
- "link",
578
- "style",
579
- "script",
580
- "noscript"
581
- ];
582
-
583
- let activeHead;
584
- const setActiveHead = (head) => activeHead = head;
585
- const getActiveHead = () => activeHead;
586
-
587
- const TagEntityBits = 10;
588
-
589
- async function normaliseEntryTags(e) {
590
- const tagPromises = [];
591
- Object.entries(e.resolvedInput || e.input).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).forEach(([k, value]) => {
592
- const v = asArray$1(value);
593
- tagPromises.push(...v.map((props) => normaliseTag(k, props)).flat());
594
- });
595
- return (await Promise.all(tagPromises)).flat().map((t, i) => {
596
- t._e = e._i;
597
- t._p = (e._i << TagEntityBits) + i;
598
- return t;
599
- });
600
- }
601
-
602
- const CorePlugins = () => [
603
- // dedupe needs to come first
604
- DedupesTagsPlugin(),
605
- SortTagsPlugin(),
606
- TitleTemplatePlugin(),
607
- ProvideTagHashPlugin(),
608
- EventHandlersPlugin(),
609
- DeprecatedTagAttrPlugin()
610
- ];
611
- const DOMPlugins = (options = {}) => [
612
- PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
613
- ];
614
- function createHead$1(options = {}) {
615
- const head = createHeadCore({
616
- ...options,
617
- plugins: [...DOMPlugins(options), ...options?.plugins || []]
618
- });
619
- setActiveHead(head);
620
- return head;
621
- }
622
- function createHeadCore(options = {}) {
623
- let entries = [];
624
- let _sde = {};
625
- let _eid = 0;
626
- const hooks = createHooks();
627
- if (options?.hooks)
628
- hooks.addHooks(options.hooks);
629
- options.plugins = [
630
- ...CorePlugins(),
631
- ...options?.plugins || []
632
- ];
633
- options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
634
- const updated = () => hooks.callHook("entries:updated", head);
635
- const head = {
636
- resolvedOptions: options,
637
- headEntries() {
638
- return entries;
639
- },
640
- get hooks() {
641
- return hooks;
642
- },
643
- use(plugin) {
644
- if (plugin.hooks)
645
- hooks.addHooks(plugin.hooks);
646
- },
647
- push(input, options2) {
648
- const activeEntry = {
649
- _i: _eid++,
650
- input,
651
- _sde: {}
652
- };
653
- if (options2?.mode)
654
- activeEntry._m = options2?.mode;
655
- entries.push(activeEntry);
656
- updated();
657
- return {
658
- dispose() {
659
- entries = entries.filter((e) => {
660
- if (e._i !== activeEntry._i)
661
- return true;
662
- _sde = { ..._sde, ...e._sde || {} };
663
- e._sde = {};
664
- updated();
665
- return false;
666
- });
667
- },
668
- // a patch is the same as creating a new entry, just a nice DX
669
- patch(input2) {
670
- entries = entries.map((e) => {
671
- if (e._i === activeEntry._i) {
672
- activeEntry.input = e.input = input2;
673
- updated();
674
- }
675
- return e;
676
- });
677
- }
678
- };
679
- },
680
- async resolveTags() {
681
- const resolveCtx = { tags: [], entries: [...entries] };
682
- await hooks.callHook("entries:resolve", resolveCtx);
683
- for (const entry of resolveCtx.entries) {
684
- for (const tag of await normaliseEntryTags(entry)) {
685
- const tagCtx = { tag, entry };
686
- await hooks.callHook("tag:normalise", tagCtx);
687
- resolveCtx.tags.push(tagCtx.tag);
688
- }
689
- }
690
- await hooks.callHook("tags:resolve", resolveCtx);
691
- return resolveCtx.tags;
692
- },
693
- _elMap: {},
694
- _popSideEffectQueue() {
695
- const sde = { ..._sde };
696
- _sde = {};
697
- return sde;
698
- }
699
- };
700
- head.hooks.callHook("init", head);
701
- return head;
702
- }
703
-
704
- function defineHeadPlugin(plugin) {
705
- return plugin;
706
- }
707
- const composableNames = [
708
- "useHead",
709
- "useTagTitle",
710
- "useTagBase",
711
- "useTagMeta",
712
- "useTagMetaFlat",
713
- // alias
714
- "useSeoMeta",
715
- "useTagLink",
716
- "useTagScript",
717
- "useTagStyle",
718
- "useTagNoscript",
719
- "useHtmlAttrs",
720
- "useBodyAttrs",
721
- "useTitleTemplate",
722
- // server only composables
723
- "useServerHead",
724
- "useServerTagTitle",
725
- "useServerTagBase",
726
- "useServerTagMeta",
727
- "useServerTagMetaFlat",
728
- "useServerTagLink",
729
- "useServerTagScript",
730
- "useServerTagStyle",
731
- "useServerTagNoscript",
732
- "useServerHtmlAttrs",
733
- "useServerBodyAttrs",
734
- "useServerTitleTemplate"
735
- ];
4
+ import { HasElementTags, defineHeadPlugin, asArray } from '@unhead/shared';
736
5
 
737
6
  function resolveUnref(r) {
738
7
  return typeof r === "function" ? r() : unref(r);
@@ -762,9 +31,6 @@ function resolveUnrefHeadInput(ref, lastKey = "") {
762
31
  }
763
32
  return root;
764
33
  }
765
- function asArray(value) {
766
- return Array.isArray(value) ? value : [value];
767
- }
768
34
 
769
35
  const Vue3 = version.startsWith("3");
770
36
  const IsBrowser = typeof window !== "undefined";
@@ -839,197 +105,16 @@ const Vue2ProvideUnheadPlugin = function(_Vue, head) {
839
105
  });
840
106
  };
841
107
 
842
- function unpackToArray(input, options) {
843
- const unpacked = [];
844
- const kFn = options.resolveKeyData || ((ctx) => ctx.key);
845
- const vFn = options.resolveValueData || ((ctx) => ctx.value);
846
- for (const [k, v] of Object.entries(input)) {
847
- unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
848
- const ctx = { key: k, value: i };
849
- const val = vFn(ctx);
850
- if (typeof val === "object")
851
- return unpackToArray(val, options);
852
- if (Array.isArray(val))
853
- return val;
854
- return {
855
- [typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
856
- [typeof options.value === "function" ? options.value(ctx) : options.value]: val
857
- };
858
- }).flat());
859
- }
860
- return unpacked;
861
- }
862
-
863
- function unpackToString(value, options) {
864
- return Object.entries(value).map(([key, value2]) => {
865
- if (typeof value2 === "object")
866
- value2 = unpackToString(value2, options);
867
- if (options.resolve) {
868
- const resolved = options.resolve({ key, value: value2 });
869
- if (resolved)
870
- return resolved;
871
- }
872
- if (typeof value2 === "number")
873
- value2 = value2.toString();
874
- if (typeof value2 === "string" && options.wrapValue) {
875
- value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
876
- value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
877
- }
878
- return `${key}${options.keyValueSeparator || ""}${value2}`;
879
- }).join(options.entrySeparator || "");
880
- }
881
-
882
- const MetaPackingSchema = {
883
- robots: {
884
- unpack: {
885
- keyValueSeparator: ":"
886
- }
887
- },
888
- // Pragma directives
889
- contentSecurityPolicy: {
890
- unpack: {
891
- keyValueSeparator: " ",
892
- entrySeparator: "; "
893
- },
894
- metaKey: "http-equiv"
895
- },
896
- fbAppId: {
897
- keyValue: "fb:app_id",
898
- metaKey: "property"
899
- },
900
- msapplicationTileImage: {
901
- keyValue: "msapplication-TileImage"
902
- },
903
- /**
904
- * Tile colour for windows
905
- */
906
- msapplicationTileColor: {
907
- keyValue: "msapplication-TileColor"
908
- },
909
- /**
910
- * URL of a config for windows tile.
911
- */
912
- msapplicationConfig: {
913
- keyValue: "msapplication-Config"
914
- },
915
- charset: {
916
- metaKey: "charset"
917
- },
918
- contentType: {
919
- metaKey: "http-equiv"
920
- },
921
- defaultStyle: {
922
- metaKey: "http-equiv"
923
- },
924
- xUaCompatible: {
925
- metaKey: "http-equiv"
926
- },
927
- refresh: {
928
- metaKey: "http-equiv"
929
- }
930
- };
931
- function resolveMetaKeyType(key) {
932
- return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
933
- }
934
-
935
- const ArrayableInputs = ["Image", "Video", "Audio"];
936
- function unpackMeta(input) {
937
- const extras = [];
938
- ArrayableInputs.forEach((key) => {
939
- const ogKey = `og:${key.toLowerCase()}`;
940
- const inputKey = `og${key}`;
941
- const val = input[inputKey];
942
- if (typeof val === "object") {
943
- (Array.isArray(val) ? val : [val]).forEach((entry) => {
944
- if (!entry)
945
- return;
946
- const unpackedEntry = unpackToArray(entry, {
947
- key: "property",
948
- value: "content",
949
- resolveKeyData({ key: key2 }) {
950
- return fixKeyCase(`${ogKey}${key2 !== "url" ? `:${key2}` : ""}`);
951
- },
952
- resolveValueData({ value }) {
953
- return typeof value === "number" ? value.toString() : value;
954
- }
955
- });
956
- extras.push(
957
- ...unpackedEntry.sort((a, b) => a.property === ogKey ? -1 : b.property === ogKey ? 1 : 0)
958
- );
959
- });
960
- delete input[inputKey];
961
- }
962
- });
963
- const meta = unpackToArray(input, {
964
- key({ key }) {
965
- return resolveMetaKeyType(key);
966
- },
967
- value({ key }) {
968
- return key === "charset" ? "charset" : "content";
969
- },
970
- resolveKeyData({ key }) {
971
- return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
972
- },
973
- resolveValueData({ value, key }) {
974
- if (value === null)
975
- return "_null";
976
- if (typeof value === "object") {
977
- const definition = MetaPackingSchema[key];
978
- if (key === "refresh")
979
- return `${value.seconds};url=${value.url}`;
980
- return unpackToString(
981
- changeKeyCasingDeep(value),
982
- {
983
- entrySeparator: ", ",
984
- keyValueSeparator: "=",
985
- resolve({ value: value2, key: key2 }) {
986
- if (value2 === null)
987
- return "";
988
- if (typeof value2 === "boolean")
989
- return `${key2}`;
990
- },
991
- ...definition?.unpack
992
- }
993
- );
994
- }
995
- return typeof value === "number" ? value.toString() : value;
996
- }
997
- });
998
- return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
999
- }
1000
-
1001
- const PropertyPrefixKeys = /^(og|fb)/;
1002
- const ColonPrefixKeys = /^(og|twitter|fb)/;
1003
- function fixKeyCase(key) {
1004
- key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
1005
- if (ColonPrefixKeys.test(key)) {
1006
- key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
1007
- }
1008
- return key;
1009
- }
1010
- function changeKeyCasingDeep(input) {
1011
- if (Array.isArray(input)) {
1012
- return input.map((entry) => changeKeyCasingDeep(entry));
1013
- }
1014
- if (typeof input !== "object" || Array.isArray(input))
1015
- return input;
1016
- const output = {};
1017
- for (const [key, value] of Object.entries(input))
1018
- output[fixKeyCase(key)] = changeKeyCasingDeep(value);
1019
- return output;
1020
- }
1021
-
1022
108
  function clientUseHead(input, options = {}) {
1023
109
  const head = injectHead();
1024
110
  const deactivated = ref(false);
1025
111
  const resolvedInput = ref({});
1026
112
  watchEffect(() => {
1027
- resolvedInput.value = resolveUnrefHeadInput(input);
113
+ resolvedInput.value = deactivated.value ? {} : resolveUnrefHeadInput(input);
1028
114
  });
1029
115
  const entry = head.push(resolvedInput.value, options);
1030
- watch([resolvedInput, deactivated], ([e, disable]) => {
1031
- if (!disable)
1032
- entry.patch(e);
116
+ watch(resolvedInput, (e) => {
117
+ entry.patch(e);
1033
118
  });
1034
119
  const vm = getCurrentInstance();
1035
120
  if (vm) {
@@ -1051,28 +136,21 @@ function serverUseHead(input, options = {}) {
1051
136
  return head.push(input, options);
1052
137
  }
1053
138
 
1054
- function useServerHead(input, options = {}) {
1055
- return useHead(input, { ...options, mode: "server" });
139
+ function useHead(input, options = {}) {
140
+ const head = injectHead();
141
+ if (head) {
142
+ const isBrowser = IsBrowser || !!head.resolvedOptions?.document;
143
+ if (options.mode === "server" && isBrowser || options.mode === "client" && !isBrowser)
144
+ return;
145
+ return isBrowser ? clientUseHead(input, options) : serverUseHead(input, options);
146
+ }
147
+ }
148
+
149
+ function useHeadSafe(input, options = {}) {
150
+ return useHead(input, { ...options, transform: whitelistSafeInput });
1056
151
  }
1057
- const useServerTagTitle = (title) => useServerHead({ title });
1058
- const useServerTitleTemplate = (titleTemplate) => useServerHead({ titleTemplate });
1059
- const useServerTagMeta = (meta) => useServerHead({ meta: asArray(meta) });
1060
- const useServerTagMetaFlat = (meta) => {
1061
- const input = ref({});
1062
- watchEffect(() => {
1063
- input.value = unpackMeta(resolveUnrefHeadInput(meta));
1064
- });
1065
- return useServerHead({ meta: input });
1066
- };
1067
- const useServerTagLink = (link) => useServerHead({ link: asArray(link) });
1068
- const useServerTagScript = (script) => useServerHead({ script: asArray(script) });
1069
- const useServerTagStyle = (style) => useServerHead({ style: asArray(style) });
1070
- const useServerTagNoscript = (noscript) => useServerHead({ noscript: asArray(noscript) });
1071
- const useServerTagBase = (base) => useServerHead({ base });
1072
- const useServerHtmlAttrs = (attrs) => useServerHead({ htmlAttrs: attrs });
1073
- const useServerBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
1074
152
 
1075
- const useSeoMeta = (input) => {
153
+ const useSeoMeta = (input, options) => {
1076
154
  const headInput = ref({});
1077
155
  watchEffect(() => {
1078
156
  const resolvedMeta = resolveUnrefHeadInput(input);
@@ -1083,18 +161,21 @@ const useSeoMeta = (input) => {
1083
161
  meta: unpackMeta(meta)
1084
162
  };
1085
163
  });
1086
- return useHead(headInput);
164
+ return useHead(headInput, options);
1087
165
  };
1088
166
 
1089
- function useHead(input, options = {}) {
1090
- const head = injectHead();
1091
- if (head) {
1092
- const isBrowser = IsBrowser || !!head.resolvedOptions?.document;
1093
- if (options.mode === "server" && isBrowser || options.mode === "client" && !isBrowser)
1094
- return;
1095
- return isBrowser ? clientUseHead(input, options) : serverUseHead(input, options);
1096
- }
167
+ function useServerHead(input, options = {}) {
168
+ return useHead(input, { ...options, mode: "server" });
1097
169
  }
170
+
171
+ function useServerHeadSafe(input, options = {}) {
172
+ return useHeadSafe(input, { ...options, mode: "server" });
173
+ }
174
+
175
+ function useServerSeoMeta(input, options) {
176
+ return useSeoMeta(input, { ...options || {}, mode: "server" });
177
+ }
178
+
1098
179
  const useTagTitle = (title) => useHead({ title });
1099
180
  const useTitleTemplate = (titleTemplate) => useHead({ titleTemplate });
1100
181
  const useTagMeta = (meta) => useHead({ meta: asArray(meta) });
@@ -1112,6 +193,23 @@ const useTagNoscript = (noscript) => useHead({ noscript: asArray(noscript) });
1112
193
  const useTagBase = (base) => useHead({ base });
1113
194
  const useHtmlAttrs = (attrs) => useHead({ htmlAttrs: attrs });
1114
195
  const useBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
196
+ const useServerTagTitle = (title) => useServerHead({ title });
197
+ const useServerTitleTemplate = (titleTemplate) => useServerHead({ titleTemplate });
198
+ const useServerTagMeta = (meta) => useServerHead({ meta: asArray(meta) });
199
+ const useServerTagMetaFlat = (meta) => {
200
+ const input = ref({});
201
+ watchEffect(() => {
202
+ input.value = unpackMeta(resolveUnrefHeadInput(meta));
203
+ });
204
+ return useServerHead({ meta: input });
205
+ };
206
+ const useServerTagLink = (link) => useServerHead({ link: asArray(link) });
207
+ const useServerTagScript = (script) => useServerHead({ script: asArray(script) });
208
+ const useServerTagStyle = (style) => useServerHead({ style: asArray(style) });
209
+ const useServerTagNoscript = (noscript) => useServerHead({ noscript: asArray(noscript) });
210
+ const useServerTagBase = (base) => useServerHead({ base });
211
+ const useServerHtmlAttrs = (attrs) => useServerHead({ htmlAttrs: attrs });
212
+ const useServerBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
1115
213
 
1116
214
  const coreComposableNames = [
1117
215
  "injectHead"
@@ -1120,4 +218,4 @@ const unheadVueComposablesImports = {
1120
218
  "@unhead/vue": [...coreComposableNames, ...composableNames]
1121
219
  };
1122
220
 
1123
- export { Vue2ProvideUnheadPlugin, VueHeadMixin, VueReactiveUseHeadPlugin, asArray, createHead, createHeadCore, headSymbol, injectHead, resolveUnrefHeadInput, unheadVueComposablesImports, useBodyAttrs, useHead, useHtmlAttrs, useSeoMeta, useServerBodyAttrs, useServerHead, useServerHtmlAttrs, useServerTagBase, useServerTagLink, useServerTagMeta, useServerTagMetaFlat, useServerTagNoscript, useServerTagScript, useServerTagStyle, useServerTagTitle, useServerTitleTemplate, useTagBase, useTagLink, useTagMeta, useTagMetaFlat, useTagNoscript, useTagScript, useTagStyle, useTagTitle, useTitleTemplate };
221
+ export { Vue2ProvideUnheadPlugin, VueHeadMixin, VueReactiveUseHeadPlugin, createHead, headSymbol, injectHead, resolveUnrefHeadInput, unheadVueComposablesImports, useBodyAttrs, useHead, useHeadSafe, useHtmlAttrs, useSeoMeta, useServerBodyAttrs, useServerHead, useServerHeadSafe, useServerHtmlAttrs, useServerSeoMeta, useServerTagBase, useServerTagLink, useServerTagMeta, useServerTagMetaFlat, useServerTagNoscript, useServerTagScript, useServerTagStyle, useServerTagTitle, useServerTitleTemplate, useTagBase, useTagLink, useTagMeta, useTagMetaFlat, useTagNoscript, useTagScript, useTagStyle, useTagTitle, useTitleTemplate };