@kgdiff/formats 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/detect.d.ts +47 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +425 -0
- package/dist/detect.js.map +1 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/dist/iri.d.ts +13 -0
- package/dist/iri.d.ts.map +1 -0
- package/dist/iri.js +91 -0
- package/dist/iri.js.map +1 -0
- package/dist/json.d.ts +31 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +787 -0
- package/dist/json.js.map +1 -0
- package/dist/jsonld.d.ts +16 -0
- package/dist/jsonld.d.ts.map +1 -0
- package/dist/jsonld.js +1249 -0
- package/dist/jsonld.js.map +1 -0
- package/dist/obo.d.ts +25 -0
- package/dist/obo.d.ts.map +1 -0
- package/dist/obo.js +478 -0
- package/dist/obo.js.map +1 -0
- package/dist/prefixes.d.ts +49 -0
- package/dist/prefixes.d.ts.map +1 -0
- package/dist/prefixes.js +177 -0
- package/dist/prefixes.js.map +1 -0
- package/dist/rdf.d.ts +46 -0
- package/dist/rdf.d.ts.map +1 -0
- package/dist/rdf.js +1047 -0
- package/dist/rdf.js.map +1 -0
- package/dist/rdfxml.d.ts +8 -0
- package/dist/rdfxml.d.ts.map +1 -0
- package/dist/rdfxml.js +529 -0
- package/dist/rdfxml.js.map +1 -0
- package/dist/shared.d.ts +117 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +355 -0
- package/dist/shared.js.map +1 -0
- package/dist/xml.d.ts +50 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +508 -0
- package/dist/xml.js.map +1 -0
- package/package.json +55 -3
- package/src/detect.ts +435 -0
- package/src/index.ts +296 -0
- package/src/iri.ts +86 -0
- package/src/json.ts +889 -0
- package/src/jsonld.ts +1376 -0
- package/src/obo.ts +557 -0
- package/src/prefixes.ts +195 -0
- package/src/rdf.ts +1181 -0
- package/src/rdfxml.ts +633 -0
- package/src/shared.ts +408 -0
- package/src/xml.ts +550 -0
package/src/shared.ts
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by every parser: deterministic ordering, attribute bags, node / edge
|
|
3
|
+
* builders, hashing and error construction. Nothing here knows about a concrete format.
|
|
4
|
+
*/
|
|
5
|
+
import type { Attrs, AttrValue, EdgeInput, GraphMeta, NodeInput, Point } from '@kgdiff/core';
|
|
6
|
+
import { KgdiffError } from '@kgdiff/core';
|
|
7
|
+
import type { ParseWarning, ParseWarningCode } from './index.js';
|
|
8
|
+
|
|
9
|
+
/** UTF-16 code unit order: locale-independent and identical in every runtime. */
|
|
10
|
+
export function compareStrings(a: string, b: string): number {
|
|
11
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Sorted, de-duplicated copy of `values` (code unit order). */
|
|
15
|
+
function sortedUnique(values: Iterable<string>): string[] {
|
|
16
|
+
return [...new Set(values)].sort(compareStrings);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `true` for plain JSON objects (not arrays, not null). */
|
|
20
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sets an own, enumerable property. Plain assignment of `__proto__` would replace the
|
|
26
|
+
* prototype instead, silently dropping a legitimate data key (JSON allows it).
|
|
27
|
+
*/
|
|
28
|
+
export function setOwn<T>(target: Record<string, T>, key: string, value: T): void {
|
|
29
|
+
if (key === '__proto__') {
|
|
30
|
+
Object.defineProperty(target, key, {
|
|
31
|
+
value,
|
|
32
|
+
enumerable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
});
|
|
36
|
+
} else target[key] = value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Own property value, ignoring anything inherited from `Object.prototype` (`constructor`, …). */
|
|
40
|
+
export function getOwn<T>(source: Readonly<Record<string, T>>, key: string): T | undefined {
|
|
41
|
+
return Object.hasOwn(source, key) ? source[key] : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Attribute values ───────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** Deepest nesting of arrays / objects accepted in attribute values. */
|
|
47
|
+
export const MAX_NESTING = 1000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Converts a parsed JSON value into an `AttrValue`, sorting object keys recursively. Throws
|
|
51
|
+
* `KgdiffError('invalid-input')` for values nested deeper than `MAX_NESTING`.
|
|
52
|
+
*/
|
|
53
|
+
export function toAttrValue(value: unknown, depth = 0): AttrValue | undefined {
|
|
54
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
55
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;
|
|
56
|
+
if (typeof value !== 'object') return undefined;
|
|
57
|
+
if (depth >= MAX_NESTING) {
|
|
58
|
+
throw invalidInput(`Values nested deeper than ${MAX_NESTING} levels are not supported.`);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
const out: AttrValue[] = [];
|
|
62
|
+
for (const item of value) {
|
|
63
|
+
const converted = toAttrValue(item, depth + 1);
|
|
64
|
+
out.push(converted === undefined ? null : converted);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
const record = value as Record<string, unknown>;
|
|
69
|
+
const out: Record<string, AttrValue> = {};
|
|
70
|
+
for (const key of Object.keys(record).sort(compareStrings)) {
|
|
71
|
+
const converted = toAttrValue(record[key], depth + 1);
|
|
72
|
+
if (converted !== undefined) setOwn(out, key, converted);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Canonical JSON (object keys sorted) used to compare and de-duplicate attribute values. */
|
|
78
|
+
function canonicalJson(value: AttrValue): string {
|
|
79
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
80
|
+
if (value !== null && typeof value === 'object') {
|
|
81
|
+
const record = value as { readonly [key: string]: AttrValue };
|
|
82
|
+
const keys = Object.keys(record).sort(compareStrings);
|
|
83
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key] ?? null)}`).join(',')}}`;
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function valueRank(value: AttrValue): number {
|
|
89
|
+
if (value === null) return 0;
|
|
90
|
+
switch (typeof value) {
|
|
91
|
+
case 'boolean':
|
|
92
|
+
return 1;
|
|
93
|
+
case 'number':
|
|
94
|
+
return 2;
|
|
95
|
+
case 'string':
|
|
96
|
+
return 3;
|
|
97
|
+
default:
|
|
98
|
+
return Array.isArray(value) ? 4 : 5;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Total order on attribute values: null < booleans < numbers < strings < arrays < objects. */
|
|
103
|
+
function compareAttrValues(a: AttrValue, b: AttrValue): number {
|
|
104
|
+
const rank = valueRank(a) - valueRank(b);
|
|
105
|
+
if (rank !== 0) return rank;
|
|
106
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b;
|
|
107
|
+
if (typeof a === 'string' && typeof b === 'string') return compareStrings(a, b);
|
|
108
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') return Number(a) - Number(b);
|
|
109
|
+
return compareStrings(canonicalJson(a), canonicalJson(b));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Collects attribute values per key. `build` sorts keys, de-duplicates values and turns a
|
|
114
|
+
* single value into a scalar (multi-values become a sorted array), unless the key is listed
|
|
115
|
+
* as always-array.
|
|
116
|
+
*/
|
|
117
|
+
export class AttrBag {
|
|
118
|
+
private readonly values = new Map<string, AttrValue[]>();
|
|
119
|
+
|
|
120
|
+
add(key: string, value: AttrValue): void {
|
|
121
|
+
const list = this.values.get(key);
|
|
122
|
+
if (list) list.push(value);
|
|
123
|
+
else this.values.set(key, [value]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Replaces every value collected for `key`. */
|
|
127
|
+
set(key: string, value: AttrValue): void {
|
|
128
|
+
this.values.set(key, [value]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
has(key: string): boolean {
|
|
132
|
+
return this.values.has(key);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
build(alwaysArray?: ReadonlySet<string>): Attrs | undefined {
|
|
136
|
+
if (this.values.size === 0) return undefined;
|
|
137
|
+
const out: Record<string, AttrValue> = {};
|
|
138
|
+
for (const key of [...this.values.keys()].sort(compareStrings)) {
|
|
139
|
+
const unique = new Map<string, AttrValue>();
|
|
140
|
+
for (const value of this.values.get(key) ?? []) unique.set(canonicalJson(value), value);
|
|
141
|
+
const sorted = [...unique.values()].sort(compareAttrValues);
|
|
142
|
+
const [first] = sorted;
|
|
143
|
+
const value =
|
|
144
|
+
sorted.length === 1 && first !== undefined && !alwaysArray?.has(key) ? first : sorted;
|
|
145
|
+
setOwn(out, key, value);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Nodes and edges ────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
/** Mutable accumulator for one node; `build` emits a `NodeInput` without empty fields. */
|
|
154
|
+
export class NodeBuilder {
|
|
155
|
+
label: string | undefined;
|
|
156
|
+
readonly types = new Set<string>();
|
|
157
|
+
readonly aliases = new Set<string>();
|
|
158
|
+
readonly attrs = new AttrBag();
|
|
159
|
+
deprecated = false;
|
|
160
|
+
readonly replaces = new Set<string>();
|
|
161
|
+
readonly replacedBy = new Set<string>();
|
|
162
|
+
|
|
163
|
+
constructor(readonly id: string) {}
|
|
164
|
+
|
|
165
|
+
build(alwaysArray?: ReadonlySet<string>): NodeInput {
|
|
166
|
+
return makeNode({
|
|
167
|
+
id: this.id,
|
|
168
|
+
label: this.label,
|
|
169
|
+
types: this.types,
|
|
170
|
+
aliases: this.aliases,
|
|
171
|
+
attrs: this.attrs.build(alwaysArray),
|
|
172
|
+
deprecated: this.deprecated,
|
|
173
|
+
replaces: this.replaces,
|
|
174
|
+
replacedBy: this.replacedBy,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface NodeFields {
|
|
180
|
+
readonly id: string;
|
|
181
|
+
readonly label?: string | undefined;
|
|
182
|
+
readonly types?: Iterable<string>;
|
|
183
|
+
readonly aliases?: Iterable<string>;
|
|
184
|
+
readonly attrs?: Attrs | undefined;
|
|
185
|
+
readonly deprecated?: boolean;
|
|
186
|
+
readonly replaces?: Iterable<string>;
|
|
187
|
+
readonly replacedBy?: Iterable<string>;
|
|
188
|
+
readonly position?: Point | undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Builds a `NodeInput` with a fixed key order and no empty fields: one type becomes a string,
|
|
193
|
+
* several a sorted array; aliases are sorted and never repeat the label; `deprecated` only
|
|
194
|
+
* appears when true.
|
|
195
|
+
*/
|
|
196
|
+
export function makeNode(fields: NodeFields): NodeInput {
|
|
197
|
+
const types = sortedUnique(fields.types ?? []);
|
|
198
|
+
const label = fields.label ?? fields.id;
|
|
199
|
+
const aliases = sortedUnique(fields.aliases ?? []).filter((alias) => alias !== label);
|
|
200
|
+
const replaces = sortedUnique(fields.replaces ?? []);
|
|
201
|
+
const replacedBy = sortedUnique(fields.replacedBy ?? []);
|
|
202
|
+
const attrs = fields.attrs && Object.keys(fields.attrs).length > 0 ? fields.attrs : undefined;
|
|
203
|
+
const [onlyType] = types;
|
|
204
|
+
return {
|
|
205
|
+
id: fields.id,
|
|
206
|
+
...(fields.label !== undefined && fields.label !== fields.id ? { label: fields.label } : {}),
|
|
207
|
+
...(types.length === 1 && onlyType !== undefined
|
|
208
|
+
? { type: onlyType }
|
|
209
|
+
: types.length > 1
|
|
210
|
+
? { type: types }
|
|
211
|
+
: {}),
|
|
212
|
+
...(aliases.length > 0 ? { aliases } : {}),
|
|
213
|
+
...(attrs ? { attrs } : {}),
|
|
214
|
+
...(fields.deprecated ? { deprecated: true } : {}),
|
|
215
|
+
...(replaces.length > 0 ? { replaces } : {}),
|
|
216
|
+
...(replacedBy.length > 0 ? { replacedBy } : {}),
|
|
217
|
+
...(fields.position ? { position: fields.position } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface EdgeFields {
|
|
222
|
+
readonly id?: string | undefined;
|
|
223
|
+
readonly source: string;
|
|
224
|
+
readonly target: string;
|
|
225
|
+
readonly predicate?: string | undefined;
|
|
226
|
+
readonly label?: string | undefined;
|
|
227
|
+
readonly attrs?: Attrs | undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Builds an `EdgeInput` with a fixed key order and no empty fields. */
|
|
231
|
+
export function makeEdge(fields: EdgeFields): EdgeInput {
|
|
232
|
+
const attrs = fields.attrs && Object.keys(fields.attrs).length > 0 ? fields.attrs : undefined;
|
|
233
|
+
return {
|
|
234
|
+
...(fields.id !== undefined ? { id: fields.id } : {}),
|
|
235
|
+
source: fields.source,
|
|
236
|
+
target: fields.target,
|
|
237
|
+
...(fields.predicate !== undefined && fields.predicate !== ''
|
|
238
|
+
? { predicate: fields.predicate }
|
|
239
|
+
: {}),
|
|
240
|
+
// An empty label is kept: it differs from the default (the predicate's readable form).
|
|
241
|
+
...(fields.label !== undefined ? { label: fields.label } : {}),
|
|
242
|
+
...(attrs ? { attrs } : {}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const nodeTieBreak = (node: NodeInput): string => JSON.stringify(node);
|
|
247
|
+
|
|
248
|
+
/** Nodes sorted by id (ties, i.e. duplicate ids, by their canonical JSON). */
|
|
249
|
+
export function sortNodes(nodes: NodeInput[]): NodeInput[] {
|
|
250
|
+
return nodes.sort(
|
|
251
|
+
(a, b) => compareStrings(a.id, b.id) || compareStrings(nodeTieBreak(a), nodeTieBreak(b)),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Edges sorted by (source, predicate, target, id, label, attrs). */
|
|
256
|
+
export function sortEdges(edges: EdgeInput[]): EdgeInput[] {
|
|
257
|
+
return edges.sort(
|
|
258
|
+
(a, b) =>
|
|
259
|
+
compareStrings(a.source, b.source) ||
|
|
260
|
+
compareStrings(a.predicate ?? '', b.predicate ?? '') ||
|
|
261
|
+
compareStrings(a.target, b.target) ||
|
|
262
|
+
compareStrings(a.id ?? '', b.id ?? '') ||
|
|
263
|
+
compareStrings(a.label ?? '', b.label ?? '') ||
|
|
264
|
+
compareStrings(a.attrs ? canonicalJson(a.attrs) : '', b.attrs ? canonicalJson(b.attrs) : ''),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Meta with keys sorted and empty values dropped; `undefined` when nothing is left. */
|
|
269
|
+
export function makeMeta(meta: Record<string, unknown>): GraphMeta | undefined {
|
|
270
|
+
const out: Record<string, unknown> = {};
|
|
271
|
+
for (const key of Object.keys(meta).sort(compareStrings)) {
|
|
272
|
+
const value = meta[key];
|
|
273
|
+
if (value !== undefined && value !== '') setOwn(out, key, value);
|
|
274
|
+
}
|
|
275
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── Hashing ────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 53-bit string hash (cyrb53), as 14 hex digits. Not cryptographic; used for content-addressed
|
|
282
|
+
* blank node ids where a collision would need ~10^8 distinct blank nodes to become likely.
|
|
283
|
+
*/
|
|
284
|
+
export function hash53(text: string): string {
|
|
285
|
+
let h1 = 0xdeadbeef;
|
|
286
|
+
let h2 = 0x41c6ce57;
|
|
287
|
+
for (let i = 0; i < text.length; i++) {
|
|
288
|
+
const code = text.charCodeAt(i);
|
|
289
|
+
h1 = Math.imul(h1 ^ code, 2654435761);
|
|
290
|
+
h2 = Math.imul(h2 ^ code, 1597334677);
|
|
291
|
+
}
|
|
292
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
293
|
+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
294
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
295
|
+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
296
|
+
const high = (h2 & 0x1fffff).toString(16).padStart(6, '0');
|
|
297
|
+
const low = (h1 >>> 0).toString(16).padStart(8, '0');
|
|
298
|
+
return high + low;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ─── Warnings and errors ────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
/** Collects warnings; counted warnings are reported once with their total. */
|
|
304
|
+
export class Warnings {
|
|
305
|
+
private readonly list: ParseWarning[] = [];
|
|
306
|
+
private readonly counts = new Map<
|
|
307
|
+
ParseWarningCode,
|
|
308
|
+
{ count: number; message: (n: number) => string }
|
|
309
|
+
>();
|
|
310
|
+
|
|
311
|
+
add(code: ParseWarningCode, message: string, line?: number): void {
|
|
312
|
+
this.list.push(line === undefined ? { code, message } : { code, message, line });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Counts an occurrence; `finish` emits one warning per code with `message(total)`. */
|
|
316
|
+
count(code: ParseWarningCode, message: (count: number) => string, by = 1): void {
|
|
317
|
+
const entry = this.counts.get(code);
|
|
318
|
+
if (entry) entry.count += by;
|
|
319
|
+
else this.counts.set(code, { count: by, message });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
finish(): ParseWarning[] {
|
|
323
|
+
const counted = [...this.counts.entries()]
|
|
324
|
+
.sort(([a], [b]) => compareStrings(a, b))
|
|
325
|
+
.map(([code, { count, message }]) => ({ code, message: message(count) }));
|
|
326
|
+
return [...this.list, ...counted];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const plural = (count: number, word: string, many = `${word}s`): string =>
|
|
331
|
+
`${count} ${count === 1 ? word : many}`;
|
|
332
|
+
|
|
333
|
+
/** A `KgdiffError('invalid-input')`; `line` (1-based) is appended to the message and details. */
|
|
334
|
+
export function invalidInput(
|
|
335
|
+
message: string,
|
|
336
|
+
details: Readonly<Record<string, unknown>> = {},
|
|
337
|
+
): KgdiffError {
|
|
338
|
+
return new KgdiffError('invalid-input', message, details);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 1-based line and column of a UTF-16 offset. */
|
|
342
|
+
export function lineAndColumn(text: string, offset: number): { line: number; column: number } {
|
|
343
|
+
let line = 1;
|
|
344
|
+
let lineStart = 0;
|
|
345
|
+
const end = Math.min(offset, text.length);
|
|
346
|
+
for (let i = 0; i < end; i++) {
|
|
347
|
+
if (text.charCodeAt(i) === 10) {
|
|
348
|
+
line++;
|
|
349
|
+
lineStart = i + 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { line, column: end - lineStart + 1 };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function stripBom(text: string): string {
|
|
356
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Exhaustiveness guard for `switch` statements. */
|
|
360
|
+
export function assertNever(value: never): never {
|
|
361
|
+
throw new KgdiffError('invalid-input', `Unexpected value: ${String(value)}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ─── Typed literals ─────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
const XSD_INTEGERS = new Set([
|
|
367
|
+
'integer',
|
|
368
|
+
'int',
|
|
369
|
+
'long',
|
|
370
|
+
'short',
|
|
371
|
+
'byte',
|
|
372
|
+
'nonNegativeInteger',
|
|
373
|
+
'positiveInteger',
|
|
374
|
+
'negativeInteger',
|
|
375
|
+
'nonPositiveInteger',
|
|
376
|
+
'unsignedLong',
|
|
377
|
+
'unsignedInt',
|
|
378
|
+
'unsignedShort',
|
|
379
|
+
'unsignedByte',
|
|
380
|
+
]);
|
|
381
|
+
const XSD_DECIMALS = new Set(['decimal', 'double', 'float']);
|
|
382
|
+
const INTEGER_LEXICAL = /^[+-]?\d+$/;
|
|
383
|
+
const DECIMAL_LEXICAL = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* JSON value of a literal with an XSD datatype (`xsdType` is the local name, e.g. `integer`):
|
|
387
|
+
* numeric types become numbers (integers only when safe), `boolean` becomes a boolean, and
|
|
388
|
+
* everything else — including lexical forms that do not parse — stays the lexical string.
|
|
389
|
+
*/
|
|
390
|
+
export function typedValue(lexical: string, xsdType: string | undefined): AttrValue {
|
|
391
|
+
if (xsdType === undefined) return lexical;
|
|
392
|
+
const trimmed = lexical.trim();
|
|
393
|
+
if (XSD_INTEGERS.has(xsdType)) {
|
|
394
|
+
if (!INTEGER_LEXICAL.test(trimmed)) return lexical;
|
|
395
|
+
const value = Number(trimmed);
|
|
396
|
+
return Number.isSafeInteger(value) ? value : lexical;
|
|
397
|
+
}
|
|
398
|
+
if (XSD_DECIMALS.has(xsdType)) {
|
|
399
|
+
if (!DECIMAL_LEXICAL.test(trimmed)) return lexical;
|
|
400
|
+
const value = Number(trimmed);
|
|
401
|
+
return Number.isFinite(value) ? value : lexical;
|
|
402
|
+
}
|
|
403
|
+
if (xsdType === 'boolean') {
|
|
404
|
+
if (trimmed === 'true' || trimmed === '1') return true;
|
|
405
|
+
if (trimmed === 'false' || trimmed === '0') return false;
|
|
406
|
+
}
|
|
407
|
+
return lexical;
|
|
408
|
+
}
|