@syntrologie/adapt-content 2.8.0-canary.43 → 2.8.0-canary.430

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.
Files changed (110) hide show
  1. package/dist/runtime.d.ts.map +1 -1
  2. package/dist/runtime.js +437 -348
  3. package/dist/runtime.js.map +7 -0
  4. package/dist/sanitizer.d.ts.map +1 -1
  5. package/dist/schema.d.ts +56 -16
  6. package/dist/schema.d.ts.map +1 -1
  7. package/dist/schema.js +363 -62
  8. package/dist/schema.js.map +7 -0
  9. package/package.json +2 -20
  10. package/dist/cdn.d.ts +0 -35
  11. package/dist/cdn.d.ts.map +0 -1
  12. package/dist/cdn.js +0 -39
  13. package/dist/components/AnchorPicker.d.ts +0 -8
  14. package/dist/components/AnchorPicker.d.ts.map +0 -1
  15. package/dist/components/AnchorPicker.js +0 -6
  16. package/dist/content-editor-state.d.ts +0 -45
  17. package/dist/content-editor-state.d.ts.map +0 -1
  18. package/dist/content-editor-state.js +0 -123
  19. package/dist/content-editor-ui.d.ts +0 -26
  20. package/dist/content-editor-ui.d.ts.map +0 -1
  21. package/dist/content-editor-ui.js +0 -301
  22. package/dist/editor.d.ts +0 -9
  23. package/dist/editor.d.ts.map +0 -1
  24. package/dist/editor.js +0 -8
  25. package/dist/reconciliation-guard.js +0 -80
  26. package/dist/sanitizer.js +0 -95
  27. package/dist/summarize.js +0 -88
  28. package/dist/types.js +0 -7
  29. package/dist/utils/selectorGenerator.d.ts +0 -6
  30. package/dist/utils/selectorGenerator.d.ts.map +0 -1
  31. package/dist/utils/selectorGenerator.js +0 -4
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.d.ts +0 -2
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.d.ts.map +0 -1
  34. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.js +0 -3
  35. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts +0 -34
  36. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts.map +0 -1
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +0 -158
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts +0 -7
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts.map +0 -1
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.js +0 -9
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +0 -23
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +0 -1
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +0 -40
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +0 -7
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +0 -1
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +0 -22
  47. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.d.ts +0 -8
  48. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.d.ts.map +0 -1
  49. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.js +0 -9
  50. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts +0 -7
  51. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts.map +0 -1
  52. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.js +0 -4
  53. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.d.ts +0 -7
  54. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.d.ts.map +0 -1
  55. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.js +0 -4
  56. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts +0 -13
  57. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts.map +0 -1
  58. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.js +0 -15
  59. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts +0 -7
  60. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts.map +0 -1
  61. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.js +0 -4
  62. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.d.ts +0 -9
  63. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.d.ts.map +0 -1
  64. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.js +0 -4
  65. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts +0 -8
  66. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts.map +0 -1
  67. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.js +0 -8
  68. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.d.ts +0 -7
  69. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.d.ts.map +0 -1
  70. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.js +0 -4
  71. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +0 -25
  72. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +0 -1
  73. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +0 -379
  74. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts +0 -8
  75. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts.map +0 -1
  76. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.js +0 -8
  77. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts +0 -8
  78. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts.map +0 -1
  79. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.js +0 -17
  80. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts +0 -32
  81. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts.map +0 -1
  82. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +0 -85
  83. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.d.ts +0 -6
  84. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.d.ts.map +0 -1
  85. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.js +0 -4
  86. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.d.ts +0 -8
  87. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.d.ts.map +0 -1
  88. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.js +0 -5
  89. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +0 -12
  90. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +0 -1
  91. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +0 -40
  92. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +0 -26
  93. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +0 -1
  94. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +0 -202
  95. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.d.ts +0 -8
  96. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.d.ts.map +0 -1
  97. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.js +0 -46
  98. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +0 -24
  99. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +0 -1
  100. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.js +0 -86
  101. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +0 -34
  102. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +0 -1
  103. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +0 -25
  104. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.d.ts +0 -37
  105. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.d.ts.map +0 -1
  106. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.js +0 -52
  107. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.d.ts +0 -22
  108. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.d.ts.map +0 -1
  109. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.js +0 -143
  110. package/node_modules/@syntrologie/shared-editor-ui/package.json +0 -47
