@wcstack/state 1.15.0 → 1.16.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.
@@ -0,0 +1,925 @@
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;
8
+
9
+ function raiseError(message) {
10
+ throw new Error(`[@wcstack/state] ${message}`);
11
+ }
12
+
13
+ /**
14
+ * errorMessages.ts
15
+ *
16
+ * Error message generation utilities used by filter functions.
17
+ *
18
+ * Main responsibilities:
19
+ * - Throws clear error messages when filter options or value type checks fail
20
+ * - Takes function name as argument to specify which filter caused the error
21
+ *
22
+ * Design points:
23
+ * - optionsRequired: Error when required option is not specified
24
+ * - optionMustBeNumber: Error when option value is not a number
25
+ * - valueMustBeNumber: Error when value is not a number
26
+ * - valueMustBeBoolean: Error when value is not boolean
27
+ * - valueMustBeDate: Error when value is not a Date
28
+ */
29
+ /**
30
+ * Throws error when filter requires at least one option but none provided.
31
+ *
32
+ * @param fnName - Name of the filter function
33
+ * @returns Never returns (always throws)
34
+ */
35
+ function optionsRequired(fnName) {
36
+ raiseError(`filter ${fnName} requires at least one option`);
37
+ }
38
+ /**
39
+ * Throws error when filter option must be a number but invalid value provided.
40
+ *
41
+ * @param fnName - Name of the filter function
42
+ * @returns Never returns (always throws)
43
+ */
44
+ function optionMustBeNumber(fnName) {
45
+ raiseError(`filter ${fnName} requires a number as option`);
46
+ }
47
+ /**
48
+ * Throws error when filter requires numeric value but non-number provided.
49
+ *
50
+ * @param fnName - Name of the filter function
51
+ * @returns Never returns (always throws)
52
+ */
53
+ function valueMustBeNumber(fnName) {
54
+ raiseError(`filter ${fnName} requires a number value`);
55
+ }
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
+ /**
66
+ * Throws error when filter requires Date value but non-Date provided.
67
+ *
68
+ * @param fnName - Name of the filter function
69
+ * @returns Never returns (always throws)
70
+ */
71
+ function valueMustBeDate(fnName) {
72
+ raiseError(`filter ${fnName} requires a date value`);
73
+ }
74
+
75
+ /**
76
+ * builtinFilters.ts
77
+ *
78
+ * Implementation file for built-in filter functions available in Structive.
79
+ *
80
+ * Main responsibilities:
81
+ * - Provides filters for conversion, comparison, formatting, and validation of numbers, strings, dates, booleans, etc.
82
+ * - Defines functions with options for each filter name, enabling flexible use during binding
83
+ * - Designed for common use as both input and output filters
84
+ *
85
+ * Design points:
86
+ * - Comprehensive coverage of diverse filters: eq, ne, lt, gt, inc, fix, locale, uc, lc, cap, trim, slice, pad, int, float, round, date, time, ymd, falsy, truthy, defaults, boolean, number, string, null, etc.
87
+ * - Rich type checking and error handling for option values
88
+ * - Centralized management of filter functions with FilterWithOptions type, easy to extend
89
+ * - Dynamic retrieval of filter functions from filter names and options via builtinFilterFn
90
+ */
91
+ function validateNumberString(value) {
92
+ if (!value || isNaN(Number(value))) {
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+ /**
98
+ * Equality filter - compares value with option.
99
+ *
100
+ * @param options - Array with comparison value as first element
101
+ * @returns Filter function that returns boolean
102
+ */
103
+ const eq = (options) => {
104
+ const opt = options?.[0] ?? optionsRequired('eq');
105
+ return (value) => {
106
+ // Align types for comparison
107
+ if (typeof value === 'number') {
108
+ if (!validateNumberString(opt)) {
109
+ optionMustBeNumber('eq');
110
+ }
111
+ return value === Number(opt);
112
+ }
113
+ if (typeof value === 'string') {
114
+ return value === opt;
115
+ }
116
+ // Strict equality for others
117
+ return value === opt;
118
+ };
119
+ };
120
+ /**
121
+ * Inequality filter - compares value with option.
122
+ *
123
+ * @param options - Array with comparison value as first element
124
+ * @returns Filter function that returns boolean
125
+ */
126
+ const ne = (options) => {
127
+ const opt = options?.[0] ?? optionsRequired('ne');
128
+ return (value) => {
129
+ // Align types for comparison
130
+ if (typeof value === 'number') {
131
+ if (!validateNumberString(opt)) {
132
+ optionMustBeNumber('ne');
133
+ }
134
+ return value !== Number(opt);
135
+ }
136
+ if (typeof value === 'string') {
137
+ return value !== opt;
138
+ }
139
+ // Strict equality for others
140
+ return value !== opt;
141
+ };
142
+ };
143
+ /**
144
+ * Boolean NOT filter - inverts boolean value.
145
+ *
146
+ * @param options - Unused
147
+ * @returns Filter function that returns inverted boolean
148
+ */
149
+ const not = (_options) => {
150
+ return (value) => {
151
+ if (typeof value !== 'boolean') {
152
+ valueMustBeBoolean('not');
153
+ }
154
+ return !value;
155
+ };
156
+ };
157
+ /**
158
+ * Less than filter - checks if value is less than option.
159
+ *
160
+ * @param options - Array with comparison number as first element
161
+ * @returns Filter function that returns boolean
162
+ */
163
+ const lt = (options) => {
164
+ const opt = options?.[0] ?? optionsRequired('lt');
165
+ if (!validateNumberString(opt)) {
166
+ optionMustBeNumber('lt');
167
+ }
168
+ return (value) => {
169
+ if (typeof value !== 'number') {
170
+ valueMustBeNumber('lt');
171
+ }
172
+ return value < Number(opt);
173
+ };
174
+ };
175
+ /**
176
+ * Less than or equal filter - checks if value is less than or equal to option.
177
+ *
178
+ * @param options - Array with comparison number as first element
179
+ * @returns Filter function that returns boolean
180
+ */
181
+ const le = (options) => {
182
+ const opt = options?.[0] ?? optionsRequired('le');
183
+ if (!validateNumberString(opt)) {
184
+ optionMustBeNumber('le');
185
+ }
186
+ return (value) => {
187
+ if (typeof value !== 'number') {
188
+ valueMustBeNumber('le');
189
+ }
190
+ return value <= Number(opt);
191
+ };
192
+ };
193
+ /**
194
+ * Greater than filter - checks if value is greater than option.
195
+ *
196
+ * @param options - Array with comparison number as first element
197
+ * @returns Filter function that returns boolean
198
+ */
199
+ const gt = (options) => {
200
+ const opt = options?.[0] ?? optionsRequired('gt');
201
+ if (!validateNumberString(opt)) {
202
+ optionMustBeNumber('gt');
203
+ }
204
+ return (value) => {
205
+ if (typeof value !== 'number') {
206
+ valueMustBeNumber('gt');
207
+ }
208
+ return value > Number(opt);
209
+ };
210
+ };
211
+ /**
212
+ * Greater than or equal filter - checks if value is greater than or equal to option.
213
+ *
214
+ * @param options - Array with comparison number as first element
215
+ * @returns Filter function that returns boolean
216
+ */
217
+ const ge = (options) => {
218
+ const opt = options?.[0] ?? optionsRequired('ge');
219
+ if (!validateNumberString(opt)) {
220
+ optionMustBeNumber('ge');
221
+ }
222
+ return (value) => {
223
+ if (typeof value !== 'number') {
224
+ valueMustBeNumber('ge');
225
+ }
226
+ return value >= Number(opt);
227
+ };
228
+ };
229
+ /**
230
+ * Increment filter - adds option value to input value.
231
+ *
232
+ * @param options - Array with increment number as first element
233
+ * @returns Filter function that returns incremented number
234
+ */
235
+ const inc = (options) => {
236
+ const opt = options?.[0] ?? optionsRequired('inc');
237
+ if (!validateNumberString(opt)) {
238
+ optionMustBeNumber('inc');
239
+ }
240
+ return (value) => {
241
+ if (typeof value !== 'number') {
242
+ valueMustBeNumber('inc');
243
+ }
244
+ return value + Number(opt);
245
+ };
246
+ };
247
+ /**
248
+ * Decrement filter - subtracts option value from input value.
249
+ *
250
+ * @param options - Array with decrement number as first element
251
+ * @returns Filter function that returns decremented number
252
+ */
253
+ const dec = (options) => {
254
+ const opt = options?.[0] ?? optionsRequired('dec');
255
+ if (!validateNumberString(opt)) {
256
+ optionMustBeNumber('dec');
257
+ }
258
+ return (value) => {
259
+ if (typeof value !== 'number') {
260
+ valueMustBeNumber('dec');
261
+ }
262
+ return value - Number(opt);
263
+ };
264
+ };
265
+ /**
266
+ * Multiply filter - multiplies value by option.
267
+ *
268
+ * @param options - Array with multiplier number as first element
269
+ * @returns Filter function that returns multiplied number
270
+ */
271
+ const mul = (options) => {
272
+ const opt = options?.[0] ?? optionsRequired('mul');
273
+ if (!validateNumberString(opt)) {
274
+ optionMustBeNumber('mul');
275
+ }
276
+ return (value) => {
277
+ if (typeof value !== 'number') {
278
+ valueMustBeNumber('mul');
279
+ }
280
+ return value * Number(opt);
281
+ };
282
+ };
283
+ /**
284
+ * Divide filter - divides value by option.
285
+ *
286
+ * @param options - Array with divisor number as first element
287
+ * @returns Filter function that returns divided number
288
+ */
289
+ const div = (options) => {
290
+ const opt = options?.[0] ?? optionsRequired('div');
291
+ if (!validateNumberString(opt)) {
292
+ optionMustBeNumber('div');
293
+ }
294
+ return (value) => {
295
+ if (typeof value !== 'number') {
296
+ valueMustBeNumber('div');
297
+ }
298
+ return value / Number(opt);
299
+ };
300
+ };
301
+ /**
302
+ * Modulo filter - returns remainder of division.
303
+ *
304
+ * @param options - Array with divisor number as first element
305
+ * @returns Filter function that returns remainder
306
+ */
307
+ const mod = (options) => {
308
+ const opt = options?.[0] ?? optionsRequired('mod');
309
+ if (!validateNumberString(opt)) {
310
+ optionMustBeNumber('mod');
311
+ }
312
+ return (value) => {
313
+ if (typeof value !== 'number') {
314
+ valueMustBeNumber('mod');
315
+ }
316
+ return value % Number(opt);
317
+ };
318
+ };
319
+ /**
320
+ * Fixed decimal filter - formats number to fixed decimal places.
321
+ *
322
+ * @param options - Array with decimal places as first element (default: 0)
323
+ * @returns Filter function that returns formatted string
324
+ */
325
+ const fix = (options) => {
326
+ const opt = options?.[0] ?? "0";
327
+ if (!validateNumberString(opt)) {
328
+ optionMustBeNumber('fix');
329
+ }
330
+ return (value) => {
331
+ if (typeof value !== 'number') {
332
+ valueMustBeNumber('fix');
333
+ }
334
+ return value.toFixed(Number(opt));
335
+ };
336
+ };
337
+ /**
338
+ * Locale number filter - formats number according to locale.
339
+ *
340
+ * @param options - Array with locale string as first element (default: config.locale)
341
+ * @returns Filter function that returns localized number string
342
+ */
343
+ const locale = (options) => {
344
+ const opt = options?.[0] ?? config.locale;
345
+ return (value) => {
346
+ if (typeof value !== 'number') {
347
+ valueMustBeNumber('locale');
348
+ }
349
+ return value.toLocaleString(opt);
350
+ };
351
+ };
352
+ /**
353
+ * Uppercase filter - converts string to uppercase.
354
+ *
355
+ * @param options - Unused
356
+ * @returns Filter function that returns uppercase string
357
+ */
358
+ const uc = (_options) => {
359
+ return (value) => {
360
+ return String(value).toUpperCase();
361
+ };
362
+ };
363
+ /**
364
+ * Lowercase filter - converts string to lowercase.
365
+ *
366
+ * @param options - Unused
367
+ * @returns Filter function that returns lowercase string
368
+ */
369
+ const lc = (_options) => {
370
+ return (value) => {
371
+ return String(value).toLowerCase();
372
+ };
373
+ };
374
+ /**
375
+ * Capitalize filter - capitalizes first character of string.
376
+ *
377
+ * @param options - Unused
378
+ * @returns Filter function that returns capitalized string
379
+ */
380
+ const cap = (_options) => {
381
+ return (value) => {
382
+ const v = String(value);
383
+ if (v.length === 0) {
384
+ return v;
385
+ }
386
+ if (v.length === 1) {
387
+ return v.toUpperCase();
388
+ }
389
+ return v.charAt(0).toUpperCase() + v.slice(1);
390
+ };
391
+ };
392
+ /**
393
+ * Trim filter - removes whitespace from both ends of string.
394
+ *
395
+ * @param options - Unused
396
+ * @returns Filter function that returns trimmed string
397
+ */
398
+ const trim = (_options) => {
399
+ return (value) => {
400
+ return String(value).trim();
401
+ };
402
+ };
403
+ /**
404
+ * Slice filter - extracts portion of string from specified index.
405
+ *
406
+ * @param options - Array with start index and optional end index
407
+ * @returns Filter function that returns sliced string
408
+ */
409
+ const slice = (options) => {
410
+ const numberedOpts = [];
411
+ const opt1 = options?.[0] ?? optionsRequired('slice');
412
+ if (!validateNumberString(opt1)) {
413
+ optionMustBeNumber('slice');
414
+ }
415
+ numberedOpts.push(Number(opt1));
416
+ const opt2 = options?.[1];
417
+ if (typeof opt2 !== 'undefined') {
418
+ if (!validateNumberString(opt2)) {
419
+ optionMustBeNumber('slice');
420
+ }
421
+ numberedOpts.push(Number(opt2));
422
+ }
423
+ return (value) => {
424
+ return String(value).slice(...numberedOpts);
425
+ };
426
+ };
427
+ /**
428
+ * Substring filter - extracts substring from specified position and length.
429
+ *
430
+ * @param options - Array with start index and length
431
+ * @returns Filter function that returns substring
432
+ */
433
+ const substr = (options) => {
434
+ const opt1 = options?.[0] ?? optionsRequired('substr');
435
+ if (!validateNumberString(opt1)) {
436
+ optionMustBeNumber('substr');
437
+ }
438
+ const opt2 = options?.[1] ?? optionsRequired('substr');
439
+ if (!validateNumberString(opt2)) {
440
+ optionMustBeNumber('substr');
441
+ }
442
+ return (value) => {
443
+ return String(value).substr(Number(opt1), Number(opt2));
444
+ };
445
+ };
446
+ /**
447
+ * Pad filter - pads string to specified length from start.
448
+ *
449
+ * @param options - Array with target length and pad string (default: '0')
450
+ * @returns Filter function that returns padded string
451
+ */
452
+ const pad = (options) => {
453
+ const opt1 = options?.[0] ?? optionsRequired('pad');
454
+ if (!validateNumberString(opt1)) {
455
+ optionMustBeNumber('pad');
456
+ }
457
+ const opt2 = options?.[1] ?? '0';
458
+ return (value) => {
459
+ return String(value).padStart(Number(opt1), opt2);
460
+ };
461
+ };
462
+ /**
463
+ * Repeat filter - repeats string specified number of times.
464
+ *
465
+ * @param options - Array with repeat count as first element
466
+ * @returns Filter function that returns repeated string
467
+ */
468
+ const rep = (options) => {
469
+ const opt = options?.[0] ?? optionsRequired('rep');
470
+ if (!validateNumberString(opt)) {
471
+ optionMustBeNumber('rep');
472
+ }
473
+ return (value) => {
474
+ return String(value).repeat(Number(opt));
475
+ };
476
+ };
477
+ /**
478
+ * Reverse filter - reverses character order in string.
479
+ *
480
+ * @param options - Unused
481
+ * @returns Filter function that returns reversed string
482
+ */
483
+ const rev = (_options) => {
484
+ return (value) => {
485
+ return String(value).split('').reverse().join('');
486
+ };
487
+ };
488
+ /**
489
+ * Integer filter - parses value to integer.
490
+ *
491
+ * @param options - Unused
492
+ * @returns Filter function that returns integer
493
+ */
494
+ const int = (_options) => {
495
+ return (value) => {
496
+ return parseInt(String(value), 10);
497
+ };
498
+ };
499
+ /**
500
+ * Float filter - parses value to floating point number.
501
+ *
502
+ * @param options - Unused
503
+ * @returns Filter function that returns float
504
+ */
505
+ const float = (_options) => {
506
+ return (value) => {
507
+ return parseFloat(String(value));
508
+ };
509
+ };
510
+ /**
511
+ * Round filter - rounds number to specified decimal places.
512
+ *
513
+ * @param options - Array with decimal places as first element (default: 0)
514
+ * @returns Filter function that returns rounded number
515
+ */
516
+ const round = (options) => {
517
+ const opt = options?.[0] ?? '0';
518
+ if (!validateNumberString(opt)) {
519
+ optionMustBeNumber('round');
520
+ }
521
+ return (value) => {
522
+ if (typeof value !== 'number') {
523
+ valueMustBeNumber('round');
524
+ }
525
+ const optValue = Math.pow(10, Number(opt));
526
+ return Math.round(value * optValue) / optValue;
527
+ };
528
+ };
529
+ /**
530
+ * Floor filter - rounds number down to specified decimal places.
531
+ *
532
+ * @param options - Array with decimal places as first element (default: 0)
533
+ * @returns Filter function that returns floored number
534
+ */
535
+ const floor = (options) => {
536
+ const opt = options?.[0] ?? '0';
537
+ if (!validateNumberString(opt)) {
538
+ optionMustBeNumber('floor');
539
+ }
540
+ return (value) => {
541
+ if (typeof value !== 'number') {
542
+ valueMustBeNumber('floor');
543
+ }
544
+ const optValue = Math.pow(10, Number(opt));
545
+ return Math.floor(value * optValue) / optValue;
546
+ };
547
+ };
548
+ /**
549
+ * Ceiling filter - rounds number up to specified decimal places.
550
+ *
551
+ * @param options - Array with decimal places as first element (default: 0)
552
+ * @returns Filter function that returns ceiled number
553
+ */
554
+ const ceil = (options) => {
555
+ const opt = options?.[0] ?? '0';
556
+ if (!validateNumberString(opt)) {
557
+ optionMustBeNumber('ceil');
558
+ }
559
+ return (value) => {
560
+ if (typeof value !== 'number') {
561
+ valueMustBeNumber('ceil');
562
+ }
563
+ const optValue = Math.pow(10, Number(opt));
564
+ return Math.ceil(value * optValue) / optValue;
565
+ };
566
+ };
567
+ /**
568
+ * Percent filter - formats number as percentage string.
569
+ *
570
+ * @param options - Array with decimal places as first element (default: 0)
571
+ * @returns Filter function that returns percentage string with '%'
572
+ */
573
+ const percent = (options) => {
574
+ const opt = options?.[0] ?? '0';
575
+ if (!validateNumberString(opt)) {
576
+ optionMustBeNumber('percent');
577
+ }
578
+ return (value) => {
579
+ if (typeof value !== 'number') {
580
+ valueMustBeNumber('percent');
581
+ }
582
+ return `${(value * 100).toFixed(Number(opt))}%`;
583
+ };
584
+ };
585
+ /**
586
+ * Date filter - formats Date object as localized date string.
587
+ *
588
+ * @param options - Array with locale string as first element (default: config.locale)
589
+ * @returns Filter function that returns date string
590
+ */
591
+ const date = (options) => {
592
+ const opt = options?.[0] ?? config.locale;
593
+ return (value) => {
594
+ if (!(value instanceof Date)) {
595
+ valueMustBeDate('date');
596
+ }
597
+ return value.toLocaleDateString(opt);
598
+ };
599
+ };
600
+ /**
601
+ * Time filter - formats Date object as localized time string.
602
+ *
603
+ * @param options - Array with locale string as first element (default: config.locale)
604
+ * @returns Filter function that returns time string
605
+ */
606
+ const time = (options) => {
607
+ const opt = options?.[0] ?? config.locale;
608
+ return (value) => {
609
+ if (!(value instanceof Date)) {
610
+ valueMustBeDate('time');
611
+ }
612
+ return value.toLocaleTimeString(opt);
613
+ };
614
+ };
615
+ /**
616
+ * DateTime filter - formats Date object as localized date and time string.
617
+ *
618
+ * @param options - Array with locale string as first element (default: config.locale)
619
+ * @returns Filter function that returns datetime string
620
+ */
621
+ const datetime = (options) => {
622
+ const opt = options?.[0] ?? config.locale;
623
+ return (value) => {
624
+ if (!(value instanceof Date)) {
625
+ valueMustBeDate('datetime');
626
+ }
627
+ return value.toLocaleString(opt);
628
+ };
629
+ };
630
+ /**
631
+ * Year-Month-Day filter - formats Date object as YYYY-MM-DD string.
632
+ *
633
+ * @param options - Array with separator string as first element (default: '-')
634
+ * @returns Filter function that returns formatted date string
635
+ */
636
+ const ymd = (options) => {
637
+ const opt = options?.[0] ?? '-';
638
+ return (value) => {
639
+ if (!(value instanceof Date)) {
640
+ valueMustBeDate('ymd');
641
+ }
642
+ const year = value.getFullYear().toString();
643
+ const month = (value.getMonth() + 1).toString().padStart(2, '0');
644
+ const day = value.getDate().toString().padStart(2, '0');
645
+ return `${year}${opt}${month}${opt}${day}`;
646
+ };
647
+ };
648
+ /**
649
+ * Falsy filter - checks if value is falsy.
650
+ *
651
+ * @param options - Unused
652
+ * @returns Filter function that returns true for false/null/undefined/0/''/NaN
653
+ */
654
+ const falsy = (_options) => {
655
+ return (value) => value === false || value === null || value === undefined || value === 0 || value === '' || Number.isNaN(value);
656
+ };
657
+ /**
658
+ * Truthy filter - checks if value is truthy.
659
+ *
660
+ * @param options - Unused
661
+ * @returns Filter function that returns true for non-falsy values
662
+ */
663
+ const truthy = (_options) => {
664
+ return (value) => value !== false && value !== null && value !== undefined && value !== 0 && value !== '' && !Number.isNaN(value);
665
+ };
666
+ /**
667
+ * Default filter - returns default value if input is falsy.
668
+ *
669
+ * @param options - Array with default value as first element
670
+ * @returns Filter function that returns value or default
671
+ */
672
+ const defaults = (options) => {
673
+ const opt = options?.[0] ?? optionsRequired('defaults');
674
+ return (value) => {
675
+ if (value === false || value === null || value === undefined || value === 0 || value === '' || Number.isNaN(value)) {
676
+ return opt;
677
+ }
678
+ return value;
679
+ };
680
+ };
681
+ /**
682
+ * Boolean filter - converts value to boolean.
683
+ *
684
+ * @param options - Unused
685
+ * @returns Filter function that returns boolean
686
+ */
687
+ const boolean = (_options) => {
688
+ return (value) => {
689
+ return Boolean(value);
690
+ };
691
+ };
692
+ /**
693
+ * Number filter - converts value to number.
694
+ *
695
+ * @param options - Unused
696
+ * @returns Filter function that returns number
697
+ */
698
+ const number = (_options) => {
699
+ return (value) => {
700
+ return Number(value);
701
+ };
702
+ };
703
+ /**
704
+ * String filter - converts value to string.
705
+ *
706
+ * @param options - Unused
707
+ * @returns Filter function that returns string
708
+ */
709
+ const string = (_options) => {
710
+ return (value) => {
711
+ return String(value);
712
+ };
713
+ };
714
+ /**
715
+ * Null filter - converts empty string to null.
716
+ *
717
+ * @param options - Unused
718
+ * @returns Filter function that returns null for empty string, otherwise original value
719
+ */
720
+ const _null = (_options) => {
721
+ return (value) => {
722
+ return (value === "") ? null : value;
723
+ };
724
+ };
725
+ const builtinFilters = {
726
+ "eq": eq,
727
+ "ne": ne,
728
+ "not": not,
729
+ "lt": lt,
730
+ "le": le,
731
+ "gt": gt,
732
+ "ge": ge,
733
+ "inc": inc,
734
+ "dec": dec,
735
+ "mul": mul,
736
+ "div": div,
737
+ "mod": mod,
738
+ "fix": fix,
739
+ "locale": locale,
740
+ "uc": uc,
741
+ "lc": lc,
742
+ "cap": cap,
743
+ "trim": trim,
744
+ "slice": slice,
745
+ "substr": substr,
746
+ "pad": pad,
747
+ "rep": rep,
748
+ "rev": rev,
749
+ "int": int,
750
+ "float": float,
751
+ "round": round,
752
+ "floor": floor,
753
+ "ceil": ceil,
754
+ "percent": percent,
755
+ "date": date,
756
+ "time": time,
757
+ "datetime": datetime,
758
+ "ymd": ymd,
759
+ "falsy": falsy,
760
+ "truthy": truthy,
761
+ "defaults": defaults,
762
+ "boolean": boolean,
763
+ "number": number,
764
+ "string": string,
765
+ "null": _null,
766
+ };
767
+ const outputBuiltinFilters = builtinFilters;
768
+
769
+ /**
770
+ * filterMeta.ts — 組み込みフィルタの構造化メタデータ(単一正本・route-a A2-1)。
771
+ *
772
+ * これまで vscode-wcs(completionData.ts BUILTIN_FILTERS)が手で持っていたフィルタの
773
+ * 引数仕様・型・説明を、実装側(@wcstack/state)に**正本として移設**したもの。
774
+ * manifest.ts がこれを公開し、vscode-wcs はそれを消費して手リストを撤去できる。
775
+ *
776
+ * 完全性は __tests__/manifest.test.ts のドリフト検出が保証する
777
+ * (filterMeta のキー集合 == builtinFilters のキー集合)。フィルタを追加して meta を
778
+ * 書き忘れると CI が落ちる。
779
+ */
780
+ /** 組み込みフィルタ名 → 構造化メタデータ。キー集合は builtinFilters と一致しなければならない。 */
781
+ const builtinFilterMeta = {
782
+ // 比較・論理
783
+ eq: { description: "等しいか比較", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
784
+ ne: { description: "異なるか比較", hasArgs: true, resultType: "boolean", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
785
+ not: { description: "ブール値を反転", hasArgs: false, resultType: "boolean", acceptTypes: ["boolean"], minArgs: 0, maxArgs: 0 },
786
+ lt: { description: "より小さいか", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
787
+ le: { description: "以下か", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
788
+ gt: { description: "より大きいか", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
789
+ ge: { description: "以上か", hasArgs: true, resultType: "boolean", acceptTypes: ["number", "string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
790
+ // 算術
791
+ inc: { description: "加算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
792
+ dec: { description: "減算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
793
+ mul: { description: "乗算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
794
+ div: { description: "除算", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
795
+ mod: { description: "剰余", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
796
+ // 数値フォーマット
797
+ fix: { description: "固定小数点表記", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
798
+ locale: { description: "ロケール形式で数値フォーマット", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["string"] },
799
+ // 文字列
800
+ uc: { description: "大文字に変換", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
801
+ lc: { description: "小文字に変換", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
802
+ cap: { description: "先頭文字を大文字に", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
803
+ trim: { description: "前後の空白を削除", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
804
+ slice: { description: "部分文字列 (start[,end])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "number"] },
805
+ substr: { description: "部分文字列 (pos,len)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "number"] },
806
+ pad: { description: "パディング (length[,char])", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 2, argTypes: ["number", "string"] },
807
+ rep: { description: "繰り返し (count)", hasArgs: true, resultType: "string", acceptTypes: ["string"], minArgs: 1, maxArgs: 1, argTypes: ["number"] },
808
+ rev: { description: "文字順を反転", hasArgs: false, resultType: "string", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
809
+ // 数値パース・丸め
810
+ int: { description: "整数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
811
+ float: { description: "浮動小数点数にパース", hasArgs: false, resultType: "number", acceptTypes: ["string", "number"], minArgs: 0, maxArgs: 0 },
812
+ round: { description: "四捨五入", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
813
+ floor: { description: "切り下げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
814
+ ceil: { description: "切り上げ", hasArgs: true, resultType: "number", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
815
+ percent: { description: "パーセンテージ形式", hasArgs: true, resultType: "string", acceptTypes: ["number"], minArgs: 0, maxArgs: 1, argTypes: ["number"] },
816
+ // 日付・時刻
817
+ date: { description: "ロケール形式の日付", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
818
+ time: { description: "ロケール形式の時刻", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
819
+ datetime: { description: "ロケール形式の日時", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
820
+ ymd: { description: "YYYY-MM-DD 形式", hasArgs: true, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 1, argTypes: ["string"] },
821
+ // 真偽値・変換
822
+ falsy: { description: "偽値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
823
+ truthy: { description: "真値か判定", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
824
+ defaults: { description: "偽値の場合デフォルト値", hasArgs: true, resultType: "passthrough", acceptTypes: "any", minArgs: 1, maxArgs: 1, argTypes: ["any"] },
825
+ boolean: { description: "ブール値に変換", hasArgs: false, resultType: "boolean", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
826
+ number: { description: "数値に変換", hasArgs: false, resultType: "number", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
827
+ string: { description: "文字列に変換", hasArgs: false, resultType: "string", acceptTypes: "any", minArgs: 0, maxArgs: 0 },
828
+ null: { description: "空文字列をnullに変換", hasArgs: false, resultType: "passthrough", acceptTypes: ["string"], minArgs: 0, maxArgs: 0 },
829
+ };
830
+
831
+ const STRUCTURAL_BINDING_TYPE_SET = new Set([
832
+ "if",
833
+ "elseif",
834
+ "else",
835
+ "for",
836
+ ]);
837
+
838
+ const DELIMITER = '.';
839
+ const WILDCARD = '*';
840
+ const MAX_WILDCARD_DEPTH = 128;
841
+ // data-wcs バインディング構文 `[prop][#mod]: [path][@state][|filter...]` の区切り文字(単一正本)。
842
+ // これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
843
+ const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
844
+ const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
845
+ const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
846
+ const STATE_NAME_SEPARATOR = '@'; // path と @stateName の区切り
847
+ const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
848
+ /**
849
+ * stackIndexByIndexName
850
+ * インデックス名からスタックインデックスへのマッピング
851
+ * $1 => 0
852
+ * $2 => 1
853
+ * :
854
+ * ${i + 1} => i
855
+ * i < MAX_WILDCARD_DEPTH
856
+ */
857
+ const tmpIndexByIndexName = {};
858
+ for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
859
+ tmpIndexByIndexName[`$${i + 1}`] = i;
860
+ }
861
+ Object.freeze(tmpIndexByIndexName);
862
+ const STATE_CONNECTED_CALLBACK_NAME = "$connectedCallback";
863
+ const STATE_DISCONNECTED_CALLBACK_NAME = "$disconnectedCallback";
864
+ const STATE_UPDATED_CALLBACK_NAME = "$updatedCallback";
865
+ const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
866
+ const STATE_BINDABLES_NAME = "$bindables";
867
+ const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
868
+ const STATE_COMMAND_NAMESPACE_NAME = "$command";
869
+ const STATE_EVENT_TOKENS_NAME = "$eventTokens";
870
+ const STATE_ON_NAME = "$on";
871
+
872
+ /**
873
+ * manifest.ts — `<wcs-state>` の構文・フィルタ・予約名を機械可読な単一正本として公開する。
874
+ *
875
+ * 目的(route-a A2-1): vscode-wcs(wcstack-intellisense)が現在ハードコードで二重実装している
876
+ * 「フィルタ一覧・構文区切り・予約名」を、state 側の実装から導出した manifest に一本化し、
877
+ * 手作業同期によるドリフトを構造的に断つための土台。
878
+ *
879
+ * 設計:
880
+ * - `filters` は実装(builtinFilters の Record キー)から **自動導出**=実装が唯一の正本。
881
+ * - 構文・予約名は config / define.ts の定数から導出。
882
+ * - 将来 `dist/wcs-manifest.json` としてビルド時に書き出し、vscode-wcs がそれを読む形に発展させる。
883
+ * - ドリフト検出テスト(__tests__/manifest.test.ts)が、フィルタ集合の golden と実装の一致を CI で保証する。
884
+ */
885
+ /** マニフェストのバージョン(構造を変えたら上げる)。 */
886
+ const WCS_MANIFEST_VERSION = 1;
887
+ /** 機械可読な単一正本を返す。vscode-wcs はこれを消費する想定。 */
888
+ function getWcsManifest() {
889
+ return {
890
+ version: WCS_MANIFEST_VERSION,
891
+ syntax: {
892
+ bindAttribute: config.bindAttributeName,
893
+ tagName: config.tagNames.state,
894
+ pathDelimiter: DELIMITER,
895
+ wildcard: WILDCARD,
896
+ delimiters: {
897
+ binding: BINDING_SEPARATOR,
898
+ propValue: PROP_VALUE_SEPARATOR,
899
+ modifier: MODIFIER_SEPARATOR,
900
+ stateName: STATE_NAME_SEPARATOR,
901
+ filter: FILTER_SEPARATOR,
902
+ },
903
+ // 正本 STRUCTURAL_BINDING_TYPE_SET から導出(手書きの二重定義を排除)。
904
+ structuralDirectives: Array.from(STRUCTURAL_BINDING_TYPE_SET),
905
+ },
906
+ // 実装(Record のキー)から自動導出。手リストを持たない=ドリフトの構造的排除。
907
+ filters: Object.keys(outputBuiltinFilters),
908
+ filterMeta: builtinFilterMeta,
909
+ reservedLifecycle: [
910
+ STATE_CONNECTED_CALLBACK_NAME,
911
+ STATE_DISCONNECTED_CALLBACK_NAME,
912
+ STATE_UPDATED_CALLBACK_NAME,
913
+ WEBCOMPONENT_STATE_READY_CALLBACK_NAME,
914
+ ],
915
+ reservedStateApi: [
916
+ STATE_BINDABLES_NAME,
917
+ STATE_COMMAND_TOKENS_NAME,
918
+ STATE_COMMAND_NAMESPACE_NAME,
919
+ STATE_EVENT_TOKENS_NAME,
920
+ STATE_ON_NAME,
921
+ ],
922
+ };
923
+ }
924
+
925
+ export { STRUCTURAL_BINDING_TYPE_SET, WCS_MANIFEST_VERSION, builtinFilterMeta, getWcsManifest };