@graphty/graph-io 0.3.48 → 0.3.50

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 (600) hide show
  1. package/dist/chunks/{approximations-C2riR4NT.js → approximations-BRXsll0T.js} +2 -2
  2. package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-BRXsll0T.js.map} +1 -1
  3. package/dist/chunks/bends-BGXjv_U4.js +129 -0
  4. package/dist/chunks/bends-BGXjv_U4.js.map +1 -0
  5. package/dist/chunks/{block-Cu22ewuS.js → block-yl0f_qLO.js} +931 -164
  6. package/dist/chunks/block-yl0f_qLO.js.map +1 -0
  7. package/dist/chunks/choose-CczZJaX_.js +717 -0
  8. package/dist/chunks/choose-CczZJaX_.js.map +1 -0
  9. package/dist/chunks/{codes-CV_Bhcyi.js → codes-BE2xir2k.js} +6 -2
  10. package/dist/chunks/codes-BE2xir2k.js.map +1 -0
  11. package/dist/chunks/{constants-D0LqcsLJ.js → constants-CKkYLPtt.js} +2 -2
  12. package/dist/chunks/{constants-D0LqcsLJ.js.map → constants-CKkYLPtt.js.map} +1 -1
  13. package/dist/chunks/{context-cgmmOxZ2.js → context-DizmppHy.js} +5 -4
  14. package/dist/chunks/{context-cgmmOxZ2.js.map → context-DizmppHy.js.map} +1 -1
  15. package/dist/chunks/{convert-BcQ8_jMo.js → convert-Ct2zelbQ.js} +7 -2
  16. package/dist/chunks/convert-Ct2zelbQ.js.map +1 -0
  17. package/dist/chunks/{credentials-BiQsnMTb.js → credentials-CmvJXYeH.js} +67 -16
  18. package/dist/chunks/credentials-CmvJXYeH.js.map +1 -0
  19. package/dist/chunks/{cx-export-BEeF6ymo.js → cx-export-Bt6NoqTQ.js} +3 -3
  20. package/dist/chunks/{cx-export-BEeF6ymo.js.map → cx-export-Bt6NoqTQ.js.map} +1 -1
  21. package/dist/chunks/descriptor-B28U25x4.js +60 -0
  22. package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
  23. package/dist/chunks/{descriptor-BJ-W0Jd-.js → descriptor-Dhez_Cew.js} +11 -2
  24. package/dist/chunks/descriptor-Dhez_Cew.js.map +1 -0
  25. package/dist/chunks/{emit-CD8xnY8s.js → emit-7Jkpg0yd.js} +1788 -133
  26. package/dist/chunks/emit-7Jkpg0yd.js.map +1 -0
  27. package/dist/chunks/{escape-DH2Egt-l.js → escape-CuCIzNMB.js} +8 -8
  28. package/dist/chunks/{escape-DH2Egt-l.js.map → escape-CuCIzNMB.js.map} +1 -1
  29. package/dist/chunks/{export-CZ7VRm4I.js → export-Cha4dRFz.js} +45 -387
  30. package/dist/chunks/export-Cha4dRFz.js.map +1 -0
  31. package/dist/chunks/{exporter-ChjL5fmP.js → exporter-1Ajud15J.js} +9 -8
  32. package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-1Ajud15J.js.map} +1 -1
  33. package/dist/chunks/{exporter-BIdaoIgh.js → exporter-B6yvL3wy.js} +7 -7
  34. package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-B6yvL3wy.js.map} +1 -1
  35. package/dist/chunks/{exporter-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
  36. package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
  37. package/dist/chunks/{exporter-DtVU80Fa.js → exporter-CA5q8ROP.js} +9 -8
  38. package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-CA5q8ROP.js.map} +1 -1
  39. package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-CQQZFJ2v.js} +1284 -156
  40. package/dist/chunks/exporter-CQQZFJ2v.js.map +1 -0
  41. package/dist/chunks/format-BXiAb-1h.js +408 -0
  42. package/dist/chunks/format-BXiAb-1h.js.map +1 -0
  43. package/dist/chunks/{generic-CP6YD34u.js → generic-Dc21MAdg.js} +387 -31
  44. package/dist/chunks/generic-Dc21MAdg.js.map +1 -0
  45. package/dist/chunks/{importer-B87_fmVx.js → importer-C6NVpCXp.js} +327 -84
  46. package/dist/chunks/importer-C6NVpCXp.js.map +1 -0
  47. package/dist/chunks/{importer-DPZDN40-.js → importer-CKFe4q54.js} +25 -17
  48. package/dist/chunks/importer-CKFe4q54.js.map +1 -0
  49. package/dist/chunks/{importer-Bji2MeWr.js → importer-CKbUbsJA.js} +13 -10
  50. package/dist/chunks/{importer-Bji2MeWr.js.map → importer-CKbUbsJA.js.map} +1 -1
  51. package/dist/chunks/{importer-CB_zV5_L.js → importer-ClCsdgTJ.js} +208 -1104
  52. package/dist/chunks/importer-ClCsdgTJ.js.map +1 -0
  53. package/dist/chunks/{importer-e3huMZbm.js → importer-CyVZTrf0.js} +14 -10
  54. package/dist/chunks/importer-CyVZTrf0.js.map +1 -0
  55. package/dist/chunks/{importer-CVT-JyDE.js → importer-DgG4v1Ms.js} +1537 -355
  56. package/dist/chunks/importer-DgG4v1Ms.js.map +1 -0
  57. package/dist/chunks/{importer-Cyy6UseH.js → importer-asFUrIsj.js} +66 -13
  58. package/dist/chunks/importer-asFUrIsj.js.map +1 -0
  59. package/dist/chunks/{input-Ccg9I22A.js → input-CAxt_aV-.js} +4 -4
  60. package/dist/chunks/{input-Ccg9I22A.js.map → input-CAxt_aV-.js.map} +1 -1
  61. package/dist/chunks/{json-elements-By172SVq.js → json-elements-Cq76WjnZ.js} +4 -4
  62. package/dist/chunks/{json-elements-By172SVq.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
  63. package/dist/chunks/{lexicon-u2SI5xcx.js → lexicon-IVRq2Uoo.js} +282 -74
  64. package/dist/chunks/lexicon-IVRq2Uoo.js.map +1 -0
  65. package/dist/chunks/{ontology-export-BwgMyuF0.js → ontology-export-OMo91mb0.js} +4 -4
  66. package/dist/chunks/{ontology-export-BwgMyuF0.js.map → ontology-export-OMo91mb0.js.map} +1 -1
  67. package/dist/chunks/{records-Cgvxz-Vx.js → records-NJiYvekn.js} +2 -2
  68. package/dist/chunks/{records-Cgvxz-Vx.js.map → records-NJiYvekn.js.map} +1 -1
  69. package/dist/chunks/{report-CgdsL9Hb.js → report-CwMfYa9J.js} +23 -23
  70. package/dist/chunks/{report-CgdsL9Hb.js.map → report-CwMfYa9J.js.map} +1 -1
  71. package/dist/chunks/round-trip-Cy3_SHRZ.js +79 -0
  72. package/dist/chunks/round-trip-Cy3_SHRZ.js.map +1 -0
  73. package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-BXwVbza3.js} +5 -5
  74. package/dist/chunks/sigv4-BXwVbza3.js.map +1 -0
  75. package/dist/chunks/{styles-CaABMoO1.js → styles-CAVmBLgp.js} +1237 -138
  76. package/dist/chunks/styles-CAVmBLgp.js.map +1 -0
  77. package/dist/chunks/styles-CPczw1Jz.js +1928 -0
  78. package/dist/chunks/styles-CPczw1Jz.js.map +1 -0
  79. package/dist/chunks/styles-boK6Ln54.js +2847 -0
  80. package/dist/chunks/styles-boK6Ln54.js.map +1 -0
  81. package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-XeFBQ6VS.js} +506 -175
  82. package/dist/chunks/styles-yed-XeFBQ6VS.js.map +1 -0
  83. package/dist/chunks/{text-BuKQVMz2.js → text-ystE2bYi.js} +2 -2
  84. package/dist/chunks/{text-BuKQVMz2.js.map → text-ystE2bYi.js.map} +1 -1
  85. package/dist/csv.js +7 -7
  86. package/dist/cx.js +2 -2
  87. package/dist/cx2.js +2 -2
  88. package/dist/cys.js +1 -1
  89. package/dist/dot.js +8 -4
  90. package/dist/dot.js.map +1 -1
  91. package/dist/gexf.js +42 -12
  92. package/dist/gexf.js.map +1 -1
  93. package/dist/gml.js +848 -194
  94. package/dist/gml.js.map +1 -1
  95. package/dist/graph-io.js +230 -193
  96. package/dist/graph-io.js.map +1 -1
  97. package/dist/graphml.js +3 -3
  98. package/dist/json.js +3 -2
  99. package/dist/neo4j.js +6 -6
  100. package/dist/obo.js +1 -1
  101. package/dist/pajek.js +1 -1
  102. package/dist/positions.js +45 -3
  103. package/dist/positions.js.map +1 -1
  104. package/dist/sources-cypher.js +141 -23
  105. package/dist/sources-cypher.js.map +1 -1
  106. package/dist/sources-ndex.js +2 -2
  107. package/dist/sources-rest.js +32 -6
  108. package/dist/sources-rest.js.map +1 -1
  109. package/dist/sources-sparql.d.ts +1 -0
  110. package/dist/sources-sparql.js +439 -0
  111. package/dist/sources-sparql.js.map +1 -0
  112. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  113. package/dist/src/formats/cx/importer.js +5 -1
  114. package/dist/src/formats/cx/importer.js.map +1 -1
  115. package/dist/src/formats/cx/styles.d.ts +24 -1
  116. package/dist/src/formats/cx/styles.d.ts.map +1 -1
  117. package/dist/src/formats/cx/styles.js +117 -11
  118. package/dist/src/formats/cx/styles.js.map +1 -1
  119. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  120. package/dist/src/formats/cx2/importer.js +1 -0
  121. package/dist/src/formats/cx2/importer.js.map +1 -1
  122. package/dist/src/formats/cx2/styles.d.ts.map +1 -1
  123. package/dist/src/formats/cx2/styles.js +3 -1
  124. package/dist/src/formats/cx2/styles.js.map +1 -1
  125. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cys/importer.js +1 -0
  127. package/dist/src/formats/cys/importer.js.map +1 -1
  128. package/dist/src/formats/cys/styles.d.ts +2 -0
  129. package/dist/src/formats/cys/styles.d.ts.map +1 -1
  130. package/dist/src/formats/cys/styles.js +15 -7
  131. package/dist/src/formats/cys/styles.js.map +1 -1
  132. package/dist/src/formats/dot/drawing.d.ts +98 -0
  133. package/dist/src/formats/dot/drawing.d.ts.map +1 -0
  134. package/dist/src/formats/dot/drawing.js +734 -0
  135. package/dist/src/formats/dot/drawing.js.map +1 -0
  136. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  137. package/dist/src/formats/dot/exporter.js +34 -2
  138. package/dist/src/formats/dot/exporter.js.map +1 -1
  139. package/dist/src/formats/dot/importer.d.ts +6 -1
  140. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  141. package/dist/src/formats/dot/importer.js +170 -33
  142. package/dist/src/formats/dot/importer.js.map +1 -1
  143. package/dist/src/formats/dot/index.d.ts +3 -1
  144. package/dist/src/formats/dot/index.d.ts.map +1 -1
  145. package/dist/src/formats/dot/index.js +2 -1
  146. package/dist/src/formats/dot/index.js.map +1 -1
  147. package/dist/src/formats/dot/label-places.d.ts +41 -0
  148. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  149. package/dist/src/formats/dot/label-places.js +185 -0
  150. package/dist/src/formats/dot/label-places.js.map +1 -0
  151. package/dist/src/formats/dot/names.d.ts +13 -0
  152. package/dist/src/formats/dot/names.d.ts.map +1 -1
  153. package/dist/src/formats/dot/names.js +13 -0
  154. package/dist/src/formats/dot/names.js.map +1 -1
  155. package/dist/src/formats/dot/record.d.ts +50 -0
  156. package/dist/src/formats/dot/record.d.ts.map +1 -0
  157. package/dist/src/formats/dot/record.js +275 -0
  158. package/dist/src/formats/dot/record.js.map +1 -0
  159. package/dist/src/formats/dot/styles.d.ts +72 -2
  160. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  161. package/dist/src/formats/dot/styles.js +1500 -119
  162. package/dist/src/formats/dot/styles.js.map +1 -1
  163. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  164. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  165. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  166. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  167. package/dist/src/formats/gexf/importer.d.ts +7 -0
  168. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  169. package/dist/src/formats/gexf/importer.js +41 -5
  170. package/dist/src/formats/gexf/importer.js.map +1 -1
  171. package/dist/src/formats/gexf/preview.d.ts +77 -0
  172. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  173. package/dist/src/formats/gexf/preview.js +191 -0
  174. package/dist/src/formats/gexf/preview.js.map +1 -0
  175. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  176. package/dist/src/formats/gexf/schema.js +2 -1
  177. package/dist/src/formats/gexf/schema.js.map +1 -1
  178. package/dist/src/formats/gexf/styles.d.ts +58 -31
  179. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  180. package/dist/src/formats/gexf/styles.js +848 -225
  181. package/dist/src/formats/gexf/styles.js.map +1 -1
  182. package/dist/src/formats/gml/importer.d.ts +5 -2
  183. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  184. package/dist/src/formats/gml/importer.js +258 -71
  185. package/dist/src/formats/gml/importer.js.map +1 -1
  186. package/dist/src/formats/gml/index.d.ts +1 -1
  187. package/dist/src/formats/gml/index.d.ts.map +1 -1
  188. package/dist/src/formats/gml/index.js +1 -1
  189. package/dist/src/formats/gml/index.js.map +1 -1
  190. package/dist/src/formats/gml/positions.d.ts +7 -0
  191. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  192. package/dist/src/formats/gml/positions.js +29 -7
  193. package/dist/src/formats/gml/positions.js.map +1 -1
  194. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  195. package/dist/src/formats/gml/styles.js +754 -102
  196. package/dist/src/formats/gml/styles.js.map +1 -1
  197. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  198. package/dist/src/formats/graphml/styles-plain.js +6 -10
  199. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  200. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  201. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  202. package/dist/src/formats/graphml/styles-yed.js +695 -164
  203. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  204. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  205. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  206. package/dist/src/formats/graphml/styles-yfiles3.js +431 -64
  207. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  208. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  209. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  210. package/dist/src/formats/graphml/yfiles.js +16 -6
  211. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  212. package/dist/src/formats/json/dialect.d.ts +3 -2
  213. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  214. package/dist/src/formats/json/dialect.js +39 -5
  215. package/dist/src/formats/json/dialect.js.map +1 -1
  216. package/dist/src/formats/json/importer.d.ts +13 -2
  217. package/dist/src/formats/json/importer.d.ts.map +1 -1
  218. package/dist/src/formats/json/importer.js +42 -6
  219. package/dist/src/formats/json/importer.js.map +1 -1
  220. package/dist/src/formats/json/index.d.ts +1 -1
  221. package/dist/src/formats/json/index.d.ts.map +1 -1
  222. package/dist/src/formats/json/index.js +1 -1
  223. package/dist/src/formats/json/index.js.map +1 -1
  224. package/dist/src/formats/json/positions.d.ts +5 -2
  225. package/dist/src/formats/json/positions.d.ts.map +1 -1
  226. package/dist/src/formats/json/positions.js +20 -4
  227. package/dist/src/formats/json/positions.js.map +1 -1
  228. package/dist/src/formats/json/styles.d.ts +12 -1
  229. package/dist/src/formats/json/styles.d.ts.map +1 -1
  230. package/dist/src/formats/json/styles.js +1843 -136
  231. package/dist/src/formats/json/styles.js.map +1 -1
  232. package/dist/src/formats/pajek/draw.d.ts +104 -0
  233. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  234. package/dist/src/formats/pajek/draw.js +382 -0
  235. package/dist/src/formats/pajek/draw.js.map +1 -0
  236. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  237. package/dist/src/formats/pajek/exporter.js +70 -51
  238. package/dist/src/formats/pajek/exporter.js.map +1 -1
  239. package/dist/src/formats/pajek/importer.d.ts +9 -0
  240. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  241. package/dist/src/formats/pajek/importer.js +233 -102
  242. package/dist/src/formats/pajek/importer.js.map +1 -1
  243. package/dist/src/formats/pajek/index.d.ts +1 -0
  244. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  245. package/dist/src/formats/pajek/index.js.map +1 -1
  246. package/dist/src/formats/pajek/styles.d.ts +15 -7
  247. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  248. package/dist/src/formats/pajek/styles.js +1395 -114
  249. package/dist/src/formats/pajek/styles.js.map +1 -1
  250. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  251. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  252. package/dist/src/formats/pajek/syntax.js +26 -2
  253. package/dist/src/formats/pajek/syntax.js.map +1 -1
  254. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  255. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  256. package/dist/src/formats/xgmml/emit.js +65 -5
  257. package/dist/src/formats/xgmml/emit.js.map +1 -1
  258. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  259. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  260. package/dist/src/formats/xgmml/styles.js +277 -60
  261. package/dist/src/formats/xgmml/styles.js.map +1 -1
  262. package/dist/src/index.d.ts +6 -5
  263. package/dist/src/index.d.ts.map +1 -1
  264. package/dist/src/index.js +6 -5
  265. package/dist/src/index.js.map +1 -1
  266. package/dist/src/positions/convert.d.ts.map +1 -1
  267. package/dist/src/positions/convert.js +11 -1
  268. package/dist/src/positions/convert.js.map +1 -1
  269. package/dist/src/positions/extract.d.ts.map +1 -1
  270. package/dist/src/positions/extract.js +53 -1
  271. package/dist/src/positions/extract.js.map +1 -1
  272. package/dist/src/positions/index.d.ts +1 -1
  273. package/dist/src/positions/index.d.ts.map +1 -1
  274. package/dist/src/positions/types.d.ts +59 -0
  275. package/dist/src/positions/types.d.ts.map +1 -1
  276. package/dist/src/sources/builtins.d.ts +1 -1
  277. package/dist/src/sources/builtins.d.ts.map +1 -1
  278. package/dist/src/sources/builtins.js +8 -2
  279. package/dist/src/sources/builtins.js.map +1 -1
  280. package/dist/src/sources/context.d.ts.map +1 -1
  281. package/dist/src/sources/context.js +1 -0
  282. package/dist/src/sources/context.js.map +1 -1
  283. package/dist/src/sources/credentials.d.ts +50 -1
  284. package/dist/src/sources/credentials.d.ts.map +1 -1
  285. package/dist/src/sources/credentials.js +69 -0
  286. package/dist/src/sources/credentials.js.map +1 -1
  287. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
  288. package/dist/src/sources/cypher/descriptor.js +5 -0
  289. package/dist/src/sources/cypher/descriptor.js.map +1 -1
  290. package/dist/src/sources/cypher/index.d.ts.map +1 -1
  291. package/dist/src/sources/cypher/index.js +125 -18
  292. package/dist/src/sources/cypher/index.js.map +1 -1
  293. package/dist/src/sources/cypher/params.d.ts +1 -1
  294. package/dist/src/sources/cypher/params.d.ts.map +1 -1
  295. package/dist/src/sources/cypher/query-builder.d.ts +17 -0
  296. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
  297. package/dist/src/sources/cypher/query-builder.js +31 -0
  298. package/dist/src/sources/cypher/query-builder.js.map +1 -1
  299. package/dist/src/sources/ids.d.ts +5 -0
  300. package/dist/src/sources/ids.d.ts.map +1 -1
  301. package/dist/src/sources/ids.js +5 -0
  302. package/dist/src/sources/ids.js.map +1 -1
  303. package/dist/src/sources/params.d.ts +3 -0
  304. package/dist/src/sources/params.d.ts.map +1 -1
  305. package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
  306. package/dist/src/sources/rest/descriptor.js +9 -0
  307. package/dist/src/sources/rest/descriptor.js.map +1 -1
  308. package/dist/src/sources/rest/openalex.d.ts +4 -2
  309. package/dist/src/sources/rest/openalex.d.ts.map +1 -1
  310. package/dist/src/sources/rest/openalex.js +4 -2
  311. package/dist/src/sources/rest/openalex.js.map +1 -1
  312. package/dist/src/sources/rest/source.d.ts.map +1 -1
  313. package/dist/src/sources/rest/source.js +38 -0
  314. package/dist/src/sources/rest/source.js.map +1 -1
  315. package/dist/src/sources/run.d.ts +2 -0
  316. package/dist/src/sources/run.d.ts.map +1 -1
  317. package/dist/src/sources/run.js +30 -3
  318. package/dist/src/sources/run.js.map +1 -1
  319. package/dist/src/sources/sigv4.d.ts +5 -3
  320. package/dist/src/sources/sigv4.d.ts.map +1 -1
  321. package/dist/src/sources/sigv4.js +6 -4
  322. package/dist/src/sources/sigv4.js.map +1 -1
  323. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  324. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  325. package/dist/src/sources/sparql/descriptor.js +64 -0
  326. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  327. package/dist/src/sources/sparql/index.d.ts +28 -0
  328. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  329. package/dist/src/sources/sparql/index.js +327 -0
  330. package/dist/src/sources/sparql/index.js.map +1 -0
  331. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  332. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  333. package/dist/src/sources/sparql/ntriples.js +180 -0
  334. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  335. package/dist/src/sources/sparql/params.d.ts +27 -0
  336. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  337. package/dist/src/sources/sparql/params.js +5 -0
  338. package/dist/src/sources/sparql/params.js.map +1 -0
  339. package/dist/src/sources/sparql/query.d.ts +73 -0
  340. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  341. package/dist/src/sources/sparql/query.js +164 -0
  342. package/dist/src/sources/sparql/query.js.map +1 -0
  343. package/dist/src/sources/types.d.ts +60 -0
  344. package/dist/src/sources/types.d.ts.map +1 -1
  345. package/dist/src/styles/annotations.d.ts +61 -5
  346. package/dist/src/styles/annotations.d.ts.map +1 -1
  347. package/dist/src/styles/annotations.js +130 -22
  348. package/dist/src/styles/annotations.js.map +1 -1
  349. package/dist/src/styles/canon/bends.d.ts +46 -0
  350. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  351. package/dist/src/styles/canon/bends.js +123 -0
  352. package/dist/src/styles/canon/bends.js.map +1 -1
  353. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  354. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  355. package/dist/src/styles/canon/color-tables.js +10 -4
  356. package/dist/src/styles/canon/color-tables.js.map +1 -1
  357. package/dist/src/styles/canon/color.d.ts +3 -1
  358. package/dist/src/styles/canon/color.d.ts.map +1 -1
  359. package/dist/src/styles/canon/color.js +103 -3
  360. package/dist/src/styles/canon/color.js.map +1 -1
  361. package/dist/src/styles/canon/enums.d.ts +2 -2
  362. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  363. package/dist/src/styles/canon/enums.js +89 -7
  364. package/dist/src/styles/canon/enums.js.map +1 -1
  365. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  366. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  367. package/dist/src/styles/canon/graphviz.js +57 -9
  368. package/dist/src/styles/canon/graphviz.js.map +1 -1
  369. package/dist/src/styles/canon/number.d.ts +8 -0
  370. package/dist/src/styles/canon/number.d.ts.map +1 -1
  371. package/dist/src/styles/canon/number.js +18 -0
  372. package/dist/src/styles/canon/number.js.map +1 -1
  373. package/dist/src/styles/canon/text.d.ts +29 -2
  374. package/dist/src/styles/canon/text.d.ts.map +1 -1
  375. package/dist/src/styles/canon/text.js +156 -16
  376. package/dist/src/styles/canon/text.js.map +1 -1
  377. package/dist/src/styles/codes.d.ts +2 -0
  378. package/dist/src/styles/codes.d.ts.map +1 -1
  379. package/dist/src/styles/codes.js +2 -0
  380. package/dist/src/styles/codes.js.map +1 -1
  381. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  382. package/dist/src/styles/cyjs/cascade.js +1139 -79
  383. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  384. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  385. package/dist/src/styles/cyjs/defaults-table.js +4 -0
  386. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  387. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  388. package/dist/src/styles/cyjs/emit.js +232 -10
  389. package/dist/src/styles/cyjs/emit.js.map +1 -1
  390. package/dist/src/styles/cyjs/face.d.ts +20 -0
  391. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  392. package/dist/src/styles/cyjs/face.js +332 -0
  393. package/dist/src/styles/cyjs/face.js.map +1 -0
  394. package/dist/src/styles/cyjs/mappers.d.ts +44 -1
  395. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  396. package/dist/src/styles/cyjs/mappers.js +475 -26
  397. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  398. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  399. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  400. package/dist/src/styles/cyjs/routes.js +360 -0
  401. package/dist/src/styles/cyjs/routes.js.map +1 -0
  402. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  403. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  404. package/dist/src/styles/cytoscape/charts.js +560 -0
  405. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  406. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  407. package/dist/src/styles/cytoscape/emit.js +156 -48
  408. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  409. package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
  410. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  411. package/dist/src/styles/cytoscape/lexicon.js +387 -77
  412. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  413. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  414. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  415. package/dist/src/styles/cytoscape/mappings.js +92 -0
  416. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  417. package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
  418. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  419. package/dist/src/styles/cytoscape/resolve.js +623 -17
  420. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  421. package/dist/src/styles/defaults.d.ts +46 -26
  422. package/dist/src/styles/defaults.d.ts.map +1 -1
  423. package/dist/src/styles/defaults.js +308 -71
  424. package/dist/src/styles/defaults.js.map +1 -1
  425. package/dist/src/styles/index.d.ts +4 -3
  426. package/dist/src/styles/index.d.ts.map +1 -1
  427. package/dist/src/styles/index.js +3 -2
  428. package/dist/src/styles/index.js.map +1 -1
  429. package/dist/src/styles/internal.d.ts +1 -0
  430. package/dist/src/styles/internal.d.ts.map +1 -1
  431. package/dist/src/styles/internal.js +1 -0
  432. package/dist/src/styles/internal.js.map +1 -1
  433. package/dist/src/styles/io/generic.d.ts.map +1 -1
  434. package/dist/src/styles/io/generic.js +56 -16
  435. package/dist/src/styles/io/generic.js.map +1 -1
  436. package/dist/src/styles/io/round-trip.d.ts +56 -0
  437. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  438. package/dist/src/styles/io/round-trip.js +138 -0
  439. package/dist/src/styles/io/round-trip.js.map +1 -0
  440. package/dist/src/styles/io/writer.d.ts.map +1 -1
  441. package/dist/src/styles/io/writer.js +6 -1
  442. package/dist/src/styles/io/writer.js.map +1 -1
  443. package/dist/src/styles/notes.d.ts.map +1 -1
  444. package/dist/src/styles/notes.js +1 -0
  445. package/dist/src/styles/notes.js.map +1 -1
  446. package/dist/src/styles/plan/features.d.ts +1 -1
  447. package/dist/src/styles/plan/features.d.ts.map +1 -1
  448. package/dist/src/styles/plan/features.js +22 -2
  449. package/dist/src/styles/plan/features.js.map +1 -1
  450. package/dist/src/styles/plan/index.d.ts +1 -1
  451. package/dist/src/styles/plan/index.d.ts.map +1 -1
  452. package/dist/src/styles/plan/index.js +1 -1
  453. package/dist/src/styles/plan/index.js.map +1 -1
  454. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  455. package/dist/src/styles/plan/plan.js +61 -4
  456. package/dist/src/styles/plan/plan.js.map +1 -1
  457. package/dist/src/styles/plan/resolve.d.ts +11 -0
  458. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  459. package/dist/src/styles/plan/resolve.js +40 -4
  460. package/dist/src/styles/plan/resolve.js.map +1 -1
  461. package/dist/src/styles/slots.d.ts +14 -0
  462. package/dist/src/styles/slots.d.ts.map +1 -0
  463. package/dist/src/styles/slots.js +20 -0
  464. package/dist/src/styles/slots.js.map +1 -0
  465. package/dist/src/styles/types.d.ts +687 -6
  466. package/dist/src/styles/types.d.ts.map +1 -1
  467. package/dist/src/styles/vocabulary.d.ts +3 -2
  468. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  469. package/dist/src/styles/vocabulary.js +224 -0
  470. package/dist/src/styles/vocabulary.js.map +1 -1
  471. package/dist/styles.js +30 -239
  472. package/dist/styles.js.map +1 -1
  473. package/dist/xgmml.js +3 -3
  474. package/package.json +27 -7
  475. package/src/formats/cx/importer.ts +6 -1
  476. package/src/formats/cx/styles.ts +143 -9
  477. package/src/formats/cx2/importer.ts +1 -0
  478. package/src/formats/cx2/styles.ts +3 -1
  479. package/src/formats/cys/importer.ts +1 -0
  480. package/src/formats/cys/styles.ts +27 -5
  481. package/src/formats/dot/drawing.ts +799 -0
  482. package/src/formats/dot/exporter.ts +37 -1
  483. package/src/formats/dot/importer.ts +192 -36
  484. package/src/formats/dot/index.ts +3 -1
  485. package/src/formats/dot/label-places.ts +241 -0
  486. package/src/formats/dot/names.ts +15 -0
  487. package/src/formats/dot/record.ts +336 -0
  488. package/src/formats/dot/styles.ts +1702 -149
  489. package/src/formats/gexf/dialog-metrics.ts +61 -0
  490. package/src/formats/gexf/importer.ts +50 -5
  491. package/src/formats/gexf/preview.ts +217 -0
  492. package/src/formats/gexf/schema.ts +2 -1
  493. package/src/formats/gexf/styles.ts +1099 -245
  494. package/src/formats/gml/importer.ts +291 -76
  495. package/src/formats/gml/index.ts +1 -1
  496. package/src/formats/gml/positions.ts +29 -7
  497. package/src/formats/gml/styles.ts +852 -110
  498. package/src/formats/graphml/styles-plain.ts +6 -15
  499. package/src/formats/graphml/styles-yed.ts +765 -176
  500. package/src/formats/graphml/styles-yfiles3.ts +487 -63
  501. package/src/formats/graphml/yfiles.ts +19 -6
  502. package/src/formats/json/dialect.ts +41 -5
  503. package/src/formats/json/importer.ts +59 -7
  504. package/src/formats/json/index.ts +1 -0
  505. package/src/formats/json/positions.ts +22 -5
  506. package/src/formats/json/styles.ts +2123 -171
  507. package/src/formats/pajek/draw.ts +469 -0
  508. package/src/formats/pajek/exporter.ts +81 -49
  509. package/src/formats/pajek/importer.ts +278 -103
  510. package/src/formats/pajek/index.ts +1 -0
  511. package/src/formats/pajek/styles.ts +1506 -133
  512. package/src/formats/pajek/syntax.ts +27 -2
  513. package/src/formats/xgmml/emit.ts +73 -5
  514. package/src/formats/xgmml/styles.ts +330 -71
  515. package/src/index.ts +12 -1
  516. package/src/positions/convert.ts +12 -1
  517. package/src/positions/extract.ts +55 -1
  518. package/src/positions/index.ts +1 -0
  519. package/src/positions/types.ts +60 -0
  520. package/src/sources/builtins.ts +8 -1
  521. package/src/sources/context.ts +1 -0
  522. package/src/sources/credentials.ts +90 -1
  523. package/src/sources/cypher/descriptor.ts +7 -1
  524. package/src/sources/cypher/index.ts +149 -18
  525. package/src/sources/cypher/params.ts +1 -1
  526. package/src/sources/cypher/query-builder.ts +33 -0
  527. package/src/sources/ids.ts +6 -0
  528. package/src/sources/params.ts +3 -0
  529. package/src/sources/rest/descriptor.ts +9 -0
  530. package/src/sources/rest/openalex.ts +4 -2
  531. package/src/sources/rest/source.ts +41 -0
  532. package/src/sources/run.ts +35 -3
  533. package/src/sources/sigv4.ts +6 -4
  534. package/src/sources/sparql/descriptor.ts +67 -0
  535. package/src/sources/sparql/index.ts +371 -0
  536. package/src/sources/sparql/ntriples.ts +219 -0
  537. package/src/sources/sparql/params.ts +28 -0
  538. package/src/sources/sparql/query.ts +216 -0
  539. package/src/sources/types.ts +68 -0
  540. package/src/styles/annotations.ts +201 -27
  541. package/src/styles/canon/bends.ts +137 -0
  542. package/src/styles/canon/color-tables.ts +10 -4
  543. package/src/styles/canon/color.ts +105 -3
  544. package/src/styles/canon/enums.ts +107 -8
  545. package/src/styles/canon/graphviz.ts +71 -10
  546. package/src/styles/canon/number.ts +19 -0
  547. package/src/styles/canon/text.ts +187 -16
  548. package/src/styles/codes.ts +2 -0
  549. package/src/styles/cyjs/cascade.ts +1303 -84
  550. package/src/styles/cyjs/defaults-table.ts +4 -0
  551. package/src/styles/cyjs/emit.ts +268 -14
  552. package/src/styles/cyjs/face.ts +372 -0
  553. package/src/styles/cyjs/mappers.ts +506 -27
  554. package/src/styles/cyjs/routes.ts +432 -0
  555. package/src/styles/cytoscape/charts.ts +684 -0
  556. package/src/styles/cytoscape/emit.ts +170 -48
  557. package/src/styles/cytoscape/lexicon.ts +431 -78
  558. package/src/styles/cytoscape/mappings.ts +108 -0
  559. package/src/styles/cytoscape/resolve.ts +741 -17
  560. package/src/styles/defaults.ts +417 -108
  561. package/src/styles/index.ts +5 -0
  562. package/src/styles/internal.ts +1 -0
  563. package/src/styles/io/generic.ts +79 -23
  564. package/src/styles/io/round-trip.ts +202 -0
  565. package/src/styles/io/writer.ts +6 -1
  566. package/src/styles/notes.ts +1 -0
  567. package/src/styles/plan/features.ts +23 -2
  568. package/src/styles/plan/index.ts +1 -1
  569. package/src/styles/plan/plan.ts +69 -5
  570. package/src/styles/plan/resolve.ts +47 -4
  571. package/src/styles/slots.ts +25 -0
  572. package/src/styles/types.ts +698 -5
  573. package/src/styles/vocabulary.ts +232 -2
  574. package/dist/chunks/block-Cu22ewuS.js.map +0 -1
  575. package/dist/chunks/choose-Cdx6Enl2.js +0 -57
  576. package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
  577. package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
  578. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  579. package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
  580. package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
  581. package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
  582. package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
  583. package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
  584. package/dist/chunks/format-BMS-1Nxb.js +0 -64
  585. package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
  586. package/dist/chunks/generic-CP6YD34u.js.map +0 -1
  587. package/dist/chunks/importer-B87_fmVx.js.map +0 -1
  588. package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
  589. package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
  590. package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
  591. package/dist/chunks/importer-DPZDN40-.js.map +0 -1
  592. package/dist/chunks/importer-e3huMZbm.js.map +0 -1
  593. package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
  594. package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
  595. package/dist/chunks/styles-CaABMoO1.js.map +0 -1
  596. package/dist/chunks/styles-P_gmFAOi.js +0 -770
  597. package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
  598. package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
  599. package/dist/chunks/writer-DvHTl36J.js +0 -449
  600. package/dist/chunks/writer-DvHTl36J.js.map +0 -1
