@kuratchi/js 0.0.3 → 0.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 +226 -50
- package/dist/cli.js +59 -17
- package/dist/compiler/index.d.ts +2 -2
- package/dist/compiler/index.js +658 -259
- package/dist/compiler/parser.d.ts +20 -4
- package/dist/compiler/parser.js +296 -12
- package/dist/compiler/template.d.ts +3 -1
- package/dist/compiler/template.js +217 -14
- package/dist/compiler/transpile.d.ts +1 -0
- package/dist/compiler/transpile.js +61 -0
- package/dist/create.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/runtime/action.d.ts +11 -0
- package/dist/runtime/action.js +14 -0
- package/dist/runtime/app.js +35 -8
- package/dist/runtime/context.d.ts +8 -2
- package/dist/runtime/context.js +31 -1
- package/dist/runtime/do.d.ts +6 -0
- package/dist/runtime/do.js +15 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/page-error.d.ts +16 -0
- package/dist/runtime/page-error.js +20 -0
- package/dist/runtime/types.d.ts +53 -33
- package/package.json +50 -46
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML file parser.
|
|
3
3
|
*
|
|
4
|
-
* Extracts the top-level <script> block (before the HTML document).
|
|
4
|
+
* Extracts the top-level compile-time <script> block (before the HTML document).
|
|
5
|
+
* A top-level block containing reactive `$:` labels is preserved in template output
|
|
6
|
+
* as client script.
|
|
5
7
|
* Everything else is the template — full HTML with native JS flow control.
|
|
6
8
|
* <style> inside the HTML is NOT extracted; it's part of the template.
|
|
7
9
|
*/
|
|
8
10
|
export interface ParsedFile {
|
|
9
|
-
/** Script content (
|
|
11
|
+
/** Script content (compile-time code, minus imports) */
|
|
10
12
|
script: string | null;
|
|
13
|
+
/** Explicit server-side load function exported from the route script */
|
|
14
|
+
loadFunction: string | null;
|
|
11
15
|
/** Template — the full HTML document with inline JS flow control */
|
|
12
16
|
template: string;
|
|
13
17
|
/** All imports from the script block */
|
|
@@ -30,11 +34,23 @@ export interface ParsedFile {
|
|
|
30
34
|
key?: string;
|
|
31
35
|
rpcId?: string;
|
|
32
36
|
}>;
|
|
37
|
+
/** Imports found in a top-level client script block */
|
|
38
|
+
clientImports: string[];
|
|
39
|
+
/** Top-level names returned from explicit load() */
|
|
40
|
+
loadReturnVars: string[];
|
|
41
|
+
/** Local aliases for Cloudflare Workers env imported from cloudflare:workers */
|
|
42
|
+
workerEnvAliases: string[];
|
|
43
|
+
}
|
|
44
|
+
interface ParseFileOptions {
|
|
45
|
+
kind?: 'route' | 'layout' | 'component';
|
|
46
|
+
filePath?: string;
|
|
33
47
|
}
|
|
34
48
|
/**
|
|
35
49
|
* Parse a .html route file.
|
|
36
50
|
*
|
|
37
|
-
* The <script> block
|
|
51
|
+
* The top-level compile-time <script> block is extracted for the compiler.
|
|
52
|
+
* If it includes reactive `$:` labels, it is preserved in template output.
|
|
38
53
|
* Everything else (the HTML document) becomes the template.
|
|
39
54
|
*/
|
|
40
|
-
export declare function parseFile(source: string): ParsedFile;
|
|
55
|
+
export declare function parseFile(source: string, options?: ParseFileOptions): ParsedFile;
|
|
56
|
+
export {};
|
package/dist/compiler/parser.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML file parser.
|
|
3
3
|
*
|
|
4
|
-
* Extracts the top-level <script> block (before the HTML document).
|
|
4
|
+
* Extracts the top-level compile-time <script> block (before the HTML document).
|
|
5
|
+
* A top-level block containing reactive `$:` labels is preserved in template output
|
|
6
|
+
* as client script.
|
|
5
7
|
* Everything else is the template — full HTML with native JS flow control.
|
|
6
8
|
* <style> inside the HTML is NOT extracted; it's part of the template.
|
|
7
9
|
*/
|
|
10
|
+
function hasReactiveLabel(scriptBody) {
|
|
11
|
+
return /\$\s*:/.test(scriptBody);
|
|
12
|
+
}
|
|
8
13
|
function splitTopLevel(input, delimiter) {
|
|
9
14
|
const parts = [];
|
|
10
15
|
let start = 0;
|
|
@@ -103,6 +108,220 @@ function pushIdentifier(name, out) {
|
|
|
103
108
|
if (!out.includes(name))
|
|
104
109
|
out.push(name);
|
|
105
110
|
}
|
|
111
|
+
function findMatchingToken(input, openPos, openChar, closeChar) {
|
|
112
|
+
let depth = 0;
|
|
113
|
+
let quote = null;
|
|
114
|
+
let escaped = false;
|
|
115
|
+
for (let i = openPos; i < input.length; i++) {
|
|
116
|
+
const ch = input[i];
|
|
117
|
+
if (quote) {
|
|
118
|
+
if (escaped) {
|
|
119
|
+
escaped = false;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ch === '\\') {
|
|
123
|
+
escaped = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (ch === quote)
|
|
127
|
+
quote = null;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
131
|
+
quote = ch;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (ch === openChar)
|
|
135
|
+
depth++;
|
|
136
|
+
else if (ch === closeChar) {
|
|
137
|
+
depth--;
|
|
138
|
+
if (depth === 0)
|
|
139
|
+
return i;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return -1;
|
|
143
|
+
}
|
|
144
|
+
function extractCloudflareEnvAliases(importLine) {
|
|
145
|
+
if (!/from\s+['"]cloudflare:workers['"]/.test(importLine))
|
|
146
|
+
return [];
|
|
147
|
+
const namedMatch = importLine.match(/import\s*\{([\s\S]*?)\}\s*from\s+['"]cloudflare:workers['"]/);
|
|
148
|
+
if (!namedMatch)
|
|
149
|
+
return [];
|
|
150
|
+
const aliases = [];
|
|
151
|
+
for (const rawPart of splitTopLevel(namedMatch[1], ',')) {
|
|
152
|
+
const part = rawPart.trim();
|
|
153
|
+
if (!part)
|
|
154
|
+
continue;
|
|
155
|
+
const envMatch = part.match(/^env(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
|
|
156
|
+
if (envMatch)
|
|
157
|
+
aliases.push(envMatch[1] || 'env');
|
|
158
|
+
}
|
|
159
|
+
return aliases;
|
|
160
|
+
}
|
|
161
|
+
function extractReturnObjectKeys(body) {
|
|
162
|
+
const keys = [];
|
|
163
|
+
let i = 0;
|
|
164
|
+
let depthParen = 0;
|
|
165
|
+
let depthBracket = 0;
|
|
166
|
+
let depthBrace = 0;
|
|
167
|
+
let quote = null;
|
|
168
|
+
let escaped = false;
|
|
169
|
+
while (i < body.length) {
|
|
170
|
+
const ch = body[i];
|
|
171
|
+
if (quote) {
|
|
172
|
+
if (escaped) {
|
|
173
|
+
escaped = false;
|
|
174
|
+
i++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ch === '\\') {
|
|
178
|
+
escaped = true;
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (ch === quote)
|
|
183
|
+
quote = null;
|
|
184
|
+
i++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
188
|
+
quote = ch;
|
|
189
|
+
i++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (ch === '(')
|
|
193
|
+
depthParen++;
|
|
194
|
+
else if (ch === ')')
|
|
195
|
+
depthParen = Math.max(0, depthParen - 1);
|
|
196
|
+
else if (ch === '[')
|
|
197
|
+
depthBracket++;
|
|
198
|
+
else if (ch === ']')
|
|
199
|
+
depthBracket = Math.max(0, depthBracket - 1);
|
|
200
|
+
else if (ch === '{')
|
|
201
|
+
depthBrace++;
|
|
202
|
+
else if (ch === '}')
|
|
203
|
+
depthBrace = Math.max(0, depthBrace - 1);
|
|
204
|
+
if (depthParen !== 0 || depthBracket !== 0 || depthBrace !== 0) {
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const rest = body.slice(i);
|
|
209
|
+
const returnMatch = /^return\b/.exec(rest);
|
|
210
|
+
if (!returnMatch) {
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
i += returnMatch[0].length;
|
|
215
|
+
while (i < body.length && /\s/.test(body[i]))
|
|
216
|
+
i++;
|
|
217
|
+
if (body[i] !== '{')
|
|
218
|
+
continue;
|
|
219
|
+
const closeIdx = findMatchingToken(body, i, '{', '}');
|
|
220
|
+
if (closeIdx === -1)
|
|
221
|
+
break;
|
|
222
|
+
const objectBody = body.slice(i + 1, closeIdx);
|
|
223
|
+
for (const rawProp of splitTopLevel(objectBody, ',')) {
|
|
224
|
+
const prop = rawProp.trim();
|
|
225
|
+
if (!prop || prop.startsWith('...'))
|
|
226
|
+
continue;
|
|
227
|
+
const keyMatch = prop.match(/^([A-Za-z_$][\w$]*)\s*(?::|$)/);
|
|
228
|
+
if (keyMatch)
|
|
229
|
+
pushIdentifier(keyMatch[1], keys);
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
return keys;
|
|
234
|
+
}
|
|
235
|
+
function extractExplicitLoad(scriptBody) {
|
|
236
|
+
let i = 0;
|
|
237
|
+
let depthParen = 0;
|
|
238
|
+
let depthBracket = 0;
|
|
239
|
+
let depthBrace = 0;
|
|
240
|
+
let quote = null;
|
|
241
|
+
let escaped = false;
|
|
242
|
+
while (i < scriptBody.length) {
|
|
243
|
+
const ch = scriptBody[i];
|
|
244
|
+
if (quote) {
|
|
245
|
+
if (escaped) {
|
|
246
|
+
escaped = false;
|
|
247
|
+
i++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (ch === '\\') {
|
|
251
|
+
escaped = true;
|
|
252
|
+
i++;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (ch === quote)
|
|
256
|
+
quote = null;
|
|
257
|
+
i++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
261
|
+
quote = ch;
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (ch === '(')
|
|
266
|
+
depthParen++;
|
|
267
|
+
else if (ch === ')')
|
|
268
|
+
depthParen = Math.max(0, depthParen - 1);
|
|
269
|
+
else if (ch === '[')
|
|
270
|
+
depthBracket++;
|
|
271
|
+
else if (ch === ']')
|
|
272
|
+
depthBracket = Math.max(0, depthBracket - 1);
|
|
273
|
+
else if (ch === '{')
|
|
274
|
+
depthBrace++;
|
|
275
|
+
else if (ch === '}')
|
|
276
|
+
depthBrace = Math.max(0, depthBrace - 1);
|
|
277
|
+
if (depthParen !== 0 || depthBracket !== 0 || depthBrace !== 0) {
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const rest = scriptBody.slice(i);
|
|
282
|
+
const fnMatch = /^export\s+(async\s+)?function\s+load\s*/.exec(rest);
|
|
283
|
+
if (!fnMatch) {
|
|
284
|
+
i++;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const openParen = i + fnMatch[0].length;
|
|
288
|
+
if (scriptBody[openParen] !== '(') {
|
|
289
|
+
throw new Error('[kuratchi compiler] Could not parse exported load() declaration.');
|
|
290
|
+
}
|
|
291
|
+
const closeParen = findMatchingToken(scriptBody, openParen, '(', ')');
|
|
292
|
+
if (closeParen === -1) {
|
|
293
|
+
throw new Error('[kuratchi compiler] Could not parse exported load() parameters.');
|
|
294
|
+
}
|
|
295
|
+
let openBrace = closeParen + 1;
|
|
296
|
+
while (openBrace < scriptBody.length && /\s/.test(scriptBody[openBrace]))
|
|
297
|
+
openBrace++;
|
|
298
|
+
if (scriptBody[openBrace] !== '{') {
|
|
299
|
+
throw new Error('[kuratchi compiler] export load() must use a function body.');
|
|
300
|
+
}
|
|
301
|
+
const closeBrace = findMatchingToken(scriptBody, openBrace, '{', '}');
|
|
302
|
+
if (closeBrace === -1) {
|
|
303
|
+
throw new Error('[kuratchi compiler] Could not parse exported load() body.');
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
loadFunction: scriptBody.slice(i, closeBrace + 1).trim(),
|
|
307
|
+
remainingScript: `${scriptBody.slice(0, i)}\n${scriptBody.slice(closeBrace + 1)}`.trim(),
|
|
308
|
+
returnVars: extractReturnObjectKeys(scriptBody.slice(openBrace + 1, closeBrace)),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return { loadFunction: null, remainingScript: scriptBody.trim(), returnVars: [] };
|
|
312
|
+
}
|
|
313
|
+
function hasFrameworkEnvEscapeHatch(source) {
|
|
314
|
+
return /\bglobalThis\.__cloudflare_env__\b/.test(source) || /\b__cloudflare_env__\b/.test(source);
|
|
315
|
+
}
|
|
316
|
+
function buildEnvAccessError(kind, filePath, detail) {
|
|
317
|
+
const label = filePath || kind;
|
|
318
|
+
const guidance = kind === 'route'
|
|
319
|
+
? 'Route top-level <script> is server-side. Client/reactive scripts cannot access Cloudflare env.'
|
|
320
|
+
: kind === 'layout'
|
|
321
|
+
? 'Layout scripts cannot access Cloudflare env directly.'
|
|
322
|
+
: 'Component scripts cannot access Cloudflare env directly.';
|
|
323
|
+
return new Error(`[kuratchi compiler] ${label}\n${detail}\n${guidance}\nRead env on the server and pass the value into the template explicitly.`);
|
|
324
|
+
}
|
|
106
325
|
function collectPatternNames(pattern, out) {
|
|
107
326
|
const p = pattern.trim();
|
|
108
327
|
if (!p)
|
|
@@ -401,22 +620,34 @@ function extractTopLevelFunctionNames(scriptBody) {
|
|
|
401
620
|
/**
|
|
402
621
|
* Parse a .html route file.
|
|
403
622
|
*
|
|
404
|
-
* The <script> block
|
|
623
|
+
* The top-level compile-time <script> block is extracted for the compiler.
|
|
624
|
+
* If it includes reactive `$:` labels, it is preserved in template output.
|
|
405
625
|
* Everything else (the HTML document) becomes the template.
|
|
406
626
|
*/
|
|
407
|
-
export function parseFile(source) {
|
|
627
|
+
export function parseFile(source, options = {}) {
|
|
628
|
+
const kind = options.kind || 'route';
|
|
408
629
|
let script = null;
|
|
630
|
+
let clientScript = null;
|
|
409
631
|
let template = source;
|
|
410
|
-
// Extract the first <script>...</script> block
|
|
411
|
-
//
|
|
632
|
+
// Extract the first top-level compile-time <script>...</script> block.
|
|
633
|
+
// Client blocks (reactive `$:` labels) stay in the template
|
|
634
|
+
// and are emitted for browser execution.
|
|
412
635
|
const scriptMatch = template.match(/^(\s*)<script(\s[^>]*)?\s*>([\s\S]*?)<\/script>/);
|
|
413
636
|
if (scriptMatch) {
|
|
414
|
-
|
|
415
|
-
|
|
637
|
+
const body = scriptMatch[3].trim();
|
|
638
|
+
if (!hasReactiveLabel(body)) {
|
|
639
|
+
script = body;
|
|
640
|
+
template = template.slice(scriptMatch[0].length).trim();
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
clientScript = body;
|
|
644
|
+
}
|
|
416
645
|
}
|
|
417
646
|
// Extract all imports from script
|
|
418
647
|
const serverImports = [];
|
|
648
|
+
const clientImports = [];
|
|
419
649
|
const componentImports = {};
|
|
650
|
+
const workerEnvAliases = [];
|
|
420
651
|
if (script) {
|
|
421
652
|
// Support both single-line and multiline static imports.
|
|
422
653
|
const importRegex = /^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm;
|
|
@@ -440,24 +671,62 @@ export function parseFile(source) {
|
|
|
440
671
|
else {
|
|
441
672
|
serverImports.push(line);
|
|
442
673
|
}
|
|
674
|
+
for (const alias of extractCloudflareEnvAliases(line)) {
|
|
675
|
+
if (!workerEnvAliases.includes(alias))
|
|
676
|
+
workerEnvAliases.push(alias);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (clientScript) {
|
|
681
|
+
const importRegex = /^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm;
|
|
682
|
+
let m;
|
|
683
|
+
while ((m = importRegex.exec(clientScript)) !== null) {
|
|
684
|
+
const line = m[0].trim();
|
|
685
|
+
clientImports.push(line);
|
|
686
|
+
if (extractCloudflareEnvAliases(line).length > 0) {
|
|
687
|
+
throw buildEnvAccessError(kind, options.filePath, 'Client <script> blocks cannot import env from cloudflare:workers.');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (hasFrameworkEnvEscapeHatch(clientScript)) {
|
|
691
|
+
throw buildEnvAccessError(kind, options.filePath, 'Client <script> blocks cannot access framework env internals.');
|
|
443
692
|
}
|
|
444
693
|
}
|
|
445
694
|
// Extract top-level variable declarations from script body (after removing imports)
|
|
446
695
|
const dataVars = [];
|
|
696
|
+
const loadReturnVars = [];
|
|
447
697
|
let scriptBody = '';
|
|
698
|
+
let loadFunction = null;
|
|
448
699
|
if (script) {
|
|
449
700
|
// Remove import lines to get the script body
|
|
450
|
-
|
|
451
|
-
const
|
|
701
|
+
const rawScriptBody = script.replace(/^\s*import[\s\S]*?from\s+['"][^'"]+['"]\s*;?/gm, '').trim();
|
|
702
|
+
const explicitLoad = extractExplicitLoad(rawScriptBody);
|
|
703
|
+
scriptBody = explicitLoad.remainingScript;
|
|
704
|
+
loadFunction = explicitLoad.loadFunction;
|
|
705
|
+
for (const name of explicitLoad.returnVars) {
|
|
706
|
+
if (!loadReturnVars.includes(name))
|
|
707
|
+
loadReturnVars.push(name);
|
|
708
|
+
}
|
|
709
|
+
if (hasFrameworkEnvEscapeHatch(rawScriptBody)) {
|
|
710
|
+
throw buildEnvAccessError(kind, options.filePath, 'Route/component scripts cannot access framework env internals.');
|
|
711
|
+
}
|
|
712
|
+
if (workerEnvAliases.length > 0 && kind !== 'route') {
|
|
713
|
+
throw buildEnvAccessError(kind, options.filePath, `Imported env from cloudflare:workers in a ${kind} script.`);
|
|
714
|
+
}
|
|
715
|
+
const transpiledScriptBody = transpileTypeScript(scriptBody, 'route-script.ts');
|
|
716
|
+
const topLevelVars = extractTopLevelDataVars(transpiledScriptBody);
|
|
452
717
|
for (const v of topLevelVars)
|
|
453
718
|
dataVars.push(v);
|
|
454
|
-
const topLevelFns = extractTopLevelFunctionNames(
|
|
719
|
+
const topLevelFns = extractTopLevelFunctionNames(transpiledScriptBody);
|
|
455
720
|
for (const fn of topLevelFns) {
|
|
456
721
|
if (!dataVars.includes(fn))
|
|
457
722
|
dataVars.push(fn);
|
|
458
723
|
}
|
|
724
|
+
for (const name of loadReturnVars) {
|
|
725
|
+
if (!dataVars.includes(name))
|
|
726
|
+
dataVars.push(name);
|
|
727
|
+
}
|
|
459
728
|
}
|
|
460
|
-
const hasLoad = scriptBody.length > 0;
|
|
729
|
+
const hasLoad = scriptBody.length > 0 || !!loadFunction;
|
|
461
730
|
// Strip HTML comments from the template before scanning for action references.
|
|
462
731
|
// This prevents commented-out code (<!-- ... -->) from being parsed as live
|
|
463
732
|
// action expressions, which would cause false "Invalid action expression" errors.
|
|
@@ -530,5 +799,20 @@ export function parseFile(source) {
|
|
|
530
799
|
if (!exists)
|
|
531
800
|
dataGetQueries.push({ fnName, argsExpr, asName, key });
|
|
532
801
|
}
|
|
533
|
-
return {
|
|
802
|
+
return {
|
|
803
|
+
script,
|
|
804
|
+
loadFunction,
|
|
805
|
+
template,
|
|
806
|
+
serverImports,
|
|
807
|
+
hasLoad,
|
|
808
|
+
actionFunctions,
|
|
809
|
+
dataVars,
|
|
810
|
+
componentImports,
|
|
811
|
+
pollFunctions,
|
|
812
|
+
dataGetQueries,
|
|
813
|
+
clientImports,
|
|
814
|
+
loadReturnVars,
|
|
815
|
+
workerEnvAliases,
|
|
816
|
+
};
|
|
534
817
|
}
|
|
818
|
+
import { transpileTypeScript } from './transpile.js';
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Syntax:
|
|
5
5
|
* {expression} → escaped output
|
|
6
|
-
* {
|
|
6
|
+
* {@html expression} → sanitized HTML output
|
|
7
|
+
* {@raw expression} → raw HTML output (unescaped)
|
|
8
|
+
* {=html expression} → legacy alias for {@raw expression}
|
|
7
9
|
* for (const x of arr) { → JS for loop (inline in HTML)
|
|
8
10
|
* <li>{x.name}</li>
|
|
9
11
|
* }
|