@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,1741 @@
1
+ /**
2
+ * DOT styles: every node's, cluster's and edge's attributes read the way Graphviz draws them, with Graphviz's
3
+ * defaults beneath, and a graph's styles written back as Graphviz attributes.
4
+ *
5
+ * Reading follows Graphviz's own rules, measured against `dot` 2.43:
6
+ *
7
+ * - Labels: a node with no `label` shows its name (`\N`), as one tool-default rule over a style column holding each
8
+ * node's name. Escapes are applied in Graphviz's two passes; an HTML label shows its text, a record label its
9
+ * fields, and a `point` draws no label.
10
+ * - Fills: `point` is always filled; `filled` and `radial` fill any shape, `striped` only boxes, `wedged` only
11
+ * ellipses; an empty `fillcolor` falls back to `color`, then light gray.
12
+ * - Colors through Graphviz's color rules: a `colorscheme` applies to its own object only, an unknown color is black,
13
+ * `none` and `invis` are transparent, and a color list is read as Graphviz draws it for that use.
14
+ * - Outlines: `plain`, `plaintext` and `none` draw none. `setlinewidth(n)` is a pen width, `bold` a width of 2.
15
+ * - Sizes: exact with `fixedsize`, an empty label or a `point`; otherwise minimums Graphviz grows to fit the label,
16
+ * carried and noted as `W_STYLE_SIZE_MINIMUM`.
17
+ * - Arrows per `dir`, `arrowhead` and `arrowtail`, with the graph's kind deciding the default: a target arrow on
18
+ * directed edges, as one tool-default rule.
19
+ * - A cluster's container node holds the cluster's attributes: its fill (`bgcolor`, or the fill when `filled`) and
20
+ * outline (`pencolor`, else `color`) are the cluster's, it is drawn as a rectangle, and it has no label by default.
21
+ * Cluster membership is offered to the planner, so clusters whose nodes share a fill become one rule.
22
+ *
23
+ * Writing: Graphviz attributes from what graphty draws (`fillcolor` with `style=filled`, `width` and `height` with
24
+ * `fixedsize=true`, `penwidth`, `style`, arrows with `dir`, font attributes, graph `bgcolor`) replace every style
25
+ * attribute the reader reads. The `graphty.styles` block is the quoted graph attribute `"graphty.styles"`, and the
26
+ * per-element values its rules read are quoted `"graphty.style.<target>.<key>"` attributes.
27
+ */
28
+
29
+ import type { GraphSnapshot } from "@graphty/graph-format";
30
+
31
+ import { isWritableDotText } from "../../common/escape.js";
32
+ import { inferTextDtype } from "../../common/text.js";
33
+ import { canonColor, colorOpacity } from "../../styles/canon/color.js";
34
+ import { canonEnum } from "../../styles/canon/enums.js";
35
+ import { canonNumber, DOT_POINTS_PER_INCH, num6 } from "../../styles/canon/number.js";
36
+ import { type CanonSite, MAX_STYLE_VALUE_LENGTH, noteAt } from "../../styles/canon/site.js";
37
+ import { type DotEscapeNames, dotLabelText, htmlLabelText, recordLabelText } from "../../styles/canon/text.js";
38
+ import { STYLE_ISSUE } from "../../styles/codes.js";
39
+ import { toolDefaults } from "../../styles/defaults.js";
40
+ import { StyleApproximations } from "../../styles/io/approximations.js";
41
+ import {
42
+ blockStyles,
43
+ parseStyleBlock,
44
+ STYLE_BLOCK_NAME,
45
+ styleBlockLosses,
46
+ writeStyleBlock,
47
+ } from "../../styles/io/block.js";
48
+ import type { ExporterStyleInput } from "../../styles/io/writer.js";
49
+ import { StyleNotes } from "../../styles/notes.js";
50
+ import { styleFingerprint } from "../../styles/plan/features.js";
51
+ import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
52
+ import type {
53
+ ImporterStyles,
54
+ StyleColumn,
55
+ StyleFeature,
56
+ StyleReadContext,
57
+ StyleRule,
58
+ StyleTarget,
59
+ StyleValue,
60
+ StyleWriteCapabilities,
61
+ } from "../../styles/types.js";
62
+ import type { LossNote } from "../../types.js";
63
+ import { type ValueColumns, valueColumns } from "../graphml/styles-yed.js";
64
+ import { CLUSTER_COLUMN, LABEL_ATTRIBUTE } from "./names.js";
65
+
66
+ /**
67
+ * The graph attribute holding the `graphty.styles` block of a DOT file graphty wrote.
68
+ * @category Built-in formats
69
+ */
70
+ export { STYLE_BLOCK_NAME as DOT_STYLE_BLOCK_ATTRIBUTE } from "../../styles/io/block.js";
71
+
72
+ /**
73
+ * What the DOT writer holds natively: Graphviz's node, cluster and edge attributes, and the graph's `bgcolor`. The
74
+ * `graphty.styles` block and the per-element values its rules read are quoted attributes.
75
+ * @category Built-in formats
76
+ */
77
+ export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
78
+ node: Object.freeze([
79
+ "fill",
80
+ "fillOpacity",
81
+ "borderColor",
82
+ "borderWidth",
83
+ "borderStyle",
84
+ "shape",
85
+ "width",
86
+ "height",
87
+ "label",
88
+ "labelColor",
89
+ "labelSize",
90
+ "labelFont",
91
+ "tooltip",
92
+ "visible",
93
+ ] as const),
94
+ edge: Object.freeze([
95
+ "color",
96
+ "opacity",
97
+ "width",
98
+ "lineStyle",
99
+ "sourceArrow",
100
+ "targetArrow",
101
+ "arrowSize",
102
+ "label",
103
+ "sourceLabel",
104
+ "targetLabel",
105
+ "labelColor",
106
+ "labelSize",
107
+ "labelFont",
108
+ "tooltip",
109
+ "visible",
110
+ ] as const),
111
+ graph: Object.freeze(["background"] as const),
112
+ block: "native",
113
+ });
114
+
115
+ /** The style column holding the name each node shows when it has no label (Graphviz's `\N`). */
116
+ const NAME_KEY = "name";
117
+
118
+ /** The style column marking directed edges, which Graphviz draws with an arrow at the target. */
119
+ const DIRECTED_KEY = "directed";
120
+
121
+ /** The style column marking a cluster's container node. */
122
+ const CLUSTER_KEY = "cluster";
123
+
124
+ /** Every node and cluster attribute the reader reads; the writer replaces them all. */
125
+ const NODE_ATTRIBUTES: readonly string[] = [
126
+ "colorscheme",
127
+ "color",
128
+ "fillcolor",
129
+ "bgcolor",
130
+ "pencolor",
131
+ "style",
132
+ "shape",
133
+ "width",
134
+ "height",
135
+ "fixedsize",
136
+ "regular",
137
+ "penwidth",
138
+ "fontname",
139
+ "fontsize",
140
+ "fontcolor",
141
+ "tooltip",
142
+ ];
143
+
144
+ /** Every edge attribute the reader reads; the writer replaces them all. */
145
+ const EDGE_ATTRIBUTES: readonly string[] = [
146
+ "colorscheme",
147
+ "color",
148
+ "style",
149
+ "penwidth",
150
+ "fontname",
151
+ "fontsize",
152
+ "fontcolor",
153
+ "tooltip",
154
+ "edgetooltip",
155
+ "arrowhead",
156
+ "arrowtail",
157
+ "dir",
158
+ "arrowsize",
159
+ "headlabel",
160
+ "taillabel",
161
+ ];
162
+
163
+ /** Attributes the reader notes as not carried. */
164
+ const NOTED_ATTRIBUTES: readonly string[] = ["peripheries", "xlabel"];
165
+
166
+ const CIRCLE_LIKE: ReadonlySet<string> = new Set(["circle", "doublecircle", "mcircle", "square", "msquare"]);
167
+ const BOX_LIKE: ReadonlySet<string> = new Set(["box", "rect", "rectangle", "square"]);
168
+ const ELLIPSE_LIKE: ReadonlySet<string> = new Set(["", "ellipse", "oval", "circle"]);
169
+ const NO_OUTLINE: ReadonlySet<string> = new Set(["plain", "plaintext", "none"]);
170
+ const RECORD: ReadonlySet<string> = new Set(["record", "mrecord"]);
171
+ const TRUE_TEXTS: ReadonlySet<string> = new Set(["true", "yes", "shape"]);
172
+
173
+ /** A point's size when the file gives none: 0.05 inch. */
174
+ const POINT_SIZE = 0.05;
175
+
176
+ /** The size in inches Graphviz gives a circle, a square or a `regular` node that states none. */
177
+ const REGULAR_SIZE = 0.5;
178
+
179
+ /** One attribute of an element as text, or null when it is unset. */
180
+ type Attr = (name: string) => string | null;
181
+
182
+ /** One element's canonical value of a property; null leaves it unset. */
183
+ type Put = (property: string, value: StyleValue | null) => void;
184
+
185
+ /** Where a value of an element was read, by property. */
186
+ type At = (property: string) => CanonSite;
187
+
188
+ /** What a node's label escapes and its defaults depend on. */
189
+ interface NodeContext {
190
+ /** The node's name (its id as text). */
191
+ readonly name: string;
192
+ /** The graph's name, for `\G`. */
193
+ readonly graph: string;
194
+ /** Whether the node is a cluster's container. */
195
+ readonly cluster: boolean;
196
+ /** The HTML strings of the file, for Graphviz's shared string pool. */
197
+ readonly html: ReadonlySet<string>;
198
+ }
199
+
200
+ /** What an edge's label escapes depend on. */
201
+ interface EdgeContext {
202
+ /** The names `\E`, `\T`, `\H` and `\G` stand for. */
203
+ readonly names: DotEscapeNames;
204
+ /** Whether the edge is directed. */
205
+ readonly directed: boolean;
206
+ /** The HTML strings of the file. */
207
+ readonly html: ReadonlySet<string>;
208
+ }
209
+
210
+ /**
211
+ * Whether a text is an HTML string (balanced angle brackets around the whole text), which graph-io keeps with its
212
+ * brackets and DOT writes bare as `<...>`.
213
+ * @param text - the text
214
+ * @returns true for an HTML string
215
+ */
216
+ export function isHtmlString(text: string): boolean {
217
+ if (text.length < 2 || !text.startsWith("<") || !text.endsWith(">")) {
218
+ return false;
219
+ }
220
+ let depth = 0;
221
+ for (let i = 0; i < text.length; i++) {
222
+ const c = text.codePointAt(i);
223
+ if (c === 0x3c) {
224
+ depth++;
225
+ } else if (c === 0x3e) {
226
+ depth--;
227
+ if (depth === 0 && i !== text.length - 1) {
228
+ return false;
229
+ }
230
+ }
231
+ }
232
+ return depth === 0;
233
+ }
234
+
235
+ /**
236
+ * The value of an attribute cell as text, or null when it is unset.
237
+ * @param table - the node, edge or graph table
238
+ * @param name - the attribute
239
+ * @param i - the row
240
+ * @returns the text
241
+ */
242
+ function cellText(table: GraphSnapshot["nodes"], name: string, i: number): string | null {
243
+ if (!table.has(name)) {
244
+ return null;
245
+ }
246
+ const v = plainCell(table.value(name, i));
247
+ if (typeof v === "string") {
248
+ return v;
249
+ }
250
+ return typeof v === "number" || typeof v === "boolean" ? String(v) : null;
251
+ }
252
+
253
+ /**
254
+ * A text, or null when it is empty.
255
+ * @param v - the text
256
+ * @returns the text
257
+ */
258
+ function nonEmpty(v: string | null): string | null {
259
+ return v === null || v === "" ? null : v;
260
+ }
261
+
262
+ /**
263
+ * The tokens of a `style` value: split on commas, lowercased, blanks before a token skipped (blanks after it make it
264
+ * a token Graphviz does not know, so they are kept).
265
+ * @param raw - the value
266
+ * @param at - where it was read
267
+ * @returns the tokens
268
+ */
269
+ function styleTokens(raw: string | null, at: CanonSite): string[] {
270
+ if (raw === null) {
271
+ return [];
272
+ }
273
+ if (raw.length > MAX_STYLE_VALUE_LENGTH) {
274
+ noteAt(at, STYLE_ISSUE.VALUE_TOO_LONG, `${String(raw.length)} characters`);
275
+ return [];
276
+ }
277
+ return raw
278
+ .toLowerCase()
279
+ .split(",")
280
+ .map((t) => t.replace(/^[ \t\r\n]+/, ""))
281
+ .filter((t) => t !== "");
282
+ }
283
+
284
+ /**
285
+ * The pen width a style's `setlinewidth(n)` gives, or null.
286
+ * @param tokens - the style's tokens
287
+ * @returns the width's text
288
+ */
289
+ function setLineWidth(tokens: readonly string[]): string | null {
290
+ for (const t of tokens) {
291
+ if (t.startsWith("setlinewidth(") && t.endsWith(")")) {
292
+ return t.slice("setlinewidth(".length, -1).trim();
293
+ }
294
+ }
295
+ return null;
296
+ }
297
+
298
+ /**
299
+ * Whether an attribute is one of Graphviz's true spellings (`true`, `yes`, a non-zero number; `fixedsize` also
300
+ * takes `shape`).
301
+ * @param raw - the value
302
+ * @returns true when set
303
+ */
304
+ function isTrue(raw: string | null): boolean {
305
+ if (raw === null) {
306
+ return false;
307
+ }
308
+ const t = raw.trim().toLowerCase();
309
+ return TRUE_TEXTS.has(t) || (/^-?\d+$/.test(t) && Number(t) !== 0);
310
+ }
311
+
312
+ /**
313
+ * The text a label draws: an HTML label's text (also for a quoted label equal to an HTML label's content elsewhere
314
+ * in the file, since Graphviz 2.43 marks HTML on a pooled string), a record's fields, the escapes applied.
315
+ * @param raw - the label as the file gives it
316
+ * @param names - what the escapes stand for
317
+ * @param html - the file's HTML strings
318
+ * @param record - whether the node is a record
319
+ * @param at - where it was read
320
+ * @returns the text
321
+ */
322
+ function labelText(
323
+ raw: string,
324
+ names: DotEscapeNames,
325
+ html: ReadonlySet<string>,
326
+ record: boolean,
327
+ at: CanonSite,
328
+ ): string {
329
+ if (isHtmlString(raw)) {
330
+ return htmlLabelText(raw, at);
331
+ }
332
+ if (html.has(`<${raw}>`)) {
333
+ return htmlLabelText(`<${raw}>`, at);
334
+ }
335
+ return dotLabelText(record ? recordLabelText(raw, at) : raw, names, at);
336
+ }
337
+
338
+ /**
339
+ * Reads a color into a property and its alpha into the property's opacity.
340
+ * @param raw - the color
341
+ * @param scheme - the object's color scheme
342
+ * @param use - how Graphviz draws it
343
+ * @param property - the color property
344
+ * @param opacity - its opacity property, or null when the alpha is dropped
345
+ * @param put - receives the values
346
+ * @param at - the sites
347
+ * @returns the alpha, or null when the color is absent
348
+ */
349
+ function readColor(
350
+ raw: string | null,
351
+ scheme: string | null,
352
+ use: "line" | "fill" | "edge",
353
+ property: string,
354
+ opacity: string | null,
355
+ put: Put,
356
+ at: At,
357
+ ): number | null {
358
+ if (raw === null) {
359
+ return null;
360
+ }
361
+ const c = canonColor(raw, "graphviz", at(property), { scheme, use });
362
+ if (c === null) {
363
+ return null;
364
+ }
365
+ put(property, c.color);
366
+ if (opacity !== null) {
367
+ put(opacity, c.alpha);
368
+ } else if (c.alpha > 0 || property !== "borderColor") {
369
+ // a transparent outline is no outline (the caller sets its width to 0); any other alpha is dropped, noted
370
+ colorOpacity(c, null, at(property), false);
371
+ }
372
+ return c.alpha;
373
+ }
374
+
375
+ /**
376
+ * A length in points as the shortest inches (at least 6 significant digits) that read back as the same length.
377
+ * @param points - the length
378
+ * @returns the inches
379
+ */
380
+ function inchesOf(points: number): number {
381
+ const want = num6(points);
382
+ for (let digits = 6; digits < 17; digits++) {
383
+ const inches = Number((points / DOT_POINTS_PER_INCH).toPrecision(digits));
384
+ if (num6(inches * DOT_POINTS_PER_INCH) === want) {
385
+ return inches + 0;
386
+ }
387
+ }
388
+ return points / DOT_POINTS_PER_INCH;
389
+ }
390
+
391
+ /**
392
+ * A size attribute in inches, or null with a note when it is not a non-negative number.
393
+ * @param raw - the value
394
+ * @param at - where it was read
395
+ * @returns the inches
396
+ */
397
+ function inches(raw: string | null, at: CanonSite): number | null {
398
+ if (raw === null) {
399
+ return null;
400
+ }
401
+ const v = raw.trim() === "" ? Number.NaN : Number(raw);
402
+ if (!Number.isFinite(v) || v < 0) {
403
+ noteAt(at, STYLE_ISSUE.BAD_VALUE, raw.slice(0, MAX_STYLE_VALUE_LENGTH));
404
+ return null;
405
+ }
406
+ return v;
407
+ }
408
+
409
+ /**
410
+ * Reads the attributes every element shares: font, tooltip, visibility.
411
+ * @param get - the element's attributes
412
+ * @param tokens - its style tokens
413
+ * @param scheme - its color scheme
414
+ * @param names - what the tooltip's escapes stand for
415
+ * @param put - receives the values
416
+ * @param at - the sites
417
+ */
418
+ function readCommon(
419
+ get: Attr,
420
+ tokens: readonly string[],
421
+ scheme: string | null,
422
+ names: DotEscapeNames,
423
+ put: Put,
424
+ at: At,
425
+ ): void {
426
+ if (tokens.includes("invis")) {
427
+ put("visible", false);
428
+ }
429
+ readColor(nonEmpty(get("fontcolor")), scheme, "line", "labelColor", null, put, at);
430
+ const size = get("fontsize");
431
+ put("labelSize", size === null ? null : canonNumber(size, at("labelSize")));
432
+ const font = get("fontname");
433
+ if (font !== null && font.length <= MAX_STYLE_VALUE_LENGTH) {
434
+ put("labelFont", font);
435
+ }
436
+ const tip = get("tooltip") ?? get("edgetooltip");
437
+ if (tip !== null) {
438
+ put("tooltip", dotLabelText(tip, names, at("tooltip")));
439
+ }
440
+ }
441
+
442
+ /** What the parts of readNode() read: the node's attributes and what is worked out from them once. */
443
+ interface NodeRead {
444
+ readonly get: Attr;
445
+ readonly cx: NodeContext;
446
+ readonly put: Put;
447
+ readonly at: At;
448
+ readonly tokens: readonly string[];
449
+ /** The shape as written, or null. */
450
+ readonly shapeRaw: string | null;
451
+ /** The shape, lowercase ("" when none is written). */
452
+ readonly shape: string;
453
+ /** Whether it is a point (never a cluster). */
454
+ readonly point: boolean;
455
+ readonly scheme: string | null;
456
+ readonly color: string | null;
457
+ }
458
+
459
+ /**
460
+ * Reads one node, or one cluster's container, the way Graphviz draws it.
461
+ * @param get - its attributes
462
+ * @param cx - its name and kind
463
+ * @param put - receives the values
464
+ * @param at - the sites
465
+ */
466
+ function readNode(get: Attr, cx: NodeContext, put: Put, at: At): void {
467
+ const tokens = styleTokens(get("style"), at("visible"));
468
+ const shapeRaw = nonEmpty(get("shape"));
469
+ const shape = (shapeRaw ?? "").toLowerCase();
470
+ const n: NodeRead = {
471
+ get,
472
+ cx,
473
+ put,
474
+ at,
475
+ tokens,
476
+ shapeRaw,
477
+ shape,
478
+ point: !cx.cluster && shape === "point",
479
+ scheme: get("colorscheme"),
480
+ color: nonEmpty(get("color")),
481
+ };
482
+ // in a node's label \E stands for nothing (measured with dot 2.43); \T and \H stand for their letters
483
+ const names: DotEscapeNames = cx.cluster ? { graph: cx.name } : { node: cx.name, graph: cx.graph, edge: "" };
484
+ readCommon(get, tokens, n.scheme, names, put, at);
485
+ if (cx.cluster) {
486
+ readClusterFill(n);
487
+ } else {
488
+ readNodeFill(n);
489
+ }
490
+ readNodeOutline(n);
491
+ readNodeShape(n);
492
+ // label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
493
+ const label = n.point ? null : get(LABEL_ATTRIBUTE);
494
+ if (label !== null) {
495
+ put("label", labelText(label, names, cx.html, RECORD.has(shape), at("label")));
496
+ }
497
+ if (!cx.cluster) {
498
+ readSize(get, shape, n.point, label ?? (n.point ? null : cx.name), put, at);
499
+ }
500
+ if (Number(get("peripheries")) > 1 || nonEmpty(get("xlabel")) !== null) {
501
+ noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, nonEmpty(get("xlabel")) === null ? "peripheries" : "xlabel");
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Reads a cluster's fill: its fill color when filled, else its background.
507
+ * @param n - the cluster
508
+ */
509
+ function readClusterFill(n: NodeRead): void {
510
+ const { get, tokens } = n;
511
+ const filled = tokens.includes("filled") || tokens.includes("radial") || tokens.includes("striped");
512
+ const bg = nonEmpty(get("bgcolor"));
513
+ const raw = filled ? (nonEmpty(get("fillcolor")) ?? n.color ?? bg ?? "lightgrey") : bg;
514
+ readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
515
+ }
516
+
517
+ /**
518
+ * Reads a node's fill: a point is always filled; a striped fill fills a box and a wedged one an ellipse only.
519
+ * @param n - the node
520
+ */
521
+ function readNodeFill(n: NodeRead): void {
522
+ const { shape, point } = n;
523
+ const fillcolor = nonEmpty(n.get("fillcolor"));
524
+ const filled =
525
+ point ||
526
+ n.tokens.some(
527
+ (t) =>
528
+ t === "filled" ||
529
+ t === "radial" ||
530
+ (t === "striped" && BOX_LIKE.has(shape)) ||
531
+ (t === "wedged" && ELLIPSE_LIKE.has(shape)),
532
+ );
533
+ if (filled) {
534
+ const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
535
+ readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
536
+ } else if (fillcolor !== null) {
537
+ noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Reads a node's or cluster's outline: its color, width and dashes.
543
+ * @param n - the node
544
+ */
545
+ function readNodeOutline(n: NodeRead): void {
546
+ const { get, cx, put, at, tokens } = n;
547
+ if (!cx.cluster && NO_OUTLINE.has(n.shape)) {
548
+ put("borderWidth", 0);
549
+ } else {
550
+ const outline = cx.cluster ? (nonEmpty(get("pencolor")) ?? n.color) : n.color;
551
+ const alpha = readColor(outline, n.scheme, "line", "borderColor", null, put, at);
552
+ const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
553
+ if (alpha === 0) {
554
+ put("borderWidth", 0);
555
+ } else if (width !== null) {
556
+ put("borderWidth", penWidth(width, get("penwidth") !== null, at("borderWidth")));
557
+ } else if (tokens.includes("bold")) {
558
+ put("borderWidth", 2);
559
+ }
560
+ }
561
+ if (tokens.includes("dashed")) {
562
+ put("borderStyle", "dashed");
563
+ } else if (tokens.includes("dotted")) {
564
+ put("borderStyle", "dotted");
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is.
570
+ * @param n - the node
571
+ */
572
+ function readNodeShape(n: NodeRead): void {
573
+ const rounded = n.tokens.includes("rounded");
574
+ if (n.cx.cluster) {
575
+ n.put("shape", rounded ? "round-rectangle" : "rectangle");
576
+ } else if (n.shapeRaw !== null) {
577
+ n.put(
578
+ "shape",
579
+ rounded && BOX_LIKE.has(n.shape)
580
+ ? "round-rectangle"
581
+ : canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape")),
582
+ );
583
+ }
584
+ }
585
+
586
+ /**
587
+ * A pen width as Graphviz draws it: a negative `penwidth` is raised to 0, its minimum, and noted.
588
+ * @param raw - the `penwidth` or `setlinewidth` value
589
+ * @param attribute - whether it is the `penwidth` attribute
590
+ * @param site - where it was read
591
+ * @returns the width, or null when it does not parse
592
+ */
593
+ function penWidth(raw: string, attribute: boolean, site: CanonSite): number | null {
594
+ const value = Number(raw.trim());
595
+ if (attribute && Number.isFinite(value) && value < 0) {
596
+ noteAt(site, STYLE_ISSUE.BAD_VALUE, raw.slice(0, MAX_STYLE_VALUE_LENGTH));
597
+ return 0;
598
+ }
599
+ return canonNumber(raw, site);
600
+ }
601
+
602
+ /**
603
+ * Reads a node's width and height: exact where Graphviz draws them exactly (`fixedsize`, an empty label that is not
604
+ * a record's, a point), otherwise minimums it grows to fit the label, noted.
605
+ * @param get - the node's attributes
606
+ * @param shape - its shape, lowercase
607
+ * @param point - whether it is a point
608
+ * @param label - the label it shows, or null
609
+ * @param put - receives the values
610
+ * @param at - the sites
611
+ */
612
+ function readSize(get: Attr, shape: string, point: boolean, label: string | null, put: Put, at: At): void {
613
+ let w = inches(get("width"), at("width"));
614
+ let h = inches(get("height"), at("height"));
615
+ const same = point || CIRCLE_LIKE.has(shape) || isTrue(get("regular"));
616
+ if (shape === "plain") {
617
+ // plain is plaintext with its width and height forced to 0: exactly the size of its label
618
+ w = h = 0;
619
+ } else if (w === null && h === null) {
620
+ if (point) {
621
+ put("width", num6(POINT_SIZE * DOT_POINTS_PER_INCH));
622
+ put("height", num6(POINT_SIZE * DOT_POINTS_PER_INCH));
623
+ return;
624
+ }
625
+ if (!same) {
626
+ return;
627
+ }
628
+ // a regular shape takes the smaller of the default width and height (0.5 inch) for both
629
+ w = h = REGULAR_SIZE;
630
+ }
631
+ let ww = w;
632
+ let hh = h;
633
+ if (point) {
634
+ ww = hh = Math.min(w ?? Infinity, h ?? Infinity);
635
+ } else if (same) {
636
+ ww = hh = Math.max(w ?? 0, h ?? 0);
637
+ }
638
+ const exact = point || isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape));
639
+ if (!exact) {
640
+ noteAt(at("width"), STYLE_ISSUE.SIZE_MINIMUM, "grows to fit its label");
641
+ }
642
+ put("width", ww === null ? null : canonNumber(ww, at("width"), DOT_POINTS_PER_INCH));
643
+ put("height", hh === null ? null : canonNumber(hh, at("height"), DOT_POINTS_PER_INCH));
644
+ }
645
+
646
+ /**
647
+ * Reads one edge the way Graphviz draws it.
648
+ * @param get - its attributes
649
+ * @param cx - its names and direction
650
+ * @param put - receives the values
651
+ * @param at - the sites
652
+ */
653
+ function readEdge(get: Attr, cx: EdgeContext, put: Put, at: At): void {
654
+ const tokens = styleTokens(get("style"), at("visible"));
655
+ const scheme = get("colorscheme");
656
+ readCommon(get, tokens, scheme, cx.names, put, at);
657
+ readColor(nonEmpty(get("color")), scheme, "edge", "color", "opacity", put, at);
658
+ readEdgeLine(get, tokens, put, at);
659
+ readEdgeArrows(get, cx, put, at);
660
+ for (const [attribute, property] of [
661
+ [LABEL_ATTRIBUTE, "label"],
662
+ ["headlabel", "targetLabel"],
663
+ ["taillabel", "sourceLabel"],
664
+ ] as const) {
665
+ const raw = get(attribute);
666
+ if (raw !== null) {
667
+ put(property, labelText(raw, cx.names, cx.html, false, at(property)));
668
+ }
669
+ }
670
+ if (nonEmpty(get("xlabel")) !== null) {
671
+ noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Reads an edge's line: its width and dashes.
677
+ * @param get - its attributes
678
+ * @param tokens - its style tokens
679
+ * @param put - receives the values
680
+ * @param at - the sites
681
+ */
682
+ function readEdgeLine(get: Attr, tokens: readonly string[], put: Put, at: At): void {
683
+ const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
684
+ if (width !== null) {
685
+ put("width", penWidth(width, get("penwidth") !== null, at("width")));
686
+ } else if (tokens.includes("bold")) {
687
+ put("width", 2);
688
+ }
689
+ if (tokens.includes("dashed")) {
690
+ put("lineStyle", "dashed");
691
+ } else if (tokens.includes("dotted")) {
692
+ put("lineStyle", "dotted");
693
+ } else if (tokens.includes("solid") || tokens.includes("bold")) {
694
+ put("lineStyle", "solid");
695
+ } else if (tokens.includes("tapered")) {
696
+ put("lineStyle", "other:tapered");
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Reads an edge's arrows and their size.
702
+ * @param get - its attributes
703
+ * @param cx - its names and direction
704
+ * @param put - receives the values
705
+ * @param at - the sites
706
+ */
707
+ function readEdgeArrows(get: Attr, cx: EdgeContext, put: Put, at: At): void {
708
+ const dirRaw = nonEmpty(get("dir"));
709
+ const head = nonEmpty(get("arrowhead"));
710
+ const tail = nonEmpty(get("arrowtail"));
711
+ // with none of these the tool-default rule draws the arrow of the graph's kind
712
+ if (dirRaw !== null || head !== null || tail !== null) {
713
+ const dir = (dirRaw ?? (cx.directed ? "forward" : "none")).trim().toLowerCase();
714
+ const forward = dir === "forward" || dir === "both";
715
+ const back = dir === "back" || dir === "both";
716
+ put("targetArrow", forward ? canonEnum("arrow", head ?? "normal", "graphviz", at("targetArrow")) : "none");
717
+ put("sourceArrow", back ? canonEnum("arrow", tail ?? "normal", "graphviz", at("sourceArrow")) : "none");
718
+ }
719
+ const size = get("arrowsize");
720
+ if (size !== null) {
721
+ put("arrowSize", canonNumber(size, at("arrowSize")));
722
+ }
723
+ }
724
+
725
+ /**
726
+ * The HTML strings of a file: its HTML node names and labels, for Graphviz's shared string pool.
727
+ * @param snapshot - the graph
728
+ * @returns the strings, with their brackets
729
+ */
730
+ function htmlStrings(snapshot: GraphSnapshot): Set<string> {
731
+ const out = new Set<string>();
732
+ for (let i = 0; i < snapshot.nodeCount; i++) {
733
+ const id = snapshot.ids.idOf(i);
734
+ if (typeof id === "string" && isHtmlString(id)) {
735
+ out.add(id);
736
+ }
737
+ }
738
+ for (const [table, n] of [
739
+ [snapshot.nodes, snapshot.nodeCount],
740
+ [snapshot.edges, snapshot.edgeCount],
741
+ ] as const) {
742
+ for (const name of [LABEL_ATTRIBUTE, "headlabel", "taillabel"].filter((c) => table.has(c))) {
743
+ addHtmlCells(table, name, n, out);
744
+ }
745
+ }
746
+ return out;
747
+ }
748
+
749
+ /**
750
+ * Adds the HTML strings of one label column.
751
+ * @param table - the node or edge table
752
+ * @param name - the column
753
+ * @param n - its rows
754
+ * @param out - receives the strings
755
+ */
756
+ function addHtmlCells(table: GraphSnapshot["nodes"], name: string, n: number, out: Set<string>): void {
757
+ for (let i = 0; i < n; i++) {
758
+ const v = cellText(table, name, i);
759
+ if (v !== null && isHtmlString(v)) {
760
+ out.add(v);
761
+ }
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Which edges are directed: every edge of a directed graph, or as its direction column says.
767
+ * @param snapshot - the graph
768
+ * @returns per edge
769
+ */
770
+ function directedEdges(snapshot: GraphSnapshot): boolean[] {
771
+ const out = new Array<boolean>(snapshot.edgeCount).fill(snapshot.directed);
772
+ const col = snapshot.directed ? snapshot.edges.byRole("directed") : null;
773
+ if (col !== null) {
774
+ for (let e = 0; e < snapshot.edgeCount; e++) {
775
+ const v = plainCell(col.value(e));
776
+ if (typeof v === "boolean") {
777
+ out[e] = v;
778
+ }
779
+ }
780
+ }
781
+ return out;
782
+ }
783
+
784
+ /**
785
+ * Whether a node is a cluster's container.
786
+ * @param snapshot - the graph
787
+ * @param i - the node
788
+ * @returns true for a container
789
+ */
790
+ function isCluster(snapshot: GraphSnapshot, i: number): boolean {
791
+ return snapshot.nodes.has(CLUSTER_COLUMN) && snapshot.nodes.value(CLUSTER_COLUMN, i) === true;
792
+ }
793
+
794
+ /**
795
+ * Per element, what its labels' escapes and defaults depend on.
796
+ * @param snapshot - the graph
797
+ * @param written - the direction every edge is written with when the graph mixes both, or null to read it per edge
798
+ * @returns the contexts
799
+ */
800
+ function contexts(
801
+ snapshot: GraphSnapshot,
802
+ written: boolean | null = null,
803
+ ): {
804
+ readonly node: (i: number) => NodeContext;
805
+ readonly edge: (e: number) => EdgeContext;
806
+ } {
807
+ const html = htmlStrings(snapshot);
808
+ const graph = snapshot.meta.name ?? "";
809
+ const own = directedEdges(snapshot);
810
+ // a graph mixing both directions is written with one, every edge then drawn as that one
811
+ const directed = written === null || own.every((d) => d === snapshot.directed) ? own : own.map(() => written);
812
+ const name = (i: number): string => String(snapshot.ids.idOf(i));
813
+ return {
814
+ node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html }),
815
+ edge: (e) => {
816
+ const tail = name(snapshot.edgeSource(e));
817
+ const head = name(snapshot.edgeTarget(e));
818
+ return {
819
+ names: { graph, tail, head, edge: `${tail}${directed[e] ? "->" : "--"}${head}` },
820
+ directed: directed[e],
821
+ html,
822
+ };
823
+ },
824
+ };
825
+ }
826
+
827
+ /**
828
+ * The label a node shows when it has none of its own: its name, escapes applied (an HTML name shows its text); null
829
+ * for a cluster's container and a point.
830
+ * @param cx - the node
831
+ * @param shape - its shape, lowercase
832
+ * @param notes - where an HTML name is noted
833
+ * @returns the label, or null
834
+ */
835
+ function defaultLabel(cx: NodeContext, shape: string, notes: StyleNotes): string | null {
836
+ if (cx.cluster || shape === "point") {
837
+ return null;
838
+ }
839
+ const at: CanonSite = { notes, target: "node", property: "label" };
840
+ return isHtmlString(cx.name)
841
+ ? htmlLabelText(cx.name, at)
842
+ : dotLabelText(String.raw`\N`, { node: cx.name, graph: cx.graph }, at);
843
+ }
844
+
845
+ /**
846
+ * The columns of a table that hold styles: Graphviz's style attributes and graphty's per-element style values.
847
+ * @param table - the node or edge table
848
+ * @param attributes - the style attributes of the table
849
+ * @returns the column names
850
+ */
851
+ function styleColumns(table: GraphSnapshot["nodes"], attributes: readonly string[]): string[] {
852
+ const own = new Set([...attributes, ...NOTED_ATTRIBUTES]);
853
+ return [...table.names()].filter((c) => own.has(c) || c.startsWith("graphty.style."));
854
+ }
855
+
856
+ /**
857
+ * The styles of a DOT file, read the way Graphviz draws it, with Graphviz's defaults beneath: an unfilled black
858
+ * ellipse 54 x 36 labeled with its name, black edges with a target arrow in a directed graph. A file graphty wrote
859
+ * gives back its `graphty.styles` block.
860
+ * @param snapshot - the graph the import built
861
+ * @param context - the work budget
862
+ * @returns the styles
863
+ */
864
+ export function dotStyles(snapshot: GraphSnapshot, context: StyleReadContext): ImporterStyles {
865
+ const notes = new StyleNotes();
866
+ const cx = contexts(snapshot);
867
+ const node = valueColumns(snapshot.nodeCount);
868
+ const nameColumn = new Array<StyleValue | null>(snapshot.nodeCount).fill(null);
869
+ const clusterColumn = new Array<StyleValue | null>(snapshot.nodeCount).fill(null);
870
+ let named = 0;
871
+ let clusters = 0;
872
+ for (let i = 0; i < snapshot.nodeCount; i++) {
873
+ context.budget.spend("node", NODE_ATTRIBUTES.length);
874
+ const get: Attr = (name) => cellText(snapshot.nodes, name, i);
875
+ const ncx = cx.node(i);
876
+ const at: At = (property) => ({ notes, target: "node", property, element: i, budget: context.budget });
877
+ readNode(
878
+ get,
879
+ ncx,
880
+ (p, v) => {
881
+ node.put(i, p, v);
882
+ },
883
+ at,
884
+ );
885
+ const shown = defaultLabel(ncx, (get("shape") ?? "").toLowerCase(), notes);
886
+ if (shown !== null) {
887
+ nameColumn[i] = shown;
888
+ named++;
889
+ }
890
+ if (ncx.cluster) {
891
+ clusterColumn[i] = true;
892
+ clusters++;
893
+ }
894
+ }
895
+ const edge = valueColumns(snapshot.edgeCount);
896
+ for (let e = 0; e < snapshot.edgeCount; e++) {
897
+ context.budget.spend("edge", EDGE_ATTRIBUTES.length);
898
+ const get: Attr = (name) => cellText(snapshot.edges, name, e);
899
+ const at: At = (property) => ({ notes, target: "edge", property, element: e, budget: context.budget });
900
+ readEdge(
901
+ get,
902
+ cx.edge(e),
903
+ (p, v) => {
904
+ edge.put(e, p, v);
905
+ },
906
+ at,
907
+ );
908
+ }
909
+ const graph: Record<string, StyleValue> = {};
910
+ const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
911
+ if (snapshot.graph.has("bgcolor") && bg !== null) {
912
+ readColor(
913
+ bg,
914
+ cellText(snapshot.graph, "colorscheme", 0),
915
+ "fill",
916
+ "background",
917
+ null,
918
+ (p, v) => {
919
+ if (v !== null) {
920
+ graph[p] = v;
921
+ }
922
+ },
923
+ (property) => ({ notes, target: "graph", property }),
924
+ );
925
+ }
926
+ const blockText = cellText(snapshot.graph, STYLE_BLOCK_NAME, 0);
927
+ if (blockText !== null) {
928
+ exactFromBlock(snapshot, blockText, { node: node.values, edge: edge.values }, cx);
929
+ }
930
+ // Graphviz's defaults that are rules: each node shows its name, and a directed edge has an arrow at its target
931
+ const nodeRules: StyleRule[] = [];
932
+ const nodeColumns = new Map<string, StyleColumn>();
933
+ if (named > 0) {
934
+ nodeColumns.set(NAME_KEY, nameColumn);
935
+ nodeRules.push({
936
+ id: "node.tool.name-label",
937
+ target: "node",
938
+ role: "passthrough",
939
+ origin: "tool",
940
+ where: { has: { style: NAME_KEY } },
941
+ map: { label: { kind: "passthrough", by: { style: NAME_KEY } } },
942
+ facts: { elements: named, reason: "NODE_NAME" },
943
+ });
944
+ }
945
+ const features: StyleFeature[] = clusters > 0 ? [{ ref: { style: CLUSTER_KEY }, values: clusterColumn }] : [];
946
+ const edgeRules: StyleRule[] = [];
947
+ const edgeColumns = new Map<string, StyleColumn>();
948
+ const directed = directedEdges(snapshot);
949
+ if (directed.some(Boolean)) {
950
+ edgeColumns.set(
951
+ DIRECTED_KEY,
952
+ directed.map((d) => (d ? true : null)),
953
+ );
954
+ edgeRules.push({
955
+ id: "edge.tool.arrow",
956
+ target: "edge",
957
+ role: "group",
958
+ origin: "tool",
959
+ where: { has: { style: DIRECTED_KEY } },
960
+ set: { targetArrow: "triangle" },
961
+ });
962
+ }
963
+ return {
964
+ canonical: true,
965
+ dialect: "graphviz",
966
+ tool: "graphviz",
967
+ units: "px",
968
+ node: { values: node.values, rules: nodeRules, columns: nodeColumns, features },
969
+ edge: { values: edge.values, rules: edgeRules, columns: edgeColumns },
970
+ graph,
971
+ notes: notes.list(),
972
+ readFrom: {
973
+ node: styleColumns(snapshot.nodes, NODE_ATTRIBUTES),
974
+ edge: styleColumns(snapshot.edges, EDGE_ATTRIBUTES),
975
+ },
976
+ };
977
+ }
978
+
979
+ // ============================================================ writing
980
+
981
+ /** One element's value of a property. */
982
+ type Get = (property: string) => StyleValue | null;
983
+
984
+ /** Records a value written as the nearest one Graphviz has. */
985
+ type Approximate = (property: string, value: StyleValue) => void;
986
+
987
+ /** A written attribute value: text, or a number written as one. */
988
+ type Written = string | number;
989
+
990
+ /** Neutral shapes by the Graphviz shape that draws them exactly. */
991
+ const SHAPES: ReadonlyMap<string, string> = new Map([
992
+ ["ellipse", "ellipse"],
993
+ ["rectangle", "box"],
994
+ ["round-rectangle", "box"],
995
+ ["triangle", "triangle"],
996
+ ["diamond", "diamond"],
997
+ ["pentagon", "pentagon"],
998
+ ["hexagon", "hexagon"],
999
+ ["octagon", "octagon"],
1000
+ ["parallelogram", "parallelogram"],
1001
+ ["trapezoid", "trapezium"],
1002
+ ["star", "star"],
1003
+ ["none", "none"],
1004
+ ]);
1005
+
1006
+ /** Neutral arrows by the Graphviz arrow that draws them. */
1007
+ const ARROWS: ReadonlyMap<string, string> = new Map([
1008
+ ["none", "none"],
1009
+ ["triangle", "normal"],
1010
+ ["triangle-open", "onormal"],
1011
+ ["vee", "vee"],
1012
+ ["tee", "tee"],
1013
+ ["diamond", "diamond"],
1014
+ ["diamond-open", "odiamond"],
1015
+ ["circle", "dot"],
1016
+ ["circle-open", "odot"],
1017
+ ["square", "box"],
1018
+ ["crow", "crow"],
1019
+ ["half", "lnormal"],
1020
+ ["inverted", "inv"],
1021
+ ]);
1022
+
1023
+ /** Line and border styles Graphviz draws exactly; the others are written as the nearest. */
1024
+ const STROKES: ReadonlyMap<string, string> = new Map([
1025
+ ["dashed", "dashed"],
1026
+ ["dotted", "dotted"],
1027
+ ]);
1028
+
1029
+ /**
1030
+ * The attribute values Graphviz draws when an attribute is absent, from the tool-defaults table: the writer leaves
1031
+ * them out, so a file states what differs from Graphviz's drawing (and a size stays one Graphviz may grow).
1032
+ * @param target - nodes or edges
1033
+ * @returns the default text of each attribute
1034
+ */
1035
+ function graphvizDefaults(target: StyleTarget): ReadonlyMap<string, Written> {
1036
+ const t = toolDefaults("graphviz")?.[target] as Readonly<Record<string, { readonly value: unknown } | undefined>>;
1037
+ const out = new Map<string, Written>();
1038
+ const add = (attribute: string, property: string, scale = 1): void => {
1039
+ const v = t[property]?.value;
1040
+ if (typeof v === "number") {
1041
+ out.set(attribute, num6(v / scale));
1042
+ } else if (typeof v === "string") {
1043
+ out.set(attribute, v);
1044
+ }
1045
+ };
1046
+ add("fontcolor", "labelColor");
1047
+ add("fontsize", "labelSize");
1048
+ add("fontname", "labelFont");
1049
+ if (target === "node") {
1050
+ add("color", "borderColor");
1051
+ add("pencolor", "borderColor");
1052
+ add("penwidth", "borderWidth");
1053
+ add("shape", "shape");
1054
+ add("width", "width", DOT_POINTS_PER_INCH);
1055
+ add("height", "height", DOT_POINTS_PER_INCH);
1056
+ } else {
1057
+ add("color", "color");
1058
+ add("penwidth", "width");
1059
+ add("arrowsize", "arrowSize");
1060
+ }
1061
+ return out;
1062
+ }
1063
+
1064
+ const DEFAULT_ATTRIBUTES: Readonly<Record<StyleTarget, ReadonlyMap<string, Written>>> = {
1065
+ node: graphvizDefaults("node"),
1066
+ edge: graphvizDefaults("edge"),
1067
+ };
1068
+
1069
+ /**
1070
+ * Leaves out every attribute whose value is the one Graphviz draws without it.
1071
+ * @param out - the attributes, changed in place
1072
+ * @param target - nodes or edges
1073
+ * @returns the attributes
1074
+ */
1075
+ function withoutDefaults(out: Map<string, Written>, target: StyleTarget): Map<string, Written> {
1076
+ for (const [attribute, value] of DEFAULT_ATTRIBUTES[target]) {
1077
+ if (out.get(attribute) === value) {
1078
+ out.delete(attribute);
1079
+ }
1080
+ }
1081
+ if (!out.has("width") && !out.has("height")) {
1082
+ out.delete("fixedsize");
1083
+ }
1084
+ return out;
1085
+ }
1086
+
1087
+ /**
1088
+ * A color as Graphviz writes it: `#rrggbb`, or `#rrggbbaa` with an opacity below 1.
1089
+ * @param color - the canonical color
1090
+ * @param opacity - the opacity, or null
1091
+ * @returns the text
1092
+ */
1093
+ function gvColor(color: string, opacity: StyleValue | null): string {
1094
+ if (typeof opacity !== "number" || opacity >= 1) {
1095
+ return color;
1096
+ }
1097
+ const a = Math.max(0, Math.min(255, Math.round(opacity * 255)));
1098
+ return color + a.toString(16).padStart(2, "0");
1099
+ }
1100
+
1101
+ /**
1102
+ * A label's text as a DOT label: backslashes and line breaks escaped, so Graphviz's escape passes give the text back.
1103
+ * @param text - the text
1104
+ * @returns the label
1105
+ */
1106
+ function escapeLabel(text: string): string {
1107
+ return text.replaceAll("\\", String.raw`\\`).replaceAll("\n", String.raw`\n`);
1108
+ }
1109
+
1110
+ /**
1111
+ * A shape, line or arrow value in Graphviz's spelling.
1112
+ * @param table - the exact spellings
1113
+ * @param value - the neutral value
1114
+ * @param nearest - the spelling written when there is no exact one
1115
+ * @param property - the property, for the approximation note
1116
+ * @param approximate - records approximations
1117
+ * @returns the spelling
1118
+ */
1119
+ function spelled(
1120
+ table: ReadonlyMap<string, string>,
1121
+ value: string,
1122
+ nearest: string,
1123
+ property: string,
1124
+ approximate: Approximate,
1125
+ ): string {
1126
+ const exact = table.get(value);
1127
+ if (exact !== undefined) {
1128
+ return exact;
1129
+ }
1130
+ if (value.startsWith("other:")) {
1131
+ return value.slice("other:".length);
1132
+ }
1133
+ approximate(property, value);
1134
+ return nearest;
1135
+ }
1136
+
1137
+ /**
1138
+ * The font, tooltip and visibility attributes every element shares.
1139
+ * @param get - the element's values
1140
+ * @param out - the attributes, added to
1141
+ * @param tokens - the style tokens, added to
1142
+ */
1143
+ function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): void {
1144
+ const color = get("labelColor");
1145
+ if (typeof color === "string") {
1146
+ out.set("fontcolor", color);
1147
+ }
1148
+ const size = get("labelSize");
1149
+ if (typeof size === "number") {
1150
+ out.set("fontsize", size);
1151
+ }
1152
+ const font = get("labelFont");
1153
+ if (typeof font === "string") {
1154
+ out.set("fontname", font);
1155
+ }
1156
+ const tip = get("tooltip");
1157
+ if (typeof tip === "string") {
1158
+ out.set("tooltip", escapeLabel(tip));
1159
+ }
1160
+ if (get("visible") === false) {
1161
+ tokens.push("invis");
1162
+ }
1163
+ }
1164
+
1165
+ /**
1166
+ * A node's or cluster's Graphviz attributes from its values.
1167
+ * @param get - the node's values
1168
+ * @param cx - the node's name and kind
1169
+ * @param approximate - records approximations
1170
+ * @param notes - where an HTML name is noted
1171
+ * @returns the attributes
1172
+ */
1173
+ function nodeAttributes(get: Get, cx: NodeContext, approximate: Approximate, notes: StyleNotes): Map<string, Written> {
1174
+ const out = new Map<string, Written>();
1175
+ const tokens: string[] = [];
1176
+ writeCommon(get, out, tokens);
1177
+ writeNodeOutline(get, cx, approximate, out, tokens);
1178
+ const gvShape = writeNodeShape(get, cx, approximate, out, tokens);
1179
+ if (!cx.cluster) {
1180
+ writeNodeSize(get, out);
1181
+ }
1182
+ // the label is written where it differs from what Graphviz shows without one (the name), "" for none
1183
+ const label = get("label");
1184
+ const shown = defaultLabel(cx, gvShape, notes);
1185
+ if (typeof label === "string" && label !== shown) {
1186
+ out.set(LABEL_ATTRIBUTE, escapeLabel(label));
1187
+ } else if (label === null && shown !== null) {
1188
+ out.set(LABEL_ATTRIBUTE, "");
1189
+ }
1190
+ if (tokens.length > 0) {
1191
+ out.set("style", tokens.join(","));
1192
+ }
1193
+ return withoutDefaults(out, "node");
1194
+ }
1195
+
1196
+ /**
1197
+ * A node's or cluster's fill and outline attributes.
1198
+ * @param get - the node's values
1199
+ * @param cx - the node's name and kind
1200
+ * @param approximate - records approximations
1201
+ * @param out - the attributes, added to
1202
+ * @param tokens - the style tokens, added to
1203
+ */
1204
+ function writeNodeOutline(
1205
+ get: Get,
1206
+ cx: NodeContext,
1207
+ approximate: Approximate,
1208
+ out: Map<string, Written>,
1209
+ tokens: string[],
1210
+ ): void {
1211
+ const fill = get("fill");
1212
+ const fillOpacity = get("fillOpacity");
1213
+ if (typeof fill === "string" && fillOpacity !== 0) {
1214
+ tokens.push("filled");
1215
+ out.set("fillcolor", gvColor(fill, fillOpacity));
1216
+ }
1217
+ const border = get("borderColor");
1218
+ if (typeof border === "string") {
1219
+ out.set(cx.cluster ? "pencolor" : "color", border);
1220
+ }
1221
+ const borderWidth = get("borderWidth");
1222
+ if (typeof borderWidth === "number") {
1223
+ out.set("penwidth", borderWidth);
1224
+ }
1225
+ const borderStyle = get("borderStyle");
1226
+ if (typeof borderStyle === "string" && borderStyle !== "solid") {
1227
+ const nearest = borderStyle.includes("dash") ? "dashed" : "solid";
1228
+ tokens.push(spelled(STROKES, borderStyle, nearest, "borderStyle", approximate));
1229
+ }
1230
+ }
1231
+
1232
+ /**
1233
+ * A node's shape attribute; a cluster is always a rectangle, rounded or not, and any other shape is approximated.
1234
+ * @param get - the node's values
1235
+ * @param cx - the node's name and kind
1236
+ * @param approximate - records approximations
1237
+ * @param out - the attributes, added to
1238
+ * @param tokens - the style tokens, added to
1239
+ * @returns the Graphviz shape written, "" for none
1240
+ */
1241
+ function writeNodeShape(
1242
+ get: Get,
1243
+ cx: NodeContext,
1244
+ approximate: Approximate,
1245
+ out: Map<string, Written>,
1246
+ tokens: string[],
1247
+ ): string {
1248
+ const shape = get("shape");
1249
+ if (typeof shape !== "string") {
1250
+ return "";
1251
+ }
1252
+ if (shape === "round-rectangle") {
1253
+ tokens.push("rounded");
1254
+ }
1255
+ if (cx.cluster) {
1256
+ if (shape !== "rectangle" && shape !== "round-rectangle") {
1257
+ approximate("shape", shape);
1258
+ }
1259
+ return "";
1260
+ }
1261
+ const gvShape = spelled(SHAPES, shape, shape === "vee" ? "invtriangle" : "box", "shape", approximate);
1262
+ out.set("shape", gvShape);
1263
+ return gvShape;
1264
+ }
1265
+
1266
+ /**
1267
+ * A node's width and height, fixed so Graphviz does not grow them to fit the label.
1268
+ * @param get - the node's values
1269
+ * @param out - the attributes, added to
1270
+ */
1271
+ function writeNodeSize(get: Get, out: Map<string, Written>): void {
1272
+ const width = get("width");
1273
+ const height = get("height");
1274
+ if (typeof width === "number" || typeof height === "number") {
1275
+ out.set("fixedsize", "true");
1276
+ }
1277
+ if (typeof width === "number") {
1278
+ out.set("width", inchesOf(width));
1279
+ }
1280
+ if (typeof height === "number") {
1281
+ out.set("height", inchesOf(height));
1282
+ }
1283
+ }
1284
+
1285
+ /**
1286
+ * An edge's Graphviz attributes from its values.
1287
+ * @param get - the edge's values
1288
+ * @param directed - whether the edge is directed (its default arrows)
1289
+ * @param approximate - records approximations
1290
+ * @returns the attributes
1291
+ */
1292
+ function edgeAttributes(get: Get, directed: boolean, approximate: Approximate): Map<string, Written> {
1293
+ const out = new Map<string, Written>();
1294
+ const tokens: string[] = [];
1295
+ writeCommon(get, out, tokens);
1296
+ const color = get("color");
1297
+ if (typeof color === "string") {
1298
+ out.set("color", gvColor(color, get("opacity")));
1299
+ }
1300
+ const width = get("width");
1301
+ if (typeof width === "number") {
1302
+ out.set("penwidth", width);
1303
+ }
1304
+ const line = get("lineStyle");
1305
+ if (typeof line === "string" && line !== "solid") {
1306
+ tokens.push(spelled(STROKES, line, line.includes("dash") ? "dashed" : "solid", "lineStyle", approximate));
1307
+ }
1308
+ writeEdgeArrows(get, directed, approximate, out);
1309
+ const arrowSize = get("arrowSize");
1310
+ if (typeof arrowSize === "number") {
1311
+ out.set("arrowsize", arrowSize);
1312
+ }
1313
+ for (const [property, attribute] of [
1314
+ ["label", LABEL_ATTRIBUTE],
1315
+ ["targetLabel", "headlabel"],
1316
+ ["sourceLabel", "taillabel"],
1317
+ ] as const) {
1318
+ const v = get(property);
1319
+ if (typeof v === "string") {
1320
+ out.set(attribute, escapeLabel(v));
1321
+ }
1322
+ }
1323
+ if (tokens.length > 0) {
1324
+ out.set("style", tokens.join(","));
1325
+ }
1326
+ return withoutDefaults(out, "edge");
1327
+ }
1328
+
1329
+ /**
1330
+ * An edge's arrow attributes, written where they differ from the graph's own: a target arrow on a directed edge.
1331
+ * @param get - the edge's values
1332
+ * @param directed - whether the edge is directed
1333
+ * @param approximate - records approximations
1334
+ * @param out - the attributes, added to
1335
+ */
1336
+ function writeEdgeArrows(get: Get, directed: boolean, approximate: Approximate, out: Map<string, Written>): void {
1337
+ const source = get("sourceArrow");
1338
+ const target = get("targetArrow");
1339
+ const tail = typeof source === "string" && source !== "none";
1340
+ const head = typeof target === "string" && target !== "none";
1341
+ const plain = directed ? head && !tail && target === "triangle" : !head && !tail;
1342
+ if (plain || (source === null && target === null)) {
1343
+ return;
1344
+ }
1345
+ let dir = "none";
1346
+ if (head && tail) {
1347
+ dir = "both";
1348
+ } else if (head) {
1349
+ dir = "forward";
1350
+ } else if (tail) {
1351
+ dir = "back";
1352
+ }
1353
+ out.set("dir", dir);
1354
+ if (head) {
1355
+ out.set("arrowhead", spelled(ARROWS, target, "normal", "targetArrow", approximate));
1356
+ }
1357
+ if (tail) {
1358
+ out.set("arrowtail", spelled(ARROWS, source, "normal", "sourceArrow", approximate));
1359
+ }
1360
+ }
1361
+
1362
+ /**
1363
+ * Each element's values of the properties the writer writes, as a getter per element.
1364
+ * @param picture - each element's value of a property
1365
+ * @param target - nodes or edges
1366
+ * @returns the getter
1367
+ */
1368
+ function getter(
1369
+ picture: (target: StyleTarget, property: string) => readonly (StyleValue | null)[],
1370
+ target: StyleTarget,
1371
+ ): (i: number) => Get {
1372
+ const columns = new Map<string, readonly (StyleValue | null)[]>();
1373
+ for (const p of DOT_STYLE_CAPABILITIES[target]) {
1374
+ columns.set(p, picture(target, p));
1375
+ }
1376
+ return (i) => (property) => columns.get(property)?.[i] ?? null;
1377
+ }
1378
+
1379
+ /**
1380
+ * For a file graphty wrote: where a value read from the attributes is what the writer writes for the block's own
1381
+ * value (a hexagon written as a hexagon, a pentagon-like vee as an inverted triangle, an opacity rounded to 255 steps,
1382
+ * a size rounded in inches), the block's exact value is the truth, so a re-import gives back the same rules; a value
1383
+ * another program changed since stays the truth.
1384
+ * @param snapshot - the graph
1385
+ * @param text - the block's text
1386
+ * @param read - the values read, changed in place
1387
+ * @param read.node - the node values
1388
+ * @param read.edge - the edge values
1389
+ * @param cx - the elements' contexts
1390
+ */
1391
+ function exactFromBlock(
1392
+ snapshot: GraphSnapshot,
1393
+ text: string,
1394
+ read: { readonly node: ValueColumns; readonly edge: ValueColumns },
1395
+ cx: ReturnType<typeof contexts>,
1396
+ ): void {
1397
+ const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
1398
+ const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "dot") : null;
1399
+ if (ex === null) {
1400
+ return;
1401
+ }
1402
+ const notes = new StyleNotes();
1403
+ for (const target of ["node", "edge"] as const) {
1404
+ const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1405
+ const get = getter((t, p) => resolveStyles(ex, t, p as never, snapshot), target);
1406
+ const values = read[target];
1407
+ for (let i = 0; i < n; i++) {
1408
+ const want = get(i);
1409
+ const back = writtenAndReadBack(target, i, want, cx, notes);
1410
+ for (const property of DOT_STYLE_CAPABILITIES[target]) {
1411
+ const native = values[property]?.[i] ?? null;
1412
+ const w = want(property);
1413
+ if (native !== w && native === (back.get(property) ?? null)) {
1414
+ values[property] ??= new Array<StyleValue | null>(n).fill(null);
1415
+ values[property][i] = w;
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+ }
1421
+
1422
+ /**
1423
+ * An element's values written as the writer writes them and read back as the reader reads them.
1424
+ * @param target - nodes or edges
1425
+ * @param i - the element
1426
+ * @param want - its values
1427
+ * @param cx - the elements' contexts
1428
+ * @param notes - a scratch for the notes the round trip makes
1429
+ * @returns the values read back, by property
1430
+ */
1431
+ function writtenAndReadBack(
1432
+ target: StyleTarget,
1433
+ i: number,
1434
+ want: Get,
1435
+ cx: ReturnType<typeof contexts>,
1436
+ notes: StyleNotes,
1437
+ ): Map<string, StyleValue> {
1438
+ const ignore: Approximate = () => undefined;
1439
+ const back = new Map<string, StyleValue>();
1440
+ const put: Put = (property, value) => {
1441
+ if (value !== null) {
1442
+ back.set(property, value);
1443
+ }
1444
+ };
1445
+ const at: At = (property) => ({ notes, target, property });
1446
+ if (target === "node") {
1447
+ const ncx = cx.node(i);
1448
+ const attrs = nodeAttributes(want, ncx, ignore, notes);
1449
+ readNode((name) => textOf(attrs.get(name)), ncx, put, at);
1450
+ } else {
1451
+ const ecx = cx.edge(i);
1452
+ const attrs = edgeAttributes(want, ecx.directed, ignore);
1453
+ readEdge((name) => textOf(attrs.get(name)), ecx, put, at);
1454
+ }
1455
+ return back;
1456
+ }
1457
+
1458
+ /**
1459
+ * A written value as the text the importer would read back.
1460
+ * @param v - the value
1461
+ * @returns its text, or null
1462
+ */
1463
+ function textOf(v: Written | undefined): string | null {
1464
+ return v === undefined ? null : String(v);
1465
+ }
1466
+
1467
+ /**
1468
+ * Whether a style column written as a DOT attribute reads back as the same values: DOT values are untyped and the
1469
+ * importer infers a column's type from its text, so a text column whose every value looks like a number does not.
1470
+ * @param col - the values
1471
+ * @returns true when it reads back unchanged
1472
+ */
1473
+ function readsBack(col: StyleColumn): boolean {
1474
+ const vals = col.filter((v): v is StyleValue => v !== null);
1475
+ if (vals.every((v) => typeof v === "number")) {
1476
+ return vals.every((v) => Number.isFinite(v));
1477
+ }
1478
+ if (vals.every((v) => typeof v === "boolean")) {
1479
+ return true;
1480
+ }
1481
+ if (vals.every((v) => typeof v === "string")) {
1482
+ return vals.some((v) => inferTextDtype(v) === "string") && vals.every((v) => isWritableDotText(v));
1483
+ }
1484
+ return false;
1485
+ }
1486
+
1487
+ /**
1488
+ * A `graphty.styles` block's JSON with every quote and backslash inside its strings written as a `\u` escape, so the
1489
+ * text holds no backslash before a quote, which a DOT quoted string cannot carry; it parses to the same value.
1490
+ * @param json - the block
1491
+ * @returns the block
1492
+ */
1493
+ function dotSafeJson(json: string): string {
1494
+ return json.replaceAll(/\\(["\\])/g, (_m, c: string) => (c === '"' ? String.raw`\u0022` : String.raw`\u005c`));
1495
+ }
1496
+
1497
+ /** A DOT export with styles worked out. */
1498
+ interface DotStyledExport {
1499
+ /** The graph to write: Graphviz attributes written from the styles, the block and the per-element values added. */
1500
+ readonly snapshot: GraphSnapshot;
1501
+ /** W_STYLE_APPROXIMATED for every value written as the nearest one Graphviz has. */
1502
+ readonly notes: readonly LossNote[];
1503
+ }
1504
+
1505
+ /**
1506
+ * Works out a DOT export with styles: every node's, cluster's and edge's Graphviz attributes from what graphty draws,
1507
+ * in place of every style attribute it had; the graph's `bgcolor`; the `graphty.styles` block as the graph attribute
1508
+ * `"graphty.styles"` and the per-element values its rules read as `"graphty.style.<target>.<key>"` attributes. A
1509
+ * style column whose values would not read back as written (text that looks like numbers) goes inside the block
1510
+ * instead, with all the others.
1511
+ * @param snapshot - the graph handed to the exporter
1512
+ * @param input - the styles, or undefined when none are written
1513
+ * @param onMixedDirection - the exporter's option: the direction a graph mixing both is written with
1514
+ * @returns the styled export, or null without styles
1515
+ */
1516
+ export function dotStyledExport(
1517
+ snapshot: GraphSnapshot,
1518
+ input: ExporterStyleInput | undefined,
1519
+ onMixedDirection?: unknown,
1520
+ ): DotStyledExport | null {
1521
+ if (input === undefined) {
1522
+ return null;
1523
+ }
1524
+ const approximated = new StyleApproximations();
1525
+ const replaced: LossNote[] = [];
1526
+ const out = snapshot.withColumns();
1527
+ const written = onMixedDirection === "directed" || onMixedDirection === "undirected";
1528
+ const cx = contexts(snapshot, written ? onMixedDirection === "directed" : null);
1529
+ const notes = new StyleNotes();
1530
+ for (const target of ["node", "edge"] as const) {
1531
+ const table = target === "node" ? out.nodes : out.edges;
1532
+ const read = dataRead(input.rules.styles.rules, target);
1533
+ const approximate: Approximate = (property, value) => {
1534
+ approximated.add(target, property, value);
1535
+ };
1536
+ const columns = attributeColumns(snapshot, input, target, { cx, approximate, notes, read });
1537
+ replaced.push(...removeStyleAttributes(table, target, columns, read));
1538
+ setAttributeColumns(table, columns);
1539
+ }
1540
+ const { background } = input.rules.styles.graph;
1541
+ if (out.graph.has("bgcolor")) {
1542
+ out.graph.remove("bgcolor");
1543
+ }
1544
+ if (typeof background === "string") {
1545
+ out.graph.set("bgcolor", [background], { dtype: "string" });
1546
+ }
1547
+ const inline = [...input.tags.node.values(), ...input.tags.edge.values()].some((col) => !readsBack(col));
1548
+ if (!inline) {
1549
+ setTagColumns(out, input);
1550
+ }
1551
+ const block = inline ? writeStyleBlock(input.rules, { inlineColumns: true, snapshot }) : input.block;
1552
+ out.graph.set(STYLE_BLOCK_NAME, [dotSafeJson(block)], { dtype: "string" });
1553
+ const losses = approximated.notes("Graphviz");
1554
+ if (inline) {
1555
+ losses.push(...styleBlockLosses(block, input.rules.styles.rules.length, "size"));
1556
+ }
1557
+ return { snapshot: out, notes: [...replaced, ...losses] };
1558
+ }
1559
+
1560
+ /** What attributeColumns() reads besides the graph and the styles. */
1561
+ interface AttributeWrite {
1562
+ /** The elements' contexts. */
1563
+ readonly cx: ReturnType<typeof contexts>;
1564
+ /** Records approximations. */
1565
+ readonly approximate: Approximate;
1566
+ /** Where an HTML name is noted. */
1567
+ readonly notes: StyleNotes;
1568
+ /** The data columns the target's rules read. */
1569
+ readonly read: ReadonlySet<string>;
1570
+ }
1571
+
1572
+ /**
1573
+ * Every element's Graphviz attributes of a target, as columns.
1574
+ * @param snapshot - the graph handed to the exporter
1575
+ * @param input - the styles
1576
+ * @param target - nodes or edges
1577
+ * @param w - the contexts, the recorders and the columns the rules read
1578
+ * @returns the attribute columns, by name
1579
+ */
1580
+ function attributeColumns(
1581
+ snapshot: GraphSnapshot,
1582
+ input: ExporterStyleInput,
1583
+ target: StyleTarget,
1584
+ w: AttributeWrite,
1585
+ ): Map<string, (Written | null)[]> {
1586
+ const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1587
+ const get = getter((t, p) => input.picture(t, p as never), target);
1588
+ const columns = new Map<string, (Written | null)[]>();
1589
+ for (let i = 0; i < n; i++) {
1590
+ const attrs =
1591
+ target === "node"
1592
+ ? nodeAttributes(get(i), w.cx.node(i), w.approximate, w.notes)
1593
+ : edgeAttributes(get(i), w.cx.edge(i).directed, w.approximate);
1594
+ // no label is written label="" (Graphviz would show the name), which a rule reading the label column
1595
+ // reads back as the empty label
1596
+ if (w.read.has(LABEL_ATTRIBUTE) && attrs.get(LABEL_ATTRIBUTE) === "" && get(i)("label") === null) {
1597
+ w.approximate("label", "");
1598
+ }
1599
+ for (const [name, value] of attrs) {
1600
+ let col = columns.get(name);
1601
+ if (col === undefined) {
1602
+ col = new Array<Written | null>(n).fill(null);
1603
+ columns.set(name, col);
1604
+ }
1605
+ col[i] = value;
1606
+ }
1607
+ }
1608
+ return columns;
1609
+ }
1610
+
1611
+ /**
1612
+ * Removes every column Graphviz reads as how to draw: the picture writes them, so a data column of the same name (a
1613
+ * `label` attribute graphty does not draw) would draw something graphty does not. One a rule reads, whose values the
1614
+ * picture does not write, is lost to the rule, and said so.
1615
+ * @param table - the node or edge table, changed in place
1616
+ * @param target - nodes or edges
1617
+ * @param columns - the attributes the picture writes
1618
+ * @param read - the data columns the rules read
1619
+ * @returns the notes of the columns lost to a rule
1620
+ */
1621
+ function removeStyleAttributes(
1622
+ table: GraphSnapshot["nodes"],
1623
+ target: StyleTarget,
1624
+ columns: ReadonlyMap<string, (Written | null)[]>,
1625
+ read: ReadonlySet<string>,
1626
+ ): LossNote[] {
1627
+ const attributes = target === "node" ? NODE_ATTRIBUTES : EDGE_ATTRIBUTES;
1628
+ const replaced: LossNote[] = [];
1629
+ for (const name of table.names()) {
1630
+ if (!attributes.includes(name) && name !== LABEL_ATTRIBUTE && !name.startsWith("graphty.style.")) {
1631
+ continue;
1632
+ }
1633
+ const differs = read.has(name) ? replacedRows(table, name, columns.get(name)) : 0;
1634
+ if (differs > 0) {
1635
+ replaced.push(
1636
+ Object.freeze({
1637
+ code: DOT_ATTRIBUTE_CLASH_CODE,
1638
+ message: `${target} column "${name}" is not written: the ${name} attribute holds what graphty draws (${String(differs)} ${target}${differs === 1 ? "" : "s"} differ)`,
1639
+ column: name,
1640
+ count: differs,
1641
+ }),
1642
+ );
1643
+ }
1644
+ table.remove(name);
1645
+ }
1646
+ return replaced;
1647
+ }
1648
+
1649
+ /**
1650
+ * Sets the attribute columns the picture writes: the label with its role, numbers as f64, the rest as text.
1651
+ * @param table - the node or edge table, changed in place
1652
+ * @param columns - the attributes
1653
+ */
1654
+ function setAttributeColumns(table: GraphSnapshot["nodes"], columns: ReadonlyMap<string, (Written | null)[]>): void {
1655
+ for (const [name, col] of columns) {
1656
+ if (name === LABEL_ATTRIBUTE) {
1657
+ table.set(name, col, { dtype: "string", role: "label" }, { replaceRole: true });
1658
+ } else if (col.every((v) => v === null || typeof v === "number")) {
1659
+ table.set(name, col, { dtype: "f64" });
1660
+ } else {
1661
+ table.set(
1662
+ name,
1663
+ col.map((v) => (v === null ? null : String(v))),
1664
+ { dtype: "string" },
1665
+ );
1666
+ }
1667
+ }
1668
+ }
1669
+
1670
+ /**
1671
+ * Sets the per-element values the rules read as `"graphty.style.<target>.<key>"` attributes.
1672
+ * @param out - the graph, changed in place
1673
+ * @param input - the styles
1674
+ */
1675
+ function setTagColumns(out: GraphSnapshot, input: ExporterStyleInput): void {
1676
+ for (const target of ["node", "edge"] as const) {
1677
+ const table = target === "node" ? out.nodes : out.edges;
1678
+ for (const [name, col] of input.tags[target]) {
1679
+ const kinds = new Set(col.filter((v) => v !== null).map((v) => typeof v));
1680
+ let dtype: "string" | "bool" | "f64" = "string";
1681
+ if (kinds.has("number")) {
1682
+ dtype = "f64";
1683
+ } else if (kinds.has("boolean")) {
1684
+ dtype = "bool";
1685
+ }
1686
+ table.set(name, col, { dtype });
1687
+ }
1688
+ }
1689
+ }
1690
+
1691
+ /**
1692
+ * The data columns the rules of a target read.
1693
+ * @param rules - the rules
1694
+ * @param target - nodes or edges
1695
+ * @returns the column names
1696
+ */
1697
+ function dataRead(rules: readonly StyleRule[], target: StyleTarget): Set<string> {
1698
+ const out = new Set<string>();
1699
+ for (const { ext: _ext, ...rule } of rules.filter((r) => r.target === target)) {
1700
+ JSON.stringify(rule, (_k, v: unknown) => {
1701
+ if (typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length === 1) {
1702
+ const { data } = v as { data?: unknown };
1703
+ if (typeof data === "string") {
1704
+ out.add(data);
1705
+ }
1706
+ }
1707
+ return v;
1708
+ });
1709
+ }
1710
+ return out;
1711
+ }
1712
+
1713
+ /**
1714
+ * A data column named like an attribute the export writes is not written: the exporter's `DOT_LOSS.ATTRIBUTE_CLASH`.
1715
+ */
1716
+ export const DOT_ATTRIBUTE_CLASH_CODE = "W_DOT_ATTRIBUTE_CLASH";
1717
+
1718
+ /**
1719
+ * How many rows of a data column the attribute written in its place does not hold as it is.
1720
+ * @param table - the table
1721
+ * @param name - the column
1722
+ * @param written - the attribute's values, or undefined when none is written
1723
+ * @returns the rows that differ
1724
+ */
1725
+ function replacedRows(
1726
+ table: GraphSnapshot["nodes"],
1727
+ name: string,
1728
+ written: readonly (Written | null)[] | undefined,
1729
+ ): number {
1730
+ const column = table.get(name);
1731
+ let n = 0;
1732
+ for (let i = 0; column !== null && i < column.length; i++) {
1733
+ const cell = column.isSet(i) ? plainCell(column.value(i)) : null;
1734
+ const w = written?.[i] ?? null;
1735
+ const text = typeof cell === "string" ? cell : JSON.stringify(cell);
1736
+ if (cell !== null && (w === null || String(w) !== text)) {
1737
+ n++;
1738
+ }
1739
+ }
1740
+ return n;
1741
+ }