@wcstack/state 3.1.0 → 3.3.0

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 (56) hide show
  1. package/README.ja.md +128 -90
  2. package/README.md +129 -91
  3. package/dist/auto.min.js +1 -1
  4. package/dist/auto.min.js.map +1 -1
  5. package/dist/define.d.ts +11 -7
  6. package/dist/index.d.ts +58 -15
  7. package/dist/index.esm.js +1 -1
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/manifest.d.ts +39 -5
  10. package/dist/manifest.esm.js +315 -253
  11. package/dist/parser.d.ts +16 -2
  12. package/dist/parser.esm.js +223 -50
  13. package/dist/split/chunks/binder.js +1 -1
  14. package/dist/split/chunks/binder.js.map +1 -1
  15. package/dist/split/chunks/checkDependency.js +1 -1
  16. package/dist/split/chunks/declarationAliases.js +2 -0
  17. package/dist/split/chunks/declarationAliases.js.map +1 -0
  18. package/dist/split/chunks/declarationHooks.js +2 -0
  19. package/dist/split/chunks/declarationHooks.js.map +1 -0
  20. package/dist/split/chunks/featureBridge.js +2 -0
  21. package/dist/split/chunks/featureBridge.js.map +1 -0
  22. package/dist/split/chunks/filterRegistry.js +1 -1
  23. package/dist/split/chunks/filterRegistry.js.map +1 -1
  24. package/dist/split/chunks/getAllPropertyDescriptors.js +1 -1
  25. package/dist/split/chunks/getAllPropertyDescriptors.js.map +1 -1
  26. package/dist/split/chunks/lifecycleHooks.js +1 -1
  27. package/dist/split/chunks/pathDiagnostics.js +1 -1
  28. package/dist/split/chunks/pathDiagnostics.js.map +1 -1
  29. package/dist/split/chunks/registerComponents.js +1 -1
  30. package/dist/split/chunks/registerComponents.js.map +1 -1
  31. package/dist/split/chunks/setAll.js +1 -1
  32. package/dist/split/chunks/setAll.js.map +1 -1
  33. package/dist/split/chunks/version.js +1 -1
  34. package/dist/split/core.d.ts +21 -9
  35. package/dist/split/core.js +1 -1
  36. package/dist/split/core.js.map +1 -1
  37. package/dist/split/features/devtools.js +1 -1
  38. package/dist/split/features/devtools.js.map +1 -1
  39. package/dist/split/features/diagnostics.js +1 -1
  40. package/dist/split/features/diagnostics.js.map +1 -1
  41. package/dist/split/features/formats.js +1 -1
  42. package/dist/split/features/formats.js.map +1 -1
  43. package/dist/split/features/recursion.js +1 -1
  44. package/dist/split/features/recursion.js.map +1 -1
  45. package/dist/split/features/scopes.js +1 -1
  46. package/dist/split/features/scopes.js.map +1 -1
  47. package/dist/split/features/ssr.js +1 -1
  48. package/dist/split/features/ssr.js.map +1 -1
  49. package/dist/split/features/temporal.js +1 -1
  50. package/dist/split/features/temporal.js.map +1 -1
  51. package/dist/wcs-manifest.json +98 -44
  52. package/package.json +1 -1
  53. package/dist/split/chunks/mount.js +0 -2
  54. package/dist/split/chunks/mount.js.map +0 -1
  55. package/dist/split/chunks/watchRuntime.js +0 -2
  56. package/dist/split/chunks/watchRuntime.js.map +0 -1
@@ -6,10 +6,99 @@ const _config = {
6
6
  // backward compatible export (read-only usage)
7
7
  const config = _config;
8
8
 
9
+ const DELIMITER = '.';
10
+ const WILDCARD = '*';
11
+ const MAX_WILDCARD_DEPTH = 128;
12
+ // data-wcs バインディング構文 `[prop][#mod]: [path][|filter...]` の区切り文字(単一正本・`@state` は v2 で撤去)。
13
+ // これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
14
+ const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
15
+ const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
16
+ const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
17
+ const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
18
+ // 修飾子(`#` 後)の語彙(単一正本)。manifest.syntax.modifiers で公開される。
19
+ // フラグ形(`#prevent` — 値を取らない)とキー値形(`#init=element` — `=` で値を取る)。
20
+ // 消費箇所(event/handler・BindingSession・twowayHandler・bindings/initialSync)は
21
+ // この定数を参照する — 文字列リテラルの散在は tooling への収載漏れの温床だった
22
+ // (docs/static-wiring-dx-design.md §2-2)。
23
+ const MODIFIER_PREVENT = 'prevent';
24
+ const MODIFIER_STOP = 'stop';
25
+ const MODIFIER_READONLY = 'ro';
26
+ const MODIFIER_FLAGS = Object.freeze([
27
+ MODIFIER_PREVENT, MODIFIER_STOP, MODIFIER_READONLY,
28
+ ]);
29
+ const MODIFIER_KEY_INIT = 'init';
30
+ const MODIFIER_KEY_SYNC = 'sync';
31
+ const MODIFIER_KEYS = Object.freeze([
32
+ MODIFIER_KEY_INIT, MODIFIER_KEY_SYNC,
33
+ ]);
34
+ // bindingType 判別と左辺 namespace の語彙(単一正本)。manifest.syntax.bindingTypes で
35
+ // 公開される。パーサ(parseBindTextsForElement)とイベント層はこの定数に分岐する。
36
+ // apply 層のディスパッチマップ(apply/applyChange.ts の applyChangeByFirstSegment)の
37
+ // キー集合との一致は __tests__/manifest.test.ts の drift テストが強制する —
38
+ // manifest エントリ(DOM 非依存)から apply 層を import しないための分離。
39
+ const ELSE_KEYWORD = 'else';
40
+ const SPREAD_PROP = '...';
41
+ const EVENT_PROP_PREFIX = 'on';
42
+ const EVENT_TOKEN_NAMESPACE = 'eventToken';
43
+ const COMMAND_NAMESPACE = 'command';
44
+ const CLASS_NAMESPACE = 'class';
45
+ const ATTR_NAMESPACE = 'attr';
46
+ const STYLE_NAMESPACE = 'style';
47
+ // リストインデックス参照名(`$1`..`$N`)の接頭辞(単一正本)。
48
+ // manifest.syntax.indexParam で公開される。
49
+ const INDEX_PARAM_PREFIX = '$';
50
+ /**
51
+ * stackIndexByIndexName
52
+ * インデックス名からスタックインデックスへのマッピング
53
+ * $1 => 0
54
+ * $2 => 1
55
+ * :
56
+ * ${i + 1} => i
57
+ * i < MAX_WILDCARD_DEPTH
58
+ */
59
+ const tmpIndexByIndexName = {};
60
+ for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
61
+ tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
62
+ }
63
+ Object.freeze(tmpIndexByIndexName);
64
+ const STATE_CONNECTED_CALLBACK_NAME = "$connectedCallback";
65
+ const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
66
+ /** 旧名 `$updatedCallback` は 3.x の間のエイリアス(要件 B12・declarationAliases.ts) */
67
+ const STATE_RENDERED_CALLBACK_NAME = "$renderedCallback";
68
+ const STATE_ERROR_CALLBACK_NAME = "$errorCallback";
69
+ const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
70
+ const STATE_BINDABLES_NAME = "$bindables";
71
+ const STATE_COMMANDS_NAME = "$commands";
72
+ const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
73
+ const STATE_COMMAND_NAMESPACE_NAME = "$command";
74
+ const STATE_EVENT_TOKENS_NAME = "$eventTokens";
75
+ const STATE_ON_NAME = "$on";
76
+ /** 旧名 `$streams` は 3.x の間のエイリアス(要件 B12・declarationAliases.ts) */
77
+ const STATE_STREAM_NAME = "$stream";
78
+ const STATE_WATCH_NAME = "$watch";
79
+ const STATE_SCAN_NAME = "$scan";
80
+ const STATE_RECURSION_NAME = "$recursion";
81
+ const STATE_LIST_KEYS_NAME = "$listKeys";
82
+ const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
83
+ const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
84
+
9
85
  function raiseError(message) {
10
86
  throw new Error(`[@wcstack/state] ${message}`);
11
87
  }
