tsbars 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +222 -0
- package/bin/mapper.js +43 -0
- package/dist/bun.d.ts +3 -0
- package/dist/bun.js +17 -0
- package/dist/compile.d.ts +3 -0
- package/dist/compile.js +6 -0
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +16 -0
- package/package.json +77 -3
- package/wasm/tsbars-mapper.wasm +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Farrel Darian
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# tsbars
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Import `.hbs` files from TypeScript and get a call signature inferred from the template.
|
|
6
|
+
|
|
7
|
+
`prompt.hbs`:
|
|
8
|
+
|
|
9
|
+
```hbs
|
|
10
|
+
Hello {{user.name}}!
|
|
11
|
+
{{#if user.admin}}You are an admin.{{/if}}
|
|
12
|
+
{{#each items}}- {{title}}
|
|
13
|
+
{{/each}}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`index.ts`:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import prompt from './prompt.hbs';
|
|
20
|
+
|
|
21
|
+
prompt({ user: { admin: true }, items: [{ title: 'Write docs' }] });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
index.ts(3,10): error TS2741: Property 'name' is missing in type '{ admin: true; }' but required in type '{ name: Printable; admin?: unknown; }'.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The import is typed as `(context: Context) => string`, where `Context` is:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
type Printable = string | number | bigint | boolean;
|
|
32
|
+
type Context = {
|
|
33
|
+
user: { name: Printable; admin?: unknown };
|
|
34
|
+
items: ReadonlyArray<{ title: Printable }>;
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A [TypeScript content mapper](https://github.com/microsoft/typescript-go/pull/4712) does the inference: the compiler hands it the template, it returns a virtual `.ts` module, and errors are mapped back to the template. At runtime the template is compiled by Handlebars through a Vite or Bun plugin.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- TypeScript `7.1.0-beta` (the native compiler), run as `tsc --noEmit --runExternalCode`.
|
|
43
|
+
- Node.js with `node:wasi`. The mapper is a bundled WebAssembly module that `bin/mapper.js` runs under it. CI uses Node 26; older versions are untested.
|
|
44
|
+
- [Handlebars](https://handlebarsjs.com) `^4.7.0` as a peer dependency.
|
|
45
|
+
- Imports must include the `.hbs` extension.
|
|
46
|
+
|
|
47
|
+
No Rust toolchain or build step is needed to use tsbars.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
Once published to npm (it isn't yet):
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pnpm add -D tsbars typescript@7.1.0-beta
|
|
55
|
+
pnpm add handlebars
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Vite is an optional peer dependency, only needed for `tsbars/vite`. To try tsbars before the first release, see [Development](#development).
|
|
59
|
+
|
|
60
|
+
## Setup
|
|
61
|
+
|
|
62
|
+
Add a content mapper to your `tsconfig.json`:
|
|
63
|
+
|
|
64
|
+
```jsonc
|
|
65
|
+
{
|
|
66
|
+
"compilerOptions": {
|
|
67
|
+
"module": "NodeNext",
|
|
68
|
+
"moduleResolution": "NodeNext",
|
|
69
|
+
"strict": true,
|
|
70
|
+
"noEmit": true
|
|
71
|
+
},
|
|
72
|
+
"contentMappers": [
|
|
73
|
+
{
|
|
74
|
+
"package": "tsbars",
|
|
75
|
+
"extensions": [".hbs"],
|
|
76
|
+
"options": { "helpers": ["now"] }
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`options.helpers` lists helpers you call without arguments (`{{now}}`). Without it, a bare `{{now}}` is read as a context variable and becomes a required property.
|
|
83
|
+
|
|
84
|
+
Type-check with:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
tsc --noEmit --runExternalCode
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`examples/basic` is a working project.
|
|
91
|
+
|
|
92
|
+
<details>
|
|
93
|
+
<summary>Editor support (not tested by us yet)</summary>
|
|
94
|
+
|
|
95
|
+
Content mappers are external code, so the editor has to opt in. In VS Code this should mean:
|
|
96
|
+
- the TypeScript Native Preview extension,
|
|
97
|
+
- a trusted workspace, with the content mapper permission granted,
|
|
98
|
+
- `js/ts.contentMappers.enabled` turned on.
|
|
99
|
+
|
|
100
|
+
We haven't verified this in an editor. Only the `tsc` path above is covered by tests.
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
## Runtime
|
|
105
|
+
|
|
106
|
+
Both plugins take [Handlebars compile options](https://handlebarsjs.com/api-reference/compilation.html) and pass them to `Handlebars.precompile`.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// vite.config.ts
|
|
110
|
+
import tsbars from 'tsbars/vite';
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
plugins: [tsbars({ noEscape: true })],
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// preload.ts, referenced from bunfig.toml: preload = ["./preload.ts"]
|
|
119
|
+
import { plugin } from 'bun';
|
|
120
|
+
import tsbars from 'tsbars/bun';
|
|
121
|
+
|
|
122
|
+
plugin(tsbars({ noEscape: true }));
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use `noEscape: true` for LLM prompts. Handlebars HTML-escapes `{{value}}` by default, so `<` and `&` in your data would come out as `<` and `&`.
|
|
126
|
+
|
|
127
|
+
Compiled templates use `handlebars/runtime`, so register helpers there:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import Handlebars from 'handlebars/runtime';
|
|
131
|
+
|
|
132
|
+
Handlebars.registerHelper('shout', (text: string) => text.toUpperCase());
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The helper must be registered before the template runs. tsbars doesn't see it, so list it in `options.helpers` if you call it with no arguments.
|
|
136
|
+
|
|
137
|
+
## How types are inferred
|
|
138
|
+
|
|
139
|
+
| Template | Inferred |
|
|
140
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
141
|
+
| `{{a.b}}` | `a: { b: Printable }` |
|
|
142
|
+
| `{{#if a}}`, `{{#unless a}}`, `{{else if a}}` | `a?: unknown`, or `a?: <type from other uses>` |
|
|
143
|
+
| `{{#each xs}}{{title}}{{/each}}` | `xs: ReadonlyArray<{ title: Printable }>` |
|
|
144
|
+
| `{{#each xs}}{{this}}{{/each}}` | `xs: ReadonlyArray<Printable>` |
|
|
145
|
+
| `{{#each xs}}{{@key}} {{this}}{{/each}}` | `xs: Readonly<Record<string, Printable>>` |
|
|
146
|
+
| `{{#each xs as \|x i\|}}{{x.id}}{{/each}}` | `xs: ReadonlyArray<{ id: Printable }>` (first block param names the item) |
|
|
147
|
+
| `{{#with a}}{{b}}{{/with}}` | `a: { b: Printable }`; optional if the block has `{{else}}` |
|
|
148
|
+
| `{{../b}}` inside a block | `b` is looked up in the parent scope, one level per `../` |
|
|
149
|
+
| `{{@root.b}}` | `b` on the root context |
|
|
150
|
+
| `{{shout name}}`, `{{upper (trim raw)}}` | `name: unknown`, `raw: unknown`; the helper itself is not part of `Context` |
|
|
151
|
+
| `{{lookup obj key}}` | `obj: unknown`, `key: Printable` |
|
|
152
|
+
| `{{#fancy z}}{{inner}}{{/fancy}}` (unknown block helper) | `z: unknown`; the body adds nothing |
|
|
153
|
+
| `{{now}}` with `"helpers": ["now"]` | nothing |
|
|
154
|
+
| no variables at all | `context?: Record<string, never>` |
|
|
155
|
+
|
|
156
|
+
Notes:
|
|
157
|
+
- `Printable` is `string | number | bigint | boolean`. A path that is both printed and used as an object (`{{a}}` and `{{a.b}}`) becomes the object.
|
|
158
|
+
- Property names that aren't valid identifiers (`{{[odd key]}}`) become quoted keys.
|
|
159
|
+
- Properties are listed in the order the template first mentions them.
|
|
160
|
+
|
|
161
|
+
### Overriding inference
|
|
162
|
+
|
|
163
|
+
Top-level comments can replace an inferred type or import one:
|
|
164
|
+
|
|
165
|
+
```hbs
|
|
166
|
+
{{!-- @import type { User } from "./user" --}}
|
|
167
|
+
{{!-- @param user User --}}
|
|
168
|
+
{{!-- @param stats.count number --}}
|
|
169
|
+
Hello {{user.name}}, you have {{stats.count}} messages.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `@param <path> <type>` replaces the inferred type at that dotted path with any TypeScript type.
|
|
173
|
+
- `@import <clause>` emits `import <clause>;` into the generated module. The clause is whatever follows `import`, for example `type { User } from "./user"`.
|
|
174
|
+
- A type error inside an annotation is reported on the template line (`profile.hbs(1,23): error TS2304: Cannot find name 'Missing'.`).
|
|
175
|
+
- A malformed directive is reported as `tsbars2` against the template.
|
|
176
|
+
|
|
177
|
+
## Limitations
|
|
178
|
+
|
|
179
|
+
- Partials contribute no types: `{{> name}}` and `{{#> name}}...{{/name}}` are skipped, including everything inside a partial block. Variables used only there won't appear in `Context`.
|
|
180
|
+
- Decorators (`{{* ... }}`) and inline partials (`{{#*inline "x"}}`) are not supported. They are parse errors.
|
|
181
|
+
- Helpers have no signatures. Their arguments are typed `unknown`, and nothing checks the helper exists.
|
|
182
|
+
- A template that fails to parse is reported as `tsbars1` (for example `broken.hbs(2,1): error tsbars1: Block \`if\` is not closed`). Its default export then accepts `any`, so importers don't produce more errors.
|
|
183
|
+
- Content mappers are a beta TypeScript feature and may change.
|
|
184
|
+
- Not published to npm yet, and editor support is untested.
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
Prerequisites: [pnpm](https://pnpm.io), [Bun](https://bun.sh) (test runner), and [rustup](https://rustup.rs). rustup installs the toolchain and the `wasm32-wasip1` target from `rust-toolchain.toml` on first use.
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
pnpm install
|
|
192
|
+
pnpm run build # tsdown -> dist/, then the mapper -> wasm/tsbars-mapper.wasm
|
|
193
|
+
pnpm run check # tsc, Biome lint, clippy, bun test, cargo test
|
|
194
|
+
pnpm run format # Biome + cargo fmt
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`bin/mapper.js` exits with a hint to run `pnpm run build:wasm` if `wasm/tsbars-mapper.wasm` is missing. To use a clone from another project, symlink it to that project's `node_modules/tsbars` (tsc only resolves content mapper packages from `node_modules`; the e2e tests do the same).
|
|
198
|
+
|
|
199
|
+
CI auto-commits formatting on pull requests, then runs `pnpm run check`. See [AGENTS.md](AGENTS.md) for the repo layout.
|
|
200
|
+
|
|
201
|
+
<details>
|
|
202
|
+
<summary>Releasing</summary>
|
|
203
|
+
|
|
204
|
+
Add a changeset with your change:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
pnpm exec changeset
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Merging it to `main` opens a "Version Packages" PR; merging that PR publishes to npm through GitHub Actions with OIDC trusted publishing. The first publish has to be done manually, then a trusted publisher configured on npmjs.com.
|
|
211
|
+
|
|
212
|
+
</details>
|
|
213
|
+
|
|
214
|
+
## Acknowledgements
|
|
215
|
+
|
|
216
|
+
tsbars is modelled on [reylo](https://github.com/colinhacks/reylo) by Colin McDonnell (MIT). Its content-mapper server, packaging and typed-import design were the reference. tsbars reimplements that approach in Rust for Handlebars.
|
|
217
|
+
|
|
218
|
+
Thanks to the TypeScript team for content mappers ([microsoft/typescript-go#4712](https://github.com/microsoft/typescript-go/pull/4712)).
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
[MIT](LICENSE)
|
package/bin/mapper.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const wasmPath = join(
|
|
7
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
'..',
|
|
9
|
+
'wasm',
|
|
10
|
+
'tsbars-mapper.wasm',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
// tsc discards our stderr, but keep it clean for direct runs. Must precede the node:wasi import.
|
|
14
|
+
process.removeAllListeners('warning');
|
|
15
|
+
const { WASI } = await import('node:wasi');
|
|
16
|
+
|
|
17
|
+
let bytes;
|
|
18
|
+
try {
|
|
19
|
+
bytes = await readFile(wasmPath);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT') throw error;
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
`tsbars: mapper not found at ${wasmPath}\nRun \`pnpm run build:wasm\` in the tsbars repo.\n`,
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fds 0/1/2 are passed straight through; process.stdin is deliberately never touched so
|
|
29
|
+
// the WASI blocking read keeps working on the inherited pipe.
|
|
30
|
+
const wasi = new WASI({
|
|
31
|
+
version: 'preview1',
|
|
32
|
+
args: ['tsbars-mapper'],
|
|
33
|
+
env: {},
|
|
34
|
+
stdin: 0,
|
|
35
|
+
stdout: 1,
|
|
36
|
+
stderr: 2,
|
|
37
|
+
returnOnExit: true,
|
|
38
|
+
});
|
|
39
|
+
const instance = await WebAssembly.instantiate(
|
|
40
|
+
await WebAssembly.compile(bytes),
|
|
41
|
+
wasi.getImportObject(),
|
|
42
|
+
);
|
|
43
|
+
process.exit(wasi.start(instance));
|
package/dist/bun.d.ts
ADDED
package/dist/bun.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { compile } from "./compile.js";
|
|
2
|
+
// src/bun.ts
|
|
3
|
+
function tsbars(options) {
|
|
4
|
+
return {
|
|
5
|
+
name: "tsbars",
|
|
6
|
+
setup(build) {
|
|
7
|
+
build.onLoad({ filter: /\.hbs$/ }, async (args) => {
|
|
8
|
+
const source = await Bun.file(args.path).text();
|
|
9
|
+
return {
|
|
10
|
+
contents: compile(source, options),
|
|
11
|
+
loader: "js"
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export { tsbars as default };
|
package/dist/compile.js
ADDED
package/dist/vite.d.ts
ADDED
package/dist/vite.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { compile } from "./compile.js";
|
|
2
|
+
// src/vite.ts
|
|
3
|
+
function tsbars(options) {
|
|
4
|
+
return {
|
|
5
|
+
name: "tsbars",
|
|
6
|
+
enforce: "pre",
|
|
7
|
+
transform(code, id) {
|
|
8
|
+
if (!id.replace(/\?.*$/, "").endsWith(".hbs")) return null;
|
|
9
|
+
return {
|
|
10
|
+
code: compile(code, options),
|
|
11
|
+
map: null
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export { tsbars as default };
|
package/package.json
CHANGED
|
@@ -1,4 +1,78 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"name": "tsbars",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed Handlebars imports via TypeScript content mappers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/fdarian/tsbars.git"
|
|
10
|
+
},
|
|
11
|
+
"author": "Farrel Darian",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"wasm",
|
|
15
|
+
"bin",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
"./compile": {
|
|
21
|
+
"types": "./dist/compile.d.ts",
|
|
22
|
+
"import": "./dist/compile.js"
|
|
23
|
+
},
|
|
24
|
+
"./vite": {
|
|
25
|
+
"types": "./dist/vite.d.ts",
|
|
26
|
+
"import": "./dist/vite.js"
|
|
27
|
+
},
|
|
28
|
+
"./bun": {
|
|
29
|
+
"types": "./dist/bun.d.ts",
|
|
30
|
+
"import": "./dist/bun.js"
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"imports": {
|
|
35
|
+
"#src/*": "./src/*"
|
|
36
|
+
},
|
|
37
|
+
"typescript": {
|
|
38
|
+
"contentMapper": {
|
|
39
|
+
"exec": [
|
|
40
|
+
"node",
|
|
41
|
+
"bin/mapper.js"
|
|
42
|
+
],
|
|
43
|
+
"compilerOptions": []
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"handlebars": "^4.7.0",
|
|
48
|
+
"vite": ">=5"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"vite": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "2.6.2",
|
|
57
|
+
"@changesets/cli": "3.0.3",
|
|
58
|
+
"@total-typescript/tsconfig": "1.0.4",
|
|
59
|
+
"@types/bun": "1.4.4",
|
|
60
|
+
"handlebars": "4.7.10",
|
|
61
|
+
"tsdown": "0.23.1",
|
|
62
|
+
"typescript": "7.1.0-beta",
|
|
63
|
+
"vite": "8.3.5"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsdown && pnpm run build:wasm",
|
|
67
|
+
"build:wasm": "cargo build --release --target wasm32-wasip1 -p tsbars-mapper && mkdir -p wasm && cp target/wasm32-wasip1/release/tsbars-mapper.wasm wasm/",
|
|
68
|
+
"format": "biome check --write && cargo fmt --all",
|
|
69
|
+
"check": "pnpm run check:type && pnpm run check:lint && pnpm run check:rust && pnpm run test",
|
|
70
|
+
"check:type": "tsc --noEmit",
|
|
71
|
+
"check:lint": "biome lint .",
|
|
72
|
+
"check:rust": "cargo clippy --all-targets -- -D warnings && cargo clippy --target wasm32-wasip1 -p tsbars-mapper -- -D warnings",
|
|
73
|
+
"test": "bun test && pnpm run test:rust",
|
|
74
|
+
"test:rust": "cargo test",
|
|
75
|
+
"ci:version": "changeset version",
|
|
76
|
+
"ci:release": "pnpm run build && changeset publish"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
Binary file
|