@unhead/dom 0.6.6 → 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,102 +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
- if (entry)
65
- entry._sde[key] = fn;
66
- delete staleSideEffects[key];
67
- };
68
- const setAttrs = ($el, sideEffects = true) => {
69
- Object.entries(tag.props).forEach(([k, value]) => {
70
- value = String(value);
71
- const attrSdeKey = `attr:${k}`;
72
- if (k === "class") {
73
- for (const c of value.split(" ")) {
74
- const classSdeKey = `${attrSdeKey}:${c}`;
75
- sideEffects && markSideEffect(classSdeKey, () => $el.classList.remove(c));
76
- if (!$el.classList.contains(c))
77
- $el.classList.add(c);
78
- }
79
- return;
80
- }
81
- if (sideEffects && !k.startsWith("data-h-"))
82
- markSideEffect(attrSdeKey, () => $el.removeAttribute(k));
83
- if ($el.getAttribute(k) !== value)
84
- $el.setAttribute(k, value);
85
- });
86
- if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
87
- $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
88
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
+ }
89
139
  if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
90
- const $el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
91
- setAttrs($el);
92
- return $el;
140
+ ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
141
+ setAttrs(ctx2, markSideEffect);
142
+ renders.push(ctx2);
143
+ continue;
93
144
  }
94
- let $newEl = dom.createElement(tag.tag);
95
- setAttrs($newEl, false);
96
- let $previousEl = head._elMap[tagRenderId];
97
- const $target = dom[tag.tagPosition?.startsWith("body") ? "body" : "head"];
98
- if (!$previousEl && tag._hash) {
99
- $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}]`);
100
148
  }
101
- if (!$previousEl) {
102
- for (const $el of tag.tagPosition === "bodyClose" ? [...$target.children].reverse() : $target.children) {
103
- const elTag = $el.tagName.toLowerCase();
104
- if (elTag !== tag.tag)
105
- continue;
106
- const key = tagDedupeKey({
107
- tag: elTag,
108
- props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
109
- });
110
- if (key === tag._d || $el.isEqualNode($newEl)) {
111
- $previousEl = $el;
112
- break;
113
- }
114
- }
149
+ if (ctx2.$el) {
150
+ if (ctx2.tag._d)
151
+ setAttrs(ctx2);
152
+ markEl(ctx2);
153
+ continue;
115
154
  }
116
- const markEl = ($el) => {
117
- head._elMap[tagRenderId] = $el;
118
- markSideEffect("el", () => {
119
- $el?.remove();
120
- 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) }), {})
121
169
  });
122
- };
123
- if ($previousEl) {
124
- markEl($previousEl);
125
- setAttrs($previousEl, false);
126
- return $previousEl;
127
- }
128
- switch (tag.tagPosition) {
129
- case "bodyClose":
130
- $newEl = dom.body.appendChild($newEl);
131
- break;
132
- case "bodyOpen":
133
- $newEl = dom.body.insertBefore($newEl, dom.body.firstChild);
134
- break;
135
- case "head":
136
- default:
137
- $newEl = dom.head.appendChild($newEl);
138
- 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
+ }
139
178
  }
140
- markEl($newEl);
141
- return $newEl;
142
- };
143
- for (const tag of await head.resolveTags()) {
144
- const entry = head.headEntries().find((e) => e._i === tag._e);
145
- const renderCtx = { $el: null, shouldRender: true, tag, entry, queuedSideEffects: staleSideEffects };
146
- await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
147
- if (!renderCtx.shouldRender)
148
- continue;
149
- renderCtx.$el = renderTag(renderCtx.tag, renderCtx.entry);
150
- await head.hooks.callHook("dom:renderTag", renderCtx);
151
- }
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);
152
199
  Object.values(staleSideEffects).forEach((fn) => fn());
153
200
  }
154
201
  exports.domUpdatePromise = null;
@@ -162,4 +209,5 @@ async function debouncedRenderDOMHead(head, options = {}) {
162
209
  }
163
210
 
164
211
  exports.debouncedRenderDOMHead = debouncedRenderDOMHead;
212
+ exports.hashCode = hashCode;
165
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,102 +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
- if (entry)
63
- entry._sde[key] = fn;
64
- delete staleSideEffects[key];
65
- };
66
- const setAttrs = ($el, sideEffects = true) => {
67
- Object.entries(tag.props).forEach(([k, value]) => {
68
- value = String(value);
69
- const attrSdeKey = `attr:${k}`;
70
- if (k === "class") {
71
- for (const c of value.split(" ")) {
72
- const classSdeKey = `${attrSdeKey}:${c}`;
73
- sideEffects && markSideEffect(classSdeKey, () => $el.classList.remove(c));
74
- if (!$el.classList.contains(c))
75
- $el.classList.add(c);
76
- }
77
- return;
78
- }
79
- if (sideEffects && !k.startsWith("data-h-"))
80
- markSideEffect(attrSdeKey, () => $el.removeAttribute(k));
81
- if ($el.getAttribute(k) !== value)
82
- $el.setAttribute(k, value);
83
- });
84
- if (TagsWithInnerContent.includes(tag.tag) && $el.innerHTML !== (tag.children || ""))
85
- $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
86
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
+ }
87
137
  if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
88
- const $el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
89
- setAttrs($el);
90
- return $el;
138
+ ctx2.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
139
+ setAttrs(ctx2, markSideEffect);
140
+ renders.push(ctx2);
141
+ continue;
91
142
  }
92
- let $newEl = dom.createElement(tag.tag);
93
- setAttrs($newEl, false);
94
- let $previousEl = head._elMap[tagRenderId];
95
- const $target = dom[tag.tagPosition?.startsWith("body") ? "body" : "head"];
96
- if (!$previousEl && tag._hash) {
97
- $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}]`);
98
146
  }
