@graphty/graph-io 0.0.0 → 0.2.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 (339) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -28
  3. package/dist/chunks/children-CL3Cy0ez.js +238 -0
  4. package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
  5. package/dist/chunks/escape-DyI8JofU.js +938 -0
  6. package/dist/chunks/escape-DyI8JofU.js.map +1 -0
  7. package/dist/chunks/importer-CQnJuWJw.js +2987 -0
  8. package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
  9. package/dist/chunks/importer-CpCpfbxr.js +2015 -0
  10. package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
  11. package/dist/chunks/importer-DbnGYr3_.js +2342 -0
  12. package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
  13. package/dist/chunks/importer-GozH8DkN.js +3050 -0
  14. package/dist/chunks/importer-GozH8DkN.js.map +1 -0
  15. package/dist/chunks/records-CGpxszm1.js +605 -0
  16. package/dist/chunks/records-CGpxszm1.js.map +1 -0
  17. package/dist/chunks/text-CajMdVFy.js +189 -0
  18. package/dist/chunks/text-CajMdVFy.js.map +1 -0
  19. package/dist/chunks/writer-DxSKC7TL.js +2842 -0
  20. package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
  21. package/dist/csv.d.ts +1 -0
  22. package/dist/csv.js +1702 -0
  23. package/dist/csv.js.map +1 -0
  24. package/dist/dot.d.ts +1 -0
  25. package/dist/dot.js +8 -0
  26. package/dist/dot.js.map +1 -0
  27. package/dist/gexf.d.ts +1 -0
  28. package/dist/gexf.js +3466 -0
  29. package/dist/gexf.js.map +1 -0
  30. package/dist/gml.d.ts +1 -0
  31. package/dist/gml.js +2647 -0
  32. package/dist/gml.js.map +1 -0
  33. package/dist/graph-io.d.ts +1 -0
  34. package/dist/graph-io.js +790 -0
  35. package/dist/graph-io.js.map +1 -0
  36. package/dist/graphml.d.ts +1 -0
  37. package/dist/graphml.js +8 -0
  38. package/dist/graphml.js.map +1 -0
  39. package/dist/json.d.ts +1 -0
  40. package/dist/json.js +11 -0
  41. package/dist/json.js.map +1 -0
  42. package/dist/neo4j.d.ts +1 -0
  43. package/dist/neo4j.js +2046 -0
  44. package/dist/neo4j.js.map +1 -0
  45. package/dist/pajek.d.ts +1 -0
  46. package/dist/pajek.js +8 -0
  47. package/dist/pajek.js.map +1 -0
  48. package/dist/src/children.d.ts +134 -0
  49. package/dist/src/children.d.ts.map +1 -0
  50. package/dist/src/children.js +274 -0
  51. package/dist/src/children.js.map +1 -0
  52. package/dist/src/common/attributes.d.ts +229 -0
  53. package/dist/src/common/attributes.d.ts.map +1 -0
  54. package/dist/src/common/attributes.js +368 -0
  55. package/dist/src/common/attributes.js.map +1 -0
  56. package/dist/src/common/codes.d.ts +105 -0
  57. package/dist/src/common/codes.d.ts.map +1 -0
  58. package/dist/src/common/codes.js +107 -0
  59. package/dist/src/common/codes.js.map +1 -0
  60. package/dist/src/common/declared-types.d.ts +84 -0
  61. package/dist/src/common/declared-types.d.ts.map +1 -0
  62. package/dist/src/common/declared-types.js +326 -0
  63. package/dist/src/common/declared-types.js.map +1 -0
  64. package/dist/src/common/direction.d.ts +206 -0
  65. package/dist/src/common/direction.d.ts.map +1 -0
  66. package/dist/src/common/direction.js +370 -0
  67. package/dist/src/common/direction.js.map +1 -0
  68. package/dist/src/common/escape.d.ts +92 -0
  69. package/dist/src/common/escape.d.ts.map +1 -0
  70. package/dist/src/common/escape.js +212 -0
  71. package/dist/src/common/escape.js.map +1 -0
  72. package/dist/src/common/export.d.ts +249 -0
  73. package/dist/src/common/export.d.ts.map +1 -0
  74. package/dist/src/common/export.js +594 -0
  75. package/dist/src/common/export.js.map +1 -0
  76. package/dist/src/common/format.d.ts +59 -0
  77. package/dist/src/common/format.d.ts.map +1 -0
  78. package/dist/src/common/format.js +106 -0
  79. package/dist/src/common/format.js.map +1 -0
  80. package/dist/src/common/ids.d.ts +83 -0
  81. package/dist/src/common/ids.d.ts.map +1 -0
  82. package/dist/src/common/ids.js +158 -0
  83. package/dist/src/common/ids.js.map +1 -0
  84. package/dist/src/common/input.d.ts +100 -0
  85. package/dist/src/common/input.d.ts.map +1 -0
  86. package/dist/src/common/input.js +335 -0
  87. package/dist/src/common/input.js.map +1 -0
  88. package/dist/src/common/lists.d.ts +34 -0
  89. package/dist/src/common/lists.d.ts.map +1 -0
  90. package/dist/src/common/lists.js +185 -0
  91. package/dist/src/common/lists.js.map +1 -0
  92. package/dist/src/common/options.d.ts +108 -0
  93. package/dist/src/common/options.d.ts.map +1 -0
  94. package/dist/src/common/options.js +265 -0
  95. package/dist/src/common/options.js.map +1 -0
  96. package/dist/src/common/report.d.ts +187 -0
  97. package/dist/src/common/report.d.ts.map +1 -0
  98. package/dist/src/common/report.js +274 -0
  99. package/dist/src/common/report.js.map +1 -0
  100. package/dist/src/common/temporal.d.ts +71 -0
  101. package/dist/src/common/temporal.d.ts.map +1 -0
  102. package/dist/src/common/temporal.js +266 -0
  103. package/dist/src/common/temporal.js.map +1 -0
  104. package/dist/src/common/text.d.ts +104 -0
  105. package/dist/src/common/text.d.ts.map +1 -0
  106. package/dist/src/common/text.js +255 -0
  107. package/dist/src/common/text.js.map +1 -0
  108. package/dist/src/common/weights.d.ts +77 -0
  109. package/dist/src/common/weights.d.ts.map +1 -0
  110. package/dist/src/common/weights.js +156 -0
  111. package/dist/src/common/weights.js.map +1 -0
  112. package/dist/src/common/writer.d.ts +51 -0
  113. package/dist/src/common/writer.d.ts.map +1 -0
  114. package/dist/src/common/writer.js +108 -0
  115. package/dist/src/common/writer.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +245 -0
  117. package/dist/src/common/xml.d.ts.map +1 -0
  118. package/dist/src/common/xml.js +942 -0
  119. package/dist/src/common/xml.js.map +1 -0
  120. package/dist/src/formats/csv/exporter.d.ts +70 -0
  121. package/dist/src/formats/csv/exporter.d.ts.map +1 -0
  122. package/dist/src/formats/csv/exporter.js +682 -0
  123. package/dist/src/formats/csv/exporter.js.map +1 -0
  124. package/dist/src/formats/csv/header.d.ts +66 -0
  125. package/dist/src/formats/csv/header.d.ts.map +1 -0
  126. package/dist/src/formats/csv/header.js +152 -0
  127. package/dist/src/formats/csv/header.js.map +1 -0
  128. package/dist/src/formats/csv/importer.d.ts +82 -0
  129. package/dist/src/formats/csv/importer.d.ts.map +1 -0
  130. package/dist/src/formats/csv/importer.js +849 -0
  131. package/dist/src/formats/csv/importer.js.map +1 -0
  132. package/dist/src/formats/csv/index.d.ts +60 -0
  133. package/dist/src/formats/csv/index.d.ts.map +1 -0
  134. package/dist/src/formats/csv/index.js +63 -0
  135. package/dist/src/formats/csv/index.js.map +1 -0
  136. package/dist/src/formats/csv/records.d.ts +188 -0
  137. package/dist/src/formats/csv/records.d.ts.map +1 -0
  138. package/dist/src/formats/csv/records.js +702 -0
  139. package/dist/src/formats/csv/records.js.map +1 -0
  140. package/dist/src/formats/csv/values.d.ts +105 -0
  141. package/dist/src/formats/csv/values.d.ts.map +1 -0
  142. package/dist/src/formats/csv/values.js +192 -0
  143. package/dist/src/formats/csv/values.js.map +1 -0
  144. package/dist/src/formats/dot/exporter.d.ts +52 -0
  145. package/dist/src/formats/dot/exporter.d.ts.map +1 -0
  146. package/dist/src/formats/dot/exporter.js +836 -0
  147. package/dist/src/formats/dot/exporter.js.map +1 -0
  148. package/dist/src/formats/dot/importer.d.ts +102 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -0
  150. package/dist/src/formats/dot/importer.js +1291 -0
  151. package/dist/src/formats/dot/importer.js.map +1 -0
  152. package/dist/src/formats/dot/index.d.ts +7 -0
  153. package/dist/src/formats/dot/index.d.ts.map +1 -0
  154. package/dist/src/formats/dot/index.js +7 -0
  155. package/dist/src/formats/dot/index.js.map +1 -0
  156. package/dist/src/formats/dot/names.d.ts +29 -0
  157. package/dist/src/formats/dot/names.d.ts.map +1 -0
  158. package/dist/src/formats/dot/names.js +28 -0
  159. package/dist/src/formats/dot/names.js.map +1 -0
  160. package/dist/src/formats/dot/tokenizer.d.ts +114 -0
  161. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
  162. package/dist/src/formats/dot/tokenizer.js +341 -0
  163. package/dist/src/formats/dot/tokenizer.js.map +1 -0
  164. package/dist/src/formats/gexf/exporter.d.ts +56 -0
  165. package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/exporter.js +1395 -0
  167. package/dist/src/formats/gexf/exporter.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +73 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -0
  170. package/dist/src/formats/gexf/importer.js +1880 -0
  171. package/dist/src/formats/gexf/importer.js.map +1 -0
  172. package/dist/src/formats/gexf/index.d.ts +96 -0
  173. package/dist/src/formats/gexf/index.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/index.js +97 -0
  175. package/dist/src/formats/gexf/index.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts +135 -0
  177. package/dist/src/formats/gexf/schema.d.ts.map +1 -0
  178. package/dist/src/formats/gexf/schema.js +323 -0
  179. package/dist/src/formats/gexf/schema.js.map +1 -0
  180. package/dist/src/formats/gml/exporter.d.ts +69 -0
  181. package/dist/src/formats/gml/exporter.d.ts.map +1 -0
  182. package/dist/src/formats/gml/exporter.js +1093 -0
  183. package/dist/src/formats/gml/exporter.js.map +1 -0
  184. package/dist/src/formats/gml/importer.d.ts +66 -0
  185. package/dist/src/formats/gml/importer.d.ts.map +1 -0
  186. package/dist/src/formats/gml/importer.js +1331 -0
  187. package/dist/src/formats/gml/importer.js.map +1 -0
  188. package/dist/src/formats/gml/index.d.ts +85 -0
  189. package/dist/src/formats/gml/index.d.ts.map +1 -0
  190. package/dist/src/formats/gml/index.js +88 -0
  191. package/dist/src/formats/gml/index.js.map +1 -0
  192. package/dist/src/formats/gml/syntax.d.ts +186 -0
  193. package/dist/src/formats/gml/syntax.d.ts.map +1 -0
  194. package/dist/src/formats/gml/syntax.js +467 -0
  195. package/dist/src/formats/gml/syntax.js.map +1 -0
  196. package/dist/src/formats/graphml/constants.d.ts +169 -0
  197. package/dist/src/formats/graphml/constants.d.ts.map +1 -0
  198. package/dist/src/formats/graphml/constants.js +165 -0
  199. package/dist/src/formats/graphml/constants.js.map +1 -0
  200. package/dist/src/formats/graphml/exporter.d.ts +34 -0
  201. package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
  202. package/dist/src/formats/graphml/exporter.js +1176 -0
  203. package/dist/src/formats/graphml/exporter.js.map +1 -0
  204. package/dist/src/formats/graphml/importer.d.ts +31 -0
  205. package/dist/src/formats/graphml/importer.d.ts.map +1 -0
  206. package/dist/src/formats/graphml/importer.js +1607 -0
  207. package/dist/src/formats/graphml/importer.js.map +1 -0
  208. package/dist/src/formats/graphml/index.d.ts +8 -0
  209. package/dist/src/formats/graphml/index.d.ts.map +1 -0
  210. package/dist/src/formats/graphml/index.js +8 -0
  211. package/dist/src/formats/graphml/index.js.map +1 -0
  212. package/dist/src/formats/graphml/tree.d.ts +72 -0
  213. package/dist/src/formats/graphml/tree.d.ts.map +1 -0
  214. package/dist/src/formats/graphml/tree.js +290 -0
  215. package/dist/src/formats/graphml/tree.js.map +1 -0
  216. package/dist/src/formats/json/dialect.d.ts +125 -0
  217. package/dist/src/formats/json/dialect.d.ts.map +1 -0
  218. package/dist/src/formats/json/dialect.js +262 -0
  219. package/dist/src/formats/json/dialect.js.map +1 -0
  220. package/dist/src/formats/json/exporter.d.ts +89 -0
  221. package/dist/src/formats/json/exporter.d.ts.map +1 -0
  222. package/dist/src/formats/json/exporter.js +1358 -0
  223. package/dist/src/formats/json/exporter.js.map +1 -0
  224. package/dist/src/formats/json/importer.d.ts +108 -0
  225. package/dist/src/formats/json/importer.d.ts.map +1 -0
  226. package/dist/src/formats/json/importer.js +1838 -0
  227. package/dist/src/formats/json/importer.js.map +1 -0
  228. package/dist/src/formats/json/index.d.ts +8 -0
  229. package/dist/src/formats/json/index.d.ts.map +1 -0
  230. package/dist/src/formats/json/index.js +8 -0
  231. package/dist/src/formats/json/index.js.map +1 -0
  232. package/dist/src/formats/neo4j/exporter.d.ts +68 -0
  233. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
  234. package/dist/src/formats/neo4j/exporter.js +1055 -0
  235. package/dist/src/formats/neo4j/exporter.js.map +1 -0
  236. package/dist/src/formats/neo4j/header.d.ts +52 -0
  237. package/dist/src/formats/neo4j/header.d.ts.map +1 -0
  238. package/dist/src/formats/neo4j/header.js +131 -0
  239. package/dist/src/formats/neo4j/header.js.map +1 -0
  240. package/dist/src/formats/neo4j/importer.d.ts +73 -0
  241. package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
  242. package/dist/src/formats/neo4j/importer.js +932 -0
  243. package/dist/src/formats/neo4j/importer.js.map +1 -0
  244. package/dist/src/formats/neo4j/index.d.ts +79 -0
  245. package/dist/src/formats/neo4j/index.d.ts.map +1 -0
  246. package/dist/src/formats/neo4j/index.js +83 -0
  247. package/dist/src/formats/neo4j/index.js.map +1 -0
  248. package/dist/src/formats/pajek/exporter.d.ts +58 -0
  249. package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
  250. package/dist/src/formats/pajek/exporter.js +825 -0
  251. package/dist/src/formats/pajek/exporter.js.map +1 -0
  252. package/dist/src/formats/pajek/importer.d.ts +88 -0
  253. package/dist/src/formats/pajek/importer.d.ts.map +1 -0
  254. package/dist/src/formats/pajek/importer.js +1047 -0
  255. package/dist/src/formats/pajek/importer.js.map +1 -0
  256. package/dist/src/formats/pajek/index.d.ts +7 -0
  257. package/dist/src/formats/pajek/index.d.ts.map +1 -0
  258. package/dist/src/formats/pajek/index.js +7 -0
  259. package/dist/src/formats/pajek/index.js.map +1 -0
  260. package/dist/src/formats/pajek/syntax.d.ts +112 -0
  261. package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
  262. package/dist/src/formats/pajek/syntax.js +269 -0
  263. package/dist/src/formats/pajek/syntax.js.map +1 -0
  264. package/dist/src/index.d.ts +35 -0
  265. package/dist/src/index.d.ts.map +1 -0
  266. package/dist/src/index.js +39 -0
  267. package/dist/src/index.js.map +1 -0
  268. package/dist/src/registry.d.ts +207 -0
  269. package/dist/src/registry.d.ts.map +1 -0
  270. package/dist/src/registry.js +481 -0
  271. package/dist/src/registry.js.map +1 -0
  272. package/dist/src/sniff.d.ts +104 -0
  273. package/dist/src/sniff.d.ts.map +1 -0
  274. package/dist/src/sniff.js +357 -0
  275. package/dist/src/sniff.js.map +1 -0
  276. package/dist/src/types.d.ts +238 -0
  277. package/dist/src/types.d.ts.map +1 -0
  278. package/dist/src/types.js +29 -0
  279. package/dist/src/types.js.map +1 -0
  280. package/dist/tsconfig.build.tsbuildinfo +1 -0
  281. package/package.json +122 -7
  282. package/src/children.ts +335 -0
  283. package/src/common/attributes.ts +520 -0
  284. package/src/common/codes.ts +153 -0
  285. package/src/common/declared-types.ts +374 -0
  286. package/src/common/direction.ts +518 -0
  287. package/src/common/escape.ts +231 -0
  288. package/src/common/export.ts +817 -0
  289. package/src/common/format.ts +111 -0
  290. package/src/common/ids.ts +176 -0
  291. package/src/common/input.ts +378 -0
  292. package/src/common/lists.ts +196 -0
  293. package/src/common/options.ts +377 -0
  294. package/src/common/report.ts +352 -0
  295. package/src/common/temporal.ts +302 -0
  296. package/src/common/text.ts +294 -0
  297. package/src/common/weights.ts +202 -0
  298. package/src/common/writer.ts +123 -0
  299. package/src/common/xml.ts +1053 -0
  300. package/src/formats/csv/exporter.ts +894 -0
  301. package/src/formats/csv/header.ts +172 -0
  302. package/src/formats/csv/importer.ts +1104 -0
  303. package/src/formats/csv/index.ts +88 -0
  304. package/src/formats/csv/records.ts +813 -0
  305. package/src/formats/csv/values.ts +224 -0
  306. package/src/formats/dot/exporter.ts +1014 -0
  307. package/src/formats/dot/importer.ts +1549 -0
  308. package/src/formats/dot/index.ts +7 -0
  309. package/src/formats/dot/names.ts +40 -0
  310. package/src/formats/dot/tokenizer.ts +384 -0
  311. package/src/formats/gexf/exporter.ts +1696 -0
  312. package/src/formats/gexf/importer.ts +2333 -0
  313. package/src/formats/gexf/index.ts +142 -0
  314. package/src/formats/gexf/schema.ts +361 -0
  315. package/src/formats/gml/exporter.ts +1404 -0
  316. package/src/formats/gml/importer.ts +1591 -0
  317. package/src/formats/gml/index.ts +128 -0
  318. package/src/formats/gml/syntax.ts +545 -0
  319. package/src/formats/graphml/constants.ts +225 -0
  320. package/src/formats/graphml/exporter.ts +1458 -0
  321. package/src/formats/graphml/importer.ts +2027 -0
  322. package/src/formats/graphml/index.ts +8 -0
  323. package/src/formats/graphml/tree.ts +318 -0
  324. package/src/formats/json/dialect.ts +317 -0
  325. package/src/formats/json/exporter.ts +1616 -0
  326. package/src/formats/json/importer.ts +2271 -0
  327. package/src/formats/json/index.ts +8 -0
  328. package/src/formats/neo4j/exporter.ts +1287 -0
  329. package/src/formats/neo4j/header.ts +156 -0
  330. package/src/formats/neo4j/importer.ts +1220 -0
  331. package/src/formats/neo4j/index.ts +116 -0
  332. package/src/formats/pajek/exporter.ts +1000 -0
  333. package/src/formats/pajek/importer.ts +1311 -0
  334. package/src/formats/pajek/index.ts +7 -0
  335. package/src/formats/pajek/syntax.ts +307 -0
  336. package/src/index.ts +244 -0
  337. package/src/registry.ts +617 -0
  338. package/src/sniff.ts +397 -0
  339. package/src/types.ts +262 -0
