@wcstack/state 3.5.4 → 4.0.0-rc.2

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 (82) hide show
  1. package/README.ja.md +669 -428
  2. package/README.md +667 -426
  3. package/dist/auto.min.js +1 -2
  4. package/dist/define.d.ts +4 -8
  5. package/dist/define.js +1 -1
  6. package/dist/index.d.ts +139 -1483
  7. package/dist/index.esm.js +1 -2
  8. package/dist/manifest.d.ts +18 -59
  9. package/dist/manifest.esm.js +542 -1202
  10. package/dist/parser.d.ts +49 -103
  11. package/dist/parser.esm.js +662 -831
  12. package/dist/split/auto.js +1 -0
  13. package/dist/split/chunks/chunk-732CB4QN.js +1 -0
  14. package/dist/split/chunks/chunk-7PN5YNZZ.js +1 -0
  15. package/dist/split/chunks/chunk-FF6KHTYS.js +1 -0
  16. package/dist/split/chunks/chunk-HFKGUDDJ.js +1 -0
  17. package/dist/split/chunks/chunk-PKDBVBAP.js +1 -0
  18. package/dist/split/chunks/chunk-QI7P7YPP.js +1 -0
  19. package/dist/split/chunks/chunk-QQH6N5CJ.js +1 -0
  20. package/dist/split/chunks/chunk-UM3O6QXD.js +1 -0
  21. package/dist/split/chunks/chunk-WEL3YSG7.js +1 -0
  22. package/dist/split/chunks/chunk-WWJNAZVP.js +1 -0
  23. package/dist/split/chunks/chunk-YOPPVWPQ.js +1 -0
  24. package/dist/split/core.d.ts +114 -1168
  25. package/dist/split/core.js +1 -2
  26. package/dist/split/features/devtools.d.ts +23 -3
  27. package/dist/split/features/devtools.js +1 -2
  28. package/dist/split/features/diagnostics.d.ts +27 -5
  29. package/dist/split/features/diagnostics.js +1 -2
  30. package/dist/split/features/formats.d.ts +21 -5
  31. package/dist/split/features/formats.js +1 -2
  32. package/dist/split/features/list-keys.d.ts +24 -0
  33. package/dist/split/features/list-keys.js +1 -0
  34. package/dist/split/features/recursion.d.ts +23 -3
  35. package/dist/split/features/recursion.js +1 -2
  36. package/dist/split/features/scopes.d.ts +25 -4
  37. package/dist/split/features/scopes.js +1 -2
  38. package/dist/split/features/ssr.d.ts +23 -3
  39. package/dist/split/features/ssr.js +1 -2
  40. package/dist/split/features/temporal.d.ts +19 -4
  41. package/dist/split/features/temporal.js +1 -2
  42. package/dist/wcs-manifest.json +43 -49
  43. package/package.json +15 -17
  44. package/dist/auto.min.js.map +0 -1
  45. package/dist/index.esm.js.map +0 -1
  46. package/dist/split/chunks/binder.js +0 -2
  47. package/dist/split/chunks/binder.js.map +0 -1
  48. package/dist/split/chunks/checkDependency.js +0 -2
  49. package/dist/split/chunks/checkDependency.js.map +0 -1
  50. package/dist/split/chunks/declarationAliases.js +0 -2
  51. package/dist/split/chunks/declarationAliases.js.map +0 -1
  52. package/dist/split/chunks/declarationHooks.js +0 -2
  53. package/dist/split/chunks/declarationHooks.js.map +0 -1
  54. package/dist/split/chunks/diagnosticsHooks.js +0 -2
  55. package/dist/split/chunks/diagnosticsHooks.js.map +0 -1
  56. package/dist/split/chunks/errorGuidance.js +0 -2
  57. package/dist/split/chunks/errorGuidance.js.map +0 -1
  58. package/dist/split/chunks/featureBridge.js +0 -2
  59. package/dist/split/chunks/featureBridge.js.map +0 -1
  60. package/dist/split/chunks/features.d.ts +0 -29
  61. package/dist/split/chunks/filterRegistry.js +0 -2
  62. package/dist/split/chunks/filterRegistry.js.map +0 -1
  63. package/dist/split/chunks/getAllPropertyDescriptors.js +0 -2
  64. package/dist/split/chunks/getAllPropertyDescriptors.js.map +0 -1
  65. package/dist/split/chunks/lifecycleHooks.js +0 -2
  66. package/dist/split/chunks/lifecycleHooks.js.map +0 -1
  67. package/dist/split/chunks/pathDiagnostics.js +0 -2
  68. package/dist/split/chunks/pathDiagnostics.js.map +0 -1
  69. package/dist/split/chunks/registerComponents.js +0 -2
  70. package/dist/split/chunks/registerComponents.js.map +0 -1
  71. package/dist/split/chunks/setAll.js +0 -2
  72. package/dist/split/chunks/setAll.js.map +0 -1
  73. package/dist/split/chunks/version.js +0 -2
  74. package/dist/split/chunks/version.js.map +0 -1
  75. package/dist/split/core.js.map +0 -1
  76. package/dist/split/features/devtools.js.map +0 -1
  77. package/dist/split/features/diagnostics.js.map +0 -1
  78. package/dist/split/features/formats.js.map +0 -1
  79. package/dist/split/features/recursion.js.map +0 -1
  80. package/dist/split/features/scopes.js.map +0 -1
  81. package/dist/split/features/ssr.js.map +0 -1
  82. package/dist/split/features/temporal.js.map +0 -1
@@ -1,1240 +1,580 @@
1
- const _config = {
2
- bindAttributeName: 'data-wcs',
3
- tagNames: {
4
- state: 'wcs-state'},
5
- locale: 'en'};
6
- // backward compatible export (read-only usage)
7
- const config = _config;
1
+ // src/hooks.ts
2
+ var hooks = {};
8
3
 
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;
4
+ // src/parser/raiseError.ts
5
+ function raiseError(message, subject, candidates) {
6
+ throw new Error(`[@wcstack/state] ${message}${hooks.explain?.(message, subject, candidates) ?? ""}`);
62
7
  }
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
8
 
