@loaders.gl/graphs 5.0.0-alpha.9

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 (53) hide show
  1. package/README.md +66 -0
  2. package/dist/dot-loader-types.d.ts +36 -0
  3. package/dist/dot-loader-types.d.ts.map +1 -0
  4. package/dist/dot-loader-types.js +26 -0
  5. package/dist/dot-loader-types.js.map +1 -0
  6. package/dist/dot-loader.cjs +1482 -0
  7. package/dist/dot-loader.cjs.map +6 -0
  8. package/dist/dot-loader.d.ts +20 -0
  9. package/dist/dot-loader.d.ts.map +1 -0
  10. package/dist/dot-loader.js +13 -0
  11. package/dist/dot-loader.js.map +1 -0
  12. package/dist/dot-types.d.ts +32 -0
  13. package/dist/dot-types.d.ts.map +1 -0
  14. package/dist/dot-types.js +5 -0
  15. package/dist/dot-types.js.map +1 -0
  16. package/dist/graph-types.d.ts +28 -0
  17. package/dist/graph-types.d.ts.map +1 -0
  18. package/dist/graph-types.js +5 -0
  19. package/dist/graph-types.js.map +1 -0
  20. package/dist/graphml-loader-types.d.ts +36 -0
  21. package/dist/graphml-loader-types.d.ts.map +1 -0
  22. package/dist/graphml-loader-types.js +26 -0
  23. package/dist/graphml-loader-types.js.map +1 -0
  24. package/dist/graphml-loader.cjs +650 -0
  25. package/dist/graphml-loader.cjs.map +6 -0
  26. package/dist/graphml-loader.d.ts +20 -0
  27. package/dist/graphml-loader.d.ts.map +1 -0
  28. package/dist/graphml-loader.js +13 -0
  29. package/dist/graphml-loader.js.map +1 -0
  30. package/dist/index.cjs +1136 -0
  31. package/dist/index.cjs.map +6 -0
  32. package/dist/index.d.ts +5 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +6 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/lib/parse-dot.d.ts +4 -0
  37. package/dist/lib/parse-dot.d.ts.map +1 -0
  38. package/dist/lib/parse-dot.js +724 -0
  39. package/dist/lib/parse-dot.js.map +1 -0
  40. package/dist/lib/parse-graphml.d.ts +6 -0
  41. package/dist/lib/parse-graphml.d.ts.map +1 -0
  42. package/dist/lib/parse-graphml.js +296 -0
  43. package/dist/lib/parse-graphml.js.map +1 -0
  44. package/package.json +58 -0
  45. package/src/dot-loader-types.ts +31 -0
  46. package/src/dot-loader.ts +14 -0
  47. package/src/dot-types.ts +38 -0
  48. package/src/graph-types.ts +33 -0
  49. package/src/graphml-loader-types.ts +31 -0
  50. package/src/graphml-loader.ts +14 -0
  51. package/src/index.ts +8 -0
  52. package/src/lib/parse-dot.ts +924 -0
  53. package/src/lib/parse-graphml.ts +414 -0
