@graphty/graph-io 0.3.48 → 0.3.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (600) hide show
  1. package/dist/chunks/{approximations-C2riR4NT.js → approximations-BRXsll0T.js} +2 -2
  2. package/dist/chunks/{approximations-C2riR4NT.js.map → approximations-BRXsll0T.js.map} +1 -1
  3. package/dist/chunks/bends-BGXjv_U4.js +129 -0
  4. package/dist/chunks/bends-BGXjv_U4.js.map +1 -0
  5. package/dist/chunks/{block-Cu22ewuS.js → block-yl0f_qLO.js} +931 -164
  6. package/dist/chunks/block-yl0f_qLO.js.map +1 -0
  7. package/dist/chunks/choose-CczZJaX_.js +717 -0
  8. package/dist/chunks/choose-CczZJaX_.js.map +1 -0
  9. package/dist/chunks/{codes-CV_Bhcyi.js → codes-BE2xir2k.js} +6 -2
  10. package/dist/chunks/codes-BE2xir2k.js.map +1 -0
  11. package/dist/chunks/{constants-D0LqcsLJ.js → constants-CKkYLPtt.js} +2 -2
  12. package/dist/chunks/{constants-D0LqcsLJ.js.map → constants-CKkYLPtt.js.map} +1 -1
  13. package/dist/chunks/{context-cgmmOxZ2.js → context-DizmppHy.js} +5 -4
  14. package/dist/chunks/{context-cgmmOxZ2.js.map → context-DizmppHy.js.map} +1 -1
  15. package/dist/chunks/{convert-BcQ8_jMo.js → convert-Ct2zelbQ.js} +7 -2
  16. package/dist/chunks/convert-Ct2zelbQ.js.map +1 -0
  17. package/dist/chunks/{credentials-BiQsnMTb.js → credentials-CmvJXYeH.js} +67 -16
  18. package/dist/chunks/credentials-CmvJXYeH.js.map +1 -0
  19. package/dist/chunks/{cx-export-BEeF6ymo.js → cx-export-Bt6NoqTQ.js} +3 -3
  20. package/dist/chunks/{cx-export-BEeF6ymo.js.map → cx-export-Bt6NoqTQ.js.map} +1 -1
  21. package/dist/chunks/descriptor-B28U25x4.js +60 -0
  22. package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
  23. package/dist/chunks/{descriptor-BJ-W0Jd-.js → descriptor-Dhez_Cew.js} +11 -2
  24. package/dist/chunks/descriptor-Dhez_Cew.js.map +1 -0
  25. package/dist/chunks/{emit-CD8xnY8s.js → emit-7Jkpg0yd.js} +1788 -133
  26. package/dist/chunks/emit-7Jkpg0yd.js.map +1 -0
  27. package/dist/chunks/{escape-DH2Egt-l.js → escape-CuCIzNMB.js} +8 -8
  28. package/dist/chunks/{escape-DH2Egt-l.js.map → escape-CuCIzNMB.js.map} +1 -1
  29. package/dist/chunks/{export-CZ7VRm4I.js → export-Cha4dRFz.js} +45 -387
  30. package/dist/chunks/export-Cha4dRFz.js.map +1 -0
  31. package/dist/chunks/{exporter-ChjL5fmP.js → exporter-1Ajud15J.js} +9 -8
  32. package/dist/chunks/{exporter-ChjL5fmP.js.map → exporter-1Ajud15J.js.map} +1 -1
  33. package/dist/chunks/{exporter-BIdaoIgh.js → exporter-B6yvL3wy.js} +7 -7
  34. package/dist/chunks/{exporter-BIdaoIgh.js.map → exporter-B6yvL3wy.js.map} +1 -1
  35. package/dist/chunks/{exporter-Dcw7Ldtq.js → exporter-BP6otMO6.js} +6 -6
  36. package/dist/chunks/{exporter-Dcw7Ldtq.js.map → exporter-BP6otMO6.js.map} +1 -1
  37. package/dist/chunks/{exporter-DtVU80Fa.js → exporter-CA5q8ROP.js} +9 -8
  38. package/dist/chunks/{exporter-DtVU80Fa.js.map → exporter-CA5q8ROP.js.map} +1 -1
  39. package/dist/chunks/{exporter-bxxjOrDZ.js → exporter-CQQZFJ2v.js} +1284 -156
  40. package/dist/chunks/exporter-CQQZFJ2v.js.map +1 -0
  41. package/dist/chunks/format-BXiAb-1h.js +408 -0
  42. package/dist/chunks/format-BXiAb-1h.js.map +1 -0
  43. package/dist/chunks/{generic-CP6YD34u.js → generic-Dc21MAdg.js} +387 -31
  44. package/dist/chunks/generic-Dc21MAdg.js.map +1 -0
  45. package/dist/chunks/{importer-B87_fmVx.js → importer-C6NVpCXp.js} +327 -84
  46. package/dist/chunks/importer-C6NVpCXp.js.map +1 -0
  47. package/dist/chunks/{importer-DPZDN40-.js → importer-CKFe4q54.js} +25 -17
  48. package/dist/chunks/importer-CKFe4q54.js.map +1 -0
  49. package/dist/chunks/{importer-Bji2MeWr.js → importer-CKbUbsJA.js} +13 -10
  50. package/dist/chunks/{importer-Bji2MeWr.js.map → importer-CKbUbsJA.js.map} +1 -1
  51. package/dist/chunks/{importer-CB_zV5_L.js → importer-ClCsdgTJ.js} +208 -1104
  52. package/dist/chunks/importer-ClCsdgTJ.js.map +1 -0
  53. package/dist/chunks/{importer-e3huMZbm.js → importer-CyVZTrf0.js} +14 -10
  54. package/dist/chunks/importer-CyVZTrf0.js.map +1 -0
  55. package/dist/chunks/{importer-CVT-JyDE.js → importer-DgG4v1Ms.js} +1537 -355
  56. package/dist/chunks/importer-DgG4v1Ms.js.map +1 -0
  57. package/dist/chunks/{importer-Cyy6UseH.js → importer-asFUrIsj.js} +66 -13
  58. package/dist/chunks/importer-asFUrIsj.js.map +1 -0
  59. package/dist/chunks/{input-Ccg9I22A.js → input-CAxt_aV-.js} +4 -4
  60. package/dist/chunks/{input-Ccg9I22A.js.map → input-CAxt_aV-.js.map} +1 -1
  61. package/dist/chunks/{json-elements-By172SVq.js → json-elements-Cq76WjnZ.js} +4 -4
  62. package/dist/chunks/{json-elements-By172SVq.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
  63. package/dist/chunks/{lexicon-u2SI5xcx.js → lexicon-IVRq2Uoo.js} +282 -74
  64. package/dist/chunks/lexicon-IVRq2Uoo.js.map +1 -0
  65. package/dist/chunks/{ontology-export-BwgMyuF0.js → ontology-export-OMo91mb0.js} +4 -4
  66. package/dist/chunks/{ontology-export-BwgMyuF0.js.map → ontology-export-OMo91mb0.js.map} +1 -1
  67. package/dist/chunks/{records-Cgvxz-Vx.js → records-NJiYvekn.js} +2 -2
  68. package/dist/chunks/{records-Cgvxz-Vx.js.map → records-NJiYvekn.js.map} +1 -1
  69. package/dist/chunks/{report-CgdsL9Hb.js → report-CwMfYa9J.js} +23 -23
  70. package/dist/chunks/{report-CgdsL9Hb.js.map → report-CwMfYa9J.js.map} +1 -1
  71. package/dist/chunks/round-trip-Cy3_SHRZ.js +79 -0
  72. package/dist/chunks/round-trip-Cy3_SHRZ.js.map +1 -0
  73. package/dist/chunks/{sigv4-LkAzsLAu.js → sigv4-BXwVbza3.js} +5 -5
  74. package/dist/chunks/sigv4-BXwVbza3.js.map +1 -0
  75. package/dist/chunks/{styles-CaABMoO1.js → styles-CAVmBLgp.js} +1237 -138
  76. package/dist/chunks/styles-CAVmBLgp.js.map +1 -0
  77. package/dist/chunks/styles-CPczw1Jz.js +1928 -0
  78. package/dist/chunks/styles-CPczw1Jz.js.map +1 -0
  79. package/dist/chunks/styles-boK6Ln54.js +2847 -0
  80. package/dist/chunks/styles-boK6Ln54.js.map +1 -0
  81. package/dist/chunks/{styles-yed-Cs1nmrjn.js → styles-yed-XeFBQ6VS.js} +506 -175
  82. package/dist/chunks/styles-yed-XeFBQ6VS.js.map +1 -0
  83. package/dist/chunks/{text-BuKQVMz2.js → text-ystE2bYi.js} +2 -2
  84. package/dist/chunks/{text-BuKQVMz2.js.map → text-ystE2bYi.js.map} +1 -1
  85. package/dist/csv.js +7 -7
  86. package/dist/cx.js +2 -2
  87. package/dist/cx2.js +2 -2
  88. package/dist/cys.js +1 -1
  89. package/dist/dot.js +8 -4
  90. package/dist/dot.js.map +1 -1
  91. package/dist/gexf.js +42 -12
  92. package/dist/gexf.js.map +1 -1
  93. package/dist/gml.js +848 -194
  94. package/dist/gml.js.map +1 -1
  95. package/dist/graph-io.js +230 -193
  96. package/dist/graph-io.js.map +1 -1
  97. package/dist/graphml.js +3 -3
  98. package/dist/json.js +3 -2
  99. package/dist/neo4j.js +6 -6
  100. package/dist/obo.js +1 -1
  101. package/dist/pajek.js +1 -1
  102. package/dist/positions.js +45 -3
  103. package/dist/positions.js.map +1 -1
  104. package/dist/sources-cypher.js +141 -23
  105. package/dist/sources-cypher.js.map +1 -1
  106. package/dist/sources-ndex.js +2 -2
  107. package/dist/sources-rest.js +32 -6
  108. package/dist/sources-rest.js.map +1 -1
  109. package/dist/sources-sparql.d.ts +1 -0
  110. package/dist/sources-sparql.js +439 -0
  111. package/dist/sources-sparql.js.map +1 -0
  112. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  113. package/dist/src/formats/cx/importer.js +5 -1
  114. package/dist/src/formats/cx/importer.js.map +1 -1
  115. package/dist/src/formats/cx/styles.d.ts +24 -1
  116. package/dist/src/formats/cx/styles.d.ts.map +1 -1
  117. package/dist/src/formats/cx/styles.js +117 -11
  118. package/dist/src/formats/cx/styles.js.map +1 -1
  119. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  120. package/dist/src/formats/cx2/importer.js +1 -0
  121. package/dist/src/formats/cx2/importer.js.map +1 -1
  122. package/dist/src/formats/cx2/styles.d.ts.map +1 -1
  123. package/dist/src/formats/cx2/styles.js +3 -1
  124. package/dist/src/formats/cx2/styles.js.map +1 -1
  125. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cys/importer.js +1 -0
  127. package/dist/src/formats/cys/importer.js.map +1 -1
  128. package/dist/src/formats/cys/styles.d.ts +2 -0
  129. package/dist/src/formats/cys/styles.d.ts.map +1 -1
  130. package/dist/src/formats/cys/styles.js +15 -7
  131. package/dist/src/formats/cys/styles.js.map +1 -1
  132. package/dist/src/formats/dot/drawing.d.ts +98 -0
  133. package/dist/src/formats/dot/drawing.d.ts.map +1 -0
  134. package/dist/src/formats/dot/drawing.js +734 -0
  135. package/dist/src/formats/dot/drawing.js.map +1 -0
  136. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  137. package/dist/src/formats/dot/exporter.js +34 -2
  138. package/dist/src/formats/dot/exporter.js.map +1 -1
  139. package/dist/src/formats/dot/importer.d.ts +6 -1
  140. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  141. package/dist/src/formats/dot/importer.js +170 -33
  142. package/dist/src/formats/dot/importer.js.map +1 -1
  143. package/dist/src/formats/dot/index.d.ts +3 -1
  144. package/dist/src/formats/dot/index.d.ts.map +1 -1
  145. package/dist/src/formats/dot/index.js +2 -1
  146. package/dist/src/formats/dot/index.js.map +1 -1
  147. package/dist/src/formats/dot/label-places.d.ts +41 -0
  148. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  149. package/dist/src/formats/dot/label-places.js +185 -0
  150. package/dist/src/formats/dot/label-places.js.map +1 -0
  151. package/dist/src/formats/dot/names.d.ts +13 -0
  152. package/dist/src/formats/dot/names.d.ts.map +1 -1
  153. package/dist/src/formats/dot/names.js +13 -0
  154. package/dist/src/formats/dot/names.js.map +1 -1
  155. package/dist/src/formats/dot/record.d.ts +50 -0
  156. package/dist/src/formats/dot/record.d.ts.map +1 -0
  157. package/dist/src/formats/dot/record.js +275 -0
  158. package/dist/src/formats/dot/record.js.map +1 -0
  159. package/dist/src/formats/dot/styles.d.ts +72 -2
  160. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  161. package/dist/src/formats/dot/styles.js +1500 -119
  162. package/dist/src/formats/dot/styles.js.map +1 -1
  163. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  164. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  165. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  166. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  167. package/dist/src/formats/gexf/importer.d.ts +7 -0
  168. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  169. package/dist/src/formats/gexf/importer.js +41 -5
  170. package/dist/src/formats/gexf/importer.js.map +1 -1
  171. package/dist/src/formats/gexf/preview.d.ts +77 -0
  172. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  173. package/dist/src/formats/gexf/preview.js +191 -0
  174. package/dist/src/formats/gexf/preview.js.map +1 -0
  175. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  176. package/dist/src/formats/gexf/schema.js +2 -1
  177. package/dist/src/formats/gexf/schema.js.map +1 -1
  178. package/dist/src/formats/gexf/styles.d.ts +58 -31
  179. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  180. package/dist/src/formats/gexf/styles.js +848 -225
  181. package/dist/src/formats/gexf/styles.js.map +1 -1
  182. package/dist/src/formats/gml/importer.d.ts +5 -2
  183. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  184. package/dist/src/formats/gml/importer.js +258 -71
  185. package/dist/src/formats/gml/importer.js.map +1 -1
  186. package/dist/src/formats/gml/index.d.ts +1 -1
  187. package/dist/src/formats/gml/index.d.ts.map +1 -1
  188. package/dist/src/formats/gml/index.js +1 -1
  189. package/dist/src/formats/gml/index.js.map +1 -1
  190. package/dist/src/formats/gml/positions.d.ts +7 -0
  191. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  192. package/dist/src/formats/gml/positions.js +29 -7
  193. package/dist/src/formats/gml/positions.js.map +1 -1
  194. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  195. package/dist/src/formats/gml/styles.js +754 -102
  196. package/dist/src/formats/gml/styles.js.map +1 -1
  197. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  198. package/dist/src/formats/graphml/styles-plain.js +6 -10
  199. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  200. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  201. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  202. package/dist/src/formats/graphml/styles-yed.js +695 -164
  203. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  204. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  205. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  206. package/dist/src/formats/graphml/styles-yfiles3.js +431 -64
  207. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  208. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  209. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  210. package/dist/src/formats/graphml/yfiles.js +16 -6
  211. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  212. package/dist/src/formats/json/dialect.d.ts +3 -2
  213. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  214. package/dist/src/formats/json/dialect.js +39 -5
  215. package/dist/src/formats/json/dialect.js.map +1 -1
  216. package/dist/src/formats/json/importer.d.ts +13 -2
  217. package/dist/src/formats/json/importer.d.ts.map +1 -1
  218. package/dist/src/formats/json/importer.js +42 -6
  219. package/dist/src/formats/json/importer.js.map +1 -1
  220. package/dist/src/formats/json/index.d.ts +1 -1
  221. package/dist/src/formats/json/index.d.ts.map +1 -1
  222. package/dist/src/formats/json/index.js +1 -1
  223. package/dist/src/formats/json/index.js.map +1 -1
  224. package/dist/src/formats/json/positions.d.ts +5 -2
  225. package/dist/src/formats/json/positions.d.ts.map +1 -1
  226. package/dist/src/formats/json/positions.js +20 -4
  227. package/dist/src/formats/json/positions.js.map +1 -1
  228. package/dist/src/formats/json/styles.d.ts +12 -1
  229. package/dist/src/formats/json/styles.d.ts.map +1 -1
  230. package/dist/src/formats/json/styles.js +1843 -136
  231. package/dist/src/formats/json/styles.js.map +1 -1
  232. package/dist/src/formats/pajek/draw.d.ts +104 -0
  233. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  234. package/dist/src/formats/pajek/draw.js +382 -0
  235. package/dist/src/formats/pajek/draw.js.map +1 -0
  236. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  237. package/dist/src/formats/pajek/exporter.js +70 -51
  238. package/dist/src/formats/pajek/exporter.js.map +1 -1
  239. package/dist/src/formats/pajek/importer.d.ts +9 -0
  240. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  241. package/dist/src/formats/pajek/importer.js +233 -102
  242. package/dist/src/formats/pajek/importer.js.map +1 -1
  243. package/dist/src/formats/pajek/index.d.ts +1 -0
  244. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  245. package/dist/src/formats/pajek/index.js.map +1 -1
  246. package/dist/src/formats/pajek/styles.d.ts +15 -7
  247. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  248. package/dist/src/formats/pajek/styles.js +1395 -114
  249. package/dist/src/formats/pajek/styles.js.map +1 -1
  250. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  251. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  252. package/dist/src/formats/pajek/syntax.js +26 -2
  253. package/dist/src/formats/pajek/syntax.js.map +1 -1
  254. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  255. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  256. package/dist/src/formats/xgmml/emit.js +65 -5
  257. package/dist/src/formats/xgmml/emit.js.map +1 -1
  258. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  259. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  260. package/dist/src/formats/xgmml/styles.js +277 -60
  261. package/dist/src/formats/xgmml/styles.js.map +1 -1
  262. package/dist/src/index.d.ts +6 -5
  263. package/dist/src/index.d.ts.map +1 -1
  264. package/dist/src/index.js +6 -5
  265. package/dist/src/index.js.map +1 -1
  266. package/dist/src/positions/convert.d.ts.map +1 -1
  267. package/dist/src/positions/convert.js +11 -1
  268. package/dist/src/positions/convert.js.map +1 -1
  269. package/dist/src/positions/extract.d.ts.map +1 -1
  270. package/dist/src/positions/extract.js +53 -1
  271. package/dist/src/positions/extract.js.map +1 -1
  272. package/dist/src/positions/index.d.ts +1 -1
  273. package/dist/src/positions/index.d.ts.map +1 -1
  274. package/dist/src/positions/types.d.ts +59 -0
  275. package/dist/src/positions/types.d.ts.map +1 -1
  276. package/dist/src/sources/builtins.d.ts +1 -1
  277. package/dist/src/sources/builtins.d.ts.map +1 -1
  278. package/dist/src/sources/builtins.js +8 -2
  279. package/dist/src/sources/builtins.js.map +1 -1
  280. package/dist/src/sources/context.d.ts.map +1 -1
  281. package/dist/src/sources/context.js +1 -0
  282. package/dist/src/sources/context.js.map +1 -1
  283. package/dist/src/sources/credentials.d.ts +50 -1
  284. package/dist/src/sources/credentials.d.ts.map +1 -1
  285. package/dist/src/sources/credentials.js +69 -0
  286. package/dist/src/sources/credentials.js.map +1 -1
  287. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -1
  288. package/dist/src/sources/cypher/descriptor.js +5 -0
  289. package/dist/src/sources/cypher/descriptor.js.map +1 -1
  290. package/dist/src/sources/cypher/index.d.ts.map +1 -1
  291. package/dist/src/sources/cypher/index.js +125 -18
  292. package/dist/src/sources/cypher/index.js.map +1 -1
  293. package/dist/src/sources/cypher/params.d.ts +1 -1
  294. package/dist/src/sources/cypher/params.d.ts.map +1 -1
  295. package/dist/src/sources/cypher/query-builder.d.ts +17 -0
  296. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
  297. package/dist/src/sources/cypher/query-builder.js +31 -0
  298. package/dist/src/sources/cypher/query-builder.js.map +1 -1
  299. package/dist/src/sources/ids.d.ts +5 -0
  300. package/dist/src/sources/ids.d.ts.map +1 -1
  301. package/dist/src/sources/ids.js +5 -0
  302. package/dist/src/sources/ids.js.map +1 -1
  303. package/dist/src/sources/params.d.ts +3 -0
  304. package/dist/src/sources/params.d.ts.map +1 -1
  305. package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
  306. package/dist/src/sources/rest/descriptor.js +9 -0
  307. package/dist/src/sources/rest/descriptor.js.map +1 -1
  308. package/dist/src/sources/rest/openalex.d.ts +4 -2
  309. package/dist/src/sources/rest/openalex.d.ts.map +1 -1
  310. package/dist/src/sources/rest/openalex.js +4 -2
  311. package/dist/src/sources/rest/openalex.js.map +1 -1
  312. package/dist/src/sources/rest/source.d.ts.map +1 -1
  313. package/dist/src/sources/rest/source.js +38 -0
  314. package/dist/src/sources/rest/source.js.map +1 -1
  315. package/dist/src/sources/run.d.ts +2 -0
  316. package/dist/src/sources/run.d.ts.map +1 -1
  317. package/dist/src/sources/run.js +30 -3
  318. package/dist/src/sources/run.js.map +1 -1
  319. package/dist/src/sources/sigv4.d.ts +5 -3
  320. package/dist/src/sources/sigv4.d.ts.map +1 -1
  321. package/dist/src/sources/sigv4.js +6 -4
  322. package/dist/src/sources/sigv4.js.map +1 -1
  323. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  324. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  325. package/dist/src/sources/sparql/descriptor.js +64 -0
  326. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  327. package/dist/src/sources/sparql/index.d.ts +28 -0
  328. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  329. package/dist/src/sources/sparql/index.js +327 -0
  330. package/dist/src/sources/sparql/index.js.map +1 -0
  331. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  332. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  333. package/dist/src/sources/sparql/ntriples.js +180 -0
  334. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  335. package/dist/src/sources/sparql/params.d.ts +27 -0
  336. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  337. package/dist/src/sources/sparql/params.js +5 -0
  338. package/dist/src/sources/sparql/params.js.map +1 -0
  339. package/dist/src/sources/sparql/query.d.ts +73 -0
  340. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  341. package/dist/src/sources/sparql/query.js +164 -0
  342. package/dist/src/sources/sparql/query.js.map +1 -0
  343. package/dist/src/sources/types.d.ts +60 -0
  344. package/dist/src/sources/types.d.ts.map +1 -1
  345. package/dist/src/styles/annotations.d.ts +61 -5
  346. package/dist/src/styles/annotations.d.ts.map +1 -1
  347. package/dist/src/styles/annotations.js +130 -22
  348. package/dist/src/styles/annotations.js.map +1 -1
  349. package/dist/src/styles/canon/bends.d.ts +46 -0
  350. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  351. package/dist/src/styles/canon/bends.js +123 -0
  352. package/dist/src/styles/canon/bends.js.map +1 -1
  353. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  354. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  355. package/dist/src/styles/canon/color-tables.js +10 -4
  356. package/dist/src/styles/canon/color-tables.js.map +1 -1
  357. package/dist/src/styles/canon/color.d.ts +3 -1
  358. package/dist/src/styles/canon/color.d.ts.map +1 -1
  359. package/dist/src/styles/canon/color.js +103 -3
  360. package/dist/src/styles/canon/color.js.map +1 -1
  361. package/dist/src/styles/canon/enums.d.ts +2 -2
  362. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  363. package/dist/src/styles/canon/enums.js +89 -7
  364. package/dist/src/styles/canon/enums.js.map +1 -1
  365. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  366. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  367. package/dist/src/styles/canon/graphviz.js +57 -9
  368. package/dist/src/styles/canon/graphviz.js.map +1 -1
  369. package/dist/src/styles/canon/number.d.ts +8 -0
  370. package/dist/src/styles/canon/number.d.ts.map +1 -1
  371. package/dist/src/styles/canon/number.js +18 -0
  372. package/dist/src/styles/canon/number.js.map +1 -1
  373. package/dist/src/styles/canon/text.d.ts +29 -2
  374. package/dist/src/styles/canon/text.d.ts.map +1 -1
  375. package/dist/src/styles/canon/text.js +156 -16
  376. package/dist/src/styles/canon/text.js.map +1 -1
  377. package/dist/src/styles/codes.d.ts +2 -0
  378. package/dist/src/styles/codes.d.ts.map +1 -1
  379. package/dist/src/styles/codes.js +2 -0
  380. package/dist/src/styles/codes.js.map +1 -1
  381. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  382. package/dist/src/styles/cyjs/cascade.js +1139 -79
  383. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  384. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  385. package/dist/src/styles/cyjs/defaults-table.js +4 -0
  386. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  387. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  388. package/dist/src/styles/cyjs/emit.js +232 -10
  389. package/dist/src/styles/cyjs/emit.js.map +1 -1
  390. package/dist/src/styles/cyjs/face.d.ts +20 -0
  391. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  392. package/dist/src/styles/cyjs/face.js +332 -0
  393. package/dist/src/styles/cyjs/face.js.map +1 -0
  394. package/dist/src/styles/cyjs/mappers.d.ts +44 -1
  395. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  396. package/dist/src/styles/cyjs/mappers.js +475 -26
  397. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  398. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  399. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  400. package/dist/src/styles/cyjs/routes.js +360 -0
  401. package/dist/src/styles/cyjs/routes.js.map +1 -0
  402. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  403. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  404. package/dist/src/styles/cytoscape/charts.js +560 -0
  405. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  406. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  407. package/dist/src/styles/cytoscape/emit.js +156 -48
  408. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  409. package/dist/src/styles/cytoscape/lexicon.d.ts +91 -3
  410. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  411. package/dist/src/styles/cytoscape/lexicon.js +387 -77
  412. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  413. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  414. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  415. package/dist/src/styles/cytoscape/mappings.js +92 -0
  416. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  417. package/dist/src/styles/cytoscape/resolve.d.ts +42 -2
  418. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  419. package/dist/src/styles/cytoscape/resolve.js +623 -17
  420. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  421. package/dist/src/styles/defaults.d.ts +46 -26
  422. package/dist/src/styles/defaults.d.ts.map +1 -1
  423. package/dist/src/styles/defaults.js +308 -71
  424. package/dist/src/styles/defaults.js.map +1 -1
  425. package/dist/src/styles/index.d.ts +4 -3
  426. package/dist/src/styles/index.d.ts.map +1 -1
  427. package/dist/src/styles/index.js +3 -2
  428. package/dist/src/styles/index.js.map +1 -1
  429. package/dist/src/styles/internal.d.ts +1 -0
  430. package/dist/src/styles/internal.d.ts.map +1 -1
  431. package/dist/src/styles/internal.js +1 -0
  432. package/dist/src/styles/internal.js.map +1 -1
  433. package/dist/src/styles/io/generic.d.ts.map +1 -1
  434. package/dist/src/styles/io/generic.js +56 -16
  435. package/dist/src/styles/io/generic.js.map +1 -1
  436. package/dist/src/styles/io/round-trip.d.ts +56 -0
  437. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  438. package/dist/src/styles/io/round-trip.js +138 -0
  439. package/dist/src/styles/io/round-trip.js.map +1 -0
  440. package/dist/src/styles/io/writer.d.ts.map +1 -1
  441. package/dist/src/styles/io/writer.js +6 -1
  442. package/dist/src/styles/io/writer.js.map +1 -1
  443. package/dist/src/styles/notes.d.ts.map +1 -1
  444. package/dist/src/styles/notes.js +1 -0
  445. package/dist/src/styles/notes.js.map +1 -1
  446. package/dist/src/styles/plan/features.d.ts +1 -1
  447. package/dist/src/styles/plan/features.d.ts.map +1 -1
  448. package/dist/src/styles/plan/features.js +22 -2
  449. package/dist/src/styles/plan/features.js.map +1 -1
  450. package/dist/src/styles/plan/index.d.ts +1 -1
  451. package/dist/src/styles/plan/index.d.ts.map +1 -1
  452. package/dist/src/styles/plan/index.js +1 -1
  453. package/dist/src/styles/plan/index.js.map +1 -1
  454. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  455. package/dist/src/styles/plan/plan.js +61 -4
  456. package/dist/src/styles/plan/plan.js.map +1 -1
  457. package/dist/src/styles/plan/resolve.d.ts +11 -0
  458. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  459. package/dist/src/styles/plan/resolve.js +40 -4
  460. package/dist/src/styles/plan/resolve.js.map +1 -1
  461. package/dist/src/styles/slots.d.ts +14 -0
  462. package/dist/src/styles/slots.d.ts.map +1 -0
  463. package/dist/src/styles/slots.js +20 -0
  464. package/dist/src/styles/slots.js.map +1 -0
  465. package/dist/src/styles/types.d.ts +687 -6
  466. package/dist/src/styles/types.d.ts.map +1 -1
  467. package/dist/src/styles/vocabulary.d.ts +3 -2
  468. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  469. package/dist/src/styles/vocabulary.js +224 -0
  470. package/dist/src/styles/vocabulary.js.map +1 -1
  471. package/dist/styles.js +30 -239
  472. package/dist/styles.js.map +1 -1
  473. package/dist/xgmml.js +3 -3
  474. package/package.json +27 -7
  475. package/src/formats/cx/importer.ts +6 -1
  476. package/src/formats/cx/styles.ts +143 -9
  477. package/src/formats/cx2/importer.ts +1 -0
  478. package/src/formats/cx2/styles.ts +3 -1
  479. package/src/formats/cys/importer.ts +1 -0
  480. package/src/formats/cys/styles.ts +27 -5
  481. package/src/formats/dot/drawing.ts +799 -0
  482. package/src/formats/dot/exporter.ts +37 -1
  483. package/src/formats/dot/importer.ts +192 -36
  484. package/src/formats/dot/index.ts +3 -1
  485. package/src/formats/dot/label-places.ts +241 -0
  486. package/src/formats/dot/names.ts +15 -0
  487. package/src/formats/dot/record.ts +336 -0
  488. package/src/formats/dot/styles.ts +1702 -149
  489. package/src/formats/gexf/dialog-metrics.ts +61 -0
  490. package/src/formats/gexf/importer.ts +50 -5
  491. package/src/formats/gexf/preview.ts +217 -0
  492. package/src/formats/gexf/schema.ts +2 -1
  493. package/src/formats/gexf/styles.ts +1099 -245
  494. package/src/formats/gml/importer.ts +291 -76
  495. package/src/formats/gml/index.ts +1 -1
  496. package/src/formats/gml/positions.ts +29 -7
  497. package/src/formats/gml/styles.ts +852 -110
  498. package/src/formats/graphml/styles-plain.ts +6 -15
  499. package/src/formats/graphml/styles-yed.ts +765 -176
  500. package/src/formats/graphml/styles-yfiles3.ts +487 -63
  501. package/src/formats/graphml/yfiles.ts +19 -6
  502. package/src/formats/json/dialect.ts +41 -5
  503. package/src/formats/json/importer.ts +59 -7
  504. package/src/formats/json/index.ts +1 -0
  505. package/src/formats/json/positions.ts +22 -5
  506. package/src/formats/json/styles.ts +2123 -171
  507. package/src/formats/pajek/draw.ts +469 -0
  508. package/src/formats/pajek/exporter.ts +81 -49
  509. package/src/formats/pajek/importer.ts +278 -103
  510. package/src/formats/pajek/index.ts +1 -0
  511. package/src/formats/pajek/styles.ts +1506 -133
  512. package/src/formats/pajek/syntax.ts +27 -2
  513. package/src/formats/xgmml/emit.ts +73 -5
  514. package/src/formats/xgmml/styles.ts +330 -71
  515. package/src/index.ts +12 -1
  516. package/src/positions/convert.ts +12 -1
  517. package/src/positions/extract.ts +55 -1
  518. package/src/positions/index.ts +1 -0
  519. package/src/positions/types.ts +60 -0
  520. package/src/sources/builtins.ts +8 -1
  521. package/src/sources/context.ts +1 -0
  522. package/src/sources/credentials.ts +90 -1
  523. package/src/sources/cypher/descriptor.ts +7 -1
  524. package/src/sources/cypher/index.ts +149 -18
  525. package/src/sources/cypher/params.ts +1 -1
  526. package/src/sources/cypher/query-builder.ts +33 -0
  527. package/src/sources/ids.ts +6 -0
  528. package/src/sources/params.ts +3 -0
  529. package/src/sources/rest/descriptor.ts +9 -0
  530. package/src/sources/rest/openalex.ts +4 -2
  531. package/src/sources/rest/source.ts +41 -0
  532. package/src/sources/run.ts +35 -3
  533. package/src/sources/sigv4.ts +6 -4
  534. package/src/sources/sparql/descriptor.ts +67 -0
  535. package/src/sources/sparql/index.ts +371 -0
  536. package/src/sources/sparql/ntriples.ts +219 -0
  537. package/src/sources/sparql/params.ts +28 -0
  538. package/src/sources/sparql/query.ts +216 -0
  539. package/src/sources/types.ts +68 -0
  540. package/src/styles/annotations.ts +201 -27
  541. package/src/styles/canon/bends.ts +137 -0
  542. package/src/styles/canon/color-tables.ts +10 -4
  543. package/src/styles/canon/color.ts +105 -3
  544. package/src/styles/canon/enums.ts +107 -8
  545. package/src/styles/canon/graphviz.ts +71 -10
  546. package/src/styles/canon/number.ts +19 -0
  547. package/src/styles/canon/text.ts +187 -16
  548. package/src/styles/codes.ts +2 -0
  549. package/src/styles/cyjs/cascade.ts +1303 -84
  550. package/src/styles/cyjs/defaults-table.ts +4 -0
  551. package/src/styles/cyjs/emit.ts +268 -14
  552. package/src/styles/cyjs/face.ts +372 -0
  553. package/src/styles/cyjs/mappers.ts +506 -27
  554. package/src/styles/cyjs/routes.ts +432 -0
  555. package/src/styles/cytoscape/charts.ts +684 -0
  556. package/src/styles/cytoscape/emit.ts +170 -48
  557. package/src/styles/cytoscape/lexicon.ts +431 -78
  558. package/src/styles/cytoscape/mappings.ts +108 -0
  559. package/src/styles/cytoscape/resolve.ts +741 -17
  560. package/src/styles/defaults.ts +417 -108
  561. package/src/styles/index.ts +5 -0
  562. package/src/styles/internal.ts +1 -0
  563. package/src/styles/io/generic.ts +79 -23
  564. package/src/styles/io/round-trip.ts +202 -0
  565. package/src/styles/io/writer.ts +6 -1
  566. package/src/styles/notes.ts +1 -0
  567. package/src/styles/plan/features.ts +23 -2
  568. package/src/styles/plan/index.ts +1 -1
  569. package/src/styles/plan/plan.ts +69 -5
  570. package/src/styles/plan/resolve.ts +47 -4
  571. package/src/styles/slots.ts +25 -0
  572. package/src/styles/types.ts +698 -5
  573. package/src/styles/vocabulary.ts +232 -2
  574. package/dist/chunks/block-Cu22ewuS.js.map +0 -1
  575. package/dist/chunks/choose-Cdx6Enl2.js +0 -57
  576. package/dist/chunks/choose-Cdx6Enl2.js.map +0 -1
  577. package/dist/chunks/codes-CV_Bhcyi.js.map +0 -1
  578. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  579. package/dist/chunks/credentials-BiQsnMTb.js.map +0 -1
  580. package/dist/chunks/descriptor-BJ-W0Jd-.js.map +0 -1
  581. package/dist/chunks/emit-CD8xnY8s.js.map +0 -1
  582. package/dist/chunks/export-CZ7VRm4I.js.map +0 -1
  583. package/dist/chunks/exporter-bxxjOrDZ.js.map +0 -1
  584. package/dist/chunks/format-BMS-1Nxb.js +0 -64
  585. package/dist/chunks/format-BMS-1Nxb.js.map +0 -1
  586. package/dist/chunks/generic-CP6YD34u.js.map +0 -1
  587. package/dist/chunks/importer-B87_fmVx.js.map +0 -1
  588. package/dist/chunks/importer-CB_zV5_L.js.map +0 -1
  589. package/dist/chunks/importer-CVT-JyDE.js.map +0 -1
  590. package/dist/chunks/importer-Cyy6UseH.js.map +0 -1
  591. package/dist/chunks/importer-DPZDN40-.js.map +0 -1
  592. package/dist/chunks/importer-e3huMZbm.js.map +0 -1
  593. package/dist/chunks/lexicon-u2SI5xcx.js.map +0 -1
  594. package/dist/chunks/sigv4-LkAzsLAu.js.map +0 -1
  595. package/dist/chunks/styles-CaABMoO1.js.map +0 -1
  596. package/dist/chunks/styles-P_gmFAOi.js +0 -770
  597. package/dist/chunks/styles-P_gmFAOi.js.map +0 -1
  598. package/dist/chunks/styles-yed-Cs1nmrjn.js.map +0 -1
  599. package/dist/chunks/writer-DvHTl36J.js +0 -449
  600. package/dist/chunks/writer-DvHTl36J.js.map +0 -1
@@ -13,7 +13,7 @@
13
13
  * `none` and `invis` are transparent, and a color list is read as Graphviz draws it for that use.
14
14
  * - Outlines: `plain`, `plaintext` and `none` draw none. `setlinewidth(n)` is a pen width, `bold` a width of 2.
15
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`.
16
+ * carried as `labelFit: "inside"` with the label's padding (`margin`, else 8 by 4 points).
17
17
  * - Arrows per `dir`, `arrowhead` and `arrowtail`, with the graph's kind deciding the default: a target arrow on
18
18
  * directed edges, as one tool-default rule.
19
19
  * - A cluster's container node holds the cluster's attributes: its fill (`bgcolor`, or the fill when `filled`) and
@@ -27,21 +27,25 @@
27
27
  */
28
28
  import { isWritableDotText } from "../../common/escape.js";
29
29
  import { inferTextDtype } from "../../common/text.js";
30
+ import { canonBends, canonNodeOffsets, flattenCubic } from "../../styles/canon/bends.js";
30
31
  import { canonColor, colorOpacity } from "../../styles/canon/color.js";
31
32
  import { canonEnum } from "../../styles/canon/enums.js";
32
33
  import { canonNumber, DOT_POINTS_PER_INCH, num6 } from "../../styles/canon/number.js";
33
34
  import { MAX_STYLE_VALUE_LENGTH, noteAt } from "../../styles/canon/site.js";
34
- import { dotLabelText, htmlLabelText, recordLabelText } from "../../styles/canon/text.js";
35
+ import { dotLabelAlign, dotLabelText, htmlLabelSpans, htmlLabelText, recordLabelText, } from "../../styles/canon/text.js";
35
36
  import { STYLE_ISSUE } from "../../styles/codes.js";
36
37
  import { toolDefaults } from "../../styles/defaults.js";
37
38
  import { StyleApproximations } from "../../styles/io/approximations.js";
38
- import { blockStyles, parseStyleBlock, STYLE_BLOCK_NAME, styleBlockLosses, writeStyleBlock, } from "../../styles/io/block.js";
39
+ import { STYLE_BLOCK_NAME, styleBlockLosses, writeStyleBlock } from "../../styles/io/block.js";
40
+ import { elementValues, takeFromBlock } from "../../styles/io/round-trip.js";
39
41
  import { sharedInExportCall } from "../../styles/io/writer.js";
40
42
  import { StyleNotes } from "../../styles/notes.js";
41
- import { styleFingerprint } from "../../styles/plan/features.js";
42
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
43
+ import { plainCell } from "../../styles/plan/resolve.js";
43
44
  import { valueColumns } from "../graphml/styles-yed.js";
44
- import { CLUSTER_COLUMN, LABEL_ATTRIBUTE } from "./names.js";
45
+ import { GRAPHVIZ_DECORATED_BOXES, GRAPHVIZ_GLYPH_SHAPES, GRAPHVIZ_POLYGON_SHAPES, graphvizFont, graphvizFontName, graphvizGlyph, graphvizPolygonCorners, isGraphvizBox, } from "./drawing.js";
46
+ import { readLabelPlaces } from "./label-places.js";
47
+ import { CLUSTER_COLUMN, LABEL_ATTRIBUTE, POS_ATTRIBUTE } from "./names.js";
48
+ import { layoutRecord } from "./record.js";
45
49
  /**
46
50
  * The graph attribute holding the `graphty.styles` block of a DOT file graphty wrote.
47
51
  * @category Built-in formats
@@ -67,6 +71,14 @@ export const DOT_STYLE_CAPABILITIES = Object.freeze({
67
71
  "labelColor",
68
72
  "labelSize",
69
73
  "labelFont",
74
+ "labelWeight",
75
+ "labelItalic",
76
+ "labelPosition",
77
+ "shapePoints",
78
+ "shapePointsStretch",
79
+ "shapeParts",
80
+ "shapeLines",
81
+ "shapeLineWidth",
70
82
  "tooltip",
71
83
  "visible",
72
84
  ]),
@@ -77,6 +89,8 @@ export const DOT_STYLE_CAPABILITIES = Object.freeze({
77
89
  "lineStyle",
78
90
  "sourceArrow",
79
91
  "targetArrow",
92
+ "sourceArrowColor",
93
+ "targetArrowColor",
80
94
  "arrowSize",
81
95
  "label",
82
96
  "sourceLabel",
@@ -84,6 +98,8 @@ export const DOT_STYLE_CAPABILITIES = Object.freeze({
84
98
  "labelColor",
85
99
  "labelSize",
86
100
  "labelFont",
101
+ "labelWeight",
102
+ "labelItalic",
87
103
  "tooltip",
88
104
  "visible",
89
105
  ]),
@@ -109,7 +125,12 @@ const NODE_ATTRIBUTES = [
109
125
  "height",
110
126
  "fixedsize",
111
127
  "regular",
128
+ "sides",
129
+ "distortion",
130
+ "skew",
131
+ "orientation",
112
132
  "penwidth",
133
+ "gradientangle",
113
134
  "fontname",
114
135
  "fontsize",
115
136
  "fontcolor",
@@ -130,8 +151,13 @@ const EDGE_ATTRIBUTES = [
130
151
  "arrowtail",
131
152
  "dir",
132
153
  "arrowsize",
154
+ "fillcolor",
133
155
  "headlabel",
134
156
  "taillabel",
157
+ "labelfontsize",
158
+ "labelfontcolor",
159
+ "labelfontname",
160
+ "xlabel",
135
161
  ];
136
162
  /** Attributes the reader notes as not carried. */
137
163
  const NOTED_ATTRIBUTES = ["peripheries", "xlabel"];
@@ -261,13 +287,234 @@ function labelText(raw, names, html, record, at) {
261
287
  }
262
288
  return dotLabelText(record ? recordLabelText(raw, at) : raw, names, at);
263
289
  }
290
+ /**
291
+ * Reads how a plain label's lines line up (`\l`, `\r`); an HTML-like or a record label lines up its own way.
292
+ * @param raw - the label as the file gives it
293
+ * @param names - what the label's escapes stand for
294
+ * @param html - the file's HTML strings
295
+ * @param record - whether the node is a record
296
+ * @param put - receives the value
297
+ */
298
+ function readLabelAlign(raw, names, html, record, put) {
299
+ if (isHtmlString(raw) || html.has(`<${raw}>`)) {
300
+ // each <BR ALIGN> lines up the line before it: one alignment on every break lines up the label
301
+ const aligns = new Set([...raw.matchAll(/<br\b([^>]*)>/gi)].map((m) => /\balign\s*=\s*["']?(left|right|center)/i.exec(m[1])?.[1].toLowerCase() ?? "center"));
302
+ // words after the last break are a line of their own, centered
303
+ const body = isHtmlString(raw) ? raw.slice(1, -1) : raw;
304
+ const last = /<br\b[^>]*>(?![\s\S]*<br\b)/i.exec(body);
305
+ const tail = last === null ? "" : body.slice(last.index + last[0].length);
306
+ if (stripTags(tail).trim() !== "") {
307
+ aligns.add("center");
308
+ }
309
+ const [only] = aligns;
310
+ if (aligns.size === 1 && only !== "center") {
311
+ put("labelTextAlign", only);
312
+ }
313
+ return;
314
+ }
315
+ if (record) {
316
+ return;
317
+ }
318
+ const align = dotLabelAlign(raw, names);
319
+ if (align !== null) {
320
+ put("labelTextAlign", align);
321
+ }
322
+ }
323
+ /**
324
+ * Text with its tags (`<` to the next `>`) dropped; a `<` with no `>` after it, and the rest, kept.
325
+ * @param text - the text
326
+ * @returns the text without tags
327
+ */
328
+ function stripTags(text) {
329
+ let out = "";
330
+ let i = 0;
331
+ while (i < text.length) {
332
+ const open = text.indexOf("<", i);
333
+ const close = open < 0 ? -1 : text.indexOf(">", open + 1);
334
+ if (close < 0) {
335
+ return out + text.slice(i);
336
+ }
337
+ out += text.slice(i, open);
338
+ i = close + 1;
339
+ }
340
+ return out;
341
+ }
342
+ /**
343
+ * Reads the font runs of an HTML-like label (`<B>`, `<I>`, `<FONT COLOR FACE POINT-SIZE>`) as Graphviz draws them:
344
+ * one run in a font of its own sets the label's own font (color, size, family, weight, slant), several are
345
+ * `labelRuns`. Underline, overline, strike-through, `<SUB>` and `<SUP>` have no neutral slot and draw plain.
346
+ * @param raw - the label as the file gives it
347
+ * @param html - the file's HTML strings
348
+ * @param scheme - the element's color scheme
349
+ * @param put - receives the values
350
+ * @param at - the sites
351
+ */
352
+ function readHtmlRuns(raw, html, scheme, put, at) {
353
+ if (!isHtmlString(raw) && !html.has(`<${raw}>`)) {
354
+ return;
355
+ }
356
+ const markup = isHtmlString(raw) ? raw : `<${raw}>`;
357
+ const runs = [];
358
+ for (const span of htmlLabelSpans(markup)) {
359
+ const run = { text: span.text, ...htmlRunFont(span.tags, scheme, at("labelRuns")) };
360
+ const last = runs.at(-1);
361
+ if (last !== undefined && JSON.stringify({ ...last, text: "" }) === JSON.stringify({ ...run, text: "" })) {
362
+ last.text = `${String(last.text)}${run.text}`;
363
+ }
364
+ else {
365
+ runs.push(run);
366
+ }
367
+ }
368
+ trimBlankRuns(runs);
369
+ if (runs.every((r) => Object.keys(r).length === 1)) {
370
+ return;
371
+ }
372
+ if (runs.length === 1) {
373
+ const { text: _text, ...font } = runs[0];
374
+ for (const [property, value] of Object.entries(font)) {
375
+ put(RUN_PROPERTY[property], value);
376
+ }
377
+ return;
378
+ }
379
+ runs[0].text = String(runs[0].text).trimStart();
380
+ const end = runs.at(-1);
381
+ if (end !== undefined) {
382
+ end.text = String(end.text).trimEnd();
383
+ }
384
+ put("labelRuns", JSON.stringify(runs));
385
+ }
386
+ /**
387
+ * Drops the blank runs at a label's ends, as htmlLabelText trims its blanks.
388
+ * @param runs - the runs, trimmed in place
389
+ */
390
+ function trimBlankRuns(runs) {
391
+ while (runs.length > 0 && String(runs[0].text).trim() === "") {
392
+ runs.shift();
393
+ }
394
+ while (runs.length > 0 && String(runs.at(-1)?.text).trim() === "") {
395
+ runs.pop();
396
+ }
397
+ }
398
+ /** The label property a run's own font field sets when the whole label is one run. */
399
+ const RUN_PROPERTY = {
400
+ weight: "labelWeight",
401
+ italic: "labelItalic",
402
+ font: "labelFont",
403
+ size: "labelSize",
404
+ color: "labelColor",
405
+ };
406
+ /**
407
+ * The font of a run inside HTML font tags, as `labelRuns` fields: bold, italic, and the innermost `FONT`'s face,
408
+ * point size and color.
409
+ * @param tags - the open tags, outermost first
410
+ * @param scheme - the element's color scheme
411
+ * @param site - where a bad color is noted
412
+ * @returns the fields that differ from the label's own font
413
+ */
414
+ function htmlRunFont(tags, scheme, site) {
415
+ const out = {};
416
+ for (const tag of tags) {
417
+ const name = /^<(\w+)/.exec(tag)?.[1].toLowerCase();
418
+ if (name === "b") {
419
+ out.weight = "bold";
420
+ }
421
+ else if (name === "i") {
422
+ out.italic = true;
423
+ }
424
+ else if (name === "font") {
425
+ for (const [attr, value] of tagAttributes(tag)) {
426
+ readFontAttribute(attr.toLowerCase(), value, scheme, site, out);
427
+ }
428
+ }
429
+ }
430
+ return out;
431
+ }
432
+ /**
433
+ * Reads one attribute of a `FONT` tag into a run's font fields: its face, point size or color.
434
+ * @param attr - the attribute's name, lowercase
435
+ * @param value - its value
436
+ * @param scheme - the element's color scheme
437
+ * @param site - where a bad color is noted
438
+ * @param out - the run's font fields, set
439
+ */
440
+ function readFontAttribute(attr, value, scheme, site, out) {
441
+ if (attr === "face" && value.trim() !== "" && value.length <= MAX_STYLE_VALUE_LENGTH) {
442
+ const drawn = graphvizFont(value);
443
+ out.font = drawn.family;
444
+ if (drawn.bold) {
445
+ out.weight = "bold";
446
+ }
447
+ if (drawn.italic) {
448
+ out.italic = true;
449
+ }
450
+ }
451
+ else if (attr === "point-size" && Number(value) > 0) {
452
+ out.size = Number(value);
453
+ }
454
+ else if (attr === "color") {
455
+ const c = canonColor(value, "graphviz", site, { scheme, use: "text" });
456
+ if (c !== null) {
457
+ out.color = c.color;
458
+ }
459
+ }
460
+ }
461
+ /** Whether a character can be part of an HTML attribute's name (a word character or `-`). */
462
+ const ATTRIBUTE_NAME_CHAR = /^[\w-]$/;
463
+ /**
464
+ * The quoted attributes of a tag, `name = "value"` or `name = 'value'`, in order; an unquoted or unclosed value is
465
+ * skipped. Scans the tag once (an attribute regex would backtrack on a long name with no `=`).
466
+ * @param tag - the tag
467
+ * @returns each attribute's name and value
468
+ */
469
+ function tagAttributes(tag) {
470
+ const attributes = [];
471
+ let i = 0;
472
+ while (i < tag.length) {
473
+ if (!ATTRIBUTE_NAME_CHAR.test(tag[i])) {
474
+ i++;
475
+ continue;
476
+ }
477
+ let end = i;
478
+ while (end < tag.length && ATTRIBUTE_NAME_CHAR.test(tag[end])) {
479
+ end++;
480
+ }
481
+ const value = quotedValueAt(tag, end);
482
+ if (value !== null) {
483
+ attributes.push([tag.slice(i, end), value.text]);
484
+ }
485
+ i = value === null ? end : value.next;
486
+ }
487
+ return attributes;
488
+ }
489
+ /**
490
+ * The quoted value of an attribute whose name ends at `at`: blanks, `=`, blanks, then text between matching quotes.
491
+ * @param tag - the tag
492
+ * @param at - where the name ends
493
+ * @returns the value and where reading goes on after its closing quote, or null when there is none
494
+ */
495
+ function quotedValueAt(tag, at) {
496
+ let i = at;
497
+ while (i < tag.length && /\s/.test(tag[i])) {
498
+ i++;
499
+ }
500
+ if (tag[i] !== "=") {
501
+ return null;
502
+ }
503
+ i++;
504
+ while (i < tag.length && /\s/.test(tag[i])) {
505
+ i++;
506
+ }
507
+ const quote = tag[i];
508
+ const close = quote === '"' || quote === "'" ? tag.indexOf(quote, i + 1) : -1;
509
+ return close < 0 ? null : { text: tag.slice(i + 1, close), next: close + 1 };
510
+ }
264
511
  /**
265
512
  * Reads a color into a property and its alpha into the property's opacity.
266
513
  * @param raw - the color
267
514
  * @param scheme - the object's color scheme
268
515
  * @param use - how Graphviz draws it
269
516
  * @param property - the color property
270
- * @param opacity - its opacity property, or null when the alpha is dropped
517
+ * @param opacity - its opacity property, or null when the alpha is dropped (an outline's is its borderOpacity)
271
518
  * @param put - receives the values
272
519
  * @param at - the sites
273
520
  * @returns the alpha, or null when the color is absent
@@ -281,11 +528,15 @@ function readColor(raw, scheme, use, property, opacity, put, at) {
281
528
  return null;
282
529
  }
283
530
  put(property, c.color);
284
- if (opacity !== null) {
531
+ if (property === "borderColor") {
532
+ // a transparent outline is no outline (the caller sets its width to 0); a part-transparent one is drawn so
533
+ put("borderOpacity", c.alpha > 0 && c.alpha < 1 ? c.alpha : null);
534
+ }
535
+ else if (opacity !== null) {
285
536
  put(opacity, c.alpha);
286
537
  }
287
- else if (c.alpha > 0 || property !== "borderColor") {
288
- // a transparent outline is no outline (the caller sets its width to 0); any other alpha is dropped, noted
538
+ else {
539
+ // any other alpha is dropped, noted
289
540
  colorOpacity(c, null, at(property), false);
290
541
  }
291
542
  return c.alpha;
@@ -332,21 +583,142 @@ function inches(raw, at) {
332
583
  * @param at - the sites
333
584
  */
334
585
  function readCommon(get, tokens, scheme, names, put, at) {
335
- if (tokens.includes("invis")) {
586
+ // Graphviz takes `invisible` for `invis` too (emit.c `parse_style`)
587
+ if (tokens.includes("invis") || tokens.includes("invisible")) {
336
588
  put("visible", false);
337
589
  }
338
- readColor(nonEmpty(get("fontcolor")), scheme, "line", "labelColor", null, put, at);
590
+ // a part-transparent font color draws its text so (`#377eb880`); an opaque one needs no opacity
591
+ const putFont = (property, value) => {
592
+ put(property, property === "labelOpacity" && value === 1 ? null : value);
593
+ };
594
+ readColor(nonEmpty(get("fontcolor")), scheme, "text", "labelColor", "labelOpacity", putFont, at);
339
595
  const size = get("fontsize");
340
596
  put("labelSize", size === null ? null : canonNumber(size, at("labelSize")));
341
597
  const font = get("fontname");
342
598
  if (font !== null && font.length <= MAX_STYLE_VALUE_LENGTH) {
343
- put("labelFont", font);
599
+ const drawn = graphvizFont(font);
600
+ put("labelFont", drawn.family);
601
+ put("labelWeight", drawn.bold ? "bold" : null);
602
+ put("labelItalic", drawn.italic ? true : null);
344
603
  }
345
604
  const tip = get("tooltip") ?? get("edgetooltip");
346
605
  if (tip !== null) {
347
606
  put("tooltip", dotLabelText(tip, names, at("tooltip")));
348
607
  }
349
608
  }