85
- function raiseError(message) {
86
- throw new Error(`[@wcstack/state] ${message}`);
9
+ // src/messages.ts
10
+ var CODES = [
11
+ "",
12
+ "binding-syntax",
13
+ "template-syntax",
14
+ "binding-path-missing",
15
+ "binding-type-expectation",
16
+ "filter-unknown",
17
+ "filter-arity",
18
+ "getter-cycle",
19
+ "getter-depth-exceeded",
20
+ "index-arity",
21
+ "index-param-range",
22
+ "recursion-unsupported",
23
+ "token-misconfigured",
24
+ "token-undeclared",
25
+ "wildcard-rank",
26
+ "spread-no-bindable",
27
+ "declaration-alias",
28
+ "name-alias"
29
+ ];
30
+ var codeOf = (id) => {
31
+ const c = CODES[id / 100 | 0];
32
+ return c ? `[wcs/${c}] ` : "";
33
+ };
34
+ function text(id, args = []) {
35
+ return hooks.render?.(id, args) ?? `${codeOf(id)}#${id}${args.map((a) => ` ${typeof a === "string" ? JSON.stringify(a) : String(a)}`).join("")}`;
36
+ }
37
+ function raise(id, args, subject, candidates) {
38
+ raiseError(text(id, args), subject, candidates);
87
39
  }
88
40
 
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,
41
+ // src/config.ts
42
+ var config = {
43
+ bindAttributeName: "data-wcs",
44
+ commentForPrefix: "wcs-for",
45
+ commentIfPrefix: "wcs-if",
46
+ commentElseIfPrefix: "wcs-elseif",
47
+ commentElsePrefix: "wcs-else",
48
+ tagNames: { state: "wcs-state", ssr: "wcs-ssr" },
49
+ locale: typeof document !== "undefined" ? document.documentElement?.lang || "en" : "en",
50
+ enableContractAnalyzer: false
100
51
  };
101
52
 
102
- /**
103
- * errorMessages.ts
104
- *
105
- * Error message generation utilities used by filter functions.
106
- *
107
- * Main responsibilities:
108
- * - Throws clear error messages when filter options or value type checks fail
109
- * - Takes function name as argument to specify which filter caused the error
110
- *
111
- * Design points:
112
- * - optionsRequired: Error when required option is not specified
113
- * - optionMustBeNumber: Error when option value is not a number
114
- * - valueMustBeNumber: Error when value is not a number
115
- * - valueMustBeDate: Error when value is not a Date
116
- * - valueMustBeArray: Error when value is not an array
117
- */
118
- /**
119
- * Throws error when filter requires at least one option but none provided.
120
- *
121
- * @param fnName - Name of the filter function
122
- * @returns Never returns (always throws)
123
- */
53
+ // src/load.ts
54
+ var FEATURE_NAMES = ["formats", "diagnostics", "temporal", "list-keys", "scopes", "recursion", "ssr", "devtools"];
55
+
56
+ // src/pattern.ts
57
+ var WILDCARD = "*";
58
+
59
+ // src/filters/errorMessages.ts
124
60
  function optionsRequired(fnName) {
125
- raiseError(`filter ${fnName} requires at least one option`);
61
+ raise(26 /* FilterOptionsRequired */, [fnName]);
126
62
  }
127
- /**
128
- * Throws error when filter option must be a number but invalid value provided.
129
- *
130
- * @param fnName - Name of the filter function
131
- * @returns Never returns (always throws)
132
- */
133
63
  function optionMustBeNumber(fnName) {
134
- raiseError(`filter ${fnName} requires a number as option`);
64
+ raise(27 /* FilterOptionNotNumber */, [fnName]);
135
65
  }
136
- /**
137
- * Throws error when filter requires numeric value but non-number provided.
138
- *
139
- * @param fnName - Name of the filter function
140
- * @returns Never returns (always throws)
141
- */
142
66
  function valueMustBeNumber(fnName) {
143
- raiseError(`filter ${fnName} requires a number value`);
67
+ raise(28 /* FilterValueNotNumber */, [fnName]);
144
68
  }
145
- /**
146
- * Throws error when filter requires Date value but non-Date provided.
147
- *
148
- * @param fnName - Name of the filter function
149
- * @returns Never returns (always throws)
150
- */
151
69
  function valueMustBeDate(fnName) {
152
- raiseError(`filter ${fnName} requires a date value`);
70
+ raise(29 /* FilterValueNotDate */, [fnName]);
153
71
  }
154
- /**
155
- * Throws error when filter requires array value but non-array provided.
156
- *
157
- * @param fnName - Name of the filter function
158
- * @returns Never returns (always throws)
159
- */
160
72
  function valueMustBeArray(fnName) {
161
- raiseError(`filter ${fnName} requires an array value`);
73
+ raise(30 /* FilterValueNotArray */, [fnName]);
162
74
  }
163
75
 
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
-
184
- /**
185
- * builtinFilters.ts
186
- *
187
- * Implementation file for built-in filter functions available in Structive.
188
- *
189
- * Main responsibilities:
190
- * - Provides filters for conversion, comparison, formatting, and validation of numbers, strings, dates, booleans, etc.
191
- * - Defines functions with options for each filter name, enabling flexible use during binding
192
- * - Designed for common use as both input and output filters
193
- *
194
- * Design points:
195
- * - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, abs, clamp, fix, locale, uc, lc, cap, trim, slice, pad, truncate, join, int, float, round, percent, unit, date, time, ymd, hms, falsy, truthy, defaults, boolean, number, string, null, etc.
196
- * - Rich type checking and error handling for option values
197
- * - Centralized management of filter functions with FilterWithOptions type, easy to extend
198
- * - Dynamic retrieval of filter functions from filter names and options via builtinFilterFn
199
- */
76
+ // src/filters/options.ts
200
77
  function validateNumberString(value) {
201
- if (!value || isNaN(Number(value))) {
202
- return false;
203
- }
204
- return true;
78
+ if (!value || isNaN(Number(value))) {
79
+ return false;
80
+ }
81
+ return true;
205
82
  }
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
83
  function numberOption(value, fnName) {
215
- if (!validateNumberString(value)) {
216
- optionMustBeNumber(fnName);
217
- }
218
- return Number(value);
84
+ if (!validateNumberString(value)) {
85
+ optionMustBeNumber(fnName);
86
+ }
87
+ return Number(value);
219
88
  }
220
- /** A required numeric option: missing → `optionsRequired`, non-numeric → `optionMustBeNumber`. */
221
89
  function requiredNumberOption(options, index, fnName) {
222
- return numberOption(options?.[index] ?? optionsRequired(fnName), fnName);
90
+ return numberOption(options?.[index] ?? optionsRequired(fnName), fnName);
223
91
  }
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
- };
247
- /**
248
- * Equality filter - compares value with option.
249
- *
250
- * @param options - Array with comparison value as first element
251
- * @returns Filter function that returns boolean
252
- */
253
- const eq = (options, literals) => {
254
- const opt = options?.[0] ?? optionsRequired('eq');
255
- // The typed literal (B9): unquoted true / false / null / numbers are typed, quoted arguments are strings
256
- const literal = literals !== undefined && literals.length > 0 ? literals[0] : opt;
257
- return (value) => {
258
- // Align types for comparison
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
- }
270
- if (!validateNumberString(opt)) {
271
- optionMustBeNumber('eq');
272
- }
273
- return value === Number(opt);
274
- }
275
- if (typeof value === 'string') {
276
- return value === opt;
277
- }
278
- // Booleans, null and the rest compare with the typed literal: eq(true) matches true (B9)
92
+ function firstLiteral(opt, literals) {
93
+ return literals !== void 0 && literals.length > 0 ? literals[0] : opt;
94
+ }
95
+
96
+ // src/filters/core.ts
97
+ var eq = (options, literals) => {
98
+ const opt = options?.[0] ?? optionsRequired("eq");
99
+ const literal = firstLiteral(opt, literals);
100
+ return (value) => {
101
+ if (typeof value === "number") {
102
+ if (typeof literal !== "string") {
279
103
  return value === literal;
280
- };
281
- };
282
- /**
283
- * Inequality filter - compares value with option.
284
- *
285
- * @param options - Array with comparison value as first element
286
- * @returns Filter function that returns boolean
287
- */
288
- const ne = (options, literals) => {
289
- const opt = options?.[0] ?? optionsRequired('ne');
290
- const literal = literals !== undefined && literals.length > 0 ? literals[0] : opt;
291
- return (value) => {
292
- // Align types for comparison
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
- }
299
- if (!validateNumberString(opt)) {
300
- optionMustBeNumber('ne');
301
- }
302
- return value !== Number(opt);
303
- }
304
- if (typeof value === 'string') {
305
- return value !== opt;
306
- }
307
- // Booleans, null and the rest compare with the typed literal (B9)
104
+ }
105
+ if (!validateNumberString(opt)) {
106
+ optionMustBeNumber("eq");
107
+ }
108
+ return value === Number(opt);
109
+ }
110
+ if (typeof value === "string") {
111
+ return value === opt;
112
+ }
113
+ return value === literal;
114
+ };
115
+ };
116
+ var ne = (options, literals) => {
117
+ const opt = options?.[0] ?? optionsRequired("ne");
118
+ const literal = firstLiteral(opt, literals);
119
+ return (value) => {
120
+ if (typeof value === "number") {
121
+ if (typeof literal !== "string") {
308
122
  return value !== literal;
309
- };
310
- };
311
- /**
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`).
322
- *
323
- * @param options - Unused
324
- * @returns Filter function that returns the inverted truthiness
325
- */
326
- const not = (_options) => {
327
- return (value) => !value;
328
- };
329
- /**
330
- * Less than filter - checks if value is less than option.
331
- *
332
- * @param options - Array with comparison number as first element
333
- * @returns Filter function that returns boolean
334
- */
335
- const lt = (options) => {
336
- const opt = requiredNumberOption(options, 0, 'lt');
337
- return (value) => {
338
- if (typeof value !== 'number') {
339
- valueMustBeNumber('lt');
340
- }
341
- return value < opt;
342
- };
343
- };
344
- /**
345
- * Less than or equal filter - checks if value is less than or equal to option.
346
- *
347
- * @param options - Array with comparison number as first element
348
- * @returns Filter function that returns boolean
349
- */
350
- const le = (options) => {
351
- const opt = requiredNumberOption(options, 0, 'le');
352
- return (value) => {
353
- if (typeof value !== 'number') {
354
- valueMustBeNumber('le');
355
- }
356
- return value <= opt;
357
- };
358
- };
359
- /**
360
- * Greater than filter - checks if value is greater than option.
361
- *
362
- * @param options - Array with comparison number as first element
363
- * @returns Filter function that returns boolean
364
- */
365
- const gt = (options) => {
366
- const opt = requiredNumberOption(options, 0, 'gt');
367
- return (value) => {
368
- if (typeof value !== 'number') {
369
- valueMustBeNumber('gt');
370
- }
371
- return value > opt;
372
- };
373
- };
374
- /**
375
- * Greater than or equal filter - checks if value is greater than or equal to option.
376
- *
377
- * @param options - Array with comparison number as first element
378
- * @returns Filter function that returns boolean
379
- */
380
- const ge = (options) => {
381
- const opt = requiredNumberOption(options, 0, 'ge');
382
- return (value) => {
383
- if (typeof value !== 'number') {
384
- valueMustBeNumber('ge');
385
- }
386
- return value >= opt;
387
- };
388
- };
389
- /**
390
- * Increment filter - adds option value to input value.
391
- *
392
- * @param options - Array with increment number as first element
393
- * @returns Filter function that returns incremented number
394
- */
395
- const inc = (options) => {
396
- const opt = requiredNumberOption(options, 0, 'add');
397
- return (value) => {
398
- if (typeof value !== 'number') {
399
- valueMustBeNumber('add');
400
- }
401
- return value + opt;
402
- };
403
- };
404
- /**
405
- * Decrement filter - subtracts option value from input value.
406
- *
407
- * @param options - Array with decrement number as first element
408
- * @returns Filter function that returns decremented number
409
- */
410
- const dec = (options) => {
411
- const opt = requiredNumberOption(options, 0, 'sub');
412
- return (value) => {
413
- if (typeof value !== 'number') {
414
- valueMustBeNumber('sub');
415
- }
416
- return value - opt;
417
- };
418
- };
419
- /**
420
- * Multiply filter - multiplies value by option.
421
- *
422
- * @param options - Array with multiplier number as first element
423
- * @returns Filter function that returns multiplied number
424
- */
425
- const mul = (options) => {
426
- const opt = requiredNumberOption(options, 0, 'mul');
427
- return (value) => {
428
- if (typeof value !== 'number') {
429
- valueMustBeNumber('mul');
430
- }
431
- return value * opt;
432
- };
433
- };
434
- /**
435
- * Divide filter - divides value by option.
436
- *
437
- * @param options - Array with divisor number as first element
438
- * @returns Filter function that returns divided number
439
- */
440
- const div = (options) => {
441
- const opt = requiredNumberOption(options, 0, 'div');
442
- return (value) => {
443
- if (typeof value !== 'number') {
444
- valueMustBeNumber('div');
445
- }
446
- return value / opt;
447
- };
448
- };
449
- /**
450
- * Modulo filter - returns remainder of division.
451
- *
452
- * @param options - Array with divisor number as first element
453
- * @returns Filter function that returns remainder
454
- */
455
- const mod = (options) => {
456
- const opt = requiredNumberOption(options, 0, 'mod');
457
- return (value) => {
458
- if (typeof value !== 'number') {
459
- valueMustBeNumber('mod');
460
- }
461
- return value % opt;
462
- };
463
- };
464
- /**
465
- * Absolute value filter - returns the magnitude of a number.
466
- *
467
- * @param options - Unused
468
- * @returns Filter function that returns the absolute value
469
- */
470
- const abs = (_options) => {
471
- return (value) => {
472
- if (typeof value !== 'number') {
473
- valueMustBeNumber('abs');
474
- }
475
- return Math.abs(value);
476
- };
477
- };
478
- /**
479
- * Clamp filter - constrains a number to the inclusive range [min, max].
480
- *
481
- * Saturating conversion in the same family as round/floor/ceil, so it stays on
482
- * the wire rather than in state. Pairs with `unit` for style bindings:
483
- * `style.width: ratio|clamp(0,1)|percent(0)`.
484
- *
485
- * @param options - Array with minimum as first element and maximum as second (both required)
486
- * @returns Filter function that returns the clamped number
487
- */
488
- const clamp = (options) => {
489
- const min = requiredNumberOption(options, 0, 'clamp');
490
- const max = requiredNumberOption(options, 1, 'clamp');
491
- return (value) => {
492
- if (typeof value !== 'number') {
493
- valueMustBeNumber('clamp');
494
- }
495
- return Math.min(Math.max(value, min), max);
496
- };
497
- };
498
- /**
499
- * Fixed decimal filter - formats number to fixed decimal places.
500
- *
501
- * @param options - Array with decimal places as first element (default: 0)
502
- * @returns Filter function that returns formatted string
503
- */
504
- const fix = (options) => {
505
- const opt = numberOption(options?.[0] ?? "0", 'toFixed');
506
- return (value) => {
507
- if (typeof value !== 'number') {
508
- valueMustBeNumber('toFixed');
509
- }
510
- return value.toFixed(opt);
511
- };
512
- };
513
- /**
514
- * Locale number filter - formats number according to locale.
515
- *
516
- * ロケール依存フィルタ(`locale` / `date` / `time` / `datetime`)は
517
- * **明示引数だけを構築時に確定し、既定の `config.locale` は適用のたびに読む**。
518
- *
519
- * 以前は `options?.[0] ?? config.locale` を返り値の関数の**外**で解決していた。
520
- * フィルタ関数はバインド構築時に一度だけ作られるので、これはロケールを
521
- * クロージャに焼き込むことを意味する。`config.locale` の確定がバインド構築より
522
- * 遅れると、それ以降どう直しても「同じページの中で日付だけ既定ロケール」が
523
- * 永続し、しかも `config.locale` は依存グラフに載らないので再描画で回復もしない。
524
- * 症状(日付だけ英語)は原因(起動順序)から遠く、追いにくい。
525
- *
526
- * 適用のたびに読めば、少なくとも**再適用されたバインドは回復する**。ロケールは
527
- * 起動時に確定する前提(docs/i18n-design.md D1)なので通常この差は現れず、
528
- * これは順序事故から復帰できるようにするための保険である。
529
- *
530
- * 明示引数(`|date(ja-JP)`)は構築時に固定でよい — バインド式の一部であり、
531
- * 実行中に変わらない。
532
- *
533
- * @param options - Array with locale string as first element (default: config.locale)
534
- * @returns Filter function that returns localized number string
535
- */
536
- const locale = (options) => {
537
- const explicit = options?.[0];
538
- return (value) => {
539
- if (typeof value !== 'number') {
540
- valueMustBeNumber('locale');
541
- }
542
- return value.toLocaleString(explicit ?? config.locale);
543
- };
544
- };
545
- /**
546
- * Uppercase filter - converts string to uppercase.
547
- *
548
- * @param options - Unused
549
- * @returns Filter function that returns uppercase string
550
- */
551
- const uc = (_options) => {
552
- return (value) => {
553
- return String(value).toUpperCase();
554
- };
555
- };
556
- /**
557
- * Lowercase filter - converts string to lowercase.
558
- *
559
- * @param options - Unused
560
- * @returns Filter function that returns lowercase string
561
- */
562
- const lc = (_options) => {
563
- return (value) => {
564
- return String(value).toLowerCase();
565
- };
566
- };
567
- /**
568
- * Capitalize filter - capitalizes first character of string.
569
- *
570
- * @param options - Unused
571
- * @returns Filter function that returns capitalized string
572
- */
573
- const cap = (_options) => {
574
- return (value) => {
575
- const v = String(value);
576
- if (v.length === 0) {
577
- return v;
578
- }
579
- if (v.length === 1) {
580
- return v.toUpperCase();
581
- }
582
- return v.charAt(0).toUpperCase() + v.slice(1);
583
- };
584
- };
585
- /**
586
- * Trim filter - removes whitespace from both ends of string.
587
- *
588
- * @param options - Unused
589
- * @returns Filter function that returns trimmed string
590
- */
591
- const trim = (_options) => {
592
- return (value) => {
593
- return String(value).trim();
594
- };
595
- };
596
- /**
597
- * Slice filter - extracts portion of string from specified index.
598
- *
599
- * @param options - Array with start index and optional end index
600
- * @returns Filter function that returns sliced string
601
- */
602
- const slice = (options) => {
603
- const numberedOpts = [requiredNumberOption(options, 0, 'slice')];
604
- const opt2 = options?.[1];
605
- if (typeof opt2 !== 'undefined') {
606
- numberedOpts.push(numberOption(opt2, 'slice'));
123
+ }
124
+ if (!validateNumberString(opt)) {
125
+ optionMustBeNumber("ne");
126
+ }
127
+ return value !== Number(opt);
607
128
  }
608
- return (value) => {
609
- return String(value).slice(...numberedOpts);
610
- };
611
- };
612
- /**
613
- * Substring filter - extracts substring from specified position and length.
614
- *
615
- * @param options - Array with start index and length
616
- * @returns Filter function that returns substring
617
- */
618
- const substr = (options) => {
619
- const opt1 = requiredNumberOption(options, 0, 'substr');
620
- const opt2 = requiredNumberOption(options, 1, 'substr');
621
- return (value) => {
622
- return String(value).substr(opt1, opt2);
623
- };
624
- };
625
- /**
626
- * Pad filter - pads string to specified length from start.
627
- *
628
- * @param options - Array with target length and pad string (default: '0')
629
- * @returns Filter function that returns padded string
630
- */
631
- const pad = (options) => {
632
- const opt1 = requiredNumberOption(options, 0, 'padStart');
633
- const opt2 = options?.[1] ?? '0';
634
- return (value) => {
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);
649
- };
650
- };
651
- /**
652
- * Repeat filter - repeats string specified number of times.
653
- *
654
- * @param options - Array with repeat count as first element
655
- * @returns Filter function that returns repeated string
656
- */
657
- const rep = (options) => {
658
- const opt = requiredNumberOption(options, 0, 'repeat');
659
- return (value) => {
660
- return String(value).repeat(opt);
661
- };
662
- };
663
- /**
664
- * Reverse filter - reverses character order in string.
665
- *
666
- * @param options - Unused
667
- * @returns Filter function that returns reversed string
668
- */
669
- const rev = (_options) => {
670
- return (value) => {
671
- return String(value).split('').reverse().join('');
672
- };
673
- };
674
- /**
675
- * Integer filter - parses value to integer.
676
- *
677
- * @param options - Unused
678
- * @returns Filter function that returns integer
679
- */
680
- const int = (_options) => {
681
- return (value) => {
682
- return parseInt(String(value), 10);
683
- };
684
- };
685
- /**
686
- * Float filter - parses value to floating point number.
687
- *
688
- * @param options - Unused
689
- * @returns Filter function that returns float
690
- */
691
- const float = (_options) => {
692
- return (value) => {
693
- return parseFloat(String(value));
694
- };
695
- };
696
- /**
697
- * Round filter - rounds number to specified decimal places.
698
- *
699
- * @param options - Array with decimal places as first element (default: 0)
700
- * @returns Filter function that returns rounded number
701
- */
702
- const round = (options) => {
703
- const optValue = Math.pow(10, numberOption(options?.[0] ?? '0', 'round'));
704
- return (value) => {
705
- if (typeof value !== 'number') {
706
- valueMustBeNumber('round');
707
- }
708
- return Math.round(value * optValue) / optValue;
709
- };
710
- };
711
- /**
712
- * Floor filter - rounds number down to specified decimal places.
713
- *
714
- * @param options - Array with decimal places as first element (default: 0)
715
- * @returns Filter function that returns floored number
716
- */
717
- const floor = (options) => {
718
- const optValue = Math.pow(10, numberOption(options?.[0] ?? '0', 'floor'));
719
- return (value) => {
720
- if (typeof value !== 'number') {
721
- valueMustBeNumber('floor');
722
- }
723
- return Math.floor(value * optValue) / optValue;
724
- };
725
- };
726
- /**
727
- * Ceiling filter - rounds number up to specified decimal places.
728
- *
729
- * @param options - Array with decimal places as first element (default: 0)
730
- * @returns Filter function that returns ceiled number
731
- */
732
- const ceil = (options) => {
733
- const optValue = Math.pow(10, numberOption(options?.[0] ?? '0', 'ceil'));
734
- return (value) => {
735
- if (typeof value !== 'number') {
736
- valueMustBeNumber('ceil');
737
- }
738
- return Math.ceil(value * optValue) / optValue;
739
- };
740
- };
741
- /**
742
- * Percent filter - formats number as percentage string.
743
- *
744
- * @param options - Array with decimal places as first element (default: 0)
745
- * @returns Filter function that returns percentage string with '%'
746
- */
747
- const percent = (options) => {
748
- const opt = numberOption(options?.[0] ?? '0', 'percent');
749
- return (value) => {
750
- if (typeof value !== 'number') {
751
- valueMustBeNumber('percent');
752
- }
753
- return `${(value * 100).toFixed(opt)}%`;
754
- };
755
- };
756
- /**
757
- * Unit filter - appends a CSS unit (or any suffix) to the value.
758
- *
759
- * A number alone does nothing in CSS, so without this the unit has to be built in
760
- * state — which drags presentation into the source of truth, and in the worst case
761
- * forces a whole derived array just to carry `"42%"` strings.
762
- * `style.height: samples.*.cpu|clamp(0,100)|fix(0)|unit(%)` keeps it on the wire.
763
- *
764
- * Accepts strings as well as numbers **on purpose**: the useful chains run through
765
- * `fix` / `percent`, which already return strings. Rejecting non-numbers here would
766
- * break exactly the combination this filter exists for.
767
- *
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.
772
- *
773
- * @param options - Array with the unit/suffix as first element (required)
774
- * @returns Filter function that returns the value with the unit appended
775
- */
776
- const unit = (options) => {
777
- const opt = options?.[0] ?? optionsRequired('unit');
778
- return (value) => {
779
- return String(value) + opt;
780
- };
781
- };
782
- /**
783
- * Join filter - joins array elements into a string.
784
- *
785
- * The default separator is `", "` rather than `","`: a bare comma is what `String()`
786
- * already produces without any filter, so defaulting to it would make `|join` a no-op.
787
- *
788
- * @param options - Array with separator as first element (default: ', ')
789
- * @returns Filter function that returns the joined string
790
- */
791
- const join = (options) => {
792
- const opt = options?.[0] ?? ', ';
793
- return (value) => {
794
- if (!Array.isArray(value)) {
795
- valueMustBeArray('join');
796
- }
797
- return value.join(opt);
798
- };
799
- };
800
- /**
801
- * Truncate filter - shortens a string and appends an ellipsis.
802
- *
803
- * The length option counts **kept characters**, not the total including the suffix,
804
- * matching the existing `slice(0, n)` reading. A string at or below the limit is
805
- * returned untouched (no suffix).
806
- *
807
- * @param options - Array with max kept length as first element and suffix as second (default: '…')
808
- * @returns Filter function that returns the truncated string
809
- */
810
- const truncate = (options) => {
811
- const maxLength = requiredNumberOption(options, 0, 'truncate');
812
- const suffix = options?.[1] ?? '…';
813
- return (value) => {
814
- const v = String(value);
815
- if (v.length <= maxLength) {
816
- return v;
817
- }
818
- return v.slice(0, maxLength) + suffix;
819
- };
820
- };
821
- /**
822
- * Date filter - formats Date object as localized date string.
823
- *
824
- * @param options - Array with locale string as first element (default: config.locale)
825
- * @returns Filter function that returns date string
826
- */
827
- const date = (options) => {
828
- // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
829
- const explicit = options?.[0];
830
- return (value) => {
831
- if (!(value instanceof Date)) {
832
- valueMustBeDate('date');
833
- }
834
- return value.toLocaleDateString(explicit ?? config.locale);
835
- };
836
- };
837
- /**
838
- * Time filter - formats Date object as localized time string.
839
- *
840
- * @param options - Array with locale string as first element (default: config.locale)
841
- * @returns Filter function that returns time string
842
- */
843
- const time = (options) => {
844
- // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
845
- const explicit = options?.[0];
846
- return (value) => {
847
- if (!(value instanceof Date)) {
848
- valueMustBeDate('time');
849
- }
850
- return value.toLocaleTimeString(explicit ?? config.locale);
851
- };
852
- };
853
- /**
854
- * DateTime filter - formats Date object as localized date and time string.
855
- *
856
- * @param options - Array with locale string as first element (default: config.locale)
857
- * @returns Filter function that returns datetime string
858
- */
859
- const datetime = (options) => {
860
- // 既定ロケールは適用のたびに読む(`locale` フィルタの注記を参照)
861
- const explicit = options?.[0];
862
- return (value) => {
863
- if (!(value instanceof Date)) {
864
- valueMustBeDate('datetime');
865
- }
866
- return value.toLocaleString(explicit ?? config.locale);
867
- };
868
- };
869
- /**
870
- * Year-Month-Day filter - formats Date object as YYYY-MM-DD string.
871
- *
872
- * @param options - Array with separator string as first element (default: '-')
873
- * @returns Filter function that returns formatted date string
874
- */
875
- const ymd = (options) => {
876
- const opt = options?.[0] ?? '-';
877
- return (value) => {
878
- if (!(value instanceof Date)) {
879
- valueMustBeDate('ymd');
880
- }
881
- const year = value.getFullYear().toString();
882
- const month = (value.getMonth() + 1).toString().padStart(2, '0');
883
- const day = value.getDate().toString().padStart(2, '0');
884
- return `${year}${opt}${month}${opt}${day}`;
885
- };
886
- };
887
- /**
888
- * Hour-Minute-Second filter - formats Date object as HH:MM:SS string.
889
- *
890
- * The counterpart of `ymd`: a fixed, zero-padded, locale-independent rendering with a
891
- * configurable separator, for when `time` (locale-formatted) is not stable enough.
892
- *
893
- * @param options - Array with separator string as first element (default: ':')
894
- * @returns Filter function that returns formatted time string
895
- */
896
- const hms = (options) => {
897
- const opt = options?.[0] ?? ':';
898
- return (value) => {
899
- if (!(value instanceof Date)) {
900
- valueMustBeDate('hms');
901
- }
902
- const hours = value.getHours().toString().padStart(2, '0');
903
- const minutes = value.getMinutes().toString().padStart(2, '0');
904
- const seconds = value.getSeconds().toString().padStart(2, '0');
905
- return `${hours}${opt}${minutes}${opt}${seconds}`;
906
- };
907
- };
908
- /**
909
- * Falsy filter - checks if value is falsy, by JavaScript's own truthiness (`!value`: false, null,
910
- * undefined, 0, -0, 0n, '' and NaN). Before 3.0 the list was spelled out and missed 0n (B10).
911
- *
912
- * @param options - Unused
913
- * @returns Filter function that returns true for falsy values
914
- */
915
- const falsy = (_options) => {
916
- return (value) => !value;
917
- };
918
- /**
919
- * Truthy filter - checks if value is truthy.
920
- *
921
- * @param options - Unused
922
- * @returns Filter function that returns true for non-falsy values
923
- */
924
- const truthy = (_options) => {
925
- // JavaScript's truthiness, the same as the `boolean` filter (B10)
926
- return (value) => !!value;
927
- };
928
- /**
929
- * Default filter - returns default value if input is falsy (JavaScript's truthiness, as `falsy`).
930
- *
931
- * @param options - Array with default value as first element
932
- * @returns Filter function that returns value or default
933
- */
934
- const defaults = (options, literals) => {
935
- const opt = options?.[0] ?? optionsRequired('defaults');
936
- // The fallback is the typed literal (B9): defaults(0) gives 0, defaults(null) gives null, defaults('0') gives "0"
937
- const fallback = literals !== undefined && literals.length > 0 ? literals[0] : opt;
938
- return (value) => {
939
- if (!value) {
940
- return fallback;
941
- }
942
- return value;
943
- };
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
- };
957
- /**
958
- * Boolean filter - converts value to boolean.
959
- *
960
- * @param options - Unused
961
- * @returns Filter function that returns boolean
962
- */
963
- const boolean = (_options) => {
964
- return (value) => {
965
- return Boolean(value);
966
- };
967
- };
968
- /**
969
- * Number filter - converts value to number.
970
- *
971
- * @param options - Unused
972
- * @returns Filter function that returns number
973
- */
974
- const number = (_options) => {
975
- return (value) => {
976
- return Number(value);
977
- };
978
- };
979
- /**
980
- * String filter - converts value to string.
981
- *
982
- * @param options - Unused
983
- * @returns Filter function that returns string
984
- */
985
- const string = (_options) => {
986
- return (value) => {
987
- return String(value);
988
- };
129
+ if (typeof value === "string") {
130
+ return value !== opt;
131
+ }
132
+ return value !== literal;
133
+ };
989
134
  };
