@wcstack/state 1.11.1 → 1.12.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 +4 -4
- package/dist/index.esm.js +225 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
- selector
|
|
17
17
|
- reactive primitive をコンポーネントへ引き込むための glue code
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
これらはどれも、設計上存在しません。
|
|
20
20
|
|
|
21
21
|
なぜなら、このライブラリでは UI と状態の結合点を JavaScript の中に置かないからです。状態を「取り出して」コンポーネントへ渡すのではなく、HTML 側がパス文字列によって状態を参照します。要素は状態を所有せず、状態も要素を知りません。両者が共有するのはパスだけです。
|
|
22
22
|
|
|
@@ -1600,7 +1600,7 @@ const html = await renderToString(template, {
|
|
|
1600
1600
|
});
|
|
1601
1601
|
```
|
|
1602
1602
|
|
|
1603
|
-
これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を自動検出し、JSON
|
|
1603
|
+
これだけです。クライアント側の `@wcstack/state` は `<wcs-ssr>` 要素を自動検出し、JSON スナップショットから状態を復元し、再レンダリングなしでリアクティビティを再開します。
|
|
1604
1604
|
|
|
1605
1605
|
### 仕組み
|
|
1606
1606
|
|
|
@@ -1608,14 +1608,14 @@ const html = await renderToString(template, {
|
|
|
1608
1608
|
|---------|------|
|
|
1609
1609
|
| **サーバー** | `renderToString()` が happy-dom でテンプレートを実行、`$connectedCallback`(`fetch()` 含む)を実行し、全バインディングを適用、ハイドレーションデータを含む `<wcs-ssr>` 要素付きのレンダリング済み HTML を出力 |
|
|
1610
1610
|
| **クライアント** | `<wcs-state enable-ssr>` が `<wcs-ssr>` の JSON から状態をロード、`$connectedCallback` をスキップ、`hydrateBindings()` が既存の DOM にリアクティビティを接続 |
|
|
1611
|
-
| **フォールバック** |
|
|
1611
|
+
| **フォールバック** | サーバー/クライアントのバージョン不一致時、SSR DOM をクリーンアップして `buildBindings()` でフルクライアントサイドレンダリングを実行 |
|
|
1612
1612
|
|
|
1613
1613
|
### `enable-ssr` の動作
|
|
1614
1614
|
|
|
1615
1615
|
| コンテキスト | 動作 |
|
|
1616
1616
|
|------------|------|
|
|
1617
1617
|
| **サーバー**(`renderToString`) | 状態 JSON、テンプレートフラグメント、プロパティデータを含む `<wcs-ssr>` を生成 |
|
|
1618
|
-
|
|
|
1618
|
+
| **クライアント**(ハイドレーション) | `<wcs-ssr>` を読み取り、状態を復元、`$connectedCallback` をスキップ、既存 DOM のバインディングをハイドレート |
|
|
1619
1619
|
|
|
1620
1620
|
API の詳細は [`@wcstack/server` README](../server/README.ja.md) を参照してください。
|
|
1621
1621
|
|
package/dist/index.esm.js
CHANGED
|
@@ -59,7 +59,7 @@ function setConfig(partialConfig) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
var version$1 = "1.
|
|
62
|
+
var version$1 = "1.12.0";
|
|
63
63
|
var pkg = {
|
|
64
64
|
version: version$1};
|
|
65
65
|
|
|
@@ -137,6 +137,8 @@ const WEBCOMPONENT_STATE_READY_CALLBACK_NAME = "$stateReadyCallback";
|
|
|
137
137
|
const STATE_BINDABLES_NAME = "$bindables";
|
|
138
138
|
const STATE_COMMAND_TOKENS_NAME = "$commandTokens";
|
|
139
139
|
const STATE_COMMAND_NAMESPACE_NAME = "$command";
|
|
140
|
+
const STATE_EVENT_TOKENS_NAME = "$eventTokens";
|
|
141
|
+
const STATE_ON_NAME = "$on";
|
|
140
142
|
const DCC_DEFINITION_ATTRIBUTE = "data-wc-definition";
|
|
141
143
|
|
|
142
144
|
const _cache$4 = new Map();
|
|
@@ -1485,6 +1487,15 @@ function parseBindTextsForElement(bindText) {
|
|
|
1485
1487
|
else {
|
|
1486
1488
|
const stateResult = parseStatePart(statePart);
|
|
1487
1489
|
const propResult = parsePropPart(propPart);
|
|
1490
|
+
// eventToken.<prop>: <name> は要素 dispatch を state へ流す pub/sub 配線。
|
|
1491
|
+
// 値適用ではないため bindingType 'event' として listener attach 経路に乗せる。
|
|
1492
|
+
if (propResult.propSegments[0] === 'eventToken') {
|
|
1493
|
+
return {
|
|
1494
|
+
...propResult,
|
|
1495
|
+
...stateResult,
|
|
1496
|
+
bindingType: 'event',
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1488
1499
|
if (propResult.propSegments[0].startsWith('on')) {
|
|
1489
1500
|
return {
|
|
1490
1501
|
...propResult,
|
|
@@ -1748,9 +1759,14 @@ function createStateAddress(pathInfo, listIndex) {
|
|
|
1748
1759
|
}
|
|
1749
1760
|
}
|
|
1750
1761
|
|
|
1762
|
+
// command-token / event-token が共有する pub/sub プリミティブ。
|
|
1751
1763
|
// _subscribers は Set のため挿入順を保持する。
|
|
1752
1764
|
// emit() は subscribe() された順に呼び出され、戻り値配列も同じ順序で返る。
|
|
1753
|
-
|
|
1765
|
+
//
|
|
1766
|
+
// 「誰が subscribe し誰が emit するか」だけが command / event の違い:
|
|
1767
|
+
// - command-token: element が subscribe / state が emit
|
|
1768
|
+
// - event-token: state(`$on`) が subscribe / element(listener) が emit
|
|
1769
|
+
class Token {
|
|
1754
1770
|
_name;
|
|
1755
1771
|
_subscribers = new Set();
|
|
1756
1772
|
constructor(name) {
|
|
@@ -1779,6 +1795,11 @@ class CommandToken {
|
|
|
1779
1795
|
return results;
|
|
1780
1796
|
}
|
|
1781
1797
|
}
|
|
1798
|
+
|
|
1799
|
+
// CommandToken は共有 pub/sub プリミティブ Token の薄い特化。
|
|
1800
|
+
// instanceof による型判別を成立させるため独立クラスとして維持する。
|
|
1801
|
+
class CommandToken extends Token {
|
|
1802
|
+
}
|
|
1782
1803
|
function isCommandToken(value) {
|
|
1783
1804
|
return value instanceof CommandToken;
|
|
1784
1805
|
}
|
|
@@ -1877,6 +1898,129 @@ function attachEventHandler(binding) {
|
|
|
1877
1898
|
return true;
|
|
1878
1899
|
}
|
|
1879
1900
|
|
|
1901
|
+
// EventToken は共有 pub/sub プリミティブ Token の薄い特化(element→state 方向)。
|
|
1902
|
+
// instanceof による型判別を成立させるため独立クラスとして維持する。
|
|
1903
|
+
class EventToken extends Token {
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const registryByStateElement$1 = new WeakMap();
|
|
1907
|
+
function getOrCreateEventToken(stateElement, name) {
|
|
1908
|
+
let registry = registryByStateElement$1.get(stateElement);
|
|
1909
|
+
if (typeof registry === "undefined") {
|
|
1910
|
+
registry = new Map();
|
|
1911
|
+
registryByStateElement$1.set(stateElement, registry);
|
|
1912
|
+
}
|
|
1913
|
+
let token = registry.get(name);
|
|
1914
|
+
if (typeof token === "undefined") {
|
|
1915
|
+
token = new EventToken(name);
|
|
1916
|
+
registry.set(name, token);
|
|
1917
|
+
}
|
|
1918
|
+
return token;
|
|
1919
|
+
}
|
|
1920
|
+
function clearEventTokenRegistry(stateElement) {
|
|
1921
|
+
registryByStateElement$1.delete(stateElement);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* eventToken.<propertyName>: <eventTokenName> バインディングの attach ハンドラ。
|
|
1926
|
+
*
|
|
1927
|
+
* command-token の双対(element→state)。要素が dispatch する CustomEvent を受けて
|
|
1928
|
+
* event-token を emit し、state 側の `$on` ハンドラ群へ pub/sub で配送する。
|
|
1929
|
+
*
|
|
1930
|
+
* 設計(MVP スコープ: wc-bindable カスタム要素のみ):
|
|
1931
|
+
* - キーは生イベント名ではなく **wcBindable property 名**。実 DOM イベント名は
|
|
1932
|
+
* wcBindable.properties[].event から解決する(command-token が wcBindable.commands で
|
|
1933
|
+
* 検証するのと対称。コロンを含む namespaced event 名と binding 構文の `:` 衝突も回避)。
|
|
1934
|
+
* - <prop> が wcBindable.properties に宣言されていることは attach 時に検証する
|
|
1935
|
+
* (要素クラス参照のみで DOM 接続に非依存。fail-fast / typo 耐性)。
|
|
1936
|
+
* - <eventTokenName> が $eventTokens に宣言されていることは **発火時** に検証する
|
|
1937
|
+
* (state 解決が必要なため。詳細は下記の fire-time 解決の注記を参照)。
|
|
1938
|
+
* - subscriber 引数規約は `(state, event, ...listIndexes)`。
|
|
1939
|
+
* - modifier `#prevent` / `#stop` は既存イベント binding と同等にサポート。
|
|
1940
|
+
*
|
|
1941
|
+
* token はイベント発火ごとに registry から解決する(getOrCreateEventToken)。これにより
|
|
1942
|
+
* state の再 set で registry が作り直されても最新の subscriber 群へ配送できる。
|
|
1943
|
+
*
|
|
1944
|
+
* state element の解決と `$eventTokens` 検証は **発火時** に行う(attach 時ではない)。
|
|
1945
|
+
* 構造ブロック(for/if)や SSR hydration では、binding 初期化時にノードが detached な
|
|
1946
|
+
* DocumentFragment / wrapper 上にあり、その時点では element.getRootNode() から state を
|
|
1947
|
+
* 解決できないため。onclick / two-way ハンドラと同じく fire-time 解決に揃えている。
|
|
1948
|
+
*/
|
|
1949
|
+
const listenerByBinding = new WeakMap();
|
|
1950
|
+
function getWcBindable$1(element) {
|
|
1951
|
+
const customTagName = getCustomElement(element);
|
|
1952
|
+
if (customTagName === null) {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
// attach 側で未定義要素は whenDefined 後に再試行するため、ここに来る時点で customClass は定義済み。
|
|
1956
|
+
const customClass = customElements.get(customTagName);
|
|
1957
|
+
const bindable = customClass?.wcBindable;
|
|
1958
|
+
if (bindable?.protocol === "wc-bindable" && bindable?.version === 1) {
|
|
1959
|
+
return bindable;
|
|
1960
|
+
}
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
function attachEventTokenHandler(binding) {
|
|
1964
|
+
if (binding.propSegments[0] !== "eventToken") {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
const element = binding.node;
|
|
1968
|
+
// カスタム要素が未定義なら定義後に再試行(wcBindable が必要なため)。
|
|
1969
|
+
const customTagName = getCustomElement(element);
|
|
1970
|
+
if (customTagName !== null && customElements.get(customTagName) === undefined) {
|
|
1971
|
+
customElements.whenDefined(customTagName).then(() => {
|
|
1972
|
+
attachEventTokenHandler(binding);
|
|
1973
|
+
});
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
// 再評価で二重 attach しない。
|
|
1977
|
+
if (listenerByBinding.has(binding)) {
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
const propertyName = binding.propSegments[1];
|
|
1981
|
+
if (typeof propertyName !== "string" || propertyName.length === 0) {
|
|
1982
|
+
raiseError(`eventToken binding requires a property name (e.g., "eventToken.error").`);
|
|
1983
|
+
}
|
|
1984
|
+
const bindable = getWcBindable$1(element);
|
|
1985
|
+
if (bindable === null) {
|
|
1986
|
+
raiseError(`eventToken binding requires a wc-bindable custom element. <${element.tagName.toLowerCase()}> is not wc-bindable.`);
|
|
1987
|
+
}
|
|
1988
|
+
const propDesc = bindable.properties.find((p) => p.name === propertyName);
|
|
1989
|
+
if (typeof propDesc === "undefined") {
|
|
1990
|
+
raiseError(`Property "${propertyName}" is not declared in wcBindable.properties of <${element.tagName.toLowerCase()}>.`);
|
|
1991
|
+
}
|
|
1992
|
+
const eventName = propDesc.event;
|
|
1993
|
+
const tokenName = binding.statePathName;
|
|
1994
|
+
const stateName = binding.stateName;
|
|
1995
|
+
const modifiers = binding.propModifiers;
|
|
1996
|
+
const handler = (event) => {
|
|
1997
|
+
if (modifiers.includes("prevent"))
|
|
1998
|
+
event.preventDefault();
|
|
1999
|
+
if (modifiers.includes("stop"))
|
|
2000
|
+
event.stopPropagation();
|
|
2001
|
+
// state は発火時の live root から解決する(attach 時は detached の可能性があるため)。
|
|
2002
|
+
const rootNode = element.getRootNode();
|
|
2003
|
+
const stateElement = getStateElementByName(rootNode, stateName);
|
|
2004
|
+
if (stateElement === null) {
|
|
2005
|
+
raiseError(`State element with name "${stateName}" not found for eventToken handler.`);
|
|
2006
|
+
}
|
|
2007
|
+
if (!stateElement.eventTokenNames.has(tokenName)) {
|
|
2008
|
+
raiseError(`eventToken "${tokenName}" is not declared in $eventTokens of state "${stateName}".`);
|
|
2009
|
+
}
|
|
2010
|
+
const loopContext = getLoopContextByNode(element);
|
|
2011
|
+
stateElement.createStateAsync("writable", async (state) => {
|
|
2012
|
+
state[setLoopContextSymbol](loopContext, () => {
|
|
2013
|
+
const indexes = loopContext?.listIndex.indexes ?? [];
|
|
2014
|
+
const token = getOrCreateEventToken(stateElement, tokenName);
|
|
2015
|
+
return token.emit(state, event, ...indexes);
|
|
2016
|
+
});
|
|
2017
|
+
});
|
|
2018
|
+
};
|
|
2019
|
+
element.addEventListener(eventName, handler);
|
|
2020
|
+
listenerByBinding.set(binding, { eventName, handler });
|
|
2021
|
+
return true;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1880
2024
|
const CHECK_TYPES = new Set(['radio', 'checkbox']);
|
|
1881
2025
|
const DEFAULT_VALUE_PROP_NAMES = new Set(['value', 'valueAsNumber', 'valueAsDate']);
|
|
1882
2026
|
function isPossibleTwoWay(node, propName) {
|
|
@@ -3871,6 +4015,10 @@ function _initializeBindings(allBindings) {
|
|
|
3871
4015
|
if (attachEventHandler(binding)) {
|
|
3872
4016
|
continue;
|
|
3873
4017
|
}
|
|
4018
|
+
// event token (element → state)
|
|
4019
|
+
if (attachEventTokenHandler(binding)) {
|
|
4020
|
+
continue;
|
|
4021
|
+
}
|
|
3874
4022
|
// two-way binding
|
|
3875
4023
|
attachTwowayEventHandler(binding);
|
|
3876
4024
|
// radio binding
|
|
@@ -4398,6 +4546,8 @@ function collectBindingsFromLiveNodes(nodes) {
|
|
|
4398
4546
|
replaceToReplaceNode(binding);
|
|
4399
4547
|
if (attachEventHandler(binding))
|
|
4400
4548
|
continue;
|
|
4549
|
+
if (attachEventTokenHandler(binding))
|
|
4550
|
+
continue;
|
|
4401
4551
|
attachTwowayEventHandler(binding);
|
|
4402
4552
|
attachRadioEventHandler(binding);
|
|
4403
4553
|
attachCheckboxEventHandler(binding);
|
|
@@ -4637,6 +4787,9 @@ async function hydrateBindings(root) {
|
|
|
4637
4787
|
if (attachEventHandler(binding)) {
|
|
4638
4788
|
continue;
|
|
4639
4789
|
}
|
|
4790
|
+
if (attachEventTokenHandler(binding)) {
|
|
4791
|
+
continue;
|
|
4792
|
+
}
|
|
4640
4793
|
attachTwowayEventHandler(binding);
|
|
4641
4794
|
attachRadioEventHandler(binding);
|
|
4642
4795
|
attachCheckboxEventHandler(binding);
|
|
@@ -5368,6 +5521,67 @@ function clearCommandNamespace(stateElement) {
|
|
|
5368
5521
|
namespaceProxyByStateElement.delete(stateElement);
|
|
5369
5522
|
}
|
|
5370
5523
|
|
|
5524
|
+
/**
|
|
5525
|
+
* `$eventTokens: ["a", "b", ...]` 配列宣言を解析し、宣言された名前群を Set で返す。
|
|
5526
|
+
*
|
|
5527
|
+
* event-token は command-token の双対(element→state 方向)。要素が dispatch する
|
|
5528
|
+
* イベントを `eventToken.<prop>: <name>` で token に流し、state 側は `$on` マップで受ける。
|
|
5529
|
+
* ここで宣言された名前のみが `eventToken.X` / `$on` の有効なチャネル名になる(typo 耐性)。
|
|
5530
|
+
*
|
|
5531
|
+
* 対応している宣言形式は **オブジェクトリテラル** のみ。
|
|
5532
|
+
*/
|
|
5533
|
+
function processEventTokensDeclaration(state) {
|
|
5534
|
+
const names = new Set();
|
|
5535
|
+
const declared = state[STATE_EVENT_TOKENS_NAME];
|
|
5536
|
+
if (typeof declared === "undefined") {
|
|
5537
|
+
return names;
|
|
5538
|
+
}
|
|
5539
|
+
if (!Array.isArray(declared)) {
|
|
5540
|
+
raiseError(`${STATE_EVENT_TOKENS_NAME} must be an array of strings.`);
|
|
5541
|
+
}
|
|
5542
|
+
for (const name of declared) {
|
|
5543
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
5544
|
+
raiseError(`${STATE_EVENT_TOKENS_NAME} entries must be non-empty strings.`);
|
|
5545
|
+
}
|
|
5546
|
+
if (names.has(name)) {
|
|
5547
|
+
raiseError(`${STATE_EVENT_TOKENS_NAME} entry "${name}" is duplicated.`);
|
|
5548
|
+
}
|
|
5549
|
+
names.add(name);
|
|
5550
|
+
}
|
|
5551
|
+
return names;
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
/**
|
|
5555
|
+
* `$on: { <name>: (state, event, ...listIndexes) => {...} }` マップを解析し、
|
|
5556
|
+
* 各ハンドラを対応する event-token に subscribe する(state 側の受信配線)。
|
|
5557
|
+
*
|
|
5558
|
+
* - `$on` のキーは `$eventTokens` で宣言済みでなければならない(typo 耐性)。
|
|
5559
|
+
* - 各値は関数でなければならない。
|
|
5560
|
+
* - 引数規約は `(state, event, ...listIndexes)`。`this` 束縛は行わず引数で state を渡すため
|
|
5561
|
+
* アロー関数で書ける(command-token の emit 規約と対称)。
|
|
5562
|
+
*
|
|
5563
|
+
* `$eventTokens` で宣言されたが `$on` に対応が無い token は subscriber ゼロ(emit は no-op)。
|
|
5564
|
+
*/
|
|
5565
|
+
function processOnDeclaration(stateElement, state, eventTokenNames) {
|
|
5566
|
+
const declared = state[STATE_ON_NAME];
|
|
5567
|
+
if (typeof declared === "undefined") {
|
|
5568
|
+
return;
|
|
5569
|
+
}
|
|
5570
|
+
if (typeof declared !== "object" || declared === null) {
|
|
5571
|
+
raiseError(`${STATE_ON_NAME} must be an object mapping event-token names to handler functions.`);
|
|
5572
|
+
}
|
|
5573
|
+
for (const [name, handler] of Object.entries(declared)) {
|
|
5574
|
+
if (!eventTokenNames.has(name)) {
|
|
5575
|
+
raiseError(`${STATE_ON_NAME} entry "${name}" is not declared in $eventTokens.`);
|
|
5576
|
+
}
|
|
5577
|
+
if (typeof handler !== "function") {
|
|
5578
|
+
raiseError(`${STATE_ON_NAME} entry "${name}" must be a function.`);
|
|
5579
|
+
}
|
|
5580
|
+
const token = getOrCreateEventToken(stateElement, name);
|
|
5581
|
+
token.subscribe(handler);
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5371
5585
|
function getterFn(name) {
|
|
5372
5586
|
return function () {
|
|
5373
5587
|
const stateEl = this.stateElement;
|
|
@@ -7199,6 +7413,7 @@ class State extends HTMLElement {
|
|
|
7199
7413
|
_boundComponentStateProp = null;
|
|
7200
7414
|
_bindableEventMap = {};
|
|
7201
7415
|
_commandTokenNames = new Set();
|
|
7416
|
+
_eventTokenNames = new Set();
|
|
7202
7417
|
constructor() {
|
|
7203
7418
|
super();
|
|
7204
7419
|
this._initializePromise = new Promise((resolve) => {
|
|
@@ -7222,7 +7437,11 @@ class State extends HTMLElement {
|
|
|
7222
7437
|
}
|
|
7223
7438
|
set _state(value) {
|
|
7224
7439
|
this._commandTokenNames = processCommandTokensDeclaration(value);
|
|
7440
|
+
this._eventTokenNames = processEventTokensDeclaration(value);
|
|
7225
7441
|
this.__state = value;
|
|
7442
|
+
// 再 set 時に二重 subscribe しないよう registry をクリアしてから $on を配線し直す。
|
|
7443
|
+
clearEventTokenRegistry(this);
|
|
7444
|
+
processOnDeclaration(this, value, this._eventTokenNames);
|
|
7226
7445
|
this._listPaths.clear();
|
|
7227
7446
|
this._elementPaths.clear();
|
|
7228
7447
|
this._getterPaths.clear();
|
|
@@ -7436,6 +7655,7 @@ class State extends HTMLElement {
|
|
|
7436
7655
|
setStateElementByName(this.rootNode, this._name, null);
|
|
7437
7656
|
clearCommandTokenRegistry(this);
|
|
7438
7657
|
clearCommandNamespace(this);
|
|
7658
|
+
clearEventTokenRegistry(this);
|
|
7439
7659
|
this._rootNode = null;
|
|
7440
7660
|
}
|
|
7441
7661
|
}
|
|
@@ -7484,6 +7704,9 @@ class State extends HTMLElement {
|
|
|
7484
7704
|
get commandTokenNames() {
|
|
7485
7705
|
return this._commandTokenNames;
|
|
7486
7706
|
}
|
|
7707
|
+
get eventTokenNames() {
|
|
7708
|
+
return this._eventTokenNames;
|
|
7709
|
+
}
|
|
7487
7710
|
setBindableEventMap(map) {
|
|
7488
7711
|
this._bindableEventMap = map;
|
|
7489
7712
|
}
|