@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,663 @@
|
|
|
1
|
+
import { TEXT_KEY, isAttributeKey } from "./conventions.js";
|
|
2
|
+
import { Predicate, SchemaAST } from "effect";
|
|
3
|
+
//#region src/namespaces.ts
|
|
4
|
+
/**
|
|
5
|
+
* @description The annotation key holding an element's namespace URI.
|
|
6
|
+
*/
|
|
7
|
+
const NAMESPACE_KEY = "xmlNamespace";
|
|
8
|
+
/**
|
|
9
|
+
* @description The annotation key holding the wire prefix for an element's namespace.
|
|
10
|
+
*/
|
|
11
|
+
const PREFIX_KEY = "xmlPrefix";
|
|
12
|
+
/**
|
|
13
|
+
* @description The annotation key holding the wire local name for an element or attribute.
|
|
14
|
+
*/
|
|
15
|
+
const NAME_KEY = "xmlName";
|
|
16
|
+
/**
|
|
17
|
+
* @description The annotation key marking a field as an XML attribute.
|
|
18
|
+
*/
|
|
19
|
+
const ATTRIBUTE_KEY = "xmlAttribute";
|
|
20
|
+
/**
|
|
21
|
+
* @description The annotation key marking a field as the element's character data.
|
|
22
|
+
*/
|
|
23
|
+
const VALUE_KEY = "xmlValue";
|
|
24
|
+
/**
|
|
25
|
+
* @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,
|
|
26
|
+
* so both are unwrapped to find the namespace the field actually carries.
|
|
27
|
+
*
|
|
28
|
+
* @param ast - The AST to read.
|
|
29
|
+
*
|
|
30
|
+
* @returns The namespace, or `undefined`.
|
|
31
|
+
*/
|
|
32
|
+
const annotationAt = (ast, key) => {
|
|
33
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34
|
+
const find = (node) => {
|
|
35
|
+
if (seen.has(node)) return void 0;
|
|
36
|
+
seen.add(node);
|
|
37
|
+
const value = SchemaAST.resolve(node)?.[key];
|
|
38
|
+
if (value !== void 0) return value;
|
|
39
|
+
if (node._tag === "Union") {
|
|
40
|
+
for (const member of node.types) {
|
|
41
|
+
const found = find(member);
|
|
42
|
+
if (found !== void 0) return found;
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
return node._tag === "Suspend" ? find(node.thunk()) : void 0;
|
|
47
|
+
};
|
|
48
|
+
return find(ast);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* @description The namespace an AST's own annotations declare, or `undefined`. `Schema.optional` and `Schema.suspend` wrap a node without moving its annotation,
|
|
52
|
+
* so both are unwrapped to find the namespace the field carries.
|
|
53
|
+
*
|
|
54
|
+
* @param ast - The AST to read.
|
|
55
|
+
*
|
|
56
|
+
* @returns The namespace, or `undefined`.
|
|
57
|
+
*/
|
|
58
|
+
const namespaceOf = (ast) => {
|
|
59
|
+
const uri = annotationAt(ast, NAMESPACE_KEY);
|
|
60
|
+
if (!Predicate.isString(uri)) return void 0;
|
|
61
|
+
const prefix = annotationAt(ast, PREFIX_KEY);
|
|
62
|
+
return {
|
|
63
|
+
uri,
|
|
64
|
+
prefix: Predicate.isString(prefix) ? prefix : ""
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* @description The wire local name an AST's own annotations declare, or `undefined`.
|
|
69
|
+
*
|
|
70
|
+
* @param ast - The AST to read.
|
|
71
|
+
*
|
|
72
|
+
* @returns The local name, or `undefined`.
|
|
73
|
+
*/
|
|
74
|
+
const nameOf = (ast) => {
|
|
75
|
+
const name = annotationAt(ast, NAME_KEY);
|
|
76
|
+
return Predicate.isString(name) ? name : void 0;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* @description The namespace a property carries when its annotation was attached with `Schema.annotateKey` rather than to the field's schema.
|
|
80
|
+
*
|
|
81
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
82
|
+
*
|
|
83
|
+
* @returns The namespace, or `undefined`.
|
|
84
|
+
*/
|
|
85
|
+
const keyNamespaceOf = (ast) => {
|
|
86
|
+
const annotations = ast.context?.annotations;
|
|
87
|
+
const uri = annotations?.[NAMESPACE_KEY];
|
|
88
|
+
if (!Predicate.isString(uri)) return void 0;
|
|
89
|
+
const prefix = annotations?.[PREFIX_KEY];
|
|
90
|
+
return {
|
|
91
|
+
uri,
|
|
92
|
+
prefix: Predicate.isString(prefix) ? prefix : ""
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* @description The wire local name a property's key annotations declare, or `undefined`.
|
|
97
|
+
*
|
|
98
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
99
|
+
*
|
|
100
|
+
* @returns The local name, or `undefined`.
|
|
101
|
+
*/
|
|
102
|
+
const keyNameOf = (ast) => {
|
|
103
|
+
const name = ast.context?.annotations?.[NAME_KEY];
|
|
104
|
+
return Predicate.isString(name) ? name : void 0;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* @description Whether an AST's own annotations mark the field as an XML attribute.
|
|
108
|
+
*
|
|
109
|
+
* @param ast - The AST to read.
|
|
110
|
+
*
|
|
111
|
+
* @returns Whether the annotation is set.
|
|
112
|
+
*/
|
|
113
|
+
const attributeOf = (ast) => annotationAt(ast, ATTRIBUTE_KEY) === true;
|
|
114
|
+
/**
|
|
115
|
+
* @description Whether a property's key annotations mark the field as an XML attribute.
|
|
116
|
+
*
|
|
117
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
118
|
+
*
|
|
119
|
+
* @returns Whether the annotation is set.
|
|
120
|
+
*/
|
|
121
|
+
const keyAttributeOf = (ast) => ast.context?.annotations?.[ATTRIBUTE_KEY] === true;
|
|
122
|
+
/**
|
|
123
|
+
* @description Whether an AST's own annotations mark the field as the element's character data.
|
|
124
|
+
*
|
|
125
|
+
* @param ast - The AST to read.
|
|
126
|
+
*
|
|
127
|
+
* @returns Whether the annotation is set.
|
|
128
|
+
*/
|
|
129
|
+
const valueOf = (ast) => annotationAt(ast, VALUE_KEY) === true;
|
|
130
|
+
/**
|
|
131
|
+
* @description Whether a property's key annotations mark the field as the element's character data.
|
|
132
|
+
*
|
|
133
|
+
* @param ast - The property's value AST, whose context holds the key annotations.
|
|
134
|
+
*
|
|
135
|
+
* @returns Whether the annotation is set.
|
|
136
|
+
*/
|
|
137
|
+
const keyValueOf = (ast) => ast.context?.annotations?.[VALUE_KEY] === true;
|
|
138
|
+
/**
|
|
139
|
+
* @description Whether a struct property holds the element's character data, marked with `xmlValue` on its schema or its key.
|
|
140
|
+
*
|
|
141
|
+
* @param ast - The property's value AST.
|
|
142
|
+
*
|
|
143
|
+
* @returns Whether the property is the value.
|
|
144
|
+
*/
|
|
145
|
+
const isValueProperty = (ast) => keyValueOf(ast) || valueOf(ast);
|
|
146
|
+
/**
|
|
147
|
+
* @description The local name a schema key names, with the attribute prefix removed. This is what a declaration resolves to.
|
|
148
|
+
*
|
|
149
|
+
* @param key - The schema key.
|
|
150
|
+
*
|
|
151
|
+
* @returns The local name.
|
|
152
|
+
*/
|
|
153
|
+
const localName = (key) => isAttributeKey(key) ? key.slice(1) : key;
|
|
154
|
+
/**
|
|
155
|
+
* @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.
|
|
156
|
+
*/
|
|
157
|
+
const PATH_SEPARATOR = "\0";
|
|
158
|
+
/**
|
|
159
|
+
* @description The path of a child element, appended to its parent's path.
|
|
160
|
+
*
|
|
161
|
+
* @param parent - The parent element's path.
|
|
162
|
+
* @param key - The child's schema key.
|
|
163
|
+
*
|
|
164
|
+
* @returns The child's path.
|
|
165
|
+
*/
|
|
166
|
+
const childPath = (parent, key) => parent === "" ? key : `${parent}${PATH_SEPARATOR}${key}`;
|
|
167
|
+
/**
|
|
168
|
+
* @description The schema key at the end of an element path.
|
|
169
|
+
*
|
|
170
|
+
* @param path - The element path.
|
|
171
|
+
*
|
|
172
|
+
* @returns The schema key of the element itself.
|
|
173
|
+
*/
|
|
174
|
+
const pathKey = (path) => {
|
|
175
|
+
const at = path.lastIndexOf(PATH_SEPARATOR);
|
|
176
|
+
return at === -1 ? path : path.slice(at + 1);
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* @description Whether a field is an attribute: the key at the end of its path carries the `@` prefix, or `xmlAttribute` marks the field.
|
|
180
|
+
*
|
|
181
|
+
* @param plan - The namespace plan.
|
|
182
|
+
* @param path - The field's element path.
|
|
183
|
+
*
|
|
184
|
+
* @returns Whether the field is an attribute.
|
|
185
|
+
*/
|
|
186
|
+
const isAttributeOf = (plan, path) => isAttributeKey(pathKey(path)) || plan.attributeKeys.has(path);
|
|
187
|
+
/**
|
|
188
|
+
* @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.
|
|
189
|
+
*
|
|
190
|
+
* @param plan - The namespace plan.
|
|
191
|
+
* @param path - The field's element path.
|
|
192
|
+
*
|
|
193
|
+
* @returns The local name.
|
|
194
|
+
*/
|
|
195
|
+
const localOf = (plan, path) => plan.nameByKey.get(path) ?? localName(pathKey(path));
|
|
196
|
+
/**
|
|
197
|
+
* @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
|
|
198
|
+
* same namespace stay distinct.
|
|
199
|
+
*
|
|
200
|
+
* @param isAttribute - Whether the name is an attribute.
|
|
201
|
+
* @param uri - The resolved namespace URI, or `undefined`.
|
|
202
|
+
* @param local - The resolved local name.
|
|
203
|
+
*
|
|
204
|
+
* @returns The lookup key.
|
|
205
|
+
*/
|
|
206
|
+
const resolvedKey = (isAttribute, uri, local) => `${isAttribute ? "@" : ""}${uri ?? ""}|${local}`;
|
|
207
|
+
/**
|
|
208
|
+
* @description The path of the parent element, one segment shorter than the field's own path.
|
|
209
|
+
*
|
|
210
|
+
* @param path - The field's element path.
|
|
211
|
+
*
|
|
212
|
+
* @returns The parent element's path, or the root sentinel.
|
|
213
|
+
*/
|
|
214
|
+
const parentPath = (path) => {
|
|
215
|
+
const at = path.lastIndexOf(PATH_SEPARATOR);
|
|
216
|
+
return at === -1 ? "" : path.slice(0, at);
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* @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
|
|
220
|
+
* to each parent's own field.
|
|
221
|
+
*
|
|
222
|
+
* @param parent - The parent element's path.
|
|
223
|
+
* @param resolved - A name from {@link resolvedKey}.
|
|
224
|
+
*
|
|
225
|
+
* @returns The lookup key.
|
|
226
|
+
*/
|
|
227
|
+
const lookupKey = (parent, resolved) => `${parent}\u0001${resolved}`;
|
|
228
|
+
/**
|
|
229
|
+
* @description Records a field's namespace, reporting a local name that would belong to two namespaces at once.
|
|
230
|
+
*
|
|
231
|
+
* @param scan - The scan state.
|
|
232
|
+
* @param path - The field's element path.
|
|
233
|
+
* @param namespace - The namespace, or `undefined` when the field has none.
|
|
234
|
+
*/
|
|
235
|
+
const record = (scan, path, namespace) => {
|
|
236
|
+
if (Predicate.isUndefined(namespace)) return;
|
|
237
|
+
const previous = scan.byKey.get(path);
|
|
238
|
+
if (Predicate.isNotUndefined(previous) && (previous.uri !== namespace.uri || previous.prefix !== namespace.prefix)) scan.problems.push(`the local name "${pathKey(path)}" belongs to more than one namespace (${previous.uri}:${previous.prefix} vs ${namespace.uri}:${namespace.prefix})`);
|
|
239
|
+
else scan.byKey.set(path, namespace);
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* @description Records a field's `xmlName` override, refusing one that carries a prefix because the prefix comes from `xmlPrefix`.
|
|
243
|
+
*
|
|
244
|
+
* @param scan - The scan state.
|
|
245
|
+
* @param path - The field's element path.
|
|
246
|
+
* @param name - The annotated local name, or `undefined`.
|
|
247
|
+
*/
|
|
248
|
+
const recordName = (scan, path, name) => {
|
|
249
|
+
if (Predicate.isUndefined(name)) return;
|
|
250
|
+
if (name.includes(":")) scan.problems.push(`xmlName "${name}" on "${pathKey(path)}" must be a local name; use xmlPrefix for the prefix`);
|
|
251
|
+
else scan.nameByKey.set(path, name);
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* @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
|
|
255
|
+
* namespaced attribute without a prefix, because a default namespace does not apply to attributes.
|
|
256
|
+
*
|
|
257
|
+
* @param scan - The scan state.
|
|
258
|
+
* @param path - The field's element path.
|
|
259
|
+
* @param isAttribute - Whether the field is an attribute.
|
|
260
|
+
* @param field - The field's own namespace, or `undefined`.
|
|
261
|
+
* @param inherited - The namespace the enclosing element passes down.
|
|
262
|
+
*/
|
|
263
|
+
const recordFieldNamespace = (scan, path, isAttribute, field, inherited) => {
|
|
264
|
+
const key = pathKey(path);
|
|
265
|
+
if (Predicate.isNotUndefined(field) && key.includes(":")) scan.problems.push(`"${key}" already carries a prefix, so it cannot also carry a namespace annotation`);
|
|
266
|
+
else if (isAttribute && Predicate.isNotUndefined(field) && field.prefix === "") scan.problems.push(`the attribute "${key}" needs xmlPrefix, because a default namespace does not apply to attributes`);
|
|
267
|
+
else record(scan, path, isAttribute ? field : field ?? inherited);
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* @description Whether a struct property is an attribute: its key carries the `@` prefix, or its own or key annotation marks it.
|
|
271
|
+
*
|
|
272
|
+
* @param key - The schema key.
|
|
273
|
+
* @param ast - The property's value AST.
|
|
274
|
+
*
|
|
275
|
+
* @returns Whether the property is an attribute.
|
|
276
|
+
*/
|
|
277
|
+
const isAttributeProperty = (key, ast) => isAttributeKey(key) || keyAttributeOf(ast) || attributeOf(ast);
|
|
278
|
+
/**
|
|
279
|
+
* @description Notes a field that `xmlAttribute` marks as an attribute but whose name does not carry the `@` prefix.
|
|
280
|
+
*
|
|
281
|
+
* @param scan - The scan state.
|
|
282
|
+
* @param path - The field's element path.
|
|
283
|
+
* @param isAttribute - Whether the field is an attribute.
|
|
284
|
+
*/
|
|
285
|
+
const noteAttribute = (scan, path, isAttribute) => {
|
|
286
|
+
if (isAttribute && !isAttributeKey(pathKey(path))) scan.attributeKeys.add(path);
|
|
287
|
+
};
|
|
288
|
+
/**
|
|
289
|
+
* @description Records a field marked `xmlValue` against the element that owns it, refusing the annotations a value cannot combine with: character data has no
|
|
290
|
+
* name and no namespace, and an element has room for only one value.
|
|
291
|
+
*
|
|
292
|
+
* @param scan - The scan state.
|
|
293
|
+
* @param elementPath - The path of the element the field belongs to, or the root sentinel.
|
|
294
|
+
* @param valueKey - The schema key of the value field.
|
|
295
|
+
* @param isAttribute - Whether the field is an attribute.
|
|
296
|
+
* @param name - The field's `xmlName` override, or `undefined`.
|
|
297
|
+
* @param field - The field's own namespace, or `undefined`.
|
|
298
|
+
*/
|
|
299
|
+
const recordValue = (scan, elementPath, valueKey, isAttribute, name, field) => {
|
|
300
|
+
if (isAttribute) scan.problems.push(`the field "${valueKey}" is marked as both an attribute and the element's value`);
|
|
301
|
+
else if (Predicate.isNotUndefined(name)) scan.problems.push(`the value field "${valueKey}" cannot have an xmlName, because character data has no name`);
|
|
302
|
+
else if (Predicate.isNotUndefined(field)) scan.problems.push(`the value field "${valueKey}" cannot have a namespace, because character data has none`);
|
|
303
|
+
else {
|
|
304
|
+
const previous = scan.valueByElement.get(elementPath);
|
|
305
|
+
if (Predicate.isNotUndefined(previous) && previous !== valueKey) scan.problems.push(`the element "${elementPath === "" ? "root" : elementPath}" has more than one value field, "${previous}" and "${valueKey}"`);
|
|
306
|
+
else scan.valueByElement.set(elementPath, valueKey);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* @description Records one struct property: its `xmlName`, its namespace, and the namespace its descendants inherit. An attribute carries a namespace only when
|
|
311
|
+
* annotated; an element field falls back to the namespace it inherits. A value field is character data, so it is recorded against its element
|
|
312
|
+
* instead.
|
|
313
|
+
*
|
|
314
|
+
* @param scan - The scan state.
|
|
315
|
+
* @param property - The property signature.
|
|
316
|
+
* @param inherited - The namespace the enclosing element passes down.
|
|
317
|
+
* @param elementPath - The path of the element the property belongs to, or the root sentinel.
|
|
318
|
+
*/
|
|
319
|
+
const scanProperty = (scan, property, inherited, elementPath) => {
|
|
320
|
+
const key = Predicate.isString(property.name) ? property.name : String(property.name);
|
|
321
|
+
const path = childPath(elementPath, key);
|
|
322
|
+
const isAttribute = isAttributeProperty(key, property.type);
|
|
323
|
+
const isValue = isValueProperty(property.type);
|
|
324
|
+
const name = keyNameOf(property.type) ?? nameOf(property.type);
|
|
325
|
+
const field = keyNamespaceOf(property.type) ?? namespaceOf(property.type);
|
|
326
|
+
if (isValue) recordValue(scan, elementPath, key, isAttribute, name, field);
|
|
327
|
+
else {
|
|
328
|
+
noteAttribute(scan, path, isAttribute);
|
|
329
|
+
recordName(scan, path, name);
|
|
330
|
+
recordFieldNamespace(scan, path, isAttribute, field, inherited);
|
|
331
|
+
}
|
|
332
|
+
scanNode(scan, property.type, isAttribute ? inherited : field ?? inherited, path);
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* @description Walks a list of child ASTs under one inherited namespace.
|
|
336
|
+
*
|
|
337
|
+
* @param scan - The scan state.
|
|
338
|
+
* @param nodes - The child ASTs.
|
|
339
|
+
* @param inherited - The namespace they inherit.
|
|
340
|
+
* @param elementPath - The path of the element they belong to, or the root sentinel.
|
|
341
|
+
*/
|
|
342
|
+
const scanAll = (scan, nodes, inherited, elementPath) => {
|
|
343
|
+
for (const node of nodes) scanNode(scan, node, inherited, elementPath);
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* @description Records the names in one object node: its properties and its index signatures, each under the namespace the object passes down.
|
|
347
|
+
*
|
|
348
|
+
* @param scan - The scan state.
|
|
349
|
+
* @param ast - The object AST.
|
|
350
|
+
* @param namespace - The namespace the object passes to its members.
|
|
351
|
+
* @param elementPath - The path of the element the object describes, or the root sentinel.
|
|
352
|
+
*/
|
|
353
|
+
const scanObject = (scan, ast, namespace, elementPath) => {
|
|
354
|
+
for (const property of ast.propertySignatures) scanProperty(scan, property, namespace, elementPath);
|
|
355
|
+
for (const index of ast.indexSignatures) scanNode(scan, index.type, namespace, elementPath);
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* @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
|
|
359
|
+
* the ambiguity and does not wrap.
|
|
360
|
+
*
|
|
361
|
+
* @param scan - The scan state.
|
|
362
|
+
* @param ast - The AST being scanned.
|
|
363
|
+
* @param elementPath - The path of the field the AST describes.
|
|
364
|
+
*/
|
|
365
|
+
const noteArray = (scan, ast, elementPath) => {
|
|
366
|
+
if (ast._tag === "Arrays" && scan.nameByKey.has(elementPath)) scan.arrayKeys.add(elementPath);
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* @description Records every name in one schema AST, carrying the namespace an element passes to its descendants and the element the names belong to.
|
|
370
|
+
*
|
|
371
|
+
* @param scan - The scan state.
|
|
372
|
+
* @param ast - The AST to walk.
|
|
373
|
+
* @param inherited - The namespace the enclosing element passes down.
|
|
374
|
+
* @param elementPath - The path of the element this AST describes, or the root sentinel.
|
|
375
|
+
*/
|
|
376
|
+
const scanNode = (scan, ast, inherited, elementPath) => {
|
|
377
|
+
if (scan.seen.has(ast)) return;
|
|
378
|
+
scan.seen.add(ast);
|
|
379
|
+
try {
|
|
380
|
+
noteArray(scan, ast, elementPath);
|
|
381
|
+
const namespace = namespaceOf(ast) ?? inherited;
|
|
382
|
+
switch (ast._tag) {
|
|
383
|
+
case "Objects":
|
|
384
|
+
scanObject(scan, ast, namespace, elementPath);
|
|
385
|
+
return;
|
|
386
|
+
case "Arrays":
|
|
387
|
+
scanAll(scan, [...ast.elements, ...ast.rest], namespace, elementPath);
|
|
388
|
+
return;
|
|
389
|
+
case "Union":
|
|
390
|
+
scanAll(scan, ast.types, namespace, elementPath);
|
|
391
|
+
return;
|
|
392
|
+
case "Suspend":
|
|
393
|
+
scanNode(scan, ast.thunk(), namespace, elementPath);
|
|
394
|
+
return;
|
|
395
|
+
case "Declaration":
|
|
396
|
+
scanAll(scan, ast.typeParameters, namespace, elementPath);
|
|
397
|
+
return;
|
|
398
|
+
default: return;
|
|
399
|
+
}
|
|
400
|
+
} finally {
|
|
401
|
+
scan.seen.delete(ast);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* @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
|
|
406
|
+
* element field records its own namespace, so encode and decode can find it by the local name alone.
|
|
407
|
+
*
|
|
408
|
+
* @param schema - The schema to walk.
|
|
409
|
+
*
|
|
410
|
+
* @returns The plan, or the annotations that cannot be honored.
|
|
411
|
+
*/
|
|
412
|
+
const namespacePlan = (schema) => {
|
|
413
|
+
const scan = {
|
|
414
|
+
byKey: /* @__PURE__ */ new Map(),
|
|
415
|
+
nameByKey: /* @__PURE__ */ new Map(),
|
|
416
|
+
attributeKeys: /* @__PURE__ */ new Set(),
|
|
417
|
+
valueByElement: /* @__PURE__ */ new Map(),
|
|
418
|
+
arrayKeys: /* @__PURE__ */ new Set(),
|
|
419
|
+
problems: [],
|
|
420
|
+
seen: /* @__PURE__ */ new Set()
|
|
421
|
+
};
|
|
422
|
+
scanNode(scan, schema.ast, void 0, "");
|
|
423
|
+
const byResolved = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const path of /* @__PURE__ */ new Set([
|
|
425
|
+
...scan.byKey.keys(),
|
|
426
|
+
...scan.nameByKey.keys(),
|
|
427
|
+
...scan.attributeKeys
|
|
428
|
+
])) {
|
|
429
|
+
const key = pathKey(path);
|
|
430
|
+
const namespace = scan.byKey.get(path);
|
|
431
|
+
const isAttribute = isAttributeKey(key) || scan.attributeKeys.has(path);
|
|
432
|
+
const local = scan.nameByKey.get(path) ?? localName(key);
|
|
433
|
+
const resolved = lookupKey(parentPath(path), resolvedKey(isAttribute, namespace?.uri, local));
|
|
434
|
+
const previous = byResolved.get(resolved);
|
|
435
|
+
if (previous !== void 0 && previous !== key) scan.problems.push(`"${previous}" and "${key}" both resolve to "${local}" under the same element`);
|
|
436
|
+
else byResolved.set(resolved, key);
|
|
437
|
+
}
|
|
438
|
+
if (scan.problems.length > 0) return { error: [...new Set(scan.problems)].join(";\n - ") };
|
|
439
|
+
return { plan: {
|
|
440
|
+
byKey: scan.byKey,
|
|
441
|
+
nameByKey: scan.nameByKey,
|
|
442
|
+
attributeKeys: scan.attributeKeys,
|
|
443
|
+
valueByElement: scan.valueByElement,
|
|
444
|
+
arrayKeys: scan.arrayKeys,
|
|
445
|
+
root: namespaceOf(schema.ast),
|
|
446
|
+
rootName: nameOf(schema.ast),
|
|
447
|
+
byResolved
|
|
448
|
+
} };
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* @description Whether a key is a namespace declaration the codec manages: `@xmlns` or `@xmlns:prefix`.
|
|
452
|
+
*
|
|
453
|
+
* @param key - The record key.
|
|
454
|
+
*
|
|
455
|
+
* @returns Whether the key is a declaration.
|
|
456
|
+
*/
|
|
457
|
+
const isDeclarationKey = (key) => key === `@xmlns` || key.startsWith(`@xmlns:`);
|
|
458
|
+
/**
|
|
459
|
+
* @description The prefix a declaration key carries, or `''` for the default namespace.
|
|
460
|
+
*
|
|
461
|
+
* @param key - A key {@link isDeclarationKey} accepted.
|
|
462
|
+
*
|
|
463
|
+
* @returns The prefix.
|
|
464
|
+
*/
|
|
465
|
+
const declarationPrefix = (key) => key === `@xmlns` ? "" : key.slice(`@xmlns:`.length);
|
|
466
|
+
/**
|
|
467
|
+
* @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
|
|
468
|
+
* `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
|
|
469
|
+
* with `xmlns=""`.
|
|
470
|
+
*
|
|
471
|
+
* @param out - The element's record, written in place.
|
|
472
|
+
* @param scope - The in-scope prefix bindings, updated in place.
|
|
473
|
+
* @param namespace - The element's namespace, or `undefined`.
|
|
474
|
+
*/
|
|
475
|
+
const declare = (out, scope, namespace) => {
|
|
476
|
+
if (Predicate.isNotUndefined(namespace) && namespace.prefix !== "") {
|
|
477
|
+
if (scope[namespace.prefix] === namespace.uri) return;
|
|
478
|
+
out[`@xmlns:${namespace.prefix}`] = namespace.uri;
|
|
479
|
+
scope[namespace.prefix] = namespace.uri;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (!Predicate.isUndefined(namespace)) {
|
|
483
|
+
if (scope[""] === namespace.uri) return;
|
|
484
|
+
out[`@xmlns`] = namespace.uri;
|
|
485
|
+
scope[""] = namespace.uri;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (Predicate.isUndefined(scope[""])) return;
|
|
489
|
+
out[`@xmlns`] = "";
|
|
490
|
+
scope[""] = void 0;
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* @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.
|
|
494
|
+
*
|
|
495
|
+
* @param plan - The namespace plan.
|
|
496
|
+
* @param path - The field's element path.
|
|
497
|
+
* @param namespace - The field's namespace, or `undefined`.
|
|
498
|
+
*
|
|
499
|
+
* @returns The key to write.
|
|
500
|
+
*/
|
|
501
|
+
const wireKey = (plan, path, namespace) => {
|
|
502
|
+
const isAttribute = isAttributeOf(plan, path);
|
|
503
|
+
const local = localOf(plan, path);
|
|
504
|
+
if (Predicate.isUndefined(namespace) || namespace.prefix === "") return isAttribute ? `@${local}` : local;
|
|
505
|
+
return `${isAttribute ? "@" : ""}${namespace.prefix}:${local}`;
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* @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
|
|
509
|
+
* element's character data, and a child element recurses with its own namespace.
|
|
510
|
+
*
|
|
511
|
+
* @param value - The element's record, keyed by the schema's local names.
|
|
512
|
+
* @param plan - The namespace plan.
|
|
513
|
+
* @param out - The wire record, written in place.
|
|
514
|
+
* @param scope - The prefix bindings in scope for this element.
|
|
515
|
+
* @param elementPath - The path of this element, or the root sentinel.
|
|
516
|
+
*/
|
|
517
|
+
const encodeFields = (value, plan, out, scope, elementPath) => {
|
|
518
|
+
const valueKey = plan.valueByElement.get(elementPath);
|
|
519
|
+
for (const [key, child] of Object.entries(value)) {
|
|
520
|
+
if (isDeclarationKey(key)) continue;
|
|
521
|
+
if (key === valueKey) {
|
|
522
|
+
out[TEXT_KEY] = child;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const path = childPath(elementPath, key);
|
|
526
|
+
const childNamespace = plan.byKey.get(path);
|
|
527
|
+
if (isAttributeOf(plan, path)) {
|
|
528
|
+
if (Predicate.isNotUndefined(childNamespace)) declare(out, scope, childNamespace);
|
|
529
|
+
out[wireKey(plan, path, childNamespace)] = child;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
out[wireKey(plan, path, childNamespace)] = encodeNames(child, plan, childNamespace, scope, path);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
/**
|
|
536
|
+
* @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
|
|
537
|
+
* subtree uses. A leaf that has to carry a declaration is wrapped as a `#text` record so the attribute has somewhere to live.
|
|
538
|
+
*
|
|
539
|
+
* @param value - The value tree, keyed by the schema's local names.
|
|
540
|
+
* @param plan - The namespace plan.
|
|
541
|
+
* @param namespace - This element's namespace.
|
|
542
|
+
* @param scope - The prefix bindings in scope above this element.
|
|
543
|
+
* @param elementPath - The path of this element, or the root sentinel.
|
|
544
|
+
*
|
|
545
|
+
* @returns The wire tree.
|
|
546
|
+
*/
|
|
547
|
+
const encodeNames = (value, plan, namespace, scope, elementPath) => {
|
|
548
|
+
if (Array.isArray(value)) return value.map((member) => encodeNames(member, plan, namespace, scope, elementPath));
|
|
549
|
+
const out = {};
|
|
550
|
+
const inner = { ...scope };
|
|
551
|
+
declare(out, inner, namespace);
|
|
552
|
+
if (Predicate.isUndefined(value)) return;
|
|
553
|
+
if (Predicate.isString(value)) return Object.keys(out).length > 0 ? {
|
|
554
|
+
...out,
|
|
555
|
+
[TEXT_KEY]: value
|
|
556
|
+
} : value;
|
|
557
|
+
if (!Predicate.isObject(value)) return value;
|
|
558
|
+
encodeFields(value, plan, out, inner, elementPath);
|
|
559
|
+
return out;
|
|
560
|
+
};
|
|
561
|
+
/**
|
|
562
|
+
* @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
|
|
563
|
+
* attribute has no namespace, because a default namespace does not apply to attributes.
|
|
564
|
+
*
|
|
565
|
+
* @param name - The name as written, without the attribute prefix.
|
|
566
|
+
* @param isAttribute - Whether the name belongs to an attribute.
|
|
567
|
+
* @param scope - The in-scope prefix bindings.
|
|
568
|
+
*
|
|
569
|
+
* @returns The resolved URI (possibly `undefined`) and local name.
|
|
570
|
+
*/
|
|
571
|
+
const resolveName = (name, isAttribute, scope) => {
|
|
572
|
+
const colon = name.indexOf(":");
|
|
573
|
+
if (colon === -1) return {
|
|
574
|
+
uri: isAttribute ? void 0 : scope[""],
|
|
575
|
+
local: name
|
|
576
|
+
};
|
|
577
|
+
return {
|
|
578
|
+
uri: scope[name.slice(0, colon)],
|
|
579
|
+
local: name.slice(colon + 1)
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
/**
|
|
583
|
+
* @description Reads an element's declarations into a fresh scope that falls back to the enclosing one.
|
|
584
|
+
*
|
|
585
|
+
* @param value - The element's record.
|
|
586
|
+
* @param scope - The bindings in scope above the element.
|
|
587
|
+
*
|
|
588
|
+
* @returns The element's own scope.
|
|
589
|
+
*/
|
|
590
|
+
const scopeOf = (value, scope) => {
|
|
591
|
+
const inner = { ...scope };
|
|
592
|
+
for (const [key, declaration] of Object.entries(value)) if (isDeclarationKey(key) && Predicate.isString(declaration)) inner[declarationPrefix(key)] = declaration;
|
|
593
|
+
return inner;
|
|
594
|
+
};
|
|
595
|
+
/**
|
|
596
|
+
* @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
|
|
597
|
+
* it unannotated.
|
|
598
|
+
*
|
|
599
|
+
* @param plan - The namespace plan.
|
|
600
|
+
* @param parent - The parent element's path.
|
|
601
|
+
* @param key - The wire key.
|
|
602
|
+
* @param scope - The in-scope prefix bindings.
|
|
603
|
+
*
|
|
604
|
+
* @returns The schema key.
|
|
605
|
+
*/
|
|
606
|
+
const schemaKey = (plan, parent, key, scope) => {
|
|
607
|
+
const isAttribute = isAttributeKey(key);
|
|
608
|
+
const { uri, local } = resolveName(localName(key), isAttribute, scope);
|
|
609
|
+
return plan.byResolved.get(lookupKey(parent, resolvedKey(isAttribute, uri, local))) ?? (isAttribute ? `@${local}` : local);
|
|
610
|
+
};
|
|
611
|
+
/**
|
|
612
|
+
* @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
|
|
613
|
+
* 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
|
|
614
|
+
* the first member stands in for the run — every member describes the same element and carries the same declaration.
|
|
615
|
+
*
|
|
616
|
+
* @param child - The child value.
|
|
617
|
+
* @param scope - The bindings in scope above the child.
|
|
618
|
+
*
|
|
619
|
+
* @returns The scope to resolve the child's own name with.
|
|
620
|
+
*/
|
|
621
|
+
const childScopeOf = (child, scope) => {
|
|
622
|
+
if (Array.isArray(child)) return child.length > 0 ? childScopeOf(child[0], scope) : scope;
|
|
623
|
+
return Predicate.isObject(child) ? scopeOf(child, scope) : scope;
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* @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
|
|
627
|
+
* 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
|
|
628
|
+
* string, which is how a namespaced leaf stays a `Schema.String`.
|
|
629
|
+
*
|
|
630
|
+
* @param value - The parsed wire tree.
|
|
631
|
+
* @param plan - The namespace plan.
|
|
632
|
+
* @param scope - The prefix bindings in scope above this element.
|
|
633
|
+
* @param elementPath - The path of this element, or the root sentinel.
|
|
634
|
+
*
|
|
635
|
+
* @returns The value tree, keyed by the schema's names.
|
|
636
|
+
*/
|
|
637
|
+
const decodeNames = (value, plan, scope, elementPath) => {
|
|
638
|
+
if (Array.isArray(value)) return value.map((member) => decodeNames(member, plan, scope, elementPath));
|
|
639
|
+
if (Predicate.isString(value) || Predicate.isUndefined(value)) return value;
|
|
640
|
+
if (!Predicate.isObject(value)) return value;
|
|
641
|
+
const record = value;
|
|
642
|
+
const inner = scopeOf(record, scope);
|
|
643
|
+
const valueKey = plan.valueByElement.get(elementPath);
|
|
644
|
+
const out = {};
|
|
645
|
+
for (const [key, child] of Object.entries(record)) {
|
|
646
|
+
if (isDeclarationKey(key)) continue;
|
|
647
|
+
if (key === "#text") {
|
|
648
|
+
out[valueKey ?? "#text"] = decodeNames(child, plan, inner, elementPath);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const childKey = schemaKey(plan, elementPath, key, childScopeOf(child, inner));
|
|
652
|
+
const path = childPath(elementPath, childKey);
|
|
653
|
+
const decoded = decodeNames(child, plan, inner, path);
|
|
654
|
+
out[childKey] = plan.arrayKeys.has(path) && !Array.isArray(decoded) ? [decoded] : decoded;
|
|
655
|
+
}
|
|
656
|
+
const keys = Object.keys(out);
|
|
657
|
+
if (keys.length === 1 && keys[0] === "#text") return out[TEXT_KEY];
|
|
658
|
+
return out;
|
|
659
|
+
};
|
|
660
|
+
//#endregion
|
|
661
|
+
export { ATTRIBUTE_KEY, NAMESPACE_KEY, NAME_KEY, PREFIX_KEY, VALUE_KEY, decodeNames, encodeNames, namespacePlan };
|
|
662
|
+
|
|
663
|
+
//# sourceMappingURL=namespaces.js.map
|