609
+ /**
610
+ * Reads a node's label and, for a node that is not a cluster, its size and how it grows to hold the label.
611
+ * @param n - the node
612
+ * @param names - what the label's escapes stand for
613
+ */
614
+ function readNodeLabelAndSize(n, names) {
615
+ const { get, cx, put, at, shape } = n;
616
+ // label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
617
+ const label = n.point ? null : get(LABEL_ATTRIBUTE);
618
+ const shown = label === null ? cx.name : labelText(label, names, cx.html, RECORD.has(shape), at("label"));
619
+ readNodeLabel(n, label, shown, names);
620
+ // plain is its label alone at size 0: with an empty label Graphviz draws nothing, filled or not; an HTML label
621
+ // with no words (a table holding an IMG) still draws its cells
622
+ const htmlLabel = label !== null && (isHtmlString(label) || cx.html.has(`<${label}>`));
623
+ if (shape === "plain" && shown === "" && !htmlLabel) {
624
+ put("visible", false);
625
+ }
626
+ if (cx.cluster) {
627
+ readClusterBox(get, put);
628
+ }
629
+ else {
630
+ readNodeSize(n, label, shown, htmlLabel, names);
631
+ }
632
+ }
633
+ /**
634
+ * Reads a node's label, its alignment and runs, and where it sits.
635
+ * @param n - the node
636
+ * @param label - its label, or null when it has none
637
+ * @param shown - the text it shows
638
+ * @param names - what the label's escapes stand for
639
+ */
640
+ function readNodeLabel(n, label, shown, names) {
641
+ const { get, cx, put, at, shape } = n;
642
+ // a cluster has no label of its own unless one is given, so an empty one is none
643
+ if (label !== null && !(cx.cluster && label === "")) {
644
+ put("label", shown);
645
+ readLabelAlign(label, names, cx.html, RECORD.has(shape), put);
646
+ readHtmlRuns(label, cx.html, n.scheme, put, at);
647
+ if (cx.cluster) {
648
+ put("labelPosition", clusterLabelPosition(get));
649
+ readClusterLabelPlace(get, put);
650
+ }
651
+ }
652
+ if (!cx.cluster && !n.point) {
653
+ const position = nodeLabelPosition(get);
654
+ put("labelPosition", position === "center" ? null : position);
655
+ }
656
+ }
657
+ /**
658
+ * Reads the size of a node that is not a cluster and how it grows to hold its label (a record's fields, or its
659
+ * label's fit).
660
+ * @param n - the node
661
+ * @param label - its label, or null when it has none
662
+ * @param shown - the text it shows
663
+ * @param htmlLabel - whether the label is HTML-like
664
+ * @param names - what the label's escapes stand for
665
+ */
666
+ function readNodeSize(n, label, shown, htmlLabel, names) {
667
+ const { get, put, at, shape } = n;
668
+ readSize(get, shape, n.point, put, at);
669
+ if (RECORD.has(shape) && label !== null && !htmlLabel) {
670
+ readRecord(n, label, names);
671
+ }
672
+ else if (!n.point) {
673
+ readLabelFit(get, shape, label, shown, put);
674
+ readQuantum(n, label);
675
+ }
676
+ }
677
+ /** Graphviz's default node width and height, in points (0.75 and 0.5 inch). */
678
+ const DEFAULT_NODE_SIZE = { w: 54, h: 36 };
679
+ /**
680
+ * Reads a record node as Graphviz lays it out: its size (at least its fields' unless `fixedsize`), the dividers
681
+ * between its fields as lines over the node, and each field's text lines where Graphviz centers or justifies them,
682
+ * in place of one label.
683
+ * @param n - the node
684
+ * @param label - its record label
685
+ * @param names - what the label's escapes stand for
686
+ */
687
+ function readRecord(n, label, names) {
688
+ const { get, put, at, cx } = n;
689
+ const pt = (attr, fallback) => {
690
+ const v = inches(get(attr), at(attr === "width" ? "width" : "height"));
691
+ return v === null ? fallback : v * DOT_POINTS_PER_INCH;
692
+ };
693
+ const size = Number(get("fontsize") ?? 14);
694
+ const lay = layoutRecord(label, !cx.flip, pt("width", DEFAULT_NODE_SIZE.w), pt("height", DEFAULT_NODE_SIZE.h), isTrue(get("fixedsize")), Number.isFinite(size) && size > 0 ? size : 14, (line) => dotLabelText(line, names));
695
+ const hw = lay.width / 2;
696
+ const hh = lay.height / 2;
697
+ const fx = (x) => String(num6(hw > 0 ? x / hw : 0));
698
+ const fy = (y) => String(num6(hh > 0 ? y / hh : 0));
699
+ put("width", num6(lay.width));
700
+ put("height", num6(lay.height));
701
+ put("label", "");
702
+ put("shapeLines", lay.dividers.length === 0
703
+ ? null
704
+ : lay.dividers.map(([x0, y0, x1, y1]) => `${fx(x0)},${fy(y0)} ${fx(x1)},${fy(y1)}`).join("; "));
705
+ put("shapeTexts", lay.texts.length === 0
706
+ ? null
707
+ : JSON.stringify(lay.texts.map((t) => [Number(fx(t.x)), Number(fy(t.y)), t.text])));
708
+ }
709
+ /**
710
+ * Reads a laid-out cluster's size from its `bb` ("llx,lly,urx,ury" in points), the box Graphviz draws round its
711
+ * members and its label; a cluster with no `bb` is drawn round its members.
712
+ * @param get - the cluster's attributes
713
+ * @param put - receives the values
714
+ */
715
+ function readClusterBox(get, put) {
716
+ const parts = (get("bb") ?? "").split(",").map(Number);
717
+ if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] >= parts[0] && parts[3] >= parts[1]) {
718
+ put("width", num6(parts[2] - parts[0]));
719
+ put("height", num6(parts[3] - parts[1]));
720
+ }
721
+ }
350
722
  /**
351
723
  * Reads one node, or one cluster's container, the way Graphviz draws it.
352
724
  * @param get - its attributes
@@ -379,20 +751,66 @@ function readNode(get, cx, put, at) {
379
751
  else {
380
752
  readNodeFill(n);
381
753
  }
754
+ readNodeBands(n);
382
755
  readNodeOutline(n);
383
756
  readNodeShape(n);
384
- // label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
385
- const label = n.point ? null : get(LABEL_ATTRIBUTE);
386
- if (label !== null) {
387
- put("label", labelText(label, names, cx.html, RECORD.has(shape), at("label")));
757
+ readNodeLabelAndSize(n, names);
758
+ if (nonEmpty(get("xlabel")) !== null) {
759
+ noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
388
760
  }
389
- if (!cx.cluster) {
390
- readSize(get, shape, n.point, label ?? (n.point ? null : cx.name), put, at);
761
+ }
762
+ /**
763
+ * Where a node's label sits: its `labelloc` puts it against the top or bottom of the node's box, inside it; it is
764
+ * centered by default.
765
+ * @param get - the node's attributes
766
+ * @returns the position
767
+ */
768
+ function nodeLabelPosition(get) {
769
+ const loc = (get("labelloc") ?? "").trim().toLowerCase();
770
+ if (loc.startsWith("t")) {
771
+ return "inside-top";
772
+ }
773
+ return loc.startsWith("b") ? "inside-bottom" : "center";
774
+ }
775
+ /** How far in from the cluster's side Graphviz draws a label justified left or right: half its label's padding. */
776
+ const CLUSTER_LABEL_INSET = 8;
777
+ /**
778
+ * Where Graphviz draws a cluster's label beyond its position: at a stored `lp` (its center) inside the cluster's
779
+ * `bb`, else `CLUSTER_LABEL_INSET` points in from the side `labeljust` puts it against.
780
+ * @param get - the cluster's attributes
781
+ * @param put - receives the values
782
+ */
783
+ function readClusterLabelPlace(get, put) {
784
+ const lp = (get("lp") ?? "").split(",").map(Number);
785
+ const bb = (get("bb") ?? "").split(",").map(Number);
786
+ if (lp.length === 2 && bb.length === 4 && [...lp, ...bb].every(Number.isFinite)) {
787
+ put("labelPosition", "center");
788
+ put("labelOffsetX", num6(lp[0] - (bb[0] + bb[2]) / 2));
789
+ put("labelOffsetY", num6((bb[1] + bb[3]) / 2 - lp[1]));
790
+ return;
791
+ }
792
+ const just = (get("labeljust") ?? "").trim().toLowerCase();
793
+ if (just.startsWith("l")) {
794
+ put("labelOffsetX", CLUSTER_LABEL_INSET);
391
795
  }
392
- if (Number(get("peripheries")) > 1 || nonEmpty(get("xlabel")) !== null) {
393
- noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, nonEmpty(get("xlabel")) === null ? "peripheries" : "xlabel");
796
+ else if (just.startsWith("r")) {
797
+ put("labelOffsetX", -CLUSTER_LABEL_INSET);
394
798
  }
395
799
  }
