@loaders.gl/graphs 5.0.0-alpha.9 → 5.0.0-beta.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.
Files changed (78) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +121 -4
  3. package/THIRD-PARTY-NOTICES.md +17 -0
  4. package/dist/dot-loader-types.d.ts +25 -8
  5. package/dist/dot-loader-types.d.ts.map +1 -1
  6. package/dist/dot-loader-types.js +2 -2
  7. package/dist/dot-loader-types.js.map +1 -1
  8. package/dist/dot-loader.cjs +638 -47
  9. package/dist/dot-loader.cjs.map +3 -3
  10. package/dist/dot-loader.d.ts +10 -6
  11. package/dist/dot-loader.d.ts.map +1 -1
  12. package/dist/dot-loader.js +3 -3
  13. package/dist/dot-loader.js.map +1 -1
  14. package/dist/dot-types.d.ts +6 -1
  15. package/dist/dot-types.d.ts.map +1 -1
  16. package/dist/gexf-loader-types.d.ts +53 -0
  17. package/dist/gexf-loader-types.d.ts.map +1 -0
  18. package/dist/gexf-loader-types.js +26 -0
  19. package/dist/gexf-loader-types.js.map +1 -0
  20. package/dist/gexf-loader.cjs +1304 -0
  21. package/dist/gexf-loader.cjs.map +6 -0
  22. package/dist/gexf-loader.d.ts +24 -0
  23. package/dist/gexf-loader.d.ts.map +1 -0
  24. package/dist/gexf-loader.js +13 -0
  25. package/dist/gexf-loader.js.map +1 -0
  26. package/dist/gexf-types.d.ts +16 -0
  27. package/dist/gexf-types.d.ts.map +1 -0
  28. package/dist/gexf-types.js +5 -0
  29. package/dist/gexf-types.js.map +1 -0
  30. package/dist/graph-types.d.ts +28 -0
  31. package/dist/graph-types.d.ts.map +1 -1
  32. package/dist/graphml-loader-types.d.ts +24 -8
  33. package/dist/graphml-loader-types.d.ts.map +1 -1
  34. package/dist/graphml-loader-types.js +2 -2
  35. package/dist/graphml-loader-types.js.map +1 -1
  36. package/dist/graphml-loader.cjs +670 -60
  37. package/dist/graphml-loader.cjs.map +3 -3
  38. package/dist/graphml-loader.d.ts +10 -6
  39. package/dist/graphml-loader.d.ts.map +1 -1
  40. package/dist/graphml-loader.js +3 -3
  41. package/dist/graphml-loader.js.map +1 -1
  42. package/dist/index.cjs +927 -59
  43. package/dist/index.cjs.map +3 -3
  44. package/dist/index.d.ts +7 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/build-graph-tables.d.ts +4 -0
  49. package/dist/lib/build-graph-tables.d.ts.map +1 -0
  50. package/dist/lib/build-graph-tables.js +143 -0
  51. package/dist/lib/build-graph-tables.js.map +1 -0
  52. package/dist/lib/parse-dot.d.ts +3 -1
  53. package/dist/lib/parse-dot.d.ts.map +1 -1
  54. package/dist/lib/parse-dot.js +38 -21
  55. package/dist/lib/parse-dot.js.map +1 -1
  56. package/dist/lib/parse-gexf.d.ts +5 -0
  57. package/dist/lib/parse-gexf.d.ts.map +1 -0
  58. package/dist/lib/parse-gexf.js +380 -0
  59. package/dist/lib/parse-gexf.js.map +1 -0
  60. package/dist/lib/parse-graphml.d.ts +2 -1
  61. package/dist/lib/parse-graphml.d.ts.map +1 -1
  62. package/dist/lib/parse-graphml.js +50 -27
  63. package/dist/lib/parse-graphml.js.map +1 -1
  64. package/package.json +13 -5
  65. package/src/dot-loader-types.ts +15 -5
  66. package/src/dot-loader.ts +7 -4
  67. package/src/dot-types.ts +7 -1
  68. package/src/gexf-loader-types.ts +41 -0
  69. package/src/gexf-loader.ts +17 -0
  70. package/src/gexf-types.ts +21 -0
  71. package/src/graph-types.ts +35 -0
  72. package/src/graphml-loader-types.ts +14 -5
  73. package/src/graphml-loader.ts +7 -4
  74. package/src/index.ts +9 -0
  75. package/src/lib/build-graph-tables.ts +208 -0
  76. package/src/lib/parse-dot.ts +44 -21
  77. package/src/lib/parse-gexf.ts +388 -0
  78. package/src/lib/parse-graphml.ts +86 -34