@@ -0,0 +1,1311 @@
1
+ /**
2
+ * The Pajek NET importer (design sections 8.4 and 8.6; research note 07 section 2.5): a
3
+ * line-oriented streaming reader over the common LineReader that pushes scalars into the sink as
4
+ * each line is read. `*Vertices N` declares vertices 1..N (0..N-1 for the zero-based files some
5
+ * scripts write, detected from the first vertex line), so the id map of a well-formed file is
6
+ * `identity` with offset 1; vertex lines add the label, the coordinates (the position role,
7
+ * units "file"), the shape keyword and the `key value` parameters (a `graphty_originalId`
8
+ * parameter, written by the exporter under `sanitizeIds: "mangle"`, is restored as the vertex's
9
+ * id under `restoreMangledIds`); `*Arcs` sections are directed
10
+ * and `*Edges` sections undirected, resolved through the common DirectionResolver under the
11
+ * mixed-direction policy; the third column of a line is the weight unless `weightFrom` says
12
+ * otherwise; `*Arcslist` / `*Edgeslist` adjacency lists and `*Matrix` rows are read too; time
13
+ * interval tokens `[1-5,7-*]` become the spells role. A second network in one file is refused
14
+ * with an issue; project-file sections (`*Partition`, `*Vector`, ...) are reported as unsupported.
15
+ */
16
+
17
+ import {
18
+ type ColumnDecl,
19
+ type ColumnHandle,
20
+ GraphFormatError,
21
+ type GraphMetaPatch,
22
+ type GraphSink,
23
+ INVALID_INDEX,
24
+ type NodeId,
25
+ type U32,
26
+ } from "@graphty/graph-format";
27
+
28
+ import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE } from "../../common/attributes.js";
29
+ import { DUPLICATE_NODE_CODE, INVALID_UTF8_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE } from "../../common/codes.js";
30
+ import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
31
+ import { coerceIdText, ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
32
+ import { LineReader, throwIfAborted } from "../../common/input.js";
33
+ import {
34
+ type ImportFormatDefaults,
35
+ reportSinkOptions,
36
+ reportUnusedOptions,
37
+ type ResolvedImportOptions,
38
+ resolveImportOptions,
39
+ SINK_OPTION_CODE,
40
+ } from "../../common/options.js";
41
+ import { ImportReportBuilder } from "../../common/report.js";
42
+ import { isNumericText, TextCellWriter, WIDENING_UNSUPPORTED_CODE } from "../../common/text.js";
43
+ import { isWeightField, parseWeightText } from "../../common/weights.js";
44
+ import { type CommonImportOptions, type GraphImporter, type ImportInput, type ImportReport } from "../../types.js";
45
+ import {
46
+ isCommentOrBlank,
47
+ isIntervalToken,
48
+ isSectionLine,
49
+ isVertexNumber,
50
+ LABEL_COLUMN,
51
+ ORIGINAL_ID_KEY,
52
+ parseIntervals,
53
+ parseSectionHeader,
54
+ POSITION_COLUMN,
55
+ RELATION_COLUMN,
56
+ type SectionHeader,
57
+ SHAPE_COLUMN,
58
+ SHAPES,
59
+ SPELLS_COLUMN,
60
+ tokenize,
61
+ VALUE_COLUMN,
62
+ VALUE_FIELD,
63
+ } from "./syntax.js";
64
+
65
+ /** The format-specific options of the Pajek importer. */
66
+ export interface PajekImportOptions {
67
+ /**
68
+ * The number of the first vertex: 1 (Pajek's rule), 0 (files written by zero-based scripts), or
69
+ * "auto" (default): 0 when the first vertex line is numbered 0, 1 otherwise.
70
+ */
71
+ firstVertex?: 0 | 1 | "auto" | undefined;
72
+ }
73
+
74
+ /** Issue codes of the Pajek importer. */
75
+ export const PAJEK_ISSUE = Object.freeze({
76
+ /** The input holds invalid UTF-8 (fatal). */
77
+ INVALID_UTF8: INVALID_UTF8_CODE,
78
+ /** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
79
+ NO_VERTICES: "E_PAJEK_NO_VERTICES",
80
+ /** Fatal: `*Vertices` without a vertex count, or one the sink cannot hold. */
81
+ VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
82
+ /** Aborts: a second `*Vertices` or `*Network` section; only one network per file is read. */
83
+ MULTIPLE_NETWORKS: "E_PAJEK_MULTIPLE_NETWORKS",
84
+ /** A data line before the first section header. */
85
+ OUTSIDE_SECTION: "E_PAJEK_OUTSIDE_SECTION",
86
+ /** A section header the importer cannot parse. */
87
+ SYNTAX: SYNTAX_CODE,
88
+ /** A double quote not closed before the end of the line. */
89
+ UNTERMINATED_QUOTE: "E_PAJEK_UNTERMINATED_QUOTE",
90
+ /** A vertex line the grammar does not accept. */
91
+ VERTEX_LINE: "E_PAJEK_VERTEX_LINE",
92
+ /** A vertex number outside the declared range. */
93
+ VERTEX_RANGE: "E_PAJEK_VERTEX_RANGE",
94
+ /** Fewer vertex lines than `*Vertices` declares (vertices without a line have no label). */
95
+ VERTEX_COUNT: "E_PAJEK_VERTEX_COUNT",
96
+ /** A second line for the same vertex; the later values overwrite. */
97
+ DUPLICATE_NODE: DUPLICATE_NODE_CODE,
98
+ /** A line (arc, edge, list or matrix row) the grammar does not accept. */
99
+ LINE: "E_PAJEK_LINE",
100
+ /** A line endpoint outside the declared vertex range (the core's code, forwarded). */
101
+ UNKNOWN_NODE: "E_UNKNOWN_NODE",
102
+ /** A malformed time interval token. */
103
+ INTERVAL: "E_PAJEK_INTERVAL",
104
+ /** A `*Matrix` section with the wrong number of rows. */
105
+ MATRIX_ROWS: "E_PAJEK_MATRIX_ROWS",
106
+ /** A project-file section (`*Partition`, `*Vector`, ...) the importer does not read. */
107
+ UNSUPPORTED_SECTION: "E_PAJEK_UNSUPPORTED_SECTION",
108
+ /** Tokens after a section header the grammar does not account for. */
109
+ HEADER_EXTRA: "W_PAJEK_HEADER_EXTRA",
110
+ /** The file declares vertices but no line section. */
111
+ NO_LINES: "W_PAJEK_NO_LINES",
112
+ /** Vertex numbering starts at 0 rather than 1. */
113
+ ZERO_BASED: "W_PAJEK_ZERO_BASED",
114
+ /** Vertex lines mix two and three coordinates. */
115
+ COORD_DIMS: "W_PAJEK_COORD_DIMS",
116
+ /** Two vertices share a label under nodeIdFrom "label" and became one node. */
117
+ LABEL_MERGED: "W_PAJEK_LABEL_MERGED",
118
+ /** Two distinct label texts became one numeric id under ids "number". */
119
+ ID_MERGED: ID_MERGED_CODE,
120
+ /** A parameter column of `2.0`-style text kept at the value-inferred dtype (the sink cannot widen). */
121
+ WIDENING_UNSUPPORTED: WIDENING_UNSUPPORTED_CODE,
122
+ /** A structural column (label, position, shape, spells, relation) renamed `<name>#<id>` because the name was taken (design section 5.6). */
123
+ COLUMN_RENAMED: RENAMED_CODE,
124
+ /** A structural column declared without its role because the sink already holds it. */
125
+ ROLE_TAKEN: ROLE_TAKEN_CODE,
126
+ /** Two vertices carry the same `graphty_originalId` under restoreMangledIds and became one node. */
127
+ ORIGINAL_ID_MERGED: "W_PAJEK_ORIGINAL_ID_MERGED",
128
+ /** A vertex line's `graphty_originalId` came after a later vertex's line had created it under its number. */
129
+ ORIGINAL_ID_UNRESTORED: "W_PAJEK_ORIGINAL_ID_UNRESTORED",
130
+ /** A common option the importer has no use for (weightFrom naming a parameter and restoreMangledIds are honoured; long, hyperedges are not). */
131
+ OPTION_IGNORED: OPTION_IGNORED_CODE,
132
+ /** A builder-policy option the caller passed that the caller's sink does not use (the shared W_SINK_OPTION). */
133
+ SINK_OPTION: SINK_OPTION_CODE,
134
+ });
135
+
136
+ /** The common options the Pajek importer reads (the rest is reported by reportUnusedOptions). */
137
+ const USED_OPTIONS: ReadonlySet<keyof CommonImportOptions> = new Set<keyof CommonImportOptions>([
138
+ "ids",
139
+ "nodeIdFrom",
140
+ "restoreMangledIds",
141
+ "addMissingNodes",
142
+ "duplicateEdges",
143
+ "selfLoops",
144
+ "onMixedDirection",
145
+ "defaultDirected",
146
+ "weightFrom",
147
+ "weightDtype",
148
+ "errorLimit",
149
+ "signal",
150
+ "onProgress",
151
+ ]);
152
+
153
+ const DEFAULTS: ImportFormatDefaults = { ids: "canonical", defaultDirected: true, weightFrom: VALUE_FIELD };
154
+
155
+ /** Lines between two checks of the cancellation signal (a whole string input is one chunk). */
156
+ const ABORT_CHECK_INTERVAL = 64;
157
+
158
+ const FIRST_VERTEX_VALUES: ReadonlySet<unknown> = new Set([0, 1, "auto"]);
159
+
160
+ const LABEL_DECL: ColumnDecl = {
161
+ name: LABEL_COLUMN,
162
+ dtype: "string",
163
+ nullable: true,
164
+ role: "label",
165
+ origin: { format: "pajek", id: "label" },
166
+ };
167
+
168
+ const SHAPE_DECL: ColumnDecl = {
169
+ name: SHAPE_COLUMN,
170
+ dtype: "dict",
171
+ nullable: true,
172
+ origin: { format: "pajek", id: "shape" },
173
+ };
174
+
175
+ const RELATION_DECL: ColumnDecl = {
176
+ name: RELATION_COLUMN,
177
+ dtype: "dict",
178
+ nullable: true,
179
+ origin: { format: "pajek", id: "relation" },
180
+ };
181
+
182
+ /**
183
+ * The spells column declaration of a domain (design section 5.1: a list of f64 pairs).
184
+ * @returns a fresh declaration
185
+ */
186
+ function spellsDecl(): ColumnDecl {
187
+ return {
188
+ name: SPELLS_COLUMN,
189
+ dtype: "list",
190
+ itemDtype: "f64",
191
+ itemComponents: 2,
192
+ nullable: true,
193
+ role: "spells",
194
+ origin: { format: "pajek", id: "interval" },
195
+ };
196
+ }
197
+
198
+ /**
199
+ * The position column declaration (design section 5.2: f32 x 3, role position, units "file").
200
+ * @param sourceDims - 2 or 3, the coordinate count of the first vertex line with coordinates
201
+ * @returns a fresh declaration
202
+ */
203
+ function positionDecl(sourceDims: number): ColumnDecl {
204
+ return {
205
+ name: POSITION_COLUMN,
206
+ dtype: "f32",
207
+ components: 3,
208
+ nullable: true,
209
+ role: "position",
210
+ origin: { format: "pajek", id: "coordinates" },
211
+ extra: { sourceDims, units: "file" },
212
+ };
213
+ }
214
+
215
+ /** A problem with one line, recorded as an issue by the per-line catch. */
216
+ class LineError extends Error {
217
+ readonly category: "parse-error" | "validation-error" | "missing-value";
218
+
219
+ readonly code: string;
220
+
221
+ /**
222
+ * Create a line error.
223
+ * @param category - the issue category
224
+ * @param code - the issue code
225
+ * @param message - a plain-ASCII message
226
+ */
227
+ constructor(category: "parse-error" | "validation-error" | "missing-value", code: string, message: string) {
228
+ super(message);
229
+ this.name = "PajekLineError";
230
+ this.category = category;
231
+ this.code = code;
232
+ }
233
+ }
234
+
235
+ /** What a vertex or line row carries besides its numbers, parsed before anything is written. */
236
+ interface RowExtras {
237
+ /** Parameter keys, in order. */
238
+ readonly keys: string[];
239
+ /** Parameter value texts, aligned with keys. */
240
+ readonly values: string[];
241
+ /** The spells of an interval token, or null. */
242
+ spells: [number, number][] | null;
243
+ }
244
+
245
+ /** The section the parser is in. */
246
+ type Section = "none" | "vertices" | "lines" | "list" | "matrix" | "skip";
247
+
248
+ /**
249
+ * One import call's state machine: the sections, the vertex range, the columns declared so far
250
+ * and the direction resolver.
251
+ */
252
+ class PajekParser {
253
+ private readonly sink: GraphSink;
254
+
255
+ private readonly report: ImportReportBuilder;
256
+
257
+ private readonly options: ResolvedImportOptions;
258
+
259
+ private readonly resolver: DirectionResolver;
260
+
261
+ private readonly coercer: IdCoercer;
262
+
263
+ private readonly firstVertex: 0 | 1 | "auto";
264
+
265
+ private section: Section = "none";
266
+
267
+ /** The direction of the current line section. */
268
+ private kind: EdgeKind = "directed";
269
+
270
+ /** The relation name of the current line section, or null. */
271
+ private relation: string | null = null;
272
+
273
+ /** `*Vertices N`; -1 before the header. */
274
+ private vertexCount = -1;
275
+
276
+ /** The first-mode count of a two-mode network, or null. */
277
+ private firstMode: number | null = null;
278
+
279
+ private networkName: string | null = null;
280
+
281
+ private base = 1;
282
+
283
+ private baseKnown = false;
284
+
285
+ private nodesCreated = false;
286
+
287
+ private verticesDone = false;
288
+
289
+ private sawLineSection = false;
290
+
291
+ /** Node index per vertex position (vertex number minus base). */
292
+ private indexOfPos: U32 = new Uint32Array(0);
293
+
294
+ /**
295
+ * The node id per vertex position when the vertex lines decide the ids (nodeIdFrom "label",
296
+ * or a restorable `graphty_originalId` under restoreMangledIds); null when the ids are the
297
+ * vertex numbers and every vertex is created up front.
298
+ */
299
+ private idOfPos: NodeId[] | null = null;
300
+
301
+ /** Which positions had a vertex line. */
302
+ private seen: Uint8Array = new Uint8Array(0);
303
+
304
+ private seenCount = 0;
305
+
306
+ /**
307
+ * Every position below this one already has an id in `idOfPos`: the high-water mark of the
308
+ * gap-fill in `vertexLine`. Without it the gap-fill restarts at zero on every vertex line, which
309
+ * is quadratic in the vertex count for a file whose lines are in ascending order -- which is the
310
+ * order Pajek itself writes.
311
+ */
312
+ private filledBelow = 0;
313
+
314
+ private matrixRows = 0;
315
+
316
+ private coordDims = 0;
317
+
318
+ private labelHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
319
+
320
+ private positionHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
321
+
322
+ private shapeHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
323
+
324
+ private nodeSpellsHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
325
+
326
+ private relationHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
327
+
328
+ private edgeSpellsHandle: ColumnHandle = INVALID_INDEX as ColumnHandle;
329
+
330
+ private readonly nodeParams = new Map<string, TextCellWriter>();
331
+
332
+ private readonly edgeParams = new Map<string, TextCellWriter>();
333
+
334
+ /**
335
+ * Create the parser for one import call.
336
+ * @param sink - the sink
337
+ * @param report - the report
338
+ * @param options - the resolved common options
339
+ * @param firstVertex - the firstVertex format option
340
+ */
341
+ constructor(
342
+ sink: GraphSink,
343
+ report: ImportReportBuilder,
344
+ options: ResolvedImportOptions,
345
+ firstVertex: 0 | 1 | "auto",
346
+ ) {
347
+ this.sink = sink;
348
+ this.report = report;
349
+ this.options = options;
350
+ this.firstVertex = firstVertex;
351
+ this.resolver = new DirectionResolver(sink, report, options.onMixedDirection);
352
+ this.coercer = new IdCoercer(options.ids);
353
+ }
354
+
355
+ /**
356
+ * Handle one line; every problem of the line is recorded as an issue and the line skipped.
357
+ * @param text - the line without its terminator
358
+ * @param line - the 1-based line number
359
+ */
360
+ line(text: string, line: number): void {
361
+ if (isCommentOrBlank(text)) {
362
+ return;
363
+ }
364
+ try {
365
+ if (isSectionLine(text)) {
366
+ this.header(text, line);
367
+ return;
368
+ }
369
+ switch (this.section) {
370
+ case "none":
371
+ throw new LineError(
372
+ "parse-error",
373
+ PAJEK_ISSUE.OUTSIDE_SECTION,
374
+ "data before the first section header; a Pajek network starts with *Vertices",
375
+ );
376
+ case "vertices":
377
+ this.vertexLine(text, line);
378
+ return;
379
+ case "lines":
380
+ this.edgeLine(text, line);
381
+ return;
382
+ case "list":
383
+ this.listLine(text, line);
384
+ return;
385
+ case "matrix":
386
+ this.matrixLine(text, line);
387
+ return;
388
+ case "skip":
389
+ return;
390
+ default: {
391
+ const name: string = this.section;
392
+ throw new Error(`unknown section state ${name}`);
393
+ }
394
+ }
395
+ } catch (err) {
396
+ if (err instanceof LineError) {
397
+ this.report.error(err.category, err.code, err.message, { line });
398
+ return;
399
+ }
400
+ this.report.recordError(err, { line });
401
+ }
402
+ }
403
+
404
+ /**
405
+ * End of input: the vertex set is completed, a network without line sections gets the default
406
+ * direction, and the metadata is written.
407
+ */
408
+ finish(): void {
409
+ if (this.vertexCount < 0) {
410
+ this.report.fail(PAJEK_ISSUE.NO_VERTICES, "not a Pajek network: no *Vertices section found");
411
+ }
412
+ this.endSection();
413
+ this.finishVertices();
414
+ if (!this.sawLineSection) {
415
+ this.report.warning(
416
+ "validation-error",
417
+ PAJEK_ISSUE.NO_LINES,
418
+ "the file declares vertices but no *Arcs, *Edges, *Arcslist, *Edgeslist or *Matrix section",
419
+ );
420
+ this.resolver.setHeader(this.options.defaultDirected);
421
+ }
422
+ if (this.coercer.mergeCount > 0) {
423
+ this.report.warning(
424
+ "coercion",
425
+ PAJEK_ISSUE.ID_MERGED,
426
+ `${this.coercer.mergeCount} label(s) merged into an id another label already produced under ids "number"`,
427
+ );
428
+ }
429
+ const meta: GraphMetaPatch = {
430
+ sourceFormat: "pajek",
431
+ ...(this.networkName === null ? {} : { name: this.networkName }),
432
+ ...(this.firstMode === null ? {} : { extra: { pajek: { firstMode: this.firstMode } } }),
433
+ };
434
+ this.sink.setMeta(meta);
435
+ }
436
+
437
+ // ============================================================ headers
438
+
439
+ /**
440
+ * A section header line.
441
+ * @param text - the line
442
+ * @param line - the line number
443
+ */
444
+ private header(text: string, line: number): void {
445
+ const skipping = this.section === "skip";
446
+ this.endSection();
447
+ const h = parseSectionHeader(text);
448
+ if (h === null) {
449
+ this.section = "skip";
450
+ throw new LineError("parse-error", PAJEK_ISSUE.SYNTAX, `cannot read the section header "${text}"`);
451
+ }
452
+ if (h.extra.length > 0) {
453
+ this.report.warning(
454
+ "validation-error",
455
+ PAJEK_ISSUE.HEADER_EXTRA,
456
+ `ignored ${h.extra.length} unexpected token(s) after *${h.keyword}: ${h.extra.join(" ")}`,
457
+ { line },
458
+ );
459
+ }
460
+ switch (h.kind) {
461
+ case "network":
462
+ if (this.networkName !== null || this.vertexCount >= 0) {
463
+ this.refuseSecondNetwork(h.keyword, line);
464
+ }
465
+ this.networkName = h.name ?? "";
466
+ this.section = "none";
467
+ return;
468
+ case "vertices":
469
+ if (skipping && this.vertexCount >= 0) {
470
+ // the `*Vertices N` line of a skipped project-file section (*Partition, *Vector)
471
+ this.section = "skip";
472
+ return;
473
+ }
474
+ this.verticesHeader(h, line);
475
+ return;
476
+ case "arcs":
477
+ case "edges":
478
+ this.lineSection(h, line, "lines", h.kind === "arcs" ? "directed" : "undirected");
479
+ return;
480
+ case "arcslist":
481
+ case "edgeslist":
482
+ this.lineSection(h, line, "list", h.kind === "arcslist" ? "directed" : "undirected");
483
+ return;
484
+ case "matrix":
485
+ this.lineSection(h, line, "matrix", "directed");
486
+ this.matrixRows = 0;
487
+ return;
488
+ case "unsupported":
489
+ this.section = "skip";
490
+ this.report.error(
491
+ "unsupported",
492
+ PAJEK_ISSUE.UNSUPPORTED_SECTION,
493
+ `the *${h.keyword} section is not supported; its lines are skipped`,
494
+ { line },
495
+ );
496
+ return;
497
+ default: {
498
+ const name: string = h.kind;
499
+ throw new Error(`unknown section kind ${name}`);
500
+ }
501
+ }
502
+ }
503
+
504
+ /**
505
+ * `*Vertices N [N1]`.
506
+ * @param h - the header
507
+ * @param line - the line number
508
+ */
509
+ private verticesHeader(h: SectionHeader, line: number): void {
510
+ if (this.vertexCount >= 0) {
511
+ this.refuseSecondNetwork(h.keyword, line);
512
+ }
513
+ if (h.count === null) {
514
+ this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, "*Vertices needs a vertex count", { line });
515
+ }
516
+ if (!Number.isSafeInteger(h.count)) {
517
+ this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: not a count`, { line });
518
+ }
519
+ try {
520
+ // the declared count must be reservable (the sink is the authority on what it can hold,
521
+ // E_TOO_LARGE beyond MAX_COUNT) before any vertex is materialised
522
+ this.sink.reserve(h.count);
523
+ } catch (err) {
524
+ const reason = err instanceof Error ? err.message : String(err);
525
+ this.report.fail(
526
+ PAJEK_ISSUE.VERTICES_COUNT,
527
+ `*Vertices ${h.count}: the sink cannot hold that many (${reason})`,
528
+ {
529
+ line,
530
+ },
531
+ );
532
+ }
533
+ this.vertexCount = h.count;
534
+ if (h.secondCount !== null) {
535
+ this.firstMode = h.secondCount;
536
+ }
537
+ this.section = "vertices";
538
+ this.indexOfPos = new Uint32Array(h.count);
539
+ this.seen = new Uint8Array(h.count);
540
+ this.filledBelow = 0;
541
+ if (this.options.nodeIdFrom === "label" || this.restoresIds()) {
542
+ this.idOfPos = new Array<NodeId>(h.count);
543
+ }
544
+ if (this.firstVertex !== "auto") {
545
+ this.setBase(this.firstVertex, line);
546
+ }
547
+ }
548
+
549
+ /**
550
+ * A line section header: the vertex set is completed, the first one sets the sink's direction
551
+ * (rule 1 of design section 8.4), later ones only switch the kind.
552
+ * @param h - the header
553
+ * @param line - the line number
554
+ * @param section - the parser state for the section
555
+ * @param kind - the direction of its lines
556
+ */
557
+ private lineSection(h: SectionHeader, line: number, section: Section, kind: EdgeKind): void {
558
+ if (this.vertexCount < 0) {
559
+ this.report.fail(
560
+ PAJEK_ISSUE.NO_VERTICES,
561
+ `*${h.keyword} before *Vertices; a Pajek network starts with *Vertices`,
562
+ { line },
563
+ );
564
+ }
565
+ this.finishVertices();
566
+ if (!this.sawLineSection) {
567
+ this.sawLineSection = true;
568
+ this.resolver.setHeader(kind === "directed", { line });
569
+ }
570
+ this.section = section;
571
+ this.kind = kind;
572
+ this.relation = h.relation === null ? null : (h.name ?? String(h.relation));
573
+ }
574
+
575
+ /**
576
+ * Refuse a second network: an issue, then ImportError with the report so far.
577
+ * @param keyword - the header keyword
578
+ * @param line - the line number
579
+ */
580
+ private refuseSecondNetwork(keyword: string, line: number): never {
581
+ const message = `*${keyword} starts a second network; the importer reads one network per file`;
582
+ this.report.error("unsupported", PAJEK_ISSUE.MULTIPLE_NETWORKS, message, { line });
583
+ throw this.report.abort(message, { code: PAJEK_ISSUE.MULTIPLE_NETWORKS, line });
584
+ }
585
+
586
+ /**
587
+ * Checks that run when a section ends.
588
+ */
589
+ private endSection(): void {
590
+ if (this.section === "matrix" && this.matrixRows !== this.vertexCount) {
591
+ this.report.error(
592
+ "validation-error",
593
+ PAJEK_ISSUE.MATRIX_ROWS,
594
+ `*Matrix has ${this.matrixRows} row(s); *Vertices declares ${this.vertexCount}`,
595
+ );
596
+ }
597
+ this.section = "none";
598
+ }
599
+
600
+ // ============================================================ vertices
601
+
602
+ /**
603
+ * Fix the vertex numbering base and, under nodeIdFrom "id" / "index", create the declared
604
+ * vertices in number order so the id map is identity.
605
+ * @param base - 0 or 1
606
+ * @param line - the line number
607
+ */
608
+ private setBase(base: 0 | 1, line: number): void {
609
+ this.base = base;
610
+ this.baseKnown = true;
611
+ if (base === 0) {
612
+ this.report.warning(
613
+ "coercion",
614
+ PAJEK_ISSUE.ZERO_BASED,
615
+ "vertex numbering starts at 0; Pajek numbers vertices from 1",
616
+ { line },
617
+ );
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Push vertices base..base+N-1 in order (nodeIdFrom "id" / "index"), so the id map is identity;
623
+ * called on the first vertex line (once it is known that the lines carry no `graphty_originalId`
624
+ * parameter to restore) or when the vertex section ends without one.
625
+ */
626
+ private createNodes(): void {
627
+ if (this.nodesCreated || this.idOfPos !== null) {
628
+ return;
629
+ }
630
+ this.nodesCreated = true;
631
+ const n = this.vertexCount;
632
+ const { sink } = this;
633
+ for (let pos = 0; pos < n; pos++) {
634
+ this.indexOfPos[pos] = sink.addNode(this.idOfNumber(pos + this.base));
635
+ }
636
+ this.report.counts.nodes += n;
637
+ }
638
+
639
+ /**
640
+ * The id of a vertex number under nodeIdFrom "id" (the number coerced by the ids rule) or
641
+ * "index" (the position).
642
+ * @param k - the vertex number
643
+ * @returns the id
644
+ */
645
+ private idOfNumber(k: number): NodeId {
646
+ if (this.options.nodeIdFrom === "index") {
647
+ return k - this.base;
648
+ }
649
+ switch (this.options.ids) {
650
+ case "canonical":
651
+ case "number":
652
+ return k;
653
+ case "keep":
654
+ case "string":
655
+ return String(k);
656
+ default:
657
+ return coerceIdText(String(k), this.options.ids);
658
+ }
659
+ }
660
+
661
+ /**
662
+ * The id of a vertex position, for addEdge.
663
+ * @param pos - the vertex number minus the base
664
+ * @returns the id
665
+ */
666
+ private idAt(pos: number): NodeId {
667
+ if (this.idOfPos !== null) {
668
+ return this.idOfPos[pos];
669
+ }
670
+ return this.idOfNumber(pos + this.base);
671
+ }
672
+
673
+ /**
674
+ * Complete the vertex set once the vertex section is over: vertices without a line are
675
+ * created (under "label" or a restored `graphty_originalId` with their number as the id), and
676
+ * a partial list is reported.
677
+ */
678
+ private finishVertices(): void {
679
+ if (this.verticesDone) {
680
+ return;
681
+ }
682
+ this.verticesDone = true;
683
+ if (!this.baseKnown) {
684
+ this.setBase(1, 0);
685
+ }
686
+ const n = this.vertexCount;
687
+ this.createNodes();
688
+ if (this.idOfPos !== null) {
689
+ for (let pos = 0; pos < n; pos++) {
690
+ if (this.idOfPos[pos] === undefined) {
691
+ this.numberedNode(this.idOfPos, pos);
692
+ }
693
+ }
694
+ }
695
+ // a partial list: fewer lines than declared, counting the lines skipped after an error
696
+ const lines = this.seenCount + this.report.counts.skippedNodes;
697
+ if (this.seenCount > 0 && lines < n) {
698
+ this.report.error(
699
+ "validation-error",
700
+ PAJEK_ISSUE.VERTEX_COUNT,
701
+ `*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`,
702
+ );
703
+ }
704
+ }
705
+
706
+ /**
707
+ * A vertex line: `number [label] [x y [z]] [shape] [key value ...] [interval]`.
708
+ * @param text - the line
709
+ * @param line - the line number
710
+ */
711
+ private vertexLine(text: string, line: number): void {
712
+ const tokens = this.tokens(text, "node");
713
+ if (!isVertexNumber(tokens[0])) {
714
+ this.report.counts.skippedNodes++;
715
+ throw new LineError(
716
+ "parse-error",
717
+ PAJEK_ISSUE.VERTEX_LINE,
718
+ `a vertex line starts with a vertex number, not "${tokens[0]}"`,
719
+ );
720
+ }
721
+ const k = Number(tokens[0]);
722
+ if (!this.baseKnown) {
723
+ this.setBase(k === 0 ? 0 : 1, line);
724
+ }
725
+ const pos = k - this.base;
726
+ if (pos < 0 || pos >= this.vertexCount) {
727
+ this.report.counts.skippedNodes++;
728
+ throw new LineError(
729
+ "validation-error",
730
+ PAJEK_ISSUE.VERTEX_RANGE,
731
+ `vertex ${k} is outside ${this.base}..${this.base + this.vertexCount - 1}`,
732
+ );
733
+ }
734
+ // parse everything before writing anything
735
+ let i = 1;
736
+ const label = tokens.length > 1 ? tokens[1] : null;
737
+ if (label !== null) {
738
+ i = 2;
739
+ }
740
+ let x = 0;
741
+ let y = 0;
742
+ let z = 0;
743
+ let dims = 0;
744
+ if (i + 1 < tokens.length && isNumericText(tokens[i]) && isNumericText(tokens[i + 1])) {
745
+ x = Number(tokens[i]);
746
+ y = Number(tokens[i + 1]);
747
+ i += 2;
748
+ dims = 2;
749
+ if (i < tokens.length && isNumericText(tokens[i])) {
750
+ z = Number(tokens[i]);
751
+ i++;
752
+ dims = 3;
753
+ }
754
+ } else if (i < tokens.length && isNumericText(tokens[i])) {
755
+ this.report.counts.skippedNodes++;
756
+ throw new LineError(
757
+ "parse-error",
758
+ PAJEK_ISSUE.VERTEX_LINE,
759
+ `vertex ${k}: a single coordinate "${tokens[i]}"; coordinates are x y [z]`,
760
+ );
761
+ }
762
+ let shape: string | null = null;
763
+ if (i < tokens.length && SHAPES.has(tokens[i])) {
764
+ shape = tokens[i];
765
+ i++;
766
+ }
767
+ const extras = this.extras(tokens, i, "node");
768
+ const restored = this.restoredId(extras);
769
+ this.createNodes();
770
+ // write
771
+ const repeated = this.seen[pos] !== 0;
772
+ if (repeated) {
773
+ this.report.warning(
774
+ "merged",
775
+ PAJEK_ISSUE.DUPLICATE_NODE,
776
+ `vertex ${k} has more than one line; the later values overwrite`,
777
+ { line, element: String(k) },
778
+ );
779
+ } else {
780
+ this.seen[pos] = 1;
781
+ this.seenCount++;
782
+ }
783
+ let index: number;
784
+ if (this.idOfPos !== null && !repeated && this.options.nodeIdFrom === "label") {
785
+ index = this.deferredNode(this.idOfPos, pos, k, label, line, "label");
786
+ } else if (this.idOfPos !== null && this.idOfPos[pos] === undefined) {
787
+ // restore mode: the earlier positions without a line yet get their numbers first, so the
788
+ // index order is the vertex number order whatever the line order (the identity id map
789
+ // of an ordered file); then this vertex with its original id, or its number
790
+ for (let p = this.filledBelow; p < pos; p++) {
791
+ if (this.idOfPos[p] === undefined) {
792
+ this.numberedNode(this.idOfPos, p);
793
+ }
794
+ }
795
+ // after the loop on purpose: a throw part-way leaves the mark at its smaller, still
796
+ // correct value and the next line re-scans the range
797
+ if (pos > this.filledBelow) {
798
+ this.filledBelow = pos;
799
+ }
800
+ index = this.deferredNode(this.idOfPos, pos, k, restored, line, "originalId");
801
+ } else {
802
+ index = this.indexOfPos[pos];
803
+ if (restored !== null && !repeated) {
804
+ // an out-of-order line: the vertex already exists under its number
805
+ this.report.warning(
806
+ "coercion",
807
+ PAJEK_ISSUE.ORIGINAL_ID_UNRESTORED,
808
+ `vertex ${k} carries ${ORIGINAL_ID_KEY} ${JSON.stringify(restored)} but a later vertex's line came first and created it under its number; the original id is not restored`,
809
+ { line, element: String(k) },
810
+ );
811
+ }
812
+ }
813
+ const { sink } = this;
814
+ if (label !== null) {
815
+ if (this.labelHandle === INVALID_INDEX) {
816
+ this.labelHandle = declareResolved(sink, "node", LABEL_DECL, this.report, { line }).handle;
817
+ }
818
+ sink.setNodeValue(this.labelHandle, index, label);
819
+ }
820
+ if (dims > 0) {
821
+ if (this.positionHandle === INVALID_INDEX) {
822
+ this.coordDims = dims;
823
+ this.positionHandle = declareResolved(sink, "node", positionDecl(dims), this.report, { line }).handle;
824
+ } else if (dims !== this.coordDims) {
825
+ this.report.warnOnce(
826
+ "validation-error",
827
+ PAJEK_ISSUE.COORD_DIMS,
828
+ `vertex lines mix ${this.coordDims} and ${dims} coordinates; the position column records sourceDims ${this.coordDims}`,
829
+ { line, element: String(k) },
830
+ );
831
+ }
832
+ sink.setNodeValue(this.positionHandle, index, [x, y, z]);
833
+ }
834
+ if (shape !== null) {
835
+ if (this.shapeHandle === INVALID_INDEX && this.nodeParams.has(SHAPE_COLUMN)) {
836
+ // an earlier `shape "<text>"` parameter owns the column: the keyword is written there
837
+ this.writeParameter("node", SHAPE_COLUMN, index, shape);
838
+ } else {
839
+ if (this.shapeHandle === INVALID_INDEX) {
840
+ this.shapeHandle = declareResolved(sink, "node", SHAPE_DECL, this.report, { line }).handle;
841
+ }
842
+ sink.setNodeValue(this.shapeHandle, index, shape);
843
+ }
844
+ }
845
+ for (let p = 0; p < extras.keys.length; p++) {
846
+ this.writeParameter("node", extras.keys[p], index, extras.values[p]);
847
+ }
848
+ if (extras.spells !== null) {
849
+ if (this.nodeSpellsHandle === INVALID_INDEX) {
850
+ this.nodeSpellsHandle = declareResolved(sink, "node", spellsDecl(), this.report, { line }).handle;
851
+ }
852
+ sink.setNodeValue(this.nodeSpellsHandle, index, extras.spells);
853
+ }
854
+ }
855
+
856
+ /**
857
+ * Whether a `graphty_originalId` parameter is restored as the vertex id: restoreMangledIds
858
+ * under nodeIdFrom "id" (under "label" / "index" the parameter stays an ordinary column).
859
+ * @returns true when the vertex lines may carry restorable ids
860
+ */
861
+ private restoresIds(): boolean {
862
+ return this.options.restoreMangledIds && this.options.nodeIdFrom === "id";
863
+ }
864
+
865
+ /**
866
+ * Push a vertex whose id is its number (restore mode, a position whose line has not come or
867
+ * carries no original id; or the end of the vertex section under "label").
868
+ * @param ids - the id per vertex position
869
+ * @param pos - the vertex position
870
+ */
871
+ private numberedNode(ids: NodeId[], pos: number): void {
872
+ const id = this.idOfNumber(pos + this.base);
873
+ ids[pos] = id;
874
+ this.indexOfPos[pos] = this.sink.addNode(id);
875
+ this.report.counts.nodes++;
876
+ }
877
+
878
+ /**
879
+ * The `graphty_originalId` parameter of a vertex line, taken out of the extras when it is to be
880
+ * restored as the id (restoreMangledIds, under nodeIdFrom "id"); otherwise null and the
881
+ * parameter stays an ordinary column.
882
+ * @param extras - the parsed extras
883
+ * @returns the original id text, or null
884
+ */
885
+ private restoredId(extras: RowExtras): string | null {
886
+ if (!this.restoresIds()) {
887
+ return null;
888
+ }
889
+ const at = extras.keys.indexOf(ORIGINAL_ID_KEY);
890
+ if (at < 0) {
891
+ return null;
892
+ }
893
+ const [text] = extras.values.splice(at, 1);
894
+ extras.keys.splice(at, 1);
895
+ return text;
896
+ }
897
+
898
+ /**
899
+ * Push the node of a vertex line whose id comes from the line itself: its label under
900
+ * nodeIdFrom "label", or its restored `graphty_originalId` (the number when the line carries
901
+ * none), reporting a merge when the id was seen before.
902
+ * @param ids - the id per vertex position
903
+ * @param pos - the vertex position
904
+ * @param k - the vertex number
905
+ * @param text - the label or original id text, or null
906
+ * @param line - the line number
907
+ * @param from - which text the id comes from
908
+ * @returns the node index
909
+ */
910
+ private deferredNode(
911
+ ids: NodeId[],
912
+ pos: number,
913
+ k: number,
914
+ text: string | null,
915
+ line: number,
916
+ from: "label" | "originalId",
917
+ ): number {
918
+ const id = text === null ? this.idOfNumber(k) : this.coercer.text(text);
919
+ const before = this.sink.indexOf(id);
920
+ const index = this.sink.addNode(id);
921
+ ids[pos] = id;
922
+ this.indexOfPos[pos] = index;
923
+ this.report.counts.nodes++;
924
+ if (before !== INVALID_INDEX) {
925
+ this.report.warning(
926
+ "merged",
927
+ from === "label" ? PAJEK_ISSUE.LABEL_MERGED : PAJEK_ISSUE.ORIGINAL_ID_MERGED,
928
+ from === "label"
929
+ ? `vertex ${k} has the label ${JSON.stringify(String(id))} of an earlier vertex; both are one node`
930
+ : `vertex ${k} has the ${ORIGINAL_ID_KEY} ${JSON.stringify(String(id))} of an earlier vertex; both are one node`,
931
+ { line, element: String(k) },
932
+ );
933
+ }
934
+ return index;
935
+ }
936
+
937
+ // ============================================================ lines
938
+
939
+ /**
940
+ * An arc or edge line: `u v [value] [key value ...] [interval]`.
941
+ * @param text - the line
942
+ * @param line - the line number
943
+ */
944
+ private edgeLine(text: string, line: number): void {
945
+ const tokens = this.tokens(text, "edge");
946
+ if (tokens.length < 2 || !isVertexNumber(tokens[0]) || !isVertexNumber(tokens[1])) {
947
+ this.report.counts.skippedEdges++;
948
+ throw new LineError(
949
+ "parse-error",
950
+ PAJEK_ISSUE.LINE,
951
+ `a line starts with two vertex numbers, not "${tokens.slice(0, 2).join(" ")}"`,
952
+ );
953
+ }
954
+ const u = this.endpoint(Number(tokens[0]));
955
+ const v = this.endpoint(Number(tokens[1]));
956
+ let i = 2;
957
+ let valueText: string | null = null;
958
+ if (i < tokens.length && isNumericText(tokens[i])) {
959
+ valueText = tokens[i];
960
+ i++;
961
+ }
962
+ const extras = this.extras(tokens, i, "edge");
963
+ const { weightFrom } = this.options;
964
+ let weight: number | undefined;
965
+ let valueIsWeight = false;
966
+ if (isWeightField(VALUE_FIELD, weightFrom)) {
967
+ valueIsWeight = true;
968
+ if (valueText !== null) {
969
+ weight = this.weightOf(valueText);
970
+ }
971
+ } else if (weightFrom !== null) {
972
+ const at = extras.keys.indexOf(weightFrom);
973
+ if (at >= 0) {
974
+ weight = this.weightOf(extras.values[at]);
975
+ extras.keys.splice(at, 1);
976
+ extras.values.splice(at, 1);
977
+ }
978
+ }
979
+ const e = this.pushEdge(u, v, weight, line);
980
+ if (valueText !== null && !valueIsWeight) {
981
+ // inferred like every parameter, so a re-import of the exporter's `value` parameter agrees
982
+ this.writeParameter("edge", VALUE_COLUMN, e, valueText);
983
+ }
984
+ this.writeEdgeExtras(e, extras);
985
+ }
986
+
987
+ /**
988
+ * An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`.
989
+ * @param text - the line
990
+ * @param line - the line number
991
+ */
992
+ private listLine(text: string, line: number): void {
993
+ const tokens = this.tokens(text, "edge");
994
+ for (const token of tokens) {
995
+ if (!isVertexNumber(token)) {
996
+ this.report.counts.skippedEdges++;
997
+ throw new LineError(
998
+ "parse-error",
999
+ PAJEK_ISSUE.LINE,
1000
+ `an adjacency list holds vertex numbers only, not "${token}"`,
1001
+ );
1002
+ }
1003
+ }
1004
+ const u = this.endpoint(Number(tokens[0]));
1005
+ for (let i = 1; i < tokens.length; i++) {
1006
+ const v = this.endpoint(Number(tokens[i]));
1007
+ const e = this.pushEdge(u, v, undefined, line);
1008
+ this.writeRelation(e);
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * A `*Matrix` row: N values, every non-zero entry an arc from the row's vertex with that value.
1014
+ * @param text - the line
1015
+ * @param line - the line number
1016
+ */
1017
+ private matrixLine(text: string, line: number): void {
1018
+ const tokens = this.tokens(text, "edge");
1019
+ const row = this.matrixRows;
1020
+ this.matrixRows++;
1021
+ if (row >= this.vertexCount) {
1022
+ throw new LineError(
1023
+ "validation-error",
1024
+ PAJEK_ISSUE.MATRIX_ROWS,
1025
+ `*Matrix row ${row + 1} is beyond the ${this.vertexCount} declared vertices`,
1026
+ );
1027
+ }
1028
+ if (tokens.length !== this.vertexCount) {
1029
+ throw new LineError(
1030
+ "parse-error",
1031
+ PAJEK_ISSUE.LINE,
1032
+ `*Matrix row ${row + 1} has ${tokens.length} value(s); *Vertices declares ${this.vertexCount}`,
1033
+ );
1034
+ }
1035
+ for (const token of tokens) {
1036
+ if (!isNumericText(token)) {
1037
+ throw new LineError(
1038
+ "parse-error",
1039
+ PAJEK_ISSUE.LINE,
1040
+ `*Matrix row ${row + 1}: "${token}" is not a number`,
1041
+ );
1042
+ }
1043
+ }
1044
+ for (let j = 0; j < tokens.length; j++) {
1045
+ const value = Number(tokens[j]);
1046
+ if (value === 0) {
1047
+ continue;
1048
+ }
1049
+ const e = this.pushEdge(row, j, value, line);
1050
+ this.writeRelation(e);
1051
+ }
1052
+ }
1053
+
1054
+ /**
1055
+ * Push one line through the direction resolver, counting the logical edges it produced.
1056
+ * @param u - the source position
1057
+ * @param v - the target position
1058
+ * @param weight - the weight, or undefined for none
1059
+ * @param line - the line number
1060
+ * @returns the primary logical edge index
1061
+ */
1062
+ private pushEdge(u: number, v: number, weight: number | undefined, line: number): number {
1063
+ const { sink } = this;
1064
+ const before = sink.edgeCount;
1065
+ const e = this.resolver.addEdge(this.idAt(u), this.idAt(v), this.kind, weight, { line });
1066
+ this.report.counts.edges += sink.edgeCount - before;
1067
+ return e;
1068
+ }
1069
+
1070
+ /**
1071
+ * Write the relation, parameters and spells of a line on its primary half.
1072
+ * @param e - the edge index
1073
+ * @param extras - the parsed extras
1074
+ */
1075
+ private writeEdgeExtras(e: number, extras: RowExtras): void {
1076
+ const { sink } = this;
1077
+ this.writeRelation(e);
1078
+ for (let p = 0; p < extras.keys.length; p++) {
1079
+ this.writeParameter("edge", extras.keys[p], e, extras.values[p]);
1080
+ }
1081
+ if (extras.spells !== null) {
1082
+ if (this.edgeSpellsHandle === INVALID_INDEX) {
1083
+ this.edgeSpellsHandle = declareResolved(sink, "edge", spellsDecl(), this.report).handle;
1084
+ }
1085
+ sink.setEdgeValue(this.edgeSpellsHandle, e, extras.spells);
1086
+ }
1087
+ }
1088
+
1089
+ /**
1090
+ * Write the current section's relation name on an edge, when the section has one.
1091
+ * @param e - the edge index
1092
+ */
1093
+ private writeRelation(e: number): void {
1094
+ if (this.relation === null) {
1095
+ return;
1096
+ }
1097
+ if (this.relationHandle === INVALID_INDEX) {
1098
+ this.relationHandle = declareResolved(this.sink, "edge", RELATION_DECL, this.report).handle;
1099
+ }
1100
+ this.sink.setEdgeValue(this.relationHandle, e, this.relation);
1101
+ }
1102
+
1103
+ /**
1104
+ * A line endpoint: the vertex number checked against the declared range; a reference outside
1105
+ * it is a missing-value error and the line is skipped (Pajek's `*Vertices N` bounds the id space,
1106
+ * so addMissingNodes does not apply).
1107
+ * @param k - the vertex number
1108
+ * @returns the vertex position
1109
+ */
1110
+ private endpoint(k: number): number {
1111
+ const pos = k - this.base;
1112
+ if (pos < 0 || pos >= this.vertexCount) {
1113
+ this.report.counts.skippedEdges++;
1114
+ throw new LineError(
1115
+ "missing-value",
1116
+ PAJEK_ISSUE.UNKNOWN_NODE,
1117
+ `vertex ${k} is outside ${this.base}..${this.base + this.vertexCount - 1}`,
1118
+ );
1119
+ }
1120
+ return pos;
1121
+ }
1122
+
1123
+ /**
1124
+ * The weight of a line value text.
1125
+ * @param text - the text
1126
+ * @returns the weight; E_INVALID_WEIGHT is recorded by the caller's catch
1127
+ */
1128
+ private weightOf(text: string): number | undefined {
1129
+ try {
1130
+ return parseWeightText(text);
1131
+ } catch (err) {
1132
+ this.report.counts.skippedEdges++;
1133
+ throw err;
1134
+ }
1135
+ }
1136
+
1137
+ // ============================================================ shared
1138
+
1139
+ /**
1140
+ * Tokenize a data line.
1141
+ * @param text - the line
1142
+ * @param domain - what is skipped on an unterminated quote
1143
+ * @returns the tokens (at least one: blank lines never reach here)
1144
+ */
1145
+ private tokens(text: string, domain: "node" | "edge"): string[] {
1146
+ const tokens = tokenize(text);
1147
+ if (tokens === null) {
1148
+ if (domain === "node") {
1149
+ this.report.counts.skippedNodes++;
1150
+ } else {
1151
+ this.report.counts.skippedEdges++;
1152
+ }
1153
+ throw new LineError("parse-error", PAJEK_ISSUE.UNTERMINATED_QUOTE, "a double quote is not closed");
1154
+ }
1155
+ return tokens;
1156
+ }
1157
+
1158
+ /**
1159
+ * The `key value` parameters and the interval token at the end of a row, validated before
1160
+ * anything is written.
1161
+ * @param tokens - the row's tokens
1162
+ * @param start - the first token to read
1163
+ * @param domain - what is skipped when the row is malformed
1164
+ * @returns the parsed extras
1165
+ */
1166
+ private extras(tokens: readonly string[], start: number, domain: "node" | "edge"): RowExtras {
1167
+ const extras: RowExtras = { keys: [], values: [], spells: null };
1168
+ for (let i = start; i < tokens.length;) {
1169
+ const token = tokens[i];
1170
+ if (isIntervalToken(token)) {
1171
+ try {
1172
+ extras.spells = parseIntervals(token);
1173
+ } catch (err) {
1174
+ this.skip(domain);
1175
+ throw new LineError(
1176
+ "validation-error",
1177
+ PAJEK_ISSUE.INTERVAL,
1178
+ err instanceof Error ? err.message : String(err),
1179
+ );
1180
+ }
1181
+ i++;
1182
+ continue;
1183
+ }
1184
+ if (i + 1 >= tokens.length) {
1185
+ this.skip(domain);
1186
+ throw new LineError(
1187
+ "parse-error",
1188
+ domain === "node" ? PAJEK_ISSUE.VERTEX_LINE : PAJEK_ISSUE.LINE,
1189
+ `parameter "${token}" has no value`,
1190
+ );
1191
+ }
1192
+ extras.keys.push(token);
1193
+ extras.values.push(tokens[i + 1]);
1194
+ i += 2;
1195
+ }
1196
+ return extras;
1197
+ }
1198
+
1199
+ /**
1200
+ * Count a skipped element.
1201
+ * @param domain - node or edge
1202
+ */
1203
+ private skip(domain: "node" | "edge"): void {
1204
+ if (domain === "node") {
1205
+ this.report.counts.skippedNodes++;
1206
+ } else {
1207
+ this.report.counts.skippedEdges++;
1208
+ }
1209
+ }
1210
+
1211
+ /**
1212
+ * Write one `key value` parameter cell through the column's text writer (the 5.1 grammar per
1213
+ * column, the lexical form kept). A `shape` parameter next to the shape keyword column goes
1214
+ * into that column (a dict takes any text), and a shape keyword next to a `shape` parameter
1215
+ * column into that one, so a file mixing the two spellings reads as one column.
1216
+ * @param domain - node or edge
1217
+ * @param key - the parameter key (the column name)
1218
+ * @param row - the node or edge index
1219
+ * @param text - the value text
1220
+ */
1221
+ private writeParameter(domain: "node" | "edge", key: string, row: number, text: string): void {
1222
+ if (domain === "node" && key === SHAPE_COLUMN && this.shapeHandle !== INVALID_INDEX) {
1223
+ this.sink.setNodeValue(this.shapeHandle, row, text);
1224
+ return;
1225
+ }
1226
+ const writers = domain === "node" ? this.nodeParams : this.edgeParams;
1227
+ let writer = writers.get(key);
1228
+ if (writer === undefined) {
1229
+ writer = new TextCellWriter(key, domain, this.sink, this.report);
1230
+ writers.set(key, writer);
1231
+ }
1232
+ writer.write(row, text);
1233
+ }
1234
+ }
1235
+
1236
+ /**
1237
+ * Resolve the firstVertex option.
1238
+ * @param value - the caller's value
1239
+ * @returns 0, 1 or "auto"; E_UNSUPPORTED for anything else
1240
+ */
1241
+ function firstVertexOption(value: unknown): 0 | 1 | "auto" {
1242
+ if (value === undefined) {
1243
+ return "auto";
1244
+ }
1245
+ if (FIRST_VERTEX_VALUES.has(value)) {
1246
+ return value as 0 | 1 | "auto";
1247
+ }
1248
+ throw new GraphFormatError(
1249
+ "E_UNSUPPORTED",
1250
+ `option firstVertex: ${JSON.stringify(value) ?? typeof value} is not 0, 1 or "auto"`,
1251
+ {
1252
+ option: "firstVertex",
1253
+ found: value,
1254
+ },
1255
+ );
1256
+ }
1257
+
1258
+ const HEAD_PATTERN = /^\s*\*(vertices|network)\b/i;
1259
+
1260
+ /** The Pajek NET importer. */
1261
+ export const pajekImporter: GraphImporter<PajekImportOptions> = Object.freeze({
1262
+ format: "pajek",
1263
+ extensions: Object.freeze([".net", ".paj"]),
1264
+ mimeTypes: Object.freeze(["text/x-pajek", "text/plain"]),
1265
+
1266
+ /**
1267
+ * Confidence that the input is a Pajek network: it starts with `*Vertices` (or `*Network`).
1268
+ * @param head - the first bytes
1269
+ * @returns 0.9 for a `*Vertices` start, 0.8 for `*Network`, 0 otherwise
1270
+ */
1271
+ sniff(head: Uint8Array): number {
1272
+ const text = new TextDecoder("utf-8").decode(head.subarray(0, Math.min(head.byteLength, 512)));
1273
+ const match = HEAD_PATTERN.exec(text.startsWith(String.fromCharCode(0xfeff)) ? text.slice(1) : text);
1274
+ if (match === null) {
1275
+ return 0;
1276
+ }
1277
+ return match[1].toLowerCase() === "vertices" ? 0.9 : 0.8;
1278
+ },
1279
+
1280
+ /**
1281
+ * Read a Pajek network into the sink.
1282
+ * @param input - the text, bytes or stream
1283
+ * @param sink - the sink
1284
+ * @param options - common and Pajek options
1285
+ * @returns the report
1286
+ */
1287
+ async import(
1288
+ input: ImportInput,
1289
+ sink: GraphSink,
1290
+ options?: PajekImportOptions & CommonImportOptions,
1291
+ ): Promise<ImportReport> {
1292
+ const resolved = resolveImportOptions(options, DEFAULTS);
1293
+ const firstVertex = firstVertexOption(options?.firstVertex);
1294
+ const report = new ImportReportBuilder("pajek", resolved.errorLimit);
1295
+ reportSinkOptions(sink, options, report);
1296
+ reportUnusedOptions(options, report, USED_OPTIONS);
1297
+ const parser = new PajekParser(sink, report, resolved, firstVertex);
1298
+ const reader = new LineReader(input, report, resolved);
1299
+ let sinceCheck = 0;
1300
+ for await (const text of reader) {
1301
+ parser.line(text, reader.line);
1302
+ if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
1303
+ sinceCheck = 0;
1304
+ throwIfAborted(resolved.signal);
1305
+ }
1306
+ }
1307
+ parser.finish();
1308
+ throwIfAborted(resolved.signal);
1309
+ return report.finish();
1310
+ },
1311
+ });