assign-gingerly 0.0.59 → 0.0.61

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.
@@ -18,7 +18,7 @@
18
18
  * },
19
19
  * fromEachItem: {
20
20
  * assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
21
- * withOptions: { withMethods: ['querySelector'], inferredAssignments: true },
21
+ * withOptions: { withMethods: ['querySelector'], infer: true },
22
22
  * resolve: { key: '?.rank' }
23
23
  * }
24
24
  * }
@@ -75,6 +75,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
75
75
  }
76
76
 
77
77
  const fromEachItem = this.config.fromEachItem;
78
+ const configs = fromEachItem?.configs; // Array form for multi-element templates
78
79
  const assignToFragment = fromEachItem?.assignToFragment ?? {};
79
80
  const withOptions = fromEachItem?.withOptions ?? {};
80
81
  const perItemResolve = fromEachItem?.resolve ?? {};
@@ -85,10 +86,10 @@ export class ManageTemplateListHandler implements AssignFromHandler {
85
86
  const sourceAssignToFragment = fromSource?.assignToFragment;
86
87
  const sourceWithOptions = fromSource?.withOptions ?? {};
87
88
 
88
- // Detect fast path: no assignToFragment patterns, just inferredAssignments
89
- const hasAssignPatterns = Object.keys(assignToFragment).length > 0;
90
- const inferredConfig = withOptions.inferredAssignments;
91
- const useFastPath = !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
89
+ // Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
90
+ const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
91
+ const inferredConfig = !configs && withOptions.infer;
92
+ const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
92
93
 
93
94
  const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
94
95
 
@@ -149,17 +150,28 @@ export class ManageTemplateListHandler implements AssignFromHandler {
149
150
  if (state.keyToNodes.has(key) && oldKeys.has(key)) {
150
151
  // Existing item — update in place
151
152
  const existingNodes = state.keyToNodes.get(key)!;
152
- const rootEl = existingNodes.find(n => n instanceof Element) as Element | undefined;
153
- if (rootEl) {
154
- const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
155
- if (shouldYield) await new Promise(r => setTimeout(r, 0));
156
- if (processInferred) {
157
- processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
158
- } else {
159
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
153
+ const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
154
+ if (shouldYield) await new Promise(r => setTimeout(r, 0));
155
+
156
+ if (configs) {
157
+ // Multi-element: zip configs with element nodes
158
+ const elements = existingNodes.filter(n => n instanceof Element) as Element[];
159
+ const len = Math.min(elements.length, configs.length);
160
+ for (let j = 0; j < len; j++) {
161
+ const cfg = configs[j];
162
+ assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
160
163
  }
161
- if (sourceAssignToFragment && options?.from) {
162
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
164
+ } else {
165
+ const rootEl = existingNodes.find(n => n instanceof Element) as Element | undefined;
166
+ if (rootEl) {
167
+ if (processInferred) {
168
+ processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
169
+ } else {
170
+ assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
171
+ }
172
+ if (sourceAssignToFragment && options?.from) {
173
+ assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
174
+ }
163
175
  }
164
176
  }
165
177
  newKeyToNodes.set(key, existingNodes);
@@ -180,21 +192,32 @@ export class ManageTemplateListHandler implements AssignFromHandler {
180
192
  const clonedNodes = Array.from(content.childNodes);
181
193
 
182
194
  // Apply per-item assignments to the cloned fragment
183
- const rootEl = clonedNodes.find(n => n instanceof Element) as Element | undefined;
184
- if (rootEl) {
185
- const tempContainer = document.createDocumentFragment();
186
- tempContainer.appendChild(content);
187
-
188
- const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
189
- if (shouldYield) await new Promise(r => setTimeout(r, 0));
190
- if (processInferred) {
191
- processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
192
- } else {
193
- assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
195
+ const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
196
+ if (shouldYield) await new Promise(r => setTimeout(r, 0));
197
+
198
+ if (configs) {
199
+ // Multi-element: zip configs with element nodes
200
+ const elements = clonedNodes.filter(n => n instanceof Element) as Element[];
201
+ const len = Math.min(elements.length, configs.length);
202
+ for (let j = 0; j < len; j++) {
203
+ const cfg = configs[j];
204
+ assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
194
205
  }
206
+ } else {
207
+ const rootEl = clonedNodes.find(n => n instanceof Element) as Element | undefined;
208
+ if (rootEl) {
209
+ const tempContainer = document.createDocumentFragment();
210
+ tempContainer.appendChild(content);
195
211
 
196
- if (sourceAssignToFragment && options?.from) {
197
- assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
212
+ if (processInferred) {
213
+ processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
214
+ } else {
215
+ assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
216
+ }
217
+
218
+ if (sourceAssignToFragment && options?.from) {
219
+ assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
220
+ }
198
221
  }
199
222
  }
200
223
 
@@ -0,0 +1,52 @@
1
+ /**
2
+ * builtIns.rangeSelector handler for assignFrom.
3
+ *
4
+ * Evaluates a value against a series of range conditions and merges
5
+ * the matched case's properties into the target.
6
+ */
7
+
8
+ import assignGingerly from '../assignGingerly.js';
9
+
10
+ const OPERATORS = new Set(['<=', '<', '>=', '>', '===', '!==']);
11
+
12
+ function checkCondition(value, op, threshold) {
13
+ switch (op) {
14
+ case '<=': return value <= threshold;
15
+ case '<': return value < threshold;
16
+ case '>=': return value >= threshold;
17
+ case '>': return value > threshold;
18
+ case '===': return value === threshold;
19
+ case '!==': return value !== threshold;
20
+ default: return false;
21
+ }
22
+ }
23
+
24
+ function caseMatches(value, caseObj) {
25
+ for (const key of Object.keys(caseObj)) {
26
+ if (OPERATORS.has(key)) {
27
+ if (!checkCondition(value, key, caseObj[key])) {
28
+ return false;
29
+ }
30
+ }
31
+ }
32
+ return true;
33
+ }
34
+
35
+ export class RangeSelectorHandler {
36
+ config;
37
+ constructor(config) {
38
+ this.config = config;
39
+ }
40
+ async assign(lhsTarget, resolvedParams) {
41
+ const { value, when } = resolvedParams;
42
+ if (!Array.isArray(when)) return;
43
+ for (const caseObj of when) {
44
+ if (caseMatches(value, caseObj)) {
45
+ if (caseObj.merge && typeof caseObj.merge === 'object') {
46
+ assignGingerly(lhsTarget, caseObj.merge);
47
+ }
48
+ return;
49
+ }
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * builtIns.rangeSelector handler for assignFrom.
3
+ *
4
+ * Evaluates a value against a series of range conditions and merges
5
+ * the matched case's properties into the target. Useful for converting
6
+ * imperative if/else-if chains into declarative JSON configs.
7
+ *
8
+ * @example
9
+ * assignFrom(element, {
10
+ * '?. =>': {
11
+ * do: 'builtIns.rangeSelector',
12
+ * get: {
13
+ * value: '?.count',
14
+ * when: [
15
+ * { '<=': 10, merge: { status: 'low' } },
16
+ * { '<': 20, merge: { status: 'medium' } },
17
+ * { merge: { status: 'high' } }
18
+ * ]
19
+ * }
20
+ * }
21
+ * }, { from: vm });
22
+ */
23
+
24
+ import type { AssignFromHandler } from '../assignFromAsync.js';
25
+ import assignGingerly from '../assignGingerly.js';
26
+
27
+ /**
28
+ * Operator keys recognized in case objects.
29
+ */
30
+ const OPERATORS = new Set(['<=', '<', '>=', '>', '===', '!==']);
31
+
32
+ /**
33
+ * Check if a single operator condition is satisfied.
34
+ */
35
+ function checkCondition(value: any, op: string, threshold: any): boolean {
36
+ switch (op) {
37
+ case '<=': return value <= threshold;
38
+ case '<': return value < threshold;
39
+ case '>=': return value >= threshold;
40
+ case '>': return value > threshold;
41
+ case '===': return value === threshold;
42
+ case '!==': return value !== threshold;
43
+ default: return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if all operator conditions in a case object are satisfied (AND logic).
49
+ * Returns true if no operator keys present (catch-all/default case).
50
+ */
51
+ function caseMatches(value: any, caseObj: Record<string, any>): boolean {
52
+ let hasCondition = false;
53
+ for (const key of Object.keys(caseObj)) {
54
+ if (OPERATORS.has(key)) {
55
+ hasCondition = true;
56
+ if (!checkCondition(value, key, caseObj[key])) {
57
+ return false;
58
+ }
59
+ }
60
+ }
61
+ // No operator keys = default/catch-all
62
+ return true;
63
+ }
64
+
65
+ /**
66
+ * RangeSelectorHandler — declarative range-based conditional merge.
67
+ */
68
+ export class RangeSelectorHandler implements AssignFromHandler {
69
+ config: any;
70
+
71
+ constructor(config: any) {
72
+ this.config = config;
73
+ }
74
+
75
+ async assign(lhsTarget: any, resolvedParams: any): Promise<void> {
76
+ const { value, when } = resolvedParams;
77
+
78
+ if (!Array.isArray(when)) return;
79
+
80
+ // Find first matching case (short-circuit)
81
+ for (const caseObj of when) {
82
+ if (caseMatches(value, caseObj)) {
83
+ if (caseObj.merge && typeof caseObj.merge === 'object') {
84
+ assignGingerly(lhsTarget, caseObj.merge);
85
+ }
86
+ return; // First match wins
87
+ }
88
+ }
89
+ }
90
+ }
@@ -81,38 +81,26 @@ export class Infer {
81
81
  return inferValueProperty(this.enhancedElement);
82
82
  }
83
83
  ['|'](itempropAttr, scopeBoundary = '[itemscope]') {
84
- const candidates = this.enhancedElement.querySelectorAll(`[itemprop="${itempropAttr}"]`);
85
- return Array.from(candidates)
86
- .filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
87
- .map(x => new Infer(x, itempropAttr));
84
+ return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
88
85
  }
89
86
  ['@'](nameAttr, scopeBoundary) {
90
- const candidates = this.enhancedElement.querySelectorAll(`[name="${nameAttr}"]`);
91
- const filtered = scopeBoundary
92
- ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
93
- : Array.from(candidates);
94
- return filtered.map(x => new Infer(x, nameAttr));
87
+ return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
95
88
  }
96
89
  ['%'](partAttr, scopeBoundary) {
97
- const candidates = this.enhancedElement.querySelectorAll(`[part~="${partAttr}"]`);
98
- const filtered = scopeBoundary
99
- ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
100
- : Array.from(candidates);
101
- return filtered.map(x => new Infer(x, partAttr));
90
+ return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
102
91
  }
103
92
  ['#'](id, scopeBoundary) {
104
- const candidates = this.enhancedElement.querySelectorAll(`#${id}`);
105
- const filtered = scopeBoundary
106
- ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
107
- : Array.from(candidates);
108
- return filtered.map(x => new Infer(x, id));
93
+ return this.#queryScoped(`#${id}`, id, scopeBoundary);
109
94
  }
110
95
  ['.'](className, scopeBoundary) {
111
- const candidates = this.enhancedElement.querySelectorAll(`.${className}`);
96
+ return this.#queryScoped(`.${className}`, className, scopeBoundary);
97
+ }
98
+ #queryScoped(selector, propName, scopeBoundary) {
99
+ const candidates = this.enhancedElement.querySelectorAll(selector);
112
100
  const filtered = scopeBoundary
113
101
  ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
114
102
  : Array.from(candidates);
115
- return filtered.map(x => new Infer(x, className));
103
+ return filtered.map(x => new Infer(x, propName));
116
104
  }
117
105
  setDisplay(vm) {
118
106
  const val = this.#propName ? vm[this.#propName] : inferBindingProperty(this.enhancedElement);
@@ -103,42 +103,31 @@ export class Infer<TValue = any, TDisplay = any> {
103
103
  }
104
104
 
105
105
  ['|'](itempropAttr: string, scopeBoundary: string = '[itemscope]'){
106
- const candidates = this.enhancedElement.querySelectorAll(`[itemprop="${itempropAttr}"]`);
107
- return Array.from(candidates)
108
- .filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
109
- .map(x => new Infer(x, itempropAttr));
106
+ return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
110
107
  }
111
108
 
112
109
  ['@'](nameAttr: string, scopeBoundary?: string){
113
- const candidates = this.enhancedElement.querySelectorAll(`[name="${nameAttr}"]`);
114
- const filtered = scopeBoundary
115
- ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
116
- : Array.from(candidates);
117
- return filtered.map(x => new Infer(x, nameAttr));
110
+ return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
118
111
  }
119
112
 
120
113
  ['%'](partAttr: string, scopeBoundary?: string){
121
- const candidates = this.enhancedElement.querySelectorAll(`[part~="${partAttr}"]`);
122
- const filtered = scopeBoundary
123
- ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
124
- : Array.from(candidates);
125
- return filtered.map(x => new Infer(x, partAttr));
114
+ return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
126
115
  }
127
116
 
128
117
  ['#'](id: string, scopeBoundary?: string){
129
- const candidates = this.enhancedElement.querySelectorAll(`#${id}`);
130
- const filtered = scopeBoundary
131
- ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
132
- : Array.from(candidates);
133
- return filtered.map(x => new Infer(x, id));
118
+ return this.#queryScoped(`#${id}`, id, scopeBoundary);
134
119
  }
135
120
 
136
121
  ['.'](className: string, scopeBoundary?: string){
137
- const candidates = this.enhancedElement.querySelectorAll(`.${className}`);
122
+ return this.#queryScoped(`.${className}`, className, scopeBoundary);
123
+ }
124
+
125
+ #queryScoped(selector: string, propName: string, scopeBoundary?: string): Infer[] {
126
+ const candidates = this.enhancedElement.querySelectorAll(selector);
138
127
  const filtered = scopeBoundary
139
128
  ? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
140
129
  : Array.from(candidates);
141
- return filtered.map(x => new Infer(x, className));
130
+ return filtered.map(x => new Infer(x, propName));
142
131
  }
143
132
 
144
133
  setDisplay(vm: any){
@@ -17,11 +17,14 @@ export function processInferredAssignments(target, from, config) {
17
17
  return;
18
18
  if (!from || typeof from !== 'object')
19
19
  return;
20
- const { byItemprop } = config;
21
- if (byItemprop) {
22
- const keys = byItemprop === true
20
+ const { byItemprop, byName } = config;
21
+ // Resolve aliases: '|' → byItemprop, '@' → byName
22
+ const effectiveByItemprop = byItemprop ?? config['|'];
23
+ const effectiveByName = byName ?? config['@'];
24
+ if (effectiveByItemprop) {
25
+ const keys = effectiveByItemprop === true
23
26
  ? Object.keys(from)
24
- : byItemprop;
27
+ : effectiveByItemprop;
25
28
  const infer = new Infer(target);
26
29
  for (const key of keys) {
27
30
  if (!(key in from))
@@ -35,4 +38,31 @@ export function processInferredAssignments(target, from, config) {
35
38
  }
36
39
  }
37
40
  }
41
+ if (effectiveByName) {
42
+ // Normalize config
43
+ let keys;
44
+ let scopeBoundary;
45
+ if (effectiveByName === true) {
46
+ keys = Object.keys(from);
47
+ }
48
+ else if (Array.isArray(effectiveByName)) {
49
+ keys = effectiveByName;
50
+ }
51
+ else {
52
+ // Object form: { props, outside }
53
+ keys = effectiveByName.props === true ? Object.keys(from) : effectiveByName.props;
54
+ scopeBoundary = effectiveByName.outside;
55
+ }
56
+ const infer = new Infer(target);
57
+ for (const key of keys) {
58
+ if (!(key in from))
59
+ continue;
60
+ const value = from[key];
61
+ // Use inferencer's ['@'] method — by name attribute, optional scope boundary
62
+ const matches = infer['@'](key, scopeBoundary);
63
+ for (const match of matches) {
64
+ match.value = value;
65
+ }
66
+ }
67
+ }
38
68
  }
@@ -20,8 +20,23 @@ export interface InferredAssignmentsConfig {
20
20
  */
21
21
  byItemprop?: string[] | true;
22
22
 
23
- // Phase II:
24
- // byName?: string[] | true;
23
+ /** Concise alias for byItemprop */
24
+ '|'?: string[] | true;
25
+
26
+ /**
27
+ * Array of property keys to distribute by name attribute.
28
+ * For each key, finds [name="${key}"] elements and sets the value using
29
+ * the inferred property (value, checked, etc.).
30
+ *
31
+ * Pass `true` to infer all keys from the `from` source object.
32
+ *
33
+ * Object form enables donut-hole scoping:
34
+ * { props: ['firstName', 'lastName'], outside: 'fieldset' }
35
+ */
36
+ byName?: string[] | true | { props: string[] | true; outside: string };
37
+
38
+ /** Concise alias for byName */
39
+ '@'?: string[] | true | { props: string[] | true; outside: string };
25
40
  }
26
41
 
27
42
  /**
@@ -39,12 +54,16 @@ export function processInferredAssignments(
39
54
  if (!(target instanceof Element)) return;
40
55
  if (!from || typeof from !== 'object') return;
41
56
 
42
- const { byItemprop } = config;
57
+ const { byItemprop, byName } = config;
43
58
 
44
- if (byItemprop) {
45
- const keys = byItemprop === true
59
+ // Resolve aliases: '|' → byItemprop, '@' → byName
60
+ const effectiveByItemprop = byItemprop ?? config['|'];
61
+ const effectiveByName = byName ?? config['@'];
62
+
63
+ if (effectiveByItemprop) {
64
+ const keys = effectiveByItemprop === true
46
65
  ? Object.keys(from)
47
- : byItemprop;
66
+ : effectiveByItemprop;
48
67
 
49
68
  const infer = new Infer(target);
50
69
 
@@ -62,4 +81,35 @@ export function processInferredAssignments(
62
81
  }
63
82
  }
64
83
  }
84
+
85
+ if (effectiveByName) {
86
+ // Normalize config
87
+ let keys: string[];
88
+ let scopeBoundary: string | undefined;
89
+
90
+ if (effectiveByName === true) {
91
+ keys = Object.keys(from);
92
+ } else if (Array.isArray(effectiveByName)) {
93
+ keys = effectiveByName;
94
+ } else {
95
+ // Object form: { props, outside }
96
+ keys = effectiveByName.props === true ? Object.keys(from) : effectiveByName.props;
97
+ scopeBoundary = effectiveByName.outside;
98
+ }
99
+
100
+ const infer = new Infer(target);
101
+
102
+ for (const key of keys) {
103
+ if (!(key in from)) continue;
104
+
105
+ const value = from[key];
106
+
107
+ // Use inferencer's ['@'] method — by name attribute, optional scope boundary
108
+ const matches: Infer[] = infer['@'](key, scopeBoundary);
109
+
110
+ for (const match of matches) {
111
+ match.value = value;
112
+ }
113
+ }
114
+ }
65
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
@@ -150,6 +150,18 @@
150
150
  "default": "./assignFrom.js",
151
151
  "types": "./assignFrom.ts"
152
152
  },
153
+ "./assignFrom-extension.js": {
154
+ "default": "./assignFrom-extension.js",
155
+ "types": "./assignFrom-extension.ts"
156
+ },
157
+ "./assignFromAsync-extension.js": {
158
+ "default": "./assignFromAsync-extension.js",
159
+ "types": "./assignFromAsync-extension.ts"
160
+ },
161
+ "./builtInEmoji.js": {
162
+ "default": "./builtInEmoji.js",
163
+ "types": "./builtInEmoji.ts"
164
+ },
153
165
  "./assignFeatures.js": {
154
166
  "default": "./assignFeatures.js",
155
167
  "types": "./assignFeatures.ts"
@@ -16,9 +16,9 @@ export default defineConfig({
16
16
  /* Fail the build on CI if you accidentally left test.only in the source code. */
17
17
  forbidOnly: !!process.env.CI,
18
18
  /* Retry on CI only */
19
- retries: process.env.CI ? 2 : 0,
19
+ retries: process.env.CI ? 1 : 0,
20
20
  /* Opt out of parallel tests on CI. */
21
- workers: process.env.CI ? 1 : undefined,
21
+ workers: process.env.CI ? 2 : undefined,
22
22
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
23
23
  reporter: [ ['html', { open: 'never' }] ],
24
24
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
@@ -50,5 +50,6 @@ export default defineConfig({
50
50
  command: 'npm run serve',
51
51
  port: 8000,
52
52
  reuseExistingServer: !process.env.CI,
53
+ timeout: 30000,
53
54
  },
54
55
  });
@@ -17,6 +17,7 @@ const BUILT_IN_MAP = {
17
17
  'builtIns.join': './handlers/join.js',
18
18
  'builtIns.microDataJoin': './handlers/microDataJoin.js',
19
19
  'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
20
+ 'builtIns.rangeSelector': './handlers/rangeSelector.js',
20
21
  };
21
22
  /**
22
23
  * Find a handler class in a dynamically imported module.
@@ -70,8 +71,13 @@ async function resolveFromHandlers(name, handlers, permissions) {
70
71
  if (typeof entry === 'function') {
71
72
  return entry;
72
73
  }
73
- // Import path string — validate and dynamically import
74
+ // String value — could be a built-in alias or an import path
74
75
  if (typeof entry === 'string') {
76
+ // Built-in alias: redirect to built-in loader
77
+ if (entry.startsWith('builtIns.')) {
78
+ return loadBuiltIn(entry);
79
+ }
80
+ // Import path string — validate and dynamically import
75
81
  if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
76
82
  throw new Error(`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
77
83
  `Only relative, absolute, or bare specifier paths are allowed (no cross-domain URLs). ` +
@@ -21,6 +21,7 @@ const BUILT_IN_MAP: Record<string, string> = {
21
21
  'builtIns.join': './handlers/join.js',
22
22
  'builtIns.microDataJoin': './handlers/microDataJoin.js',
23
23
  'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
24
+ 'builtIns.rangeSelector': './handlers/rangeSelector.js',
24
25
  };
25
26
 
26
27
  /**
@@ -81,8 +82,14 @@ async function resolveFromHandlers(
81
82
  return entry as AssignFromHandlerConstructor;
82
83
  }
83
84
 
84
- // Import path string — validate and dynamically import
85
+ // String value — could be a built-in alias or an import path
85
86
  if (typeof entry === 'string') {
87
+ // Built-in alias: redirect to built-in loader
88
+ if (entry.startsWith('builtIns.')) {
89
+ return loadBuiltIn(entry);
90
+ }
91
+
92
+ // Import path string — validate and dynamically import
86
93
  if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
87
94
  throw new Error(
88
95
  `assignFrom: handler "${name}" has an invalid import path "${entry}". ` +