@@ -0,0 +1,388 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {XMLParser, XMLValidator} from 'fast-xml-parser';
6
+ import type {GraphNode, GraphEdge, GraphShape, GraphAttributeType} from '../graph-types';
7
+ import type {GEXFOutput} from '../gexf-types';
8
+ import {buildGraphTables} from './build-graph-tables';
9
+
10
+ /** XML element and attribute representation. */
11
+ type XMLElement = Record<string, unknown>;
12
+
13
+ /** A scoped application attribute declaration. */
14
+ type AttributeDefinition = {
15
+ /** Application attribute title. */
16
+ name: string;
17
+ /** Arrow-compatible scalar or list type. */
18
+ type: GraphAttributeType;
19
+ /** Default value, when supplied. */
20
+ defaultValue?: unknown;
21
+ };
22
+
23
+ const parser = new XMLParser({
24
+ ignoreAttributes: false,
25
+ parseAttributeValue: false,
26
+ parseTagValue: false,
27
+ trimValues: false
28
+ });
29
+ const GEXF_NAMESPACE = /^https?:\/\/(?:www\.)?gexf.net\/1\.(?:2(?:draft)?|3)(\/viz)?$/;
30
+ const TEMPORAL_ATTRIBUTES = [
31
+ 'start',
32
+ 'end',
33
+ 'startopen',
34
+ 'endopen',
35
+ 'timestamp',
36
+ 'timestamps',
37
+ 'intervals'
38
+ ];
39
+
40
+ /** Parses a static GEXF document directly into the selected graph representation. */
41
+ export function parseGEXF(
42
+ input: string | ArrayBuffer | Uint8Array,
43
+ shape: GraphShape = 'arrow-table'
44
+ ): GEXFOutput {
45
+ const text = typeof input === 'string' ? input : new TextDecoder().decode(input);
46
+ const validation = XMLValidator.validate(text);
47
+ if (validation !== true) throw new Error(`Invalid GEXF XML: ${validation.err.msg}`);
48
+ // External and custom entity declarations are outside the supported format subset.
49
+ if (/<!DOCTYPE\s/i.test(text)) throw new Error('GEXF document types are unsupported.');
50
+ const document = normalizeNamespaces(parser.parse(text) as XMLElement, {});
51
+ const root = getElement(document.gexf, 'gexf');
52
+ const version = root['@_version'] as string | undefined;
53
+ if (version && !['1.2', '1.2draft', '1.3'].includes(version)) {
54
+ throw new Error(`Unsupported GEXF version: ${version}`);
55
+ }
56
+ const graph = getElement(root.graph, 'graph');
57
+ const directed = parseDirection(graph['@_defaultedgetype'] ?? 'undirected');
58
+ const definitions = {
59
+ nodes: new Map<string, AttributeDefinition>(),
60
+ edges: new Map<string, AttributeDefinition>()
61
+ };
62
+ for (const attributes of getElements(graph.attributes)) {
63
+ const domain = attributes['@_class'];
64
+ if (domain !== 'node' && domain !== 'edge')
65
+ throw new Error('GEXF attributes require a node or edge class.');
66
+ const scoped = domain === 'node' ? definitions.nodes : definitions.edges;
67
+ const names = new Set(Array.from(scoped.values(), definition => definition.name));
68
+ for (const attribute of getElements(attributes.attribute)) {
69
+ const identifier = requireAttribute(attribute, 'id');
70
+ const name = requireAttribute(attribute, 'title');
71
+ if (name === 'gexf' || names.has(name) || scoped.has(identifier)) {
72
+ throw new Error(`Duplicate or reserved GEXF attribute: ${name}`);
73
+ }
74
+ names.add(name);
75
+ const type = parseAttributeType(requireAttribute(attribute, 'type'));
76
+ scoped.set(identifier, {
77
+ name,
78
+ type,
79
+ defaultValue:
80
+ attribute.default === undefined
81
+ ? undefined
82
+ : castValue(readTextContent(attribute.default), type)
83
+ });
84
+ }
85
+ }
86
+ const nodeElements = getElements(getContainer(graph.nodes).node);
87
+ const edgeElements = getElements(getContainer(graph.edges).edge);
88
+ const nodeIdentifiers = new Set<string>();
89
+ for (const node of nodeElements) {
90
+ const identifier = requireAttribute(node, 'id');
91
+ if (nodeIdentifiers.has(identifier)) throw new Error(`Duplicate GEXF node ID: ${identifier}`);
92
+ nodeIdentifiers.add(identifier);
93
+ }
94
+ const edgeIdentifiers = new Set(
95
+ edgeElements
96
+ .map(edge => edge['@_id'])
97
+ .filter((identifier): identifier is string => typeof identifier === 'string')
98
+ );
99
+ const seenEdges = new Set<string>();
100
+ /** Emits node records directly to the table builder. */
101
+ function* iterateNodes(): Iterable<GraphNode> {
102
+ for (const node of nodeElements) yield parseRecord(node, definitions.nodes);
103
+ }
104
+ /** Emits edge records without materializing a plain graph. */
105
+ function* iterateEdges(): Iterable<GraphEdge> {
106
+ for (const [index, edge] of edgeElements.entries()) {
107
+ const sourceId = requireAttribute(edge, 'source');
108
+ const targetId = requireAttribute(edge, 'target');
109
+ if (!nodeIdentifiers.has(sourceId) || !nodeIdentifiers.has(targetId)) {
110
+ throw new Error('GEXF edge references an undeclared node.');
111
+ }
112
+ let identifier = edge['@_id'] === undefined ? undefined : requireAttribute(edge, 'id');
113
+ if (identifier === undefined) {
114
+ identifier = `edge-${index}`;
115
+ while (edgeIdentifiers.has(identifier)) identifier = `_${identifier}`;
116
+ edgeIdentifiers.add(identifier);
117
+ }
118
+ if (seenEdges.has(identifier)) throw new Error(`Duplicate GEXF edge ID: ${identifier}`);
119
+ seenEdges.add(identifier);
120
+ yield {
121
+ ...parseRecord(edge, definitions.edges, identifier),
122
+ sourceId,
123
+ targetId,
124
+ directed: edge['@_type'] === undefined ? directed : parseDirection(edge['@_type'])
125
+ };
126
+ }
127
+ }
128
+ const metadataAttributes: Record<string, string> = Object.create(null);
129
+ const meta = getContainer(root.meta);
130
+ for (const name of ['creator', 'description', 'keywords']) {
131
+ if (meta[name] !== undefined) metadataAttributes[name] = readTextContent(meta[name]);
132
+ }
133
+ if (typeof meta['@_lastmodifieddate'] === 'string')
134
+ metadataAttributes.lastmodifieddate = meta['@_lastmodifieddate'];
135
+ const declarations = {
136
+ nodes: new Map(
137
+ Array.from(definitions.nodes.values(), definition => [definition.name, definition.type])
138
+ ),
139
+ edges: new Map(
140
+ Array.from(definitions.edges.values(), definition => [definition.name, definition.type])
141
+ )
142
+ };
143
+ return {
144
+ ...buildGraphTables(iterateNodes(), iterateEdges(), shape, declarations),
145
+ metadata: {version, directed, attributes: metadataAttributes}
146
+ };
147
+ }
148
+
149
+ /** Resolves inherited namespace bindings without treating foreign elements as GEXF. */
150
+ function normalizeNamespaces(element: XMLElement, inherited: Record<string, string>): XMLElement {
151
+ const namespaces: Record<string, string> = Object.assign(Object.create(null), inherited);
152
+ for (const [name, value] of Object.entries(element)) {
153
+ if (name === '@_xmlns') namespaces[''] = String(value);
154
+ else if (name.startsWith('@_xmlns:')) namespaces[name.slice(8)] = String(value);
155
+ }
156
+ const normalized: XMLElement = Object.create(null);
157
+ for (const [name, value] of Object.entries(element)) {
158
+ if (name.startsWith('@_') || name === '#text' || name.startsWith('?')) {
159
+ normalized[name] = value;
160
+ continue;
161
+ }
162
+ const separator = name.indexOf(':');
163
+ const prefix = separator < 0 ? '' : name.slice(0, separator);
164
+ const localName = name.slice(separator + 1);
165
+ // Namespace declarations on this child also scope the child's own name.
166
+ const children = Array.isArray(value) ? value : [value];
167
+ for (const child of children) {
168
+ const object = typeof child === 'object' && child !== null ? (child as XMLElement) : {};
169
+ const namespace = object[prefix ? `@_xmlns:${prefix}` : '@_xmlns'] ?? namespaces[prefix];
170
+ const match = typeof namespace === 'string' ? GEXF_NAMESPACE.exec(namespace) : null;
171
+ const key = match
172
+ ? `${match[1] ? 'viz:' : ''}${localName}`
173
+ : namespace || prefix
174
+ ? `foreign:${name}`
175
+ : localName;
176
+ const childValue =
177
+ typeof child === 'object' && child !== null
178
+ ? match || (!namespace && !prefix)
179
+ ? normalizeNamespaces(object, namespaces)
180
+ : object
181
+ : child;
182
+ if (normalized[key] === undefined) normalized[key] = childValue;
183
+ else
184
+ normalized[key] = [
185
+ ...(Array.isArray(normalized[key]) ? (normalized[key] as unknown[]) : [normalized[key]]),
186
+ childValue
187
+ ];
188
+ }
189
+ }
190
+ assertStaticElement(normalized);
191
+ return normalized;
192
+ }
193
+
194
+ /** Rejects temporal and hierarchical constructs that flat static tables cannot represent. */
195
+ function assertStaticElement(element: XMLElement): void {
196
+ if (
197
+ (element['@_mode'] !== undefined && element['@_mode'] !== 'static') ||
198
+ TEMPORAL_ATTRIBUTES.some(name => element[`@_${name}`] !== undefined) ||
199
+ element.spells !== undefined
200
+ ) {
201
+ throw new Error('Dynamic GEXF graphs and temporal values are unsupported.');
202
+ }
203
+ if (
204
+ element['@_pid'] !== undefined ||
205
+ element.parents !== undefined ||
206
+ (element['@_id'] !== undefined && (element.nodes !== undefined || element.edges !== undefined))
207
+ ) {
208
+ throw new Error('Hierarchical GEXF graphs are unsupported.');
209
+ }
210
+ }
211
+
212
+ /** Reads a singleton element, including an empty element. */
213
+ function getElement(value: unknown, name: string): XMLElement {
214
+ if (value === '') return {};
215
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value as XMLElement;
216
+ throw new Error(`GEXF requires one <${name}> element.`);
217
+ }
218
+
219
+ /** Reads scalar text from elements that also declare local namespace bindings. */
220
+ function readTextContent(value: unknown): string {
221
+ if (typeof value === 'string') return value;
222
+ const text = getElement(value, 'text')['#text'];
223
+ return typeof text === 'string' ? text : '';
224
+ }
225
+
226
+ /** Reads an optional container while rejecting repeated containers. */
227
+ function getContainer(value: unknown): XMLElement {
228
+ return value === undefined ? {} : getElement(value, 'container');
229
+ }
230
+
231
+ /** Reads repeated or singleton element records. */
232
+ function getElements(value: unknown): XMLElement[] {
233
+ if (value === undefined) return [];
234
+ return (Array.isArray(value) ? value : [value]).map(entry => getElement(entry, 'record'));
235
+ }
236
+
237
+ /** Reads a required XML attribute without changing identifier or label whitespace. */
238
+ function requireAttribute(element: XMLElement, name: string): string {
239
+ const value = element[`@_${name}`];
240
+ if (typeof value !== 'string' || value === '')
241
+ throw new Error(`GEXF requires a ${name} attribute.`);
242
+ return value;
243
+ }
244
+
245
+ /** Reads a direction while rejecting bidirectional mutual edges. */
246
+ function parseDirection(value: unknown): boolean {
247
+ if (value !== 'directed' && value !== 'undirected')
248
+ throw new Error(`Unsupported GEXF edge type: ${String(value)}`);
249
+ return value === 'directed';
250
+ }
251
+
252
+ /** Maps supported GEXF scalar and list types to graph column declarations. */
253
+ function parseAttributeType(value: string): GraphAttributeType {
254
+ if (value.startsWith('list') && !value.slice(4).startsWith('list')) {
255
+ return {list: parseAttributeType(value.slice(4))};
256
+ }
257
+ if (value === 'integer' || value === 'byte' || value === 'short') return 'int';
258
+ if (value === 'boolean' || value === 'long' || value === 'float' || value === 'double')
259
+ return value;
260
+ if (['string', 'anyURI', 'date', 'char', 'bigdecimal', 'biginteger'].includes(value))
261
+ return 'string';
262
+ throw new Error(`Unsupported GEXF attribute type: ${value}`);
263
+ }
264
+
265
+ /** Casts exact declared values; invalid scalar text survives Arrow promotion to Utf8. */
266
+ function castValue(text: string, type: GraphAttributeType): unknown {
267
+ if (typeof type === 'object') {
268
+ return parseListValues(text).map(value => castValue(value, type.list));
269
+ }
270
+ if (type === 'string') return text;
271
+ const trimmed = text.trim();
272
+ if (type === 'boolean') {
273
+ if (trimmed === 'true' || trimmed === '1') return true;
274
+ if (trimmed === 'false' || trimmed === '0') return false;
275
+ return text;
276
+ }
277
+ if (type === 'int' || type === 'long') {
278
+ if (!/^[+-]?\d+$/.test(trimmed)) return text;
279
+ return type === 'long'
280
+ ? BigInt(trimmed)
281
+ : Number.isSafeInteger(Number(trimmed))
282
+ ? Number(trimmed)
283
+ : text;
284
+ }
285
+ if (trimmed === 'INF') return Infinity;
286
+ if (trimmed === '-INF') return -Infinity;
287
+ if (trimmed === 'NaN') return NaN;
288
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(trimmed)) return text;
289
+ const number = Number(trimmed);
290
+ const represented = type === 'float' ? Math.fround(number) : number;
291
+ const nonzero = /[1-9]/.test(trimmed.split(/[eE]/)[0]);
292
+ return Number.isFinite(represented) && (represented !== 0 || !nonzero) ? number : text;
293
+ }
294
+
295
+ /** Reads bracketed and legacy lists, preserving quoted delimiters and string whitespace. */
296
+ function parseListValues(text: string): string[] {
297
+ const trimmed = text.trim();
298
+ const bracketed = trimmed.startsWith('[');
299
+ if (bracketed !== trimmed.endsWith(']')) throw new Error('Invalid GEXF list brackets.');
300
+ const contents = bracketed ? trimmed.slice(1, -1) : trimmed;
301
+ if (!contents.trim()) return [];
302
+ const values: string[] = [];
303
+ let value = '';
304
+ let quote = '';
305
+ let quoted = false;
306
+ let closed = false;
307
+ for (let index = 0; index < contents.length; index++) {
308
+ const character = contents[index];
309
+ if (quote) {
310
+ if (character === '\\' && (contents[index + 1] === quote || contents[index + 1] === '\\')) {
311
+ value += contents[++index];
312
+ } else if (character === quote) {
313
+ quote = '';
314
+ closed = true;
315
+ } else value += character;
316
+ } else if (character === ',' || (!bracketed && (character === '|' || character === ';'))) {
317
+ values.push(quoted ? value : value.trim());
318
+ value = '';
319
+ quoted = false;
320
+ closed = false;
321
+ } else if (closed) {
322
+ if (character.trim()) throw new Error('Invalid GEXF list text after a quote.');
323
+ } else if ((character === "'" || character === '"') && !value.trim()) {
324
+ quote = character;
325
+ quoted = true;
326
+ value = '';
327
+ } else value += character;
328
+ }
329
+ if (quote) throw new Error('Unterminated GEXF list quote.');
330
+ values.push(quoted ? value : value.trim());
331
+ return values;
332
+ }
333
+
334
+ /** Converts application attributes and native GEXF properties to a collision-free record. */
335
+ function parseRecord(
336
+ element: XMLElement,
337
+ definitions: Map<string, AttributeDefinition>,
338
+ identifier?: string
339
+ ): GraphNode {
340
+ const attributes: Record<string, unknown> = Object.create(null);
341
+ for (const definition of definitions.values()) {
342
+ if (definition.defaultValue !== undefined)
343
+ attributes[definition.name] = Array.isArray(definition.defaultValue)
344
+ ? [...definition.defaultValue]
345
+ : definition.defaultValue;
346
+ }
347
+ for (const attvalue of getElements(getContainer(element.attvalues).attvalue)) {
348
+ const key = requireAttribute(attvalue, 'for');
349
+ const definition = definitions.get(key);
350
+ if (!definition) throw new Error(`Undeclared GEXF attribute: ${key}`);
351
+ attributes[definition.name] = castValue(requireValue(attvalue), definition.type);
352
+ }
353
+ const native: Record<string, unknown> = Object.create(null);
354
+ if (element['@_weight'] !== undefined)
355
+ native.weight = castValue(String(element['@_weight']), 'double');
356
+ if (element['@_kind'] !== undefined) native.kind = element['@_kind'];
357
+ const visualization: Record<string, unknown> = Object.create(null);
358
+ for (const name of ['color', 'position', 'size', 'shape', 'thickness']) {
359
+ const value = element[`viz:${name}`];
360
+ if (value === undefined) continue;
361
+ const properties: Record<string, unknown> = Object.create(null);
362
+ for (const [key, property] of Object.entries(getElement(value, `viz:${name}`))) {
363
+ if (key.startsWith('@_') && !key.startsWith('@_xmlns')) {
364
+ const propertyName = key.slice(2);
365
+ properties[propertyName] =
366
+ ['r', 'g', 'b', 'a', 'x', 'y', 'z'].includes(propertyName) ||
367
+ (propertyName === 'value' && (name === 'size' || name === 'thickness'))
368
+ ? castValue(String(property), 'double')
369
+ : property;
370
+ }
371
+ }
372
+ visualization[name] = properties;
373
+ }
374
+ if (Object.keys(visualization).length) native.viz = visualization;
375
+ if (Object.keys(native).length) attributes.gexf = native;
376
+ return {
377
+ id: identifier ?? requireAttribute(element, 'id'),
378
+ label: element['@_label'] as string | undefined,
379
+ attributes
380
+ };
381
+ }
382
+
383
+ /** Reads an attribute value, allowing the empty string. */
384
+ function requireValue(element: XMLElement): string {
385
+ const value = element['@_value'];
386
+ if (typeof value !== 'string') throw new Error('GEXF attvalue requires a value attribute.');
387
+ return value;
388
+ }
@@ -4,7 +4,16 @@
4
4
 
