@graphty/graph-io 0.3.9 → 0.3.11

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 (125) hide show
  1. package/README.md +29 -5
  2. package/dist/chunks/{escape-BjmIaFFo.js → escape-D1f9-cwf.js} +4 -4
  3. package/dist/chunks/{escape-BjmIaFFo.js.map → escape-D1f9-cwf.js.map} +1 -1
  4. package/dist/chunks/{importer-BddBW3sf.js → importer-9PxhqH4v.js} +336 -92
  5. package/dist/chunks/importer-9PxhqH4v.js.map +1 -0
  6. package/dist/chunks/{importer-DfI8POXb.js → importer-B8lsjFWx.js} +6 -5
  7. package/dist/chunks/importer-B8lsjFWx.js.map +1 -0
  8. package/dist/chunks/{importer-BtHFICnf.js → importer-C3HcYWAn.js} +5 -3
  9. package/dist/chunks/importer-C3HcYWAn.js.map +1 -0
  10. package/dist/chunks/{importer-BOxwmef_.js → importer-C7mnGdr_.js} +125 -8
  11. package/dist/chunks/importer-C7mnGdr_.js.map +1 -0
  12. package/dist/chunks/{records-BKSMowhR.js → records-IHsCfv7s.js} +20 -7
  13. package/dist/chunks/records-IHsCfv7s.js.map +1 -0
  14. package/dist/chunks/{writer-BdMak4_J.js → writer-BtWpUaiH.js} +4 -4
  15. package/dist/chunks/{writer-BdMak4_J.js.map → writer-BtWpUaiH.js.map} +1 -1
  16. package/dist/csv.js +239 -18
  17. package/dist/csv.js.map +1 -1
  18. package/dist/dot.js +1 -1
  19. package/dist/gexf.js +72 -33
  20. package/dist/gexf.js.map +1 -1
  21. package/dist/gml.js +44 -23
  22. package/dist/gml.js.map +1 -1
  23. package/dist/graph-io.js +11 -11
  24. package/dist/graphml.js +1 -1
  25. package/dist/json.js +1 -1
  26. package/dist/neo4j.js +5 -4
  27. package/dist/neo4j.js.map +1 -1
  28. package/dist/pajek.js +1 -1
  29. package/dist/src/common/export.js +1 -1
  30. package/dist/src/common/export.js.map +1 -1
  31. package/dist/src/formats/csv/exporter.d.ts +15 -4
  32. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  33. package/dist/src/formats/csv/exporter.js +125 -6
  34. package/dist/src/formats/csv/exporter.js.map +1 -1
  35. package/dist/src/formats/csv/importer.d.ts +36 -4
  36. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  37. package/dist/src/formats/csv/importer.js +169 -8
  38. package/dist/src/formats/csv/importer.js.map +1 -1
  39. package/dist/src/formats/csv/records.d.ts +14 -1
  40. package/dist/src/formats/csv/records.d.ts.map +1 -1
  41. package/dist/src/formats/csv/records.js +28 -7
  42. package/dist/src/formats/csv/records.js.map +1 -1
  43. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  44. package/dist/src/formats/dot/exporter.js +2 -0
  45. package/dist/src/formats/dot/exporter.js.map +1 -1
  46. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  47. package/dist/src/formats/gexf/exporter.js +36 -10
  48. package/dist/src/formats/gexf/exporter.js.map +1 -1
  49. package/dist/src/formats/gexf/importer.d.ts +9 -4
  50. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  51. package/dist/src/formats/gexf/importer.js +44 -21
  52. package/dist/src/formats/gexf/importer.js.map +1 -1
  53. package/dist/src/formats/gexf/index.d.ts +1 -1
  54. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  55. package/dist/src/formats/gexf/index.js +1 -1
  56. package/dist/src/formats/gexf/index.js.map +1 -1
  57. package/dist/src/formats/gexf/schema.d.ts +2 -0
  58. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  59. package/dist/src/formats/gexf/schema.js +18 -0
  60. package/dist/src/formats/gexf/schema.js.map +1 -1
  61. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  62. package/dist/src/formats/gml/exporter.js +1 -0
  63. package/dist/src/formats/gml/exporter.js.map +1 -1
  64. package/dist/src/formats/gml/importer.d.ts +7 -4
  65. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  66. package/dist/src/formats/gml/importer.js +32 -19
  67. package/dist/src/formats/gml/importer.js.map +1 -1
  68. package/dist/src/formats/gml/index.d.ts +3 -1
  69. package/dist/src/formats/gml/index.d.ts.map +1 -1
  70. package/dist/src/formats/gml/index.js +4 -2
  71. package/dist/src/formats/gml/index.js.map +1 -1
  72. package/dist/src/formats/graphml/constants.d.ts +2 -0
  73. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  74. package/dist/src/formats/graphml/constants.js +2 -0
  75. package/dist/src/formats/graphml/constants.js.map +1 -1
  76. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  77. package/dist/src/formats/graphml/exporter.js +14 -2
  78. package/dist/src/formats/graphml/exporter.js.map +1 -1
  79. package/dist/src/formats/graphml/importer.d.ts +2 -1
  80. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  81. package/dist/src/formats/graphml/importer.js +91 -27
  82. package/dist/src/formats/graphml/importer.js.map +1 -1
  83. package/dist/src/formats/graphml/yfiles.d.ts +62 -0
  84. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -0
  85. package/dist/src/formats/graphml/yfiles.js +269 -0
  86. package/dist/src/formats/graphml/yfiles.js.map +1 -0
  87. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/json/exporter.js +9 -1
  89. package/dist/src/formats/json/exporter.js.map +1 -1
  90. package/dist/src/formats/json/importer.d.ts +14 -0
  91. package/dist/src/formats/json/importer.d.ts.map +1 -1
  92. package/dist/src/formats/json/importer.js +165 -4
  93. package/dist/src/formats/json/importer.js.map +1 -1
  94. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  95. package/dist/src/formats/neo4j/exporter.js +1 -0
  96. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  97. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  98. package/dist/src/formats/pajek/exporter.js +3 -2
  99. package/dist/src/formats/pajek/exporter.js.map +1 -1
  100. package/package.json +2 -2
  101. package/src/common/export.ts +1 -1
  102. package/src/formats/csv/exporter.ts +140 -10
  103. package/src/formats/csv/importer.ts +207 -18
  104. package/src/formats/csv/records.ts +41 -6
  105. package/src/formats/dot/exporter.ts +2 -0
  106. package/src/formats/gexf/exporter.ts +40 -11
  107. package/src/formats/gexf/importer.ts +47 -25
  108. package/src/formats/gexf/index.ts +1 -1
  109. package/src/formats/gexf/schema.ts +18 -0
  110. package/src/formats/gml/exporter.ts +1 -0
  111. package/src/formats/gml/importer.ts +47 -24
  112. package/src/formats/gml/index.ts +4 -1
  113. package/src/formats/graphml/constants.ts +2 -0
  114. package/src/formats/graphml/exporter.ts +21 -2
  115. package/src/formats/graphml/importer.ts +99 -28
  116. package/src/formats/graphml/yfiles.ts +292 -0
  117. package/src/formats/json/exporter.ts +9 -1
  118. package/src/formats/json/importer.ts +202 -4
  119. package/src/formats/neo4j/exporter.ts +1 -0
  120. package/src/formats/pajek/exporter.ts +3 -1
  121. package/dist/chunks/importer-BOxwmef_.js.map +0 -1
  122. package/dist/chunks/importer-BddBW3sf.js.map +0 -1
  123. package/dist/chunks/importer-BtHFICnf.js.map +0 -1
  124. package/dist/chunks/importer-DfI8POXb.js.map +0 -1
  125. package/dist/chunks/records-BKSMowhR.js.map +0 -1
