assign-gingerly 0.0.54 → 0.0.55

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/README.md +143 -2
  2. package/assignFrom.js +37 -18
  3. package/assignFrom.ts +62 -3
  4. package/assignGingerly.js +34 -1
  5. package/assignGingerly.ts +54 -1
  6. package/beVigilant.js +73 -0
  7. package/beVigilant.ts +85 -0
  8. package/enhanceAll.js +106 -0
  9. package/enhanceAll.ts +138 -0
  10. package/handlers/join.js +74 -0
  11. package/handlers/join.ts +80 -0
  12. package/handlers/lazyLoad.js +212 -0
  13. package/handlers/lazyLoad.ts +307 -0
  14. package/handlers/lazyLoadSwitch.js +58 -0
  15. package/handlers/lazyLoadSwitch.ts +63 -0
  16. package/handlers/microDataJoin.js +184 -0
  17. package/handlers/microDataJoin.ts +270 -0
  18. package/inferencer/.gitmodules +3 -0
  19. package/inferencer/.vscode/settings.json +2 -0
  20. package/inferencer/InferencedPropagator.js +230 -0
  21. package/inferencer/InferencedPropagator.ts +269 -0
  22. package/inferencer/LICENSE +21 -0
  23. package/inferencer/README.md +524 -0
  24. package/inferencer/Requirements/SupportForPropagator.md +368 -0
  25. package/inferencer/imports.html +7 -0
  26. package/inferencer/inferencer.js +254 -0
  27. package/inferencer/inferencer.ts +292 -0
  28. package/inferencer/package-lock.json +129 -0
  29. package/inferencer/package.json +60 -0
  30. package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
  31. package/inferencer/playwright-report/index.html +90 -0
  32. package/inferencer/playwright.config.ts +54 -0
  33. package/inferencer/test-results/.last-run.json +6 -0
  34. package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
  35. package/inferencer/tests/inferencedPropagator.html +428 -0
  36. package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
  37. package/inferencer/tests/inferencer.html +355 -0
  38. package/inferencer/tests/inferencer.spec.ts +19 -0
  39. package/inferencer/tsconfig.json +19 -0
  40. package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
  41. package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
  42. package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
  43. package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
  44. package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
  45. package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
  46. package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
  47. package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
  48. package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
  49. package/inferencer/types/LICENSE +21 -0
  50. package/inferencer/types/NewCustomElement.md +388 -0
  51. package/inferencer/types/NewCustomElementFeature.md +683 -0
  52. package/inferencer/types/NewEnhancementInstructions.md +705 -0
  53. package/inferencer/types/README.md +2 -0
  54. package/inferencer/types/agrace/types.d.ts +11 -0
  55. package/inferencer/types/assign-gingerly/types.d.ts +572 -0
  56. package/inferencer/types/be-a-beacon/types.d.ts +17 -0
  57. package/inferencer/types/be-bound/types.d.ts +66 -0
  58. package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
  59. package/inferencer/types/be-calculating/types.d.ts +54 -0
  60. package/inferencer/types/be-clonable/types.d.ts +38 -0
  61. package/inferencer/types/be-committed/types.d.ts +22 -0
  62. package/inferencer/types/be-consoling/types.d.ts +24 -0
  63. package/inferencer/types/be-decked-with/types.d.ts +26 -0
  64. package/inferencer/types/be-delible/types.d.ts +27 -0
  65. package/inferencer/types/be-dispatching/types.d.ts +34 -0
  66. package/inferencer/types/be-evanescent/types.d.ts +20 -0
  67. package/inferencer/types/be-flashy/types.d.ts +21 -0
  68. package/inferencer/types/be-gone/types.d.ts +25 -0
  69. package/inferencer/types/be-observing/types.d.ts +55 -0
  70. package/inferencer/types/be-reflective/types.d.ts +78 -0
  71. package/inferencer/types/be-reformable/types.d.ts +49 -0
  72. package/inferencer/types/be-render-neutral/types.d.ts +32 -0
  73. package/inferencer/types/be-switched/types.d.ts +146 -0
  74. package/inferencer/types/be-typed/types.d.ts +32 -0
  75. package/inferencer/types/be-valued/types.d.ts +22 -0
  76. package/inferencer/types/data-props/types.d.ts +34 -0
  77. package/inferencer/types/do-inc/types.d.ts +56 -0
  78. package/inferencer/types/do-invoke/types.d.ts +38 -0
  79. package/inferencer/types/do-merge/types.d.ts +28 -0
  80. package/inferencer/types/do-toggle/types.d.ts +31 -0
  81. package/inferencer/types/face-up/types.d.ts +100 -0
  82. package/inferencer/types/fetch-for/types.d.ts +36 -0
  83. package/inferencer/types/folder-picker/types.d.ts +21 -0
  84. package/inferencer/types/global.d.ts +29 -0
  85. package/inferencer/types/id-generation/types.d.ts +26 -0
  86. package/inferencer/types/inferencer/types.d.ts +46 -0
  87. package/inferencer/types/mount-observer/types.d.ts +363 -0
  88. package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
  89. package/inferencer/types/pipe-in/types.d.ts +52 -0
  90. package/inferencer/types/roundabout/types.d.ts +268 -0
  91. package/inferencer/types/soak-up/types.d.ts +40 -0
  92. package/inferencer/types/templ-maker/types.d.ts +43 -0
  93. package/inferencer/types/time-ticker/types.d.ts +62 -0
  94. package/inferencer/types/truth-sourcer/types.d.ts +44 -0
  95. package/inferencer/upSearch.js +27 -0
  96. package/inferencer/upSearch.ts +26 -0
  97. package/inferencer/withScopePerimeter.js +27 -0
  98. package/inferencer/withScopePerimeter.ts +33 -0
  99. package/inferredAssignments.js +38 -0
  100. package/inferredAssignments.ts +65 -0
  101. package/isAllowedImportPath.js +42 -0
  102. package/isAllowedImportPath.ts +53 -0
  103. package/package.json +27 -1
  104. package/paths.js +49 -1
  105. package/paths.ts +80 -1
  106. package/processHandlerCommands.js +19 -19
  107. package/processHandlerCommands.ts +10 -13
  108. package/resolveIdRef.js +144 -125
  109. package/resolveIdRef.ts +30 -0
  110. package/types/assign-gingerly/types.d.ts +12 -0
