@wcstack/state 3.5.3 → 4.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +669 -428
- package/README.md +667 -426
- package/dist/auto.min.js +1 -2
- package/dist/define.d.ts +4 -8
- package/dist/define.js +1 -1
- package/dist/index.d.ts +139 -1483
- package/dist/index.esm.js +1 -2
- package/dist/manifest.d.ts +18 -59
- package/dist/manifest.esm.js +542 -1202
- package/dist/parser.d.ts +49 -103
- package/dist/parser.esm.js +662 -831
- package/dist/split/auto.js +1 -0
- package/dist/split/chunks/chunk-732CB4QN.js +1 -0
- package/dist/split/chunks/chunk-7PN5YNZZ.js +1 -0
- package/dist/split/chunks/chunk-FF6KHTYS.js +1 -0
- package/dist/split/chunks/chunk-HFKGUDDJ.js +1 -0
- package/dist/split/chunks/chunk-PKDBVBAP.js +1 -0
- package/dist/split/chunks/chunk-QI7P7YPP.js +1 -0
- package/dist/split/chunks/chunk-QQH6N5CJ.js +1 -0
- package/dist/split/chunks/chunk-UM3O6QXD.js +1 -0
- package/dist/split/chunks/chunk-WTD5TVPZ.js +1 -0
- package/dist/split/chunks/chunk-WWJNAZVP.js +1 -0
- package/dist/split/chunks/chunk-YOPPVWPQ.js +1 -0
- package/dist/split/core.d.ts +114 -1168
- package/dist/split/core.js +1 -2
- package/dist/split/features/devtools.d.ts +23 -3
- package/dist/split/features/devtools.js +1 -2
- package/dist/split/features/diagnostics.d.ts +27 -5
- package/dist/split/features/diagnostics.js +1 -2
- package/dist/split/features/formats.d.ts +21 -5
- package/dist/split/features/formats.js +1 -2
- package/dist/split/features/list-keys.d.ts +24 -0
- package/dist/split/features/list-keys.js +1 -0
- package/dist/split/features/recursion.d.ts +23 -3
- package/dist/split/features/recursion.js +1 -2
- package/dist/split/features/scopes.d.ts +25 -4
- package/dist/split/features/scopes.js +1 -2
- package/dist/split/features/ssr.d.ts +23 -3
- package/dist/split/features/ssr.js +1 -2
- package/dist/split/features/temporal.d.ts +19 -4
- package/dist/split/features/temporal.js +1 -2
- package/dist/wcs-manifest.json +43 -49
- package/package.json +15 -17
- package/dist/auto.min.js.map +0 -1
- package/dist/index.esm.js.map +0 -1
- package/dist/split/chunks/binder.js +0 -2
- package/dist/split/chunks/binder.js.map +0 -1
- package/dist/split/chunks/checkDependency.js +0 -2
- package/dist/split/chunks/checkDependency.js.map +0 -1
- package/dist/split/chunks/declarationAliases.js +0 -2
- package/dist/split/chunks/declarationAliases.js.map +0 -1
- package/dist/split/chunks/declarationHooks.js +0 -2
- package/dist/split/chunks/declarationHooks.js.map +0 -1
- package/dist/split/chunks/diagnosticsHooks.js +0 -2
- package/dist/split/chunks/diagnosticsHooks.js.map +0 -1
- package/dist/split/chunks/errorGuidance.js +0 -2
- package/dist/split/chunks/errorGuidance.js.map +0 -1
- package/dist/split/chunks/featureBridge.js +0 -2
- package/dist/split/chunks/featureBridge.js.map +0 -1
- package/dist/split/chunks/features.d.ts +0 -29
- package/dist/split/chunks/filterRegistry.js +0 -2
- package/dist/split/chunks/filterRegistry.js.map +0 -1
- package/dist/split/chunks/getAllPropertyDescriptors.js +0 -2
- package/dist/split/chunks/getAllPropertyDescriptors.js.map +0 -1
- package/dist/split/chunks/lifecycleHooks.js +0 -2
- package/dist/split/chunks/lifecycleHooks.js.map +0 -1
- package/dist/split/chunks/pathDiagnostics.js +0 -2
- package/dist/split/chunks/pathDiagnostics.js.map +0 -1
- package/dist/split/chunks/registerComponents.js +0 -2
- package/dist/split/chunks/registerComponents.js.map +0 -1
- package/dist/split/chunks/setAll.js +0 -2
- package/dist/split/chunks/setAll.js.map +0 -1
- package/dist/split/chunks/version.js +0 -2
- package/dist/split/chunks/version.js.map +0 -1
- package/dist/split/core.js.map +0 -1
- package/dist/split/features/devtools.js.map +0 -1
- package/dist/split/features/diagnostics.js.map +0 -1
- package/dist/split/features/formats.js.map +0 -1
- package/dist/split/features/recursion.js.map +0 -1
- package/dist/split/features/scopes.js.map +0 -1
- package/dist/split/features/ssr.js.map +0 -1
- package/dist/split/features/temporal.js.map +0 -1
package/dist/parser.esm.js
CHANGED
|
@@ -1,878 +1,709 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
23
|
+
}
|
|
24
|
+
return prev[b.length];
|
|
128
25
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
187
|
+
}
|
|
188
|
+
if (inQuote !== null) {
|
|
189
|
+
raise(107 /* UnterminatedQuote */, [inQuote, argsText]);
|
|
190
|
+
}
|
|
191
|
+
flush(true);
|
|
192
|
+
return { args, literals };
|
|
368
193
|
}
|
|
369
194
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
209
|
+
}
|
|
210
|
+
return found;
|
|
392
211
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
558
|
-
|
|
234
|
+
if (openParenIndex !== -1 && closeParenIndex === -1) {
|
|
235
|
+
raise(108 /* FilterUnclosed */, [filterText]);
|
|
559
236
|
}
|
|
560
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
524
|
+
return splitOutsideQuotes(bindText, BINDING_SEPARATOR);
|
|
681
525
|
}
|
|
682
526
|
function parseBindTextsForElement(bindText) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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:
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
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
|
+
};
|