@syntrologie/adapt-content 2.8.0-canary.62 → 2.8.0-canary.621

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 (115) hide show
  1. package/dist/chunk-PZ5AY32C.js +10 -0
  2. package/dist/chunk-PZ5AY32C.js.map +7 -0
  3. package/dist/runtime.d.ts +8 -1
  4. package/dist/runtime.d.ts.map +1 -1
  5. package/dist/runtime.js +503 -348
  6. package/dist/runtime.js.map +7 -0
  7. package/dist/sanitizer.d.ts.map +1 -1
  8. package/dist/schema.d.ts +62 -22
  9. package/dist/schema.d.ts.map +1 -1
  10. package/dist/schema.js +4777 -62
  11. package/dist/schema.js.map +7 -0
  12. package/dist/types.d.ts +12 -0
  13. package/dist/types.d.ts.map +1 -1
  14. package/package.json +2 -23
  15. package/dist/cdn.d.ts +0 -35
  16. package/dist/cdn.d.ts.map +0 -1
  17. package/dist/cdn.js +0 -39
  18. package/dist/components/AnchorPicker.d.ts +0 -8
  19. package/dist/components/AnchorPicker.d.ts.map +0 -1
  20. package/dist/components/AnchorPicker.js +0 -6
  21. package/dist/content-editor-state.d.ts +0 -45
  22. package/dist/content-editor-state.d.ts.map +0 -1
  23. package/dist/content-editor-state.js +0 -123
  24. package/dist/content-editor-ui.d.ts +0 -26
  25. package/dist/content-editor-ui.d.ts.map +0 -1
  26. package/dist/content-editor-ui.js +0 -291
  27. package/dist/editor.d.ts +0 -9
  28. package/dist/editor.d.ts.map +0 -1
  29. package/dist/editor.js +0 -8
  30. package/dist/reconciliation-guard.js +0 -80
  31. package/dist/sanitizer.js +0 -95
  32. package/dist/summarize.js +0 -88
  33. package/dist/types.js +0 -7
  34. package/dist/utils/selectorGenerator.d.ts +0 -6
  35. package/dist/utils/selectorGenerator.d.ts.map +0 -1
  36. package/dist/utils/selectorGenerator.js +0 -4
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.d.ts +0 -2
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.d.ts.map +0 -1
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/cn.js +0 -3
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts +0 -34
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts.map +0 -1
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +0 -158
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts +0 -7
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts.map +0 -1
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.js +0 -9
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +0 -23
  47. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +0 -1
  48. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +0 -40
  49. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +0 -7
  50. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +0 -1
  51. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +0 -22
  52. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.d.ts +0 -8
  53. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.d.ts.map +0 -1
  54. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.js +0 -9
  55. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts +0 -7
  56. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts.map +0 -1
  57. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.js +0 -4
  58. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.d.ts +0 -7
  59. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.d.ts.map +0 -1
  60. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorBody.js +0 -4
  61. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts +0 -13
  62. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts.map +0 -1
  63. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.js +0 -15
  64. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts +0 -7
  65. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts.map +0 -1
  66. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.js +0 -4
  67. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.d.ts +0 -9
  68. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.d.ts.map +0 -1
  69. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorHeader.js +0 -4
  70. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts +0 -8
  71. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts.map +0 -1
  72. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.js +0 -8
  73. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.d.ts +0 -7
  74. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.d.ts.map +0 -1
  75. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorLayout.js +0 -4
  76. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +0 -25
  77. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +0 -1
  78. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +0 -379
  79. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts +0 -8
  80. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts.map +0 -1
  81. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.js +0 -8
  82. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts +0 -8
  83. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts.map +0 -1
  84. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.js +0 -17
  85. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts +0 -32
  86. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts.map +0 -1
  87. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +0 -85
  88. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.d.ts +0 -6
  89. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.d.ts.map +0 -1
  90. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EmptyState.js +0 -4
  91. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.d.ts +0 -8
  92. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.d.ts.map +0 -1
  93. package/node_modules/@syntrologie/shared-editor-ui/dist/components/GroupHeader.js +0 -5
  94. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +0 -12
  95. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +0 -1
  96. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +0 -40
  97. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +0 -26
  98. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +0 -1
  99. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +0 -202
  100. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.d.ts +0 -8
  101. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.d.ts.map +0 -1
  102. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useElementRect.js +0 -46
  103. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +0 -24
  104. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +0 -1
  105. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.js +0 -86
  106. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +0 -34
  107. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +0 -1
  108. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +0 -25
  109. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.d.ts +0 -37
  110. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.d.ts.map +0 -1
  111. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/elementChainRecommender.js +0 -52
  112. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.d.ts +0 -22
  113. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.d.ts.map +0 -1
  114. package/node_modules/@syntrologie/shared-editor-ui/dist/utils/selectorGenerator.js +0 -143
  115. package/node_modules/@syntrologie/shared-editor-ui/package.json +0 -47