@@ -0,0 +1,307 @@
1
+ /**
2
+ * builtIns.lazyLoad handler for assignFrom.
3
+ *
4
+ * Conditionally clones a template into a target element, using comment markers
5
+ * to track the inserted content. Supports show/hide and show/remove modes.
6
+ *
7
+ * This handler is auto-loaded by processHandlerCommands when `do: 'builtIns.lazyLoad'`
8
+ * is encountered — no explicit import is needed.
9
+ *
10
+ * @example
11
+ * assignFrom(document.body, {
12
+ * '?.querySelector?..mainView =>': {
13
+ * do: 'builtIns.lazyLoad',
14
+ * resolve: {
15
+ * if: '?.isHappy',
16
+ * instantiate: 'globalThis://happyMood',
17
+ * }
18
+ * }
19
+ * }, { withMethods: ['querySelector'], from: myVM });
20
+ */
21
+
22
+ import type { AssignFromHandler } from '../assignFrom.js';
23
+ import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
24
+
25
+ const MARKER_START_PREFIX = '?start name="';
26
+ const MARKER_END = '?end';
27
+
28
+ /**
29
+ * Context passed to onInstantiated callbacks.
30
+ */
31
+ export interface LazyLoadInstantiatedContext {
32
+ /** The inserted child nodes */
33
+ nodes: Node[];
34
+ /** The target element containing the markers */
35
+ target: Element;
36
+ /** The full handler config */
37
+ config: any;
38
+ /** The resolved parameters */
39
+ resolvedParams: Record<string, any>;
40
+ }
41
+
42
+ /**
43
+ * Get the marker name from a template element (uses its id).
44
+ */
45
+ function getMarkerName(templateEl: any): string {
46
+ if (templateEl instanceof HTMLTemplateElement) {
47
+ return templateEl.id || 'anonymous';
48
+ }
49
+ if (typeof templateEl === 'string') {
50
+ return templateEl;
51
+ }
52
+ return 'anonymous';
53
+ }
54
+
55
+ /**
56
+ * Find existing start/end comment markers in a target element.
57
+ * Returns [startMarker, endMarker] or [null, null] if not found.
58
+ */
59
+ function findMarkers(target: Element, name: string): [Comment | null, Comment | null] {
60
+ const startText = `${MARKER_START_PREFIX}${name}"`;
61
+ let startMarker: Comment | null = null;
62
+ let endMarker: Comment | null = null;
63
+
64
+ const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
65
+ let node: Comment | null;
66
+ while ((node = walker.nextNode() as Comment | null)) {
67
+ if (!startMarker && node.data === startText) {
68
+ startMarker = node;
69
+ } else if (startMarker && !endMarker && node.data === MARKER_END) {
70
+ endMarker = node;
71
+ break;
72
+ }
73
+ }
74
+
75
+ return [startMarker, endMarker];
76
+ }
77
+
78
+ /**
79
+ * Create start/end markers and insert them into the target.
80
+ */
81
+ function createMarkers(target: Element, name: string, method: string): [Comment, Comment] {
82
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
83
+ const endMarker = document.createComment(MARKER_END);
84
+
85
+ if (method === 'prepend') {
86
+ target.prepend(endMarker);
87
+ target.prepend(startMarker);
88
+ } else {
89
+ // Default: appendChild
90
+ target.appendChild(startMarker);
91
+ target.appendChild(endMarker);
92
+ }
93
+
94
+ return [startMarker, endMarker];
95
+ }
96
+
97
+ /**
98
+ * Get all nodes between start and end markers.
99
+ */
100
+ function getNodesBetweenMarkers(start: Comment, end: Comment): Node[] {
101
+ const nodes: Node[] = [];
102
+ let current: Node | null = start.nextSibling;
103
+ while (current && current !== end) {
104
+ nodes.push(current);
105
+ current = current.nextSibling;
106
+ }
107
+ return nodes;
108
+ }
109
+
110
+ /**
111
+ * LazyLoadHandler — built-in handler for conditional template instantiation.
112
+ *
113
+ * Exported so it can be subclassed for custom behavior.
114
+ */
115
+ export class LazyLoadHandler implements AssignFromHandler {
116
+ config: any;
117
+
118
+ constructor(config: any) {
119
+ this.config = config;
120
+ }
121
+
122
+ async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<void> {
123
+ const {
124
+ if: condition,
125
+ instantiate,
126
+ method = 'appendChild',
127
+ forget = false,
128
+ transitional = false,
129
+ hideClass = DEFAULT_HIDE_CLASS,
130
+ } = resolvedParams;
131
+
132
+ if (!(lhsTarget instanceof Element)) {
133
+ throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
134
+ }
135
+
136
+ const name = getMarkerName(instantiate);
137
+
138
+ // Find or create markers
139
+ let [startMarker, endMarker] = findMarkers(lhsTarget, name);
140
+
141
+ if (condition) {
142
+ // SHOW
143
+ if (startMarker && endMarker) {
144
+ // Markers exist — check if content is hidden or removed
145
+ const nodes = getNodesBetweenMarkers(startMarker, endMarker);
146
+ if (nodes.length > 0) {
147
+ // Content exists — show it
148
+ withTransition(startMarker, 'show', transitional, () => {
149
+ for (const node of nodes) {
150
+ if (node instanceof Element) {
151
+ if (transitional) {
152
+ node.classList.remove(hideClass);
153
+ } else {
154
+ node.removeAttribute('hidden');
155
+ }
156
+ }
157
+ }
158
+ });
159
+ } else {
160
+ // Content was removed (forget mode) — re-clone
161
+ if (transitional) {
162
+ ensureHideStyle(lhsTarget.getRootNode());
163
+ withTransition(startMarker, 'show', true, () => {
164
+ // cloneAndInsert is async but the transition callback is sync
165
+ // For transitions with clone, we insert synchronously
166
+ this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
167
+ });
168
+ } else {
169
+ await this.cloneAndInsert(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
170
+ }
171
+ }
172
+ } else {
173
+ // No markers — first time. Create markers and clone template.
174
+ [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
175
+ if (transitional) {
176
+ ensureHideStyle(lhsTarget.getRootNode());
177
+ withTransition(startMarker, 'show', true, () => {
178
+ this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
179
+ });
180
+ } else {
181
+ await this.cloneAndInsert(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
182
+ }
183
+ }
184
+ } else {
185
+ // HIDE or REMOVE
186
+ if (startMarker && endMarker) {
187
+ const nodes = getNodesBetweenMarkers(startMarker, endMarker);
188
+ if (nodes.length === 0) return;
189
+
190
+ if (forget) {
191
+ // Remove nodes entirely (markers persist for re-insertion)
192
+ withTransition(startMarker, 'hide', transitional, () => {
193
+ for (const node of nodes) {
194
+ node.parentNode?.removeChild(node);
195
+ }
196
+ });
197
+ } else {
198
+ // Hide nodes
199
+ withTransition(startMarker, 'hide', transitional, () => {
200
+ for (const node of nodes) {
201
+ if (node instanceof Element) {
202
+ if (transitional) {
203
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass);
204
+ node.classList.add(hideClass);
205
+ } else {
206
+ node.setAttribute('hidden', '');
207
+ }
208
+ }
209
+ }
210
+ });
211
+ }
212
+ }
213
+ // If no markers exist and condition is false, do nothing (never loaded)
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Clone a template and insert its content between the markers (synchronous version).
219
+ * Used inside view transition callbacks which must be synchronous.
220
+ * Does not call async hooks (onCloneInserted, onInstantiated).
221
+ */
222
+ protected cloneAndInsertSync(
223
+ templateEl: any,
224
+ startMarker: Comment,
225
+ endMarker: Comment,
226
+ lhsTarget: Element,
227
+ resolvedParams: Record<string, any>
228
+ ): Node[] {
229
+ let content: DocumentFragment;
230
+
231
+ if (templateEl instanceof HTMLTemplateElement) {
232
+ content = templateEl.content.cloneNode(true) as DocumentFragment;
233
+ } else if (templateEl instanceof DocumentFragment) {
234
+ content = templateEl.cloneNode(true) as DocumentFragment;
235
+ } else {
236
+ throw new Error(
237
+ `builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`
238
+ );
239
+ }
240
+
241
+ const nodes = Array.from(content.childNodes);
242
+ endMarker.parentNode!.insertBefore(content, endMarker);
243
+ return nodes;
244
+ }
245
+
246
+ /**
247
+ * Clone a template and insert its content between the markers.
248
+ * Calls onCloneInserted hook and onInstantiated callback after insertion.
249
+ */
250
+ protected async cloneAndInsert(
251
+ templateEl: any,
252
+ startMarker: Comment,
253
+ endMarker: Comment,
254
+ lhsTarget: Element,
255
+ resolvedParams: Record<string, any>
256
+ ): Promise<Node[]> {
257
+ let content: DocumentFragment;
258
+
259
+ if (templateEl instanceof HTMLTemplateElement) {
260
+ content = templateEl.content.cloneNode(true) as DocumentFragment;
261
+ } else if (templateEl instanceof DocumentFragment) {
262
+ content = templateEl.cloneNode(true) as DocumentFragment;
263
+ } else {
264
+ throw new Error(
265
+ `builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`
266
+ );
267
+ }
268
+
269
+ // Capture nodes before insertion (childNodes empties after insertBefore)
270
+ const nodes = Array.from(content.childNodes);
271
+
272
+ // Insert before endMarker
273
+ endMarker.parentNode!.insertBefore(content, endMarker);
274
+
275
+ // Call protected hook (for subclass overrides)
276
+ await this.onCloneInserted(nodes, lhsTarget, resolvedParams);
277
+
278
+ // Call onInstantiated callback if provided (resolved from VM)
279
+ if (resolvedParams.onInstantiated && typeof resolvedParams.onInstantiated === 'function') {
280
+ const ctx: LazyLoadInstantiatedContext = {
281
+ nodes,
282
+ target: lhsTarget,
283
+ config: this.config,
284
+ resolvedParams
285
+ };
286
+ await resolvedParams.onInstantiated(ctx);
287
+ }
288
+
289
+ return nodes;
290
+ }
291
+
292
+ /**
293
+ * Hook called after template content is cloned and inserted.
294
+ * Override in subclasses for custom post-clone logic.
295
+ *
296
+ * @param nodes - The inserted child nodes
297
+ * @param lhsTarget - The target element
298
+ * @param resolvedParams - The resolved parameters
299
+ */
300
+ protected async onCloneInserted(
301
+ nodes: Node[],
302
+ lhsTarget: Element,
303
+ resolvedParams: Record<string, any>
304
+ ): Promise<void> {
305
+ // No-op by default. Subclasses override.
306
+ }
307
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * builtIns.lazyLoadSwitch handler for assignFrom.
3
+ *
4
+ * Extends LazyLoadHandler with a comparison-based condition instead of a
5
+ * pre-computed boolean. Evaluates `lhs op rhs` to determine whether to
6
+ * show or hide the template content.
7
+ *
8
+ * This handler is auto-loaded by processHandlerCommands when
9
+ * `do: 'builtIns.lazyLoadSwitch'` is encountered — no explicit import is needed.
10
+ *
11
+ * Useful for routing and multi-way conditional rendering where multiple
12
+ * handlers in an array represent different "cases."
13
+ *
14
+ * @example
15
+ * assignFrom(el, {
16
+ * '?.querySelector?..routerOutlet =>': [
17
+ * { do: 'builtIns.lazyLoadSwitch', resolve: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView' } },
18
+ * { do: 'builtIns.lazyLoadSwitch', resolve: { lhs: '?.route', rhs: 'settings', instantiate: 'globalThis://settingsView' } },
19
+ * ]
20
+ * }, { withMethods: ['querySelector'], from: vm, protocols: { globalThis: k => globalThis[k] } });
21
+ */
22
+ import { LazyLoadHandler } from './lazyLoad.js';
23
+ /**
24
+ * Evaluate a comparison operation.
25
+ *
26
+ * @param lhs - Left-hand side value
27
+ * @param op - Operator string (default: '===')
28
+ * @param rhs - Right-hand side value
29
+ * @returns Boolean result of the comparison
30
+ */
31
+ function evaluateOp(lhs, op, rhs) {
32
+ switch (op) {
33
+ case '===': return lhs === rhs;
34
+ case '!==': return lhs !== rhs;
35
+ case '==': return lhs == rhs;
36
+ case '!=': return lhs != rhs;
37
+ case '<': return lhs < rhs;
38
+ case '>': return lhs > rhs;
39
+ case '<=': return lhs <= rhs;
40
+ case '>=': return lhs >= rhs;
41
+ default:
42
+ throw new Error(`builtIns.lazyLoadSwitch: unsupported operator "${op}"`);
43
+ }
44
+ }
45
+ /**
46
+ * LazyLoadSwitchHandler — comparison-based conditional template instantiation.
47
+ *
48
+ * Extends LazyLoadHandler by evaluating `lhs op rhs` to compute the condition,
49
+ * then delegating all DOM logic to the parent class.
50
+ */
51
+ export class LazyLoadSwitchHandler extends LazyLoadHandler {
52
+ async assign(lhsTarget, resolvedParams) {
53
+ const { lhs, op = '===', rhs, ...rest } = resolvedParams;
54
+ const condition = evaluateOp(lhs, op, rhs);
55
+ // Delegate to parent with computed condition
56
+ return super.assign(lhsTarget, { ...rest, if: condition });
57
+ }
58
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * builtIns.lazyLoadSwitch handler for assignFrom.
3
+ *
4
+ * Extends LazyLoadHandler with a comparison-based condition instead of a
5
+ * pre-computed boolean. Evaluates `lhs op rhs` to determine whether to
6
+ * show or hide the template content.
7
+ *
8
+ * This handler is auto-loaded by processHandlerCommands when
9
+ * `do: 'builtIns.lazyLoadSwitch'` is encountered — no explicit import is needed.
10
+ *
11
+ * Useful for routing and multi-way conditional rendering where multiple
12
+ * handlers in an array represent different "cases."
13
+ *
14
+ * @example
15
+ * assignFrom(el, {
16
+ * '?.querySelector?..routerOutlet =>': [
17
+ * { do: 'builtIns.lazyLoadSwitch', resolve: { lhs: '?.route', rhs: 'home', instantiate: 'globalThis://homeView' } },
18
+ * { do: 'builtIns.lazyLoadSwitch', resolve: { lhs: '?.route', rhs: 'settings', instantiate: 'globalThis://settingsView' } },
19
+ * ]
20
+ * }, { withMethods: ['querySelector'], from: vm, protocols: { globalThis: k => globalThis[k] } });
21
+ */
22
+
23
+ import { LazyLoadHandler } from './lazyLoad.js';
24
+ import type { AssignFromHandler } from '../assignFrom.js';
25
+
26
+ /**
27
+ * Evaluate a comparison operation.
28
+ *
29
+ * @param lhs - Left-hand side value
30
+ * @param op - Operator string (default: '===')
31
+ * @param rhs - Right-hand side value
32
+ * @returns Boolean result of the comparison
33
+ */
34
+ function evaluateOp(lhs: any, op: string, rhs: any): boolean {
35
+ switch (op) {
36
+ case '===': return lhs === rhs;
37
+ case '!==': return lhs !== rhs;
38
+ case '==': return lhs == rhs;
39
+ case '!=': return lhs != rhs;
40
+ case '<': return lhs < rhs;
41
+ case '>': return lhs > rhs;
42
+ case '<=': return lhs <= rhs;
43
+ case '>=': return lhs >= rhs;
44
+ default:
45
+ throw new Error(`builtIns.lazyLoadSwitch: unsupported operator "${op}"`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * LazyLoadSwitchHandler — comparison-based conditional template instantiation.
51
+ *
52
+ * Extends LazyLoadHandler by evaluating `lhs op rhs` to compute the condition,
53
+ * then delegating all DOM logic to the parent class.
54
+ */
55
+ export class LazyLoadSwitchHandler extends LazyLoadHandler implements AssignFromHandler {
56
+
57
+ async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<void> {
58
+ const { lhs, op = '===', rhs, ...rest } = resolvedParams;
59
+ const condition = evaluateOp(lhs, op, rhs);
60
+ // Delegate to parent with computed condition
61
+ return super.assign(lhsTarget, { ...rest, if: condition });
62
+ }
63
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * builtIns.microDataJoin handler for assignFrom.
3
+ *
4
+ * Renders a template array as semantic microdata-annotated DOM elements.
5
+ * Each dynamic value gets an HTML element based on its resolved type,
6
+ * with itemprop set to the property name.
7
+ *
8
+ * This handler is auto-loaded by processHandlerCommands when
9
+ * `do: 'builtIns.microDataJoin'` is encountered.
10
+ */
11
+ const MARKER_START_PREFIX = '?start name="';
12
+ const MARKER_END = '?end';
13
+ const MARKER_NAME = 'microDataJoin';
14
+
15
+ function findMarkers(target) {
16
+ const startText = `${MARKER_START_PREFIX}${MARKER_NAME}"`;
17
+ let startMarker = null;
18
+ let endMarker = null;
19
+ const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
20
+ let node;
21
+ while ((node = walker.nextNode())) {
22
+ if (!startMarker && node.data === startText) {
23
+ startMarker = node;
24
+ } else if (startMarker && !endMarker && node.data === MARKER_END) {
25
+ endMarker = node;
26
+ break;
27
+ }
28
+ }
29
+ return [startMarker, endMarker];
30
+ }
31
+
32
+ function createMarkers(target) {
33
+ const startMarker = document.createComment(`${MARKER_START_PREFIX}${MARKER_NAME}"`);
34
+ const endMarker = document.createComment(MARKER_END);
35
+ target.appendChild(startMarker);
36
+ target.appendChild(endMarker);
37
+ return [startMarker, endMarker];
38
+ }
39
+
40
+ function getNodesBetweenMarkers(start, end) {
41
+ const nodes = [];
42
+ let current = start.nextSibling;
43
+ while (current && current !== end) {
44
+ nodes.push(current);
45
+ current = current.nextSibling;
46
+ }
47
+ return nodes;
48
+ }
49
+
50
+ function formatForDisplay(value) {
51
+ if (value instanceof Date) {
52
+ return value.toLocaleDateString();
53
+ }
54
+ if (typeof value === 'number') {
55
+ return value.toLocaleString();
56
+ }
57
+ if (typeof value === 'boolean') {
58
+ return '';
59
+ }
60
+ return String(value ?? '');
61
+ }
62
+
63
+ function formatForAttribute(value) {
64
+ if (value instanceof Date) {
65
+ return value.toISOString();
66
+ }
67
+ return String(value);
68
+ }
69
+
70
+ function createElementForValue(prop, value) {
71
+ if (value instanceof Date) {
72
+ const el = document.createElement('time');
73
+ el.setAttribute('itemprop', prop);
74
+ el.setAttribute('datetime', formatForAttribute(value));
75
+ el.textContent = formatForDisplay(value);
76
+ return el;
77
+ }
78
+ if (typeof value === 'number') {
79
+ const el = document.createElement('data');
80
+ el.setAttribute('itemprop', prop);
81
+ el.setAttribute('value', formatForAttribute(value));
82
+ el.textContent = formatForDisplay(value);
83
+ return el;
84
+ }
85
+ if (typeof value === 'boolean') {
86
+ const el = document.createElement('data');
87
+ el.setAttribute('itemprop', prop);
88
+ el.setAttribute('value', String(value));
89
+ return el;
90
+ }
91
+ const el = document.createElement('span');
92
+ el.setAttribute('itemprop', prop);
93
+ el.textContent = String(value ?? '');
94
+ return el;
95
+ }
96
+
97
+ function updateElement(el, prop, value) {
98
+ if (value instanceof Date) {
99
+ el.setAttribute('datetime', formatForAttribute(value));
100
+ el.textContent = formatForDisplay(value);
101
+ } else if (typeof value === 'number') {
102
+ el.setAttribute('value', formatForAttribute(value));
103
+ el.textContent = formatForDisplay(value);
104
+ } else if (typeof value === 'boolean') {
105
+ el.setAttribute('value', String(value));
106
+ } else {
107
+ el.textContent = String(value ?? '');
108
+ }
109
+ }
110
+
111
+ function processTemplate(template) {
112
+ const result = [];
113
+ for (const item of template) {
114
+ if (Array.isArray(item)) {
115
+ const hasNull = item.some(el =>
116
+ el && typeof el === 'object' && 'prop' in el && el.val == null
117
+ );
118
+ if (hasNull) continue;
119
+ result.push(...processTemplate(item));
120
+ } else if (item == null) {
121
+ continue;
122
+ } else {
123
+ result.push(item);
124
+ }
125
+ }
126
+ return result;
127
+ }
128
+
129
+ export class MicroDataJoinHandler {
130
+ config;
131
+ constructor(config) {
132
+ this.config = config;
133
+ }
134
+ async assign(lhsTarget, resolvedParams) {
135
+ const { template } = resolvedParams;
136
+ if (!template || !Array.isArray(template)) {
137
+ throw new Error('builtIns.microDataJoin: resolve.template must be an array');
138
+ }
139
+ if (!(lhsTarget instanceof Element)) {
140
+ throw new Error('builtIns.microDataJoin: lhsTarget must be a DOM Element');
141
+ }
142
+ if (!lhsTarget.hasAttribute('itemscope')) {
143
+ lhsTarget.setAttribute('itemscope', '');
144
+ }
145
+ const processed = processTemplate(template);
146
+ let [startMarker, endMarker] = findMarkers(lhsTarget);
147
+ const isUpdate = startMarker !== null && endMarker !== null;
148
+ if (!isUpdate) {
149
+ [startMarker, endMarker] = createMarkers(lhsTarget);
150
+ }
151
+ if (isUpdate) {
152
+ const existingNodes = getNodesBetweenMarkers(startMarker, endMarker);
153
+ let nodeIdx = 0;
154
+ for (const segment of processed) {
155
+ if (typeof segment === 'string') {
156
+ const node = existingNodes[nodeIdx];
157
+ if (node && node.nodeType === Node.TEXT_NODE) {
158
+ if (node.textContent !== segment) {
159
+ node.textContent = segment;
160
+ }
161
+ }
162
+ nodeIdx++;
163
+ } else {
164
+ const node = existingNodes[nodeIdx];
165
+ if (node && node instanceof Element && node.getAttribute('itemprop') === segment.prop) {
166
+ updateElement(node, segment.prop, segment.val);
167
+ }
168
+ nodeIdx++;
169
+ }
170
+ }
171
+ } else {
172
+ const fragment = document.createDocumentFragment();
173
+ for (const segment of processed) {
174
+ if (typeof segment === 'string') {
175
+ fragment.appendChild(document.createTextNode(segment));
176
+ } else {
177
+ const el = createElementForValue(segment.prop, segment.val);
178
+ fragment.appendChild(el);
179
+ }
180
+ }
181
+ endMarker.parentNode.insertBefore(fragment, endMarker);
182
+ }
183
+ }
184
+ }