800
+ /**
801
+ * Where a cluster's label sits: inside the cluster's box (Graphviz grows the box to hold it), at the top unless
802
+ * `labelloc` is `b`, centered unless `labeljust` is `l` or `r`.
803
+ * @param get - the cluster's attributes
804
+ * @returns the position
805
+ */
806
+ function clusterLabelPosition(get) {
807
+ const loc = (get("labelloc") ?? "").trim().toLowerCase().startsWith("b") ? "inside-bottom" : "inside-top";
808
+ const just = (get("labeljust") ?? "").trim().toLowerCase();
809
+ if (just.startsWith("l")) {
810
+ return `${loc}-left`;
811
+ }
812
+ return just.startsWith("r") ? `${loc}-right` : loc;
813
+ }
396
814
  /**
397
815
  * Reads a cluster's fill: its fill color when filled, else its background.
398
816
  * @param n - the cluster
@@ -403,27 +821,154 @@ function readClusterFill(n) {
403
821
  const bg = nonEmpty(get("bgcolor"));
404
822
  const raw = filled ? (nonEmpty(get("fillcolor")) ?? n.color ?? bg ?? "lightgrey") : bg;
405
823
  readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
824
+ if (!tokens.includes("striped")) {
825
+ readFillGradient(n, raw);
826
+ }
827
+ }
828
+ /**
829
+ * Reads a gradient fill (`fillcolor="a:b"`, `gradientangle`): Graphviz runs it from the list's first color to
830
+ * its second, `gradientangle` degrees counter-clockwise from left to right (0 by default), which is the neutral
831
+ * angle's clockwise turn negated, from 0 up to 360. `style=radial` runs it out from the middle instead. A second color
832
+ * left out is black; colors after the second are not drawn. A first color's `;fraction` makes Graphviz switch colors
833
+ * at that fraction, which its SVG writes as stops at the fraction less 0.001 and at the fraction.
834
+ * @param n - the node or cluster
835
+ * @param raw - its fill color value
836
+ */
837
+ function readFillGradient(n, raw) {
838
+ const colon = raw === null ? -1 : raw.indexOf(":");
839
+ if (raw === null || colon < 0) {
840
+ return;
841
+ }
842
+ const radial = n.tokens.includes("radial");
843
+ if (radial) {
844
+ // ponytail: a radial gradientangle moves Graphviz's focus off the middle; graphty's radial ramp has no focus
845
+ n.put("fillGradientKind", "radial");
846
+ }
847
+ const frac = Number(raw.slice(0, colon).split(";")[1]);
848
+ if (frac > 0 && frac < 1) {
849
+ n.put("fillGradientStops", `${String(num6(frac - 0.001))} ${String(num6(frac))}`);
850
+ }
851
+ const second = raw
852
+ .slice(colon + 1)
853
+ .split(":")[0]
854
+ .split(";")[0];
855
+ const c = canonColor(second === "" ? "black" : second, "graphviz", n.at("fillGradient"), {
856
+ scheme: n.scheme,
857
+ use: "fill",
858
+ });
859
+ if (c === null) {
860
+ return;
861
+ }
862
+ n.put("fillGradient", c.color);
863
+ if (radial) {
864
+ return;
865
+ }
866
+ const angle = Number(nonEmpty(n.get("gradientangle")) ?? "0");
867
+ // 0 up to 360: the neutral angle turns clockwise on screen, Graphviz's counter-clockwise
868
+ const turned = Number.isFinite(angle) ? ((-angle % 360) + 360) % 360 : 0;
869
+ n.put("fillGradientAngle", num6(turned) + 0);
406
870
  }
