@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
- return { builtin: isBuiltin(current) ? localName(current) : "string", facets, chain };
25
- };
26
- const integerTypes = new Set([
27
- "int",
28
- "integer",
29
- "long",
30
- "short",
31
- "byte",
32
- "nonNegativeInteger",
33
- "positiveInteger",
34
- "nonPositiveInteger",
35
- "negativeInteger",
36
- "unsignedInt",
37
- "unsignedLong",
38
- "unsignedShort",
39
- "unsignedByte"
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
- date: /^-?\d{4,}-\d{2}-\d{2}(Z|[+-]\d{2}:\d{2})?$/,
47
- dateTime: /^-?\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/,
48
- time: /^\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/,
49
- gYear: /^-?\d{4,}(Z|[+-]\d{2}:\d{2})?$/,
50
- base64Binary: /^[A-Za-z0-9+/\s]*=?=?\s*$/,
51
- hexBinary: /^([0-9A-Fa-f]{2})*$/
52
- };
53
- /** XSD patterns are implicitly anchored and mostly JS-compatible. */
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(`^(?:${pattern.replace(/\\i/g, "[A-Za-z_:]").replace(/\\c/g, "[A-Za-z0-9._:-]")})$`, "u");
336
+ return new RegExp(source, "u");
57
337
  }
58
338
  catch {
59
339
  return undefined;
60
340
  }
61
341
  };
62
- const digitsOf = (value) => {
63
- const unsigned = value.replace(/^[+-]/, "");
64
- const [whole = "", fraction = ""] = unsigned.split(".");
65
- const trimmedFraction = fraction.replace(/0+$/, "");
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
- total: whole.replace(/^0+(?=\d)/, "").length + trimmedFraction.length,
68
- fraction: trimmedFraction.length
347
+ negative: match[1] === "-",
348
+ whole: (match[2] ?? "").replace(/^0+/, ""),
349
+ fraction: (match[3] ?? "").replace(/0+$/, "")
69
350
  };
70
351
  };
71
- export const checkSimple = (catalog, type, value) => {
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
- if (integerTypes.has(builtin)) {
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.test(value))
441
+ if (shape !== undefined && !shape(value))
82
442
  problems.push(`"${value}" is not a ${builtin}`);
83
443
  }
84
- const numeric = integerTypes.has(builtin) || ["decimal", "float", "double"].includes(builtin);
444
+ const lexicallyValid = problems.length === 0;
85
445
  for (const facets of view.facets) {
86
- if (facets.enumeration !== undefined && !facets.enumeration.includes(value)) {
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 matches = facets.pattern.some((pattern) => xsdRegex(pattern)?.test(value) ?? true);
91
- if (!matches)
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 = [...value].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(`length ${length}, expected ${facets.length}`);
463
+ problems.push(`${unit} ${length}, expected ${facets.length}`);
97
464
  }
98
465
  if (facets.minLength !== undefined && length < facets.minLength) {
99
- problems.push(`length ${length} below ${facets.minLength}`);
466
+ problems.push(`${unit} ${length} below ${facets.minLength}`);
100
467
  }
101
468
  if (facets.maxLength !== undefined && length > facets.maxLength) {
102
- problems.push(`length ${length} above ${facets.maxLength}`);
469
+ problems.push(`${unit} ${length} above ${facets.maxLength}`);
103
470
  }
104
- if (numeric && /^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(value)) {
105
- const number = Number(value);
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
- if (facets.minInclusive !== undefined && number < Number(facets.minInclusive)) {
114
- problems.push(`${value} below ${facets.minInclusive}`);
115
- }
116
- if (facets.maxInclusive !== undefined && number > Number(facets.maxInclusive)) {
117
- problems.push(`${value} above ${facets.maxInclusive}`);
118
- }
119
- if (facets.minExclusive !== undefined && number <= Number(facets.minExclusive)) {
120
- problems.push(`${value} not above ${facets.minExclusive}`);
121
- }
122
- if (facets.maxExclusive !== undefined && number >= Number(facets.maxExclusive)) {
123
- problems.push(`${value} not below ${facets.maxExclusive}`);
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, then field meaning, then facets, then the builtin. */
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 firstBranch = new Map();
337
- for (const field of type.fields) {
338
- if (field.choice !== undefined && !firstBranch.has(field.choice))
339
- firstBranch.set(field.choice, field.branch);
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 = field.choice !== undefined && firstBranch.get(field.choice) !== field.branch;
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
- // Validation of values
804
+ // Content models over value maps
406
805
  const join = (path, segment) => path === "" ? segment : `${path}.${segment}`;
407
- const validateComplex = (catalog, type, value, path, issues) => {
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
- for (const problem of checkSimple(catalog, attribute.type, item)) {
437
- issues.push({ path: join(path, `@${attribute.name}`), detail: problem });
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
- for (const problem of checkSimple(catalog, type.textType, text))
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
- // Choices: at most one alternative (branch) present; inside the chosen
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
- const fieldPath = join(path, field.name);
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
- if (typeof value !== "string") {
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
- for (const problem of checkSimple(catalog, typeName, value))
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
- return `<${name}>${escapeText(typeof item === "string" ? item : "")}</${name}>`;
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
- ? [` ${attribute.name}="${escapeAttribute(attributeValue)}"`]
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.fields
582
- .flatMap((field) => {
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, and repeated singletons become issues; values are kept
607
- * as found so a response can be analysed even when it breaks its schema.
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
- if (!type.attributes.some((declared) => declared.name === attribute.name.local)) {
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: join(path, `@${attribute.name.local}`),
634
- detail: "attribute not declared"
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
- for (const child of elements(node)) {
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, "");