Skip to content

Commit fc3ca46

Browse files
author
Sascha Dobschal
committed
security and performance fixes by AI
1 parent aba578b commit fc3ca46

4 files changed

Lines changed: 396 additions & 103 deletions

File tree

example/example.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const element = html`
5252
<button onclick="${() => (elementShown.value = !elementShown.value)}">
5353
Toggle
5454
</button>
55-
${() => elementShown.value ? `<span>This is a hidden element.</span>` : ""}
55+
${() => elementShown.value ? html`<span>This is a hidden element.</span>` : ""}
5656
</p>
5757
5858
<h2>Render list of HTML elements dynamically based on observable array</h2>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dobschal/html.js",
3-
"version": "1.0.6",
3+
"version": "1.1.0",
44
"main": "index.js",
55
"scripts": {
66
"dev": "vite",

src/html.js

Lines changed: 93 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@ function convertStringToDomNodes(htmlString) {
88
return Array.from(fakeParent.content.childNodes);
99
}
1010

11+
function resolveReactive(arg, callback) {
12+
if (arg?.isObservable) return arg.subscribe(callback);
13+
if (typeof arg === "function") return Computed(arg).subscribe(callback);
14+
callback(arg);
15+
return null;
16+
}
17+
18+
function isInsideTag(str) {
19+
let inTag = false;
20+
let quote = null;
21+
for (let j = 0; j < str.length; j++) {
22+
const ch = str[j];
23+
if (quote) {
24+
if (ch === quote) quote = null;
25+
} else if (inTag) {
26+
if (ch === '"' || ch === "'") {
27+
quote = ch;
28+
} else if (ch === '>') {
29+
inTag = false;
30+
}
31+
} else if (ch === '<') {
32+
inTag = true;
33+
}
34+
}
35+
return inTag;
36+
}
37+
1138
function isArrayOfSupportedValues(value) {
1239
return Array.isArray(value)
1340
&& value.every((item) => ["string", "number", "boolean"].includes(typeof item) || item instanceof HTMLElement || item instanceof Node);
@@ -28,7 +55,7 @@ function replaceStringOrHTMLElement(placeholderNode, value) {
2855
if (item instanceof HTMLElement || item instanceof Node) {
2956
domNodes.push(item);
3057
} else {
31-
domNodes.push(...convertStringToDomNodes(item));
58+
domNodes.push(document.createTextNode(String(item)));
3259
}
3360
}
3461
placeholderNode.replaceWith(...domNodes);
@@ -37,39 +64,23 @@ function replaceStringOrHTMLElement(placeholderNode, value) {
3764
placeholderNode.replaceWith(value);
3865
return [value];
3966
} else if (["string", "number", "boolean"].includes(typeof value)) {
40-
const domNodes = convertStringToDomNodes(value);
41-
placeholderNode.replaceWith(...domNodes);
42-
return domNodes;
67+
const textNode = document.createTextNode(String(value));
68+
placeholderNode.replaceWith(textNode);
69+
return [textNode];
4370
} else {
4471
throw new Error("Unsupported type for template placeholder: " + value);
4572
}
4673
}
4774

4875
function replacePlaceholderNode(placeholderNode, arg) {
49-
if (arg?.isObservable) {
50-
let elements = [placeholderNode];
51-
arg.subscribe((value) => {
52-
for (let i = 1; i < elements.length; i++) {
53-
const element = elements[i];
54-
element.remove();
55-
}
56-
elements = replaceStringOrHTMLElement(elements[0], value);
57-
});
58-
return;
59-
}
60-
if (typeof arg === "function") {
61-
const computed = Computed(arg);
62-
let elements = [placeholderNode];
63-
computed.subscribe((value) => {
64-
for (let i = 1; i < elements.length; i++) {
65-
const element = elements[i];
66-
element.remove();
67-
}
68-
elements = replaceStringOrHTMLElement(elements[0], value);
69-
});
70-
return;
71-
}
72-
replaceStringOrHTMLElement(placeholderNode, arg);
76+
let elements = [placeholderNode];
77+
return resolveReactive(arg, (value) => {
78+
for (let i = 1; i < elements.length; i++) {
79+
const element = elements[i];
80+
element.remove();
81+
}
82+
elements = replaceStringOrHTMLElement(elements[0], value);
83+
});
7384
}
7485

7586
function makePlaceholderId(i, instanceId) {
@@ -108,27 +119,12 @@ function findNodeByAttributeKey(fakeParent, attributeKey) {
108119
}
109120

110121
function handleClassList(node, arg, placeholder) {
111-
if (arg?.isObservable) {
112-
let currentClass = placeholder;
113-
arg.subscribe((value) => {
114-
if (currentClass) node.classList.remove(currentClass);
115-
if (value) node.classList.add(value);
116-
currentClass = value;
117-
});
118-
return;
119-
}
120-
if (typeof arg === "function") {
121-
const computed = Computed(arg);
122-
let currentClass = placeholder;
123-
computed.subscribe((value) => {
124-
if (currentClass) node.classList.remove(currentClass);
125-
if (value) node.classList.add(value);
126-
currentClass = value;
127-
});
128-
return;
129-
}
130-
if(arg) node.classList.add(arg);
131-
node.classList.remove(placeholder);
122+
let currentClass = placeholder;
123+
return resolveReactive(arg, (value) => {
124+
if (currentClass) node.classList.remove(currentClass);
125+
if (value) node.classList.add(value);
126+
currentClass = value;
127+
});
132128
}
133129

134130
// this handles the case that the whole attribute including key and value is
@@ -137,8 +133,16 @@ function handleClassList(node, arg, placeholder) {
137133
function handleDynamicAttribute(node, arg, placeholder) {
138134

139135
function update(val) {
140-
let [key, value] = val.split("=", 2);
141-
value = (value ?? "''").slice(1, -1);
136+
const eqIndex = val.indexOf("=");
137+
let key, value;
138+
if (eqIndex === -1) {
139+
key = val;
140+
value = "";
141+
} else {
142+
key = val.slice(0, eqIndex);
143+
value = val.slice(eqIndex + 1);
144+
value = value.slice(1, -1);
145+
}
142146
const lastKey = node.getAttribute(placeholder);
143147
if (lastKey && node.hasAttribute(lastKey)) {
144148
node.removeAttribute(lastKey);
@@ -149,18 +153,7 @@ function handleDynamicAttribute(node, arg, placeholder) {
149153
}
150154
}
151155

152-
if (arg?.isObservable) {
153-
arg.subscribe((value) => update(value));
154-
return;
155-
}
156-
157-
if (typeof arg === "function") {
158-
const computed = Computed(arg);
159-
computed.subscribe((value) => update(value));
160-
return;
161-
}
162-
163-
update(arg);
156+
return resolveReactive(arg, (value) => update(value));
164157
}
165158

166159
function handleIfAttribute(node, attributeKey, arg) {
@@ -198,44 +191,25 @@ function handleIfAttribute(node, attributeKey, arg) {
198191
}
199192
}
200193

201-
if (arg?.isObservable) {
202-
arg.subscribe((value) => update(value));
203-
return;
204-
}
205-
206-
if (typeof arg === "function") {
207-
const computed = Computed(arg);
208-
computed.subscribe((value) => update(value));
209-
return;
210-
}
211-
212-
update(arg);
194+
return resolveReactive(arg, (value) => update(value));
213195
}
214196

215197
function replaceAttributePlaceholder(node, attributeKey, arg, placeholder) {
216198

217199
// If no attribute key is given, the whole attribute will be replaced
218200
if(!attributeKey) {
219-
handleDynamicAttribute(node, arg, placeholder);
220-
return;
221-
}
222-
223-
if (attributeKey === "if") {
224-
handleIfAttribute(node, attributeKey, arg);
225-
return;
201+
return handleDynamicAttribute(node, arg, placeholder);
226202
}
227203

228-
if (attributeKey === "if-not") {
229-
handleIfAttribute(node, attributeKey, arg);
230-
return;
204+
if (attributeKey === "if" || attributeKey === "if-not") {
205+
return handleIfAttribute(node, attributeKey, arg);
231206
}
232207

233208
if (attributeKey === "class") {
234209
if (!node.classList.contains(placeholder)) {
235210
throw new Error("Fatal: Could not find placeholder in class attribute: " + placeholder);
236211
}
237-
handleClassList(node, arg, placeholder);
238-
return;
212+
return handleClassList(node, arg, placeholder);
239213
}
240214

241215
if (attributeKey.startsWith("on")) {
@@ -244,31 +218,32 @@ function replaceAttributePlaceholder(node, attributeKey, arg, placeholder) {
244218
}
245219
node.addEventListener(attributeKey.slice(2), arg);
246220
node.removeAttribute(attributeKey);
247-
return;
221+
return null;
248222
}
249223
const [before, after] = node.getAttribute(attributeKey).split(placeholder);
250224

251225
if (arg?.isObservable) {
252226
if (attributeKey === "value") {
253227
node.addEventListener("input", (event) => arg.value = event.target.value);
254228
}
255-
arg.subscribe((value) => {
229+
const unsub = arg.subscribe((value) => {
256230
setNodeAttribute(node, attributeKey, before + value + after);
257231
placeholder = value;
258232
});
259-
return;
233+
return unsub;
260234
}
261235

262236
if (typeof arg === "function") {
263237
const computed = Computed(arg);
264-
computed.subscribe((value) => {
238+
const unsub = computed.subscribe((value) => {
265239
setNodeAttribute(node, attributeKey, before + value + after);
266240
placeholder = value;
267241
});
268-
return;
242+
return unsub;
269243
}
270244

271245
setNodeAttribute(node, attributeKey, before + arg + after);
246+
return null;
272247
}
273248

274249
function setNodeAttribute(node, attributeKey, value) {
@@ -284,6 +259,7 @@ function html(templateParts, ...args) {
284259

285260
const instanceId = id++;
286261
const htmlPlaceholderIndices = new Set();
262+
const unsubscribes = [];
287263

288264
const htmlWithPlaceholders = templateParts.reduce((acc, part, i) => {
289265
if (i === 0) {
@@ -298,9 +274,7 @@ function html(templateParts, ...args) {
298274
}
299275
args[i] = "";
300276
}
301-
const amountCloseTags = ((acc + part).match(/>/g) || []).length;
302-
const amountOpenTags = ((acc + part).match(/</g) || []).length;
303-
const isAttribute = amountCloseTags !== amountOpenTags;
277+
const isAttribute = isInsideTag(acc + part);
304278
if (isAttribute) {
305279
return acc + part + makePlaceholderId(i, instanceId);
306280
}
@@ -317,7 +291,8 @@ function html(templateParts, ...args) {
317291
if (!placeholderNode) {
318292
throw new Error("Fatal: Could not find placeholder for argument: " + i);
319293
}
320-
replacePlaceholderNode(placeholderNode, arg);
294+
const unsub = replacePlaceholderNode(placeholderNode, arg);
295+
if (unsub) unsubscribes.push(unsub);
321296
} else {
322297
let [node, attributeKey] = findNodeByAttributeValue(fakeParent.content, makePlaceholderId(i, instanceId));
323298

@@ -331,14 +306,33 @@ function html(templateParts, ...args) {
331306
}
332307

333308
if (node.tagName === "HOLD-PASS") {
334-
setTimeout(() => replaceAttributePlaceholder(node, attributeKey, arg, makePlaceholderId(i, instanceId)));
309+
setTimeout(() => {
310+
const unsub = replaceAttributePlaceholder(node, attributeKey, arg, makePlaceholderId(i, instanceId));
311+
if (unsub) unsubscribes.push(unsub);
312+
});
335313
} else {
336-
replaceAttributePlaceholder(node, attributeKey, arg, makePlaceholderId(i, instanceId));
314+
const unsub = replaceAttributePlaceholder(node, attributeKey, arg, makePlaceholderId(i, instanceId));
315+
if (unsub) unsubscribes.push(unsub);
337316
}
338317
}
339318
});
340319

341-
return fakeParent.content.childNodes.length > 1 ? Array.from(fakeParent.content.childNodes) : fakeParent.content.firstChild;
320+
const result = fakeParent.content.childNodes.length > 1 ? Array.from(fakeParent.content.childNodes) : fakeParent.content.firstChild;
321+
322+
function dispose() {
323+
for (const unsub of unsubscribes) {
324+
if (typeof unsub === "function") unsub();
325+
}
326+
unsubscribes.length = 0;
327+
}
328+
329+
if (Array.isArray(result)) {
330+
result.dispose = dispose;
331+
} else if (result) {
332+
result.dispose = dispose;
333+
}
334+
335+
return result;
342336
}
343337

344338
customElements.define("hold-pass", class extends HTMLElement {

0 commit comments

Comments
 (0)