@unhead/dom 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,6 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  const TagsWithInnerContent = ["script", "style", "noscript"];
4
+ const HasElementTags = [
5
+ "base",
6
+ "meta",
7
+ "link",
8
+ "style",
9
+ "script",
10
+ "noscript"
11
+ ];
4
12
 
5
13
  const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
6
14
  const ArrayMetaProperties = [
@@ -41,6 +49,39 @@ function tagDedupeKey(tag) {
41
49
  return false;
42
50
  }
43
51
 
52
+ const setAttrs = (ctx, markSideEffect) => {
53
+ const { tag, $el } = ctx;
54
+ if (!$el)
55
+ return;
56
+ Object.entries(tag.props).forEach(([k, value]) => {
57
+ value = String(value);
58
+ const attrSdeKey = `attr:${k}`;
59
+ if (k === "class") {
60
+ for (const c of value.split(" ")) {
61
+ const classSdeKey = `${attrSdeKey}:${c}`;
62
+ if (markSideEffect)
63
+ markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
64
+ if (!$el.classList.contains(c))
65
+ $el.classList.add(c);
66
+ }
67
+ return;
68
+ }
69
+ if (markSideEffect && !k.startsWith("data-h-"))
70
+ markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
71
+ if ($el.getAttribute(k) !== value)
72
+ $el.setAttribute(k, value);
73
+ });
74
+ if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
75
+ $el.innerHTML = tag.children || "";
76
+ };
77
+
78
+ function hashCode(s) {
79
+ let h = 9;
80
+ for (let i = 0; i < s.length; )
81
+ h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
82
+ return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
83
+ }
84
+
44
85
  async function renderDOMHead(head, options = {}) {
45
86
  const ctx = { shouldRender: true };
46
87
  await head.hooks.callHook("dom:beforeRender", ctx);
@@ -53,101 +94,108 @@ async function renderDOMHead(head, options = {}) {
53
94
  staleSideEffects[key] = fn;
54
95
  });
55
96
  });
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 || "";
97
+ const preRenderTag = async (tag) => {
98
+ const entry = head.headEntries().find((e) => e._i === tag._e);
99
+ const renderCtx = {
100
+ renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
101
+ $el: null,
102
+ shouldRender: true,
103
+ tag,
104
+ entry,
105
+ staleSideEffects
87
106
  };
107
+ await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
108
+ return renderCtx;
109
+ };
110
+ const renders = [];
111
+ const pendingRenders = {
112
+ body: [],
113
+ head: []
114
+ };
115
+ const markSideEffect = (ctx2, key, fn) => {
116
+ key = `${ctx2.renderId}:${key}`;
117
+ if (ctx2.entry)
118
+ ctx2.entry._sde[key] = fn;
119
+ delete staleSideEffects[key];
120
+ };
121
+ const markEl = (ctx2) => {
122
+ head._elMap[ctx2.renderId] = ctx2.$el;
123
+ renders.push(ctx2);
124
+ markSideEffect(ctx2, "el", () => {
125
+ ctx2.$el?.remove();
126
+ delete head._elMap[ctx2.renderId];
127
+ });
128
+ };
129
+ for (const t of await head.resolveTags()) {
130
+ const ctx2 = await preRenderTag(t);
131
+ if (!ctx2.shouldRender)
132
+ continue;
133
+ const { tag } = ctx2;
134
+ if (tag.tag === "title") {
135
+ dom.title = tag.children || "";
136
+ renders.push(ctx2);
137
+ continue;
138
+ }
88
139
  if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
89
- const $el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
90
- setAttrs($el);
91
- return $el;
140
+ ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
141
+ setAttrs(ctx2, markSideEffect);
142
+ renders.push(ctx2);
143
+ continue;
92
144
  }
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}]`);
145
+ ctx2.$el = head._elMap[ctx2.renderId];
146
+ if (!ctx2.$el && tag._hash) {
147
+ ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
99
148
  }
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
- }
149
+ if (ctx2.$el) {
150
+ if (ctx2.tag._d)
151
+ setAttrs(ctx2);
152
+ markEl(ctx2);
153
+ continue;
114
154
  }
115
- const markEl = ($el) => {
116
- head._elMap[tagRenderId] = $el;
117
- markSideEffect("el", () => {
118
- $el?.remove();
119
- delete head._elMap[tagRenderId];
155
+ ctx2.$el = dom.createElement(tag.tag);
156
+ setAttrs(ctx2);
157
+ pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
158
+ }
159
+ Object.entries(pendingRenders).forEach(([pos, queue]) => {
160
+ if (!queue.length)
161
+ return;
162
+ for (const $el of [...dom[pos].children].reverse()) {
163
+ const elTag = $el.tagName.toLowerCase();
164
+ if (!HasElementTags.includes(elTag))
165
+ continue;
166
+ const dedupeKey = tagDedupeKey({
167
+ tag: elTag,
168
+ props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
120
169
  });
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;
170
+ const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode(ctx2.$el)));
171
+ if (matchIdx !== -1) {
172
+ const ctx2 = queue[matchIdx];
173
+ ctx2.$el = $el;
174
+ setAttrs(ctx2);
175
+ markEl(ctx2);
176
+ delete queue[matchIdx];
177
+ }
138
178
  }
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
- }
179
+ queue.forEach((ctx2) => {
180
+ if (!ctx2.$el)
181
+ return;
182
+ switch (ctx2.tag.tagPosition) {
183
+ case "bodyClose":
184
+ dom.body.appendChild(ctx2.$el);
185
+ break;
186
+ case "bodyOpen":
187
+ dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
188
+ break;
189
+ case "head":
190
+ default:
191
+ dom.head.appendChild(ctx2.$el);
192
+ break;
193
+ }
194
+ markEl(ctx2);
195
+ });
196
+ });
197
+ for (const ctx2 of renders)
198
+ await head.hooks.callHook("dom:renderTag", ctx2);
151
199
  Object.values(staleSideEffects).forEach((fn) => fn());
152
200
  }
153
201
  exports.domUpdatePromise = null;
@@ -161,4 +209,5 @@ async function debouncedRenderDOMHead(head, options = {}) {
161
209
  }
162
210
 
163
211
  exports.debouncedRenderDOMHead = debouncedRenderDOMHead;
212
+ exports.hashCode = hashCode;
164
213
  exports.renderDOMHead = renderDOMHead;
package/dist/index.d.ts CHANGED
@@ -21,4 +21,6 @@ declare function debouncedRenderDOMHead<T extends Unhead<any>>(head: T, options?
21
21
  delayFn?: (fn: () => void) => void;
22
22
  }): Promise<void>;
23
23
 
24
- export { RenderDomHeadOptions, debouncedRenderDOMHead, domUpdatePromise, renderDOMHead };
24
+ declare function hashCode(s: string): string;
25
+
26
+ export { RenderDomHeadOptions, debouncedRenderDOMHead, domUpdatePromise, hashCode, renderDOMHead };
package/dist/index.mjs CHANGED
@@ -1,4 +1,12 @@
1
1
  const TagsWithInnerContent = ["script", "style", "noscript"];
2
+ const HasElementTags = [
3
+ "base",
4
+ "meta",
5
+ "link",
6
+ "style",
7
+ "script",
8
+ "noscript"
9
+ ];
2
10
 
3
11
  const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
4
12
  const ArrayMetaProperties = [
@@ -39,6 +47,39 @@ function tagDedupeKey(tag) {
39
47
  return false;
40
48
  }
41
49
 
50
+ const setAttrs = (ctx, markSideEffect) => {
51
+ const { tag, $el } = ctx;
52
+ if (!$el)
53
+ return;
54
+ Object.entries(tag.props).forEach(([k, value]) => {
55
+ value = String(value);
56
+ const attrSdeKey = `attr:${k}`;
57
+ if (k === "class") {
58
+ for (const c of value.split(" ")) {
59
+ const classSdeKey = `${attrSdeKey}:${c}`;
60
+ if (markSideEffect)
61
+ markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
62
+ if (!$el.classList.contains(c))
63
+ $el.classList.add(c);
64
+ }
65
+ return;
66
+ }
67
+ if (markSideEffect && !k.startsWith("data-h-"))
68
+ markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
69
+ if ($el.getAttribute(k) !== value)
70
+ $el.setAttribute(k, value);
71
+ });
72
+ if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
73
+ $el.innerHTML = tag.children || "";
74
+ };
75
+
76
+ function hashCode(s) {
77
+ let h = 9;
78
+ for (let i = 0; i < s.length; )
79
+ h = Math.imul(h ^ s.charCodeAt(i++), 9 ** 9);
80
+ return ((h ^ h >>> 9) + 65536).toString(16).substring(1, 8).toLowerCase();
81
+ }
82
+
42
83
  async function renderDOMHead(head, options = {}) {
43
84
  const ctx = { shouldRender: true };
44
85
  await head.hooks.callHook("dom:beforeRender", ctx);
@@ -51,101 +92,108 @@ async function renderDOMHead(head, options = {}) {
51
92
  staleSideEffects[key] = fn;
52
93
  });
53
94
  });
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 || "";
95
+ const preRenderTag = async (tag) => {
96
+ const entry = head.headEntries().find((e) => e._i === tag._e);
97
+ const renderCtx = {
98
+ renderId: tag._d || hashCode(JSON.stringify({ ...tag, _e: void 0, _p: void 0 })),
99
+ $el: null,
100
+ shouldRender: true,
101
+ tag,
102
+ entry,
103
+ staleSideEffects
85
104
  };
105
+ await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
106
+ return renderCtx;
107
+ };
108
+ const renders = [];
109
+ const pendingRenders = {
110
+ body: [],
111
+ head: []
112
+ };
113
+ const markSideEffect = (ctx2, key, fn) => {
114
+ key = `${ctx2.renderId}:${key}`;
115
+ if (ctx2.entry)
116
+ ctx2.entry._sde[key] = fn;
117
+ delete staleSideEffects[key];
118
+ };
119
+ const markEl = (ctx2) => {
120
+ head._elMap[ctx2.renderId] = ctx2.$el;
121
+ renders.push(ctx2);
122
+ markSideEffect(ctx2, "el", () => {
123
+ ctx2.$el?.remove();
124
+ delete head._elMap[ctx2.renderId];
125
+ });
126
+ };
127
+ for (const t of await head.resolveTags()) {
128
+ const ctx2 = await preRenderTag(t);
129
+ if (!ctx2.shouldRender)
130
+ continue;
131
+ const { tag } = ctx2;
132
+ if (tag.tag === "title") {
133
+ dom.title = tag.children || "";
134
+ renders.push(ctx2);
135
+ continue;
136
+ }
86
137
  if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
87
- const $el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
88
- setAttrs($el);
89
- return $el;
138
+ ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
139
+ setAttrs(ctx2, markSideEffect);
140
+ renders.push(ctx2);
141
+ continue;
90
142
  }
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}]`);
143
+ ctx2.$el = head._elMap[ctx2.renderId];
144
+ if (!ctx2.$el && tag._hash) {
145
+ ctx2.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._hash}]`);
97
146
  }
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
- }
147
+ if (ctx2.$el) {
148
+ if (ctx2.tag._d)
149
+ setAttrs(ctx2);
150
+ markEl(ctx2);
151
+ continue;
112
152
  }
113
- const markEl = ($el) => {
114
- head._elMap[tagRenderId] = $el;
115
- markSideEffect("el", () => {
116
- $el?.remove();
117
- delete head._elMap[tagRenderId];
153
+ ctx2.$el = dom.createElement(tag.tag);
154
+ setAttrs(ctx2);
155
+ pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx2);
156
+ }
157
+ Object.entries(pendingRenders).forEach(([pos, queue]) => {
158
+ if (!queue.length)
159
+ return;
160
+ for (const $el of [...dom[pos].children].reverse()) {
161
+ const elTag = $el.tagName.toLowerCase();
162
+ if (!HasElementTags.includes(elTag))
163
+ continue;
164
+ const dedupeKey = tagDedupeKey({
165
+ tag: elTag,
166
+ props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
118
167
  });
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;
168
+ const matchIdx = queue.findIndex((ctx2) => ctx2 && (ctx2.tag._d === dedupeKey || $el.isEqualNode(ctx2.$el)));
169
+ if (matchIdx !== -1) {
170
+ const ctx2 = queue[matchIdx];
171
+ ctx2.$el = $el;
172
+ setAttrs(ctx2);
173
+ markEl(ctx2);
174
+ delete queue[matchIdx];
175
+ }
136
176
  }
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
- }
177
+ queue.forEach((ctx2) => {
178
+ if (!ctx2.$el)
179
+ return;
180
+ switch (ctx2.tag.tagPosition) {
181
+ case "bodyClose":
182
+ dom.body.appendChild(ctx2.$el);
183
+ break;
184
+ case "bodyOpen":
185
+ dom.body.insertBefore(ctx2.$el, dom.body.firstChild);
186
+ break;
187
+ case "head":
188
+ default:
189
+ dom.head.appendChild(ctx2.$el);
190
+ break;
191
+ }
192
+ markEl(ctx2);
193
+ });
194
+ });
195
+ for (const ctx2 of renders)
196
+ await head.hooks.callHook("dom:renderTag", ctx2);
149
197
  Object.values(staleSideEffects).forEach((fn) => fn());
150
198
  }
151
199
  let domUpdatePromise = null;
@@ -158,4 +206,4 @@ async function debouncedRenderDOMHead(head, options = {}) {
158
206
  return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
159
207
  }
160
208
 
161
- export { debouncedRenderDOMHead, domUpdatePromise, renderDOMHead };
209
+ export { debouncedRenderDOMHead, domUpdatePromise, hashCode, renderDOMHead };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unhead/dom",
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",
@@ -30,7 +30,7 @@
30
30
  "dist"
31
31
  ],
32
32
  "dependencies": {
33
- "@unhead/schema": "0.6.5"
33
+ "@unhead/schema": "0.6.7"
34
34
  },
35
35
  "devDependencies": {
36
36
  "zhead": "1.0.0-beta.13"