@llm4ts/shell 2.13.0 → 2.13.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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ComplexTypeDef, isBuiltin, localName, SimpleTypeDef, splitClark, typeByName } from "./Catalog.js";
|
|
1
|
+
import { ComplexTypeDef, contentModel, emptiable, isBuiltin, localName, particleFields, qname, SimpleTypeDef, splitClark, typeByName, XsdNamespace } from "./Catalog.js";
|
|
2
2
|
import { kindForName } from "./Masking.js";
|
|
3
3
|
import { escapeAttribute, escapeText, elements, textOf } from "./Xml.js";
|
|
4
4
|
import { isYamlList, isYamlMap, yamlKey, yamlScalar } from "./Yaml.js";
|
|
@@ -9,100 +9,468 @@ import { isYamlList, isYamlMap, yamlKey, yamlScalar } from "./Yaml.js";
|
|
|
9
9
|
// keys starting with `@`; the text of a simple-content type is `#text`.
|
|
10
10
|
// Child order on the wire follows the XSD sequence, not the file.
|
|
11
11
|
export const XsiNamespace = "http://www.w3.org/2001/XMLSchema-instance";
|
|
12
|
+
/** Built-in list types and their item types. */
|
|
13
|
+
const builtinLists = {
|
|
14
|
+
NMTOKENS: "NMTOKEN",
|
|
15
|
+
IDREFS: "IDREF",
|
|
16
|
+
ENTITIES: "ENTITY"
|
|
17
|
+
};
|
|
12
18
|
export const simpleView = (catalog, name) => {
|
|
13
19
|
const facets = [];
|
|
14
20
|
const chain = [];
|
|
15
21
|
let current = name;
|
|
22
|
+
let itemType;
|
|
23
|
+
let union = false;
|
|
16
24
|
for (let depth = 0; depth < 16 && !isBuiltin(current); depth++) {
|
|
17
25
|
const type = typeByName(catalog, current);
|
|
18
26
|
if (!(type instanceof SimpleTypeDef))
|
|
19
27
|
break;
|
|
20
|
-
facets.push(type.facets);
|
|
21
28
|
chain.push(localName(type.name));
|
|
29
|
+
if (type.variety === "list") {
|
|
30
|
+
itemType = type.itemType ?? qname(XsdNamespace, "string");
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (type.variety === "union")
|
|
34
|
+
union = true;
|
|
35
|
+
facets.push(type.facets);
|
|
22
36
|
current = type.base;
|
|
23
37
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
]
|
|
38
|
+
const builtin = isBuiltin(current) ? localName(current) : "string";
|
|
39
|
+
const listItem = builtinLists[builtin];
|
|
40
|
+
if (itemType === undefined && listItem !== undefined)
|
|
41
|
+
itemType = qname(XsdNamespace, listItem);
|
|
42
|
+
return { builtin, facets, chain, itemType, union };
|
|
43
|
+
};
|
|
44
|
+
/** Inclusive bounds of the integer built-ins; `undefined` is unbounded. */
|
|
45
|
+
const integerRanges = {
|
|
46
|
+
integer: [undefined, undefined],
|
|
47
|
+
nonNegativeInteger: [0n, undefined],
|
|
48
|
+
positiveInteger: [1n, undefined],
|
|
49
|
+
nonPositiveInteger: [undefined, 0n],
|
|
50
|
+
negativeInteger: [undefined, -1n],
|
|
51
|
+
long: [-(2n ** 63n), 2n ** 63n - 1n],
|
|
52
|
+
int: [-(2n ** 31n), 2n ** 31n - 1n],
|
|
53
|
+
short: [-32768n, 32767n],
|
|
54
|
+
byte: [-128n, 127n],
|
|
55
|
+
unsignedLong: [0n, 2n ** 64n - 1n],
|
|
56
|
+
unsignedInt: [0n, 2n ** 32n - 1n],
|
|
57
|
+
unsignedShort: [0n, 65535n],
|
|
58
|
+
unsignedByte: [0n, 255n]
|
|
59
|
+
};
|
|
60
|
+
const integerTypes = new Set(Object.keys(integerRanges));
|
|
61
|
+
const isDecimal = (builtin) => builtin === "decimal" || integerTypes.has(builtin);
|
|
62
|
+
const isFloating = (builtin) => builtin === "float" || builtin === "double";
|
|
63
|
+
/** Built-ins derived from xs:string by whitespace: token and its descendants collapse. */
|
|
64
|
+
const replacing = new Set(["normalizedString"]);
|
|
65
|
+
const preserving = new Set(["string", "anySimpleType", "anyType"]);
|
|
66
|
+
/** The effective whiteSpace facet: the most derived one, else the built-in's. */
|
|
67
|
+
const whiteSpaceOf = (view) => {
|
|
68
|
+
for (const facets of view.facets)
|
|
69
|
+
if (facets.whiteSpace !== undefined)
|
|
70
|
+
return facets.whiteSpace;
|
|
71
|
+
if (view.itemType !== undefined)
|
|
72
|
+
return "collapse";
|
|
73
|
+
if (view.union || preserving.has(view.builtin))
|
|
74
|
+
return "preserve";
|
|
75
|
+
return replacing.has(view.builtin) ? "replace" : "collapse";
|
|
76
|
+
};
|
|
77
|
+
const normalizeSpace = (value, mode) => mode === "preserve"
|
|
78
|
+
? value
|
|
79
|
+
: mode === "replace"
|
|
80
|
+
? value.replace(/[\t\n\r]/g, " ")
|
|
81
|
+
: value.replace(/[\t\n\r ]+/g, " ").trim();
|
|
82
|
+
const zone = "(Z|[+-]\\d{2}:\\d{2})?";
|
|
83
|
+
const validZone = (text) => {
|
|
84
|
+
if (text === undefined || text === "Z")
|
|
85
|
+
return true;
|
|
86
|
+
const hours = Number(text.slice(1, 3));
|
|
87
|
+
const minutes = Number(text.slice(4, 6));
|
|
88
|
+
return minutes < 60 && (hours < 14 || (hours === 14 && minutes === 0));
|
|
89
|
+
};
|
|
90
|
+
const leapYear = (year) => {
|
|
91
|
+
// The leap rule only needs the year modulo 400, which its last four digits give.
|
|
92
|
+
const last = Number(year.replace(/^-/, "").slice(-4));
|
|
93
|
+
return last % 4 === 0 && (last % 100 !== 0 || last % 400 === 0);
|
|
94
|
+
};
|
|
95
|
+
const daysIn = (year, month) => month === 2 ? (leapYear(year) ? 29 : 28) : [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
96
|
+
const validMonth = (month) => Number(month) >= 1 && Number(month) <= 12;
|
|
97
|
+
const validDate = (year, month, day) => validMonth(month) && Number(day) >= 1 && Number(day) <= daysIn(year, Number(month));
|
|
98
|
+
const validTime = (hours, minutes, seconds, fraction) => (Number(hours) < 24 && Number(minutes) < 60 && Number(seconds) < 60) ||
|
|
99
|
+
(hours === "24" && minutes === "00" && seconds === "00" && /^(\.0+)?$/.test(fraction));
|
|
100
|
+
const shapes = {
|
|
101
|
+
date: new RegExp(`^(-?\\d{4,})-(\\d{2})-(\\d{2})${zone}$`),
|
|
102
|
+
dateTime: new RegExp(`^(-?\\d{4,})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?${zone}$`),
|
|
103
|
+
time: new RegExp(`^(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?${zone}$`),
|
|
104
|
+
gYearMonth: new RegExp(`^(-?\\d{4,})-(\\d{2})${zone}$`),
|
|
105
|
+
gYear: new RegExp(`^(-?\\d{4,})${zone}$`),
|
|
106
|
+
gMonth: new RegExp(`^--(\\d{2})(?:--)?${zone}$`),
|
|
107
|
+
gDay: new RegExp(`^---(\\d{2})${zone}$`),
|
|
108
|
+
gMonthDay: new RegExp(`^--(\\d{2})-(\\d{2})${zone}$`)
|
|
109
|
+
};
|
|
41
110
|
const lexical = {
|
|
42
|
-
boolean: /^(true|false|1|0)
|
|
43
|
-
decimal: /^[+-]?(\d+(\.\d*)?|\.\d+)
|
|
44
|
-
float: /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)
|
|
45
|
-
double: /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
111
|
+
boolean: (value) => /^(true|false|1|0)$/.test(value),
|
|
112
|
+
decimal: (value) => /^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(value),
|
|
113
|
+
float: (value) => /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)$/.test(value),
|
|
114
|
+
double: (value) => /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)$/.test(value),
|
|
115
|
+
duration: (value) => /^-?P(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/.test(value),
|
|
116
|
+
date: (value) => {
|
|
117
|
+
const match = shapes.date?.exec(value);
|
|
118
|
+
return (match != null &&
|
|
119
|
+
validDate(match[1] ?? "", match[2] ?? "", match[3] ?? "") &&
|
|
120
|
+
validZone(match[4]));
|
|
121
|
+
},
|
|
122
|
+
dateTime: (value) => {
|
|
123
|
+
const match = shapes.dateTime?.exec(value);
|
|
124
|
+
return (match != null &&
|
|
125
|
+
validDate(match[1] ?? "", match[2] ?? "", match[3] ?? "") &&
|
|
126
|
+
validTime(match[4] ?? "", match[5] ?? "", match[6] ?? "", match[7] ?? "") &&
|
|
127
|
+
validZone(match[8]));
|
|
128
|
+
},
|
|
129
|
+
time: (value) => {
|
|
130
|
+
const match = shapes.time?.exec(value);
|
|
131
|
+
return (match != null &&
|
|
132
|
+
validTime(match[1] ?? "", match[2] ?? "", match[3] ?? "", match[4] ?? "") &&
|
|
133
|
+
validZone(match[5]));
|
|
134
|
+
},
|
|
135
|
+
gYearMonth: (value) => {
|
|
136
|
+
const match = shapes.gYearMonth?.exec(value);
|
|
137
|
+
return match != null && validMonth(match[2] ?? "") && validZone(match[3]);
|
|
138
|
+
},
|
|
139
|
+
gYear: (value) => {
|
|
140
|
+
const match = shapes.gYear?.exec(value);
|
|
141
|
+
return match != null && validZone(match[2]);
|
|
142
|
+
},
|
|
143
|
+
gMonth: (value) => {
|
|
144
|
+
const match = shapes.gMonth?.exec(value);
|
|
145
|
+
return match != null && validMonth(match[1] ?? "") && validZone(match[2]);
|
|
146
|
+
},
|
|
147
|
+
gDay: (value) => {
|
|
148
|
+
const match = shapes.gDay?.exec(value);
|
|
149
|
+
return match != null && Number(match[1]) >= 1 && Number(match[1]) <= 31 && validZone(match[2]);
|
|
150
|
+
},
|
|
151
|
+
gMonthDay: (value) => {
|
|
152
|
+
const match = shapes.gMonthDay?.exec(value);
|
|
153
|
+
return (match != null &&
|
|
154
|
+
validMonth(match[1] ?? "") &&
|
|
155
|
+
Number(match[2]) >= 1 &&
|
|
156
|
+
// A leap year: --02-29 is a valid month-day.
|
|
157
|
+
Number(match[2]) <= daysIn("2000", Number(match[1])) &&
|
|
158
|
+
validZone(match[3]));
|
|
159
|
+
},
|
|
160
|
+
// Whole quanta of four characters, padding only at the end.
|
|
161
|
+
base64Binary: (value) => /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value.replace(/[\t\n\r ]/g, "")),
|
|
162
|
+
hexBinary: (value) => /^([0-9A-Fa-f]{2})*$/.test(value)
|
|
163
|
+
};
|
|
164
|
+
// XSD regular expressions: implicitly anchored, no anchors of their own,
|
|
165
|
+
// `.` excludes only line ends, \d \w \s \i \c and \p{Is<Block>} mean more
|
|
166
|
+
// (or less) than in JavaScript, and classes can be subtracted. Translated
|
|
167
|
+
// token by token; anything without a faithful translation is refused.
|
|
168
|
+
const unicodeBlocks = {
|
|
169
|
+
BasicLatin: "\\u{0}-\\u{7F}",
|
|
170
|
+
"Latin-1Supplement": "\\u{80}-\\u{FF}",
|
|
171
|
+
"LatinExtended-A": "\\u{100}-\\u{17F}",
|
|
172
|
+
"LatinExtended-B": "\\u{180}-\\u{24F}",
|
|
173
|
+
IPAExtensions: "\\u{250}-\\u{2AF}",
|
|
174
|
+
SpacingModifierLetters: "\\u{2B0}-\\u{2FF}",
|
|
175
|
+
CombiningDiacriticalMarks: "\\u{300}-\\u{36F}",
|
|
176
|
+
Greek: "\\u{370}-\\u{3FF}",
|
|
177
|
+
GreekandCoptic: "\\u{370}-\\u{3FF}",
|
|
178
|
+
Cyrillic: "\\u{400}-\\u{4FF}",
|
|
179
|
+
Armenian: "\\u{530}-\\u{58F}",
|
|
180
|
+
Hebrew: "\\u{590}-\\u{5FF}",
|
|
181
|
+
Arabic: "\\u{600}-\\u{6FF}",
|
|
182
|
+
Devanagari: "\\u{900}-\\u{97F}",
|
|
183
|
+
Thai: "\\u{E00}-\\u{E7F}",
|
|
184
|
+
LatinExtendedAdditional: "\\u{1E00}-\\u{1EFF}",
|
|
185
|
+
GreekExtended: "\\u{1F00}-\\u{1FFF}",
|
|
186
|
+
GeneralPunctuation: "\\u{2000}-\\u{206F}",
|
|
187
|
+
SuperscriptsandSubscripts: "\\u{2070}-\\u{209F}",
|
|
188
|
+
CurrencySymbols: "\\u{20A0}-\\u{20CF}",
|
|
189
|
+
LetterlikeSymbols: "\\u{2100}-\\u{214F}",
|
|
190
|
+
NumberForms: "\\u{2150}-\\u{218F}",
|
|
191
|
+
Arrows: "\\u{2190}-\\u{21FF}",
|
|
192
|
+
MathematicalOperators: "\\u{2200}-\\u{22FF}",
|
|
193
|
+
BoxDrawing: "\\u{2500}-\\u{257F}",
|
|
194
|
+
CJKSymbolsandPunctuation: "\\u{3000}-\\u{303F}",
|
|
195
|
+
Hiragana: "\\u{3040}-\\u{309F}",
|
|
196
|
+
Katakana: "\\u{30A0}-\\u{30FF}",
|
|
197
|
+
CJKUnifiedIdeographs: "\\u{4E00}-\\u{9FFF}",
|
|
198
|
+
HangulSyllables: "\\u{AC00}-\\u{D7AF}",
|
|
199
|
+
PrivateUse: "\\u{E000}-\\u{F8FF}",
|
|
200
|
+
AlphabeticPresentationForms: "\\u{FB00}-\\u{FB4F}",
|
|
201
|
+
HalfwidthandFullwidthForms: "\\u{FF00}-\\u{FFEF}",
|
|
202
|
+
Specials: "\\u{FFF0}-\\u{FFFF}"
|
|
203
|
+
};
|
|
204
|
+
const set = (members) => ({ inside: members, atom: `[${members}]` });
|
|
205
|
+
const complement = (members) => ({ inside: undefined, atom: `[^${members}]` });
|
|
206
|
+
const classEscapes = {
|
|
207
|
+
d: { inside: "\\p{Nd}", atom: "\\p{Nd}" },
|
|
208
|
+
D: { inside: "\\P{Nd}", atom: "\\P{Nd}" },
|
|
209
|
+
s: set(" \\t\\n\\r"),
|
|
210
|
+
S: complement(" \\t\\n\\r"),
|
|
211
|
+
w: set("\\p{L}\\p{M}\\p{N}\\p{S}"),
|
|
212
|
+
W: set("\\p{P}\\p{Z}\\p{C}"),
|
|
213
|
+
i: set("\\p{L}_:"),
|
|
214
|
+
I: complement("\\p{L}_:"),
|
|
215
|
+
c: set("\\p{L}\\p{M}\\p{N}._:\\-"),
|
|
216
|
+
C: complement("\\p{L}\\p{M}\\p{N}._:\\-")
|
|
217
|
+
};
|
|
218
|
+
const categories = /^(L[ultmo]?|M[nce]?|N[dlo]?|P[cdseifo]?|Z[slp]?|S[mcko]?|C[cfons]?)$/;
|
|
219
|
+
/** Translate an XSD pattern to an anchored JavaScript source; undefined when impossible. */
|
|
220
|
+
export const translatePattern = (pattern) => {
|
|
221
|
+
let index = 0;
|
|
222
|
+
const escape = () => {
|
|
223
|
+
const char = pattern[index + 1];
|
|
224
|
+
index += 2;
|
|
225
|
+
if (char === undefined)
|
|
226
|
+
return undefined;
|
|
227
|
+
if (char === "n")
|
|
228
|
+
return { inside: "\\n", atom: "\\n" };
|
|
229
|
+
if (char === "r")
|
|
230
|
+
return { inside: "\\r", atom: "\\r" };
|
|
231
|
+
if (char === "t")
|
|
232
|
+
return { inside: "\\t", atom: "\\t" };
|
|
233
|
+
if (char === "-")
|
|
234
|
+
return { inside: "\\-", atom: "-" };
|
|
235
|
+
if ("\\|.?*+(){}[]^".includes(char))
|
|
236
|
+
return { inside: `\\${char}`, atom: `\\${char}` };
|
|
237
|
+
const known = classEscapes[char];
|
|
238
|
+
if (known !== undefined)
|
|
239
|
+
return known;
|
|
240
|
+
if (char !== "p" && char !== "P")
|
|
241
|
+
return undefined;
|
|
242
|
+
const close = pattern.indexOf("}", index);
|
|
243
|
+
if (pattern[index] !== "{" || close < 0)
|
|
244
|
+
return undefined;
|
|
245
|
+
const property = pattern.slice(index + 1, close);
|
|
246
|
+
index = close + 1;
|
|
247
|
+
if (property.startsWith("Is")) {
|
|
248
|
+
const block = unicodeBlocks[property.slice(2)];
|
|
249
|
+
if (block === undefined)
|
|
250
|
+
return undefined;
|
|
251
|
+
return char === "p" ? set(block) : complement(block);
|
|
252
|
+
}
|
|
253
|
+
if (!categories.test(property))
|
|
254
|
+
return undefined;
|
|
255
|
+
const atom = `\\${char}{${property}}`;
|
|
256
|
+
return { inside: atom, atom };
|
|
257
|
+
};
|
|
258
|
+
// A class body after its `[`, through its `]`; subtraction as a lookahead.
|
|
259
|
+
const characterClass = () => {
|
|
260
|
+
const negated = pattern[index] === "^";
|
|
261
|
+
if (negated)
|
|
262
|
+
index++;
|
|
263
|
+
let members = "";
|
|
264
|
+
let subtracted;
|
|
265
|
+
for (;;) {
|
|
266
|
+
const char = pattern[index];
|
|
267
|
+
if (char === undefined)
|
|
268
|
+
return undefined;
|
|
269
|
+
if (char === "]") {
|
|
270
|
+
index++;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (char === "-" && pattern[index + 1] === "[") {
|
|
274
|
+
index += 2;
|
|
275
|
+
subtracted = characterClass();
|
|
276
|
+
if (subtracted === undefined || pattern[index] !== "]")
|
|
277
|
+
return undefined;
|
|
278
|
+
index++;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
if (char === "\\") {
|
|
282
|
+
const escaped = escape();
|
|
283
|
+
if (escaped?.inside === undefined)
|
|
284
|
+
return undefined;
|
|
285
|
+
members += escaped.inside;
|
|
286
|
+
}
|
|
287
|
+
else if (char === "[") {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
members += char;
|
|
292
|
+
index++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (members === "")
|
|
296
|
+
return undefined;
|
|
297
|
+
const base = `[${negated ? "^" : ""}${members}]`;
|
|
298
|
+
return subtracted === undefined ? base : `(?:(?!${subtracted})${base})`;
|
|
299
|
+
};
|
|
300
|
+
let source = "";
|
|
301
|
+
while (index < pattern.length) {
|
|
302
|
+
const char = pattern[index] ?? "";
|
|
303
|
+
if (char === "\\") {
|
|
304
|
+
const escaped = escape();
|
|
305
|
+
if (escaped === undefined)
|
|
306
|
+
return undefined;
|
|
307
|
+
source += escaped.atom;
|
|
308
|
+
}
|
|
309
|
+
else if (char === "[") {
|
|
310
|
+
index++;
|
|
311
|
+
const translated = characterClass();
|
|
312
|
+
if (translated === undefined)
|
|
313
|
+
return undefined;
|
|
314
|
+
source += translated;
|
|
315
|
+
}
|
|
316
|
+
else if (char === ".") {
|
|
317
|
+
source += "[^\\n\\r]";
|
|
318
|
+
index++;
|
|
319
|
+
}
|
|
320
|
+
else if (char === "^" || char === "$") {
|
|
321
|
+
source += `\\${char}`;
|
|
322
|
+
index++;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
source += char;
|
|
326
|
+
index++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return `^(?:${source})$`;
|
|
330
|
+
};
|
|
54
331
|
const xsdRegex = (pattern) => {
|
|
332
|
+
const source = translatePattern(pattern);
|
|
333
|
+
if (source === undefined)
|
|
334
|
+
return undefined;
|
|
55
335
|
try {
|
|
56
|
-
return new RegExp(
|
|
336
|
+
return new RegExp(source, "u");
|
|
57
337
|
}
|
|
58
338
|
catch {
|
|
59
339
|
return undefined;
|
|
60
340
|
}
|
|
61
341
|
};
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
342
|
+
const decimalParts = (value) => {
|
|
343
|
+
const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(value);
|
|
344
|
+
if (match === null || (match[2] === "" && (match[3] ?? "") === ""))
|
|
345
|
+
return undefined;
|
|
66
346
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
347
|
+
negative: match[1] === "-",
|
|
348
|
+
whole: (match[2] ?? "").replace(/^0+/, ""),
|
|
349
|
+
fraction: (match[3] ?? "").replace(/0+$/, "")
|
|
69
350
|
};
|
|
70
351
|
};
|
|
71
|
-
|
|
352
|
+
/** Exact comparison of two decimal literals: -1, 0, 1, or undefined when either is not one. */
|
|
353
|
+
const compareDecimal = (left, right) => {
|
|
354
|
+
const a = decimalParts(left);
|
|
355
|
+
const b = decimalParts(right);
|
|
356
|
+
if (a === undefined || b === undefined)
|
|
357
|
+
return undefined;
|
|
358
|
+
const scale = Math.max(a.fraction.length, b.fraction.length);
|
|
359
|
+
const scaled = (parts) => {
|
|
360
|
+
const magnitude = BigInt(`${parts.whole}${parts.fraction.padEnd(scale, "0")}` || "0");
|
|
361
|
+
return parts.negative ? -magnitude : magnitude;
|
|
362
|
+
};
|
|
363
|
+
const x = scaled(a);
|
|
364
|
+
const y = scaled(b);
|
|
365
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
366
|
+
};
|
|
367
|
+
const floating = (value) => value === "INF" ? Infinity : value === "-INF" ? -Infinity : Number(value);
|
|
368
|
+
/** Order two values of a numeric built-in; undefined when not comparable. */
|
|
369
|
+
const compareValues = (builtin, left, right) => {
|
|
370
|
+
if (isDecimal(builtin))
|
|
371
|
+
return compareDecimal(left, right);
|
|
372
|
+
if (isFloating(builtin)) {
|
|
373
|
+
const x = floating(left);
|
|
374
|
+
const y = floating(right);
|
|
375
|
+
if (Number.isNaN(x) || Number.isNaN(y))
|
|
376
|
+
return undefined;
|
|
377
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
};
|
|
381
|
+
/** Equality in the value space of the built-in (`1.0` is `1.00` as a decimal). */
|
|
382
|
+
const sameValue = (builtin, left, right) => {
|
|
383
|
+
if (isDecimal(builtin) || isFloating(builtin)) {
|
|
384
|
+
const order = compareValues(builtin, left, right);
|
|
385
|
+
if (order !== undefined)
|
|
386
|
+
return order === 0;
|
|
387
|
+
return isFloating(builtin) && left === "NaN" && right === "NaN";
|
|
388
|
+
}
|
|
389
|
+
if (builtin === "boolean") {
|
|
390
|
+
const truth = (value) => (value === "1" ? "true" : value === "0" ? "false" : value);
|
|
391
|
+
return truth(left) === truth(right);
|
|
392
|
+
}
|
|
393
|
+
return left === right;
|
|
394
|
+
};
|
|
395
|
+
/** Length in the facet's unit: octets for binaries, code points otherwise. */
|
|
396
|
+
const lengthOf = (builtin, value) => {
|
|
397
|
+
if (builtin === "hexBinary")
|
|
398
|
+
return Math.floor(value.length / 2);
|
|
399
|
+
if (builtin === "base64Binary") {
|
|
400
|
+
const compact = value.replace(/[\t\n\r ]/g, "");
|
|
401
|
+
return Math.floor(compact.length / 4) * 3 - (compact.match(/=+$/)?.[0].length ?? 0);
|
|
402
|
+
}
|
|
403
|
+
return [...value].length;
|
|
404
|
+
};
|
|
405
|
+
const digitsOf = (value) => {
|
|
406
|
+
const parts = decimalParts(value);
|
|
407
|
+
const whole = parts?.whole ?? "";
|
|
408
|
+
const fraction = parts?.fraction ?? "";
|
|
409
|
+
return { total: whole.length + fraction.length, fraction: fraction.length };
|
|
410
|
+
};
|
|
411
|
+
export const checkSimple = (catalog, type, raw) => {
|
|
72
412
|
const view = simpleView(catalog, type);
|
|
413
|
+
// Every check sees the value after its whiteSpace facet normalizes it.
|
|
414
|
+
const value = normalizeSpace(raw, whiteSpaceOf(view));
|
|
73
415
|
const problems = [];
|
|
74
416
|
const builtin = view.builtin;
|
|
75
|
-
|
|
417
|
+
const items = view.itemType === undefined ? undefined : value === "" ? [] : value.split(" ");
|
|
418
|
+
if (items !== undefined) {
|
|
419
|
+
for (const item of items) {
|
|
420
|
+
for (const problem of checkSimple(catalog, view.itemType ?? type, item)) {
|
|
421
|
+
problems.push(`item ${problem}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (view.union) {
|
|
426
|
+
// Member types are not modelled; any string is accepted.
|
|
427
|
+
}
|
|
428
|
+
else if (integerTypes.has(builtin)) {
|
|
429
|
+
const [low, high] = integerRanges[builtin] ?? [undefined, undefined];
|
|
76
430
|
if (!/^[+-]?\d+$/.test(value))
|
|
77
431
|
problems.push(`"${value}" is not an ${builtin}`);
|
|
432
|
+
else {
|
|
433
|
+
const number = BigInt(value.replace(/^\+/, ""));
|
|
434
|
+
if ((low !== undefined && number < low) || (high !== undefined && number > high)) {
|
|
435
|
+
problems.push(`${value} is out of the ${builtin} range`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
78
438
|
}
|
|
79
439
|
else {
|
|
80
440
|
const shape = lexical[builtin];
|
|
81
|
-
if (shape !== undefined && !shape
|
|
441
|
+
if (shape !== undefined && !shape(value))
|
|
82
442
|
problems.push(`"${value}" is not a ${builtin}`);
|
|
83
443
|
}
|
|
84
|
-
const
|
|
444
|
+
const lexicallyValid = problems.length === 0;
|
|
85
445
|
for (const facets of view.facets) {
|
|
86
|
-
if (facets.enumeration !== undefined &&
|
|
446
|
+
if (facets.enumeration !== undefined &&
|
|
447
|
+
!facets.enumeration.some((option) => sameValue(builtin, normalizeSpace(option, whiteSpaceOf(view)), value))) {
|
|
87
448
|
problems.push(`"${value}" is not one of ${facets.enumeration.join(", ")}`);
|
|
88
449
|
}
|
|
89
450
|
if (facets.pattern !== undefined && facets.pattern.length > 0) {
|
|
90
|
-
const
|
|
91
|
-
|
|
451
|
+
const regexes = facets.pattern.map(xsdRegex);
|
|
452
|
+
const untranslatable = facets.pattern.filter((_, position) => regexes[position] === undefined);
|
|
453
|
+
if (untranslatable.length > 0) {
|
|
454
|
+
problems.push(`pattern ${untranslatable.join(" | ")} cannot be checked`);
|
|
455
|
+
}
|
|
456
|
+
else if (!regexes.some((regex) => regex?.test(value) ?? false)) {
|
|
92
457
|
problems.push(`"${value}" does not match ${facets.pattern.join(" | ")}`);
|
|
458
|
+
}
|
|
93
459
|
}
|
|
94
|
-
const length =
|
|
460
|
+
const length = items === undefined ? lengthOf(builtin, value) : items.length;
|
|
461
|
+
const unit = items === undefined ? "length" : "items";
|
|
95
462
|
if (facets.length !== undefined && length !== facets.length) {
|
|
96
|
-
problems.push(
|
|
463
|
+
problems.push(`${unit} ${length}, expected ${facets.length}`);
|
|
97
464
|
}
|
|
98
465
|
if (facets.minLength !== undefined && length < facets.minLength) {
|
|
99
|
-
problems.push(
|
|
466
|
+
problems.push(`${unit} ${length} below ${facets.minLength}`);
|
|
100
467
|
}
|
|
101
468
|
if (facets.maxLength !== undefined && length > facets.maxLength) {
|
|
102
|
-
problems.push(
|
|
469
|
+
problems.push(`${unit} ${length} above ${facets.maxLength}`);
|
|
103
470
|
}
|
|
104
|
-
if (
|
|
105
|
-
|
|
471
|
+
if (items !== undefined || !lexicallyValid)
|
|
472
|
+
continue;
|
|
473
|
+
if (isDecimal(builtin)) {
|
|
106
474
|
const digits = digitsOf(value);
|
|
107
475
|
if (facets.totalDigits !== undefined && digits.total > facets.totalDigits) {
|
|
108
476
|
problems.push(`${digits.total} digits, at most ${facets.totalDigits}`);
|
|
@@ -110,22 +478,38 @@ export const checkSimple = (catalog, type, value) => {
|
|
|
110
478
|
if (facets.fractionDigits !== undefined && digits.fraction > facets.fractionDigits) {
|
|
111
479
|
problems.push(`${digits.fraction} fraction digits, at most ${facets.fractionDigits}`);
|
|
112
480
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
481
|
+
}
|
|
482
|
+
const compare = (bound) => bound === undefined ? undefined : compareValues(builtin, value, bound.trim());
|
|
483
|
+
const minInclusive = compare(facets.minInclusive);
|
|
484
|
+
const maxInclusive = compare(facets.maxInclusive);
|
|
485
|
+
const minExclusive = compare(facets.minExclusive);
|
|
486
|
+
const maxExclusive = compare(facets.maxExclusive);
|
|
487
|
+
if (minInclusive !== undefined && minInclusive < 0) {
|
|
488
|
+
problems.push(`${value} below ${facets.minInclusive}`);
|
|
489
|
+
}
|
|
490
|
+
if (maxInclusive !== undefined && maxInclusive > 0) {
|
|
491
|
+
problems.push(`${value} above ${facets.maxInclusive}`);
|
|
492
|
+
}
|
|
493
|
+
if (minExclusive !== undefined && minExclusive <= 0) {
|
|
494
|
+
problems.push(`${value} not above ${facets.minExclusive}`);
|
|
495
|
+
}
|
|
496
|
+
if (maxExclusive !== undefined && maxExclusive >= 0) {
|
|
497
|
+
problems.push(`${value} not below ${facets.maxExclusive}`);
|
|
125
498
|
}
|
|
126
499
|
}
|
|
127
500
|
return problems;
|
|
128
501
|
};
|
|
502
|
+
/** A `fixed=` value: the only one allowed (an empty element takes it). */
|
|
503
|
+
const checkFixed = (catalog, type, raw, fixed, emptyTakesFixed) => {
|
|
504
|
+
const view = simpleView(catalog, type);
|
|
505
|
+
const mode = whiteSpaceOf(view);
|
|
506
|
+
const value = normalizeSpace(raw, mode);
|
|
507
|
+
if (emptyTakesFixed && raw === "")
|
|
508
|
+
return [];
|
|
509
|
+
return sameValue(view.builtin, value, normalizeSpace(fixed, mode))
|
|
510
|
+
? []
|
|
511
|
+
: [`"${value}" is fixed to "${fixed}"`];
|
|
512
|
+
};
|
|
129
513
|
// ---------------------------------------------------------------------------
|
|
130
514
|
// Example values
|
|
131
515
|
/** A string matching a simple XSD pattern: literals, classes, groups (first branch), quantifiers. */
|
|
@@ -240,11 +624,18 @@ const builtinExamples = {
|
|
|
240
624
|
dateTime: "2026-01-15T10:00:00Z",
|
|
241
625
|
time: "10:00:00",
|
|
242
626
|
gYear: "2026",
|
|
627
|
+
gYearMonth: "2026-01",
|
|
628
|
+
gMonth: "--01",
|
|
629
|
+
gDay: "---15",
|
|
630
|
+
gMonthDay: "--01-15",
|
|
631
|
+
duration: "P1D",
|
|
243
632
|
base64Binary: "",
|
|
244
633
|
hexBinary: ""
|
|
245
634
|
};
|
|
246
|
-
/** An example value for a field: a seed,
|
|
635
|
+
/** An example value for a field: its fixed value, a seed, field meaning, facets, the builtin. */
|
|
247
636
|
export const exampleValue = (catalog, field, seeds = new Map()) => {
|
|
637
|
+
if (field.fixed !== undefined)
|
|
638
|
+
return field.fixed;
|
|
248
639
|
const seeded = seeds.get(field.name);
|
|
249
640
|
if (seeded !== undefined)
|
|
250
641
|
return seeded;
|
|
@@ -333,13 +724,21 @@ export const skeletonYaml = (catalog, element, indent, options = {}) => {
|
|
|
333
724
|
}
|
|
334
725
|
// The first alternative of each choice is written out; the others (whole
|
|
335
726
|
// branches, not single fields) are commented as `or`.
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
727
|
+
const alternatives = new Set();
|
|
728
|
+
const markAlternatives = (particle) => {
|
|
729
|
+
if (particle.kind === "element")
|
|
730
|
+
return;
|
|
731
|
+
particle.children.forEach((child, position) => {
|
|
732
|
+
if (particle.kind === "choice" && position > 0) {
|
|
733
|
+
for (const field of particleFields(child))
|
|
734
|
+
alternatives.add(field);
|
|
735
|
+
}
|
|
736
|
+
markAlternatives(child);
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
markAlternatives(contentModel(type));
|
|
341
740
|
for (const field of type.fields) {
|
|
342
|
-
const alternative =
|
|
741
|
+
const alternative = alternatives.has(field);
|
|
343
742
|
const optional = field.minOccurs === 0 && options.includeOptional !== true;
|
|
344
743
|
const comment = commented || optional || alternative;
|
|
345
744
|
fieldLines(field, depth, level, comment, alternative);
|
|
@@ -402,9 +801,168 @@ export const skeletonYaml = (catalog, element, indent, options = {}) => {
|
|
|
402
801
|
return lines;
|
|
403
802
|
};
|
|
404
803
|
// ---------------------------------------------------------------------------
|
|
405
|
-
//
|
|
804
|
+
// Content models over value maps
|
|
406
805
|
const join = (path, segment) => path === "" ? segment : `${path}.${segment}`;
|
|
407
|
-
const
|
|
806
|
+
const unbounded = (occurs) => occurs === "unbounded" ? Infinity : occurs;
|
|
807
|
+
/** How many elements a value map holds for a field. */
|
|
808
|
+
const countIn = (value, name) => {
|
|
809
|
+
const item = value[name];
|
|
810
|
+
return item === undefined ? 0 : isYamlList(item) ? item.length : 1;
|
|
811
|
+
};
|
|
812
|
+
const labelOf = (particle, nested = false) => {
|
|
813
|
+
if (particle.kind === "element")
|
|
814
|
+
return particle.field.name;
|
|
815
|
+
const parts = particle.children.map((child) => labelOf(child, true));
|
|
816
|
+
if (particle.kind !== "choice")
|
|
817
|
+
return parts.join(" + ");
|
|
818
|
+
return nested ? `(${parts.join(" | ")})` : parts.join(" | ");
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Whether the element counts of a value map can come from `particle`
|
|
822
|
+
* occurring `times` times. A value map keeps no order, so this is the
|
|
823
|
+
* question XSD asks of it: a repeating sequence needs balanced counts, a
|
|
824
|
+
* repeating choice any mix of its alternatives. A term is tried up to as
|
|
825
|
+
* many repetitions as there are elements (a minimal repetition is never
|
|
826
|
+
* empty; beyond it an emptiable term stays satisfiable).
|
|
827
|
+
*/
|
|
828
|
+
const fitsCounts = (value, particle, times) => {
|
|
829
|
+
const memo = new Map();
|
|
830
|
+
const sizes = new Map();
|
|
831
|
+
const sizeOf = (node) => {
|
|
832
|
+
const known = sizes.get(node);
|
|
833
|
+
if (known !== undefined)
|
|
834
|
+
return known;
|
|
835
|
+
const size = particleFields(node).reduce((sum, field) => sum + countIn(value, field.name), 0);
|
|
836
|
+
sizes.set(node, size);
|
|
837
|
+
return size;
|
|
838
|
+
};
|
|
839
|
+
const term = (node, repetitions) => {
|
|
840
|
+
if (node.kind === "element")
|
|
841
|
+
return countIn(value, node.field.name) === repetitions;
|
|
842
|
+
if (node.kind !== "choice")
|
|
843
|
+
return node.children.every((child) => occurs(child, repetitions));
|
|
844
|
+
// Split the repetitions among the alternatives.
|
|
845
|
+
let reachable = new Set([0]);
|
|
846
|
+
for (const child of node.children) {
|
|
847
|
+
const next = new Set();
|
|
848
|
+
for (const done of reachable) {
|
|
849
|
+
for (let share = 0; done + share <= repetitions; share++) {
|
|
850
|
+
if (occurs(child, share))
|
|
851
|
+
next.add(done + share);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
reachable = next;
|
|
855
|
+
}
|
|
856
|
+
return reachable.has(repetitions);
|
|
857
|
+
};
|
|
858
|
+
const occurs = (node, count) => {
|
|
859
|
+
const cached = memo.get(node)?.get(count);
|
|
860
|
+
if (cached !== undefined)
|
|
861
|
+
return cached;
|
|
862
|
+
const from = count * node.min;
|
|
863
|
+
const to = count === 0 ? 0 : Math.min(count * unbounded(node.max), Math.max(from, sizeOf(node)));
|
|
864
|
+
let result = false;
|
|
865
|
+
for (let repetitions = from; repetitions <= to && !result; repetitions++) {
|
|
866
|
+
result = term(node, repetitions);
|
|
867
|
+
}
|
|
868
|
+
memo.set(node, (memo.get(node) ?? new Map()).set(count, result));
|
|
869
|
+
return result;
|
|
870
|
+
};
|
|
871
|
+
return occurs(particle, times);
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* Check a value map against a complex type's content model: required
|
|
875
|
+
* fields and choices, all-or-nothing optional groups, balanced repeating
|
|
876
|
+
* groups, and each field's number of occurrences. Values are not descended.
|
|
877
|
+
*/
|
|
878
|
+
const checkContent = (type, value, path) => {
|
|
879
|
+
const issues = [];
|
|
880
|
+
const where = path === "" ? "(root)" : path;
|
|
881
|
+
const present = (particle) => particleFields(particle).some((field) => countIn(value, field.name) > 0);
|
|
882
|
+
const counts = (particle) => [...new Set(particleFields(particle).map((field) => field.name))]
|
|
883
|
+
.map((name) => `${name} ${countIn(value, name)}`)
|
|
884
|
+
.join(", ");
|
|
885
|
+
// `repeating`: inside a group that repeats, where different occurrences
|
|
886
|
+
// may take different alternatives of a choice.
|
|
887
|
+
const walk = (particle, repeating) => {
|
|
888
|
+
if (particle.kind === "element") {
|
|
889
|
+
const field = particle.field;
|
|
890
|
+
const count = countIn(value, field.name);
|
|
891
|
+
if (count === 0 && particle.min > 0) {
|
|
892
|
+
issues.push({ path: join(path, field.name), detail: "required field is missing" });
|
|
893
|
+
}
|
|
894
|
+
else if (!repeating &&
|
|
895
|
+
count > 0 &&
|
|
896
|
+
count < particle.min &&
|
|
897
|
+
field.minOccurs < particle.min) {
|
|
898
|
+
issues.push({
|
|
899
|
+
path: join(path, field.name),
|
|
900
|
+
detail: `at least ${particle.min} occurrences`
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const here = present(particle);
|
|
906
|
+
if (!here && (particle.min === 0 || emptiable(particle)))
|
|
907
|
+
return;
|
|
908
|
+
if (!here && particle.kind === "choice") {
|
|
909
|
+
issues.push({
|
|
910
|
+
path: where,
|
|
911
|
+
detail: `one of ${particle.children.map((child) => labelOf(child, true)).join(", ")} is required`
|
|
912
|
+
});
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const repeats = unbounded(particle.max) > 1;
|
|
916
|
+
const before = issues.length;
|
|
917
|
+
if (particle.kind === "choice" && !(repeating || repeats)) {
|
|
918
|
+
const taken = particle.children.filter(present);
|
|
919
|
+
if (taken.length > 1) {
|
|
920
|
+
issues.push({
|
|
921
|
+
path: where,
|
|
922
|
+
detail: `choose one of ${taken.map((child) => labelOf(child, true)).join(", ")}`
|
|
923
|
+
});
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
for (const child of taken)
|
|
927
|
+
walk(child, false);
|
|
928
|
+
}
|
|
929
|
+
else if (particle.kind === "choice") {
|
|
930
|
+
for (const child of particle.children.filter(present))
|
|
931
|
+
walk(child, true);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
for (const child of particle.children)
|
|
935
|
+
walk(child, repeating || repeats);
|
|
936
|
+
}
|
|
937
|
+
if (repeats && here && issues.length === before && !fitsCounts(value, particle, 1)) {
|
|
938
|
+
issues.push({
|
|
939
|
+
path: where,
|
|
940
|
+
detail: `${labelOf(particle)} repeat${particle.kind === "choice" ? "s" : " together"} as a group; counts ${counts(particle)} do not fit`
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
walk(contentModel(type), false);
|
|
945
|
+
for (const field of type.fields) {
|
|
946
|
+
const item = value[field.name];
|
|
947
|
+
if (item === undefined)
|
|
948
|
+
continue;
|
|
949
|
+
const fieldPath = join(path, field.name);
|
|
950
|
+
const count = isYamlList(item) ? item.length : 1;
|
|
951
|
+
const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
|
|
952
|
+
if (isYamlList(item) && !repeated) {
|
|
953
|
+
issues.push({ path: fieldPath, detail: "occurs at most once; write a single value" });
|
|
954
|
+
}
|
|
955
|
+
if (count < field.minOccurs)
|
|
956
|
+
issues.push({ path: fieldPath, detail: `at least ${field.minOccurs} occurrences` });
|
|
957
|
+
if (field.maxOccurs !== "unbounded" && count > field.maxOccurs) {
|
|
958
|
+
issues.push({ path: fieldPath, detail: `at most ${field.maxOccurs} occurrences` });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return issues;
|
|
962
|
+
};
|
|
963
|
+
// ---------------------------------------------------------------------------
|
|
964
|
+
// Validation of values
|
|
965
|
+
const validateComplex = (catalog, type, value, path, issues, fixed) => {
|
|
408
966
|
const known = new Set([
|
|
409
967
|
...type.fields.map((field) => field.name),
|
|
410
968
|
...type.attributes.map((attribute) => `@${attribute.name}`),
|
|
@@ -419,95 +977,49 @@ const validateComplex = (catalog, type, value, path, issues) => {
|
|
|
419
977
|
}
|
|
420
978
|
for (const attribute of type.attributes) {
|
|
421
979
|
const item = value[`@${attribute.name}`];
|
|
980
|
+
const attributePath = join(path, `@${attribute.name}`);
|
|
422
981
|
if (item === undefined || item === null) {
|
|
423
982
|
if (attribute.required)
|
|
424
|
-
issues.push({
|
|
425
|
-
path: join(path, `@${attribute.name}`),
|
|
426
|
-
detail: "required attribute is missing"
|
|
427
|
-
});
|
|
983
|
+
issues.push({ path: attributePath, detail: "required attribute is missing" });
|
|
428
984
|
}
|
|
429
985
|
else if (typeof item !== "string") {
|
|
430
|
-
issues.push({
|
|
431
|
-
path: join(path, `@${attribute.name}`),
|
|
432
|
-
detail: "an attribute holds a single value"
|
|
433
|
-
});
|
|
986
|
+
issues.push({ path: attributePath, detail: "an attribute holds a single value" });
|
|
434
987
|
}
|
|
435
988
|
else {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
989
|
+
const problems = [
|
|
990
|
+
...checkSimple(catalog, attribute.type, item),
|
|
991
|
+
...(attribute.fixed === undefined
|
|
992
|
+
? []
|
|
993
|
+
: checkFixed(catalog, attribute.type, item, attribute.fixed, false))
|
|
994
|
+
];
|
|
995
|
+
for (const problem of problems)
|
|
996
|
+
issues.push({ path: attributePath, detail: problem });
|
|
439
997
|
}
|
|
440
998
|
}
|
|
441
999
|
if (type.textType !== undefined) {
|
|
442
1000
|
const text = value["#text"];
|
|
443
1001
|
if (typeof text === "string") {
|
|
444
|
-
|
|
1002
|
+
const problems = [
|
|
1003
|
+
...checkSimple(catalog, type.textType, text),
|
|
1004
|
+
...(fixed === undefined ? [] : checkFixed(catalog, type.textType, text, fixed, true))
|
|
1005
|
+
];
|
|
1006
|
+
for (const problem of problems)
|
|
445
1007
|
issues.push({ path: join(path, "#text"), detail: problem });
|
|
446
1008
|
}
|
|
447
1009
|
}
|
|
448
|
-
|
|
449
|
-
// one its required fields are required; a required choice needs one.
|
|
450
|
-
const branches = new Map();
|
|
451
|
-
for (const field of type.fields) {
|
|
452
|
-
if (field.choice === undefined)
|
|
453
|
-
continue;
|
|
454
|
-
const byBranch = branches.get(field.choice) ?? new Map();
|
|
455
|
-
byBranch.set(field.branch, [...(byBranch.get(field.branch) ?? []), field.name]);
|
|
456
|
-
branches.set(field.choice, byBranch);
|
|
457
|
-
}
|
|
458
|
-
const taken = new Map();
|
|
459
|
-
for (const field of type.fields) {
|
|
460
|
-
if (field.choice !== undefined && value[field.name] !== undefined) {
|
|
461
|
-
taken.set(field.choice, new Set([...(taken.get(field.choice) ?? []), field.branch]));
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const where = path === "" ? "(root)" : path;
|
|
465
|
-
const label = (choice, branch) => (branches.get(choice)?.get(branch) ?? []).join(" + ");
|
|
466
|
-
for (const [choice, present] of taken) {
|
|
467
|
-
if (present.size > 1) {
|
|
468
|
-
issues.push({
|
|
469
|
-
path: where,
|
|
470
|
-
detail: `choose one of ${[...present].map((branch) => label(choice, branch)).join(", ")}`
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
for (const [choice, byBranch] of branches) {
|
|
475
|
-
if ((taken.get(choice)?.size ?? 0) > 0)
|
|
476
|
-
continue;
|
|
477
|
-
const required = type.fields.some((field) => field.choice === choice && field.minOccurs > 0);
|
|
478
|
-
if (required) {
|
|
479
|
-
issues.push({
|
|
480
|
-
path: where,
|
|
481
|
-
detail: `one of ${[...byBranch.keys()].map((branch) => label(choice, branch)).join(", ")} is required`
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
}
|
|
1010
|
+
issues.push(...checkContent(type, value, path));
|
|
485
1011
|
for (const field of type.fields) {
|
|
486
1012
|
const item = value[field.name];
|
|
487
|
-
|
|
488
|
-
if (item === undefined) {
|
|
489
|
-
const inTakenBranch = field.choice !== undefined && (taken.get(field.choice)?.has(field.branch) ?? false);
|
|
490
|
-
if (field.minOccurs > 0 && (field.choice === undefined || inTakenBranch)) {
|
|
491
|
-
issues.push({ path: fieldPath, detail: "required field is missing" });
|
|
492
|
-
}
|
|
1013
|
+
if (item === undefined)
|
|
493
1014
|
continue;
|
|
494
|
-
|
|
1015
|
+
const fieldPath = join(path, field.name);
|
|
495
1016
|
const items = isYamlList(item) ? item : [item];
|
|
496
|
-
const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
|
|
497
|
-
if (isYamlList(item) && !repeated) {
|
|
498
|
-
issues.push({ path: fieldPath, detail: "occurs at most once; write a single value" });
|
|
499
|
-
}
|
|
500
|
-
if (items.length < field.minOccurs)
|
|
501
|
-
issues.push({ path: fieldPath, detail: `at least ${field.minOccurs} occurrences` });
|
|
502
|
-
if (field.maxOccurs !== "unbounded" && items.length > field.maxOccurs) {
|
|
503
|
-
issues.push({ path: fieldPath, detail: `at most ${field.maxOccurs} occurrences` });
|
|
504
|
-
}
|
|
505
1017
|
items.forEach((entry, position) => {
|
|
506
|
-
validateValue(catalog, field.type, field.nillable, entry, isYamlList(item) ? `${fieldPath}[${position}]` : fieldPath, issues);
|
|
1018
|
+
validateValue(catalog, field.type, field.nillable, entry, isYamlList(item) ? `${fieldPath}[${position}]` : fieldPath, issues, field.fixed);
|
|
507
1019
|
});
|
|
508
1020
|
}
|
|
509
1021
|
};
|
|
510
|
-
const validateValue = (catalog, typeName, nillable, value, path, issues) => {
|
|
1022
|
+
const validateValue = (catalog, typeName, nillable, value, path, issues, fixed) => {
|
|
511
1023
|
if (value === null) {
|
|
512
1024
|
if (!nillable)
|
|
513
1025
|
issues.push({ path, detail: "is not nillable; give a value or remove the field" });
|
|
@@ -516,31 +1028,87 @@ const validateValue = (catalog, typeName, nillable, value, path, issues) => {
|
|
|
516
1028
|
const type = typeByName(catalog, typeName);
|
|
517
1029
|
if (type instanceof ComplexTypeDef) {
|
|
518
1030
|
if (typeof value === "string" && type.textType !== undefined) {
|
|
519
|
-
validateComplex(catalog, type, { "#text": value }, path, issues);
|
|
1031
|
+
validateComplex(catalog, type, { "#text": value }, path, issues, fixed);
|
|
520
1032
|
}
|
|
521
1033
|
else if (!isYamlMap(value)) {
|
|
522
1034
|
issues.push({ path, detail: `expects fields of ${localName(type.name).replace(/^~/, "")}` });
|
|
523
1035
|
}
|
|
524
1036
|
else {
|
|
525
|
-
validateComplex(catalog, type, value, path, issues);
|
|
1037
|
+
validateComplex(catalog, type, value, path, issues, fixed);
|
|
526
1038
|
}
|
|
527
1039
|
return;
|
|
528
1040
|
}
|
|
529
|
-
|
|
1041
|
+
// A simple value may also be written as `"#text": value` (a simple-typed root).
|
|
1042
|
+
let text = value;
|
|
1043
|
+
if (isYamlMap(value) && typeof value["#text"] === "string") {
|
|
1044
|
+
for (const key of Object.keys(value)) {
|
|
1045
|
+
if (key !== "#text")
|
|
1046
|
+
issues.push({ path: join(path, key), detail: "not a field of a simple value" });
|
|
1047
|
+
}
|
|
1048
|
+
text = value["#text"];
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof text !== "string") {
|
|
530
1051
|
issues.push({ path, detail: "expects a single value" });
|
|
531
1052
|
return;
|
|
532
1053
|
}
|
|
533
|
-
|
|
1054
|
+
const problems = [
|
|
1055
|
+
...checkSimple(catalog, typeName, text),
|
|
1056
|
+
...(fixed === undefined ? [] : checkFixed(catalog, typeName, text, fixed, true))
|
|
1057
|
+
];
|
|
1058
|
+
for (const problem of problems)
|
|
534
1059
|
issues.push({ path, detail: problem });
|
|
535
1060
|
};
|
|
536
1061
|
/** Validate a value tree against a global element's type. */
|
|
537
1062
|
export const validateInstance = (catalog, element, value) => {
|
|
538
1063
|
const issues = [];
|
|
539
|
-
validateValue(catalog, element.type, element.nillable, value, "", issues);
|
|
1064
|
+
validateValue(catalog, element.type, element.nillable, value, "", issues, element.fixed);
|
|
540
1065
|
return issues.map((issue) => (issue.path === "" ? { ...issue, path: "(root)" } : issue));
|
|
541
1066
|
};
|
|
542
1067
|
// ---------------------------------------------------------------------------
|
|
543
1068
|
// Value → XML
|
|
1069
|
+
/**
|
|
1070
|
+
* The children of a value map in wire order: the content model is walked
|
|
1071
|
+
* and each occurrence takes the next entries, so a repeating sequence is
|
|
1072
|
+
* written as interleaved groups and a repeating choice as its alternatives.
|
|
1073
|
+
* Entries the model leaves over (an invalid value) follow in field order.
|
|
1074
|
+
*/
|
|
1075
|
+
const wireOrder = (type, value) => {
|
|
1076
|
+
const queues = new Map();
|
|
1077
|
+
for (const field of type.fields) {
|
|
1078
|
+
const item = value[field.name];
|
|
1079
|
+
if (item !== undefined && !queues.has(field.name)) {
|
|
1080
|
+
queues.set(field.name, isYamlList(item) ? [...item] : [item]);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const out = [];
|
|
1084
|
+
const remaining = (particle) => particleFields(particle).some((field) => (queues.get(field.name)?.length ?? 0) > 0);
|
|
1085
|
+
const take = (field) => {
|
|
1086
|
+
const queue = queues.get(field.name);
|
|
1087
|
+
if (queue !== undefined && queue.length > 0)
|
|
1088
|
+
out.push({ field, entry: queue.shift() ?? null });
|
|
1089
|
+
};
|
|
1090
|
+
const occurrence = (particle) => {
|
|
1091
|
+
const limit = unbounded(particle.max);
|
|
1092
|
+
for (let count = 0; count < limit && remaining(particle); count++) {
|
|
1093
|
+
if (particle.kind === "element")
|
|
1094
|
+
take(particle.field);
|
|
1095
|
+
else if (particle.kind === "choice") {
|
|
1096
|
+
const branch = particle.children.find(remaining);
|
|
1097
|
+
if (branch !== undefined)
|
|
1098
|
+
occurrence(branch);
|
|
1099
|
+
}
|
|
1100
|
+
else
|
|
1101
|
+
particle.children.forEach(occurrence);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
occurrence(contentModel(type));
|
|
1105
|
+
for (const field of type.fields) {
|
|
1106
|
+
const queue = queues.get(field.name) ?? [];
|
|
1107
|
+
while (queue.length > 0)
|
|
1108
|
+
take(field);
|
|
1109
|
+
}
|
|
1110
|
+
return out;
|
|
1111
|
+
};
|
|
544
1112
|
/** Serialize an instance of `element` with prefixes `ns1`, `ns2`… declared on the root. */
|
|
545
1113
|
export const instanceToXml = (catalog, element, value) => {
|
|
546
1114
|
const prefixes = new Map();
|
|
@@ -566,26 +1134,27 @@ export const instanceToXml = (catalog, element, value) => {
|
|
|
566
1134
|
}
|
|
567
1135
|
const type = typeByName(catalog, typeName);
|
|
568
1136
|
if (!(type instanceof ComplexTypeDef)) {
|
|
569
|
-
|
|
1137
|
+
const text = typeof item === "string"
|
|
1138
|
+
? item
|
|
1139
|
+
: isYamlMap(item) && typeof item["#text"] === "string"
|
|
1140
|
+
? item["#text"]
|
|
1141
|
+
: "";
|
|
1142
|
+
return `<${name}>${escapeText(text)}</${name}>`;
|
|
570
1143
|
}
|
|
571
1144
|
const map = typeof item === "string" ? { "#text": item } : isYamlMap(item) ? item : {};
|
|
572
1145
|
const attributes = type.attributes
|
|
573
1146
|
.flatMap((attribute) => {
|
|
574
1147
|
const attributeValue = map[`@${attribute.name}`];
|
|
575
1148
|
return typeof attributeValue === "string"
|
|
576
|
-
? [
|
|
1149
|
+
? [
|
|
1150
|
+
` ${qualified(attribute.namespace ?? "", attribute.name)}="${escapeAttribute(attributeValue)}"`
|
|
1151
|
+
]
|
|
577
1152
|
: [];
|
|
578
1153
|
})
|
|
579
1154
|
.join("");
|
|
580
1155
|
const text = typeof map["#text"] === "string" ? escapeText(map["#text"]) : "";
|
|
581
|
-
const children = type
|
|
582
|
-
.
|
|
583
|
-
const child = map[field.name];
|
|
584
|
-
if (child === undefined)
|
|
585
|
-
return [];
|
|
586
|
-
const entries = isYamlList(child) ? child : [child];
|
|
587
|
-
return entries.map((entry) => write(qualified(field.namespace, field.name), field.type, entry));
|
|
588
|
-
})
|
|
1156
|
+
const children = wireOrder(type, map)
|
|
1157
|
+
.map(({ field, entry }) => write(qualified(field.namespace, field.name), field.type, entry))
|
|
589
1158
|
.join("");
|
|
590
1159
|
const content = `${text}${children}`;
|
|
591
1160
|
return content === "" ? `<${name}${attributes}/>` : `<${name}${attributes}>${content}</${name}>`;
|
|
@@ -601,10 +1170,77 @@ export const instanceToXml = (catalog, element, value) => {
|
|
|
601
1170
|
};
|
|
602
1171
|
// ---------------------------------------------------------------------------
|
|
603
1172
|
// XML → value
|
|
1173
|
+
/** Whether a sequence of child names can be read in order by the content model. */
|
|
1174
|
+
const followsModel = (model, names) => {
|
|
1175
|
+
// Every position reachable after one occurrence of a particle from `from`.
|
|
1176
|
+
const ends = (particle, from) => {
|
|
1177
|
+
const result = new Set();
|
|
1178
|
+
if (particle.min === 0)
|
|
1179
|
+
result.add(from);
|
|
1180
|
+
const seen = new Set();
|
|
1181
|
+
let frontier = new Set([from]);
|
|
1182
|
+
const limit = unbounded(particle.max);
|
|
1183
|
+
for (let count = 1; count <= limit && frontier.size > 0; count++) {
|
|
1184
|
+
const next = new Set();
|
|
1185
|
+
for (const position of frontier)
|
|
1186
|
+
for (const end of termEnds(particle, position))
|
|
1187
|
+
next.add(end);
|
|
1188
|
+
if (count < particle.min) {
|
|
1189
|
+
frontier = next;
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
for (const end of next)
|
|
1193
|
+
result.add(end);
|
|
1194
|
+
frontier = new Set([...next].filter((end) => !seen.has(end)));
|
|
1195
|
+
for (const end of next)
|
|
1196
|
+
seen.add(end);
|
|
1197
|
+
}
|
|
1198
|
+
return result;
|
|
1199
|
+
};
|
|
1200
|
+
const termEnds = (particle, from) => {
|
|
1201
|
+
if (particle.kind === "element") {
|
|
1202
|
+
return names[from] === particle.field.name ? new Set([from + 1]) : new Set();
|
|
1203
|
+
}
|
|
1204
|
+
if (particle.kind === "choice") {
|
|
1205
|
+
return new Set(particle.children.flatMap((child) => [...ends(child, from)]));
|
|
1206
|
+
}
|
|
1207
|
+
if (particle.kind === "sequence") {
|
|
1208
|
+
let positions = new Set([from]);
|
|
1209
|
+
for (const child of particle.children) {
|
|
1210
|
+
positions = new Set([...positions].flatMap((position) => [...ends(child, position)]));
|
|
1211
|
+
}
|
|
1212
|
+
return positions;
|
|
1213
|
+
}
|
|
1214
|
+
// xs:all: every child at most once, in any order.
|
|
1215
|
+
const result = new Set();
|
|
1216
|
+
const visited = new Set();
|
|
1217
|
+
const explore = (position, used) => {
|
|
1218
|
+
const key = `${position}:${[...used].join(",")}`;
|
|
1219
|
+
if (visited.has(key))
|
|
1220
|
+
return;
|
|
1221
|
+
visited.add(key);
|
|
1222
|
+
const complete = particle.children.every((child, index) => used.has(index) || emptiable(child));
|
|
1223
|
+
if (complete)
|
|
1224
|
+
result.add(position);
|
|
1225
|
+
particle.children.forEach((child, index) => {
|
|
1226
|
+
if (used.has(index))
|
|
1227
|
+
return;
|
|
1228
|
+
for (const end of ends(child, position)) {
|
|
1229
|
+
if (end !== position)
|
|
1230
|
+
explore(end, new Set([...used, index]));
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
};
|
|
1234
|
+
explore(from, new Set());
|
|
1235
|
+
return result;
|
|
1236
|
+
};
|
|
1237
|
+
return ends(model, 0).has(names.length);
|
|
1238
|
+
};
|
|
604
1239
|
/**
|
|
605
1240
|
* Read an XML element as an instance of `element`'s type. Unknown children,
|
|
606
|
-
* wrong namespaces,
|
|
607
|
-
* as found so a response can be
|
|
1241
|
+
* wrong namespaces, repeated singletons, and children out of the schema's
|
|
1242
|
+
* order become issues; values are kept as found so a response can be
|
|
1243
|
+
* analysed even when it breaks its schema.
|
|
608
1244
|
*/
|
|
609
1245
|
export const instanceFromXml = (catalog, element, xml) => {
|
|
610
1246
|
const issues = [];
|
|
@@ -618,29 +1254,48 @@ export const instanceFromXml = (catalog, element, xml) => {
|
|
|
618
1254
|
const nil = (node) => node.attributes.some((attribute) => attribute.name.namespace === XsiNamespace &&
|
|
619
1255
|
attribute.name.local === "nil" &&
|
|
620
1256
|
(attribute.value === "true" || attribute.value === "1"));
|
|
1257
|
+
const noChildren = (node, path) => {
|
|
1258
|
+
const children = elements(node);
|
|
1259
|
+
if (children.length > 0) {
|
|
1260
|
+
issues.push({
|
|
1261
|
+
path: path === "" ? "(root)" : path,
|
|
1262
|
+
detail: `unexpected child element ${children.map((child) => child.name.local).join(", ")} in a simple value`
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
621
1266
|
const read = (node, typeName, path) => {
|
|
622
1267
|
if (nil(node))
|
|
623
1268
|
return null;
|
|
624
1269
|
const type = typeByName(catalog, typeName);
|
|
625
|
-
if (!(type instanceof ComplexTypeDef))
|
|
1270
|
+
if (!(type instanceof ComplexTypeDef)) {
|
|
1271
|
+
noChildren(node, path);
|
|
626
1272
|
return textOf(node);
|
|
1273
|
+
}
|
|
627
1274
|
const result = {};
|
|
628
1275
|
for (const attribute of node.attributes) {
|
|
629
1276
|
if (attribute.name.namespace === XsiNamespace)
|
|
630
1277
|
continue;
|
|
631
|
-
|
|
1278
|
+
const attributePath = join(path, `@${attribute.name.local}`);
|
|
1279
|
+
const declared = type.attributes.find((candidate) => candidate.name === attribute.name.local);
|
|
1280
|
+
if (declared === undefined) {
|
|
1281
|
+
issues.push({ path: attributePath, detail: "attribute not declared" });
|
|
1282
|
+
}
|
|
1283
|
+
else if ((declared.namespace ?? "") !== attribute.name.namespace) {
|
|
632
1284
|
issues.push({
|
|
633
|
-
path:
|
|
634
|
-
detail: "attribute
|
|
1285
|
+
path: attributePath,
|
|
1286
|
+
detail: `namespace ${attribute.name.namespace === "" ? "(none)" : attribute.name.namespace}, expected ${declared.namespace ?? "(none)"}`
|
|
635
1287
|
});
|
|
636
1288
|
}
|
|
637
1289
|
result[`@${attribute.name.local}`] = attribute.value;
|
|
638
1290
|
}
|
|
639
|
-
if (type.textType !== undefined)
|
|
1291
|
+
if (type.textType !== undefined) {
|
|
1292
|
+
noChildren(node, path);
|
|
640
1293
|
result["#text"] = textOf(node);
|
|
1294
|
+
}
|
|
641
1295
|
else if (textOf(node).trim() !== "")
|
|
642
1296
|
issues.push({ path: path === "" ? "(root)" : path, detail: "unexpected text content" });
|
|
643
|
-
|
|
1297
|
+
const order = [];
|
|
1298
|
+
for (const child of type.textType === undefined ? elements(node) : []) {
|
|
644
1299
|
const field = type.fields.find((candidate) => candidate.name === child.name.local);
|
|
645
1300
|
const childPath = join(path, child.name.local);
|
|
646
1301
|
if (field === undefined) {
|
|
@@ -648,6 +1303,7 @@ export const instanceFromXml = (catalog, element, xml) => {
|
|
|
648
1303
|
result[child.name.local] = textOf(child);
|
|
649
1304
|
continue;
|
|
650
1305
|
}
|
|
1306
|
+
order.push(field.name);
|
|
651
1307
|
if (child.name.namespace !== field.namespace) {
|
|
652
1308
|
issues.push({
|
|
653
1309
|
path: childPath,
|
|
@@ -668,6 +1324,14 @@ export const instanceFromXml = (catalog, element, xml) => {
|
|
|
668
1324
|
result[field.name] = childValue;
|
|
669
1325
|
}
|
|
670
1326
|
}
|
|
1327
|
+
// Order only matters once the counts fit: otherwise validation says why.
|
|
1328
|
+
const model = contentModel(type);
|
|
1329
|
+
if (!followsModel(model, order) && checkContent(type, result, path).length === 0) {
|
|
1330
|
+
issues.push({
|
|
1331
|
+
path: path === "" ? "(root)" : path,
|
|
1332
|
+
detail: `children ${order.join(", ")} are out of the schema's order ${[...new Set(particleFields(model).map((field) => field.name))].join(", ")}`
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
671
1335
|
return result;
|
|
672
1336
|
};
|
|
673
1337
|
const value = read(xml, element.type, "");
|