@graphty/graph-io 0.0.0 → 0.2.0

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,111 @@
1
+ /**
2
+ * Number formatting for exporters (design section 3.7): an f32 weight is written as the shortest
3
+ * decimal that round-trips through Math.fround (never `String(x)`, which prints
4
+ * 0.10000000149011612 for a stored 0.1); an f64 value as its shortest JS text; a GML real always
5
+ * with a decimal point so the dtype survives (design section 8.5); non-finite values as the
6
+ * spelling the target syntax accepts.
7
+ */
8
+
9
+ /**
10
+ * The shortest decimal text that reads back to the same f32 value through Math.fround.
11
+ * @param value - an f32 value (a JS number holding one)
12
+ * @returns the text; "Infinity" / "-Infinity" / "NaN" for the non-finite values, "0" for both zeros
13
+ */
14
+ export function formatF32(value: number): string {
15
+ if (!Number.isFinite(value)) {
16
+ return String(value);
17
+ }
18
+ if (value === 0) {
19
+ return "0";
20
+ }
21
+ for (let digits = 1; digits <= 9; digits++) {
22
+ const text = value.toPrecision(digits);
23
+ if (Math.fround(Number(text)) === value) {
24
+ return String(Number(text));
25
+ }
26
+ }
27
+ return String(value);
28
+ }
29
+
30
+ /**
31
+ * The shortest text of an f64 value: `String(x)`, which is already the shortest round-tripping
32
+ * decimal in JS.
33
+ * @param value - the value
34
+ * @returns the text; "Infinity" / "-Infinity" / "NaN" for the non-finite values
35
+ */
36
+ export function formatF64(value: number): string {
37
+ return String(value);
38
+ }
39
+
40
+ /**
41
+ * A decimal text with a decimal point or an exponent guaranteed for a finite value (`2.0`,
42
+ * `1e+21`, `1.5e-7`), so an
43
+ * untyped re-import keeps the column f64 rather than i32 (design section 8.5); negative zero is
44
+ * written `-0.0` so it reads back as -0; non-finite values are the JS spellings (`Infinity`,
45
+ * `-Infinity`, `NaN`), which the CSV / DOT / Pajek importers read back as text. The one
46
+ * implementation of the "decimal point guaranteed" rule for every text format; GML has its own
47
+ * spellings of the non-finite values in formatGmlReal().
48
+ * @param value - the value
49
+ * @param dtype - the column dtype the value comes from; f32 values use the shortest fround-round-trip text
50
+ * @returns the text
51
+ */
52
+ export function formatDecimal(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8" = "f64"): string {
53
+ if (!Number.isFinite(value)) {
54
+ return String(value);
55
+ }
56
+ if (Object.is(value, -0)) {
57
+ return "-0.0";
58
+ }
59
+ const text = formatNumber(value, dtype);
60
+ // an exponent form (`1e+21`, `1.5e-7`) is already f64 text under the 5.1 grammar
61
+ return text.includes(".") || text.includes("e") ? text : `${text}.0`;
62
+ }
63
+
64
+ /**
65
+ * A GML real: the shortest text with a decimal point guaranteed (`2.0`, `1.0e-7`), so a re-import
66
+ * keeps the column real rather than int. Non-finite values have no GML spelling and are written
67
+ * as the texts NetworkX's writer emits and its reader accepts, `+INF`, `-INF` and `NAN` (the
68
+ * lowercase `inf` / `nan` would lex as keys there).
69
+ * @param value - the value
70
+ * @param dtype - the column dtype the value comes from; f32 values use the shortest fround-round-trip text
71
+ * @returns the text
72
+ */
73
+ export function formatGmlReal(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8" = "f64"): string {
74
+ if (Number.isNaN(value)) {
75
+ return "NAN";
76
+ }
77
+ if (!Number.isFinite(value)) {
78
+ return value > 0 ? "+INF" : "-INF";
79
+ }
80
+ const text = formatDecimal(value, dtype);
81
+ if (text.includes(".")) {
82
+ return text;
83
+ }
84
+ // NetworkX's real pattern requires a decimal point even in the exponent form (`1.0e-7`)
85
+ const e = text.indexOf("e");
86
+ return `${text.slice(0, e)}.0${text.slice(e)}`;
87
+ }
88
+
89
+ /**
90
+ * An integer text for a value known to be integral (an i32 / u32 / u8 cell, an f64 that holds an
91
+ * integer), avoiding the exponent form `String()` uses above 1e21.
92
+ * @param value - an integral value
93
+ * @returns the digits
94
+ */
95
+ export function formatInteger(value: number): string {
96
+ if (Number.isSafeInteger(value) || !Number.isFinite(value)) {
97
+ return String(value);
98
+ }
99
+ return BigInt(value).toString();
100
+ }
101
+
102
+ /**
103
+ * The text of a numeric cell of any numeric dtype, dispatching on the dtype: f32 through
104
+ * formatF32, everything else through formatF64.
105
+ * @param value - the value
106
+ * @param dtype - the column dtype the value came from
107
+ * @returns the text
108
+ */
109
+ export function formatNumber(value: number, dtype: "f32" | "f64" | "i32" | "u32" | "u8"): string {
110
+ return dtype === "f32" ? formatF32(value) : formatF64(value);
111
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Id coercion (design section 4.1): the importer-side rule that turns a text cell or a JSON value
3
+ * into a NodeId before it reaches the sink. The core never coerces.
4
+ *
5
+ * - "keep": typed values pass through; anything that is not a string or a number is E_INVALID_ID.
6
+ * - "canonical": text becomes a number iff it is canonical integer text (`/^-?(0|[1-9][0-9]*)$/`
7
+ * and a safe integer, excluding "-0" which would collide with "0"); every other text stays a
8
+ * string. Injective on text, so `String(id)` on export reproduces the cell exactly: "01", "1.0"
9
+ * and "+1" stay strings, "1" becomes 1. Typed values pass through as under "keep".
10
+ * - "string": `String(v)` for strings, numbers, booleans, bigints and null; anything else is
11
+ * E_INVALID_ID.
12
+ * - "number": `Number(text)` for text (empty or whitespace-only text, NaN and non-finite results
13
+ * are E_INVALID_ID); numbers pass through; anything else is E_INVALID_ID. This rule can merge
14
+ * distinct cells ("01" and "1"); IdCoercer counts such merges so the importer can report them as
15
+ * coercion issues.
16
+ */
17
+
18
+ import { GraphFormatError, type IdCoercion, type NodeId } from "@graphty/graph-format";
19
+
20
+ import { ID_MERGED_CODE } from "./codes.js";
21
+
22
+ const CANONICAL_INTEGER = /^-?(0|[1-9][0-9]*)$/;
23
+
24
+ export { ID_MERGED_CODE };
25
+
26
+ /**
27
+ * The "canonical" rule on one text cell.
28
+ * @param text - the cell text, exactly as read
29
+ * @returns the number for canonical safe-integer text (never for "-0"), the text itself otherwise
30
+ */
31
+ export function canonicalId(text: string): NodeId {
32
+ if (text !== "-0" && CANONICAL_INTEGER.test(text)) {
33
+ const n = Number(text);
34
+ if (Number.isSafeInteger(n)) {
35
+ return n;
36
+ }
37
+ }
38
+ return text;
39
+ }
40
+
41
+ /**
42
+ * Whether a text cell is canonical integer text under the "canonical" rule (a number after import).
43
+ * @param text - the cell text
44
+ * @returns true when canonicalId(text) returns a number
45
+ */
46
+ export function isCanonicalIntegerText(text: string): boolean {
47
+ return typeof canonicalId(text) === "number";
48
+ }
49
+
50
+ /**
51
+ * Coerce a text cell under a rule.
52
+ * @param text - the cell text, exactly as read
53
+ * @param mode - the coercion rule
54
+ * @returns the id; E_INVALID_ID when the rule rejects the text
55
+ */
56
+ export function coerceIdText(text: string, mode: IdCoercion): NodeId {
57
+ switch (mode) {
58
+ case "keep":
59
+ case "string":
60
+ return text;
61
+ case "canonical":
62
+ return canonicalId(text);
63
+ case "number": {
64
+ if (text.trim().length === 0) {
65
+ throw invalidId(text, "empty text is not a number");
66
+ }
67
+ const n = Number(text);
68
+ if (!Number.isFinite(n)) {
69
+ throw invalidId(text, "not a finite number");
70
+ }
71
+ return n;
72
+ }
73
+ default: {
74
+ const name: string = mode;
75
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown id coercion ${name}`, { option: "ids", found: name });
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Coerce a typed value (a JSON scalar, a record field) under a rule.
82
+ * @param value - the value
83
+ * @param mode - the coercion rule
84
+ * @returns the id; E_INVALID_ID when the rule rejects the value
85
+ */
86
+ export function coerceId(value: unknown, mode: IdCoercion): NodeId {
87
+ if (typeof value === "string") {
88
+ return coerceIdText(value, mode);
89
+ }
90
+ if (typeof value === "number") {
91
+ if (!Number.isFinite(value)) {
92
+ throw invalidId(value, "not a finite number");
93
+ }
94
+ return mode === "string" ? String(value) : value;
95
+ }
96
+ if (mode === "string" && (typeof value === "boolean" || typeof value === "bigint" || value === null)) {
97
+ return String(value);
98
+ }
99
+ throw invalidId(value, `a ${value === null ? "null" : typeof value} is not an id under ids: "${mode}"`);
100
+ }
101
+
102
+ /**
103
+ * The E_INVALID_ID error of a rejected value.
104
+ * @param value - the rejected value
105
+ * @param reason - why
106
+ * @returns the error
107
+ */
108
+ function invalidId(value: unknown, reason: string): GraphFormatError {
109
+ const shown = typeof value === "string" || typeof value === "number" ? value : typeof value;
110
+ return new GraphFormatError("E_INVALID_ID", `invalid node id ${JSON.stringify(shown)}: ${reason}`, {
111
+ reason,
112
+ value: typeof value === "bigint" ? value.toString() : value,
113
+ });
114
+ }
115
+
116
+ /**
117
+ * A stateful coercer for one import call: applies the rule and, under "number", detects merges
118
+ * (two distinct texts mapping to one number) so the importer can report them as coercion issues
119
+ * (design section 4.1).
120
+ */
121
+ export class IdCoercer {
122
+ /** The rule. */
123
+ readonly mode: IdCoercion;
124
+
125
+ /** Texts merged into an id another text already produced. */
126
+ mergeCount = 0;
127
+
128
+ /** The text that first produced each numeric id, kept only under "number". */
129
+ private readonly firstText: Map<number, string> | null;
130
+
131
+ /** The merge detected by the most recent text() call, or null. */
132
+ lastMerge: { readonly id: number; readonly text: string; readonly previousText: string } | null = null;
133
+
134
+ /**
135
+ * Create a coercer.
136
+ * @param mode - the rule
137
+ */
138
+ constructor(mode: IdCoercion) {
139
+ this.mode = mode;
140
+ this.firstText = mode === "number" ? new Map() : null;
141
+ }
142
+
143
+ /**
144
+ * Coerce a text cell, recording a merge under "number" when a different text already produced
145
+ * the same number.
146
+ * @param text - the cell text
147
+ * @returns the id
148
+ */
149
+ text(text: string): NodeId {
150
+ const id = coerceIdText(text, this.mode);
151
+ this.lastMerge = null;
152
+ if (this.firstText !== null && typeof id === "number") {
153
+ const previous = this.firstText.get(id);
154
+ if (previous === undefined) {
155
+ this.firstText.set(id, text);
156
+ } else if (previous !== text) {
157
+ this.mergeCount++;
158
+ this.lastMerge = { id, text, previousText: previous };
159
+ }
160
+ }
161
+ return id;
162
+ }
163
+
164
+ /**
165
+ * Coerce a typed value.
166
+ * @param value - the value
167
+ * @returns the id
168
+ */
169
+ value(value: unknown): NodeId {
170
+ if (typeof value === "string") {
171
+ return this.text(value);
172
+ }
173
+ this.lastMerge = null;
174
+ return coerceId(value, this.mode);
175
+ }
176
+ }
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Input handling shared by every importer (design section 8.4): the ImportInput union (whole
3
+ * text, whole bytes, a byte stream, an async iterable of text or byte chunks) read as a sequence of
4
+ * UTF-8 text chunks, as lines, or as one string, with BOM handling, cancellation through an
5
+ * AbortSignal and byte progress.
6
+ *
7
+ * Bytes are decoded with `new TextDecoder("utf-8", { fatal: true })` in streaming mode, so a
8
+ * multi-byte character split across two chunks decodes correctly and an invalid sequence is a
9
+ * parse-error (issue code E_INVALID_UTF8) that aborts the import, never a silent U+FFFD that could
10
+ * alias two ids. A leading U+FEFF is stripped from text and from decoded bytes alike.
11
+ */
12
+
13
+ import { GraphFormatError } from "@graphty/graph-format";
14
+
15
+ import { type ImportInput } from "../types.js";
16
+ import { INVALID_UTF8_CODE } from "./codes.js";
17
+ import { type ImportReportBuilder } from "./report.js";
18
+
19
+ export { INVALID_UTF8_CODE };
20
+
21
+ /**
22
+ * Cancellation and progress hooks of the reader; the resolved importer options satisfy this shape.
23
+ * Consumed by the per-format importers and exporters under src/formats.
24
+ * @public
25
+ */
26
+ export interface ReadOptions {
27
+ /** The cancellation signal, or null / undefined for none. */
28
+ readonly signal?: AbortSignal | null | undefined;
29
+ /** The progress callback, or null / undefined for none. */
30
+ readonly onProgress?: ((bytesDone: number, bytesTotal?: number) => void) | null | undefined;
31
+ }
32
+
33
+ /**
34
+ * Bytes decoded per call when the input is one in-memory Uint8Array: every consumer of textChunks()
35
+ * then sees bounded chunks whatever the input shape (a line reader or a record reader that holds
36
+ * one parse result per chunk never holds more than this much text at once), and progress stays
37
+ * granular.
38
+ */
39
+ const DECODE_SLICE = 256 * 1024;
40
+
41
+ const BOM = String.fromCharCode(0xfeff);
42
+
43
+ /**
44
+ * Whether a value is an ImportInput this module can read.
45
+ * @param input - any value
46
+ * @returns true for a string, a Uint8Array, a ReadableStream or an async iterable
47
+ */
48
+ export function isImportInput(input: unknown): input is ImportInput {
49
+ if (typeof input === "string" || input instanceof Uint8Array) {
50
+ return true;
51
+ }
52
+ if (typeof input !== "object" || input === null) {
53
+ return false;
54
+ }
55
+ return typeof (input as { getReader?: unknown }).getReader === "function" || Symbol.asyncIterator in input;
56
+ }
57
+
58
+ /**
59
+ * The total size of an in-memory input (bytes for a Uint8Array, UTF-16 code units for a string),
60
+ * for the `bytesTotal` argument of onProgress; null for a stream or an iterable.
61
+ * @param input - the input
62
+ * @returns the size, or null when unknown up front
63
+ */
64
+ export function inputLength(input: ImportInput): number | null {
65
+ if (typeof input === "string") {
66
+ return input.length;
67
+ }
68
+ if (input instanceof Uint8Array) {
69
+ return input.byteLength;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Throw the signal's reason when it is aborted, exactly as the platform's
76
+ * `AbortSignal.throwIfAborted()` does: the reason as it is (the DOMException named "AbortError"
77
+ * of a reason-less abort, or whatever the caller passed to `abort(reason)`, an Error or not), so a
78
+ * caller can compare the rejection with `signal.reason`. Only a runtime that stores no reason at
79
+ * all gets a synthesised AbortError.
80
+ * @param signal - the signal, or null
81
+ */
82
+ export function throwIfAborted(signal: AbortSignal | null | undefined): void {
83
+ if (signal === null || signal === undefined || !signal.aborted) {
84
+ return;
85
+ }
86
+ const { reason }: { reason: unknown } = signal;
87
+ if (reason === undefined) {
88
+ throw abortError();
89
+ }
90
+ // the platform contract: the reason itself, whatever its type
91
+ throw reason as Error;
92
+ }
93
+
94
+ /**
95
+ * An abort error for a signal that carries no reason.
96
+ * @returns a DOMException named "AbortError" where DOMException exists, else an Error with that name
97
+ */
98
+ function abortError(): Error {
99
+ const message = "The import was aborted";
100
+ if (typeof DOMException === "function") {
101
+ return new DOMException(message, "AbortError");
102
+ }
103
+ const err = new Error(message);
104
+ err.name = "AbortError";
105
+ return err;
106
+ }
107
+
108
+ /**
109
+ * Read an ImportInput as a sequence of decoded text chunks. Chunk boundaries carry no meaning:
110
+ * a caller that needs lines uses LineReader, one that needs the whole document uses readText().
111
+ * The signal is checked before every chunk; a stream is cancelled when the consumer stops early
112
+ * or the signal fires. Progress is reported after every chunk.
113
+ * @param input - the input
114
+ * @param report - the report the decode error is recorded in (E_INVALID_UTF8, then ImportError)
115
+ * @param options - cancellation and progress
116
+ * @yields decoded text; the first chunk has any leading BOM removed
117
+ * @returns nothing
118
+ */
119
+ export async function* textChunks(
120
+ input: ImportInput,
121
+ report: ImportReportBuilder,
122
+ options: ReadOptions = {},
123
+ ): AsyncGenerator<string, void, undefined> {
124
+ const signal = options.signal ?? null;
125
+ const onProgress = options.onProgress ?? null;
126
+ const total = inputLength(input);
127
+ let done = 0;
128
+ let first = true;
129
+ const emit = (text: string): string => {
130
+ if (first && text.length > 0) {
131
+ first = false;
132
+ return text.startsWith(BOM) ? text.slice(1) : text;
133
+ }
134
+ return text;
135
+ };
136
+ throwIfAborted(signal);
137
+ if (typeof input === "string") {
138
+ const text = emit(input);
139
+ done = input.length;
140
+ if (text.length > 0) {
141
+ yield text;
142
+ }
143
+ onProgress?.(done, total ?? undefined);
144
+ return;
145
+ }
146
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
147
+ const decode = (bytes: Uint8Array, stream: boolean): string => {
148
+ try {
149
+ return decoder.decode(bytes, { stream });
150
+ } catch (err) {
151
+ if (err instanceof TypeError) {
152
+ report.fail(INVALID_UTF8_CODE, `invalid UTF-8 near byte ${done}`, undefined, { byteOffset: done });
153
+ }
154
+ throw err;
155
+ }
156
+ };
157
+ if (input instanceof Uint8Array) {
158
+ for (let offset = 0; offset < input.byteLength; offset += DECODE_SLICE) {
159
+ throwIfAborted(signal);
160
+ const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, input.byteLength));
161
+ const text = emit(decode(slice, offset + DECODE_SLICE < input.byteLength));
162
+ done = Math.min(offset + DECODE_SLICE, input.byteLength);
163
+ if (text.length > 0) {
164
+ yield text;
165
+ }
166
+ onProgress?.(done, total ?? undefined);
167
+ }
168
+ if (input.byteLength === 0) {
169
+ onProgress?.(0, 0);
170
+ }
171
+ return;
172
+ }
173
+ const chunks = isReadableStream(input) ? streamChunks(input, signal) : input;
174
+ for await (const chunk of chunks) {
175
+ throwIfAborted(signal);
176
+ let text: string;
177
+ if (typeof chunk === "string") {
178
+ // finish any byte sequence still pending in the decoder before switching to text
179
+ const pending = decode(new Uint8Array(0), false);
180
+ text = emit(pending + chunk);
181
+ done += chunk.length;
182
+ } else if (chunk instanceof Uint8Array) {
183
+ text = emit(decode(chunk, true));
184
+ done += chunk.byteLength;
185
+ } else {
186
+ throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
187
+ reason: "chunk type",
188
+ found: typeof chunk,
189
+ });
190
+ }
191
+ if (text.length > 0) {
192
+ yield text;
193
+ }
194
+ onProgress?.(done);
195
+ }
196
+ const tail = emit(decode(new Uint8Array(0), false));
197
+ if (tail.length > 0) {
198
+ yield tail;
199
+ }
200
+ onProgress?.(done, done);
201
+ }
202
+
203
+ /**
204
+ * Whether an input is a ReadableStream (by duck type, so a stream from another realm qualifies).
205
+ * @param input - a non-string, non-Uint8Array input
206
+ * @returns true for a ReadableStream
207
+ */
208
+ function isReadableStream(
209
+ input: ReadableStream<Uint8Array> | AsyncIterable<string | Uint8Array>,
210
+ ): input is ReadableStream<Uint8Array> {
211
+ return typeof (input as { getReader?: unknown }).getReader === "function";
212
+ }
213
+
214
+ /**
215
+ * Iterate a ReadableStream through a reader, cancelling the stream when iteration stops early.
216
+ * @param stream - the stream
217
+ * @param signal - the cancellation signal, or null
218
+ * @yields the stream's chunks
219
+ * @returns nothing
220
+ */
221
+ async function* streamChunks(
222
+ stream: ReadableStream<Uint8Array>,
223
+ signal: AbortSignal | null,
224
+ ): AsyncGenerator<Uint8Array, void, undefined> {
225
+ const reader = stream.getReader();
226
+ let finished = false;
227
+ try {
228
+ for (;;) {
229
+ throwIfAborted(signal);
230
+ const { done, value } = await reader.read();
231
+ if (done) {
232
+ finished = true;
233
+ return;
234
+ }
235
+ yield value;
236
+ }
237
+ } finally {
238
+ if (!finished) {
239
+ await reader.cancel(signal?.reason).catch(() => undefined);
240
+ }
241
+ reader.releaseLock();
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Read the whole input as one string (the GML / DOT / JSON path, design section 8.4).
247
+ * @param input - the input
248
+ * @param report - the report the decode error is recorded in
249
+ * @param options - cancellation and progress
250
+ * @returns the decoded text without a leading BOM
251
+ */
252
+ export async function readText(
253
+ input: ImportInput,
254
+ report: ImportReportBuilder,
255
+ options: ReadOptions = {},
256
+ ): Promise<string> {
257
+ if (typeof input === "string") {
258
+ throwIfAborted(options.signal);
259
+ options.onProgress?.(input.length, input.length);
260
+ return input.startsWith(BOM) ? input.slice(1) : input;
261
+ }
262
+ const parts: string[] = [];
263
+ for await (const chunk of textChunks(input, report, options)) {
264
+ parts.push(chunk);
265
+ }
266
+ return parts.length === 1 ? parts[0] : parts.join("");
267
+ }
268
+
269
+ /**
270
+ * Lines of an ImportInput without allocating anything per line but the string itself: iterate with
271
+ * `for await (const text of reader)` and read `reader.line` (1-based) for the line just yielded.
272
+ * `\n`, `\r\n` and lone `\r` all end a line; the terminator is not part of the text; a final
273
+ * line without a terminator is yielded when non-empty, and every line in between is yielded even
274
+ * when empty (the importer decides what a blank line means).
275
+ */
276
+ export class LineReader implements AsyncIterable<string> {
277
+ private readonly input: ImportInput;
278
+
279
+ private readonly report: ImportReportBuilder;
280
+
281
+ private readonly options: ReadOptions;
282
+
283
+ private lineNumber = 0;
284
+
285
+ /**
286
+ * Create a reader over an input; nothing is read until iteration starts.
287
+ * @param input - the input
288
+ * @param report - the report decode errors are recorded in
289
+ * @param options - cancellation and progress
290
+ */
291
+ constructor(input: ImportInput, report: ImportReportBuilder, options: ReadOptions = {}) {
292
+ this.input = input;
293
+ this.report = report;
294
+ this.options = options;
295
+ }
296
+
297
+ /**
298
+ * The 1-based number of the line most recently yielded.
299
+ * @returns the line number; 0 before the first line
300
+ */
301
+ get line(): number {
302
+ return this.lineNumber;
303
+ }
304
+
305
+ /**
306
+ * Iterate the lines.
307
+ * @yields one line at a time, terminator removed
308
+ * @returns nothing
309
+ */
310
+ async *[Symbol.asyncIterator](): AsyncGenerator<string, void, undefined> {
311
+ // The pieces of the line in progress (none of them holds a line break, except that the last
312
+ // may end with a `\r` whose meaning the next chunk decides); joined once when the line ends,
313
+ // so a line spanning many chunks costs its length, not its length times the chunk count.
314
+ const pending: string[] = [];
315
+ let trailingCr = false;
316
+ for await (const chunk of textChunks(this.input, this.report, this.options)) {
317
+ let start = 0;
318
+ const end = chunk.length;
319
+ if (trailingCr) {
320
+ // the previous chunk ended with `\r`: that ended a line, and a leading `\n` here is
321
+ // the second half of the same terminator
322
+ trailingCr = false;
323
+ this.lineNumber++;
324
+ const joined = pending.join("");
325
+ pending.length = 0;
326
+ yield joined.slice(0, -1);
327
+ if (chunk.charCodeAt(0) === 10) {
328
+ start = 1;
329
+ }
330
+ }
331
+ // the next `\n` and `\r` at or after `start`; each is searched for once per chunk and
332
+ // again only after it was consumed, so a chunk is scanned once whatever its line count
333
+ let nl = chunk.indexOf("\n", start);
334
+ let cr = chunk.indexOf("\r", start);
335
+ while (nl >= 0 || cr >= 0) {
336
+ let cut: number;
337
+ let next: number;
338
+ if (cr >= 0 && (nl < 0 || cr < nl)) {
339
+ if (cr === end - 1) {
340
+ // a trailing \r may be the first half of \r\n split across chunks
341
+ break;
342
+ }
343
+ cut = cr;
344
+ next = chunk.charCodeAt(cr + 1) === 10 ? cr + 2 : cr + 1;
345
+ } else {
346
+ cut = nl;
347
+ next = nl + 1;
348
+ }
349
+ this.lineNumber++;
350
+ const piece = chunk.slice(start, cut);
351
+ if (pending.length === 0) {
352
+ yield piece;
353
+ } else {
354
+ pending.push(piece);
355
+ const joined = pending.join("");
356
+ pending.length = 0;
357
+ yield joined;
358
+ }
359
+ start = next;
360
+ if (nl >= 0 && nl < next) {
361
+ nl = chunk.indexOf("\n", next);
362
+ }
363
+ if (cr >= 0 && cr < next) {
364
+ cr = chunk.indexOf("\r", next);
365
+ }
366
+ }
367
+ if (start < end) {
368
+ pending.push(start === 0 ? chunk : chunk.slice(start));
369
+ trailingCr = chunk.charCodeAt(end - 1) === 13;
370
+ }
371
+ }
372
+ if (pending.length > 0) {
373
+ this.lineNumber++;
374
+ const joined = pending.join("");
375
+ yield joined.endsWith("\r") ? joined.slice(0, -1) : joined;
376
+ }
377
+ }
378
+ }