990
- /**
991
- * Null filter - converts empty string to null.
992
- *
993
- * @param options - Unused
994
- * @returns Filter function that returns null for empty string, otherwise original value
995
- */
996
- const _null = (_options) => {
135
+ var not = () => (value) => !value;
136
+ var numeric = (name, op) => ({
137
+ factory: (options) => {
138
+ const opt = requiredNumberOption(options, 0, name);
997
139
  return (value) => {
998
- return (value === "") ? null : value;
140
+ if (typeof value !== "number") {
141
+ valueMustBeNumber(name);
142
+ }
143
+ return op(value, opt);
999
144
  };
145
+ },
146
+ arity: [1, 1]
147
+ });
148
+ var truthy = () => (value) => !!value;
149
+ var falsy = () => (value) => !value;
150
+ var boolean = () => (value) => Boolean(value);
151
+ var abs = () => (value) => {
152
+ if (typeof value !== "number") {
153
+ valueMustBeNumber("abs");
154
+ }
155
+ return Math.abs(value);
156
+ };
157
+ var clamp = (options) => {
158
+ const min = requiredNumberOption(options, 0, "clamp");
159
+ const max = requiredNumberOption(options, 1, "clamp");
160
+ return (value) => {
161
+ if (typeof value !== "number") {
162
+ valueMustBeNumber("clamp");
163
+ }
164
+ return Math.min(Math.max(value, min), max);
165
+ };
166
+ };
167
+ var number = () => (value) => Number(value);
168
+ var string = () => (value) => String(value);
169
+ var int = () => (value) => parseInt(String(value), 10);
170
+ var float = () => (value) => parseFloat(String(value));
171
+ var defaults = (options, literals) => {
172
+ const opt = options?.[0] ?? optionsRequired("defaults");
173
+ const fallback = firstLiteral(opt, literals);
174
+ return (value) => {
175
+ if (!value) {
176
+ return fallback;
177
+ }
178
+ return value;
179
+ };
180
+ };
181
+ var coalesce = (options, literals) => {
182
+ const opt = options?.[0] ?? optionsRequired("coalesce");
183
+ const fallback = firstLiteral(opt, literals);
184
+ return (value) => value ?? fallback;
185
+ };
186
+ var nullIfEmpty = () => (value) => value === "" ? null : value;
187
+ var coreFilters = {
188
+ eq: { factory: eq, arity: [1, 1] },
189
+ ne: { factory: ne, arity: [1, 1] },
190
+ not: { factory: not, arity: [0, 0] },
191
+ lt: numeric("lt", (value, opt) => value < opt),
192
+ le: numeric("le", (value, opt) => value <= opt),
193
+ gt: numeric("gt", (value, opt) => value > opt),
194
+ ge: numeric("ge", (value, opt) => value >= opt),
195
+ add: numeric("add", (value, opt) => value + opt),
196
+ sub: numeric("sub", (value, opt) => value - opt),
197
+ mul: numeric("mul", (value, opt) => value * opt),
198
+ div: numeric("div", (value, opt) => value / opt),
199
+ mod: numeric("mod", (value, opt) => value % opt),
200
+ abs: { factory: abs, arity: [0, 0] },
201
+ clamp: { factory: clamp, arity: [2, 2] },
202
+ int: { factory: int, arity: [0, 0] },
203
+ float: { factory: float, arity: [0, 0] },
204
+ falsy: { factory: falsy, arity: [0, 0] },
205
+ truthy: { factory: truthy, arity: [0, 0] },
206
+ defaults: { factory: defaults, arity: [1, 1] },
207
+ coalesce: { factory: coalesce, arity: [1, 1] },
208
+ boolean: { factory: boolean, arity: [0, 0] },
209
+ number: { factory: number, arity: [0, 0] },
210
+ string: { factory: string, arity: [0, 0] },
211
+ nullIfEmpty: { factory: nullIfEmpty, arity: [0, 0] }
1000
212
  };
1001
- const builtinFilters = {
1002
- "eq": eq,
1003
- "ne": ne,
1004
- "not": not,
1005
- "lt": lt,
1006
- "le": le,
1007
- "gt": gt,
1008
- "ge": ge,
1009
- "add": inc,
1010
- "sub": dec,
1011
- "mul": mul,
1012
- "div": div,
1013
- "mod": mod,
1014
- "abs": abs,
1015
- "clamp": clamp,
1016
- "toFixed": fix,
1017
- "locale": locale,
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),
1030
- "join": join,
1031
- "int": int,
1032
- "float": float,
1033
- "round": round,
1034
- "floor": floor,
1035
- "ceil": ceil,
1036
- "percent": percent,
1037
- "unit": nullishPassthrough(unit),
1038
- "date": date,
1039
- "time": time,
1040
- "datetime": datetime,
1041
- "ymd": ymd,
1042
- "hms": hms,
1043
- "falsy": falsy,
1044
- "truthy": truthy,
1045
- "defaults": defaults,
1046
- "coalesce": coalesce,
1047
- "boolean": boolean,
1048
- "number": number,
1049
- "string": string,
1050
- "nullIfEmpty": _null,
1051
- };
1052
- const outputBuiltinFilters = builtinFilters;
1053
213
 