5
5
  import {XMLParser} from 'fast-xml-parser';
6
6
 
7
- import type {GraphData, GraphEdge, GraphNode} from '../graph-types';
7
+ import type {
8
+ GraphData,
9
+ GraphEdge,
10
+ GraphNode,
11
+ GraphOutput,
12
+ GraphShape,
13
+ GraphAttributeSchemas,
14
+ GraphAttributeType
15
+ } from '../graph-types';
16
+ import {buildGraphTables} from './build-graph-tables';
8
17
 
9
18
  const XML_ATTRIBUTE_PREFIX = '@_';
10
19
  const XML_TEXT_KEY = '#text';
@@ -12,9 +21,6 @@ const XML_TEXT_KEY = '#text';
12
21
  /** Supported key scopes. */
13
22
  type GraphMLDomain = 'all' | 'node' | 'edge' | 'graph';
14
23
 
15
- /** Supported scalar attribute types. */
16
- type GraphMLAttributeType = 'boolean' | 'int' | 'long' | 'float' | 'double' | 'string';
17
-
18
24
  /** A declared GraphML key and its default. */
19
25
  type GraphMLKeyDefinition = {
20
26
  /** Key identifier. */
@@ -24,9 +30,11 @@ type GraphMLKeyDefinition = {
24
30
  /** Elements to which defaults apply. */
25
31
  domain: GraphMLDomain;
26
32
  /** Declared scalar type. */
27
- type: GraphMLAttributeType;
33
+ type: GraphAttributeType;
28
34
  /** Typed default value. */
29
35
  defaultValue?: unknown;
36
+ /** Whether long values retain exact integer precision. */
37
+ lossless: boolean;
30
38
  };
31
39
 
32
40
  /** XML element representation emitted by fast-xml-parser. */
@@ -39,7 +47,7 @@ const graphmlParser = new XMLParser({
39
47
  ignoreAttributes: false,
40
48
  attributeNamePrefix: XML_ATTRIBUTE_PREFIX,
41
49
  textNodeName: XML_TEXT_KEY,
42
- trimValues: true,
50
+ trimValues: false,
43
51
  parseAttributeValue: false,
44
52
  parseTagValue: false,
45
53
  removeNSPrefix: true
@@ -49,7 +57,12 @@ const graphmlParser = new XMLParser({
49
57
  export type GraphMLInput = string | ArrayBuffer | Uint8Array;
50
58
 
51
59
  /** Parses the first GraphML graph into framework-independent node and edge records. */
52
- export function parseGraphML(graphml: GraphMLInput): GraphData {
60
+ export function parseGraphML(graphml: GraphMLInput): GraphData;
61
+ export function parseGraphML(graphml: GraphMLInput, shape: GraphShape): GraphOutput;
62
+ export function parseGraphML(
63
+ graphml: GraphMLInput,
64
+ shape: GraphShape = 'plain-graph-data'
65
+ ): GraphOutput {
53
66
  const xmlText = decodeGraphML(graphml);
54
67
  const document = graphmlParser.parse(xmlText) as GraphMLObject;
55
68
  const graphmlRoot = getGraphMLRoot(document);
@@ -62,22 +75,50 @@ export function parseGraphML(graphml: GraphMLInput): GraphData {
62
75
  throw new Error('GraphML document does not contain a <graph> element.');
63
76
  }
64
77
 
65
- const keyDefinitions = collectKeyDefinitions(graphmlRoot, graphElement);
78
+ const keyDefinitions = collectKeyDefinitions(
79
+ graphmlRoot,
80
+ graphElement,
81
+ shape !== 'plain-graph-data'
82
+ );
66
83
  const defaultDirected = parseEdgeDefault(graphElement[`${XML_ATTRIBUTE_PREFIX}edgedefault`]);
67
84
 
68
- const nodes = normalizeArray(graphElement.node).map(node => parseNode(node, keyDefinitions));
69
- const edges = normalizeArray(graphElement.edge).map((edge, index) =>
70
- parseEdge(edge, index, keyDefinitions, defaultDirected)
85
+ const declarations: GraphAttributeSchemas = {nodes: new Map(), edges: new Map()};
86
+ for (const definition of keyDefinitions.values()) {
87
+ if (definition.domain === 'all' || definition.domain === 'node')
88
+ declarations.nodes.set(definition.name, definition.type);
89
+ if (definition.domain === 'all' || definition.domain === 'edge')
90
+ declarations.edges.set(definition.name, definition.type);
91
+ }
92
+ return buildGraphTables(
93
+ iterateNodes(graphElement.node, keyDefinitions),
94
+ iterateEdges(graphElement.edge, keyDefinitions, defaultDirected),
95
+ shape,
96
+ declarations
71
97
  );
98
+ }
72
99
 
73
- const filteredNodes = nodes.filter((node): node is GraphNode => Boolean(node));
74
- const filteredEdges = edges.filter((edge): edge is GraphEdge => Boolean(edge));
100
+ /** Emits valid nodes directly to the selected output builder. */
101
+ function* iterateNodes(
102
+ nodes: unknown,
103
+ definitions: Map<string, GraphMLKeyDefinition>
104
+ ): Iterable<GraphNode> {
105
+ for (const node of normalizeArray(nodes)) {
106
+ const record = parseNode(node, definitions);
107
+ if (record) yield record;
108
+ }
109
+ }
75
110
 
76
- return {
77
- shape: 'plain-graph-data',
78
- nodes: filteredNodes,
79
- edges: filteredEdges
80
- } satisfies GraphData;
111
+ /** Emits valid edges while preserving generated IDs and document order. */
112
+ function* iterateEdges(
113
+ edges: unknown,
114
+ definitions: Map<string, GraphMLKeyDefinition>,
115
+ directed: boolean
116
+ ): Iterable<GraphEdge> {
117
+ let index = 0;
118
+ for (const edge of normalizeArray(edges)) {
119
+ const record = parseEdge(edge, index++, definitions, directed);
120
+ if (record) yield record;
121
+ }
81
122
  }
82
123
 
83
124
  /** Decodes text or UTF-8 binary input. */
@@ -110,13 +151,18 @@ function getGraphMLRoot(document: GraphMLObject): GraphMLObject | null {
110
151
  /** Selects the first graph element. */
111
152
  function getGraphElement(graphmlRoot: GraphMLObject): GraphMLObject | null {
112
153
  const graph = graphmlRoot.graph;
154
+ if (typeof graph === 'string' && !graph.trim()) {
155
+ return {};
156
+ }
113
157
  if (!graph) {
114
158
  return null;
115
159
  }
116
160
 
117
161
  if (Array.isArray(graph)) {
118
- const firstGraph = graph.find(entry => isObject(entry));
119
- return isObject(firstGraph) ? firstGraph : null;
162
+ const firstGraph = graph.find(
163
+ entry => isObject(entry) || (typeof entry === 'string' && !entry.trim())
164
+ );
165
+ return isObject(firstGraph) ? firstGraph : typeof firstGraph === 'string' ? {} : null;
120
166
  }
121
167
 
122
168
  return isObject(graph) ? graph : null;
@@ -125,7 +171,8 @@ function getGraphElement(graphmlRoot: GraphMLObject): GraphMLObject | null {
125
171
  /** Collects scoped keys and typed defaults. */
126
172
  function collectKeyDefinitions(
127
173
  graphmlRoot: GraphMLObject,
128
- graphElement: GraphMLObject
174
+ graphElement: GraphMLObject,
175
+ lossless: boolean
129
176
  ): Map<string, GraphMLKeyDefinition> {
130
177
  const keys = new Map<string, GraphMLKeyDefinition>();
131
178
 
@@ -140,9 +187,10 @@ function collectKeyDefinitions(
140
187
  const name = String(candidate[`${XML_ATTRIBUTE_PREFIX}attr.name`] ?? id).trim();
141
188
  const type = normalizeType(candidate[`${XML_ATTRIBUTE_PREFIX}attr.type`]);
142
189
  const defaultNode = candidate.default ?? null;
143
- const defaultValue = defaultNode !== null ? castDataValue(defaultNode, type) : undefined;
190
+ const defaultValue =
191
+ defaultNode !== null ? castDataValue(defaultNode, type, lossless) : undefined;
144
192
 
145
- keys.set(id, {id, domain, name, type, defaultValue});
193
+ keys.set(id, {id, domain, name, type, defaultValue, lossless});
146
194
  }
147
195
  }
148
196
  }
@@ -260,14 +308,17 @@ function assignAttributeFromDataEntry(
260
308
 
261
309
  const definition = keyDefinitions.get(keyId);
262
310
  const attributeName = definition?.name ?? keyId;
263
- const value = castDataValue(entry, definition?.type ?? 'string');
311
+ const value =
312
+ definition?.type === 'boolean' && extractTextContent(entry) === undefined
313
+ ? false
314
+ : castDataValue(entry, definition?.type ?? 'string', definition?.lossless);
264
315
  if (value !== undefined) {
265
316
  attributes[attributeName] = value;
266
317
  }
267
318
  }
268
319
 
269
320
  /** Converts GraphML scalar types while preserving unparseable numeric text. */
270
- function castDataValue(value: unknown, type: GraphMLAttributeType): unknown {
321
+ function castDataValue(value: unknown, type: GraphAttributeType, lossless = false): unknown {
271
322
  if (value === null || typeof value === 'undefined') {
272
323
  return undefined;
273
324
  }
@@ -280,6 +331,9 @@ function castDataValue(value: unknown, type: GraphMLAttributeType): unknown {
280
331
  if (type === 'boolean') {
281
332
  return parseBoolean(text);
282
333
  }
334
+ if (type === 'long' && lossless) {
335
+ return /^[+-]?\d+$/.test(text.trim()) ? BigInt(text.trim()) : text;
336
+ }
283
337
  if (type === 'int' || type === 'long') {
284
338
  const parsed = Number.parseInt(text, 10);
285
339
  return Number.isNaN(parsed) ? text : parsed;
@@ -307,15 +361,14 @@ function extractTextContent(value: unknown): string | undefined {
307
361
 
308
362
  if (isObject(value)) {
309
363
  const text = value[XML_TEXT_KEY];
310
- if (typeof text === 'string') {
311
- return text;
312
- }
313
-
314
364
  const nonAttributeEntries = Object.entries(value).filter(
315
- ([key]) => !key.startsWith(XML_ATTRIBUTE_PREFIX)
365
+ ([key]) => !key.startsWith(XML_ATTRIBUTE_PREFIX) && key !== XML_TEXT_KEY
316
366
  );
317
367
  if (nonAttributeEntries.length === 0) {
318
- return undefined;
368
+ return typeof text === 'string' ? text : undefined;
369
+ }
370
+ if (typeof text === 'string' && text.trim()) {
371
+ nonAttributeEntries.push([XML_TEXT_KEY, text]);
319
372
  }
320
373
  return JSON.stringify(Object.fromEntries(nonAttributeEntries));
321
374
  }
@@ -350,7 +403,7 @@ function normalizeDomain(value: unknown): GraphMLDomain {
350
403
  }
351
404
 
352
405
  /** Normalizes supported GraphML scalar types. */
353
- function normalizeType(value: unknown): GraphMLAttributeType {
406
+ function normalizeType(value: unknown): GraphAttributeType {
354
407
  if (typeof value !== 'string') {
355
408
  return 'string';
356
409
  }
@@ -368,7 +421,6 @@ function normalizeType(value: unknown): GraphMLAttributeType {
368
421
  return 'string';
369
422
  }
370
423
 
371
- /** Converts an edge element with its effective direction. */
372
424
  /** Reads the graph-wide default edge direction. */
373
425
  function parseEdgeDefault(value: unknown): boolean {
374
426
  if (typeof value !== 'string') {
@@ -410,5 +462,5 @@ function parseBoolean(value: string): boolean {
410
462
  if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'n') {
411
463
  return false;
412
464
  }
413
- return Boolean(value);
465
+ return Boolean(normalized);
414
466
  }