@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,878 +1,709 @@
1
- const DELIMITER = '.';
2
- const WILDCARD = '*';
3
- const MAX_WILDCARD_DEPTH = 128;
4
- /**
5
- * 1 本のパスが持てるセグメント数の上限(`getPathInfo` が初回 intern のときだけ検査する)。
6
- *
7
- * `PathInfo` は自分の**全ての接頭辞**を intern するので、深さ N のパス 1 本で N 個の
8
- * `PathInfo` ができ、それぞれが長さ k の配列と Set を持つ = 時間もメモリも O(N²)。
9
- * 実測(Node 22・パス 1 本)で depth 400 → 145ms / 61MB、800 → 1162ms / 424MB、
10
- * 2000 → 既定 4GB ヒープで OOM。**約 4KB の `data-wcs` 属性値 1 つでタブを落とせた。**
11
- * ここで打ち切ると最悪でも 256² ≒ 65k 単位に収まる。
12
- *
13
- * 値は `MAX_WILDCARD_DEPTH`(128 = `$1..$128` の表の大きさ)を**使い切れる**ことから決めた:
14
- * `$recursion` の展開(`recursion/expand.ts`)が深さ 128 で作るパスは `a.*.b.*.…` の形で
15
- * 257 セグメントを超える。そこを割ると、ドキュメント済みの再帰深さが打ち切られてしまう。
16
- * 512 なら最悪でも 512² ≒ 262k 単位で、上の実測の 400 段(145ms / 61MB)の延長に収まる。
17
- * 実用のパスは 10 段に満たない。
18
- */
19
- const MAX_PATH_SEGMENTS = 512;
20
- // data-wcs バインディング構文 `[prop][#mod]: [path][|filter...]` の区切り文字(単一正本・`@state` は v2 で撤去)。
21
- // これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
22
- const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
23
- const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
24
- const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
25
- const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
26
- // bindingType 判別と左辺 namespace の語彙(単一正本)。manifest.syntax.bindingTypes で
27
- // 公開される。パーサ(parseBindTextsForElement)とイベント層はこの定数に分岐する。
28
- // apply 層のディスパッチマップ(apply/applyChange.ts の applyChangeByFirstSegment)の
29
- // キー集合との一致は __tests__/manifest.test.ts の drift テストが強制する —
30
- // manifest エントリ(DOM 非依存)から apply 層を import しないための分離。
31
- const ELSE_KEYWORD = 'else';
32
- const SPREAD_PROP = '...';
33
- const EVENT_PROP_PREFIX = 'on';
34
- const EVENT_TOKEN_NAMESPACE = 'eventToken';
35
- /**
36
- * `<wcs-state mount>` の左辺 `state.<key>: path` — ボリュームの注入口(要件 B14③・3.x 計画 D28)。
37
- * 束縛ではなくマウントの宣言なので、束縛の収集(getParseBindTextResults)は作らず、ボリュームが接ぎ木の時に読む
38
- */
39
- const VOLUME_INJECTION_PROP = 'state';
40
- const COMMAND_NAMESPACE = 'command';
41
- const CLASS_NAMESPACE = 'class';
42
- const ATTR_NAMESPACE = 'attr';
43
- const STYLE_NAMESPACE = 'style';
44
- // リストインデックス参照名(`$1`..`$N`)の接頭辞(単一正本)。
45
- // manifest.syntax.indexParam で公開される。
46
- const INDEX_PARAM_PREFIX = '$';
47
- /**
48
- * stackIndexByIndexName
49
- * インデックス名からスタックインデックスへのマッピング
50
- * $1 => 0
51
- * $2 => 1
52
- * :
53
- * ${i + 1} => i
54
- * i < MAX_WILDCARD_DEPTH
55
- */
56
- const tmpIndexByIndexName = {};
57
- for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
58
- tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
59
- }
60
- Object.freeze(tmpIndexByIndexName);
61
- /**
62
- * 再帰ワイルドカード。オーサリング層($recursion 宣言・getter キー・API 引数)にだけ
63
- * 現れ、PathInfo には決して降ろさない — wildcardCount が不定になると ListIndex 連鎖長・
64
- * $1..$n・$resolve の厳密一致・走査の段数が同時に壊れる
65
- * (docs/state-recursive-path-design.md §2-1)。
66
- */
67
- const RECURSION_WILDCARD = "**";
68
-
69
- function raiseError(message) {
70
- throw new Error(`[@wcstack/state] ${message}`);
71
- }
1
+ // src/hooks.ts
2
+ var hooks = {};
72
3
 
