@unhead/vue 0.6.5 → 0.6.7

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 CHANGED
@@ -1,7 +1,223 @@
1
1
  'use strict';
2
2
 
3
- const vue = require('vue');
4
3
  const hookable = require('hookable');
4
+ const vue = require('vue');
5
+
6
+ const TagsWithInnerContent = ["script", "style", "noscript"];
7
+ const HasElementTags$1 = [
8
+ "base",
9
+ "meta",
10
+ "link",
11
+ "style",
12
+ "script",
13
+ "noscript"
14
+ ];
15
+
16
+ const UniqueTags$1 = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
17
+ const ArrayMetaProperties$1 = [
18
+ "og:image",
19
+ "og:video",
20
+ "og:audio",
21
+ "og:locale:alternate",
22
+ "video:actor",
23
+ "video:director",
24
+ "video:writer",
25
+ "video:tag",
26
+ "article:author",
27
+ "article:tag",
28
+ "book:tag",
29
+ "book:author",
30
+ "music:album",
31
+ "music:musician"
32
+ ];
33
+ function tagDedupeKey$1(tag) {
34
+ const { props, tag: tagName } = tag;
35
+ if (UniqueTags$1.includes(tagName))
36
+ return tagName;
37
+ if (tagName === "link" && props.rel === "canonical")
38
+ return "canonical";
39
+ if (props.charset)
40
+ return "charset";
41
+ const name = ["id"];
42
+ if (tagName === "meta")
43
+ name.push(...["name", "property", "http-equiv"]);
44
+ for (const n of name) {
45
+ if (typeof props[n] !== "undefined") {
46
+ const val = String(props[n]);
47
+ if (ArrayMetaProperties$1.findIndex((p) => val.startsWith(p)) !== -1)
48
+ return false;
49
+ return `${tagName}:${n}:${val}`;
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+
55
+ const setAttrs = (ctx, markSideEffect) => {
56
+ const { tag, $el } = ctx;
57
+ if (!$el)
58
+ return;
59
+ Object.entries(tag.props).forEach(([k, value]) => {
60
+ value = String(value);
61
+ const attrSdeKey = `attr:${k}`;
62
+ if (k === "class") {
63
+ for (const c of value.split(" ")) {
64
+ const classSdeKey = `${attrSdeKey}:${c}`;
65
+ if (markSideEffect)
66
+ markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
67
+ if (!$el.classList.contains(c))
68
+ $el.classList.add(c);
69
+ }
70
+ return;
71
+ }
72
+ if (markSideEffect && !k.startsWith("data-h-"))
73
+ markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
74
+ if ($el.getAttribute(k) !== value)
75
+ $el.setAttribute(k, value);
76
+ });
77
+ if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
78
+ $el.innerHTML = tag.children || "";
79
+ };
80
+
81
+ function hashCode(s) {
82
+ let h = 9;
83
+ for (let i = 0; i < s.length; )
84
+ h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
85
+ return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
86
+ }
87
+
88
+ async function renderDOMHead(head, options = {}) {
89
+ const ctx = { shouldRender: true };
90
+ await head.hooks.callHook("dom:beforeRender", ctx);
91
+ if (!ctx.shouldRender)
92
+ return;
93
+ const dom = options.document || window.document;
94
+ const staleSideEffects = head._popSideEffectQueue();
95
+ head.headEntries().map((entry) => entry._sde).forEach((sde) => {
96
+ Object.entries(sde).forEach(([key, fn]) => {
97
+ staleSideEffects[key] = fn;
98
+ });
99
+ });
100
+ const preRenderTag = async (tag) => {
101
+ const entry = head.headEntries().find((e) => e._i === tag._e);
102
+ const renderCtx = {
103
+ renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
104
+ $el: null,
105
+ shouldRender: true,
106
+ tag,
107
+ entry,
108
+ staleSideEffects
109
+ };
110
+ await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
111
+ return renderCtx;
112
+ };
113
+ const renders = [];
114
+ const pendingRenders = {
115
+ body: [],
116
+ head: []
117
+ };
118
+ const markSideEffect = (ctx2, key, fn) => {
119
+ key = `${ctx2.renderId}:${key}`;
120
+ if (ctx2.entry)
121
+ ctx2.entry._sde[key] = fn;
122
+ delete staleSideEffects[key];
123
+ };
124
+ const markEl = (ctx2) => {
125
+ head._elMap[ctx2.renderId] = ctx2.$el;
126
+ renders.push(ctx2);
127
+ markSideEffect(ctx2, "el", () => {
128
+ ctx2.$el?.remove();
129
+ delete head._elMap[ctx2.renderId];
130
+ });
131
+ };
132
+ for (const t of await head.resolveTags()) {
133
+ const ctx2 = await preRenderTag(t);
134
+ if (!ctx2.shouldRender)
135
+ continue;
136
+ const { tag } = ctx2;
137
+ if (tag.tag === "title") {
138
+ dom.title = tag.children || "";
139
+ renders.push(ctx2);
140
+ continue;
141
+ }
142
+ if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
143
+ ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
144
+ setAttrs(ctx2, markSideEffect);
145
+ renders.push(ctx2);
146
+ continue;
147
+ }
148
+ ctx2.$el = head._elMap[ctx2.renderId];
149
+ if (!ctx2.$el && tag._hash) {
150
+ ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
151
+ }
152
+ if (ctx2.$el) {
153
+ if (ctx2.tag._d)
154
+ setAttrs(ctx2);
155
+ markEl(ctx2);
156
+ continue;
157
+ }
158
+ ctx2.$el = dom.createElement(tag.tag);
159
+ setAttrs(ctx2);
160
+ pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
161
+ }
162
+ Object.entries(pendingRenders).forEach(([pos, queue]) => {
163
+ if (!queue.length)
164
+ return;
165
+ for (const $el of [...dom[pos].children].reverse()) {
166
+ const elTag = $el.tagName.toLowerCase();
167
+ if (!HasElementTags$1.includes(elTag))
168
+ continue;
169
+ const dedupeKey = tagDedupeKey$1({
170
+ tag: elTag,
171
+ props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
172
+ });
173
+ const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode(ctx2.$el)));
174
+ if (matchIdx !== -1) {
175
+ const ctx2 = queue[matchIdx];
176
+ ctx2.$el = $el;
177
+ setAttrs(ctx2);
178
+ markEl(ctx2);
179
+ delete queue[matchIdx];
180
+ }
181
+ }
182
+ queue.forEach((ctx2) => {
183
+ if (!ctx2.$el)
184
+ return;
185
+ switch (ctx2.tag.tagPosition) {
186
+ case "bodyClose":
187
+ dom.body.appendChild(ctx2.$el);
188
+ break;
189
+ case "bodyOpen":
190
+ dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
191
+ break;
192
+ case "head":
193
+ default:
194
+ dom.head.appendChild(ctx2.$el);
195
+ break;
196
+ }
197
+ markEl(ctx2);
198
+ });
199
+ });
200
+ for (const ctx2 of renders)
201
+ await head.hooks.callHook("dom:renderTag", ctx2);
202
+ Object.values(staleSideEffects).forEach((fn) => fn());
203
+ }
204
+ let domUpdatePromise = null;
205
+ async function debouncedRenderDOMHead(head, options = {}) {
206
+ function doDomUpdate() {
207
+ domUpdatePromise = null;
208
+ return renderDOMHead(head, options);
209
+ }
210
+ const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
211
+ return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
212
+ }
213
+
214
+ const index = {
215
+ __proto__: null,
216
+ debouncedRenderDOMHead: debouncedRenderDOMHead,
217
+ get domUpdatePromise () { return domUpdatePromise; },
218
+ hashCode: hashCode,
219
+ renderDOMHead: renderDOMHead
220
+ };
5
221
 