package/dist/dot.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, a, d, b } from "./chunks/importer-BtHFICnf.js";
1
+ import { D, a, d, b } from "./chunks/importer-C3HcYWAn.js";
2
2
  export {
3
3
  D as DOT_ISSUE,
4
4
  a as DOT_LOSS,
package/dist/gexf.js CHANGED
@@ -1,6 +1,6 @@
1
- import { aM as TIME_TEXT_ROLE, aN as TIME_TEXT_SUFFIX, aF as resolveImportOptions, I as ImportReportBuilder, aC as reportSinkOptions, aD as reportUnusedOptions, t as textChunks, aa as XML_SYNTAX_CODE, a as throwIfAborted, v as IdCoercer, l as DirectionResolver, _ as STRAY_TEXT_CODE, K as NO_GRAPH_CODE, d as COUNT_HINT_CODE, aw as isWeightField, aO as declareAttribute, ai as declareResolved, y as MISSING_ID_CODE, k as DUPLICATE_NODE_CODE, a6 as UNKNOWN_PARENT_CODE, M as MISSING_ENDPOINT_CODE, i as DUPLICATE_EDGE_ID_CODE, aA as parseWeightText, aP as parseDeclaredTemporal, ah as declareCompanion, aQ as parseDeclaredValue, aR as losesPrecision, T as PRECISION_CODE, aS as parseTimeText, aT as splitListText, a4 as UNKNOWN_ELEMENT_CODE, o as ID_MERGED_CODE, aU as timeTextCompanion, aV as parseDecimalText, ac as capabilities, L as LOSS, ax as joinText, ak as encodeChunks, aG as sanitizeIds, az as pairFolding, al as explicitWeights, aE as resolveExportOptions, ad as checkCapabilities, aW as formatTimeValue, an as formatF32, ao as formatF64, aX as mapDeclaredType, aY as joinListText, aZ as formatTemporal, aq as formatInteger, at as isCanonicalIntegerText, z as MIXED_DIRECTION_CODE, g as DIRECTION_FORCED_CODE, h as DIRECTION_REFUSED_CODE, Y as SINK_OPTION_CODE, P as OPTION_IGNORED_CODE, X as ROLE_TAKEN_CODE, C as COLUMN_RENAMED_CODE, b as BAD_OPTIONS_CODE, B as BAD_DEFAULT_CODE, a3 as UNKNOWN_ATTR_TYPE_CODE, a5 as UNKNOWN_ENCODING_CODE, n as ENCODING_FALLBACK_CODE, s as INVALID_ENCODING_CODE, u as INVALID_UTF8_CODE } from "./chunks/writer-BdMak4_J.js";
1
+ import { aM as TIME_TEXT_ROLE, aN as TIME_TEXT_SUFFIX, aF as resolveImportOptions, I as ImportReportBuilder, aC as reportSinkOptions, aD as reportUnusedOptions, t as textChunks, aa as XML_SYNTAX_CODE, a as throwIfAborted, v as IdCoercer, l as DirectionResolver, _ as STRAY_TEXT_CODE, K as NO_GRAPH_CODE, d as COUNT_HINT_CODE, aw as isWeightField, aO as declareAttribute, ai as declareResolved, y as MISSING_ID_CODE, k as DUPLICATE_NODE_CODE, a6 as UNKNOWN_PARENT_CODE, M as MISSING_ENDPOINT_CODE, i as DUPLICATE_EDGE_ID_CODE, aA as parseWeightText, aP as parseDeclaredTemporal, ah as declareCompanion, aQ as parseDeclaredValue, aR as losesPrecision, T as PRECISION_CODE, aS as parseTimeText, aT as splitListText, a4 as UNKNOWN_ELEMENT_CODE, o as ID_MERGED_CODE, aU as timeTextCompanion, aV as parseDecimalText, ac as capabilities, L as LOSS, ax as joinText, ak as encodeChunks, aG as sanitizeIds, az as pairFolding, al as explicitWeights, aE as resolveExportOptions, ad as checkCapabilities, aW as formatTimeValue, an as formatF32, ao as formatF64, aX as mapDeclaredType, aY as joinListText, aZ as formatTemporal, aq as formatInteger, at as isCanonicalIntegerText, z as MIXED_DIRECTION_CODE, g as DIRECTION_FORCED_CODE, h as DIRECTION_REFUSED_CODE, Y as SINK_OPTION_CODE, P as OPTION_IGNORED_CODE, X as ROLE_TAKEN_CODE, C as COLUMN_RENAMED_CODE, b as BAD_OPTIONS_CODE, B as BAD_DEFAULT_CODE, a3 as UNKNOWN_ATTR_TYPE_CODE, a5 as UNKNOWN_ENCODING_CODE, n as ENCODING_FALLBACK_CODE, s as INVALID_ENCODING_CODE, u as INVALID_UTF8_CODE } from "./chunks/writer-BtWpUaiH.js";
2
2
  import { GraphFormatError, INVALID_INDEX, MAX_COUNT } from "@graphty/graph-format";
3
- import { t as tokenizeXml, d as xmlDeclaredEncoding, X as XmlSyntaxError, l as localName, e as isWhitespace, x as xmlIllegalTextNotes, f as escapeXmlText, g as escapeXmlAttribute } from "./chunks/escape-BjmIaFFo.js";
3
+ import { t as tokenizeXml, d as xmlDeclaredEncoding, X as XmlSyntaxError, l as localName, e as isWhitespace, x as xmlIllegalTextNotes, f as escapeXmlText, g as escapeXmlAttribute } from "./chunks/escape-D1f9-cwf.js";
4
4
  const GEXF_FORMAT = "gexf";
5
5
  const GEXF_NAMESPACES = {
6
6
  "1.2": { gexf: "http://www.gexf.net/1.2draft", viz: "http://www.gexf.net/1.2draft/viz" },
@@ -20,6 +20,7 @@ const NODE_COLUMNS = Object.freeze({
20
20
  end: "end",
21
21
  timestamp: "timestamp",
22
22
  spells: "spells",
23
+ spellsOpen: "spells.open",
23
24
  timestamps: "timestamps",
24
25
  open: "open",
25
26
  position: "position",
@@ -36,6 +37,7 @@ const EDGE_COLUMNS = Object.freeze({
36
37
  end: "end",
37
38
  timestamp: "timestamp",
38
39
  spells: "spells",
40
+ spellsOpen: "spells.open",
39
41
  timestamps: "timestamps",
40
42
  open: "open",
41
43
  color: "color",
@@ -94,6 +96,14 @@ const NODE_DECLS = {
94
96
  origin: XML_ORIGIN
95
97
  },
96
98
  open: { name: NODE_COLUMNS.open, dtype: "u8", role: "open", nullable: true, origin: XML_ORIGIN },
99
+ spellsOpen: {
100
+ name: NODE_COLUMNS.spellsOpen,
101
+ dtype: "list",
102
+ itemDtype: "u8",
103
+ role: "spellsOpen",
104
+ nullable: true,
105
+ origin: XML_ORIGIN
106
+ },
97
107
  position: {
98
108
  name: NODE_COLUMNS.position,
99
109
  dtype: "f32",
@@ -141,6 +151,14 @@ const EDGE_DECLS = {
141
151
  origin: XML_ORIGIN
142
152
  },
143
153
  open: { name: EDGE_COLUMNS.open, dtype: "u8", role: "open", nullable: true, origin: XML_ORIGIN },
154
+ spellsOpen: {
155
+ name: EDGE_COLUMNS.spellsOpen,
156
+ dtype: "list",
157
+ itemDtype: "u8",
158
+ role: "spellsOpen",
159
+ nullable: true,
160
+ origin: XML_ORIGIN
161
+ },
144
162
  color: {
145
163
  name: EDGE_COLUMNS.color,
146
164
  dtype: "f32",
@@ -268,6 +286,9 @@ const SPELL_OPEN_CODE = "W_GEXF_SPELL_OPEN_DROPPED";
268
286
  const VIZ_VALUE_CODE = "W_GEXF_VIZ_VALUE";
269
287
  const OPEN_BOUND_CONFLICT_CODE = "W_GEXF_OPEN_BOUND_CONFLICT";
270
288
  const EDGE_TYPES = /* @__PURE__ */ new Set(["directed", "undirected", "mutual"]);
289
+ function edgeTypeKeyword(text) {
290
+ return text.trim().toLowerCase();
291
+ }
271
292
  const ABORT_CHECK_INTERVAL = 64;
272
293
  const NODE_ATTRIBUTES = /* @__PURE__ */ new Set([
273
294
  "id",
@@ -433,6 +454,7 @@ class GexfReader {
433
454
  this.attributeText = null;
434
455
  this.attributeTextValue = "";
435
456
  this.spellPairs = null;
457
+ this.spellOpen = [];
436
458
  this.spellsWhere = null;
437
459
  this.parentIds = null;
438
460
  this.nodesSeen = false;
@@ -450,6 +472,7 @@ class GexfReader {
450
472
  end: new TemporalColumn("node", NODE_DECLS.end),
451
473
  timestamp: new TemporalColumn("node", NODE_DECLS.timestamp),
452
474
  spells: new LazyColumn("node", NODE_DECLS.spells),
475
+ spellsOpen: new LazyColumn("node", NODE_DECLS.spellsOpen),
453
476
  timestamps: new LazyColumn("node", NODE_DECLS.timestamps),
454
477
  open: new LazyColumn("node", NODE_DECLS.open),
455
478
  color: new LazyColumn("node", NODE_DECLS.color),
@@ -461,6 +484,7 @@ class GexfReader {
461
484
  end: new TemporalColumn("edge", EDGE_DECLS.end),
462
485
  timestamp: new TemporalColumn("edge", EDGE_DECLS.timestamp),
463
486
  spells: new LazyColumn("edge", EDGE_DECLS.spells),
487
+ spellsOpen: new LazyColumn("edge", EDGE_DECLS.spellsOpen),
464
488
  timestamps: new LazyColumn("edge", EDGE_DECLS.timestamps),
465
489
  open: new LazyColumn("edge", EDGE_DECLS.open),
466
490
  color: new LazyColumn("edge", EDGE_DECLS.color),
@@ -716,8 +740,8 @@ class GexfReader {
716
740
  const edgeType2 = attrs.get("defaultedgetype");
717
741
  if (edgeType2 === void 0) {
718
742
  this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
719
- } else if (EDGE_TYPES.has(edgeType2)) {
720
- this.defaultKind = edgeType2;
743
+ } else if (EDGE_TYPES.has(edgeTypeKeyword(edgeType2))) {
744
+ this.defaultKind = edgeTypeKeyword(edgeType2);
721
745
  } else {
722
746
  this.defaultKind = this.options.defaultDirected ? "directed" : "undirected";
723
747
  this.warnHeader("defaultedgetype", edgeType2, where);
@@ -758,7 +782,7 @@ class GexfReader {
758
782
  }
759
783
  }
760
784
  const extra = {};
761
- for (const key of ["start", "end", "timestamp"]) {
785
+ for (const key of ["defaultedgetype", "start", "end", "timestamp"]) {
762
786
  const value = attrs.get(key);
763
787
  if (value !== void 0) {
764
788
  extra[key] = value;
@@ -1186,6 +1210,7 @@ class GexfReader {
1186
1210
  return;
1187
1211
  case "spells":
1188
1212
  this.spellPairs = [];
1213
+ this.spellOpen = [];
1189
1214
  this.spellsWhere = frame.where;
1190
1215
  this.ctx.push(
1191
1216
  11
@@ -1347,13 +1372,14 @@ class GexfReader {
1347
1372
  return;
1348
1373
  }
1349
1374
  let kind = this.defaultKind;
1350
- const type = attrs.get("type");
1351
- if (type !== void 0) {
1375
+ const typeText = attrs.get("type");
1376
+ if (typeText !== void 0) {
1377
+ const type = edgeTypeKeyword(typeText);
1352
1378
  if (!EDGE_TYPES.has(type)) {
1353
1379
  report.error(
1354
1380
  "validation-error",
1355
1381
  EDGE_TYPE_CODE,
1356
- `edge type "${type}" is not directed, undirected or mutual`,
1382
+ `edge type "${typeText}" is not directed, undirected or mutual`,
1357
1383
  where
1358
1384
  );
1359
1385
  report.counts.skippedEdges++;
@@ -1468,6 +1494,7 @@ class GexfReader {
1468
1494
  return;
1469
1495
  case "spells":
1470
1496
  this.spellPairs = [];
1497
+ this.spellOpen = [];
1471
1498
  this.spellsWhere = edge.where;
1472
1499
  this.ctx.push(
1473
1500
  11
@@ -1875,35 +1902,33 @@ class GexfReader {
1875
1902
  const where = this.spellsWhere ?? { line };
1876
1903
  try {
1877
1904
  const bounds = this.readBounds(attrs, where);
1878
- if (bounds.open !== 0) {
1879
- this.report.warnOnce(
1880
- "unsupported",
1881
- SPELL_OPEN_CODE,
1882
- "startopen / endopen on a <spell> cannot be kept; the spell is stored closed",
1883
- where
1884
- );
1885
- }
1886
1905
  this.spellPairs?.push([bounds.start.value, bounds.end.value]);
1906
+ this.spellOpen.push(bounds.open);
1887
1907
  } catch (err) {
1888
1908
  this.report.recordError(err, where);
1889
1909
  }
1890
1910
  }
1891
- /** Close `<spells>`: write the pairs into the open element's spells column. */
1911
+ /**
1912
+ * Close `<spells>`: write the pairs into the open element's spells column, and their open bits
1913
+ * into the spells.open column when any spell has an open bound.
1914
+ */
1892
1915
  finishSpells() {
1893
1916
  const pairs = this.spellPairs;
1917
+ const open = this.spellOpen;
1894
1918
  const where = this.spellsWhere ?? void 0;
1895
1919
  this.spellPairs = null;
1920
+ this.spellOpen = [];
1896
1921
  this.spellsWhere = null;
1897
1922
  if (pairs === null) {
1898
1923
  return;
1899
1924
  }
1900
1925
  try {
1901
- if (this.edge !== null) {
1902
- this.edgeColumns.spells.set(this.sink, this.report, this.edge.index, pairs, where);
1903
- } else {
1904
- const frame = this.nodeStack[this.nodeStack.length - 1];
1905
- if (frame.index !== INVALID_INDEX) {
1906
- this.nodeColumns.spells.set(this.sink, this.report, frame.index, pairs, where);
1926
+ const columns = this.edge === null ? this.nodeColumns : this.edgeColumns;
1927
+ const { index } = this.edge ?? this.nodeStack[this.nodeStack.length - 1];
1928
+ if (index !== INVALID_INDEX) {
1929
+ columns.spells.set(this.sink, this.report, index, pairs, where);
1930
+ if (open.some((bits) => bits !== 0)) {
1931
+ columns.spellsOpen.set(this.sink, this.report, index, open, where);
1907
1932
  }
1908
1933
  }
1909
1934
  } catch (err) {
@@ -2364,6 +2389,8 @@ function roleShapeOk(role, column, domain) {
2364
2389
  return domain === "edge" && isNumericScalar(column);
2365
2390
  case "spells":
2366
2391
  return isNumericList(column, 2);
2392
+ case "spellsOpen":
2393
+ return column.dtype === "list" && column.meta.itemDtype === "u8";
2367
2394
  case "timestamps":
2368
2395
  return isNumericList(column, 1);
2369
2396
  case "position":
@@ -2387,6 +2414,7 @@ const MAPPED_ROLES = /* @__PURE__ */ new Set([
2387
2414
  "end",
2388
2415
  "timestamp",
2389
2416
  "spells",
2417
+ "spellsOpen",
2390
2418
  "timestamps",
2391
2419
  "open",
2392
2420
  "position",
@@ -2405,6 +2433,7 @@ const ROLE_NAMES = Object.freeze({
2405
2433
  end: NODE_COLUMNS.end,
2406
2434
  timestamp: NODE_COLUMNS.timestamp,
2407
2435
  spells: NODE_COLUMNS.spells,
2436
+ spellsOpen: NODE_COLUMNS.spellsOpen,
2408
2437
  timestamps: NODE_COLUMNS.timestamps,
2409
2438
  open: NODE_COLUMNS.open,
2410
2439
  position: NODE_COLUMNS.position,
@@ -2425,6 +2454,7 @@ function collectRoles(table, domain, note) {
2425
2454
  end: null,
2426
2455
  timestamp: null,
2427
2456
  spells: null,
2457
+ spellsOpen: null,
2428
2458
  timestamps: null,
2429
2459
  open: null,
2430
2460
  position: null,
@@ -2953,14 +2983,15 @@ function lifetimeAttrs(roles, row, plan) {
2953
2983
  function spellsElement(roles, row, plan, indent) {
2954
2984
  const pairs = [];
2955
2985
  if (roles.spells !== null && roles.spells.isSet(row)) {
2956
- for (const pair of listItems(roles.spells, row)) {
2986
+ const open = plan.version === "1.2" && roles.spellsOpen !== null && roles.spellsOpen.isSet(row) ? listItems(roles.spellsOpen, row) : [];
2987
+ listItems(roles.spells, row).forEach((pair, i) => {
2957
2988
  const [s, e] = Array.from(pair);
2958
- pairs.push([s, e]);
2959
- }
2989
+ pairs.push([s, e, open[i] ?? 0]);
2990
+ });
2960
2991
  }
2961
2992
  if (plan.version === "1.2" && roles.timestamps !== null && roles.timestamps.isSet(row)) {
2962
2993
  for (const t of listItems(roles.timestamps, row)) {
2963
- pairs.push([t, t]);
2994
+ pairs.push([t, t, 0]);
2964
2995
  }
2965
2996
  }
2966
2997
  if (pairs.length === 0) {
@@ -2968,13 +2999,15 @@ function spellsElement(roles, row, plan, indent) {
2968
2999
  }
2969
3000
  let out = `${indent}<spells>
2970
3001
  `;
2971
- for (const [s, e] of pairs) {
3002
+ for (const [s, e, open] of pairs) {
2972
3003
  let attrs = "";
2973
3004
  if (Number.isFinite(s)) {
2974
- attrs += ` start="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
3005
+ const name = (open & OPEN_START) === 0 ? "start" : "startopen";
3006
+ attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(s, plan.timeFormat))}"`;
2975
3007
  }
2976
3008
  if (Number.isFinite(e)) {
2977
- attrs += ` end="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
3009
+ const name = (open & OPEN_END) === 0 ? "end" : "endopen";
3010
+ attrs += ` ${name}="${escapeXmlAttribute(formatTimeValue(e, plan.timeFormat))}"`;
2978
3011
  }
2979
3012
  out += `${indent} <spell${attrs}/>
2980
3013
  `;
@@ -3240,7 +3273,7 @@ ${body} </node>
3240
3273
  const edgeList = snapshot.edgeList();
3241
3274
  const folding = pairFolding(snapshot, { foldMutual: true });
3242
3275
  const weights = explicitWeights(snapshot);
3243
- const defaultType = snapshot.directed ? "directed" : "undirected";
3276
+ const defaultType = defaultEdgeType(snapshot);
3244
3277
  let written = 0;
3245
3278
  for (let e = 0; e < snapshot.edgeCount; e++) {
3246
3279
  if (edgeType(snapshot, e, folding) !== null) {
@@ -3312,7 +3345,7 @@ ${body} </meta>
3312
3345
  }
3313
3346
  function graphStart(snapshot, plan) {
3314
3347
  const { meta } = snapshot;
3315
- let attrs = ` defaultedgetype="${snapshot.directed ? "directed" : "undirected"}"`;
3348
+ let attrs = ` defaultedgetype="${defaultEdgeType(snapshot)}"`;
3316
3349
  let mode = meta.mode ?? "static";
3317
3350
  if (plan.temporal && mode === "static") {
3318
3351
  mode = "dynamic";
@@ -3360,6 +3393,12 @@ function graphStart(snapshot, plan) {
3360
3393
  return ` <graph${attrs}>
3361
3394
  `;
3362
3395
  }
3396
+ function defaultEdgeType(snapshot) {
3397
+ if (!snapshot.directed) {
3398
+ return "undirected";
3399
+ }
3400
+ return graphExtraText(snapshot, "defaultedgetype")?.trim().toLowerCase() === "mutual" ? "mutual" : "directed";
3401
+ }
3363
3402
  function graphExtraText(snapshot, key) {
3364
3403
  const gexfExtra = snapshot.meta.extra.gexf;
3365
3404
  if (typeof gexfExtra !== "object" || gexfExtra === null) {
@@ -3467,7 +3506,7 @@ const GEXF_ISSUE = Object.freeze({
3467
3506
  VIZ_SKIPPED: VIZ_SKIPPED_CODE,
3468
3507
  /** A 1.2 dynamic viz element whose bounds were dropped. */
3469
3508
  VIZ_DYNAMIC_DROPPED: VIZ_DYNAMIC_CODE,
3470
- /** A 1.2 startopen / endopen spell stored closed. */
3509
+ /** Deprecated and no longer recorded: open spells are kept in the spells.open column. */
3471
3510
  SPELL_OPEN_DROPPED: SPELL_OPEN_CODE,
3472
3511
  /** A viz value that could not be read. */
3473
3512
  VIZ_VALUE: VIZ_VALUE_CODE,