@syntrologie/adapt-content 2.8.0-canary.164 → 2.8.0-canary.166
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/runtime.js +425 -366
- package/dist/runtime.js.map +7 -0
- package/dist/schema.js +285 -113
- package/dist/schema.js.map +7 -0
- package/package.json +2 -2
- package/dist/reconciliation-guard.js +0 -80
- package/dist/sanitizer.js +0 -95
- package/dist/summarize.js +0 -88
- package/dist/types.js +0 -7
package/dist/runtime.js
CHANGED
|
@@ -1,390 +1,449 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
// src/reconciliation-guard.ts
|
|
2
|
+
function guardAgainstReconciliation(container, anchor, reinsertFn, opts) {
|
|
3
|
+
const maxRetries = opts?.maxRetries ?? 3;
|
|
4
|
+
const debounceMs = opts?.debounceMs ?? 50;
|
|
5
|
+
const observeTarget = container.parentElement ?? anchor.parentElement;
|
|
6
|
+
if (!observeTarget) return () => {
|
|
7
|
+
};
|
|
8
|
+
let retries = 0;
|
|
9
|
+
let debounceTimer = null;
|
|
10
|
+
let disconnected = false;
|
|
11
|
+
const observer = new MutationObserver((mutations) => {
|
|
12
|
+
if (disconnected) return;
|
|
13
|
+
for (const mutation of mutations) {
|
|
14
|
+
for (const removed of mutation.removedNodes) {
|
|
15
|
+
if (removed !== container) continue;
|
|
16
|
+
if (retries >= maxRetries) {
|
|
17
|
+
observer.disconnect();
|
|
18
|
+
disconnected = true;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
22
|
+
debounceTimer = setTimeout(() => {
|
|
23
|
+
if (disconnected) return;
|
|
24
|
+
if (!anchor.isConnected) {
|
|
25
|
+
observer.disconnect();
|
|
26
|
+
disconnected = true;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
retries++;
|
|
30
|
+
try {
|
|
31
|
+
reinsertFn();
|
|
32
|
+
} catch {
|
|
33
|
+
observer.disconnect();
|
|
34
|
+
disconnected = true;
|
|
35
|
+
}
|
|
36
|
+
}, debounceMs);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
19
39
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
});
|
|
41
|
+
observer.observe(observeTarget, { childList: true, subtree: true });
|
|
42
|
+
return () => {
|
|
43
|
+
disconnected = true;
|
|
44
|
+
observer.disconnect();
|
|
45
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/sanitizer.ts
|
|
50
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
51
|
+
"b",
|
|
52
|
+
"strong",
|
|
53
|
+
"i",
|
|
54
|
+
"em",
|
|
55
|
+
"u",
|
|
56
|
+
"span",
|
|
57
|
+
"div",
|
|
58
|
+
"p",
|
|
59
|
+
"br",
|
|
60
|
+
"ul",
|
|
61
|
+
"ol",
|
|
62
|
+
"li",
|
|
63
|
+
"code",
|
|
64
|
+
"pre",
|
|
65
|
+
"small",
|
|
66
|
+
"sup",
|
|
67
|
+
"sub",
|
|
68
|
+
"a",
|
|
69
|
+
"button",
|
|
70
|
+
// SVG elements (for inline Lucide icons in config HTML)
|
|
71
|
+
"svg",
|
|
72
|
+
"path",
|
|
73
|
+
"circle",
|
|
74
|
+
"line",
|
|
75
|
+
"polyline",
|
|
76
|
+
"polygon",
|
|
77
|
+
"rect",
|
|
78
|
+
"g"
|
|
79
|
+
]);
|
|
80
|
+
function sanitizeHtml(html) {
|
|
81
|
+
const hasNative = typeof window.Sanitizer === "function";
|
|
82
|
+
if (hasNative) {
|
|
83
|
+
try {
|
|
84
|
+
const s = new window.Sanitizer({});
|
|
85
|
+
const frag = s.sanitizeToFragment(html);
|
|
86
|
+
const div = document.createElement("div");
|
|
87
|
+
div.append(frag);
|
|
88
|
+
return div.innerHTML;
|
|
89
|
+
} catch {
|
|
23
90
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
existing.remove();
|
|
91
|
+
}
|
|
92
|
+
const tpl = document.createElement("template");
|
|
93
|
+
tpl.innerHTML = html;
|
|
94
|
+
const root = tpl.content;
|
|
95
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
|
|
96
|
+
const toRemove = [];
|
|
97
|
+
while (walker.nextNode()) {
|
|
98
|
+
const el = walker.currentNode;
|
|
99
|
+
const tag = el.tagName.toLowerCase();
|
|
100
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
101
|
+
toRemove.push(el);
|
|
102
|
+
continue;
|
|
37
103
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
case 'before':
|
|
48
|
-
anchorEl.insertAdjacentElement('beforebegin', container);
|
|
49
|
-
break;
|
|
50
|
-
case 'after':
|
|
51
|
-
anchorEl.insertAdjacentElement('afterend', container);
|
|
52
|
-
break;
|
|
53
|
-
case 'prepend':
|
|
54
|
-
anchorEl.insertBefore(container, anchorEl.firstChild);
|
|
55
|
-
break;
|
|
56
|
-
case 'append':
|
|
57
|
-
anchorEl.appendChild(container);
|
|
58
|
-
break;
|
|
59
|
-
case 'replace':
|
|
60
|
-
originalContent = anchorEl.innerHTML;
|
|
61
|
-
anchorEl.replaceWith(container);
|
|
62
|
-
break;
|
|
104
|
+
for (const attr of Array.from(el.attributes)) {
|
|
105
|
+
const name = attr.name.toLowerCase();
|
|
106
|
+
const value = attr.value.trim().toLowerCase();
|
|
107
|
+
const isEvent = name.startsWith("on");
|
|
108
|
+
const isUrlAttr = name === "href" || name === "src" || name === "formaction";
|
|
109
|
+
const isDangerousUrl = isUrlAttr && (value.startsWith("javascript:") || value.startsWith("vbscript:") || value.startsWith("data:text/html"));
|
|
110
|
+
if (isEvent || isDangerousUrl) {
|
|
111
|
+
el.removeAttribute(attr.name);
|
|
112
|
+
}
|
|
63
113
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const handle = window.SynOS?.handle;
|
|
70
|
-
if (handle) {
|
|
71
|
-
handle.open();
|
|
72
|
-
handle.runtime?.events?.publish('notification.deep_link', { tileId, itemId });
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
container.style.cursor = 'pointer';
|
|
76
|
-
container.addEventListener('click', deepLinkHandler);
|
|
114
|
+
}
|
|
115
|
+
const svgs = Array.from(root.querySelectorAll("svg"));
|
|
116
|
+
for (const svg of svgs) {
|
|
117
|
+
if (toRemove.some((el) => svg.contains(el) && el.tagName.toLowerCase() === "script")) {
|
|
118
|
+
toRemove.push(svg);
|
|
77
119
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
position: action.position,
|
|
83
|
-
});
|
|
84
|
-
// Guard against React reconciliation removing our container.
|
|
85
|
-
// The reinsert function re-applies the same insertion strategy.
|
|
86
|
-
const reinsertFn = () => {
|
|
87
|
-
switch (action.position) {
|
|
88
|
-
case 'before':
|
|
89
|
-
anchorEl.insertAdjacentElement('beforebegin', container);
|
|
90
|
-
break;
|
|
91
|
-
case 'after':
|
|
92
|
-
anchorEl.insertAdjacentElement('afterend', container);
|
|
93
|
-
break;
|
|
94
|
-
case 'prepend':
|
|
95
|
-
anchorEl.insertBefore(container, anchorEl.firstChild);
|
|
96
|
-
break;
|
|
97
|
-
case 'append':
|
|
98
|
-
anchorEl.appendChild(container);
|
|
99
|
-
break;
|
|
100
|
-
case 'replace':
|
|
101
|
-
// Cannot re-insert for replace — anchor was already replaced
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
const guardCleanup = guardAgainstReconciliation(container, anchorEl, reinsertFn);
|
|
106
|
-
return {
|
|
107
|
-
cleanup: () => {
|
|
108
|
-
if (deepLinkHandler) {
|
|
109
|
-
container.removeEventListener('click', deepLinkHandler);
|
|
110
|
-
}
|
|
111
|
-
guardCleanup();
|
|
112
|
-
// Skip DOM mutations if nodes are already detached (SPA navigation)
|
|
113
|
-
if (!container.isConnected)
|
|
114
|
-
return;
|
|
115
|
-
try {
|
|
116
|
-
if (action.position === 'replace' && originalContent !== null) {
|
|
117
|
-
// Restore original element
|
|
118
|
-
const restoredEl = document.createElement(anchorEl.tagName);
|
|
119
|
-
restoredEl.innerHTML = originalContent;
|
|
120
|
-
// Copy attributes
|
|
121
|
-
Array.from(anchorEl.attributes).forEach((attr) => {
|
|
122
|
-
restoredEl.setAttribute(attr.name, attr.value);
|
|
123
|
-
});
|
|
124
|
-
container.replaceWith(restoredEl);
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
container.remove();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// DOM nodes already removed by host framework — safe to ignore
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
updateFn: (changes) => {
|
|
135
|
-
if ('html' in changes && typeof changes.html === 'string') {
|
|
136
|
-
container.innerHTML = sanitizeHtml(changes.html);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
};
|
|
141
|
-
/**
|
|
142
|
-
* Walk the DOM to find the deepest descendant that uniquely carries
|
|
143
|
-
* text content. Avoids destroying sibling elements (icons, images)
|
|
144
|
-
* when setting text on a container like a button.
|
|
145
|
-
*/
|
|
146
|
-
function findTextTarget(el) {
|
|
147
|
-
if (el.children.length === 0)
|
|
148
|
-
return el;
|
|
149
|
-
const textChildren = Array.from(el.children).filter((child) => child.textContent?.trim());
|
|
150
|
-
if (textChildren.length === 1) {
|
|
151
|
-
const child = textChildren[0];
|
|
152
|
-
if (child.textContent?.trim() === el.textContent?.trim()) {
|
|
153
|
-
return findTextTarget(child);
|
|
154
|
-
}
|
|
120
|
+
}
|
|
121
|
+
for (const el of toRemove) {
|
|
122
|
+
while (el.firstChild) {
|
|
123
|
+
el.parentNode?.insertBefore(el.firstChild, el);
|
|
155
124
|
}
|
|
156
|
-
|
|
125
|
+
el.remove();
|
|
126
|
+
}
|
|
127
|
+
return tpl.innerHTML;
|
|
157
128
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
129
|
+
|
|
130
|
+
// src/runtime.ts
|
|
131
|
+
var executeInsertHtml = async (action, context) => {
|
|
132
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
133
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
134
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
|
|
135
|
+
}
|
|
136
|
+
if (!anchorEl) {
|
|
137
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
138
|
+
return { cleanup: () => {
|
|
139
|
+
} };
|
|
140
|
+
}
|
|
141
|
+
const sanitizedHtml = sanitizeHtml(action.html);
|
|
142
|
+
const dedupAttr = "data-syntro-insert-label";
|
|
143
|
+
const label = action.label;
|
|
144
|
+
if (label) {
|
|
145
|
+
const escapedLabel = CSS.escape(label);
|
|
146
|
+
const searchRoot = anchorEl.parentElement ?? anchorEl;
|
|
147
|
+
const existing = searchRoot.querySelector(`[${dedupAttr}="${escapedLabel}"]`);
|
|
148
|
+
if (existing) existing.remove();
|
|
149
|
+
}
|
|
150
|
+
const container = document.createElement("div");
|
|
151
|
+
container.setAttribute("data-syntro-action-id", context.generateId());
|
|
152
|
+
if (label) container.setAttribute(dedupAttr, label);
|
|
153
|
+
container.innerHTML = sanitizedHtml;
|
|
154
|
+
let originalContent = null;
|
|
155
|
+
switch (action.position) {
|
|
156
|
+
case "before":
|
|
157
|
+
anchorEl.insertAdjacentElement("beforebegin", container);
|
|
158
|
+
break;
|
|
159
|
+
case "after":
|
|
160
|
+
anchorEl.insertAdjacentElement("afterend", container);
|
|
161
|
+
break;
|
|
162
|
+
case "prepend":
|
|
163
|
+
anchorEl.insertBefore(container, anchorEl.firstChild);
|
|
164
|
+
break;
|
|
165
|
+
case "append":
|
|
166
|
+
anchorEl.appendChild(container);
|
|
167
|
+
break;
|
|
168
|
+
case "replace":
|
|
169
|
+
originalContent = anchorEl.innerHTML;
|
|
170
|
+
anchorEl.replaceWith(container);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
let deepLinkHandler = null;
|
|
174
|
+
if (action.deepLink) {
|
|
175
|
+
const { tileId, itemId } = action.deepLink;
|
|
176
|
+
deepLinkHandler = () => {
|
|
177
|
+
const handle = window.SynOS?.handle;
|
|
178
|
+
if (handle) {
|
|
179
|
+
handle.open();
|
|
180
|
+
handle.runtime?.events?.publish("notification.deep_link", { tileId, itemId });
|
|
181
|
+
}
|
|
191
182
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
183
|
+
container.style.cursor = "pointer";
|
|
184
|
+
container.addEventListener("click", deepLinkHandler);
|
|
185
|
+
}
|
|
186
|
+
context.publishEvent("action.applied", {
|
|
187
|
+
id: context.generateId(),
|
|
188
|
+
kind: "content:insertHtml",
|
|
189
|
+
anchorId: action.anchorId,
|
|
190
|
+
position: action.position
|
|
191
|
+
});
|
|
192
|
+
const reinsertFn = () => {
|
|
193
|
+
switch (action.position) {
|
|
194
|
+
case "before":
|
|
195
|
+
anchorEl.insertAdjacentElement("beforebegin", container);
|
|
196
|
+
break;
|
|
197
|
+
case "after":
|
|
198
|
+
anchorEl.insertAdjacentElement("afterend", container);
|
|
199
|
+
break;
|
|
200
|
+
case "prepend":
|
|
201
|
+
anchorEl.insertBefore(container, anchorEl.firstChild);
|
|
202
|
+
break;
|
|
203
|
+
case "append":
|
|
204
|
+
anchorEl.appendChild(container);
|
|
205
|
+
break;
|
|
206
|
+
case "replace":
|
|
207
|
+
break;
|
|
209
208
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
};
|
|
210
|
+
const guardCleanup = guardAgainstReconciliation(container, anchorEl, reinsertFn);
|
|
211
|
+
return {
|
|
212
|
+
cleanup: () => {
|
|
213
|
+
if (deepLinkHandler) {
|
|
214
|
+
container.removeEventListener("click", deepLinkHandler);
|
|
215
|
+
}
|
|
216
|
+
guardCleanup();
|
|
217
|
+
if (!container.isConnected) return;
|
|
218
|
+
try {
|
|
219
|
+
if (action.position === "replace" && originalContent !== null) {
|
|
220
|
+
const restoredEl = document.createElement(anchorEl.tagName);
|
|
221
|
+
restoredEl.innerHTML = originalContent;
|
|
222
|
+
Array.from(anchorEl.attributes).forEach((attr) => {
|
|
223
|
+
restoredEl.setAttribute(attr.name, attr.value);
|
|
224
|
+
});
|
|
225
|
+
container.replaceWith(restoredEl);
|
|
226
|
+
} else {
|
|
227
|
+
container.remove();
|
|
218
228
|
}
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
updateFn: (changes) => {
|
|
233
|
+
if ("html" in changes && typeof changes.html === "string") {
|
|
234
|
+
container.innerHTML = sanitizeHtml(changes.html);
|
|
235
|
+
}
|
|
219
236
|
}
|
|
220
|
-
|
|
221
|
-
const originalValue = anchorEl.getAttribute(action.attr);
|
|
222
|
-
const hadAttribute = anchorEl.hasAttribute(action.attr);
|
|
223
|
-
// Set new attribute
|
|
224
|
-
anchorEl.setAttribute(action.attr, action.value);
|
|
225
|
-
context.publishEvent('action.applied', {
|
|
226
|
-
id: context.generateId(),
|
|
227
|
-
kind: 'content:setAttr',
|
|
228
|
-
anchorId: action.anchorId,
|
|
229
|
-
attr: action.attr,
|
|
230
|
-
});
|
|
231
|
-
return {
|
|
232
|
-
cleanup: () => {
|
|
233
|
-
if (!anchorEl.isConnected)
|
|
234
|
-
return;
|
|
235
|
-
if (hadAttribute && originalValue !== null) {
|
|
236
|
-
anchorEl.setAttribute(action.attr, originalValue);
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
anchorEl.removeAttribute(action.attr);
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
updateFn: (changes) => {
|
|
243
|
-
if ('value' in changes && typeof changes.value === 'string') {
|
|
244
|
-
anchorEl.setAttribute(action.attr, changes.value);
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
};
|
|
237
|
+
};
|
|
248
238
|
};
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
239
|
+
function findTextTarget(el) {
|
|
240
|
+
if (el.children.length === 0) return el;
|
|
241
|
+
const textChildren = Array.from(el.children).filter((child) => child.textContent?.trim());
|
|
242
|
+
if (textChildren.length === 1) {
|
|
243
|
+
const child = textChildren[0];
|
|
244
|
+
if (child.textContent?.trim() === el.textContent?.trim()) {
|
|
245
|
+
return findTextTarget(child);
|
|
256
246
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
247
|
+
}
|
|
248
|
+
return el;
|
|
249
|
+
}
|
|
250
|
+
var executeSetText = async (action, context) => {
|
|
251
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
252
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
253
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
|
|
254
|
+
}
|
|
255
|
+
if (!anchorEl) {
|
|
256
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
257
|
+
return { cleanup: () => {
|
|
258
|
+
} };
|
|
259
|
+
}
|
|
260
|
+
const textTarget = findTextTarget(anchorEl);
|
|
261
|
+
const originalText = textTarget.textContent ?? "";
|
|
262
|
+
textTarget.textContent = action.text;
|
|
263
|
+
context.publishEvent("action.applied", {
|
|
264
|
+
id: context.generateId(),
|
|
265
|
+
kind: "content:setText",
|
|
266
|
+
anchorId: action.anchorId
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
cleanup: () => {
|
|
270
|
+
if (!anchorEl.isConnected) return;
|
|
271
|
+
textTarget.textContent = originalText;
|
|
272
|
+
},
|
|
273
|
+
updateFn: (changes) => {
|
|
274
|
+
if ("text" in changes && typeof changes.text === "string") {
|
|
275
|
+
textTarget.textContent = changes.text;
|
|
276
|
+
}
|
|
260
277
|
}
|
|
261
|
-
|
|
262
|
-
const hadClass = anchorEl.classList.contains(action.className);
|
|
263
|
-
// Add class
|
|
264
|
-
anchorEl.classList.add(action.className);
|
|
265
|
-
context.publishEvent('action.applied', {
|
|
266
|
-
id: context.generateId(),
|
|
267
|
-
kind: 'content:addClass',
|
|
268
|
-
anchorId: action.anchorId,
|
|
269
|
-
className: action.className,
|
|
270
|
-
});
|
|
271
|
-
return {
|
|
272
|
-
cleanup: () => {
|
|
273
|
-
if (!anchorEl.isConnected)
|
|
274
|
-
return;
|
|
275
|
-
// Only remove if we added it
|
|
276
|
-
if (!hadClass) {
|
|
277
|
-
anchorEl.classList.remove(action.className);
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
};
|
|
278
|
+
};
|
|
281
279
|
};
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
var executeSetAttr = async (action, context) => {
|
|
281
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
282
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
283
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
|
|
284
|
+
}
|
|
285
|
+
if (!anchorEl) {
|
|
286
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
287
|
+
return { cleanup: () => {
|
|
288
|
+
} };
|
|
289
|
+
}
|
|
290
|
+
const lowerAttr = action.attr.toLowerCase();
|
|
291
|
+
if (lowerAttr.startsWith("on")) {
|
|
292
|
+
throw new Error(`Dangerous attribute not allowed: ${action.attr}`);
|
|
293
|
+
}
|
|
294
|
+
const isUrlAttr = lowerAttr === "href" || lowerAttr === "src" || lowerAttr === "formaction";
|
|
295
|
+
if (isUrlAttr) {
|
|
296
|
+
const lowerValue = action.value.trim().toLowerCase();
|
|
297
|
+
if (lowerValue.startsWith("javascript:") || lowerValue.startsWith("vbscript:") || lowerValue.startsWith("data:text/html")) {
|
|
298
|
+
throw new Error(`Dangerous URL not allowed in ${action.attr}: ${action.value}`);
|
|
289
299
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
300
|
+
}
|
|
301
|
+
const originalValue = anchorEl.getAttribute(action.attr);
|
|
302
|
+
const hadAttribute = anchorEl.hasAttribute(action.attr);
|
|
303
|
+
anchorEl.setAttribute(action.attr, action.value);
|
|
304
|
+
context.publishEvent("action.applied", {
|
|
305
|
+
id: context.generateId(),
|
|
306
|
+
kind: "content:setAttr",
|
|
307
|
+
anchorId: action.anchorId,
|
|
308
|
+
attr: action.attr
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
cleanup: () => {
|
|
312
|
+
if (!anchorEl.isConnected) return;
|
|
313
|
+
if (hadAttribute && originalValue !== null) {
|
|
314
|
+
anchorEl.setAttribute(action.attr, originalValue);
|
|
315
|
+
} else {
|
|
316
|
+
anchorEl.removeAttribute(action.attr);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
updateFn: (changes) => {
|
|
320
|
+
if ("value" in changes && typeof changes.value === "string") {
|
|
321
|
+
anchorEl.setAttribute(action.attr, changes.value);
|
|
322
|
+
}
|
|
293
323
|
}
|
|
294
|
-
|
|
295
|
-
const hadClass = anchorEl.classList.contains(action.className);
|
|
296
|
-
// Remove class
|
|
297
|
-
anchorEl.classList.remove(action.className);
|
|
298
|
-
context.publishEvent('action.applied', {
|
|
299
|
-
id: context.generateId(),
|
|
300
|
-
kind: 'content:removeClass',
|
|
301
|
-
anchorId: action.anchorId,
|
|
302
|
-
className: action.className,
|
|
303
|
-
});
|
|
304
|
-
return {
|
|
305
|
-
cleanup: () => {
|
|
306
|
-
if (!anchorEl.isConnected)
|
|
307
|
-
return;
|
|
308
|
-
// Only re-add if we removed it
|
|
309
|
-
if (hadClass) {
|
|
310
|
-
anchorEl.classList.add(action.className);
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
};
|
|
324
|
+
};
|
|
314
325
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
+
var executeAddClass = async (action, context) => {
|
|
327
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
328
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
329
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
|
|
330
|
+
}
|
|
331
|
+
if (!anchorEl) {
|
|
332
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
333
|
+
return { cleanup: () => {
|
|
334
|
+
} };
|
|
335
|
+
}
|
|
336
|
+
const hadClass = anchorEl.classList.contains(action.className);
|
|
337
|
+
anchorEl.classList.add(action.className);
|
|
338
|
+
context.publishEvent("action.applied", {
|
|
339
|
+
id: context.generateId(),
|
|
340
|
+
kind: "content:addClass",
|
|
341
|
+
anchorId: action.anchorId,
|
|
342
|
+
className: action.className
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
cleanup: () => {
|
|
346
|
+
if (!anchorEl.isConnected) return;
|
|
347
|
+
if (!hadClass) {
|
|
348
|
+
anchorEl.classList.remove(action.className);
|
|
349
|
+
}
|
|
326
350
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
var executeRemoveClass = async (action, context) => {
|
|
354
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
355
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
356
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
|
|
357
|
+
}
|
|
358
|
+
if (!anchorEl) {
|
|
359
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
360
|
+
return { cleanup: () => {
|
|
361
|
+
} };
|
|
362
|
+
}
|
|
363
|
+
const hadClass = anchorEl.classList.contains(action.className);
|
|
364
|
+
anchorEl.classList.remove(action.className);
|
|
365
|
+
context.publishEvent("action.applied", {
|
|
366
|
+
id: context.generateId(),
|
|
367
|
+
kind: "content:removeClass",
|
|
368
|
+
anchorId: action.anchorId,
|
|
369
|
+
className: action.className
|
|
370
|
+
});
|
|
371
|
+
return {
|
|
372
|
+
cleanup: () => {
|
|
373
|
+
if (!anchorEl.isConnected) return;
|
|
374
|
+
if (hadClass) {
|
|
375
|
+
anchorEl.classList.add(action.className);
|
|
376
|
+
}
|
|
332
377
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
var executeSetStyle = async (action, context) => {
|
|
381
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
382
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
383
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
|
|
384
|
+
}
|
|
385
|
+
if (!anchorEl) {
|
|
386
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
387
|
+
return { cleanup: () => {
|
|
388
|
+
} };
|
|
389
|
+
}
|
|
390
|
+
const originalStyles = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const prop of Object.keys(action.styles)) {
|
|
392
|
+
const current = anchorEl.style.getPropertyValue(prop);
|
|
393
|
+
originalStyles.set(prop, current);
|
|
394
|
+
}
|
|
395
|
+
for (const [prop, value] of Object.entries(action.styles)) {
|
|
396
|
+
anchorEl.style.setProperty(prop, value);
|
|
397
|
+
}
|
|
398
|
+
context.publishEvent("action.applied", {
|
|
399
|
+
id: context.generateId(),
|
|
400
|
+
kind: "content:setStyle",
|
|
401
|
+
anchorId: action.anchorId,
|
|
402
|
+
styles: Object.keys(action.styles)
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
cleanup: () => {
|
|
406
|
+
if (!anchorEl.isConnected) return;
|
|
407
|
+
for (const [prop, originalValue] of originalStyles) {
|
|
408
|
+
if (originalValue) {
|
|
409
|
+
anchorEl.style.setProperty(prop, originalValue);
|
|
410
|
+
} else {
|
|
411
|
+
anchorEl.style.removeProperty(prop);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
updateFn: (changes) => {
|
|
416
|
+
if ("styles" in changes && typeof changes.styles === "object" && changes.styles) {
|
|
417
|
+
for (const [prop, value] of Object.entries(changes.styles)) {
|
|
418
|
+
anchorEl.style.setProperty(prop, value);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
336
421
|
}
|
|
337
|
-
|
|
338
|
-
id: context.generateId(),
|
|
339
|
-
kind: 'content:setStyle',
|
|
340
|
-
anchorId: action.anchorId,
|
|
341
|
-
styles: Object.keys(action.styles),
|
|
342
|
-
});
|
|
343
|
-
return {
|
|
344
|
-
cleanup: () => {
|
|
345
|
-
if (!anchorEl.isConnected)
|
|
346
|
-
return;
|
|
347
|
-
// Restore original styles
|
|
348
|
-
for (const [prop, originalValue] of originalStyles) {
|
|
349
|
-
if (originalValue) {
|
|
350
|
-
anchorEl.style.setProperty(prop, originalValue);
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
anchorEl.style.removeProperty(prop);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
updateFn: (changes) => {
|
|
358
|
-
if ('styles' in changes && typeof changes.styles === 'object' && changes.styles) {
|
|
359
|
-
for (const [prop, value] of Object.entries(changes.styles)) {
|
|
360
|
-
anchorEl.style.setProperty(prop, value);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
},
|
|
364
|
-
};
|
|
422
|
+
};
|
|
365
423
|
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
export const executors = [
|
|
374
|
-
{ kind: 'content:insertHtml', executor: executeInsertHtml },
|
|
375
|
-
{ kind: 'content:setText', executor: executeSetText },
|
|
376
|
-
{ kind: 'content:setAttr', executor: executeSetAttr },
|
|
377
|
-
{ kind: 'content:addClass', executor: executeAddClass },
|
|
378
|
-
{ kind: 'content:removeClass', executor: executeRemoveClass },
|
|
379
|
-
{ kind: 'content:setStyle', executor: executeSetStyle },
|
|
424
|
+
var executors = [
|
|
425
|
+
{ kind: "content:insertHtml", executor: executeInsertHtml },
|
|
426
|
+
{ kind: "content:setText", executor: executeSetText },
|
|
427
|
+
{ kind: "content:setAttr", executor: executeSetAttr },
|
|
428
|
+
{ kind: "content:addClass", executor: executeAddClass },
|
|
429
|
+
{ kind: "content:removeClass", executor: executeRemoveClass },
|
|
430
|
+
{ kind: "content:setStyle", executor: executeSetStyle }
|
|
380
431
|
];
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
432
|
+
var runtime = {
|
|
433
|
+
id: "adaptive-content",
|
|
434
|
+
version: "1.0.0",
|
|
435
|
+
name: "Content",
|
|
436
|
+
description: "DOM manipulation for text, attributes, and styles",
|
|
437
|
+
executors
|
|
438
|
+
};
|
|
439
|
+
export {
|
|
440
|
+
executeAddClass,
|
|
441
|
+
executeInsertHtml,
|
|
442
|
+
executeRemoveClass,
|
|
443
|
+
executeSetAttr,
|
|
444
|
+
executeSetStyle,
|
|
445
|
+
executeSetText,
|
|
446
|
+
executors,
|
|
447
|
+
runtime
|
|
390
448
|
};
|
|
449
|
+
//# sourceMappingURL=runtime.js.map
|