package/dist/runtime.js CHANGED
@@ -1,372 +1,461 @@
1
- /**
2
- * Adaptive Content - Runtime Module
3
- *
4
- * DOM manipulation actions: insertHtml, setText, setAttr, addClass, removeClass, setStyle.
5
- * These follow the hostPatcher snapshot pattern for safe reversibility.
6
- */
7
- import { guardAgainstReconciliation } from './reconciliation-guard';
8
- import { sanitizeHtml } from './sanitizer';
9
- // ============================================================================
10
- // Executors
11
- // ============================================================================
12
- /**
13
- * Execute an insertHtml action
14
- */
15
- export const executeInsertHtml = async (action, context) => {
16
- let anchorEl = context.resolveAnchor(action.anchorId);
17
- if (!anchorEl && context.waitForAnchor) {
18
- anchorEl = await context.waitForAnchor(action.anchorId, 3000);
19
- }
20
- if (!anchorEl) {
21
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
22
- return { cleanup: () => { } };
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
+ }
23
39
  }
24
- // Sanitize HTML content using context utility
25
- const sanitizedHtml = sanitizeHtml(action.html);
26
- // Dedup: if a container for this action already exists, remove it first.
27
- // Uses the action label as a stable identifier across re-applications.
28
- // Check both inside the anchor (prepend/append) and in the parent (before/after).
29
- const dedupAttr = 'data-syntro-insert-label';
30
- const label = action.label;
31
- if (label) {
32
- const escapedLabel = CSS.escape(label);
33
- const searchRoot = anchorEl.parentElement ?? anchorEl;
34
- const existing = searchRoot.querySelector(`[${dedupAttr}="${escapedLabel}"]`);
35
- if (existing)
36
- existing.remove();
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 normalizeUrlAttr(value) {
81
+ return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().toLowerCase();
82
+ }
83
+ function isDangerousUrlAttr(name, value) {
84
+ if (name !== "href" && name !== "src" && name !== "formaction") return false;
85
+ const normalized = normalizeUrlAttr(value);
86
+ return normalized.startsWith("javascript:") || normalized.startsWith("vbscript:") || normalized.startsWith("data:");
87
+ }
88
+ function sanitizeHtml(html) {
89
+ const hasNative = typeof window.Sanitizer === "function";
90
+ if (hasNative) {
91
+ try {
92
+ const s = new window.Sanitizer({});
93
+ const frag = s.sanitizeToFragment(html);
94
+ const div = document.createElement("div");
95
+ div.append(frag);
96
+ return div.innerHTML;
97
+ } catch {
37
98
  }
38
- // Create container for inserted content
39
- const container = document.createElement('div');
40
- container.setAttribute('data-syntro-action-id', context.generateId());
41
- if (label)
42
- container.setAttribute(dedupAttr, label);
43
- container.innerHTML = sanitizedHtml;
44
- // Keep track of original state for replace position
45
- let originalContent = null;
46
- switch (action.position) {
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;
99
+ }
100
+ const tpl = document.createElement("template");
101
+ tpl.innerHTML = html;
102
+ const root = tpl.content;
103
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
104
+ const toRemove = [];
105
+ while (walker.nextNode()) {
106
+ const el = walker.currentNode;
107
+ const tag = el.tagName.toLowerCase();
108
+ if (!ALLOWED_TAGS.has(tag)) {
109
+ toRemove.push(el);
110
+ continue;
63
111
  }
64
- // Deep-link click handler — opens canvas + publishes deep-link event
65
- let deepLinkHandler = null;
66
- if (action.deepLink) {
67
- const { tileId, itemId } = action.deepLink;
68
- deepLinkHandler = () => {
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);
112
+ for (const attr of Array.from(el.attributes)) {
113
+ const name = attr.name.toLowerCase();
114
+ const isEvent = name.startsWith("on");
115
+ if (isEvent || isDangerousUrlAttr(name, attr.value)) {
116
+ el.removeAttribute(attr.name);
117
+ }
77
118
  }
78
- context.publishEvent('action.applied', {
79
- id: context.generateId(),
80
- kind: 'content:insertHtml',
81
- anchorId: action.anchorId,
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
- * Execute a setText action
143
- */
144
- export const executeSetText = async (action, context) => {
145
- let anchorEl = context.resolveAnchor(action.anchorId);
146
- if (!anchorEl && context.waitForAnchor) {
147
- anchorEl = await context.waitForAnchor(action.anchorId, 3000);
119
+ }
120
+ const svgs = Array.from(root.querySelectorAll("svg"));
121
+ for (const svg of svgs) {
122
+ if (toRemove.some((el) => svg.contains(el) && el.tagName.toLowerCase() === "script")) {
123
+ toRemove.push(svg);
148
124
  }
149
- if (!anchorEl) {
150
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
151
- return { cleanup: () => { } };
125
+ }
126
+ for (const el of toRemove) {
127
+ while (el.firstChild) {
128
+ el.parentNode?.insertBefore(el.firstChild, el);
152
129
  }
153
- // Snapshot original text
154
- const originalText = anchorEl.textContent ?? '';
155
- // Set new text
156
- anchorEl.textContent = action.text;
157
- context.publishEvent('action.applied', {
158
- id: context.generateId(),
159
- kind: 'content:setText',
160
- anchorId: action.anchorId,
161
- });
162
- return {
163
- cleanup: () => {
164
- if (!anchorEl.isConnected)
165
- return;
166
- anchorEl.textContent = originalText;
167
- },
168
- updateFn: (changes) => {
169
- if ('text' in changes && typeof changes.text === 'string') {
170
- anchorEl.textContent = changes.text;
171
- }
172
- },
130
+ el.remove();
131
+ }
132
+ return tpl.innerHTML;
133
+ }
134
+
135
+ // src/runtime.ts
136
+ var executeInsertHtml = async (action, context) => {
137
+ let anchorEl = context.resolveAnchor(action.anchorId);
138
+ if (!anchorEl && context.waitForAnchor) {
139
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
140
+ }
141
+ if (!anchorEl) {
142
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
143
+ return { cleanup: () => {
144
+ } };
145
+ }
146
+ const sanitizedHtml = sanitizeHtml(action.html);
147
+ const dedupAttr = "data-syntro-insert-label";
148
+ const label = action.label;
149
+ if (label) {
150
+ const searchRoot = anchorEl.parentElement ?? anchorEl;
151
+ const existing = Array.from(searchRoot.querySelectorAll(`[${dedupAttr}]`)).find(
152
+ (candidate) => candidate.getAttribute(dedupAttr) === label
153
+ );
154
+ if (existing) existing.remove();
155
+ }
156
+ const container = document.createElement("div");
157
+ container.setAttribute("data-syntro-action-id", context.generateId());
158
+ if (label) container.setAttribute(dedupAttr, label);
159
+ container.innerHTML = sanitizedHtml;
160
+ let originalContent = null;
161
+ switch (action.position) {
162
+ case "before":
163
+ anchorEl.insertAdjacentElement("beforebegin", container);
164
+ break;
165
+ case "after":
166
+ anchorEl.insertAdjacentElement("afterend", container);
167
+ break;
168
+ case "prepend":
169
+ anchorEl.insertBefore(container, anchorEl.firstChild);
170
+ break;
171
+ case "append":
172
+ anchorEl.appendChild(container);
173
+ break;
174
+ case "replace":
175
+ originalContent = anchorEl.innerHTML;
176
+ anchorEl.replaceWith(container);
177
+ break;
178
+ }
179
+ let deepLinkHandler = null;
180
+ if (action.deepLink) {
181
+ const { tileId, itemId } = action.deepLink;
182
+ deepLinkHandler = () => {
183
+ const handle = window.SynOS?.handle;
184
+ if (handle) {
185
+ handle.open();
186
+ handle.runtime?.events?.publish("notification.deep_link", { tileId, itemId });
187
+ }
173
188
  };
174
- };
175
- /**
176
- * Execute a setAttr action
177
- */
178
- export const executeSetAttr = async (action, context) => {
179
- let anchorEl = context.resolveAnchor(action.anchorId);
180
- if (!anchorEl && context.waitForAnchor) {
181
- anchorEl = await context.waitForAnchor(action.anchorId, 3000);
182
- }
183
- if (!anchorEl) {
184
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
185
- return { cleanup: () => { } };
186
- }
187
- // Block dangerous attributes (case-insensitive)
188
- const lowerAttr = action.attr.toLowerCase();
189
- if (lowerAttr.startsWith('on')) {
190
- throw new Error(`Dangerous attribute not allowed: ${action.attr}`);
189
+ container.style.cursor = "pointer";
190
+ container.addEventListener("click", deepLinkHandler);
191
+ }
192
+ context.publishEvent("action.applied", {
193
+ id: context.generateId(),
194
+ kind: "content:insertHtml",
195
+ anchorId: action.anchorId,
196
+ position: action.position
197
+ });
198
+ const reinsertFn = () => {
199
+ switch (action.position) {
200
+ case "before":
201
+ anchorEl.insertAdjacentElement("beforebegin", container);
202
+ break;
203
+ case "after":
204
+ anchorEl.insertAdjacentElement("afterend", container);
205
+ break;
206
+ case "prepend":
207
+ anchorEl.insertBefore(container, anchorEl.firstChild);
208
+ break;
209
+ case "append":
210
+ anchorEl.appendChild(container);
211
+ break;
212
+ case "replace":
213
+ break;
191
214
  }
192
- // Block dangerous URIs in URL-bearing attributes
193
- const isUrlAttr = lowerAttr === 'href' || lowerAttr === 'src' || lowerAttr === 'formaction';
194
- if (isUrlAttr) {
195
- const lowerValue = action.value.trim().toLowerCase();
196
- if (lowerValue.startsWith('javascript:') ||
197
- lowerValue.startsWith('vbscript:') ||
198
- lowerValue.startsWith('data:text/html')) {
199
- throw new Error(`Dangerous URL not allowed in ${action.attr}: ${action.value}`);
215
+ };
216
+ const guardCleanup = guardAgainstReconciliation(container, anchorEl, reinsertFn);
217
+ return {
218
+ cleanup: () => {
219
+ if (deepLinkHandler) {
220
+ container.removeEventListener("click", deepLinkHandler);
221
+ }
222
+ guardCleanup();
223
+ if (!container.isConnected) return;
224
+ try {
225
+ if (action.position === "replace" && originalContent !== null) {
226
+ const restoredEl = document.createElement(anchorEl.tagName);
227
+ restoredEl.innerHTML = originalContent;
228
+ Array.from(anchorEl.attributes).forEach((attr) => {
229
+ restoredEl.setAttribute(attr.name, attr.value);
230
+ });
231
+ container.replaceWith(restoredEl);
232
+ } else {
233
+ container.remove();
200
234
  }
235
+ } catch {
236
+ }
237
+ },
238
+ updateFn: (changes) => {
239
+ if ("html" in changes && typeof changes.html === "string") {
240
+ container.innerHTML = sanitizeHtml(changes.html);
241
+ }
201
242
  }
202
- // Snapshot original attribute value
203
- const originalValue = anchorEl.getAttribute(action.attr);
204
- const hadAttribute = anchorEl.hasAttribute(action.attr);
205
- // Set new attribute
206
- anchorEl.setAttribute(action.attr, action.value);
207
- context.publishEvent('action.applied', {
208
- id: context.generateId(),
209
- kind: 'content:setAttr',
210
- anchorId: action.anchorId,
211
- attr: action.attr,
212
- });
213
- return {
214
- cleanup: () => {
215
- if (!anchorEl.isConnected)
216
- return;
217
- if (hadAttribute && originalValue !== null) {
218
- anchorEl.setAttribute(action.attr, originalValue);
219
- }
220
- else {
221
- anchorEl.removeAttribute(action.attr);
222
- }
223
- },
224
- updateFn: (changes) => {
225
- if ('value' in changes && typeof changes.value === 'string') {
226
- anchorEl.setAttribute(action.attr, changes.value);
227
- }
228
- },
229
- };
243
+ };
230
244
  };
231
- /**
232
- * Execute an addClass action
233
- */
234
- export const executeAddClass = async (action, context) => {
235
- let anchorEl = context.resolveAnchor(action.anchorId);
236
- if (!anchorEl && context.waitForAnchor) {
237
- anchorEl = await context.waitForAnchor(action.anchorId, 3000);
245
+ function findTextTarget(el) {
246
+ if (el.children.length === 0) return el;
247
+ const textChildren = Array.from(el.children).filter((child) => child.textContent?.trim());
248
+ if (textChildren.length === 1) {
249
+ const child = textChildren[0];
250
+ if (child.textContent?.trim() === el.textContent?.trim()) {
251
+ return findTextTarget(child);
238
252
  }
239
- if (!anchorEl) {
240
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
241
- return { cleanup: () => { } };
253
+ }
254
+ return el;
255
+ }
256
+ var executeSetText = async (action, context) => {
257
+ let anchorEl = context.resolveAnchor(action.anchorId);
258
+ if (!anchorEl && context.waitForAnchor) {
259
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
260
+ }
261
+ if (!anchorEl) {
262
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
263
+ return { cleanup: () => {
264
+ } };
265
+ }
266
+ const textTarget = findTextTarget(anchorEl);
267
+ const originalText = textTarget.textContent ?? "";
268
+ textTarget.textContent = action.text;
269
+ context.publishEvent("action.applied", {
270
+ id: context.generateId(),
271
+ kind: "content:setText",
272
+ anchorId: action.anchorId
273
+ });
274
+ return {
275
+ cleanup: () => {
276
+ if (!anchorEl.isConnected) return;
277
+ textTarget.textContent = originalText;
278
+ },
279
+ updateFn: (changes) => {
280
+ if ("text" in changes && typeof changes.text === "string") {
281
+ textTarget.textContent = changes.text;
282
+ }
242
283
  }
243
- // Check if class was already present
244
- const hadClass = anchorEl.classList.contains(action.className);
245
- // Add class
246
- anchorEl.classList.add(action.className);
247
- context.publishEvent('action.applied', {
248
- id: context.generateId(),
249
- kind: 'content:addClass',
250
- anchorId: action.anchorId,
251
- className: action.className,
252
- });
253
- return {
254
- cleanup: () => {
255
- if (!anchorEl.isConnected)
256
- return;
257
- // Only remove if we added it
258
- if (!hadClass) {
259
- anchorEl.classList.remove(action.className);
260
- }
261
- },
262
- };
284
+ };
263
285
  };
264
- /**
265
- * Execute a removeClass action
266
- */
267
- export const executeRemoveClass = async (action, context) => {
268
- let anchorEl = context.resolveAnchor(action.anchorId);
269
- if (!anchorEl && context.waitForAnchor) {
270
- anchorEl = await context.waitForAnchor(action.anchorId, 3000);
286
+ var executeSetAttr = async (action, context) => {
287
+ let anchorEl = context.resolveAnchor(action.anchorId);
288
+ if (!anchorEl && context.waitForAnchor) {
289
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
290
+ }
291
+ if (!anchorEl) {
292
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
293
+ return { cleanup: () => {
294
+ } };
295
+ }
296
+ const lowerAttr = action.attr.toLowerCase();
297
+ if (lowerAttr.startsWith("on")) {
298
+ throw new Error(`Dangerous attribute not allowed: ${action.attr}`);
299
+ }
300
+ const isUrlAttr = lowerAttr === "href" || lowerAttr === "src" || lowerAttr === "formaction";
301
+ if (isUrlAttr) {
302
+ const lowerValue = action.value.trim().toLowerCase();
303
+ if (lowerValue.startsWith("javascript:") || lowerValue.startsWith("vbscript:") || lowerValue.startsWith("data:text/html")) {
304
+ throw new Error(`Dangerous URL not allowed in ${action.attr}: ${action.value}`);
271
305
  }
272
- if (!anchorEl) {
273
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
274
- return { cleanup: () => { } };
306
+ }
307
+ const originalValue = anchorEl.getAttribute(action.attr);
308
+ const hadAttribute = anchorEl.hasAttribute(action.attr);
309
+ anchorEl.setAttribute(action.attr, action.value);
310
+ context.publishEvent("action.applied", {
311
+ id: context.generateId(),
312
+ kind: "content:setAttr",
313
+ anchorId: action.anchorId,
314
+ attr: action.attr
315
+ });
316
+ return {
317
+ cleanup: () => {
318
+ if (!anchorEl.isConnected) return;
319
+ if (hadAttribute && originalValue !== null) {
320
+ anchorEl.setAttribute(action.attr, originalValue);
321
+ } else {
322
+ anchorEl.removeAttribute(action.attr);
323
+ }
324
+ },
325
+ updateFn: (changes) => {
326
+ if ("value" in changes && typeof changes.value === "string") {
327
+ anchorEl.setAttribute(action.attr, changes.value);
328
+ }
275
329
  }
276
- // Check if class was present
277
- const hadClass = anchorEl.classList.contains(action.className);
278
- // Remove class
279
- anchorEl.classList.remove(action.className);
280
- context.publishEvent('action.applied', {
281
- id: context.generateId(),
282
- kind: 'content:removeClass',
283
- anchorId: action.anchorId,
284
- className: action.className,
285
- });
286
- return {
287
- cleanup: () => {
288
- if (!anchorEl.isConnected)
289
- return;
290
- // Only re-add if we removed it
291
- if (hadClass) {
292
- anchorEl.classList.add(action.className);
293
- }
294
- },
295
- };
330
+ };
296
331
  };
297
- /**
298
- * Execute a setStyle action
299
- */
300
- export const executeSetStyle = async (action, context) => {
301
- let anchorEl = context.resolveAnchor(action.anchorId);
302
- if (!anchorEl && context.waitForAnchor) {
303
- anchorEl = await context.waitForAnchor(action.anchorId, 3000);
332
+ var executeAddClass = async (action, context) => {
333
+ let anchorEl = context.resolveAnchor(action.anchorId);
334
+ if (!anchorEl && context.waitForAnchor) {
335
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
336
+ }
337
+ if (!anchorEl) {
338
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
339
+ return { cleanup: () => {
340
+ } };
341
+ }
342
+ const hadClass = anchorEl.classList.contains(action.className);
343
+ anchorEl.classList.add(action.className);
344
+ context.publishEvent("action.applied", {
345
+ id: context.generateId(),
346
+ kind: "content:addClass",
347
+ anchorId: action.anchorId,
348
+ className: action.className
349
+ });
350
+ return {
351
+ cleanup: () => {
352
+ if (!anchorEl.isConnected) return;
353
+ if (!hadClass) {
354
+ anchorEl.classList.remove(action.className);
355
+ }
304
356
  }
305
- if (!anchorEl) {
306
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
307
- return { cleanup: () => { } };
357
+ };
358
+ };
359
+ var executeRemoveClass = async (action, context) => {
360
+ let anchorEl = context.resolveAnchor(action.anchorId);
361
+ if (!anchorEl && context.waitForAnchor) {
362
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
363
+ }
364
+ if (!anchorEl) {
365
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
366
+ return { cleanup: () => {
367
+ } };
368
+ }
369
+ const hadClass = anchorEl.classList.contains(action.className);
370
+ anchorEl.classList.remove(action.className);
371
+ context.publishEvent("action.applied", {
372
+ id: context.generateId(),
373
+ kind: "content:removeClass",
374
+ anchorId: action.anchorId,
375
+ className: action.className
376
+ });
377
+ return {
378
+ cleanup: () => {
379
+ if (!anchorEl.isConnected) return;
380
+ if (hadClass) {
381
+ anchorEl.classList.add(action.className);
382
+ }
308
383
  }
309
- // Snapshot original styles
310
- const originalStyles = new Map();
311
- for (const prop of Object.keys(action.styles)) {
312
- const current = anchorEl.style.getPropertyValue(prop);
313
- originalStyles.set(prop, current);
384
+ };
385
+ };
386
+ var executeSetStyle = async (action, context) => {
387
+ let anchorEl = context.resolveAnchor(action.anchorId);
388
+ if (!anchorEl && context.waitForAnchor) {
389
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
390
+ }
391
+ if (!anchorEl) {
392
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
393
+ return { cleanup: () => {
394
+ } };
395
+ }
396
+ const originalStyles = /* @__PURE__ */ new Map();
397
+ for (const prop of Object.keys(action.styles)) {
398
+ const current = anchorEl.style.getPropertyValue(prop);
399
+ originalStyles.set(prop, current);
400
+ }
401
+ for (const [prop, value] of Object.entries(action.styles)) {
402
+ const el = anchorEl;
403
+ el.style.setProperty(prop, value);
404
+ if (value !== "" && el.style.getPropertyValue(prop) === "") {
405
+ console.warn(
406
+ `[adaptive-content] setStyle: '${prop}: ${value}' was silently dropped \u2014 '${prop}' is not a recognized CSS property. Use kebab-case (e.g. 'justify-content' not 'justifyContent').`
407
+ );
314
408
  }
315
- // Apply new styles
316
- for (const [prop, value] of Object.entries(action.styles)) {
317
- anchorEl.style.setProperty(prop, value);
409
+ }
410
+ context.publishEvent("action.applied", {
411
+ id: context.generateId(),
412
+ kind: "content:setStyle",
413
+ anchorId: action.anchorId,
414
+ styles: Object.keys(action.styles)
415
+ });
416
+ return {
417
+ cleanup: () => {
418
+ if (!anchorEl.isConnected) return;
419
+ for (const [prop, originalValue] of originalStyles) {
420
+ if (originalValue) {
421
+ anchorEl.style.setProperty(prop, originalValue);
422
+ } else {
423
+ anchorEl.style.removeProperty(prop);
424
+ }
425
+ }
426
+ },
427
+ updateFn: (changes) => {
428
+ if ("styles" in changes && typeof changes.styles === "object" && changes.styles) {
429
+ for (const [prop, value] of Object.entries(changes.styles)) {
430
+ anchorEl.style.setProperty(prop, value);
431
+ }
432
+ }
318
433
  }
319
- context.publishEvent('action.applied', {
320
- id: context.generateId(),
321
- kind: 'content:setStyle',
322
- anchorId: action.anchorId,
323
- styles: Object.keys(action.styles),
324
- });
325
- return {
326
- cleanup: () => {
327
- if (!anchorEl.isConnected)
328
- return;
329
- // Restore original styles
330
- for (const [prop, originalValue] of originalStyles) {
331
- if (originalValue) {
332
- anchorEl.style.setProperty(prop, originalValue);
333
- }
334
- else {
335
- anchorEl.style.removeProperty(prop);
336
- }
337
- }
338
- },
339
- updateFn: (changes) => {
340
- if ('styles' in changes && typeof changes.styles === 'object' && changes.styles) {
341
- for (const [prop, value] of Object.entries(changes.styles)) {
342
- anchorEl.style.setProperty(prop, value);
343
- }
344
- }
345
- },
346
- };
434
+ };
347
435
  };
348
- // ============================================================================
349
- // Executor Definitions for Registration
350
- // ============================================================================
351
- /**
352
- * All executors provided by this app.
353
- * These are registered with the runtime's ExecutorRegistry.
354
- */
355
- export const executors = [
356
- { kind: 'content:insertHtml', executor: executeInsertHtml },
357
- { kind: 'content:setText', executor: executeSetText },
358
- { kind: 'content:setAttr', executor: executeSetAttr },
359
- { kind: 'content:addClass', executor: executeAddClass },
360
- { kind: 'content:removeClass', executor: executeRemoveClass },
361
- { kind: 'content:setStyle', executor: executeSetStyle },
436
+ var executors = [
437
+ { kind: "content:insertHtml", executor: executeInsertHtml },
438
+ { kind: "content:setText", executor: executeSetText },
439
+ { kind: "content:setAttr", executor: executeSetAttr },
440
+ { kind: "content:addClass", executor: executeAddClass },
441
+ { kind: "content:removeClass", executor: executeRemoveClass },
442
+ { kind: "content:setStyle", executor: executeSetStyle }
362
443
  ];
363
- /**
364
- * App runtime manifest.
365
- */
366
- export const runtime = {
367
- id: 'adaptive-content',
368
- version: '1.0.0',
369
- name: 'Content',
370
- description: 'DOM manipulation for text, attributes, and styles',
371
- executors,
444
+ var runtime = {
445
+ id: "adaptive-content",
446
+ version: "1.0.0",
447
+ name: "Content",
448
+ description: "DOM manipulation for text, attributes, and styles",
449
+ executors
450
+ };
451
+ export {
452
+ executeAddClass,
453
+ executeInsertHtml,
454
+ executeRemoveClass,
455
+ executeSetAttr,
456
+ executeSetStyle,
457
+ executeSetText,
458
+ executors,
459
+ runtime
372
460
  };
461
+ //# sourceMappingURL=runtime.js.map