@@ -0,0 +1,414 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {XMLParser} from 'fast-xml-parser';
6
+
7
+ import type {GraphData, GraphEdge, GraphNode} from '../graph-types';
8
+
9
+ const XML_ATTRIBUTE_PREFIX = '@_';
10
+ const XML_TEXT_KEY = '#text';
11
+
12
+ /** Supported key scopes. */
13
+ type GraphMLDomain = 'all' | 'node' | 'edge' | 'graph';
14
+
15
+ /** Supported scalar attribute types. */
16
+ type GraphMLAttributeType = 'boolean' | 'int' | 'long' | 'float' | 'double' | 'string';
17
+
18
+ /** A declared GraphML key and its default. */
19
+ type GraphMLKeyDefinition = {
20
+ /** Key identifier. */
21
+ id: string;
22
+ /** Application attribute name. */
23
+ name: string;
24
+ /** Elements to which defaults apply. */
25
+ domain: GraphMLDomain;
26
+ /** Declared scalar type. */
27
+ type: GraphMLAttributeType;
28
+ /** Typed default value. */
29
+ defaultValue?: unknown;
30
+ };
31
+
32
+ /** XML element representation emitted by fast-xml-parser. */
33
+ type GraphMLObject = Record<string, unknown> & {
34
+ /** Element text content. */
35
+ [XML_TEXT_KEY]?: string;
36
+ };
37
+
38
+ const graphmlParser = new XMLParser({
39
+ ignoreAttributes: false,
40
+ attributeNamePrefix: XML_ATTRIBUTE_PREFIX,
41
+ textNodeName: XML_TEXT_KEY,
42
+ trimValues: true,
43
+ parseAttributeValue: false,
44
+ parseTagValue: false,
45
+ removeNSPrefix: true
46
+ });
47
+
48
+ /** Accepted GraphML document inputs. */
49
+ export type GraphMLInput = string | ArrayBuffer | Uint8Array;
50
+
51
+ /** Parses the first GraphML graph into framework-independent node and edge records. */
52
+ export function parseGraphML(graphml: GraphMLInput): GraphData {
53
+ const xmlText = decodeGraphML(graphml);
54
+ const document = graphmlParser.parse(xmlText) as GraphMLObject;
55
+ const graphmlRoot = getGraphMLRoot(document);
56
+ if (!graphmlRoot) {
57
+ throw new Error('GraphML document does not contain a <graphml> element.');
58
+ }
59
+
60
+ const graphElement = getGraphElement(graphmlRoot);
61
+ if (!graphElement) {
62
+ throw new Error('GraphML document does not contain a <graph> element.');
63
+ }
64
+
65
+ const keyDefinitions = collectKeyDefinitions(graphmlRoot, graphElement);
66
+ const defaultDirected = parseEdgeDefault(graphElement[`${XML_ATTRIBUTE_PREFIX}edgedefault`]);
67
+
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)
71
+ );
72
+
73
+ const filteredNodes = nodes.filter((node): node is GraphNode => Boolean(node));
74
+ const filteredEdges = edges.filter((edge): edge is GraphEdge => Boolean(edge));
75
+
76
+ return {
77
+ shape: 'plain-graph-data',
78
+ nodes: filteredNodes,
79
+ edges: filteredEdges
80
+ } satisfies GraphData;
81
+ }
82
+
83
+ /** Decodes text or UTF-8 binary input. */
84
+ function decodeGraphML(graphml: GraphMLInput): string {
85
+ if (typeof graphml === 'string') {
86
+ return graphml;
87
+ }
88
+
89
+ if (graphml instanceof Uint8Array) {
90
+ return new TextDecoder().decode(graphml);
91
+ }
92
+
93
+ if (graphml instanceof ArrayBuffer) {
94
+ return new TextDecoder().decode(new Uint8Array(graphml));
95
+ }
96
+
97
+ throw new Error('Unsupported GraphML input. Expected a string, ArrayBuffer, or Uint8Array.');
98
+ }
99
+
100
+ /** Finds the GraphML document root. */
101
+ function getGraphMLRoot(document: GraphMLObject): GraphMLObject | null {
102
+ const root = document.graphml;
103
+ if (isObject(root)) {
104
+ return root;
105
+ }
106
+
107
+ return null;
108
+ }
109
+
110
+ /** Selects the first graph element. */
111
+ function getGraphElement(graphmlRoot: GraphMLObject): GraphMLObject | null {
112
+ const graph = graphmlRoot.graph;
113
+ if (!graph) {
114
+ return null;
115
+ }
116
+
117
+ if (Array.isArray(graph)) {
118
+ const firstGraph = graph.find(entry => isObject(entry));
119
+ return isObject(firstGraph) ? firstGraph : null;
120
+ }
121
+
122
+ return isObject(graph) ? graph : null;
123
+ }
124
+
125
+ /** Collects scoped keys and typed defaults. */
126
+ function collectKeyDefinitions(
127
+ graphmlRoot: GraphMLObject,
128
+ graphElement: GraphMLObject
129
+ ): Map<string, GraphMLKeyDefinition> {
130
+ const keys = new Map<string, GraphMLKeyDefinition>();
131
+
132
+ for (const candidate of [
133
+ ...normalizeArray(graphmlRoot.key),
134
+ ...normalizeArray(graphElement.key)
135
+ ]) {
136
+ if (isObject(candidate)) {
137
+ const id = String(candidate[`${XML_ATTRIBUTE_PREFIX}id`] ?? '').trim();
138
+ if (id) {
139
+ const domain = normalizeDomain(candidate[`${XML_ATTRIBUTE_PREFIX}for`]);
140
+ const name = String(candidate[`${XML_ATTRIBUTE_PREFIX}attr.name`] ?? id).trim();
141
+ const type = normalizeType(candidate[`${XML_ATTRIBUTE_PREFIX}attr.type`]);
142
+ const defaultNode = candidate.default ?? null;
143
+ const defaultValue = defaultNode !== null ? castDataValue(defaultNode, type) : undefined;
144
+
145
+ keys.set(id, {id, domain, name, type, defaultValue});
146
+ }
147
+ }
148
+ }
149
+
150
+ return keys;
151
+ }
152
+
153
+ /** Converts a node element into a graph record. */
154
+ function parseNode(
155
+ node: unknown,
156
+ keyDefinitions: Map<string, GraphMLKeyDefinition>
157
+ ): GraphNode | null {
158
+ if (!isObject(node)) {
159
+ return null;
160
+ }
161
+
162
+ const id = node[`${XML_ATTRIBUTE_PREFIX}id`];
163
+ if (typeof id !== 'string') {
164
+ return null;
165
+ }
166
+
167
+ const attributes = buildAttributeBag('node', node.data, keyDefinitions);
168
+
169
+ const graphNode: GraphNode = {
170
+ id,
171
+ attributes: Object.keys(attributes).length > 0 ? attributes : undefined
172
+ };
173
+
174
+ const label = attributes.label;
175
+ if (typeof label === 'string') {
176
+ graphNode.label = label;
177
+ }
178
+
179
+ return graphNode;
180
+ }
181
+
182
+ /** Converts an edge element with its effective direction. */
183
+ function parseEdge(
184
+ edge: unknown,
185
+ index: number,
186
+ keyDefinitions: Map<string, GraphMLKeyDefinition>,
187
+ defaultDirected: boolean
188
+ ): GraphEdge | null {
189
+ if (!isObject(edge)) {
190
+ return null;
191
+ }
192
+
193
+ const sourceId = edge[`${XML_ATTRIBUTE_PREFIX}source`];
194
+ const targetId = edge[`${XML_ATTRIBUTE_PREFIX}target`];
195
+ if (typeof sourceId !== 'string') {
196
+ return null;
197
+ }
198
+ if (typeof targetId !== 'string') {
199
+ return null;
200
+ }
201
+
202
+ const rawId = edge[`${XML_ATTRIBUTE_PREFIX}id`];
203
+ const id = typeof rawId === 'string' ? rawId : `edge-${index}`;
204
+ const directed = parseDirected(edge[`${XML_ATTRIBUTE_PREFIX}directed`], defaultDirected);
205
+ const attributes = buildAttributeBag('edge', edge.data, keyDefinitions);
206
+
207
+ const graphEdge: GraphEdge = {
208
+ id,
209
+ sourceId,
210
+ targetId,
211
+ directed,
212
+ attributes: Object.keys(attributes).length > 0 ? attributes : undefined
213
+ };
214
+
215
+ const label = attributes.label;
216
+ if (typeof label === 'string') {
217
+ graphEdge.label = label;
218
+ }
219
+
220
+ return graphEdge;
221
+ }
222
+
223
+ /** Applies scoped defaults followed by explicit data values. */
224
+ function buildAttributeBag(
225
+ domain: GraphMLDomain,
226
+ data: unknown,
227
+ keyDefinitions: Map<string, GraphMLKeyDefinition>
228
+ ): Record<string, unknown> {
229
+ const attributes: Record<string, unknown> = Object.create(null);
230
+
231
+ for (const key of keyDefinitions.values()) {
232
+ if (key.domain === 'all' || key.domain === domain) {
233
+ if (key.defaultValue !== undefined) {
234
+ attributes[key.name] = key.defaultValue;
235
+ }
236
+ }
237
+ }
238
+
239
+ for (const entry of normalizeArray(data)) {
240
+ assignAttributeFromDataEntry(entry, keyDefinitions, attributes);
241
+ }
242
+
243
+ return attributes;
244
+ }
245
+
246
+ /** Assigns a data value using its declared key or original key ID. */
247
+ function assignAttributeFromDataEntry(
248
+ entry: unknown,
249
+ keyDefinitions: Map<string, GraphMLKeyDefinition>,
250
+ attributes: Record<string, unknown>
251
+ ): void {
252
+ if (!isObject(entry)) {
253
+ return;
254
+ }
255
+
256
+ const keyId = entry[`${XML_ATTRIBUTE_PREFIX}key`];
257
+ if (typeof keyId !== 'string') {
258
+ return;
259
+ }
260
+
261
+ const definition = keyDefinitions.get(keyId);
262
+ const attributeName = definition?.name ?? keyId;
263
+ const value = castDataValue(entry, definition?.type ?? 'string');
264
+ if (value !== undefined) {
265
+ attributes[attributeName] = value;
266
+ }
267
+ }
268
+
269
+ /** Converts GraphML scalar types while preserving unparseable numeric text. */
270
+ function castDataValue(value: unknown, type: GraphMLAttributeType): unknown {
271
+ if (value === null || typeof value === 'undefined') {
272
+ return undefined;
273
+ }
274
+
275
+ const text = extractTextContent(value);
276
+ if (text === undefined) {
277
+ return undefined;
278
+ }
279
+
280
+ if (type === 'boolean') {
281
+ return parseBoolean(text);
282
+ }
283
+ if (type === 'int' || type === 'long') {
284
+ const parsed = Number.parseInt(text, 10);
285
+ return Number.isNaN(parsed) ? text : parsed;
286
+ }
287
+ if (type === 'float' || type === 'double') {
288
+ const parsed = Number.parseFloat(text);
289
+ return Number.isNaN(parsed) ? text : parsed;
290
+ }
291
+ return text;
292
+ }
293
+
294
+ /** Extracts scalar text or serializes structured vendor data. */
295
+ function extractTextContent(value: unknown): string | undefined {
296
+ if (typeof value === 'string') {
297
+ return value;
298
+ }
299
+
300
+ if (typeof value === 'number' || typeof value === 'boolean') {
301
+ return String(value);
302
+ }
303
+
304
+ if (Array.isArray(value)) {
305
+ return extractTextContent(value[0]);
306
+ }
307
+
308
+ if (isObject(value)) {
309
+ const text = value[XML_TEXT_KEY];
310
+ if (typeof text === 'string') {
311
+ return text;
312
+ }
313
+
314
+ const nonAttributeEntries = Object.entries(value).filter(
315
+ ([key]) => !key.startsWith(XML_ATTRIBUTE_PREFIX)
316
+ );
317
+ if (nonAttributeEntries.length === 0) {
318
+ return undefined;
319
+ }
320
+ return JSON.stringify(Object.fromEntries(nonAttributeEntries));
321
+ }
322
+
323
+ return undefined;
324
+ }
325
+
326
+ /** Normalizes singleton and repeated XML elements. */
327
+ function normalizeArray<T>(value: T | T[] | null | undefined): T[] {
328
+ if (!value) {
329
+ return [];
330
+ }
331
+ return Array.isArray(value) ? value : [value];
332
+ }
333
+
334
+ /** Tests whether a value is an XML element object. */
335
+ function isObject(value: unknown): value is GraphMLObject {
336
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
337
+ }
338
+
339
+ /** Normalizes a key scope, defaulting to all elements. */
340
+ function normalizeDomain(value: unknown): GraphMLDomain {
341
+ if (typeof value !== 'string') {
342
+ return 'all';
343
+ }
344
+
345
+ const lower = value.toLowerCase();
346
+ if (lower === 'node' || lower === 'edge' || lower === 'graph' || lower === 'all') {
347
+ return lower;
348
+ }
349
+ return 'all';
350
+ }
351
+
352
+ /** Normalizes supported GraphML scalar types. */
353
+ function normalizeType(value: unknown): GraphMLAttributeType {
354
+ if (typeof value !== 'string') {
355
+ return 'string';
356
+ }
357
+
358
+ const lower = value.toLowerCase();
359
+ if (
360
+ lower === 'boolean' ||
361
+ lower === 'int' ||
362
+ lower === 'long' ||
363
+ lower === 'float' ||
364
+ lower === 'double'
365
+ ) {
366
+ return lower;
367
+ }
368
+ return 'string';
369
+ }
370
+
371
+ /** Converts an edge element with its effective direction. */
372
+ /** Reads the graph-wide default edge direction. */
373
+ function parseEdgeDefault(value: unknown): boolean {
374
+ if (typeof value !== 'string') {
375
+ return false;
376
+ }
377
+ return value.toLowerCase() !== 'undirected';
378
+ }
379
+
380
+ /** Reads an edge direction override. */
381
+ function parseDirected(value: unknown, defaultDirected: boolean): boolean {
382
+ if (typeof value === 'string') {
383
+ const normalized = value.trim().toLowerCase();
384
+ if (
385
+ normalized === 'true' ||
386
+ normalized === '1' ||
387
+ normalized === 'yes' ||
388
+ normalized === 'directed'
389
+ ) {
390
+ return true;
391
+ }
392
+ if (
393
+ normalized === 'false' ||
394
+ normalized === '0' ||
395
+ normalized === 'no' ||
396
+ normalized === 'undirected'
397
+ ) {
398
+ return false;
399
+ }
400
+ }
401
+ return defaultDirected;
402
+ }
403
+
404
+ /** Converts common boolean spellings. */
405
+ function parseBoolean(value: string): boolean {
406
+ const normalized = value.trim().toLowerCase();
407
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'y') {
408
+ return true;
409
+ }
410
+ if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'n') {
411
+ return false;
412
+ }
413
+ return Boolean(value);
414
+ }