407
871
  /**
408
872
  * Reads a node's fill: a point is always filled; a striped fill fills a box and a wedged one an ellipse only.
409
873
  * @param n - the node
410
874
  */
411
875
  function readNodeFill(n) {
412
- const { shape, point } = n;
876
+ const { point } = n;
413
877
  const fillcolor = nonEmpty(n.get("fillcolor"));
414
- const filled = point ||
415
- n.tokens.some((t) => t === "filled" ||
416
- t === "radial" ||
417
- (t === "striped" && BOX_LIKE.has(shape)) ||
418
- (t === "wedged" && ELLIPSE_LIKE.has(shape)));
419
- if (filled) {
878
+ if (isFilled(n)) {
420
879
  const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
421
880
  readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
881
+ // stripes on a box and wedges on an ellipse take the color list in place of a gradient
882
+ const banded = (n.tokens.includes("striped") && BOX_LIKE.has(n.shape)) ||
883
+ (n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape));
884
+ if ((n.tokens.includes("filled") || n.tokens.includes("radial")) && !banded) {
885
+ readFillGradient(n, raw);
886
+ }
422
887
  }
423
888
  else if (fillcolor !== null) {
424
889
  noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
425
890
  }
426
891
  }
892
+ /**
893
+ * Reads a node's striped or wedged fill (`style=striped` on a box, `style=wedged` on an ellipse) from its color
894
+ * list, `color;fraction` separated by `:`: a color with no fraction shares what the others leave, the last color takes
895
+ * what sized colors leave when none is unsized, and the shares stop at the whole
896
+ * (https://graphviz.org/docs/attr-types/colorList/). Graphviz draws stripes left to right and
897
+ * wedges counter-clockwise from 3 o'clock, which is the same pie as the wedges in reverse order clockwise from there.
898
+ * @param n - the node
899
+ */
900
+ function readNodeBands(n) {
901
+ // a cluster is a box, so it takes stripes too (and never wedges)
902
+ const striped = n.tokens.includes("striped") && (n.cx.cluster || BOX_LIKE.has(n.shape));
903
+ const wedged = !n.cx.cluster && n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape);
904
+ const list = nonEmpty(n.get("fillcolor")) ?? n.color;
905
+ if ((!striped && !wedged) || !list?.includes(":")) {
906
+ return;
907
+ }
908
+ const bands = colorBands(list, n.scheme, "fill", n.at(striped ? "stripeColors" : "pieColors"));
909
+ if (bands.length === 0) {
910
+ return;
911
+ }
912
+ const order = striped ? bands : [...bands].reverse();
913
+ const prefix = striped ? "stripe" : "pie";
914
+ n.put(`${prefix}Colors`, order.map((b) => b.color).join(" "));
915
+ n.put(`${prefix}Sizes`, order.map((b) => String(b.size)).join(" "));
916
+ if (striped) {
917
+ n.put("stripeDirection", "right");
918
+ }
919
+ else {
920
+ n.put("pieFitBox", true);
921
+ n.put("pieRotation", 90);
922
+ }
923
+ }
924
+ /**
925
+ * The bands of a Graphviz color list (`color;fraction` separated by `:`), in order: a color with no fraction shares
926
+ * what the others leave, the last color takes what sized colors leave when none is unsized, and the shares stop at
927
+ * the whole (https://graphviz.org/docs/attr-types/colorList/, emit.c `parseSegs`).
928
+ * @param list - the color list
929
+ * @param scheme - the color scheme
930
+ * @param use - how Graphviz draws the colors
931
+ * @param site - where it was read
932
+ * @returns each band's color and share of the whole
933
+ */
934
+ function colorBands(list, scheme, use, site) {
935
+ const parts = list.split(":").map((part) => {
936
+ const [color, fraction] = part.split(";");
937
+ const c = canonColor(color.trim(), "graphviz", site, { scheme, use });
938
+ const f = fraction === undefined ? Number.NaN : Number(fraction);
939
+ return { color: c?.color ?? null, fraction: Number.isFinite(f) && f >= 0 ? f : null };
940
+ });
941
+ const kept = parts.flatMap((p) => (p.color === null ? [] : [{ color: p.color, fraction: p.fraction }]));
942
+ const given = kept.reduce((sum, p) => sum + (p.fraction ?? 0), 0);
943
+ const unsized = kept.filter((p) => p.fraction === null).length;
944
+ const share = unsized > 0 ? Math.max(0, 1 - given) / unsized : 0;
945
+ let used = 0;
946
+ const bands = kept.flatMap((p) => {
947
+ const size = Math.min(p.fraction ?? share, 1 - used);
948
+ used += Math.max(size, 0);
949
+ return size > 0 ? [{ color: p.color, size: num6(size) }] : [];
950
+ });
951
+ // every color sized and short of the whole: Graphviz gives what is left to the last one (emit.c parseSegs)
952
+ const last = bands.at(-1);
953
+ if (last !== undefined && unsized === 0 && used < 1) {
954
+ last.size = num6(last.size + 1 - used);
955
+ }
956
+ return bands;
957
+ }
958
+ /**
959
+ * Whether Graphviz fills a node: a point always; `filled` and `radial` any shape, `striped` a box and `wedged` an
960
+ * ellipse only.
961
+ * @param n - the node
962
+ * @returns true when its box or shape is filled
963
+ */
964
+ function isFilled(n) {
965
+ const { shape, point } = n;
966
+ return (point ||
967
+ n.tokens.some((t) => t === "filled" ||
968
+ t === "radial" ||
969
+ (t === "striped" && BOX_LIKE.has(shape)) ||
970
+ (t === "wedged" && ELLIPSE_LIKE.has(shape))));
971
+ }
427
972
  /**
428
973
  * Reads a node's or cluster's outline: its color, width and dashes.
429
974
  * @param n - the node
@@ -437,7 +982,8 @@ function readNodeOutline(n) {
437
982
  const outline = cx.cluster ? (nonEmpty(get("pencolor")) ?? n.color) : n.color;
438
983
  const alpha = readColor(outline, n.scheme, "line", "borderColor", null, put, at);
439
984
  const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
440
- if (alpha === 0) {
985
+ // peripheries=0 draws no outline, on a node and on a cluster alike
986
+ if (alpha === 0 || Number(nonEmpty(get("peripheries")) ?? "1") === 0) {
441
987
  put("borderWidth", 0);
442
988
  }
443
989
  else if (width !== null) {
@@ -453,21 +999,293 @@ function readNodeOutline(n) {
453
999
  else if (tokens.includes("dotted")) {
454
1000
  put("borderStyle", "dotted");
455
1001
  }
1002
+ if (!cx.cluster) {
1003
+ readNodeRings(n);
1004
+ }
1005
+ }
1006
+ /** The outlines Graphviz draws round a node of these shapes when `peripheries` is not set. */
1007
+ export const SHAPE_PERIPHERIES = new Map([
1008
+ ["doublecircle", 2],
1009
+ ["doubleoctagon", 2],
1010
+ ["tripleoctagon", 3],
1011
+ ]);
1012
+ /** How far apart Graphviz draws a node's outlines, center to center (`GAP`, lib/common/shapes.c). */
1013
+ export const PERIPHERY_GAP = 4;
1014
+ /**
1015
+ * Reads a node's second outline (`peripheries=2`, `doublecircle`): Graphviz draws it `PERIPHERY_GAP` points outside
1016
+ * the first, in the same color, width and dashes. graphty draws one outer ring, so a third and later outline is
1017
+ * noted as not carried.
1018
+ * @param n - the node
1019
+ */
1020
+ function readNodeRings(n) {
1021
+ const raw = nonEmpty(n.get("peripheries"));
1022
+ const count = raw === null ? (SHAPE_PERIPHERIES.get(n.shape) ?? 1) : Math.trunc(Number(raw));
1023
+ if (Number.isNaN(count) || count <= 1 || NO_OUTLINE.has(n.shape)) {
1024
+ return;
1025
+ }
1026
+ const c = n.color === null
1027
+ ? null
1028
+ : canonColor(n.color, "graphviz", n.at("outerBorderColor"), {
1029
+ scheme: n.scheme,
1030
+ use: "line",
1031
+ });
1032
+ if (c !== null && c.alpha === 0) {
1033
+ return;
1034
+ }
1035
+ const widthRaw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
1036
+ const parsed = widthRaw === null ? Number.NaN : Number(widthRaw);
1037
+ const unset = n.tokens.includes("bold") ? 2 : 1;
1038
+ const pen = Number.isFinite(parsed) ? Math.max(0, parsed) : unset;
1039
+ n.put("outerBorderColor", c?.color ?? "#000000");
1040
+ n.put("outerBorderOpacity", c !== null && c.alpha < 1 ? c.alpha : null);
1041
+ n.put("outerBorderWidth", num6(pen));
1042
+ // the gap between the outline's outer edge and the ring's inner edge
1043
+ n.put("outerBorderOffset", num6(Math.max(0, PERIPHERY_GAP - pen)));
1044
+ if (n.tokens.includes("dashed") || n.tokens.includes("dotted")) {
1045
+ n.put("outerBorderStyle", n.tokens.includes("dashed") ? "dashed" : "dotted");
1046
+ }
1047
+ if (count > 2) {
1048
+ noteAt(n.at("outerBorderWidth"), STYLE_ISSUE.NOT_CARRIED, `peripheries=${String(count)}`);
1049
+ }
456
1050
  }
