grammatik 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 +443 -0
- package/bin/grammatik.js +115 -0
- package/dist/cjs/index.js +3010 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/runtime.js +639 -0
- package/dist/esm/index.js +2959 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/runtime.js +605 -0
- package/dist/types/index.d.ts +214 -0
- package/dist/types/runtime.d.ts +40 -0
- package/package.json +65 -0
- package/src/analyze.d.ts +78 -0
- package/src/analyze.js +602 -0
- package/src/codegen.d.ts +36 -0
- package/src/codegen.js +635 -0
- package/src/definition.d.ts +133 -0
- package/src/definition.js +291 -0
- package/src/emit.d.ts +23 -0
- package/src/emit.js +374 -0
- package/src/firstchars.d.ts +6 -0
- package/src/firstchars.js +353 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +15 -0
- package/src/lexer.d.ts +96 -0
- package/src/lexer.js +358 -0
- package/src/methods.d.ts +20 -0
- package/src/methods.js +61 -0
- package/src/methodsblock.js +197 -0
- package/src/parser.d.ts +130 -0
- package/src/parser.js +194 -0
- package/src/plan.d.ts +29 -0
- package/src/plan.js +938 -0
- package/src/runtime.js +16 -0
- package/src/state.d.ts +98 -0
- package/src/state.js +121 -0
- package/src/values.js +67 -0
package/src/emit.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { createLexer } from './lexer.js'
|
|
5
|
+
import { analyze } from './analyze.js'
|
|
6
|
+
import { createPlanner } from './plan.js'
|
|
7
|
+
import { createCodegen } from './codegen.js'
|
|
8
|
+
import { usesOuterScope } from './plan.js'
|
|
9
|
+
import { emitMethodsBlock, evaluateMethodsBlock } from './methodsblock.js'
|
|
10
|
+
import { createValueMethods } from './values.js'
|
|
11
|
+
import { Compiler, LogicEngine } from 'json-logic-engine'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Things the engine's compiler can emit that a generated file has no way to
|
|
15
|
+
* provide: its coercion helpers, its `truthy`, closures captured by a compile
|
|
16
|
+
* hook (`reduce` and friends), and raw logic kept for lazy operators. An action
|
|
17
|
+
* whose source touches any of them keeps its `action({...})` wrapper.
|
|
18
|
+
*
|
|
19
|
+
* `engine.methods` is deliberately *not* here — it is only a lookup table for
|
|
20
|
+
* the semantic methods the caller registers, and a generated file can hold one
|
|
21
|
+
* of those itself.
|
|
22
|
+
*/
|
|
23
|
+
const UNSUPPORTED = /\b(coerceArray|precoerceNumber|compareCheck|assertAllowedDepth|assertSize|asyncIterators|notTraversed)\b|(?<!engine\.)\bmethods\[|engine\.(?!methods\b)/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tries to compile one action to inlinable source.
|
|
27
|
+
*
|
|
28
|
+
* Returns null when it cannot, which is always safe: the caller falls back to
|
|
29
|
+
* shipping the logic as data and letting the engine compile it at import.
|
|
30
|
+
*/
|
|
31
|
+
function inlineAction (logic, engine) {
|
|
32
|
+
try {
|
|
33
|
+
// `build` populates the buildState defaults (notably `compile`); a fresh
|
|
34
|
+
// state then collects exactly what this one action needs.
|
|
35
|
+
const seed = { engine }
|
|
36
|
+
Compiler.build(logic, seed)
|
|
37
|
+
const state = {
|
|
38
|
+
engine, compile: seed.compile, values: [], methods: [], notTraversed: [], processing: [], state: {}
|
|
39
|
+
}
|
|
40
|
+
const source = Compiler.buildString(logic, state)
|
|
41
|
+
|
|
42
|
+
if (state.methods.length || state.notTraversed.length || state.processing.length) return null
|
|
43
|
+
if (UNSUPPORTED.test(source)) return null
|
|
44
|
+
|
|
45
|
+
// Constants the compiler hoisted. Emittable only if they are really data.
|
|
46
|
+
let values = null
|
|
47
|
+
if (state.values.length > 0) {
|
|
48
|
+
values = JSON.stringify(state.values)
|
|
49
|
+
if (JSON.parse(values) === undefined) return null
|
|
50
|
+
if (JSON.stringify(JSON.parse(values)) !== values) return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
source,
|
|
55
|
+
values,
|
|
56
|
+
needsMethods: source.includes('engine.methods'),
|
|
57
|
+
needsAbove: /\babove\b/.test(source)
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
import { GrammarError } from './parser.js'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The block's methods, or null when it cannot be evaluated here — an importing
|
|
67
|
+
* block, or a host with no `new Function`. Failing is not fatal: the actions
|
|
68
|
+
* that would have been compiled ship as JSON Logic instead.
|
|
69
|
+
*/
|
|
70
|
+
function tryEvaluate (body) {
|
|
71
|
+
try {
|
|
72
|
+
return evaluateMethodsBlock(body).methods
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Writes a grammar out as a JavaScript module, the way Peggy emits a parser.
|
|
80
|
+
*
|
|
81
|
+
* The generated file contains the same rule functions `createParser` would build
|
|
82
|
+
* at run time, as source. What that buys is not speed — it is the same code —
|
|
83
|
+
* but everything around it: the grammar is analyzed once, at build time, so
|
|
84
|
+
* there is no FIRST-set computation or planning at startup; the file is a
|
|
85
|
+
* reviewable, diffable artifact; and nothing calls `new Function`, so it runs
|
|
86
|
+
* under a Content-Security-Policy without `unsafe-eval`.
|
|
87
|
+
*
|
|
88
|
+
* The file imports `grammatik/runtime` for the lexer and parse state, and
|
|
89
|
+
* `json-logic-engine` for the semantic actions, which stay as JSON Logic data
|
|
90
|
+
* in the file and are compiled at import. Actions can be interpreted instead
|
|
91
|
+
* with `execution: 'interpreted'`, which keeps the whole module eval-free.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
const quote = (value) => JSON.stringify(value)
|
|
95
|
+
|
|
96
|
+
/** Pretty-prints data with a stable indent, for readability in the output. */
|
|
97
|
+
const literal = (value, indent = 0) => JSON.stringify(value, null, 2)
|
|
98
|
+
.split('\n')
|
|
99
|
+
.join('\n' + ' '.repeat(indent))
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {import('./parser.js').GrammarSpec} spec
|
|
103
|
+
* @param {{ memo?: boolean, strict?: boolean, ll1?: boolean, maxSteps?: number,
|
|
104
|
+
* positions?: 'full'|'offset', execution?: 'generated'|'interpreted',
|
|
105
|
+
* format?: 'esm'|'cjs', validate?: boolean, moduleName?: string,
|
|
106
|
+
* runtimeSpecifier?: string, engineSpecifier?: string,
|
|
107
|
+
* methods?: Record<string, any> }} [options]
|
|
108
|
+
* @returns {string} JavaScript module source
|
|
109
|
+
*/
|
|
110
|
+
export function emitModule (spec, options = {}) {
|
|
111
|
+
const { tokens: tokenDefs, rules, start } = spec
|
|
112
|
+
const {
|
|
113
|
+
memo = false,
|
|
114
|
+
strict = true,
|
|
115
|
+
ll1 = true,
|
|
116
|
+
maxSteps = 0,
|
|
117
|
+
positions = 'full',
|
|
118
|
+
execution = 'generated',
|
|
119
|
+
format = 'esm',
|
|
120
|
+
validate = true,
|
|
121
|
+
moduleName = spec.name || 'parser',
|
|
122
|
+
runtimeSpecifier = 'grammatik/runtime',
|
|
123
|
+
engineSpecifier = 'json-logic-engine',
|
|
124
|
+
methods: semanticMethods
|
|
125
|
+
} = options
|
|
126
|
+
|
|
127
|
+
if (format !== 'esm' && format !== 'cjs') throw new Error(`emitModule: format must be 'esm' or 'cjs', got ${quote(format)}`)
|
|
128
|
+
|
|
129
|
+
// A `methods { ... }` block travels with the grammar and is written into the
|
|
130
|
+
// output as source. Nothing evaluates it here, so this path stays eval-free.
|
|
131
|
+
const methodsBlock = options.methodsBlock !== undefined ? options.methodsBlock : spec.methodsBlock
|
|
132
|
+
const block = methodsBlock ? emitMethodsBlock(methodsBlock, format) : null
|
|
133
|
+
if (!start || !(start in rules)) throw new Error(`emitModule: start rule '${start}' is not defined in rules`)
|
|
134
|
+
|
|
135
|
+
const lexer = createLexer(tokenDefs, { positions })
|
|
136
|
+
const analysis = analyze(rules, { tokenNames: lexer.tokenNames })
|
|
137
|
+
if (validate && analysis.errors.length > 0) throw new GrammarError(analysis.errors)
|
|
138
|
+
|
|
139
|
+
// The planner is needed only so codegen can record what each helper is; the
|
|
140
|
+
// closures it makes are thrown away.
|
|
141
|
+
const firsts = ll1 ? analysis.firsts : null
|
|
142
|
+
const engine = { build: () => { throw new Error('not compiled during emit') }, run: () => {} }
|
|
143
|
+
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false })
|
|
144
|
+
const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps })
|
|
145
|
+
|
|
146
|
+
codegen.compile(start)
|
|
147
|
+
|
|
148
|
+
const reached = [...codegen.ruleIndex.entries()].sort((a, b) => a[1] - b[1])
|
|
149
|
+
const unsupported = codegen.ORIGINS
|
|
150
|
+
.map((origin, index) => ({ origin, index }))
|
|
151
|
+
.filter(({ origin }) => origin.kind === 'unknownRule')
|
|
152
|
+
if (unsupported.length > 0) {
|
|
153
|
+
const names = unsupported.map(({ origin }) => origin.name).join(', ')
|
|
154
|
+
throw new Error(`emitModule: cannot emit a grammar with unknown rules (${names})`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Each action is compiled to source where possible and shipped as data where
|
|
158
|
+
// not. An action that climbs to the parser state always stays interpreted:
|
|
159
|
+
// the engine does not thread `above` into built functions.
|
|
160
|
+
//
|
|
161
|
+
// Compiling an action that calls a method needs that method to exist, so a
|
|
162
|
+
// grammar carrying its own methods block is evaluated here — the functions are
|
|
163
|
+
// used only to compile, never emitted. A block that imports cannot be
|
|
164
|
+
// evaluated in place; the CLI resolves those as a module and passes them in,
|
|
165
|
+
// and without that the actions simply ship as data.
|
|
166
|
+
const blockMethods = block && !semanticMethods ? tryEvaluate(methodsBlock) : null
|
|
167
|
+
const compileEngine = new LogicEngine()
|
|
168
|
+
for (const [name, method] of Object.entries(createValueMethods())) compileEngine.addMethod(name, method)
|
|
169
|
+
for (const [name, method] of Object.entries(semanticMethods || blockMethods || {})) compileEngine.addMethod(name, method)
|
|
170
|
+
|
|
171
|
+
let needsEngine = execution === 'interpreted'
|
|
172
|
+
let needsAbove = false
|
|
173
|
+
|
|
174
|
+
const emitAction = (logic) => {
|
|
175
|
+
if (usesOuterScope(logic) || execution === 'interpreted') {
|
|
176
|
+
needsEngine = true
|
|
177
|
+
return `interpretedAction(${literal(logic, 2)})`
|
|
178
|
+
}
|
|
179
|
+
const inlined = inlineAction(logic, compileEngine)
|
|
180
|
+
if (!inlined) {
|
|
181
|
+
needsEngine = true
|
|
182
|
+
return `action(${literal(logic, 2)})`
|
|
183
|
+
}
|
|
184
|
+
if (inlined.needsAbove) needsAbove = true
|
|
185
|
+
const body = `(context) => { let prev; return ${inlined.source} }`
|
|
186
|
+
return inlined.values ? `((values) => ${body})(${inlined.values})` : body
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const helpers = codegen.ORIGINS.map((origin) => {
|
|
190
|
+
if (origin.kind === 'constant') return `constant(${literal(origin.node, 2)})`
|
|
191
|
+
if (origin.kind === 'action') return emitAction(origin.logic)
|
|
192
|
+
if (origin.kind === 'valueExpression') return `valueExpression(${emitAction(origin.logic)})`
|
|
193
|
+
throw new Error(`emitModule: unsupported helper (${origin.kind})`)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// With every action inlined, all that is left of the engine is a table of the
|
|
197
|
+
// caller's own semantic methods — which the file can hold itself.
|
|
198
|
+
const engineFree = !needsEngine
|
|
199
|
+
|
|
200
|
+
const ruleSources = reached.map(([name, index]) =>
|
|
201
|
+
`// ${name}\nR[${index}] = ${codegen.SOURCES[index]}`
|
|
202
|
+
).join('\n\n')
|
|
203
|
+
|
|
204
|
+
const interpretedActions = execution === 'interpreted'
|
|
205
|
+
|
|
206
|
+
// The two formats differ only in how the file opens and closes; every rule,
|
|
207
|
+
// helper and action between them is the same source.
|
|
208
|
+
const cjs = format === 'cjs'
|
|
209
|
+
// A module that still has an engine runs some actions through it, and those
|
|
210
|
+
// actions may use the value methods, so it has to register them the way
|
|
211
|
+
// `createParser` does. They come from the runtime rather than being written
|
|
212
|
+
// out here, so there is one implementation rather than two.
|
|
213
|
+
const runtimeImports = ['FAIL as F', 'expect as E', 'createState', 'buildParseError', 'createLexer']
|
|
214
|
+
if (needsEngine) runtimeImports.push('createValueMethods')
|
|
215
|
+
|
|
216
|
+
const importRuntime = cjs
|
|
217
|
+
? `const { ${runtimeImports.map((b) => b.replace(' as ', ': ')).join(', ')} } = require(${quote(runtimeSpecifier)})`
|
|
218
|
+
: `import { ${runtimeImports.join(', ')} } from ${quote(runtimeSpecifier)}`
|
|
219
|
+
const importEngine = engineFree
|
|
220
|
+
? ''
|
|
221
|
+
: cjs
|
|
222
|
+
? `const { LogicEngine } = require(${quote(engineSpecifier)})\n`
|
|
223
|
+
: `import { LogicEngine } from ${quote(engineSpecifier)}\n`
|
|
224
|
+
|
|
225
|
+
const PUBLIC = ['parse', 'parseTokens', 'tokenize', 'tokens', 'start', 'registerMethods']
|
|
226
|
+
|
|
227
|
+
// A block's named exports come out of the generated parser too: a grammar that
|
|
228
|
+
// needs a helper on the host side can ship it rather than make the caller keep
|
|
229
|
+
// a matching file in sync.
|
|
230
|
+
for (const name of block ? block.names : []) {
|
|
231
|
+
if (PUBLIC.includes(name)) throw new Error(`emitModule: the methods block exports '${name}', which the generated parser already exports`)
|
|
232
|
+
}
|
|
233
|
+
const exported = block ? [...PUBLIC, ...block.names] : PUBLIC
|
|
234
|
+
|
|
235
|
+
const exports = cjs
|
|
236
|
+
? `module.exports = { ${exported.join(', ')} }\n// Named the same under \`default\`, so an \`import parser from\` interops.\nmodule.exports.default = module.exports`
|
|
237
|
+
: `export { ${exported.join(', ')} }\nexport default { ${PUBLIC.join(', ')} }`
|
|
238
|
+
|
|
239
|
+
return `// Generated by grammatik from the ${quote(moduleName)} grammar. Do not edit.
|
|
240
|
+
//
|
|
241
|
+
// The grammar was analyzed when this file was written: FIRST sets, left-recursion
|
|
242
|
+
// checks and rule specialization all happened then. Importing it does no grammar
|
|
243
|
+
// work and generates no code.
|
|
244
|
+
${engineFree
|
|
245
|
+
? '// Every semantic action was compiled to source too, so this file has no\n// dependency on json-logic-engine at all.'
|
|
246
|
+
: interpretedActions
|
|
247
|
+
? '// Semantic actions are interpreted, so nothing here calls `eval`.'
|
|
248
|
+
: '// Semantic actions that could not be compiled to source are shipped as JSON\n// Logic and built by json-logic-engine when this module loads.'}
|
|
249
|
+
|
|
250
|
+
${importRuntime}
|
|
251
|
+
${importEngine}
|
|
252
|
+
|
|
253
|
+
const tokens = ${literal(tokenDefs)}
|
|
254
|
+
|
|
255
|
+
const start = ${quote(start)}
|
|
256
|
+
|
|
257
|
+
const lexer = createLexer(tokens, { positions: ${quote(positions)} })
|
|
258
|
+
const tokenize = lexer.tokenize
|
|
259
|
+
|
|
260
|
+
${engineFree
|
|
261
|
+
? `// Every semantic action compiled to source, so the engine reduced to a table
|
|
262
|
+
// of the methods below. Nothing here imports json-logic-engine.
|
|
263
|
+
const M = Object.create(null)
|
|
264
|
+
const engine = { methods: M }
|
|
265
|
+
${needsAbove ? 'const above = []\n' : ''}
|
|
266
|
+
/**
|
|
267
|
+
* Semantic methods the grammar refers to. Pass the same object you would give
|
|
268
|
+
* \`createParser\`; anything the actions call must be registered before use.
|
|
269
|
+
*/
|
|
270
|
+
function registerMethods (methods) {
|
|
271
|
+
for (const [name, method] of Object.entries(methods || {})) {
|
|
272
|
+
M[name] = typeof method === 'function' ? { method } : method
|
|
273
|
+
}
|
|
274
|
+
return engine
|
|
275
|
+
}`
|
|
276
|
+
: `const engine = new LogicEngine()
|
|
277
|
+
for (const [name, method] of Object.entries(createValueMethods())) engine.addMethod(name, method)
|
|
278
|
+
${needsAbove ? 'const above = []\n' : ''}
|
|
279
|
+
/**
|
|
280
|
+
* Semantic methods the grammar refers to. Pass the same object you would give
|
|
281
|
+
* \`createParser\`; anything the actions call must be registered before use.
|
|
282
|
+
*/
|
|
283
|
+
function registerMethods (methods) {
|
|
284
|
+
for (const [name, method] of Object.entries(methods || {})) engine.addMethod(name, method)
|
|
285
|
+
return engine
|
|
286
|
+
}`}
|
|
287
|
+
|
|
288
|
+
${block
|
|
289
|
+
? `// ---------------------------------------------------------------------------
|
|
290
|
+
// The grammar's \`methods { ... }\` block, written out as source. It is registered
|
|
291
|
+
// below, so importing this module is enough — no wiring at the call site.
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
${block.source}
|
|
295
|
+
|
|
296
|
+
registerMethods(${block.table})
|
|
297
|
+
`
|
|
298
|
+
: ''}
|
|
299
|
+
${engineFree ? '' : `const interpretedAction = (logic) => (bindings, state) => engine.run(logic, bindings, { above: [state] })
|
|
300
|
+
`}
|
|
301
|
+
${engineFree
|
|
302
|
+
? ''
|
|
303
|
+
: interpretedActions
|
|
304
|
+
? 'const action = interpretedAction'
|
|
305
|
+
: `const action = (logic) => {
|
|
306
|
+
// Compiled on first use, so \`registerMethods\` can run first.
|
|
307
|
+
let fn
|
|
308
|
+
return (bindings, state) => {
|
|
309
|
+
if (fn === undefined) {
|
|
310
|
+
try {
|
|
311
|
+
const built = engine.build(logic)
|
|
312
|
+
fn = typeof built === 'function' ? (b) => built(b) : () => built
|
|
313
|
+
} catch {
|
|
314
|
+
fn = (b, s) => engine.run(logic, b, { above: [s] })
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return fn(bindings, state)
|
|
318
|
+
}
|
|
319
|
+
}`}
|
|
320
|
+
|
|
321
|
+
/** A JSON Logic value expression sitting in parser position. */
|
|
322
|
+
const valueExpression = (run) => (state) => run(state.bindings || {}, state)
|
|
323
|
+
const constant = (value) => () => value
|
|
324
|
+
|
|
325
|
+
const P = (op, at) => {
|
|
326
|
+
throw new Error(\`'\${op}' matched without consuming any input at token index \${at}; this would loop forever.\`)
|
|
327
|
+
}
|
|
328
|
+
const T = () => { throw new Error('Parser exceeded maxSteps (${maxSteps}); the grammar is likely looping.') }
|
|
329
|
+
|
|
330
|
+
/** Rule functions, indexed. Filled below; recursion resolves through the array. */
|
|
331
|
+
const R = []
|
|
332
|
+
|
|
333
|
+
/** Values the rules close over: semantic actions and literals. */
|
|
334
|
+
const H = [
|
|
335
|
+
${helpers.map((source, index) => ` /* ${index} */ ${source}`).join(',\n')}
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
${ruleSources}
|
|
339
|
+
|
|
340
|
+
const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name, index])))}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Parses a token stream you lexed yourself.
|
|
344
|
+
* @param {any[]} tokenList
|
|
345
|
+
* @param {string} [source]
|
|
346
|
+
*/
|
|
347
|
+
function parseTokens (tokenList, source, track = false) {
|
|
348
|
+
const state = createState(tokenList, {}, { memo: ${memo}, maxSteps: ${maxSteps}, track })
|
|
349
|
+
state.source = source
|
|
350
|
+
state.positions = ${quote(positions)}
|
|
351
|
+
state.ruleNames[0] = start
|
|
352
|
+
state.ruleDepth = 1
|
|
353
|
+
|
|
354
|
+
const result = R[RULES[start]](state)
|
|
355
|
+
const trailing = ${strict ? 'state.idx < tokenList.length' : 'false'}
|
|
356
|
+
if (result !== F && !trailing) return result
|
|
357
|
+
|
|
358
|
+
if (!track) return parseTokens(tokenList, source, true)
|
|
359
|
+
if (trailing && state.maxIdx < state.idx) {
|
|
360
|
+
state.maxIdx = state.idx
|
|
361
|
+
state.expected = ['end of input']
|
|
362
|
+
state.failureStack = state.ruleNames.slice(0, state.ruleDepth)
|
|
363
|
+
}
|
|
364
|
+
throw buildParseError(state)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Lexes and parses text. @param {string} text */
|
|
368
|
+
function parse (text) {
|
|
369
|
+
return parseTokens(lexer.tokenize(text), text)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
${exports}
|
|
373
|
+
`
|
|
374
|
+
}
|