1054
- /**
1055
- * filterMeta.ts — 組み込みフィルタの構造化メタデータ(単一正本・route-a A2-1)。
1056
- *
1057
- * これまで vscode-wcs(completionData.ts BUILTIN_FILTERS)が手で持っていたフィルタの
1058
- * 引数仕様・型・説明を、実装側(@wcstack/state)に**正本として移設**したもの。
1059
- * manifest.ts がこれを公開し、vscode-wcs はそれを消費して手リストを撤去できる。
1060
- *
1061
- * 完全性は __tests__/manifest.test.ts のドリフト検出が保証する
1062
- * (filterMeta のキー集合 == builtinFilters のキー集合)。フィルタを追加して meta を
1063
- * 書き忘れると CI が落ちる。
1064
- */
1065
- /** 組み込みフィルタ名 → 構造化メタデータ。キー集合は builtinFilters と一致しなければならない。 */
1066
- const builtinFilterMeta = {
1067
- // 比較・論理
1068
- eq: { description: "等しいか比較", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
1069
- ne: { description: "異なるか比較", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
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 },
1073
- lt: { description: "より小さいか", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1074
- le: { description: "以下か", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1075
- gt: { description: "より大きいか", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1076
- ge: { description: "以上か", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1077
- // 算術
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"] },
1080
- mul: { description: "乗算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1081
- div: { description: "除算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1082
- mod: { description: "剰余", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
1083
- abs: { description: "絶対値", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
1084
- clamp: { description: "範囲内に丸める (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
1085
- // 数値フォーマット
1086
- toFixed: { description: "固定小数点表記", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
1087
- locale: { description: "ロケール形式で数値フォーマット", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
1088
- // 文字列
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 },
1092
- trim: { description: "前後の空白を削除", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
1093
- slice: { description: "部分文字列 (start[,end])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "number"] },
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"] },
1103
- // 数値パース・丸め
1104
- int: { description: "整数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
1105
- float: { description: "浮動小数点数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
1106
- round: { description: "四捨五入", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
1107
- floor: { description: "切り下げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
1108
- ceil: { description: "切り上げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
1109
- percent: { description: "パーセンテージ形式", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
1110
- // number だけでなく string も受ける。実用チェーンは fix / percent の後ろに繋がり、
1111
- // それらは既に string を返すため(builtinFilters.ts の unit を参照)
1112
- unit: { description: "単位(接尾辞)を付加", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
1113
- // 日付・時刻
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"] },
1121
- // 真偽値・変換
1122
- falsy: { description: "偽値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1123
- truthy: { description: "真値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
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"] },
1126
- boolean: { description: "ブール値に変換", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1127
- number: { description: "数値に変換", hasArgs: false, resultType: "number", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1128
- string: { description: "文字列に変換", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
1129
- nullIfEmpty: { description: "空文字列をnullに変換", hasArgs: false, resultType: "passthrough", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
214
+ // src/filters/formats.ts
215
+ var nullishPassthrough = (factory) => (options) => {
216
+ const filterFn = factory(options);
217
+ return (value) => value == null ? value : filterFn(value);
218
+ };
219
+ var toFixed = (options) => {
220
+ const opt = numberOption(options?.[0] ?? "0", "toFixed");
221
+ return (value) => {
222
+ if (typeof value !== "number") {
223
+ valueMustBeNumber("toFixed");
224
+ }
225
+ return value.toFixed(opt);
226
+ };
227
+ };
228
+ var round = (options) => {
229
+ const optValue = Math.pow(10, numberOption(options?.[0] ?? "0", "round"));
230
+ return (value) => {
231
+ if (typeof value !== "number") {
232
+ valueMustBeNumber("round");
233
+ }
234
+ return Math.round(value * optValue) / optValue;
235
+ };
236
+ };
237
+ var floor = (options) => {
238
+ const optValue = Math.pow(10, numberOption(options?.[0] ?? "0", "floor"));
239
+ return (value) => {
240
+ if (typeof value !== "number") {
241
+ valueMustBeNumber("floor");
242
+ }
243
+ return Math.floor(value * optValue) / optValue;
244
+ };
245
+ };
246
+ var ceil = (options) => {
247
+ const optValue = Math.pow(10, numberOption(options?.[0] ?? "0", "ceil"));
248
+ return (value) => {
249
+ if (typeof value !== "number") {
250
+ valueMustBeNumber("ceil");
251
+ }
252
+ return Math.ceil(value * optValue) / optValue;
253
+ };
254
+ };
255
+ var percent = (options) => {
256
+ const opt = numberOption(options?.[0] ?? "0", "percent");
257
+ return (value) => {
258
+ if (typeof value !== "number") {
259
+ valueMustBeNumber("percent");
260
+ }
261
+ return `${(value * 100).toFixed(opt)}%`;
262
+ };
263
+ };
264
+ var unit = (options) => {
265
+ const opt = options?.[0] ?? optionsRequired("unit");
266
+ return (value) => String(value) + opt;
267
+ };
268
+ var seen;
269
+ var valid = "en";
270
+ function defaultLocale() {
271
+ if (config.locale !== seen) {
272
+ seen = config.locale;
273
+ try {
274
+ valid = Intl.getCanonicalLocales(seen)[0];
275
+ } catch {
276
+ console.warn(`[@wcstack/state] ${text(48 /* LocaleInvalid */, [seen])}`);
277
+ valid = "en";
278
+ }
279
+ }
280
+ return valid;
281
+ }
282
+ var locale = (options) => {
283
+ const explicit = options?.[0];
284
+ return (value) => {
285
+ if (typeof value !== "number") {
286
+ valueMustBeNumber("locale");
287
+ }
288
+ return value.toLocaleString(explicit ?? defaultLocale());
289
+ };
290
+ };
291
+ var upper = () => (value) => String(value).toUpperCase();
292
+ var lower = () => (value) => String(value).toLowerCase();
293
+ var capitalize = () => (value) => {
294
+ const v = String(value);
295
+ if (v.length === 0) {
296
+ return v;
297
+ }
298
+ if (v.length === 1) {
299
+ return v.toUpperCase();
300
+ }
301
+ return v.charAt(0).toUpperCase() + v.slice(1);
302
+ };
303
+ var trim = () => (value) => String(value).trim();
304
+ var slice = (options) => {
305
+ const numberedOpts = [requiredNumberOption(options, 0, "slice")];
306
+ const opt2 = options?.[1];
307
+ if (typeof opt2 !== "undefined") {
308
+ numberedOpts.push(numberOption(opt2, "slice"));
309
+ }
310
+ return (value) => String(value).slice(...numberedOpts);
311
+ };
312
+ var padStart = (options) => {
313
+ const opt1 = requiredNumberOption(options, 0, "padStart");
314
+ const opt2 = options?.[1] ?? "0";
315
+ return (value) => String(value).padStart(opt1, opt2);
316
+ };
317
+ var padEnd = (options) => {
318
+ const opt1 = requiredNumberOption(options, 0, "padEnd");
319
+ const opt2 = options?.[1] ?? " ";
320
+ return (value) => String(value).padEnd(opt1, opt2);
321
+ };
322
+ var repeat = (options) => {
323
+ const opt = requiredNumberOption(options, 0, "repeat");
324
+ return (value) => String(value).repeat(opt);
325
+ };
326
+ var reverse = () => (value) => String(value).split("").reverse().join("");
327
+ var truncate = (options) => {
328
+ const maxLength = requiredNumberOption(options, 0, "truncate");
329
+ const suffix = options?.[1] ?? "\u2026";
330
+ return (value) => {
331
+ const v = String(value);
332
+ if (v.length <= maxLength) {
333
+ return v;
334
+ }
335
+ return v.slice(0, maxLength) + suffix;
336
+ };
337
+ };
338
+ var join = (options) => {
339
+ const opt = options?.[0] ?? ", ";
340
+ return (value) => {
341
+ if (!Array.isArray(value)) {
342
+ valueMustBeArray("join");
343
+ }
344
+ return value.join(opt);
345
+ };
346
+ };
347
+ var date = (options) => {
348
+ const explicit = options?.[0];
349
+ return (value) => {
350
+ if (!(value instanceof Date)) {
351
+ valueMustBeDate("date");
352
+ }
353
+ return value.toLocaleDateString(explicit ?? defaultLocale());
354
+ };
355
+ };
356
+ var time = (options) => {
357
+ const explicit = options?.[0];
358
+ return (value) => {
359
+ if (!(value instanceof Date)) {
360
+ valueMustBeDate("time");
361
+ }
362
+ return value.toLocaleTimeString(explicit ?? defaultLocale());
363
+ };
364
+ };
365
+ var datetime = (options) => {
366
+ const explicit = options?.[0];
367
+ return (value) => {
368
+ if (!(value instanceof Date)) {
369
+ valueMustBeDate("datetime");
370
+ }
371
+ return value.toLocaleString(explicit ?? defaultLocale());
372
+ };
373
+ };
374
+ var ymd = (options) => {
375
+ const opt = options?.[0] ?? "-";
376
+ return (value) => {
377
+ if (!(value instanceof Date)) {
378
+ valueMustBeDate("ymd");
379
+ }
380
+ const year = value.getFullYear().toString();
381
+ const month = (value.getMonth() + 1).toString().padStart(2, "0");
382
+ const day = value.getDate().toString().padStart(2, "0");
383
+ return `${year}${opt}${month}${opt}${day}`;
384
+ };
385
+ };
386
+ var hms = (options) => {
387
+ const opt = options?.[0] ?? ":";
388
+ return (value) => {
389
+ if (!(value instanceof Date)) {
390
+ valueMustBeDate("hms");
391
+ }
392
+ const hours = value.getHours().toString().padStart(2, "0");
393
+ const minutes = value.getMinutes().toString().padStart(2, "0");
394
+ const seconds = value.getSeconds().toString().padStart(2, "0");
395
+ return `${hours}${opt}${minutes}${opt}${seconds}`;
396
+ };
397
+ };
398
+ var formatFilters = {
399
+ toFixed: { factory: toFixed, arity: [0, 1] },
400
+ locale: { factory: locale, arity: [0, 1] },
401
+ // The `String(value)` family passes an absent value through (B8 — see nullishPassthrough)
402
+ upper: { factory: nullishPassthrough(upper), arity: [0, 0] },
403
+ lower: { factory: nullishPassthrough(lower), arity: [0, 0] },
404
+ capitalize: { factory: nullishPassthrough(capitalize), arity: [0, 0] },
405
+ trim: { factory: nullishPassthrough(trim), arity: [0, 0] },
406
+ slice: { factory: nullishPassthrough(slice), arity: [1, 2] },
407
+ // The length is required too (the implementation reads both)
408
+ padStart: { factory: nullishPassthrough(padStart), arity: [1, 2] },
409
+ padEnd: { factory: nullishPassthrough(padEnd), arity: [1, 2] },
410
+ repeat: { factory: nullishPassthrough(repeat), arity: [1, 1] },
411
+ reverse: { factory: nullishPassthrough(reverse), arity: [0, 0] },
412
+ truncate: { factory: nullishPassthrough(truncate), arity: [1, 2] },
413
+ join: { factory: join, arity: [0, 1] },
414
+ round: { factory: round, arity: [0, 1] },
415
+ floor: { factory: floor, arity: [0, 1] },
416
+ ceil: { factory: ceil, arity: [0, 1] },
417
+ percent: { factory: percent, arity: [0, 1] },
418
+ unit: { factory: nullishPassthrough(unit), arity: [1, 1] },
419
+ // The locale-dependent three take a locale like `locale` does (`date(ja-JP)`)
420
+ date: { factory: date, arity: [0, 1] },
421
+ time: { factory: time, arity: [0, 1] },
422
+ datetime: { factory: datetime, arity: [0, 1] },
423
+ ymd: { factory: ymd, arity: [0, 1] },
424
+ hms: { factory: hms, arity: [0, 1] }
1130
425
  };
1131
426
 
1132
- const STRUCTURAL_BINDING_TYPE_SET = new Set([
1133
- "if",
1134
- "elseif",
1135
- "else",
1136
- "for",
427
+ // src/parser/types.ts
428
+ var STRUCTURAL_BINDING_TYPE_SET = /* @__PURE__ */ new Set([
429
+ "if",
430
+ "elseif",
431
+ "else",
432
+ "for"
1137
433
  ]);
1138
434
 
1139
- /**
1140
- * manifest.ts — `<wcs-state>` の構文・フィルタ・予約名を機械可読な単一正本として公開する。
1141
- *
1142
- * 目的(route-a A2-1): vscode-wcs(wcstack-intellisense)が現在ハードコードで二重実装している
1143
- * 「フィルタ一覧・構文区切り・予約名」を、state 側の実装から導出した manifest に一本化し、
1144
- * 手作業同期によるドリフトを構造的に断つための土台。
1145
- *
1146
- * 設計:
1147
- * - `filters` は実装(builtinFilters の Record キー)から **自動導出**=実装が唯一の正本。
1148
- * - 構文・予約名は config / define.ts の定数から導出。
1149
- * - 将来 `dist/wcs-manifest.json` としてビルド時に書き出し、vscode-wcs がそれを読む形に発展させる。
1150
- * - ドリフト検出テスト(__tests__/manifest.test.ts)が、フィルタ集合の golden と実装の一致を CI で保証する。
1151
- */
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
- });
1169
- /** 機械可読な単一正本を返す。vscode-wcs はこれを消費する想定。 */
435
+ // src/parser/define.ts
436
+ var DELIMITER = ".";
437
+ var BINDING_SEPARATOR = ";";
438
+ var PROP_VALUE_SEPARATOR = ":";
439
+ var MODIFIER_SEPARATOR = "#";
440
+ var FILTER_SEPARATOR = "|";
441
+ var ELSE_KEYWORD = "else";
442
+ var SPREAD_PROP = "...";
443
+ var EVENT_PROP_PREFIX = "on";
444
+ var EVENT_TOKEN_NAMESPACE = "eventToken";
445
+ var COMMAND_NAMESPACE = "command";
446
+ var CLASS_NAMESPACE = "class";
447
+ var ATTR_NAMESPACE = "attr";
448
+ var STYLE_NAMESPACE = "style";
449
+ var MAX_INDEX_PARAM = 128;
450
+
451
+ // src/public/filterMeta.ts
452
+ var builtinFilterMeta = {
453
+ // 比較・論理
454
+ eq: { description: "\u7B49\u3057\u3044\u304B\u6BD4\u8F03", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
455
+ ne: { description: "\u7570\u306A\u308B\u304B\u6BD4\u8F03", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
456
+ // `not` は真偽性(truthy / falsy)の反転。`if:` が `Boolean()` で寄せるのと同じ規則で、
457
+ // `else:` はこのフィルタを足した束縛として組み立てられる(structural/createNotFilter.ts)
458
+ not: { description: "\u771F\u507D\u6027\u3092\u53CD\u8EE2\uFF08falsy \u2192 true\uFF09", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
459
+ lt: { description: "\u3088\u308A\u5C0F\u3055\u3044\u304B", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
460
+ le: { description: "\u4EE5\u4E0B\u304B", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
461
+ gt: { description: "\u3088\u308A\u5927\u304D\u3044\u304B", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
462
+ ge: { description: "\u4EE5\u4E0A\u304B", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
463
+ // 算術
464
+ add: { description: "\u52A0\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
465
+ sub: { description: "\u6E1B\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
466
+ mul: { description: "\u4E57\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
467
+ div: { description: "\u9664\u7B97", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
468
+ mod: { description: "\u5270\u4F59", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
469
+ abs: { description: "\u7D76\u5BFE\u5024", hasArgs: false, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 0 },
470
+ clamp: { description: "\u7BC4\u56F2\u5185\u306B\u4E38\u3081\u308B (min,max)", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 2, maxArgs: 2, argTypes: ["number", "number"] },
471
+ // 数値フォーマット
472
+ toFixed: { description: "\u56FA\u5B9A\u5C0F\u6570\u70B9\u8868\u8A18", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
473
+ locale: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u3067\u6570\u5024\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
474
+ // 文字列
475
+ upper: { description: "\u5927\u6587\u5B57\u306B\u5909\u63DB", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
476
+ lower: { description: "\u5C0F\u6587\u5B57\u306B\u5909\u63DB", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
477
+ capitalize: { description: "\u5148\u982D\u6587\u5B57\u3092\u5927\u6587\u5B57\u306B", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
478
+ trim: { description: "\u524D\u5F8C\u306E\u7A7A\u767D\u3092\u524A\u9664", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
479
+ slice: { description: "\u90E8\u5206\u6587\u5B57\u5217 (start[,end])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "number"] },
480
+ padStart: { description: "\u5148\u982D\u3092\u57CB\u3081\u308B (length[,char]\u3002char \u306E\u65E2\u5B9A\u306F 0 \u2014 JS \u306E\u65E2\u5B9A\u306F\u7A7A\u767D\u306A\u306E\u3067\u6CE8\u610F)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
481
+ padEnd: { description: "\u672B\u5C3E\u3092\u57CB\u3081\u308B (length[,char]\u3002char \u306E\u65E2\u5B9A\u306F\u7A7A\u767D \u2014 JS \u3068\u540C\u3058)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
482
+ repeat: { description: "\u7E70\u308A\u8FD4\u3057 (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
483
+ reverse: { description: "\u6587\u5B57\u9806\u3092\u53CD\u8EE2", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
484
+ truncate: { description: "\u5207\u308A\u8A70\u3081\u3066\u7701\u7565\u8A18\u53F7 (length[,suffix]\u3002suffix \u306E\u65E2\u5B9A\u306F \u2026 \u2014 U+2026 \u306E 1 \u6587\u5B57)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
485
+ join: { description: "\u914D\u5217\u3092\u9023\u7D50 ([separator]\u3002\u65E2\u5B9A\u306F\u30AB\u30F3\u30DE + \u7A7A\u767D)", hasArgs: true, resultType: "string", acceptTypes: ["array"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
486
+ // 数値パース・丸め
487
+ int: { description: "\u6574\u6570\u306B\u30D1\u30FC\u30B9", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
488
+ float: { description: "\u6D6E\u52D5\u5C0F\u6570\u70B9\u6570\u306B\u30D1\u30FC\u30B9", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
489
+ round: { description: "\u56DB\u6368\u4E94\u5165", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
490
+ floor: { description: "\u5207\u308A\u4E0B\u3052", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
491
+ ceil: { description: "\u5207\u308A\u4E0A\u3052", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
492
+ percent: { description: "\u30D1\u30FC\u30BB\u30F3\u30C6\u30FC\u30B8\u5F62\u5F0F", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
493
+ // number だけでなく string も受ける。実用チェーンは fix / percent の後ろに繋がり、
494
+ // それらは既に string を返すため(builtinFilters.ts の unit を参照)
495
+ unit: { description: "\u5358\u4F4D\uFF08\u63A5\u5C3E\u8F9E\uFF09\u3092\u4ED8\u52A0", hasArgs: true, resultType: "string", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["string"] },
496
+ // 日付・時刻
497
+ // `locale` と同じくロケールを 1 つ受ける(`date(ja-JP)`)。実装は最初からこれを読んでいたが、
498
+ // メタデータ側が maxArgs: 0 だったため lint と補完が正しい書き方を誤りとして報告していた
499
+ date: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u65E5\u4ED8 ([locale]\u3002\u65E2\u5B9A\u306F config.locale)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
500
+ time: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u6642\u523B ([locale]\u3002\u65E2\u5B9A\u306F config.locale)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
501
+ datetime: { description: "\u30ED\u30B1\u30FC\u30EB\u5F62\u5F0F\u306E\u65E5\u6642 ([locale]\u3002\u65E2\u5B9A\u306F config.locale)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
502
+ ymd: { description: "YYYY-MM-DD \u5F62\u5F0F ([separator]\u3002\u65E2\u5B9A\u306F -)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
503
+ hms: { description: "HH:MM:SS \u5F62\u5F0F ([separator]\u3002\u65E2\u5B9A\u306F :)", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
504
+ // 真偽値・変換
505
+ falsy: { description: "\u507D\u5024\u304B\u5224\u5B9A", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
506
+ truthy: { description: "\u771F\u5024\u304B\u5224\u5B9A", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
507
+ defaults: { description: "\u507D\u5024\u306E\u5834\u5408\u30C7\u30D5\u30A9\u30EB\u30C8\u5024", hasArgs: true, resultType: "passthrough", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
508
+ coalesce: { description: "null / undefined \u306E\u5834\u5408\u30C7\u30D5\u30A9\u30EB\u30C8\u5024", hasArgs: true, resultType: "passthrough", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
509
+ boolean: { description: "\u30D6\u30FC\u30EB\u5024\u306B\u5909\u63DB", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
510
+ number: { description: "\u6570\u5024\u306B\u5909\u63DB", hasArgs: false, resultType: "number", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
511
+ string: { description: "\u6587\u5B57\u5217\u306B\u5909\u63DB", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
512
+ nullIfEmpty: { description: "\u7A7A\u6587\u5B57\u5217\u3092null\u306B\u5909\u63DB", hasArgs: false, resultType: "passthrough", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 }
513
+ };
514
+
515
+ // src/public/manifest.ts
516
+ var WCS_MANIFEST_VERSION = 2;
517
+ var builtinFilterAliases = Object.freeze({});
518
+ var DECLARATION_ALIASES = Object.freeze({});
519
+ var STATE_API_ALIASES = Object.freeze({});
520
+ var MODIFIER_FLAGS = Object.freeze(["prevent", "stop", "ro", "direct"]);
521
+ var MODIFIER_KEYS = Object.freeze(["init", "sync"]);
522
+ var INDEX_PARAM_PREFIX = "$";
523
+ var BEHAVIOR_OPTION_KEYS = ["enableMustache", "sameValueGuard", "enableDirectionalInitialSync"];
1170
524
  function getWcsManifest() {
1171
- return {
1172
- version: WCS_MANIFEST_VERSION,
1173
- syntax: {
1174
- bindAttribute: config.bindAttributeName,
1175
- tagName: config.tagNames.state,
1176
- pathDelimiter: DELIMITER,
1177
- wildcard: WILDCARD,
1178
- delimiters: {
1179
- binding: BINDING_SEPARATOR,
1180
- propValue: PROP_VALUE_SEPARATOR,
1181
- modifier: MODIFIER_SEPARATOR,
1182
- filter: FILTER_SEPARATOR,
1183
- },
1184
- // 正本 STRUCTURAL_BINDING_TYPE_SET から導出(手書きの二重定義を排除)。
1185
- structuralDirectives: Array.from(STRUCTURAL_BINDING_TYPE_SET),
1186
- modifiers: {
1187
- flags: MODIFIER_FLAGS,
1188
- keyValue: MODIFIER_KEYS,
1189
- eventNamePrefix: EVENT_PROP_PREFIX,
1190
- },
1191
- indexParam: {
1192
- prefix: INDEX_PARAM_PREFIX,
1193
- maxDepth: MAX_WILDCARD_DEPTH,
1194
- },
1195
- bindingTypes: {
1196
- elseKeyword: ELSE_KEYWORD,
1197
- spread: SPREAD_PROP,
1198
- eventPropertyPrefix: EVENT_PROP_PREFIX,
1199
- explicitPropertyPrefix: DELIMITER,
1200
- propNamespaces: {
1201
- eventToken: EVENT_TOKEN_NAMESPACE,
1202
- command: COMMAND_NAMESPACE,
1203
- class: CLASS_NAMESPACE,
1204
- attr: ATTR_NAMESPACE,
1205
- style: STYLE_NAMESPACE,
1206
- },
1207
- },
1208
- },
1209
- // 実装(Record のキー)から自動導出。手リストを持たない=ドリフトの構造的排除。
1210
- filters: Object.keys(outputBuiltinFilters),
1211
- filterMeta: builtinFilterMeta,
1212
- filterAliases: builtinFilterAliases,
1213
- declarationAliases: DECLARATION_ALIASES,
1214
- apiAliases: STATE_API_ALIASES,
1215
- reservedLifecycle: [
1216
- STATE_CONNECTED_CALLBACK_NAME,
1217
- STATE_DISCONNECTED_CALLBACK_NAME,
1218
- STATE_RENDERED_CALLBACK_NAME,
1219
- STATE_ERROR_CALLBACK_NAME,
1220
- WEBCOMPONENT_STATE_READY_CALLBACK_NAME,
1221
- ],
1222
- reservedStateApi: [
1223
- STATE_BINDABLES_NAME,
1224
- STATE_COMMANDS_NAME,
1225
- STATE_COMMAND_TOKENS_NAME,
1226
- STATE_COMMAND_NAMESPACE_NAME,
1227
- STATE_EVENT_TOKENS_NAME,
1228
- STATE_ON_NAME,
1229
- STATE_STREAM_NAME,
1230
- STATE_WATCH_NAME,
1231
- STATE_SCAN_NAME,
1232
- STATE_LIST_KEYS_NAME,
1233
- STATE_RECURSION_NAME,
1234
- STATE_STREAM_STATUS_NAMESPACE_NAME,
1235
- STATE_STREAM_ERROR_NAMESPACE_NAME,
1236
- ],
1237
- };
525
+ return {
526
+ version: WCS_MANIFEST_VERSION,
527
+ syntax: {
528
+ bindAttribute: config.bindAttributeName,
529
+ tagName: config.tagNames.state,
530
+ pathDelimiter: DELIMITER,
531
+ wildcard: WILDCARD,
532
+ delimiters: { binding: BINDING_SEPARATOR, propValue: PROP_VALUE_SEPARATOR, modifier: MODIFIER_SEPARATOR, filter: FILTER_SEPARATOR },
533
+ structuralDirectives: Array.from(STRUCTURAL_BINDING_TYPE_SET),
534
+ modifiers: { flags: MODIFIER_FLAGS, keyValue: MODIFIER_KEYS, eventNamePrefix: EVENT_PROP_PREFIX },
535
+ indexParam: { prefix: INDEX_PARAM_PREFIX, maxDepth: MAX_INDEX_PARAM },
536
+ bindingTypes: {
537
+ elseKeyword: ELSE_KEYWORD,
538
+ spread: SPREAD_PROP,
539
+ eventPropertyPrefix: EVENT_PROP_PREFIX,
540
+ explicitPropertyPrefix: DELIMITER,
541
+ propNamespaces: { eventToken: EVENT_TOKEN_NAMESPACE, command: COMMAND_NAMESPACE, class: CLASS_NAMESPACE, attr: ATTR_NAMESPACE, style: STYLE_NAMESPACE }
542
+ }
543
+ },
544
+ // quoted: `filters` is an internal name the bundles shorten (mangle.mjs)
545
+ "filters": [...Object.keys(coreFilters), ...Object.keys(formatFilters)],
546
+ filterMeta: builtinFilterMeta,
547
+ filterAliases: builtinFilterAliases,
548
+ declarationAliases: DECLARATION_ALIASES,
549
+ apiAliases: STATE_API_ALIASES,
550
+ reservedLifecycle: ["$connectedCallback", "$disconnectedCallback", "$renderedCallback", "$errorCallback", "$stateReadyCallback"],
551
+ reservedStateApi: [
552
+ "$bindables",
553
+ "$commands",
554
+ "$commandTokens",
555
+ "$command",
556
+ "$eventTokens",
557
+ "$on",
558
+ "$stream",
559
+ "$watch",
560
+ "$listKeys",
561
+ "$recursion",
562
+ "$streamStatus",
563
+ "$streamError",
564
+ "$behavior",
565
+ "$features"
566
+ ],
567
+ // every option is a boolean, true when left out (engine.ts `loadTarget`: `typeof … === "boolean"`, `?? true`)
568
+ behaviorOptions: Object.fromEntries(BEHAVIOR_OPTION_KEYS.map((key) => [key, { type: "boolean", default: true }])),
569
+ features: [...FEATURE_NAMES]
570
+ };
1238
571
  }
1239
-
1240
- export { DECLARATION_ALIASES, STATE_API_ALIASES, STRUCTURAL_BINDING_TYPE_SET, WCS_MANIFEST_VERSION, builtinFilterAliases, builtinFilterMeta, getWcsManifest };
572
+ export {
573
+ DECLARATION_ALIASES,
574
+ STATE_API_ALIASES,
575
+ STRUCTURAL_BINDING_TYPE_SET,
576
+ WCS_MANIFEST_VERSION,
577
+ builtinFilterAliases,
578
+ builtinFilterMeta,
579
+ getWcsManifest
580
+ };