457
1051
  /**
458
- * Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is.
1052
+ * Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is. Graphviz draws
1053
+ * a striped box with square corners whatever `rounded` says.
459
1054
  * @param n - the node
460
1055
  */
461
1056
  function readNodeShape(n) {
462
- const rounded = n.tokens.includes("rounded");
1057
+ const rounded = n.tokens.includes("rounded") && !n.tokens.includes("striped");
1058
+ const turned = turnedPolygon(n);
463
1059
  if (n.cx.cluster) {
464
1060
  n.put("shape", rounded ? "round-rectangle" : "rectangle");
465
1061
  }
1062
+ else if (n.shape === "polygon") {
1063
+ readPolygon(n, rounded);
1064
+ }
1065
+ else if (turned !== null) {
1066
+ n.put("shape", "polygon");
1067
+ n.put("shapePoints", cornersText(graphvizDrawnCorners(turned)));
1068
+ n.put("shapePointsStretch", false);
1069
+ }
1070
+ else if (NAMED_OUTLINES.has(n.shape)) {
1071
+ n.put("shape", "polygon");
1072
+ n.put("shapePoints", NAMED_OUTLINES.get(n.shape) ?? null);
1073
+ // a polygon shape's corners are where Graphviz draws them, one reaching past its box too
1074
+ n.put("shapePointsStretch", false);
1075
+ }
1076
+ else if (GRAPHVIZ_GLYPH_SHAPES.has(n.shape)) {
1077
+ const size = (name) => {
1078
+ const v = inches(n.get(name), n.at(name));
1079
+ return v === null ? null : v * DOT_POINTS_PER_INCH;
1080
+ };
1081
+ putGlyph(n.shape, size("width"), size("height"), n.put, () => {
1082
+ const raw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
1083
+ const pen = raw === null ? Number.NaN : Number(raw);
1084
+ if (Number.isFinite(pen)) {
1085
+ return Math.max(0, pen);
1086
+ }
1087
+ return n.tokens.includes("bold") ? 2 : 1;
1088
+ }, () => n.color === null
1089
+ ? null
1090
+ : (canonColor(n.color, "graphviz", n.at("outerBorderColor"), { scheme: n.scheme, use: "line" })
1091
+ ?.color ?? null));
1092
+ }
466
1093
  else if (n.shapeRaw !== null) {
467
- n.put("shape", rounded && BOX_LIKE.has(n.shape)
1094
+ const shape = rounded && BOX_LIKE.has(n.shape)
468
1095
  ? "round-rectangle"
469
- : canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape")));
1096
+ : canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape"));
1097
+ // a filled shape=none is its box filled, with no outline (its border is 0): a rectangle, not a node with no body
1098
+ n.put("shape", shape === "none" && isFilled(n) ? "rectangle" : shape);
1099
+ }
1100
+ }
1101
+ /** The polygon shapes `orientation` turns, by their own sides and orientation (`shapes.c`). */
1102
+ const TURNABLE_POLYGONS = new Map([
1103
+ ...[...BOX_LIKE].map((name) => [name, { sides: 4, orientation: 0 }]),
1104
+ ["diamond", { sides: 4, orientation: 45 }],
1105
+ ["hexagon", { sides: 6, orientation: 0 }],
1106
+ ["octagon", { sides: 8, orientation: 0 }],
1107
+ ]);
1108
+ /**
1109
+ * The polygon a named polygon shape is when its `orientation` turns it off its own axes (a box turned 30 degrees),
1110
+ * or null when it is drawn as its shape: Graphviz adds the attribute to the shape's own orientation.
1111
+ * @param n - the node
1112
+ * @returns the polygon, or null
1113
+ */
1114
+ function turnedPolygon(n) {
1115
+ const turn = Number(nonEmpty(n.get("orientation")) ?? "0");
1116
+ const own = TURNABLE_POLYGONS.get(n.shape) ?? GRAPHVIZ_POLYGON_SHAPES.get(n.shape);
1117
+ if (own === undefined || !Number.isFinite(turn) || turn % 360 === 0) {
1118
+ return null;
1119
+ }
1120
+ const polygon = {
1121
+ distortion: 0,
1122
+ skew: 0,
1123
+ ...own,
1124
+ orientation: own.orientation + Math.max(-360, turn),
1125
+ };
1126
+ // a quarter turn of a box is a box again
1127
+ return isGraphvizBox(polygon) ? null : polygon;
1128
+ }
1129
+ /**
1130
+ * Reads a `shape=polygon` node: a box when it is one (four sides, no distortion or skew, square to the axes), an
1131
+ * ellipse below three sides, else its corners as Graphviz places them.
1132
+ * @param n - the node
1133
+ * @param rounded - whether its style rounds a box's corners
1134
+ */
1135
+ function readPolygon(n, rounded) {
1136
+ const number = (name, fallback, least) => {
1137
+ const raw = nonEmpty(n.get(name));
1138
+ const v = raw === null ? Number.NaN : Number(raw);
1139
+ return Number.isFinite(v) ? Math.max(least, v) : fallback;
1140
+ };
1141
+ const polygon = {
1142
+ sides: Math.trunc(number("sides", 4, 0)),
1143
+ distortion: number("distortion", 0, -100),
1144
+ skew: number("skew", 0, -100),
1145
+ orientation: number("orientation", 0, -360),
1146
+ };
1147
+ if (isGraphvizBox(polygon)) {
1148
+ n.put("shape", rounded ? "round-rectangle" : "rectangle");
1149
+ }
1150
+ else if (polygon.sides < 3) {
1151
+ n.put("shape", "ellipse");
1152
+ }
1153
+ else {
1154
+ n.put("shape", "polygon");
1155
+ n.put("shapePoints", cornersText(graphvizDrawnCorners(polygon)));
1156
+ n.put("shapePointsStretch", false);
1157
+ }
1158
+ }
1159
+ /**
1160
+ * A Graphviz polygon's corners where Graphviz draws them, as fractions of its box's half width and half height: it
1161
+ * grows a polygon that falls short of the box along an axis until it reaches the box's sides, and keeps one that
1162
+ * reaches past them (a distortion, a skew, an egg) as it is (shapes.c `poly_init`: `scalex = bb.x / xmax`).
1163
+ * @param polygon - the polygon's attributes
1164
+ * @returns the corners
1165
+ */
1166
+ function graphvizDrawnCorners(polygon) {
1167
+ const corners = graphvizPolygonCorners(polygon);
1168
+ const grow = (axis) => Math.max(1, 1 / Math.max(...corners.map((c) => Math.abs(c[axis]))));
1169
+ const [gx, gy] = [grow(0), grow(1)];
1170
+ return corners.map(([x, y]) => [x * gx, y * gy]);
1171
+ }
1172
+ /**
1173
+ * Corners as a `shapePoints` value.
1174
+ * @param corners - the corners
1175
+ * @returns `x,y x,y ...`
1176
+ */
1177
+ function cornersText(corners) {
1178
+ return corners.map(([x, y]) => `${num6(x)},${num6(y)}`).join(" ");
1179
+ }
1180
+ /** The outline of each Graphviz polygon shape graphty draws as a polygon of its corners, by shape name. */
1181
+ const NAMED_OUTLINES = new Map([...GRAPHVIZ_POLYGON_SHAPES].map(([name, polygon]) => [name, cornersText(graphvizDrawnCorners(polygon))]));
1182
+ /**
1183
+ * A Graphviz shape drawn as pieces of its own (`graphvizGlyph`) at a node's size, as the neutral values that draw it:
1184
+ * a polygon of its outline drawn where it says, its further pieces (`shapeParts`) and lines (`shapeLines`), each as
1185
+ * fractions of the node's half width and half height. An `underline` is filled with no line round it, its line alone
1186
+ * drawn in the pen's width.
1187
+ * @param shape - the shape name, lowercase
1188
+ * @param width - the node's width in points, or null for Graphviz's default
1189
+ * @param height - its height, or null
1190
+ * @param put - receives the values
1191
+ * @param pen - the node's pen width, for an outline drawn as a line alone and a ring
1192
+ * @param color - the node's pen color, for a ring
1193
+ */
1194
+ export function putGlyph(shape, width, height, put, pen, color) {
1195
+ const text = glyphTexts(shape, width, height);
1196
+ if (text === null) {
1197
+ return;
1198
+ }
1199
+ put("shape", "polygon");
1200
+ put("shapePoints", text.outline);
1201
+ put("shapePointsStretch", false);
1202
+ put("shapeParts", text.parts);
1203
+ put("shapeLines", text.lines);
1204
+ if (text.unstroked) {
1205
+ put("shapeLineWidth", pen());
1206
+ put("borderWidth", 0);
1207
+ }
1208
+ if (text.ring !== null) {
1209
+ // drawn in the pen, its gap the distance less a pen's width (the outline's half and its own)
1210
+ put("outerBorderColor", color() ?? "#000000");
1211
+ put("outerBorderWidth", num6(pen()));
1212
+ put("outerBorderOffset", num6(Math.max(0, text.ring - pen())));
1213
+ }
1214
+ }
1215
+ /**
1216
+ * A Graphviz glyph's pieces as `shapePoints`, `shapeParts` and `shapeLines` text at a node's size, or null for a shape
1217
+ * that is not one.
1218
+ * @param shape - the shape name, lowercase
1219
+ * @param width - the node's width in points, or null for Graphviz's default
1220
+ * @param height - its height, or null
1221
+ * @returns the texts (parts and lines null when there are none)
1222
+ */
1223
+ function glyphTexts(shape, width, height) {
1224
+ const glyph = graphvizGlyph(shape, width, height);
1225
+ if (glyph === null) {
1226
+ return null;
1227
+ }
1228
+ // fractions of the half width and half height the glyph was laid out at
1229
+ const half = (v, fallback) => (v !== null && v > 0 ? v : fallback) / 2;
1230
+ const [W, H] = [half(width, 54), half(height, 36)];
1231
+ const list = (points) => cornersText(points.map(([x, y]) => [x / W, y / H]));
1232
+ const lists = (all) => all.length === 0 ? null : all.map(list).join("; ");
1233
+ return {
1234
+ outline: list(glyph.outline),
1235
+ parts: lists(glyph.parts),
1236
+ lines: lists(glyph.lines),
1237
+ unstroked: glyph.unstroked === true,
1238
+ ring: glyph.ring ?? null,
1239
+ };
1240
+ }
1241
+ /**
1242
+ * The corners of a Graphviz shape graphty draws as a polygon (`house`, `cylinder`, `note` ...), as a `shapePoints`
1243
+ * value, or undefined for a shape graphty draws by name.
1244
+ * @param shape - the shape name, lowercase
1245
+ * @returns the corners, or undefined
1246
+ */
1247
+ export function graphvizOutline(shape) {
1248
+ return NAMED_OUTLINES.get(shape);
1249
+ }
1250
+ /**
1251
+ * Whether Graphviz draws a shape with no outline (`plain`, `plaintext`, `none`).
1252
+ * @param shape - the shape name, lowercase
1253
+ * @returns true for a shape with no outline
1254
+ */
1255
+ export function graphvizHasNoOutline(shape) {
1256
+ return NO_OUTLINE.has(shape);
1257
+ }
1258
+ /** The shape name of each outline in `NAMED_OUTLINES`, so a written node keeps its Graphviz shape. */
1259
+ const OUTLINE_NAMES = new Map([...NAMED_OUTLINES].map(([name, text]) => [text, name]));
1260
+ /**
1261
+ * The Graphviz glyph shape a polygon is at a node's size, its outline, parts and lines alike, so a written node keeps
1262
+ * its shape (a ribosome site and a protease site share their outline and differ in their stem).
1263
+ * @param get - the node's values
1264
+ * @param width - the node's width in points, or null
1265
+ * @param height - its height, or null
1266
+ * @returns the shape name and whether it is filled with no line round it, or undefined
1267
+ */
1268
+ function glyphOf(get, width, height) {
1269
+ const text = (property) => {
1270
+ const v = get(property);
1271
+ return typeof v === "string" ? v.trim() : null;
1272
+ };
1273
+ const outline = text("shapePoints");
1274
+ if (outline === null) {
1275
+ return undefined;
1276
+ }
1277
+ // a glyph with another number of corners cannot match: skip writing its texts, which is most of the work
1278
+ const corners = outline === "" ? 0 : outline.split(/\s+/).length;
1279
+ for (const name of GRAPHVIZ_GLYPH_SHAPES) {
1280
+ if (graphvizGlyph(name, width, height)?.outline.length !== corners) {
1281
+ continue;
1282
+ }
1283
+ const g = glyphTexts(name, width, height);
1284
+ if (g !== null && g.outline === outline && g.parts === text("shapeParts") && g.lines === text("shapeLines")) {
1285
+ return { name, unstroked: g.unstroked };
1286
+ }
470
1287
  }
1288
+ return undefined;
471
1289
  }
472
1290
  /**
473
1291
  * A pen width as Graphviz draws it: a negative `penwidth` is raised to 0, its minimum, and noted.
@@ -486,15 +1304,14 @@ function penWidth(raw, attribute, site) {
486
1304
  }
487
1305
  /**
488
1306
  * Reads a node's width and height: exact where Graphviz draws them exactly (`fixedsize`, an empty label that is not
489
- * a record's, a point), otherwise minimums it grows to fit the label, noted.
1307
+ * a record's, a point), otherwise minimums it grows to fit the label (`readLabelFit`).
490
1308
  * @param get - the node's attributes
491
1309
  * @param shape - its shape, lowercase
492
1310
  * @param point - whether it is a point
493
- * @param label - the label it shows, or null
494
1311
  * @param put - receives the values
495
1312
  * @param at - the sites
496
1313
  */
497
- function readSize(get, shape, point, label, put, at) {
1314
+ function readSize(get, shape, point, put, at) {
498
1315
  let w = inches(get("width"), at("width"));
499
1316
  let h = inches(get("height"), at("height"));
500
1317
  const same = point || CIRCLE_LIKE.has(shape) || isTrue(get("regular"));
@@ -522,13 +1339,80 @@ function readSize(get, shape, point, label, put, at) {
522
1339
  else if (same) {
523
1340
  ww = hh = Math.max(w ?? 0, h ?? 0);
524
1341
  }
525
- const exact = point || isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape));
526
- if (!exact) {
527
- noteAt(at("width"), STYLE_ISSUE.SIZE_MINIMUM, "grows to fit its label");
528
- }
529
1342
  put("width", ww === null ? null : canonNumber(ww, at("width"), DOT_POINTS_PER_INCH));
530
1343
  put("height", hh === null ? null : canonNumber(hh, at("height"), DOT_POINTS_PER_INCH));
531
1344
  }
1345
+ /**
1346
+ * Reads the graph's `quantum` for a node that grows to hold its label: Graphviz rounds the padded label box up to a
1347
+ * multiple of it before fitting the outline round it (`poly_init`), so the node is at least one quantum across, an
1348
+ * ellipse's box times the square root of 2.
1349
+ * @param n - the node
1350
+ * @param label - its `label` attribute, or null
1351
+ */
1352
+ function readQuantum(n, label) {
1353
+ const { get, put, shape } = n;
1354
+ let scale = BOX_LIKE.has(shape) ? 1 : null;
1355
+ if (ELLIPSE_LIKE.has(shape)) {
1356
+ scale = Math.SQRT2;
1357
+ }
1358
+ const w = Number(get("width") ?? Number.NaN);
1359
+ const h = Number(get("height") ?? Number.NaN);
1360
+ // ponytail: one quantum is the least size; a label box over one quantum is not rounded up to the next, and only
1361
+ // boxes and ellipses with their size given are read (other polygons fit their box by factors of their own)
1362
+ if (n.cx.quantum === 0 || scale === null || label === "" || isTrue(get("fixedsize")) || !(w >= 0 && h >= 0)) {
1363
+ return;
1364
+ }
1365
+ const least = n.cx.quantum * scale;
1366
+ put("width", num6(Math.max(w * DOT_POINTS_PER_INCH, least)));
1367
+ put("height", num6(Math.max(h * DOT_POINTS_PER_INCH, least)));
1368
+ }
1369
+ /** Graphviz's padding around a node's label when it has no `margin`: 8 points left and right, 4 above and below. */
1370
+ const LABEL_PAD = { x: 8, y: 4 };
1371
+ /** Graphviz's line height, as a multiple of the font size (`LINESPACING`, lib/common/const.h). */
1372
+ const LINE_SPACING = 1.2;
1373
+ /**
1374
+ * Reads how a node that is not drawn at a fixed size grows to hold its label (`poly_init`, lib/common/shapes.c):
1375
+ * its padded label box fits inside its outline, the `margin` (else 8 points by 4) on each side, and a circle, a
1376
+ * square or a `regular` node stays as wide as it is tall. The neutral label box is one font size per line, so the
1377
+ * rest of Graphviz's line height (1.2 font sizes) goes into the vertical padding.
1378
+ * @param get - the node's attributes
1379
+ * @param shape - its shape, lowercase
1380
+ * @param label - its `label` attribute, or null
1381
+ * @param shown - the label it shows
1382
+ * @param put - receives the values
1383
+ */
1384
+ function readLabelFit(get, shape, label, shown, put) {
1385
+ if (isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape))) {
1386
+ return;
1387
+ }
1388
+ const lines = Math.max(1, shown.replace(/\n$/, "").split("\n").length);
1389
+ const size = Number(get("fontsize") ?? 14);
1390
+ const fontSize = Number.isFinite(size) && size > 0 ? size : 14;
1391
+ const margin = (get("margin") ?? "")
1392
+ .split(",")
1393
+ .filter((text) => text.trim() !== "")
1394
+ .map(Number)
1395
+ .filter(isLength);
1396
+ // plain is plaintext with a margin of 0
1397
+ const pad = shape === "plain" ? { x: 0, y: 0 } : LABEL_PAD;
1398
+ const padX = margin.length > 0 ? margin[0] * DOT_POINTS_PER_INCH : pad.x;
1399
+ const padY = margin.length > 0 ? (margin[1] ?? margin[0]) * DOT_POINTS_PER_INCH : pad.y;
1400
+ // a decorated box holds its label as a box does, exactly
1401
+ put("labelFit", GRAPHVIZ_DECORATED_BOXES.has(shape) ? "bounds" : "inside");
1402
+ put("labelPaddingX", num6(padX));
1403
+ put("labelPaddingY", num6(padY + ((LINE_SPACING - 1) * fontSize * lines) / 2));
1404
+ if (CIRCLE_LIKE.has(shape) || isTrue(get("regular"))) {
1405
+ put("labelFitSquare", true);
1406
+ }
1407
+ }
1408
+ /**
1409
+ * Whether a `margin` part is a usable length: a number at least 0.
1410
+ * @param value - the part
1411
+ * @returns whether it is
1412
+ */
1413
+ function isLength(value) {
1414
+ return Number.isFinite(value) && value >= 0;
1415
+ }
532
1416
  /**
533
1417
  * Reads one edge the way Graphviz draws it.
534
1418
  * @param get - its attributes
@@ -541,6 +1425,7 @@ function readEdge(get, cx, put, at) {
541
1425
  const scheme = get("colorscheme");
542
1426
  readCommon(get, tokens, scheme, cx.names, put, at);
543
1427
  readColor(nonEmpty(get("color")), scheme, "edge", "color", "opacity", put, at);
1428
+ readEdgeSegments(nonEmpty(get("color")), scheme, put, at);
544
1429
  readEdgeLine(get, tokens, put, at);
545
1430
  readEdgeArrows(get, cx, put, at);
546
1431
  for (const [attribute, property] of [
@@ -548,14 +1433,241 @@ function readEdge(get, cx, put, at) {
548
1433
  ["headlabel", "targetLabel"],
549
1434
  ["taillabel", "sourceLabel"],
550
1435
  ]) {
551
- const raw = get(attribute);
1436
+ // an edge's xlabel is drawn beside its middle, as its label is: it is the label when there is no other
1437
+ const raw = attribute === LABEL_ATTRIBUTE ? (get(attribute) ?? nonEmpty(get("xlabel"))) : get(attribute);
552
1438
  if (raw !== null) {
553
1439
  put(property, labelText(raw, cx.names, cx.html, false, at(property)));
1440
+ if (property === "label") {
1441
+ readLabelAlign(raw, cx.names, cx.html, false, put);
1442
+ readHtmlRuns(raw, cx.html, scheme, put, at);
1443
+ }
554
1444
  }
555
1445
  }
556
- if (nonEmpty(get("xlabel")) !== null) {
1446
+ if (get(LABEL_ATTRIBUTE) !== null && nonEmpty(get("xlabel")) !== null) {
557
1447
  noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
558
1448
  }
1449
+ readEndLabelFace(get, scheme, put, at);
1450
+ }
1451
+ /**
1452
+ * Reads the face of an edge's head and tail labels where it is not the label's: `labelfontsize`, `labelfontcolor` and
1453
+ * `labelfontname` (https://graphviz.org/docs/attrs/labelfontsize/), each the edge's `fontsize`, `fontcolor` and
1454
+ * `fontname` when unset, which the end labels then take from the label.
1455
+ * @param get - the edge's attributes
1456
+ * @param scheme - its color scheme
1457
+ * @param put - receives the values
1458
+ * @param at - the sites
1459
+ */
1460
+ function readEndLabelFace(get, scheme, put, at) {
1461
+ const ends = ["target", "source"].filter((end) => get(end === "target" ? "headlabel" : "taillabel") !== null);
1462
+ const size = get("labelfontsize");
1463
+ const font = get("labelfontname");
1464
+ for (const end of ends) {
1465
+ readColor(nonEmpty(get("labelfontcolor")), scheme, "text", `${end}LabelColor`, null, put, at);
1466
+ if (size !== null) {
1467
+ put(`${end}LabelSize`, canonNumber(size, at(`${end}LabelSize`)));
1468
+ }
1469
+ // ponytail: the family only; a bold or italic end-label face is drawn in the label's weight and slant
1470
+ if (font !== null && font.length <= MAX_STYLE_VALUE_LENGTH) {
1471
+ put(`${end}LabelFont`, graphvizFont(font).family);
1472
+ }
1473
+ }
1474
+ }
1475
+ /**
1476
+ * Reads an edge's colored segments: a color list with a `;fraction` in it (`red;0.3:blue`) draws the line in one piece
1477
+ * per color along its length, which is a linear gradient with a hard switch at each band's end. A list with no
1478
+ * fraction draws parallel lines, which graphty does not draw (the line is in the first color).
1479
+ * @param list - the edge's `color`
1480
+ * @param scheme - its color scheme
1481
+ * @param put - receives the values
1482
+ * @param at - the sites
1483
+ */
1484
+ function readEdgeSegments(list, scheme, put, at) {
1485
+ if (list === null || !list.includes(":") || !list.includes(";")) {
1486
+ return;
1487
+ }
1488
+ const bands = colorBands(list, scheme, "edge", at("gradientColors"));
1489
+ if (bands.length < 2) {
1490
+ return;
1491
+ }
1492
+ const colors = [];
1493
+ const stops = [];
1494
+ let start = 0;
1495
+ for (const band of bands) {
1496
+ colors.push(band.color, band.color);
1497
+ stops.push(num6(start), num6(Math.min(1, start + band.size)));
1498
+ start += band.size;
1499
+ }
1500
+ put("gradientKind", "linear");
1501
+ put("gradientColors", colors.join(" "));
1502
+ put("gradientStops", stops.join(" "));
1503
+ }
1504
+ /**
1505
+ * What an edge's label places are read against: the nodes' centers and sizes.
1506
+ * @param snapshot - the graph
1507
+ * @returns the context
1508
+ */
1509
+ function labelPlaceContext(snapshot) {
1510
+ const column = snapshot.nodes.byRole("position");
1511
+ return {
1512
+ center: (i) => {
1513
+ const v = column?.isSet(i) === true ? column.value(i) : null;
1514
+ return v === null ? null : [v[0], v[1]];
1515
+ },
1516
+ node: (i, name) => nonEmpty(cellText(snapshot.nodes, name, i)),
1517
+ external: (cellText(snapshot.graph, "layout", 0)?.trim().toLowerCase() ?? "dot") !== "dot",
1518
+ };
1519
+ }
1520
+ /**
1521
+ * The route Graphviz laid an edge along, from its `pos`: `[e,x,y] [s,x,y]` arrow tips, then the B-spline's `3n + 1`
1522
+ * control points (y up), flattened into bends between the two nodes' centers. Null for an edge Graphviz drew
1523
+ * straight, a `pos` that is not a spline, or nodes without positions.
1524
+ * @param snapshot - the graph
1525
+ * @param e - the edge
1526
+ * @param pos - its `pos` text
1527
+ * @returns the `bends` value, or null
1528
+ */
1529
+ function splineBends(snapshot, e, pos) {
1530
+ if (pos === null) {
1531
+ return null;
1532
+ }
1533
+ const column = snapshot.nodes.byRole("position");
1534
+ const center = (i) => {
1535
+ const v = column?.isSet(i) === true ? column.value(i) : null;
1536
+ return v === null ? null : [v[0], v[1]];
1537
+ };
1538
+ const [source, target] = [center(snapshot.edgeSource(e)), center(snapshot.edgeTarget(e))];
1539
+ if (source === null || target === null) {
1540
+ return null;
1541
+ }
1542
+ return graphvizPosBends(source, target, pos);
1543
+ }
1544
+ /**
1545
+ * The bends of an edge Graphviz laid, from its `pos` text (`[e,x,y] [s,x,y]` arrow tips, then each spline's
1546
+ * `3n + 1` control points, y up, splines separated by `;`), between the two nodes' centers in the same coordinates.
1547
+ * @param source - the source node's center
1548
+ * @param target - the target node's center
1549
+ * @param pos - the edge's `pos` text
1550
+ * @returns the `bends` value, or null for a straight route or a `pos` that is not a spline
1551
+ * @category Styles
1552
+ */
1553
+ export function graphvizPosBends(source, target, pos) {
1554
+ // the arrow tips of the first spline's start and the last spline's end
1555
+ const tip = (spline, flag) => {
1556
+ const token = (spline ?? "")
1557
+ .trim()
1558
+ .split(/\s+/)
1559
+ .find((t) => t.startsWith(`${flag},`));
1560
+ const [x, y] = (token ?? "").slice(2).split(",").map(Number);
1561
+ return token !== undefined && Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
1562
+ };
1563
+ const parts = pos.split(";");
1564
+ const tips = { start: tip(parts[0], "s"), end: tip(parts.at(-1), "e") };
1565
+ // ponytail: the splines of a concentrated edge (";"-separated) are joined end to end
1566
+ const splines = parts.map((spline) => spline
1567
+ .trim()
1568
+ .split(/\s+/)
1569
+ .filter((t) => !/^[es],/.test(t))
1570
+ .map((t) => {
1571
+ const [x, y] = t.split(",").map(Number);
1572
+ return [x, y];
1573
+ }));
1574
+ return graphvizRouteBends(source, target, splines, tips);
1575
+ }
1576
+ /** How far off the line between its nodes' centers, in points, a Graphviz route may pass and still be straight. */
1577
+ const GRAPHVIZ_STRAIGHT_POINTS = 1;
1578
+ /** How far apart Graphviz's neato lays the loops on one node, across (x) and along (y) its side, in points. */
1579
+ const GRAPHVIZ_LOOP_STEP = { x: 18, y: 9 };
1580
+ /**
1581
+ * The self-loops Graphviz lays itself, with no `pos` of their own and no ports: neato's `selfRight` (splines.c), on
1582
+ * the node's right side, the k-th of a node's n loops two cubic curves from its center out to `rw + 18k` points right
1583
+ * of it and back, `9k / n` points above and then below its center line (measured on Graphviz 14.1.2's and 2.43's
1584
+ * drawings of nodes from 0.33 to 1 inch).
1585
+ * @param snapshot - the graph
1586
+ * @returns the bends of an edge that is such a loop, or null
1587
+ */
1588
+ function graphvizLoops(snapshot) {
1589
+ const portless = (get) => nonEmpty(get("tailport")) === null &&
1590
+ nonEmpty(get("headport")) === null &&
1591
+ nonEmpty(get(POS_ATTRIBUTE)) === null;
1592
+ const onNode = new Map();
1593
+ // ponytail: ortho routes a loop round the node's corners and none draws no edge; both keep the element's loop.
1594
+ const splines = (nonEmpty(cellText(snapshot.graph, "splines", 0)) ?? "").toLowerCase();
1595
+ // a file graphty wrote keeps its loops' look in its own block
1596
+ const laid = splines !== "ortho" && splines !== "none" && nonEmpty(cellText(snapshot.graph, STYLE_BLOCK_NAME, 0)) === null;
1597
+ for (let e = 0; e < snapshot.edgeCount && laid; e++) {
1598
+ const n = snapshot.edgeSource(e);
1599
+ if (n === snapshot.edgeTarget(e) && portless((name) => cellText(snapshot.edges, name, e))) {
1600
+ onNode.set(n, [...(onNode.get(n) ?? []), e]);
1601
+ }
1602
+ }
1603
+ const inches = (n, name, fallback) => {
1604
+ const v = Number(nonEmpty(cellText(snapshot.nodes, name, n)) ?? Number.NaN);
1605
+ return (Number.isFinite(v) && v > 0 ? v : fallback) * DOT_POINTS_PER_INCH;
1606
+ };
1607
+ return {
1608
+ bendsOf: (e, get) => {
1609
+ const n = snapshot.edgeSource(e);
1610
+ const list = onNode.get(n);
1611
+ if (n !== snapshot.edgeTarget(e) || list === undefined || !portless(get)) {
1612
+ return null;
1613
+ }
1614
+ // ponytail: a node Graphviz grows to fit its label is taken at its stated width and height
1615
+ const [w, h] = [inches(n, "width", 0.75), inches(n, "height", 0.5)];
1616
+ const k = list.indexOf(e) + 1;
1617
+ const dx = w / 2 + GRAPHVIZ_LOOP_STEP.x * k;
1618
+ const dy = (GRAPHVIZ_LOOP_STEP.y * k) / list.length;
1619
+ const curve = flattenCubic([
1620
+ [0, 0],
1621
+ [dx / 3, dy],
1622
+ [dx, dy],
1623
+ [dx, 0],
1624
+ [dx, -dy],
1625
+ [dx / 3, -dy],
1626
+ [0, 0],
1627
+ ]);
1628
+ return curve === null ? null : canonNodeOffsets([0, 0], [w, h], curve.slice(1, -1), false);
1629
+ },
1630
+ };
1631
+ }
1632
+ /**
1633
+ * The bends of a route Graphviz laid: B-splines of `3n + 1` control points each (y up), flattened into bends between
1634
+ * the two nodes' centers. Null for a route on the line between the centers, which is the straight edge it looks like,
1635
+ * or for control points that are not a spline. Also reads the `Line` of a GML file Graphviz's gv2gml wrote.
1636
+ * @param source - the source node's center
1637
+ * @param target - the target node's center
1638
+ * @param splines - the control points of each spline, joined end to end
1639
+ * @param tips - the arrow tips `pos` stores (`s,x,y` and `e,x,y`): the line runs on to each, along its arrow
1640
+ * @param tips.start - the tail arrow's tip, or null
1641
+ * @param tips.end - the head arrow's tip, or null
1642
+ * @returns the `bends` value, or null
1643
+ */
1644
+ export function graphvizRouteBends(source, target, splines, tips) {
1645
+ const points = [];
1646
+ for (const controls of splines) {
1647
+ const flat = flattenCubic(controls);
1648
+ if (flat === null || flat.some(([x, y]) => !Number.isFinite(x) || !Number.isFinite(y))) {
1649
+ return null;
1650
+ }
1651
+ points.push(...flat);
1652
+ }
1653
+ // a tip where the spline already ends adds no point
1654
+ const apart = (a, b) => b === undefined || Math.hypot(a[0] - b[0], a[1] - b[1]) > 1e-6;
1655
+ const start = tips?.start ?? null;
1656
+ const end = tips?.end ?? null;
1657
+ if (start !== null && apart(start, points[0])) {
1658
+ points.unshift(start);
1659
+ }
1660
+ if (end !== null && apart(end, points.at(-1))) {
1661
+ points.push(end);
1662
+ }
1663
+ const [dx, dy] = [target[0] - source[0], target[1] - source[1]];
1664
+ const length = Math.hypot(dx, dy);
1665
+ const off = length > 0
1666
+ ? Math.max(...points.map(([x, y]) => Math.abs((x - source[0]) * dy - (y - source[1]) * dx))) / length
1667
+ : 0;
1668
+ // a route on the line between the centers is the straight edge it looks like: Graphviz draws its straight lines
1669
+ // (`splines=false`) up to half a point off that line
1670
+ return off > Math.max(1e-3 * length, GRAPHVIZ_STRAIGHT_POINTS) ? canonBends(source, target, points, false) : null;
559
1671
  }