12
88
 
89
+ /**
90
+ * declarationAliases.ts — 宣言キーの旧名 → 正式名(要件 B12・docs/state-3x-naming.ja.md V13 / V14)。
91
+ *
92
+ * `$updatedCallback` → `$renderedCallback`、`$streams` → `$stream`。旧名は 3.x の間は受け、4.0 で外す(D4)。
93
+ * state オブジェクトがランタイムに入る入口(State の `_state` と `_loadStateFromSource`、マウント記録)で
94
+ * 1 回だけ正規化し、以降のすべての読み手は正式名だけを見る。両方の綴りを宣言したらどちらが効くのか
95
+ * 書き手に見えないので、名指しで落とす(D38)。
96
+ */
97
+ const DECLARATION_ALIASES = {
98
+ $updatedCallback: STATE_RENDERED_CALLBACK_NAME,
99
+ $streams: STATE_STREAM_NAME,
100
+ };
101
+
13
102
  /**
14
103
  * errorMessages.ts
15
104
  *
@@ -23,8 +112,8 @@ function raiseError(message) {
23
112
  * - optionsRequired: Error when required option is not specified
24
113
  * - optionMustBeNumber: Error when option value is not a number
25
114
  * - valueMustBeNumber: Error when value is not a number
26
- * - valueMustBeBoolean: Error when value is not boolean
27
115
  * - valueMustBeDate: Error when value is not a Date
116
+ * - valueMustBeArray: Error when value is not an array
28
117
  */
