assign-gingerly 0.0.54 → 0.0.56

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 (112) 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 +186 -0
  13. package/handlers/lazyLoad.ts +281 -0
  14. package/handlers/lazyLoadSwitch.js +58 -0
  15. package/handlers/lazyLoadSwitch.ts +64 -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/markerUtils.js +127 -0
  104. package/markerUtils.ts +162 -0
  105. package/package.json +31 -1
  106. package/paths.js +49 -1
  107. package/paths.ts +80 -1
  108. package/processHandlerCommands.js +19 -19
  109. package/processHandlerCommands.ts +10 -13
  110. package/resolveIdRef.js +144 -125
  111. package/resolveIdRef.ts +30 -0
  112. package/types/assign-gingerly/types.d.ts +87 -0
@@ -0,0 +1,281 @@
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 type { LazyLoadResolvedParams, LazyLoadInstantiatedContext } from '../types/assign-gingerly/types.js';
24
+ import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
25
+ import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
26
+
27
+ export type { LazyLoadResolvedParams, LazyLoadInstantiatedContext };
28
+
29
+ /**
30
+ * Get the marker name from a template element (uses its id).
31
+ */
32
+ function getMarkerName(templateEl: any): string {
33
+ if (templateEl instanceof HTMLTemplateElement) {
34
+ return templateEl.id || 'anonymous';
35
+ }
36
+ if (typeof templateEl === 'string') {
37
+ return templateEl;
38
+ }
39
+ return 'anonymous';
40
+ }
41
+
42
+ /**
43
+ * LazyLoadHandler — built-in handler for conditional template instantiation.
44
+ *
45
+ * Exported so it can be subclassed for custom behavior.
46
+ */
47
+ export class LazyLoadHandler implements AssignFromHandler {
48
+ config: any;
49
+
50
+ constructor(config: any) {
51
+ this.config = config;
52
+ }
53
+
54
+ async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams): Promise<void> {
55
+ const {
56
+ if: condition,
57
+ instantiate,
58
+ method = 'appendChild',
59
+ forget = false,
60
+ transitional = false,
61
+ hideClass = DEFAULT_HIDE_CLASS,
62
+ markerName,
63
+ toggleInert = false,
64
+ toggleDisabled = false,
65
+ } = resolvedParams;
66
+
67
+ if (!(lhsTarget instanceof Element)) {
68
+ throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
69
+ }
70
+
71
+ const name = markerName ?? getMarkerName(instantiate) ?? (lhsTarget.id || 'anonymous');
72
+
73
+ // Find or create markers based on method
74
+ let [startMarker, endMarker] = method === 'after'
75
+ ? findMarkersSibling(lhsTarget, name)
76
+ : findMarkers(lhsTarget, name);
77
+
78
+ if (condition) {
79
+ // SHOW
80
+ if (startMarker && endMarker) {
81
+ const nodes = getNodesBetweenMarkers(startMarker, endMarker);
82
+ if (nodes.length > 0) {
83
+ // Content exists — show it
84
+ withTransition(startMarker, 'show', transitional, () => {
85
+ this.showNodes(nodes, transitional, hideClass, toggleInert, toggleDisabled);
86
+ });
87
+ } else {
88
+ // Content was removed (forget mode) — re-clone
89
+ if (transitional) {
90
+ ensureHideStyle(lhsTarget.getRootNode());
91
+ withTransition(startMarker, 'show', true, () => {
92
+ this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
93
+ });
94
+ } else {
95
+ await this.cloneAndInsert(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
96
+ }
97
+ }
98
+ } else {
99
+ // No markers — first time. Create markers and clone template.
100
+ if (method === 'after') {
101
+ [startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
102
+ } else {
103
+ [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
104
+ }
105
+ if (transitional) {
106
+ ensureHideStyle(lhsTarget.getRootNode());
107
+ withTransition(startMarker, 'show', true, () => {
108
+ this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
109
+ });
110
+ } else {
111
+ await this.cloneAndInsert(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
112
+ }
113
+ }
114
+ } else {
115
+ // HIDE or REMOVE
116
+ if (startMarker && endMarker) {
117
+ const nodes = getNodesBetweenMarkers(startMarker, endMarker);
118
+ if (nodes.length === 0) return;
119
+
120
+ if (forget) {
121
+ withTransition(startMarker, 'hide', transitional, () => {
122
+ for (const node of nodes) {
123
+ node.parentNode?.removeChild(node);
124
+ }
125
+ });
126
+ } else {
127
+ withTransition(startMarker, 'hide', transitional, () => {
128
+ this.hideNodes(nodes, lhsTarget, transitional, hideClass, toggleInert, toggleDisabled);
129
+ });
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Show nodes — remove hidden/class and restore inert/disabled state.
137
+ */
138
+ protected showNodes(
139
+ nodes: Node[],
140
+ transitional: boolean,
141
+ hideClass: string,
142
+ toggleInert: boolean,
143
+ toggleDisabled: boolean
144
+ ): void {
145
+ for (const node of nodes) {
146
+ if (node instanceof Element) {
147
+ if (transitional) {
148
+ node.classList.remove(hideClass);
149
+ } else {
150
+ node.removeAttribute('hidden');
151
+ }
152
+ if (toggleInert) {
153
+ node.removeAttribute('inert');
154
+ }
155
+ if (toggleDisabled && 'disabled' in node) {
156
+ (node as any).disabled = false;
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Hide nodes — add hidden/class and set inert/disabled state.
164
+ */
165
+ protected hideNodes(
166
+ nodes: Node[],
167
+ lhsTarget: Element,
168
+ transitional: boolean,
169
+ hideClass: string,
170
+ toggleInert: boolean,
171
+ toggleDisabled: boolean
172
+ ): void {
173
+ for (const node of nodes) {
174
+ if (node instanceof Element) {
175
+ if (transitional) {
176
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass);
177
+ node.classList.add(hideClass);
178
+ } else {
179
+ node.setAttribute('hidden', '');
180
+ }
181
+ if (toggleInert) {
182
+ node.setAttribute('inert', '');
183
+ }
184
+ if (toggleDisabled && 'disabled' in node) {
185
+ (node as any).disabled = true;
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Clone a template and insert its content between the markers (synchronous version).
193
+ * Used inside view transition callbacks which must be synchronous.
194
+ * Does not call async hooks (onCloneInserted, onInstantiated).
195
+ */
196
+ protected cloneAndInsertSync(
197
+ templateEl: any,
198
+ startMarker: Comment,
199
+ endMarker: Comment,
200
+ lhsTarget: Element,
201
+ resolvedParams: LazyLoadResolvedParams
202
+ ): Node[] {
203
+ let content: DocumentFragment;
204
+
205
+ if (templateEl instanceof HTMLTemplateElement) {
206
+ content = templateEl.content.cloneNode(true) as DocumentFragment;
207
+ } else if (templateEl instanceof DocumentFragment) {
208
+ content = templateEl.cloneNode(true) as DocumentFragment;
209
+ } else {
210
+ throw new Error(
211
+ `builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`
212
+ );
213
+ }
214
+
215
+ const nodes = Array.from(content.childNodes);
216
+ endMarker.parentNode!.insertBefore(content, endMarker);
217
+ return nodes;
218
+ }
219
+
220
+ /**
221
+ * Clone a template and insert its content between the markers.
222
+ * Calls onCloneInserted hook and onInstantiated callback after insertion.
223
+ */
224
+ protected async cloneAndInsert(
225
+ templateEl: any,
226
+ startMarker: Comment,
227
+ endMarker: Comment,
228
+ lhsTarget: Element,
229
+ resolvedParams: LazyLoadResolvedParams
230
+ ): Promise<Node[]> {
231
+ let content: DocumentFragment;
232
+
233
+ if (templateEl instanceof HTMLTemplateElement) {
234
+ content = templateEl.content.cloneNode(true) as DocumentFragment;
235
+ } else if (templateEl instanceof DocumentFragment) {
236
+ content = templateEl.cloneNode(true) as DocumentFragment;
237
+ } else {
238
+ throw new Error(
239
+ `builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`
240
+ );
241
+ }
242
+
243
+ // Capture nodes before insertion (childNodes empties after insertBefore)
244
+ const nodes = Array.from(content.childNodes);
245
+
246
+ // Insert before endMarker
247
+ endMarker.parentNode!.insertBefore(content, endMarker);
248
+
249
+ // Call protected hook (for subclass overrides)
250
+ await this.onCloneInserted(nodes, lhsTarget, resolvedParams);
251
+
252
+ // Call onInstantiated callback if provided (resolved from VM)
253
+ if (resolvedParams.onInstantiated && typeof resolvedParams.onInstantiated === 'function') {
254
+ const ctx: LazyLoadInstantiatedContext = {
255
+ nodes,
256
+ target: lhsTarget,
257
+ config: this.config,
258
+ resolvedParams
259
+ };
260
+ await resolvedParams.onInstantiated(ctx);
261
+ }
262
+
263
+ return nodes;
264
+ }
265
+
266
+ /**
267
+ * Hook called after template content is cloned and inserted.
268
+ * Override in subclasses for custom post-clone logic.
269
+ *
270
+ * @param nodes - The inserted child nodes
271
+ * @param lhsTarget - The target element
272
+ * @param resolvedParams - The resolved parameters
273
+ */
274
+ protected async onCloneInserted(
275
+ nodes: Node[],
276
+ lhsTarget: Element,
277
+ resolvedParams: LazyLoadResolvedParams
278
+ ): Promise<void> {
279
+ // No-op by default. Subclasses override.
280
+ }
281
+ }
@@ -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,64 @@
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
+ import type { LazyLoadSwitchResolvedParams } from '../types/assign-gingerly/types.js';
26
+
27
+ /**
28
+ * Evaluate a comparison operation.
29
+ *
30
+ * @param lhs - Left-hand side value
31
+ * @param op - Operator string (default: '===')
32
+ * @param rhs - Right-hand side value
33
+ * @returns Boolean result of the comparison
34
+ */
35
+ function evaluateOp(lhs: any, op: string, rhs: any): boolean {
36
+ switch (op) {
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
+ case '>=': return lhs >= rhs;
45
+ default:
46
+ throw new Error(`builtIns.lazyLoadSwitch: unsupported operator "${op}"`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * LazyLoadSwitchHandler — comparison-based conditional template instantiation.
52
+ *
53
+ * Extends LazyLoadHandler by evaluating `lhs op rhs` to compute the condition,
54
+ * then delegating all DOM logic to the parent class.
55
+ */
56
+ export class LazyLoadSwitchHandler extends LazyLoadHandler implements AssignFromHandler {
57
+
58
+ async assign(lhsTarget: any, resolvedParams: LazyLoadSwitchResolvedParams): Promise<void> {
59
+ const { lhs, op = '===', rhs, ...rest } = resolvedParams;
60
+ const condition = evaluateOp(lhs, op, rhs);
61
+ // Delegate to parent with computed condition
62
+ return super.assign(lhsTarget, { ...rest, if: condition });
63
+ }
64
+ }
@@ -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
+ }