560
1672
  /**
561
1673
  * Reads an edge's line: its width and dashes.
@@ -599,15 +1711,88 @@ function readEdgeArrows(get, cx, put, at) {
599
1711
  // with none of these the tool-default rule draws the arrow of the graph's kind
600
1712
  if (dirRaw !== null || head !== null || tail !== null) {
601
1713
  const dir = (dirRaw ?? (cx.directed ? "forward" : "none")).trim().toLowerCase();
602
- const forward = dir === "forward" || dir === "both";
603
- const back = dir === "back" || dir === "both";
604
- put("targetArrow", forward ? canonEnum("arrow", head ?? "normal", "graphviz", at("targetArrow")) : "none");
605
- put("sourceArrow", back ? canonEnum("arrow", tail ?? "normal", "graphviz", at("sourceArrow")) : "none");
1714
+ readEdgeArrow("target", head, dir === "forward" || dir === "both", put, at);
1715
+ readEdgeArrow("source", tail, dir === "back" || dir === "both", put, at);
606
1716
  }
607
1717
  const size = get("arrowsize");
608
1718
  if (size !== null) {
609
1719
  put("arrowSize", canonNumber(size, at("arrowSize")));
610
1720
  }
1721
+ // an edge's `fillcolor` fills its arrowheads (their outline stays the edge's color)
1722
+ const fill = nonEmpty(get("fillcolor"));
1723
+ for (const property of ["targetArrowColor", "sourceArrowColor"]) {
1724
+ readColor(fill, get("colorscheme"), "fill", property, null, put, at);
1725
+ }
1726
+ }
1727
+ /**
1728
+ * Reads one end's arrow: its shape, or none when the edge's direction draws no arrow there, and the side it keeps.
1729
+ * @param end - which end
1730
+ * @param raw - its `arrowhead` or `arrowtail`, or null
1731
+ * @param on - whether the edge's direction draws an arrow there
1732
+ * @param put - receives the values
1733
+ * @param at - the sites
1734
+ */
1735
+ function readEdgeArrow(end, raw, on, put, at) {
1736
+ const arrow = on ? graphvizArrowValue(raw ?? "normal", at(`${end}Arrow`)) : null;
1737
+ put(`${end}Arrow`, arrow === null ? "none" : arrow.value);
1738
+ if (arrow?.side !== null && arrow?.side !== undefined) {
1739
+ put(`${end}ArrowSide`, arrow.side);
1740
+ }
1741
+ }
1742
+ /** The sides an `l` or `r` arrow modifier keeps, by their first letters. */
1743
+ const ARROW_SIDES = ["left", "right"];
1744
+ /** The open forms of Graphviz arrows that a neutral shape draws; the others are drawn filled. */
1745
+ const GRAPHVIZ_OPEN_ARROWS = new Set(["normal", "diamond", "dot", "box", "inv"]);
1746
+ /** The open Graphviz arrows the neutral vocabulary has no name for, by the value graphty draws them as. */
1747
+ const GRAPHVIZ_OTHER_ARROWS = new Map([
1748
+ ["obox", "other:open-square"],
1749
+ ["oinv", "other:open-inverted"],
1750
+ ]);
1751
+ /**
1752
+ * The neutral arrow value Graphviz draws for an arrow name (`onormal`, `lbox`, `invempty`), with an open shape the
1753
+ * neutral vocabulary has no name for (`obox`, `oinv`) as the value graphty draws it as, and the side an `l` or `r`
1754
+ * keeps. Read so by the DOT reader and by the GML reader for gv2gml's `targetArrow` / `sourceArrow`.
1755
+ * @param raw - the arrow name
1756
+ * @param site - where it was read
1757
+ * @returns the value and its side (null for the whole shape)
1758
+ */
1759
+ export function graphvizArrowValue(raw, site) {
1760
+ const first = graphvizArrow(raw);
1761
+ const other = first === null ? undefined : GRAPHVIZ_OTHER_ARROWS.get(first.name);
1762
+ return {
1763
+ value: other ?? canonEnum("arrow", first?.name ?? raw, "graphviz", site),
1764
+ side: first?.side ?? null,
1765
+ };
1766
+ }
1767
+ /**
1768
+ * The arrow Graphviz draws at an edge's very end, from an arrow name of up to four shapes (`dotodot`,
1769
+ * `lteeoldiamond`): the first shape that is not `none`, with its `o` (open) kept where a neutral shape draws it, and
1770
+ * the side an `l` or `r` keeps alone ("clip the shape, leaving only the part to the left of the edge", looking along
1771
+ * the edge toward the arrow's tip).
1772
+ * @param raw - the arrow name
1773
+ * @returns the shape's own name and its side (null for the whole shape), or null for a name outside the grammar (a
1774
+ * synonym such as `empty`)
1775
+ */
1776
+ function graphvizArrow(raw) {
1777
+ let rest = raw.trim().toLowerCase();
1778
+ // the one synonym the grammar would misread (as `inv`): arrows.c's `invempty` is an open `inv`
1779
+ if (rest === "invempty") {
1780
+ rest = "oinv";
1781
+ }
1782
+ while (rest.length > 0) {
1783
+ const m = /^([olr]{0,3})(normal|crow|tee|box|diamond|dot|none|inv|vee|icurve|curve)/.exec(rest);
1784
+ if (m === null) {
1785
+ return null;
1786
+ }
1787
+ rest = rest.slice(m[0].length);
1788
+ const [, mods, shape] = m;
1789
+ if (shape !== "none") {
1790
+ const side = ARROW_SIDES.find((s) => mods.includes(s[0])) ?? null;
1791
+ const open = mods.includes("o") && GRAPHVIZ_OPEN_ARROWS.has(shape) ? "o" : "";
1792
+ return { name: open + shape, side };
1793
+ }
1794
+ }
1795
+ return null;
611
1796
  }
