@graphty/graph-io 0.3.26 → 0.3.28
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/dist/chunks/{choose-BbJI55dx.js → choose-DrXtu_96.js} +2 -2
- package/dist/chunks/{choose-BbJI55dx.js.map → choose-DrXtu_96.js.map} +1 -1
- package/dist/chunks/{constants-BCsFsMj4.js → constants-ikG85dT8.js} +2 -2
- package/dist/chunks/{constants-BCsFsMj4.js.map → constants-ikG85dT8.js.map} +1 -1
- package/dist/chunks/{escape-D0g6NNUy.js → escape-D1HPTUrP.js} +10 -9
- package/dist/chunks/escape-D1HPTUrP.js.map +1 -0
- package/dist/chunks/{export-BTI4yQgy.js → export-CHd4tQE2.js} +4 -4
- package/dist/chunks/export-CHd4tQE2.js.map +1 -0
- package/dist/chunks/{exporter-Dxh231ZF.js → exporter-Cl021ZIE.js} +72 -48
- package/dist/chunks/exporter-Cl021ZIE.js.map +1 -0
- package/dist/chunks/{exporter-Cra2GYcN.js → exporter-D23CKNqF.js} +5 -5
- package/dist/chunks/exporter-D23CKNqF.js.map +1 -0
- package/dist/chunks/{exporter-tne475KK.js → exporter-DfWTgrti.js} +5 -5
- package/dist/chunks/exporter-DfWTgrti.js.map +1 -0
- package/dist/chunks/{exporter-BMqk5oxW.js → exporter-DhtW6FxK.js} +3 -3
- package/dist/chunks/{exporter-BMqk5oxW.js.map → exporter-DhtW6FxK.js.map} +1 -1
- package/dist/chunks/{importer-Bcl1d4xH.js → importer-B2W_58pz.js} +5 -5
- package/dist/chunks/importer-B2W_58pz.js.map +1 -0
- package/dist/chunks/{importer-90YWhJoF.js → importer-BSALvkui.js} +9 -9
- package/dist/chunks/importer-BSALvkui.js.map +1 -0
- package/dist/chunks/{importer-DfVu6j59.js → importer-BVRE--Un.js} +4 -4
- package/dist/chunks/importer-BVRE--Un.js.map +1 -0
- package/dist/chunks/{importer-BfIsp1f9.js → importer-BZB7iP9r.js} +35 -9
- package/dist/chunks/importer-BZB7iP9r.js.map +1 -0
- package/dist/chunks/{importer-DCjOvO7U.js → importer-CIAzt-wo.js} +6 -6
- package/dist/chunks/importer-CIAzt-wo.js.map +1 -0
- package/dist/chunks/{importer-Dku_VAIY.js → importer-DpEW5zeG.js} +4 -4
- package/dist/chunks/{importer-Dku_VAIY.js.map → importer-DpEW5zeG.js.map} +1 -1
- package/dist/chunks/{importer-C_TEA1zJ.js → importer-z_FL4VMb.js} +9 -9
- package/dist/chunks/importer-z_FL4VMb.js.map +1 -0
- package/dist/chunks/{json-elements-3tnyt5uZ.js → json-elements-BAmNBZok.js} +4 -4
- package/dist/chunks/json-elements-BAmNBZok.js.map +1 -0
- package/dist/chunks/{ontology-export-B9Tcf37D.js → ontology-export-Bab4eUA8.js} +2 -2
- package/dist/chunks/{ontology-export-B9Tcf37D.js.map → ontology-export-Bab4eUA8.js.map} +1 -1
- package/dist/chunks/{records-CQXT__mY.js → records-CdlvL4RO.js} +2 -2
- package/dist/chunks/{records-CQXT__mY.js.map → records-CdlvL4RO.js.map} +1 -1
- package/dist/chunks/{text-DmcUs5_Q.js → text-DzXjAiTl.js} +2 -2
- package/dist/chunks/{text-DmcUs5_Q.js.map → text-DzXjAiTl.js.map} +1 -1
- package/dist/csv.js +16 -6
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +2 -2
- package/dist/gml.js +20 -11
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +26 -26
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +4 -4
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/declared-types.d.ts.map +1 -1
- package/dist/src/common/declared-types.js +2 -2
- package/dist/src/common/declared-types.js.map +1 -1
- package/dist/src/common/escape.d.ts.map +1 -1
- package/dist/src/common/escape.js +6 -5
- package/dist/src/common/escape.js.map +1 -1
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/json-elements.js +1 -1
- package/dist/src/common/json-elements.js.map +1 -1
- package/dist/src/common/lists.js +1 -1
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/common/xml.js +2 -2
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.js +1 -1
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +20 -2
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/cx/importer.js +2 -2
- package/dist/src/formats/cx/importer.js.map +1 -1
- package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
- package/dist/src/formats/cx2/exporter.js +84 -33
- package/dist/src/formats/cx2/exporter.js.map +1 -1
- package/dist/src/formats/cx2/importer.js +1 -1
- package/dist/src/formats/cx2/importer.js.map +1 -1
- package/dist/src/formats/cys/session.js +3 -3
- package/dist/src/formats/cys/session.js.map +1 -1
- package/dist/src/formats/dot/exporter.js +30 -4
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +23 -6
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/syntax.js +2 -2
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +1 -1
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.js +1 -1
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.js +3 -3
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +1 -1
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/values.js +1 -1
- package/dist/src/formats/xgmml/values.js.map +1 -1
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js.map +1 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/common/declared-types.ts +3 -12
- package/src/common/escape.ts +6 -5
- package/src/common/input.ts +2 -2
- package/src/common/json-elements.ts +1 -1
- package/src/common/lists.ts +1 -1
- package/src/common/xml.ts +2 -2
- package/src/common/zip.ts +1 -1
- package/src/formats/csv/exporter.ts +1 -1
- package/src/formats/csv/importer.ts +21 -2
- package/src/formats/cx/importer.ts +2 -2
- package/src/formats/cx2/exporter.ts +124 -50
- package/src/formats/cx2/importer.ts +1 -1
- package/src/formats/cys/session.ts +3 -3
- package/src/formats/dot/exporter.ts +37 -4
- package/src/formats/dot/importer.ts +1 -1
- package/src/formats/gml/exporter.ts +24 -6
- package/src/formats/gml/syntax.ts +2 -2
- package/src/formats/json/dialect.ts +1 -1
- package/src/formats/neo4j/exporter.ts +2 -2
- package/src/formats/pajek/exporter.ts +1 -1
- package/src/formats/pajek/importer.ts +1 -1
- package/src/formats/pajek/syntax.ts +3 -3
- package/src/formats/xgmml/exporter.ts +1 -1
- package/src/formats/xgmml/values.ts +1 -1
- package/src/registry.ts +9 -10
- package/src/sniff.ts +2 -21
- package/src/types.ts +1 -7
- package/dist/chunks/escape-D0g6NNUy.js.map +0 -1
- package/dist/chunks/export-BTI4yQgy.js.map +0 -1
- package/dist/chunks/exporter-Cra2GYcN.js.map +0 -1
- package/dist/chunks/exporter-Dxh231ZF.js.map +0 -1
- package/dist/chunks/exporter-tne475KK.js.map +0 -1
- package/dist/chunks/importer-90YWhJoF.js.map +0 -1
- package/dist/chunks/importer-Bcl1d4xH.js.map +0 -1
- package/dist/chunks/importer-BfIsp1f9.js.map +0 -1
- package/dist/chunks/importer-C_TEA1zJ.js.map +0 -1
- package/dist/chunks/importer-DCjOvO7U.js.map +0 -1
- package/dist/chunks/importer-DfVu6j59.js.map +0 -1
- package/dist/chunks/json-elements-3tnyt5uZ.js.map +0 -1
|
@@ -229,7 +229,7 @@ function nonFiniteCount(column: Column): number {
|
|
|
229
229
|
if ((value as readonly number[]).some((v) => !Number.isFinite(v))) {
|
|
230
230
|
count++;
|
|
231
231
|
}
|
|
232
|
-
} else if (!Number.isFinite(value
|
|
232
|
+
} else if (!Number.isFinite(value)) {
|
|
233
233
|
count++;
|
|
234
234
|
}
|
|
235
235
|
}
|
|
@@ -270,18 +270,26 @@ function planAttributes(
|
|
|
270
270
|
return plans;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/** A note-recording callback. */
|
|
274
|
+
type Note = (code: string, message: string, column?: string | null, count?: number | null) => void;
|
|
275
|
+
|
|
273
276
|
/**
|
|
274
|
-
*
|
|
277
|
+
* The generic notes of checkCapabilities(), adapted to CX2: bypass columns hold any JSON value,
|
|
278
|
+
* ids are counted by planNodeIds(), nested values are written as JSON text, and only the node
|
|
279
|
+
* label is written into the "n" slot (an edge label keeps its own name but loses its role).
|
|
275
280
|
* @param snapshot - the snapshot
|
|
276
281
|
* @param common - the resolved common options
|
|
277
|
-
* @
|
|
282
|
+
* @param notes - the notes so far, appended to
|
|
283
|
+
* @param note - the recorder
|
|
284
|
+
* @returns the fatal condition, or null
|
|
278
285
|
*/
|
|
279
|
-
function
|
|
280
|
-
|
|
286
|
+
function capabilityNotes(
|
|
287
|
+
snapshot: GraphSnapshot,
|
|
288
|
+
common: ResolvedExportOptions,
|
|
289
|
+
notes: LossNote[],
|
|
290
|
+
note: Note,
|
|
291
|
+
): GraphFormatError | null {
|
|
281
292
|
let fatal: GraphFormatError | null = null;
|
|
282
|
-
const note = (code: string, message: string, column: string | null = null, count: number | null = null): void => {
|
|
283
|
-
notes.push(Object.freeze({ code, message, column, count }));
|
|
284
|
-
};
|
|
285
293
|
// bypass columns are written as bypasses, which hold any JSON value
|
|
286
294
|
const bypassNames = new Set(
|
|
287
295
|
[...snapshot.nodes, ...snapshot.edges].filter((c) => isBypass(c) || isZ(c)).map((c) => c.meta.name),
|
|
@@ -290,11 +298,7 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
|
|
|
290
298
|
roles: SLOT_ROLES,
|
|
291
299
|
roleNames: ROLE_NAMES,
|
|
292
300
|
})) {
|
|
293
|
-
if (gen
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
if (gen.code === LOSS.ID_CHARSET || gen.code === LOSS.ID_MANGLED) {
|
|
297
|
-
// counted below: CX2 also keeps integer ids beyond 2^53, which the generic rule refuses
|
|
301
|
+
if (!keepGenericNote(gen, bypassNames)) {
|
|
298
302
|
continue;
|
|
299
303
|
}
|
|
300
304
|
if (gen.code === LOSS.JSON) {
|
|
@@ -311,27 +315,83 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
|
|
|
311
315
|
}
|
|
312
316
|
notes.push(gen);
|
|
313
317
|
}
|
|
318
|
+
const edgeLabel = snapshot.edges.byRole("label");
|
|
319
|
+
if (edgeLabel !== null) {
|
|
320
|
+
note(
|
|
321
|
+
LOSS.ROLE,
|
|
322
|
+
`edge column "${edgeLabel.meta.name}" (label) is written as a plain attribute; CX2 edges have no label slot and the role is lost`,
|
|
323
|
+
edgeLabel.meta.name,
|
|
324
|
+
edgeLabel.length - edgeLabel.nullCount,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return fatal;
|
|
328
|
+
}
|
|
314
329
|
|
|
315
|
-
|
|
316
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Whether a generic note applies to CX2 as it stands.
|
|
332
|
+
* @param gen - the generic note
|
|
333
|
+
* @param bypassNames - the names of the columns written as bypasses
|
|
334
|
+
* @returns false for a note CX2 drops or replaces
|
|
335
|
+
*/
|
|
336
|
+
function keepGenericNote(gen: LossNote, bypassNames: ReadonlySet<string>): boolean {
|
|
337
|
+
if (gen.column !== null && bypassNames.has(gen.column) && (gen.code === LOSS.JSON || gen.code === LOSS.DTYPE)) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (gen.code === LOSS.COLUMN_NAME_CHANGED && gen.message.startsWith("edge column")) {
|
|
341
|
+
// only the node label is written into the "n" slot; an edge label keeps its own name
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
// ids are counted by planNodeIds(): CX2 also keeps integer ids beyond 2^53, which the generic rule refuses
|
|
345
|
+
return gen.code !== LOSS.ID_CHARSET && gen.code !== LOSS.ID_MANGLED;
|
|
346
|
+
}
|
|
317
347
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Note the NaN and infinite values written as null: attribute cells, explicit weights, positions.
|
|
350
|
+
* @param snapshot - the snapshot
|
|
351
|
+
* @param attrs - every planned attribute
|
|
352
|
+
* @param weights - the explicit weights
|
|
353
|
+
* @param position - the position column, or null
|
|
354
|
+
* @param note - the recorder
|
|
355
|
+
*/
|
|
356
|
+
function nonFiniteNote(
|
|
357
|
+
snapshot: GraphSnapshot,
|
|
358
|
+
attrs: readonly AttributePlan[],
|
|
359
|
+
weights: ExplicitWeights,
|
|
360
|
+
position: Column | null,
|
|
361
|
+
note: Note,
|
|
362
|
+
): void {
|
|
363
|
+
let nonfinite = 0;
|
|
364
|
+
for (const p of attrs) {
|
|
365
|
+
nonfinite += nonFiniteCount(p.column);
|
|
366
|
+
}
|
|
367
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
368
|
+
if (weights.isExplicit(e) && !Number.isFinite(weights.value(e))) {
|
|
369
|
+
nonfinite++;
|
|
370
|
+
}
|
|
322
371
|
}
|
|
372
|
+
if (position !== null) {
|
|
373
|
+
nonfinite += nonFinitePositions(position);
|
|
374
|
+
}
|
|
375
|
+
if (nonfinite > 0) {
|
|
376
|
+
note(
|
|
377
|
+
CX2_LOSS.NONFINITE_AS_NULL,
|
|
378
|
+
`${nonfinite} NaN or infinite value${plural(nonfinite)} cannot be written in CX2; written as null, they read back unset`,
|
|
379
|
+
null,
|
|
380
|
+
nonfinite,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
323
384
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Plan the edge attributes, noting a plain `weight` column that clashes with the weight key.
|
|
387
|
+
* @param snapshot - the snapshot
|
|
388
|
+
* @param weights - the explicit weights
|
|
389
|
+
* @param note - the recorder
|
|
390
|
+
* @returns the edge attribute plans
|
|
391
|
+
*/
|
|
392
|
+
function planEdgeAttributes(snapshot: GraphSnapshot, weights: ExplicitWeights, note: Note): AttributePlan[] {
|
|
332
393
|
const plainWeight = snapshot.edges.get(WEIGHT_ATTRIBUTE);
|
|
333
|
-
|
|
334
|
-
if (weightColumnClash) {
|
|
394
|
+
if (plainWeight !== null && plainWeight.meta.role === null) {
|
|
335
395
|
note(
|
|
336
396
|
LOSS.WEIGHT_KEY_CLASH,
|
|
337
397
|
weights.weighted
|
|
@@ -341,7 +401,7 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
|
|
|
341
401
|
null,
|
|
342
402
|
);
|
|
343
403
|
}
|
|
344
|
-
|
|
404
|
+
return planAttributes(
|
|
345
405
|
snapshot.edges,
|
|
346
406
|
(c) =>
|
|
347
407
|
(c.meta.role !== null && (SKIPPED_ROLES.has(c.meta.role) || c.meta.role === "id")) ||
|
|
@@ -350,33 +410,47 @@ function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
|
|
|
350
410
|
(c) => c.meta.name,
|
|
351
411
|
weights.weighted ? new Set([WEIGHT_ATTRIBUTE]) : new Set(),
|
|
352
412
|
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Plan an export: the notes, the fatal condition and what is written.
|
|
417
|
+
* @param snapshot - the snapshot
|
|
418
|
+
* @param common - the resolved common options
|
|
419
|
+
* @returns the plan
|
|
420
|
+
*/
|
|
421
|
+
function plan(snapshot: GraphSnapshot, common: ResolvedExportOptions): Plan {
|
|
422
|
+
const notes: LossNote[] = [];
|
|
423
|
+
const note: Note = (code, message, column = null, count = null) => {
|
|
424
|
+
notes.push(Object.freeze({ code, message, column, count }));
|
|
425
|
+
};
|
|
426
|
+
let fatal = capabilityNotes(snapshot, common, notes, note);
|
|
427
|
+
|
|
428
|
+
const folding = pairFolding(snapshot);
|
|
429
|
+
directionNotes(snapshot, folding, common, CX2_LOSS.UNDIRECTED_AS_DIRECTED, note);
|
|
430
|
+
|
|
431
|
+
const planned = planNodeIds(snapshot, common, "CX2", ORIGINAL_ID_ATTRIBUTE, note);
|
|
432
|
+
const ids = planned instanceof GraphFormatError ? null : planned;
|
|
433
|
+
if (planned instanceof GraphFormatError) {
|
|
434
|
+
fatal ??= planned;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const weights = explicitWeights(snapshot);
|
|
438
|
+
const label = snapshot.nodes.byRole("label");
|
|
439
|
+
const nodeAttrs = planAttributes(
|
|
440
|
+
snapshot.nodes,
|
|
441
|
+
(c) => (c.meta.role !== null && SKIPPED_ROLES.has(c.meta.role)) || isBypass(c) || isZ(c),
|
|
442
|
+
(c) => (c === label && !snapshot.nodes.has("name") ? "name" : c.meta.name),
|
|
443
|
+
ids !== null && ids.changed > 0 ? new Set([ORIGINAL_ID_ATTRIBUTE]) : new Set(),
|
|
444
|
+
);
|
|
445
|
+
const edgeAttrs = planEdgeAttributes(snapshot, weights, note);
|
|
353
446
|
const graphAttrs = planAttributes(
|
|
354
447
|
snapshot.graph,
|
|
355
448
|
() => false,
|
|
356
449
|
(c) => c.meta.name,
|
|
357
450
|
).map((p) => ({ ...p, key: p.name }));
|
|
358
451
|
|
|
359
|
-
let nonfinite = 0;
|
|
360
|
-
for (const p of [...nodeAttrs, ...edgeAttrs, ...graphAttrs]) {
|
|
361
|
-
nonfinite += nonFiniteCount(p.column);
|
|
362
|
-
}
|
|
363
|
-
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
364
|
-
if (weights.isExplicit(e) && !Number.isFinite(weights.value(e))) {
|
|
365
|
-
nonfinite++;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
452
|
const position = snapshot.nodes.byRole("position");
|
|
369
|
-
|
|
370
|
-
nonfinite += nonFinitePositions(position);
|
|
371
|
-
}
|
|
372
|
-
if (nonfinite > 0) {
|
|
373
|
-
note(
|
|
374
|
-
CX2_LOSS.NONFINITE_AS_NULL,
|
|
375
|
-
`${nonfinite} NaN or infinite value${plural(nonfinite)} cannot be written in CX2; written as null, they read back unset`,
|
|
376
|
-
null,
|
|
377
|
-
nonfinite,
|
|
378
|
-
);
|
|
379
|
-
}
|
|
453
|
+
nonFiniteNote(snapshot, [...nodeAttrs, ...edgeAttrs, ...graphAttrs], weights, position, note);
|
|
380
454
|
|
|
381
455
|
const edgeIds = planEdgeIds(snapshot, note);
|
|
382
456
|
const z = [...snapshot.nodes].find(isZ) ?? null;
|
|
@@ -709,7 +709,7 @@ function readDescriptor(value: unknown, line: number, doc: Cx2Document, report:
|
|
|
709
709
|
const text = typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
|
|
710
710
|
// the major version: "2.0", "2.1", "2.0.1", "2.1-beta", " 2.0" are all CX2
|
|
711
711
|
const match = /^\s*(\d+)(?=$|[.\s-])/.exec(text);
|
|
712
|
-
const major = match === null ? NaN : Number(match[1]);
|
|
712
|
+
const major = match === null ? Number.NaN : Number(match[1]);
|
|
713
713
|
if (major === 1) {
|
|
714
714
|
report.fail(
|
|
715
715
|
CX2_ISSUE.VERSION,
|
|
@@ -113,7 +113,7 @@ export interface Session {
|
|
|
113
113
|
* @category Plugin helpers
|
|
114
114
|
*/
|
|
115
115
|
export function urlDecode(text: string): string {
|
|
116
|
-
const plus = text.
|
|
116
|
+
const plus = text.replaceAll("+", " ");
|
|
117
117
|
try {
|
|
118
118
|
return decodeURIComponent(plus);
|
|
119
119
|
} catch {
|
|
@@ -282,13 +282,13 @@ function layoutOf(zipEntries: readonly ZipEntry[], report: ImportReportBuilder):
|
|
|
282
282
|
`the session has ${markers.length} version markers (${listed(markers)}); ${markers[0]} is used and the others are not read`,
|
|
283
283
|
);
|
|
284
284
|
}
|
|
285
|
-
|
|
285
|
+
const anchor = marker ?? cysession;
|
|
286
|
+
if (anchor === undefined) {
|
|
286
287
|
report.fail(
|
|
287
288
|
CYS_ISSUE.NOT_SESSION,
|
|
288
289
|
"the zip holds neither a session version marker (<x.y.z>.version) nor cysession.xml: it is not a Cytoscape session",
|
|
289
290
|
);
|
|
290
291
|
}
|
|
291
|
-
const anchor = marker ?? (cysession as string);
|
|
292
292
|
const root = anchor.slice(0, anchor.lastIndexOf("/") + 1);
|
|
293
293
|
let era: "2" | "3" = "2";
|
|
294
294
|
let version = "2.0.0";
|
|
@@ -377,6 +377,11 @@ class ExportPlan {
|
|
|
377
377
|
|
|
378
378
|
private readonly targetPort: Column | null;
|
|
379
379
|
|
|
380
|
+
/** The node and edge label role columns, written under `label` whatever their name. */
|
|
381
|
+
private readonly nodeLabel: Column | null;
|
|
382
|
+
|
|
383
|
+
private readonly edgeLabel: Column | null;
|
|
384
|
+
|
|
380
385
|
private readonly children: ChildrenCsr;
|
|
381
386
|
|
|
382
387
|
private readonly extraNotes: LossNote[] = [];
|
|
@@ -417,6 +422,8 @@ class ExportPlan {
|
|
|
417
422
|
this.folding = pairFolding(snapshot);
|
|
418
423
|
this.sourcePort = edges.byRole("sourcePort");
|
|
419
424
|
this.targetPort = edges.byRole("targetPort");
|
|
425
|
+
this.nodeLabel = nodes.byRole("label");
|
|
426
|
+
this.edgeLabel = edges.byRole("label");
|
|
420
427
|
this.nodeColumns = [...nodes].filter((c) => this.writesNodeColumn(c));
|
|
421
428
|
this.edgeColumns = [...edges].filter((c) => this.writesEdgeColumn(c));
|
|
422
429
|
this.graphColumns = [...graph].filter(
|
|
@@ -444,7 +451,7 @@ class ExportPlan {
|
|
|
444
451
|
this.clash("node", name, "the position role is written as pos");
|
|
445
452
|
return false;
|
|
446
453
|
}
|
|
447
|
-
return
|
|
454
|
+
return this.labelFree("node", column, this.nodeLabel);
|
|
448
455
|
}
|
|
449
456
|
|
|
450
457
|
/**
|
|
@@ -468,7 +475,23 @@ class ExportPlan {
|
|
|
468
475
|
this.clash("edge", name, "the weight is written as weight");
|
|
469
476
|
return false;
|
|
470
477
|
}
|
|
471
|
-
return
|
|
478
|
+
return this.labelFree("edge", column, this.edgeLabel);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Whether a column may be written: false (reported) for a plain `label` column when the label
|
|
483
|
+
* role column is named otherwise, since that one is written as `label`.
|
|
484
|
+
* @param domain - the table
|
|
485
|
+
* @param column - the column
|
|
486
|
+
* @param labelRole - the table's label role column, or null
|
|
487
|
+
* @returns true when written
|
|
488
|
+
*/
|
|
489
|
+
private labelFree(domain: string, column: Column, labelRole: Column | null): boolean {
|
|
490
|
+
if (column.meta.name !== LABEL_ATTRIBUTE || labelRole === null || labelRole === column) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
this.clash(domain, column.meta.name, "the label role is written as label");
|
|
494
|
+
return false;
|
|
472
495
|
}
|
|
473
496
|
|
|
474
497
|
/**
|
|
@@ -703,7 +726,7 @@ class ExportPlan {
|
|
|
703
726
|
}
|
|
704
727
|
for (const columns of [this.nodeColumns, this.edgeColumns, this.graphColumns]) {
|
|
705
728
|
for (const column of columns) {
|
|
706
|
-
visit(column
|
|
729
|
+
visit(this.attributeName(column), "attribute name");
|
|
707
730
|
if (column.dtype === "string" || column.dtype === "dict") {
|
|
708
731
|
for (let r = 0; r < column.length; r++) {
|
|
709
732
|
if (column.isSet(r)) {
|
|
@@ -844,6 +867,16 @@ class ExportPlan {
|
|
|
844
867
|
return parts.join(",");
|
|
845
868
|
}
|
|
846
869
|
|
|
870
|
+
/**
|
|
871
|
+
* The attribute a column is written as: `label` for a node or edge label role column (the
|
|
872
|
+
* importer gives that attribute the role back), the column name otherwise.
|
|
873
|
+
* @param column - the column
|
|
874
|
+
* @returns the attribute name
|
|
875
|
+
*/
|
|
876
|
+
private attributeName(column: Column): string {
|
|
877
|
+
return column === this.nodeLabel || column === this.edgeLabel ? LABEL_ATTRIBUTE : column.meta.name;
|
|
878
|
+
}
|
|
879
|
+
|
|
847
880
|
/**
|
|
848
881
|
* The `name=value` attributes of one row over a column list (set cells only).
|
|
849
882
|
* @param columns - the columns
|
|
@@ -858,7 +891,7 @@ class ExportPlan {
|
|
|
858
891
|
}
|
|
859
892
|
const text = cellText(column, row);
|
|
860
893
|
if (text !== null) {
|
|
861
|
-
out.push(`${quoteDotId(column
|
|
894
|
+
out.push(`${quoteDotId(this.attributeName(column))}=${writeId(text)}`);
|
|
862
895
|
}
|
|
863
896
|
}
|
|
864
897
|
return out;
|
|
@@ -514,7 +514,7 @@ function countGraphs(lexer: DotTokenizer, first: DotToken): number {
|
|
|
514
514
|
if (!isPunct(token, "{")) {
|
|
515
515
|
throw new DotSyntaxError(`expected "{" after the graph header, found ${describeToken(token)}`, token.line);
|
|
516
516
|
}
|
|
517
|
-
for (let depth = 1; depth > 0;
|
|
517
|
+
for (let depth = 1; depth > 0;) {
|
|
518
518
|
token = lexer.next();
|
|
519
519
|
if (token.kind === "eof") {
|
|
520
520
|
throw new DotSyntaxError('missing "}" at the end of a graph', token.line);
|
|
@@ -305,6 +305,25 @@ function versionText(text: string): string {
|
|
|
305
305
|
return NUMBER_TEXT.test(text) ? text : quoteGmlString(text);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* A node or edge table's label role column, which is written under "label" (the key the importer
|
|
310
|
+
* gives the role back to): the key is marked used so a plain column of that name cannot have it.
|
|
311
|
+
* @param columns - the written columns
|
|
312
|
+
* @param label - the table name
|
|
313
|
+
* @param used - the keys already taken, gaining "label" when there is a label role column
|
|
314
|
+
* @returns the label role column, or undefined
|
|
315
|
+
*/
|
|
316
|
+
function labelRoleColumn(columns: readonly Column[], label: string, used: Set<string>): Column | undefined {
|
|
317
|
+
if (label !== "node" && label !== "edge") {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
const column = columns.find((c) => c.meta.role === "label");
|
|
321
|
+
if (column !== undefined) {
|
|
322
|
+
used.add("label");
|
|
323
|
+
}
|
|
324
|
+
return column;
|
|
325
|
+
}
|
|
326
|
+
|
|
308
327
|
/**
|
|
309
328
|
* Select the written columns of a table and assign their keys, recording invalid and reserved
|
|
310
329
|
* keys as notes (or rewriting them under mangle).
|
|
@@ -326,14 +345,13 @@ function selectColumns(
|
|
|
326
345
|
): WrittenColumn[] {
|
|
327
346
|
const used = new Set<string>(reserved);
|
|
328
347
|
const out: WrittenColumn[] = [];
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const preferred = preferredKey(column.meta);
|
|
348
|
+
const columns = [...table].filter((c) => isWritten(c) && filter(c));
|
|
349
|
+
const labelRole = labelRoleColumn(columns, label, used);
|
|
350
|
+
for (const column of columns) {
|
|
351
|
+
const preferred = column === labelRole ? "label" : preferredKey(column.meta);
|
|
334
352
|
let key = preferred;
|
|
335
353
|
const valid = isGmlKey(preferred);
|
|
336
|
-
const taken = used.has(preferred);
|
|
354
|
+
const taken = column !== labelRole && used.has(preferred);
|
|
337
355
|
if (!valid || taken) {
|
|
338
356
|
const code = valid ? RESERVED_KEY_CODE : INVALID_KEY_CODE;
|
|
339
357
|
let why = "is not a GML key";
|
|
@@ -499,7 +499,7 @@ export function numberOfText(text: string): number {
|
|
|
499
499
|
if (NON_FINITE_TEXT.test(text)) {
|
|
500
500
|
const lower = text.toLowerCase();
|
|
501
501
|
if (lower.endsWith("nan")) {
|
|
502
|
-
return NaN;
|
|
502
|
+
return Number.NaN;
|
|
503
503
|
}
|
|
504
504
|
return lower.startsWith("-") ? -Infinity : Infinity;
|
|
505
505
|
}
|
|
@@ -536,7 +536,7 @@ export function isGmlKey(text: string): boolean {
|
|
|
536
536
|
* @category Plugin helpers
|
|
537
537
|
*/
|
|
538
538
|
export function mangleGmlKey(text: string): string {
|
|
539
|
-
const body = text.
|
|
539
|
+
const body = text.replaceAll(/\W/g, "_");
|
|
540
540
|
return /^[A-Za-z]/.test(body) ? body : `x${body}`;
|
|
541
541
|
}
|
|
542
542
|
|
|
@@ -102,7 +102,7 @@ export interface JsonShapeMeta {
|
|
|
102
102
|
*/
|
|
103
103
|
export function jsonShapeOf(snapshot: GraphSnapshot): JsonShapeMeta | null {
|
|
104
104
|
const shape = snapshot.meta.extra[META_KEY];
|
|
105
|
-
return isJsonObject(shape) ?
|
|
105
|
+
return isJsonObject(shape) ? shape : null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
@@ -1374,7 +1374,7 @@ function formatPoint(value: unknown): string {
|
|
|
1374
1374
|
if (typeof item === "number") {
|
|
1375
1375
|
parts.push(`${key}:${formatF64(item)}`);
|
|
1376
1376
|
} else if (typeof item === "string") {
|
|
1377
|
-
parts.push(`${key}:'${item.
|
|
1377
|
+
parts.push(`${key}:'${item.replaceAll("'", "")}'`);
|
|
1378
1378
|
} else {
|
|
1379
1379
|
return JSON.stringify(value) ?? "";
|
|
1380
1380
|
}
|
|
@@ -1514,7 +1514,7 @@ export function exportNeo4jFiles(
|
|
|
1514
1514
|
try {
|
|
1515
1515
|
return Promise.resolve(plan(snapshot, options).files());
|
|
1516
1516
|
} catch (err) {
|
|
1517
|
-
return Promise.reject(err
|
|
1517
|
+
return Promise.reject(err);
|
|
1518
1518
|
}
|
|
1519
1519
|
}
|
|
1520
1520
|
|
|
@@ -528,7 +528,7 @@ function firstModeOf(snapshot: GraphSnapshot): number | null {
|
|
|
528
528
|
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= snapshot.nodeCount) {
|
|
529
529
|
return value;
|
|
530
530
|
}
|
|
531
|
-
return NaN;
|
|
531
|
+
return Number.NaN;
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
/**
|
|
@@ -1607,7 +1607,7 @@ class PajekParser {
|
|
|
1607
1607
|
private extras(tokens: readonly string[], start: number, domain: "node" | "edge", line: number): RowExtras {
|
|
1608
1608
|
const extras: RowExtras = { keys: [], values: [], spells: null };
|
|
1609
1609
|
const seen = new Set<string>();
|
|
1610
|
-
for (let i = start; i < tokens.length;
|
|
1610
|
+
for (let i = start; i < tokens.length;) {
|
|
1611
1611
|
const token = tokens[i];
|
|
1612
1612
|
if (token.startsWith("[")) {
|
|
1613
1613
|
extras.spells = this.intervals(token, domain);
|
|
@@ -438,7 +438,7 @@ export function parseIntervals(token: string): [number, number][] {
|
|
|
438
438
|
}
|
|
439
439
|
const spells: [number, number][] = [];
|
|
440
440
|
for (const part of body.split(",")) {
|
|
441
|
-
const match = TIME_POINT.exec(part.
|
|
441
|
+
const match = TIME_POINT.exec(part.replaceAll(/\s+/g, ""));
|
|
442
442
|
if (match === null) {
|
|
443
443
|
throw new Error(`malformed time interval ${token}: "${part}" is not a-b, a-* or a`);
|
|
444
444
|
}
|
|
@@ -485,7 +485,7 @@ export function decodeCharacterReferences(text: string, onUnknown?: (reference:
|
|
|
485
485
|
if (!text.includes("&#")) {
|
|
486
486
|
return text;
|
|
487
487
|
}
|
|
488
|
-
return text.
|
|
488
|
+
return text.replaceAll(CHARACTER_REFERENCE, (whole, hex: string | undefined, dec: string | undefined) => {
|
|
489
489
|
const code = hex === undefined ? Number(dec) : Number.parseInt(hex, 16);
|
|
490
490
|
if (code <= 0x10ffff) {
|
|
491
491
|
return String.fromCodePoint(code);
|
|
@@ -506,7 +506,7 @@ export function encodeCharacterReferences(text: string): string {
|
|
|
506
506
|
if (!text.includes("&#")) {
|
|
507
507
|
return text;
|
|
508
508
|
}
|
|
509
|
-
return text.
|
|
509
|
+
return text.replaceAll(CHARACTER_REFERENCE, (whole) => `&${whole.slice(1)}`);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
/**
|
|
@@ -772,7 +772,7 @@ function valueText(value: unknown, dtype: string | null, escapes: boolean): stri
|
|
|
772
772
|
} else if (typeof value === "number") {
|
|
773
773
|
text = dtype === "f32" ? formatF32(value) : formatF64(value);
|
|
774
774
|
} else if (typeof value === "string") {
|
|
775
|
-
text = escapes ? value.
|
|
775
|
+
text = escapes ? value.replaceAll("\n", String.raw`\n`).replaceAll("\t", String.raw`\t`) : value;
|
|
776
776
|
} else if (ArrayBuffer.isView(value)) {
|
|
777
777
|
text = JSON.stringify(Array.from(value as unknown as ArrayLike<number>));
|
|
778
778
|
} else {
|
|
@@ -187,5 +187,5 @@ export function parseScalar(text: string, kind: ScalarKind, unescape: boolean):
|
|
|
187
187
|
* @returns the text with `\n` and `\t` decoded
|
|
188
188
|
*/
|
|
189
189
|
function unescapeCytoscape(text: string): string {
|
|
190
|
-
return text.includes("\\") ? text.
|
|
190
|
+
return text.includes("\\") ? text.replaceAll(String.raw`\t`, "\t").replaceAll(String.raw`\n`, "\n") : text;
|
|
191
191
|
}
|
package/src/registry.ts
CHANGED
|
@@ -421,16 +421,15 @@ export class FormatRegistry {
|
|
|
421
421
|
): Promise<ImportGraphResult> {
|
|
422
422
|
const opts = options as ImportGraphOptions;
|
|
423
423
|
const picked = await this.choose(input, opts);
|
|
424
|
-
const unknown = this.unknownOptions(opts).map(
|
|
425
|
-
(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}),
|
|
424
|
+
const unknown = this.unknownOptions(opts).map((name): ImportIssue =>
|
|
425
|
+
Object.freeze({
|
|
426
|
+
category: "unsupported",
|
|
427
|
+
severity: "warning",
|
|
428
|
+
code: UNKNOWN_OPTION_CODE,
|
|
429
|
+
message: unknownOptionText(name),
|
|
430
|
+
line: null,
|
|
431
|
+
element: name,
|
|
432
|
+
}),
|
|
434
433
|
);
|
|
435
434
|
const chosen = unknown.length === 0 ? picked : { ...picked, warnings: [...picked.warnings, ...unknown] };
|
|
436
435
|
const { importer } = chosen;
|
package/src/sniff.ts
CHANGED
|
@@ -32,19 +32,7 @@ import { type GraphImporter } from "./types.js";
|
|
|
32
32
|
* @category Formats and detection
|
|
33
33
|
*/
|
|
34
34
|
export type GraphFormatName =
|
|
35
|
-
| "
|
|
36
|
-
| "graphml"
|
|
37
|
-
| "gml"
|
|
38
|
-
| "dot"
|
|
39
|
-
| "pajek"
|
|
40
|
-
| "csv"
|
|
41
|
-
| "json"
|
|
42
|
-
| "neo4j"
|
|
43
|
-
| "xgmml"
|
|
44
|
-
| "cx2"
|
|
45
|
-
| "cx"
|
|
46
|
-
| "obo"
|
|
47
|
-
| "cys";
|
|
35
|
+
"gexf" | "graphml" | "gml" | "dot" | "pajek" | "csv" | "json" | "neo4j" | "xgmml" | "cx2" | "cx" | "obo" | "cys";
|
|
48
36
|
|
|
49
37
|
/**
|
|
50
38
|
* A format name: one of the built-in names (with editor autocomplete) or the name of a format you
|
|
@@ -345,14 +333,7 @@ export function sniffJsonDialectHead(head: Uint8Array | string): JsonImportDiale
|
|
|
345
333
|
|
|
346
334
|
/** Where a key was seen while scanning a truncated head. */
|
|
347
335
|
type KeyPath =
|
|
348
|
-
| ""
|
|
349
|
-
| "graph"
|
|
350
|
-
| "options"
|
|
351
|
-
| "nodes[0]"
|
|
352
|
-
| "edges[0]"
|
|
353
|
-
| "links[0]"
|
|
354
|
-
| "graphs[0].nodes[]"
|
|
355
|
-
| "graphs[0].edges[]";
|
|
336
|
+
"" | "graph" | "options" | "nodes[0]" | "edges[0]" | "links[0]" | "graphs[0].nodes[]" | "graphs[0].edges[]";
|
|
356
337
|
|
|
357
338
|
/**
|
|
358
339
|
* A partial document rebuilt from the keys a truncated head reveals: every key gets a placeholder
|
package/src/types.ts
CHANGED
|
@@ -458,13 +458,7 @@ export interface GraphExporter<Opts = unknown> {
|
|
|
458
458
|
* @category Reports and errors
|
|
459
459
|
*/
|
|
460
460
|
export type IssueCategory =
|
|
461
|
-
| "
|
|
462
|
-
| "missing-value"
|
|
463
|
-
| "validation-error"
|
|
464
|
-
| "unsupported"
|
|
465
|
-
| "precision"
|
|
466
|
-
| "coercion"
|
|
467
|
-
| "merged";
|
|
461
|
+
"parse-error" | "missing-value" | "validation-error" | "unsupported" | "precision" | "coercion" | "merged";
|
|
468
462
|
|
|
469
463
|
/**
|
|
470
464
|
* One problem found while importing.
|