29
118
  /**
30
119
  * Throws error when filter requires at least one option but none provided.
@@ -53,15 +142,6 @@ function optionMustBeNumber(fnName) {
53
142
  function valueMustBeNumber(fnName) {
54
143
  raiseError(`filter ${fnName} requires a number value`);
55
144
  }
56
- /**
57
- * Throws error when filter requires boolean value but non-boolean provided.
58
- *
59
- * @param fnName - Name of the filter function
60
- * @returns Never returns (always throws)
61
- */
62
- function valueMustBeBoolean(fnName) {
63
- raiseError(`filter ${fnName} requires a boolean value`);
64
- }
65
145
  /**
66
146
  * Throws error when filter requires Date value but non-Date provided.
67
147
  *
@@ -81,6 +161,26 @@ function valueMustBeArray(fnName) {
81
161
  raiseError(`filter ${fnName} requires an array value`);
82
162
  }
83
163
 
164
+ /**
165
+ * filters/filterAliases.ts — 組み込みフィルタの旧名 → 正式名(要件 B12・docs/state-3x-naming.ja.md V1〜V9)。
166
+ *
167
+ * 旧名は 3.x の間エイリアスとして残り、4.0 で外す(D4)。解決は登録簿(core/filterRegistry)が行い、
168
+ * 実装・引数の個数・メタデータは正式名だけが持つ。formats の install と manifest(tooling)の両方が読むので、
169
+ * 実装にもメタデータにも依存しない小さな表として独立させている。
170
+ */
171
+ const builtinFilterAliases = {
172
+ inc: "add",
173
+ dec: "sub",
174
+ fix: "toFixed",
175
+ uc: "upper",
176
+ lc: "lower",
177
+ cap: "capitalize",
178
+ rep: "repeat",
179
+ rev: "reverse",
180
+ pad: "padStart",
181
+ null: "nullIfEmpty",
182
+ };
183
+
84
184
  /**
85
185
  * builtinFilters.ts
86
186
  *
@@ -103,6 +203,47 @@ function validateNumberString(value) {
103
203
  }
104
204
  return true;
105
205
  }
206
+ /**
207
+ * Reads one numeric option **at construction time** and returns the number itself.
208
+ *
209
+ * Every numeric filter used to write out the same three lines (`options?.[i] ?? optionsRequired`,
210
+ * `validateNumberString`, `optionMustBeNumber`) and then call `Number(opt)` again on **every
211
+ * apply**. Folding it here keeps the option checks in one place and takes the conversion off the
212
+ * hot path.
213
+ */
214
+ function numberOption(value, fnName) {
215
+ if (!validateNumberString(value)) {
216
+ optionMustBeNumber(fnName);
217
+ }
218
+ return Number(value);
219
+ }
220
+ /** A required numeric option: missing → `optionsRequired`, non-numeric → `optionMustBeNumber`. */
221
+ function requiredNumberOption(options, index, fnName) {
222
+ return numberOption(options?.[index] ?? optionsRequired(fnName), fnName);
223
+ }
224
+ /**
225
+ * Extends the display-side empty-value contract (requirement B8) to the formatting filters.
226
+ *
227
+ * A filter that builds its result with `String(value)` turns `undefined` / `null` into the
228
+ * *characters* `"undefined"` / `"null"`, so `attr.title: x|trim` wrote `title="undefined"` where
229
+ * the unfiltered `attr.title: x` correctly removes the attribute — one filter was enough to undo
230
+ * what B8 established. This family therefore passes an absent value straight through and leaves
231
+ * the decision to the apply side (attribute removed, class cleared, style cleared, text emptied).
232
+ *
233
+ * Deliberately **not** wrapped:
234
+ * - filters for which an absent value *is* the input: `defaults` / `coalesce` / `nullIfEmpty` /
235
+ * `boolean` / `truthy` / `falsy` / `not` / `eq` / `ne`;
236
+ * - the conversions, whose whole job is to convert: `int` / `float` / `number` / `string`;
237
+ * - the filters that reject a value of the wrong type (the number, date and array families).
238
+ * Their throw is confined to the one binding and reported through `$errorCallback`
239
+ * (`apply/applyChangeFromBindings.ts`), which is a different quality of failure from silently
240
+ * painting the word "undefined" into the page.
241
+ */
242
+ const nullishPassthrough = (factory) => (options) => {
243
+ const filterFn = factory(options);
244
+ // `== null` は null と undefined の両方(意図的な緩い比較)
245
+ return (value) => (value == null ? value : filterFn(value));
246
+ };
106
247
  /**
107
248
  * Equality filter - compares value with option.
108
249
  *
@@ -116,6 +257,16 @@ const eq = (options, literals) => {
116
257
  return (value) => {
117
258
  // Align types for comparison
118
259
  if (typeof value === 'number') {
260
+ // A typed literal that is not a string compares as itself (B9). A number is never equal to
261
+ // `true` / `false` / `null`, so `selectedId|eq(null)` is simply false once the id is a
262
+ // number — it used to throw `optionMustBeNumber` on **every apply**, which broke every
263
+ // binding on a path that is sometimes null and sometimes numeric. Only an unquoted
264
+ // non-number (a bare string such as `eq(abc)`) still reports the type mismatch, and it has
265
+ // to stay inside the closure: `eq` accepts values of any type, so `status|eq(active)` is
266
+ // perfectly valid and cannot be rejected at construction the way `lt` / `add` can.
267
+ if (typeof literal !== 'string') {
268
+ return value === literal;
269
+ }
119
270
  if (!validateNumberString(opt)) {
120
271
  optionMustBeNumber('eq');
121
272
  }
@@ -140,6 +291,11 @@ const ne = (options, literals) => {
140
291
  return (value) => {
141
292
  // Align types for comparison
142
293
  if (typeof value === 'number') {
294
+ // Same as `eq`: a typed `true` / `false` / `null` compares as itself instead of being
295
+ // forced through `Number()` (B9)
296
+ if (typeof literal !== 'string') {
297
+ return value !== literal;
298
+ }
143
299
  if (!validateNumberString(opt)) {
144
300
  optionMustBeNumber('ne');
145
301
  }
@@ -153,18 +309,22 @@ const ne = (options, literals) => {
153
309
  };
154
310
  };
155
311
  /**
156
- * Boolean NOT filter - inverts boolean value.
312
+ * Boolean NOT filter - inverts the truthiness of the value.
313
+ *
314
+ * Deliberately the same lenient `!value` as the engine-owned copy in
315
+ * `core/filterRegistry.ts` (`CORE_FILTERS.not`). `if:` / `else:` are built as one
316
+ * `if` parse result plus an engine-injected `not` (`structural/createNotFilter.ts`),
317
+ * and `apply/applyChangeToIf.ts` coerces the `if` side with `Boolean()`. A strict
318
+ * `not` therefore made `else:` throw — and render neither branch — whenever the
319
+ * condition was a falsy non-boolean (`0` / `""` / `undefined` / `null`). Keeping the
320
+ * two implementations identical also means a page behaves the same whether or not
321
+ * `features/formats` is installed (the split core entry registers no `not`).
157
322
  *
158
323
  * @param options - Unused
159
- * @returns Filter function that returns inverted boolean
324
+ * @returns Filter function that returns the inverted truthiness
160
325
  */
161
326
  const not = (_options) => {
162
- return (value) => {
163
- if (typeof value !== 'boolean') {
164
- valueMustBeBoolean('not');
165
- }
166
- return !value;
167
- };
327
+ return (value) => !value;
168
328
  };
169
329
  /**
170
330
  * Less than filter - checks if value is less than option.
@@ -173,15 +333,12 @@ const not = (_options) => {
173
333
  * @returns Filter function that returns boolean
174
334
  */
175
335
  const lt = (options) => {
176
- const opt = options?.[0] ?? optionsRequired('lt');
177
- if (!validateNumberString(opt)) {
178
- optionMustBeNumber('lt');
179
- }
336
+ const opt = requiredNumberOption(options, 0, 'lt');
180
337
  return (value) => {
181
338
  if (typeof value !== 'number') {
182
339
  valueMustBeNumber('lt');
183
340
  }
184
- return value < Number(opt);
341
+ return value < opt;
185
342
  };
186
343
  };
187
344
  /**
@@ -191,15 +348,12 @@ const lt = (options) => {
191
348
  * @returns Filter function that returns boolean
192
349
  */
193
350
  const le = (options) => {
194
- const opt = options?.[0] ?? optionsRequired('le');
195
- if (!validateNumberString(opt)) {
196
- optionMustBeNumber('le');
197
- }
351
+ const opt = requiredNumberOption(options, 0, 'le');
198
352
  return (value) => {
199
353
  if (typeof value !== 'number') {
200
354
  valueMustBeNumber('le');
201
355
  }
202
- return value <= Number(opt);
356
+ return value <= opt;
203
357
  };
204
358
  };
205
359
  /**
@@ -209,15 +363,12 @@ const le = (options) => {
209
363
  * @returns Filter function that returns boolean
210
364
  */
211
365
  const gt = (options) => {
212
- const opt = options?.[0] ?? optionsRequired('gt');
213
- if (!validateNumberString(opt)) {
214
- optionMustBeNumber('gt');
215
- }
366
+ const opt = requiredNumberOption(options, 0, 'gt');
216
367
  return (value) => {
217
368
  if (typeof value !== 'number') {
218
369
  valueMustBeNumber('gt');
219
370
  }
220
- return value > Number(opt);
371
+ return value > opt;
221
372
  };
222
373
  };