612
1797
  /**
613
1798
  * The HTML strings of a file: its HTML node names and labels, for Graphviz's shared string pool.
@@ -683,12 +1868,16 @@ function isCluster(snapshot, i) {
683
1868
  function contexts(snapshot, written = null) {
684
1869
  const html = htmlStrings(snapshot);
685
1870
  const graph = snapshot.meta.name ?? "";
1871
+ const rankdir = (cellText(snapshot.graph, "rankdir", 0) ?? "").trim().toUpperCase();
1872
+ const flip = rankdir === "LR" || rankdir === "RL";
1873
+ const q = Number(cellText(snapshot.graph, "quantum", 0) ?? Number.NaN);
1874
+ const quantum = Number.isFinite(q) && q > 0 ? q * DOT_POINTS_PER_INCH : 0;
686
1875
  const own = directedEdges(snapshot);
687
1876
  // a graph mixing both directions is written with one, every edge then drawn as that one
688
1877
  const directed = written === null || own.every((d) => d === snapshot.directed) ? own : own.map(() => written);
689
1878
  const name = (i) => String(snapshot.ids.idOf(i));
690
1879
  return {
691
- node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html }),
1880
+ node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html, flip, quantum }),
692
1881
  edge: (e) => {
693
1882
  const tail = name(snapshot.edgeSource(e));
694
1883
  const head = name(snapshot.edgeTarget(e));
@@ -727,6 +1916,25 @@ function styleColumns(table, attributes) {
727
1916
  const own = new Set([...attributes, ...NOTED_ATTRIBUTES]);
728
1917
  return [...table.names()].filter((c) => own.has(c) || c.startsWith("graphty.style."));
729
1918
  }
1919
+ /**
1920
+ * The graph's own styles: its `bgcolor` as its background.
1921
+ * @param snapshot - the graph
1922
+ * @param notes - where a bad color is noted
1923
+ * @returns the graph's values
1924
+ */
1925
+ function graphBackground(snapshot, notes) {
1926
+ const graph = {};
1927
+ const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
1928
+ if (!snapshot.graph.has("bgcolor") || bg === null) {
1929
+ return graph;
1930
+ }
1931
+ readColor(bg, cellText(snapshot.graph, "colorscheme", 0), "fill", "background", null, (p, v) => {
1932
+ if (v !== null) {
1933
+ graph[p] = v;
1934
+ }
1935
+ }, (property) => ({ notes, target: "graph", property }));
1936
+ return graph;
1937
+ }
730
1938
  /**
731
1939
  * The styles of a DOT file, read the way Graphviz draws it, with Graphviz's defaults beneath: an unfilled black
732
1940
  * ellipse 54 x 36 labeled with its name, black edges with a target arrow in a directed graph. A file graphty wrote
@@ -762,6 +1970,11 @@ export function dotStyles(snapshot, context) {
762
1970
  }
763
1971
  }
764
1972
  const edge = valueColumns(snapshot.edgeCount);
1973
+ const loops = graphvizLoops(snapshot);
1974
+ // `splines=none` and `splines=""` draw no edges at all (https://graphviz.org/docs/attrs/splines/)
1975
+ const splines = cellText(snapshot.graph, "splines", 0)?.trim().toLowerCase();
1976
+ const noEdges = splines === "" || splines === "none";
1977
+ const places = labelPlaceContext(snapshot);
765
1978
  for (let e = 0; e < snapshot.edgeCount; e++) {
766
1979
  context.budget.spend("edge", EDGE_ATTRIBUTES.length);
767
1980
  const get = (name) => cellText(snapshot.edges, name, e);
@@ -769,16 +1982,18 @@ export function dotStyles(snapshot, context) {
769
1982
  readEdge(get, cx.edge(e), (p, v) => {
770
1983
  edge.put(e, p, v);
771
1984
  }, at);
1985
+ if (noEdges) {
1986
+ edge.put(e, "visible", false);
1987
+ }
1988
+ const bends = splineBends(snapshot, e, get(POS_ATTRIBUTE)) ?? loops.bendsOf(e, get);
1989
+ if (bends !== null) {
1990
+ edge.put(e, "bends", bends);
1991
+ }
1992
+ readLabelPlaces(snapshot, places, e, get, (p, v) => {
1993
+ edge.put(e, p, v);
1994
+ });
772
1995
  }
773
- const graph = {};
774
- const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
775
- if (snapshot.graph.has("bgcolor") && bg !== null) {
776
- readColor(bg, cellText(snapshot.graph, "colorscheme", 0), "fill", "background", null, (p, v) => {
777
- if (v !== null) {
778
- graph[p] = v;
779
- }
780
- }, (property) => ({ notes, target: "graph", property }));
781
- }
1996
+ const graph = graphBackground(snapshot, notes);
782
1997
  const blockText = cellText(snapshot.graph, STYLE_BLOCK_NAME, 0);
783
1998
  if (blockText !== null) {
784
1999
  exactFromBlock(snapshot, blockText, { node: node.values, edge: edge.values }, cx);
@@ -856,8 +2071,9 @@ const ARROWS = new Map([
856
2071
  ["circle-open", "odot"],
857
2072
  ["square", "box"],
858
2073
  ["crow", "crow"],
859
- ["half", "lnormal"],
860
2074
  ["inverted", "inv"],
2075
+ ["other:open-square", "obox"],
2076
+ ["other:open-inverted", "oinv"],
861
2077
  ]);
862
2078
  /** Line and border styles Graphviz draws exactly; the others are written as the nearest. */