package/dist/runtime.js CHANGED
@@ -1,372 +1,527 @@
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
+ import "./chunk-PZ5AY32C.js";
2
+
3
+ // src/reconciliation-guard.ts
4
+ function guardAgainstReconciliation(container, anchor, reinsertFn, opts) {
5
+ const maxRetries = opts?.maxRetries ?? 3;
6
+ const debounceMs = opts?.debounceMs ?? 50;
7
+ const observeTarget = container.parentElement ?? anchor.parentElement;
8
+ if (!observeTarget) return () => {
9
+ };
10
+ let retries = 0;
11
+ let debounceTimer = null;
12
+ let disconnected = false;
13
+ const observer = new MutationObserver((mutations) => {
14
+ if (disconnected) return;
15
+ for (const mutation of mutations) {
16
+ for (const removed of mutation.removedNodes) {
17
+ if (removed !== container) continue;
18
+ if (retries >= maxRetries) {
19
+ observer.disconnect();
20
+ disconnected = true;
21
+ return;
22
+ }
23
+ if (debounceTimer) clearTimeout(debounceTimer);
24
+ debounceTimer = setTimeout(() => {
25
+ if (disconnected) return;
26
+ if (!anchor.isConnected) {
27
+ observer.disconnect();
28
+ disconnected = true;
29
+ return;
30
+ }
31
+ retries++;
32
+ try {
33
+ reinsertFn();
34
+ } catch {
35
+ observer.disconnect();
36
+ disconnected = true;
37
+ }
38
+ }, debounceMs);
39
+ return;
40
+ }
23
41
  }
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();
42
+ });
43
+ observer.observe(observeTarget, { childList: true, subtree: true });
44
+ return () => {
45
+ disconnected = true;
46
+ observer.disconnect();
47
+ if (debounceTimer) clearTimeout(debounceTimer);
48
+ };
49
+ }
50
+
51
+ // src/sanitizer.ts
52
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
53
+ "b",
54
+ "strong",
55
+ "i",
56
+ "em",
57
+ "u",
58
+ "span",
59
+ "div",
60
+ "p",
61
+ "br",
62
+ "ul",
63
+ "ol",
64
+ "li",
65
+ "code",
66
+ "pre",
67
+ "small",
68
+ "sup",
69
+ "sub",
70
+ "a",
71
+ "button",
72
+ // SVG elements (for inline Lucide icons in config HTML)
73
+ "svg",
74
+ "path",
75
+ "circle",
76
+ "line",
77
+ "polyline",
78
+ "polygon",
79
+ "rect",
80
+ "g"
81
+ ]);
82
+ var SYNTRO_ELEMENT_RE = /^syntro-[a-z][a-z0-9-]*$/;
83
+ var CONTAINS_SYNTRO_ELEMENT_RE = /<syntro-[a-z]/i;
84
+ function isAllowedTag(tag) {
85
+ return ALLOWED_TAGS.has(tag) || SYNTRO_ELEMENT_RE.test(tag);
86
+ }
87
+ function normalizeUrlAttr(value) {
88
+ const withoutControlCharacters = Array.from(value, (character) => {
89
+ const codePoint = character.codePointAt(0) ?? 0;
90
+ return codePoint <= 31 || codePoint === 127 ? "" : character;
91
+ }).join("");
92
+ return withoutControlCharacters.trim().toLowerCase();
93
+ }
94
+ function isDangerousUrlAttr(name, value) {
95
+ if (name !== "href" && name !== "src" && name !== "formaction") return false;
96
+ const normalized = normalizeUrlAttr(value);
97
+ return normalized.startsWith("javascript:") || normalized.startsWith("vbscript:") || normalized.startsWith("data:");
98
+ }
99
+ function sanitizeHtml(html) {
100
+ const hasNative = typeof window.Sanitizer === "function" && !CONTAINS_SYNTRO_ELEMENT_RE.test(html);
101
+ if (hasNative) {
102
+ try {
103
+ const s = new window.Sanitizer({});
104
+ const frag = s.sanitizeToFragment(html);
105
+ const div = document.createElement("div");
106
+ div.append(frag);
107
+ return div.innerHTML;
108
+ } catch {
37
109
  }
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;
110
+ }
111
+ const tpl = document.createElement("template");
112
+ tpl.innerHTML = html;
113
+ const root = tpl.content;
114
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
115
+ const toRemove = [];
116
+ while (walker.nextNode()) {
117
+ const el = walker.currentNode;
118
+ const tag = el.tagName.toLowerCase();
119
+ if (!isAllowedTag(tag)) {
120
+ toRemove.push(el);
121
+ continue;
63
122
  }
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);
123
+ for (const attr of Array.from(el.attributes)) {
124
+ const name = attr.name.toLowerCase();
125
+ const isEvent = name.startsWith("on");
126
+ if (isEvent || isDangerousUrlAttr(name, attr.value)) {
127
+ el.removeAttribute(attr.name);
128
+ }
77
129
  }
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);
130
+ }
131
+ const svgs = Array.from(root.querySelectorAll("svg"));
132
+ for (const svg of svgs) {
133
+ if (toRemove.some((el) => svg.contains(el) && el.tagName.toLowerCase() === "script")) {
134
+ toRemove.push(svg);
148
135
  }
149
- if (!anchorEl) {
150
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
151
- return { cleanup: () => { } };
136
+ }
137
+ for (const el of toRemove) {
138
+ while (el.firstChild) {
139
+ el.parentNode?.insertBefore(el.firstChild, el);
152
140
  }
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
- },
141
+ el.remove();
142
+ }
143
+ return tpl.innerHTML;
144
+ }
145
+
146
+ // src/runtime.ts
147
+ var executeInsertHtml = async (action, context) => {
148
+ let anchorEl = context.resolveAnchor(action.anchorId);
149
+ if (!anchorEl && context.waitForAnchor) {
150
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
151
+ }
152
+ if (!anchorEl) {
153
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
154
+ return { cleanup: () => {
155
+ } };
156
+ }
157
+ const sanitizedHtml = sanitizeHtml(action.html);
158
+ const dedupAttr = "data-syntro-insert-label";
159
+ const label = action.label;
160
+ if (label) {
161
+ const searchRoot = anchorEl.parentElement ?? anchorEl;
162
+ const existing = Array.from(searchRoot.querySelectorAll(`[${dedupAttr}]`)).find(
163
+ (candidate) => candidate.getAttribute(dedupAttr) === label
164
+ );
165
+ if (existing) existing.remove();
166
+ }
167
+ const container = document.createElement("div");
168
+ container.setAttribute("data-syntro-action-id", context.generateId());
169
+ if (label) container.setAttribute(dedupAttr, label);
170
+ container.innerHTML = sanitizedHtml;
171
+ let originalContent = null;
172
+ switch (action.position) {
173
+ case "before":
174
+ anchorEl.insertAdjacentElement("beforebegin", container);
175
+ break;
176
+ case "after":
177
+ anchorEl.insertAdjacentElement("afterend", container);
178
+ break;
179
+ case "prepend":
180
+ anchorEl.insertBefore(container, anchorEl.firstChild);
181
+ break;
182
+ case "append":
183
+ anchorEl.appendChild(container);
184
+ break;
185
+ case "replace":
186
+ originalContent = anchorEl.innerHTML;
187
+ anchorEl.replaceWith(container);
188
+ break;
189
+ }
190
+ let deepLinkHandler = null;
191
+ if (action.deepLink) {
192
+ const { tileId, itemId } = action.deepLink;
193
+ deepLinkHandler = () => {
194
+ const handle = window.SynOS?.handle;
195
+ if (handle) {
196
+ handle.open();
197
+ handle.runtime?.events?.publish("notification.deep_link", { tileId, itemId });
198
+ }
173
199
  };
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}`);
200
+ container.style.cursor = "pointer";
201
+ container.addEventListener("click", deepLinkHandler);
202
+ }
203
+ context.publishEvent("action.applied", {
204
+ id: context.generateId(),
205
+ kind: "content:insertHtml",
206
+ anchorId: action.anchorId,
207
+ position: action.position
208
+ });
209
+ const reinsertFn = () => {
210
+ switch (action.position) {
211
+ case "before":
212
+ anchorEl.insertAdjacentElement("beforebegin", container);
213
+ break;
214
+ case "after":
215
+ anchorEl.insertAdjacentElement("afterend", container);
216
+ break;
217
+ case "prepend":
218
+ anchorEl.insertBefore(container, anchorEl.firstChild);
219
+ break;
220
+ case "append":
221
+ anchorEl.appendChild(container);
222
+ break;
223
+ case "replace":
224
+ break;
191
225
  }
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}`);
226
+ };
227
+ const guardCleanup = guardAgainstReconciliation(container, anchorEl, reinsertFn);
228
+ return {
229
+ cleanup: () => {
230
+ if (deepLinkHandler) {
231
+ container.removeEventListener("click", deepLinkHandler);
232
+ }
233
+ guardCleanup();
234
+ if (!container.isConnected) return;
235
+ try {
236
+ if (action.position === "replace" && originalContent !== null) {
237
+ const restoredEl = document.createElement(anchorEl.tagName);
238
+ restoredEl.innerHTML = originalContent;
239
+ Array.from(anchorEl.attributes).forEach((attr) => {
240
+ restoredEl.setAttribute(attr.name, attr.value);
241
+ });
242
+ container.replaceWith(restoredEl);
243
+ } else {
244
+ container.remove();
200
245
  }
246
+ } catch {
247
+ }
248
+ },
249
+ updateFn: (changes) => {
250
+ if ("html" in changes && typeof changes.html === "string") {
251
+ container.innerHTML = sanitizeHtml(changes.html);
252
+ }
201
253
  }
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
- };
254
+ };
230
255
  };
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);
256
+ function findTextTarget(el) {
257
+ if (el.children.length === 0) return el;
258
+ const textChildren = Array.from(el.children).filter((child) => child.textContent?.trim());
259
+ if (textChildren.length === 1) {
260
+ const child = textChildren[0];
261
+ if (child.textContent?.trim() === el.textContent?.trim()) {
262
+ return findTextTarget(child);
238
263
  }
239
- if (!anchorEl) {
240
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
241
- return { cleanup: () => { } };
264
+ }
265
+ return el;
266
+ }
267
+ var executeSetText = async (action, context) => {
268
+ let anchorEl = context.resolveAnchor(action.anchorId);
269
+ if (!anchorEl && context.waitForAnchor) {
270
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
271
+ }
272
+ if (!anchorEl) {
273
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
274
+ return { cleanup: () => {
275
+ } };
276
+ }
277
+ const textTarget = findTextTarget(anchorEl);
278
+ const originalText = textTarget.textContent ?? "";
279
+ textTarget.textContent = action.text;
280
+ context.publishEvent("action.applied", {
281
+ id: context.generateId(),
282
+ kind: "content:setText",
283
+ anchorId: action.anchorId
284
+ });
285
+ return {
286
+ cleanup: () => {
287
+ if (!anchorEl.isConnected) return;
288
+ textTarget.textContent = originalText;
289
+ },
290
+ updateFn: (changes) => {
291
+ if ("text" in changes && typeof changes.text === "string") {
292
+ textTarget.textContent = changes.text;
293
+ }
242
294
  }
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
- };
295
+ };
263
296
  };
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);
297
+ var executeSetAttr = async (action, context) => {
298
+ let anchorEl = context.resolveAnchor(action.anchorId);
299
+ if (!anchorEl && context.waitForAnchor) {
300
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
301
+ }
302
+ if (!anchorEl) {
303
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
304
+ return { cleanup: () => {
305
+ } };
306
+ }
307
+ const lowerAttr = action.attr.toLowerCase();
308
+ if (lowerAttr.startsWith("on")) {
309
+ throw new Error(`Dangerous attribute not allowed: ${action.attr}`);
310
+ }
311
+ const isUrlAttr = lowerAttr === "href" || lowerAttr === "src" || lowerAttr === "formaction";
312
+ if (isUrlAttr) {
313
+ const lowerValue = action.value.trim().toLowerCase();
314
+ if (lowerValue.startsWith("javascript:") || lowerValue.startsWith("vbscript:") || lowerValue.startsWith("data:text/html")) {
315
+ throw new Error(`Dangerous URL not allowed in ${action.attr}: ${action.value}`);
271
316
  }
272
- if (!anchorEl) {
273
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
274
- return { cleanup: () => { } };
317
+ }
318
+ const originalValue = anchorEl.getAttribute(action.attr);
319
+ const hadAttribute = anchorEl.hasAttribute(action.attr);
320
+ anchorEl.setAttribute(action.attr, action.value);
321
+ context.publishEvent("action.applied", {
322
+ id: context.generateId(),
323
+ kind: "content:setAttr",
324
+ anchorId: action.anchorId,
325
+ attr: action.attr
326
+ });
327
+ return {
328
+ cleanup: () => {
329
+ if (!anchorEl.isConnected) return;
330
+ if (hadAttribute && originalValue !== null) {
331
+ anchorEl.setAttribute(action.attr, originalValue);
332
+ } else {
333
+ anchorEl.removeAttribute(action.attr);
334
+ }
335
+ },
336
+ updateFn: (changes) => {
337
+ if ("value" in changes && typeof changes.value === "string") {
338
+ anchorEl.setAttribute(action.attr, changes.value);
339
+ }
275
340
  }
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
- };
341
+ };
296
342
  };
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);
343
+ var executeAddClass = async (action, context) => {
344
+ let anchorEl = context.resolveAnchor(action.anchorId);
345
+ if (!anchorEl && context.waitForAnchor) {
346
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
347
+ }
348
+ if (!anchorEl) {
349
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
350
+ return { cleanup: () => {
351
+ } };
352
+ }
353
+ const hadClass = anchorEl.classList.contains(action.className);
354
+ anchorEl.classList.add(action.className);
355
+ context.publishEvent("action.applied", {
356
+ id: context.generateId(),
357
+ kind: "content:addClass",
358
+ anchorId: action.anchorId,
359
+ className: action.className
360
+ });
361
+ return {
362
+ cleanup: () => {
363
+ if (!anchorEl.isConnected) return;
364
+ if (!hadClass) {
365
+ anchorEl.classList.remove(action.className);
366
+ }
304
367
  }
305
- if (!anchorEl) {
306
- console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
307
- return { cleanup: () => { } };
368
+ };
369
+ };
370
+ var executeRemoveClass = async (action, context) => {
371
+ let anchorEl = context.resolveAnchor(action.anchorId);
372
+ if (!anchorEl && context.waitForAnchor) {
373
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
374
+ }
375
+ if (!anchorEl) {
376
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
377
+ return { cleanup: () => {
378
+ } };
379
+ }
380
+ const hadClass = anchorEl.classList.contains(action.className);
381
+ anchorEl.classList.remove(action.className);
382
+ context.publishEvent("action.applied", {
383
+ id: context.generateId(),
384
+ kind: "content:removeClass",
385
+ anchorId: action.anchorId,
386
+ className: action.className
387
+ });
388
+ return {
389
+ cleanup: () => {
390
+ if (!anchorEl.isConnected) return;
391
+ if (hadClass) {
392
+ anchorEl.classList.add(action.className);
393
+ }
308
394
  }
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);
395
+ };
396
+ };
397
+ var executeSetStyle = async (action, context) => {
398
+ let anchorEl = context.resolveAnchor(action.anchorId);
399
+ if (!anchorEl && context.waitForAnchor) {
400
+ anchorEl = await context.waitForAnchor(action.anchorId, 3e3);
401
+ }
402
+ if (!anchorEl) {
403
+ console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
404
+ return { cleanup: () => {
405
+ } };
406
+ }
407
+ const originalStyles = /* @__PURE__ */ new Map();
408
+ for (const prop of Object.keys(action.styles)) {
409
+ const current = anchorEl.style.getPropertyValue(prop);
410
+ originalStyles.set(prop, current);
411
+ }
412
+ for (const [prop, value] of Object.entries(action.styles)) {
413
+ const el = anchorEl;
414
+ el.style.setProperty(prop, value);
415
+ if (value !== "" && el.style.getPropertyValue(prop) === "") {
416
+ console.warn(
417
+ `[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').`
418
+ );
314
419
  }
315
- // Apply new styles
316
- for (const [prop, value] of Object.entries(action.styles)) {
317
- anchorEl.style.setProperty(prop, value);
420
+ }
421
+ context.publishEvent("action.applied", {
422
+ id: context.generateId(),
423
+ kind: "content:setStyle",
424
+ anchorId: action.anchorId,
425
+ styles: Object.keys(action.styles)
426
+ });
427
+ return {
428
+ cleanup: () => {
429
+ if (!anchorEl.isConnected) return;
430
+ for (const [prop, originalValue] of originalStyles) {
431
+ if (originalValue) {
432
+ anchorEl.style.setProperty(prop, originalValue);
433
+ } else {
434
+ anchorEl.style.removeProperty(prop);
435
+ }
436
+ }
437
+ },
438
+ updateFn: (changes) => {
439
+ if ("styles" in changes && typeof changes.styles === "object" && changes.styles) {
440
+ for (const [prop, value] of Object.entries(changes.styles)) {
441
+ anchorEl.style.setProperty(prop, value);
442
+ }
443
+ }
318
444
  }
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
- };
445
+ };
347
446
  };
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 },
447
+ var CSS_BREAKOUT_CHARS = /[{}]|\/\*/;
448
+ function hasCssBreakout(selector, styles) {
449
+ if (CSS_BREAKOUT_CHARS.test(selector)) return true;
450
+ return Object.values(styles).some((value) => CSS_BREAKOUT_CHARS.test(value));
451
+ }
452
+ var HIDE_BY_SELECTOR_RULE_ID = "syntro-hide-by-selector";
453
+ function hideBySelectorRule(selector, styles) {
454
+ const declarations = Object.entries(styles).map(([prop, value]) => `${prop}: ${value} !important;`).join(" ");
455
+ return `:is(${selector}):not(#${HIDE_BY_SELECTOR_RULE_ID}) { ${declarations} }`;
456
+ }
457
+ var executeHideBySelector = async (action, context) => {
458
+ if (context.matchesAnchorRoute && !context.matchesAnchorRoute(action.anchorId)) {
459
+ return { cleanup: () => {
460
+ } };
461
+ }
462
+ if (hasCssBreakout(action.anchorId.selector, action.styles)) {
463
+ console.error(
464
+ '[adaptive-content] Refusing to inject content:hideBySelector rule \u2014 selector or style value contains "{", "}", or "/*" (CSS rule breakout). This should have been rejected by the authoring-time schema (HideBySelectorZ); refusing at the injection sink as a second guard.',
465
+ action.anchorId.selector,
466
+ action.styles
467
+ );
468
+ return { cleanup: () => {
469
+ } };
470
+ }
471
+ const styleEl = document.createElement("style");
472
+ styleEl.setAttribute("data-syntro-action-id", context.generateId());
473
+ styleEl.textContent = hideBySelectorRule(action.anchorId.selector, action.styles);
474
+ document.head.appendChild(styleEl);
475
+ context.publishEvent("action.applied", {
476
+ id: context.generateId(),
477
+ kind: "content:hideBySelector",
478
+ anchorId: action.anchorId,
479
+ styles: Object.keys(action.styles)
480
+ });
481
+ return {
482
+ cleanup: () => {
483
+ styleEl.remove();
484
+ },
485
+ updateFn: (changes) => {
486
+ if ("styles" in changes && typeof changes.styles === "object" && changes.styles) {
487
+ const newStyles = changes.styles;
488
+ if (hasCssBreakout(action.anchorId.selector, newStyles)) {
489
+ console.error(
490
+ '[adaptive-content] Refusing to update content:hideBySelector rule \u2014 updated style value contains "{", "}", or "/*" (CSS rule breakout).',
491
+ newStyles
492
+ );
493
+ return;
494
+ }
495
+ styleEl.textContent = hideBySelectorRule(action.anchorId.selector, newStyles);
496
+ }
497
+ }
498
+ };
499
+ };
500
+ var executors = [
501
+ { kind: "content:insertHtml", executor: executeInsertHtml },
502
+ { kind: "content:setText", executor: executeSetText },
503
+ { kind: "content:setAttr", executor: executeSetAttr },
504
+ { kind: "content:addClass", executor: executeAddClass },
505
+ { kind: "content:removeClass", executor: executeRemoveClass },
506
+ { kind: "content:setStyle", executor: executeSetStyle },
507
+ { kind: "content:hideBySelector", executor: executeHideBySelector }
362
508
  ];
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,
509
+ var runtime = {
510
+ id: "adaptive-content",
511
+ version: "1.0.0",
512
+ name: "Content",
513
+ description: "DOM manipulation for text, attributes, and styles",
514
+ executors
515
+ };
516
+ export {
517
+ executeAddClass,
518
+ executeHideBySelector,
519
+ executeInsertHtml,
520
+ executeRemoveClass,
521
+ executeSetAttr,
522
+ executeSetStyle,
523
+ executeSetText,
524
+ executors,
525
+ runtime
372
526
  };
527
+ //# sourceMappingURL=runtime.js.map