@graphty/graph-io 0.3.43 → 0.3.44

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 (674) hide show
  1. package/dist/chunks/approximations-3Q4TsdTh.js +40 -0
  2. package/dist/chunks/approximations-3Q4TsdTh.js.map +1 -0
  3. package/dist/chunks/block-JQiwViEt.js +2777 -0
  4. package/dist/chunks/block-JQiwViEt.js.map +1 -0
  5. package/dist/chunks/{choose-DvpbdRuz.js → choose-BemygfH8.js} +3 -2
  6. package/dist/chunks/{choose-DvpbdRuz.js.map → choose-BemygfH8.js.map} +1 -1
  7. package/dist/chunks/{constants-fT3E8JYY.js → constants-BFUR_CtU.js} +6 -2
  8. package/dist/chunks/constants-BFUR_CtU.js.map +1 -0
  9. package/dist/chunks/convert-BcQ8_jMo.js +59 -0
  10. package/dist/chunks/convert-BcQ8_jMo.js.map +1 -0
  11. package/dist/chunks/emit-DCwFLts5.js +3456 -0
  12. package/dist/chunks/emit-DCwFLts5.js.map +1 -0
  13. package/dist/chunks/{escape-B62ZGM_a.js → escape-B1rMftd9.js} +43 -20
  14. package/dist/chunks/escape-B1rMftd9.js.map +1 -0
  15. package/dist/chunks/{export-rKLeGQMo.js → export-D7IGzTG4.js} +196 -792
  16. package/dist/chunks/export-D7IGzTG4.js.map +1 -0
  17. package/dist/chunks/{exporter-BWwEuOPT.js → exporter-BtnqYuuj.js} +22 -12
  18. package/dist/chunks/exporter-BtnqYuuj.js.map +1 -0
  19. package/dist/chunks/{exporter-yLxKBvLE.js → exporter-ChPyhBOv.js} +4 -3
  20. package/dist/chunks/{exporter-yLxKBvLE.js.map → exporter-ChPyhBOv.js.map} +1 -1
  21. package/dist/chunks/{exporter-8hLNemFk.js → exporter-KEPxYUXD.js} +133 -74
  22. package/dist/chunks/exporter-KEPxYUXD.js.map +1 -0
  23. package/dist/chunks/{exporter-RVtXVetw.js → exporter-quPUCBiC.js} +1805 -287
  24. package/dist/chunks/exporter-quPUCBiC.js.map +1 -0
  25. package/dist/chunks/format-GDmO6m6Q.js +676 -0
  26. package/dist/chunks/format-GDmO6m6Q.js.map +1 -0
  27. package/dist/chunks/generic-BL06jrJf.js +1347 -0
  28. package/dist/chunks/generic-BL06jrJf.js.map +1 -0
  29. package/dist/chunks/{importer-WrGRZb0N.js → importer-B7r2owVf.js} +449 -102
  30. package/dist/chunks/importer-B7r2owVf.js.map +1 -0
  31. package/dist/chunks/{importer-Ht7LNg8L.js → importer-BFmOP96W.js} +116 -296
  32. package/dist/chunks/importer-BFmOP96W.js.map +1 -0
  33. package/dist/chunks/{importer-BbNb7eNg.js → importer-C2nzGhdV.js} +1390 -129
  34. package/dist/chunks/importer-C2nzGhdV.js.map +1 -0
  35. package/dist/chunks/{importer-Dz7tI8Sa.js → importer-CbX2_rNF.js} +617 -115
  36. package/dist/chunks/importer-CbX2_rNF.js.map +1 -0
  37. package/dist/chunks/{importer-D794XUTe.js → importer-CdzeN6X4.js} +31 -8
  38. package/dist/chunks/importer-CdzeN6X4.js.map +1 -0
  39. package/dist/chunks/{importer-xZ-3suY1.js → importer-ChKNC_GN.js} +791 -135
  40. package/dist/chunks/importer-ChKNC_GN.js.map +1 -0
  41. package/dist/chunks/{importer-C98dpSH4.js → importer-dwoYpSqb.js} +141 -82
  42. package/dist/chunks/importer-dwoYpSqb.js.map +1 -0
  43. package/dist/chunks/index-B9fdNpTA.js +4310 -0
  44. package/dist/chunks/index-B9fdNpTA.js.map +1 -0
  45. package/dist/chunks/{json-elements-DKSqpNW1.js → json-elements-C4FwgnH5.js} +123 -32
  46. package/dist/chunks/json-elements-C4FwgnH5.js.map +1 -0
  47. package/dist/chunks/lexicon-B-knUi-l.js +508 -0
  48. package/dist/chunks/lexicon-B-knUi-l.js.map +1 -0
  49. package/dist/chunks/{ontology-export-CtRXjg_i.js → ontology-export-BSpbY5FL.js} +3 -2
  50. package/dist/chunks/{ontology-export-CtRXjg_i.js.map → ontology-export-BSpbY5FL.js.map} +1 -1
  51. package/dist/chunks/{records-Cu4bxYOm.js → records-Nwz_rbNt.js} +2 -2
  52. package/dist/chunks/{records-Cu4bxYOm.js.map → records-Nwz_rbNt.js.map} +1 -1
  53. package/dist/chunks/styles-Brt9JMfs.js +690 -0
  54. package/dist/chunks/styles-Brt9JMfs.js.map +1 -0
  55. package/dist/chunks/styles-_N-C8S-U.js +2102 -0
  56. package/dist/chunks/styles-_N-C8S-U.js.map +1 -0
  57. package/dist/chunks/styles-yed-DAfcIkf7.js +842 -0
  58. package/dist/chunks/styles-yed-DAfcIkf7.js.map +1 -0
  59. package/dist/chunks/{text-DHOJXnED.js → text-CgzMqHQb.js} +2 -2
  60. package/dist/chunks/{text-DHOJXnED.js.map → text-CgzMqHQb.js.map} +1 -1
  61. package/dist/chunks/units-XMZFIFe1.js +19 -0
  62. package/dist/chunks/units-XMZFIFe1.js.map +1 -0
  63. package/dist/csv.js +5 -4
  64. package/dist/csv.js.map +1 -1
  65. package/dist/cx.js +1 -1
  66. package/dist/cx2.js +2 -2
  67. package/dist/cys.js +1 -1
  68. package/dist/dot.js +5 -2
  69. package/dist/dot.js.map +1 -1
  70. package/dist/gexf.js +292 -412
  71. package/dist/gexf.js.map +1 -1
  72. package/dist/gml.js +8 -3031
  73. package/dist/gml.js.map +1 -1
  74. package/dist/graph-io.js +354 -240
  75. package/dist/graph-io.js.map +1 -1
  76. package/dist/graphml.js +4 -2
  77. package/dist/graphml.js.map +1 -1
  78. package/dist/json.js +1 -1
  79. package/dist/neo4j.js +4 -3
  80. package/dist/neo4j.js.map +1 -1
  81. package/dist/obo.js +1 -1
  82. package/dist/pajek.js +4 -2
  83. package/dist/positions.d.ts +1 -0
  84. package/dist/positions.js +427 -0
  85. package/dist/positions.js.map +1 -0
  86. package/dist/src/common/codes.d.ts +6 -0
  87. package/dist/src/common/codes.d.ts.map +1 -1
  88. package/dist/src/common/codes.js +6 -0
  89. package/dist/src/common/codes.js.map +1 -1
  90. package/dist/src/common/escape.d.ts.map +1 -1
  91. package/dist/src/common/escape.js +4 -2
  92. package/dist/src/common/escape.js.map +1 -1
  93. package/dist/src/common/export.d.ts +10 -0
  94. package/dist/src/common/export.d.ts.map +1 -1
  95. package/dist/src/common/export.js +123 -100
  96. package/dist/src/common/export.js.map +1 -1
  97. package/dist/src/common/input.d.ts.map +1 -1
  98. package/dist/src/common/input.js +8 -1
  99. package/dist/src/common/input.js.map +1 -1
  100. package/dist/src/common/json-elements.d.ts +12 -9
  101. package/dist/src/common/json-elements.d.ts.map +1 -1
  102. package/dist/src/common/json-elements.js +160 -37
  103. package/dist/src/common/json-elements.js.map +1 -1
  104. package/dist/src/common/notes.d.ts +42 -0
  105. package/dist/src/common/notes.d.ts.map +1 -0
  106. package/dist/src/common/notes.js +72 -0
  107. package/dist/src/common/notes.js.map +1 -0
  108. package/dist/src/common/report.d.ts +18 -0
  109. package/dist/src/common/report.d.ts.map +1 -1
  110. package/dist/src/common/report.js +36 -1
  111. package/dist/src/common/report.js.map +1 -1
  112. package/dist/src/common/units.d.ts +24 -0
  113. package/dist/src/common/units.d.ts.map +1 -0
  114. package/dist/src/common/units.js +26 -0
  115. package/dist/src/common/units.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +18 -0
  117. package/dist/src/common/xml.d.ts.map +1 -1
  118. package/dist/src/common/xml.js +42 -5
  119. package/dist/src/common/xml.js.map +1 -1
  120. package/dist/src/formats/cx/exporter.d.ts +6 -0
  121. package/dist/src/formats/cx/exporter.d.ts.map +1 -1
  122. package/dist/src/formats/cx/exporter.js +71 -8
  123. package/dist/src/formats/cx/exporter.js.map +1 -1
  124. package/dist/src/formats/cx/importer.d.ts +5 -2
  125. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cx/importer.js +102 -72
  127. package/dist/src/formats/cx/importer.js.map +1 -1
  128. package/dist/src/formats/cx/styles.d.ts +102 -0
  129. package/dist/src/formats/cx/styles.d.ts.map +1 -0
  130. package/dist/src/formats/cx/styles.js +225 -0
  131. package/dist/src/formats/cx/styles.js.map +1 -0
  132. package/dist/src/formats/cx2/exporter.d.ts +5 -0
  133. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  134. package/dist/src/formats/cx2/exporter.js +181 -71
  135. package/dist/src/formats/cx2/exporter.js.map +1 -1
  136. package/dist/src/formats/cx2/importer.d.ts +4 -1
  137. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  138. package/dist/src/formats/cx2/importer.js +19 -1
  139. package/dist/src/formats/cx2/importer.js.map +1 -1
  140. package/dist/src/formats/cx2/styles.d.ts +18 -0
  141. package/dist/src/formats/cx2/styles.d.ts.map +1 -0
  142. package/dist/src/formats/cx2/styles.js +24 -0
  143. package/dist/src/formats/cx2/styles.js.map +1 -0
  144. package/dist/src/formats/cys/constants.d.ts +2 -0
  145. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  146. package/dist/src/formats/cys/constants.js +2 -0
  147. package/dist/src/formats/cys/constants.js.map +1 -1
  148. package/dist/src/formats/cys/exporter.d.ts +6 -2
  149. package/dist/src/formats/cys/exporter.d.ts.map +1 -1
  150. package/dist/src/formats/cys/exporter.js +215 -53
  151. package/dist/src/formats/cys/exporter.js.map +1 -1
  152. package/dist/src/formats/cys/importer.d.ts +2 -1
  153. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  154. package/dist/src/formats/cys/importer.js +119 -6
  155. package/dist/src/formats/cys/importer.js.map +1 -1
  156. package/dist/src/formats/cys/session.d.ts +2 -0
  157. package/dist/src/formats/cys/session.d.ts.map +1 -1
  158. package/dist/src/formats/cys/session.js +5 -0
  159. package/dist/src/formats/cys/session.js.map +1 -1
  160. package/dist/src/formats/cys/styles.d.ts +38 -0
  161. package/dist/src/formats/cys/styles.d.ts.map +1 -0
  162. package/dist/src/formats/cys/styles.js +213 -0
  163. package/dist/src/formats/cys/styles.js.map +1 -0
  164. package/dist/src/formats/cys/tables.d.ts +8 -3
  165. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  166. package/dist/src/formats/cys/tables.js +88 -34
  167. package/dist/src/formats/cys/tables.js.map +1 -1
  168. package/dist/src/formats/dot/exporter.d.ts +2 -0
  169. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  170. package/dist/src/formats/dot/exporter.js +195 -81
  171. package/dist/src/formats/dot/exporter.js.map +1 -1
  172. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  173. package/dist/src/formats/dot/importer.js +100 -20
  174. package/dist/src/formats/dot/importer.js.map +1 -1
  175. package/dist/src/formats/dot/index.d.ts +1 -0
  176. package/dist/src/formats/dot/index.d.ts.map +1 -1
  177. package/dist/src/formats/dot/index.js +1 -0
  178. package/dist/src/formats/dot/index.js.map +1 -1
  179. package/dist/src/formats/dot/positions.d.ts +14 -0
  180. package/dist/src/formats/dot/positions.d.ts.map +1 -0
  181. package/dist/src/formats/dot/positions.js +70 -0
  182. package/dist/src/formats/dot/positions.js.map +1 -0
  183. package/dist/src/formats/dot/styles.d.ts +82 -0
  184. package/dist/src/formats/dot/styles.d.ts.map +1 -0
  185. package/dist/src/formats/dot/styles.js +1484 -0
  186. package/dist/src/formats/dot/styles.js.map +1 -0
  187. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  188. package/dist/src/formats/gexf/exporter.js +20 -5
  189. package/dist/src/formats/gexf/exporter.js.map +1 -1
  190. package/dist/src/formats/gexf/importer.d.ts +8 -1
  191. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  192. package/dist/src/formats/gexf/importer.js +251 -131
  193. package/dist/src/formats/gexf/importer.js.map +1 -1
  194. package/dist/src/formats/gexf/index.d.ts +8 -0
  195. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  196. package/dist/src/formats/gexf/index.js +10 -2
  197. package/dist/src/formats/gexf/index.js.map +1 -1
  198. package/dist/src/formats/gexf/schema.d.ts +5 -0
  199. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  200. package/dist/src/formats/gexf/schema.js +5 -0
  201. package/dist/src/formats/gexf/schema.js.map +1 -1
  202. package/dist/src/formats/gexf/styles.d.ts +74 -0
  203. package/dist/src/formats/gexf/styles.d.ts.map +1 -0
  204. package/dist/src/formats/gexf/styles.js +604 -0
  205. package/dist/src/formats/gexf/styles.js.map +1 -0
  206. package/dist/src/formats/gml/exporter.d.ts +1 -5
  207. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  208. package/dist/src/formats/gml/exporter.js +19 -8
  209. package/dist/src/formats/gml/exporter.js.map +1 -1
  210. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  211. package/dist/src/formats/gml/importer.js +4 -0
  212. package/dist/src/formats/gml/importer.js.map +1 -1
  213. package/dist/src/formats/gml/index.d.ts +2 -1
  214. package/dist/src/formats/gml/index.d.ts.map +1 -1
  215. package/dist/src/formats/gml/index.js +2 -1
  216. package/dist/src/formats/gml/index.js.map +1 -1
  217. package/dist/src/formats/gml/positions.d.ts +14 -0
  218. package/dist/src/formats/gml/positions.d.ts.map +1 -0
  219. package/dist/src/formats/gml/positions.js +46 -0
  220. package/dist/src/formats/gml/positions.js.map +1 -0
  221. package/dist/src/formats/gml/styles.d.ts +59 -0
  222. package/dist/src/formats/gml/styles.d.ts.map +1 -0
  223. package/dist/src/formats/gml/styles.js +1056 -0
  224. package/dist/src/formats/gml/styles.js.map +1 -0
  225. package/dist/src/formats/gml/syntax.d.ts +11 -0
  226. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  227. package/dist/src/formats/gml/syntax.js +151 -62
  228. package/dist/src/formats/gml/syntax.js.map +1 -1
  229. package/dist/src/formats/graphml/constants.d.ts +4 -0
  230. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  231. package/dist/src/formats/graphml/constants.js +5 -1
  232. package/dist/src/formats/graphml/constants.js.map +1 -1
  233. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  234. package/dist/src/formats/graphml/exporter.js +133 -65
  235. package/dist/src/formats/graphml/exporter.js.map +1 -1
  236. package/dist/src/formats/graphml/importer.d.ts +4 -1
  237. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  238. package/dist/src/formats/graphml/importer.js +100 -44
  239. package/dist/src/formats/graphml/importer.js.map +1 -1
  240. package/dist/src/formats/graphml/index.d.ts +1 -0
  241. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  242. package/dist/src/formats/graphml/index.js +1 -0
  243. package/dist/src/formats/graphml/index.js.map +1 -1
  244. package/dist/src/formats/graphml/styles-plain.d.ts +19 -0
  245. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -0
  246. package/dist/src/formats/graphml/styles-plain.js +187 -0
  247. package/dist/src/formats/graphml/styles-plain.js.map +1 -0
  248. package/dist/src/formats/graphml/styles-yed.d.ts +179 -0
  249. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -0
  250. package/dist/src/formats/graphml/styles-yed.js +1086 -0
  251. package/dist/src/formats/graphml/styles-yed.js.map +1 -0
  252. package/dist/src/formats/graphml/styles-yfiles3.d.ts +19 -0
  253. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -0
  254. package/dist/src/formats/graphml/styles-yfiles3.js +508 -0
  255. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -0
  256. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  257. package/dist/src/formats/graphml/tree.js +34 -19
  258. package/dist/src/formats/graphml/tree.js.map +1 -1
  259. package/dist/src/formats/graphml/yfiles.d.ts +16 -2
  260. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  261. package/dist/src/formats/graphml/yfiles.js +83 -12
  262. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  263. package/dist/src/formats/json/exporter.d.ts +5 -0
  264. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  265. package/dist/src/formats/json/exporter.js +68 -25
  266. package/dist/src/formats/json/exporter.js.map +1 -1
  267. package/dist/src/formats/json/importer.d.ts +22 -3
  268. package/dist/src/formats/json/importer.d.ts.map +1 -1
  269. package/dist/src/formats/json/importer.js +439 -264
  270. package/dist/src/formats/json/importer.js.map +1 -1
  271. package/dist/src/formats/json/positions.d.ts +16 -0
  272. package/dist/src/formats/json/positions.d.ts.map +1 -0
  273. package/dist/src/formats/json/positions.js +44 -0
  274. package/dist/src/formats/json/positions.js.map +1 -0
  275. package/dist/src/formats/json/styles.d.ts +102 -0
  276. package/dist/src/formats/json/styles.d.ts.map +1 -0
  277. package/dist/src/formats/json/styles.js +1753 -0
  278. package/dist/src/formats/json/styles.js.map +1 -0
  279. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  280. package/dist/src/formats/neo4j/exporter.js +2 -1
  281. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  282. package/dist/src/formats/pajek/exporter.d.ts +2 -0
  283. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/pajek/exporter.js +142 -76
  285. package/dist/src/formats/pajek/exporter.js.map +1 -1
  286. package/dist/src/formats/pajek/importer.d.ts +4 -0
  287. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  288. package/dist/src/formats/pajek/importer.js +60 -6
  289. package/dist/src/formats/pajek/importer.js.map +1 -1
  290. package/dist/src/formats/pajek/index.d.ts +2 -0
  291. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  292. package/dist/src/formats/pajek/index.js +2 -0
  293. package/dist/src/formats/pajek/index.js.map +1 -1
  294. package/dist/src/formats/pajek/styles.d.ts +50 -0
  295. package/dist/src/formats/pajek/styles.d.ts.map +1 -0
  296. package/dist/src/formats/pajek/styles.js +569 -0
  297. package/dist/src/formats/pajek/styles.js.map +1 -0
  298. package/dist/src/formats/pajek/syntax.d.ts +6 -0
  299. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  300. package/dist/src/formats/pajek/syntax.js +6 -0
  301. package/dist/src/formats/pajek/syntax.js.map +1 -1
  302. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  303. package/dist/src/formats/xgmml/columns.js +34 -9
  304. package/dist/src/formats/xgmml/columns.js.map +1 -1
  305. package/dist/src/formats/xgmml/constants.d.ts +2 -0
  306. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  307. package/dist/src/formats/xgmml/constants.js +3 -1
  308. package/dist/src/formats/xgmml/constants.js.map +1 -1
  309. package/dist/src/formats/xgmml/document.d.ts +12 -8
  310. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  311. package/dist/src/formats/xgmml/document.js +56 -13
  312. package/dist/src/formats/xgmml/document.js.map +1 -1
  313. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  314. package/dist/src/formats/xgmml/emit.js +12 -6
  315. package/dist/src/formats/xgmml/emit.js.map +1 -1
  316. package/dist/src/formats/xgmml/exporter.d.ts +3 -1
  317. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  318. package/dist/src/formats/xgmml/exporter.js +22 -9
  319. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  320. package/dist/src/formats/xgmml/importer.d.ts +2 -1
  321. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  322. package/dist/src/formats/xgmml/importer.js +31 -24
  323. package/dist/src/formats/xgmml/importer.js.map +1 -1
  324. package/dist/src/formats/xgmml/styles.d.ts +104 -0
  325. package/dist/src/formats/xgmml/styles.d.ts.map +1 -0
  326. package/dist/src/formats/xgmml/styles.js +310 -0
  327. package/dist/src/formats/xgmml/styles.js.map +1 -0
  328. package/dist/src/index.d.ts +9 -7
  329. package/dist/src/index.d.ts.map +1 -1
  330. package/dist/src/index.js +7 -7
  331. package/dist/src/index.js.map +1 -1
  332. package/dist/src/positions/codes.d.ts +31 -0
  333. package/dist/src/positions/codes.d.ts.map +1 -0
  334. package/dist/src/positions/codes.js +26 -0
  335. package/dist/src/positions/codes.js.map +1 -0
  336. package/dist/src/positions/convert.d.ts +58 -0
  337. package/dist/src/positions/convert.d.ts.map +1 -0
  338. package/dist/src/positions/convert.js +107 -0
  339. package/dist/src/positions/convert.js.map +1 -0
  340. package/dist/src/positions/extract.d.ts +27 -0
  341. package/dist/src/positions/extract.d.ts.map +1 -0
  342. package/dist/src/positions/extract.js +526 -0
  343. package/dist/src/positions/extract.js.map +1 -0
  344. package/dist/src/positions/index.d.ts +10 -0
  345. package/dist/src/positions/index.d.ts.map +1 -0
  346. package/dist/src/positions/index.js +9 -0
  347. package/dist/src/positions/index.js.map +1 -0
  348. package/dist/src/positions/types.d.ts +208 -0
  349. package/dist/src/positions/types.d.ts.map +1 -0
  350. package/dist/src/positions/types.js +10 -0
  351. package/dist/src/positions/types.js.map +1 -0
  352. package/dist/src/registry.d.ts +30 -0
  353. package/dist/src/registry.d.ts.map +1 -1
  354. package/dist/src/registry.js +130 -9
  355. package/dist/src/registry.js.map +1 -1
  356. package/dist/src/styles/annotations.d.ts +132 -0
  357. package/dist/src/styles/annotations.d.ts.map +1 -0
  358. package/dist/src/styles/annotations.js +320 -0
  359. package/dist/src/styles/annotations.js.map +1 -0
  360. package/dist/src/styles/budget.d.ts +45 -0
  361. package/dist/src/styles/budget.d.ts.map +1 -0
  362. package/dist/src/styles/budget.js +58 -0
  363. package/dist/src/styles/budget.js.map +1 -0
  364. package/dist/src/styles/canon/bends.d.ts +32 -0
  365. package/dist/src/styles/canon/bends.d.ts.map +1 -0
  366. package/dist/src/styles/canon/bends.js +74 -0
  367. package/dist/src/styles/canon/bends.js.map +1 -0
  368. package/dist/src/styles/canon/color-tables.d.ts +22 -0
  369. package/dist/src/styles/canon/color-tables.d.ts.map +1 -0
  370. package/dist/src/styles/canon/color-tables.js +369 -0
  371. package/dist/src/styles/canon/color-tables.js.map +1 -0
  372. package/dist/src/styles/canon/color.d.ts +66 -0
  373. package/dist/src/styles/canon/color.d.ts.map +1 -0
  374. package/dist/src/styles/canon/color.js +548 -0
  375. package/dist/src/styles/canon/color.js.map +1 -0
  376. package/dist/src/styles/canon/enums.d.ts +25 -0
  377. package/dist/src/styles/canon/enums.d.ts.map +1 -0
  378. package/dist/src/styles/canon/enums.js +187 -0
  379. package/dist/src/styles/canon/enums.js.map +1 -0
  380. package/dist/src/styles/canon/graphviz.d.ts +48 -0
  381. package/dist/src/styles/canon/graphviz.d.ts.map +1 -0
  382. package/dist/src/styles/canon/graphviz.js +160 -0
  383. package/dist/src/styles/canon/graphviz.js.map +1 -0
  384. package/dist/src/styles/canon/index.d.ts +13 -0
  385. package/dist/src/styles/canon/index.d.ts.map +1 -0
  386. package/dist/src/styles/canon/index.js +13 -0
  387. package/dist/src/styles/canon/index.js.map +1 -0
  388. package/dist/src/styles/canon/number.d.ts +59 -0
  389. package/dist/src/styles/canon/number.d.ts.map +1 -0
  390. package/dist/src/styles/canon/number.js +142 -0
  391. package/dist/src/styles/canon/number.js.map +1 -0
  392. package/dist/src/styles/canon/site.d.ts +51 -0
  393. package/dist/src/styles/canon/site.d.ts.map +1 -0
  394. package/dist/src/styles/canon/site.js +63 -0
  395. package/dist/src/styles/canon/site.js.map +1 -0
  396. package/dist/src/styles/canon/text.d.ts +61 -0
  397. package/dist/src/styles/canon/text.d.ts.map +1 -0
  398. package/dist/src/styles/canon/text.js +199 -0
  399. package/dist/src/styles/canon/text.js.map +1 -0
  400. package/dist/src/styles/codes.d.ts +95 -0
  401. package/dist/src/styles/codes.d.ts.map +1 -0
  402. package/dist/src/styles/codes.js +97 -0
  403. package/dist/src/styles/codes.js.map +1 -0
  404. package/dist/src/styles/cyjs/cascade.d.ts +134 -0
  405. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -0
  406. package/dist/src/styles/cyjs/cascade.js +2252 -0
  407. package/dist/src/styles/cyjs/cascade.js.map +1 -0
  408. package/dist/src/styles/cyjs/defaults-table.d.ts +25 -0
  409. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -0
  410. package/dist/src/styles/cyjs/defaults-table.js +76 -0
  411. package/dist/src/styles/cyjs/defaults-table.js.map +1 -0
  412. package/dist/src/styles/cyjs/emit.d.ts +82 -0
  413. package/dist/src/styles/cyjs/emit.d.ts.map +1 -0
  414. package/dist/src/styles/cyjs/emit.js +790 -0
  415. package/dist/src/styles/cyjs/emit.js.map +1 -0
  416. package/dist/src/styles/cyjs/index.d.ts +13 -0
  417. package/dist/src/styles/cyjs/index.d.ts.map +1 -0
  418. package/dist/src/styles/cyjs/index.js +13 -0
  419. package/dist/src/styles/cyjs/index.js.map +1 -0
  420. package/dist/src/styles/cyjs/live.d.ts +46 -0
  421. package/dist/src/styles/cyjs/live.d.ts.map +1 -0
  422. package/dist/src/styles/cyjs/live.js +108 -0
  423. package/dist/src/styles/cyjs/live.js.map +1 -0
  424. package/dist/src/styles/cyjs/mappers.d.ts +147 -0
  425. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -0
  426. package/dist/src/styles/cyjs/mappers.js +744 -0
  427. package/dist/src/styles/cyjs/mappers.js.map +1 -0
  428. package/dist/src/styles/cyjs/selector.d.ts +113 -0
  429. package/dist/src/styles/cyjs/selector.d.ts.map +1 -0
  430. package/dist/src/styles/cyjs/selector.js +611 -0
  431. package/dist/src/styles/cyjs/selector.js.map +1 -0
  432. package/dist/src/styles/cyjs/stylesheet-text.d.ts +37 -0
  433. package/dist/src/styles/cyjs/stylesheet-text.d.ts.map +1 -0
  434. package/dist/src/styles/cyjs/stylesheet-text.js +181 -0
  435. package/dist/src/styles/cyjs/stylesheet-text.js.map +1 -0
  436. package/dist/src/styles/cytoscape/emit.d.ts +125 -0
  437. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -0
  438. package/dist/src/styles/cytoscape/emit.js +1025 -0
  439. package/dist/src/styles/cytoscape/emit.js.map +1 -0
  440. package/dist/src/styles/cytoscape/index.d.ts +10 -0
  441. package/dist/src/styles/cytoscape/index.d.ts.map +1 -0
  442. package/dist/src/styles/cytoscape/index.js +10 -0
  443. package/dist/src/styles/cytoscape/index.js.map +1 -0
  444. package/dist/src/styles/cytoscape/lexicon.d.ts +49 -0
  445. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -0
  446. package/dist/src/styles/cytoscape/lexicon.js +603 -0
  447. package/dist/src/styles/cytoscape/lexicon.js.map +1 -0
  448. package/dist/src/styles/cytoscape/mappings.d.ts +171 -0
  449. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -0
  450. package/dist/src/styles/cytoscape/mappings.js +460 -0
  451. package/dist/src/styles/cytoscape/mappings.js.map +1 -0
  452. package/dist/src/styles/cytoscape/resolve.d.ts +137 -0
  453. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -0
  454. package/dist/src/styles/cytoscape/resolve.js +381 -0
  455. package/dist/src/styles/cytoscape/resolve.js.map +1 -0
  456. package/dist/src/styles/cytoscape/translate.d.ts +50 -0
  457. package/dist/src/styles/cytoscape/translate.d.ts.map +1 -0
  458. package/dist/src/styles/cytoscape/translate.js +156 -0
  459. package/dist/src/styles/cytoscape/translate.js.map +1 -0
  460. package/dist/src/styles/defaults.d.ts +90 -0
  461. package/dist/src/styles/defaults.d.ts.map +1 -0
  462. package/dist/src/styles/defaults.js +414 -0
  463. package/dist/src/styles/defaults.js.map +1 -0
  464. package/dist/src/styles/index.d.ts +15 -0
  465. package/dist/src/styles/index.d.ts.map +1 -0
  466. package/dist/src/styles/index.js +14 -0
  467. package/dist/src/styles/index.js.map +1 -0
  468. package/dist/src/styles/internal.d.ts +17 -0
  469. package/dist/src/styles/internal.d.ts.map +1 -0
  470. package/dist/src/styles/internal.js +16 -0
  471. package/dist/src/styles/internal.js.map +1 -0
  472. package/dist/src/styles/io/approximations.d.ts +24 -0
  473. package/dist/src/styles/io/approximations.d.ts.map +1 -0
  474. package/dist/src/styles/io/approximations.js +51 -0
  475. package/dist/src/styles/io/approximations.js.map +1 -0
  476. package/dist/src/styles/io/block.d.ts +166 -0
  477. package/dist/src/styles/io/block.d.ts.map +1 -0
  478. package/dist/src/styles/io/block.js +930 -0
  479. package/dist/src/styles/io/block.js.map +1 -0
  480. package/dist/src/styles/io/generic.d.ts +88 -0
  481. package/dist/src/styles/io/generic.d.ts.map +1 -0
  482. package/dist/src/styles/io/generic.js +516 -0
  483. package/dist/src/styles/io/generic.js.map +1 -0
  484. package/dist/src/styles/io/index.d.ts +8 -0
  485. package/dist/src/styles/io/index.d.ts.map +1 -0
  486. package/dist/src/styles/io/index.js +7 -0
  487. package/dist/src/styles/io/index.js.map +1 -0
  488. package/dist/src/styles/io/writer.d.ts +80 -0
  489. package/dist/src/styles/io/writer.d.ts.map +1 -0
  490. package/dist/src/styles/io/writer.js +596 -0
  491. package/dist/src/styles/io/writer.js.map +1 -0
  492. package/dist/src/styles/notes.d.ts +47 -0
  493. package/dist/src/styles/notes.d.ts.map +1 -0
  494. package/dist/src/styles/notes.js +140 -0
  495. package/dist/src/styles/notes.js.map +1 -0
  496. package/dist/src/styles/plan/classes.d.ts +31 -0
  497. package/dist/src/styles/plan/classes.d.ts.map +1 -0
  498. package/dist/src/styles/plan/classes.js +139 -0
  499. package/dist/src/styles/plan/classes.js.map +1 -0
  500. package/dist/src/styles/plan/continuous.d.ts +32 -0
  501. package/dist/src/styles/plan/continuous.d.ts.map +1 -0
  502. package/dist/src/styles/plan/continuous.js +165 -0
  503. package/dist/src/styles/plan/continuous.js.map +1 -0
  504. package/dist/src/styles/plan/discrete.d.ts +39 -0
  505. package/dist/src/styles/plan/discrete.d.ts.map +1 -0
  506. package/dist/src/styles/plan/discrete.js +127 -0
  507. package/dist/src/styles/plan/discrete.js.map +1 -0
  508. package/dist/src/styles/plan/families.d.ts +31 -0
  509. package/dist/src/styles/plan/families.d.ts.map +1 -0
  510. package/dist/src/styles/plan/families.js +95 -0
  511. package/dist/src/styles/plan/families.js.map +1 -0
  512. package/dist/src/styles/plan/features.d.ts +58 -0
  513. package/dist/src/styles/plan/features.d.ts.map +1 -0
  514. package/dist/src/styles/plan/features.js +185 -0
  515. package/dist/src/styles/plan/features.js.map +1 -0
  516. package/dist/src/styles/plan/index.d.ts +7 -0
  517. package/dist/src/styles/plan/index.d.ts.map +1 -0
  518. package/dist/src/styles/plan/index.js +7 -0
  519. package/dist/src/styles/plan/index.js.map +1 -0
  520. package/dist/src/styles/plan/plan.d.ts +71 -0
  521. package/dist/src/styles/plan/plan.d.ts.map +1 -0
  522. package/dist/src/styles/plan/plan.js +893 -0
  523. package/dist/src/styles/plan/plan.js.map +1 -0
  524. package/dist/src/styles/plan/resolve.d.ts +137 -0
  525. package/dist/src/styles/plan/resolve.d.ts.map +1 -0
  526. package/dist/src/styles/plan/resolve.js +587 -0
  527. package/dist/src/styles/plan/resolve.js.map +1 -0
  528. package/dist/src/styles/types.d.ts +702 -0
  529. package/dist/src/styles/types.d.ts.map +1 -0
  530. package/dist/src/styles/types.js +10 -0
  531. package/dist/src/styles/types.js.map +1 -0
  532. package/dist/src/styles/vocabulary.d.ts +41 -0
  533. package/dist/src/styles/vocabulary.d.ts.map +1 -0
  534. package/dist/src/styles/vocabulary.js +140 -0
  535. package/dist/src/styles/vocabulary.js.map +1 -0
  536. package/dist/src/types.d.ts +64 -1
  537. package/dist/src/types.d.ts.map +1 -1
  538. package/dist/src/types.js.map +1 -1
  539. package/dist/styles.d.ts +1 -0
  540. package/dist/styles.js +322 -0
  541. package/dist/styles.js.map +1 -0
  542. package/dist/xgmml.js +5 -4
  543. package/dist/xgmml.js.map +1 -1
  544. package/package.json +13 -2
  545. package/src/common/codes.ts +7 -0
  546. package/src/common/escape.ts +5 -2
  547. package/src/common/export.ts +179 -140
  548. package/src/common/input.ts +8 -4
  549. package/src/common/json-elements.ts +169 -38
  550. package/src/common/notes.ts +94 -0
  551. package/src/common/report.ts +41 -1
  552. package/src/common/units.ts +42 -0
  553. package/src/common/xml.ts +45 -5
  554. package/src/formats/cx/exporter.ts +95 -11
  555. package/src/formats/cx/importer.ts +126 -90
  556. package/src/formats/cx/styles.ts +304 -0
  557. package/src/formats/cx2/exporter.ts +234 -77
  558. package/src/formats/cx2/importer.ts +19 -1
  559. package/src/formats/cx2/styles.ts +29 -0
  560. package/src/formats/cys/constants.ts +3 -0
  561. package/src/formats/cys/exporter.ts +246 -60
  562. package/src/formats/cys/importer.ts +125 -9
  563. package/src/formats/cys/session.ts +6 -0
  564. package/src/formats/cys/styles.ts +254 -0
  565. package/src/formats/cys/tables.ts +117 -48
  566. package/src/formats/dot/exporter.ts +262 -126
  567. package/src/formats/dot/importer.ts +111 -20
  568. package/src/formats/dot/index.ts +1 -0
  569. package/src/formats/dot/positions.ts +77 -0
  570. package/src/formats/dot/styles.ts +1741 -0
  571. package/src/formats/gexf/exporter.ts +27 -11
  572. package/src/formats/gexf/importer.ts +308 -158
  573. package/src/formats/gexf/index.ts +10 -0
  574. package/src/formats/gexf/schema.ts +5 -0
  575. package/src/formats/gexf/styles.ts +719 -0
  576. package/src/formats/gml/exporter.ts +33 -14
  577. package/src/formats/gml/importer.ts +4 -0
  578. package/src/formats/gml/index.ts +2 -1
  579. package/src/formats/gml/positions.ts +52 -0
  580. package/src/formats/gml/styles.ts +1248 -0
  581. package/src/formats/gml/syntax.ts +173 -62
  582. package/src/formats/graphml/constants.ts +5 -0
  583. package/src/formats/graphml/exporter.ts +163 -71
  584. package/src/formats/graphml/importer.ts +119 -45
  585. package/src/formats/graphml/index.ts +1 -0
  586. package/src/formats/graphml/styles-plain.ts +236 -0
  587. package/src/formats/graphml/styles-yed.ts +1221 -0
  588. package/src/formats/graphml/styles-yfiles3.ts +573 -0
  589. package/src/formats/graphml/tree.ts +36 -20
  590. package/src/formats/graphml/yfiles.ts +89 -13
  591. package/src/formats/json/exporter.ts +76 -26
  592. package/src/formats/json/importer.ts +541 -322
  593. package/src/formats/json/positions.ts +50 -0
  594. package/src/formats/json/styles.ts +2082 -0
  595. package/src/formats/neo4j/exporter.ts +2 -1
  596. package/src/formats/pajek/exporter.ts +195 -106
  597. package/src/formats/pajek/importer.ts +78 -9
  598. package/src/formats/pajek/index.ts +2 -0
  599. package/src/formats/pajek/styles.ts +685 -0
  600. package/src/formats/pajek/syntax.ts +7 -0
  601. package/src/formats/xgmml/columns.ts +39 -9
  602. package/src/formats/xgmml/constants.ts +3 -0
  603. package/src/formats/xgmml/document.ts +54 -12
  604. package/src/formats/xgmml/emit.ts +11 -6
  605. package/src/formats/xgmml/exporter.ts +32 -13
  606. package/src/formats/xgmml/importer.ts +43 -29
  607. package/src/formats/xgmml/styles.ts +400 -0
  608. package/src/index.ts +16 -0
  609. package/src/positions/codes.ts +32 -0
  610. package/src/positions/convert.ts +123 -0
  611. package/src/positions/extract.ts +619 -0
  612. package/src/positions/index.ts +23 -0
  613. package/src/positions/types.ts +221 -0
  614. package/src/registry.ts +179 -8
  615. package/src/styles/annotations.ts +478 -0
  616. package/src/styles/budget.ts +60 -0
  617. package/src/styles/canon/bends.ts +84 -0
  618. package/src/styles/canon/color-tables.ts +386 -0
  619. package/src/styles/canon/color.ts +624 -0
  620. package/src/styles/canon/enums.ts +211 -0
  621. package/src/styles/canon/graphviz.ts +189 -0
  622. package/src/styles/canon/index.ts +13 -0
  623. package/src/styles/canon/number.ts +152 -0
  624. package/src/styles/canon/site.ts +87 -0
  625. package/src/styles/canon/text.ts +221 -0
  626. package/src/styles/codes.ts +104 -0
  627. package/src/styles/cyjs/cascade.ts +2670 -0
  628. package/src/styles/cyjs/defaults-table.ts +82 -0
  629. package/src/styles/cyjs/emit.ts +951 -0
  630. package/src/styles/cyjs/index.ts +37 -0
  631. package/src/styles/cyjs/live.ts +147 -0
  632. package/src/styles/cyjs/mappers.ts +879 -0
  633. package/src/styles/cyjs/selector.ts +778 -0
  634. package/src/styles/cyjs/stylesheet-text.ts +214 -0
  635. package/src/styles/cytoscape/emit.ts +1264 -0
  636. package/src/styles/cytoscape/index.ts +40 -0
  637. package/src/styles/cytoscape/lexicon.ts +656 -0
  638. package/src/styles/cytoscape/mappings.ts +603 -0
  639. package/src/styles/cytoscape/resolve.ts +521 -0
  640. package/src/styles/cytoscape/translate.ts +237 -0
  641. package/src/styles/defaults.ts +504 -0
  642. package/src/styles/index.ts +57 -0
  643. package/src/styles/internal.ts +17 -0
  644. package/src/styles/io/approximations.ts +47 -0
  645. package/src/styles/io/block.ts +1138 -0
  646. package/src/styles/io/generic.ts +687 -0
  647. package/src/styles/io/index.ts +18 -0
  648. package/src/styles/io/writer.ts +745 -0
  649. package/src/styles/notes.ts +165 -0
  650. package/src/styles/plan/classes.ts +166 -0
  651. package/src/styles/plan/continuous.ts +218 -0
  652. package/src/styles/plan/discrete.ts +165 -0
  653. package/src/styles/plan/families.ts +115 -0
  654. package/src/styles/plan/features.ts +234 -0
  655. package/src/styles/plan/index.ts +7 -0
  656. package/src/styles/plan/plan.ts +1129 -0
  657. package/src/styles/plan/resolve.ts +685 -0
  658. package/src/styles/types.ts +814 -0
  659. package/src/styles/vocabulary.ts +172 -0
  660. package/src/types.ts +76 -1
  661. package/dist/chunks/constants-fT3E8JYY.js.map +0 -1
  662. package/dist/chunks/escape-B62ZGM_a.js.map +0 -1
  663. package/dist/chunks/export-rKLeGQMo.js.map +0 -1
  664. package/dist/chunks/exporter-8hLNemFk.js.map +0 -1
  665. package/dist/chunks/exporter-BWwEuOPT.js.map +0 -1
  666. package/dist/chunks/exporter-RVtXVetw.js.map +0 -1
  667. package/dist/chunks/importer-BbNb7eNg.js.map +0 -1
  668. package/dist/chunks/importer-C98dpSH4.js.map +0 -1
  669. package/dist/chunks/importer-D794XUTe.js.map +0 -1
  670. package/dist/chunks/importer-Dz7tI8Sa.js.map +0 -1
  671. package/dist/chunks/importer-Ht7LNg8L.js.map +0 -1
  672. package/dist/chunks/importer-WrGRZb0N.js.map +0 -1
  673. package/dist/chunks/importer-xZ-3suY1.js.map +0 -1
  674. package/dist/chunks/json-elements-DKSqpNW1.js.map +0 -1
