@wcstack/state 1.27.0 → 1.28.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/dist/auto.min.js +1 -1
- package/dist/auto.min.js.map +1 -1
- package/dist/index.d.ts +45 -3
- package/dist/index.esm.js +207 -61
- package/dist/index.esm.js.map +1 -1
- package/dist/manifest.d.ts +35 -0
- package/dist/manifest.esm.js +54 -1
- package/dist/parser.d.ts +74 -0
- package/dist/parser.esm.js +1452 -0
- package/dist/wcs-manifest.json +29 -1
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -226,7 +226,12 @@ interface IFilterInfo {
|
|
|
226
226
|
readonly args: string[];
|
|
227
227
|
readonly filterFn: FilterFn;
|
|
228
228
|
}
|
|
229
|
-
|
|
229
|
+
/**
|
|
230
|
+
* バインディング式のパース結果(DOM 非依存の部分)。`@wcstack/state/parser` の
|
|
231
|
+
* ParseBindTextResult がこれをそのまま公開するため、Node 等の DOM lib 型を
|
|
232
|
+
* ここに足してはならない(足すなら IBindingInfo 側へ)。
|
|
233
|
+
*/
|
|
234
|
+
interface IParsedBinding {
|
|
230
235
|
readonly propName: string;
|
|
231
236
|
readonly propSegments: string[];
|
|
232
237
|
readonly propModifiers: string[];
|
|
@@ -235,11 +240,13 @@ interface IBindingInfo {
|
|
|
235
240
|
readonly stateName: string;
|
|
236
241
|
readonly inFilters: IFilterInfo[];
|
|
237
242
|
readonly outFilters: IFilterInfo[];
|
|
238
|
-
readonly node: Node;
|
|
239
|
-
readonly replaceNode: Node;
|
|
240
243
|
readonly bindingType: BindingType;
|
|
241
244
|
readonly uuid?: string | null;
|
|
242
245
|
}
|
|
246
|
+
interface IBindingInfo extends IParsedBinding {
|
|
247
|
+
readonly node: Node;
|
|
248
|
+
readonly replaceNode: Node;
|
|
249
|
+
}
|
|
243
250
|
|
|
244
251
|
interface IState {
|
|
245
252
|
[key: string]: any;
|
|
@@ -691,6 +698,41 @@ interface IWcsManifest {
|
|
|
691
698
|
};
|
|
692
699
|
/** 構造ディレクティブ(`<template data-wcs="for: ...">` 等) */
|
|
693
700
|
structuralDirectives: readonly string[];
|
|
701
|
+
/**
|
|
702
|
+
* 修飾子(`#` 後)の語彙。flags は値を取らない形(`#prevent`)、keyValue は
|
|
703
|
+
* `=` で値を取る形(`#init=element`)、eventNamePrefix は `on` + イベント名の形
|
|
704
|
+
* (`#onchange` — two-way / radio / checkbox のイベント名上書き。README「Modifiers」)。
|
|
705
|
+
* define.ts の定数が単一正本で、ランタイムの消費箇所も同じ定数に分岐する。
|
|
706
|
+
*/
|
|
707
|
+
modifiers: {
|
|
708
|
+
flags: readonly string[];
|
|
709
|
+
keyValue: readonly string[];
|
|
710
|
+
eventNamePrefix: string;
|
|
711
|
+
};
|
|
712
|
+
/** リストインデックス参照名(`$1`..`$N`)。prefix + 1 始まり連番、maxDepth まで。 */
|
|
713
|
+
indexParam: {
|
|
714
|
+
prefix: string;
|
|
715
|
+
maxDepth: number;
|
|
716
|
+
};
|
|
717
|
+
/**
|
|
718
|
+
* bindingType 判別の語彙(parseBindTextsForElement の分岐と同一の定数から導出)。
|
|
719
|
+
* 判別順: else → spread → 構造ディレクティブ/radio/checkbox → eventToken・`on*`
|
|
720
|
+
* (event)→ prop。propNamespaces は左辺先頭セグメントの特殊 namespace で、
|
|
721
|
+
* apply 層のディスパッチキー集合との一致はテストが強制する。
|
|
722
|
+
* 既知の未収載: `radio` / `checkbox`(BindingType union のみが正本)。
|
|
723
|
+
*/
|
|
724
|
+
bindingTypes: {
|
|
725
|
+
elseKeyword: string;
|
|
726
|
+
spread: string;
|
|
727
|
+
eventPropertyPrefix: string;
|
|
728
|
+
propNamespaces: {
|
|
729
|
+
eventToken: string;
|
|
730
|
+
command: string;
|
|
731
|
+
class: string;
|
|
732
|
+
attr: string;
|
|
733
|
+
style: string;
|
|
734
|
+
};
|
|
735
|
+
};
|
|
694
736
|
};
|
|
695
737
|
/** 組み込みフィルタ名(builtinFilters から自動導出=実装が正本) */
|
|
696
738
|
filters: string[];
|
package/dist/index.esm.js
CHANGED
|
@@ -113,6 +113,38 @@ const PROP_VALUE_SEPARATOR = ':'; // 左辺(prop)と右辺(path)の区切り
|
|
|
113
113
|
const MODIFIER_SEPARATOR = '#'; // prop と修飾子の区切り
|
|
114
114
|
const STATE_NAME_SEPARATOR = '@'; // path と @stateName の区切り
|
|
115
115
|
const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
|
|
116
|
+
// 修飾子(`#` 後)の語彙(単一正本)。manifest.syntax.modifiers で公開される。
|
|
117
|
+
// フラグ形(`#prevent` — 値を取らない)とキー値形(`#init=element` — `=` で値を取る)。
|
|
118
|
+
// 消費箇所(event/handler・BindingSession・twowayHandler・bindings/initialSync)は
|
|
119
|
+
// この定数を参照する — 文字列リテラルの散在は tooling への収載漏れの温床だった
|
|
120
|
+
// (docs/static-wiring-dx-design.md §2-2)。
|
|
121
|
+
const MODIFIER_PREVENT = 'prevent';
|
|
122
|
+
const MODIFIER_STOP = 'stop';
|
|
123
|
+
const MODIFIER_READONLY = 'ro';
|
|
124
|
+
const MODIFIER_FLAGS = Object.freeze([
|
|
125
|
+
MODIFIER_PREVENT, MODIFIER_STOP, MODIFIER_READONLY,
|
|
126
|
+
]);
|
|
127
|
+
const MODIFIER_KEY_INIT = 'init';
|
|
128
|
+
const MODIFIER_KEY_SYNC = 'sync';
|
|
129
|
+
const MODIFIER_KEYS = Object.freeze([
|
|
130
|
+
MODIFIER_KEY_INIT, MODIFIER_KEY_SYNC,
|
|
131
|
+
]);
|
|
132
|
+
// bindingType 判別と左辺 namespace の語彙(単一正本)。manifest.syntax.bindingTypes で
|
|
133
|
+
// 公開される。パーサ(parseBindTextsForElement)とイベント層はこの定数に分岐する。
|
|
134
|
+
// apply 層のディスパッチマップ(apply/applyChange.ts の applyChangeByFirstSegment)の
|
|
135
|
+
// キー集合との一致は __tests__/manifest.test.ts の drift テストが強制する —
|
|
136
|
+
// manifest エントリ(DOM 非依存)から apply 層を import しないための分離。
|
|
137
|
+
const ELSE_KEYWORD = 'else';
|
|
138
|
+
const SPREAD_PROP = '...';
|
|
139
|
+
const EVENT_PROP_PREFIX = 'on';
|
|
140
|
+
const EVENT_TOKEN_NAMESPACE = 'eventToken';
|
|
141
|
+
const COMMAND_NAMESPACE = 'command';
|
|
142
|
+
const CLASS_NAMESPACE = 'class';
|
|
143
|
+
const ATTR_NAMESPACE = 'attr';
|
|
144
|
+
const STYLE_NAMESPACE = 'style';
|
|
145
|
+
// リストインデックス参照名(`$1`..`$N`)の接頭辞(単一正本)。
|
|
146
|
+
// manifest.syntax.indexParam で公開される。
|
|
147
|
+
const INDEX_PARAM_PREFIX = '$';
|
|
116
148
|
/**
|
|
117
149
|
* stackIndexByIndexName
|
|
118
150
|
* インデックス名からスタックインデックスへのマッピング
|
|
@@ -124,7 +156,7 @@ const FILTER_SEPARATOR = '|'; // フィルタパイプの区切り
|
|
|
124
156
|
*/
|
|
125
157
|
const tmpIndexByIndexName = {};
|
|
126
158
|
for (let i = 0; i < MAX_WILDCARD_DEPTH; i++) {
|
|
127
|
-
tmpIndexByIndexName[
|
|
159
|
+
tmpIndexByIndexName[`${INDEX_PARAM_PREFIX}${i + 1}`] = i;
|
|
128
160
|
}
|
|
129
161
|
const INDEX_BY_INDEX_NAME = Object.freeze(tmpIndexByIndexName);
|
|
130
162
|
const NO_SET_TIMEOUT = 60 * 1000; // 1分
|
|
@@ -640,6 +672,75 @@ const STRUCTURAL_BINDING_TYPE_SET = new Set([
|
|
|
640
672
|
"for",
|
|
641
673
|
]);
|
|
642
674
|
|
|
675
|
+
/**
|
|
676
|
+
* errorGuidance.ts — エラーメッセージへの self-fix 誘導(GTM 2-5 /
|
|
677
|
+
* docs/static-wiring-dx-design.md §3)。
|
|
678
|
+
*
|
|
679
|
+
* コンソールは「書き手(人間・AI とも)が誤った瞬間に必ず読む面」なので、
|
|
680
|
+
* (a) did-you-mean 候補 (b) lint への誘導 をエラーメッセージ自体に埋め込む。
|
|
681
|
+
* ここの関数は全て**エラーパスでのみ**呼ばれる — 正常系のコストはゼロ。
|
|
682
|
+
* auto.min.js に同梱されるため文字列は最小限に保つ(エラーパス専用モジュールの
|
|
683
|
+
* 遅延 import は `src/auto.ts` の SRI 自己完結制約で不可)。
|
|
684
|
+
*
|
|
685
|
+
* 診断 code の語彙はコンソール → lint → IDE の三面で共有する:
|
|
686
|
+
* メッセージ先頭の `[wcs/...]` は wcstack-intellisense / @wcstack/lint の
|
|
687
|
+
* 安定診断 code(packages/vscode-wcs/src/core/diagnostics.ts)と同一。
|
|
688
|
+
*/
|
|
689
|
+
/** 挿入・削除・置換の編集距離。長さ差が max を超えたら早期に max+1 を返す。 */
|
|
690
|
+
function editDistance(a, b, max) {
|
|
691
|
+
if (Math.abs(a.length - b.length) > max) {
|
|
692
|
+
return max + 1;
|
|
693
|
+
}
|
|
694
|
+
const prev = new Array(b.length + 1);
|
|
695
|
+
const curr = new Array(b.length + 1);
|
|
696
|
+
for (let j = 0; j <= b.length; j++) {
|
|
697
|
+
prev[j] = j;
|
|
698
|
+
}
|
|
699
|
+
for (let i = 1; i <= a.length; i++) {
|
|
700
|
+
curr[0] = i;
|
|
701
|
+
for (let j = 1; j <= b.length; j++) {
|
|
702
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
703
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
704
|
+
}
|
|
705
|
+
for (let j = 0; j <= b.length; j++) {
|
|
706
|
+
prev[j] = curr[j];
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return prev[b.length];
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* 候補集合から編集距離 2 以内の最近傍を探し、` Did you mean "<best>"?` を返す。
|
|
713
|
+
* 該当なしは空文字。規準(距離 2・同距離は先勝ち・大小文字は畳んで比較)は
|
|
714
|
+
* lint の did-you-mean(ioNodeValidator の suggestion)と同じ — 三面で提案が
|
|
715
|
+
* 割れないように揃えている。動的キー等で候補が列挙できないサイトでは呼ばない
|
|
716
|
+
* = 誘導文のみに縮退(設計 §3 の縮退)。
|
|
717
|
+
*/
|
|
718
|
+
function didYouMean(input, candidates) {
|
|
719
|
+
// 空入力(`a|` の末尾パイプ等)に短い候補を提案しても無意味なので出さない。
|
|
720
|
+
if (input.length === 0) {
|
|
721
|
+
return "";
|
|
722
|
+
}
|
|
723
|
+
const folded = input.toLowerCase();
|
|
724
|
+
let best = null;
|
|
725
|
+
let bestDistance = 3;
|
|
726
|
+
for (const candidate of candidates) {
|
|
727
|
+
const distance = editDistance(folded, candidate.toLowerCase(), 2);
|
|
728
|
+
if (distance < bestDistance) {
|
|
729
|
+
best = candidate;
|
|
730
|
+
bestDistance = distance;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return best !== null ? ` Did you mean "${best}"?` : "";
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* lint への誘導(誘導付きメッセージ共通の一文)。
|
|
737
|
+
* **lint が実際にそのケースを検出するサイトにだけ付ける** — 検出しないケースに
|
|
738
|
+
* 付けると「エラー → lint 実行 → clean」の空振りで検証ループの信頼を毀損する
|
|
739
|
+
* (DCC 宣言・watch の一部 shape・構造型単独バインディング違反は lint 未検出のため
|
|
740
|
+
* 付けない。lint 側への検査追加は follow-up)。
|
|
741
|
+
*/
|
|
742
|
+
const LINT_HINT = " Validate statically: npx @wcstack/lint <file>.";
|
|
743
|
+
|
|
643
744
|
/**
|
|
644
745
|
* errorMessages.ts
|
|
645
746
|
*
|
|
@@ -1558,7 +1659,8 @@ const builtinFiltersByFilterIOType = {
|
|
|
1558
1659
|
const builtinFilterFn = (name, options) => (filters) => {
|
|
1559
1660
|
const filter = filters[name];
|
|
1560
1661
|
if (!filter) {
|
|
1561
|
-
|
|
1662
|
+
// lint の wcs/filter-unknown と同じ語彙・同じ did-you-mean 規準(三面同語彙)。
|
|
1663
|
+
raiseError(`[wcs/filter-unknown] filter not found: ${name}.${didYouMean(name, Object.keys(filters))}${LINT_HINT}`);
|
|
1562
1664
|
}
|
|
1563
1665
|
return filter(options);
|
|
1564
1666
|
};
|
|
@@ -1779,11 +1881,11 @@ function parseBindTextsForElement(bindText) {
|
|
|
1779
1881
|
}
|
|
1780
1882
|
const propPart = bindText.slice(0, separatorIndex).trim();
|
|
1781
1883
|
const statePart = bindText.slice(separatorIndex + 1).trim();
|
|
1782
|
-
if (propPart ===
|
|
1884
|
+
if (propPart === ELSE_KEYWORD) {
|
|
1783
1885
|
const pathInfo = getPathInfo('#else');
|
|
1784
1886
|
return {
|
|
1785
|
-
propName:
|
|
1786
|
-
propSegments: [
|
|
1887
|
+
propName: ELSE_KEYWORD,
|
|
1888
|
+
propSegments: [ELSE_KEYWORD],
|
|
1787
1889
|
propModifiers: [],
|
|
1788
1890
|
statePathName: '#else',
|
|
1789
1891
|
statePathInfo: pathInfo,
|
|
@@ -1793,7 +1895,7 @@ function parseBindTextsForElement(bindText) {
|
|
|
1793
1895
|
bindingType: 'else',
|
|
1794
1896
|
};
|
|
1795
1897
|
}
|
|
1796
|
-
else if (propPart ===
|
|
1898
|
+
else if (propPart === SPREAD_PROP) {
|
|
1797
1899
|
const stateResult = parseStatePart(statePart);
|
|
1798
1900
|
if (stateResult.outFilters.length > 0) {
|
|
1799
1901
|
raiseError(`Invalid spread binding "${bindText}": filters are not allowed on spread targets.`);
|
|
@@ -1802,8 +1904,8 @@ function parseBindTextsForElement(bindText) {
|
|
|
1802
1904
|
raiseError(`Invalid spread binding "${bindText}": spread target path is required.`);
|
|
1803
1905
|
}
|
|
1804
1906
|
return {
|
|
1805
|
-
propName:
|
|
1806
|
-
propSegments: [
|
|
1907
|
+
propName: SPREAD_PROP,
|
|
1908
|
+
propSegments: [SPREAD_PROP],
|
|
1807
1909
|
propModifiers: [],
|
|
1808
1910
|
inFilters: [],
|
|
1809
1911
|
...stateResult,
|
|
@@ -1830,14 +1932,14 @@ function parseBindTextsForElement(bindText) {
|
|
|
1830
1932
|
const propResult = parsePropPart(propPart);
|
|
1831
1933
|
// eventToken.<prop>: <name> は要素 dispatch を state へ流す pub/sub 配線。
|
|
1832
1934
|
// 値適用ではないため bindingType 'event' として listener attach 経路に乗せる。
|
|
1833
|
-
if (propResult.propSegments[0] ===
|
|
1935
|
+
if (propResult.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
|
|
1834
1936
|
return {
|
|
1835
1937
|
...propResult,
|
|
1836
1938
|
...stateResult,
|
|
1837
1939
|
bindingType: 'event',
|
|
1838
1940
|
};
|
|
1839
1941
|
}
|
|
1840
|
-
if (propResult.propSegments[0].startsWith(
|
|
1942
|
+
if (propResult.propSegments[0].startsWith(EVENT_PROP_PREFIX)) {
|
|
1841
1943
|
return {
|
|
1842
1944
|
...propResult,
|
|
1843
1945
|
...stateResult,
|
|
@@ -1857,7 +1959,9 @@ function parseBindTextsForElement(bindText) {
|
|
|
1857
1959
|
if (results.length > 1) {
|
|
1858
1960
|
const isIncludeSingleBinding = results.some(r => STRUCTURAL_BINDING_TYPE_SET.has(r.bindingType));
|
|
1859
1961
|
if (isIncludeSingleBinding) {
|
|
1860
|
-
|
|
1962
|
+
// LINT_HINT は付けない: 単独バインディング検査は lint 側に未実装で、誘導が
|
|
1963
|
+
// 空振りする(lint への検査追加は follow-up)。
|
|
1964
|
+
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">).`);
|
|
1861
1965
|
}
|
|
1862
1966
|
}
|
|
1863
1967
|
return results;
|
|
@@ -2733,8 +2837,8 @@ function getHandlerKey$3(binding, eventName) {
|
|
|
2733
2837
|
function getEventName$2(binding) {
|
|
2734
2838
|
let eventName = 'input';
|
|
2735
2839
|
for (const modifier of binding.propModifiers) {
|
|
2736
|
-
if (modifier.startsWith(
|
|
2737
|
-
eventName = modifier.slice(
|
|
2840
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
2841
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
2738
2842
|
}
|
|
2739
2843
|
}
|
|
2740
2844
|
return eventName;
|
|
@@ -2789,7 +2893,7 @@ const checkboxEventHandlerFunction = (stateName, statePathName, inFilters) => (e
|
|
|
2789
2893
|
});
|
|
2790
2894
|
};
|
|
2791
2895
|
function attachCheckboxEventHandler(binding) {
|
|
2792
|
-
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(
|
|
2896
|
+
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2793
2897
|
const eventName = getEventName$2(binding);
|
|
2794
2898
|
const key = getHandlerKey$3(binding, eventName);
|
|
2795
2899
|
let checkboxEventHandler = handlerByHandlerKey$3.get(key);
|
|
@@ -2804,7 +2908,7 @@ function attachCheckboxEventHandler(binding) {
|
|
|
2804
2908
|
return false;
|
|
2805
2909
|
}
|
|
2806
2910
|
function detachCheckboxEventHandler(binding) {
|
|
2807
|
-
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(
|
|
2911
|
+
if (binding.bindingType === "checkbox" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
2808
2912
|
const eventName = getEventName$2(binding);
|
|
2809
2913
|
const key = getHandlerKey$3(binding, eventName);
|
|
2810
2914
|
const checkboxEventHandler = handlerByHandlerKey$3.get(key);
|
|
@@ -2987,7 +3091,7 @@ function getWcBindable$1(element) {
|
|
|
2987
3091
|
return readBindableDeclaration(element);
|
|
2988
3092
|
}
|
|
2989
3093
|
function attachEventTokenHandler(binding) {
|
|
2990
|
-
if (binding.propSegments[0] !==
|
|
3094
|
+
if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
|
|
2991
3095
|
return false;
|
|
2992
3096
|
}
|
|
2993
3097
|
const element = binding.node;
|
|
@@ -3014,16 +3118,16 @@ function attachEventTokenHandler(binding) {
|
|
|
3014
3118
|
}
|
|
3015
3119
|
const propDesc = bindable.knownProperties.get(propertyName);
|
|
3016
3120
|
if (typeof propDesc === "undefined") {
|
|
3017
|
-
raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}
|
|
3121
|
+
raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}>.${didYouMean(propertyName, bindable.knownProperties.keys())}`);
|
|
3018
3122
|
}
|
|
3019
3123
|
const eventName = propDesc.event;
|
|
3020
3124
|
const tokenName = binding.statePathName;
|
|
3021
3125
|
const stateName = binding.stateName;
|
|
3022
3126
|
const modifiers = binding.propModifiers;
|
|
3023
3127
|
const handler = (event) => {
|
|
3024
|
-
if (modifiers.includes(
|
|
3128
|
+
if (modifiers.includes(MODIFIER_PREVENT))
|
|
3025
3129
|
event.preventDefault();
|
|
3026
|
-
if (modifiers.includes(
|
|
3130
|
+
if (modifiers.includes(MODIFIER_STOP))
|
|
3027
3131
|
event.stopPropagation();
|
|
3028
3132
|
// state は発火時の live root から解決する(attach 時は detached の可能性があるため)。
|
|
3029
3133
|
const rootNode = element.getRootNode();
|
|
@@ -3032,7 +3136,8 @@ function attachEventTokenHandler(binding) {
|
|
|
3032
3136
|
raiseError(`State element with name "${stateName}" not found for eventToken handler.`);
|
|
3033
3137
|
}
|
|
3034
3138
|
if (!stateElement.eventTokenNames.has(tokenName)) {
|
|
3035
|
-
|
|
3139
|
+
// lint も同じケースを wcs/token-undeclared で検出する(三面同語彙)。
|
|
3140
|
+
raiseError(`[wcs/token-undeclared] eventToken "${tokenName}" is not declared in $eventTokens of state "${stateName}".${didYouMean(tokenName, stateElement.eventTokenNames)}${LINT_HINT}`);
|
|
3036
3141
|
}
|
|
3037
3142
|
const loopContext = getLoopContextByNode(element);
|
|
3038
3143
|
stateElement.createStateAsync("writable", async (state) => {
|
|
@@ -3052,7 +3157,7 @@ function attachEventTokenHandler(binding) {
|
|
|
3052
3157
|
return true;
|
|
3053
3158
|
}
|
|
3054
3159
|
function detachEventTokenHandler(binding) {
|
|
3055
|
-
if (binding.propSegments[0] !==
|
|
3160
|
+
if (binding.propSegments[0] !== EVENT_TOKEN_NAMESPACE) {
|
|
3056
3161
|
return false;
|
|
3057
3162
|
}
|
|
3058
3163
|
const listener = listenerByBinding.get(binding);
|
|
@@ -3105,13 +3210,13 @@ const handlerByHandlerKey$2 = new Map();
|
|
|
3105
3210
|
// binding を強参照しない台帳(handlerBindingRegistry.ts のリーク解説を参照)
|
|
3106
3211
|
const bindingRegistry$2 = createHandlerBindingRegistry();
|
|
3107
3212
|
function getHandlerKey$2(binding) {
|
|
3108
|
-
const modifierKey = binding.propModifiers.filter(m => m ===
|
|
3213
|
+
const modifierKey = binding.propModifiers.filter(m => m === MODIFIER_PREVENT || m === MODIFIER_STOP).sort().join(',');
|
|
3109
3214
|
return `${binding.stateName}::${binding.statePathName}::${modifierKey}`;
|
|
3110
3215
|
}
|
|
3111
3216
|
const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathInfo) => (event) => {
|
|
3112
|
-
if (modifiers.includes(
|
|
3217
|
+
if (modifiers.includes(MODIFIER_PREVENT))
|
|
3113
3218
|
event.preventDefault();
|
|
3114
|
-
if (modifiers.includes(
|
|
3219
|
+
if (modifiers.includes(MODIFIER_STOP))
|
|
3115
3220
|
event.stopPropagation();
|
|
3116
3221
|
const node = event.target;
|
|
3117
3222
|
const rootNode = node.getRootNode();
|
|
@@ -3145,7 +3250,7 @@ const stateEventHandlerFunction = (stateName, handlerName, modifiers, statePathI
|
|
|
3145
3250
|
});
|
|
3146
3251
|
};
|
|
3147
3252
|
function attachEventHandler(binding) {
|
|
3148
|
-
if (!binding.propName.startsWith(
|
|
3253
|
+
if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
|
|
3149
3254
|
return false;
|
|
3150
3255
|
}
|
|
3151
3256
|
const key = getHandlerKey$2(binding);
|
|
@@ -3160,7 +3265,7 @@ function attachEventHandler(binding) {
|
|
|
3160
3265
|
return true;
|
|
3161
3266
|
}
|
|
3162
3267
|
function detachEventHandler(binding) {
|
|
3163
|
-
if (!binding.propName.startsWith(
|
|
3268
|
+
if (!binding.propName.startsWith(EVENT_PROP_PREFIX)) {
|
|
3164
3269
|
return false;
|
|
3165
3270
|
}
|
|
3166
3271
|
const key = getHandlerKey$2(binding);
|
|
@@ -3189,8 +3294,8 @@ function getHandlerKey$1(binding, eventName) {
|
|
|
3189
3294
|
function getEventName$1(binding) {
|
|
3190
3295
|
let eventName = 'input';
|
|
3191
3296
|
for (const modifier of binding.propModifiers) {
|
|
3192
|
-
if (modifier.startsWith(
|
|
3193
|
-
eventName = modifier.slice(
|
|
3297
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
3298
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
3194
3299
|
}
|
|
3195
3300
|
}
|
|
3196
3301
|
return eventName;
|
|
@@ -3226,7 +3331,7 @@ const radioEventHandlerFunction = (stateName, statePathName, inFilters) => (even
|
|
|
3226
3331
|
});
|
|
3227
3332
|
};
|
|
3228
3333
|
function attachRadioEventHandler(binding) {
|
|
3229
|
-
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(
|
|
3334
|
+
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3230
3335
|
const eventName = getEventName$1(binding);
|
|
3231
3336
|
const key = getHandlerKey$1(binding, eventName);
|
|
3232
3337
|
let radioEventHandler = handlerByHandlerKey$1.get(key);
|
|
@@ -3241,7 +3346,7 @@ function attachRadioEventHandler(binding) {
|
|
|
3241
3346
|
return false;
|
|
3242
3347
|
}
|
|
3243
3348
|
function detachRadioEventHandler(binding) {
|
|
3244
|
-
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(
|
|
3349
|
+
if (binding.bindingType === "radio" && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3245
3350
|
const eventName = getEventName$1(binding);
|
|
3246
3351
|
const key = getHandlerKey$1(binding, eventName);
|
|
3247
3352
|
const radioEventHandler = handlerByHandlerKey$1.get(key);
|
|
@@ -3480,10 +3585,10 @@ function getEventName(binding) {
|
|
|
3480
3585
|
eventName = propDesc.event;
|
|
3481
3586
|
}
|
|
3482
3587
|
}
|
|
3483
|
-
// 3.modifier
|
|
3588
|
+
// 3.modifier(`#onchange` 等 — `on` + イベント名の修飾子形。README「Modifiers」参照)
|
|
3484
3589
|
for (const modifier of binding.propModifiers) {
|
|
3485
|
-
if (modifier.startsWith(
|
|
3486
|
-
eventName = modifier.slice(
|
|
3590
|
+
if (modifier.startsWith(EVENT_PROP_PREFIX)) {
|
|
3591
|
+
eventName = modifier.slice(EVENT_PROP_PREFIX.length);
|
|
3487
3592
|
}
|
|
3488
3593
|
}
|
|
3489
3594
|
return eventName;
|
|
@@ -3652,7 +3757,7 @@ function attachTwowayEventHandler(binding) {
|
|
|
3652
3757
|
return;
|
|
3653
3758
|
}
|
|
3654
3759
|
}
|
|
3655
|
-
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(
|
|
3760
|
+
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3656
3761
|
const eventName = getEventName(binding);
|
|
3657
3762
|
const valueGetter = getValueGetter(binding);
|
|
3658
3763
|
const isOccurrence = isOccurrenceProperty(binding);
|
|
@@ -3678,7 +3783,7 @@ function detachTwowayEventHandler(binding) {
|
|
|
3678
3783
|
return;
|
|
3679
3784
|
}
|
|
3680
3785
|
}
|
|
3681
|
-
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(
|
|
3786
|
+
if (isPossibleTwoWay(binding.node, binding.propName) && binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
3682
3787
|
const eventName = getEventName(binding);
|
|
3683
3788
|
const valueGetter = getValueGetter(binding);
|
|
3684
3789
|
const key = getHandlerKey(binding, eventName, valueGetter !== null, isOccurrenceProperty(binding));
|
|
@@ -4266,7 +4371,7 @@ function readOption(binding, key) {
|
|
|
4266
4371
|
continue;
|
|
4267
4372
|
const modifierKey = modifier.slice(0, separator).trim();
|
|
4268
4373
|
const value = modifier.slice(separator + 1).trim();
|
|
4269
|
-
if (modifierKey !==
|
|
4374
|
+
if (modifierKey !== MODIFIER_KEY_INIT && modifierKey !== MODIFIER_KEY_SYNC) {
|
|
4270
4375
|
raiseError(`Unknown binding modifier "${modifierKey}" in "${modifier}".`);
|
|
4271
4376
|
}
|
|
4272
4377
|
if (modifierKey !== key)
|
|
@@ -4313,8 +4418,8 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
4313
4418
|
}
|
|
4314
4419
|
return STATE_CALL_POLICY;
|
|
4315
4420
|
}
|
|
4316
|
-
const explicitAuthority = parseAuthority(readOption(binding,
|
|
4317
|
-
const syncOn = parseSyncOn(readOption(binding,
|
|
4421
|
+
const explicitAuthority = parseAuthority(readOption(binding, MODIFIER_KEY_INIT));
|
|
4422
|
+
const syncOn = parseSyncOn(readOption(binding, MODIFIER_KEY_SYNC));
|
|
4318
4423
|
if (binding.bindingType === "event") {
|
|
4319
4424
|
if (explicitAuthority !== null && explicitAuthority !== "none") {
|
|
4320
4425
|
raiseError("Event bindings only allow init=none.");
|
|
@@ -4326,7 +4431,7 @@ function resolveInitialSyncPolicy(binding) {
|
|
|
4326
4431
|
// property authority 検証(未宣言なら raiseError)に掛けてはならない。値の初期同期を
|
|
4327
4432
|
// 持たない配線なので、現行互換の "state" authority を返す(command token は従来通り
|
|
4328
4433
|
// 初期 apply で配線される)。
|
|
4329
|
-
if (binding.propSegments[0] ===
|
|
4434
|
+
if (binding.propSegments[0] === COMMAND_NAMESPACE) {
|
|
4330
4435
|
return statePolicy("state", syncOn);
|
|
4331
4436
|
}
|
|
4332
4437
|
if (binding.bindingType !== "prop") {
|
|
@@ -5099,7 +5204,7 @@ class BindingSession {
|
|
|
5099
5204
|
record.eventAttached = true;
|
|
5100
5205
|
return;
|
|
5101
5206
|
}
|
|
5102
|
-
if (binding.propSegments[0] ===
|
|
5207
|
+
if (binding.propSegments[0] === EVENT_TOKEN_NAMESPACE) {
|
|
5103
5208
|
this.attachAfterDefinition(record, () => {
|
|
5104
5209
|
if (attachEventTokenHandler(binding)) {
|
|
5105
5210
|
addRecordTeardown(record, () => detachEventTokenHandler(binding));
|
|
@@ -5125,7 +5230,7 @@ class BindingSession {
|
|
|
5125
5230
|
// isPossibleTwoWay の未定義 CE raiseError も踏まない)。
|
|
5126
5231
|
if (config.enableDirectionalInitialSync
|
|
5127
5232
|
&& isPossibleTwoWay(binding.node, binding.propName)
|
|
5128
|
-
&& binding.propModifiers.indexOf(
|
|
5233
|
+
&& binding.propModifiers.indexOf(MODIFIER_READONLY) === -1) {
|
|
5129
5234
|
const removeObserver = addTwowayValueObserver(binding.node, binding.propName, (value) => {
|
|
5130
5235
|
if (!this.isAlive(record, record.generation))
|
|
5131
5236
|
return;
|
|
@@ -5544,7 +5649,8 @@ function applyChangeToCommand(binding, _context, newValue) {
|
|
|
5544
5649
|
raiseError(`command binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
|
|
5545
5650
|
}
|
|
5546
5651
|
if (!bindable.declaredCommands.has(methodName)) {
|
|
5547
|
-
|
|
5652
|
+
// eventTokenHandler の property 検証と対双の did-you-mean(設計 §3)。
|
|
5653
|
+
raiseError(`Command "${methodName}" is not declared in wcBindable.commands of <${element.tagName.toLowerCase()}>.${didYouMean(methodName, bindable.declaredCommands.keys())}`);
|
|
5548
5654
|
}
|
|
5549
5655
|
// ここまで来たら旧解除して新 subscribe に切り替える。
|
|
5550
5656
|
if (existing) {
|
|
@@ -6204,7 +6310,7 @@ function compileRowPlan(fragmentInfo) {
|
|
|
6204
6310
|
// command.<name>(prop 扱い)と eventToken.<prop>(event 扱い)は token 配線の
|
|
6205
6311
|
// teardown / attach 分岐が要るため不適格
|
|
6206
6312
|
const namespace = template.propSegments[0];
|
|
6207
|
-
if (namespace ===
|
|
6313
|
+
if (namespace === COMMAND_NAMESPACE || namespace === EVENT_TOKEN_NAMESPACE) {
|
|
6208
6314
|
return null;
|
|
6209
6315
|
}
|
|
6210
6316
|
if (bindingType === "text") {
|
|
@@ -7298,12 +7404,15 @@ function scheduleDeferredApply(binding, tagName) {
|
|
|
7298
7404
|
}, reject);
|
|
7299
7405
|
}
|
|
7300
7406
|
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7407
|
+
// キーは define.ts の namespace 語彙定数(manifest.syntax.bindingTypes.propNamespaces と
|
|
7408
|
+
// 同一の正本)。集合の一致は __tests__/manifest.test.ts の drift テストが強制するため
|
|
7409
|
+
// export する(manifest エントリは DOM 非依存でこのファイルを import できない)。
|
|
7410
|
+
const applyChangeByFirstSegment = Object.freeze({
|
|
7411
|
+
[CLASS_NAMESPACE]: applyChangeToClass,
|
|
7412
|
+
[ATTR_NAMESPACE]: applyChangeToAttribute,
|
|
7413
|
+
[STYLE_NAMESPACE]: applyChangeToStyle,
|
|
7414
|
+
[COMMAND_NAMESPACE]: applyChangeToCommand,
|
|
7415
|
+
});
|
|
7307
7416
|
const applyChangeByBindingType = {
|
|
7308
7417
|
"text": applyChangeToText,
|
|
7309
7418
|
"for": applyChangeToFor,
|
|
@@ -8036,7 +8145,7 @@ async function buildBindings(root) {
|
|
|
8036
8145
|
}
|
|
8037
8146
|
}
|
|
8038
8147
|
|
|
8039
|
-
var version = "1.
|
|
8148
|
+
var version = "1.28.0";
|
|
8040
8149
|
var pkg = {
|
|
8041
8150
|
version: version};
|
|
8042
8151
|
|
|
@@ -9636,7 +9745,7 @@ function processOnDeclaration(stateElement, state, eventTokenNames) {
|
|
|
9636
9745
|
}
|
|
9637
9746
|
for (const [name, handler] of Object.entries(declared)) {
|
|
9638
9747
|
if (!eventTokenNames.has(name)) {
|
|
9639
|
-
raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens
|
|
9748
|
+
raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens.${didYouMean(name, eventTokenNames)}`);
|
|
9640
9749
|
}
|
|
9641
9750
|
if (typeof handler !== "function") {
|
|
9642
9751
|
raiseError(`${STATE_ON_NAME} entry "${name}" must be a function.`);
|
|
@@ -10674,41 +10783,43 @@ function processWatchDeclaration(stateElement, state) {
|
|
|
10674
10783
|
return null;
|
|
10675
10784
|
}
|
|
10676
10785
|
if (typeof declared !== "object" || declared === null) {
|
|
10677
|
-
|
|
10786
|
+
// 非オブジェクト形は lint 側では候補ゼロ扱いで検出されないため LINT_HINT なし
|
|
10787
|
+
// (以下、lint が実際に検出する shape(非関数・$ 始まり・@ 越境・空セグメント)にだけ付ける)。
|
|
10788
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} must be an object mapping state paths to handler functions.`);
|
|
10678
10789
|
}
|
|
10679
10790
|
const entries = new Map();
|
|
10680
10791
|
const paths = new Set();
|
|
10681
10792
|
let order = 0;
|
|
10682
10793
|
for (const [path, handler] of Object.entries(declared)) {
|
|
10683
10794
|
if (typeof handler !== "function") {
|
|
10684
|
-
raiseError(
|
|
10795
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must be a function.${LINT_HINT}`);
|
|
10685
10796
|
}
|
|
10686
10797
|
if (path.length === 0) {
|
|
10687
|
-
raiseError(
|
|
10798
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry name must be a non-empty state path.`);
|
|
10688
10799
|
}
|
|
10689
10800
|
if (path.startsWith("$")) {
|
|
10690
|
-
raiseError(
|
|
10801
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not start with "$" (reserved namespace).${LINT_HINT}`);
|
|
10691
10802
|
}
|
|
10692
10803
|
// 越境 watch は不採用(設計 D8)。他 state のアドレスは発火対象にしないため、
|
|
10693
10804
|
// `@stateName` 付きのパスは受け取った時点で落とす(黙って発火しないより良い)。
|
|
10694
10805
|
if (path.includes(STATE_NAME_SEPARATOR)) {
|
|
10695
|
-
raiseError(
|
|
10806
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not target another state ("${STATE_NAME_SEPARATOR}" is not allowed); watch only paths of its own state.${LINT_HINT}`);
|
|
10696
10807
|
}
|
|
10697
10808
|
// Object.prototype の継承名は `path in state` 系の判定を汚すため一律拒否する
|
|
10698
10809
|
// (processStreamsDeclaration と同じ防衛線)。
|
|
10699
10810
|
if (path in Object.prototype) {
|
|
10700
|
-
raiseError(
|
|
10811
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" must not be a property name inherited from Object.prototype (e.g. "__proto__", "constructor").`);
|
|
10701
10812
|
}
|
|
10702
10813
|
const pathInfo = getPathInfo(path);
|
|
10703
10814
|
// 空セグメント("a..b" / 先頭・末尾の ".")は getPathInfo が黙って受理してしまうため、
|
|
10704
10815
|
// ここで落とす。放置すると解決不能なアドレスを依存グラフへ登録することになる。
|
|
10705
10816
|
for (const segment of pathInfo.segments) {
|
|
10706
10817
|
if (segment.length === 0) {
|
|
10707
|
-
raiseError(
|
|
10818
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" has an empty path segment.${LINT_HINT}`);
|
|
10708
10819
|
}
|
|
10709
10820
|
}
|
|
10710
10821
|
if (pathInfo.wildcardCount > MAX_WILDCARD_DEPTH) {
|
|
10711
|
-
raiseError(
|
|
10822
|
+
raiseError(`[wcs/watch-declaration-invalid] ${STATE_WATCH_NAME} entry "${path}" exceeds the maximum wildcard depth (${MAX_WILDCARD_DEPTH}).`);
|
|
10712
10823
|
}
|
|
10713
10824
|
entries.set(path, {
|
|
10714
10825
|
path,
|
|
@@ -11171,6 +11282,20 @@ function getAllPropertyDescriptors(obj) {
|
|
|
11171
11282
|
* 双方向バインド・spread・initialSync の bindable 判定が**警告なしで**丸ごと死ぬ。
|
|
11172
11283
|
* 自前のファクトリが自前の reader に棄却される状態なので、生成前に落とす。
|
|
11173
11284
|
*/
|
|
11285
|
+
/**
|
|
11286
|
+
* did-you-mean の候補(エラーパス専用)。`$` 予約名と継承 `constructor` を除き、
|
|
11287
|
+
* `$bindables` には値プロパティだけ・`$commands` にはメソッドだけを提案する
|
|
11288
|
+
* (逆側を提案すると次は「is a method / is not a method」エラーに嵌まるため)。
|
|
11289
|
+
*/
|
|
11290
|
+
function dccCandidateNames(descriptors, kind) {
|
|
11291
|
+
return Object.keys(descriptors).filter((key) => {
|
|
11292
|
+
if (key.startsWith("$") || key === "constructor") {
|
|
11293
|
+
return false;
|
|
11294
|
+
}
|
|
11295
|
+
const isMethod = typeof descriptors[key].value === "function";
|
|
11296
|
+
return kind === "method" ? isMethod : !isMethod;
|
|
11297
|
+
});
|
|
11298
|
+
}
|
|
11174
11299
|
function readNameList(state, declarationName) {
|
|
11175
11300
|
const declared = state[declarationName];
|
|
11176
11301
|
if (typeof declared === "undefined") {
|
|
@@ -11223,7 +11348,7 @@ function processDccDeclarations(state) {
|
|
|
11223
11348
|
streamBackedBindables.push(name);
|
|
11224
11349
|
continue;
|
|
11225
11350
|
}
|
|
11226
|
-
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state
|
|
11351
|
+
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "value"))}`);
|
|
11227
11352
|
}
|
|
11228
11353
|
if (typeof descriptor.value === "function") {
|
|
11229
11354
|
raiseError(`${STATE_BINDABLES_NAME} entry "${name}" is a method. Declare it in ${STATE_COMMANDS_NAME} instead.`);
|
|
@@ -11232,7 +11357,7 @@ function processDccDeclarations(state) {
|
|
|
11232
11357
|
for (const name of commands) {
|
|
11233
11358
|
const descriptor = descriptors[name];
|
|
11234
11359
|
if (typeof descriptor === "undefined") {
|
|
11235
|
-
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state
|
|
11360
|
+
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not declared on the state.${didYouMean(name, dccCandidateNames(descriptors, "method"))}`);
|
|
11236
11361
|
}
|
|
11237
11362
|
if (typeof descriptor.value !== "function") {
|
|
11238
11363
|
raiseError(`${STATE_COMMANDS_NAME} entry "${name}" is not a method. Declare it in ${STATE_BINDABLES_NAME} instead.`);
|
|
@@ -14722,6 +14847,27 @@ function getWcsManifest() {
|
|
|
14722
14847
|
},
|
|
14723
14848
|
// 正本 STRUCTURAL_BINDING_TYPE_SET から導出(手書きの二重定義を排除)。
|
|
14724
14849
|
structuralDirectives: Array.from(STRUCTURAL_BINDING_TYPE_SET),
|
|
14850
|
+
modifiers: {
|
|
14851
|
+
flags: MODIFIER_FLAGS,
|
|
14852
|
+
keyValue: MODIFIER_KEYS,
|
|
14853
|
+
eventNamePrefix: EVENT_PROP_PREFIX,
|
|
14854
|
+
},
|
|
14855
|
+
indexParam: {
|
|
14856
|
+
prefix: INDEX_PARAM_PREFIX,
|
|
14857
|
+
maxDepth: MAX_WILDCARD_DEPTH,
|
|
14858
|
+
},
|
|
14859
|
+
bindingTypes: {
|
|
14860
|
+
elseKeyword: ELSE_KEYWORD,
|
|
14861
|
+
spread: SPREAD_PROP,
|
|
14862
|
+
eventPropertyPrefix: EVENT_PROP_PREFIX,
|
|
14863
|
+
propNamespaces: {
|
|
14864
|
+
eventToken: EVENT_TOKEN_NAMESPACE,
|
|
14865
|
+
command: COMMAND_NAMESPACE,
|
|
14866
|
+
class: CLASS_NAMESPACE,
|
|
14867
|
+
attr: ATTR_NAMESPACE,
|
|
14868
|
+
style: STYLE_NAMESPACE,
|
|
14869
|
+
},
|
|
14870
|
+
},
|
|
14725
14871
|
},
|
|
14726
14872
|
// 実装(Record のキー)から自動導出。手リストを持たない=ドリフトの構造的排除。
|
|
14727
14873
|
filters: Object.keys(outputBuiltinFilters),
|