73
- const _cache = new Map();
74
- /**
75
- * **tooling 専用**(`@wcstack/state/parser` の clearParserCaches からのみ呼ぶ)。
76
- * ランタイム文脈で呼んではならない — PathInfo のインスタンス同一性は正規化キー
77
- * (依存グラフ・アドレス比較)の前提であり、クリアすると同一パスの新旧インスタンスが
78
- * 併存して identity 比較が黙って壊れる。言語サーバー等の長時間プロセスが、編集中の
79
- * 中間パス(`user.n` 等)の恒久 intern によるメモリ単調増加を断つための出口。
80
- */
81
- function clearPathInfoCacheForTooling() {
82
- _cache.clear();
83
- }
84
- let id = 0;
85
- function getPathInfo(path) {
86
- let pathInfo = _cache.get(path);
87
- if (typeof pathInfo !== "undefined") {
88
- return pathInfo;
89
- }
90
- // 再帰ワイルドカードはオーサリング層の記号で、ここへ降りてきてはならない
91
- // (降ろすと wildcardCount が不定になり ListIndex 連鎖長・$1..$n・$resolve の
92
- // 厳密一致・走査の段数が同時に壊れる。設計書 D2)。到達したということは、
93
- // `**` を解釈しない消費者に `**` パスが渡ったということ。通常のパスはこの検査を
94
- // 初回 intern のときにしか払わない(`**` パスは intern されないので読むたびに落ちる)。
95
- if (path.indexOf(RECURSION_WILDCARD) !== -1) {
96
- raiseError(`[wcs/recursion-unsupported] "${path}" uses "${RECURSION_WILDCARD}", which is not accepted here. ` +
97
- `It is only meaningful in a $recursion declaration, in a recursive getter key, and in the path ` +
98
- `argument of $getAll / $setAll — and only when the state declares a $recursion anchor.`);
99
- }
100
- // 深さの上限(初回 intern のときだけ払う)。`PathInfo` は**全ての接頭辞**を intern するので
101
- // 深さ N のパス 1 本で時間もメモリも O(N²) になり、上限が無いと約 4KB の属性値 1 つで
102
- // タブが落ちた(実測値は `MAX_PATH_SEGMENTS` の注記)。
103
- //
104
- // **ワイルドカードの段数はここでは見ない。** 段数の上限(`MAX_WILDCARD_DEPTH` = manifest の
105
- // `indexParam.maxDepth`)は `$1..$N` の表が引けるかという別の話で、`recursion/expand.ts` の
106
- // `[wcs/recursion-depth-exceeded]` と `proxy/traps/get.ts` の `$N` 範囲外診断が、原因を
107
- // 名指しできる場所で受け持っている(番人: `integration.recursionPrerequisites.test.ts` が
108
- // 「PathInfo 自体には段数の上限が無い」を固定している)。ここで先に落とすと、深さ超過か
109
- // 循環かの切り分けが効かなくなる
110
- const segmentCount = countSegments(path);
111
- if (segmentCount > MAX_PATH_SEGMENTS) {
112
- raiseError(`[wcs/binding-syntax] "${path}" has ${segmentCount} path segments — the limit is ${MAX_PATH_SEGMENTS}. ` +
113
- `Every prefix of a path is interned, so the cost grows with the square of the depth.`);
4
+ // src/diagnostics/guidance.ts
5
+ function editDistance(a, b, max) {
6
+ if (Math.abs(a.length - b.length) > max) {
7
+ return max + 1;
8
+ }
9
+ const prev = new Array(b.length + 1);
10
+ const curr = new Array(b.length + 1);
11
+ for (let j = 0; j <= b.length; j++) {
12
+ prev[j] = j;
13
+ }
14
+ for (let i = 1; i <= a.length; i++) {
15
+ curr[0] = i;
16
+ for (let j = 1; j <= b.length; j++) {
17
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
18
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
114
19
  }
115
- pathInfo = Object.freeze(new PathInfo(path));
116
- _cache.set(path, pathInfo);
117
- return pathInfo;
118
- }
119
- /** `.` の数 + 1。`split` の配列を作らずに数える(初回 intern のときだけ通る) */
120
- function countSegments(path) {
121
- let count = 1;
122
- for (let i = 0; i < path.length; i++) {
123
- if (path[i] === DELIMITER) {
124
- count++;
125
- }
20
+ for (let j = 0; j <= b.length; j++) {
21
+ prev[j] = curr[j];
126
22
  }
127
- return count;
23
+ }
24
+ return prev[b.length];
128
25
  }
129
- class PathInfo {
130
- id = ++id;
131
- path;
132
- segments;
133
- lastSegment;
134
- cumulativePaths;
135
- cumulativePathSet;
136
- cumulativePathInfos;
137
- cumulativePathInfoSet;
138
- parentPath;
139
- wildcardPaths;
140
- wildcardPathSet;
141
- indexByWildcardPath;
142
- wildcardPathInfos;
143
- wildcardPathInfoSet;
144
- wildcardParentPaths;
145
- wildcardParentPathSet;
146
- wildcardParentPathInfos;
147
- wildcardParentPathInfoSet;
148
- wildcardPositions;
149
- lastWildcardPath;
150
- lastWildcardInfo;
151
- wildcardCount;
152
- parentPathInfo;
153
- constructor(path) {
154
- // Helper to get or create StructuredPathInfo instances, avoiding redundant creation for self-reference
155
- const getPattern = (_path) => {
156
- return (path === _path) ? this : getPathInfo(_path);
157
- };
158
- // Split the pattern into individual path segments (e.g., "items.*.name" → ["items", "*", "name"])
159
- const segments = path.split(".");
160
- // Arrays to track all cumulative paths from root to each segment
161
- const cumulativePaths = [];
162
- const cumulativePathInfos = [];
163
- // Arrays to track wildcard-specific information
164
- const wildcardPaths = [];
165
- const indexByWildcardPath = {}; // Maps wildcard path to its index position
166
- const wildcardPathInfos = [];
167
- const wildcardParentPaths = []; // Paths of parent segments for each wildcard
168
- const wildcardParentPathInfos = [];
169
- const wildcardPositions = [];
170
- let currentPatternPath = "", prevPatternPath = "";
171
- let wildcardCount = 0;
172
- // Iterate through each segment to build cumulative paths and identify wildcards
173
- for (let i = 0; i < segments.length; i++) {
174
- currentPatternPath += segments[i];
175
- // If this segment is a wildcard, track it with all wildcard-specific metadata
176
- if (segments[i] === WILDCARD) {
177
- wildcardPaths.push(currentPatternPath);
178
- indexByWildcardPath[currentPatternPath] = wildcardCount; // Store wildcard's ordinal position
179
- wildcardPathInfos.push(getPattern(currentPatternPath));
180
- wildcardParentPaths.push(prevPatternPath); // Parent path is the previous cumulative path
181
- wildcardParentPathInfos.push(getPattern(prevPatternPath));
182
- wildcardPositions.push(i);
183
- wildcardCount++;
184
- }
185
- // Track all cumulative paths for hierarchical navigation (e.g., "items", "items.*", "items.*.name")
186
- cumulativePaths.push(currentPatternPath);
187
- cumulativePathInfos.push(getPattern(currentPatternPath));
188
- // Save current path as previous for next iteration, then add separator
189
- prevPatternPath = currentPatternPath;
190
- currentPatternPath += ".";
191
- }
192
- // Determine the deepest (last) wildcard path and the parent path of the entire pattern
193
- const lastWildcardPath = wildcardPaths.length > 0 ? wildcardPaths[wildcardPaths.length - 1] : null;
194
- const parentPath = cumulativePaths.length > 1 ? cumulativePaths[cumulativePaths.length - 2] : null;
195
- // Assign all analyzed data to readonly properties
196
- this.path = path;
197
- this.segments = segments;
198
- this.lastSegment = segments[segments.length - 1];
199
- this.cumulativePaths = cumulativePaths;
200
- this.cumulativePathSet = new Set(cumulativePaths); // Set for fast lookup
201
- this.cumulativePathInfos = cumulativePathInfos;
202
- this.cumulativePathInfoSet = new Set(cumulativePathInfos);
203
- this.wildcardPaths = wildcardPaths;
204
- this.wildcardPathSet = new Set(wildcardPaths);
205
- this.indexByWildcardPath = indexByWildcardPath;
206
- this.wildcardPathInfos = wildcardPathInfos;
207
- this.wildcardPathInfoSet = new Set(wildcardPathInfos);
208
- this.wildcardParentPaths = wildcardParentPaths;
209
- this.wildcardParentPathSet = new Set(wildcardParentPaths);
210
- this.wildcardParentPathInfos = wildcardParentPathInfos;
211
- this.wildcardParentPathInfoSet = new Set(wildcardParentPathInfos);
212
- this.wildcardPositions = wildcardPositions;
213
- this.lastWildcardPath = lastWildcardPath;
214
- this.lastWildcardInfo = lastWildcardPath ? getPattern(lastWildcardPath) : null;
215
- this.parentPath = parentPath;
216
- this.parentPathInfo = parentPath ? getPattern(parentPath) : null;
217
- this.wildcardCount = wildcardCount;
26
+ function didYouMean(input, candidates) {
27
+ if (input.length === 0) {
28
+ return "";
29
+ }
30
+ const folded = input.toLowerCase();
31
+ let best = null;
32
+ let bestDistance = 3;
33
+ for (const candidate of candidates) {
34
+ const distance = editDistance(folded, candidate.toLowerCase(), 2);
35
+ if (distance < bestDistance) {
36
+ best = candidate;
37
+ bestDistance = distance;
218
38
  }
39
+ }
40
+ return best !== null ? ` Did you mean "${best}"?` : "";
219
41
  }
42
+ var LINT_HINT = " Validate statically: npx @wcstack/lint <file>.";
220
43
 
221
- /**
222
- * errorGuidance.ts — エラーメッセージへの self-fix 誘導(GTM 2-5 /
223
- * docs/static-wiring-dx-design.md §3)。
224
- *
225
- * コンソールは「書き手(人間・AI とも)が誤った瞬間に必ず読む面」なので、
226
- * (a) did-you-mean 候補 (b) lint への誘導 をエラーメッセージ自体に埋め込む。
227
- * ここの関数は全て**エラーパスでのみ**呼ばれる — 正常系のコストはゼロ。
228
- * auto.min.js に同梱されるため文字列は最小限に保つ(エラーパス専用モジュールの
229
- * 遅延 import は `src/auto.ts` の SRI 自己完結制約で不可)。
230
- *
231
- * 診断 code の語彙はコンソール → lint → IDE の三面で共有する:
232
- * メッセージ先頭の `[wcs/...]` は wcstack-intellisense / @wcstack/lint の
233
- * 安定診断 code(packages/vscode-wcs/src/core/diagnostics.ts)と同一。
234
- */
235
- /** 挿入・削除・置換の編集距離。長さ差が max を超えたら早期に max+1 を返す。 */
236
- /**
237
- * lint への誘導(誘導付きメッセージ共通の一文)。
238
- * **lint が実際にそのケースを検出するサイトにだけ付ける** — 検出しないケースに
239
- * 付けると「エラー → lint 実行 → clean」の空振りで検証ループの信頼を毀損する。
240
- * 現在 lint 未検出のため付けないもの: DCC 宣言・watch の空キー / Object.prototype
241
- * 継承名 / ワイルドカード深度超過。
242
- * なお hint 付きサイト内でも被覆は部分的でありうる(例: `$watch: ident` の実体が
243
- * 非オブジェクトだった場合、ランタイムは評価後の値で raise するが lint は宣言 shape
244
- * から断定できず沈黙する)。サイト粒度の hint ではこの残余は構造的に避けられない。
245
- */
246
- const LINT_HINT = " Validate statically: npx @wcstack/lint <file>.";
44
+ // src/parser/raiseError.ts
45
+ function raiseError(message, subject, candidates) {
46
+ throw new Error(`[@wcstack/state] ${message}${hooks.explain?.(message, subject, candidates) ?? ""}`);
47
+ }
247
48
 
248
- const STRUCTURAL_BINDING_TYPE_SET = new Set([
249
- "if",
250
- "elseif",
251
- "else",
252
- "for",
253
- ]);
49
+ // src/messages.ts
50
+ var CODES = [
51
+ "",
52
+ "binding-syntax",
53
+ "template-syntax",
54
+ "binding-path-missing",
55
+ "binding-type-expectation",
56
+ "filter-unknown",
57
+ "filter-arity",
58
+ "getter-cycle",
59
+ "getter-depth-exceeded",
60
+ "index-arity",
61
+ "index-param-range",
62
+ "recursion-unsupported",
63
+ "token-misconfigured",
64
+ "token-undeclared",
65
+ "wildcard-rank",
66
+ "spread-no-bindable",
67
+ "declaration-alias",
68
+ "name-alias"
69
+ ];
70
+ var codeOf = (id) => {
71
+ const c = CODES[id / 100 | 0];
72
+ return c ? `[wcs/${c}] ` : "";
73
+ };
74
+ function text(id, args = []) {
75
+ return hooks.render?.(id, args) ?? `${codeOf(id)}#${id}${args.map((a) => ` ${typeof a === "string" ? JSON.stringify(a) : String(a)}`).join("")}`;
76
+ }
77
+ function raise(id, args, subject, candidates) {
78
+ raiseError(text(id, args), subject, candidates);
79
+ }
254
80
 
255
- /**
256
- * core/filterRegistry.ts — フィルタ実関数の登録簿(設計案 §4、要件 D16)。
257
- *
258
- * 文法(`path|filter(args)` の解析)は core に残り、**実関数は登録簿から束縛計画の段で引く**。
259
- * 解析の段は名前と引数しか作らない(`bindTextParser/parseFilters.ts`)ので、パーサだけを使う
260
- * tooling(`@wcstack/state/parser`)はフィルタの実装を 1 バイトも引き込まない。
261
- *
262
- * 書式フィルタ群(`uc` / `date` / `round` …)は `features/formats` が install で登録する。
263
- * core が自前で持つのは、エンジン自身が差し込む `not` だけ(`if` / `else` の反転 —
264
- * structural/notFilter.ts)。未知のフィルタは束縛計画の段で名指しで落ちる(従来は解析時)。
265
- */
266
- /** 名前 + 引数 + 入出力ごとに解決済みの実関数(解決は 1 回だけ) */
267
- const resolvedByKey = new Map();
268
- /** 解決済みの答えを捨てる(tooling: `@wcstack/state/parser` の clearParserCaches) */
269
- function clearFilterResolutionCache() {
270
- resolvedByKey.clear();
81
+ // src/filters/registry.ts
82
+ var FORMATS_FILTER_NAMES = [
83
+ "toFixed",
84
+ "locale",
85
+ "upper",
86
+ "lower",
87
+ "capitalize",
88
+ "trim",
89
+ "slice",
90
+ "padStart",
91
+ "padEnd",
92
+ "repeat",
93
+ "reverse",
94
+ "truncate",
95
+ "join",
96
+ "round",
97
+ "floor",
98
+ "ceil",
99
+ "percent",
100
+ "unit",
101
+ "date",
102
+ "time",
103
+ "datetime",
104
+ "ymd",
105
+ "hms"
106
+ ];
107
+ var definitions = /* @__PURE__ */ new Map();
108
+ function hasFilter(name) {
109
+ return definitions.has(name);
271
110
  }
272
111
 
273
- /**
274
- * フィルタ引数リストのパース。`filter(a, b)` の `a, b` 部分を受け取る。
275
- *
276
- * トリムの規則は「**クォートの外側だけ**」。`fix( 2 )` のような書き癖を吸収するために
277
- * 素の引数は前後をトリムするが、クォートは「ここは literal」という宣言なので中身の
278
- * 空白は残す。両方まとめてトリムしていたため `pad(5, ' ')` が空文字パディング
279
- * (=無変化)に化けており、空白区切りの `join(' / ')` も指定できなかった。
280
- */
281
- /** 引数 1 つを確定する。クォート由来の文字が入った範囲より外側だけをトリムする。 */
282
- function finalizeArg(text, firstQuoteStart, lastQuoteEnd) {
283
- // 先頭側: 最初のクォート文字より前だけが削れる(クォートが無ければ全体が対象)
284
- const startLimit = firstQuoteStart === -1 ? text.length : firstQuoteStart;
285
- let start = 0;
286
- while (start < startLimit && /\s/.test(text[start])) {
287
- start++;
288
- }
289
- // 末尾側: 最後のクォート文字より後ろだけが削れる(クォートが無ければ全体が対象)
290
- const endLimit = lastQuoteEnd === -1 ? 0 : lastQuoteEnd;
291
- let end = text.length;
292
- while (end > endLimit && /\s/.test(text[end - 1])) {
293
- end--;
294
- }
295
- return text.slice(start, end);
112
+ // src/pattern.ts
113
+ var WILDCARD = "*";
114
+
115
+ // src/parser/define.ts
116
+ var DELIMITER = ".";
117
+ var BINDING_SEPARATOR = ";";
118
+ var PROP_VALUE_SEPARATOR = ":";
119
+ var MODIFIER_SEPARATOR = "#";
120
+ var FILTER_SEPARATOR = "|";
121
+ var ELSE_KEYWORD = "else";
122
+ var SPREAD_PROP = "...";
123
+ var EVENT_PROP_PREFIX = "on";
124
+ var EVENT_TOKEN_NAMESPACE = "eventToken";
125
+ var VOLUME_INJECTION_PROP = "state";
126
+ var COMMAND_NAMESPACE = "command";
127
+ var CLASS_NAMESPACE = "class";
128
+ var ATTR_NAMESPACE = "attr";
129
+ var STYLE_NAMESPACE = "style";
130
+ var MAX_PATH_SEGMENTS = 512;
131
+ var MAX_INDEX_PARAM = 128;
132
+ var RECURSION_WILDCARD = "**";
133
+
134
+ // src/parser/parseFilterArgs.ts
135
+ function finalizeArg(text2, firstQuoteStart, lastQuoteEnd) {
136
+ const startLimit = firstQuoteStart === -1 ? text2.length : firstQuoteStart;
137
+ const endLimit = lastQuoteEnd === -1 ? 0 : lastQuoteEnd;
138
+ return text2.slice(startLimit - text2.slice(0, startLimit).trimStart().length, endLimit + text2.slice(endLimit).trimEnd().length);
296
139
  }
297
- /** 引用符の無い引数の型(要件 B9): true / false / null / 数値は型付き、それ以外は文字列 */
298
- const NUMBER_LITERAL = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
299
- function toLiteral(text, quoted) {
300
- if (quoted)
301
- return text;
302
- if (text === "true")
303
- return true;
304
- if (text === "false")
305
- return false;
306
- if (text === "null")
307
- return null;
308
- return NUMBER_LITERAL.test(text) ? Number(text) : text;
140
+ var NUMBER_LITERAL = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
141
+ function toLiteral(text2, quoted) {
142
+ if (quoted) return text2;
143
+ if (text2 === "true") return true;
144
+ if (text2 === "false") return false;
145
+ if (text2 === "null") return null;
146
+ return NUMBER_LITERAL.test(text2) ? Number(text2) : text2;
309
147
  }
310
- /**
311
- * 引数の原文と、その型付きの値(要件 B9)を一緒に返す。原文は引用符を外したもの。
312
- *
313
- * 末尾の空引数だけが落ちる(`"a,"` → 1 個、`",a"` → 2 個、`","` → 1 個)。`filter()` を
314
- * 「引数 0 個」と読むための規則で、先頭・中間の空引数は位置を保つために残す。
315
- */
316
148
  function parseFilterArgsWithLiterals(argsText) {
317
- const args = [];
318
- const literals = [];
319
- let current = '';
320
- let inQuote = null;
321
- let hasQuote = false;
322
- let firstQuoteStart = -1;
323
- let lastQuoteEnd = -1;
324
- const flush = () => {
325
- const arg = finalizeArg(current, firstQuoteStart, lastQuoteEnd);
326
- args.push(arg);
327
- literals.push(toLiteral(arg, hasQuote));
328
- current = '';
329
- hasQuote = false;
330
- firstQuoteStart = -1;
331
- lastQuoteEnd = -1;
332
- };
333
- for (let i = 0; i < argsText.length; i++) {
334
- const char = argsText[i];
335
- if (inQuote) {
336
- if (char === inQuote) {
337
- inQuote = null;
338
- }
339
- else {
340
- if (firstQuoteStart === -1) {
341
- firstQuoteStart = current.length;
342
- }
343
- current += char;
344
- lastQuoteEnd = current.length;
345
- }
346
- }
347
- else if (char === '"' || char === "'") {
348
- inQuote = char;
349
- hasQuote = true;
350
- }
351
- else if (char === ',') {
352
- flush();
353
- }
354
- else {
355
- current += char;
356
- }
357
- }
358
- if (inQuote !== null) {
359
- // 閉じていない引用符は受理しない(要件 B2)。以前は黙って閉じたことにしていた
360
- raiseError(`[wcs/binding-syntax] unterminated ${inQuote} quote in the filter arguments "(${argsText})". Close the quote.${LINT_HINT}`);
149
+ const args = [];
150
+ const literals = [];
151
+ let current = "";
152
+ let inQuote = null;
153
+ let hasQuote = false;
154
+ let firstQuoteStart = -1;
155
+ let lastQuoteEnd = -1;
156
+ const flush = (last) => {
157
+ const arg = finalizeArg(current, firstQuoteStart, lastQuoteEnd);
158
+ if (!last || arg || hasQuote) {
159
+ args.push(arg);
160
+ literals.push(toLiteral(arg, hasQuote));
361
161
  }
362
- const last = finalizeArg(current, firstQuoteStart, lastQuoteEnd);
363
- if (last || hasQuote) {
364
- args.push(last);
365
- literals.push(toLiteral(last, hasQuote));
162
+ current = "";
163
+ hasQuote = false;
164
+ firstQuoteStart = -1;
165
+ lastQuoteEnd = -1;
166
+ };
167
+ for (let i = 0; i < argsText.length; i++) {
168
+ const char = argsText[i];
169
+ if (inQuote) {
170
+ if (char === inQuote) {
171
+ inQuote = null;
172
+ } else {
173
+ if (firstQuoteStart === -1) {
174
+ firstQuoteStart = current.length;
175
+ }
176
+ current += char;
177
+ lastQuoteEnd = current.length;
178
+ }
179
+ } else if (char === '"' || char === "'") {
180
+ inQuote = char;
181
+ hasQuote = true;
182
+ } else if (char === ",") {
183
+ flush();
184
+ } else {
185
+ current += char;
366
186
  }
367
- return { args, literals };
187
+ }
188
+ if (inQuote !== null) {
189
+ raise(107 /* UnterminatedQuote */, [inQuote, argsText]);
190
+ }
191
+ flush(true);
192
+ return { args, literals };
368
193
  }
369
194
 
370
- const trimFn = (s) => s.trim();
371
- const isQuote = (c) => c === "'" || c === '"';
372
- /**
373
- * `text` の中で、引用符(`'` / `"`)の外にある最初の `char` の位置。無ければ -1(要件 B1)。
374
- * 閉じていない引用符はそのまま末尾まで続く扱い — 不正な引用符はフィルタ引数の段で名指しで落ちる。
375
- */
376
- function indexOfOutsideQuotes(text, char) {
377
- let quote = null;
378
- for (let i = 0; i < text.length; i++) {
379
- const c = text[i];
380
- if (quote !== null) {
381
- if (c === quote)
382
- quote = null;
383
- }
384
- else if (isQuote(c)) {
385
- quote = c;
386
- }
387
- else if (c === char) {
388
- return i;
389
- }
195
+ // src/parser/utils.ts
196
+ var trimFn = (s) => s.trim();
197
+ function outsideQuotes(text2, char) {
198
+ const found = [];
199
+ let quote = null;
200
+ for (let i = 0; i < text2.length; i++) {
201
+ const c = text2[i];
202
+ if (quote !== null) {
203
+ if (c === quote) quote = null;
204
+ } else if (c === "'" || c === '"') {
205
+ quote = c;
206
+ } else if (c === char) {
207
+ found.push(i);
390
208
  }
391
- return -1;
209
+ }
210
+ return found;
392
211
  }
393
- /**
394
- * `text` の中で、引用符の外にある**最後の** `char` の位置。無ければ -1(要件 B1)。
395
- * 引用符は前からしか追えないので、前向きに走査して最後の一致を控える。
396
- * フィルタの閉じ括弧を探す用(`a|foo(')')` の引用符内の `)` を終端と誤認しないため)。
397
- */
398
- function lastIndexOfOutsideQuotes(text, char) {
399
- let quote = null;
400
- let found = -1;
401
- for (let i = 0; i < text.length; i++) {
402
- const c = text[i];
403
- if (quote !== null) {
404
- if (c === quote)
405
- quote = null;
406
- }
407
- else if (isQuote(c)) {
408
- quote = c;
409
- }
410
- else if (c === char) {
411
- found = i;
412
- }
413
- }
414
- return found;
415
- }
416
- /**
417
- * `separator` で区切る。ただし引用符の中は区切らない(要件 B1): `join(';')` や `join('|')` の
418
- * 区切り文字は引数であって、バインディングやフィルタの区切りではない。
419
- */
420
- function splitOutsideQuotes(text, separator) {
421
- const parts = [];
422
- let quote = null;
423
- let start = 0;
424
- for (let i = 0; i < text.length; i++) {
425
- const c = text[i];
426
- if (quote !== null) {
427
- if (c === quote)
428
- quote = null;
429
- }
430
- else if (isQuote(c)) {
431
- quote = c;
432
- }
433
- else if (c === separator) {
434
- parts.push(text.slice(start, i));
435
- start = i + 1;
436
- }
437
- }
438
- parts.push(text.slice(start));
439
- return parts;
212
+ var indexOfOutsideQuotes = (text2, char) => outsideQuotes(text2, char)[0] ?? -1;
213
+ var lastIndexOfOutsideQuotes = (text2, char) => outsideQuotes(text2, char).pop() ?? -1;
214
+ function splitOutsideQuotes(text2, separator) {
215
+ const parts = [];
216
+ let start = 0;
217
+ for (const i of outsideQuotes(text2, separator)) {
218
+ parts.push(text2.slice(start, i));
219
+ start = i + 1;
220
+ }
221
+ parts.push(text2.slice(start));
222
+ return parts;
440
223
  }
441
224
 
442
- /** tooling 専用(parser.ts の clearParserCaches からのみ呼ぶ)。 */
443
- function clearFilterFnCacheForTooling() {
444
- clearFilterResolutionCache();
445
- }
446
- // format: filterName(arg1,arg2) or filterName
447
- /**
448
- * 文法の段(要件 D16): 名前と引数だけを読む。**実関数は引かない** — 束縛計画の段で
449
- * 登録簿から解決する(`core/filterRegistry.ts`・`bindings/getBindingInfos.ts`)。
450
- * 未知のフィルタもここでは落とさない: パーサだけを使う tooling は実装を持たないので、
451
- * 「知らない名前」を解析の段で判定できない。
452
- */
453
- /**
454
- * @param filterTextList 個々のフィルタの原文(`|` で切った後)
455
- * @param filterIOType 入力(左辺)/ 出力(右辺)。実関数は引かないが、助言の出し分けに使う
456
- * @param sourceText 診断に埋める原文。呼び出し側は**その辺の全文**(`propPart` / `statePart`)を
457
- * 渡すこと。`|` より後ろだけを渡すと、`textContent: a|` のように末尾が空の形で
458
- * 空文字になり、つなぎ直し(`filterTextList.join("|")`)と同じで文脈が消える。
459
- * 省略時のつなぎ直しは、原文を持たない直接の呼び出し用のフォールバック
460
- */
225
+ // src/parser/parseFilters.ts
461
226
  function parseFilters(filterTextList, filterIOType, sourceText) {
462
- const source = sourceText ?? filterTextList.join("|");
463
- return filterTextList.map((filterText) => {
464
- // 括弧も引用符の外だけで探す(要件 B1)。素の `indexOf` / `lastIndexOf` だと
465
- // `foo(')')` の引用符内の `)` を終端に取り、「閉じ括弧が無い」ではなく
466
- // 「引用符が閉じていない」という見当違いの診断になっていた
467
- const openParenIndex = indexOfOutsideQuotes(filterText, '(');
468
- let closeParenIndex = lastIndexOfOutsideQuotes(filterText, ')');
469
- if (closeParenIndex === -1) {
470
- // 引用符の外に閉じ括弧が無い = 引用符が閉じていない形(`join('x)`)か、本当に無いか。
471
- // 前者で「閉じ括弧が無い」と言うのは見当違いなので素の探索へ落とし、引数の段の
472
- // `[wcs/binding-syntax] unterminated ' quote` に診断させる
473
- closeParenIndex = filterText.lastIndexOf(')');
474
- }
475
- // check parentheses
476
- if (openParenIndex !== -1 && closeParenIndex === -1) {
477
- raiseError(`[wcs/binding-syntax] Invalid filter format: missing closing parenthesis in "${filterText}".${LINT_HINT}`);
478
- }
479
- if (closeParenIndex !== -1 && openParenIndex === -1) {
480
- raiseError(`[wcs/binding-syntax] Invalid filter format: missing opening parenthesis in "${filterText}".${LINT_HINT}`);
481
- }
482
- if (closeParenIndex !== -1 && closeParenIndex < openParenIndex) {
483
- // `foo)1(` — 以前は `substring` が start > end で引数を入れ替えるため、フィルタ名
484
- // `foo)1` として受理されていた
485
- raiseError(`[wcs/binding-syntax] Invalid filter format: ")" comes before "(" in "${filterText}".${LINT_HINT}`);
486
- }
487
- if (closeParenIndex !== -1 && filterText.slice(closeParenIndex + 1).trim().length > 0) {
488
- // 閉じ括弧の後ろの残余を**黙って捨てていた**。`n|fix(2)uc` は `uc` が消えて診断ゼロで
489
- // 通り、括弧の無い `n|ucuc` は `[wcs/filter-unknown]` で落ちる — 括弧の有無で非対称だった。
490
- // 実害は「`|` の打ち忘れでフィルタが 1 本消えても無診断」
491
- const trailing = filterText.slice(closeParenIndex + 1).trim();
492
- raiseError(`[wcs/binding-syntax] "${filterText}": unexpected "${trailing}" after the filter's closing ")" — ` +
493
- `separate filters with "|" (write "${filterText.slice(0, closeParenIndex + 1)}|${trailing}").${LINT_HINT}`);
494
- }
495
- const filterName = (openParenIndex === -1 ? filterText : filterText.substring(0, openParenIndex)).trim();
496
- if (filterName.length === 0) {
497
- // 空のフィルタ(`x|`・`x||y`・`x|(1)`)は文法の誤り。解析の段で名指しで落とす — 未知の
498
- // フィルタとは別物で、実関数の解決(束縛計画の段)まで持ち越すと tooling の解析が素通りする
499
- raiseError(`[wcs/binding-syntax] an empty filter in "${source}" — remove the extra "|" or name the filter.${LINT_HINT}`);
500
- }
501
- if (filterName.includes(MODIFIER_SEPARATOR)) {
502
- // フィルタ名に `#` が飲まれた形。`[wcs/filter-unknown] filter not found: trim#ro` だと
503
- // 診断が指す先が実際の誤りと違う(要件 B4 の並び)。
504
- // 助言は辺で分ける — **修飾子は左辺にしか存在しない**ので、右辺で「修飾子をフィルタより
505
- // 前に書け」と言うと成立しない直し方(`textContent#ro|trim: x`)を勧めることになる
506
- const [name, modifiers] = filterName.split(MODIFIER_SEPARATOR);
507
- raiseError(filterIOType === "input"
508
- ? `[wcs/binding-syntax] "${filterName}" is not a filter name: a modifier list ` +
509
- `"${MODIFIER_SEPARATOR}${modifiers}" comes before the input filters, not inside one — write ` +
510
- `"<property>${MODIFIER_SEPARATOR}${modifiers}|${name}".${LINT_HINT}`
511
- : `[wcs/binding-syntax] "${filterName}" is not a filter name: "${MODIFIER_SEPARATOR}" cannot appear ` +
512
- `in one. Modifiers belong on the left side of the binding, before the ":" — write "${name}" ` +
513
- `here.${LINT_HINT}`);
514
- }
515
- if (openParenIndex === -1) {
516
- // no arguments
517
- return { filterName, args: [], literals: [] };
518
- }
519
- const argsText = filterText.substring(openParenIndex + 1, closeParenIndex);
520
- return { filterName, ...parseFilterArgsWithLiterals(argsText) };
521
- });
522
- }
523
-
524
- const cacheFilterInfos$1 = new Map();
525
- /** tooling 専用(parser.ts の clearParserCaches からのみ呼ぶ)。 */
526
- function clearPropPartCacheForTooling() {
527
- cacheFilterInfos$1.clear();
528
- }
529
- // format: propName#moodifier1,modifier2
530
- // propName-format: path.to.property (e.g., textContent, style.color, not include :)
531
- // special path:
532
- // 'attr.attributeName' for attributes (e.g., attr.href, attr.data-id)
533
- // 'style.propertyName' for style properties (e.g., style.backgroundColor, style.fontSize)
534
- // 'class.className' for class names (e.g., class.active, class.hidden)
535
- // 'onclick', 'onchange' etc. for event listeners
536
- function parsePropPart(propPart) {
537
- const pos = indexOfOutsideQuotes(propPart, FILTER_SEPARATOR);
538
- let propText = '';
539
- let filterTexts = [];
540
- let filtersText = '';
541
- let filters = [];
542
- if (pos !== -1) {
543
- propText = propPart.slice(0, pos).trim();
544
- filtersText = propPart.slice(pos + 1).trim();
545
- if (cacheFilterInfos$1.has(filtersText)) {
546
- filters = cacheFilterInfos$1.get(filtersText);
547
- }
548
- else {
549
- filterTexts = splitOutsideQuotes(filtersText, FILTER_SEPARATOR).map(trimFn);
550
- // 診断に埋める原文は**左辺の全文**。`|` より後ろだけを渡すと `value|:` のように
551
- // 末尾が空の形で空文字になる(解析結果のキャッシュ鍵は従来どおり `filtersText`。
552
- // 落ちた解析はキャッシュに載らないので、原文を混ぜても鍵は汚れない)
553
- filters = parseFilters(filterTexts, "input", propPart);
554
- cacheFilterInfos$1.set(filtersText, filters);
555
- }
227
+ const source = sourceText ?? filterTextList.join("|");
228
+ return filterTextList.map((filterText) => {
229
+ const openParenIndex = indexOfOutsideQuotes(filterText, "(");
230
+ let closeParenIndex = lastIndexOfOutsideQuotes(filterText, ")");
231
+ if (closeParenIndex === -1) {
232
+ closeParenIndex = filterText.lastIndexOf(")");
556
233
  }
557
- else {
558
- propText = propPart.trim();
234
+ if (openParenIndex !== -1 && closeParenIndex === -1) {
235
+ raise(108 /* FilterUnclosed */, [filterText]);
559
236
  }
560
- // **不変条件**: ここから下の `split` は素で走らせてよい。`propText` は「引用符外の最初の `|`
561
- // より前」のスライスであり、引用符を含みうるのは入力フィルタの引数(`|` の後ろ)だけなので、
562
- // `#` も `,` も `.` も引用符の中に現れない。**修飾子の値に引用符を許す拡張(例
563
- // `value#fmt('a,b'): x`)を入れるなら、この 3 つも `splitOutsideQuotes` に替えること** —
564
- // 替え忘れると `structural/expandShorthandPaths.ts` で起きた「無言で 1 行も描画されない」
565
- // と同じ欠陥クラスが再発する。
566
- const modifierParts = propText.split(MODIFIER_SEPARATOR).map(trimFn);
567
- if (modifierParts.length > 2) {
568
- // 修飾子の並びは 1 つだけ(要件 B2)。`value#ro#wo` は以前 `ro` だけを残して黙って捨てていた
569
- raiseError(`[wcs/binding-syntax] "${propText}": a binding takes one modifier list after a single "${MODIFIER_SEPARATOR}" — write "${modifierParts[0]}${MODIFIER_SEPARATOR}${modifierParts.slice(1).join(",")}".${LINT_HINT}`);
237
+ if (closeParenIndex !== -1 && openParenIndex === -1) {
238
+ raise(109 /* FilterUnopened */, [filterText]);
570
239
  }
571
- const [propName, propModifiersText] = modifierParts;
572
- const propSegments = propName.split(DELIMITER).map(trimFn);
573
- // 明示のプロパティ形(`.name:` — 要件 B5 / D34)だけは先頭の空セグメントが正しい形。
574
- // それ以外で空のセグメントが残るのは書き間違い: 左辺が空(`": x"` / `"#ro: x"` / `"|trim: x"`)だと
575
- // `element[""] = value` の expando ができて完全に沈黙し、末尾が空(`"foo.: x"`)だと適用の段で
576
- // 素の TypeError になる。どちらも解析の段で名指しで落とす(`.: x` 等は下の D34 の検査が受け持つ)
577
- const isExplicitProperty = propSegments.length > 1 && propSegments[0] === '';
578
- if (!isExplicitProperty && (propName.length === 0 || propSegments.some((segment) => segment.length === 0))) {
579
- raiseError(`[wcs/binding-syntax] "${propPart}": the left side of a binding must name a property — ` +
580
- `write "<property>: <path>" (modifiers and input filters come after the name).${LINT_HINT}`);
240
+ if (closeParenIndex !== -1 && closeParenIndex < openParenIndex) {
241
+ raise(110 /* FilterParenOrder */, [filterText]);
581
242
  }
582
- const propModifiers = propModifiersText
583
- ? propModifiersText.split(',').map(trimFn)
584
- : [];
585
- return {
586
- propName,
587
- propSegments,
588
- propModifiers,
589
- inFilters: filters,
590
- };
243
+ if (closeParenIndex !== -1 && filterText.slice(closeParenIndex + 1).trim().length > 0) {
244
+ const trailing = filterText.slice(closeParenIndex + 1).trim();
245
+ raise(111 /* FilterTrailing */, [filterText, trailing]);
246
+ }
247
+ const filterName = (openParenIndex === -1 ? filterText : filterText.substring(0, openParenIndex)).trim();
248
+ if (filterName.length === 0) {
249
+ raise(112 /* FilterEmpty */, [source]);
250
+ }
251
+ if (filterName.includes(MODIFIER_SEPARATOR)) {
252
+ const [, modifiers] = filterName.split(MODIFIER_SEPARATOR);
253
+ raise(filterIOType === "input" ? 113 /* FilterNameHasModifiersInput */ : 114 /* FilterNameHasModifiersOutput */, [filterName, modifiers]);
254
+ }
255
+ if (openParenIndex === -1) {
256
+ return { filterName, args: [], literals: [] };
257
+ }
258
+ const argsText = filterText.substring(openParenIndex + 1, closeParenIndex);
259
+ return { filterName, ...parseFilterArgsWithLiterals(argsText) };
260
+ });
261
+ }
262
+ function splitFilters(part, filterIOType, cache2) {
263
+ const pos = indexOfOutsideQuotes(part, FILTER_SEPARATOR);
264
+ if (pos === -1) return [part.trim(), []];
265
+ const filtersText = part.slice(pos + 1).trim();
266
+ let filters = cache2.get(filtersText);
267
+ if (filters === void 0) {
268
+ filters = parseFilters(splitOutsideQuotes(filtersText, FILTER_SEPARATOR).map(trimFn), filterIOType, part);
269
+ cache2.set(filtersText, filters);
270
+ }
271
+ return [part.slice(0, pos).trim(), filters];
591
272
  }
592
273
 
593
- const cacheFilterInfos = new Map();
594
- /** tooling 専用(parser.ts の clearParserCaches からのみ呼ぶ)。 */
595
- function clearStatePartCacheForTooling() {
596
- cacheFilterInfos.clear();
274
+ // src/parser/parseStatePart.ts
275
+ var cacheFilterInfos = /* @__PURE__ */ new Map();
276
+ var clearStatePartCache = () => cacheFilterInfos.clear();
277
+ function checkPathLikeGetPathInfo(path) {
278
+ if (path.includes(RECURSION_WILDCARD)) {
279
+ recursionUnsupported(path);
280
+ }
281
+ const segmentCount = path.split(DELIMITER).length;
282
+ if (segmentCount > MAX_PATH_SEGMENTS) {
283
+ raise(117 /* TooManySegments */, [path, segmentCount]);
284
+ }
285
+ }
286
+ function recursionUnsupported(path) {
287
+ raise(1101 /* RecursionUnsupported */, [path]);
597
288
  }
598
- // format: statePath|filter|filter
599
- // statePath-format: path.to.property (e.g., user.name.first, users.*.name, users.0.name, not include @)
600
- // filters-format: filterName or filterName(arg1,arg2)
601
289
  function parseStatePart(statePart) {
602
- // 引用符の中の `|` はフィルタの区切りではない(要件 B1 — `join('|')`)
603
- const pos = indexOfOutsideQuotes(statePart, FILTER_SEPARATOR);
604
- let stateAndPath = '';
605
- let filterTexts = [];
606
- let filtersText = '';
607
- let filters = [];
608
- if (pos !== -1) {
609
- stateAndPath = statePart.slice(0, pos).trim();
610
- filtersText = statePart.slice(pos + 1).trim();
611
- if (cacheFilterInfos.has(filtersText)) {
612
- filters = cacheFilterInfos.get(filtersText);
613
- }
614
- else {
615
- filterTexts = splitOutsideQuotes(filtersText, FILTER_SEPARATOR).map(trimFn);
616
- // 診断に埋める原文は**右辺の全文**(`parsePropPart` と同じ理由 — `a|` が空文字になる)
617
- filters = parseFilters(filterTexts, "output", statePart);
618
- cacheFilterInfos.set(filtersText, filters);
619
- }
620
- }
621
- else {
622
- stateAndPath = statePart.trim();
623
- }
624
- if (stateAndPath.indexOf("@") !== -1) {
625
- // 名前次元は v2 で撤去(docs/state-mount-design.md D16 / §9)。パスは 1 本のツリー。
626
- raiseError(`"${stateAndPath}": the "@name" selector was removed in v2 — there is a single state tree. ` +
627
- `Mount the named state onto the tree (<wcs-state mount="...">) and read it by its path prefix instead.`);
628
- }
629
- const statePathName = stateAndPath;
630
- // 右辺も左辺(`parsePropPart`)と同じ規準で空セグメントを弾く(要件 B1)。放っておくと
631
- // `a.` / `a..b` は診断ゼロで `["a","",""]` のまま intern され、`textContent:` /
632
- // `textContent: |uc` は適用の段で「`Path ""` が無い」というパス名が空の診断になる。
633
- //
634
- // **判定の前にループ相対の短縮形を正規化する。** `.` で始まる右辺は `for` 行の相対パスで
635
- // (`structural/expandShorthandPaths.ts` / `bindTextParser/expandSpread.ts` が生成し、正本の
636
- // パーサは展開前の原文も解析する)、先頭の空セグメント 1 つは正当。とくに **`.` 単独は
637
- // 「行そのもの」を指す正規の書き方**で、README と `examples/recursive-tree/index.html` の
638
- // `state: .` がそれ — 「先頭は許すが末尾は拒否」と素朴に書くと `"."` は
639
- // `["", ""]` = 先頭かつ末尾なので落ちる。取り除いた残りが空でも通すこと
640
- const isLoopRelative = statePathName.startsWith(DELIMITER);
641
- const body = isLoopRelative ? statePathName.slice(DELIMITER.length) : statePathName;
642
- const hasEmptySegment = body.length > 0 && body.split(DELIMITER).some((segment) => segment.length === 0);
643
- if (hasEmptySegment || (!isLoopRelative && body.length === 0)) {
644
- raiseError(`[wcs/binding-syntax] "${statePart}": the right side of a binding must name a state path — ` +
645
- `write "<property>: <path>" (a path segment cannot be empty; "." alone and a leading "." are ` +
646
- `the loop-relative shorthand).${LINT_HINT}`);
290
+ const [stateAndPath, filters] = splitFilters(statePart, "output", cacheFilterInfos);
291
+ if (stateAndPath.includes("@")) {
292
+ raise(32 /* SelectorRemoved */, [stateAndPath]);
293
+ }
294
+ const statePathName = stateAndPath;
295
+ const isLoopRelative = statePathName.startsWith(DELIMITER);
296
+ const body = isLoopRelative ? statePathName.slice(DELIMITER.length) : statePathName;
297
+ const hasEmptySegment = body.length > 0 && body.split(DELIMITER).some((segment) => segment.length === 0);
298
+ if (hasEmptySegment || !isLoopRelative && body.length === 0) {
299
+ raise(119 /* EmptySegment */, [statePart]);
300
+ }
301
+ checkPathLikeGetPathInfo(statePathName);
302
+ return {
303
+ statePathName,
304
+ outFilters: filters
305
+ };
306
+ }
307
+
308
+ // src/engine.ts
309
+ var MAX_DRAIN_PASSES = 32;
310
+ var MAX_RENDER_CHAIN = 100;
311
+
312
+ // src/diagnostics/messages.ts
313
+ var MOVED = ["enableMustache", "sameValueGuard", "enableDirectionalInitialSync"];
314
+ var element = (tag, at) => `<${tag}${at.map((a, i) => i % 2 ? `="${a}"` : ` ${a}`).join("")}>`;
315
+ var CSP_GUIDE = "https://github.com/wcstack/wcstack/blob/main/docs/csp.md";
316
+ var SENTENCES = {
317
+ [1 /* ScanRemoved */]: () => "$scan was removed (use $watch or $on)",
318
+ [2 /* GetterWithoutSetter */]: (p) => `"${p}" is a getter without a setter`,
319
+ [3 /* NoRow */]: (p) => `no row for "${p}"`,
320
+ [4 /* ParentNotObject */]: (p, parent) => `cannot write "${p}": its parent is ${parent}`,
321
+ [5 /* NotAMethod */]: (name) => `"${name}" is not a method`,
322
+ [6 /* GetAllNoCommonLevel */]: (p) => `$getAll("${p}"): no loop level in common with the context`,
323
+ [7 /* EqIndexNoRow */]: (p) => `$eqIndex("${p}") needs a list row scope.`,
324
+ [8 /* Readonly */]: () => "This state is readonly.",
325
+ [9 /* SetAllNeedsIndexes */]: (p) => `$setAll("${p}") needs indexes ([] for every match)`,
326
+ [10 /* SetAllSpreadLength */]: (p, n) => `$setAll("${p}", \u2026, { spread: true }) needs an array of ${n} values`,
327
+ [11 /* DrainNotSettled */]: () => `updates did not settle after ${MAX_DRAIN_PASSES} passes`,
328
+ [41 /* RenderChain */]: () => `render chain depth limit exceeded (${MAX_RENDER_CHAIN} drains that rendering itself started); bindings for this batch were not applied.`,
329
+ [12 /* BindingFailed */]: (type, p) => `binding "${type}: ${p}" failed to apply.`,
330
+ [13 /* LoadFailed */]: (src, status) => `failed to load "${src}": ${status}`,
331
+ [14 /* ElementFailed */]: () => "this <wcs-state> failed to initialize; create a new one",
332
+ [15 /* NotInitialized */]: () => "state is not initialized",
333
+ [16 /* NoScript */]: (id) => `no <script> with id "${id}"`,
334
+ [42 /* InlineBlocked */]: () => `The inline <script> of <wcs-state> was blocked by Content-Security-Policy. Inline state is evaluated through a blob: URL: give the page's nonce to the <script> that loads @wcstack/state, or allow blob: in script-src. Moving the state into an external file (src="./state.js") needs neither. See ${CSP_GUIDE}`,
335
+ [43 /* InlineFailed */]: (detail) => `Failed to evaluate the inline <script> of <wcs-state>: ${detail}. If this page sets a Content-Security-Policy, see ${CSP_GUIDE}`,
336
+ [17 /* TokenSubscriberThrew */]: (name) => `a subscriber of token "${name}" threw.`,
337
+ [18 /* TokenListNotArray */]: (key) => `${key} must be an array of strings.`,
338
+ [19 /* TokenEntryEmpty */]: (key) => `${key} entries must be non-empty strings.`,
339
+ [20 /* TokenEntryReserved */]: (key, name, reserved) => `${key} entry "${name}" conflicts with the reserved namespace name "${reserved}".`,
340
+ [21 /* TokenEntryDuplicated */]: (key, name) => `${key} entry "${name}" is duplicated.`,
341
+ [22 /* OnNotObject */]: () => "$on must be an object of handlers.",
342
+ [23 /* OnEntryUndeclared */]: (name) => `$on entry "${name}" is not declared in $eventTokens.`,
343
+ [24 /* OnEntryNotFunction */]: (name) => `$on entry "${name}" must be a function.`,
344
+ [25 /* NamingLimit */]: (limit) => `view-transition naming-limit (${limit}) reached.`,
345
+ [26 /* FilterOptionsRequired */]: (fn) => `filter ${fn} requires at least one option`,
346
+ [27 /* FilterOptionNotNumber */]: (fn) => `filter ${fn} requires a number as option`,
347
+ [28 /* FilterValueNotNumber */]: (fn) => `filter ${fn} requires a number value`,
348
+ [29 /* FilterValueNotDate */]: (fn) => `filter ${fn} requires a date value`,
349
+ [30 /* FilterValueNotArray */]: (fn) => `filter ${fn} requires an array value`,
350
+ [31 /* DirectionalSyncDisabled */]: () => "init=/sync= modifiers require enableDirectionalInitialSync.",
351
+ [33 /* ModifierUnknown */]: (key, modifier) => `Unknown binding modifier "${key}" in "${modifier}".`,
352
+ [34 /* ModifierTwice */]: (key) => `Binding modifier "${key}" may only be specified once.`,
353
+ [35 /* ModifierValue */]: (key, value) => `Invalid ${key} modifier value "${value}".`,
354
+ [36 /* EventInitNone */]: () => "Event bindings only allow init=none.",
355
+ [37 /* InitUnsupported */]: (type, init) => `Binding type "${type}" does not support init=${init}.`,
356
+ [38 /* MemberUndeclared */]: (name) => `Property "${name}" is not declared by wcBindable.`,
357
+ [39 /* InitIncompatible */]: (init, name) => `init=${init} is incompatible with wcBindable member "${name}".`,
358
+ [40 /* SyncConnectNeedsOutput */]: (name) => `sync=connect requires observable property "${name}".`,
359
+ [44 /* OptionInvalid */]: (where, key) => `${where}: "${key}" is not one of its options, or not of the option's type.${where === "bootstrapState" && MOVED.includes(key) ? " 4.0 moved it to the state's $behavior." : ""}`,
360
+ [45 /* BehaviorChanged */]: () => "a re-set state may not change $behavior: create the element again.",
361
+ [47 /* SecondRoot */]: () => `a second <wcs-state> on the same root: there is one state tree per root \u2014 graft a subtree with <wcs-state mount="path"> (v1's name="\u2026" is gone: read the mounted state by its path).`,
362
+ [48 /* LocaleInvalid */]: (l) => `the locale "${l}" (<html lang> or bootstrapState's locale) is not a language tag Intl takes (en-US, not en_US): the locale filters use "en".`,
363
+ [46 /* FeaturesNotArray */]: () => '$features must be an array of add-on names (["temporal", "formats"]).',
364
+ [49 /* InitFailed */]: (tag, ...at) => `${element(tag, at)} failed to initialize.`,
365
+ [50 /* ConnectedFailed */]: (tag, ...at) => `${element(tag, at)} $connectedCallback failed.`,
366
+ [101 /* BindTextNoColon */]: (t) => `Invalid bindText: "${t}". Missing ':' separator between propPart and statePart.`,
367
+ [102 /* StructuralTakesNoModifiers */]: (t, keyword) => `"${t}": "${keyword}" takes no modifiers or filters on its left side \u2014 write "${keyword}:".`,
368
+ [103 /* ElseTakesNoValue */]: (t) => `"${t}": "else" takes no value \u2014 write "else:".`,
369
+ [104 /* SpreadNoPath */]: (t) => `Invalid spread binding "${t}": spread target path is required.`,
370
+ [105 /* SpreadNoFilters */]: (t) => `Invalid spread binding "${t}": filters are not allowed on spread targets.`,
371
+ [106 /* LeadingDotNamespace */]: (prop) => `"${prop}": a leading "." binds an element property by name \u2014 write a non-empty property that is not a namespace (class, attr, style, command, eventToken, state).`,
372
+ [107 /* UnterminatedQuote */]: (quote, args) => `unterminated ${quote} quote in the filter arguments "(${args})". Close the quote.`,
373
+ [108 /* FilterUnclosed */]: (f) => `Invalid filter format: missing closing parenthesis in "${f}".`,
374
+ [109 /* FilterUnopened */]: (f) => `Invalid filter format: missing opening parenthesis in "${f}".`,
375
+ [110 /* FilterParenOrder */]: (f) => `Invalid filter format: ")" comes before "(" in "${f}".`,
376
+ [111 /* FilterTrailing */]: (f, trailing) => `"${f}": unexpected "${trailing}" after the filter's closing ")" \u2014 separate filters with "|" (write "${f.slice(0, f.lastIndexOf(")") + 1)}|${trailing}").`,
377
+ [112 /* FilterEmpty */]: (source) => `an empty filter in "${source}" \u2014 remove the extra "|" or name the filter.`,
378
+ [113 /* FilterNameHasModifiersInput */]: (name, mods) => `"${name}" is not a filter name: a modifier list "${MODIFIER_SEPARATOR}${mods}" comes before the input filters, not inside one`,
379
+ [114 /* FilterNameHasModifiersOutput */]: (name) => `"${name}" is not a filter name: "${MODIFIER_SEPARATOR}" cannot appear in one.`,
380
+ [115 /* OneModifierList */]: (prop) => `"${prop}": a binding takes one modifier list after a single "#"`,
381
+ [116 /* NoPropertyName */]: (prop) => `"${prop}": the left side of a binding must name a property \u2014 write "<property>: <path>" (modifiers and input filters come after the name).`,
382
+ [117 /* TooManySegments */]: (p, n) => `"${p}" has ${n} path segments \u2014 the limit is ${MAX_PATH_SEGMENTS}.`,
383
+ [32 /* SelectorRemoved */]: (t) => `"${t}": the "@name" selector was removed in v2 \u2014 there is a single state tree. Mount the named state onto the tree (<wcs-state mount="...">) and read it by its path prefix instead.`,
384
+ [120 /* UnsafeSegment */]: (p) => `"${p}": a state path cannot go through "__proto__" or "prototype" (it would reach every object's prototype).`,
385
+ [121 /* ForNoFilters */]: (t) => `"${t}": "for:" takes no filters \u2014 a row is "<path>.<index>", so the rows of a filtered list would name other elements. Declare a getter that returns the filtered list and loop over it ("for: <getter>").`,
386
+ [119 /* EmptySegment */]: (t) => `"${t}": the right side of a binding must name a state path \u2014 write "<property>: <path>" (a path segment cannot be empty; "." alone and a leading "." are the loop-relative shorthand).`,
387
+ [201 /* StructuralNotSingle */]: (t) => `Invalid bindText: "${t}". 'if', 'elseif', 'else', and 'for' bindings must be single binding.`,
388
+ [202 /* ElseWithoutIf */]: (type) => `"${type}:" must follow an "if:" template`,
389
+ [204 /* TemplateHandedOver */]: (type) => `a "${type}:" template at the top of inserted content was not rendered, as it would render beside itself out of the inserter's reach: wrap it in an element.`,
390
+ [203 /* OuterInTemplate */]: (name) => `"${name}:" replaces its element, so it cannot be used inside a "for" / "if" template (a row or branch keeps its nodes by position): bind innerHTML: on a wrapper element instead.`,
391
+ [301 /* PathMissing */]: (p) => `Path "${p}" does not exist on the state tree.`,
392
+ [401 /* ClassNeedsBoolean */]: (name, type) => `class.${name} needs a boolean, got ${type}.`,
393
+ [501 /* FilterUnknown */]: (name) => `filter not found: ${name}.`,
394
+ [601 /* FilterTooFewArgs */]: (name, min, given) => `filter "${name}" requires at least ${min} argument(s) (${given} given).`,
395
+ [602 /* FilterTooManyArgs */]: (name, max, given) => `filter "${name}" accepts at most ${max} argument(s) (${given} given).`,
396
+ [701 /* GetterCycle */]: (p) => `"${p}" depends on itself`,
397
+ [801 /* GetterDepth */]: (p) => `"${p}"`,
398
+ [901 /* IndexArityExact */]: (api, p, depth, n) => `${api}("${p}") takes ${depth} index(es), got ${n}.`,
399
+ [902 /* IndexArityAtMost */]: (api, p, depth, n) => `${api}("${p}") takes at most ${depth} index(es), got ${n}.`,
400
+ [1001 /* IndexParamRange */]: (key) => `"${key}": list index parameters run from $1 to $${MAX_INDEX_PARAM}.`,
401
+ [1101 /* RecursionUnsupported */]: (p) => `"${p}" uses "${RECURSION_WILDCARD}", which is not accepted here.`,
402
+ [1201 /* CommandRightSide */]: (prop, p) => `"${prop}: ${p}": the right-hand side must be $command.<name>`,
403
+ [1202 /* NoBindable */]: (tag, what) => `<${tag}> declares no static wcBindable (${what}).`,
404
+ [1203 /* NoCommand */]: (tag, method) => `<${tag}> declares no command "${method}".`,
405
+ [1204 /* NoProperty */]: (tag, prop) => `<${tag}> declares no property "${prop}".`,
406
+ [1301 /* EventTokenUndeclared */]: (name) => `eventToken "${name}" is not declared in $eventTokens.`,
407
+ [1302 /* CommandTokenUndeclared */]: (name) => `"$command.${name}" is not declared in $commandTokens.`,
408
+ [1401 /* WildcardNoLoop */]: (p, depth, n = 0) => `"${p}" needs ${depth} enclosing loop level(s); the scope provides ${n}.`,
409
+ [1402 /* WildcardRelative */]: (p) => `"${p}" is relative: it needs an enclosing "for" template`,
410
+ [1403 /* WildcardOtherList */]: (p, over, loop) => `"${p}" ranges over the rows of "${over}", but the enclosing "for" template at that level renders "${loop}".`,
411
+ [1501 /* SpreadNoBindable */]: (tag, what) => `<${tag}> declares no static wcBindable (${what}).`,
412
+ [1601 /* DeclarationRemoved */]: (old, name) => `${old} was removed: write ${name}.`,
413
+ [1701 /* ApiRemoved */]: (old, name) => `${old} was removed: write ${name}.`
414
+ };
415
+
416
+ // src/diagnostics/explain.ts
417
+ function render(id, args) {
418
+ const sentence = SENTENCES[id];
419
+ return sentence === void 0 ? `${codeOf(id)}#${id} ${args.join(" ")}` : codeOf(id) + sentence(...args);
420
+ }
421
+ var LINT_CODES = /* @__PURE__ */ new Set([
422
+ "binding-syntax",
423
+ "template-syntax",
424
+ "filter-unknown",
425
+ "filter-arity",
426
+ "index-arity",
427
+ "wildcard-rank",
428
+ "token-undeclared",
429
+ "binding-path-missing",
430
+ "index-param-range"
431
+ ]);
432
+ var GUIDES = [
433
+ [/no loop level in common with the context/, "; pass indexes ([] for all)."],
434
+ [/"([^"#]+)#([^"]*)" is not a filter name: a modifier list .* comes before the input filters/, ' \u2014 write "<property>#$2|$1".'],
435
+ [/\[wcs\/recursion-unsupported\]/, " It is only meaningful in a $recursion declaration, in a recursive getter key, and in the path argument of $getAll / $setAll \u2014 and only when the state declares a $recursion anchor."],
436
+ [/must be single binding/, ' Put the structural binding alone in its own data-wcs (e.g. <template data-wcs="for: items">).'],
437
+ [/\[wcs\/wildcard-rank\] .* needs \d+ enclosing/, ' Wrap it in that many "for" templates, or use $resolve(path, indexes) to name the row explicitly.'],
438
+ [/\[wcs\/wildcard-rank\] .* ranges over the rows of/, ' A "*" in a binding is the row of the loop around it: read a row of another list in a getter, with $resolve(path, indexes).'],
439
+ [/\[wcs\/binding-type-expectation\] class\.([^ ]+)/, ' Write "class.$1: path|truthy" to toggle on truthiness.'],
440
+ [/path segments — the limit/, " Every prefix of a path is interned, so the cost grows with the square of the depth."],
441
+ [/\[wcs\/index-arity\] \$resolve/, ' $resolve takes one index per "*"; $getAll / $setAll take at most that many (fewer expands the rest).'],
442
+ [/"([^"#]+)#[^"]*" is not a filter name: "#" cannot appear in one/, ' Modifiers belong on the left side of the binding, before the ":" \u2014 write "$1" here.']
443
+ ];
444
+ var RENAMED_FILTERS = {
445
+ inc: "add",
446
+ dec: "sub",
447
+ fix: "toFixed",
448
+ uc: "upper",
449
+ lc: "lower",
450
+ cap: "capitalize",
451
+ rep: "repeat",
452
+ rev: "reverse",
453
+ pad: "padStart",
454
+ null: "nullIfEmpty"
455
+ };
456
+ function removedFilter(name) {
457
+ if (name === "substr") return ' "substr" was removed in 4.0 \u2014 write slice(start, start + length): slice takes the end index, not a length.';
458
+ const to = Object.hasOwn(RENAMED_FILTERS, name) ? RENAMED_FILTERS[name] : null;
459
+ return to === null ? null : ` "${name}" was renamed "${to}" in 3.2 and removed in 4.0 \u2014 write "${to}".`;
460
+ }
461
+ function explain(message, subject, candidates) {
462
+ const removed = subject !== void 0 && message.includes("[wcs/filter-unknown]") ? removedFilter(subject) : null;
463
+ let out = removed ?? (subject !== void 0 && candidates !== void 0 ? didYouMean(subject, candidates) : "");
464
+ for (const [re, text2] of GUIDES) {
465
+ const m = re.exec(message);
466
+ if (m !== null) out += text2.replace(/\$(\d)/g, (_, i) => m[Number(i)]);
467
+ }
468
+ const mods = /"([^"#]+)#([^"]+)": a binding takes one modifier list/.exec(message);
469
+ if (mods !== null) out += ` \u2014 write "${mods[1]}#${mods[2].split("#").join(",")}".`;
470
+ if (message.includes("[wcs/filter-unknown]")) {
471
+ if (message.includes("formats add-on")) out += ` On a split auto page: features="formats" on the root, or "$features": ["formats"].`;
472
+ else if (!FORMATS_FILTER_NAMES.some(hasFilter)) {
473
+ out += ` No formatting filters are installed \u2014 add the formats add-on: installFeatures([formats]) from "@wcstack/state/features/formats" (a split auto page: features="formats", or "$features").`;
647
474
  }
648
- const pathInfo = getPathInfo(statePathName);
649
- return {
650
- statePathName,
651
- statePathInfo: pathInfo,
652
- outFilters: filters,
653
- };
475
+ }
476
+ const code = /\[wcs\/([\w-]+)\]/.exec(message);
477
+ if (code !== null && LINT_CODES.has(code[1]) && !/ path segments — the limit is |inserted content was not rendered|formats add-on/.test(message)) out += LINT_HINT;
478
+ return out;
654
479
  }
655
480
 
656
- // format: propPart:statePart; propPart:statePart; ...
657
- // special-propPart:
658
- // if: statePart (single binding for conditional rendering)
659
- // else: (single binding for conditional rendering, and statePart is ignored)
660
- // elseif: statePart only (single binding for conditional rendering)
661
- // for: statePart only (single binding for loop rendering)
662
- // onclick: statePart, onchange: statePart etc. (event listeners)
663
- // ...: statePart (spread — expand wcBindable properties+inputs of target object)
664
- /** 左辺に修飾子も入力フィルタも取らない束縛(構造ディレクティブと spread)— 付いていれば拒否する(要件 B4) */
665
- const KEYWORDS_WITHOUT_MODIFIERS = new Set([ELSE_KEYWORD, 'if', 'elseif', 'for', SPREAD_PROP]);
666
- /**
667
- * 明示のプロパティ形(`.name:`)の先頭に置けない語 — 名前空間として読まれる語(要件 B5)。
668
- * `state`(`VOLUME_INJECTION_PROP`)も含む: `<wcs-state mount>` 上の左辺 `state.<key>:` は
669
- * 注入の宣言(要件 B14③)なので、`.state.taxRate:` はプロパティか注入か曖昧になる。
670
- */
671
- const EXPLICIT_PROPERTY_REJECTED_HEADS = new Set([
672
- CLASS_NAMESPACE, ATTR_NAMESPACE, STYLE_NAMESPACE, COMMAND_NAMESPACE, EVENT_TOKEN_NAMESPACE,
673
- VOLUME_INJECTION_PROP,
481
+ // src/parser/parsePropPart.ts
482
+ var cacheFilterInfos2 = /* @__PURE__ */ new Map();
483
+ var clearPropPartCache = () => cacheFilterInfos2.clear();
484
+ function parsePropPart(propPart) {
485
+ const [propText, filters] = splitFilters(propPart, "input", cacheFilterInfos2);
486
+ const modifierParts = propText.split(MODIFIER_SEPARATOR).map(trimFn);
487
+ if (modifierParts.length > 2) {
488
+ raise(115 /* OneModifierList */, [propText]);
489
+ }
490
+ const [propName, propModifiersText] = modifierParts;
491
+ const propSegments = propName.split(DELIMITER).map(trimFn);
492
+ const isExplicitProperty = propSegments.length > 1 && propSegments[0] === "";
493
+ if (!isExplicitProperty && (propName.length === 0 || propSegments.some((segment) => segment.length === 0))) {
494
+ raise(116 /* NoPropertyName */, [propPart]);
495
+ }
496
+ const propModifiers = propModifiersText ? propModifiersText.split(",").map(trimFn) : [];
497
+ return {
498
+ propName,
499
+ propSegments,
500
+ propModifiers,
501
+ inFilters: filters
502
+ };
503
+ }
504
+
505
+ // src/parser/types.ts
506
+ var STRUCTURAL_BINDING_TYPE_SET = /* @__PURE__ */ new Set([
507
+ "if",
508
+ "elseif",
509
+ "else",
510
+ "for"
511
+ ]);
512
+
513
+ // src/parser/parseBindTextsForElement.ts
514
+ var KEYWORDS_WITHOUT_MODIFIERS = /* @__PURE__ */ new Set([ELSE_KEYWORD, "if", "elseif", "for", SPREAD_PROP]);
515
+ var EXPLICIT_PROPERTY_REJECTED_HEADS = /* @__PURE__ */ new Set([
516
+ CLASS_NAMESPACE,
517
+ ATTR_NAMESPACE,
518
+ STYLE_NAMESPACE,
519
+ COMMAND_NAMESPACE,
520
+ EVENT_TOKEN_NAMESPACE,
521
+ VOLUME_INJECTION_PROP
674
522
  ]);
675
- /**
676
- * `data-wcs` の値をバインディングごとに区切る(前後の空白は残す — tooling が位置を数えられるように)。
677
- * 引用符の中の `;` は区切りではない(要件 B1 — `join(';')`)。ランタイムと tooling(`@wcstack/state/parser`)で共有する
678
- */
679
523
  function splitBindTexts(bindText) {
680
- return splitOutsideQuotes(bindText, BINDING_SEPARATOR);
524
+ return splitOutsideQuotes(bindText, BINDING_SEPARATOR);
681
525
  }
682
526
  function parseBindTextsForElement(bindText) {
683
- const [...bindTexts] = splitBindTexts(bindText).map(trimFn).filter(s => s.length > 0);
684
- const results = bindTexts.map((bindText) => {
685
- // 左辺と右辺の区切りも引用符の外だけで探す(要件 B1)。`value|defaults(':'): path` の
686
- // 引数の中の `:` を区切りとして拾っていた
687
- const separatorIndex = indexOfOutsideQuotes(bindText, PROP_VALUE_SEPARATOR);
688
- if (separatorIndex === -1) {
689
- raiseError(`[wcs/binding-syntax] Invalid bindText: "${bindText}". Missing ':' separator between propPart and statePart.${LINT_HINT}`);
690
- }
691
- const propPart = bindText.slice(0, separatorIndex).trim();
692
- const statePart = bindText.slice(separatorIndex + 1).trim();
693
- // 種別は修飾子・入力フィルタより前の名前で決める(要件 B4)。以前は左辺全体との完全一致で
694
- // 判定していたので、`radio#ro:` が汎用プロパティに落ちていた。
695
- // **不変条件**: ここは素の `split` でよい — 取り出すのは最初の区切りより前の**先頭**片で、
696
- // 引用符を含みうるのは入力フィルタの引数(引用符外の最初の `|` より後ろ)だけなので、
697
- // `#` も `|` も引用符の中では出会わない。**修飾子の値に引用符を許す拡張を入れるなら
698
- // `indexOfOutsideQuotes` に替えること**(`parsePropPart.ts` の同じ注記と対)。
699
- const keyword = propPart.split(MODIFIER_SEPARATOR)[0].split(FILTER_SEPARATOR)[0].trim();
700
- if (keyword !== propPart && KEYWORDS_WITHOUT_MODIFIERS.has(keyword)) {
701
- raiseError(`[wcs/binding-syntax] "${bindText}": "${keyword}" takes no modifiers or filters on its left side — write "${keyword}:".${LINT_HINT}`);
702
- }
703
- if (propPart === ELSE_KEYWORD) {
704
- if (statePart.length > 0) {
705
- // else は値を取らない(要件 B2)。以前は右辺を黙って捨てていた
706
- raiseError(`[wcs/binding-syntax] "${bindText}": "else" takes no value — write "else:".${LINT_HINT}`);
707
- }
708
- const pathInfo = getPathInfo('#else');
709
- return {
710
- propName: ELSE_KEYWORD,
711
- propSegments: [ELSE_KEYWORD],
712
- propModifiers: [],
713
- statePathName: '#else',
714
- statePathInfo: pathInfo,
715
- inFilters: [],
716
- outFilters: [],
717
- bindingType: 'else',
718
- };
719
- }
720
- else if (propPart === SPREAD_PROP) {
721
- // 空の右辺は spread 専用の語彙で先に落とす。`parseStatePart` の一般の空パス診断
722
- // (「the right side of a binding must name a state path」)より、ここでは
723
- // 「spread target path is required」のほうが直し方を指している
724
- if (statePart.length === 0) {
725
- raiseError(`[wcs/binding-syntax] Invalid spread binding "${bindText}": spread target path is required.${LINT_HINT}`);
726
- }
727
- const stateResult = parseStatePart(statePart);
728
- if (stateResult.outFilters.length > 0) {
729
- raiseError(`[wcs/binding-syntax] Invalid spread binding "${bindText}": filters are not allowed on spread targets.${LINT_HINT}`);
730
- }
731
- return {
732
- propName: SPREAD_PROP,
733
- propSegments: [SPREAD_PROP],
734
- propModifiers: [],
735
- inFilters: [],
736
- ...stateResult,
737
- bindingType: 'spread',
738
- };
739
- }
740
- else if (propPart === 'if'
741
- || propPart === 'elseif'
742
- || propPart === 'for') {
743
- const stateResult = parseStatePart(statePart);
744
- return {
745
- propName: propPart,
746
- propSegments: [propPart],
747
- propModifiers: [],
748
- inFilters: [],
749
- ...stateResult,
750
- bindingType: propPart,
751
- };
752
- }
753
- else if (keyword === 'radio' || keyword === 'checkbox') {
754
- // 修飾子(`#ro`・`#onchange` …)と入力フィルタは radio / checkbox のハンドラが読む(要件 B4)
755
- const stateResult = parseStatePart(statePart);
756
- const propResult = parsePropPart(propPart);
757
- return {
758
- ...propResult,
759
- ...stateResult,
760
- bindingType: keyword,
761
- };
762
- }
763
- else {
764
- const stateResult = parseStatePart(statePart);
765
- const propResult = parsePropPart(propPart);
766
- // 左辺の先頭ドット(`.online:`)は明示のプロパティ形(要件 B5・3.x 計画 D34)。ドットの無い形と
767
- // 同じ束縛だが、`on` で始まってもイベントにはならない(`online:` は "line" イベントを待つ)。
768
- // 名前空間の語(`.class.x` / `.command.x` …)はプロパティか名前空間か曖昧なので受けない
769
- if (propResult.propSegments[0] === '' && propResult.propSegments.length > 1) {
770
- const propSegments = propResult.propSegments.slice(1);
771
- if (propSegments.includes('') || EXPLICIT_PROPERTY_REJECTED_HEADS.has(propSegments[0])) {
772
- raiseError(`[wcs/binding-syntax] "${propPart}": a leading "." binds an element property by name — ` +
773
- `write a non-empty property that is not a namespace (${[...EXPLICIT_PROPERTY_REJECTED_HEADS].join(", ")}).${LINT_HINT}`);
774
- }
775
- return {
776
- ...propResult,
777
- propName: propSegments.join(DELIMITER),
778
- propSegments,
779
- ...stateResult,
780
- bindingType: 'prop',
781
- };
782
- }
783
- // eventToken.<prop>: <name> は要素 dispatch を state へ流す pub/sub 配線。
784
- // 値適用ではないため bindingType 'event' として listener attach 経路に乗せる。
785
- if (propResult.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
786
- return {
787
- ...propResult,
788
- ...stateResult,
789
- bindingType: 'event',
790
- };
791
- }
792
- if (propResult.propSegments[0].startsWith(EVENT_PROP_PREFIX)) {
793
- return {
794
- ...propResult,
795
- ...stateResult,
796
- bindingType: 'event',
797
- };
798
- }
799
- else {
800
- return {
801
- ...propResult,
802
- ...stateResult,
803
- bindingType: 'prop',
804
- };
805
- }
806
- }
807
- });
808
- // check for sigle binding for 'if', 'elseif', 'else', 'for'
809
- if (results.length > 1) {
810
- const isIncludeSingleBinding = results.some(r => STRUCTURAL_BINDING_TYPE_SET.has(r.bindingType));
811
- if (isIncludeSingleBinding) {
812
- // lint 側の単独バインディング検査(bindingValidator の structuralMustBeSingle)が
813
- // 同じケースを検出するため誘導を付ける(三面同語彙)。
814
- raiseError(`[wcs/template-syntax] Invalid bindText: "${bindText}". 'if', 'elseif', 'else', and 'for' bindings must be single binding. Put the structural binding alone in its own data-wcs (e.g. <template data-wcs="for: items">).${LINT_HINT}`);
815
- }
527
+ const bindTexts = splitBindTexts(bindText).map(trimFn).filter((s) => s.length > 0);
528
+ const results = bindTexts.map((bindText2) => {
529
+ const separatorIndex = indexOfOutsideQuotes(bindText2, PROP_VALUE_SEPARATOR);
530
+ if (separatorIndex === -1) {
531
+ raise(101 /* BindTextNoColon */, [bindText2]);
816
532
  }
817
- return results;
818
- }
819
-
820
- function parseBindTextForEmbeddedNode(bindText) {
821
- const stateResult = parseStatePart(bindText);
822
- return {
823
- propName: 'textContent',
824
- propSegments: ['textContent'],
533
+ const propPart = bindText2.slice(0, separatorIndex).trim();
534
+ const statePart = bindText2.slice(separatorIndex + 1).trim();
535
+ const keyword = propPart.split(MODIFIER_SEPARATOR)[0].split(FILTER_SEPARATOR)[0].trim();
536
+ if (KEYWORDS_WITHOUT_MODIFIERS.has(keyword)) {
537
+ if (keyword !== propPart) {
538
+ raise(102 /* StructuralTakesNoModifiers */, [bindText2, keyword]);
539
+ }
540
+ let stateResult2;
541
+ if (keyword === ELSE_KEYWORD) {
542
+ if (statePart.length > 0) {
543
+ raise(103 /* ElseTakesNoValue */, [bindText2]);
544
+ }
545
+ stateResult2 = { statePathName: "#else", outFilters: [] };
546
+ } else if (keyword === SPREAD_PROP) {
547
+ if (statePart.length === 0) {
548
+ raise(104 /* SpreadNoPath */, [bindText2]);
549
+ }
550
+ stateResult2 = parseStatePart(statePart);
551
+ if (stateResult2.outFilters.length > 0) {
552
+ raise(105 /* SpreadNoFilters */, [bindText2]);
553
+ }
554
+ } else {
555
+ stateResult2 = parseStatePart(statePart);
556
+ if (keyword === "for" && stateResult2.outFilters.length > 0) {
557
+ raise(121 /* ForNoFilters */, [bindText2]);
558
+ }
559
+ }
560
+ return {
561
+ propName: keyword,
562
+ propSegments: [keyword],
825
563
  propModifiers: [],
826
564
  inFilters: [],
565
+ ...stateResult2,
566
+ bindingType: keyword === SPREAD_PROP ? "spread" : keyword
567
+ };
568
+ }
569
+ const stateResult = parseStatePart(statePart);
570
+ const propResult = parsePropPart(propPart);
571
+ const [head] = propResult.propSegments;
572
+ if (head === "" && propResult.propSegments.length > 1) {
573
+ const propSegments = propResult.propSegments.slice(1);
574
+ if (propSegments.includes("") || EXPLICIT_PROPERTY_REJECTED_HEADS.has(propSegments[0])) {
575
+ raise(106 /* LeadingDotNamespace */, [propPart]);
576
+ }
577
+ return {
578
+ ...propResult,
579
+ propName: propSegments.join(DELIMITER),
580
+ propSegments,
827
581
  ...stateResult,
828
- bindingType: 'text',
582
+ bindingType: "prop"
583
+ };
584
+ }
585
+ return {
586
+ ...propResult,
587
+ ...stateResult,
588
+ // 修飾子(`#ro`・`#onchange` …)と入力フィルタは radio / checkbox のハンドラが読む(要件 B4)。
589
+ // eventToken.<prop>: <name> は要素 dispatch を state へ流す pub/sub 配線。
590
+ // 値適用ではないため bindingType 'event' として listener attach 経路に乗せる。
591
+ bindingType: keyword === "radio" || keyword === "checkbox" ? keyword : head === EVENT_TOKEN_NAMESPACE || head.startsWith(EVENT_PROP_PREFIX) ? "event" : "prop"
829
592
  };
593
+ });
594
+ if (results.length > 1) {
595
+ const isIncludeSingleBinding = results.some((r) => STRUCTURAL_BINDING_TYPE_SET.has(r.bindingType));
596
+ if (isIncludeSingleBinding) {
597
+ raise(201 /* StructuralNotSingle */, [bindText]);
598
+ }
599
+ }
600
+ return results;
830
601
  }
831
602
 
832
- /**
833
- * parser.ts — `data-wcs` バインディング構文の正本パーサを tooling 向けに公開する
834
- * サブパスエントリ(`@wcstack/state/parser`)。
835
- *
836
- * `./manifest` と同じ「実装が唯一の正本」パターン(docs/static-wiring-dx-design.md D2)。
837
- * vscode-wcs の正規表現パーサ・devtools の declaredScan 簡易パーサという複製実装を
838
- * 段階的にこの正本へ寄せるための土台。
839
- *
840
- * 契約:
841
- * - DOM 非依存・純関数(bindText 文字列 → ParseBindTextResult[])。Node でそのまま動く
842
- * (__tests__/parser.test.ts が node 環境で検証する)。
843
- * - **位置情報は持たず、不正構文は raiseError で throw する**。エラー耐性と診断 range の
844
- * 生成は消費側(vscode-wcs の positional ラッパー)の責務(同 D3)— ランタイムの
845
- * サイズと責務をここで増やさない。
846
- * - `getPathInfo` はパス文字列の解析済みビュー(セグメント・ワイルドカード位置・親パス
847
- * チェーン)を返す純関数。静的依存グラフの親チェーン展開はこの情報から機械的に再現できる。
848
- * 同一パス → 同一インスタンスの保証は**このエントリのモジュールインスタンス内**でのみ
849
- * 成立する(`.` エントリは別バンドル=別キャッシュ。ランタイムの PathInfo と identity
850
- * 比較してはならない)。キャッシュは無制限(evict なし)— 言語サーバー等の長時間
851
- * プロセスではメモリが増え続ける点に留意(断ち方は `clearPathInfoCacheForTooling`)。
852
- * **増え方はパス種数への単調比例ではない**: `PathInfo` は自分の全ての接頭辞を intern
853
- * するので、1 本のパスが持ち込む量はその深さの 2 乗に比例する。深さは
854
- * `MAX_PATH_SEGMENTS` で頭打ちになる(超えたパスは `[wcs/binding-syntax]` で拒否)。
855
- * - `ParseBindTextResult.uuid` はランタイム内部(構造テンプレートのハイドレーション台帳)
856
- * 用のフィールドで、このパーサの戻り値では常に undefined。
857
- *
858
- * 公開面は意図的に最小(公開=恒久契約)。`expandSpread` は live Element と
859
- * CustomElementRegistry を要するためここには含めない — ブラウザ内の消費者
860
- * (devtools の declared 正本化)は state 自身が pull API で答える。
861
- */
862
- /**
863
- * このエントリの内部キャッシュ(PathInfo intern・propPart/statePart のパース結果・フィルタ関数クロージャ)を全て捨てる。
864
- *
865
- * 言語サーバー等の**長時間プロセス専用**。編集中の中間パス(`user.n` 等)が
866
- * 無制限キャッシュに恒久 intern されてメモリが単調増加するため、ドキュメント
867
- * クローズ等の区切りで呼ぶ。クリア後の getPathInfo は同一パスに**新しい**
868
- * インスタンスを返す — 「同一パス → 同一参照」の保証はクリアを跨がない。
869
- * ランタイム(`.` エントリ)にはこの API は無く、呼ばれることもない。
870
- */
871
- function clearParserCaches() {
872
- clearPathInfoCacheForTooling();
873
- clearPropPartCacheForTooling();
874
- clearStatePartCacheForTooling();
875
- clearFilterFnCacheForTooling();
603
+ // src/parser/parseBindTextForEmbeddedNode.ts
604
+ function parseBindTextForEmbeddedNode(bindText) {
605
+ const stateResult = parseStatePart(bindText);
606
+ return {
607
+ propName: "textContent",
608
+ propSegments: ["textContent"],
609
+ propModifiers: [],
610
+ inFilters: [],
611
+ ...stateResult,
612
+ bindingType: "text"
613
+ };
876
614
  }
877
615
 
878
- export { clearParserCaches, getPathInfo, indexOfOutsideQuotes, parseBindTextForEmbeddedNode, parseBindTextsForElement, splitBindTexts, splitOutsideQuotes };
616
+ // src/public/pathInfo.ts
617
+ var cache = /* @__PURE__ */ new Map();
618
+ var ids = 0;
619
+ function clearPathInfoCache() {
620
+ cache.clear();
621
+ }
622
+ function getPathInfo(path) {
623
+ const known = cache.get(path);
624
+ if (known !== void 0) return known;
625
+ if (path.includes(RECURSION_WILDCARD)) raise(1101 /* RecursionUnsupported */, [path]);
626
+ const count = path.split(DELIMITER).length;
627
+ if (count > MAX_PATH_SEGMENTS) raise(117 /* TooManySegments */, [path, count]);
628
+ const info = Object.freeze(new PathInfo(path));
629
+ cache.set(path, info);
630
+ return info;
631
+ }
632
+ var PathInfo = class {
633
+ constructor(path) {
634
+ this.id = ++ids;
635
+ this.cumulativePaths = [];
636
+ this.cumulativePathInfos = [];
637
+ this.wildcardPaths = [];
638
+ this.indexByWildcardPath = {};
639
+ this.wildcardPathInfos = [];
640
+ this.wildcardParentPaths = [];
641
+ this.wildcardParentPathInfos = [];
642
+ this.wildcardPositions = [];
643
+ const info = (p) => p === path ? this : getPathInfo(p);
644
+ const segments = path.split(DELIMITER);
645
+ let current = "";
646
+ let prev = "";
647
+ for (let i = 0; i < segments.length; i++) {
648
+ current += segments[i];
649
+ if (segments[i] === WILDCARD) {
650
+ this.indexByWildcardPath[current] = this.wildcardPaths.length;
651
+ this.wildcardPaths.push(current);
652
+ this.wildcardPathInfos.push(info(current));
653
+ this.wildcardParentPaths.push(prev);
654
+ this.wildcardParentPathInfos.push(info(prev));
655
+ this.wildcardPositions.push(i);
656
+ }
657
+ this.cumulativePaths.push(current);
658
+ this.cumulativePathInfos.push(info(current));
659
+ prev = current;
660
+ current += DELIMITER;
661
+ }
662
+ const last = this.wildcardPaths.length > 0 ? this.wildcardPaths[this.wildcardPaths.length - 1] : null;
663
+ const parent = this.cumulativePaths.length > 1 ? this.cumulativePaths[this.cumulativePaths.length - 2] : null;
664
+ this.path = path;
665
+ this.segments = segments;
666
+ this.lastSegment = segments[segments.length - 1];
667
+ this.cumulativePathSet = new Set(this.cumulativePaths);
668
+ this.cumulativePathInfoSet = new Set(this.cumulativePathInfos);
669
+ this.wildcardPathSet = new Set(this.wildcardPaths);
670
+ this.wildcardPathInfoSet = new Set(this.wildcardPathInfos);
671
+ this.wildcardParentPathSet = new Set(this.wildcardParentPaths);
672
+ this.wildcardParentPathInfoSet = new Set(this.wildcardParentPathInfos);
673
+ this.lastWildcardPath = last;
674
+ this.lastWildcardInfo = last !== null ? info(last) : null;
675
+ this.parentPath = parent;
676
+ this.parentPathInfo = parent !== null ? info(parent) : null;
677
+ this.wildcardCount = this.wildcardPaths.length;
678
+ }
679
+ };
680
+
681
+ // src/public/parser.ts
682
+ hooks.render = render;
683
+ hooks.explain = explain;
684
+ var withInfo = (b) => {
685
+ const path = b.statePathName;
686
+ const notPath = path.startsWith("$command.") || b.bindingType === "event" && (b.propSegments[0] === "eventToken" || !path.includes("."));
687
+ if (!notPath && path.split(".").some((s) => s === "__proto__" || s === "prototype")) raise(120 /* UnsafeSegment */, [path]);
688
+ return { ...b, statePathInfo: getPathInfo(path) };
689
+ };
690
+ function parseBindTextsForElement2(bindText) {
691
+ return parseBindTextsForElement(bindText).map(withInfo);
692
+ }
693
+ function parseBindTextForEmbeddedNode2(bindText) {
694
+ return withInfo(parseBindTextForEmbeddedNode(bindText));
695
+ }
696
+ function clearParserCaches() {
697
+ clearPathInfoCache();
698
+ clearPropPartCache();
699
+ clearStatePartCache();
700
+ }
701
+ export {
702
+ clearParserCaches,
703
+ getPathInfo,
704
+ indexOfOutsideQuotes,
705
+ parseBindTextForEmbeddedNode2 as parseBindTextForEmbeddedNode,
706
+ parseBindTextsForElement2 as parseBindTextsForElement,
707
+ splitBindTexts,
708
+ splitOutsideQuotes
709
+ };