@wcstack/state 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +128 -90
- package/README.md +129 -91
- package/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/define.d.ts +11 -7
- package/dist/index.d.ts +58 -15
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +39 -5
- package/dist/manifest.esm.js +315 -253
- package/dist/parser.d.ts +16 -2
- package/dist/parser.esm.js +223 -50
- package/dist/split/chunks/binder.js +1 -1
- package/dist/split/chunks/binder.js.map +1 -1
- package/dist/split/chunks/checkDependency.js +1 -1
- package/dist/split/chunks/declarationAliases.js +2 -0
- package/dist/split/chunks/declarationAliases.js.map +1 -0
- package/dist/split/chunks/declarationHooks.js +2 -0
- package/dist/split/chunks/declarationHooks.js.map +1 -0
- package/dist/split/chunks/featureBridge.js +2 -0
- package/dist/split/chunks/featureBridge.js.map +1 -0
- package/dist/split/chunks/filterRegistry.js +1 -1
- package/dist/split/chunks/filterRegistry.js.map +1 -1
- package/dist/split/chunks/getAllPropertyDescriptors.js +1 -1
- package/dist/split/chunks/getAllPropertyDescriptors.js.map +1 -1
- package/dist/split/chunks/lifecycleHooks.js +1 -1
- package/dist/split/chunks/pathDiagnostics.js +1 -1
- package/dist/split/chunks/pathDiagnostics.js.map +1 -1
- package/dist/split/chunks/registerComponents.js +1 -1
- package/dist/split/chunks/registerComponents.js.map +1 -1
- package/dist/split/chunks/setAll.js +1 -1
- package/dist/split/chunks/setAll.js.map +1 -1
- package/dist/split/chunks/version.js +1 -1
- package/dist/split/core.d.ts +21 -9
- package/dist/split/core.js +1 -1
- package/dist/split/core.js.map +1 -1
- package/dist/split/features/devtools.js +1 -1
- package/dist/split/features/devtools.js.map +1 -1
- package/dist/split/features/diagnostics.js +1 -1
- package/dist/split/features/diagnostics.js.map +1 -1
- package/dist/split/features/formats.js +1 -1
- package/dist/split/features/formats.js.map +1 -1
- package/dist/split/features/recursion.js +1 -1
- package/dist/split/features/recursion.js.map +1 -1
- package/dist/split/features/scopes.js +1 -1
- package/dist/split/features/scopes.js.map +1 -1
- package/dist/split/features/ssr.js +1 -1
- package/dist/split/features/ssr.js.map +1 -1
- package/dist/split/features/temporal.js +1 -1
- package/dist/split/features/temporal.js.map +1 -1
- package/dist/wcs-manifest.json +98 -44
- package/package.json +1 -1
- package/dist/split/chunks/mount.js +0 -2
- package/dist/split/chunks/mount.js.map +0 -1
- package/dist/split/chunks/watchRuntime.js +0 -2
- package/dist/split/chunks/watchRuntime.js.map +0 -1
package/dist/parser.d.ts
CHANGED
|
@@ -85,6 +85,17 @@ type ParseBindTextResult = IParsedBinding;
|
|
|
85
85
|
declare function splitBindTexts(bindText: string): string[];
|
|
86
86
|
declare function parseBindTextsForElement(bindText: string): ParseBindTextResult[];
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* `text` の中で、引用符(`'` / `"`)の外にある最初の `char` の位置。無ければ -1(要件 B1)。
|
|
90
|
+
* 閉じていない引用符はそのまま末尾まで続く扱い — 不正な引用符はフィルタ引数の段で名指しで落ちる。
|
|
91
|
+
*/
|
|
92
|
+
declare function indexOfOutsideQuotes(text: string, char: string): number;
|
|
93
|
+
/**
|
|
94
|
+
* `separator` で区切る。ただし引用符の中は区切らない(要件 B1): `join(';')` や `join('|')` の
|
|
95
|
+
* 区切り文字は引数であって、バインディングやフィルタの区切りではない。
|
|
96
|
+
*/
|
|
97
|
+
declare function splitOutsideQuotes(text: string, separator: string): string[];
|
|
98
|
+
|
|
88
99
|
declare function parseBindTextForEmbeddedNode(bindText: string): ParseBindTextResult;
|
|
89
100
|
|
|
90
101
|
declare function getPathInfo(path: string): IPathInfo;
|
|
@@ -108,7 +119,10 @@ declare function getPathInfo(path: string): IPathInfo;
|
|
|
108
119
|
* 同一パス → 同一インスタンスの保証は**このエントリのモジュールインスタンス内**でのみ
|
|
109
120
|
* 成立する(`.` エントリは別バンドル=別キャッシュ。ランタイムの PathInfo と identity
|
|
110
121
|
* 比較してはならない)。キャッシュは無制限(evict なし)— 言語サーバー等の長時間
|
|
111
|
-
*
|
|
122
|
+
* プロセスではメモリが増え続ける点に留意(断ち方は `clearPathInfoCacheForTooling`)。
|
|
123
|
+
* **増え方はパス種数への単調比例ではない**: `PathInfo` は自分の全ての接頭辞を intern
|
|
124
|
+
* するので、1 本のパスが持ち込む量はその深さの 2 乗に比例する。深さは
|
|
125
|
+
* `MAX_PATH_SEGMENTS` で頭打ちになる(超えたパスは `[wcs/binding-syntax]` で拒否)。
|
|
112
126
|
* - `ParseBindTextResult.uuid` はランタイム内部(構造テンプレートのハイドレーション台帳)
|
|
113
127
|
* 用のフィールドで、このパーサの戻り値では常に undefined。
|
|
114
128
|
*
|
|
@@ -128,5 +142,5 @@ declare function getPathInfo(path: string): IPathInfo;
|
|
|
128
142
|
*/
|
|
129
143
|
declare function clearParserCaches(): void;
|
|
130
144
|
|
|
131
|
-
export { clearParserCaches, getPathInfo, parseBindTextForEmbeddedNode, parseBindTextsForElement, splitBindTexts };
|
|
145
|
+
export { clearParserCaches, getPathInfo, indexOfOutsideQuotes, parseBindTextForEmbeddedNode, parseBindTextsForElement, splitBindTexts, splitOutsideQuotes };
|
|
132
146
|
export type { BindingType, IFilterInfo, IPathInfo, ParseBindTextResult };
|
package/dist/parser.esm.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
const DELIMITER = '.';
|
|
2
2
|
const WILDCARD = '*';
|
|
3
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;
|
|
4
20
|
// data-wcs バインディング構文 `[prop][#mod]: [path][|filter...]` の区切り文字(単一正本・`@state` は v2 で撤去)。
|
|
5
21
|
// これらは「死守の壁(構文契約)」であり値は不変。manifest.syntax.delimiters で公開される。
|
|
6
22
|
const BINDING_SEPARATOR = ';'; // 複数バインディングの区切り
|
|
@@ -16,6 +32,11 @@ const ELSE_KEYWORD = 'else';
|
|
|
16
32
|
const SPREAD_PROP = '...';
|
|
17
33
|
const EVENT_PROP_PREFIX = 'on';
|
|
18
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';
|
|
19
40
|
const COMMAND_NAMESPACE = 'command';
|
|
20
41
|
const CLASS_NAMESPACE = 'class';
|
|
21
42
|
const ATTR_NAMESPACE = 'attr';
|
|
@@ -76,10 +97,35 @@ function getPathInfo(path) {
|
|
|
76
97
|
`It is only meaningful in a $recursion declaration, in a recursive getter key, and in the path ` +
|
|
77
98
|
`argument of $getAll / $setAll — and only when the state declares a $recursion anchor.`);
|
|
78
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.`);
|
|
114
|
+
}
|
|
79
115
|
pathInfo = Object.freeze(new PathInfo(path));
|
|
80
116
|
_cache.set(path, pathInfo);
|
|
81
117
|
return pathInfo;
|
|
82
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
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return count;
|
|
128
|
+
}
|
|
83
129
|
class PathInfo {
|
|
84
130
|
id = ++id;
|
|
85
131
|
path;
|
|
@@ -261,7 +307,12 @@ function toLiteral(text, quoted) {
|
|
|
261
307
|
return null;
|
|
262
308
|
return NUMBER_LITERAL.test(text) ? Number(text) : text;
|
|
263
309
|
}
|
|
264
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* 引数の原文と、その型付きの値(要件 B9)を一緒に返す。原文は引用符を外したもの。
|
|
312
|
+
*
|
|
313
|
+
* 末尾の空引数だけが落ちる(`"a,"` → 1 個、`",a"` → 2 個、`","` → 1 個)。`filter()` を
|
|
314
|
+
* 「引数 0 個」と読むための規則で、先頭・中間の空引数は位置を保つために残す。
|
|
315
|
+
*/
|
|
265
316
|
function parseFilterArgsWithLiterals(argsText) {
|
|
266
317
|
const args = [];
|
|
267
318
|
const literals = [];
|
|
@@ -316,43 +367,6 @@ function parseFilterArgsWithLiterals(argsText) {
|
|
|
316
367
|
return { args, literals };
|
|
317
368
|
}
|
|
318
369
|
|
|
319
|
-
/** tooling 専用(parser.ts の clearParserCaches からのみ呼ぶ)。 */
|
|
320
|
-
function clearFilterFnCacheForTooling() {
|
|
321
|
-
clearFilterResolutionCache();
|
|
322
|
-
}
|
|
323
|
-
// format: filterName(arg1,arg2) or filterName
|
|
324
|
-
/**
|
|
325
|
-
* 文法の段(要件 D16): 名前と引数だけを読む。**実関数は引かない** — 束縛計画の段で
|
|
326
|
-
* 登録簿から解決する(`core/filterRegistry.ts`・`bindings/getBindingInfos.ts`)。
|
|
327
|
-
* 未知のフィルタもここでは落とさない: パーサだけを使う tooling は実装を持たないので、
|
|
328
|
-
* 「知らない名前」を解析の段で判定できない。
|
|
329
|
-
*/
|
|
330
|
-
function parseFilters(filterTextList, _filterIOType) {
|
|
331
|
-
return filterTextList.map((filterText) => {
|
|
332
|
-
const openParenIndex = filterText.indexOf('(');
|
|
333
|
-
const closeParenIndex = filterText.lastIndexOf(')');
|
|
334
|
-
// check parentheses
|
|
335
|
-
if (openParenIndex !== -1 && closeParenIndex === -1) {
|
|
336
|
-
raiseError(`Invalid filter format: missing closing parenthesis in "${filterText}"`);
|
|
337
|
-
}
|
|
338
|
-
if (closeParenIndex !== -1 && openParenIndex === -1) {
|
|
339
|
-
raiseError(`Invalid filter format: missing opening parenthesis in "${filterText}"`);
|
|
340
|
-
}
|
|
341
|
-
const filterName = (openParenIndex === -1 ? filterText : filterText.substring(0, openParenIndex)).trim();
|
|
342
|
-
if (filterName.length === 0) {
|
|
343
|
-
// 空のフィルタ(`x|`・`x||y`・`x|(1)`)は文法の誤り。解析の段で名指しで落とす — 未知の
|
|
344
|
-
// フィルタとは別物で、実関数の解決(束縛計画の段)まで持ち越すと tooling の解析が素通りする
|
|
345
|
-
raiseError(`[wcs/binding-syntax] an empty filter in "${filterTextList.join("|")}" — remove the extra "|" or name the filter.${LINT_HINT}`);
|
|
346
|
-
}
|
|
347
|
-
if (openParenIndex === -1) {
|
|
348
|
-
// no arguments
|
|
349
|
-
return { filterName, args: [], literals: [] };
|
|
350
|
-
}
|
|
351
|
-
const argsText = filterText.substring(openParenIndex + 1, closeParenIndex);
|
|
352
|
-
return { filterName, ...parseFilterArgsWithLiterals(argsText) };
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
370
|
const trimFn = (s) => s.trim();
|
|
357
371
|
const isQuote = (c) => c === "'" || c === '"';
|
|
358
372
|
/**
|
|
@@ -376,6 +390,29 @@ function indexOfOutsideQuotes(text, char) {
|
|
|
376
390
|
}
|
|
377
391
|
return -1;
|
|
378
392
|
}
|
|
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
|
+
}
|
|
379
416
|
/**
|
|
380
417
|
* `separator` で区切る。ただし引用符の中は区切らない(要件 B1): `join(';')` や `join('|')` の
|
|
381
418
|
* 区切り文字は引数であって、バインディングやフィルタの区切りではない。
|
|
@@ -402,6 +439,88 @@ function splitOutsideQuotes(text, separator) {
|
|
|
402
439
|
return parts;
|
|
403
440
|
}
|
|
404
441
|
|
|
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
|
+
*/
|
|
461
|
+
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
|
+
|
|
405
524
|
const cacheFilterInfos$1 = new Map();
|
|
406
525
|
/** tooling 専用(parser.ts の clearParserCaches からのみ呼ぶ)。 */
|
|
407
526
|
function clearPropPartCacheForTooling() {
|
|
@@ -428,13 +547,22 @@ function parsePropPart(propPart) {
|
|
|
428
547
|
}
|
|
429
548
|
else {
|
|
430
549
|
filterTexts = splitOutsideQuotes(filtersText, FILTER_SEPARATOR).map(trimFn);
|
|
431
|
-
|
|
550
|
+
// 診断に埋める原文は**左辺の全文**。`|` より後ろだけを渡すと `value|:` のように
|
|
551
|
+
// 末尾が空の形で空文字になる(解析結果のキャッシュ鍵は従来どおり `filtersText`。
|
|
552
|
+
// 落ちた解析はキャッシュに載らないので、原文を混ぜても鍵は汚れない)
|
|
553
|
+
filters = parseFilters(filterTexts, "input", propPart);
|
|
432
554
|
cacheFilterInfos$1.set(filtersText, filters);
|
|
433
555
|
}
|
|
434
556
|
}
|
|
435
557
|
else {
|
|
436
558
|
propText = propPart.trim();
|
|
437
559
|
}
|
|
560
|
+
// **不変条件**: ここから下の `split` は素で走らせてよい。`propText` は「引用符外の最初の `|`
|
|
561
|
+
// より前」のスライスであり、引用符を含みうるのは入力フィルタの引数(`|` の後ろ)だけなので、
|
|
562
|
+
// `#` も `,` も `.` も引用符の中に現れない。**修飾子の値に引用符を許す拡張(例
|
|
563
|
+
// `value#fmt('a,b'): x`)を入れるなら、この 3 つも `splitOutsideQuotes` に替えること** —
|
|
564
|
+
// 替え忘れると `structural/expandShorthandPaths.ts` で起きた「無言で 1 行も描画されない」
|
|
565
|
+
// と同じ欠陥クラスが再発する。
|
|
438
566
|
const modifierParts = propText.split(MODIFIER_SEPARATOR).map(trimFn);
|
|
439
567
|
if (modifierParts.length > 2) {
|
|
440
568
|
// 修飾子の並びは 1 つだけ(要件 B2)。`value#ro#wo` は以前 `ro` だけを残して黙って捨てていた
|
|
@@ -442,6 +570,15 @@ function parsePropPart(propPart) {
|
|
|
442
570
|
}
|
|
443
571
|
const [propName, propModifiersText] = modifierParts;
|
|
444
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}`);
|
|
581
|
+
}
|
|
445
582
|
const propModifiers = propModifiersText
|
|
446
583
|
? propModifiersText.split(',').map(trimFn)
|
|
447
584
|
: [];
|
|
@@ -476,7 +613,8 @@ function parseStatePart(statePart) {
|
|
|
476
613
|
}
|
|
477
614
|
else {
|
|
478
615
|
filterTexts = splitOutsideQuotes(filtersText, FILTER_SEPARATOR).map(trimFn);
|
|
479
|
-
|
|
616
|
+
// 診断に埋める原文は**右辺の全文**(`parsePropPart` と同じ理由 — `a|` が空文字になる)
|
|
617
|
+
filters = parseFilters(filterTexts, "output", statePart);
|
|
480
618
|
cacheFilterInfos.set(filtersText, filters);
|
|
481
619
|
}
|
|
482
620
|
}
|
|
@@ -489,6 +627,24 @@ function parseStatePart(statePart) {
|
|
|
489
627
|
`Mount the named state onto the tree (<wcs-state mount="...">) and read it by its path prefix instead.`);
|
|
490
628
|
}
|
|
491
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}`);
|
|
647
|
+
}
|
|
492
648
|
const pathInfo = getPathInfo(statePathName);
|
|
493
649
|
return {
|
|
494
650
|
statePathName,
|
|
@@ -507,9 +663,14 @@ function parseStatePart(statePart) {
|
|
|
507
663
|
// ...: statePart (spread — expand wcBindable properties+inputs of target object)
|
|
508
664
|
/** 左辺に修飾子も入力フィルタも取らない束縛(構造ディレクティブと spread)— 付いていれば拒否する(要件 B4) */
|
|
509
665
|
const KEYWORDS_WITHOUT_MODIFIERS = new Set([ELSE_KEYWORD, 'if', 'elseif', 'for', SPREAD_PROP]);
|
|
510
|
-
/**
|
|
666
|
+
/**
|
|
667
|
+
* 明示のプロパティ形(`.name:`)の先頭に置けない語 — 名前空間として読まれる語(要件 B5)。
|
|
668
|
+
* `state`(`VOLUME_INJECTION_PROP`)も含む: `<wcs-state mount>` 上の左辺 `state.<key>:` は
|
|
669
|
+
* 注入の宣言(要件 B14③)なので、`.state.taxRate:` はプロパティか注入か曖昧になる。
|
|
670
|
+
*/
|
|
511
671
|
const EXPLICIT_PROPERTY_REJECTED_HEADS = new Set([
|
|
512
672
|
CLASS_NAMESPACE, ATTR_NAMESPACE, STYLE_NAMESPACE, COMMAND_NAMESPACE, EVENT_TOKEN_NAMESPACE,
|
|
673
|
+
VOLUME_INJECTION_PROP,
|
|
513
674
|
]);
|
|
514
675
|
/**
|
|
515
676
|
* `data-wcs` の値をバインディングごとに区切る(前後の空白は残す — tooling が位置を数えられるように)。
|
|
@@ -521,14 +682,20 @@ function splitBindTexts(bindText) {
|
|
|
521
682
|
function parseBindTextsForElement(bindText) {
|
|
522
683
|
const [...bindTexts] = splitBindTexts(bindText).map(trimFn).filter(s => s.length > 0);
|
|
523
684
|
const results = bindTexts.map((bindText) => {
|
|
524
|
-
|
|
685
|
+
// 左辺と右辺の区切りも引用符の外だけで探す(要件 B1)。`value|defaults(':'): path` の
|
|
686
|
+
// 引数の中の `:` を区切りとして拾っていた
|
|
687
|
+
const separatorIndex = indexOfOutsideQuotes(bindText, PROP_VALUE_SEPARATOR);
|
|
525
688
|
if (separatorIndex === -1) {
|
|
526
|
-
raiseError(`Invalid bindText: "${bindText}". Missing ':' separator between propPart and statePart
|
|
689
|
+
raiseError(`[wcs/binding-syntax] Invalid bindText: "${bindText}". Missing ':' separator between propPart and statePart.${LINT_HINT}`);
|
|
527
690
|
}
|
|
528
691
|
const propPart = bindText.slice(0, separatorIndex).trim();
|
|
529
692
|
const statePart = bindText.slice(separatorIndex + 1).trim();
|
|
530
693
|
// 種別は修飾子・入力フィルタより前の名前で決める(要件 B4)。以前は左辺全体との完全一致で
|
|
531
|
-
// 判定していたので、`radio#ro:`
|
|
694
|
+
// 判定していたので、`radio#ro:` が汎用プロパティに落ちていた。
|
|
695
|
+
// **不変条件**: ここは素の `split` でよい — 取り出すのは最初の区切りより前の**先頭**片で、
|
|
696
|
+
// 引用符を含みうるのは入力フィルタの引数(引用符外の最初の `|` より後ろ)だけなので、
|
|
697
|
+
// `#` も `|` も引用符の中では出会わない。**修飾子の値に引用符を許す拡張を入れるなら
|
|
698
|
+
// `indexOfOutsideQuotes` に替えること**(`parsePropPart.ts` の同じ注記と対)。
|
|
532
699
|
const keyword = propPart.split(MODIFIER_SEPARATOR)[0].split(FILTER_SEPARATOR)[0].trim();
|
|
533
700
|
if (keyword !== propPart && KEYWORDS_WITHOUT_MODIFIERS.has(keyword)) {
|
|
534
701
|
raiseError(`[wcs/binding-syntax] "${bindText}": "${keyword}" takes no modifiers or filters on its left side — write "${keyword}:".${LINT_HINT}`);
|
|
@@ -551,12 +718,15 @@ function parseBindTextsForElement(bindText) {
|
|
|
551
718
|
};
|
|
552
719
|
}
|
|
553
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
|
+
}
|
|
554
727
|
const stateResult = parseStatePart(statePart);
|
|
555
728
|
if (stateResult.outFilters.length > 0) {
|
|
556
|
-
raiseError(`Invalid spread binding "${bindText}": filters are not allowed on spread targets
|
|
557
|
-
}
|
|
558
|
-
if (stateResult.statePathName.length === 0) {
|
|
559
|
-
raiseError(`Invalid spread binding "${bindText}": spread target path is required.`);
|
|
729
|
+
raiseError(`[wcs/binding-syntax] Invalid spread binding "${bindText}": filters are not allowed on spread targets.${LINT_HINT}`);
|
|
560
730
|
}
|
|
561
731
|
return {
|
|
562
732
|
propName: SPREAD_PROP,
|
|
@@ -678,7 +848,10 @@ function parseBindTextForEmbeddedNode(bindText) {
|
|
|
678
848
|
* 同一パス → 同一インスタンスの保証は**このエントリのモジュールインスタンス内**でのみ
|
|
679
849
|
* 成立する(`.` エントリは別バンドル=別キャッシュ。ランタイムの PathInfo と identity
|
|
680
850
|
* 比較してはならない)。キャッシュは無制限(evict なし)— 言語サーバー等の長時間
|
|
681
|
-
*
|
|
851
|
+
* プロセスではメモリが増え続ける点に留意(断ち方は `clearPathInfoCacheForTooling`)。
|
|
852
|
+
* **増え方はパス種数への単調比例ではない**: `PathInfo` は自分の全ての接頭辞を intern
|
|
853
|
+
* するので、1 本のパスが持ち込む量はその深さの 2 乗に比例する。深さは
|
|
854
|
+
* `MAX_PATH_SEGMENTS` で頭打ちになる(超えたパスは `[wcs/binding-syntax]` で拒否)。
|
|
682
855
|
* - `ParseBindTextResult.uuid` はランタイム内部(構造テンプレートのハイドレーション台帳)
|
|
683
856
|
* 用のフィールドで、このパーサの戻り値では常に undefined。
|
|
684
857
|
*
|
|
@@ -702,4 +875,4 @@ function clearParserCaches() {
|
|
|
702
875
|
clearFilterFnCacheForTooling();
|
|
703
876
|
}
|
|
704
877
|
|
|
705
|
-
export { clearParserCaches, getPathInfo, parseBindTextForEmbeddedNode, parseBindTextsForElement, splitBindTexts };
|
|
878
|
+
export { clearParserCaches, getPathInfo, indexOfOutsideQuotes, parseBindTextForEmbeddedNode, parseBindTextsForElement, splitBindTexts, splitOutsideQuotes };
|