@syntrologie/adapt-content 2.8.0-canary.31 → 2.8.0-canary.310

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 (109) hide show
  1. package/dist/runtime.d.ts.map +1 -1
  2. package/dist/runtime.js +431 -348
  3. package/dist/runtime.js.map +7 -0
  4. package/dist/schema.d.ts +56 -16
  5. package/dist/schema.d.ts.map +1 -1
  6. package/dist/schema.js +363 -62
  7. package/dist/schema.js.map +7 -0
  8. package/package.json +8 -26
  9. package/dist/cdn.d.ts +0 -35
  10. package/dist/cdn.d.ts.map +0 -1
  11. package/dist/cdn.js +0 -39
  12. package/dist/components/AnchorPicker.d.ts +0 -8
  13. package/dist/components/AnchorPicker.d.ts.map +0 -1
  14. package/dist/components/AnchorPicker.js +0 -6
  15. package/dist/content-editor-state.d.ts +0 -45
  16. package/dist/content-editor-state.d.ts.map +0 -1
  17. package/dist/content-editor-state.js +0 -123
  18. package/dist/content-editor-ui.d.ts +0 -26
  19. package/dist/content-editor-ui.d.ts.map +0 -1
  20. package/dist/content-editor-ui.js +0 -301
  21. package/dist/editor.d.ts +0 -9
  22. package/dist/editor.d.ts.map +0 -1
  23. package/dist/editor.js +0 -8
  24. package/dist/reconciliation-guard.js +0 -80
  25. package/dist/sanitizer.js +0 -95
  26. package/dist/summarize.js +0 -88
  27. package/dist/types.js +0 -7
  28. package/dist/utils/selectorGenerator.d.ts +0 -6
  29. package/dist/utils/selectorGenerator.d.ts.map +0 -1
  30. package/dist/utils/selectorGenerator.js +0 -4
  31. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.d.ts +0 -2
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.d.ts.map +0 -1
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.js +0 -3
  34. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts +0 -34
  35. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts.map +0 -1
  36. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +0 -158
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts +0 -7
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts.map +0 -1
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.js +0 -9
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +0 -23
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +0 -1
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +0 -40
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +0 -7
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +0 -1
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +0 -22
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.d.ts +0 -8
  47. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.d.ts.map +0 -1
  48. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.js +0 -9
  49. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts +0 -7
  50. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts.map +0 -1
  51. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.js +0 -4
  52. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.d.ts +0 -7
  53. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.d.ts.map +0 -1
  54. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.js +0 -4
  55. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts +0 -13
  56. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts.map +0 -1
  57. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.js +0 -15
  58. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts +0 -7
  59. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts.map +0 -1
  60. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.js +0 -4
  61. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.d.ts +0 -9
  62. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.d.ts.map +0 -1
  63. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.js +0 -4
  64. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts +0 -8
  65. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts.map +0 -1
  66. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.js +0 -8
  67. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.d.ts +0 -7
  68. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.d.ts.map +0 -1
  69. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.js +0 -4
  70. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +0 -25
  71. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +0 -1
  72. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +0 -379
  73. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts +0 -8
  74. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts.map +0 -1
  75. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.js +0 -8
  76. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts +0 -8
  77. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts.map +0 -1
  78. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.js +0 -17
  79. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts +0 -32
  80. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts.map +0 -1
  81. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +0 -85
  82. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.d.ts +0 -6
  83. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.d.ts.map +0 -1
  84. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.js +0 -4
  85. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.d.ts +0 -8
  86. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.d.ts.map +0 -1
  87. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.js +0 -5
  88. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +0 -12
  89. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +0 -1
  90. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +0 -40
  91. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +0 -26
  92. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +0 -1
  93. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +0 -202
  94. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.d.ts +0 -8
  95. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.d.ts.map +0 -1
  96. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.js +0 -46
  97. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +0 -24
  98. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +0 -1
  99. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.js +0 -86
  100. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +0 -34
  101. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +0 -1
  102. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +0 -25
  103. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.d.ts +0 -37
  104. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.d.ts.map +0 -1
  105. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.js +0 -52
  106. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.d.ts +0 -22
  107. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.d.ts.map +0 -1
  108. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.js +0 -143
  109. package/node_modules/@syntrologie/shared-editor-ui/package.json +0 -45