223
374
  /**
@@ -227,15 +378,12 @@ const gt = (options) => {
227
378
  * @returns Filter function that returns boolean
228
379
  */
229
380
  const ge = (options) => {
230
- const opt = options?.[0] ?? optionsRequired('ge');
231
- if (!validateNumberString(opt)) {
232
- optionMustBeNumber('ge');
233
- }
381
+ const opt = requiredNumberOption(options, 0, 'ge');
234
382
  return (value) => {
235
383
  if (typeof value !== 'number') {
236
384
  valueMustBeNumber('ge');
237
385
  }
238
- return value >= Number(opt);
386
+ return value >= opt;
239
387
  };
240
388
  };
241
389
  /**
@@ -245,15 +393,12 @@ const ge = (options) => {
245
393
  * @returns Filter function that returns incremented number
246
394
  */
247
395
  const inc = (options) => {
248
- const opt = options?.[0] ?? optionsRequired('inc');
249
- if (!validateNumberString(opt)) {
250
- optionMustBeNumber('inc');
251
- }
396
+ const opt = requiredNumberOption(options, 0, 'add');
252
397
  return (value) => {
253
398
  if (typeof value !== 'number') {
254
- valueMustBeNumber('inc');
399
+ valueMustBeNumber('add');
255
400
  }
256
- return value + Number(opt);
401
+ return value + opt;
257
402
  };
258
403
  };
259
404
  /**
@@ -263,15 +408,12 @@ const inc = (options) => {
263
408
  * @returns Filter function that returns decremented number
264
409
  */
265
410
  const dec = (options) => {
266
- const opt = options?.[0] ?? optionsRequired('dec');
267
- if (!validateNumberString(opt)) {
268
- optionMustBeNumber('dec');
269
- }
411
+ const opt = requiredNumberOption(options, 0, 'sub');
270
412
  return (value) => {
271
413
  if (typeof value !== 'number') {
272
- valueMustBeNumber('dec');
414
+ valueMustBeNumber('sub');
273
415
  }
274
- return value - Number(opt);
416
+ return value - opt;
275
417
  };
276
418
  };
277
419
  /**
@@ -281,15 +423,12 @@ const dec = (options) => {
281
423
  * @returns Filter function that returns multiplied number
282
424
  */
283
425
  const mul = (options) => {
284
- const opt = options?.[0] ?? optionsRequired('mul');
285
- if (!validateNumberString(opt)) {
286
- optionMustBeNumber('mul');
287
- }
426
+ const opt = requiredNumberOption(options, 0, 'mul');
288
427
  return (value) => {
289
428
  if (typeof value !== 'number') {
290
429
  valueMustBeNumber('mul');
291
430
  }
292
- return value * Number(opt);
431
+ return value * opt;
293
432
  };
294
433
  };
295
434
  /**
@@ -299,15 +438,12 @@ const mul = (options) => {
299
438
  * @returns Filter function that returns divided number
300
439
  */
301
440
  const div = (options) => {
302
- const opt = options?.[0] ?? optionsRequired('div');
303
- if (!validateNumberString(opt)) {
304
- optionMustBeNumber('div');
305
- }
441
+ const opt = requiredNumberOption(options, 0, 'div');
306
442
  return (value) => {
307
443
  if (typeof value !== 'number') {
308
444
  valueMustBeNumber('div');
309
445
  }
310
- return value / Number(opt);
446
+ return value / opt;
311
447
  };
312
448
  };
313
449
  /**
@@ -317,15 +453,12 @@ const div = (options) => {
317
453
  * @returns Filter function that returns remainder
318
454
  */
319
455
  const mod = (options) => {
320
- const opt = options?.[0] ?? optionsRequired('mod');
321
- if (!validateNumberString(opt)) {
322
- optionMustBeNumber('mod');
323
- }
456
+ const opt = requiredNumberOption(options, 0, 'mod');
324
457
  return (value) => {
325
458
  if (typeof value !== 'number') {
326
459
  valueMustBeNumber('mod');
327
460
  }
328
- return value % Number(opt);
461
+ return value % opt;
329
462
  };
330
463
  };
331
464
  /**
@@ -353,16 +486,8 @@ const abs = (_options) => {
353
486
  * @returns Filter function that returns the clamped number
354
487
  */