863
2079
  const STROKES = new Map([
@@ -884,7 +2100,10 @@ function graphvizDefaults(target) {
884
2100
  };
885
2101
  add("fontcolor", "labelColor");
886
2102
  add("fontsize", "labelSize");
887
- add("fontname", "labelFont");
2103
+ const font = t.labelFont?.value;
2104
+ if (typeof font === "string") {
2105
+ out.set("fontname", graphvizFontName(font, false, false));
2106
+ }
888
2107
  if (target === "node") {
889
2108
  add("color", "borderColor");
890
2109
  add("pencolor", "borderColor");
@@ -935,12 +2154,16 @@ function gvColor(color, opacity) {
935
2154
  return color + a.toString(16).padStart(2, "0");
936
2155
  }
937
2156
  /**
938
- * A label's text as a DOT label: backslashes and line breaks escaped, so Graphviz's escape passes give the text back.
2157
+ * A label's text as a DOT label: backslashes, line breaks and `&` escaped, so Graphviz's escape and entity passes
2158
+ * give the text back.
939
2159
  * @param text - the text
940
2160
  * @returns the label
941
2161
  */
942
2162
  function escapeLabel(text) {
943
- return text.replaceAll("\\", String.raw `\\`).replaceAll("\n", String.raw `\n`);
2163
+ return text
2164
+ .replaceAll("&", "&amp;")
2165
+ .replaceAll("\\", String.raw `\\`)
2166
+ .replaceAll("\n", String.raw `\n`);
944
2167
  }
945
2168
  /**
946
2169
  * A shape, line or arrow value in Graphviz's spelling.
@@ -965,10 +2188,11 @@ function spelled(table, value, nearest, property, approximate) {
965
2188
  /**
966
2189
  * The font, tooltip and visibility attributes every element shares.
967
2190
  * @param get - the element's values
2191
+ * @param approximate - records approximations
968
2192
  * @param out - the attributes, added to
969
2193
  * @param tokens - the style tokens, added to
970
2194
  */
971
- function writeCommon(get, out, tokens) {
2195
+ function writeCommon(get, approximate, out, tokens) {
972
2196
  const color = get("labelColor");
973
2197
  if (typeof color === "string") {
974
2198
  out.set("fontcolor", color);
@@ -978,8 +2202,21 @@ function writeCommon(get, out, tokens) {
978
2202
  out.set("fontsize", size);
979
2203
  }
980
2204
  const font = get("labelFont");
2205
+ const bold = get("labelWeight") === "bold";
2206
+ const italic = get("labelItalic") === true;
981
2207
  if (typeof font === "string") {
982
- out.set("fontname", font);
2208
+ const name = graphvizFontName(font, bold, italic);
2209
+ out.set("fontname", name);
2210
+ const drawn = graphvizFont(name);
2211
+ if (drawn.bold !== bold) {
2212
+ approximate("labelWeight", get("labelWeight") ?? "normal");
2213
+ }
2214
+ if (drawn.italic !== italic) {
2215
+ approximate("labelItalic", italic);
2216
+ }
2217
+ }
2218
+ else if (bold || italic) {
2219
+ approximate(bold ? "labelWeight" : "labelItalic", bold ? "bold" : true);
983
2220
  }
984
2221
  const tip = get("tooltip");
985
2222
  if (typeof tip === "string") {
@@ -993,14 +2230,15 @@ function writeCommon(get, out, tokens) {
993
2230
  * A node's or cluster's Graphviz attributes from its values.
994
2231
  * @param get - the node's values
995
2232
  * @param cx - the node's name and kind
2233
+ * @param raw - its attributes as data
996
2234
  * @param approximate - records approximations
997
2235
  * @param notes - where an HTML name is noted
998
2236
  * @returns the attributes
999
2237
  */
1000
- function nodeAttributes(get, cx, approximate, notes) {
2238
+ function nodeAttributes(get, cx, raw, approximate, notes) {
1001
2239
  const out = new Map();
1002
2240
  const tokens = [];
1003
- writeCommon(get, out, tokens);
2241
+ writeCommon(get, approximate, out, tokens);
1004
2242
  writeNodeOutline(get, cx, approximate, out, tokens);
1005
2243
  const gvShape = writeNodeShape(get, cx, approximate, out, tokens);
1006
2244
  if (!cx.cluster) {
@@ -1015,11 +2253,26 @@ function nodeAttributes(get, cx, approximate, notes) {
1015
2253
  else if (label === null && shown !== null) {
1016
2254
  out.set(LABEL_ATTRIBUTE, "");
1017
2255
  }
2256
+ noteLabelPosition(get("labelPosition"), cx, raw, approximate);
1018
2257
  if (tokens.length > 0) {
1019
2258
  out.set("style", tokens.join(","));
1020
2259
  }
1021
2260
  return withoutDefaults(out, "node");
1022
2261
  }
2262
+ /**
2263
+ * Notes a label position Graphviz does not draw: a node's label sits where its own `labelloc` (kept as data) puts
2264
+ * it, and a cluster's where its `labelloc` and `labeljust` do.
2265
+ * @param position - the label's position
2266
+ * @param cx - the node's name and kind
2267
+ * @param raw - the node's attributes as data
2268
+ * @param approximate - records approximations
2269
+ */
2270
+ function noteLabelPosition(position, cx, raw, approximate) {
2271
+ if (typeof position === "string" &&
2272
+ position !== (cx.cluster ? clusterLabelPosition(raw) : nodeLabelPosition(raw))) {
2273
+ approximate("labelPosition", position);
2274
+ }
2275
+ }
1023
2276
  /**
1024
2277
  * A node's or cluster's fill and outline attributes.
1025
2278
  * @param get - the node's values
@@ -1072,19 +2325,91 @@ function writeNodeShape(get, cx, approximate, out, tokens) {
1072
2325
  }
1073
2326
  return "";
1074
2327
  }
2328
+ const size = (name) => {
2329
+ const v = get(name);
2330
+ return typeof v === "number" ? v : null;
2331
+ };
2332
+ const glyph = shape === "polygon" ? glyphOf(get, size("width"), size("height")) : undefined;
2333
+ if (glyph !== undefined) {
2334
+ return writeGlyph(get, glyph, out);
2335
+ }
2336
+ approximateShapeLines(get, approximate);
2337
+ const polygon = shape === "polygon" ? writePolygon(get("shapePoints"), approximate, out) : null;
2338
+ if (polygon !== null) {
2339
+ // Graphviz draws its polygon's corners where it lays them, which read back unstretched
2340
+ if (get("shapePointsStretch") !== false) {
2341
+ approximate("shapePointsStretch", true);
2342
+ }
2343
+ return polygon;
2344
+ }
1075
2345
  const gvShape = spelled(SHAPES, shape, shape === "vee" ? "invtriangle" : "box", "shape", approximate);
1076
2346
  out.set("shape", gvShape);
1077
2347
  return gvShape;
1078
2348
  }
1079
2349
  /**
1080
- * A node's width and height, fixed so Graphviz does not grow them to fit the label.
2350
+ * A glyph's shape attribute, and its line's width as its pen's when it draws no outline.
2351
+ * @param get - the node's values
2352
+ * @param glyph - the glyph
2353
+ * @param out - the attributes, added to
2354
+ * @returns the Graphviz shape written
2355
+ */
2356
+ function writeGlyph(get, glyph, out) {
2357
+ out.set("shape", glyph.name);
2358
+ // an underline's line is drawn in its pen, which draws no outline round it
2359
+ const line = get("shapeLineWidth");
2360
+ if (glyph.unstroked && typeof line === "number") {
2361
+ out.set("penwidth", line);
2362
+ }
2363
+ return glyph.name;
2364
+ }
2365
+ /**
2366
+ * Notes a node's pieces and lines of no Graphviz shape as approximated: Graphviz has no attribute for them.
2367
+ * @param get - the node's values
2368
+ * @param approximate - records approximations
2369
+ */
2370
+ function approximateShapeLines(get, approximate) {
2371
+ for (const property of ["shapeParts", "shapeLines", "shapeLineWidth"]) {
2372
+ const v = get(property);
2373
+ if (v !== null && v !== undefined) {
2374
+ approximate(property, v);
2375
+ }
2376
+ }
2377
+ }
2378
+ /**
2379
+ * A polygon's shape attribute: the Graphviz shape whose outline it is, else `shape=polygon` with its number of sides.
2380
+ * @param points - its `shapePoints`
2381
+ * @param approximate - records approximations
2382
+ * @param out - the attributes, added to
2383
+ * @returns the Graphviz shape written, or null for corners too few to be a polygon
2384
+ */
2385
+ function writePolygon(points, approximate, out) {
2386
+ if (typeof points !== "string") {
2387
+ return null;
2388
+ }
2389
+ const named = OUTLINE_NAMES.get(points.trim());
2390
+ if (named !== undefined) {
2391
+ out.set("shape", named);
2392
+ return named;
2393
+ }
2394
+ // the corners themselves are not Graphviz attributes: the polygon keeps its number of sides
2395
+ const sides = points.trim().split(/\s+/).length;
2396
+ if (sides < 3) {
2397
+ return null;
2398
+ }
2399
+ out.set("shape", "polygon");
2400
+ out.set("sides", sides);
2401
+ approximate("shapePoints", points);
2402
+ return "polygon";
2403
+ }
2404
+ /**
2405
+ * A node's width and height, fixed so Graphviz does not grow them to fit the label, unless the node grows to fit it.
1081
2406
  * @param get - the node's values
1082
2407
  * @param out - the attributes, added to
1083
2408
  */
1084
2409
  function writeNodeSize(get, out) {
1085
2410
  const width = get("width");
1086
2411
  const height = get("height");
1087
- if (typeof width === "number" || typeof height === "number") {
2412
+ if ((typeof width === "number" || typeof height === "number") && typeof get("labelFit") !== "string") {
1088
2413
  out.set("fixedsize", "true");
1089
2414
  }
1090
2415
  if (typeof width === "number") {
@@ -1104,7 +2429,7 @@ function writeNodeSize(get, out) {
1104
2429
  function edgeAttributes(get, directed, approximate) {
1105
2430
  const out = new Map();
1106
2431
  const tokens = [];
1107
- writeCommon(get, out, tokens);
2432
+ writeCommon(get, approximate, out, tokens);
1108
2433
  const color = get("color");
1109
2434
  if (typeof color === "string") {
1110
2435
  out.set("color", gvColor(color, get("opacity")));
@@ -1118,6 +2443,16 @@ function edgeAttributes(get, directed, approximate) {
1118
2443
  tokens.push(spelled(STROKES, line, line.includes("dash") ? "dashed" : "solid", "lineStyle", approximate));
1119
2444
  }
1120
2445
  writeEdgeArrows(get, directed, approximate, out);
2446
+ // Graphviz fills both arrowheads with the one `fillcolor`
2447
+ const head = get("targetArrowColor");
2448
+ const tail = get("sourceArrowColor");
2449
+ const fill = head ?? tail;
2450
+ if (typeof fill === "string") {
2451
+ out.set("fillcolor", fill);
2452
+ if (head !== null && tail !== null && head !== tail) {
2453
+ approximate("sourceArrowColor", tail);
2454
+ }
2455
+ }
1121
2456
  const arrowSize = get("arrowSize");
1122
2457
  if (typeof arrowSize === "number") {
1123
2458
  out.set("arrowsize", arrowSize);
@@ -1149,7 +2484,7 @@ function writeEdgeArrows(get, directed, approximate, out) {
1149
2484
  const target = get("targetArrow");
1150
2485
  const tail = typeof source === "string" && source !== "none";
1151
2486
  const head = typeof target === "string" && target !== "none";
1152
- const plain = directed ? head && !tail && target === "triangle" : !head && !tail;
2487
+ const plain = directed ? head && !tail && target === "triangle" && get("targetArrowSide") === null : !head && !tail;
1153
2488
  if (plain || (source === null && target === null)) {
1154
2489
  return;
1155
2490
  }
@@ -1164,25 +2499,23 @@ function writeEdgeArrows(get, directed, approximate, out) {
1164
2499
  dir = "back";
1165
2500
  }
1166
2501
  out.set("dir", dir);
2502
+ // a one-sided arrow is its shape with an `l` or `r` before it; the neutral half arrow, one barb, is nearest to the
2503
+ // left half of Graphviz's triangle, which reads back as that
2504
+ const arrow = (value, side, property) => {
2505
+ if (value === "half") {
2506
+ approximate(property, value);
2507
+ approximate(`${property}Side`, value);
2508
+ return "lnormal";
2509
+ }
2510
+ const name = spelled(ARROWS, value, "normal", property, approximate);
2511
+ return side === "left" || side === "right" ? `${side[0]}${name}` : name;
2512
+ };
1167
2513
  if (head) {
1168
- out.set("arrowhead", spelled(ARROWS, target, "normal", "targetArrow", approximate));
2514
+ out.set("arrowhead", arrow(target, get("targetArrowSide"), "targetArrow"));
1169
2515
  }
1170
2516
  if (tail) {
1171
- out.set("arrowtail", spelled(ARROWS, source, "normal", "sourceArrow", approximate));
1172
- }
1173
- }
1174
- /**
1175
- * Each element's values of the properties the writer writes, as a getter per element.
1176
- * @param picture - each element's value of a property
1177
- * @param target - nodes or edges
1178
- * @returns the getter
1179
- */
1180
- function getter(picture, target) {
1181
- const columns = new Map();
1182
- for (const p of DOT_STYLE_CAPABILITIES[target]) {
1183
- columns.set(p, picture(target, p));
2517
+ out.set("arrowtail", arrow(source, get("sourceArrowSide"), "sourceArrow"));
1184
2518
  }
1185
- return (i) => (property) => columns.get(property)?.[i] ?? null;
1186
2519
  }
1187
2520
  /**
1188
2521
  * For a file graphty wrote: where a value read from the attributes is what the writer writes for the block's own
@@ -1197,40 +2530,31 @@ function getter(picture, target) {
1197
2530
  * @param cx - the elements' contexts
1198
2531
  */
1199
2532
  function exactFromBlock(snapshot, text, read, cx) {
1200
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
1201
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "dot") : null;
1202
- if (ex === null) {
1203
- return;
1204
- }
1205
2533
  const notes = new StyleNotes();
1206
- for (const target of ["node", "edge"]) {
1207
- const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1208
- const get = getter((t, p) => resolveStyles(ex, t, p, snapshot), target);
1209
- const values = read[target];
1210
- for (let i = 0; i < n; i++) {
1211
- const want = get(i);
1212
- const back = writtenAndReadBack(target, i, want, cx, notes);
1213
- for (const property of DOT_STYLE_CAPABILITIES[target]) {
1214
- const native = values[property]?.[i] ?? null;
1215
- const w = want(property);
1216
- if (native !== w && native === (back.get(property) ?? null)) {
1217
- values[property] ?? (values[property] = new Array(n).fill(null));
1218
- values[property][i] = w;
1219
- }
1220
- }
1221
- }
1222
- }
2534
+ takeFromBlock(snapshot, text, "dot", (target) => ({
2535
+ values: read[target],
2536
+ properties: DOT_STYLE_CAPABILITIES[target],
2537
+ writes: DOT_STYLE_CAPABILITIES[target],
2538
+ readBack: (i, want) => {
2539
+ const raw = (name) => cellText(target === "node" ? snapshot.nodes : snapshot.edges, name, i);
2540
+ const back = writtenAndReadBack(target, i, want, { cx, raw }, notes);
2541
+ return (property) => back.get(property) ?? null;
2542
+ },
2543
+ }));
1223
2544
  }
1224
2545
  /**
1225
2546
  * An element's values written as the writer writes them and read back as the reader reads them.
1226
2547
  * @param target - nodes or edges
1227
2548
  * @param i - the element
1228
2549
  * @param want - its values
1229
- * @param cx - the elements' contexts
2550
+ * @param data - the elements' contexts, and the element's attributes as data (what the writer leaves as it is)
2551
+ * @param data.cx - the elements' contexts
2552
+ * @param data.raw - the element's attributes as data
1230
2553
  * @param notes - a scratch for the notes the round trip makes
1231
2554
  * @returns the values read back, by property
1232
2555
  */
1233
- function writtenAndReadBack(target, i, want, cx, notes) {
2556
+ function writtenAndReadBack(target, i, want, data, notes) {
2557
+ const { cx, raw } = data;
1234
2558
  const ignore = () => undefined;
1235
2559
  const back = new Map();
1236
2560
  const put = (property, value) => {
@@ -1241,8 +2565,10 @@ function writtenAndReadBack(target, i, want, cx, notes) {
1241
2565
  const at = (property) => ({ notes, target, property });
1242
2566
  if (target === "node") {
1243
2567
  const ncx = cx.node(i);
1244
- const attrs = nodeAttributes(want, ncx, ignore, notes);
1245
- readNode((name) => textOf(attrs.get(name)), ncx, put, at);
2568
+ const attrs = nodeAttributes(want, ncx, raw, ignore, notes);
2569
+ // a cluster's label position stays in its data (`labelloc`, `labeljust`), which the writer leaves alone
2570
+ const kept = (name) => (name === "labelloc" || name === "labeljust" ? raw(name) : null);
2571
+ readNode((name) => (attrs.has(name) ? textOf(attrs.get(name)) : kept(name)), ncx, put, at);
1246
2572
  }
1247
2573
  else {
1248
2574
  const ecx = cx.edge(i);
@@ -1359,28 +2685,83 @@ function styledExport(snapshot, input, onMixedDirection) {
1359
2685
  */
1360
2686
  function attributeColumns(snapshot, input, target, w) {
1361
2687
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1362
- const get = getter((t, p) => input.picture(t, p), target);
2688
+ const get = elementValues((t, p) => input.picture(t, p), target, DOT_STYLE_CAPABILITIES[target]);
1363
2689
  const columns = new Map();
1364
2690
  for (let i = 0; i < n; i++) {
1365
2691
  const attrs = target === "node"
1366
- ? nodeAttributes(get(i), w.cx.node(i), w.approximate, w.notes)
2692
+ ? nodeAttributes(get(i), w.cx.node(i), (name) => cellText(snapshot.nodes, name, i), w.approximate, w.notes)
1367
2693
  : edgeAttributes(get(i), w.cx.edge(i).directed, w.approximate);
2694
+ // a self-loop's route is not written: Graphviz lays the loop itself, which is what reads back
2695
+ if (target === "edge" && snapshot.edgeSource(i) === snapshot.edgeTarget(i)) {
2696
+ w.approximate("bends", "self-loop");
2697
+ }
1368
2698
  // no label is written label="" (Graphviz would show the name), which a rule reading the label column
1369
2699
  // reads back as the empty label
1370
2700
  if (w.read.has(LABEL_ATTRIBUTE) && attrs.get(LABEL_ATTRIBUTE) === "" && get(i)("label") === null) {
1371
2701
  w.approximate("label", "");
1372
2702
  }
1373
- for (const [name, value] of attrs) {
1374
- let col = columns.get(name);
1375
- if (col === undefined) {
1376
- col = new Array(n).fill(null);
1377
- columns.set(name, col);
1378
- }
1379
- col[i] = value;
1380
- }
2703
+ setColumnValues(columns, attrs, i, n);
2704
+ }
2705
+ if (target === "node") {
2706
+ stopInheritance(snapshot, w.cx, columns, input.rules.styles.graph.background);
1381
2707
  }
1382
2708
  return columns;
1383
2709
  }
2710
+ /**
2711
+ * Puts one element's attributes into the columns, adding a column for an attribute not seen before.
2712
+ * @param columns - the columns, by name
2713
+ * @param attrs - the element's attributes
2714
+ * @param i - the element
2715
+ * @param n - how many elements there are
2716
+ */
2717
+ function setColumnValues(columns, attrs, i, n) {
2718
+ for (const [name, value] of attrs) {
2719
+ let col = columns.get(name);
2720
+ if (col === undefined) {
2721
+ col = new Array(n).fill(null);
2722
+ columns.set(name, col);
2723
+ }
2724
+ col[i] = value;
2725
+ }
2726
+ }
2727
+ /** What a cluster draws when neither it nor a graph around it sets the attribute: Graphviz's defaults. */
2728
+ const CLUSTER_UNSET = new Map([
2729
+ ["style", ""],
2730
+ ["penwidth", 1],
2731
+ ["color", ""],
2732
+ ["pencolor", ""],
2733
+ ["fillcolor", ""],
2734
+ ["bgcolor", ""],
2735
+ ["fontname", "Times-Roman"],
2736
+ ["fontsize", 14],
2737
+ ["fontcolor", "black"],
2738
+ [LABEL_ATTRIBUTE, ""],
2739
+ ]);
2740
+ /**
2741
+ * Gives every cluster its own value of each attribute a graph around it sets, Graphviz's default where the cluster
2742
+ * has none, since Graphviz gives a cluster the attributes of the graphs around it (the root's are kept as data).
2743
+ * @param snapshot - the graph handed to the exporter
2744
+ * @param cx - the elements' contexts
2745
+ * @param columns - the node attributes written, by name, changed in place
2746
+ * @param background - the background the writer gives the root graph (its `bgcolor`), which a cluster would inherit
2747
+ */
2748
+ function stopInheritance(snapshot, cx, columns, background) {
2749
+ const clusters = Array.from({ length: snapshot.nodeCount }, (_, i) => i).filter((i) => cx.node(i).cluster);
2750
+ for (const [name, unset] of CLUSTER_UNSET) {
2751
+ const col = columns.get(name);
2752
+ const enclosing = cellText(snapshot.graph, name, 0) !== null ||
2753
+ (name === "bgcolor" && typeof background === "string") ||
2754
+ clusters.some((i) => (col?.[i] ?? null) !== null);
2755
+ if (!enclosing) {
2756
+ continue;
2757
+ }
2758
+ const filled = col ?? new Array(snapshot.nodeCount).fill(null);
2759
+ for (const i of clusters) {
2760
+ filled[i] ?? (filled[i] = unset);
2761
+ }
2762
+ columns.set(name, filled);
2763
+ }
2764
+ }
1384
2765
  /**
1385
2766
  * Removes every column Graphviz reads as how to draw: the picture writes them, so a data column of the same name (a
1386
2767
  * `label` attribute graphty does not draw) would draw something graphty does not. One a rule reads, whose values the