@endevops/effect-codec-xml 0.0.1
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/LICENSE-is-entities +21 -0
- package/LICENSE-is-xml-naming +21 -0
- package/README.md +415 -0
- package/dist/codec.d.ts +48 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +63 -0
- package/dist/codec.js.map +1 -0
- package/dist/conventions.d.ts +88 -0
- package/dist/conventions.d.ts.map +1 -0
- package/dist/conventions.js +113 -0
- package/dist/conventions.js.map +1 -0
- package/dist/entities/entity-decoder.d.ts +333 -0
- package/dist/entities/entity-decoder.d.ts.map +1 -0
- package/dist/entities/entity-decoder.js +841 -0
- package/dist/entities/entity-decoder.js.map +1 -0
- package/dist/entities/entity-tables.js +16 -0
- package/dist/entities/entity-tables.js.map +1 -0
- package/dist/errors.d.ts +49 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/namespaces.d.ts +101 -0
- package/dist/namespaces.d.ts.map +1 -0
- package/dist/namespaces.js +663 -0
- package/dist/namespaces.js.map +1 -0
- package/dist/naming.d.ts +149 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +296 -0
- package/dist/naming.js.map +1 -0
- package/dist/parse.d.ts +75 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +437 -0
- package/dist/parse.js.map +1 -0
- package/dist/render.d.ts +99 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +509 -0
- package/dist/render.js.map +1 -0
- package/dist/xml-error.d.ts +172 -0
- package/dist/xml-error.d.ts.map +1 -0
- package/dist/xml-error.js +157 -0
- package/dist/xml-error.js.map +1 -0
- package/dist/xml-value.d.ts +42 -0
- package/dist/xml-value.d.ts.map +1 -0
- package/dist/xml-value.js +79 -0
- package/dist/xml-value.js.map +1 -0
- package/package.json +69 -0
- package/src/codec.ts +136 -0
- package/src/conventions.ts +145 -0
- package/src/entities/entity-decoder.ts +1248 -0
- package/src/entities/entity-tables.ts +18 -0
- package/src/errors.ts +55 -0
- package/src/index.ts +79 -0
- package/src/namespaces.ts +968 -0
- package/src/naming.ts +519 -0
- package/src/parse.ts +597 -0
- package/src/render.ts +708 -0
- package/src/xml-error.ts +168 -0
- package/src/xml-value.ts +108 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
// Namespaces: attributing an element to a namespace URI, and resolving that at
|
|
2
|
+
// the text boundary.
|
|
3
|
+
//
|
|
4
|
+
// A schema describes a value in local names. This module lets a schema node say
|
|
5
|
+
// which namespace its element belongs to, through Effect's own annotations:
|
|
6
|
+
//
|
|
7
|
+
// - `xmlNamespace` is the namespace URI of the element. A field that carries
|
|
8
|
+
// one has its element placed in that namespace; the field's name stays the
|
|
9
|
+
// local name, so the schema does not hard-code a wire prefix.
|
|
10
|
+
// - `xmlPrefix` is the wire prefix to write for that URI. When it is omitted
|
|
11
|
+
// the namespace is written as the default namespace (`xmlns="…"`), and an
|
|
12
|
+
// unprefixed element name is used.
|
|
13
|
+
// - `xmlName` is the wire local name to write when it differs from the schema
|
|
14
|
+
// field's own name. It applies to an element or an attribute, and a colon
|
|
15
|
+
// is not allowed because the prefix comes from `xmlPrefix`.
|
|
16
|
+
// - `xmlAttribute` marks a field as an attribute without the schema key
|
|
17
|
+
// carrying the `@` prefix. A namespaced attribute still needs `xmlPrefix`,
|
|
18
|
+
// because a default namespace does not apply to attributes.
|
|
19
|
+
// - `xmlValue` marks one field as the element's character data, the `#text`
|
|
20
|
+
// value, for an element that also carries attributes or children.
|
|
21
|
+
//
|
|
22
|
+
// The namespace of an element is inherited by its descendants, the way an XML
|
|
23
|
+
// default namespace is. An attribute never inherits: it is in a namespace only
|
|
24
|
+
// when it is annotated with one explicitly, because a default namespace does
|
|
25
|
+
// not apply to attributes.
|
|
26
|
+
//
|
|
27
|
+
// On the way out, each element writes its own declaration when the prefix or
|
|
28
|
+
// default is not already in scope. On the way in, the parser's own declarations
|
|
29
|
+
// are read into scope and every name is resolved to its URI, so a document that
|
|
30
|
+
// binds the same URI to a different prefix still decodes to the same value. The
|
|
31
|
+
// declaration attributes are dropped from the decoded value; they are the
|
|
32
|
+
// codec's to manage, not the schema's.
|
|
33
|
+
//
|
|
34
|
+
// The plan is built per local name, which is what a schema field is. One local
|
|
35
|
+
// name cannot belong to two namespaces in one codec; that is reported when the
|
|
36
|
+
// codec is built rather than guessed at.
|
|
37
|
+
|
|
38
|
+
import type { Schema } from 'effect';
|
|
39
|
+
|
|
40
|
+
import { Predicate, SchemaAST } from 'effect';
|
|
41
|
+
|
|
42
|
+
import type { XmlRecord, XmlValue } from './xml-value.ts';
|
|
43
|
+
|
|
44
|
+
import { ATTRIBUTE_PREFIX, isAttributeKey, TEXT_KEY } from './conventions.ts';
|
|
45
|
+
|
|
46
|
+
declare module 'effect/Schema' {
|
|
47
|
+
namespace Annotations {
|
|
48
|
+
interface XmlAnnotations {
|
|
49
|
+
/**
|
|
50
|
+
* @description The namespace URI this schema's element belongs to. Read as the local name on the wire, with `xmlPrefix` choosing the prefix.
|
|
51
|
+
*/
|
|
52
|
+
readonly xmlNamespace?: string | undefined;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @description The wire prefix to write for {@link xmlNamespace}. Omit it to write the namespace as the default (`xmlns="…"`) with unprefixed element names.
|
|
56
|
+
*/
|
|
57
|
+
readonly xmlPrefix?: string | undefined;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @description The local name to write for an element or attribute, when it differs from the schema key. A colon is not allowed here; the prefix comes from
|
|
61
|
+
* {@link xmlPrefix}.
|
|
62
|
+
*/
|
|
63
|
+
readonly xmlName?: string | undefined;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @description Whether this field is an XML attribute rather than a child element. Use it to keep the schema key a plain name instead of carrying the `@`
|
|
67
|
+
* prefix. A namespaced attribute still needs `xmlPrefix`, because a default namespace does not apply to attributes.
|
|
68
|
+
*/
|
|
69
|
+
readonly xmlAttribute?: boolean | undefined;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @description Whether this field holds the element's character data, the `#text` value, rather than a child element. It has no name, so it cannot be
|
|
73
|
+
* combined with `xmlAttribute`, `xmlName`, or a namespace.
|
|
74
|
+
*/
|
|
75
|
+
readonly xmlValue?: boolean | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Annotations extends XmlAnnotations {}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @description The annotation key holding an element's namespace URI.
|
|
84
|
+
*/
|
|
85
|
+
export const NAMESPACE_KEY = 'xmlNamespace';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @description The annotation key holding the wire prefix for an element's namespace.
|
|
89
|
+
*/
|
|
90
|
+
export const PREFIX_KEY = 'xmlPrefix';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @description The annotation key holding the wire local name for an element or attribute.
|
|
94
|
+
*/
|
|
95
|
+
export const NAME_KEY = 'xmlName';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @description The annotation key marking a field as an XML attribute.
|
|
99
|
+
*/
|
|
100
|
+
export const ATTRIBUTE_KEY = 'xmlAttribute';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @description The annotation key marking a field as the element's character data.
|
|
104
|
+
*/
|
|
105
|
+
export const VALUE_KEY = 'xmlValue';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @description An element's namespace: the URI, and the prefix to write it with. An empty prefix is the default namespace.
|
|
109
|
+
*/
|
|
110
|
+
export interface XmlNamespace {
|
|
111
|
+
readonly uri: string;
|
|
112
|
+
readonly prefix: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @description What one codec needs to place and resolve names: the namespace of every local name in the schema, the root element's namespace, and the reverse
|
|
117
|
+
* lookup from a resolved `(uri, local)` back to the schema's key.
|
|
118
|
+
*/
|
|
119
|
+
export interface NamespacePlan {
|
|
120
|
+
/**
|
|
121
|
+
* @description The namespace of each field, keyed by the field's element path, so the same name nested differently stays apart.
|
|
122
|
+
*/
|
|
123
|
+
readonly byKey: ReadonlyMap<string, XmlNamespace>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @description The wire local name of each field that overrides the schema's own name with `xmlName`, keyed by the field's element path.
|
|
127
|
+
*/
|
|
128
|
+
readonly nameByKey: ReadonlyMap<string, string>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @description The paths of the fields that `xmlAttribute` marks as attributes but whose names do not carry the `@` prefix.
|
|
132
|
+
*/
|
|
133
|
+
readonly attributeKeys: ReadonlySet<string>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @description The value field of each element, keyed by the element's path. The root's path is {@link ROOT_ELEMENT}.
|
|
137
|
+
*/
|
|
138
|
+
readonly valueByElement: ReadonlyMap<string, string>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @description The paths of array fields that name their element with `xmlName`, so a single occurrence decodes as a one-member array.
|
|
142
|
+
*/
|
|
143
|
+
readonly arrayKeys: ReadonlySet<string>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @description The root element's namespace, or `undefined` when the root is unannotated.
|
|
147
|
+
*/
|
|
148
|
+
readonly root: XmlNamespace | undefined;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @description The root element's local name from its `xmlName` annotation, or `undefined`.
|
|
152
|
+
*/
|
|
153
|
+
readonly rootName: string | undefined;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @description The schema key for a resolved name, keyed by kind, `uri`, and local name. Lets a document with any prefix, and an element beside an attribute of
|
|
157
|
+
* the same name, resolve back to the schema.
|
|
158
|
+
*/
|
|
159
|
+
readonly byResolved: ReadonlyMap<string, string>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,
|
|
164
|
+
* so both are unwrapped to find the namespace the field actually carries.
|
|
165
|
+
*
|
|
166
|
+
* @param ast - The AST to read.
|
|
167
|
+
*
|
|
168
|
+
* @returns The namespace, or `undefined`.
|
|
169
|
+
*/
|
|
170
|
+
const annotationAt = (ast: SchemaAST.AST, key: string): unknown => {
|
|
171
|
+
const seen = new Set<SchemaAST.AST>();
|
|
172
|
+
const find = (node: SchemaAST.AST): unknown => {
|
|
173
|
+
if (seen.has(node)) return undefined;
|
|
174
|
+
seen.add(node);
|
|
175
|
+
const value = SchemaAST.resolve(node)?.[key];
|
|
176
|
+
if (value !== undefined) return value;
|
|
177
|
+
if (node._tag === 'Union') {
|
|
178
|
+
for (const member of node.types) {
|
|
179
|
+
const found = find(member);
|
|
180
|
+
if (found !== undefined) return found;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
return node._tag === 'Suspend' ? find(node.thunk()) : undefined;
|
|
185
|
+
};
|
|
186
|
+
return find(ast);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,
|
|
191
|
+
* so both are unwrapped to find the namespace the field carries.
|
|
192
|
+
*
|
|
193
|
+
* @param ast - The AST to read.
|
|
194
|
+
*
|
|
195
|
+
* @returns The namespace, or `undefined`.
|
|
196
|
+
*/
|
|
197
|
+
const namespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {
|
|
198
|
+
const uri = annotationAt(ast, NAMESPACE_KEY);
|
|
199
|
+
if (!Predicate.isString(uri)) return undefined;
|
|
200
|
+
const prefix = annotationAt(ast, PREFIX_KEY);
|
|
201
|
+
return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @description The wire local name an AST's own annotations declare, or `undefined`.
|
|
206
|
+
*
|
|
207
|
+
* @param ast - The AST to read.
|
|
208
|
+
*
|
|
209
|
+
* @returns The local name, or `undefined`.
|
|
210
|
+
*/
|
|
211
|
+
const nameOf = (ast: SchemaAST.AST): string | undefined => {
|
|
212
|
+
const name = annotationAt(ast, NAME_KEY);
|
|
213
|
+
return Predicate.isString(name) ? name : undefined;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @description The namespace a property carries when its annotation was attached with `Schema.annotateKey` rather than to the field's schema.
|
|
218
|
+
*
|
|
219
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
220
|
+
*
|
|
221
|
+
* @returns The namespace, or `undefined`.
|
|
222
|
+
*/
|
|
223
|
+
const keyNamespaceOf = (ast: SchemaAST.AST): XmlNamespace | undefined => {
|
|
224
|
+
const annotations = ast.context?.annotations;
|
|
225
|
+
const uri = annotations?.[NAMESPACE_KEY];
|
|
226
|
+
if (!Predicate.isString(uri)) return undefined;
|
|
227
|
+
const prefix = annotations?.[PREFIX_KEY];
|
|
228
|
+
return { uri, prefix: Predicate.isString(prefix) ? prefix : '' };
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @description The wire local name a property's key annotations declare, or `undefined`.
|
|
233
|
+
*
|
|
234
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
235
|
+
*
|
|
236
|
+
* @returns The local name, or `undefined`.
|
|
237
|
+
*/
|
|
238
|
+
const keyNameOf = (ast: SchemaAST.AST): string | undefined => {
|
|
239
|
+
const name = ast.context?.annotations?.[NAME_KEY];
|
|
240
|
+
return Predicate.isString(name) ? name : undefined;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @description Whether an AST's own annotations mark the field as an XML attribute.
|
|
245
|
+
*
|
|
246
|
+
* @param ast - The AST to read.
|
|
247
|
+
*
|
|
248
|
+
* @returns Whether the annotation is set.
|
|
249
|
+
*/
|
|
250
|
+
const attributeOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, ATTRIBUTE_KEY) === true;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @description Whether a property's key annotations mark the field as an XML attribute.
|
|
254
|
+
*
|
|
255
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
256
|
+
*
|
|
257
|
+
* @returns Whether the annotation is set.
|
|
258
|
+
*/
|
|
259
|
+
const keyAttributeOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[ATTRIBUTE_KEY] === true;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @description Whether an AST's own annotations mark the field as the element's character data.
|
|
263
|
+
*
|
|
264
|
+
* @param ast - The AST to read.
|
|
265
|
+
*
|
|
266
|
+
* @returns Whether the annotation is set.
|
|
267
|
+
*/
|
|
268
|
+
const valueOf = (ast: SchemaAST.AST): boolean => annotationAt(ast, VALUE_KEY) === true;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @description Whether a property's key annotations mark the field as the element's character data.
|
|
272
|
+
*
|
|
273
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
274
|
+
*
|
|
275
|
+
* @returns Whether the annotation is set.
|
|
276
|
+
*/
|
|
277
|
+
const keyValueOf = (ast: SchemaAST.AST): boolean => ast.context?.annotations?.[VALUE_KEY] === true;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @description Whether a struct property holds the element's character data, marked with `xmlValue` on its schema or its key.
|
|
281
|
+
*
|
|
282
|
+
* @param ast - The property's value AST.
|
|
283
|
+
*
|
|
284
|
+
* @returns Whether the property is the value.
|
|
285
|
+
*/
|
|
286
|
+
const isValueProperty = (ast: SchemaAST.AST): boolean => keyValueOf(ast) || valueOf(ast);
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @description The local name a schema key names, with the attribute prefix removed. This is what a declaration resolves to.
|
|
290
|
+
*
|
|
291
|
+
* @param key - The schema key.
|
|
292
|
+
*
|
|
293
|
+
* @returns The local name.
|
|
294
|
+
*/
|
|
295
|
+
const localName = (key: string): string => (isAttributeKey(key) ? key.slice(ATTRIBUTE_PREFIX.length) : key);
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @description The path of the root element. A plan is keyed by the path of the element a field belongs to, so the same name nested differently stays apart. The
|
|
299
|
+
* path is the schema keys from the root joined by {@link PATH_SEPARATOR}.
|
|
300
|
+
*/
|
|
301
|
+
export const ROOT_ELEMENT = '';
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @description The separator between the schema keys in an element path. A NUL is not a legal XML name character, so it cannot collide with a key.
|
|
305
|
+
*/
|
|
306
|
+
const PATH_SEPARATOR = '\u0000';
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @description The path of a child element, appended to its parent's path.
|
|
310
|
+
*
|
|
311
|
+
* @param parent - The parent element's path.
|
|
312
|
+
* @param key - The child's schema key.
|
|
313
|
+
*
|
|
314
|
+
* @returns The child's path.
|
|
315
|
+
*/
|
|
316
|
+
const childPath = (parent: string, key: string): string => (parent === ROOT_ELEMENT ? key : `${parent}${PATH_SEPARATOR}${key}`);
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @description The schema key at the end of an element path.
|
|
320
|
+
*
|
|
321
|
+
* @param path - The element path.
|
|
322
|
+
*
|
|
323
|
+
* @returns The schema key of the element itself.
|
|
324
|
+
*/
|
|
325
|
+
const pathKey = (path: string): string => {
|
|
326
|
+
const at = path.lastIndexOf(PATH_SEPARATOR);
|
|
327
|
+
return at === -1 ? path : path.slice(at + 1);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @description Whether a field is an attribute: the key at the end of its path carries the `@` prefix, or `xmlAttribute` marks the field.
|
|
332
|
+
*
|
|
333
|
+
* @param plan - The namespace plan.
|
|
334
|
+
* @param path - The field's element path.
|
|
335
|
+
*
|
|
336
|
+
* @returns Whether the field is an attribute.
|
|
337
|
+
*/
|
|
338
|
+
const isAttributeOf = (plan: NamespacePlan, path: string): boolean => isAttributeKey(pathKey(path)) || plan.attributeKeys.has(path);
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @description The wire local name of a field: its `xmlName` override, or the key at the end of its path with the attribute prefix removed.
|
|
342
|
+
*
|
|
343
|
+
* @param plan - The namespace plan.
|
|
344
|
+
* @param path - The field's element path.
|
|
345
|
+
*
|
|
346
|
+
* @returns The local name.
|
|
347
|
+
*/
|
|
348
|
+
const localOf = (plan: NamespacePlan, path: string): string => plan.nameByKey.get(path) ?? localName(pathKey(path));
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* @description The reverse-lookup key for a resolved wire name. The attribute flag is part of it, so an element and an attribute of the same local name in the
|
|
352
|
+
* same namespace stay distinct.
|
|
353
|
+
*
|
|
354
|
+
* @param isAttribute - Whether the name is an attribute.
|
|
355
|
+
* @param uri - The resolved namespace URI, or `undefined`.
|
|
356
|
+
* @param local - The resolved local name.
|
|
357
|
+
*
|
|
358
|
+
* @returns The lookup key.
|
|
359
|
+
*/
|
|
360
|
+
const resolvedKey = (isAttribute: boolean, uri: string | undefined, local: string): string =>
|
|
361
|
+
`${isAttribute ? ATTRIBUTE_PREFIX : ''}${uri ?? ''}|${local}`;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @description The path of the parent element, one segment shorter than the field's own path.
|
|
365
|
+
*
|
|
366
|
+
* @param path - The field's element path.
|
|
367
|
+
*
|
|
368
|
+
* @returns The parent element's path, or the root sentinel.
|
|
369
|
+
*/
|
|
370
|
+
const parentPath = (path: string): string => {
|
|
371
|
+
const at = path.lastIndexOf(PATH_SEPARATOR);
|
|
372
|
+
return at === -1 ? ROOT_ELEMENT : path.slice(0, at);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @description The reverse-lookup key for a wire name under one parent element. The parent's path is part of it, so the same wire name under two parents resolves
|
|
377
|
+
* to each parent's own field.
|
|
378
|
+
*
|
|
379
|
+
* @param parent - The parent element's path.
|
|
380
|
+
* @param resolved - A name from {@link resolvedKey}.
|
|
381
|
+
*
|
|
382
|
+
* @returns The lookup key.
|
|
383
|
+
*/
|
|
384
|
+
const lookupKey = (parent: string, resolved: string): string => `${parent}\u0001${resolved}`;
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @description The mutable state one namespace scan carries: the plan under construction, the local names that resolve to more than one namespace, and the AST
|
|
388
|
+
* nodes already visited so a recursive schema terminates.
|
|
389
|
+
*/
|
|
390
|
+
interface Scan {
|
|
391
|
+
readonly byKey: Map<string, XmlNamespace>;
|
|
392
|
+
readonly nameByKey: Map<string, string>;
|
|
393
|
+
readonly attributeKeys: Set<string>;
|
|
394
|
+
readonly valueByElement: Map<string, string>;
|
|
395
|
+
readonly arrayKeys: Set<string>;
|
|
396
|
+
readonly problems: Array<string>;
|
|
397
|
+
/**
|
|
398
|
+
* @description The AST nodes on the current scan path. A recursive schema terminates because the `Suspend` node is still on the path when its thunk is reached,
|
|
399
|
+
* and a schema reused under two sibling paths is scanned once per path because each node is removed again on the way out.
|
|
400
|
+
*/
|
|
401
|
+
readonly seen: Set<SchemaAST.AST>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @description Records a field's namespace, reporting a local name that would belong to two namespaces at once.
|
|
406
|
+
*
|
|
407
|
+
* @param scan - The scan state.
|
|
408
|
+
* @param path - The field's element path.
|
|
409
|
+
* @param namespace - The namespace, or `undefined` when the field has none.
|
|
410
|
+
*/
|
|
411
|
+
const record = (scan: Scan, path: string, namespace: XmlNamespace | undefined): void => {
|
|
412
|
+
if (Predicate.isUndefined(namespace)) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const previous = scan.byKey.get(path);
|
|
417
|
+
if (Predicate.isNotUndefined(previous) && (previous.uri !== namespace.uri || previous.prefix !== namespace.prefix)) {
|
|
418
|
+
scan.problems.push(
|
|
419
|
+
`the local name "${pathKey(path)}" belongs to more than one namespace (${previous.uri}:${previous.prefix} vs ${namespace.uri}:${namespace.prefix})`
|
|
420
|
+
);
|
|
421
|
+
} else {
|
|
422
|
+
scan.byKey.set(path, namespace);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* @description Records a field's `xmlName` override, refusing one that carries a prefix because the prefix comes from `xmlPrefix`.
|
|
428
|
+
*
|
|
429
|
+
* @param scan - The scan state.
|
|
430
|
+
* @param path - The field's element path.
|
|
431
|
+
* @param name - The annotated local name, or `undefined`.
|
|
432
|
+
*/
|
|
433
|
+
const recordName = (scan: Scan, path: string, name: string | undefined): void => {
|
|
434
|
+
if (Predicate.isUndefined(name)) return;
|
|
435
|
+
|
|
436
|
+
if (name.includes(':')) {
|
|
437
|
+
scan.problems.push(`xmlName "${name}" on "${pathKey(path)}" must be a local name; use xmlPrefix for the prefix`);
|
|
438
|
+
} else {
|
|
439
|
+
scan.nameByKey.set(path, name);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @description Records a field's namespace, refusing the two annotations that cannot be honored: a namespaced field whose name already carries a prefix, and a
|
|
445
|
+
* namespaced attribute without a prefix, because a default namespace does not apply to attributes.
|
|
446
|
+
*
|
|
447
|
+
* @param scan - The scan state.
|
|
448
|
+
* @param path - The field's element path.
|
|
449
|
+
* @param isAttribute - Whether the field is an attribute.
|
|
450
|
+
* @param field - The field's own namespace, or `undefined`.
|
|
451
|
+
* @param inherited - The namespace the enclosing element passes down.
|
|
452
|
+
*/
|
|
453
|
+
const recordFieldNamespace = (
|
|
454
|
+
scan: Scan,
|
|
455
|
+
path: string,
|
|
456
|
+
isAttribute: boolean,
|
|
457
|
+
field: XmlNamespace | undefined,
|
|
458
|
+
inherited: XmlNamespace | undefined
|
|
459
|
+
): void => {
|
|
460
|
+
const key = pathKey(path);
|
|
461
|
+
if (Predicate.isNotUndefined(field) && key.includes(':')) {
|
|
462
|
+
scan.problems.push(`"${key}" already carries a prefix, so it cannot also carry a namespace annotation`);
|
|
463
|
+
} else if (isAttribute && Predicate.isNotUndefined(field) && field.prefix === '') {
|
|
464
|
+
scan.problems.push(`the attribute "${key}" needs xmlPrefix, because a default namespace does not apply to attributes`);
|
|
465
|
+
} else {
|
|
466
|
+
record(scan, path, isAttribute ? field : (field ?? inherited));
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* @description Whether a struct property is an attribute: its key carries the `@` prefix, or its own or key annotation marks it.
|
|
472
|
+
*
|
|
473
|
+
* @param key - The schema key.
|
|
474
|
+
* @param ast - The property's value AST.
|
|
475
|
+
*
|
|
476
|
+
* @returns Whether the property is an attribute.
|
|
477
|
+
*/
|
|
478
|
+
const isAttributeProperty = (key: string, ast: SchemaAST.AST): boolean => isAttributeKey(key) || keyAttributeOf(ast) || attributeOf(ast);
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* @description Notes a field that `xmlAttribute` marks as an attribute but whose name does not carry the `@` prefix.
|
|
482
|
+
*
|
|
483
|
+
* @param scan - The scan state.
|
|
484
|
+
* @param path - The field's element path.
|
|
485
|
+
* @param isAttribute - Whether the field is an attribute.
|
|
486
|
+
*/
|
|
487
|
+
const noteAttribute = (scan: Scan, path: string, isAttribute: boolean): void => {
|
|
488
|
+
if (isAttribute && !isAttributeKey(pathKey(path))) {
|
|
489
|
+
scan.attributeKeys.add(path);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @description Records a field marked `xmlValue` against the element that owns it, refusing the annotations a value cannot combine with: character data has no
|
|
495
|
+
* name and no namespace, and an element has room for only one value.
|
|
496
|
+
*
|
|
497
|
+
* @param scan - The scan state.
|
|
498
|
+
* @param elementPath - The path of the element the field belongs to, or the root sentinel.
|
|
499
|
+
* @param valueKey - The schema key of the value field.
|
|
500
|
+
* @param isAttribute - Whether the field is an attribute.
|
|
501
|
+
* @param name - The field's `xmlName` override, or `undefined`.
|
|
502
|
+
* @param field - The field's own namespace, or `undefined`.
|
|
503
|
+
*/
|
|
504
|
+
const recordValue = (
|
|
505
|
+
scan: Scan,
|
|
506
|
+
elementPath: string,
|
|
507
|
+
valueKey: string,
|
|
508
|
+
isAttribute: boolean,
|
|
509
|
+
name: string | undefined,
|
|
510
|
+
field: XmlNamespace | undefined
|
|
511
|
+
): void => {
|
|
512
|
+
if (isAttribute) scan.problems.push(`the field "${valueKey}" is marked as both an attribute and the element's value`);
|
|
513
|
+
else if (Predicate.isNotUndefined(name))
|
|
514
|
+
scan.problems.push(`the value field "${valueKey}" cannot have an xmlName, because character data has no name`);
|
|
515
|
+
else if (Predicate.isNotUndefined(field))
|
|
516
|
+
scan.problems.push(`the value field "${valueKey}" cannot have a namespace, because character data has none`);
|
|
517
|
+
else {
|
|
518
|
+
const previous = scan.valueByElement.get(elementPath);
|
|
519
|
+
if (Predicate.isNotUndefined(previous) && previous !== valueKey) {
|
|
520
|
+
scan.problems.push(
|
|
521
|
+
`the element "${elementPath === ROOT_ELEMENT ? 'root' : elementPath}" has more than one value field, "${previous}" and "${valueKey}"`
|
|
522
|
+
);
|
|
523
|
+
} else {
|
|
524
|
+
scan.valueByElement.set(elementPath, valueKey);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* @description Records one struct property: its `xmlName`, its namespace, and the namespace its descendants inherit. An attribute carries a namespace only when
|
|
531
|
+
* annotated; an element field falls back to the namespace it inherits. A value field is character data, so it is recorded against its element
|
|
532
|
+
* instead.
|
|
533
|
+
*
|
|
534
|
+
* @param scan - The scan state.
|
|
535
|
+
* @param property - The property signature.
|
|
536
|
+
* @param inherited - The namespace the enclosing element passes down.
|
|
537
|
+
* @param elementPath - The path of the element the property belongs to, or the root sentinel.
|
|
538
|
+
*/
|
|
539
|
+
const scanProperty = (scan: Scan, property: SchemaAST.PropertySignature, inherited: XmlNamespace | undefined, elementPath: string): void => {
|
|
540
|
+
const key = Predicate.isString(property.name) ? property.name : String(property.name);
|
|
541
|
+
const path = childPath(elementPath, key);
|
|
542
|
+
const isAttribute = isAttributeProperty(key, property.type);
|
|
543
|
+
const isValue = isValueProperty(property.type);
|
|
544
|
+
const name = keyNameOf(property.type) ?? nameOf(property.type);
|
|
545
|
+
const field = keyNamespaceOf(property.type) ?? namespaceOf(property.type);
|
|
546
|
+
if (isValue) recordValue(scan, elementPath, key, isAttribute, name, field);
|
|
547
|
+
else {
|
|
548
|
+
noteAttribute(scan, path, isAttribute);
|
|
549
|
+
recordName(scan, path, name);
|
|
550
|
+
recordFieldNamespace(scan, path, isAttribute, field, inherited);
|
|
551
|
+
}
|
|
552
|
+
scanNode(scan, property.type, isAttribute ? inherited : (field ?? inherited), path);
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* @description Walks a list of child ASTs under one inherited namespace.
|
|
557
|
+
*
|
|
558
|
+
* @param scan - The scan state.
|
|
559
|
+
* @param nodes - The child ASTs.
|
|
560
|
+
* @param inherited - The namespace they inherit.
|
|
561
|
+
* @param elementPath - The path of the element they belong to, or the root sentinel.
|
|
562
|
+
*/
|
|
563
|
+
const scanAll = (scan: Scan, nodes: ReadonlyArray<SchemaAST.AST>, inherited: XmlNamespace | undefined, elementPath: string): void => {
|
|
564
|
+
for (const node of nodes) scanNode(scan, node, inherited, elementPath);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* @description Records the names in one object node: its properties and its index signatures, each under the namespace the object passes down.
|
|
569
|
+
*
|
|
570
|
+
* @param scan - The scan state.
|
|
571
|
+
* @param ast - The object AST.
|
|
572
|
+
* @param namespace - The namespace the object passes to its members.
|
|
573
|
+
* @param elementPath - The path of the element the object describes, or the root sentinel.
|
|
574
|
+
*/
|
|
575
|
+
const scanObject = (scan: Scan, ast: SchemaAST.Objects, namespace: XmlNamespace | undefined, elementPath: string): void => {
|
|
576
|
+
for (const property of ast.propertySignatures) {
|
|
577
|
+
scanProperty(scan, property, namespace, elementPath);
|
|
578
|
+
}
|
|
579
|
+
for (const index of ast.indexSignatures) {
|
|
580
|
+
scanNode(scan, index.type, namespace, elementPath);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @description Notes an array field that names its element with `xmlName`, so the decoder reads a single occurrence as a one-member array. An unnamed array keeps
|
|
586
|
+
* the ambiguity and does not wrap.
|
|
587
|
+
*
|
|
588
|
+
* @param scan - The scan state.
|
|
589
|
+
* @param ast - The AST being scanned.
|
|
590
|
+
* @param elementPath - The path of the field the AST describes.
|
|
591
|
+
*/
|
|
592
|
+
const noteArray = (scan: Scan, ast: SchemaAST.AST, elementPath: string): void => {
|
|
593
|
+
if (ast._tag === 'Arrays' && scan.nameByKey.has(elementPath)) {
|
|
594
|
+
scan.arrayKeys.add(elementPath);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @description Records every name in one schema AST, carrying the namespace an element passes to its descendants and the element the names belong to.
|
|
600
|
+
*
|
|
601
|
+
* @param scan - The scan state.
|
|
602
|
+
* @param ast - The AST to walk.
|
|
603
|
+
* @param inherited - The namespace the enclosing element passes down.
|
|
604
|
+
* @param elementPath - The path of the element this AST describes, or the root sentinel.
|
|
605
|
+
*/
|
|
606
|
+
const scanNode = (scan: Scan, ast: SchemaAST.AST, inherited: XmlNamespace | undefined, elementPath: string): void => {
|
|
607
|
+
if (scan.seen.has(ast)) return;
|
|
608
|
+
scan.seen.add(ast);
|
|
609
|
+
try {
|
|
610
|
+
noteArray(scan, ast, elementPath);
|
|
611
|
+
const namespace = namespaceOf(ast) ?? inherited;
|
|
612
|
+
switch (ast._tag) {
|
|
613
|
+
case 'Objects':
|
|
614
|
+
scanObject(scan, ast, namespace, elementPath);
|
|
615
|
+
return;
|
|
616
|
+
case 'Arrays':
|
|
617
|
+
scanAll(scan, [...ast.elements, ...ast.rest], namespace, elementPath);
|
|
618
|
+
return;
|
|
619
|
+
case 'Union':
|
|
620
|
+
scanAll(scan, ast.types, namespace, elementPath);
|
|
621
|
+
return;
|
|
622
|
+
case 'Suspend':
|
|
623
|
+
scanNode(scan, ast.thunk(), namespace, elementPath);
|
|
624
|
+
return;
|
|
625
|
+
case 'Declaration':
|
|
626
|
+
scanAll(scan, ast.typeParameters, namespace, elementPath);
|
|
627
|
+
return;
|
|
628
|
+
default:
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
} finally {
|
|
632
|
+
scan.seen.delete(ast);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* @description Collects the namespace and name of every field in a schema. A namespace is inherited by descendant elements, the way a default namespace is, and an
|
|
638
|
+
* element field records its own namespace, so encode and decode can find it by the local name alone.
|
|
639
|
+
*
|
|
640
|
+
* @param schema - The schema to walk.
|
|
641
|
+
*
|
|
642
|
+
* @returns The plan, or the annotations that cannot be honored.
|
|
643
|
+
*/
|
|
644
|
+
export const namespacePlan = (schema: Schema.Constraint): { readonly plan: NamespacePlan } | { readonly error: string } => {
|
|
645
|
+
const scan: Scan = {
|
|
646
|
+
byKey: new Map(),
|
|
647
|
+
nameByKey: new Map(),
|
|
648
|
+
attributeKeys: new Set(),
|
|
649
|
+
valueByElement: new Map(),
|
|
650
|
+
arrayKeys: new Set(),
|
|
651
|
+
problems: [],
|
|
652
|
+
seen: new Set(),
|
|
653
|
+
};
|
|
654
|
+
scanNode(scan, schema.ast, undefined, ROOT_ELEMENT);
|
|
655
|
+
|
|
656
|
+
const byResolved = new Map<string, string>();
|
|
657
|
+
for (const path of new Set([...scan.byKey.keys(), ...scan.nameByKey.keys(), ...scan.attributeKeys])) {
|
|
658
|
+
const key = pathKey(path);
|
|
659
|
+
const namespace = scan.byKey.get(path);
|
|
660
|
+
const isAttribute = isAttributeKey(key) || scan.attributeKeys.has(path);
|
|
661
|
+
const local = scan.nameByKey.get(path) ?? localName(key);
|
|
662
|
+
const resolved = lookupKey(parentPath(path), resolvedKey(isAttribute, namespace?.uri, local));
|
|
663
|
+
const previous = byResolved.get(resolved);
|
|
664
|
+
|
|
665
|
+
if (previous !== undefined && previous !== key) {
|
|
666
|
+
scan.problems.push(`"${previous}" and "${key}" both resolve to "${local}" under the same element`);
|
|
667
|
+
} else {
|
|
668
|
+
byResolved.set(resolved, key);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (scan.problems.length > 0) {
|
|
673
|
+
return { error: [...new Set(scan.problems)].join(';\n\t- ') };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
plan: {
|
|
678
|
+
byKey: scan.byKey,
|
|
679
|
+
nameByKey: scan.nameByKey,
|
|
680
|
+
attributeKeys: scan.attributeKeys,
|
|
681
|
+
valueByElement: scan.valueByElement,
|
|
682
|
+
arrayKeys: scan.arrayKeys,
|
|
683
|
+
root: namespaceOf(schema.ast),
|
|
684
|
+
rootName: nameOf(schema.ast),
|
|
685
|
+
byResolved,
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* @description Whether a key is a namespace declaration the codec manages: `@xmlns` or `@xmlns:prefix`.
|
|
692
|
+
*
|
|
693
|
+
* @param key - The record key.
|
|
694
|
+
*
|
|
695
|
+
* @returns Whether the key is a declaration.
|
|
696
|
+
*/
|
|
697
|
+
const isDeclarationKey = (key: string): boolean => key === `${ATTRIBUTE_PREFIX}xmlns` || key.startsWith(`${ATTRIBUTE_PREFIX}xmlns:`);
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* @description The prefix a declaration key carries, or `''` for the default namespace.
|
|
701
|
+
*
|
|
702
|
+
* @param key - A key {@link isDeclarationKey} accepted.
|
|
703
|
+
*
|
|
704
|
+
* @returns The prefix.
|
|
705
|
+
*/
|
|
706
|
+
const declarationPrefix = (key: string): string => (key === `${ATTRIBUTE_PREFIX}xmlns` ? '' : key.slice(`${ATTRIBUTE_PREFIX}xmlns:`.length));
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* @description Writes the declaration for a namespace into an element's record, when the scope does not already bind it. A prefixed namespace is declared as
|
|
710
|
+
* `xmlns:prefix`; the default namespace as `xmlns`; and an element that leaves a default namespace in scope for no namespace of its own clears it
|
|
711
|
+
* with `xmlns=""`.
|
|
712
|
+
*
|
|
713
|
+
* @param out - The element's record, written in place.
|
|
714
|
+
* @param scope - The in-scope prefix bindings, updated in place.
|
|
715
|
+
* @param namespace - The element's namespace, or `undefined`.
|
|
716
|
+
*/
|
|
717
|
+
const declare = (out: Record<string, XmlValue>, scope: Record<string, string | undefined>, namespace: XmlNamespace | undefined): void => {
|
|
718
|
+
if (Predicate.isNotUndefined(namespace) && namespace.prefix !== '') {
|
|
719
|
+
if (scope[namespace.prefix] === namespace.uri) return;
|
|
720
|
+
out[`${ATTRIBUTE_PREFIX}xmlns:${namespace.prefix}`] = namespace.uri;
|
|
721
|
+
scope[namespace.prefix] = namespace.uri;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (!Predicate.isUndefined(namespace)) {
|
|
726
|
+
if (scope[''] === namespace.uri) return;
|
|
727
|
+
out[`${ATTRIBUTE_PREFIX}xmlns`] = namespace.uri;
|
|
728
|
+
scope[''] = namespace.uri;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (Predicate.isUndefined(scope[''])) return;
|
|
733
|
+
|
|
734
|
+
out[`${ATTRIBUTE_PREFIX}xmlns`] = '';
|
|
735
|
+
scope[''] = undefined;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* @description The wire key for one field: the local name with the prefix its namespace declares, or the local name unchanged when there is no prefix.
|
|
740
|
+
*
|
|
741
|
+
* @param plan - The namespace plan.
|
|
742
|
+
* @param path - The field's element path.
|
|
743
|
+
* @param namespace - The field's namespace, or `undefined`.
|
|
744
|
+
*
|
|
745
|
+
* @returns The key to write.
|
|
746
|
+
*/
|
|
747
|
+
const wireKey = (plan: NamespacePlan, path: string, namespace: XmlNamespace | undefined): string => {
|
|
748
|
+
const isAttribute = isAttributeOf(plan, path);
|
|
749
|
+
const local = localOf(plan, path);
|
|
750
|
+
|
|
751
|
+
if (Predicate.isUndefined(namespace) || namespace.prefix === '') {
|
|
752
|
+
return isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return `${isAttribute ? ATTRIBUTE_PREFIX : ''}${namespace.prefix}:${local}`;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* @description Writes one record's fields under the element's scope: an attribute is a leaf and declares its prefix on this element, the value field becomes the
|
|
760
|
+
* element's character data, and a child element recurses with its own namespace.
|
|
761
|
+
*
|
|
762
|
+
* @param value - The element's record, keyed by the schema's local names.
|
|
763
|
+
* @param plan - The namespace plan.
|
|
764
|
+
* @param out - The wire record, written in place.
|
|
765
|
+
* @param scope - The prefix bindings in scope for this element.
|
|
766
|
+
* @param elementPath - The path of this element, or the root sentinel.
|
|
767
|
+
*/
|
|
768
|
+
const encodeFields = (
|
|
769
|
+
value: XmlRecord,
|
|
770
|
+
plan: NamespacePlan,
|
|
771
|
+
out: Record<string, XmlValue>,
|
|
772
|
+
scope: Record<string, string | undefined>,
|
|
773
|
+
elementPath: string
|
|
774
|
+
): void => {
|
|
775
|
+
const valueKey = plan.valueByElement.get(elementPath);
|
|
776
|
+
for (const [key, child] of Object.entries(value)) {
|
|
777
|
+
if (isDeclarationKey(key)) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (key === valueKey) {
|
|
782
|
+
out[TEXT_KEY] = child;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const path = childPath(elementPath, key);
|
|
787
|
+
const childNamespace = plan.byKey.get(path);
|
|
788
|
+
if (isAttributeOf(plan, path)) {
|
|
789
|
+
if (Predicate.isNotUndefined(childNamespace)) {
|
|
790
|
+
declare(out, scope, childNamespace);
|
|
791
|
+
}
|
|
792
|
+
out[wireKey(plan, path, childNamespace)] = child;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
out[wireKey(plan, path, childNamespace)] = encodeNames(child, plan, childNamespace, scope, path);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* @description Rewrites a value tree into its namespaced wire form: every field with a plan entry gets its prefix, and every element declares the namespace its
|
|
802
|
+
* subtree uses. A leaf that has to carry a declaration is wrapped as a `#text` record so the attribute has somewhere to live.
|
|
803
|
+
*
|
|
804
|
+
* @param value - The value tree, keyed by the schema's local names.
|
|
805
|
+
* @param plan - The namespace plan.
|
|
806
|
+
* @param namespace - This element's namespace.
|
|
807
|
+
* @param scope - The prefix bindings in scope above this element.
|
|
808
|
+
* @param elementPath - The path of this element, or the root sentinel.
|
|
809
|
+
*
|
|
810
|
+
* @returns The wire tree.
|
|
811
|
+
*/
|
|
812
|
+
export const encodeNames = (
|
|
813
|
+
value: XmlValue,
|
|
814
|
+
plan: NamespacePlan,
|
|
815
|
+
namespace: XmlNamespace | undefined,
|
|
816
|
+
scope: Record<string, string | undefined>,
|
|
817
|
+
elementPath: string
|
|
818
|
+
): XmlValue => {
|
|
819
|
+
if (Array.isArray(value)) {
|
|
820
|
+
return value.map(member => encodeNames(member, plan, namespace, scope, elementPath));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const out: Record<string, XmlValue> = {};
|
|
824
|
+
const inner = { ...scope };
|
|
825
|
+
declare(out, inner, namespace);
|
|
826
|
+
|
|
827
|
+
if (Predicate.isUndefined(value)) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
if (Predicate.isString(value)) {
|
|
831
|
+
return Object.keys(out).length > 0 ? { ...out, [TEXT_KEY]: value } : value;
|
|
832
|
+
}
|
|
833
|
+
// `Predicate.isObject` narrows to a generic index signature, so the value
|
|
834
|
+
// tree's own record type is named here.
|
|
835
|
+
if (!Predicate.isObject(value)) {
|
|
836
|
+
return value;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
encodeFields(value as XmlRecord, plan, out, inner, elementPath);
|
|
840
|
+
return out;
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* @description Resolves a wire name to its URI and local name against the in-scope bindings. An unprefixed element takes the default namespace; an unprefixed
|
|
845
|
+
* attribute has no namespace, because a default namespace does not apply to attributes.
|
|
846
|
+
*
|
|
847
|
+
* @param name - The name as written, without the attribute prefix.
|
|
848
|
+
* @param isAttribute - Whether the name belongs to an attribute.
|
|
849
|
+
* @param scope - The in-scope prefix bindings.
|
|
850
|
+
*
|
|
851
|
+
* @returns The resolved URI (possibly `undefined`) and local name.
|
|
852
|
+
*/
|
|
853
|
+
const resolveName = (
|
|
854
|
+
name: string,
|
|
855
|
+
isAttribute: boolean,
|
|
856
|
+
scope: Record<string, string | undefined>
|
|
857
|
+
): { readonly uri: string | undefined; readonly local: string } => {
|
|
858
|
+
const colon = name.indexOf(':');
|
|
859
|
+
if (colon === -1) return { uri: isAttribute ? undefined : scope[''], local: name };
|
|
860
|
+
return { uri: scope[name.slice(0, colon)], local: name.slice(colon + 1) };
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* @description Reads an element's declarations into a fresh scope that falls back to the enclosing one.
|
|
865
|
+
*
|
|
866
|
+
* @param value - The element's record.
|
|
867
|
+
* @param scope - The bindings in scope above the element.
|
|
868
|
+
*
|
|
869
|
+
* @returns The element's own scope.
|
|
870
|
+
*/
|
|
871
|
+
const scopeOf = (value: XmlRecord, scope: Record<string, string | undefined>): Record<string, string | undefined> => {
|
|
872
|
+
const inner = { ...scope };
|
|
873
|
+
|
|
874
|
+
for (const [key, declaration] of Object.entries(value)) {
|
|
875
|
+
if (isDeclarationKey(key) && Predicate.isString(declaration)) {
|
|
876
|
+
inner[declarationPrefix(key)] = declaration;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return inner;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* @description The schema key a wire key resolves to under one parent element: the plan's key for its URI and local name, or the local name when the schema left
|
|
885
|
+
* it unannotated.
|
|
886
|
+
*
|
|
887
|
+
* @param plan - The namespace plan.
|
|
888
|
+
* @param parent - The parent element's path.
|
|
889
|
+
* @param key - The wire key.
|
|
890
|
+
* @param scope - The in-scope prefix bindings.
|
|
891
|
+
*
|
|
892
|
+
* @returns The schema key.
|
|
893
|
+
*/
|
|
894
|
+
const schemaKey = (plan: NamespacePlan, parent: string, key: string, scope: Record<string, string | undefined>): string => {
|
|
895
|
+
const isAttribute = isAttributeKey(key);
|
|
896
|
+
const { uri, local } = resolveName(localName(key), isAttribute, scope);
|
|
897
|
+
return plan.byResolved.get(lookupKey(parent, resolvedKey(isAttribute, uri, local))) ?? (isAttribute ? `${ATTRIBUTE_PREFIX}${local}` : local);
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* @description The scope a child element resolves its own name against: the declarations it carries on itself, layered over the parent scope. An element may
|
|
902
|
+
* declare the prefix it uses on the element itself, so its own name is read with those bindings in scope. A repeated element arrives as an array, so
|
|
903
|
+
* the first member stands in for the run — every member describes the same element and carries the same declaration.
|
|
904
|
+
*
|
|
905
|
+
* @param child - The child value.
|
|
906
|
+
* @param scope - The bindings in scope above the child.
|
|
907
|
+
*
|
|
908
|
+
* @returns The scope to resolve the child's own name with.
|
|
909
|
+
*/
|
|
910
|
+
const childScopeOf = (child: XmlValue, scope: Record<string, string | undefined>): Record<string, string | undefined> => {
|
|
911
|
+
if (Array.isArray(child)) {
|
|
912
|
+
return child.length > 0 ? childScopeOf(child[0], scope) : scope;
|
|
913
|
+
}
|
|
914
|
+
return Predicate.isObject(child) ? scopeOf(child as XmlRecord, scope) : scope;
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* @description Rewrites a wire value tree back to the schema's local names, resolving every name against the declarations the document carries and dropping those
|
|
919
|
+
* declarations. Character data maps to the value field of the element it belongs to. A record left holding only character data collapses back to that
|
|
920
|
+
* string, which is how a namespaced leaf stays a `Schema.String`.
|
|
921
|
+
*
|
|
922
|
+
* @param value - The parsed wire tree.
|
|
923
|
+
* @param plan - The namespace plan.
|
|
924
|
+
* @param scope - The prefix bindings in scope above this element.
|
|
925
|
+
* @param elementPath - The path of this element, or the root sentinel.
|
|
926
|
+
*
|
|
927
|
+
* @returns The value tree, keyed by the schema's names.
|
|
928
|
+
*/
|
|
929
|
+
export const decodeNames = (value: XmlValue, plan: NamespacePlan, scope: Record<string, string | undefined>, elementPath: string): XmlValue => {
|
|
930
|
+
if (Array.isArray(value)) {
|
|
931
|
+
return value.map(member => decodeNames(member, plan, scope, elementPath));
|
|
932
|
+
}
|
|
933
|
+
if (Predicate.isString(value) || Predicate.isUndefined(value)) {
|
|
934
|
+
return value;
|
|
935
|
+
}
|
|
936
|
+
if (!Predicate.isObject(value)) {
|
|
937
|
+
return value;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// `Predicate.isObject` narrows to a generic index signature, so the value
|
|
941
|
+
// tree's own record type is named here.
|
|
942
|
+
const record = value as XmlRecord;
|
|
943
|
+
const inner = scopeOf(record, scope);
|
|
944
|
+
const valueKey = plan.valueByElement.get(elementPath);
|
|
945
|
+
const out: Record<string, XmlValue> = {};
|
|
946
|
+
|
|
947
|
+
for (const [key, child] of Object.entries(record)) {
|
|
948
|
+
if (isDeclarationKey(key)) {
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (key === TEXT_KEY) {
|
|
953
|
+
out[valueKey ?? TEXT_KEY] = decodeNames(child, plan, inner, elementPath);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const childKey = schemaKey(plan, elementPath, key, childScopeOf(child, inner));
|
|
958
|
+
const path = childPath(elementPath, childKey);
|
|
959
|
+
const decoded = decodeNames(child, plan, inner, path);
|
|
960
|
+
out[childKey] = plan.arrayKeys.has(path) && !Array.isArray(decoded) ? [decoded] : decoded;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const keys = Object.keys(out);
|
|
964
|
+
if (keys.length === 1 && keys[0] === TEXT_KEY) {
|
|
965
|
+
return out[TEXT_KEY];
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
};
|