355
488
  const clamp = (options) => {
356
- const opt1 = options?.[0] ?? optionsRequired('clamp');
357
- if (!validateNumberString(opt1)) {
358
- optionMustBeNumber('clamp');
359
- }
360
- const opt2 = options?.[1] ?? optionsRequired('clamp');
361
- if (!validateNumberString(opt2)) {
362
- optionMustBeNumber('clamp');
363
- }
364
- const min = Number(opt1);
365
- const max = Number(opt2);
489
+ const min = requiredNumberOption(options, 0, 'clamp');
490
+ const max = requiredNumberOption(options, 1, 'clamp');
366
491
  return (value) => {
367
492
  if (typeof value !== 'number') {
368
493
  valueMustBeNumber('clamp');
@@ -377,15 +502,12 @@ const clamp = (options) => {
377
502
  * @returns Filter function that returns formatted string
378
503
  */
379
504
  const fix = (options) => {
380
- const opt = options?.[0] ?? "0";
381
- if (!validateNumberString(opt)) {
382
- optionMustBeNumber('fix');
383
- }
505
+ const opt = numberOption(options?.[0] ?? "0", 'toFixed');
384
506
  return (value) => {
385
507
  if (typeof value !== 'number') {
386
- valueMustBeNumber('fix');
508
+ valueMustBeNumber('toFixed');
387
509
  }
388
- return value.toFixed(Number(opt));
510
+ return value.toFixed(opt);
389
511
  };
390
512
  };
391
513
  /**
@@ -478,18 +600,10 @@ const trim = (_options) => {
478
600
  * @returns Filter function that returns sliced string
479
601
  */
480
602
  const slice = (options) => {
481
- const numberedOpts = [];
482
- const opt1 = options?.[0] ?? optionsRequired('slice');
483
- if (!validateNumberString(opt1)) {
484
- optionMustBeNumber('slice');
485
- }
486
- numberedOpts.push(Number(opt1));
603
+ const numberedOpts = [requiredNumberOption(options, 0, 'slice')];
487
604
  const opt2 = options?.[1];
488
605
  if (typeof opt2 !== 'undefined') {
489
- if (!validateNumberString(opt2)) {
490
- optionMustBeNumber('slice');
491
- }
492
- numberedOpts.push(Number(opt2));
606
+ numberedOpts.push(numberOption(opt2, 'slice'));
493
607
  }
494
608
  return (value) => {
495
609
  return String(value).slice(...numberedOpts);
@@ -502,16 +616,10 @@ const slice = (options) => {
502
616
  * @returns Filter function that returns substring
503
617
  */
504
618
  const substr = (options) => {
505
- const opt1 = options?.[0] ?? optionsRequired('substr');
506
- if (!validateNumberString(opt1)) {
507
- optionMustBeNumber('substr');
508
- }
509
- const opt2 = options?.[1] ?? optionsRequired('substr');
510
- if (!validateNumberString(opt2)) {
511
- optionMustBeNumber('substr');
512
- }
619
+ const opt1 = requiredNumberOption(options, 0, 'substr');
620
+ const opt2 = requiredNumberOption(options, 1, 'substr');
513
621
  return (value) => {
514
- return String(value).substr(Number(opt1), Number(opt2));
622
+ return String(value).substr(opt1, opt2);
515
623
  };
516
624
  };
517
625
  /**
@@ -521,13 +629,23 @@ const substr = (options) => {
521
629
  * @returns Filter function that returns padded string
522
630
  */
523
631
  const pad = (options) => {
524
- const opt1 = options?.[0] ?? optionsRequired('pad');
525
- if (!validateNumberString(opt1)) {
526
- optionMustBeNumber('pad');
527
- }
632
+ const opt1 = requiredNumberOption(options, 0, 'padStart');
528
633
  const opt2 = options?.[1] ?? '0';
529
634
  return (value) => {
530
- return String(value).padStart(Number(opt1), opt2);
635
+ return String(value).padStart(opt1, opt2);
636
+ };
637
+ };
638
+ /**
639
+ * padEnd filter - pads string to specified length from the end (the pair of `padStart`, requirement B12).
640
+ *
641
+ * @param options - Array with target length and pad string (default: ' ')
642
+ * @returns Filter function that returns padded string
643
+ */
644
+ const padEnd = (options) => {
645
+ const opt1 = requiredNumberOption(options, 0, 'padEnd');
646
+ const opt2 = options?.[1] ?? ' ';
647
+ return (value) => {
648
+ return String(value).padEnd(opt1, opt2);
531
649
  };
532
650
  };
533
651
  /**
@@ -537,12 +655,9 @@ const pad = (options) => {
537
655
  * @returns Filter function that returns repeated string
538
656
  */
539
657
  const rep = (options) => {
540
- const opt = options?.[0] ?? optionsRequired('rep');
541
- if (!validateNumberString(opt)) {
542
- optionMustBeNumber('rep');
543
- }
658
+ const opt = requiredNumberOption(options, 0, 'repeat');
544
659
  return (value) => {
545
- return String(value).repeat(Number(opt));
660
+ return String(value).repeat(opt);
546
661
  };
547
662
  };
548
663
  /**
@@ -585,15 +700,11 @@ const float = (_options) => {
585
700
  * @returns Filter function that returns rounded number
586
701
  */
587
702
  const round = (options) => {
588
- const opt = options?.[0] ?? '0';
589
- if (!validateNumberString(opt)) {
590
- optionMustBeNumber('round');
591
- }
703
+ const optValue = Math.pow(10, numberOption(options?.[0] ?? '0', 'round'));
592
704
  return (value) => {
593
705
  if (typeof value !== 'number') {
594
706
  valueMustBeNumber('round');
595
707
  }
596
- const optValue = Math.pow(10, Number(opt));
597
708
  return Math.round(value * optValue) / optValue;
598
709
  };
599
710
  };
@@ -604,15 +715,11 @@ const round = (options) => {
604
715
  * @returns Filter function that returns floored number
605
716
  */
606
717
  const floor = (options) => {
607
- const opt = options?.[0] ?? '0';
608
- if (!validateNumberString(opt)) {
609
- optionMustBeNumber('floor');
610
- }
718
+ const optValue = Math.pow(10, numberOption(options?.[0] ?? '0', 'floor'));
611
719
  return (value) => {
612
720
  if (typeof value !== 'number') {
613
721
  valueMustBeNumber('floor');
614
722
  }
615
- const optValue = Math.pow(10, Number(opt));
616
723
  return Math.floor(value * optValue) / optValue;
617
724
  };
618
725
  };
@@ -623,15 +730,11 @@ const floor = (options) => {
623
730
  * @returns Filter function that returns ceiled number
624
731
  */
625
732
  const ceil = (options) => {
626
- const opt = options?.[0] ?? '0';
627
- if (!validateNumberString(opt)) {
628
- optionMustBeNumber('ceil');
629
- }
733
+ const optValue = Math.pow(10, numberOption(options?.[0] ?? '0', 'ceil'));
630
734
  return (value) => {
631
735
  if (typeof value !== 'number') {
632
736
  valueMustBeNumber('ceil');
633
737
  }
634
- const optValue = Math.pow(10, Number(opt));
635
738
  return Math.ceil(value * optValue) / optValue;
636
739
  };
637
740
  };
@@ -642,15 +745,12 @@ const ceil = (options) => {
642
745
  * @returns Filter function that returns percentage string with '%'
643
746
  */
644
747
  const percent = (options) => {
645
- const opt = options?.[0] ?? '0';
646
- if (!validateNumberString(opt)) {
647
- optionMustBeNumber('percent');
648
- }
748
+ const opt = numberOption(options?.[0] ?? '0', 'percent');
649
749
  return (value) => {
650
750
  if (typeof value !== 'number') {
651
751
  valueMustBeNumber('percent');
652
752
  }
653
- return `${(value * 100).toFixed(Number(opt))}%`;
753
+ return `${(value * 100).toFixed(opt)}%`;
654
754
  };
655
755
  };
656
756
  /**
@@ -665,8 +765,10 @@ const percent = (options) => {
665
765
  * `fix` / `percent`, which already return strings. Rejecting non-numbers here would
666
766
  * break exactly the combination this filter exists for.
667
767
  *
668
- * `null` / `undefined` pass through untouched rather than becoming `"undefinedpx"`,
669
- * so the binding layer's "undefined skips the write, null clears" semantics survive.
768
+ * `null` / `undefined` pass through untouched rather than becoming `"undefinedpx"`, so the
769
+ * binding layer's "undefined skips the write, null clears" semantics survive. That guard is no
770
+ * longer written here: it is `nullishPassthrough`, shared with the rest of the `String(value)`
771
+ * family, which used to paint `"undefined"` into the page for exactly the same reason.
670
772
  *
671
773
  * @param options - Array with the unit/suffix as first element (required)
672
774
  * @returns Filter function that returns the value with the unit appended
@@ -674,9 +776,6 @@ const percent = (options) => {
674
776
  const unit = (options) => {
675
777
  const opt = options?.[0] ?? optionsRequired('unit');
676
778
  return (value) => {
677
- if (value === null || typeof value === 'undefined') {
678
- return value;
679
- }
680
779
  return String(value) + opt;
681
780
  };
682
781
  };
@@ -709,11 +808,7 @@ const join = (options) => {
709
808
  * @returns Filter function that returns the truncated string
710
809
  */
711
810
  const truncate = (options) => {
712
- const opt1 = options?.[0] ?? optionsRequired('truncate');
713
- if (!validateNumberString(opt1)) {
714
- optionMustBeNumber('truncate');
715
- }
716
- const maxLength = Number(opt1);
811
+ const maxLength = requiredNumberOption(options, 0, 'truncate');
717
812
  const suffix = options?.[1] ?? '…';
718
813
  return (value) => {
719
814
  const v = String(value);
@@ -847,6 +942,18 @@ const defaults = (options, literals) => {
847
942
  return value;
848
943
  };
849
944
  };
945
+ /**
946
+ * coalesce filter - returns the default only for null / undefined (`defaults` also replaces 0, false and "").
947
+ * Requirement B12: the nullish counterpart of `defaults`, read like SQL COALESCE.
948
+ *
949
+ * @param options - Array with default value as first element
950
+ * @returns Filter function that returns value or default
951
+ */
952
+ const coalesce = (options, literals) => {
953
+ const opt = options?.[0] ?? optionsRequired('coalesce');
954
+ const fallback = literals !== undefined && literals.length > 0 ? literals[0] : opt;
955
+ return (value) => value ?? fallback;
956
+ };
850
957
  /**
851
958
  * Boolean filter - converts value to boolean.
852
959
  *
@@ -899,25 +1006,27 @@ const builtinFilters = {
899
1006
  "le": le,
900
1007
  "gt": gt,
901
1008
  "ge": ge,
902
- "inc": inc,
903
- "dec": dec,
1009
+ "add": inc,
1010
+ "sub": dec,
904
1011
  "mul": mul,
905
1012
  "div": div,
906
1013
  "mod": mod,
907
1014
  "abs": abs,
908
1015
  "clamp": clamp,
909
- "fix": fix,
1016
+ "toFixed": fix,
910
1017
  "locale": locale,
911
- "uc": uc,
912
- "lc": lc,
913
- "cap": cap,
914
- "trim": trim,
915
- "slice": slice,
916
- "substr": substr,
917
- "pad": pad,
918
- "rep": rep,
919
- "rev": rev,
920
- "truncate": truncate,
1018
+ // `String(value)` で組み立てる族は、値が無いときに素通しする(要件 B8 — nullishPassthrough を参照)
1019
+ "upper": nullishPassthrough(uc),
1020
+ "lower": nullishPassthrough(lc),
1021
+ "capitalize": nullishPassthrough(cap),
1022
+ "trim": nullishPassthrough(trim),
1023
+ "slice": nullishPassthrough(slice),
1024
+ "substr": nullishPassthrough(substr),
1025
+ "padStart": nullishPassthrough(pad),
1026
+ "padEnd": nullishPassthrough(padEnd),
1027
+ "repeat": nullishPassthrough(rep),
1028
+ "reverse": nullishPassthrough(rev),
1029
+ "truncate": nullishPassthrough(truncate),
921
1030
  "join": join,
922
1031
  "int": int,
923
1032
  "float": float,
@@ -925,7 +1034,7 @@ const builtinFilters = {
925
1034
  "floor": floor,
926
1035
  "ceil": ceil,
927
1036
  "percent": percent,
928
- "unit": unit,
1037
+ "unit": nullishPassthrough(unit),
929
1038
  "date": date,
930
1039
  "time": time,
931
1040
  "datetime": datetime,
@@ -934,10 +1043,11 @@ const builtinFilters = {
934
1043
  "falsy": falsy,
935
1044
  "truthy": truthy,
936
1045
  "defaults": defaults,
1046
+ "coalesce": coalesce,
937
1047
  "boolean": boolean,
938
1048
  "number": number,
939
1049
  "string": string,
940
- "null": _null,
1050
+ "nullIfEmpty": _null,
941
1051
  };
942
1052
  const outputBuiltinFilters = builtinFilters;
943
1053
 
@@ -957,34 +1067,39 @@ const builtinFilterMeta = {
957
1067
  // 比較・論理
958
1068
  eq: { description: "等しいか比較", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
959
1069
  ne: { description: "異なるか比較", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
960
- not: { description: "ブール値を反転", hasArgs: false, resultType: "boolean", acceptTypes: ["boolean"], minArgs: 0, maxArgs: 0 },
1070
+ // `not` は真偽性(truthy / falsy)の反転。`if:` が `Boolean()` で寄せるのと同じ規則で、
1071
+ // `else:` はこのフィルタを足した束縛として組み立てられる(structural/createNotFilter.ts)
1072
+ not: { description: "真偽性を反転(falsy → true)", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
961
1073
  lt: { description: "より小さいか", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
962
1074
  le: { description: "以下か", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
963
1075
  gt: { description: "より大きいか", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
964
1076
  ge: { description: "以上か", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
965
1077
  // 算術
966
- inc: { description: "加算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
967
- dec: { description: "減算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
1078
+ add: { description: "加算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1079
+ sub: { description: "減算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
968
1080
  mul: { description: "乗算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
969
1081
  div: { description: "除算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
970
1082
  mod: { description: "剰余", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
971
1083
  abs: { description: "絶対値", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
972
1084
  clamp: { description: "範囲内に丸める (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
973
1085
  // 数値フォーマット
974
- fix: { description: "固定小数点表記", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
1086
+ toFixed: { description: "固定小数点表記", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
975
1087
  locale: { description: "ロケール形式で数値フォーマット", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
976
1088
  // 文字列
977
- uc: { description: "大文字に変換", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
978
- lc: { description: "小文字に変換", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
979
- cap: { description: "先頭文字を大文字に", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
1089
+ upper: { description: "大文字に変換", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
1090
+ lower: { description: "小文字に変換", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
1091
+ capitalize: { description: "先頭文字を大文字に", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
980
1092
  trim: { description: "前後の空白を削除", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
981
1093
  slice: { description: "部分文字列 (start[,end])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "number"] },
982
- substr: { description: "部分文字列 (pos,len)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "number"] },
983
- pad: { description: "パディング (length[,char])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
984
- rep: { description: "繰り返し (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
985
- rev: { description: "文字順を反転", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
986
- truncate: { description: "切り詰めて省略記号 (length[,suffix])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
987
- join: { description: "配列を連結 ([separator])", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1094
+ // 長さは省略できない(実装が両方読む)。minArgs: 1 だった頃は補完・lint が `substr(0)` を
1095
+ // 通し、実行時にだけ落ちていた
1096
+ substr: { description: "部分文字列 (pos,len)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
1097
+ padStart: { description: "先頭を埋める (length[,char]。char の既定は 0 — JS の既定は空白なので注意)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
1098
+ padEnd: { description: "末尾を埋める (length[,char]。char の既定は空白 — JS と同じ)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
1099
+ repeat: { description: "繰り返し (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1100
+ reverse: { description: "文字順を反転", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
1101
+ truncate: { description: "切り詰めて省略記号 (length[,suffix]。suffix の既定は … — U+2026 の 1 文字)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
1102
+ join: { description: "配列を連結 ([separator]。既定はカンマ + 空白)", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
988
1103
  // 数値パース・丸め
989
1104
  int: { description: "整数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
990
1105
  float: { description: "浮動小数点数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
@@ -996,19 +1111,22 @@ const builtinFilterMeta = {
996
1111
  // それらは既に string を返すため(builtinFilters.ts の unit を参照)
997
1112
  unit: { description: "単位(接尾辞)を付加", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
998
1113
  // 日付・時刻
999
- date: { description: "ロケール形式の日付", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1000
- time: { description: "ロケール形式の時刻", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1001
- datetime: { description: "ロケール形式の日時", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1002
- ymd: { description: "YYYY-MM-DD 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1003
- hms: { description: "HH:MM:SS 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1114
+ // `locale` と同じくロケールを 1 つ受ける(`date(ja-JP)`)。実装は最初からこれを読んでいたが、
1115
+ // メタデータ側が maxArgs: 0 だったため lint と補完が正しい書き方を誤りとして報告していた
1116
+ date: { description: "ロケール形式の日付 ([locale]。既定は config.locale)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1117
+ time: { description: "ロケール形式の時刻 ([locale]。既定は config.locale)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1118
+ datetime: { description: "ロケール形式の日時 ([locale]。既定は config.locale)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1119
+ ymd: { description: "YYYY-MM-DD 形式 ([separator]。既定は -)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1120
+ hms: { description: "HH:MM:SS 形式 ([separator]。既定は :)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1004
1121
  // 真偽値・変換
1005
1122
  falsy: { description: "偽値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1006
1123
  truthy: { description: "真値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1007
1124
  defaults: { description: "偽値の場合デフォルト値", hasArgs: true, resultType: "passthrough", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
1125
+ coalesce: { description: "null / undefined の場合デフォルト値", hasArgs: true, resultType: "passthrough", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
1008
1126
  boolean: { description: "ブール値に変換", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1009
1127
  number: { description: "数値に変換", hasArgs: false, resultType: "number", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1010
1128
  string: { description: "文字列に変換", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1011
- null: { description: "空文字列をnullに変換", hasArgs: false, resultType: "passthrough", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
1129
+ nullIfEmpty: { description: "空文字列をnullに変換", hasArgs: false, resultType: "passthrough", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
1012
1130
  };
1013
1131
 
1014
1132
  const STRUCTURAL_BINDING_TYPE_SET = new Set([
@@ -1018,80 +1136,6 @@ const STRUCTURAL_BINDING_TYPE_SET = new Set([
1018
1136
  "for",
1019
1137
  ]);
1020
1138
 
1021
- const DELIMITER = '.';
1022
- const WILDCARD = '*';
1023
- const MAX_WILDCARD_DEPTH = 128;
1024
- // data-wcs バインディング構文 `[prop][#mod]: [path][|filter...]` の区切り文字(単一正本・`@state` は v2 で撤去)。
1025
- // これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
1026
- const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
1027
- const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
1028
- const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
1029
- const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
1030
- // 修飾子(`#` 後)の語彙(単一正本)。manifest.syntax.modifiers で公開される。
1031
- // フラグ形(`#prevent` — 値を取らない)とキー値形(`#init=element` — `=` で値を取る)。
1032
- // 消費箇所(event/handler・BindingSession・twowayHandler・bindings/initialSync)は
1033
- // この定数を参照する — 文字列リテラルの散在は tooling への収載漏れの温床だった
1034
- // (docs/static-wiring-dx-design.md §2-2)。
1035
- const MODIFIER_PREVENT = 'prevent';
1036
- const MODIFIER_STOP = 'stop';
1037
- const MODIFIER_READONLY = 'ro';
1038
- const MODIFIER_FLAGS = Object.freeze([
1039
- MODIFIER_PREVENT, MODIFIER_STOP, MODIFIER_READONLY,
1040
- ]);
1041
- const MODIFIER_KEY_INIT = 'init';
1042
- const MODIFIER_KEY_SYNC = 'sync';
1043
- const MODIFIER_KEYS = Object.freeze([
1044
- MODIFIER_KEY_INIT, MODIFIER_KEY_SYNC,
1045
- ]);
1046
- // bindingType 判別と左辺 namespace の語彙(単一正本)。manifest.syntax.bindingTypes で
1047
- // 公開される。パーサ(parseBindTextsForElement)とイベント層はこの定数に分岐する。
1048
- // apply 層のディスパッチマップ(apply/applyChange.ts の applyChangeByFirstSegment)の
1049
- // キー集合との一致は __tests__/manifest.test.ts の drift テストが強制する —
1050
- // manifest エントリ(DOM 非依存)から apply 層を import しないための分離。
1051
- const ELSE_KEYWORD = 'else';
1052
- const SPREAD_PROP = '...';
1053
- const EVENT_PROP_PREFIX = 'on';
1054
- const EVENT_TOKEN_NAMESPACE = 'eventToken';
1055
- const COMMAND_NAMESPACE = 'command';
1056
- const CLASS_NAMESPACE = 'class';
1057
- const ATTR_NAMESPACE = 'attr';
1058
- const STYLE_NAMESPACE = 'style';
1059
- // リストインデックス参照名(`$1`..`$N`)の接頭辞(単一正本)。
1060
- // manifest.syntax.indexParam で公開される。
1061
- const INDEX_PARAM_PREFIX = '$';
1062
- /**
1063
- * stackIndexByIndexName
1064
- * インデックス名からスタックインデックスへのマッピング
1065
- * $1 => 0
1066
- * $2 => 1
1067
- * :
1068
- * ${i + 1} => i
1069
- * i < MAX_WILDCARD_DEPTH
1070
- */
1071
- const tmpIndexByIndexName = {};
1072
- for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
1073
- tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
1074
- }
1075
- Object.freeze(tmpIndexByIndexName);
1076
- const STATE_CONNECTED_CALLBACK_NAME = "$connectedCallback";
1077
- const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
1078
- const STATE_UPDATED_CALLBACK_NAME = "$updatedCallback";
1079
- const STATE_ERROR_CALLBACK_NAME = "$errorCallback";
1080
- const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
1081
- const STATE_BINDABLES_NAME = "$bindables";
1082
- const STATE_COMMANDS_NAME = "$commands";
1083
- const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
1084
- const STATE_COMMAND_NAMESPACE_NAME = "$command";
1085
- const STATE_EVENT_TOKENS_NAME = "$eventTokens";
1086
- const STATE_ON_NAME = "$on";
1087
- const STATE_STREAMS_NAME = "$streams";
1088
- const STATE_WATCH_NAME = "$watch";
1089
- const STATE_SCAN_NAME = "$scan";
1090
- const STATE_RECURSION_NAME = "$recursion";
1091
- const STATE_LIST_KEYS_NAME = "$listKeys";
1092
- const STATE_STREAM_STATUS_NAMESPACE_NAME = "$streamStatus";
1093
- const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
1094
-
1095
1139
  /**
1096
1140
  * manifest.ts — `<wcs-state>` の構文・フィルタ・予約名を機械可読な単一正本として公開する。
1097
1141
  *
@@ -1105,8 +1149,23 @@ const STATE_STREAM_ERROR_NAMESPACE_NAME = "$streamError";
1105
1149
  * - 将来 `dist/wcs-manifest.json` としてビルド時に書き出し、vscode-wcs がそれを読む形に発展させる。
1106
1150
  * - ドリフト検出テスト(__tests__/manifest.test.ts)が、フィルタ集合の golden と実装の一致を CI で保証する。
1107
1151
  */
1108
- /** マニフェストのバージョン(構造を変えたら上げる)。 */
1109
- const WCS_MANIFEST_VERSION = 1;
1152
+ /**
1153
+ * マニフェストのバージョン(構造を変えたら上げる)。
1154
+ *
1155
+ * 3.1 で `syntax.bindingTypes.explicitPropertyPrefix`、3.2 で `filterAliases`、
1156
+ * 3.x の次で `declarationAliases` / `apiAliases` を足したので 2。
1157
+ * 消費側(vscode-wcs)はまだこの定数を参照していないが、公開している以上ドリフトさせない。
1158
+ */
1159
+ const WCS_MANIFEST_VERSION = 2;
1160
+ /**
1161
+ * state API の旧名 → 正式名(要件 B12・docs/state-3x-naming.ja.md)。正本は
1162
+ * `proxy/traps/get.ts` の case ラベルで、ここはそれを機械可読にした写し
1163
+ * (一致は `__tests__/manifest.test.ts` が固定する)。
1164
+ */
1165
+ const STATE_API_ALIASES = Object.freeze({
1166
+ $trackDependency: "$dependOn",
1167
+ $untrackDependency: "$untracked",
1168
+ });
1110
1169
  /** 機械可読な単一正本を返す。vscode-wcs はこれを消費する想定。 */
1111
1170
  function getWcsManifest() {
1112
1171
  return {
@@ -1150,10 +1209,13 @@ function getWcsManifest() {
1150
1209
  // 実装(Record のキー)から自動導出。手リストを持たない=ドリフトの構造的排除。
1151
1210
  filters: Object.keys(outputBuiltinFilters),
1152
1211
  filterMeta: builtinFilterMeta,
1212
+ filterAliases: builtinFilterAliases,
1213
+ declarationAliases: DECLARATION_ALIASES,
1214
+ apiAliases: STATE_API_ALIASES,
1153
1215
  reservedLifecycle: [
1154
1216
  STATE_CONNECTED_CALLBACK_NAME,
1155
1217
  STATE_DISCONNECTED_CALLBACK_NAME,
1156
- STATE_UPDATED_CALLBACK_NAME,
1218
+ STATE_RENDERED_CALLBACK_NAME,
1157
1219
  STATE_ERROR_CALLBACK_NAME,
1158
1220
  WEBCOMPONENT_STATE_READY_CALLBACK_NAME,
1159
1221
  ],
@@ -1164,7 +1226,7 @@ function getWcsManifest() {
1164
1226
  STATE_COMMAND_NAMESPACE_NAME,
1165
1227
  STATE_EVENT_TOKENS_NAME,
1166
1228
  STATE_ON_NAME,
1167
- STATE_STREAMS_NAME,
1229
+ STATE_STREAM_NAME,
1168
1230
  STATE_WATCH_NAME,
1169
1231
  STATE_SCAN_NAME,
1170
1232
  STATE_LIST_KEYS_NAME,
@@ -1175,4 +1237,4 @@ function getWcsManifest() {
1175
1237
  };
1176
1238
  }
1177
1239
 
1178
- export { STRUCTURAL_BINDING_TYPE_SET, WCS_MANIFEST_VERSION, builtinFilterMeta, getWcsManifest };
1240
+ export { DECLARATION_ALIASES, STATE_API_ALIASES, STRUCTURAL_BINDING_TYPE_SET, WCS_MANIFEST_VERSION, builtinFilterAliases, builtinFilterMeta, getWcsManifest };