@vertekum/schema-builder 0.1.6 → 0.2.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/CHANGELOG.md +28 -0
- package/README.md +6 -2
- package/docs/build.md +31 -6
- package/docs/emission.md +110 -48
- package/docs/expressions.md +12 -2
- package/docs/format.md +75 -0
- package/docs/language.md +41 -26
- package/docs/lint.md +23 -2
- package/docs/modules.md +8 -5
- package/docs/scales.md +29 -2
- package/grammar/dfn.tmLanguage.json +33 -4
- package/package.json +3 -3
- package/src/api.ts +16 -3
- package/src/ast.ts +23 -9
- package/src/build.ts +224 -28
- package/src/cli.ts +360 -32
- package/src/emit.ts +189 -65
- package/src/format.ts +273 -0
- package/src/index.ts +27 -1
- package/src/lint.ts +21 -4
- package/src/parser.ts +98 -13
- package/src/resolve.ts +12 -5
- package/src/tokenizer.ts +56 -4
package/src/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import { evaluateScale } from '@vertekum/core';
|
|
2
3
|
import type { Node, Ref } from './ast';
|
|
3
4
|
import { DfnError } from './error';
|
|
@@ -11,14 +12,39 @@ import type { ResolvedModule } from './resolve';
|
|
|
11
12
|
export interface TreeNode {
|
|
12
13
|
children: Map<string, TreeNode>;
|
|
13
14
|
open: boolean;
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* PATTERNS whose full member sets landed at this position — unmodified terminal
|
|
17
|
+
* references to public productions (local or imported) or to an imported def-scope
|
|
18
|
+
* module's root. Emission deep-checks each against the position's children and, when
|
|
19
|
+
* intact, emits a `$ref` to the pattern's `$def` instead of expanding; a merge that
|
|
20
|
+
* grew a member drops the pattern back to expansion. Keyed collision-proof.
|
|
21
|
+
*/
|
|
22
|
+
patterns?: Map<string, PatternRef>;
|
|
23
|
+
/**
|
|
24
|
+
* This subtree is verbatim one top-level branch of an imported DOCUMENT module's own
|
|
25
|
+
* artifact. Emission MAY replace it with a `$ref` into `…#/properties/<top>` (linked
|
|
26
|
+
* mode); inline mode ignores the tag.
|
|
27
|
+
*/
|
|
28
|
+
link?: { module: ResolvedModule; top: string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One pattern source: a production (local when `module` is absent) or a def-scope root. */
|
|
32
|
+
export interface PatternRef {
|
|
33
|
+
name: string;
|
|
34
|
+
module?: ResolvedModule;
|
|
35
|
+
/** The pattern is the module's ROOT def (`$defs.<filename>`), not a production. */
|
|
36
|
+
root?: boolean;
|
|
15
37
|
}
|
|
16
38
|
|
|
17
39
|
const leaf = (): TreeNode => ({ children: new Map(), open: false });
|
|
18
40
|
|
|
19
41
|
function merge(into: TreeNode, from: TreeNode): void {
|
|
20
42
|
into.open = into.open || from.open;
|
|
21
|
-
|
|
43
|
+
into.link = undefined;
|
|
44
|
+
if (from.patterns) {
|
|
45
|
+
into.patterns = into.patterns ?? new Map();
|
|
46
|
+
for (const [key, ref] of from.patterns) into.patterns.set(key, ref);
|
|
47
|
+
}
|
|
22
48
|
for (const [name, child] of from.children) {
|
|
23
49
|
const existing = into.children.get(name);
|
|
24
50
|
if (existing) merge(existing, child);
|
|
@@ -31,10 +57,27 @@ interface Scope {
|
|
|
31
57
|
module: ResolvedModule;
|
|
32
58
|
/** Local productions currently expanding, for reference-cycle detection. */
|
|
33
59
|
expanding: Set<string>;
|
|
60
|
+
/** Lint's warning sink (open-merge, …); absent in plain builds. */
|
|
61
|
+
warn?: (file: string, line: number, column: number, message: string) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The `<@key/name>, …` listing a miss suggests — an error should name the fix, not just the gap. */
|
|
65
|
+
function productionsOf(key: string, imported: ResolvedModule): string {
|
|
66
|
+
return [...imported.module.productions.keys()]
|
|
67
|
+
.filter((p) => !imported.module.private.has(p))
|
|
68
|
+
.map((p) => `<@${key}/${p}>`)
|
|
69
|
+
.join(', ');
|
|
34
70
|
}
|
|
35
71
|
|
|
36
72
|
/** Find what `<name>` / `<@name>` means in this scope. Imported roots go by module name. */
|
|
37
|
-
function target(
|
|
73
|
+
function target(
|
|
74
|
+
ref: Ref,
|
|
75
|
+
scope: Scope,
|
|
76
|
+
): { node: Node; scope: Scope; importedRoot?: ResolvedModule } {
|
|
77
|
+
// Every error here names the module whose TEXT holds the ref — `scope.module` — so a
|
|
78
|
+
// failure inside an imported production's body is attributed to that file, not to
|
|
79
|
+
// whichever module the walk started from.
|
|
80
|
+
const file = scope.module.path;
|
|
38
81
|
if (!ref.imported) {
|
|
39
82
|
const production = scope.module.module.productions.get(ref.name);
|
|
40
83
|
if (!production) {
|
|
@@ -42,6 +85,7 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
42
85
|
`unknown production '<${ref.name}>'`,
|
|
43
86
|
ref.line,
|
|
44
87
|
ref.column,
|
|
88
|
+
file,
|
|
45
89
|
);
|
|
46
90
|
}
|
|
47
91
|
return { node: production, scope };
|
|
@@ -51,36 +95,76 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
51
95
|
if (ref.from) {
|
|
52
96
|
const imported = scope.module.imports.get(ref.from);
|
|
53
97
|
if (!imported) {
|
|
54
|
-
throw new DfnError(
|
|
98
|
+
throw new DfnError(
|
|
99
|
+
`no import named '${ref.from}'`,
|
|
100
|
+
ref.line,
|
|
101
|
+
ref.column,
|
|
102
|
+
file,
|
|
103
|
+
);
|
|
55
104
|
}
|
|
56
105
|
const production = imported.module.productions.get(ref.name);
|
|
106
|
+
if (production && imported.module.private.has(ref.name)) {
|
|
107
|
+
throw new DfnError(
|
|
108
|
+
`'${ref.name}' is private to ${basename(imported.path)} — its public productions: ${productionsOf(ref.from, imported)}`,
|
|
109
|
+
ref.line,
|
|
110
|
+
ref.column,
|
|
111
|
+
file,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
57
114
|
if (!production) {
|
|
58
115
|
throw new DfnError(
|
|
59
|
-
`'${ref.from}' has no production '${ref.name}'`,
|
|
116
|
+
`'${ref.from}' has no production '${ref.name}' — it declares: ${productionsOf(ref.from, imported)}`,
|
|
60
117
|
ref.line,
|
|
61
118
|
ref.column,
|
|
119
|
+
file,
|
|
62
120
|
);
|
|
63
121
|
}
|
|
64
122
|
return {
|
|
65
123
|
node: production,
|
|
66
|
-
scope: { module: imported, expanding: new Set() },
|
|
124
|
+
scope: { module: imported, expanding: new Set(), warn: scope.warn },
|
|
67
125
|
};
|
|
68
126
|
}
|
|
69
127
|
|
|
70
|
-
const hits: Array<{
|
|
128
|
+
const hits: Array<{
|
|
129
|
+
node: Node;
|
|
130
|
+
scope: Scope;
|
|
131
|
+
importedRoot?: ResolvedModule;
|
|
132
|
+
}> = [];
|
|
71
133
|
for (const [key, imported] of scope.module.imports) {
|
|
72
|
-
const importedScope: Scope = {
|
|
134
|
+
const importedScope: Scope = {
|
|
135
|
+
module: imported,
|
|
136
|
+
expanding: new Set(),
|
|
137
|
+
warn: scope.warn,
|
|
138
|
+
};
|
|
73
139
|
const production = imported.module.productions.get(ref.name);
|
|
74
|
-
if (production
|
|
140
|
+
if (production && !imported.module.private.has(ref.name)) {
|
|
141
|
+
hits.push({ node: production, scope: importedScope });
|
|
142
|
+
}
|
|
75
143
|
if (key === ref.name && imported.module.root) {
|
|
76
|
-
hits.push({
|
|
144
|
+
hits.push({
|
|
145
|
+
node: imported.module.root,
|
|
146
|
+
scope: importedScope,
|
|
147
|
+
importedRoot: imported,
|
|
148
|
+
});
|
|
77
149
|
}
|
|
78
150
|
}
|
|
79
151
|
if (hits.length === 0) {
|
|
152
|
+
// The near-miss that reads as a missing `use`: the import EXISTS under this key, but
|
|
153
|
+
// `<@key>` means its root and a fragment has none. Say so, and name the real forms.
|
|
154
|
+
const fragment = scope.module.imports.get(ref.name);
|
|
155
|
+
if (fragment) {
|
|
156
|
+
throw new DfnError(
|
|
157
|
+
`'${ref.name}' is imported, but it declares no root (a fragment) — reference one of its productions: ${productionsOf(ref.name, fragment)}`,
|
|
158
|
+
ref.line,
|
|
159
|
+
ref.column,
|
|
160
|
+
file,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
80
163
|
throw new DfnError(
|
|
81
164
|
`no import provides '<@${ref.name}>'`,
|
|
82
165
|
ref.line,
|
|
83
166
|
ref.column,
|
|
167
|
+
file,
|
|
84
168
|
);
|
|
85
169
|
}
|
|
86
170
|
if (hits.length > 1) {
|
|
@@ -88,6 +172,7 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
88
172
|
`'<@${ref.name}>' is ambiguous across imports — qualify it: <@module/${ref.name}>`,
|
|
89
173
|
ref.line,
|
|
90
174
|
ref.column,
|
|
175
|
+
file,
|
|
91
176
|
);
|
|
92
177
|
}
|
|
93
178
|
return hits[0];
|
|
@@ -117,6 +202,8 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
117
202
|
max: node.max,
|
|
118
203
|
step: node.step,
|
|
119
204
|
pad: node.pad,
|
|
205
|
+
prefix: node.prefix,
|
|
206
|
+
suffix: node.suffix,
|
|
120
207
|
}
|
|
121
208
|
: {
|
|
122
209
|
kind: 'multiplied',
|
|
@@ -125,6 +212,8 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
125
212
|
factor: node.step,
|
|
126
213
|
quantum: node.quantum,
|
|
127
214
|
pad: node.pad,
|
|
215
|
+
prefix: node.prefix,
|
|
216
|
+
suffix: node.suffix,
|
|
128
217
|
},
|
|
129
218
|
);
|
|
130
219
|
} catch (error) {
|
|
@@ -132,6 +221,7 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
132
221
|
error instanceof Error ? error.message : String(error),
|
|
133
222
|
node.line,
|
|
134
223
|
node.column,
|
|
224
|
+
scope.module.path,
|
|
135
225
|
);
|
|
136
226
|
}
|
|
137
227
|
if (scale.collisions.length > 0) {
|
|
@@ -139,6 +229,7 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
139
229
|
`scale steps ${scale.collisions.join(', ')} quantize onto earlier names — the quantum is too coarse for the factor`,
|
|
140
230
|
node.line,
|
|
141
231
|
node.column,
|
|
232
|
+
scope.module.path,
|
|
142
233
|
);
|
|
143
234
|
}
|
|
144
235
|
const forest = leaf();
|
|
@@ -149,9 +240,26 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
149
240
|
}
|
|
150
241
|
case 'alt': {
|
|
151
242
|
const forest = leaf();
|
|
152
|
-
|
|
153
|
-
|
|
243
|
+
const forests = node.options.map((option) => ({
|
|
244
|
+
option,
|
|
245
|
+
forest: evaluate(option, scope, tail),
|
|
246
|
+
}));
|
|
247
|
+
// Opening a subset of a position's set opens the WHOLE position — additions carry
|
|
248
|
+
// no mark of which family they claim. Legal, but never silent.
|
|
249
|
+
const open = forests.find((f) => f.forest.open);
|
|
250
|
+
const closed = forests.find(
|
|
251
|
+
(f) => !f.forest.open && f.forest.children.size > 0,
|
|
252
|
+
);
|
|
253
|
+
if (open && closed && scope.warn) {
|
|
254
|
+
const at = open.option as { line?: number; column?: number };
|
|
255
|
+
scope.warn(
|
|
256
|
+
scope.module.path,
|
|
257
|
+
at.line ?? 1,
|
|
258
|
+
at.column ?? 1,
|
|
259
|
+
'an open set merges with closed siblings here — the whole position becomes open (additions are permitted beside every member)',
|
|
260
|
+
);
|
|
154
261
|
}
|
|
262
|
+
for (const f of forests) merge(forest, f.forest);
|
|
155
263
|
return forest;
|
|
156
264
|
}
|
|
157
265
|
case 'path': {
|
|
@@ -181,52 +289,110 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
181
289
|
return forest;
|
|
182
290
|
}
|
|
183
291
|
case 'ref': {
|
|
184
|
-
const {
|
|
292
|
+
const {
|
|
293
|
+
node: production,
|
|
294
|
+
scope: refScope,
|
|
295
|
+
importedRoot,
|
|
296
|
+
} = target(node, scope);
|
|
185
297
|
if (!node.imported) {
|
|
186
298
|
if (scope.expanding.has(node.name)) {
|
|
187
299
|
throw new DfnError(
|
|
188
300
|
`'<${node.name}>' expands through itself`,
|
|
189
301
|
node.line,
|
|
190
302
|
node.column,
|
|
303
|
+
scope.module.path,
|
|
191
304
|
);
|
|
192
305
|
}
|
|
193
306
|
scope.expanding.add(node.name);
|
|
194
307
|
}
|
|
195
308
|
const forest = evaluate(production, refScope, tail);
|
|
196
309
|
if (!node.imported) scope.expanding.delete(node.name);
|
|
197
|
-
|
|
310
|
+
// Set-valued operands: a member REFERENCE contributes every top-level name of the
|
|
311
|
+
// set it denotes (evaluated in this scope, privacy and all); strings pass through.
|
|
312
|
+
const expandOperands = (list: Ref['omit']): string[] => {
|
|
313
|
+
const out: string[] = [];
|
|
314
|
+
for (const member of list) {
|
|
315
|
+
if (typeof member === 'string') {
|
|
316
|
+
out.push(member);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const operand = evaluate(member, scope, leaf);
|
|
320
|
+
for (const name of operand.children.keys()) out.push(name);
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
};
|
|
324
|
+
for (const omitted of expandOperands(node.omit)) {
|
|
198
325
|
if (!forest.children.delete(omitted)) {
|
|
199
326
|
throw new DfnError(
|
|
200
327
|
`'<${node.name}>' has no member '${omitted}' to omit`,
|
|
201
328
|
node.line,
|
|
202
329
|
node.column,
|
|
330
|
+
scope.module.path,
|
|
203
331
|
);
|
|
204
332
|
}
|
|
205
333
|
}
|
|
206
334
|
if (node.pick.length > 0) {
|
|
207
|
-
|
|
335
|
+
const picks = expandOperands(node.pick);
|
|
336
|
+
for (const picked of picks) {
|
|
208
337
|
if (!forest.children.has(picked)) {
|
|
209
338
|
throw new DfnError(
|
|
210
339
|
`'<${node.name}>' has no member '${picked}' to pick`,
|
|
211
340
|
node.line,
|
|
212
341
|
node.column,
|
|
342
|
+
scope.module.path,
|
|
213
343
|
);
|
|
214
344
|
}
|
|
215
345
|
}
|
|
216
|
-
const keep = new Set(
|
|
346
|
+
const keep = new Set(picks);
|
|
217
347
|
for (const name of [...forest.children.keys()]) {
|
|
218
348
|
if (!keep.has(name)) forest.children.delete(name);
|
|
219
349
|
}
|
|
220
350
|
}
|
|
221
351
|
if (node.open) forest.open = true;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
352
|
+
const unmodified =
|
|
353
|
+
node.pick.length === 0 && node.omit.length === 0 && !node.open;
|
|
354
|
+
// A modified set is a different set — it never shares a pattern's $def; a tail
|
|
355
|
+
// forces expansion (a $ref cannot be parameterized). Tags are inert unless
|
|
356
|
+
// emission chooses to reference them.
|
|
357
|
+
if (unmodified && isTerminal(tail())) {
|
|
358
|
+
if (importedRoot) {
|
|
359
|
+
const nature =
|
|
360
|
+
importedRoot.module.meta.scope ??
|
|
361
|
+
(importedRoot.module.root ? 'document' : 'def');
|
|
362
|
+
if (nature === 'document') {
|
|
363
|
+
// Verbatim top-level branches of the child DOCUMENT's artifact.
|
|
364
|
+
for (const [top, child] of forest.children) {
|
|
365
|
+
child.link = { module: importedRoot, top };
|
|
366
|
+
}
|
|
367
|
+
} else if (nature === 'def') {
|
|
368
|
+
forest.patterns = forest.patterns ?? new Map();
|
|
369
|
+
forest.patterns.set(`@${importedRoot.path}//root`, {
|
|
370
|
+
name: importedRoot.name,
|
|
371
|
+
module: importedRoot,
|
|
372
|
+
root: true,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// inline nature: pure expansion, no tag.
|
|
376
|
+
} else if (!node.imported) {
|
|
377
|
+
if (!scope.module.module.private.has(node.name)) {
|
|
378
|
+
forest.patterns = forest.patterns ?? new Map();
|
|
379
|
+
forest.patterns.set(node.name, { name: node.name });
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// An imported PRODUCTION (qualified or search-resolved): public by
|
|
383
|
+
// construction (target refuses privates). Pattern in the child's artifact
|
|
384
|
+
// unless the child is inline-natured.
|
|
385
|
+
const child = refScope.module;
|
|
386
|
+
const nature =
|
|
387
|
+
child.module.meta.scope ?? (child.module.root ? 'document' : 'def');
|
|
388
|
+
if (nature !== 'inline') {
|
|
389
|
+
forest.patterns = forest.patterns ?? new Map();
|
|
390
|
+
forest.patterns.set(`@${child.path}/${node.name}`, {
|
|
391
|
+
name: node.name,
|
|
392
|
+
module: child,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
230
396
|
}
|
|
231
397
|
return forest;
|
|
232
398
|
}
|
|
@@ -241,6 +407,9 @@ function isNameSet(node: Node): boolean {
|
|
|
241
407
|
return true;
|
|
242
408
|
case 'alt':
|
|
243
409
|
return node.options.every(isNameSet);
|
|
410
|
+
case 'group':
|
|
411
|
+
// Decorative brackets do not change a set's nature.
|
|
412
|
+
return isNameSet(node.node);
|
|
244
413
|
default:
|
|
245
414
|
return false;
|
|
246
415
|
}
|
|
@@ -251,12 +420,31 @@ function isTerminal(node: TreeNode): boolean {
|
|
|
251
420
|
}
|
|
252
421
|
|
|
253
422
|
/** Expand a module's root into its name tree. Fragments-only modules cannot build. */
|
|
254
|
-
export function build(
|
|
423
|
+
export function build(
|
|
424
|
+
resolved: ResolvedModule,
|
|
425
|
+
options: {
|
|
426
|
+
warn?: (
|
|
427
|
+
file: string,
|
|
428
|
+
line: number,
|
|
429
|
+
column: number,
|
|
430
|
+
message: string,
|
|
431
|
+
) => void;
|
|
432
|
+
} = {},
|
|
433
|
+
): TreeNode {
|
|
255
434
|
const root = resolved.module.root;
|
|
256
435
|
if (!root) {
|
|
257
|
-
throw new DfnError(
|
|
436
|
+
throw new DfnError(
|
|
437
|
+
`${resolved.name}.dfn declares no root`,
|
|
438
|
+
1,
|
|
439
|
+
1,
|
|
440
|
+
resolved.path,
|
|
441
|
+
);
|
|
258
442
|
}
|
|
259
|
-
const scope: Scope = {
|
|
443
|
+
const scope: Scope = {
|
|
444
|
+
module: resolved,
|
|
445
|
+
expanding: new Set(),
|
|
446
|
+
warn: options.warn,
|
|
447
|
+
};
|
|
260
448
|
|
|
261
449
|
// An aggregate root unions imported roots; the same top-level name arriving from two different
|
|
262
450
|
// module refs is a collision, not a merge — each module owns its top. `[a | b]` and `a | b`
|
|
@@ -274,6 +462,7 @@ export function build(resolved: ResolvedModule): TreeNode {
|
|
|
274
462
|
`top-level '${name}' comes from both '${owner}' and '${option.name}'`,
|
|
275
463
|
option.line,
|
|
276
464
|
option.column,
|
|
465
|
+
resolved.path,
|
|
277
466
|
);
|
|
278
467
|
}
|
|
279
468
|
seen.set(name, option.name);
|
|
@@ -296,12 +485,17 @@ export function build(resolved: ResolvedModule): TreeNode {
|
|
|
296
485
|
export function evaluateProduction(
|
|
297
486
|
resolved: ResolvedModule,
|
|
298
487
|
name: string,
|
|
488
|
+
warn?: (file: string, line: number, column: number, message: string) => void,
|
|
299
489
|
): TreeNode {
|
|
300
490
|
const production = resolved.module.productions.get(name);
|
|
301
491
|
if (!production) {
|
|
302
492
|
throw new DfnError(`no production '${name}'`, 1, 1);
|
|
303
493
|
}
|
|
304
|
-
const scope: Scope = {
|
|
494
|
+
const scope: Scope = {
|
|
495
|
+
module: resolved,
|
|
496
|
+
expanding: new Set([name]),
|
|
497
|
+
warn,
|
|
498
|
+
};
|
|
305
499
|
return evaluate(production, scope, leaf);
|
|
306
500
|
}
|
|
307
501
|
|
|
@@ -332,6 +526,7 @@ export function assertOpenSetsAreNameSets(
|
|
|
332
526
|
'* opens a set of names, not sub-paths',
|
|
333
527
|
node.line,
|
|
334
528
|
node.column,
|
|
529
|
+
scope.module.path,
|
|
335
530
|
);
|
|
336
531
|
}
|
|
337
532
|
assertOpenSetsAreNameSets(resolved, node.node, scope);
|
|
@@ -344,6 +539,7 @@ export function assertOpenSetsAreNameSets(
|
|
|
344
539
|
`'<${node.name}*>' opens a set, but the production is not name-only`,
|
|
345
540
|
node.line,
|
|
346
541
|
node.column,
|
|
542
|
+
scope.module.path,
|
|
347
543
|
);
|
|
348
544
|
}
|
|
349
545
|
}
|