assign-gingerly 0.0.77 โ†’ 0.0.79

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.
package/DX/emojis.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * | ๐Ÿท๏ธ | builtIns.microDataJoin |
23
23
  * | ๐Ÿ“‹ | builtIns.manageTemplateList |
24
24
  */
25
- export const builtInEmoji: Record<string, string> = {
25
+ export const builtInEmoji= {
26
26
  '๐Ÿ“ฆ': 'builtIns.lazyLoad',
27
27
  '๐ŸŽš๏ธ': 'builtIns.lazyLoadSwitch',
28
28
  '๐Ÿ”—': 'builtIns.join',
@@ -31,7 +31,7 @@ export const builtInEmoji: Record<string, string> = {
31
31
  '๐Ÿ“Š': 'builtIns.rangeSelector',
32
32
  };
33
33
 
34
- export const akaMethods: Record<string, string> = {
34
+ export const akaMethods = {
35
35
  '๐Ÿ”': 'querySelector',
36
36
  '๐Ÿงบ': 'querySelectorAll',
37
37
  '+': 'add',
@@ -53,7 +53,7 @@ export const akaMethods: Record<string, string> = {
53
53
  //'๐Ÿงฉ': 'includes'
54
54
  };
55
55
 
56
- export const aka: Record<string, string> = {
56
+ export const aka = {
57
57
  'ยฉ๏ธ': 'content?.cloneNode?.true',
58
58
  //'๐Ÿ”Ž': 'clone?.querySelector'
59
59
  // textContent is a property, not a method โ€” aliasing it via akaMethods
package/assignGingerly.js CHANGED
@@ -433,13 +433,14 @@ export function evaluatePathWithMethods(target, pathParts, value, withMethods, p
433
433
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
434
434
  const method = current[methodName];
435
435
  if (typeof method === 'function') {
436
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
436
437
  if (isZeroArgMethod || nextIsMethod) {
437
438
  // Zero-arg call - next part is either a method or explicitly not an argument
438
- current = method.call(current);
439
+ current = method.call(current, ...appendArgs);
439
440
  }
440
441
  else {
441
442
  // Only current is method - call with next part as string arg
442
- current = method.call(current, nextPart);
443
+ current = method.call(current, nextPart, ...appendArgs);
443
444
  i++; // Skip next part since we consumed it as argument
444
445
  }
445
446
  }
@@ -541,8 +542,9 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
541
542
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
542
543
  const method = current[methodName];
543
544
  if (typeof method === 'function') {
545
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
544
546
  if (isZeroArgMethod) {
545
- current = method.call(current);
547
+ current = method.call(current, ...appendArgs);
546
548
  continue;
547
549
  }
548
550
  // For methods in the middle, we need to check the next part
@@ -551,15 +553,15 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
551
553
  const nextIsMethod = nextPart && (withMethods.has(nextPart)
552
554
  || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
553
555
  if (nextIsMethod) {
554
- current = method.call(current);
556
+ current = method.call(current, ...appendArgs);
555
557
  }
556
558
  else if (nextPart) {
557
- current = method.call(current, nextPart);
559
+ current = method.call(current, nextPart, ...appendArgs);
558
560
  // Skip next part
559
561
  pathToForEach.splice(nextIndex, 1);
560
562
  }
561
563
  else {
562
- current = method.call(current);
564
+ current = method.call(current, ...appendArgs);
563
565
  }
564
566
  }
565
567
  else {
@@ -582,15 +584,16 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
582
584
  // Last segment is a method - call it
583
585
  const method = result.target[result.lastKey];
584
586
  if (typeof method === 'function') {
587
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
585
588
  if (result.isZeroArg) {
586
- // Trailing | marker - call with no arguments, ignoring the value
587
- method.call(result.target);
589
+ // Trailing | marker - call with appended args only, ignoring the value
590
+ method.call(result.target, ...appendArgs);
588
591
  }
589
592
  else if (Array.isArray(value)) {
590
- method.apply(result.target, value);
593
+ method.apply(result.target, [...value, ...appendArgs]);
591
594
  }
592
595
  else {
593
- method.call(result.target, value);
596
+ method.call(result.target, value, ...appendArgs);
594
597
  }
595
598
  }
596
599
  }
@@ -991,12 +994,13 @@ export function assignGingerly(target, source, options, permissionProcessor) {
991
994
  // Last segment is a method โ€” call it
992
995
  const method = result.target[result.lastKey];
993
996
  if (typeof method === 'function') {
994
- // Trailing | marker - call with no arguments, ignoring the value
997
+ const appendArgs = capturedPermissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
998
+ // Trailing | marker - call with appended args only, ignoring the value
995
999
  const returnVal = result.isZeroArg
996
- ? method.call(result.target)
1000
+ ? method.call(result.target, ...appendArgs)
997
1001
  : Array.isArray(capturedValue)
998
- ? method.apply(result.target, capturedValue)
999
- : method.call(result.target, capturedValue);
1002
+ ? method.apply(result.target, [...capturedValue, ...appendArgs])
1003
+ : method.call(result.target, capturedValue, ...appendArgs);
1000
1004
  // If it's an async method, await it (for side effects)
1001
1005
  if (result.isAsyncMethod)
1002
1006
  await returnVal;
@@ -1035,15 +1039,16 @@ export function assignGingerly(target, source, options, permissionProcessor) {
1035
1039
  // Last segment is a method - call it
1036
1040
  const method = result.target[result.lastKey];
1037
1041
  if (typeof method === 'function') {
1042
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
1038
1043
  if (result.isZeroArg) {
1039
- // Trailing | marker - call with no arguments, ignoring the value
1040
- method.call(result.target);
1044
+ // Trailing | marker - call with appended args only, ignoring the value
1045
+ method.call(result.target, ...appendArgs);
1041
1046
  }
1042
1047
  else if (Array.isArray(value)) {
1043
- method.apply(result.target, value);
1048
+ method.apply(result.target, [...value, ...appendArgs]);
1044
1049
  }
1045
1050
  else {
1046
- method.call(result.target, value);
1051
+ method.call(result.target, value, ...appendArgs);
1047
1052
  }
1048
1053
  }
1049
1054
  // Silently skip if not a function
@@ -1117,15 +1122,16 @@ export function assignGingerly(target, source, options, permissionProcessor) {
1117
1122
  const methodName = isZeroArgKey ? key.slice(0, -1) : key;
1118
1123
  const method = target[methodName];
1119
1124
  if (typeof method === 'function') {
1125
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
1120
1126
  if (isZeroArgKey) {
1121
- // Trailing | marker - call with no arguments, ignoring the value
1122
- method.call(target);
1127
+ // Trailing | marker - call with appended args only, ignoring the value
1128
+ method.call(target, ...appendArgs);
1123
1129
  }
1124
1130
  else if (Array.isArray(value)) {
1125
- method.apply(target, value);
1131
+ method.apply(target, [...value, ...appendArgs]);
1126
1132
  }
1127
1133
  else {
1128
- method.call(target, value);
1134
+ method.call(target, value, ...appendArgs);
1129
1135
  }
1130
1136
  }
1131
1137
  // Silently skip if not a function
package/assignGingerly.ts CHANGED
@@ -525,12 +525,13 @@ export function evaluatePathWithMethods(
525
525
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
526
526
  const method = current[methodName];
527
527
  if (typeof method === 'function') {
528
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
528
529
  if (isZeroArgMethod || nextIsMethod) {
529
530
  // Zero-arg call - next part is either a method or explicitly not an argument
530
- current = method.call(current);
531
+ current = method.call(current, ...appendArgs);
531
532
  } else {
532
533
  // Only current is method - call with next part as string arg
533
- current = method.call(current, nextPart);
534
+ current = method.call(current, nextPart, ...appendArgs);
534
535
  i++; // Skip next part since we consumed it as argument
535
536
  }
536
537
  } else {
@@ -649,8 +650,9 @@ function applyToEach(
649
650
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
650
651
  const method = current[methodName];
651
652
  if (typeof method === 'function') {
653
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
652
654
  if (isZeroArgMethod) {
653
- current = method.call(current);
655
+ current = method.call(current, ...appendArgs);
654
656
  continue;
655
657
  }
656
658
  // For methods in the middle, we need to check the next part
@@ -659,13 +661,13 @@ function applyToEach(
659
661
  const nextIsMethod = nextPart && (withMethods.has(nextPart)
660
662
  || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
661
663
  if (nextIsMethod) {
662
- current = method.call(current);
664
+ current = method.call(current, ...appendArgs);
663
665
  } else if (nextPart) {
664
- current = method.call(current, nextPart);
666
+ current = method.call(current, nextPart, ...appendArgs);
665
667
  // Skip next part
666
668
  pathToForEach.splice(nextIndex, 1);
667
669
  } else {
668
- current = method.call(current);
670
+ current = method.call(current, ...appendArgs);
669
671
  }
670
672
  } else {
671
673
  current = current[methodName];
@@ -687,13 +689,14 @@ function applyToEach(
687
689
  // Last segment is a method - call it
688
690
  const method = result.target[result.lastKey];
689
691
  if (typeof method === 'function') {
692
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
690
693
  if (result.isZeroArg) {
691
- // Trailing | marker - call with no arguments, ignoring the value
692
- method.call(result.target);
694
+ // Trailing | marker - call with appended args only, ignoring the value
695
+ method.call(result.target, ...appendArgs);
693
696
  } else if (Array.isArray(value)) {
694
- method.apply(result.target, value);
697
+ method.apply(result.target, [...value, ...appendArgs]);
695
698
  } else {
696
- method.call(result.target, value);
699
+ method.call(result.target, value, ...appendArgs);
697
700
  }
698
701
  }
699
702
  } else {
@@ -1130,12 +1133,13 @@ export function assignGingerly(
1130
1133
  // Last segment is a method โ€” call it
1131
1134
  const method = result.target[result.lastKey];
1132
1135
  if (typeof method === 'function') {
1133
- // Trailing | marker - call with no arguments, ignoring the value
1136
+ const appendArgs = capturedPermissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
1137
+ // Trailing | marker - call with appended args only, ignoring the value
1134
1138
  const returnVal = result.isZeroArg
1135
- ? method.call(result.target)
1139
+ ? method.call(result.target, ...appendArgs)
1136
1140
  : Array.isArray(capturedValue)
1137
- ? method.apply(result.target, capturedValue)
1138
- : method.call(result.target, capturedValue);
1141
+ ? method.apply(result.target, [...capturedValue, ...appendArgs])
1142
+ : method.call(result.target, capturedValue, ...appendArgs);
1139
1143
  // If it's an async method, await it (for side effects)
1140
1144
  if (result.isAsyncMethod) await returnVal;
1141
1145
  }
@@ -1171,13 +1175,14 @@ export function assignGingerly(
1171
1175
  // Last segment is a method - call it
1172
1176
  const method = result.target[result.lastKey];
1173
1177
  if (typeof method === 'function') {
1178
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
1174
1179
  if (result.isZeroArg) {
1175
- // Trailing | marker - call with no arguments, ignoring the value
1176
- method.call(result.target);
1180
+ // Trailing | marker - call with appended args only, ignoring the value
1181
+ method.call(result.target, ...appendArgs);
1177
1182
  } else if (Array.isArray(value)) {
1178
- method.apply(result.target, value);
1183
+ method.apply(result.target, [...value, ...appendArgs]);
1179
1184
  } else {
1180
- method.call(result.target, value);
1185
+ method.call(result.target, value, ...appendArgs);
1181
1186
  }
1182
1187
  }
1183
1188
  // Silently skip if not a function
@@ -1245,13 +1250,14 @@ export function assignGingerly(
1245
1250
  const methodName = isZeroArgKey ? key.slice(0, -1) : key;
1246
1251
  const method = target[methodName];
1247
1252
  if (typeof method === 'function') {
1253
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
1248
1254
  if (isZeroArgKey) {
1249
- // Trailing | marker - call with no arguments, ignoring the value
1250
- method.call(target);
1255
+ // Trailing | marker - call with appended args only, ignoring the value
1256
+ method.call(target, ...appendArgs);
1251
1257
  } else if (Array.isArray(value)) {
1252
- method.apply(target, value);
1258
+ method.apply(target, [...value, ...appendArgs]);
1253
1259
  } else {
1254
- method.call(target, value);
1260
+ method.call(target, value, ...appendArgs);
1255
1261
  }
1256
1262
  }
1257
1263
  // Silently skip if not a function
@@ -1,11 +1,14 @@
1
1
  import { isAllowedUrl } from './isAllowedUrl.js';
2
+ import { getValue } from '../resolve/getValues.js';
2
3
  export class PermissionProcessor {
3
4
  constructor(permissions) {
4
5
  this.permissions = permissions;
5
6
  const { props, attrs } = buildMaps(permissions);
6
7
  this.props = props;
7
8
  this.attrs = attrs;
8
- this.methods = buildMethodSet(permissions);
9
+ const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
10
+ this.blockedMethods = blockedMethods;
11
+ this.configuredMethods = configuredMethods;
9
12
  this.warned = new Set();
10
13
  this.warnedMethods = new Set();
11
14
  }
@@ -25,11 +28,29 @@ export class PermissionProcessor {
25
28
  return true;
26
29
  }
27
30
  checkRestrictedMethod(methodName) {
28
- if (!this.methods.has(methodName))
31
+ if (!this.blockedMethods.has(methodName))
29
32
  return false;
30
33
  this.warnRestrictedMethod(methodName);
31
34
  return true;
32
35
  }
36
+ getMethodAppendArgs(methodName) {
37
+ const config = this.configuredMethods.get(methodName);
38
+ if (!config)
39
+ return undefined;
40
+ const rawArgs = config.appendArgs ?? config.addArgs;
41
+ if (!rawArgs || rawArgs.length === 0)
42
+ return undefined;
43
+ const resolved = [];
44
+ for (const arg of rawArgs) {
45
+ if (typeof arg === 'string' && arg.startsWith('?.')) {
46
+ resolved.push(getValue(arg, this.permissions));
47
+ }
48
+ else {
49
+ resolved.push(arg);
50
+ }
51
+ }
52
+ return resolved;
53
+ }
33
54
  redirectRestrictedProp(target, key, value) {
34
55
  if (!this.props.has(key))
35
56
  return false;
@@ -151,16 +172,22 @@ function normalizeAttrNames(attr, propNames) {
151
172
  return propNames;
152
173
  return normalizeStrings(attr);
153
174
  }
154
- function buildMethodSet(permissions) {
175
+ function buildMethodMaps(permissions) {
155
176
  const methodSettings = permissions?.restrictedMethodSettings;
156
- const methods = new Set();
177
+ const blockedMethods = new Set();
178
+ const configuredMethods = new Map();
157
179
  if (!methodSettings || methodSettings.length === 0) {
158
- return methods;
180
+ return { blockedMethods, configuredMethods };
159
181
  }
160
182
  for (const setting of methodSettings) {
161
183
  if (typeof setting === 'string') {
162
- methods.add(setting);
184
+ blockedMethods.add(setting);
185
+ continue;
186
+ }
187
+ if (configuredMethods.has(setting.method)) {
188
+ throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
163
189
  }
190
+ configuredMethods.set(setting.method, setting);
164
191
  }
165
- return methods;
192
+ return { blockedMethods, configuredMethods };
166
193
  }
@@ -1,4 +1,5 @@
1
- import type { AssignPermissions, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
1
+ import type { AssignPermissions, RestrictedMethodConfig, RestrictedPropSetting } from '../types/assign-gingerly/types.js';
2
+ import { getValue } from '../resolve/getValues.js';
2
3
  import { isAllowedUrl } from './isAllowedUrl.js';
3
4
 
4
5
  export interface RestrictedPropSettingsMap {
@@ -10,7 +11,8 @@ export class PermissionProcessor {
10
11
  private readonly permissions: AssignPermissions | undefined;
11
12
  private readonly props: Map<string, RestrictedPropSetting | undefined>;
12
13
  private readonly attrs: Map<string, RestrictedPropSetting | undefined>;
13
- private readonly methods: Set<string>;
14
+ private readonly blockedMethods: Set<string>;
15
+ private readonly configuredMethods: Map<string, RestrictedMethodConfig>;
14
16
  private readonly warned = new Set<string>();
15
17
  private readonly warnedMethods = new Set<string>();
16
18
 
@@ -19,7 +21,9 @@ export class PermissionProcessor {
19
21
  const { props, attrs } = buildMaps(permissions);
20
22
  this.props = props;
21
23
  this.attrs = attrs;
22
- this.methods = buildMethodSet(permissions);
24
+ const { blockedMethods, configuredMethods } = buildMethodMaps(permissions);
25
+ this.blockedMethods = blockedMethods;
26
+ this.configuredMethods = configuredMethods;
23
27
  }
24
28
 
25
29
  get crossDomainImports(): boolean {
@@ -41,11 +45,36 @@ export class PermissionProcessor {
41
45
  }
42
46
 
43
47
  checkRestrictedMethod(methodName: string): boolean {
44
- if (!this.methods.has(methodName)) return false;
48
+ if (!this.blockedMethods.has(methodName)) return false;
45
49
  this.warnRestrictedMethod(methodName);
46
50
  return true;
47
51
  }
48
52
 
53
+ /**
54
+ * Returns the resolved appendArgs for a configured method, if any.
55
+ * - String entries that start with `?.` are resolved against the permissions object.
56
+ * - Non-path strings are returned as-is.
57
+ * - Methods listed as plain strings in restrictedMethodSettings do not return args;
58
+ * they are fully blocked via checkRestrictedMethod.
59
+ */
60
+ getMethodAppendArgs(methodName: string): any[] | undefined {
61
+ const config = this.configuredMethods.get(methodName);
62
+ if (!config) return undefined;
63
+
64
+ const rawArgs = config.appendArgs ?? config.addArgs;
65
+ if (!rawArgs || rawArgs.length === 0) return undefined;
66
+
67
+ const resolved: any[] = [];
68
+ for (const arg of rawArgs) {
69
+ if (typeof arg === 'string' && arg.startsWith('?.')) {
70
+ resolved.push(getValue(arg, this.permissions));
71
+ } else {
72
+ resolved.push(arg);
73
+ }
74
+ }
75
+ return resolved;
76
+ }
77
+
49
78
  redirectRestrictedProp(target: any, key: string, value: any): boolean {
50
79
  if (!this.props.has(key)) return false;
51
80
  const setting = this.props.get(key);
@@ -192,19 +221,25 @@ function normalizeAttrNames(
192
221
  return normalizeStrings(attr);
193
222
  }
194
223
 
195
- function buildMethodSet(permissions: AssignPermissions | undefined): Set<string> {
224
+ function buildMethodMaps(permissions: AssignPermissions | undefined): { blockedMethods: Set<string>; configuredMethods: Map<string, RestrictedMethodConfig> } {
196
225
  const methodSettings = permissions?.restrictedMethodSettings;
197
- const methods = new Set<string>();
226
+ const blockedMethods = new Set<string>();
227
+ const configuredMethods = new Map<string, RestrictedMethodConfig>();
198
228
  if (!methodSettings || methodSettings.length === 0) {
199
- return methods;
229
+ return { blockedMethods, configuredMethods };
200
230
  }
201
231
 
202
232
  for (const setting of methodSettings) {
203
233
  if (typeof setting === 'string') {
204
- methods.add(setting);
234
+ blockedMethods.add(setting);
235
+ continue;
236
+ }
237
+
238
+ if (configuredMethods.has(setting.method)) {
239
+ throw new Error(`assignGingerly: duplicate restrictedMethodSettings entry for '${setting.method}'.`);
205
240
  }
206
- // Phase II: object-form RestrictedMethodConfig entries are ignored for now.
241
+ configuredMethods.set(setting.method, setting);
207
242
  }
208
243
 
209
- return methods;
244
+ return { blockedMethods, configuredMethods };
210
245
  }
@@ -43,13 +43,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
43
43
  // Async method โ€” call and await
44
44
  const method = current[baseName];
45
45
  if (typeof method === 'function') {
46
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
46
47
  if (isZeroArgAsync || nextIsMethod) {
47
48
  // Zero-arg call โ€” next is either a method or explicitly not an argument
48
- current = await method.call(current);
49
+ current = await method.call(current, ...appendArgs);
49
50
  }
50
51
  else {
51
52
  // Call with next part as string arg, then await
52
- current = await method.call(current, nextPart);
53
+ current = await method.call(current, nextPart, ...appendArgs);
53
54
  i++; // Skip next part since we consumed it as argument
54
55
  }
55
56
  }
@@ -65,13 +66,14 @@ export async function evaluatePathWithAsyncMethods(target, pathParts, value, wit
65
66
  // Sync method โ€” same logic as evaluatePathWithMethods
66
67
  const method = current[baseName];
67
68
  if (typeof method === 'function') {
69
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
68
70
  if (isZeroArgSync || nextIsMethod) {
69
71
  // Zero-arg call โ€” next is either a method or explicitly not an argument
70
- current = method.call(current);
72
+ current = method.call(current, ...appendArgs);
71
73
  }
72
74
  else {
73
75
  // Call with next part as string arg
74
- current = method.call(current, nextPart);
76
+ current = method.call(current, nextPart, ...appendArgs);
75
77
  i++; // Skip next part since we consumed it as argument
76
78
  }
77
79
  }
@@ -69,12 +69,13 @@ export async function evaluatePathWithAsyncMethods(
69
69
  // Async method โ€” call and await
70
70
  const method = current[baseName];
71
71
  if (typeof method === 'function') {
72
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
72
73
  if (isZeroArgAsync || nextIsMethod) {
73
74
  // Zero-arg call โ€” next is either a method or explicitly not an argument
74
- current = await method.call(current);
75
+ current = await method.call(current, ...appendArgs);
75
76
  } else {
76
77
  // Call with next part as string arg, then await
77
- current = await method.call(current, nextPart);
78
+ current = await method.call(current, nextPart, ...appendArgs);
78
79
  i++; // Skip next part since we consumed it as argument
79
80
  }
80
81
  } else {
@@ -88,12 +89,13 @@ export async function evaluatePathWithAsyncMethods(
88
89
  // Sync method โ€” same logic as evaluatePathWithMethods
89
90
  const method = current[baseName];
90
91
  if (typeof method === 'function') {
92
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(baseName) ?? [];
91
93
  if (isZeroArgSync || nextIsMethod) {
92
94
  // Zero-arg call โ€” next is either a method or explicitly not an argument
93
- current = method.call(current);
95
+ current = method.call(current, ...appendArgs);
94
96
  } else {
95
97
  // Call with next part as string arg
96
- current = method.call(current, nextPart);
98
+ current = method.call(current, nextPart, ...appendArgs);
97
99
  i++; // Skip next part since we consumed it as argument
98
100
  }
99
101
  } else {
@@ -8,9 +8,10 @@ This document provides step-by-step instructions for creating a **brand new** cu
8
8
  - Custom element features (composable behavior classes injected into elements) โ€” see [NewCustomElementFeature.md](./NewCustomElementFeature.md)
9
9
  - Enhancements (declarative behaviors attached to existing elements via attributes) โ€” see [NewEnhancementInstructions.md](./NewEnhancementInstructions.md)
10
10
 
11
- ## Reference Implementation
11
+ ## Reference Implementations
12
12
 
13
13
  - **[time-ticker](https://github.com/bahrus/time-ticker)** โ€” A non-visual custom element that fires events periodically. Demonstrates extending `ElementMaker`, a custom feature (`TimeTicker`), roundabout wiring via `defRef.json`, and the `def.js` / `wireFeatures.js` pattern.
14
+ - **[scratch-box](https://github.com/bahrus/scratch-box)** โ€” A visual, form-associated custom element with a declarative shadow DOM template and zero custom element JavaScript. Demonstrates `cede` script definition from a static `root.html` and JSON feature configuration.
14
15
 
15
16
  ## Prerequisites
16
17
 
@@ -51,7 +52,9 @@ This document provides step-by-step instructions for creating a **brand new** cu
51
52
  },
52
53
  "dependencies": {
53
54
  "assign-gingerly": "0.0.48",
54
- "el-maker": "0.0.0"
55
+ "el-maker": "0.0.0",
56
+ "imp-h": "0.0.5",
57
+ "mount-observer": "0.1.50"
55
58
  }
56
59
  }
57
60
  ```
@@ -92,7 +95,12 @@ export type T = AllProps;
92
95
  - `AllProps` โ€” includes internal/computed state managed by roundabout
93
96
  - Export `T` as a convenience alias for use in `defRef.mjs` type annotations
94
97
 
95
- ## Step 4: Create the Element Class
98
+ ## Step 4: Create the Element Class, if the complexity is too much for a "code-free" solution.
99
+
100
+ For visual web components that use declarative Shadow DOM, step 4 should be considered as a last resort, after exausting:
101
+
102
+ 1. The power of assign-gingerly/assignFrom/RoundaboutLib configuration (JSON)
103
+ 2. Defining a new reusable custom element feature to include with el-maker's package (Step 5).
96
104
 
97
105
  Create `[element-name]-element.js` (e.g., `my-element-element.js`):
98
106
 
@@ -375,9 +383,176 @@ Plus infrastructure:
375
383
 
376
384
  For elements like `time-ticker` that have no HTML template or shadow DOM, simply don't activate the `templateMaker` feature in `wireFeatures.js`. The feature remains declared in `supportedFeatures` (inherited from `ElementMaker`) but is never instantiated because no `assignFeatures` call references it.
377
385
 
378
- ## Elements With HTML (Coming Soon)
386
+ ## Elements With HTML: Declarative Shadow DOM Without a JS Class
387
+
388
+ For visual elements you can skip the `def.js` / `wireFeatures.js` / custom element class entirely and register the element with a `cede` script that extends `el-maker`. The scratch-box checkbox is the reference implementation of this pattern: it is built from a single static HTML file (`root.html`) and a JSON feature configuration (`el-maker.json`).
389
+
390
+ ### How scratch-box is structured
391
+
392
+ | File | Role |
393
+ |------|------|
394
+ | `root.html` | Declarative shadow DOM template, styles, inner form, and enhancement metadata. |
395
+ | `el-maker.mjs` | Type-checked configuration generator for the ElementMaker features. |
396
+ | `el-maker.json` | Generated JSON consumed by the `cede` script. |
397
+
398
+ ### The template file (`root.html`)
399
+
400
+ The host element declares its shadow root declaratively, then contains everything needed inside the shadow DOM, including styles, a form element, and a `<be-hive>` block that wires up declarative enhancements:
401
+
402
+ ```html
403
+ <scratch-box>
404
+ <template shadowrootmode=open>
405
+ <style adopt>
406
+ :host[hidden] { display:none; }
407
+ :host { display:block; background-color: HSL(250, 22%, 41%); padding: 1vw; }
408
+ /* ... remaining styles ... */
409
+ </style>
410
+ <form class="checkbox-wrapper">
411
+ <input ๐Ÿชข name=value type="checkbox" id="option"/>
412
+ <link itemprop=value>
413
+ <label for="option">
414
+ <slot name="labelTxt">scratch-box</slot>
415
+ <svg viewBox="0 0 60 40" aria-hidden="true" focusable="false">
416
+ <path d="M21,2 ..." stroke-width="4" fill="none" stroke-dasharray="270" stroke-dashoffset="270"></path>
417
+ </svg>
418
+ </label>
419
+ </form>
420
+
421
+ <be-hive>
422
+ <script type=emc-parser
423
+ src="be-hive/parsers/parse-grouped-capture-statements.js"
424
+ parser-name=parse-grouped-capture-statements></script>
425
+ <script type=emc
426
+ src="be-bound/๐Ÿชข.json"
427
+ wait-for-parsers=parse-grouped-capture-statements></script>
428
+ </be-hive>
429
+ </template>
430
+ </scratch-box>
431
+ ```
432
+
433
+ Key details:
434
+
435
+ - `shadowrootmode=open` gives the element a declarative shadow DOM that the browser attaches before any script runs.
436
+ - The internal checkbox is named `value` and carries the `๐Ÿชข` emoji attribute. That marks it for the `be-bound` enhancement so the host `value` property and the inner checkbox `checked` state stay in sync.
437
+ - `<link itemprop=value>` lets the `faceUp` feature expose the element as a form-associated value without any JS wiring.
438
+ - The `<slot name="labelTxt">` lets users provide the label from light DOM via `<span slot="labelTxt">...</span>`.
439
+ - `<style adopt>` with `adopt` ensures the styles are adopted into the shadow root instead of a separate `<style>` element.
440
+
441
+ ### The feature configuration (`el-maker.mjs` โ†’ `el-maker.json`)
442
+
443
+ Instead of `wireFeatures.js`, the features are declared in JSON and consumed by the `cede` script. The source file is type-checked TypeScript via JSDoc comments and outputs `el-maker.json`:
444
+
445
+ ```javascript
446
+ //@ts-check
447
+
448
+ import { writeFileSync } from 'fs';
449
+ import { fileURLToPath } from 'url';
450
+ import {akaMethods as m, aka, builtInEmoji} from 'assign-gingerly/DX/emojis.js';
451
+
452
+ /** @import {FontFaceFeatureConfig} from './types/font-face-feature/types'; */
453
+ /** @import {EndUserProps} from './types'; */
454
+ /** @import {RoundaboutOptions} from './types/roundabout/types' */
455
+ /** @import {ElMakerConfig} from './types/el-maker/types' */
456
+
457
+ const props = {
458
+ value: 'value',
459
+ name: 'name',
460
+ disabled: 'disabled',
461
+ };
462
+
463
+ const fontFaceFeatureConfig = {
464
+ fontFamilies: [
465
+ {
466
+ name: 'Indie Flower',
467
+ url: 'https://fonts.gstatic.com/s/indieflower/v24/m8JVjfNVeKWVnh3QMuKkFcZVZ0uH5dI.woff2',
468
+ descriptors: {
469
+ style: 'normal',
470
+ weight: '400',
471
+ unicodeRange: '...',
472
+ },
473
+ },
474
+ // additional font-face descriptors...
475
+ ],
476
+ };
477
+
478
+ const raConfig = {
479
+ assignOptions: {
480
+ akaMethods: {
481
+ '๐Ÿ”': m['๐Ÿ”']
482
+ }
483
+ },
484
+ merges: [
485
+ {
486
+ ifKeyIn: ['disabled'],
487
+ assign: {
488
+ '?.shadowRoot?.๐Ÿ”?.input?.disabled': '?.disabled',
489
+ }
490
+ },
491
+ ],
492
+ };
493
+
494
+ /** @type {ElMakerConfig<EndUserProps>} */
495
+ const features = {
496
+ assignFeatures: {
497
+ faceUp: { customData: { integrateWithRoundabout: true } },
498
+ truthSourcer: {},
499
+ roundabout: { customData: { raConfig } },
500
+ fontMgr: { customData: { fontFaceFeatureConfig } },
501
+ templateMaker: {},
502
+ },
503
+ };
504
+
505
+ export function render() {
506
+ return JSON.stringify(features, null, 4);
507
+ }
508
+
509
+ const __filename = fileURLToPath(import.meta.url);
510
+ const outputFile = __filename.replace(/\.mjs$/, '.json');
511
+ writeFileSync(outputFile, render(), 'utf8');
512
+ ```
513
+
514
+ Run `node el-maker.mjs` (or `npm run build-el-maker` if your `package.json` includes a watch script) to regenerate `el-maker.json`.
515
+
516
+ ### Registering the element in a page
517
+
518
+ The element is defined by a `cede` script. Include `imp-h` (or another template importer) to fetch `root.html`, and `el-maker/def.js` to provide the base class machinery:
519
+
520
+ ```html
521
+ <!DOCTYPE html>
522
+ <html lang="en">
523
+ <head>
524
+ <meta charset="UTF-8">
525
+ <title>scratch-box demo</title>
526
+ <script type=module>
527
+ import 'be-hive/be-hive.js';
528
+ import 'imp-h/imp-h.js';
529
+ import 'el-maker/def.js';
530
+ </script>
531
+ </head>
532
+ <body>
533
+ <scratch-box imp-h="scratch-box/root.html">
534
+ <span slot=labelTxt>Create demo</span>
535
+ <script type=cede data-extends=el-maker src="scratch-box/el-maker.json"></script>
536
+ </scratch-box>
537
+ </body>
538
+ </html>
539
+ ```
540
+
541
+ Notes:
542
+
543
+ - `imp-h` observes the `imp-h` attribute and imports the declarative shadow DOM template from `root.html`.
544
+ - The `<script type=cede data-extends=el-maker>` tells the mount observer to register the host element by extending `ElementMaker` and applying the feature JSON.
545
+ - No JS class file is required because all behavior is provided by the configured ElementMaker features and the declarative shadow DOM.
546
+
547
+ ### When to use this pattern
548
+
549
+ Use the declarative shadow DOM + `cede` pattern when:
550
+
551
+ - The element is primarily visual and static.
552
+ - You want server-side rendering and progressive enhancement with no client-side custom element class.
553
+ - Feature configuration (form association, attribute reflection, reactive wiring, fonts) is sufficient for all behavior.
379
554
 
380
- For elements that render static or dynamic HTML, the `templateMaker` feature handles template instantiation and shadow DOM attachment. Documentation for this pattern โ€” including how to declare templates, bind data, and integrate with roundabout โ€” will be added in a future update.
555
+ When you need custom runtime behavior beyond the shared features, fall back to the class-based pattern in the earlier sections and add your own feature.
381
556
 
382
557
  ## Tips
383
558
 
@@ -955,7 +955,8 @@ export interface RestrictedPropSetting {
955
955
  */
956
956
  export interface RestrictedMethodConfig {
957
957
  method: string;
958
- addArgs?: string[]; // Phase V: append sanitizer args
958
+ appendArgs?: string[]; // Phase II: append args to each method call
959
+ addArgs?: string[]; // Deprecated alias for appendArgs
959
960
  }
960
961
 
961
962
  /**
@@ -982,6 +983,8 @@ export interface AssignPermissions {
982
983
  /** Sanitizer options (Phase III+) */
983
984
  sanitizerOptions?: Record<string, any>;
984
985
 
986
+ customSettings?: any;
987
+
985
988
  /** Restricted method settings (Phase IV+) */
986
989
  restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
987
990
  }
@@ -999,6 +1002,7 @@ export declare class PermissionProcessor {
999
1002
  get hasAttrs(): boolean;
1000
1003
  checkRestrictedProp(key: string): boolean;
1001
1004
  checkRestrictedMethod(methodName: string): boolean;
1005
+ getMethodAppendArgs(methodName: string): any[] | undefined;
1002
1006
  redirectRestrictedProp(target: any, key: string, value: any): boolean;
1003
1007
  checkRestrictedAttributeCall(methodName: string, args: any[]): { blocked: boolean; attrName?: string };
1004
1008
  }
@@ -52,6 +52,11 @@ export interface FaceUpProps {
52
52
  */
53
53
  value: string | File | FormData | null;
54
54
 
55
+ /**
56
+ * The control name used when submitting the form.
57
+ */
58
+ name: string;
59
+
55
60
  /**
56
61
  * Internal state for form restoration (optional).
57
62
  * If provided, passed as the second argument to setFormValue().
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.77",
3
+ "version": "0.0.79",
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": {
@@ -955,7 +955,8 @@ export interface RestrictedPropSetting {
955
955
  */
956
956
  export interface RestrictedMethodConfig {
957
957
  method: string;
958
- addArgs?: string[]; // Phase V: append sanitizer args
958
+ appendArgs?: string[]; // Phase II: append args to each method call
959
+ addArgs?: string[]; // Deprecated alias for appendArgs
959
960
  }
960
961
 
961
962
  /**
@@ -982,6 +983,8 @@ export interface AssignPermissions {
982
983
  /** Sanitizer options (Phase III+) */
983
984
  sanitizerOptions?: Record<string, any>;
984
985
 
986
+ customSettings?: any;
987
+
985
988
  /** Restricted method settings (Phase IV+) */
986
989
  restrictedMethodSettings?: Array<string | RestrictedMethodConfig>;
987
990
  }
@@ -999,6 +1002,7 @@ export declare class PermissionProcessor {
999
1002
  get hasAttrs(): boolean;
1000
1003
  checkRestrictedProp(key: string): boolean;
1001
1004
  checkRestrictedMethod(methodName: string): boolean;
1005
+ getMethodAppendArgs(methodName: string): any[] | undefined;
1002
1006
  redirectRestrictedProp(target: any, key: string, value: any): boolean;
1003
1007
  checkRestrictedAttributeCall(methodName: string, args: any[]): { blocked: boolean; attrName?: string };
1004
1008
  }