• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-element / 20514590651

26 Dec 2025 02:37AM UTC coverage: 70.559% (-0.3%) from 70.836%
20514590651

push

github

apowers313
ci: fix npm ci

9591 of 13363 branches covered (71.77%)

Branch coverage included in aggregate %.

25136 of 35854 relevant lines covered (70.11%)

6233.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.93
/src/data/GMLDataSource.ts
1
import type {AdHocData} from "../config/common.js";
2
import {BaseDataSourceConfig, DataSource, DataSourceChunk} from "./DataSource.js";
15✔
3

4
// GML has no additional config currently, so just use the base config
5
export type GMLDataSourceConfig = BaseDataSourceConfig;
6

7
interface GMLValue {
8
    [key: string]: string | number | GMLValue | GMLValue[] | (string | number)[];
9
}
10

11
/**
12
 * Data source for loading graph data from GML (Graph Modeling Language) files.
13
 * Supports hierarchical graph structures with typed attributes.
14
 */
15
export class GMLDataSource extends DataSource {
15✔
16
    static readonly type = "gml";
18✔
17

18
    private config: GMLDataSourceConfig;
19

20
    /**
21
     * Creates a new GMLDataSource instance.
22
     * @param config - Configuration options for GML parsing and data loading
23
     */
24
    constructor(config: GMLDataSourceConfig) {
18✔
25
        super(config.errorLimit ?? 100, config.chunkSize);
25✔
26
        this.config = config;
25✔
27
    }
25✔
28

29
    protected getConfig(): BaseDataSourceConfig {
18✔
30
        return this.config;
25✔
31
    }
25✔
32

33
    /**
34
     * Fetches and parses GML format data into graph chunks.
35
     * @yields DataSourceChunk objects containing parsed nodes and edges
36
     */
37
    async *sourceFetchData(): AsyncGenerator<DataSourceChunk, void, unknown> {
18✔
38
    // Get GML content
39
        const gmlContent = await this.getContent();
25✔
40

41
        // Parse GML
42
        const graph = this.parseGML(gmlContent);
24✔
43

44
        if (!graph) {
24!
45
            return;
1✔
46
        }
1✔
47

48
        // Extract nodes and edges
49
        const nodes = this.extractNodes(graph);
20✔
50
        const edges = this.extractEdges(graph);
20✔
51

52
        // Use shared chunking helper
53
        yield* this.chunkData(nodes, edges);
20✔
54
    }
25✔
55

56
    private parseGML(content: string): GMLValue | null {
18✔
57
    // Simple GML parser
58
    // GML format: key [ ... ] or key value
59
        const tokens = this.tokenize(content);
24✔
60
        const result = this.parseValue(tokens);
24✔
61

62
        if (result && typeof result === "object" && "graph" in result) {
24✔
63
            return result.graph as GMLValue;
21✔
64
        }
21!
65

66
        throw new Error("Invalid GML: missing graph element");
3✔
67
    }
24✔
68

69
    private tokenize(content: string): string[] {
18✔
70
        const tokens: string[] = [];
24✔
71
        let current = "";
24✔
72
        let inString = false;
24✔
73
        let inComment = false;
24✔
74

75
        // eslint-disable-next-line @typescript-eslint/prefer-for-of -- Need index for look-ahead and manual increment
76
        for (let i = 0; i < content.length; i++) {
24✔
77
            const char = content[i];
200,626✔
78
            // const nextChar = content[i + 1]; // Unused
79

80
            // Handle comments
81
            if (!inString && char === "#") {
200,626!
82
                inComment = true;
1✔
83
                continue;
1✔
84
            }
1✔
85

86
            if (inComment) {
200,626!
87
                if (char === "\n") {
8✔
88
                    inComment = false;
1✔
89
                }
1✔
90

91
                continue;
8✔
92
            }
8✔
93

94
            // Handle strings
95
            if (char === "\"") {
200,626✔
96
                if (inString) {
891✔
97
                    tokens.push(current);
445✔
98
                    current = "";
445✔
99
                    inString = false;
445✔
100
                } else {
458✔
101
                    inString = true;
446✔
102
                }
446✔
103

104
                continue;
891✔
105
            }
891✔
106

107
            if (inString) {
200,626✔
108
                current += char;
4,068✔
109
                continue;
4,068✔
110
            }
4,068✔
111

112
            // Handle structural characters
113
            if (char === "[" || char === "]") {
200,626✔
114
                if (current.trim()) {
13,363!
115
                    tokens.push(current.trim());
×
116
                    current = "";
×
117
                }
×
118

119
                tokens.push(char);
13,363✔
120
                continue;
13,363✔
121
            }
13,363✔
122

123
            // Handle whitespace
124
            if (/\s/.test(char)) {
200,626✔
125
                if (current.trim()) {
101,281✔
126
                    tokens.push(current.trim());
23,363✔
127
                    current = "";
23,363✔
128
                }
23,363✔
129

130
                continue;
101,281✔
131
            }
101,281✔
132

133
            current += char;
81,014✔
134
        }
81,014✔
135

136
        if (current.trim()) {
24!
137
            tokens.push(current.trim());
2✔
138
        }
2✔
139

140
        return tokens;
24✔
141
    }
24✔
142

143
    private parseValue(tokens: string[]): GMLValue | string | number | null {
18✔
144
        if (tokens.length === 0) {
6,707!
145
            return null;
2✔
146
        }
2✔
147

148
        const result: GMLValue = {};
6,705✔
149
        let i = 0;
6,705✔
150

151
        while (i < tokens.length) {
6,707✔
152
            const key = tokens[i];
15,247✔
153

154
            if (key === "]") {
15,247!
155
                break;
×
156
            }
×
157

158
            if (key === "[") {
15,247!
159
                i++;
×
160
                continue;
×
161
            }
×
162

163
            // Look ahead for value
164
            if (i + 1 < tokens.length) {
15,247✔
165
                const next = tokens[i + 1];
15,246✔
166

167
                if (next === "[") {
15,246✔
168
                    // Complex value
169
                    i += 2; // Skip key and '['
6,683✔
170
                    // const nested: GMLValue[] = []; // Unused
171
                    let depth = 1;
6,683✔
172
                    const start = i;
6,683✔
173

174
                    // Find matching ']'
175
                    while (i < tokens.length && depth > 0) {
6,683✔
176
                        if (tokens[i] === "[") {
60,923✔
177
                            depth++;
6,668✔
178
                        }
6,668✔
179

180
                        if (tokens[i] === "]") {
60,923✔
181
                            depth--;
13,347✔
182
                        }
13,347✔
183

184
                        if (depth > 0) {
60,923✔
185
                            i++;
54,243✔
186
                        }
54,243✔
187
                    }
60,923✔
188

189
                    // Parse nested content
190
                    const nestedTokens = tokens.slice(start, i);
6,683✔
191
                    const nestedValue = this.parseValue(nestedTokens);
6,683✔
192

193
                    // Handle multiple values with same key (like multiple nodes)
194
                    if (key in result) {
6,683✔
195
                        if (Array.isArray(result[key])) {
6,620✔
196
                            (result[key] as GMLValue[]).push(nestedValue as GMLValue);
6,596✔
197
                        } else {
6,620✔
198
                            result[key] = [result[key] as GMLValue, nestedValue as GMLValue];
24✔
199
                        }
24✔
200
                    } else {
6,675✔
201
                        result[key] = nestedValue as GMLValue;
63✔
202
                    }
63✔
203

204
                    i++; // Skip ']'
6,683✔
205
                } else if (next !== "]") {
15,246✔
206
                    // Simple value
207
                    const value = this.parseSimpleValue(next);
8,563✔
208

209
                    // Handle multiple values with same key
210
                    if (key in result) {
8,563!
211
                        if (Array.isArray(result[key])) {
1!
212
                            (result[key] as (string | number)[]).push(value);
×
213
                        } else {
1✔
214
                            result[key] = [result[key] as string | number, value];
1✔
215
                        }
1✔
216
                    } else {
8,563✔
217
                        result[key] = value;
8,562✔
218
                    }
8,562✔
219

220
                    i += 2; // Skip key and value
8,563✔
221
                } else {
8,563!
222
                    i++;
×
223
                }
×
224
            } else {
15,247!
225
                i++;
1✔
226
            }
1✔
227
        }
15,247✔
228

229
        return result;
6,705✔
230
    }
6,707✔
231

232
    private parseSimpleValue(value: string): string | number {
18✔
233
    // Try to parse as number
234
        if (/^-?\d+$/.test(value)) {
8,563✔
235
            return parseInt(value, 10);
8,087✔
236
        }
8,087✔
237

238
        if (/^-?\d+\.\d+$/.test(value)) {
8,563✔
239
            return parseFloat(value);
26✔
240
        }
26✔
241

242
        return value;
450✔
243
    }
8,563✔
244

245
    private extractNodes(graph: GMLValue): AdHocData[] {
18✔
246
        const nodes: Record<string, unknown>[] = [];
20✔
247

248
        if (!graph.node) {
20!
249
            return [] as AdHocData[];
1✔
250
        }
1✔
251

252
        const nodeArray = Array.isArray(graph.node) ? graph.node : [graph.node];
20!
253

254
        for (const node of nodeArray) {
20✔
255
            try {
5,345✔
256
                if (typeof node !== "object" || !("id" in node)) {
5,345!
257
                    this.errorAggregator.addError({
1✔
258
                        message: "Node missing id attribute",
1✔
259
                        category: "missing-value",
1✔
260
                        field: "id",
1✔
261
                    });
1✔
262
                    continue;
1✔
263
                }
1✔
264

265
                const nodeData: Record<string, unknown> = {... node};
5,344✔
266
                nodes.push(nodeData);
5,344✔
267
            } catch (error) {
5,345!
268
                const canContinue = this.errorAggregator.addError({
×
269
                    message: `Failed to parse node: ${error instanceof Error ? error.message : String(error)}`,
×
270
                    category: "parse-error",
×
271
                });
×
272

273
                if (!canContinue) {
×
274
                    throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
275
                }
×
276
            }
×
277
        }
5,345✔
278

279
        return nodes as AdHocData[];
19✔
280
    }
20✔
281

282
    private extractEdges(graph: GMLValue): AdHocData[] {
18✔
283
        const edges: Record<string, unknown>[] = [];
20✔
284

285
        if (!graph.edge) {
20!
286
            return [] as AdHocData[];
5✔
287
        }
5✔
288

289
        const edgeArray = Array.isArray(graph.edge) ? graph.edge : [graph.edge];
20!
290

291
        for (const edge of edgeArray) {
20✔
292
            try {
1,308✔
293
                if (typeof edge !== "object" || !("source" in edge) || !("target" in edge)) {
1,308!
294
                    this.errorAggregator.addError({
1✔
295
                        message: "Edge missing source or target attribute",
1✔
296
                        category: "missing-value",
1✔
297
                        field: typeof edge === "object" && !("source" in edge) ? "source" : "target",
1!
298
                    });
1✔
299
                    continue;
1✔
300
                }
1✔
301

302
                const edgeData: Record<string, unknown> = {
1,307✔
303
                    src: edge.source,
1,307✔
304
                    dst: edge.target,
1,307✔
305
                    ... edge,
1,307✔
306
                };
1,307✔
307

308
                // Remove redundant source/target fields
309
                delete edgeData.source;
1,307✔
310
                delete edgeData.target;
1,307✔
311

312
                edges.push(edgeData);
1,307✔
313
            } catch (error) {
1,308!
314
                const canContinue = this.errorAggregator.addError({
×
315
                    message: `Failed to parse edge: ${error instanceof Error ? error.message : String(error)}`,
×
316
                    category: "parse-error",
×
317
                });
×
318

319
                if (!canContinue) {
×
320
                    throw new Error(`Too many errors (${this.errorAggregator.getErrorCount()}), aborting parse`);
×
321
                }
×
322
            }
×
323
        }
1,308✔
324

325
        return edges as AdHocData[];
15✔
326
    }
20✔
327
}
18✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc