aberdeen 1.0.0 → 1.0.5
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.md +137 -141
- package/dist/aberdeen.d.ts +15 -5
- package/dist/aberdeen.js +15 -10
- package/dist/aberdeen.js.map +3 -3
- package/dist-min/aberdeen.js +3 -3
- package/dist-min/aberdeen.js.map +3 -3
- package/html-to-aberdeen +354 -0
- package/package.json +9 -6
- package/src/aberdeen.ts +29 -15
package/README.md
CHANGED
|
@@ -1,181 +1,177 @@
|
|
|
1
|
-
|
|
1
|
+
# [Aberdeen](https://aberdeenjs.org/) [](https://github.com/vanviegen/aberdeen/blob/master/LICENSE.txt) [](https://badge.fury.io/js/aberdeen)  [](https://github.com/vanviegen/aberdeen)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build fast reactive UIs in pure TypeScript/JavaScript without a virtual DOM.
|
|
4
|
+
|
|
5
|
+
Aberdeen's approach is refreshingly simple:
|
|
6
|
+
|
|
7
|
+
> Use many small anonymous functions for emitting DOM elements, and automatically rerun them when their underlying data changes. JavaScript `Proxy` is used to track reads and updates to this data, which can consist of anything, from simple values to complex, typed, and deeply nested data structures.
|
|
4
8
|
|
|
5
9
|
## Why use Aberdeen?
|
|
6
10
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- It comes with batteries included:
|
|
13
|
-
- Client-side routing.
|
|
14
|
-
- Revertible patches, for optimistic user-interface updates.
|
|
15
|
-
- Component-local CSS generator.
|
|
16
|
-
- Helper functions for reactively working with data, such as for deriving, (multi)mapping, filtering, partitioning and counting.
|
|
17
|
-
- A couple of add/remove transition effects, to get you started.
|
|
11
|
+
- 🎩 **Simple:** Express UIs naturally in JavaScript/TypeScript, without build steps or JSX, and with a minimal amount of concepts you need to learn.
|
|
12
|
+
- ⏩ **Fast:** No virtual DOM. Aberdeen intelligently updates only the minimal, necessary parts of your UI when proxied data changes.
|
|
13
|
+
- 👥 **Awesome lists**: It's very easy and performant to reactively display data sorted by whatever you like.
|
|
14
|
+
- 🔬 **Tiny:** Around 5KB (minimized and gzipped) and with zero runtime dependencies.
|
|
15
|
+
- 🔋 **Batteries included**: Comes with client-side routing, revertible patches for optimistic user-interface updates, component-local CSS, helper functions for transforming reactive data (mapping, partitioning, filtering, etc) and hide/unhide transition effects. No bikeshedding required!
|
|
18
16
|
|
|
19
17
|
## Why *not* use Aberdeen?
|
|
20
18
|
|
|
21
|
-
- There are not many of us -Aberdeen developers- yet, so don't expect terribly helpful
|
|
22
|
-
- You'd have to code things yourself, instead of duct-taping together a gazillion React ecosystem libraries.
|
|
19
|
+
- 🤷 **Lack of community:** There are not many of us -Aberdeen developers- yet, so don't expect terribly helpful Stack Overflow/AI answers.
|
|
20
|
+
- 📚 **Lack of ecosystem:** You'd have to code things yourself, instead of duct-taping together a gazillion React ecosystem libraries.
|
|
23
21
|
|
|
24
|
-
##
|
|
22
|
+
## Examples
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
First, let's start with the obligatory reactive counter example. If you're reading this on [the official website](https://aberdeenjs.org) you should see a working demo below the code, and an 'edit' button in the top-right corner of the code, to play around.
|
|
27
25
|
|
|
28
26
|
```javascript
|
|
29
|
-
import {$, proxy,
|
|
30
|
-
|
|
31
|
-
// Helper functions
|
|
32
|
-
|
|
33
|
-
function calculateWinner(board) {
|
|
34
|
-
const lines = [
|
|
35
|
-
[0, 1, 2], [3, 4, 5], [6, 7, 8], // horizontal
|
|
36
|
-
[0, 3, 6], [1, 4, 7], [2, 5, 8], // vertical
|
|
37
|
-
[0, 4, 8], [2, 4, 6] // diagonal
|
|
38
|
-
];
|
|
39
|
-
for (const [a, b, c] of lines) {
|
|
40
|
-
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
|
|
41
|
-
return board[a];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
27
|
+
import {$, proxy, ref} from 'aberdeen';
|
|
45
28
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
29
|
+
// Define some state as a proxied (observable) object
|
|
30
|
+
const state = proxy({question: "How many roads must a man walk down?", answer: 42});
|
|
49
31
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
32
|
+
$('h3', () => {
|
|
33
|
+
// This function reruns whenever the question or the answer changes
|
|
34
|
+
$(`:${state.question} ↪ ${state.answer || 'Blowing in the wind'}`)
|
|
35
|
+
});
|
|
53
36
|
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
// Two-way bind state.question to an <input>
|
|
38
|
+
$('input', {placeholder: 'Question', bind: ref(state, 'question')})
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
// Allow state.answer to be modified using both an <input> and buttons
|
|
41
|
+
$('div.row', {$marginTop: '1em'}, () => {
|
|
42
|
+
$('button:-', {click: () => state.answer--});
|
|
43
|
+
$('input', {type: 'number', bind: ref(state, 'answer')})
|
|
44
|
+
$('button:+', {click: () => state.answer++});
|
|
45
|
+
});
|
|
46
|
+
```
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
const newBoard = board.slice();
|
|
62
|
-
newBoard[position] = getCurrentMarker(board);
|
|
63
|
-
|
|
64
|
-
// Truncate any future states, and write a new future
|
|
65
|
-
history.current++;
|
|
66
|
-
history.boards.length = history.current;
|
|
67
|
-
history.boards.push(newBoard);
|
|
68
|
-
}
|
|
48
|
+
Okay, next up is a somewhat more complex app - a todo-list with the following behavior:
|
|
69
49
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
gridTemplateColumns: '1fr 1fr 1fr',
|
|
77
|
-
'> *': {
|
|
78
|
-
width: '2em',
|
|
79
|
-
height: '2em',
|
|
80
|
-
padding: 0,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
50
|
+
- New items open in an 'editing state'.
|
|
51
|
+
- Items that are in 'editing state' show a text input, a save button and a cancel button. Done status cannot be toggled while editing.
|
|
52
|
+
- Pressing one of the buttons, or pressing enter will transition from 'editing state' to 'viewing state', saving the new label text unless cancel was pressed.
|
|
53
|
+
- In 'viewing state', the label is shown as non-editable. There's an 'Edit' link, that will transition the item to 'editing state'. Clicking anywhere else will toggle the done status.
|
|
54
|
+
- The list of items is sorted alphabetically by label. Items move when 'save' changes their label.
|
|
55
|
+
- Items that are created, moved or deleted grow and shrink as appropriate.
|
|
83
56
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function drawBoard(history) {
|
|
87
|
-
$('div', boardStyle, () => {
|
|
88
|
-
for(let pos=0; pos<9; pos++) {
|
|
89
|
-
$('button.square', () => {
|
|
90
|
-
let marker = getBoard(history)[pos];
|
|
91
|
-
if (marker) {
|
|
92
|
-
$({ text: marker });
|
|
93
|
-
} else {
|
|
94
|
-
$({ click: () => markSquare(history, pos) });
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
}
|
|
57
|
+
Pfew.. now let's look at the code:
|
|
100
58
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const board = getBoard(history);
|
|
105
|
-
const winner = calculateWinner(board);
|
|
106
|
-
if (winner) {
|
|
107
|
-
$(`:Winner: ${winner}!`);
|
|
108
|
-
} else if (board.filter(square=>square).length === 9) {
|
|
109
|
-
$(`:It's a draw...`);
|
|
110
|
-
} else {
|
|
111
|
-
$(`:Current player: ${getCurrentMarker(board)}`);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
59
|
+
```typescript
|
|
60
|
+
import {$, proxy, onEach, insertCss, peek, observe, unproxy, ref} from "aberdeen";
|
|
61
|
+
import {grow, shrink} from "aberdeen/transitions";
|
|
115
62
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
$('button', {
|
|
121
|
-
// A text node:
|
|
122
|
-
text: index,
|
|
123
|
-
// Conditional css class:
|
|
124
|
-
".outline": observe(() => history.current != index),
|
|
125
|
-
// Inline styles:
|
|
126
|
-
$marginRight: "0.5em",
|
|
127
|
-
$marginTop: "0.5em",
|
|
128
|
-
// Event listener:
|
|
129
|
-
click: () => history.current = index,
|
|
130
|
-
});
|
|
131
|
-
});
|
|
63
|
+
// We'll use a simple class to store our data.
|
|
64
|
+
class TodoItem {
|
|
65
|
+
constructor(public label: string = '', public done: boolean = false) {}
|
|
66
|
+
toggle() { this.done = !this.done; }
|
|
132
67
|
}
|
|
133
68
|
|
|
69
|
+
// The top-level user interface.
|
|
134
70
|
function drawMain() {
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
71
|
+
// Add some initial items. We'll wrap a proxy() around it!
|
|
72
|
+
let items: TodoItem[] = proxy([
|
|
73
|
+
new TodoItem('Make todo-list demo', true),
|
|
74
|
+
new TodoItem('Learn Aberdeen', false),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// Draw the list, ordered by label.
|
|
78
|
+
onEach(items, drawItem, item => item.label);
|
|
140
79
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
$('
|
|
144
|
-
|
|
145
|
-
|
|
80
|
+
// Add item and delete checked buttons.
|
|
81
|
+
$('div.row', () => {
|
|
82
|
+
$('button:+', {
|
|
83
|
+
click: () => items.push(new TodoItem("")),
|
|
84
|
+
});
|
|
85
|
+
$('button.outline:Delete checked', {
|
|
86
|
+
click: () => {
|
|
87
|
+
for(let idx in items) {
|
|
88
|
+
if (items[idx].done) delete items[idx];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
146
91
|
});
|
|
147
92
|
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Called for each todo list item.
|
|
96
|
+
function drawItem(item) {
|
|
97
|
+
// Items without a label open in editing state.
|
|
98
|
+
// Note that we're creating this proxy outside the `div.row` scope
|
|
99
|
+
// create below, so that it will persist when that state reruns.
|
|
100
|
+
let editing: {value: boolean} = proxy(item.label == '');
|
|
101
|
+
|
|
102
|
+
$('div.row', todoItemStyle, {create:grow, destroy: shrink}, () => {
|
|
103
|
+
// Conditionally add a class to `div.row`, based on item.done
|
|
104
|
+
$({".done": ref(item,'done')});
|
|
105
|
+
|
|
106
|
+
// The checkmark is hidden using CSS
|
|
107
|
+
$('div.checkmark:✅');
|
|
108
|
+
|
|
109
|
+
if (editing.value) {
|
|
110
|
+
// Label <input>. Save using enter or button.
|
|
111
|
+
function save() {
|
|
112
|
+
editing.value = false;
|
|
113
|
+
item.label = inputElement.value;
|
|
114
|
+
}
|
|
115
|
+
let inputElement = $('input', {
|
|
116
|
+
placeholder: 'Label',
|
|
117
|
+
value: item.label,
|
|
118
|
+
keydown: e => e.key==='Enter' && save(),
|
|
119
|
+
});
|
|
120
|
+
$('button.outline:Cancel', {click: () => editing.value = false});
|
|
121
|
+
$('button:Save', {click: save});
|
|
122
|
+
} else {
|
|
123
|
+
// Label as text.
|
|
124
|
+
$('p:' + item.label);
|
|
125
|
+
|
|
126
|
+
// Edit icon, if not done.
|
|
127
|
+
if (!item.done) {
|
|
128
|
+
$('a:Edit', {
|
|
129
|
+
click: e => {
|
|
130
|
+
editing.value = true;
|
|
131
|
+
e.stopPropagation(); // We don't want to toggle as well.
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Clicking a row toggles done.
|
|
137
|
+
$({click: () => item.done = !item.done, $cursor: 'pointer'});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
148
140
|
}
|
|
149
141
|
|
|
150
|
-
//
|
|
142
|
+
// Insert some component-local CSS, specific for this demo.
|
|
143
|
+
const todoItemStyle = insertCss({
|
|
144
|
+
marginBottom: "0.5rem",
|
|
145
|
+
".checkmark": {
|
|
146
|
+
opacity: 0.2,
|
|
147
|
+
},
|
|
148
|
+
"&.done": {
|
|
149
|
+
textDecoration: "line-through",
|
|
150
|
+
".checkmark": {
|
|
151
|
+
opacity: 1,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
151
155
|
|
|
156
|
+
// Go!
|
|
152
157
|
drawMain();
|
|
153
158
|
```
|
|
154
159
|
|
|
155
160
|
Some further examples:
|
|
156
161
|
|
|
157
|
-
- [Input
|
|
158
|
-
- [
|
|
159
|
-
- [
|
|
160
|
-
- [
|
|
162
|
+
- [Input demo](https://aberdeenjs.org/examples/input/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/input)
|
|
163
|
+
- [Tic Tac Toe demo](https://aberdeenjs.org/examples/tictactoe/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/tictactoe)
|
|
164
|
+
- [List demo](https://aberdeenjs.org/examples/list/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/list)
|
|
165
|
+
- [Routing demo](https://aberdeenjs.org/examples/router/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/router)
|
|
166
|
+
- [JS Framework Benchmark demo](https://aberdeenjs.org/examples/js-framework-benchmark/) - [Source](https://github.com/vanviegen/aberdeen/tree/master/examples/js-framework-benchmark)
|
|
161
167
|
|
|
168
|
+
## Learning Aberdeen
|
|
162
169
|
|
|
163
|
-
|
|
170
|
+
- [Tutorial](https://aberdeenjs.org/Tutorial/)
|
|
171
|
+
- [Reference documentation](https://aberdeenjs.org/modules.html)
|
|
164
172
|
|
|
165
|
-
|
|
166
|
-
- [Reference documentation](https://vanviegen.github.io/aberdeen/modules.html)
|
|
173
|
+
And you may want to study the examples above, of course!
|
|
167
174
|
|
|
168
175
|
## News
|
|
169
176
|
|
|
170
|
-
- **2025-
|
|
171
|
-
|
|
172
|
-
## Roadmap
|
|
173
|
-
|
|
174
|
-
- [x] Support for (dis)appear transitions.
|
|
175
|
-
- [x] A better alternative for scheduleTask.
|
|
176
|
-
- [x] A simple router.
|
|
177
|
-
- [x] Optimistic client-side predictions.
|
|
178
|
-
- [x] Performance profiling and tuning regarding lists.
|
|
179
|
-
- [x] Support for (component local) CSS
|
|
180
|
-
- [ ] Architecture document.
|
|
181
|
-
- [ ] SVG support.
|
|
177
|
+
- **2025-05-07**: After five years of working on this library on and off, I'm finally happy with its API and the developer experience it offers. I'm calling it 1.0! To celebrate, I've created some pretty fancy (if I may say so myself) interactive documentation and a tutorial.
|
package/dist/aberdeen.d.ts
CHANGED
|
@@ -308,22 +308,32 @@ export declare function ref<T extends TargetType, K extends keyof T>(target: T,
|
|
|
308
308
|
* - `{html: string}`: Add the value as HTML to the *current* element. This should only be used in exceptional situations. And of course, beware of XSS.
|
|
309
309
|
* - `{element: Node}`: Add a pre-existing HTML `Node` to the *current* element.
|
|
310
310
|
*
|
|
311
|
+
* @returns The most inner DOM element that was created (not counting text nodes nor elements created by content functions),
|
|
312
|
+
* or undefined if no elements were created.
|
|
311
313
|
*
|
|
312
314
|
* @example Create Element
|
|
313
315
|
* ```typescript
|
|
314
316
|
* $('button.secondary.outline:Submit', {
|
|
315
|
-
* disabled:
|
|
317
|
+
* disabled: false,
|
|
316
318
|
* click: () => console.log('Clicked!'),
|
|
317
319
|
* $color: 'red'
|
|
318
320
|
* });
|
|
319
321
|
* ```
|
|
320
322
|
*
|
|
321
|
-
* @example Nested Elements
|
|
323
|
+
* @example Create Nested Elements
|
|
324
|
+
* ```typescript
|
|
325
|
+
* let inputElement: Element = $('label:Click me', 'input', {type: 'checkbox'});
|
|
326
|
+
* // You should usually not touch raw DOM elements, unless when integrating
|
|
327
|
+
* // with non-Aberdeen code.
|
|
328
|
+
* console.log('DOM element:', inputElement);
|
|
329
|
+
* ```
|
|
330
|
+
*
|
|
331
|
+
* @example Content Functions & Reactive Scope
|
|
322
332
|
* ```typescript
|
|
323
333
|
* const state = proxy({ count: 0 });
|
|
324
334
|
* $('div', () => { // Outer element
|
|
325
335
|
* // This scope re-renders when state.count changes
|
|
326
|
-
* $(
|
|
336
|
+
* $(`p:Count is ${state.count}`);
|
|
327
337
|
* $('button:Increment', { click: () => state.count++ });
|
|
328
338
|
* });
|
|
329
339
|
* ```
|
|
@@ -348,7 +358,7 @@ export declare function ref<T extends TargetType, K extends keyof T>(target: T,
|
|
|
348
358
|
* });
|
|
349
359
|
* ```
|
|
350
360
|
*/
|
|
351
|
-
export declare function $(...args: (string | null | undefined | false | (() => void) | Record<string, any>)[]): void;
|
|
361
|
+
export declare function $(...args: (string | null | undefined | false | (() => void) | Record<string, any>)[]): void | Element;
|
|
352
362
|
/**
|
|
353
363
|
* Inserts CSS rules into the document, optionally scoping them with a unique class name.
|
|
354
364
|
*
|
|
@@ -465,7 +475,7 @@ export declare function setErrorHandler(handler?: (error: Error) => boolean | un
|
|
|
465
475
|
* call or a {@link $} element's render function).
|
|
466
476
|
*
|
|
467
477
|
* **Note:** While this provides access to the DOM element, directly manipulating it outside
|
|
468
|
-
* of Aberdeen's control is generally discouraged. Prefer
|
|
478
|
+
* of Aberdeen's control is generally discouraged. Prefer reactive updates using {@link $}.
|
|
469
479
|
*
|
|
470
480
|
* @returns The current parent `Element` for DOM insertion.
|
|
471
481
|
*
|
package/dist/aberdeen.js
CHANGED
|
@@ -834,8 +834,7 @@ var refHandler = {
|
|
|
834
834
|
function ref(target, index) {
|
|
835
835
|
return new Proxy({ proxy: target, index }, refHandler);
|
|
836
836
|
}
|
|
837
|
-
function applyBind(
|
|
838
|
-
const el = _el;
|
|
837
|
+
function applyBind(el, target) {
|
|
839
838
|
let onProxyChange;
|
|
840
839
|
let onInputChange;
|
|
841
840
|
let type = el.getAttribute("type");
|
|
@@ -857,7 +856,11 @@ function applyBind(_el, target) {
|
|
|
857
856
|
onInputChange = () => target.value = type === "number" || type === "range" ? el.value === "" ? null : +el.value : el.value;
|
|
858
857
|
if (value === undefined)
|
|
859
858
|
onInputChange();
|
|
860
|
-
onProxyChange = () =>
|
|
859
|
+
onProxyChange = () => {
|
|
860
|
+
el.value = target.value;
|
|
861
|
+
if (el.tagName === "SELECT" && el.value != target.value)
|
|
862
|
+
throw new Error(`SELECT has no '${target.value}' OPTION (yet)`);
|
|
863
|
+
};
|
|
861
864
|
}
|
|
862
865
|
observe(onProxyChange);
|
|
863
866
|
el.addEventListener("input", onInputChange);
|
|
@@ -903,6 +906,7 @@ var SPECIAL_PROPS = {
|
|
|
903
906
|
function $(...args) {
|
|
904
907
|
let savedCurrentScope;
|
|
905
908
|
let err;
|
|
909
|
+
let result;
|
|
906
910
|
for (let arg of args) {
|
|
907
911
|
if (arg == null || arg === false)
|
|
908
912
|
continue;
|
|
@@ -932,17 +936,17 @@ function $(...args) {
|
|
|
932
936
|
err = `Tag '${arg}' cannot contain space`;
|
|
933
937
|
break;
|
|
934
938
|
} else {
|
|
935
|
-
|
|
939
|
+
result = document.createElement(arg);
|
|
936
940
|
if (classes)
|
|
937
|
-
|
|
941
|
+
result.className = classes.replaceAll(".", " ");
|
|
938
942
|
if (text)
|
|
939
|
-
|
|
940
|
-
addNode(
|
|
943
|
+
result.textContent = text;
|
|
944
|
+
addNode(result);
|
|
941
945
|
if (!savedCurrentScope) {
|
|
942
946
|
savedCurrentScope = currentScope;
|
|
943
947
|
}
|
|
944
|
-
let newScope = new ChainedScope(
|
|
945
|
-
newScope.lastChild =
|
|
948
|
+
let newScope = new ChainedScope(result, true);
|
|
949
|
+
newScope.lastChild = result.lastChild || undefined;
|
|
946
950
|
if (topRedrawScope === currentScope)
|
|
947
951
|
topRedrawScope = newScope;
|
|
948
952
|
currentScope = newScope;
|
|
@@ -968,6 +972,7 @@ function $(...args) {
|
|
|
968
972
|
}
|
|
969
973
|
if (err)
|
|
970
974
|
throw new Error(err);
|
|
975
|
+
return result;
|
|
971
976
|
}
|
|
972
977
|
var cssCount = 0;
|
|
973
978
|
function insertCss(style, global = false) {
|
|
@@ -1195,5 +1200,5 @@ export {
|
|
|
1195
1200
|
$
|
|
1196
1201
|
};
|
|
1197
1202
|
|
|
1198
|
-
//# debugId=
|
|
1203
|
+
//# debugId=D0AD62307DFA7AC364756E2164756E21
|
|
1199
1204
|
//# sourceMappingURL=aberdeen.js.map
|