99
- if (!$previousEl) {
100
- for (const $el of tag.tagPosition === "bodyClose" ? [...$target.children].reverse() : $target.children) {
101
- const elTag = $el.tagName.toLowerCase();
102
- if (elTag !== tag.tag)
103
- continue;
104
- const key = tagDedupeKey({
105
- tag: elTag,
106
- props: $el.getAttributeNames().reduce((props, name) => ({ ...props, [name]: $el.getAttribute(name) }), {})
107
- });
108
- if (key === tag._d || $el.isEqualNode($newEl)) {
109
- $previousEl = $el;
110
- break;
111
- }
112
- }
147
+ if (ctx2.$el) {
148
+ if (ctx2.tag._d)
149
+ setAttrs(ctx2);
150
+ markEl(ctx2);
151
+ continue;
113
152
  }
114
- const markEl = ($el) => {
115
- head._elMap[tagRenderId] = $el;
116
- markSideEffect("el", () => {
117
- $el?.remove();
118
- 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) }), {})
119
167
  });
120
- };
121
- if ($previousEl) {
122
- markEl($previousEl);
123
- setAttrs($previousEl, false);
124
- return $previousEl;
125
- }
126
- switch (tag.tagPosition) {
127
- case "bodyClose":
128
- $newEl = dom.body.appendChild($newEl);
129
- break;
130
- case "bodyOpen":
131
- $newEl = dom.body.insertBefore($newEl, dom.body.firstChild);
132
- break;
133
- case "head":
134
- default:
135
- $newEl = dom.head.appendChild($newEl);
136
- 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
+ }
137
176
  }
138
- markEl($newEl);
139
- return $newEl;
140
- };
141
- for (const tag of await head.resolveTags()) {
142
- const entry = head.headEntries().find((e) => e._i === tag._e);
143
- const renderCtx = { $el: null, shouldRender: true, tag, entry, queuedSideEffects: staleSideEffects };
144
- await head.hooks.callHook("dom:beforeRenderTag", renderCtx);
145
- if (!renderCtx.shouldRender)
146
- continue;
147
- renderCtx.$el = renderTag(renderCtx.tag, renderCtx.entry);
148
- await head.hooks.callHook("dom:renderTag", renderCtx);
149
- }
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);
150
197
  Object.values(staleSideEffects).forEach((fn) => fn());
151
198
  }
152
199
  let domUpdatePromise = null;
@@ -159,4 +206,4 @@ async function debouncedRenderDOMHead(head, options = {}) {
159
206
  return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
160
207
  }
161
208
 
162
- 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.6",
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.6"
33
+ "@unhead/schema": "0.6.7"
34
34
  },
35
35
  "devDependencies": {
36
36
  "zhead": "1.0.0-beta.13"