package/dist/runtime.js CHANGED
@@ -1,372 +1,455 @@
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 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 {
37
90
  }
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;
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;
63
103
  }
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);
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
+ }
77
113
  }
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);
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);
148
119
  }
149
- if (!anchorEl) {
150
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
151
- return { cleanup: () => { } };
120
+ }
121
+ for (const el of toRemove) {
122
+ while (el.firstChild) {
123
+ el.parentNode?.insertBefore(el.firstChild, el);
152
124
  }
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
- },
125
+ el.remove();
126
+ }
127
+ return tpl.innerHTML;
128
+ }
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
+ }
173
182
  };
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}`);
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;
191
208
  }
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}`);
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();
200
228
  }
229
+ } catch {
230
+ }
231
+ },
232
+ updateFn: (changes) => {
233
+ if ("html" in changes && typeof changes.html === "string") {
234
+ container.innerHTML = sanitizeHtml(changes.html);
235
+ }
201
236
  }
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
- };
237
+ };
230
238
  };
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);
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);
238
246
  }
239
- if (!anchorEl) {
240
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
241
- return { cleanup: () => { } };
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
+ }
242
277
  }
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
- };
278
+ };
263
279
  };
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);
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}`);
271
299
  }
272
- if (!anchorEl) {
273
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
274
- return { cleanup: () => { } };
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
+ }
275
323
  }
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
- };
324
+ };
296
325
  };
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);
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
+ }
304
350
  }
305
- if (!anchorEl) {
306
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
307
- return { cleanup: () => { } };
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
+ }
308
377
  }
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);
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
+ const el = anchorEl;
397
+ el.style.setProperty(prop, value);
398
+ if (value !== "" && el.style.getPropertyValue(prop) === "") {
399
+ console.warn(
400
+ `[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').`
401
+ );
314
402
  }
315
- // Apply new styles
316
- for (const [prop, value] of Object.entries(action.styles)) {
317
- anchorEl.style.setProperty(prop, value);
403
+ }
404
+ context.publishEvent("action.applied", {
405
+ id: context.generateId(),
406
+ kind: "content:setStyle",
407
+ anchorId: action.anchorId,
408
+ styles: Object.keys(action.styles)
409
+ });
410
+ return {
411
+ cleanup: () => {
412
+ if (!anchorEl.isConnected) return;
413
+ for (const [prop, originalValue] of originalStyles) {
414
+ if (originalValue) {
415
+ anchorEl.style.setProperty(prop, originalValue);
416
+ } else {
417
+ anchorEl.style.removeProperty(prop);
418
+ }
419
+ }
420
+ },
421
+ updateFn: (changes) => {
422
+ if ("styles" in changes && typeof changes.styles === "object" && changes.styles) {
423
+ for (const [prop, value] of Object.entries(changes.styles)) {
424
+ anchorEl.style.setProperty(prop, value);
425
+ }
426
+ }
318
427
  }
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
- };
428
+ };
347
429
  };
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 },
430
+ var executors = [
431
+ { kind: "content:insertHtml", executor: executeInsertHtml },
432
+ { kind: "content:setText", executor: executeSetText },
433
+ { kind: "content:setAttr", executor: executeSetAttr },
434
+ { kind: "content:addClass", executor: executeAddClass },
435
+ { kind: "content:removeClass", executor: executeRemoveClass },
436
+ { kind: "content:setStyle", executor: executeSetStyle }
362
437
  ];
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,
438
+ var runtime = {
439
+ id: "adaptive-content",
440
+ version: "1.0.0",
441
+ name: "Content",
442
+ description: "DOM manipulation for text, attributes, and styles",
443
+ executors
444
+ };
445
+ export {
446
+ executeAddClass,
447
+ executeInsertHtml,
448
+ executeRemoveClass,
449
+ executeSetAttr,
450
+ executeSetStyle,
451
+ executeSetText,
452
+ executors,
453
+ runtime
372
454
  };
455
+ //# sourceMappingURL=runtime.js.map