@@ -16,27 +16,33 @@
16
16
 
17
17
  import type { GraphSnapshot } from "@graphty/graph-format";
18
18
 
19
- import { type BendPoint, bendsToPoints, canonBends } from "../../styles/canon/bends.js";
19
+ import {
20
+ type BendPoint,
21
+ bendsToPoints,
22
+ canonBends,
23
+ canonNodeOffsets,
24
+ nodeOffsetsToPoints,
25
+ } from "../../styles/canon/bends.js";
20
26
  import { canonColor, colorOpacity } from "../../styles/canon/color.js";
21
27
  import { canonEnum } from "../../styles/canon/enums.js";
22
- import { canonNumber } from "../../styles/canon/number.js";
28
+ import { canonNumber, canonSigned, num6 } from "../../styles/canon/number.js";
23
29
  import { type CanonSite, MAX_STYLE_VALUE_LENGTH, noteAt, scalarText } from "../../styles/canon/site.js";
24
30
  import { canonText } from "../../styles/canon/text.js";
25
31
  import { STYLE_ISSUE } from "../../styles/codes.js";
26
- import { CYTOSCAPE_LEXICON } from "../../styles/cytoscape/lexicon.js";
32
+ import { CYTOSCAPE_LEXICON, javaFamily } from "../../styles/cytoscape/lexicon.js";
27
33
  import { StyleApproximations } from "../../styles/io/approximations.js";
