ripple 0.2.4 → 0.2.6
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 +3 -380
- package/package.json +5 -2
- package/src/compiler/phases/1-parse/index.js +13 -4
- package/src/compiler/phases/2-analyze/index.js +7 -1
- package/src/compiler/phases/2-analyze/prune.js +232 -109
- package/src/compiler/phases/3-transform/index.js +44 -14
- package/src/compiler/phases/3-transform/segments.js +12 -9
- package/src/runtime/internal/client/blocks.js +27 -5
- package/src/runtime/internal/client/constants.js +1 -0
- package/src/runtime/internal/client/index.js +3 -4
- package/src/runtime/internal/client/render.js +32 -3
- package/src/runtime/internal/client/runtime.js +6 -1
- package/src/runtime/internal/client/utils.js +1 -0
- package/src/utils/builders.js +11 -0
- package/tests/__snapshots__/basic.test.ripple.snap +66 -0
- package/tests/basic.test.ripple +273 -0
- package/tests/use.test.ripple +32 -0
package/README.md
CHANGED
|
@@ -1,387 +1,10 @@
|
|
|
1
1
|
<picture>
|
|
2
|
-
<source media="(prefers-color-scheme: dark)" srcset="assets/ripple-dark.png">
|
|
3
|
-
<img src="assets/ripple-light.png" alt="Ripple - the elegant UI framework for the web" />
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/trueadm/ripple/master/assets/ripple-dark.png">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/trueadm/ripple/master/assets/ripple-light.png" alt="Ripple - the elegant UI framework for the web" />
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
6
|
# What is Ripple?
|
|
7
7
|
|
|
8
8
|
> Currently, this project is still in early development, and should not be used in production.
|
|
9
9
|
|
|
10
|
-
Ripple is a TypeScript UI framework for the web.
|
|
11
|
-
|
|
12
|
-
I wrote Ripple as a love letter for frontend web – and this is largely a project that I built in less than a week, so it's very raw.
|
|
13
|
-
|
|
14
|
-
Personally, I ([@trueadm](https://github.com/trueadm)) have been involved in some truly amazing frontend frameworks along their journeys – from [Inferno](https://github.com/infernojs/inferno), where it all began, to [React](https://github.com/facebook/react) and the journey of React Hooks, to creating [Lexical](https://github.com/facebook/lexical), to [Svelte 5](https://github.com/sveltejs/svelte) and its new compiler and signal-based reactivity runtime. Along that journey, I collected ideas, and intriguing thoughts that may or may not pay off. Given my time between roles, I decided it was the best opportunity to try them out, and for open source to see what I was cooking.
|
|
15
|
-
|
|
16
|
-
Ripple was designed to be a JS/TS-first framework, rather than HTML-first. Ripple modules have their own `.ripple` extension and these modules
|
|
17
|
-
fully support TypeScript. By introducing a new extension, it affords Ripple to invent its own superset language, that plays really nicely with
|
|
18
|
-
TypeScript and JSX, but with a few interesting touches. In my experience, this has led to better DX not only for humans, but also for LLMs.
|
|
19
|
-
|
|
20
|
-
Right now, there will be plenty of bugs, things just won't work either and you'll find TODOs everywhere. At this stage, Ripple is more of an early alpha version of something that _might_ be, rather than something you should try and adopt. If anything, maybe some of the ideas can be shared and incubated back into other frameworks. There's also a lot of similarities with Svelte 5, and that's not by accident, that's because of my recent time working on Svelte 5.
|
|
21
|
-
|
|
22
|
-
If you'd like to know more, join the [Ripple Discord](https://discord.gg/JBF2ySrh2W).
|
|
23
|
-
|
|
24
|
-
## Features
|
|
25
|
-
|
|
26
|
-
- **Reactive State Management**: Built-in reactivity with `$` prefixed variables
|
|
27
|
-
- **Component-Based Architecture**: Clean, reusable components with props and children
|
|
28
|
-
- **JSX-like Syntax**: Familiar templating with Ripple-specific enhancements
|
|
29
|
-
- **TypeScript Support**: Full TypeScript integration with type checking
|
|
30
|
-
- **VSCode Integration**: Rich editor support with diagnostics, syntax highlighting, and IntelliSense
|
|
31
|
-
|
|
32
|
-
## Missing Features
|
|
33
|
-
|
|
34
|
-
- **SSR**: Ripple is currently an SPA only, this is because I haven't gotten around to it
|
|
35
|
-
- **Testing & Types**: The codebase is very raw with limited types (I've opted for JavaScript only to avoid build problems). There aren't any tests either – I've been using the `playground` directory to manually test things as I go
|
|
36
|
-
|
|
37
|
-
## Quick Start
|
|
38
|
-
|
|
39
|
-
### Try Ripple
|
|
40
|
-
|
|
41
|
-
> We're working hard on getting an online playgroud available. Watch this space!
|
|
42
|
-
|
|
43
|
-
You can try Ripple now by using our basic Vite template by running these commands in your terminal:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx degit trueadm/ripple/templates/basic my-app
|
|
47
|
-
cd my-app
|
|
48
|
-
npm i # or yarn or pnpm
|
|
49
|
-
npm run dev # or yarn or pnpm
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Mounting your app
|
|
53
|
-
|
|
54
|
-
You can use the `mount` API from the `ripple` package to render your Ripple component, using the `target`
|
|
55
|
-
option to specify what DOM element you want to render the component.
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
// index.ts
|
|
59
|
-
import { mount } from 'ripple';
|
|
60
|
-
import { App } from '/App.ripple';
|
|
61
|
-
|
|
62
|
-
mount(App, {
|
|
63
|
-
props: {
|
|
64
|
-
title: 'Hello world!'
|
|
65
|
-
},
|
|
66
|
-
target: document.getElementById('root')
|
|
67
|
-
});
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Key Concepts
|
|
71
|
-
|
|
72
|
-
### Components
|
|
73
|
-
|
|
74
|
-
Define reusable components with the `component` keyword. These are similar to functions in that they have `props`, but crucially,
|
|
75
|
-
they allow for a JSX-like syntax to be defined alongside standard TypeScript. That means you do not _return JSX_ like in other frameworks,
|
|
76
|
-
but you instead use it like a JavaScript statement, as shown:
|
|
77
|
-
|
|
78
|
-
```ripple
|
|
79
|
-
component Button(props: { text: string, onClick: () => void }) {
|
|
80
|
-
<button onClick={props.onClick}>
|
|
81
|
-
{props.text}
|
|
82
|
-
</button>
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Usage
|
|
86
|
-
component App() {
|
|
87
|
-
<Button text="Click me" onClick={() => console.log("Clicked!")} />
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-

|
|
92
|
-
|
|
93
|
-
Ripple's templating language also supports shorthands and object spreads too:
|
|
94
|
-
|
|
95
|
-
```svelte
|
|
96
|
-
// you can do a normal prop
|
|
97
|
-
<div onClick={onClick}>{text}</div>
|
|
98
|
-
|
|
99
|
-
// or using the shorthand prop
|
|
100
|
-
<div {onClick}>{text}</div>
|
|
101
|
-
|
|
102
|
-
// and you can spread props
|
|
103
|
-
<div {...properties}>{text}</div>
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Reactive Variables
|
|
107
|
-
|
|
108
|
-
Variables prefixed with `$` are automatically reactive:
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
let $name = "World";
|
|
112
|
-
let $count = 0;
|
|
113
|
-
|
|
114
|
-
// Updates automatically trigger re-renders
|
|
115
|
-
$count++;
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Object properties prefixed with `$` are also automatically reactive:
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
let counter = { $current: 0 };
|
|
122
|
-
|
|
123
|
-
// Updates automatically trigger re-renders
|
|
124
|
-
counter.$current++;
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Derived values are simply `$` variables that combined different parts of state:
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
let $count = 0;
|
|
131
|
-
let $double = $count * 2;
|
|
132
|
-
let $quadruple = $double * 2;
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
That means `$count` itself might be derived if it were to reference another reactive property. For example:
|
|
136
|
-
|
|
137
|
-
```ripple
|
|
138
|
-
component Counter({ $startingCount }) {
|
|
139
|
-
let $count = $startingCount;
|
|
140
|
-
let $double = $count * 2;
|
|
141
|
-
let $quadruple = $double * 2;
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Now given `$startingCount` is reactive, it would mean that `$count` might reset each time an incoming change to `$startingCount` occurs. That might not be desirable, so Ripple provides a way to `untrack` reactivity in those cases:
|
|
146
|
-
|
|
147
|
-
```ripple
|
|
148
|
-
import { untrack } from 'ripple';
|
|
149
|
-
|
|
150
|
-
component Counter({ $startingCount }) {
|
|
151
|
-
let $count = untrack(() => $startingCount);
|
|
152
|
-
let $double = $count * 2;
|
|
153
|
-
let $quadruple = $double * 2;
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Now `$count` will only reactively create its value on initialization.
|
|
158
|
-
|
|
159
|
-
> Note: you cannot define reactive variables in module/global scope, they have to be created on access from an active component
|
|
160
|
-
|
|
161
|
-
### Effects
|
|
162
|
-
|
|
163
|
-
When dealing with reactive state, you might want to be able to create side-effects based upon changes that happen upon updates.
|
|
164
|
-
To do this, you can use `effect`:
|
|
165
|
-
|
|
166
|
-
```ripple
|
|
167
|
-
import { effect } from 'ripple';
|
|
168
|
-
|
|
169
|
-
component App() {
|
|
170
|
-
let $count = 0;
|
|
171
|
-
|
|
172
|
-
effect(() => {
|
|
173
|
-
console.log($count);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
<button onClick={() => $count++}>Increment</button>
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Control flow
|
|
181
|
-
|
|
182
|
-
The JSX-like syntax might take some time to get used to if you're coming from another framework. For one, templating in Ripple
|
|
183
|
-
can only occur _inside_ a `component` body – you can't create JSX inside functions, or assign it to variables as an expression.
|
|
184
|
-
|
|
185
|
-
```ripple
|
|
186
|
-
<div>
|
|
187
|
-
// you can create variables inside the template!
|
|
188
|
-
const str = "hello world";
|
|
189
|
-
|
|
190
|
-
console.log(str); // and function calls too!
|
|
191
|
-
|
|
192
|
-
debugger; // you can put breakpoints anywhere to help debugging!
|
|
193
|
-
|
|
194
|
-
{str}
|
|
195
|
-
</div>
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-

|
|
199
|
-
|
|
200
|
-
Note that strings inside the template need to be inside `{"string"}`, you can't do `<div>hello</div>` as Ripple
|
|
201
|
-
has no idea if `hello` is a string or maybe some JavaScript code that needs evaluating, so just ensure you wrap them
|
|
202
|
-
in curly braces. This shouldn't be an issue in the real-world anyway, as you'll likely use an i18n library that means
|
|
203
|
-
using JavaScript expressions regardless.
|
|
204
|
-
|
|
205
|
-
### If statements
|
|
206
|
-
|
|
207
|
-
If blocks work seemlessly with Ripple's templating language, you can put them inside the JSX-like
|
|
208
|
-
statements, making control-flow far easier to read and reason with.
|
|
209
|
-
|
|
210
|
-
```ripple
|
|
211
|
-
component Truthy({ x }) {
|
|
212
|
-
<div>
|
|
213
|
-
if (x) {
|
|
214
|
-
<span>
|
|
215
|
-
{"x is truthy"}
|
|
216
|
-
</span>
|
|
217
|
-
} else {
|
|
218
|
-
<span>
|
|
219
|
-
{"x is falsy"}
|
|
220
|
-
</span>
|
|
221
|
-
}
|
|
222
|
-
</div>
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-

|
|
227
|
-
|
|
228
|
-
### For statements
|
|
229
|
-
|
|
230
|
-
You can render collections using a `for...of` block, and you don't need to specify a `key` prop like
|
|
231
|
-
other frameworks.
|
|
232
|
-
|
|
233
|
-
```ripple
|
|
234
|
-
component ListView({ title, items }) {
|
|
235
|
-
<h2>{title}</h2>
|
|
236
|
-
<ul>
|
|
237
|
-
for (const item of items) {
|
|
238
|
-
<li>{item.text}</li>
|
|
239
|
-
}
|
|
240
|
-
</ul>
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-

|
|
245
|
-
|
|
246
|
-
### Try statements
|
|
247
|
-
|
|
248
|
-
Try blocks work to building the foundation for **error boundaries**, when the runtime encounters
|
|
249
|
-
an error in the `try` block, you can easily render a fallback in the `catch` block.
|
|
250
|
-
|
|
251
|
-
```ripple
|
|
252
|
-
import { reportError } from 'some-library';
|
|
253
|
-
|
|
254
|
-
component ErrorBoundary() {
|
|
255
|
-
<div>
|
|
256
|
-
try {
|
|
257
|
-
<ComponentThatFails />
|
|
258
|
-
} catch (e) {
|
|
259
|
-
reportError(e);
|
|
260
|
-
|
|
261
|
-
<div>{"An error occured! " + e.message}</div>
|
|
262
|
-
}
|
|
263
|
-
</div>
|
|
264
|
-
}
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-

|
|
268
|
-
|
|
269
|
-
### Props
|
|
270
|
-
|
|
271
|
-
If you want a prop to be reactive, you should also give it a `$` prefix.
|
|
272
|
-
|
|
273
|
-
```ripple
|
|
274
|
-
component Button(props: { $text: string, onClick: () => void }) {
|
|
275
|
-
<button onClick={props.onClick}>
|
|
276
|
-
{props.$text}
|
|
277
|
-
</button>
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Usage
|
|
281
|
-
<Button $text={some_text} onClick={() => console.log("Clicked!")} />
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Children
|
|
285
|
-
|
|
286
|
-
Use `$children` prop and then use it in the form of `<$children />` for component composition.
|
|
287
|
-
|
|
288
|
-
When you pass in children to a component, it gets implicitly passed as the `$children` prop, in the form of a component.
|
|
289
|
-
|
|
290
|
-
```ripple
|
|
291
|
-
import type { Component } from 'ripple';
|
|
292
|
-
|
|
293
|
-
component Card(props: { $children: Component }) {
|
|
294
|
-
<div class="card">
|
|
295
|
-
<$children />
|
|
296
|
-
</div>
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Usage
|
|
300
|
-
<Card>
|
|
301
|
-
<p>{"Card content here"}</p>
|
|
302
|
-
</Card>
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
You could also explicitly write the same code as shown:
|
|
306
|
-
|
|
307
|
-
```ripple
|
|
308
|
-
import type { Component } from 'ripple';
|
|
309
|
-
|
|
310
|
-
component Card(props: { $children: Component }) {
|
|
311
|
-
<div class="card">
|
|
312
|
-
<$children />
|
|
313
|
-
</div>
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Usage with explicit component
|
|
317
|
-
<Card>
|
|
318
|
-
component $children() {
|
|
319
|
-
<p>{"Card content here"}</p>
|
|
320
|
-
}
|
|
321
|
-
</Card>
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### Events
|
|
325
|
-
|
|
326
|
-
Like React, events are props that start with `on` and then continue with an uppercase character, such as:
|
|
327
|
-
|
|
328
|
-
- `onClick`
|
|
329
|
-
- `onPointerMove`
|
|
330
|
-
- `onPointerDown`
|
|
331
|
-
- `onKeyDown`
|
|
332
|
-
|
|
333
|
-
For `capture` phase events, just add `Capture` to the end of the prop name:
|
|
334
|
-
|
|
335
|
-
- `onClickCapture`
|
|
336
|
-
- `onPointerMoveCapture`
|
|
337
|
-
- `onPointerDownCapture`
|
|
338
|
-
- `onKeyDownCapture`
|
|
339
|
-
|
|
340
|
-
> Note: Some events are automatically delegated where possible by Ripple to improve runtime performance.
|
|
341
|
-
|
|
342
|
-
### Styling
|
|
343
|
-
|
|
344
|
-
Ripple supports native CSS styling that is localized to the given component using the `<style>` element.
|
|
345
|
-
|
|
346
|
-
```ripple
|
|
347
|
-
component MyComponent() {
|
|
348
|
-
<div class="container">
|
|
349
|
-
<h1>{"Hello World"}</h1>
|
|
350
|
-
</div>
|
|
351
|
-
|
|
352
|
-
<style>
|
|
353
|
-
.container {
|
|
354
|
-
background: blue;
|
|
355
|
-
padding: 1rem;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
h1 {
|
|
359
|
-
color: white;
|
|
360
|
-
font-size: 2rem;
|
|
361
|
-
}
|
|
362
|
-
</style>
|
|
363
|
-
}
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
> Note: the `<style>` element must be top-level within a `component`.
|
|
367
|
-
|
|
368
|
-
## VSCode Extension
|
|
369
|
-
|
|
370
|
-
The [Ripple VSCode extension](https://marketplace.visualstudio.com/items?itemName=ripplejs.ripple-vscode-plugin) provides:
|
|
371
|
-
|
|
372
|
-
- **Syntax Highlighting** for `.ripple` files
|
|
373
|
-
- **Real-time Diagnostics** for compilation errors
|
|
374
|
-
- **TypeScript Integration** for type checking
|
|
375
|
-
- **IntelliSense** for autocompletion
|
|
376
|
-
|
|
377
|
-
You can find the extension on the VS Code Marketplace as [`Ripple for VS Code`](https://marketplace.visualstudio.com/items?itemName=ripplejs.ripple-vscode-plugin).
|
|
378
|
-
|
|
379
|
-
## Playground
|
|
380
|
-
|
|
381
|
-
Feel free to play around with how Ripple works. If you clone the repo, you can then:
|
|
382
|
-
|
|
383
|
-
```bash
|
|
384
|
-
pnpm i && cd playground && pnpm dev
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
The playground uses Ripple's Vite plugin, where you can play around with things inside the `playground/src` directory.
|
|
10
|
+
Ripple is a TypeScript UI framework for the web. To find out more, view [Ripple's Github README](https://github.com/trueadm/ripple).
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Ripple is a TypeScript UI framework for the web",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.6",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index.js",
|
|
9
9
|
"main": "src/runtime/index.js",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@jridgewell/sourcemap-codec": "^1.5.5",
|
|
52
|
-
"@types/estree": "^1.0.8",
|
|
53
52
|
"acorn": "^8.15.0",
|
|
54
53
|
"acorn-typescript": "^1.4.13",
|
|
55
54
|
"esrap": "^2.1.0",
|
|
@@ -57,5 +56,9 @@
|
|
|
57
56
|
"magic-string": "^0.30.17",
|
|
58
57
|
"muggle-string": "^0.4.1",
|
|
59
58
|
"zimmerframe": "^1.1.2"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/estree": "^1.0.8",
|
|
62
|
+
"typescript": "^5.9.2"
|
|
60
63
|
}
|
|
61
64
|
}
|
|
@@ -33,9 +33,9 @@ function RipplePlugin(config) {
|
|
|
33
33
|
node.default = true;
|
|
34
34
|
this.next();
|
|
35
35
|
this.enterScope(0);
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
node.id = this.type.label === 'name' ? this.parseIdent() : null;
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
this.parseFunctionParams(node);
|
|
40
40
|
this.eat(tt.braceL);
|
|
41
41
|
node.body = [];
|
|
@@ -52,7 +52,7 @@ function RipplePlugin(config) {
|
|
|
52
52
|
|
|
53
53
|
return node;
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
return super.parseExportDefaultDeclaration();
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -91,7 +91,16 @@ function RipplePlugin(config) {
|
|
|
91
91
|
jsx_parseAttribute() {
|
|
92
92
|
let node = this.startNode();
|
|
93
93
|
if (this.eat(tt.braceL)) {
|
|
94
|
-
if (this.type ===
|
|
94
|
+
if (this.type.label === '@') {
|
|
95
|
+
this.next();
|
|
96
|
+
if (this.value !== 'use') {
|
|
97
|
+
this.unexpected();
|
|
98
|
+
}
|
|
99
|
+
this.next();
|
|
100
|
+
node.argument = this.parseMaybeAssign();
|
|
101
|
+
this.expect(tt.braceR);
|
|
102
|
+
return this.finishNode(node, 'UseAttribute');
|
|
103
|
+
} else if (this.type === tt.ellipsis) {
|
|
95
104
|
this.expect(tt.ellipsis);
|
|
96
105
|
node.argument = this.parseMaybeAssign();
|
|
97
106
|
this.expect(tt.braceR);
|
|
@@ -71,7 +71,13 @@ function mark_as_tracked(path) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const visitors = {
|
|
74
|
-
_(node, { state, next }) {
|
|
74
|
+
_(node, { state, next, path }) {
|
|
75
|
+
// Set up metadata.path for each node (needed for CSS pruning)
|
|
76
|
+
if (!node.metadata) {
|
|
77
|
+
node.metadata = {};
|
|
78
|
+
}
|
|
79
|
+
node.metadata.path = [...path];
|
|
80
|
+
|
|
75
81
|
const scope = state.scopes.get(node);
|
|
76
82
|
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
|
|
77
83
|
},
|