@unhead/vue 0.6.3 → 0.6.5

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.
@@ -0,0 +1,164 @@
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;
@@ -0,0 +1,161 @@
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 };