28
34
  import {
29
- blockStyles,
30
35
  parseStyleBlock,
31
36
  STYLE_BLOCK_NAME,
32
37
  styleBlockLosses,
33
38
  styleTag,
34
39
  writeStyleBlock,
35
40
  } from "../../styles/io/block.js";
41
+ import { elementValues, takeFromBlock } from "../../styles/io/round-trip.js";
36
42
  import { dataRead, type ExporterStyleInput } from "../../styles/io/writer.js";
37
43
  import { StyleNotes } from "../../styles/notes.js";
38
44
  import { styleFingerprint } from "../../styles/plan/features.js";
39
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
45
+ import { plainCell } from "../../styles/plan/resolve.js";
40
46
  import type {
41
47
  ImporterStyles,
42
48
  StyleColumn,
@@ -48,9 +54,19 @@ import type {
48
54
  StyleWriteCapabilities,
49
55
  } from "../../styles/types.js";
50
56
  import type { LossNote } from "../../types.js";
57
+ import { GRAPHVIZ_GLYPH_SHAPES } from "../dot/drawing.js";
58
+ import {
59
+ graphvizArrowValue,
60
+ graphvizHasNoOutline,
61
+ graphvizOutline,
62
+ graphvizRouteBends,
63
+ PERIPHERY_GAP,
64
+ putGlyph,
65
+ SHAPE_PERIPHERIES,
66
+ } from "../dot/styles.js";
51
67
  import { gephiRules } from "../gexf/styles.js";
52
68
  import {
53
- flowchartShape,
69
+ configurationOutline,
54
70
  FONT_STYLES,
55
71
  type ValueColumns,
56
72
  valueColumns,
@@ -58,16 +74,18 @@ import {
58
74
  YED_LINES,
59
75
  YED_SHAPES,
60
76
  yedColor,
77
+ yedFontName,
61
78
  yedSpelled,
62
79
  yedValue,
63
80
  } from "../graphml/styles-yed.js";
81
+ import { writtenByGv2gml } from "./positions.js";
64
82
  import { KEY_MANGLED_CODE, mangleGmlKey } from "./syntax.js";
65
83
 
66
84
  /** A GML record as the importer keeps it (a json cell). */
67
85
  type Rec = Record<string, unknown>;
68
86
 
69
87
  /** The key vocabulary a file's graphics are read with. */
70
- type Vocabulary = "yed" | "cytoscape" | "ogdf" | "graphlet";
88
+ type Vocabulary = "yed" | "cytoscape" | "ogdf" | "graphlet" | "graphviz";
71
89
 
72
90
  /** Who wrote a GML file, as far as its styles go. */
73
91
  interface GmlWriter {
@@ -83,6 +101,8 @@ interface GmlWriter {
83
101
  readonly units: "px" | "relative";
84
102
  /** Whether it draws the `label` key of an element that has graphics (the GML draft does; yEd and Gephi do not). */
85
103
  readonly labelKey: boolean;
104
+ /** Whether the graph is directed, where its edges' arrows depend on it (gv2gml's). */
105
+ readonly directed?: boolean;
86
106
  }
87
107
 
88
108
  /** The writers recognized by their `Creator` text, first match wins. */
@@ -110,7 +130,8 @@ const WRITERS: readonly (readonly [RegExp, GmlWriter])[] = [
110
130
  vocabulary: "graphlet",
111
131
  spelling: "css",
112
132
  units: "relative",
113
- labelKey: false,
133
+ // Gephi's preview draws node labels as the GEXF reader reads them (formats/gexf/styles.ts)
134
+ labelKey: true,
114
135
  },
115
136
  ],
116
137
  [
@@ -134,6 +155,19 @@ const UNKNOWN_WRITER: GmlWriter = {
134
155
  labelKey: true,
135
156
  };
136
157
 
158
+ /**
159
+ * Graphviz's gv2gml (no `Creator`; `version 2` inside the graph): Graphviz's attribute values (shape names, colors,
160
+ * arrow names, sizes in points, y up) under GML keys, with Graphviz's defaults beneath.
161
+ */
162
+ const GV2GML_WRITER: GmlWriter = {
163
+ dialect: "graphviz",
164
+ tool: "graphviz",
165
+ vocabulary: "graphviz",
166
+ spelling: "graphviz",
167
+ units: "px",
168
+ labelKey: true,
169
+ };
170
+
137
171
  /** A file graphty wrote: yEd keys, and the `graphty.styles` block gives the rules. */
138
172
  const GRAPHTY_WRITER: GmlWriter = { ...WRITERS[0][1], tool: null };
139
173
 
@@ -175,8 +209,29 @@ function graphText(snapshot: GraphSnapshot, name: string): string | null {
175
209
  }
176
210
 
177
211
  /**
178
- * Who wrote a GML file: the `Creator` line (top level, or a key of the graph block as Gephi writes it), or graphty when
179
- * the file holds a `graphty.styles` block.
212
+ * A node label's `anchor` as a label position: yEd's `n`, `s`, `e`, `w` and the corners `nw`, `ne`, `sw`, `se` place it
213
+ * outside the node; `c` and the inside places `t`, `b`, `l`, `r`, `tl`, `tr`, `bl`, `br` keep it inside, which graphty
214
+ * draws centered.
215
+ */
216
+ const GML_ANCHORS: ReadonlyMap<string, string> = new Map([
217
+ ...["c", "t", "b", "l", "r", "tl", "tr", "bl", "br"].map((a): [string, string] => [a, "center"]),
218
+ ["n", "top"],
219
+ ["s", "bottom"],
220
+ ["e", "right"],
221
+ ["w", "left"],
222
+ ["nw", "top-left"],
223
+ ["ne", "top-right"],
224
+ ["sw", "bottom-left"],
225
+ ["se", "bottom-right"],
226
+ ]);
227
+
228
+ /** Node keys only yEd writes: its group hierarchy. */
229
+ const YED_NODE_KEYS: readonly string[] = ["isGroup", "gid"];
230
+
231
+ /**
232
+ * Who wrote a GML file: the `Creator` line (top level, or a key of the graph block as Gephi writes it), graphty when
233
+ * the file holds a `graphty.styles` block, or yEd when a file with no `Creator` holds keys only yEd writes (`isGroup`,
234
+ * `gid`, the graph's `hierarchic`).
180
235
  * @param snapshot - the graph
181
236
  * @returns the writer
182
237
  */
@@ -190,7 +245,14 @@ function writerOf(snapshot: GraphSnapshot): GmlWriter {
190
245
  return writer;
191
246
  }
192
247
  }
193
- return UNKNOWN_WRITER;
248
+ // keys only yEd writes name it even without a Creator line (BioNetGen writes yEd GML with none)
249
+ if (
250
+ creator.trim() === "" &&
251
+ (YED_NODE_KEYS.some((k) => snapshot.nodes.has(k)) || snapshot.graph.has("hierarchic"))
252
+ ) {
253
+ return WRITERS[0][1];
254
+ }
255
+ return creator.trim() === "" && writtenByGv2gml(snapshot) ? GV2GML_WRITER : UNKNOWN_WRITER;
194
256
  }
195
257
 
196
258
  // ============================================================ reading
@@ -209,19 +271,68 @@ const ARROW_ENDS: ReadonlyMap<string, readonly [string, string]> = new Map([
209
271
  ["both", ["triangle", "triangle"]],
210
272
  ]);
211
273
 
212
- /** OGDF's stroke types (`stipple`), by number. */
274
+ /**
275
+ * The node shape names Cytoscape's GML reader knows (NodeShapeVisualProperty, any case, with Cytoscape 2.x's `rect`
276
+ * and `round_rect`); any other name it ignores, so its default style's shape is drawn.
277
+ */
278
+ const CYTOSCAPE_GML_SHAPES: ReadonlyMap<string, string> = new Map([
279
+ ["ellipse", "ellipse"],
280
+ ["round_rectangle", "round-rectangle"],
281
+ ["round_rect", "round-rectangle"],
282
+ ["rectangle", "rectangle"],
283
+ ["rect", "rectangle"],
284
+ ["hexagon", "hexagon"],
285
+ ["octagon", "octagon"],
286
+ ["parallelogram", "parallelogram"],
287
+ ["triangle", "triangle"],
288
+ ["vee", "vee"],
289
+ ["diamond", "diamond"],
290
+ ]);
291
+
292
+ /** OGDF's stroke types (`stipple`): by number as older OGDF writes them, or by name as GraphIO::writeGML does. */
213
293
  const OGDF_STROKES: readonly string[] = ["none", "solid", "dashed", "dotted", "dash-dot", "dash-dot"];
294
+ const OGDF_STROKE_NAMES: ReadonlyMap<string, string> = new Map([
295
+ ["none", "none"],
296
+ ["solid", "solid"],
297
+ ["dash", "dashed"],
298
+ ["dot", "dotted"],
299
+ ["dashdot", "dash-dot"],
300
+ ["dashdotdot", "dash-dot"],
301
+ ]);
214
302
 
215
- /** OGDF's shape names the common table does not know. */
303
+ /** OGDF's shape names the common table does not know (an Image node without its image draws as its box). */
216
304
  const OGDF_SHAPES: ReadonlyMap<string, string> = new Map([
217
305
  ["rect", "rectangle"],
306
+ ["roundedrect", "round-rectangle"],
218
307
  ["rhomb", "diamond"],
219
- ["trapeze", "trapezoid"],
220
- ["invtrapeze", "trapezoid"],
221
- ["invtriangle", "triangle"],
222
- ["invparallelogram", "parallelogram"],
308
+ ["image", "rectangle"],
309
+ ]);
310
+
311
+ /**
312
+ * OGDF's shapes that GraphIO::drawSVG draws as polygons no named shape matches, with their corners as `shapePoints`
313
+ * (fractions of the half width and height, y up), measured in its drawing of
314
+ * graph-io/test/fidelity/specimens/gml/specimen-node-shapes-p5.gml: the triangles, the pentagon, the hexagon and the
315
+ * octagon are regular, their corners on the ellipse of the node's box (so they are drawn where the points say, not
316
+ * stretched to the box: `OGDF_REGULAR`); the trapezes and parallelograms fill the box, their short side half its width.
317
+ */
318
+ const OGDF_POLYGONS: ReadonlyMap<string, string> = new Map([
319
+ ["triangle", "0,1 0.866,-0.5 -0.866,-0.5"],
320
+ ["invtriangle", "0,-1 0.866,0.5 -0.866,0.5"],
321
+ ["pentagon", "0,1 0.951,0.309 0.588,-0.809 -0.588,-0.809 -0.951,0.309"],
322
+ ["hexagon", "1,0 0.5,0.866 -0.5,0.866 -1,0 -0.5,-0.866 0.5,-0.866"],
323
+ [
324
+ "octagon",
325
+ "0.924,0.383 0.383,0.924 -0.383,0.924 -0.924,0.383 -0.924,-0.383 -0.383,-0.924 0.383,-0.924 0.924,-0.383",
326
+ ],
327
+ ["trapeze", "-1,-1 1,-1 0.5,1 -0.5,1"],
328
+ ["invtrapeze", "-1,1 1,1 0.5,-1 -0.5,-1"],
329
+ ["parallelogram", "-1,-1 0.5,-1 1,1 -0.5,1"],
330
+ ["invparallelogram", "-1,1 0.5,1 1,-1 -0.5,-1"],
223
331
  ]);
224
332
 
333
+ /** OGDF's regular polygons, whose corners lie on the ellipse of the node's box rather than reaching its sides. */
334
+ const OGDF_REGULAR: ReadonlySet<string> = new Set(["triangle", "invtriangle", "pentagon", "hexagon", "octagon"]);
335
+
225
336
  /** Keys only yEd writes in a label record: its `fill` is the label's background, not the text color. */
226
337
  const YED_LABEL_KEYS: readonly string[] = [
227
338
  "outline",
@@ -308,16 +419,48 @@ function strokeStyle(raw: unknown, kind: "borderStyle" | "lineStyle", writer: Gm
308
419
  return null;
309
420
  }
310
421
  if (writer.vocabulary === "ogdf") {
311
- const stroke = OGDF_STROKES[Number(raw)] as string | undefined;
312
- if (stroke === undefined || stroke === "none") {
422
+ const stroke = ogdfStroke(raw);
423
+ if (stroke === undefined) {
313
424
  noteAt(at, STYLE_ISSUE.UNKNOWN_VALUE, sampleOf(raw));
314
425
  return null;
315
426
  }
316
- return stroke;
427
+ // stroke "none" draws no line at all: the callers turn the border or the edge off
428
+ return stroke === "none" ? null : stroke;
317
429
  }
318
430
  return canonEnum(kind, raw, writer.spelling, at);
319
431
  }
320
432
 
433
+ /**
434
+ * An OGDF `stipple`: a StrokeType by number or by name.
435
+ * @param raw - the value
436
+ * @returns the line style, "none", or undefined for a value OGDF does not know
437
+ */
438
+ function ogdfStroke(raw: unknown): string | undefined {
439
+ const text = typeof raw === "string" || typeof raw === "number" ? String(raw).trim() : "";
440
+ return /^\d+$/.test(text) ? OGDF_STROKES[Number(text)] : OGDF_STROKE_NAMES.get(text.toLowerCase());
441
+ }
442
+
443
+ /**
444
+ * Reads an OGDF node `type` (a Shape name): a polygon where OGDF draws one, else the shape it names.
445
+ * @param raw - the value
446
+ * @param writer - the writer
447
+ * @param put - receives the values
448
+ * @param at - the sites
449
+ */
450
+ function readOgdfShape(raw: string, writer: GmlWriter, put: Put, at: At): void {
451
+ const name = raw.trim().toLowerCase();
452
+ const points = OGDF_POLYGONS.get(name);
453
+ if (points !== undefined) {
454
+ put("shape", "polygon");
455
+ put("shapePoints", points);
456
+ if (OGDF_REGULAR.has(name)) {
457
+ put("shapePointsStretch", false);
458
+ }
459
+ return;
460
+ }
461
+ put("shape", canonEnum("shape", OGDF_SHAPES.get(name) ?? raw, writer.spelling, at("shape")));
462
+ }
463
+
321
464
  /**
322
465
  * Reads one node's `graphics` record.
323
466
  * @param g - the record
@@ -327,21 +470,198 @@ function strokeStyle(raw: unknown, kind: "borderStyle" | "lineStyle", writer: Gm
327
470
  */
328
471
  function readNodeGraphics(g: Rec, writer: GmlWriter, put: Put, at: At): void {
329
472
  const v = writer.vocabulary;
473
+ if (v === "graphviz") {
474
+ readGv2gmlNode(g, writer, put, at);
475
+ return;
476
+ }
330
477
  readNodeFill(g, writer, put, at);
331
478
  readNodeOutline(g, writer, put, at);
332
479
  put(
333
480
  "borderStyle",
334
481
  strokeStyle(v === "ogdf" ? g.stipple : g.outlineStyle, "borderStyle", writer, at("borderStyle")),
335
482
  );
336
- put("width", g.w === undefined ? null : canonNumber(g.w, at("width")));
337
- put("height", g.h === undefined ? null : canonNumber(g.h, at("height")));
338
- if (g.type !== undefined) {
339
- const raw =
340
- v === "ogdf" && typeof g.type === "string" ? (OGDF_SHAPES.get(g.type.toLowerCase()) ?? g.type) : g.type;
341
- put("shape", canonEnum("shape", raw, writer.spelling, at("shape")));
483
+ if (v === "ogdf" && g.stipple !== undefined && ogdfStroke(g.stipple) === "none") {
484
+ put("borderWidth", 0);
485
+ }
486
+ readNodeSize(g, writer, put, at);
487
+ if (v === "ogdf") {
488
+ readOgdfLabelOffset(g.label, put, at);
489
+ }
490
+ readNodeShape(g, writer, put, at);
491
+ }
492
+
493
+ /**
494
+ * Reads a node's size from its `graphics` record.
495
+ * @param g - the record
496
+ * @param writer - the writer
497
+ * @param put - receives the values
498
+ * @param at - the sites
499
+ */
500
+ function readNodeSize(g: Rec, writer: GmlWriter, put: Put, at: At): void {
501
+ if (writer.tool === "gephi") {
502
+ // Gephi's w is a node's size, a radius: its disc is twice as wide, and its h is never read
503
+ const size = g.w === undefined ? null : canonNumber(g.w, at("width"));
504
+ put("width", size === null ? null : 2 * size);
505
+ put("height", size === null ? null : 2 * size);
506
+ } else {
507
+ put("width", g.w === undefined ? null : canonNumber(g.w, at("width")));
508
+ put("height", g.h === undefined ? null : canonNumber(g.h, at("height")));
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Reads a node's shape from its `graphics` record, in the writer's vocabulary.
514
+ * @param g - the record
515
+ * @param writer - the writer
516
+ * @param put - receives the values
517
+ * @param at - the sites
518
+ */
519
+ function readNodeShape(g: Rec, writer: GmlWriter, put: Put, at: At): void {
520
+ const v = writer.vocabulary;
521
+ if (v === "ogdf" && typeof g.type === "string") {
522
+ readOgdfShape(g.type, writer, put, at);
523
+ } else if (v === "cytoscape" && g.type !== undefined) {
524
+ const name = scalarText(g.type, at("shape"))?.trim().toLowerCase() ?? "";
525
+ const shape = CYTOSCAPE_GML_SHAPES.get(name);
526
+ if (shape === undefined) {
527
+ noteAt(at("shape"), STYLE_ISSUE.UNKNOWN_VALUE, name.slice(0, MAX_STYLE_VALUE_LENGTH));
528
+ }
529
+ put("shape", shape ?? null);
530
+ } else if (g.type !== undefined) {
531
+ put("shape", canonEnum("shape", g.type, writer.spelling, at("shape")));
342
532
  } else if (v === "yed" && typeof g.customconfiguration === "string") {
343
533
  // yEd's flowchart palette keeps a node's shape as a configuration, not a `type`.
344
- put("shape", flowchartShape(g.customconfiguration));
534
+ const outline = configurationOutline(g.customconfiguration);
535
+ put("shape", outline?.shape ?? null);
536
+ if (outline?.points !== undefined) {
537
+ put("shapePoints", outline.points);
538
+ }
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Reads where OGDF draws a node's label: GraphIO::writeGML writes the label's center as an offset from the node's
544
+ * center (`graphics [ label [ x y z ] ]`, GraphAttributes::xLabel / yLabel, y down), and GraphIO::drawSVG draws it there.
545
+ * @param label - the graphics record's `label` value
546
+ * @param put - receives the values
547
+ * @param at - the sites
548
+ */
549
+ function readOgdfLabelOffset(label: unknown, put: Put, at: At): void {
550
+ if (typeof label !== "object" || label === null || Array.isArray(label)) {
551
+ return;
552
+ }
553
+ const { x, y } = label as Rec;
554
+ put("labelOffsetX", x === undefined ? null : canonSigned(x, at("labelOffsetX")));
555
+ put("labelOffsetY", y === undefined ? null : canonSigned(y, at("labelOffsetY")));
556
+ }
557
+
558
+ /**
559
+ * Reads one node's `graphics` record as Graphviz's gv2gml writes it (cmd/tools/gv2gml.c emitNodeAttrs): `fill` is the
560
+ * node's `fillcolor` (or its `color`), drawn only where `hasFill 1` says `style=filled`; `outline` its pen color,
561
+ * `width` its pen width, `w` and `H` its size in points, `type` its Graphviz shape, `visible 0` its `style=invis`.
562
+ * @param g - the record
563
+ * @param writer - the writer
564
+ * @param put - receives the values
565
+ * @param at - the sites
566
+ */
567
+ function readGv2gmlNode(g: Rec, writer: GmlWriter, put: Put, at: At): void {
568
+ const shape = typeof g.type === "string" ? g.type.trim().toLowerCase() : "";
569
+ if (Number(g.hasFill) === 1 || shape === "point") {
570
+ // filled: the opacity is stated even when opaque, over Graphviz's default of no fill
571
+ const c = canonColor(g.fill ?? "black", writer.spelling, at("fill"), { use: "fill" });
572
+ put("fill", c?.color ?? null);
573
+ put("fillOpacity", c?.alpha ?? null);
574
+ } else if (g.fill !== undefined) {
575
+ // Graphviz draws an unfilled node's inside see-through (the tool default), whatever its color
576
+ noteAt(at("fill"), STYLE_ISSUE.UNFILLED, sampleOf(g.fill));
577
+ }
578
+ if (graphvizHasNoOutline(shape)) {
579
+ put("borderWidth", 0);
580
+ } else {
581
+ readColor(g.outline, "borderColor", null, writer, put, at);
582
+ put("borderWidth", g.width === undefined ? null : canonNumber(g.width, at("borderWidth")));
583
+ }
584
+ const borderStyle = strokeStyle(g.outlineStyle, "borderStyle", writer, at("borderStyle"));
585
+ put("borderStyle", borderStyle);
586
+ readGv2gmlRings(g, shape, borderStyle, writer, put, at);
587
+ // gv2gml's w and H are the node's outer size: Graphviz draws each further outline PERIPHERY_GAP outside the one
588
+ // within, so graphty's node (the innermost outline, its ring outside it) is that much smaller
589
+ const rings = 2 * PERIPHERY_GAP * ((SHAPE_PERIPHERIES.get(shape) ?? 1) - 1);
590
+ const inner = (raw: unknown, property: string): number | null => {
591
+ const n = raw === undefined ? null : canonNumber(raw, at(property));
592
+ return n === null ? null : Math.max(0, n - rings);
593
+ };
594
+ put("width", inner(g.w, "width"));
595
+ // gv2gml writes the height as H (gv2gml.c emitReal("H", ...)); h where a later edit spelled it as GML does
596
+ put("height", inner(first(g, "H", "h"), "height"));
597
+ if (Number(g.visible) === 0 && g.visible !== undefined) {
598
+ put("visible", false);
599
+ }
600
+ const outline = graphvizOutline(shape);
601
+ if (shape === "polygon") {
602
+ // ponytail: Graphviz's polygon is a box unless its sides, skew or distortion say otherwise; gv2gml copies
603
+ // those as node keys beside graphics, which this record does not see. Read them when a file has them.
604
+ put("shape", "rectangle");
605
+ } else if (outline !== undefined) {
606
+ put("shape", "polygon");
607
+ put("shapePoints", outline);
608
+ // a Graphviz polygon's corners are where Graphviz draws them, as the DOT reader reads them
609
+ put("shapePointsStretch", false);
610
+ } else if (GRAPHVIZ_GLYPH_SHAPES.has(shape)) {
611
+ const size = (raw: unknown, property: string): number | null =>
612
+ raw === undefined ? null : canonNumber(raw, at(property));
613
+ putGlyph(
614
+ shape,
615
+ size(g.w, "width"),
616
+ size(first(g, "H", "h"), "height"),
617
+ put,
618
+ () => {
619
+ const pen = g.width === undefined ? 1 : Number(g.width);
620
+ return Number.isFinite(pen) ? Math.max(0, pen) : 1;
621
+ },
622
+ () =>
623
+ canonColor(g.outline ?? "black", writer.spelling, at("outerBorderColor"), { use: "line" })?.color ??
624
+ null,
625
+ );
626
+ } else if (g.type !== undefined) {
627
+ put("shape", canonEnum("shape", g.type, "graphviz", at("shape")));
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Reads the second outline Graphviz draws round a `doublecircle`, `doubleoctagon` or `tripleoctagon` (shapes.c):
633
+ * `PERIPHERY_GAP` points outside the first, in its color, width and dashes, as the DOT reader reads it. graphty draws
634
+ * one outer ring, so a third outline is noted as not carried.
635
+ * @param g - the record
636
+ * @param shape - its Graphviz shape, lowercase
637
+ * @param style - its outline's dashes, as read
638
+ * @param writer - the writer
639
+ * @param put - receives the values
640
+ * @param at - the sites
641
+ */
642
+ function readGv2gmlRings(g: Rec, shape: string, style: string | null, writer: GmlWriter, put: Put, at: At): void {
643
+ // ponytail: gv2gml copies a `peripheries` attribute as a node key beside graphics, which this record does not
644
+ // see; the shapes' own outlines are read. Read the key when a file sets it.
645
+ const count = SHAPE_PERIPHERIES.get(shape) ?? 1;
646
+ if (count < 2) {
647
+ return;
648
+ }
649
+ const c = canonColor(g.outline ?? "black", writer.spelling, at("outerBorderColor"), { use: "line" });
650
+ if (c !== null && c.alpha === 0) {
651
+ return;
652
+ }
653
+ const parsed = g.width === undefined ? 1 : Number(g.width);
654
+ const pen = Number.isFinite(parsed) ? Math.max(0, parsed) : 1;
655
+ put("outerBorderColor", c?.color ?? "#000000");
656
+ put("outerBorderOpacity", c !== null && c.alpha < 1 ? c.alpha : null);
657
+ put("outerBorderWidth", pen);
658
+ // the gap between the outline's outer edge and the ring's inner edge
659
+ put("outerBorderOffset", Math.max(0, PERIPHERY_GAP - pen));
660
+ if (style === "dashed" || style === "dotted") {
661
+ put("outerBorderStyle", style);
662
+ }
663
+ if (count > 2) {
664
+ noteAt(at("outerBorderWidth"), STYLE_ISSUE.NOT_CARRIED, `type ${shape}`);
345
665
  }
346
666
  }
347
667
 
@@ -377,7 +697,8 @@ function readNodeOutline(g: Rec, writer: GmlWriter, put: Put, at: At): void {
377
697
  put("borderWidth", 0);
378
698
  return;
379
699
  }
380
- readColor(v === "ogdf" ? g.line : g.outline, "borderColor", null, writer, put, at);
700
+ // OGDF's GraphIO::writeGML writes the stroke color as `outline`; older OGDF wrote `line`
701
+ readColor(v === "ogdf" ? first(g, "outline", "line") : g.outline, "borderColor", null, writer, put, at);
381
702
  const width = outlineWidthOf(g, v);
382
703
  put("borderWidth", width === undefined ? null : canonNumber(width, at("borderWidth")));
383
704
  }
@@ -410,10 +731,32 @@ function outlineWidthOf(g: Rec, v: GmlWriter["vocabulary"]): unknown {
410
731
  */
411
732
  function readEdgeGraphics(g: Rec, writer: GmlWriter, put: Put, at: At): void {
412
733
  const v = writer.vocabulary;
734
+ if (v === "graphviz") {
735
+ readGv2gmlEdge(g, writer, put, at);
736
+ return;
737
+ }
413
738
  readColor(g.fill, "color", "opacity", writer, put, at);
414
739
  const width = v === "ogdf" ? first(g, "lineWidth", "width") : g.width;
415
740
  put("width", width === undefined ? null : canonNumber(width, at("width")));
416
741
  put("lineStyle", strokeStyle(v === "ogdf" ? g.stipple : g.style, "lineStyle", writer, at("lineStyle")));
742
+ if (v === "ogdf" && g.stipple !== undefined && ogdfStroke(g.stipple) === "none") {
743
+ put("visible", false);
744
+ }
745
+ readEdgeArrows(g, writer, put, at);
746
+ if (v === "yed") {
747
+ readYedCurve(g, put);
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Reads an edge's arrows from its `graphics` record: `arrow` names the ends that have one, and `sourceArrow` and
753
+ * `targetArrow` (or their snake-case spellings) name each end's arrow.
754
+ * @param g - the record
755
+ * @param writer - the writer
756
+ * @param put - receives the values
757
+ * @param at - the sites
758
+ */
759
+ function readEdgeArrows(g: Rec, writer: GmlWriter, put: Put, at: At): void {
417
760
  if (typeof g.arrow === "string") {
418
761
  const ends = ARROW_ENDS.get(g.arrow.trim().toLowerCase());
419
762
  if (ends === undefined) {
@@ -433,14 +776,72 @@ function readEdgeGraphics(g: Rec, writer: GmlWriter, put: Put, at: At): void {
433
776
  }
434
777
  put(
435
778
  property,
436
- v === "cytoscape" && cytoscapeArrow !== undefined
779
+ writer.vocabulary === "cytoscape" && cytoscapeArrow !== undefined
437
780
  ? cytoscapeArrow(raw, at(property))
438
781
  : canonEnum("arrow", raw, writer.spelling, at(property)),
439
782
  );
440
783
  }
441
- if (v === "yed") {
442
- readYedCurve(g, put);
784
+ }
785
+
786
+ /** gv2gml's `arrow`, from Graphviz's `dir`: `first` is `dir=forward` (the head's arrow), `last` is `dir=back`. */
787
+ const GV2GML_DIRECTIONS: ReadonlyMap<string, readonly [boolean, boolean]> = new Map([
788
+ ["first", [false, true]],
789
+ ["last", [true, false]],
790
+ ["both", [true, true]],
791
+ ["none", [false, false]],
792
+ ]);
793
+
794
+ /**
795
+ * Reads one edge's `graphics` record as Graphviz's gv2gml writes it (gv2gml.c emitEdgeAttrs): `fill` its color,
796
+ * `width` its pen width (2 for `style=bold`), `style` its dashes, `visible 0` its `style=invis`, `targetArrow` and
797
+ * `sourceArrow` its `arrowhead` and `arrowtail`, and `arrow` its `dir`. With none of the arrow keys an edge of a
798
+ * directed graph has Graphviz's default arrowhead, and one of an undirected graph none.
799
+ * @param g - the record
800
+ * @param writer - the writer
801
+ * @param put - receives the values
802
+ * @param at - the sites
803
+ */
804
+ function readGv2gmlEdge(g: Rec, writer: GmlWriter, put: Put, at: At): void {
805
+ readColor(g.fill, "color", "opacity", writer, put, at);
806
+ put("width", g.width === undefined ? null : canonNumber(g.width, at("width")));
807
+ put("lineStyle", strokeStyle(g.style, "lineStyle", writer, at("lineStyle")));
808
+ if (Number(g.visible) === 0 && g.visible !== undefined) {
809
+ put("visible", false);
810
+ }
811
+ const ends = gv2gmlArrowEnds(g, writer.directed === true);
812
+ if (ends === null) {
813
+ return;
814
+ }
815
+ for (const [end, raw, on] of [
816
+ ["target", g.targetArrow, ends[1]],
817
+ ["source", g.sourceArrow, ends[0]],
818
+ ] as const) {
819
+ const name = typeof raw === "string" ? raw : "normal";
820
+ const arrow = on ? graphvizArrowValue(name, at(`${end}Arrow`)) : null;
821
+ put(`${end}Arrow`, arrow === null ? "none" : arrow.value);
822
+ if (arrow?.side !== null && arrow?.side !== undefined) {
823
+ put(`${end}ArrowSide`, arrow.side);
824
+ }
825
+ }
826
+ }
827
+
828
+ /**
829
+ * Which ends of a gv2gml edge Graphviz draws an arrow at, as its `dir` says (forward in a directed graph when the
830
+ * record names no arrow), or null for an undirected graph's edge that names none.
831
+ * @param g - the edge's `graphics` record
832
+ * @param directed - whether the graph is directed (its `dir` defaults to forward)
833
+ * @returns [source, target], or null
834
+ */
835
+ function gv2gmlArrowEnds(g: Rec, directed: boolean): readonly [boolean, boolean] | null {
836
+ if (g.arrow === undefined && g.targetArrow === undefined && g.sourceArrow === undefined) {
837
+ // Graphviz's dir is forward in a digraph and its arrowhead normal: gv2gml writes neither when unset
838
+ return directed ? [false, true] : null;
839
+ }
840
+ let dir = directed ? "first" : "none";
841
+ if (typeof g.arrow === "string") {
842
+ dir = g.arrow.trim().toLowerCase();
443
843
  }
844
+ return GV2GML_DIRECTIONS.get(dir) ?? null;
444
845
  }
445
846
 
446
847
  /**
@@ -460,10 +861,11 @@ function readYedCurve(g: Rec, put: Put): void {
460
861
  * Reads an element's label records: the first visible one with text; further visible ones are noted.
461
862
  * @param raw - the `LabelGraphics` value (one record or several)
462
863
  * @param writer - the writer
864
+ * @param node - whether the label is a node's: its `anchor` places it (`c`, `n`, `t`, `se`, ...)
463
865
  * @param put - receives the values
464
866
  * @param at - the sites
465
867
  */
466
- function readLabelGraphics(raw: unknown, writer: GmlWriter, put: Put, at: At): void {
868
+ function readLabelGraphics(raw: unknown, writer: GmlWriter, node: boolean, put: Put, at: At): void {
467
869
  const visible = (Array.isArray(raw) ? raw : [raw]).filter((l): l is Rec => isRec(l) && Number(l.visible) !== 0);
468
870
  const lg = visible.at(0);
469
871
  if (lg === undefined) {
@@ -474,30 +876,57 @@ function readLabelGraphics(raw: unknown, writer: GmlWriter, put: Put, at: At): v
474
876
  noteAt(at("label"), STYLE_ISSUE.EXTRA_LABELS, `${String(extra + 1)} labels`);
475
877
  }
476
878
  if (lg.text !== undefined) {
477
- put("label", canonText(lg.text, at("label")));
879
+ put(
880
+ "label",
881
+ writer.vocabulary === "graphviz" ? graphvizText(lg.text, at("label")) : canonText(lg.text, at("label")),
882
+ );
478
883
  }
479
884
  // the GML draft's `fill` is the text color; yEd's is the label's background box, its `color` the text color
480
885
  const yedLabel = writer.vocabulary === "yed" || YED_LABEL_KEYS.some((k) => Object.hasOwn(lg, k));
481
- if (lg.color !== undefined) {
482
- readColor(lg.color, "labelColor", null, writer, put, at);
886
+ if (lg.color !== undefined || lg.fontColor !== undefined) {
887
+ // gv2gml writes a label's fontcolor as fontColor (as color with -y); yEd's #RRGGBBAA alpha is the words'
888
+ readColor(lg.color ?? lg.fontColor, "labelColor", "labelOpacity", writer, put, at);
483
889
  } else if (lg.fill !== undefined && !yedLabel) {
484
890
  readColor(lg.fill, "labelColor", null, writer, put, at);
485
891
  }
486
892
  if (lg.fill !== undefined && yedLabel) {
487
893
  noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "label background");
488
894
  }
489
- readLabelFont(lg, put, at);
895
+ // yEd and Cytoscape are Java programs: a Java logical font (Dialog, SansSerif, ...) is drawn in Java's face for it
896
+ readLabelFont(lg, yedLabel || writer.vocabulary === "cytoscape", put, at);
897
+ if (node && lg.anchor !== undefined) {
898
+ const anchor = scalarText(lg.anchor, at("labelPosition"))?.trim().toLowerCase() ?? "";
899
+ const known = GML_ANCHORS.get(anchor);
900
+ if (known === undefined) {
901
+ noteAt(at("labelPosition"), STYLE_ISSUE.UNKNOWN_VALUE, anchor.slice(0, MAX_STYLE_VALUE_LENGTH));
902
+ }
903
+ put("labelPosition", known ?? null);
904
+ }
905
+ }
906
+
907
+ /**
908
+ * A label's text as Graphviz draws it: gv2gml copies a DOT label as written, so its line breaks are still the escapes
909
+ * `\n`, `\l` and `\r` (centered, left and right lines, drawn here as plain line breaks).
910
+ * @param raw - the text
911
+ * @param at - the site
912
+ * @returns the text
913
+ */
914
+ function graphvizText(raw: unknown, at: CanonSite): string | null {
915
+ const text = canonText(raw, at);
916
+ return text === null ? null : text.replaceAll(/\\[nlr]/g, "\n").replace(/\n$/, "");
490
917
  }
491
918
 
492
919
  /**
493
920
  * Reads a label record's font: its size, name, weight and slant.
494
921
  * @param lg - the record
922
+ * @param java - whether a Java program wrote it, whose logical font names are drawn in Java's faces
495
923
  * @param put - receives the values
496
924
  * @param at - the sites
497
925
  */
498
- function readLabelFont(lg: Rec, put: Put, at: At): void {
926
+ function readLabelFont(lg: Rec, java: boolean, put: Put, at: At): void {
499
927
  put("labelSize", lg.fontSize === undefined ? null : canonNumber(lg.fontSize, at("labelSize")));
500
- put("labelFont", lg.fontName === undefined ? null : canonText(lg.fontName, at("labelFont")));
928
+ const font = lg.fontName === undefined ? null : canonText(lg.fontName, at("labelFont"));
929
+ put("labelFont", java && typeof font === "string" ? javaFamily(font) : font);
501
930
  const style = scalarText(lg.fontStyle, at("labelWeight"))?.trim().toLowerCase();
502
931
  if (style !== undefined) {
503
932
  const known = FONT_STYLES.get(style);
@@ -538,16 +967,90 @@ function readElement(
538
967
  (target === "node" ? readNodeGraphics : readEdgeGraphics)(g, writer, put, at);
539
968
  }
540
969
  if (lg !== null && lg !== undefined) {
541
- readLabelGraphics(lg, writer, putLabel, at);
970
+ readLabelGraphics(lg, writer, target === "node", putLabel, at);
542
971
  }
543
972
  // the GML draft draws the label key; Cytoscape reads it as the node's name, which its default style shows
544
973
  const drawn =
545
974
  writer.vocabulary === "cytoscape" ? target === "node" : writer.labelKey && (isRec(g) || lg !== undefined);
546
975
  if (!text && drawn && label !== null && label !== undefined) {
547
- put("label", canonText(label, at("label")));
976
+ put(
977
+ "label",
978
+ writer.vocabulary === "graphviz" ? graphvizText(label, at("label")) : canonText(label, at("label")),
979
+ );
980
+ // the GML draft names no place for a node's label; a plain GML file's is centered on its node, as OGDF and
981
+ // Cytoscape draw it (yEd's GML keeps its own label models, in LabelGraphics)
982
+ if (target === "node" && writer.tool === null && writer.vocabulary === "graphlet") {
983
+ put("labelPosition", "center");
984
+ }
985
+ }
986
+ }
987
+
988
+ /** The font size GraphIO::drawSVG draws every label in (SVGSettings), by which it sets a cluster's label off its box. */
989
+ const OGDF_SVG_FONT_SIZE = 10;
990
+
991
+ /**
992
+ * Places each OGDF cluster's label where GraphIO::drawSVG draws it (SvgPrinter::drawCluster): one font size below the
993
+ * cluster's box where more of the drawing lies below it than above, else above it; starting at the box's left side
994
+ * where more of the drawing lies left of it than right, else ending at its right side. The importer reads a cluster as
995
+ * a group node with the id `cluster<id>`; its box is its center and its `w` and `h`.
996
+ * @param snapshot - the graph
997
+ * @param values - the node values read so far, given the label's place
998
+ */
999
+ function placeOgdfClusterLabels(snapshot: GraphSnapshot, values: ValueColumns): void {
1000
+ const n = snapshot.nodeCount;
1001
+ const boxOf = (i: number): [number, number, number, number] => {
1002
+ const [x, y] = centerOf(snapshot, i);
1003
+ const [w, h] = sizeOf(snapshot, i);
1004
+ return [x - w / 2, y - h / 2, x + w / 2, y + h / 2];
1005
+ };
1006
+ // ponytail: drawSVG's bounding box also holds the edges' bends; nodes and clusters decide every sheet so far
1007
+ const all = [Infinity, Infinity, -Infinity, -Infinity];
1008
+ for (let i = 0; i < n; i++) {
1009
+ const b = boxOf(i);
1010
+ if (b.every(Number.isFinite)) {
1011
+ all[0] = Math.min(all[0], b[0]);
1012
+ all[1] = Math.min(all[1], b[1]);
1013
+ all[2] = Math.max(all[2], b[2]);
1014
+ all[3] = Math.max(all[3], b[3]);
1015
+ }
1016
+ }
1017
+ const put = (property: string, i: number, value: StyleValue): void => {
1018
+ values[property] ??= new Array<StyleValue | null>(n).fill(null);
1019
+ values[property][i] = value;
1020
+ };
1021
+ for (let i = 0; i < n; i++) {
1022
+ const id = snapshot.ids.idOf(i);
1023
+ const [x0, y0, x1, y1] = boxOf(i);
1024
+ if (typeof id !== "string" || !id.startsWith("cluster") || ![x0, y0, x1, y1].every(Number.isFinite)) {
1025
+ continue;
1026
+ }
1027
+ const below = all[3] - y1 > y0 - all[1];
1028
+ const start = x0 - all[0] > all[2] - x1;
1029
+ put("labelPosition", i, "center");
1030
+ put("labelOffsetX", i, ((start ? -1 : 1) * (x1 - x0)) / 2);
1031
+ put("labelShiftX", i, start ? 0.5 : -0.5);
1032
+ put("labelOffsetY", i, (below ? 1 : -1) * ((y1 - y0) / 2 + OGDF_SVG_FONT_SIZE));
548
1033
  }
549
1034
  }
550
1035
 
1036
+ /**
1037
+ * An element's `label` key. Cytoscape's GML reader names a node by its label and, where the label is empty or
1038
+ * missing, by its id, and its default style draws that name.
1039
+ * @param snapshot - the graph
1040
+ * @param writer - the writer
1041
+ * @param target - nodes or edges
1042
+ * @param i - the row
1043
+ * @returns the label
1044
+ */
1045
+ function labelOf(snapshot: GraphSnapshot, writer: GmlWriter, target: StyleTarget, i: number): unknown {
1046
+ const table = target === "node" ? snapshot.nodes : snapshot.edges;
1047
+ const label = cellOf(table, "label", i);
1048
+ if (writer.vocabulary === "cytoscape" && target === "node" && (label === undefined || label === "")) {
1049
+ return String(snapshot.ids.idOf(i));
1050
+ }
1051
+ return label;
1052
+ }
1053
+
551
1054
  /**
552
1055
  * The value of a column, or undefined when the table has no such column.
553
1056
  * @param table - the node or edge table
@@ -576,25 +1079,224 @@ function centerOf(snapshot: GraphSnapshot, i: number): BendPoint {
576
1079
  }
577
1080
 
578
1081
  /**
579
- * An edge's bends: the points of its `graphics` `Line` between the first and the last, which are where it leaves
580
- * the source and meets the target.
1082
+ * A node's width and height: its `graphics` `w` and `h`.
1083
+ * @param snapshot - the graph
1084
+ * @param i - the node's index
1085
+ * @returns the size; NaN where the node has none
1086
+ */
1087
+ function sizeOf(snapshot: GraphSnapshot, i: number): BendPoint {
1088
+ const g = cellOf(snapshot.nodes, "graphics", i);
1089
+ return isRec(g) ? [Number(g.w ?? Number.NaN), Number(g.h ?? Number.NaN)] : [Number.NaN, Number.NaN];
1090
+ }
1091
+
1092
+ /**
1093
+ * An edge's route from its `graphics` `Line`, whose first and last points are where it attaches to its source and
1094
+ * target (its ports: the centers unless yEd moved them) and whose points between are its bends: the bends relative to
1095
+ * the two node centers (a self-loop's to its node's size), and each port off its node's center relative to the node.
1096
+ * OGDF's `Line` holds the bends alone (GraphIO::writeGML writes no end point).
581
1097
  * @param snapshot - the graph
582
1098
  * @param g - the edge's `graphics` value
583
1099
  * @param e - the edge's row
584
- * @returns the `bends` value, or null without bends
1100
+ * @param bendsOnly - whether the points are the bends alone (OGDF)
1101
+ * @param put - receives the values
585
1102
  */
586
- function lineBends(snapshot: GraphSnapshot, g: unknown, e: number): string | null {
1103
+ function readLineRoute(
1104
+ snapshot: GraphSnapshot,
1105
+ g: unknown,
1106
+ e: number,
1107
+ bendsOnly: boolean,
1108
+ put: (property: string, value: string) => void,
1109
+ ): void {
587
1110
  const raw = isRec(g) && isRec(g.Line) ? g.Line.point : undefined;
588
- if (!Array.isArray(raw) || raw.length < 3) {
589
- return null;
1111
+ const list = isRec(raw) ? [raw] : raw;
1112
+ if (!Array.isArray(list) || list.length < (bendsOnly ? 1 : 2)) {
1113
+ return;
590
1114
  }
591
- const points = raw.slice(1, -1).map((p): BendPoint => (isRec(p) ? [Number(p.x), Number(p.y)] : [Number.NaN, 0]));
592
- return canonBends(
1115
+ const all = list.map((p): BendPoint => (isRec(p) ? [Number(p.x), Number(p.y)] : [Number.NaN, 0]));
1116
+ const [s, t] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
1117
+ const points = bendsOnly ? all : all.slice(1, -1);
1118
+ const bends =
1119
+ s === t
1120
+ ? canonNodeOffsets(centerOf(snapshot, s), sizeOf(snapshot, s), points, true)
1121
+ : canonBends(centerOf(snapshot, s), centerOf(snapshot, t), points, true);
1122
+ if (bends !== null) {
1123
+ put("bends", bends);
1124
+ }
1125
+ if (bendsOnly) {
1126
+ return;
1127
+ }
1128
+ for (const [property, k, end] of [
1129
+ ["sourcePort", s, all[0]],
1130
+ ["targetPort", t, all.at(-1) ?? all[0]],
1131
+ ] as const) {
1132
+ const center = centerOf(snapshot, k);
1133
+ const port =
1134
+ end[0] === center[0] && end[1] === center[1]
1135
+ ? null
1136
+ : canonNodeOffsets(center, sizeOf(snapshot, k), [end], true);
1137
+ if (port !== null) {
1138
+ put(property, port);
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ /**
1144
+ * A gv2gml edge's route: its `Line` is the edge's Graphviz `pos`, the B-spline's `3n + 1` control points from the tail
1145
+ * to the head (y up), with the arrow tips Graphviz keeps apart (`s,` and `e,`) written as plain first and last points;
1146
+ * the points beyond `3n + 1` are those tips (the source's first when there are two or it has the arrow). A route on
1147
+ * the line between the centers is the straight edge it looks like and gives no bends.
1148
+ * @param snapshot - the graph
1149
+ * @param g - the edge's `graphics` value
1150
+ * @param e - the edge's row
1151
+ * @param put - receives the values
1152
+ */
1153
+ function readGv2gmlRoute(
1154
+ snapshot: GraphSnapshot,
1155
+ g: unknown,
1156
+ e: number,
1157
+ put: (property: string, value: string) => void,
1158
+ ): void {
1159
+ const raw = isRec(g) && isRec(g.Line) ? g.Line.point : undefined;
1160
+ const list = isRec(raw) ? [raw] : raw;
1161
+ if (!Array.isArray(list) || !isRec(g)) {
1162
+ return;
1163
+ }
1164
+ let points = list.map((p): BendPoint => (isRec(p) ? [Number(p.x), Number(p.y)] : [Number.NaN, 0]));
1165
+ const tips = (points.length - 1) % 3;
1166
+ const sourceTip = tips === 2 || (tips === 1 && gv2gmlArrowEnds(g, snapshot.directed)?.[0] === true);
1167
+ points = points.slice(sourceTip ? 1 : 0, points.length - (tips === 2 || (tips === 1 && !sourceTip) ? 1 : 0));
1168
+ const bends = graphvizRouteBends(
593
1169
  centerOf(snapshot, snapshot.edgeSource(e)),
594
1170
  centerOf(snapshot, snapshot.edgeTarget(e)),
595
- points,
596
- true,
1171
+ [points],
597
1172
  );
1173
+ if (bends !== null) {
1174
+ put("bends", bends);
1175
+ }
1176
+ }
1177
+
1178
+ /**
1179
+ * Where graphty-element hangs an edge's label on a route drawn through these points (its centers and bends): the
1180
+ * middle of its middle segment, or the joint between its two middle ones; `labelOffsetX` / `labelOffsetY` move it
1181
+ * from there.
1182
+ * @param points - the route, from the source's center to the target's
1183
+ * @returns the point
1184
+ */
1185
+ function routeMiddle(points: readonly BendPoint[]): BendPoint {
1186
+ const m = points.length - 1;
1187
+ if (m % 2 === 0) {
1188
+ return points[m / 2];
1189
+ }
1190
+ const [a, b] = [points[(m - 1) / 2], points[(m + 1) / 2]];
1191
+ return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
1192
+ }
1193
+
1194
+ /**
1195
+ * Puts an edge label's offset from where graphty-element hangs it, when the program draws it elsewhere.
1196
+ * @param at - the label's center, in the file's coordinates
1197
+ * @param middle - where graphty-element hangs it, in the same coordinates
1198
+ * @param yUp - whether the file's y grows upward
1199
+ * @param put - receives the values
1200
+ */
1201
+ function putEdgeLabelOffset(
1202
+ at: BendPoint,
1203
+ middle: BendPoint,
1204
+ yUp: boolean,
1205
+ put: (property: string, value: StyleValue) => void,
1206
+ ): void {
1207
+ const dx = num6(at[0] - middle[0]);
1208
+ const dy = num6((yUp ? -1 : 1) * (at[1] - middle[1]));
1209
+ if (Number.isFinite(dx) && Number.isFinite(dy) && (dx !== 0 || dy !== 0)) {
1210
+ // moved from the line's middle itself, not from graphty's default place beside the line
1211
+ put("labelPosition", "center");
1212
+ put("labelOffsetX", dx);
1213
+ put("labelOffsetY", dy);
1214
+ }
1215
+ }
1216
+
1217
+ /**
1218
+ * A gv2gml edge label's place: gv2gml copies Graphviz's `lp` (the label's center, in points, y up) as a plain edge
1219
+ * key, and Graphviz draws the label there, beside its line rather than on it.
1220
+ * @param snapshot - the graph
1221
+ * @param e - the edge's row
1222
+ * @param bent - whether its route has bends
1223
+ * @param put - receives the values
1224
+ */
1225
+ function readGv2gmlLabelPlace(
1226
+ snapshot: GraphSnapshot,
1227
+ e: number,
1228
+ bent: boolean,
1229
+ put: (property: string, value: StyleValue) => void,
1230
+ ): void {
1231
+ const lp = cellOf(snapshot.edges, "lp", e);
1232
+ const m = typeof lp === "string" ? /^\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*$/.exec(lp) : null;
1233
+ // ponytail: a curved route's middle is where graphty-element samples its curve; only a straight one is placed
1234
+ if (m === null || bent) {
1235
+ return;
1236
+ }
1237
+ const [s, t] = [centerOf(snapshot, snapshot.edgeSource(e)), centerOf(snapshot, snapshot.edgeTarget(e))];
1238
+ putEdgeLabelOffset([Number(m[1]), Number(m[2])], routeMiddle([s, t]), true, put);
1239
+ }
1240
+
1241
+ /**
1242
+ * An OGDF edge label's place: GraphIO::drawSVG (SvgPrinter::drawEdge) draws it at the middle of the route's first
1243
+ * segment, from the source's center to its first bend, not at the route's middle.
1244
+ * @param snapshot - the graph
1245
+ * @param g - the edge's `graphics` value
1246
+ * @param e - the edge's row
1247
+ * @param put - receives the values
1248
+ */
1249
+ function readOgdfLabelPlace(
1250
+ snapshot: GraphSnapshot,
1251
+ g: unknown,
1252
+ e: number,
1253
+ put: (property: string, value: StyleValue) => void,
1254
+ ): void {
1255
+ const raw = isRec(g) && isRec(g.Line) ? g.Line.point : undefined;
1256
+ const list = isRec(raw) ? [raw] : raw;
1257
+ const [s, t] = [centerOf(snapshot, snapshot.edgeSource(e)), centerOf(snapshot, snapshot.edgeTarget(e))];
1258
+ if (!Array.isArray(list) || snapshot.edgeSource(e) === snapshot.edgeTarget(e)) {
1259
+ return;
1260
+ }
1261
+ const same = (a: BendPoint, b: BendPoint): boolean => Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-6;
1262
+ // the Line OGDF writes may start and end at the centers
1263
+ const bends = list
1264
+ .map((p): BendPoint => (isRec(p) ? [Number(p.x), Number(p.y)] : [Number.NaN, 0]))
1265
+ .filter((p) => !same(p, s) && !same(p, t));
1266
+ if (bends.length === 0) {
1267
+ return;
1268
+ }
1269
+ const route = [s, ...bends, t];
1270
+ putEdgeLabelOffset(
1271
+ [(route[0][0] + route[1][0]) / 2, (route[0][1] + route[1][1]) / 2],
1272
+ routeMiddle(route),
1273
+ false,
1274
+ put,
1275
+ );
1276
+ }
1277
+
1278
+ /**
1279
+ * Hides the edges Gephi's own GML importer leaves out: an edge whose `directed` key disagrees with the graph's
1280
+ * `directed` header (Gephi 0.11 loads 4 of `specimen-arrows-p6`'s 5 edges). A file with no header is read as mixed and
1281
+ * every edge is loaded. The edge and its data stay; Gephi draws nothing there, so graphty draws nothing.
1282
+ * @param snapshot - the graph
1283
+ * @param values - the edge values read so far, given `visible`
1284
+ */
1285
+ function hideGephiDroppedEdges(snapshot: GraphSnapshot, values: ValueColumns): void {
1286
+ const header = (snapshot.meta.extra.gml as Record<string, unknown> | undefined)?.directed;
1287
+ const directed = snapshot.edges.byRole("directed");
1288
+ // with no `directed` header Gephi reads a mixed graph and loads every edge (Gephi Toolkit 0.11.3, checked)
1289
+ if (directed === null || header === undefined) {
1290
+ return;
1291
+ }
1292
+ const graphDirected = Number(header) === 1;
1293
+ for (let e = 0; e < snapshot.edgeCount; e++) {
1294
+ const v = plainCell(directed.value(e));
1295
+ if (typeof v === "boolean" && v !== graphDirected) {
1296
+ values.visible ??= new Array<StyleValue | null>(snapshot.edgeCount).fill(null);
1297
+ values.visible[e] = false;
1298
+ }
1299
+ }
598
1300
  }
599
1301
 
600
1302
  /**
@@ -626,7 +1328,7 @@ function readValues(
626
1328
  target,
627
1329
  cellOf(table, "graphics", i),
628
1330
  cellOf(table, "LabelGraphics", i),
629
- cellOf(table, "label", i),
1331
+ labelOf(snapshot, writer, target, i),
630
1332
  writer,
631
1333
  (property, value) => {
632
1334
  if (value !== null) {
@@ -636,12 +1338,32 @@ function readValues(
636
1338
  },
637
1339
  at,
638
1340
  );
639
- if (target === "edge") {
640
- const bends = lineBends(snapshot, cellOf(table, "graphics", i), i);
641
- if (bends !== null) {
1341
+ if (target === "edge" && writer.vocabulary === "graphviz") {
1342
+ let bent = false;
1343
+ readGv2gmlRoute(snapshot, cellOf(table, "graphics", i), i, (property, value) => {
642
1344
  found = true;
643
- out.put(i, "bends", bends);
644
- }
1345
+ bent ||= property === "bends";
1346
+ out.put(i, property, value);
1347
+ });
1348
+ readGv2gmlLabelPlace(snapshot, i, bent, (property, value) => {
1349
+ out.put(i, property, value);
1350
+ });
1351
+ } else if (target === "edge" && writer.vocabulary === "ogdf") {
1352
+ readOgdfLabelPlace(snapshot, cellOf(table, "graphics", i), i, (property, value) => {
1353
+ out.put(i, property, value);
1354
+ });
1355
+ }
1356
+ if (target === "edge" && writer.vocabulary !== "graphviz") {
1357
+ readLineRoute(
1358
+ snapshot,
1359
+ cellOf(table, "graphics", i),
1360
+ i,
1361
+ writer.vocabulary === "ogdf",
1362
+ (property, value) => {
1363
+ found = true;
1364
+ out.put(i, property, value);
1365
+ },
1366
+ );
645
1367
  }
646
1368
  }
647
1369
  return out.values;
@@ -720,8 +1442,11 @@ function styleColumns(table: GraphSnapshot["nodes"]): string[] {
720
1442
  */
721
1443
  export function gmlStyles(snapshot: GraphSnapshot, context: StyleReadContext): ImporterStyles | null {
722
1444
  const notes = new StyleNotes();
723
- const writer = writerOf(snapshot);
1445
+ const writer = { ...writerOf(snapshot), directed: snapshot.directed };
724
1446
  const values = readValues(snapshot, writer, notes, context);
1447
+ if (writer.vocabulary === "ogdf") {
1448
+ placeOgdfClusterLabels(snapshot, values.node);
1449
+ }
725
1450
  const blockText = graphText(snapshot, GML_STYLE_BLOCK_KEY);
726
1451
  const block = blockText === null ? null : blockWithValues(snapshot, blockText);
727
1452
  if (block !== null) {
@@ -730,10 +1455,32 @@ export function gmlStyles(snapshot: GraphSnapshot, context: StyleReadContext): I
730
1455
  if (writer.tool === null && !values.found && block === null) {
731
1456
  return null;
732
1457
  }
1458
+ if (writer.tool === "gephi" && block === null) {
1459
+ // Gephi draws every edge in its end nodes' mixed color (EdgeColor MIXED), whatever its own
1460
+ delete values.edge.color;
1461
+ // and as thick as its weight, the `value` Gephi writes, also where the reader kept `value` as plain data
1462
+ if (snapshot.edges.byRole("weight") === null && snapshot.edges.has("value")) {
1463
+ values.edge.width = Array.from({ length: snapshot.edgeCount }, (_, e) => {
1464
+ const w = cellOf(snapshot.edges, "value", e);
1465
+ if (typeof w !== "number" || !Number.isFinite(w)) {
1466
+ return null;
1467
+ }
1468
+ // a weight of 0 or below is drawn 1 wide, so the edge stays visible
1469
+ return w > 0 ? w : 1;
1470
+ });
1471
+ }
1472
+ hideGephiDroppedEdges(snapshot, values.edge);
1473
+ }
1474
+ const none = { rules: [], columns: new Map<string, StyleColumn>() };
733
1475
  const gephi =
734
1476
  writer.tool === "gephi"
735
- ? gephiRules(snapshot, values.node, values.edge, context.budget)
736
- : { node: {}, edge: {} };
1477
+ ? gephiRules(
1478
+ snapshot,
1479
+ { fill: values.node.fill, width: values.node.width },
1480
+ { color: values.edge.color, width: values.edge.width },
1481
+ context.budget,
1482
+ )
1483
+ : { node: none, edge: none };
737
1484
  return {
738
1485
  canonical: true,
739
1486
  dialect: writer.dialect,
@@ -783,6 +1530,8 @@ export const GML_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
783
1530
  "targetArrow",
784
1531
  "curved",
785
1532
  "bends",
1533
+ "sourcePort",
1534
+ "targetPort",
786
1535
  "label",
787
1536
  "labelColor",
788
1537
  "labelSize",
@@ -915,7 +1664,7 @@ function edgeGraphics(get: Get, approximate: Approximate): Rec {
915
1664
  for (const property of ["sourceArrow", "targetArrow"]) {
916
1665
  const arrow = get(property);
917
1666
  if (typeof arrow === "string") {
918
- g[property] = yedSpelled(YED_ARROWS, arrow, "standard", property, approximate);
1667
+ g[property] = yedSpelled(YED_ARROWS, arrow, "delta", property, approximate);
919
1668
  }
920
1669
  }
921
1670
  const curved = get("curved");
@@ -946,7 +1695,8 @@ function labelGraphics(get: Get): Rec | null {
946
1695
  }
947
1696
  const font = get("labelFont");
948
1697
  if (typeof font === "string") {
949
- lg.fontName = font;
1698
+ // a Java face is written back as the logical font yEd draws in it
1699
+ lg.fontName = yedFontName(font);
950
1700
  }
951
1701
  const weight = get("labelWeight");
952
1702
  const italic = get("labelItalic");
@@ -993,23 +1743,6 @@ function mergedLabels(old: unknown, written: Rec | null): unknown {
993
1743
  return head === undefined ? rest : [head, ...rest];
994
1744
  }
995
1745
 
996
- /**
997
- * Each element's values of the properties the writer writes, as a getter per element.
998
- * @param picture - each element's value of a property
999
- * @param target - nodes or edges
1000
- * @returns the getter
1001
- */
1002
- function getter(
1003
- picture: (target: StyleTarget, property: string) => readonly (StyleValue | null)[],
1004
- target: StyleTarget,
1005
- ): (i: number) => Get {
1006
- const columns = new Map<string, readonly (StyleValue | null)[]>();
1007
- for (const p of GML_STYLE_CAPABILITIES[target]) {
1008
- columns.set(p, picture(target, p));
1009
- }
1010
- return (i) => (property) => columns.get(property)?.[i] ?? null;
1011
- }
1012
-
1013
1746
  /**
1014
1747
  * For a file graphty wrote: where a value read from the graphics is what the writer writes for the block's own value
1015
1748
  * (a pentagon written as a rectangle, an opacity rounded to 255 steps), the block's exact value is the truth, so a
@@ -1025,29 +1758,16 @@ function exactFromBlock(
1025
1758
  text: string,
1026
1759
  read: { readonly node: ValueColumns; readonly edge: ValueColumns },
1027
1760
  ): void {
1028
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
1029
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "gml") : null;
1030
- if (ex === null) {
1031
- return;
1032
- }
1033
1761
  const notes = new StyleNotes();
1034
- for (const target of ["node", "edge"] as const) {
1035
- const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1036
- const get = getter((t, p) => resolveStyles(ex, t, p as never, snapshot), target);
1037
- const values = read[target];
1038
- for (let i = 0; i < n; i++) {
1039
- const want = get(i);
1762
+ takeFromBlock(snapshot, text, "gml", (target) => ({
1763
+ values: read[target],
1764
+ properties: GML_STYLE_CAPABILITIES[target],
1765
+ writes: GML_STYLE_CAPABILITIES[target],
1766
+ readBack: (_i, want) => {
1040
1767
  const back = writtenAndReadBack(target, want, notes);
1041
- for (const property of GML_STYLE_CAPABILITIES[target]) {
1042
- const native = values[property]?.[i] ?? null;
1043
- const w = want(property);
1044
- if (native !== w && native === (back.get(property) ?? null)) {
1045
- values[property] ??= new Array<StyleValue | null>(n).fill(null);
1046
- values[property][i] = w;
1047
- }
1048
- }
1049
- }
1050
- }
1768
+ return (property) => back.get(property) ?? null;
1769
+ },
1770
+ }));
1051
1771
  }
1052
1772
 
1053
1773
  /**
@@ -1155,13 +1875,18 @@ function writeGraphicsColumns(
1155
1875
  const approximate: Approximate = (property, value) => {
1156
1876
  approximated.add(target, property, value);
1157
1877
  };
1158
- const get = getter((t, p) => input.picture(t, p as never), target);
1878
+ const get = elementValues((t, p) => input.picture(t, p as never), target, GML_STYLE_CAPABILITIES[target]);
1879
+ const getNode = elementValues((t, p) => input.picture(t, p as never), "node", GML_STYLE_CAPABILITIES.node);
1880
+ const size = (k: number): BendPoint => {
1881
+ const [w, h] = [getNode(k)("width"), getNode(k)("height")];
1882
+ return typeof w === "number" && typeof h === "number" ? [w, h] : sizeOf(snapshot, k);
1883
+ };
1159
1884
  const graphics: unknown[] = [];
1160
1885
  const labels: unknown[] = [];
1161
1886
  for (let i = 0; i < n; i++) {
1162
1887
  const g = target === "node" ? nodeGraphics(get(i), approximate) : edgeGraphics(get(i), approximate);
1163
1888
  if (target === "edge") {
1164
- const line = bendsLine(snapshot, get(i)("bends"), i);
1889
+ const line = routeLine(snapshot, get(i), i, size);
1165
1890
  if (line !== null) {
1166
1891
  g.Line = line;
1167
1892
  }
@@ -1182,21 +1907,38 @@ function writeGraphicsColumns(
1182
1907
  }
1183
1908
 
1184
1909
  /**
1185
- * An edge's `Line` from its `bends` value: the node centers it is written between, with the bends placed between them,
1186
- * so the bends read back as written.
1910
+ * An edge's `Line` from its `bends`, `sourcePort` and `targetPort` values: from its source port (the node's center
1911
+ * without one) through the bends placed between the node centers (a self-loop's around its node) to its target port,
1912
+ * so the route reads back as written.
1187
1913
  * @param snapshot - the graph handed to the exporter
1188
- * @param bends - the edge's `bends` value
1914
+ * @param get - the edge's values
1189
1915
  * @param e - the edge's row
1916
+ * @param size - each node's written width and height
1190
1917
  * @returns the `Line` record, or null without a value or without both centers
1191
1918
  */
1192
- function bendsLine(snapshot: GraphSnapshot, bends: StyleValue | null, e: number): Rec | null {
1193
- const source = centerOf(snapshot, snapshot.edgeSource(e));
1194
- const target = centerOf(snapshot, snapshot.edgeTarget(e));
1195
- const points = typeof bends === "string" ? bendsToPoints(source, target, bends, true) : null;
1196
- if (points === null || ![...source, ...target].every(Number.isFinite)) {
1919
+ function routeLine(snapshot: GraphSnapshot, get: Get, e: number, size: (k: number) => BendPoint): Rec | null {
1920
+ const [s, t] = [snapshot.edgeSource(e), snapshot.edgeTarget(e)];
1921
+ const source = centerOf(snapshot, s);
1922
+ const target = centerOf(snapshot, t);
1923
+ if (![...source, ...target].every(Number.isFinite)) {
1924
+ return null;
1925
+ }
1926
+ const bends = get("bends");
1927
+ let points: BendPoint[] | null = null;
1928
+ if (typeof bends === "string") {
1929
+ points =
1930
+ s === t ? nodeOffsetsToPoints(source, size(s), bends, true) : bendsToPoints(source, target, bends, true);
1931
+ }
1932
+ const port = (property: string, k: number, center: BendPoint): BendPoint | null => {
1933
+ const value = get(property);
1934
+ return typeof value === "string" ? (nodeOffsetsToPoints(center, size(k), value, true)?.[0] ?? null) : null;
1935
+ };
1936
+ const from = port("sourcePort", s, source);
1937
+ const to = port("targetPort", t, target);
1938
+ if (points === null && from === null && to === null) {
1197
1939
  return null;
1198
1940
  }
1199
- return { point: [source, ...points, target].map(([x, y]) => ({ x, y })) };
1941
+ return { point: [from ?? source, ...(points ?? []), to ?? target].map(([x, y]) => ({ x, y })) };
1200
1942
  }
1201
1943
 
1202
1944
  /**