6
222
  const ValidHeadTags = [
7
223
  "title",
@@ -295,7 +511,7 @@ const PatchDomOnEntryUpdatesPlugin = (options) => {
295
511
  let delayFn = options?.delayFn;
296
512
  if (!delayFn && typeof requestAnimationFrame !== "undefined")
297
513
  delayFn = requestAnimationFrame;
298
- import('./chunks/index.cjs').then(({ debouncedRenderDOMHead }) => {
514
+ Promise.resolve().then(function () { return index; }).then(({ debouncedRenderDOMHead }) => {
299
515
  debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
300
516
  });
301
517
  }
@@ -345,16 +561,18 @@ const EventHandlersPlugin = () => {
345
561
  const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
346
562
  const eventName = k.slice(2).toLowerCase();
347
563
  const eventDedupeKey = `data-h-${eventName}`;
348
- delete ctx.queuedSideEffects[sdeKey];
564
+ delete ctx.staleSideEffects[sdeKey];
349
565
  if ($el.hasAttribute(eventDedupeKey))
350
566
  return;
351
567
  const handler = value;
352
568
  $el.setAttribute(eventDedupeKey, "");
353
569
  $eventListenerTarget.addEventListener(eventName, handler);
354
- ctx.entry._sde[sdeKey] = () => {
355
- $eventListenerTarget.removeEventListener(eventName, handler);
356
- $el.removeAttribute(eventDedupeKey);
357
- };
570
+ if (ctx.entry) {
571
+ ctx.entry._sde[sdeKey] = () => {
572
+ $eventListenerTarget.removeEventListener(eventName, handler);
573
+ $el.removeAttribute(eventDedupeKey);
574
+ };
575
+ }
358
576
  });
359
577
  if (ctx.tag._delayedSrc) {
360
578
  $el.setAttribute("src", ctx.tag._delayedSrc);
@@ -367,12 +585,6 @@ const EventHandlersPlugin = () => {
367
585
  function asArray$1(value) {
368
586
  return Array.isArray(value) ? value : [value];
369
587
  }
370
- function hashCode(s) {
371
- let h = 9;
372
- for (let i = 0; i < s.length; )
373
- h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
374
- return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
375
- }
376
588
  const HasElementTags = [
377
589
  "base",
378
590
  "meta",
@@ -398,7 +610,26 @@ function normaliseEntryTags(e) {
398
610
  });
399
611
  }
400
612
 
613
+ const CorePlugins = () => [
614
+ DedupesTagsPlugin(),
615
+ SortTagsPlugin(),
616
+ TitleTemplatePlugin(),
617
+ ProvideTagHashPlugin(),
618
+ EventHandlersPlugin(),
619
+ DeprecatedTagAttrPlugin()
620
+ ];
621
+ const DOMPlugins = (options = {}) => [
622
+ PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
623
+ ];
401
624
  function createHead$1(options = {}) {
625
+ const head = createHeadCore({
626
+ ...options,
627
+ plugins: [...DOMPlugins(options), ...options?.plugins || []]
628
+ });
629
+ setActiveHead(head);
630
+ return head;
631
+ }
632
+ function createHeadCore(options = {}) {
402
633
  let entries = [];
403
634
  let _sde = {};
404
635
  let _eid = 0;
@@ -406,17 +637,11 @@ function createHead$1(options = {}) {
406
637
  if (options?.hooks)
407
638
  hooks.addHooks(options.hooks);
408
639
  options.plugins = [
409
- DeprecatedTagAttrPlugin(),
410
- DedupesTagsPlugin(),
411
- SortTagsPlugin(),
412
- TitleTemplatePlugin(),
413
- EventHandlersPlugin(),
414
- ProvideTagHashPlugin(),
415
- PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn }),
640
+ ...CorePlugins(),
416
641
  ...options?.plugins || []
417
642
  ];
418
643
  options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
419
- const triggerUpdateHook = () => hooks.callHook("entries:updated", head);
644
+ const updated = () => hooks.callHook("entries:updated", head);
420
645
  const head = {
421
646
  resolvedOptions: options,
422
647
  headEntries() {
@@ -434,27 +659,23 @@ function createHead$1(options = {}) {
434
659
  if (options2?.mode)
435
660
  activeEntry._m = options2?.mode;
436
661
  entries.push(activeEntry);
437
- triggerUpdateHook();
438
- const queueSideEffects = (e) => {
439
- _sde = { ..._sde, ...e._sde || {} };
440
- e._sde = {};
441
- triggerUpdateHook();
442
- };
662
+ updated();
443
663
  return {
444
664
  dispose() {
445
665
  entries = entries.filter((e) => {
446
666
  if (e._i !== activeEntry._i)
447
667
  return true;
448
- queueSideEffects(e);
668
+ _sde = { ..._sde, ...e._sde || {} };
669
+ e._sde = {};
670
+ updated();
449
671
  return false;
450
672
  });
451
673
  },
452
674
  patch(input2) {
453
675
  entries = entries.map((e) => {
454
676
  if (e._i === activeEntry._i) {
455
- queueSideEffects(e);
456
677
  activeEntry.input = e.input = input2;
457
- activeEntry._i = e._i = _eid++;
678
+ updated();
458
679
  }
459
680
  return e;
460
681
  });
@@ -482,7 +703,6 @@ function createHead$1(options = {}) {
482
703
  }
483
704
  };
484
705
  head.hooks.callHook("init", head);
485
- setActiveHead(head);
486
706
  return head;
487
707
  }
488
708
 
@@ -495,6 +715,7 @@ const composableNames = [
495
715
  "useTagBase",
496
716
  "useTagMeta",
497
717
  "useTagMetaFlat",
718
+ "useSeoMeta",
498
719
  "useTagLink",
499
720
  "useTagScript",
500
721
  "useTagStyle",
@@ -554,14 +775,13 @@ function injectHead() {
554
775
  return vue.getCurrentInstance() && vue.inject(headSymbol) || getActiveHead();
555
776
  }
556
777
  function createHead(options = {}) {
557
- const plugins = [
558
- VueReactiveUseHeadPlugin(),
559
- ...options?.plugins || []
560
- ];
561
778
  const head = createHead$1({
562
779
  ...options,
563
780
  domDelayFn: (fn) => setTimeout(() => vue.nextTick(() => fn()), 10),
564
- plugins
781
+ plugins: [
782
+ VueReactiveUseHeadPlugin(),
783
+ ...options?.plugins || []
784
+ ]
565
785
  });
566
786
  const vuePlugin = {
567
787
  install(app) {
@@ -819,6 +1039,7 @@ const useTagMetaFlat = (meta) => {
819
1039
  });
820
1040
  return useHead({ meta: input });
821
1041
  };
1042
+ const useSeoMeta = useTagMetaFlat;
822
1043
  const useTagLink = (link) => useHead({ link: asArray(link) });
823
1044
  const useTagScript = (script) => useHead({ script: asArray(script) });
824
1045
  const useTagStyle = (style) => useHead({ style: asArray(style) });
@@ -842,6 +1063,7 @@ exports.VueHeadMixin = VueHeadMixin;
842
1063
  exports.VueReactiveUseHeadPlugin = VueReactiveUseHeadPlugin;
843
1064
  exports.asArray = asArray;
844
1065
  exports.createHead = createHead;
1066
+ exports.createHeadCore = createHeadCore;
845
1067
  exports.headSymbol = headSymbol;
846
1068
  exports.injectHead = injectHead;
847
1069
  exports.resolveUnrefHeadInput = resolveUnrefHeadInput;
@@ -849,6 +1071,7 @@ exports.unheadVueComposablesImports = unheadVueComposablesImports;
849
1071
  exports.useBodyAttrs = useBodyAttrs;
850
1072
  exports.useHead = useHead;
851
1073
  exports.useHtmlAttrs = useHtmlAttrs;
1074
+ exports.useSeoMeta = useSeoMeta;
852
1075
  exports.useServerBodyAttrs = useServerBodyAttrs;
853
1076
  exports.useServerHead = useServerHead;
854
1077
  exports.useServerHtmlAttrs = useServerHtmlAttrs;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  import * as _unhead_schema from '@unhead/schema';
2
- import { Title as Title$1, TitleTemplate as TitleTemplate$1, EntryAugmentation, Base as Base$1, Link as Link$1, Meta as Meta$1, Style as Style$1, Script as Script$1, Noscript as Noscript$1, DataKeys, SchemaAugmentations, DefinedValueOrEmptyObject, MergeHead, BaseHtmlAttr, MaybeArray, BaseBodyAttr, Unhead, CreateHeadOptions, HeadEntryOptions, MetaFlatInput, ActiveHeadEntry } from '@unhead/schema';
2
+ import { Head, CreateHeadOptions, Unhead, Title as Title$1, TitleTemplate as TitleTemplate$1, EntryAugmentation, Base as Base$1, Link as Link$1, Meta as Meta$1, Style as Style$1, Script as Script$1, Noscript as Noscript$1, DataKeys, SchemaAugmentations, DefinedValueOrEmptyObject, MergeHead, BaseHtmlAttr, MaybeArray, BaseBodyAttr, HeadEntryOptions, MetaFlatInput, ActiveHeadEntry } from '@unhead/schema';
3
3
  export { ActiveHeadEntry, Head, HeadEntryOptions, HeadTag, MergeHead, Unhead } from '@unhead/schema';
4
4
  import { ComputedRef, Ref, Plugin } from 'vue';
5
5
 
6
+ /**
7
+ * Creates a core instance of unhead. Does not provide a global ctx for composables to work
8
+ * and does not register DOM plugins.
9
+ *
10
+ * @param options
11
+ */
12
+ declare function createHeadCore<T extends {} = Head>(options?: CreateHeadOptions): Unhead<T>;
13
+
6
14
  declare type MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>;
7
15
  declare type MaybeComputedRef<T> = T | MaybeReadonlyRef<T> | Ref<T>;
8
16
  declare type MaybeComputedRefEntries<T> = MaybeComputedRef<T> | {
@@ -138,6 +146,7 @@ declare const useTagTitle: (title: ReactiveHead['title']) => void | ActiveHeadEn
138
146
  declare const useTitleTemplate: (titleTemplate: ReactiveHead['titleTemplate']) => void | ActiveHeadEntry<UseHeadInput<MergeHead>>;
139
147
  declare const useTagMeta: (meta: Arrayable<Meta>) => void | ActiveHeadEntry<UseHeadInput<MergeHead>>;
140
148
  declare const useTagMetaFlat: (meta: MaybeComputedRefEntries<MetaFlatInput>) => void | ActiveHeadEntry<UseHeadInput<MergeHead>>;
149
+ declare const useSeoMeta: (meta: MaybeComputedRefEntries<MetaFlatInput>) => void | ActiveHeadEntry<UseHeadInput<MergeHead>>;
141
150
  declare const useTagLink: (link: Arrayable<Link>) => void | ActiveHeadEntry<UseHeadInput<MergeHead>>;
142
151
  declare const useTagScript: (script: Arrayable<Script>) => void | ActiveHeadEntry<UseHeadInput<MergeHead>>;
143
152
  declare const useTagStyle: (style: Arrayable<Style>) => void | ActiveHeadEntry<UseHeadInput<MergeHead>>;
@@ -151,4 +160,4 @@ declare const unheadVueComposablesImports: {
151
160
  imports: string[];
152
161
  }[];
153
162
 
154
- export { Arrayable, Base, BodyAttributes, HtmlAttributes, Link, MaybeComputedRef, MaybeComputedRefEntries, MaybeReadonlyRef, Meta, Noscript, ReactiveHead, Script, Style, Title, TitleTemplate, UseHeadInput, Vue2ProvideUnheadPlugin, VueHeadClient, VueHeadMixin, VueReactiveUseHeadPlugin, asArray, createHead, headSymbol, injectHead, resolveUnrefHeadInput, unheadVueComposablesImports, useBodyAttrs, useHead, useHtmlAttrs, useServerBodyAttrs, useServerHead, useServerHtmlAttrs, useServerTagBase, useServerTagLink, useServerTagMeta, useServerTagMetaFlat, useServerTagNoscript, useServerTagScript, useServerTagStyle, useServerTagTitle, useServerTitleTemplate, useTagBase, useTagLink, useTagMeta, useTagMetaFlat, useTagNoscript, useTagScript, useTagStyle, useTagTitle, useTitleTemplate };
163
+ export { Arrayable, Base, BodyAttributes, HtmlAttributes, Link, MaybeComputedRef, MaybeComputedRefEntries, MaybeReadonlyRef, Meta, Noscript, ReactiveHead, Script, Style, Title, TitleTemplate, UseHeadInput, Vue2ProvideUnheadPlugin, VueHeadClient, 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 };
package/dist/index.mjs CHANGED
@@ -1,5 +1,221 @@
1
- import { unref, isRef, version, getCurrentInstance, inject, nextTick, ref, watchEffect, watch, onBeforeUnmount } from 'vue';
2
1
  import { createHooks } from 'hookable';
2
+ import { unref, isRef, version, getCurrentInstance, inject, nextTick, ref, watchEffect, watch, onBeforeUnmount } 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
+ const ArrayMetaProperties$1 = [
16
+ "og:image",
17
+ "og:video",
18
+ "og:audio",
19
+ "og:locale:alternate",
20
+ "video:actor",
21
+ "video:director",
22
+ "video:writer",
23
+ "video:tag",
24
+ "article:author",
25
+ "article:tag",
26
+ "book:tag",
27
+ "book:author",
28
+ "music:album",
29
+ "music:musician"
30
+ ];
31
+ function tagDedupeKey$1(tag) {
32
+ const { props, tag: tagName } = tag;
33
+ if (UniqueTags$1.includes(tagName))
34
+ return tagName;
35
+ if (tagName === "link" && props.rel === "canonical")
36
+ return "canonical";
37
+ if (props.charset)
38
+ return "charset";
39
+ const name = ["id"];
40
+ if (tagName === "meta")
41
+ name.push(...["name", "property", "http-equiv"]);
42
+ for (const n of name) {
43
+ if (typeof props[n] !== "undefined") {
44
+ const val = String(props[n]);
45
+ if (ArrayMetaProperties$1.findIndex((p) => val.startsWith(p)) !== -1)
46
+ return false;
47
+ return `${tagName}:${n}:${val}`;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ const setAttrs = (ctx, markSideEffect) => {
54
+ const { tag, $el } = ctx;
55
+ if (!$el)
56
+ return;
57
+ Object.entries(tag.props).forEach(([k, value]) => {
58
+ value = String(value);
59
+ const attrSdeKey = `attr:${k}`;
60
+ if (k === "class") {
61
+ for (const c of value.split(" ")) {
62
+ const classSdeKey = `${attrSdeKey}:${c}`;
63
+ if (markSideEffect)
64
+ markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
65
+ if (!$el.classList.contains(c))
66
+ $el.classList.add(c);
67
+ }
68
+ return;
69
+ }
70
+ if (markSideEffect && !k.startsWith("data-h-"))
71
+ markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
72
+ if ($el.getAttribute(k) !== value)
73
+ $el.setAttribute(k, value);
74
+ });
75
+ if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
76
+ $el.innerHTML = tag.children || "";
77
+ };
78
+
79
+ function hashCode(s) {
80
+ let h = 9;
81
+ for (let i = 0; i < s.length; )
82
+ h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
83
+ return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
84
+ }
85
+
86
+ async function renderDOMHead(head, options = {}) {
87
+ const ctx = { shouldRender: true };
88
+ await head.hooks.callHook("dom:beforeRender", ctx);
89
+ if (!ctx.shouldRender)
90
+ return;
91
+ const dom = options.document || window.document;
92
+ const staleSideEffects = head._popSideEffectQueue();
93
+ head.headEntries().map((entry) => entry._sde).forEach((sde) => {
94
+ Object.entries(sde).forEach(([key, fn]) => {
95
+ staleSideEffects[key] = fn;
96
+ });
97
+ });
98
+ const preRenderTag = async (tag) => {
99
+ const entry = head.headEntries().find((e) => e._i === tag._e);
100
+ const renderCtx = {
101
+ renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
102
+ $el: null,
103
+ shouldRender: true,
104
+ tag,
105
+ entry,
106
+ staleSideEffects
107
+ };
108
+ await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
109
+ return renderCtx;
110
+ };
111
+ const renders = [];
112
+ const pendingRenders = {
113
+ body: [],
114
+ head: []
115
+ };
116
+ const markSideEffect = (ctx2, key, fn) => {
117
+ key = `${ctx2.renderId}:${key}`;
118
+ if (ctx2.entry)
119
+ ctx2.entry._sde[key] = fn;
120
+ delete staleSideEffects[key];
121
+ };
122
+ const markEl = (ctx2) => {
123
+ head._elMap[ctx2.renderId] = ctx2.$el;
124
+ renders.push(ctx2);
125
+ markSideEffect(ctx2, "el", () => {
126
+ ctx2.$el?.remove();
127
+ delete head._elMap[ctx2.renderId];
128
+ });
129
+ };
130
+ for (const t of await head.resolveTags()) {
131
+ const ctx2 = await preRenderTag(t);
132
+ if (!ctx2.shouldRender)
133
+ continue;
134
+ const { tag } = ctx2;
135
+ if (tag.tag === "title") {
136
+ dom.title = tag.children || "";
137
+ renders.push(ctx2);
138
+ continue;
139
+ }
140
+ if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
141
+ ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
142
+ setAttrs(ctx2, markSideEffect);
143
+ renders.push(ctx2);
144
+ continue;
145
+ }
146
+ ctx2.$el = head._elMap[ctx2.renderId];
147
+ if (!ctx2.$el && tag._hash) {
148
+ ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
149
+ }
150
+ if (ctx2.$el) {
151
+ if (ctx2.tag._d)
152
+ setAttrs(ctx2);
153
+ markEl(ctx2);
154
+ continue;
155
+ }
156
+ ctx2.$el = dom.createElement(tag.tag);
157
+ setAttrs(ctx2);
158
+ pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
159
+ }
160
+ Object.entries(pendingRenders).forEach(([pos, queue]) => {
161
+ if (!queue.length)
162
+ return;
163
+ for (const $el of [...dom[pos].children].reverse()) {
164
+ const elTag = $el.tagName.toLowerCase();
165
+ if (!HasElementTags$1.includes(elTag))
166
+ continue;
167
+ const dedupeKey = tagDedupeKey$1({
168
+ tag: elTag,
169
+ props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
170
+ });
171
+ const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode(ctx2.$el)));
172
+ if (matchIdx !== -1) {
173
+ const ctx2 = queue[matchIdx];
174
+ ctx2.$el = $el;
175
+ setAttrs(ctx2);
176
+ markEl(ctx2);
177
+ delete queue[matchIdx];
178
+ }
179
+ }
180
+ queue.forEach((ctx2) => {
181
+ if (!ctx2.$el)
182
+ return;
183
+ switch (ctx2.tag.tagPosition) {
184
+ case "bodyClose":
185
+ dom.body.appendChild(ctx2.$el);
186
+ break;
187
+ case "bodyOpen":
188
+ dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
189
+ break;
190
+ case "head":
191
+ default:
192
+ dom.head.appendChild(ctx2.$el);
193
+ break;
194
+ }
195
+ markEl(ctx2);
196
+ });
197
+ });
198
+ for (const ctx2 of renders)
199
+ await head.hooks.callHook("dom:renderTag", ctx2);
200
+ Object.values(staleSideEffects).forEach((fn) => fn());
201
+ }
202
+ let domUpdatePromise = null;
203
+ async function debouncedRenderDOMHead(head, options = {}) {
204
+ function doDomUpdate() {
205
+ domUpdatePromise = null;
206
+ return renderDOMHead(head, options);
207
+ }
208
+ const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
209
+ return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
210
+ }
211
+
212
+ const index = {
213
+ __proto__: null,
214
+ debouncedRenderDOMHead: debouncedRenderDOMHead,
215
+ get domUpdatePromise () { return domUpdatePromise; },
216
+ hashCode: hashCode,
217
+ renderDOMHead: renderDOMHead
218
+ };
3
219
 
4
220
  const ValidHeadTags = [
5
221
  "title",
@@ -293,7 +509,7 @@ const PatchDomOnEntryUpdatesPlugin = (options) => {
293
509
  let delayFn = options?.delayFn;
294
510
  if (!delayFn && typeof requestAnimationFrame !== "undefined")
295
511
  delayFn = requestAnimationFrame;
296
- import('./chunks/index.mjs').then(({ debouncedRenderDOMHead }) => {
512
+ Promise.resolve().then(function () { return index; }).then(({ debouncedRenderDOMHead }) => {
297
513
  debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
298
514
  });
299
515
  }
@@ -343,16 +559,18 @@ const EventHandlersPlugin = () => {
343
559
  const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
344
560
  const eventName = k.slice(2).toLowerCase();
345
561
  const eventDedupeKey = `data-h-${eventName}`;
346
- delete ctx.queuedSideEffects[sdeKey];
562
+ delete ctx.staleSideEffects[sdeKey];
347
563
  if ($el.hasAttribute(eventDedupeKey))
348
564
  return;
349
565
  const handler = value;
350
566
  $el.setAttribute(eventDedupeKey, "");
351
567
  $eventListenerTarget.addEventListener(eventName, handler);
352
- ctx.entry._sde[sdeKey] = () => {
353
- $eventListenerTarget.removeEventListener(eventName, handler);
354
- $el.removeAttribute(eventDedupeKey);
355
- };
568
+ if (ctx.entry) {
569
+ ctx.entry._sde[sdeKey] = () => {
570
+ $eventListenerTarget.removeEventListener(eventName, handler);
571
+ $el.removeAttribute(eventDedupeKey);
572
+ };
573
+ }
356
574
  });
357
575
  if (ctx.tag._delayedSrc) {
358
576
  $el.setAttribute("src", ctx.tag._delayedSrc);
@@ -365,12 +583,6 @@ const EventHandlersPlugin = () => {
365
583
  function asArray$1(value) {
366
584
  return Array.isArray(value) ? value : [value];
367
585
  }
368
- function hashCode(s) {
369
- let h = 9;
370
- for (let i = 0; i < s.length; )
371
- h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
372
- return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
373
- }
374
586
  const HasElementTags = [
375
587
  "base",
376
588
  "meta",
@@ -396,7 +608,26 @@ function normaliseEntryTags(e) {
396
608
  });
397
609
  }
398
610
 
611
+ const CorePlugins = () => [
612
+ DedupesTagsPlugin(),
613
+ SortTagsPlugin(),
614
+ TitleTemplatePlugin(),
615
+ ProvideTagHashPlugin(),
616
+ EventHandlersPlugin(),
617
+ DeprecatedTagAttrPlugin()
618
+ ];
619
+ const DOMPlugins = (options = {}) => [
620
+ PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
621
+ ];
399
622
  function createHead$1(options = {}) {
623
+ const head = createHeadCore({
624
+ ...options,
625
+ plugins: [...DOMPlugins(options), ...options?.plugins || []]
626
+ });
627
+ setActiveHead(head);
628
+ return head;
629
+ }
630
+ function createHeadCore(options = {}) {
400
631
  let entries = [];
401
632
  let _sde = {};
402
633
  let _eid = 0;
@@ -404,17 +635,11 @@ function createHead$1(options = {}) {
404
635
  if (options?.hooks)
405
636
  hooks.addHooks(options.hooks);
406
637
  options.plugins = [
407
- DeprecatedTagAttrPlugin(),
408
- DedupesTagsPlugin(),
409
- SortTagsPlugin(),
410
- TitleTemplatePlugin(),
411
- EventHandlersPlugin(),
412
- ProvideTagHashPlugin(),
413
- PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn }),
638
+ ...CorePlugins(),
414
639
  ...options?.plugins || []
415
640
  ];
416
641
  options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
417
- const triggerUpdateHook = () => hooks.callHook("entries:updated", head);
642
+ const updated = () => hooks.callHook("entries:updated", head);
418
643
  const head = {
419
644
  resolvedOptions: options,
420
645
  headEntries() {
@@ -432,27 +657,23 @@ function createHead$1(options = {}) {
432
657
  if (options2?.mode)
433
658
  activeEntry._m = options2?.mode;
434
659
  entries.push(activeEntry);
435
- triggerUpdateHook();
436
- const queueSideEffects = (e) => {
437
- _sde = { ..._sde, ...e._sde || {} };
438
- e._sde = {};
439
- triggerUpdateHook();
440
- };
660
+ updated();
441
661
  return {
442
662
  dispose() {
443
663
  entries = entries.filter((e) => {
444
664
  if (e._i !== activeEntry._i)
445
665
  return true;
446
- queueSideEffects(e);
666
+ _sde = { ..._sde, ...e._sde || {} };
667
+ e._sde = {};
668
+ updated();
447
669
  return false;
448
670
  });
449
671
  },
450
672
  patch(input2) {
451
673
  entries = entries.map((e) => {
452
674
  if (e._i === activeEntry._i) {
453
- queueSideEffects(e);
454
675
  activeEntry.input = e.input = input2;
455
- activeEntry._i = e._i = _eid++;
676
+ updated();
456
677
  }
457
678
  return e;
458
679
  });
@@ -480,7 +701,6 @@ function createHead$1(options = {}) {
480
701
  }
481
702
  };
482
703
  head.hooks.callHook("init", head);
483
- setActiveHead(head);
484
704
  return head;
485
705
  }
486
706
 
@@ -493,6 +713,7 @@ const composableNames = [
493
713
  "useTagBase",
494
714
  "useTagMeta",
495
715
  "useTagMetaFlat",
716
+ "useSeoMeta",
496
717
  "useTagLink",
497
718
  "useTagScript",
498
719
  "useTagStyle",
@@ -552,14 +773,13 @@ function injectHead() {
552
773
  return getCurrentInstance() && inject(headSymbol) || getActiveHead();
553
774
  }
554
775
  function createHead(options = {}) {
555
- const plugins = [
556
- VueReactiveUseHeadPlugin(),
557
- ...options?.plugins || []
558
- ];
559
776
  const head = createHead$1({
560
777
  ...options,
561
778
  domDelayFn: (fn) => setTimeout(() => nextTick(() => fn()), 10),
562
- plugins
779
+ plugins: [
780
+ VueReactiveUseHeadPlugin(),
781
+ ...options?.plugins || []
782
+ ]
563
783
  });
564
784
  const vuePlugin = {
565
785
  install(app) {
@@ -817,6 +1037,7 @@ const useTagMetaFlat = (meta) => {
817
1037
  });
818
1038
  return useHead({ meta: input });
819
1039
  };
1040
+ const useSeoMeta = useTagMetaFlat;
820
1041
  const useTagLink = (link) => useHead({ link: asArray(link) });
821
1042
  const useTagScript = (script) => useHead({ script: asArray(script) });
822
1043
  const useTagStyle = (style) => useHead({ style: asArray(style) });
@@ -835,4 +1056,4 @@ const unheadVueComposablesImports = [
835
1056
  }
836
1057
  ];
837
1058
 
838
- export { Vue2ProvideUnheadPlugin, VueHeadMixin, VueReactiveUseHeadPlugin, asArray, createHead, headSymbol, injectHead, resolveUnrefHeadInput, unheadVueComposablesImports, useBodyAttrs, useHead, useHtmlAttrs, useServerBodyAttrs, useServerHead, useServerHtmlAttrs, useServerTagBase, useServerTagLink, useServerTagMeta, useServerTagMetaFlat, useServerTagNoscript, useServerTagScript, useServerTagStyle, useServerTagTitle, useServerTitleTemplate, useTagBase, useTagLink, useTagMeta, useTagMetaFlat, useTagNoscript, useTagScript, useTagStyle, useTagTitle, useTitleTemplate };
1059
+ 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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unhead/vue",
3
3
  "type": "module",
4
- "version": "0.6.5",
4
+ "version": "0.6.7",
5
5
  "packageManager": "pnpm@7.14.0",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",
@@ -33,11 +33,11 @@
33
33
  "vue": ">=2.7 || >=3"
34
34
  },
35
35
  "dependencies": {
36
- "@unhead/schema": "0.6.5",
36
+ "@unhead/schema": "0.6.7",
37
37
  "hookable": "^5.4.1"
38
38
  },
39
39
  "devDependencies": {
40
- "unhead": "0.6.5",
40
+ "unhead": "0.6.7",
41
41
  "vue": "^3.2.45"
42
42
  },
43
43
  "scripts": {
@@ -1,164 +0,0 @@
1
- 'use strict';
2
-
3
- const TagsWithInnerContent = ["script", "style", "noscript"];
4
-
5
- const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
6
- const ArrayMetaProperties = [
7
- "og:image",
8
- "og:video",
9
- "og:audio",
10
- "og:locale:alternate",
11
- "video:actor",
12
- "video:director",
13
- "video:writer",
14
- "video:tag",
15
- "article:author",
16
- "article:tag",
17
- "book:tag",
18
- "book:author",
19
- "music:album",
20
- "music:musician"
21
- ];
22
- function tagDedupeKey(tag) {
23
- const { props, tag: tagName } = tag;
24
- if (UniqueTags.includes(tagName))
25
- return tagName;
26
- if (tagName === "link" && props.rel === "canonical")
27
- return "canonical";
28
- if (props.charset)
29
- return "charset";
30
- const name = ["id"];
31
- if (tagName === "meta")
32
- name.push(...["name", "property", "http-equiv"]);
33
- for (const n of name) {
34
- if (typeof props[n] !== "undefined") {
35
- const val = String(props[n]);
36
- if (ArrayMetaProperties.findIndex((p) => val.startsWith(p)) !== -1)
37
- return false;
38
- return `${tagName}:${n}:${val}`;
39
- }
40
- }
41
- return false;
42
- }
43
-
44
- async function renderDOMHead(head, options = {}) {
45
- const ctx = { shouldRender: true };
46
- await head.hooks.callHook("dom:beforeRender", ctx);
47
- if (!ctx.shouldRender)
48
- return;
49
- const dom = options.document || window.document;
50
- const staleSideEffects = head._popSideEffectQueue();
51
- head.headEntries().map((entry) => entry._sde).forEach((sde) => {
52
- Object.entries(sde).forEach(([key, fn]) => {
53
- staleSideEffects[key] = fn;
54
- });
55
- });
56
- const renderTag = (tag, entry) => {
57
- if (tag.tag === "title" && tag.children) {
58
- dom.title = tag.children;
59
- return dom.head.querySelector("title");
60
- }
61
- const tagRenderId = tag._d || tag._p;
62
- const markSideEffect = (key, fn) => {
63
- key = `${tagRenderId}:${key}`;
64
- entry._sde[key] = fn;
65
- delete staleSideEffects[key];
66
- };
67
- const setAttrs = ($el, sideEffects = true) => {
68
- Object.entries(tag.props).forEach(([k, value]) => {
69
- value = String(value);
70
- const attrSdeKey = `attr:${k}`;
71
- if (k === "class") {
72
- for (const c of value.split(" ")) {
73
- const classSdeKey = `${attrSdeKey}:${c}`;
74
- sideEffects && markSideEffect(classSdeKey, () => $el.classList.remove(c));
75
- if (!$el.classList.contains(c))
76
- $el.classList.add(c);
77
- }
78
- return;
79
- }
80
- if (sideEffects && !k.startsWith("data-h-"))
81
- markSideEffect(attrSdeKey, () => $el.removeAttribute(k));
82
- if ($el.getAttribute(k) !== value)
83
- $el.setAttribute(k, value);
84
- });
85
- if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
86
- $el.innerHTML = tag.children || "";
87
- };
88
- if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
89
- const $el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
90
- setAttrs($el);
91
- return $el;
92
- }
93
- let $newEl = dom.createElement(tag.tag);
94
- setAttrs($newEl, false);
95
- let $previousEl = head._elMap[tagRenderId];
96
- const $target = dom[tag.tagPosition?.startsWith("body") ? "body" : "head"];
97
- if (!$previousEl && tag._hash) {
98
- $previousEl = $target.querySelector(`${tag.tag}[data-h-${tag._hash}]`);
99
- }
100
- if (!$previousEl) {
101
- for (const $el of tag.tagPosition === "bodyClose" ? [...$target.children].reverse() : $target.children) {
102
- const elTag = $el.tagName.toLowerCase();
103
- if (elTag !== tag.tag)
104
- continue;
105
- const key = tagDedupeKey({
106
- tag: elTag,
107
- props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
108
- });
109
- if (key === tag._d || $el.isEqualNode($newEl)) {
110
- $previousEl = $el;
111
- break;
112
- }
113
- }
114
- }
115
- const markEl = ($el) => {
116
- head._elMap[tagRenderId] = $el;
117
- markSideEffect("el", () => {
118
- $el?.remove();
119
- delete head._elMap[tagRenderId];
120
- });
121
- };
122
- if ($previousEl) {
123
- markEl($previousEl);
124
- setAttrs($previousEl, false);
125
- return $previousEl;
126
- }
127
- switch (tag.tagPosition) {
128
- case "bodyClose":
129
- $newEl = dom.body.appendChild($newEl);
130
- break;
131
- case "bodyOpen":
132
- $newEl = dom.body.insertBefore($newEl, dom.body.firstChild);
133
- break;
134
- case "head":
135
- default:
136
- $newEl = dom.head.appendChild($newEl);
137
- break;
138
- }
139
- markEl($newEl);
140
- return $newEl;
141
- };
142
- for (const tag of await head.resolveTags()) {
143
- const entry = head.headEntries().find((e) => e._i === Number(tag._e));
144
- const renderCtx = { $el: null, shouldRender: true, tag, entry, queuedSideEffects: staleSideEffects };
145
- await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
146
- if (!renderCtx.shouldRender)
147
- continue;
148
- renderCtx.$el = renderTag(renderCtx.tag, renderCtx.entry);
149
- await head.hooks.callHook("dom:renderTag", renderCtx);
150
- }
151
- Object.values(staleSideEffects).forEach((fn) => fn());
152
- }
153
- exports.domUpdatePromise = null;
154
- async function debouncedRenderDOMHead(head, options = {}) {
155
- function doDomUpdate() {
156
- exports.domUpdatePromise = null;
157
- return renderDOMHead(head, options);
158
- }
159
- const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
160
- return exports.domUpdatePromise = exports.domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
161
- }
162
-
163
- exports.debouncedRenderDOMHead = debouncedRenderDOMHead;
164
- exports.renderDOMHead = renderDOMHead;
@@ -1,161 +0,0 @@
1
- const TagsWithInnerContent = ["script", "style", "noscript"];
2
-
3
- const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
4
- const ArrayMetaProperties = [
5
- "og:image",
6
- "og:video",
7
- "og:audio",
8
- "og:locale:alternate",
9
- "video:actor",
10
- "video:director",
11
- "video:writer",
12
- "video:tag",
13
- "article:author",
14
- "article:tag",
15
- "book:tag",
16
- "book:author",
17
- "music:album",
18
- "music:musician"
19
- ];
20
- function tagDedupeKey(tag) {
21
- const { props, tag: tagName } = tag;
22
- if (UniqueTags.includes(tagName))
23
- return tagName;
24
- if (tagName === "link" && props.rel === "canonical")
25
- return "canonical";
26
- if (props.charset)
27
- return "charset";
28
- const name = ["id"];
29
- if (tagName === "meta")
30
- name.push(...["name", "property", "http-equiv"]);
31
- for (const n of name) {
32
- if (typeof props[n] !== "undefined") {
33
- const val = String(props[n]);
34
- if (ArrayMetaProperties.findIndex((p) => val.startsWith(p)) !== -1)
35
- return false;
36
- return `${tagName}:${n}:${val}`;
37
- }
38
- }
39
- return false;
40
- }
41
-
42
- async function renderDOMHead(head, options = {}) {
43
- const ctx = { shouldRender: true };
44
- await head.hooks.callHook("dom:beforeRender", ctx);
45
- if (!ctx.shouldRender)
46
- return;
47
- const dom = options.document || window.document;
48
- const staleSideEffects = head._popSideEffectQueue();
49
- head.headEntries().map((entry) => entry._sde).forEach((sde) => {
50
- Object.entries(sde).forEach(([key, fn]) => {
51
- staleSideEffects[key] = fn;
52
- });
53
- });
54
- const renderTag = (tag, entry) => {
55
- if (tag.tag === "title" && tag.children) {
56
- dom.title = tag.children;
57
- return dom.head.querySelector("title");
58
- }
59
- const tagRenderId = tag._d || tag._p;
60
- const markSideEffect = (key, fn) => {
61
- key = `${tagRenderId}:${key}`;
62
- entry._sde[key] = fn;
63
- delete staleSideEffects[key];
64
- };
65
- const setAttrs = ($el, sideEffects = true) => {
66
- Object.entries(tag.props).forEach(([k, value]) => {
67
- value = String(value);
68
- const attrSdeKey = `attr:${k}`;
69
- if (k === "class") {
70
- for (const c of value.split(" ")) {
71
- const classSdeKey = `${attrSdeKey}:${c}`;
72
- sideEffects && markSideEffect(classSdeKey, () => $el.classList.remove(c));
73
- if (!$el.classList.contains(c))
74
- $el.classList.add(c);
75
- }
76
- return;
77
- }
78
- if (sideEffects && !k.startsWith("data-h-"))
79
- markSideEffect(attrSdeKey, () => $el.removeAttribute(k));
80
- if ($el.getAttribute(k) !== value)
81
- $el.setAttribute(k, value);
82
- });
83
- if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
84
- $el.innerHTML = tag.children || "";
85
- };
86
- if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
87
- const $el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
88
- setAttrs($el);
89
- return $el;
90
- }
91
- let $newEl = dom.createElement(tag.tag);
92
- setAttrs($newEl, false);
93
- let $previousEl = head._elMap[tagRenderId];
94
- const $target = dom[tag.tagPosition?.startsWith("body") ? "body" : "head"];
95
- if (!$previousEl && tag._hash) {
96
- $previousEl = $target.querySelector(`${tag.tag}[data-h-${tag._hash}]`);
97
- }
98
- if (!$previousEl) {
99
- for (const $el of tag.tagPosition === "bodyClose" ? [...$target.children].reverse() : $target.children) {
100
- const elTag = $el.tagName.toLowerCase();
101
- if (elTag !== tag.tag)
102
- continue;
103
- const key = tagDedupeKey({
104
- tag: elTag,
105
- props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
106
- });
107
- if (key === tag._d || $el.isEqualNode($newEl)) {
108
- $previousEl = $el;
109
- break;
110
- }
111
- }
112
- }
113
- const markEl = ($el) => {
114
- head._elMap[tagRenderId] = $el;
115
- markSideEffect("el", () => {
116
- $el?.remove();
117
- delete head._elMap[tagRenderId];
118
- });
119
- };
120
- if ($previousEl) {
121
- markEl($previousEl);
122
- setAttrs($previousEl, false);
123
- return $previousEl;
124
- }
125
- switch (tag.tagPosition) {
126
- case "bodyClose":
127
- $newEl = dom.body.appendChild($newEl);
128
- break;
129
- case "bodyOpen":
130
- $newEl = dom.body.insertBefore($newEl, dom.body.firstChild);
131
- break;
132
- case "head":
133
- default:
134
- $newEl = dom.head.appendChild($newEl);
135
- break;
136
- }
137
- markEl($newEl);
138
- return $newEl;
139
- };
140
- for (const tag of await head.resolveTags()) {
141
- const entry = head.headEntries().find((e) => e._i === Number(tag._e));
142
- const renderCtx = { $el: null, shouldRender: true, tag, entry, queuedSideEffects: staleSideEffects };
143
- await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
144
- if (!renderCtx.shouldRender)
145
- continue;
146
- renderCtx.$el = renderTag(renderCtx.tag, renderCtx.entry);
147
- await head.hooks.callHook("dom:renderTag", renderCtx);
148
- }
149
- Object.values(staleSideEffects).forEach((fn) => fn());
150
- }
151
- let domUpdatePromise = null;
152
- async function debouncedRenderDOMHead(head, options = {}) {
153
- function doDomUpdate() {
154
- domUpdatePromise = null;
155
- return renderDOMHead(head, options);
156
- }
157
- const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
158
- return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
159
- }
160
-
161
- export { debouncedRenderDOMHead, domUpdatePromise, renderDOMHead };