@@ -0,0 +1,2082 @@
1
+ /**
2
+ * JSON styles: each dialect read the way the program that draws it draws it, and a graph's styles written back into
3
+ * each dialect's own style keys.
4
+ *
5
+ * - Cytoscape.js: the stylesheet (an array or a CSS-like text) over the raw element JSON, through graph-io's
6
+ * Cytoscape.js engine, with Cytoscape.js's defaults beneath it; an element's own `style` is a bypass. Cytoscape
7
+ * desktop's export without a stylesheet gets Cytoscape desktop's default style instead.
8
+ * - graphology: the keys sigma.js draws (`color`, `size`, `label`, `hidden`), with sigma's defaults. Gephi's JSON
9
+ * export for sigma.js is read the same way.
10
+ * - vis.js: the keys vis-network draws, `options.nodes` and `options.edges` as declared defaults and
11
+ * `options.groups` as group rules, with vis-network's defaults, its palette for groups the file does not define,
12
+ * and an edge without a color of its own in its source node's border color.
13
+ * - NetworkX node-link: no style keys, NetworkX's drawing defaults.
14
+ *
15
+ * Writing: Cytoscape.js gets a stylesheet with the element data its selectors read and a bypass where no rule gives
16
+ * the value; graphology and vis.js get their per-element keys; node-link, d3 and JGF only the `graphty.styles` block
17
+ * and the tagged per-element values. The block is a graph attribute where the dialect has graph attributes
18
+ * (node-link, JGF, graphology) and a top-level key elsewhere (d3, vis.js, Cytoscape.js).
19
+ */
20
+
21
+ import type { GraphSnapshot } from "@graphty/graph-format";
22
+
23
+ import { canonColor } from "../../styles/canon/color.js";
24
+ import { canonEnum } from "../../styles/canon/enums.js";
25
+ import { canonBoolean, canonNumber, canonOpacity } from "../../styles/canon/number.js";
26
+ import type { CanonSite } from "../../styles/canon/site.js";
27
+ import { canonText } from "../../styles/canon/text.js";
28
+ import { STYLE_ISSUE } from "../../styles/codes.js";
29
+ import { cytoscapeJsStyles } from "../../styles/cyjs/cascade.js";
30
+ import {
31
+ cyjsHoldsExactly,
32
+ CYTOSCAPE_IMPORT_KEY,
33
+ type CytoscapeStylesheetRule,
34
+ toCytoscapeStylesheet,
35
+ } from "../../styles/cyjs/emit.js";
36
+ import { CYJS_EDGE_PROPERTIES, CYJS_NODE_PROPERTIES } from "../../styles/cyjs/mappers.js";
37
+ import { toolDefaults } from "../../styles/defaults.js";
38
+ import { StyleApproximations } from "../../styles/io/approximations.js";
39
+ import { blockStyles, parseStyleBlock, STYLE_BLOCK_NAME } from "../../styles/io/block.js";
40
+ import type { ExporterStyleInput } from "../../styles/io/writer.js";
41
+ import { StyleNotes } from "../../styles/notes.js";
42
+ import { styleFingerprint } from "../../styles/plan/features.js";
43
+ import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
44
+ import type {
45
+ ColumnRef,
46
+ EdgeStyleProperty,
47
+ ImporterStyles,
48
+ NodeStyleProperty,
49
+ Predicate,
50
+ StyleColumn,
51
+ StyleNote,
52
+ StyleReadContext,
53
+ StyleRule,
54
+ StyleTarget,
55
+ StyleValue,
56
+ StyleWriteCapabilities,
57
+ } from "../../styles/types.js";
58
+ import type { LossNote } from "../../types.js";
59
+ import { isJsonObject, type JsonDialect, type JsonImportDialect } from "./dialect.js";
60
+
61
+ /**
62
+ * What a JSON import keeps for its styles (`report.keepForStyles()`): the dialect, a `graphty.styles` block the
63
+ * document holds as a top-level key, and what each dialect's reader needs beyond the graph.
64
+ */
65
+ export interface JsonStyleSource {
66
+ /** The dialect the document was read as. */
67
+ readonly dialect: JsonImportDialect;
68
+ /** A `graphty.styles` block held as a top-level key, or null. */
69
+ readonly block: string | null;
70
+ /** Cytoscape.js: the elements as the file holds them. */
71
+ readonly elements?: unknown;
72
+ /** Cytoscape.js: the stylesheet, as the file holds it. */
73
+ readonly style?: unknown;
74
+ /** Cytoscape.js: the `generated_by` of Cytoscape desktop's export, or null. */
75
+ readonly generatedBy?: string | null;
76
+ /** Cytoscape.js: the id graph-io gave each node element without a `data.id`. */
77
+ readonly generatedIds?: ReadonlyMap<object, string>;
78
+ /** vis.js: the document's `options`. */
79
+ readonly options?: unknown;
80
+ /** vis.js: a top-level `groups` object. */
81
+ readonly groups?: unknown;
82
+ /** graphology: whether the document is Gephi's JSON export (`id` instead of `key`). */
83
+ readonly gephi?: boolean;
84
+ /** node-link: whether the document has NetworkX's `directed`, `multigraph` and `graph` keys. */
85
+ readonly networkx?: boolean;
86
+ }
87
+
88
+ /** The writable values of one row. */
89
+ type Row = Record<string, StyleValue | null>;
90
+ /** Per-property values of one target, by element index. */
91
+ type Values = Record<string, (StyleValue | null)[]>;
92
+
93
+ const TARGETS: readonly StyleTarget[] = ["node", "edge"];
94
+
95
+ // ============================================================ reading
96
+
97
+ /**
98
+ * The styles of a JSON document, read by its dialect's reader from what the import kept.
99
+ * @param snapshot - the graph the import built
100
+ * @param context - the work budget and what the import kept
101
+ * @returns the styles, or null when the dialect has none (a block held as a graph attribute is read by graph-io)
102
+ */
103
+ export function jsonStyles(snapshot: GraphSnapshot, context: StyleReadContext): ImporterStyles | null {
104
+ const src = context.kept as JsonStyleSource | undefined;
105
+ if (!isJsonObject(src)) {
106
+ return null;
107
+ }
108
+ switch (src.dialect) {
109
+ case "cytoscape":
110
+ return cytoscapeStyles(snapshot, src, context);
111
+ case "graphology":
112
+ return graphologyStyles(snapshot, src, context);
113
+ case "vis":
114
+ return visStyles(snapshot, src, context);
115
+ case "node-link":
116
+ if (src.networkx === true) {
117
+ return networkxStyles(snapshot);
118
+ }
119
+ return src.block === null ? null : { canonical: true, block: src.block };
120
+ default:
121
+ return src.block === null ? null : { canonical: true, block: src.block };
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Cytoscape.js: the stylesheet over the raw elements. A file graphty wrote is read without Cytoscape.js's own default
127
+ * rules (its block holds graphty's defaults); Cytoscape desktop's export without a stylesheet gets Cytoscape desktop's
128
+ * default style.
129
+ * @param snapshot - the graph
130
+ * @param src - what the import kept
131
+ * @param context - the work budget
132
+ * @returns the styles
133
+ */
134
+ function cytoscapeStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context: StyleReadContext): ImporterStyles {
135
+ const sheetless =
136
+ (src.style === undefined || src.style === null) && src.generatedBy?.startsWith("cytoscape-") === true;
137
+ // a file graphty wrote: its stylesheet states graphty's styles, beneath which graphty draws its own defaults
138
+ const graphty = src.block !== null && parseStyleBlock(src.block, styleFingerprint(snapshot)).ok;
139
+ const read = cytoscapeJsStyles({
140
+ elements: withGeneratedIds(src.elements, src.generatedIds),
141
+ style: sheetless ? [] : (src.style ?? []),
142
+ snapshot,
143
+ toolDefaults: !graphty && !sheetless,
144
+ budget: context.budget,
145
+ signal: context.signal,
146
+ });
147
+ if (!("canonical" in read)) {
148
+ return read;
149
+ }
150
+ let tool: string | null = "cytoscape-js";
151
+ if (graphty) {
152
+ tool = null;
153
+ exactFromBlock(
154
+ snapshot,
155
+ src.block,
156
+ { node: (read.node?.values ?? {}) as Values, edge: (read.edge?.values ?? {}) as Values },
157
+ CYJS_WRITTEN,
158
+ cyjsHoldsExactly,
159
+ );
160
+ } else if (sheetless) {
161
+ tool = "cytoscape";
162
+ }
163
+ return { ...read, tool, block: src.block };
164
+ }
165
+
166
+ /**
167
+ * Cytoscape.js trims every text it draws: a label with blanks around it reads back trimmed.
168
+ * @param property - the text property
169
+ * @returns what a written value reads back as
170
+ */
171
+ const trimmed =
172
+ (property: string) =>
173
+ (want: (p: string) => StyleValue | null): StyleValue | null => {
174
+ const v = want(property);
175
+ return typeof v === "string" ? v.trim() : null;
176
+ };
177
+
178
+ /** The text properties Cytoscape.js trims. */
179
+ const CYJS_TEXT: Readonly<Record<StyleTarget, readonly string[]>> = {
180
+ node: ["label"],
181
+ edge: ["label", "sourceLabel", "targetLabel"],
182
+ };
183
+
184
+ const CYJS_WRITTEN: Written = {
185
+ node: Object.fromEntries(CYJS_TEXT.node.map((p) => [p, trimmed(p)])),
186
+ edge: {
187
+ ...Object.fromEntries(CYJS_TEXT.edge.map((p) => [p, trimmed(p)])),
188
+ // Cytoscape.js draws a loop curved whatever its curve style says
189
+ curved: (want, e, snapshot) =>
190
+ snapshot.edgeSource(e) === snapshot.edgeTarget(e) && want("curved") !== null ? true : want("curved"),
191
+ },
192
+ };
193
+
194
+ /**
195
+ * The elements with the ids graph-io gave the nodes that have none, so the engine matches them to the graph.
196
+ * @param elements - the elements as the file holds them
197
+ * @param ids - the generated ids
198
+ * @returns the elements, copied only where an id is added
199
+ */
200
+ function withGeneratedIds(elements: unknown, ids: ReadonlyMap<object, string> | undefined): unknown {
201
+ if (ids === undefined || ids.size === 0) {
202
+ return elements;
203
+ }
204
+ const patch = (e: unknown): unknown => {
205
+ const id = isJsonObject(e) ? ids.get(e) : undefined;
206
+ return id === undefined || !isJsonObject(e) ? e : { ...e, data: { ...(e.data as object), id } };
207
+ };
208
+ if (Array.isArray(elements)) {
209
+ return elements.map(patch);
210
+ }
211
+ if (isJsonObject(elements) && Array.isArray(elements.nodes)) {
212
+ return { ...elements, nodes: elements.nodes.map(patch) };
213
+ }
214
+ return elements;
215
+ }
216
+
217
+ /**
218
+ * NetworkX node-link: no style keys, NetworkX's drawing defaults, and an arrow at the target of every directed edge.
219
+ * @param snapshot - the graph
220
+ * @returns the styles
221
+ */
222
+ function networkxStyles(snapshot: GraphSnapshot): ImporterStyles {
223
+ return {
224
+ canonical: true,
225
+ dialect: null,
226
+ tool: "networkx",
227
+ units: "px",
228
+ node: { values: {} },
229
+ edge: directedArrows(snapshot),
230
+ };
231
+ }
232
+
233
+ /**
234
+ * The edge output marking directed edges with the tool's target arrow.
235
+ * @param snapshot - the graph
236
+ * @returns the edge output
237
+ */
238
+ function directedArrows(snapshot: GraphSnapshot): {
239
+ values: Values;
240
+ rules: StyleRule[];
241
+ columns: Map<string, StyleColumn>;
242
+ } {
243
+ if (!snapshot.directed || snapshot.edgeCount === 0) {
244
+ return { values: {}, rules: [], columns: new Map() };
245
+ }
246
+ const col = snapshot.edges.byRole("directed");
247
+ const flags = Array.from({ length: snapshot.edgeCount }, (_v, e) =>
248
+ col === null || plainCell(col.value(e)) !== false ? true : null,
249
+ );
250
+ return {
251
+ values: {},
252
+ rules: [
253
+ {
254
+ id: "edge.tool.arrow",
255
+ target: "edge",
256
+ role: "group",
257
+ origin: "tool",
258
+ where: { has: { style: "directed" } },
259
+ set: { targetArrow: "triangle" },
260
+ },
261
+ ],
262
+ columns: new Map([["directed", flags]]),
263
+ };
264
+ }
265
+
266
+ /**
267
+ * One element's values of a column, with a reader per value.
268
+ * @param snapshot - the graph
269
+ * @param target - nodes or edges
270
+ * @param name - the column
271
+ * @returns the cell reader, or null when the graph has no such column
272
+ */
273
+ function cells(snapshot: GraphSnapshot, target: StyleTarget, name: string): ((i: number) => unknown) | null {
274
+ const table = target === "node" ? snapshot.nodes : snapshot.edges;
275
+ return table.has(name) ? (i: number) => plainCell(table.value(name, i)) : null;
276
+ }
277
+
278
+ /**
279
+ * A values table that makes each property's column on first use.
280
+ * @param n - the element count
281
+ * @returns the values and a setter
282
+ */
283
+ function valuesOf(n: number): { values: Values; put: (p: string, i: number, v: StyleValue | null) => void } {
284
+ const values: Values = {};
285
+ return {
286
+ values,
287
+ put: (p, i, v) => {
288
+ if (v !== null) {
289
+ setValue(values, p, n, i, v);
290
+ }
291
+ },
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Sets one element's value of a property, making the property's column on first use.
297
+ * @param values - the values per property
298
+ * @param property - the property
299
+ * @param n - the element count
300
+ * @param i - the element
301
+ * @param v - the value; null clears it
302
+ */
303
+ function setValue(values: Values, property: string, n: number, i: number, v: StyleValue | null): void {
304
+ values[property] ??= new Array<StyleValue | null>(n).fill(null);
305
+ values[property][i] = v;
306
+ }
307
+
308
+ /**
309
+ * graphology: the keys sigma.js draws, with sigma's defaults.
310
+ * @param snapshot - the graph
311
+ * @param src - what the import kept
312
+ * @param context - the work budget
313
+ * @returns the styles
314
+ */
315
+ function graphologyStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context: StyleReadContext): ImporterStyles {
316
+ const notes = new StyleNotes();
317
+ const out: Record<StyleTarget, Values> = { node: {}, edge: {} };
318
+ const readFrom: Record<StyleTarget, string[]> = { node: [], edge: [] };
319
+ for (const target of TARGETS) {
320
+ const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
321
+ const { values, put } = valuesOf(n);
322
+ const keys: SigmaKeys = {
323
+ color: cells(snapshot, target, "color"),
324
+ size: cells(snapshot, target, "size"),
325
+ label: cells(snapshot, target, "label"),
326
+ hidden: cells(snapshot, target, "hidden"),
327
+ };
328
+ readFrom[target].push(...Object.entries(keys).flatMap(([name, c]) => (c === null ? [] : [name])));
329
+ for (let i = 0; i < n; i++) {
330
+ const site = (property: string): CanonSite => ({
331
+ notes,
332
+ target,
333
+ property,
334
+ element: i,
335
+ budget: context.budget,
336
+ });
337
+ readSigmaRow(keys, target, i, put, site);
338
+ }
339
+ out[target] = values;
340
+ }
341
+ exactFromBlock(snapshot, blockText(snapshot, src), out, GRAPHOLOGY_WRITTEN);
342
+ return {
343
+ canonical: true,
344
+ dialect: "graphology",
345
+ tool: "sigma",
346
+ opacityInColor: true,
347
+ units: "relative",
348
+ node: { values: out.node },
349
+ edge: { values: out.edge },
350
+ notes: notes.list(),
351
+ readFrom,
352
+ };
353
+ }
354
+
355
+ /** The cell readers of the keys sigma.js draws, null for a key the graph does not have. */
356
+ interface SigmaKeys {
357
+ readonly color: ((i: number) => unknown) | null;
358
+ readonly size: ((i: number) => unknown) | null;
359
+ readonly label: ((i: number) => unknown) | null;
360
+ readonly hidden: ((i: number) => unknown) | null;
361
+ }
362
+
363
+ /**
364
+ * Reads one element's sigma.js keys.
365
+ * @param keys - the cell readers
366
+ * @param target - nodes or edges
367
+ * @param i - the element
368
+ * @param put - the setter
369
+ * @param site - where a value of a property was read
370
+ */
371
+ function readSigmaRow(
372
+ keys: SigmaKeys,
373
+ target: StyleTarget,
374
+ i: number,
375
+ put: (p: string, i: number, v: StyleValue | null) => void,
376
+ site: (property: string) => CanonSite,
377
+ ): void {
378
+ const { color, size, label, hidden } = keys;
379
+ if (color !== null) {
380
+ const [fill, opacity] = target === "node" ? ["fill", "fillOpacity"] : ["color", "opacity"];
381
+ putColor(put, fill, opacity, i, color(i), site(fill));
382
+ }
383
+ if (size !== null) {
384
+ const v = canonNumber(size(i), site("width"));
385
+ put("width", i, v);
386
+ if (target === "node") {
387
+ put("height", i, v);
388
+ }
389
+ }
390
+ if (label !== null) {
391
+ put("label", i, canonText(label(i), site("label")));
392
+ }
393
+ if (hidden !== null && canonBoolean(hidden(i), site("visible")) === true) {
394
+ put("visible", i, false);
395
+ }
396
+ }
397
+
398
+ /**
399
+ * A CSS color into a property and its opacity.
400
+ * @param put - the setter
401
+ * @param property - the color property
402
+ * @param opacity - its opacity property, or null when it has none
403
+ * @param i - the element
404
+ * @param raw - the value
405
+ * @param site - where it was read
406
+ */
407
+ function putColor(
408
+ put: (p: string, i: number, v: StyleValue | null) => void,
409
+ property: string,
410
+ opacity: string | null,
411
+ i: number,
412
+ raw: unknown,
413
+ site: CanonSite,
414
+ ): void {
415
+ if (raw === null || raw === undefined || raw === "") {
416
+ return;
417
+ }
418
+ const c = canonColor(raw, "css", site);
419
+ if (c === null) {
420
+ return;
421
+ }
422
+ put(property, i, c.color);
423
+ if (c.alpha < 1) {
424
+ if (opacity === null) {
425
+ site.notes.add(STYLE_ISSUE.ALPHA_DROPPED, site.target, property, { sample: raw, element: i });
426
+ } else {
427
+ put(opacity, i, c.alpha);
428
+ }
429
+ }
430
+ }
431
+
432
+ /**
433
+ * The text of the block a document holds, as a top-level key or as a graph attribute, or null.
434
+ * @param snapshot - the graph
435
+ * @param src - what the import kept
436
+ * @returns the block's text
437
+ */
438
+ function blockText(snapshot: GraphSnapshot, src: JsonStyleSource): string | null {
439
+ if (src.block !== null) {
440
+ return src.block;
441
+ }
442
+ if (!snapshot.graph.has(STYLE_BLOCK_NAME)) {
443
+ return null;
444
+ }
445
+ const v = plainCell(snapshot.graph.value(STYLE_BLOCK_NAME, 0));
446
+ return typeof v === "string" ? v : null;
447
+ }
448
+
449
+ /** What a writer writes for a value that its dialect holds only approximately, per target and property. */
450
+ type Written = Partial<
451
+ Record<
452
+ StyleTarget,
453
+ Record<
454
+ string,
455
+ (want: (p: string) => StyleValue | null, i: number, snapshot: GraphSnapshot) => StyleValue | null
456
+ >
457
+ >
458
+ >;
459
+
460
+ /**
461
+ * For a file graphty wrote: where a native value is what the writer writes for the block's value (a width and a
462
+ * height written as one size, a shape written as the nearest one the dialect has), the block's exact value is the
463
+ * truth, so a re-import gives the same rules. A value another program changed stays the truth.
464
+ * @param snapshot - the graph
465
+ * @param text - the block's text, or null
466
+ * @param values - the native values per target, changed in place
467
+ * @param written - what the writer writes for each approximated property
468
+ * @param holdsExactly - whether the dialect holds a value as it is; where it does not, the native value tells
469
+ * nothing about the block's (the writer never wrote it), so the block's value is the truth
470
+ */
471
+ function exactFromBlock(
472
+ snapshot: GraphSnapshot,
473
+ text: string | null,
474
+ values: Record<StyleTarget, Values>,
475
+ written: Written,
476
+ holdsExactly?: (target: StyleTarget, property: string, value: StyleValue) => boolean,
477
+ ): void {
478
+ if (text === null) {
479
+ return;
480
+ }
481
+ const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
482
+ const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "json") : null;
483
+ if (ex === null) {
484
+ return;
485
+ }
486
+ for (const target of TARGETS) {
487
+ const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
488
+ const cache = new Map<string, readonly (StyleValue | null)[]>();
489
+ const resolved = (p: string): readonly (StyleValue | null)[] => {
490
+ let r = cache.get(p);
491
+ if (r === undefined) {
492
+ r = resolveStyles(ex, target, p as never, snapshot);
493
+ cache.set(p, r);
494
+ }
495
+ return r;
496
+ };
497
+ if (holdsExactly !== undefined) {
498
+ const properties = new Set(
499
+ ex.styles.rules
500
+ .filter((r) => r.target === target)
501
+ .flatMap((r) => [...Object.keys(r.set ?? {}), ...Object.keys(r.map ?? {})]),
502
+ );
503
+ for (const property of properties) {
504
+ const held = (w: StyleValue): boolean => holdsExactly(target, property, w);
505
+ takeUnheld(values[target], property, resolved(property), n, held);
506
+ }
507
+ }
508
+ for (const [property, write] of Object.entries(written[target] ?? {})) {
509
+ takeWritten(values[target], property, n, resolved, write, snapshot);
510
+ }
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Takes the block's value of a property wherever the native value is what the writer writes for the block's values.
516
+ * @param column - the native values of the target, changed in place
517
+ * @param property - the property
518
+ * @param n - the element count
519
+ * @param resolved - the block's values of a property
520
+ * @param write - what the writer writes for an element, from the block's values
521
+ * @param snapshot - the graph
522
+ */
523
+ function takeWritten(
524
+ column: Values,
525
+ property: string,
526
+ n: number,
527
+ resolved: (p: string) => readonly (StyleValue | null)[],
528
+ write: (want: (p: string) => StyleValue | null, i: number, snapshot: GraphSnapshot) => StyleValue | null,
529
+ snapshot: GraphSnapshot,
530
+ ): void {
531
+ const want = resolved(property);
532
+ for (let i = 0; i < n; i++) {
533
+ const w = want[i];
534
+ const native = column[property]?.[i] ?? null;
535
+ // a native value the writer made from the block's other values (vis.js's one size for a width) is the
536
+ // block's own value, none included
537
+ if (native !== w && native !== null && native === write((p) => resolved(p)[i], i, snapshot)) {
538
+ setValue(column, property, n, i, w);
539
+ }
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Takes the block's value of a property wherever the dialect does not hold it exactly.
545
+ * @param values - the native values of the target, changed in place
546
+ * @param property - the property
547
+ * @param want - the block's values
548
+ * @param n - the element count
549
+ * @param held - whether the dialect holds a value exactly
550
+ */
551
+ function takeUnheld(
552
+ values: Values,
553
+ property: string,
554
+ want: readonly (StyleValue | null)[],
555
+ n: number,
556
+ held: (w: StyleValue) => boolean,
557
+ ): void {
558
+ for (let i = 0; i < n; i++) {
559
+ const w = want[i];
560
+ if (w !== null && !held(w)) {
561
+ setValue(values, property, n, i, w);
562
+ }
563
+ }
564
+ }
565
+
566
+ /**
567
+ * One size for a width and a height: the larger, as graphology and vis.js write it.
568
+ * @param want - the block's values of the element
569
+ * @returns the size
570
+ */
571
+ const oneSize = (want: (p: string) => StyleValue | null): StyleValue | null => {
572
+ const w = want("width");
573
+ const h = want("height");
574
+ return typeof w === "number" && typeof h === "number" ? Math.max(w, h) : (w ?? h);
575
+ };
576
+
577
+ const GRAPHOLOGY_WRITTEN: Written = { node: { width: oneSize, height: oneSize } };
578
+
579
+ // ------------------------------------------------------------ vis.js
580
+
581
+ /** vis-network's node shapes that take their size from `size` (the others are sized by their label). */
582
+ const VIS_SIZED: ReadonlySet<string> = new Set([
583
+ "image",
584
+ "circularImage",
585
+ "diamond",
586
+ "dot",
587
+ "star",
588
+ "triangle",
589
+ "triangleDown",
590
+ "hexagon",
591
+ "square",
592
+ "icon",
593
+ ]);
594
+
595
+ /**
596
+ * vis-network's palette for a group the file does not define, in the order groups first appear (vis-network
597
+ * 10.1.2, `Groups._defaultGroups`): border, background.
598
+ */
599
+ const VIS_GROUP_PALETTE: readonly (readonly [string, string])[] = [
600
+ ["#2B7CE9", "#97C2FC"],
601
+ ["#FFA500", "#FFFF00"],
602
+ ["#FA0A10", "#FB7E81"],
603
+ ["#41A906", "#7BE141"],
604
+ ["#E129F0", "#EB7DF4"],
605
+ ["#7C29F0", "#AD85E4"],
606
+ ["#C37F00", "#FFA807"],
607
+ ["#4220FB", "#6E6EFD"],
608
+ ["#FD5A77", "#FFC0CB"],
609
+ ["#4AD63A", "#C2FABC"],
610
+ ["#990000", "#EE0000"],
611
+ ["#FF6000", "#FF6000"],
612
+ ["#97C2FC", "#2B7CE9"],
613
+ ["#399605", "#255C03"],
614
+ ["#B70054", "#FF007E"],
615
+ ["#AD85E4", "#7C29F0"],
616
+ ["#4557FA", "#000EA1"],
617
+ ["#FFC0CB", "#FD5A77"],
618
+ ["#C2FABC", "#74D66A"],
619
+ ["#EE0000", "#990000"],
620
+ ];
621
+
622
+ /** vis-network's edge color when an edge inherits none (vis-network 10.1.2, `color.color`). */
623
+ const VIS_EDGE_COLOR = "#848484";
624
+
625
+ /**
626
+ * vis-network's reading of a node color given as text (vis-util `parseColor`): a hex or `rgb()` color gets a border
627
+ * 20% darker and 25% more saturated; any other text is both.
628
+ * @param text - the color
629
+ * @returns the background and the border
630
+ */
631
+ function visParseColor(text: string): { background: string; border: string } {
632
+ let hex = text;
633
+ const rgb = /^rgb\( *(\d{1,3}) *, *(\d{1,3}) *, *(\d{1,3}) *\)$/i.exec(text);
634
+ if (rgb?.slice(1).every((c) => Number(c) <= 255)) {
635
+ hex = `#${((1 << 24) + (Number(rgb[1]) << 16) + (Number(rgb[2]) << 8) + Number(rgb[3])).toString(16).slice(1)}`;
636
+ }
637
+ if (!/^#([0-9a-f]{6}|[0-9a-f]{3})$/i.test(hex)) {
638
+ return { background: text, border: text };
639
+ }
640
+ const full = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
641
+ return { background: full, border: visBorder(full) };
642
+ }
643
+
644
+ /**
645
+ * vis-util's border of a background color: 20% darker and 25% more saturated, through HSV.
646
+ * @param full - the background, `#rrggbb`
647
+ * @returns the border, `#rrggbb`
648
+ */
649
+ function visBorder(full: string): string {
650
+ const n = Number.parseInt(full.slice(1), 16);
651
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => c / 255);
652
+ const max = Math.max(r, g, b);
653
+ const min = Math.min(r, g, b);
654
+ let h = 0;
655
+ let s = 0;
656
+ if (max !== min) {
657
+ let d = b - r;
658
+ let k = 5;
659
+ if (r === min) {
660
+ d = g - b;
661
+ k = 3;
662
+ } else if (b === min) {
663
+ d = r - g;
664
+ k = 1;
665
+ }
666
+ h = (60 * (k - d / (max - min))) / 360;
667
+ s = (max - min) / max;
668
+ }
669
+ const sv = Math.min(1, s * 1.25);
670
+ const vv = max * 0.8;
671
+ const i = Math.floor(h * 6);
672
+ const f = h * 6 - i;
673
+ const p = vv * (1 - sv);
674
+ const q = vv * (1 - f * sv);
675
+ const t = vv * (1 - (1 - f) * sv);
676
+ const rgbOut = [
677
+ [vv, t, p],
678
+ [q, vv, p],
679
+ [p, vv, t],
680
+ [p, q, vv],
681
+ [t, p, vv],
682
+ [vv, p, q],
683
+ ][i % 6].map((c) => Math.floor(c * 255));
684
+ return `#${((1 << 24) + (rgbOut[0] << 16) + (rgbOut[1] << 8) + rgbOut[2]).toString(16).slice(1)}`;
685
+ }
686
+
687
+ /** One layer of vis.js options for an element: the file's raw values, before canonicalization. */
688
+ interface VisLayer {
689
+ fill?: unknown;
690
+ border?: unknown;
691
+ shape?: unknown;
692
+ size?: unknown;
693
+ borderWidth?: unknown;
694
+ opacity?: unknown;
695
+ hidden?: unknown;
696
+ label?: unknown;
697
+ title?: unknown;
698
+ fontColor?: unknown;
699
+ fontSize?: unknown;
700
+ fontFace?: unknown;
701
+ width?: unknown;
702
+ dashes?: unknown;
703
+ arrows?: unknown;
704
+ edgeColor?: unknown;
705
+ edgeOpacity?: unknown;
706
+ inherit?: unknown;
707
+ }
708
+
709
+ /**
710
+ * The vis.js options of one node or edge record (or of `options.nodes`, `options.edges`, a group) as a layer.
711
+ * @param o - the options
712
+ * @param target - nodes or edges
713
+ * @returns the layer
714
+ */
715
+ function visLayer(o: unknown, target: StyleTarget): VisLayer {
716
+ if (!isJsonObject(o)) {
717
+ return {};
718
+ }
719
+ const layer: VisLayer = {};
720
+ const set: SetLayer = (k, v) => {
721
+ if (v !== undefined && v !== null) {
722
+ layer[k] = v;
723
+ }
724
+ };
725
+ if (target === "node") {
726
+ visNodeLayer(o, set);
727
+ } else {
728
+ visEdgeLayer(o, set);
729
+ }
730
+ set("hidden", o.hidden);
731
+ set("label", o.label);
732
+ set("title", o.title);
733
+ visFontLayer(o.font, set);
734
+ return layer;
735
+ }
736
+
737
+ /** Sets a key of a vis.js layer when the value is neither undefined nor null. */
738
+ type SetLayer = (k: keyof VisLayer, v: unknown) => void;
739
+
740
+ /**
741
+ * The node keys of a vis.js options object: a color given as text has vis-util's darker border.
742
+ * @param o - the options
743
+ * @param set - the layer's setter
744
+ */
745
+ function visNodeLayer(o: Record<string, unknown>, set: SetLayer): void {
746
+ const { color } = o;
747
+ if (typeof color === "string") {
748
+ const parsed = visParseColor(color.trim());
749
+ set("fill", parsed.background);
750
+ set("border", parsed.border);
751
+ } else if (isJsonObject(color)) {
752
+ set("fill", color.background);
753
+ set("border", color.border);
754
+ }
755
+ set("shape", o.shape);
756
+ set("size", o.size);
757
+ set("borderWidth", o.borderWidth);
758
+ set("opacity", o.opacity);
759
+ }
760
+
761
+ /**
762
+ * The edge keys of a vis.js options object: a color of its own stops the edge inheriting its node's.
763
+ * @param o - the options
764
+ * @param set - the layer's setter
765
+ */
766
+ function visEdgeLayer(o: Record<string, unknown>, set: SetLayer): void {
767
+ const { color } = o;
768
+ if (typeof color === "string") {
769
+ set("edgeColor", color);
770
+ set("inherit", false);
771
+ } else if (isJsonObject(color)) {
772
+ set("edgeColor", color.color);
773
+ set("edgeOpacity", color.opacity);
774
+ const own = color.color !== undefined || color.highlight !== undefined || color.hover !== undefined;
775
+ set("inherit", own ? false : color.inherit);
776
+ }
777
+ set("width", o.width);
778
+ set("dashes", o.dashes);
779
+ set("arrows", o.arrows);
780
+ }
781
+
782
+ /**
783
+ * The font keys of a vis.js options object: an object, or text like `"14px arial red"`.
784
+ * @param font - the `font` value
785
+ * @param set - the layer's setter
786
+ */
787
+ function visFontLayer(font: unknown, set: SetLayer): void {
788
+ if (isJsonObject(font)) {
789
+ set("fontColor", font.color);
790
+ set("fontSize", font.size);
791
+ set("fontFace", font.face);
792
+ } else if (typeof font === "string") {
793
+ const m = /^\s*(\d+(?:\.\d+)?)(?:px)?\s+(\S+)(?:\s+(\S+))?/.exec(font.slice(0, 256));
794
+ if (m !== null) {
795
+ set("fontSize", Number(m[1]));
796
+ set("fontFace", m[2]);
797
+ set("fontColor", m[3]);
798
+ }
799
+ }
800
+ }
801
+
802
+ /**
803
+ * The canonical values of one vis.js layer.
804
+ * @param layer - the layer
805
+ * @param target - nodes or edges
806
+ * @param site - where its values are read
807
+ * @param sized - nodes: whether the shape takes its size from `size`
808
+ * @returns the values
809
+ */
810
+ function visValues(layer: VisLayer, target: StyleTarget, site: (property: string) => CanonSite, sized: boolean): Row {
811
+ const out: Row = {};
812
+ const put: PutRow = (p, v) => {
813
+ if (v !== null) {
814
+ out[p] = v;
815
+ }
816
+ };
817
+ const color = (p: string, opacity: string | null, raw: unknown): void => {
818
+ if (raw === undefined || raw === null || raw === "") {
819
+ return;
820
+ }
821
+ const c = canonColor(raw, "vis", site(p));
822
+ if (c !== null) {
823
+ put(p, c.color);
824
+ if (c.alpha < 1 && opacity !== null) {
825
+ put(opacity, c.alpha);
826
+ }
827
+ }
828
+ };
829
+ if (target === "node") {
830
+ color("fill", "fillOpacity", layer.fill);
831
+ color("borderColor", null, layer.border);
832
+ visNodeValues(layer, sized, put, site);
833
+ } else {
834
+ color("color", "opacity", layer.edgeColor);
835
+ visEdgeValues(layer, put, site);
836
+ }
837
+ if (layer.hidden !== undefined && canonBoolean(layer.hidden, site("visible")) === true) {
838
+ put("visible", false);
839
+ }
840
+ if (layer.label !== undefined) {
841
+ put("label", canonText(layer.label, site("label")));
842
+ }
843
+ if (layer.title !== undefined) {
844
+ put("tooltip", canonText(layer.title, site("tooltip")));
845
+ }
846
+ color("labelColor", null, layer.fontColor);
847
+ if (layer.fontSize !== undefined) {
848
+ put("labelSize", canonNumber(layer.fontSize, site("labelSize")));
849
+ }
850
+ if (layer.fontFace !== undefined) {
851
+ put("labelFont", canonText(layer.fontFace, site("labelFont")));
852
+ }
853
+ return out;
854
+ }
855
+
856
+ /** Sets a value of a row when it is not null. */
857
+ type PutRow = (p: string, v: StyleValue | null) => void;
858
+
859
+ /**
860
+ * The node values of a vis.js layer besides its colors.
861
+ * @param layer - the layer
862
+ * @param sized - whether the shape takes its size from `size`
863
+ * @param put - the row's setter
864
+ * @param site - where its values are read
865
+ */
866
+ function visNodeValues(layer: VisLayer, sized: boolean, put: PutRow, site: (property: string) => CanonSite): void {
867
+ if (layer.shape !== undefined) {
868
+ put("shape", canonEnum("shape", layer.shape, "vis", site("shape")));
869
+ }
870
+ if (sized && layer.size !== undefined) {
871
+ const v = canonNumber(layer.size, site("width"));
872
+ put("width", v);
873
+ put("height", v);
874
+ }
875
+ if (layer.borderWidth !== undefined) {
876
+ put("borderWidth", canonNumber(layer.borderWidth, site("borderWidth")));
877
+ }
878
+ if (layer.opacity !== undefined) {
879
+ put("opacity", canonOpacity(layer.opacity, "vis", site("opacity")));
880
+ }
881
+ }
882
+
883
+ /**
884
+ * The edge values of a vis.js layer besides its color.
885
+ * @param layer - the layer
886
+ * @param put - the row's setter
887
+ * @param site - where its values are read
888
+ */
889
+ function visEdgeValues(layer: VisLayer, put: PutRow, site: (property: string) => CanonSite): void {
890
+ if (layer.edgeOpacity !== undefined) {
891
+ put("opacity", canonOpacity(layer.edgeOpacity, "vis", site("opacity")));
892
+ }
893
+ if (layer.width !== undefined) {
894
+ put("width", canonNumber(layer.width, site("width")));
895
+ }
896
+ if (layer.dashes !== undefined) {
897
+ put("lineStyle", layer.dashes === false || layer.dashes === "false" ? "solid" : "dashed");
898
+ }
899
+ if (layer.arrows !== undefined) {
900
+ const [to, from] = visArrows(layer.arrows, site);
901
+ put("targetArrow", to);
902
+ put("sourceArrow", from);
903
+ }
904
+ }
905
+
906
+ /**
907
+ * vis.js `arrows` as the target and source arrows: `"to"`, `"to, from"`, or `{ to: true | { enabled, type } }`.
908
+ * @param arrows - the value
909
+ * @param site - where it is read
910
+ * @returns the target arrow and the source arrow
911
+ */
912
+ function visArrows(arrows: unknown, site: (property: string) => CanonSite): [string | null, string | null] {
913
+ const end = (name: string, property: string): string | null => {
914
+ if (typeof arrows === "string") {
915
+ return new RegExp(String.raw`\b${name}\b`).test(arrows.slice(0, 256)) ? "triangle" : "none";
916
+ }
917
+ if (!isJsonObject(arrows)) {
918
+ return null;
919
+ }
920
+ const a = arrows[name];
921
+ if (a === true) {
922
+ return "triangle";
923
+ }
924
+ if (!isJsonObject(a) || a.enabled === false) {
925
+ return "none";
926
+ }
927
+ return canonEnum("arrow", a.type ?? "arrow", "vis", site(property));
928
+ };
929
+ return [end("to", "targetArrow"), end("from", "sourceArrow")];
930
+ }
931
+
932
+ /**
933
+ * vis.js: the per-element keys over the file's groups and `options.nodes` / `options.edges`, beneath them
934
+ * vis-network's defaults. The defaults and groups the file declares become declared rules; a group the file does not
935
+ * define gets vis-network's palette, as a rule of the tool's; an edge without a color of its own takes its source
936
+ * node's border color (its target's under `inherit: "to"`).
937
+ * @param snapshot - the graph
938
+ * @param src - what the import kept
939
+ * @param context - the work budget
940
+ * @returns the styles
941
+ */
942
+ function visStyles(snapshot: GraphSnapshot, src: JsonStyleSource, context: StyleReadContext): ImporterStyles {
943
+ const notes = new StyleNotes();
944
+ const options = isJsonObject(src.options) ? src.options : {};
945
+ const n = snapshot.nodeCount;
946
+ const ruleSite =
947
+ (target: StyleTarget) =>
948
+ (property: string): CanonSite => ({ notes, target, property });
949
+ const nodeRules: StyleRule[] = [];
950
+ const nodeColumns = new Map<string, StyleColumn>();
951
+
952
+ // the node defaults and groups the file declares
953
+ const nodeDefaults = visLayer(options.nodes, "node");
954
+ const groupCol = cells(snapshot, "node", "group");
955
+ const groupOf = (i: number): string | null => {
956
+ const g = groupCol?.(i);
957
+ return typeof g === "string" || typeof g === "number" ? String(g) : null;
958
+ };
959
+ const { groupLayers, paletteRows } = visGroupLayers(n, groupOf, visGroupDefs(src, options));
960
+ context.budget.spend("node", n);
961
+
962
+ // what vis-network draws per node: its own keys over its group over options.nodes
963
+ const node = valuesOf(n);
964
+ const records = nodeRecords(snapshot);
965
+ const rawShape = (layer: VisLayer): string | null => (typeof layer.shape === "string" ? layer.shape : null);
966
+ for (let i = 0; i < n; i++) {
967
+ const g = groupOf(i);
968
+ const own = records(i);
969
+ const group = g === null ? {} : (groupLayers.get(g) ?? {});
970
+ const shape = rawShape(own) ?? rawShape(group) ?? rawShape(nodeDefaults) ?? "ellipse";
971
+ const site = (property: string): CanonSite => ({
972
+ notes,
973
+ target: "node",
974
+ property,
975
+ element: i,
976
+ budget: context.budget,
977
+ });
978
+ const merged = { ...nodeDefaults, ...group, ...own };
979
+ for (const [p, v] of Object.entries(visValues(merged, "node", site, VIS_SIZED.has(shape)))) {
980
+ node.put(p, i, v);
981
+ }
982
+ }
983
+ const declaredNode = visValues(nodeDefaults, "node", ruleSite("node"), true);
984
+ if (Object.keys(declaredNode).length > 0) {
985
+ nodeRules.push(rule("node.vis.defaults", "node", "defaults", "declared", { all: true }, declaredNode));
986
+ }
987
+ let k = 0;
988
+ for (const [g, layer] of groupLayers) {
989
+ const set = paletteRows.get(g) ?? visValues(layer, "node", ruleSite("node"), true);
990
+ if (Object.keys(set).length === 0) {
991
+ continue;
992
+ }
993
+ const key = `group.${String(k)}`;
994
+ nodeColumns.set(
995
+ key,
996
+ Array.from({ length: n }, (_v, i) => (groupOf(i) === g ? true : null)),
997
+ );
998
+ const origin = paletteRows.has(g) ? "tool" : "declared";
999
+ const r = rule(`node.vis.group.${String(k++)}`, "node", "group", origin, { has: { style: key } }, set);
1000
+ nodeRules.push({ ...r, facts: { names: [[key, g]] } });
1001
+ }
1002
+
1003
+ const toolNode = toolDefaults("vis-network")?.node;
1004
+ const borderOf = (i: number): StyleValue =>
1005
+ node.values.borderColor?.[i] ?? (toolNode?.borderColor?.value as StyleValue | undefined) ?? "#2b7ce9";
1006
+ const edge = visEdges(snapshot, visLayer(options.edges, "edge"), borderOf, notes, context);
1007
+ const values = { node: node.values, edge: edge.values };
1008
+ exactFromBlock(snapshot, src.block, values, VIS_WRITTEN);
1009
+ return {
1010
+ canonical: true,
1011
+ dialect: "vis",
1012
+ tool: "vis-network",
1013
+ opacityInColor: true,
1014
+ units: "px",
1015
+ node: { values: values.node, rules: nodeRules, columns: nodeColumns },
1016
+ edge: { values: values.edge, rules: edge.rules, columns: edge.columns },
1017
+ notes: notes.list(),
1018
+ block: src.block,
1019
+ readFrom: {
1020
+ node: VIS_KEYS.node.filter((x) => snapshot.nodes.has(x)),
1021
+ edge: VIS_KEYS.edge.filter((x) => snapshot.edges.has(x)),
1022
+ },
1023
+ };
1024
+ }
1025
+
1026
+ /**
1027
+ * vis.js: the group definitions, from `options.groups` or a top-level `groups`.
1028
+ * @param src - what the import kept
1029
+ * @param options - the file's `options`
1030
+ * @returns the definitions by group name
1031
+ */
1032
+ function visGroupDefs(src: JsonStyleSource, options: Record<string, unknown>): Record<string, unknown> {
1033
+ if (isJsonObject(options.groups)) {
1034
+ return options.groups;
1035
+ }
1036
+ return isJsonObject(src.groups) ? src.groups : {};
1037
+ }
1038
+
1039
+ /**
1040
+ * vis.js: each group the nodes name, in the order first named, with its layer; a group the file does not define gets
1041
+ * vis-network's palette.
1042
+ * @param n - the node count
1043
+ * @param groupOf - each node's group
1044
+ * @param groupDefs - the group definitions
1045
+ * @returns the layers by group, and the palette values of the groups that took them
1046
+ */
1047
+ function visGroupLayers(
1048
+ n: number,
1049
+ groupOf: (i: number) => string | null,
1050
+ groupDefs: Record<string, unknown>,
1051
+ ): { groupLayers: Map<string, VisLayer>; paletteRows: Map<string, Row> } {
1052
+ const groupLayers = new Map<string, VisLayer>();
1053
+ const paletteRows = new Map<string, Row>();
1054
+ let palette = 0;
1055
+ for (let i = 0; i < n; i++) {
1056
+ const g = groupOf(i);
1057
+ if (g === null || groupLayers.has(g)) {
1058
+ continue;
1059
+ }
1060
+ if (Object.hasOwn(groupDefs, g)) {
1061
+ groupLayers.set(g, visLayer(groupDefs[g], "node"));
1062
+ } else {
1063
+ const [border, background] = VIS_GROUP_PALETTE[palette++ % VIS_GROUP_PALETTE.length];
1064
+ groupLayers.set(g, { fill: background, border });
1065
+ paletteRows.set(g, { fill: background.toLowerCase(), borderColor: border.toLowerCase() });
1066
+ }
1067
+ }
1068
+ return { groupLayers, paletteRows };
1069
+ }
1070
+
1071
+ /**
1072
+ * vis.js edges: their own keys over `options.edges`; an edge without a color of its own takes its source node's
1073
+ * border color (its target's under `inherit: "to"`).
1074
+ * @param snapshot - the graph
1075
+ * @param edgeDefaults - `options.edges`
1076
+ * @param borderOf - each node's drawn border color
1077
+ * @param notes - where problems are noted
1078
+ * @param context - the work budget
1079
+ * @returns the edge values, rules and style columns
1080
+ */
1081
+ function visEdges(
1082
+ snapshot: GraphSnapshot,
1083
+ edgeDefaults: VisLayer,
1084
+ borderOf: (node: number) => StyleValue,
1085
+ notes: StyleNotes,
1086
+ context: StyleReadContext,
1087
+ ): { values: Values; rules: StyleRule[]; columns: Map<string, StyleColumn> } {
1088
+ const m = snapshot.edgeCount;
1089
+ const edge = valuesOf(m);
1090
+ const edgeRecords = elementRecords(snapshot, "edge");
1091
+ const inherited: (StyleValue | null)[] = new Array<StyleValue | null>(m).fill(null);
1092
+ let inheriting = 0;
1093
+ for (let e = 0; e < m; e++) {
1094
+ const own = edgeRecords(e);
1095
+ const merged = { ...edgeDefaults, ...own };
1096
+ const site = (property: string): CanonSite => ({
1097
+ notes,
1098
+ target: "edge",
1099
+ property,
1100
+ element: e,
1101
+ budget: context.budget,
1102
+ });
1103
+ const vals = visValues(merged, "edge", site, true);
1104
+ for (const [p, v] of Object.entries(vals)) {
1105
+ edge.put(p, e, v);
1106
+ }
1107
+ const inherit = own.inherit ?? edgeDefaults.inherit ?? "from";
1108
+ if (inherit === false || inherit === "false") {
1109
+ if (vals.color === undefined) {
1110
+ edge.put("color", e, VIS_EDGE_COLOR);
1111
+ }
1112
+ continue;
1113
+ }
1114
+ if (inherit === "both") {
1115
+ notes.add(STYLE_ISSUE.GRADIENT, "edge", "color", { sample: "inherit both", element: e });
1116
+ }
1117
+ inherited[e] = borderOf(inherit === "to" ? snapshot.edgeTarget(e) : snapshot.edgeSource(e));
1118
+ inheriting++;
1119
+ }
1120
+ context.budget.spend("edge", m);
1121
+ const edgeRules: StyleRule[] = [];
1122
+ const edgeColumns = new Map<string, StyleColumn>();
1123
+ const declaredEdge = visValues(edgeDefaults, "edge", (property) => ({ notes, target: "edge", property }), true);
1124
+ if (Object.keys(declaredEdge).length > 0) {
1125
+ edgeRules.push(rule("edge.vis.defaults", "edge", "defaults", "declared", { all: true }, declaredEdge));
1126
+ }
1127
+ if (inheriting > 0) {
1128
+ // vis-network draws such an edge in its source node's color. graphty-element cannot bind an edge's color to
1129
+ // its source node's drawn color yet, so the color is frozen here; replace this column with that binding once
1130
+ // the element has it (tracking issue "graphty-element: bind an edge's color to its source node's drawn color").
1131
+ edgeColumns.set("source-color", inherited);
1132
+ edgeRules.push({
1133
+ id: "edge.tool.source-color",
1134
+ target: "edge",
1135
+ role: "passthrough",
1136
+ origin: "tool",
1137
+ where: { has: { style: "source-color" } },
1138
+ map: { color: { kind: "passthrough", by: { style: "source-color" } } },
1139
+ facts: { elements: inheriting, reason: "SOURCE_NODE_COLOR" },
1140
+ });
1141
+ }
1142
+ return { values: edge.values, rules: edgeRules, columns: edgeColumns };
1143
+ }
1144
+
1145
+ /** The vis.js keys the reader reads, per target. */
1146
+ const VIS_KEYS: Readonly<Record<StyleTarget, readonly string[]>> = {
1147
+ node: ["color", "shape", "size", "borderWidth", "opacity", "hidden", "label", "title", "font"],
1148
+ edge: ["color", "width", "dashes", "arrows", "hidden", "label", "title", "font"],
1149
+ };
1150
+
1151
+ /**
1152
+ * Each node's own vis.js keys as a layer.
1153
+ * @param snapshot - the graph
1154
+ * @returns the layer of a node
1155
+ */
1156
+ function nodeRecords(snapshot: GraphSnapshot): (i: number) => VisLayer {
1157
+ return elementRecords(snapshot, "node");
1158
+ }
1159
+
1160
+ /**
1161
+ * Each element's own vis.js keys as a layer, read from the graph's columns.
1162
+ * @param snapshot - the graph
1163
+ * @param target - nodes or edges
1164
+ * @returns the layer of an element
1165
+ */
1166
+ function elementRecords(snapshot: GraphSnapshot, target: StyleTarget): (i: number) => VisLayer {
1167
+ const readers = VIS_KEYS[target]
1168
+ .map((name) => [name, cells(snapshot, target, name)] as const)
1169
+ .filter((x): x is readonly [string, (i: number) => unknown] => x[1] !== null);
1170
+ return (i) => {
1171
+ const record: Record<string, unknown> = {};
1172
+ for (const [name, read] of readers) {
1173
+ const v = read(i);
1174
+ if (v !== null) {
1175
+ record[name] = v;
1176
+ }
1177
+ }
1178
+ return visLayer(record, target);
1179
+ };
1180
+ }
1181
+
1182
+ /**
1183
+ * A rule.
1184
+ * @param id - its id
1185
+ * @param target - nodes or edges
1186
+ * @param role - what it is
1187
+ * @param origin - where it came from
1188
+ * @param where - which elements
1189
+ * @param set - the values
1190
+ * @returns the rule
1191
+ */
1192
+ function rule(
1193
+ id: string,
1194
+ target: StyleTarget,
1195
+ role: StyleRule["role"],
1196
+ origin: StyleRule["origin"],
1197
+ where: StyleRule["where"],
1198
+ set: Row,
1199
+ ): StyleRule {
1200
+ const values: Record<string, StyleValue> = {};
1201
+ for (const [p, v] of Object.entries(set)) {
1202
+ if (v !== null) {
1203
+ values[p] = v;
1204
+ }
1205
+ }
1206
+ return { id, target, role, origin, where, set: values };
1207
+ }
1208
+
1209
+ // ============================================================ writing
1210
+
1211
+ /** The neutral node shape to vis-network's shape; the boolean says whether it is exact. */
1212
+ const VIS_SHAPES: ReadonlyMap<string, readonly [string, boolean]> = new Map([
1213
+ ["ellipse", ["dot", true]],
1214
+ ["rectangle", ["square", true]],
1215
+ ["round-rectangle", ["box", true]],
1216
+ ["triangle", ["triangle", true]],
1217
+ ["diamond", ["diamond", true]],
1218
+ ["star", ["star", true]],
1219
+ ["hexagon", ["hexagon", true]],
1220
+ ["none", ["text", true]],
1221
+ ["pentagon", ["hexagon", false]],
1222
+ ["octagon", ["hexagon", false]],
1223
+ ["parallelogram", ["box", false]],
1224
+ ["trapezoid", ["box", false]],
1225
+ ["vee", ["triangle", false]],
1226
+ ]);
1227
+
1228
+ /** The neutral arrow to vis-network's arrow type; the boolean says whether it is exact. */
1229
+ const VIS_ARROW_TYPES: ReadonlyMap<string, readonly [string, boolean]> = new Map([
1230
+ ["triangle", ["arrow", true]],
1231
+ ["tee", ["bar", true]],
1232
+ ["circle", ["circle", true]],
1233
+ ["square", ["box", true]],
1234
+ ["crow", ["crow", true]],
1235
+ ["diamond", ["diamond", true]],
1236
+ ["vee", ["vee", true]],
1237
+ ["inverted", ["inv_triangle", true]],
1238
+ ["triangle-open", ["arrow", false]],
1239
+ ["diamond-open", ["diamond", false]],
1240
+ ["circle-open", ["circle", false]],
1241
+ ["half", ["arrow", false]],
1242
+ ]);
1243
+
1244
+ /**
1245
+ * A neutral shape as vis-network's: the shape, and whether it is exact.
1246
+ * @param shape - the neutral shape
1247
+ * @returns the vis-network shape and whether it is exact
1248
+ */
1249
+ function visShape(shape: string): readonly [string, boolean] {
1250
+ const known = VIS_SHAPES.get(shape);
1251
+ if (known !== undefined) {
1252
+ return known;
1253
+ }
1254
+ return shape.startsWith("other:") ? [shape.slice(6), true] : ["dot", false];
1255
+ }
1256
+
1257
+ /**
1258
+ * A neutral arrow as vis-network's arrow type: the type (null for none), and whether it is exact.
1259
+ * @param arrow - the neutral arrow
1260
+ * @returns the type and whether it is exact
1261
+ */
1262
+ function visArrowType(arrow: string): readonly [string | null, boolean] {
1263
+ if (arrow === "none") {
1264
+ return [null, true];
1265
+ }
1266
+ const known = VIS_ARROW_TYPES.get(arrow);
1267
+ if (known !== undefined) {
1268
+ return known;
1269
+ }
1270
+ return arrow.startsWith("other:") ? [arrow.slice(6), true] : ["arrow", false];
1271
+ }
1272
+
1273
+ /** What reading a written vis.js file gives back for the values vis.js holds only approximately. */
1274
+ /**
1275
+ * A node size as a written vis.js file gives it back: one size for both, and none for a shape vis-network sizes by
1276
+ * its label.
1277
+ * @param want - the block's values of the element
1278
+ * @returns the size read back
1279
+ */
1280
+ const visSize = (want: (p: string) => StyleValue | null): StyleValue | null => {
1281
+ const s = want("shape");
1282
+ const shape = typeof s === "string" ? visShape(s)[0] : "ellipse";
1283
+ return VIS_SIZED.has(shape) ? oneSize(want) : null;
1284
+ };
1285
+
1286
+ const VIS_WRITTEN: Written = {
1287
+ node: {
1288
+ width: visSize,
1289
+ height: visSize,
1290
+ shape: (want) => {
1291
+ const s = want("shape");
1292
+ if (typeof s !== "string") {
1293
+ return null;
1294
+ }
1295
+ const site = { notes: new StyleNotes(), target: "node" as const, property: "shape" };
1296
+ return canonEnum("shape", visShape(s)[0], "vis", site);
1297
+ },
1298
+ },
1299
+ edge: {
1300
+ lineStyle: (want) => {
1301
+ const s = want("lineStyle");
1302
+ if (s === null) {
1303
+ return null;
1304
+ }
1305
+ return s === "solid" ? "solid" : "dashed";
1306
+ },
1307
+ targetArrow: (want) => arrowBack(want("targetArrow")),
1308
+ sourceArrow: (want) => arrowBack(want("sourceArrow")),
1309
+ },
1310
+ };
1311
+
1312
+ /**
1313
+ * What a written arrow reads back as.
1314
+ * @param a - the neutral arrow
1315
+ * @returns the arrow read back
1316
+ */
1317
+ function arrowBack(a: StyleValue | null): StyleValue | null {
1318
+ if (typeof a !== "string") {
1319
+ return null;
1320
+ }
1321
+ const [type] = visArrowType(a);
1322
+ const site = { notes: new StyleNotes(), target: "edge" as const, property: "targetArrow" };
1323
+ return type === null ? "none" : canonEnum("arrow", type, "vis", site);
1324
+ }
1325
+
1326
+ /**
1327
+ * The neutral properties Cytoscape.js holds natively, per target (what the stylesheet emitter writes).
1328
+ * @param map - the emitter's Cytoscape.js properties
1329
+ * @param extra - the neutral properties written by a rule of their own
1330
+ * @returns the properties
1331
+ */
1332
+ function cyjsProperties(map: ReadonlyMap<string, { readonly property: string }>, extra: readonly string[]): string[] {
1333
+ const out = new Set<string>();
1334
+ for (const spec of map.values()) {
1335
+ if (!spec.property.startsWith("_")) {
1336
+ out.add(spec.property);
1337
+ }
1338
+ }
1339
+ for (const p of extra) {
1340
+ out.add(p);
1341
+ }
1342
+ return [...out];
1343
+ }
1344
+
1345
+ /**
1346
+ * The neutral properties each style-writing dialect holds natively. Not part of the published entry points: the
1347
+ * documentation's per-dialect tables read it, through a dynamic import knip cannot follow (hence public).
1348
+ * @public
1349
+ */
1350
+ export const NATIVE: Readonly<
1351
+ Record<"cytoscape" | "graphology" | "vis", Readonly<Record<StyleTarget, readonly string[]>>>
1352
+ > = {
1353
+ cytoscape: {
1354
+ node: cyjsProperties(CYJS_NODE_PROPERTIES, ["labelPosition", "labelItalic", "visible"]),
1355
+ edge: cyjsProperties(CYJS_EDGE_PROPERTIES, ["curved", "labelItalic", "visible"]),
1356
+ },
1357
+ graphology: {
1358
+ node: ["fill", "fillOpacity", "width", "height", "label", "visible"],
1359
+ edge: ["color", "opacity", "width", "label", "visible"],
1360
+ },
1361
+ vis: {
1362
+ node: [
1363
+ "fill",
1364
+ "fillOpacity",
1365
+ "borderColor",
1366
+ "borderWidth",
1367
+ "shape",
1368
+ "width",
1369
+ "height",
1370
+ "opacity",
1371
+ "label",
1372
+ "tooltip",
1373
+ "labelColor",
1374
+ "labelSize",
1375
+ "labelFont",
1376
+ "visible",
1377
+ ],
1378
+ edge: [
1379
+ "color",
1380
+ "opacity",
1381
+ "width",
1382
+ "lineStyle",
1383
+ "targetArrow",
1384
+ "sourceArrow",
1385
+ "label",
1386
+ "tooltip",
1387
+ "labelColor",
1388
+ "labelSize",
1389
+ "labelFont",
1390
+ "visible",
1391
+ ],
1392
+ },
1393
+ };
1394
+
1395
+ /**
1396
+ * What the JSON writer holds natively, over every dialect: Cytoscape.js's stylesheet, vis.js's and graphology's
1397
+ * per-element keys. A dialect that holds less (node-link, d3, JGF hold none) reports the rest from `check()`. The
1398
+ * block goes where each dialect keeps extra data.
1399
+ * @category Built-in formats
1400
+ */
1401
+ export const JSON_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
1402
+ node: Object.freeze([
1403
+ ...new Set([...NATIVE.cytoscape.node, ...NATIVE.vis.node, ...NATIVE.graphology.node]),
1404
+ ]) as readonly NodeStyleProperty[],
1405
+ edge: Object.freeze([
1406
+ ...new Set([...NATIVE.cytoscape.edge, ...NATIVE.vis.edge, ...NATIVE.graphology.edge]),
1407
+ ]) as readonly EdgeStyleProperty[],
1408
+ block: "native",
1409
+ });
1410
+
1411
+ /** A JSON export with styles worked out. */
1412
+ export interface JsonStyledExport {
1413
+ /** The graph to write: the per-element style keys, the tagged values and, where it belongs there, the block. */
1414
+ readonly snapshot: GraphSnapshot;
1415
+ /** The block to write as a top-level key (d3, vis.js, Cytoscape.js), or null. */
1416
+ readonly topLevelBlock: string | null;
1417
+ /** Cytoscape.js: the stylesheet to write as `style`, or null. */
1418
+ readonly stylesheet: readonly CytoscapeStylesheetRule[] | null;
1419
+ /**
1420
+ * vis.js: the `groups` to write, one empty definition per value of the nodes' `group` key, or null. vis-network
1421
+ * colors a node of a group the file does not define from its own palette; a defined group draws nothing of its
1422
+ * own, so the nodes look as graphty draws them.
1423
+ */
1424
+ readonly groups?: Readonly<Record<string, Readonly<Record<string, never>>>> | null;
1425
+ /** What the dialect does not hold, or holds only approximately. */
1426
+ readonly notes: readonly LossNote[];
1427
+ }
1428
+
1429
+ /** The graph attribute the block is written as, in the dialects with graph attributes. */
1430
+ const BLOCK_AS_ATTRIBUTE: ReadonlySet<JsonDialect> = new Set(["node-link", "jgf", "graphology"]);
1431
+
1432
+ /**
1433
+ * Works out a JSON export with styles for one dialect.
1434
+ * @param snapshot - the graph handed to the exporter
1435
+ * @param dialect - the dialect written
1436
+ * @param input - the styles, or undefined when none are written
1437
+ * @returns the styled export, or null without styles
1438
+ */
1439
+ export function jsonStyledExport(
1440
+ snapshot: GraphSnapshot,
1441
+ dialect: JsonDialect,
1442
+ input: ExporterStyleInput | undefined,
1443
+ ): JsonStyledExport | null {
1444
+ if (input === undefined) {
1445
+ return null;
1446
+ }
1447
+ const native = dialect === "cytoscape" || dialect === "graphology" || dialect === "vis" ? NATIVE[dialect] : null;
1448
+ const notes: LossNote[] = [...notWritten(input, dialect, native), ...rulesNotKept(snapshot, dialect, input)];
1449
+ if (dialect === "obographs") {
1450
+ return { snapshot, topLevelBlock: null, stylesheet: null, groups: null, notes };
1451
+ }
1452
+ const out = snapshot.withColumns();
1453
+ const read = dataColumnsRead(input.rules.styles.rules);
1454
+ for (const [table, target] of [
1455
+ [out.nodes, "node"],
1456
+ [out.edges, "edge"],
1457
+ ] as const) {
1458
+ removeEarlierStyleKeys(table, dialect, read[target]);
1459
+ for (const [name, col] of input.tags[target]) {
1460
+ setColumn(table, name, col);
1461
+ }
1462
+ }
1463
+ if (out.graph.has(STYLE_BLOCK_NAME)) {
1464
+ out.graph.remove(STYLE_BLOCK_NAME);
1465
+ }
1466
+ if (BLOCK_AS_ATTRIBUTE.has(dialect)) {
1467
+ out.graph.set(STYLE_BLOCK_NAME, [input.block], { dtype: "string" });
1468
+ }
1469
+ let stylesheet: readonly CytoscapeStylesheetRule[] | null = null;
1470
+ const approximated = new StyleApproximations();
1471
+ const replaced = new Replaced(out.nodes);
1472
+ if (dialect === "cytoscape") {
1473
+ const emitted = writeCytoscapeStyles(out, input);
1474
+ ({ stylesheet } = emitted);
1475
+ notes.push(...emitted.notes.map(lossOf));
1476
+ addUntrimmedText(input, approximated);
1477
+ } else if (dialect === "graphology") {
1478
+ writeGraphology(out, input, approximated, replaced);
1479
+ } else if (dialect === "vis") {
1480
+ writeVis(out, input, approximated, replaced);
1481
+ }
1482
+ notes.push(...approximated.notes(`the ${dialect} dialect of JSON`), ...replaced.notes(dialect));
1483
+ return {
1484
+ snapshot: out,
1485
+ topLevelBlock: BLOCK_AS_ATTRIBUTE.has(dialect) ? null : input.block,
1486
+ stylesheet,
1487
+ groups: dialect === "vis" ? visGroups(out) : null,
1488
+ notes,
1489
+ };
1490
+ }
1491
+
1492
+ /**
1493
+ * W_STYLE_RULES_NOT_KEPT for a dialect that cannot give the rules back: JGF when it changes the node ids' type or
1494
+ * order, OBO Graphs always.
1495
+ * @param snapshot - the graph handed to the exporter
1496
+ * @param dialect - the dialect written
1497
+ * @param input - the styles
1498
+ * @returns the notes
1499
+ */
1500
+ function rulesNotKept(snapshot: GraphSnapshot, dialect: JsonDialect, input: ExporterStyleInput): LossNote[] {
1501
+ let message: string | null = null;
1502
+ if (dialect === "jgf" && !jgfKeepsNodes(snapshot)) {
1503
+ message =
1504
+ "the style rules are not kept: JGF changes the node ids' type or order (number ids become text, integer-like ids are read first), so the graphty.styles block no longer matches the graph read back";
1505
+ } else if (dialect === "obographs") {
1506
+ message = "the style rules are not kept: OBO Graphs has no place for them";
1507
+ }
1508
+ if (message === null) {
1509
+ return [];
1510
+ }
1511
+ return [
1512
+ Object.freeze({
1513
+ code: STYLE_ISSUE.RULES_NOT_KEPT,
1514
+ message,
1515
+ column: null,
1516
+ count: input.rules.styles.rules.length,
1517
+ }),
1518
+ ];
1519
+ }
1520
+
1521
+ /**
1522
+ * Removes the style values an earlier write left in the data, unless the styles themselves read them as data, and,
1523
+ * for Cytoscape.js, the element style an import kept.
1524
+ * @param table - the node or edge table, changed in place
1525
+ * @param dialect - the dialect written
1526
+ * @param read - the data columns the target's rules read
1527
+ */
1528
+ function removeEarlierStyleKeys(table: Table, dialect: JsonDialect, read: ReadonlySet<string>): void {
1529
+ for (const name of table.names()) {
1530
+ const styleKey =
1531
+ name.startsWith("graphty.style.") || name.startsWith("graphty_style_") || name === CYTOSCAPE_IMPORT_KEY;
1532
+ if (styleKey && !read.has(name)) {
1533
+ table.remove(name);
1534
+ }
1535
+ }
1536
+ if (dialect === "cytoscape") {
1537
+ for (const name of ["style#element", "css#element"].filter((c) => table.has(c))) {
1538
+ table.remove(name);
1539
+ }
1540
+ }
1541
+ }
1542
+
1543
+ /**
1544
+ * Writes the Cytoscape.js element data and bypasses the stylesheet reads.
1545
+ * @param out - the graph written, changed in place
1546
+ * @param input - the styles
1547
+ * @returns what toCytoscapeStylesheet() emitted
1548
+ */
1549
+ function writeCytoscapeStyles(out: GraphSnapshot, input: ExporterStyleInput): ReturnType<typeof toCytoscapeStylesheet> {
1550
+ // the element data the stylesheet is checked against is the data written: without the style values of an
1551
+ // earlier write, which are not written again
1552
+ const features = {
1553
+ node: input.rules.features.node.filter((f) => !("data" in f.ref) || out.nodes.has(f.ref.data)),
1554
+ edge: input.rules.features.edge.filter((f) => !("data" in f.ref) || out.edges.has(f.ref.data)),
1555
+ };
1556
+ const emitted = toCytoscapeStylesheet({ ...input.rules, features });
1557
+ for (const [table, target] of [
1558
+ [out.nodes, "node"],
1559
+ [out.edges, "edge"],
1560
+ ] as const) {
1561
+ const rows = emitted.elementData[target];
1562
+ const keys = new Set(rows.flatMap((row) => Object.keys(row ?? {})));
1563
+ for (const key of keys) {
1564
+ setColumn(
1565
+ table,
1566
+ key,
1567
+ rows.map((row) => row?.[key] ?? null),
1568
+ );
1569
+ }
1570
+ const bypass = emitted.bypass[target].map((b) => (b === null || Object.keys(b).length === 0 ? null : b));
1571
+ if (bypass.some((b) => b !== null)) {
1572
+ setColumn(table, "style#element", bypass);
1573
+ }
1574
+ }
1575
+ return emitted;
1576
+ }
1577
+
1578
+ /**
1579
+ * Counts as approximated the text values with leading or trailing white space, which Cytoscape.js trims.
1580
+ * @param input - the styles
1581
+ * @param approximated - where approximations are counted
1582
+ */
1583
+ function addUntrimmedText(input: ExporterStyleInput, approximated: StyleApproximations): void {
1584
+ for (const target of TARGETS) {
1585
+ for (const property of CYJS_TEXT[target]) {
1586
+ for (const v of input.picture(target, property as never)) {
1587
+ if (typeof v === "string" && v.trim() !== v) {
1588
+ approximated.add(target, property, JSON.stringify(v));
1589
+ }
1590
+ }
1591
+ }
1592
+ }
1593
+ }
1594
+
1595
+ /**
1596
+ * vis.js: one empty group definition per value of the nodes' `group` key, so vis-network draws none of its palette.
1597
+ * @param snapshot - the graph written
1598
+ * @returns the definitions, or null when no node names a group
1599
+ */
1600
+ function visGroups(snapshot: GraphSnapshot): Record<string, Record<string, never>> | null {
1601
+ const column = snapshot.nodes.get("group");
1602
+ if (column === null || column === undefined) {
1603
+ return null;
1604
+ }
1605
+ const groups = new Map<string, Record<string, never>>();
1606
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1607
+ const value = column.isSet(i) ? column.value(i) : null;
1608
+ if (typeof value === "string" || typeof value === "number") {
1609
+ groups.set(String(value), {});
1610
+ }
1611
+ }
1612
+ return groups.size === 0 ? null : Object.fromEntries(groups);
1613
+ }
1614
+
1615
+ /**
1616
+ * The data columns rules read, per target.
1617
+ * @param rules - the rules
1618
+ * @returns the column names
1619
+ */
1620
+ function dataColumnsRead(rules: readonly StyleRule[]): Record<StyleTarget, Set<string>> {
1621
+ const out: Record<StyleTarget, Set<string>> = { node: new Set(), edge: new Set() };
1622
+ for (const rule of rules) {
1623
+ const refs = [
1624
+ ...predicateRefs(rule.where),
1625
+ ...Object.values(rule.map ?? {}).flatMap((m) => (m === undefined ? [] : [m.by])),
1626
+ ];
1627
+ for (const ref of refs) {
1628
+ if ("data" in ref) {
1629
+ out[rule.target].add(ref.data);
1630
+ }
1631
+ }
1632
+ }
1633
+ return out;
1634
+ }
1635
+
1636
+ /**
1637
+ * The columns a predicate tests.
1638
+ * @param where - the predicate
1639
+ * @returns the column references
1640
+ */
1641
+ function predicateRefs(where: Predicate): ColumnRef[] {
1642
+ const refs: ColumnRef[] = [];
1643
+ const stack: Predicate[] = [where];
1644
+ while (stack.length > 0) {
1645
+ const p = stack.pop() as Predicate;
1646
+ if ("has" in p) {
1647
+ refs.push(p.has);
1648
+ } else if ("eq" in p) {
1649
+ refs.push(p.eq[0]);
1650
+ } else if ("in" in p) {
1651
+ refs.push(p.in[0]);
1652
+ } else if ("and" in p) {
1653
+ stack.push(...p.and);
1654
+ } else if ("or" in p) {
1655
+ stack.push(...p.or);
1656
+ } else if ("not" in p) {
1657
+ stack.push(p.not);
1658
+ }
1659
+ }
1660
+ return refs;
1661
+ }
1662
+
1663
+ /**
1664
+ * Whether JGF reads the nodes back with the same ids in the same order: no number ids (JGF keys nodes by text), and
1665
+ * the integer-like ids (which JSON parsers enumerate first, ascending) already first and ascending.
1666
+ * @param snapshot - the graph
1667
+ * @returns true when the block's fingerprint survives
1668
+ */
1669
+ function jgfKeepsNodes(snapshot: GraphSnapshot): boolean {
1670
+ let last = -1;
1671
+ let other = false;
1672
+ for (let i = 0; i < snapshot.nodeCount; i++) {
1673
+ const id = snapshot.ids.idOf(i);
1674
+ if (typeof id === "number") {
1675
+ return false;
1676
+ }
1677
+ if (/^(0|[1-9]\d*)$/.test(id) && Number(id) < 4294967295) {
1678
+ if (other || Number(id) <= last) {
1679
+ return false;
1680
+ }
1681
+ last = Number(id);
1682
+ } else {
1683
+ other = true;
1684
+ }
1685
+ }
1686
+ return true;
1687
+ }
1688
+
1689
+ /**
1690
+ * W_STYLE_NOT_WRITTEN for each property the styles use that the dialect does not hold but some other dialect does
1691
+ * (graph-io reports the properties no dialect holds).
1692
+ * @param input - the styles
1693
+ * @param dialect - the dialect
1694
+ * @param native - what it holds, or null for none
1695
+ * @returns the notes
1696
+ */
1697
+ function notWritten(
1698
+ input: ExporterStyleInput,
1699
+ dialect: JsonDialect,
1700
+ native: Readonly<Record<StyleTarget, readonly string[]>> | null,
1701
+ ): LossNote[] {
1702
+ const out: LossNote[] = [];
1703
+ for (const target of TARGETS) {
1704
+ const all = new Set<string>(JSON_STYLE_CAPABILITIES[target]);
1705
+ const held = new Set<string>(native?.[target] ?? []);
1706
+ const used = new Set<string>();
1707
+ for (const r of input.rules.styles.rules) {
1708
+ if (r.target === target) {
1709
+ for (const p of [...Object.keys(r.set ?? {}), ...Object.keys(r.map ?? {})]) {
1710
+ used.add(p);
1711
+ }
1712
+ }
1713
+ }
1714
+ for (const property of used) {
1715
+ if (!all.has(property) || held.has(property)) {
1716
+ continue;
1717
+ }
1718
+ const count = input.picture(target, property as never).filter((v) => v !== null).length;
1719
+ out.push(
1720
+ Object.freeze({
1721
+ code: STYLE_ISSUE.NOT_WRITTEN,
1722
+ message: `${target} ${property} is not written: the ${dialect} dialect of JSON has no place for it`,
1723
+ column: null,
1724
+ count,
1725
+ style: Object.freeze({ target, property: property as never }),
1726
+ }),
1727
+ );
1728
+ }
1729
+ }
1730
+ return out;
1731
+ }
1732
+
1733
+ /**
1734
+ * A style note as a loss note.
1735
+ * @param n - the note
1736
+ * @returns the loss note
1737
+ */
1738
+ function lossOf(n: StyleNote): LossNote {
1739
+ return Object.freeze({
1740
+ code: n.code,
1741
+ message: n.message,
1742
+ column: null,
1743
+ count: n.params.count,
1744
+ style:
1745
+ n.params.target === null || n.params.property === null
1746
+ ? undefined
1747
+ : Object.freeze({ target: n.params.target, property: n.params.property as never }),
1748
+ });
1749
+ }
1750
+
1751
+ /** An editable attribute table of `snapshot.withColumns()`. */
1752
+ type Table = ReturnType<GraphSnapshot["withColumns"]>["nodes"];
1753
+
1754
+ /**
1755
+ * Sets a column, with the dtype its values have: numbers, booleans, text, else JSON.
1756
+ * @param table - the table
1757
+ * @param name - the column
1758
+ * @param values - the values by row, null where unset
1759
+ */
1760
+ function setColumn(table: Table, name: string, values: readonly unknown[]): void {
1761
+ const kinds = new Set(values.filter((v) => v !== null && v !== undefined).map((v) => typeof v));
1762
+ let dtype: "f64" | "bool" | "string" | "json" = "json";
1763
+ if (kinds.size === 1) {
1764
+ if (kinds.has("number")) {
1765
+ dtype = "f64";
1766
+ } else if (kinds.has("boolean")) {
1767
+ dtype = "bool";
1768
+ } else if (kinds.has("string")) {
1769
+ dtype = "string";
1770
+ }
1771
+ }
1772
+ // an unset row is undefined: null is a value in a json column
1773
+ table.set(
1774
+ name,
1775
+ values.map((v) => (v === null ? undefined : v)),
1776
+ { dtype },
1777
+ );
1778
+ }
1779
+
1780
+ /**
1781
+ * Writes a picture into a style key: every element gets the picture's value, and an element the picture gives none
1782
+ * loses the key, so a program reading the file draws what graphty draws. A value the column held before that differs
1783
+ * is counted, for the loss note.
1784
+ * @param table - the table
1785
+ * @param name - the column
1786
+ * @param values - the picture, null where it has no value
1787
+ * @param replaced - counts the values replaced, per column
1788
+ */
1789
+ function writeKey(table: Table, name: string, values: readonly unknown[], replaced: Replaced): void {
1790
+ const old = table.get(name);
1791
+ let changed = 0;
1792
+ if (old !== null) {
1793
+ for (let i = 0; i < values.length; i++) {
1794
+ const before = plainCell(old.value(i));
1795
+ const after = values[i] ?? null;
1796
+ if (before !== null && JSON.stringify(before) !== JSON.stringify(after)) {
1797
+ changed++;
1798
+ }
1799
+ }
1800
+ }
1801
+ if (changed > 0) {
1802
+ replaced.add(table === replaced.nodes ? "node" : "edge", name, changed);
1803
+ }
1804
+ if (values.every((v) => v === null || v === undefined)) {
1805
+ if (old !== null) {
1806
+ table.remove(name);
1807
+ }
1808
+ return;
1809
+ }
1810
+ setColumn(table, name, values);
1811
+ }
1812
+
1813
+ /**
1814
+ * The code of the note that a data value held under a style key is replaced by what graphty draws.
1815
+ * @category Built-in formats
1816
+ */
1817
+ export const STYLE_KEY_REPLACED_CODE = "W_JSON_STYLE_KEY_REPLACED";
1818
+
1819
+ /** Counts the data values a style key's picture replaces, per target and key. */
1820
+ class Replaced {
1821
+ readonly #counts = new Map<string, number>();
1822
+
1823
+ /**
1824
+ * Counts the replaced values of one export.
1825
+ * @param nodes - the node table being written, which tells node columns from edge columns
1826
+ */
1827
+ constructor(readonly nodes: Table) {}
1828
+
1829
+ /**
1830
+ * Counts replaced values.
1831
+ * @param target - nodes or edges
1832
+ * @param key - the key
1833
+ * @param count - how many
1834
+ */
1835
+ add(target: StyleTarget, key: string, count: number): void {
1836
+ this.#counts.set(`${target}\u0000${key}`, (this.#counts.get(`${target}\u0000${key}`) ?? 0) + count);
1837
+ }
1838
+
1839
+ /**
1840
+ * The notes, one per key.
1841
+ * @param dialect - the dialect, for the message
1842
+ * @returns the notes
1843
+ */
1844
+ notes(dialect: JsonDialect): LossNote[] {
1845
+ return Array.from(this.#counts, ([key, count]) => {
1846
+ const [target, name] = key.split("\u0000");
1847
+ return Object.freeze({
1848
+ code: STYLE_KEY_REPLACED_CODE,
1849
+ message: `${target} column "${name}" is the ${dialect} dialect's style key: ${String(count)} ${target}${count === 1 ? "" : "s"} get what graphty draws instead of the value the column held`,
1850
+ column: name,
1851
+ count,
1852
+ });
1853
+ });
1854
+ }
1855
+ }
1856
+
1857
+ /**
1858
+ * A color with its alpha as CSS text: `#rrggbb`, or `rgba()` below full opacity.
1859
+ * @param color - the color
1860
+ * @param alpha - the opacity, or null
1861
+ * @returns the text, or null
1862
+ */
1863
+ function cssColor(color: StyleValue | null, alpha: StyleValue | null): string | null {
1864
+ if (typeof color !== "string") {
1865
+ return null;
1866
+ }
1867
+ if (typeof alpha !== "number" || alpha >= 1) {
1868
+ return color;
1869
+ }
1870
+ const n = Number.parseInt(color.slice(1), 16);
1871
+ return `rgba(${String((n >> 16) & 255)},${String((n >> 8) & 255)},${String(n & 255)},${String(alpha)})`;
1872
+ }
1873
+
1874
+ /**
1875
+ * One size for a node's width and height, noting a node whose two differ.
1876
+ * @param width - the widths
1877
+ * @param height - the heights
1878
+ * @param approximated - where approximations are counted
1879
+ * @returns the sizes
1880
+ */
1881
+ function nodeSizes(
1882
+ width: readonly (StyleValue | null)[],
1883
+ height: readonly (StyleValue | null)[],
1884
+ approximated: StyleApproximations,
1885
+ ): (number | null)[] {
1886
+ return width.map((_v, i) => {
1887
+ const w = width[i];
1888
+ const h = height[i];
1889
+ if (typeof w === "number" && typeof h === "number" && w !== h) {
1890
+ approximated.add("node", w > h ? "height" : "width", `${String(w)} x ${String(h)}`);
1891
+ return Math.max(w, h);
1892
+ }
1893
+ if (typeof w === "number") {
1894
+ return w;
1895
+ }
1896
+ return typeof h === "number" ? h : null;
1897
+ });
1898
+ }
1899
+
1900
+ /**
1901
+ * graphology: sigma.js's keys from the picture.
1902
+ * @param out - the graph being written
1903
+ * @param input - the styles
1904
+ * @param approximated - where approximations are counted
1905
+ * @param replaced - where replaced data values are counted
1906
+ */
1907
+ function writeGraphology(
1908
+ out: ReturnType<GraphSnapshot["withColumns"]>,
1909
+ input: ExporterStyleInput,
1910
+ approximated: StyleApproximations,
1911
+ replaced: Replaced,
1912
+ ): void {
1913
+ const p = (target: StyleTarget, property: string): readonly (StyleValue | null)[] =>
1914
+ input.picture(target, property as never);
1915
+ for (const [table, target] of [
1916
+ [out.nodes, "node"],
1917
+ [out.edges, "edge"],
1918
+ ] as const) {
1919
+ const [fill, opacity] = target === "node" ? ["fill", "fillOpacity"] : ["color", "opacity"];
1920
+ const colors = p(target, fill);
1921
+ const alphas = p(target, opacity);
1922
+ writeKey(
1923
+ table,
1924
+ "color",
1925
+ colors.map((c, i) => cssColor(c, alphas[i])),
1926
+ replaced,
1927
+ );
1928
+ writeKey(
1929
+ table,
1930
+ "size",
1931
+ target === "node" ? nodeSizes(p("node", "width"), p("node", "height"), approximated) : p("edge", "width"),
1932
+ replaced,
1933
+ );
1934
+ writeKey(table, "label", p(target, "label"), replaced);
1935
+ writeKey(
1936
+ table,
1937
+ "hidden",
1938
+ p(target, "visible").map((v) => (v === false ? true : null)),
1939
+ replaced,
1940
+ );
1941
+ }
1942
+ }
1943
+
1944
+ /**
1945
+ * vis.js: vis-network's keys from the picture.
1946
+ * @param out - the graph being written
1947
+ * @param input - the styles
1948
+ * @param approximated - where approximations are counted
1949
+ * @param replaced - where replaced data values are counted
1950
+ */
1951
+ function writeVis(
1952
+ out: ReturnType<GraphSnapshot["withColumns"]>,
1953
+ input: ExporterStyleInput,
1954
+ approximated: StyleApproximations,
1955
+ replaced: Replaced,
1956
+ ): void {
1957
+ const p = (target: StyleTarget, property: string): readonly (StyleValue | null)[] =>
1958
+ input.picture(target, property as never);
1959
+ const font = (target: StyleTarget): (Record<string, StyleValue> | null)[] => {
1960
+ const color = p(target, "labelColor");
1961
+ const size = p(target, "labelSize");
1962
+ const face = p(target, "labelFont");
1963
+ return color.map((_v, i) => {
1964
+ const f: Record<string, StyleValue> = {};
1965
+ if (color[i] !== null) {
1966
+ f.color = color[i];
1967
+ }
1968
+ if (size[i] !== null) {
1969
+ f.size = size[i];
1970
+ }
1971
+ if (face[i] !== null) {
1972
+ f.face = face[i];
1973
+ }
1974
+ return Object.keys(f).length === 0 ? null : f;
1975
+ });
1976
+ };
1977
+ const label = (table: Table, target: StyleTarget): void => {
1978
+ writeKey(table, "label", p(target, "label"), replaced);
1979
+ writeKey(table, "title", p(target, "tooltip"), replaced);
1980
+ writeKey(table, "font", font(target), replaced);
1981
+ writeKey(
1982
+ table,
1983
+ "hidden",
1984
+ p(target, "visible").map((v) => (v === false ? true : null)),
1985
+ replaced,
1986
+ );
1987
+ };
1988
+
1989
+ const fill = p("node", "fill");
1990
+ const fillOpacity = p("node", "fillOpacity");
1991
+ const border = p("node", "borderColor");
1992
+ writeKey(
1993
+ out.nodes,
1994
+ "color",
1995
+ fill.map((_v, i) => {
1996
+ const c: Record<string, string> = {};
1997
+ const bg = cssColor(fill[i], fillOpacity[i]);
1998
+ if (bg !== null) {
1999
+ c.background = bg;
2000
+ }
2001
+ if (typeof border[i] === "string") {
2002
+ c.border = border[i];
2003
+ }
2004
+ return Object.keys(c).length === 0 ? null : c;
2005
+ }),
2006
+ replaced,
2007
+ );
2008
+ const sizes = nodeSizes(p("node", "width"), p("node", "height"), approximated);
2009
+ const shapes = p("node", "shape").map((s, i) => {
2010
+ const [shape, exact] = typeof s === "string" ? visShape(s) : [null, true];
2011
+ if (!exact) {
2012
+ approximated.add("node", "shape", s);
2013
+ }
2014
+ if (sizes[i] !== null && !VIS_SIZED.has(shape ?? "ellipse")) {
2015
+ approximated.add("node", "width", `${String(s ?? "ellipse")} sized by its label`);
2016
+ }
2017
+ return shape;
2018
+ });
2019
+ writeKey(out.nodes, "shape", shapes, replaced);
2020
+ writeKey(out.nodes, "size", sizes, replaced);
2021
+ writeKey(out.nodes, "borderWidth", p("node", "borderWidth"), replaced);
2022
+ writeKey(out.nodes, "opacity", p("node", "opacity"), replaced);
2023
+ label(out.nodes, "node");
2024
+
2025
+ const color = p("edge", "color");
2026
+ const opacity = p("edge", "opacity");
2027
+ writeKey(
2028
+ out.edges,
2029
+ "color",
2030
+ color.map((_v, e) => {
2031
+ const c: Record<string, StyleValue> = {};
2032
+ if (typeof color[e] === "string") {
2033
+ c.color = color[e];
2034
+ }
2035
+ if (typeof opacity[e] === "number") {
2036
+ c.opacity = opacity[e];
2037
+ }
2038
+ return Object.keys(c).length === 0 ? null : c;
2039
+ }),
2040
+ replaced,
2041
+ );
2042
+ writeKey(out.edges, "width", p("edge", "width"), replaced);
2043
+ writeKey(
2044
+ out.edges,
2045
+ "dashes",
2046
+ p("edge", "lineStyle").map((s) => {
2047
+ if (typeof s !== "string") {
2048
+ return null;
2049
+ }
2050
+ if (s !== "solid" && s !== "dashed") {
2051
+ approximated.add("edge", "lineStyle", s);
2052
+ }
2053
+ return s !== "solid";
2054
+ }),
2055
+ replaced,
2056
+ );
2057
+ const to = p("edge", "targetArrow");
2058
+ const from = p("edge", "sourceArrow");
2059
+ writeKey(
2060
+ out.edges,
2061
+ "arrows",
2062
+ to.map((_v, e) => {
2063
+ const a: Record<string, { enabled: boolean; type?: string }> = {};
2064
+ for (const [end, value, property] of [
2065
+ ["to", to[e], "targetArrow"],
2066
+ ["from", from[e], "sourceArrow"],
2067
+ ] as const) {
2068
+ if (typeof value !== "string") {
2069
+ continue;
2070
+ }
2071
+ const [type, exact] = visArrowType(value);
2072
+ if (!exact) {
2073
+ approximated.add("edge", property, value);
2074
+ }
2075
+ a[end] = type === null ? { enabled: false } : { enabled: true, type };
2076
+ }
2077
+ return Object.keys(a).length === 0 ? null : a;
2078
+ }),
2079
+ replaced,
2080
+ );
2081
+ label(out.edges, "edge");
2082
+ }