@graphty/graph-io 0.3.47 → 0.3.49

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 (770) hide show
  1. package/dist/chunks/{approximations-wWjCRzKj.js → approximations-Bzn3L-je.js} +2 -2
  2. package/dist/chunks/{approximations-wWjCRzKj.js.map → approximations-Bzn3L-je.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-DiGq58c3.js → block-iptrlMBb.js} +947 -134
  6. package/dist/chunks/block-iptrlMBb.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-BE2xir2k.js +157 -0
  10. package/dist/chunks/codes-BE2xir2k.js.map +1 -0
  11. package/dist/chunks/{constants-BKwqelYD.js → constants-CKkYLPtt.js} +2 -2
  12. package/dist/chunks/{constants-BKwqelYD.js.map → constants-CKkYLPtt.js.map} +1 -1
  13. package/dist/chunks/context-C3p0RQxR.js +664 -0
  14. package/dist/chunks/context-C3p0RQxR.js.map +1 -0
  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-DXHx7nwR.js +349 -0
  18. package/dist/chunks/credentials-DXHx7nwR.js.map +1 -0
  19. package/dist/chunks/cx-export-Bt6NoqTQ.js +259 -0
  20. package/dist/chunks/cx-export-Bt6NoqTQ.js.map +1 -0
  21. package/dist/chunks/descriptor-B28U25x4.js +60 -0
  22. package/dist/chunks/descriptor-B28U25x4.js.map +1 -0
  23. package/dist/chunks/descriptor-C-YDhtsn.js +48 -0
  24. package/dist/chunks/descriptor-C-YDhtsn.js.map +1 -0
  25. package/dist/chunks/descriptor-DLA5kINx.js +142 -0
  26. package/dist/chunks/descriptor-DLA5kINx.js.map +1 -0
  27. package/dist/chunks/{emit-CwU8cUmG.js → emit-BwHQkZle.js} +1690 -103
  28. package/dist/chunks/emit-BwHQkZle.js.map +1 -0
  29. package/dist/chunks/{escape-CgzxiYPg.js → escape-CuCIzNMB.js} +12 -12
  30. package/dist/chunks/{escape-CgzxiYPg.js.map → escape-CuCIzNMB.js.map} +1 -1
  31. package/dist/chunks/{export-BvqAXCKh.js → export-Cha4dRFz.js} +48 -1780
  32. package/dist/chunks/export-Cha4dRFz.js.map +1 -0
  33. package/dist/chunks/{exporter--Mk9gXCJ.js → exporter-BP6otMO6.js} +6 -4
  34. package/dist/chunks/{exporter--Mk9gXCJ.js.map → exporter-BP6otMO6.js.map} +1 -1
  35. package/dist/chunks/{exporter-CrDWIyiq.js → exporter-D2CETOMK.js} +1222 -154
  36. package/dist/chunks/exporter-D2CETOMK.js.map +1 -0
  37. package/dist/chunks/{exporter-DndFlGSa.js → exporter-D9yMtocA.js} +7 -6
  38. package/dist/chunks/{exporter-DndFlGSa.js.map → exporter-D9yMtocA.js.map} +1 -1
  39. package/dist/chunks/exporter-_jrBTnos.js +1009 -0
  40. package/dist/chunks/exporter-_jrBTnos.js.map +1 -0
  41. package/dist/chunks/{exporter-CJgfhF-l.js → exporter-nDfxIK5D.js} +9 -7
  42. package/dist/chunks/{exporter-CJgfhF-l.js.map → exporter-nDfxIK5D.js.map} +1 -1
  43. package/dist/chunks/format-BXiAb-1h.js +408 -0
  44. package/dist/chunks/format-BXiAb-1h.js.map +1 -0
  45. package/dist/chunks/{generic-Dq1ntt9q.js → generic-ymWCETAJ.js} +387 -31
  46. package/dist/chunks/generic-ymWCETAJ.js.map +1 -0
  47. package/dist/chunks/{importer-CMmeMWlC.js → importer-B92x1xY5.js} +15 -263
  48. package/dist/chunks/importer-B92x1xY5.js.map +1 -0
  49. package/dist/chunks/{importer-DiZRmOQG.js → importer-CT9mJ1nn.js} +328 -90
  50. package/dist/chunks/importer-CT9mJ1nn.js.map +1 -0
  51. package/dist/chunks/{importer-COTK9Tsl.js → importer-DaLTjbds.js} +116 -1099
  52. package/dist/chunks/importer-DaLTjbds.js.map +1 -0
  53. package/dist/chunks/{importer-iFYPEKaJ.js → importer-GREKMdIf.js} +28 -16
  54. package/dist/chunks/importer-GREKMdIf.js.map +1 -0
  55. package/dist/chunks/{importer-BSvfrX_h.js → importer-PqaaKKnY.js} +23 -1016
  56. package/dist/chunks/importer-PqaaKKnY.js.map +1 -0
  57. package/dist/chunks/{importer-yh_aBCU6.js → importer-Vn5QvmmJ.js} +66 -12
  58. package/dist/chunks/importer-Vn5QvmmJ.js.map +1 -0
  59. package/dist/chunks/{importer-e6LGpqHB.js → importer-vRq4h3j5.js} +1470 -354
  60. package/dist/chunks/importer-vRq4h3j5.js.map +1 -0
  61. package/dist/chunks/input-CAxt_aV-.js +1397 -0
  62. package/dist/chunks/input-CAxt_aV-.js.map +1 -0
  63. package/dist/chunks/{json-elements-DJZRZ6jd.js → json-elements-Cq76WjnZ.js} +4 -4
  64. package/dist/chunks/{json-elements-DJZRZ6jd.js.map → json-elements-Cq76WjnZ.js.map} +1 -1
  65. package/dist/chunks/{lexicon-CT69Kd87.js → lexicon-Clf4QIyl.js} +299 -77
  66. package/dist/chunks/lexicon-Clf4QIyl.js.map +1 -0
  67. package/dist/chunks/{ontology-export-G4MqRdux.js → ontology-export-OMo91mb0.js} +4 -3
  68. package/dist/chunks/{ontology-export-G4MqRdux.js.map → ontology-export-OMo91mb0.js.map} +1 -1
  69. package/dist/chunks/{records-CiaWZrFH.js → records-NJiYvekn.js} +2 -2
  70. package/dist/chunks/{records-CiaWZrFH.js.map → records-NJiYvekn.js.map} +1 -1
  71. package/dist/chunks/{format-BpQBx8MZ.js → report-CwMfYa9J.js} +70 -127
  72. package/dist/chunks/report-CwMfYa9J.js.map +1 -0
  73. package/dist/chunks/round-trip-Zm3_ivgq.js +79 -0
  74. package/dist/chunks/round-trip-Zm3_ivgq.js.map +1 -0
  75. package/dist/chunks/sigv4-EokFic1V.js +247 -0
  76. package/dist/chunks/sigv4-EokFic1V.js.map +1 -0
  77. package/dist/chunks/{styles-BQnsga7p.js → styles-CCs214_3.js} +1300 -168
  78. package/dist/chunks/styles-CCs214_3.js.map +1 -0
  79. package/dist/chunks/styles-Cm62yyQX.js +2812 -0
  80. package/dist/chunks/styles-Cm62yyQX.js.map +1 -0
  81. package/dist/chunks/styles-DPQQIig4.js +1928 -0
  82. package/dist/chunks/styles-DPQQIig4.js.map +1 -0
  83. package/dist/chunks/styles-yed-UMFdNqze.js +1213 -0
  84. package/dist/chunks/styles-yed-UMFdNqze.js.map +1 -0
  85. package/dist/chunks/{text-CzJNFOS9.js → text-ystE2bYi.js} +2 -2
  86. package/dist/chunks/{text-CzJNFOS9.js.map → text-ystE2bYi.js.map} +1 -1
  87. package/dist/csv.js +7 -5
  88. package/dist/csv.js.map +1 -1
  89. package/dist/cx.js +5 -4
  90. package/dist/cx.js.map +1 -1
  91. package/dist/cx2.js +2 -2
  92. package/dist/cys.js +1 -1
  93. package/dist/dot.js +5 -4
  94. package/dist/dot.js.map +1 -1
  95. package/dist/gexf.js +42 -10
  96. package/dist/gexf.js.map +1 -1
  97. package/dist/gml.js +850 -193
  98. package/dist/gml.js.map +1 -1
  99. package/dist/graph-io.js +1189 -220
  100. package/dist/graph-io.js.map +1 -1
  101. package/dist/graphml.js +3 -3
  102. package/dist/json.js +3 -2
  103. package/dist/neo4j.js +6 -4
  104. package/dist/neo4j.js.map +1 -1
  105. package/dist/obo.js +1 -1
  106. package/dist/pajek.js +1 -1
  107. package/dist/positions.js +45 -3
  108. package/dist/positions.js.map +1 -1
  109. package/dist/sources-cypher.d.ts +1 -0
  110. package/dist/sources-cypher.js +1351 -0
  111. package/dist/sources-cypher.js.map +1 -0
  112. package/dist/sources-ndex.d.ts +1 -0
  113. package/dist/sources-ndex.js +278 -0
  114. package/dist/sources-ndex.js.map +1 -0
  115. package/dist/sources-rest.d.ts +1 -0
  116. package/dist/sources-rest.js +1377 -0
  117. package/dist/sources-rest.js.map +1 -0
  118. package/dist/sources-sparql.d.ts +1 -0
  119. package/dist/sources-sparql.js +405 -0
  120. package/dist/sources-sparql.js.map +1 -0
  121. package/dist/src/common/report.d.ts +6 -3
  122. package/dist/src/common/report.d.ts.map +1 -1
  123. package/dist/src/common/report.js +15 -9
  124. package/dist/src/common/report.js.map +1 -1
  125. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cx/importer.js +5 -1
  127. package/dist/src/formats/cx/importer.js.map +1 -1
  128. package/dist/src/formats/cx/styles.d.ts +24 -1
  129. package/dist/src/formats/cx/styles.d.ts.map +1 -1
  130. package/dist/src/formats/cx/styles.js +118 -11
  131. package/dist/src/formats/cx/styles.js.map +1 -1
  132. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  133. package/dist/src/formats/cx2/importer.js +1 -0
  134. package/dist/src/formats/cx2/importer.js.map +1 -1
  135. package/dist/src/formats/cx2/styles.d.ts.map +1 -1
  136. package/dist/src/formats/cx2/styles.js +3 -1
  137. package/dist/src/formats/cx2/styles.js.map +1 -1
  138. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  139. package/dist/src/formats/cys/importer.js +1 -0
  140. package/dist/src/formats/cys/importer.js.map +1 -1
  141. package/dist/src/formats/cys/styles.d.ts +2 -0
  142. package/dist/src/formats/cys/styles.d.ts.map +1 -1
  143. package/dist/src/formats/cys/styles.js +15 -7
  144. package/dist/src/formats/cys/styles.js.map +1 -1
  145. package/dist/src/formats/dot/drawing.d.ts +98 -0
  146. package/dist/src/formats/dot/drawing.d.ts.map +1 -0
  147. package/dist/src/formats/dot/drawing.js +734 -0
  148. package/dist/src/formats/dot/drawing.js.map +1 -0
  149. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  150. package/dist/src/formats/dot/importer.js +108 -31
  151. package/dist/src/formats/dot/importer.js.map +1 -1
  152. package/dist/src/formats/dot/label-places.d.ts +34 -0
  153. package/dist/src/formats/dot/label-places.d.ts.map +1 -0
  154. package/dist/src/formats/dot/label-places.js +179 -0
  155. package/dist/src/formats/dot/label-places.js.map +1 -0
  156. package/dist/src/formats/dot/record.d.ts +50 -0
  157. package/dist/src/formats/dot/record.d.ts.map +1 -0
  158. package/dist/src/formats/dot/record.js +275 -0
  159. package/dist/src/formats/dot/record.js.map +1 -0
  160. package/dist/src/formats/dot/styles.d.ts +62 -2
  161. package/dist/src/formats/dot/styles.d.ts.map +1 -1
  162. package/dist/src/formats/dot/styles.js +1457 -120
  163. package/dist/src/formats/dot/styles.js.map +1 -1
  164. package/dist/src/formats/gexf/dialog-metrics.d.ts +15 -0
  165. package/dist/src/formats/gexf/dialog-metrics.d.ts.map +1 -0
  166. package/dist/src/formats/gexf/dialog-metrics.js +59 -0
  167. package/dist/src/formats/gexf/dialog-metrics.js.map +1 -0
  168. package/dist/src/formats/gexf/importer.d.ts +7 -0
  169. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  170. package/dist/src/formats/gexf/importer.js +41 -5
  171. package/dist/src/formats/gexf/importer.js.map +1 -1
  172. package/dist/src/formats/gexf/preview.d.ts +77 -0
  173. package/dist/src/formats/gexf/preview.d.ts.map +1 -0
  174. package/dist/src/formats/gexf/preview.js +191 -0
  175. package/dist/src/formats/gexf/preview.js.map +1 -0
  176. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  177. package/dist/src/formats/gexf/schema.js +2 -1
  178. package/dist/src/formats/gexf/schema.js.map +1 -1
  179. package/dist/src/formats/gexf/styles.d.ts +60 -16
  180. package/dist/src/formats/gexf/styles.d.ts.map +1 -1
  181. package/dist/src/formats/gexf/styles.js +883 -131
  182. package/dist/src/formats/gexf/styles.js.map +1 -1
  183. package/dist/src/formats/gml/importer.d.ts +5 -2
  184. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  185. package/dist/src/formats/gml/importer.js +258 -71
  186. package/dist/src/formats/gml/importer.js.map +1 -1
  187. package/dist/src/formats/gml/index.d.ts +1 -1
  188. package/dist/src/formats/gml/index.d.ts.map +1 -1
  189. package/dist/src/formats/gml/index.js +1 -1
  190. package/dist/src/formats/gml/index.js.map +1 -1
  191. package/dist/src/formats/gml/positions.d.ts +7 -0
  192. package/dist/src/formats/gml/positions.d.ts.map +1 -1
  193. package/dist/src/formats/gml/positions.js +29 -7
  194. package/dist/src/formats/gml/positions.js.map +1 -1
  195. package/dist/src/formats/gml/styles.d.ts.map +1 -1
  196. package/dist/src/formats/gml/styles.js +756 -103
  197. package/dist/src/formats/gml/styles.js.map +1 -1
  198. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -1
  199. package/dist/src/formats/graphml/styles-plain.js +8 -12
  200. package/dist/src/formats/graphml/styles-plain.js.map +1 -1
  201. package/dist/src/formats/graphml/styles-yed.d.ts +21 -15
  202. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -1
  203. package/dist/src/formats/graphml/styles-yed.js +749 -148
  204. package/dist/src/formats/graphml/styles-yed.js.map +1 -1
  205. package/dist/src/formats/graphml/styles-yfiles3.d.ts +1 -1
  206. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
  207. package/dist/src/formats/graphml/styles-yfiles3.js +429 -64
  208. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
  209. package/dist/src/formats/graphml/yfiles.d.ts +3 -3
  210. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  211. package/dist/src/formats/graphml/yfiles.js +16 -6
  212. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  213. package/dist/src/formats/json/dialect.d.ts +3 -2
  214. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  215. package/dist/src/formats/json/dialect.js +39 -5
  216. package/dist/src/formats/json/dialect.js.map +1 -1
  217. package/dist/src/formats/json/importer.d.ts +13 -2
  218. package/dist/src/formats/json/importer.d.ts.map +1 -1
  219. package/dist/src/formats/json/importer.js +42 -6
  220. package/dist/src/formats/json/importer.js.map +1 -1
  221. package/dist/src/formats/json/index.d.ts +1 -1
  222. package/dist/src/formats/json/index.d.ts.map +1 -1
  223. package/dist/src/formats/json/index.js +1 -1
  224. package/dist/src/formats/json/index.js.map +1 -1
  225. package/dist/src/formats/json/positions.d.ts +5 -2
  226. package/dist/src/formats/json/positions.d.ts.map +1 -1
  227. package/dist/src/formats/json/positions.js +20 -4
  228. package/dist/src/formats/json/positions.js.map +1 -1
  229. package/dist/src/formats/json/styles.d.ts +12 -1
  230. package/dist/src/formats/json/styles.d.ts.map +1 -1
  231. package/dist/src/formats/json/styles.js +1755 -136
  232. package/dist/src/formats/json/styles.js.map +1 -1
  233. package/dist/src/formats/pajek/draw.d.ts +96 -0
  234. package/dist/src/formats/pajek/draw.d.ts.map +1 -0
  235. package/dist/src/formats/pajek/draw.js +372 -0
  236. package/dist/src/formats/pajek/draw.js.map +1 -0
  237. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  238. package/dist/src/formats/pajek/exporter.js +70 -51
  239. package/dist/src/formats/pajek/exporter.js.map +1 -1
  240. package/dist/src/formats/pajek/importer.d.ts +9 -0
  241. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  242. package/dist/src/formats/pajek/importer.js +233 -102
  243. package/dist/src/formats/pajek/importer.js.map +1 -1
  244. package/dist/src/formats/pajek/index.d.ts +1 -0
  245. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  246. package/dist/src/formats/pajek/index.js.map +1 -1
  247. package/dist/src/formats/pajek/styles.d.ts +15 -7
  248. package/dist/src/formats/pajek/styles.d.ts.map +1 -1
  249. package/dist/src/formats/pajek/styles.js +1279 -114
  250. package/dist/src/formats/pajek/styles.js.map +1 -1
  251. package/dist/src/formats/pajek/syntax.d.ts +10 -1
  252. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  253. package/dist/src/formats/pajek/syntax.js +26 -2
  254. package/dist/src/formats/pajek/syntax.js.map +1 -1
  255. package/dist/src/formats/xgmml/emit.d.ts +1 -0
  256. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  257. package/dist/src/formats/xgmml/emit.js +65 -5
  258. package/dist/src/formats/xgmml/emit.js.map +1 -1
  259. package/dist/src/formats/xgmml/styles.d.ts +23 -3
  260. package/dist/src/formats/xgmml/styles.d.ts.map +1 -1
  261. package/dist/src/formats/xgmml/styles.js +278 -60
  262. package/dist/src/formats/xgmml/styles.js.map +1 -1
  263. package/dist/src/index.d.ts +10 -2
  264. package/dist/src/index.d.ts.map +1 -1
  265. package/dist/src/index.js +11 -2
  266. package/dist/src/index.js.map +1 -1
  267. package/dist/src/positions/convert.d.ts.map +1 -1
  268. package/dist/src/positions/convert.js +11 -1
  269. package/dist/src/positions/convert.js.map +1 -1
  270. package/dist/src/positions/extract.d.ts.map +1 -1
  271. package/dist/src/positions/extract.js +53 -1
  272. package/dist/src/positions/extract.js.map +1 -1
  273. package/dist/src/positions/index.d.ts +1 -1
  274. package/dist/src/positions/index.d.ts.map +1 -1
  275. package/dist/src/positions/types.d.ts +59 -0
  276. package/dist/src/positions/types.d.ts.map +1 -1
  277. package/dist/src/registry.d.ts +173 -1
  278. package/dist/src/registry.d.ts.map +1 -1
  279. package/dist/src/registry.js +196 -1
  280. package/dist/src/registry.js.map +1 -1
  281. package/dist/src/sources/builtins.d.ts +11 -0
  282. package/dist/src/sources/builtins.d.ts.map +1 -0
  283. package/dist/src/sources/builtins.js +53 -0
  284. package/dist/src/sources/builtins.js.map +1 -0
  285. package/dist/src/sources/codes.d.ts +230 -0
  286. package/dist/src/sources/codes.d.ts.map +1 -0
  287. package/dist/src/sources/codes.js +58 -0
  288. package/dist/src/sources/codes.js.map +1 -0
  289. package/dist/src/sources/context.d.ts +112 -0
  290. package/dist/src/sources/context.d.ts.map +1 -0
  291. package/dist/src/sources/context.js +740 -0
  292. package/dist/src/sources/context.js.map +1 -0
  293. package/dist/src/sources/credentials.d.ts +179 -0
  294. package/dist/src/sources/credentials.d.ts.map +1 -0
  295. package/dist/src/sources/credentials.js +301 -0
  296. package/dist/src/sources/credentials.js.map +1 -0
  297. package/dist/src/sources/cypher/descriptor.d.ts +21 -0
  298. package/dist/src/sources/cypher/descriptor.d.ts.map +1 -0
  299. package/dist/src/sources/cypher/descriptor.js +124 -0
  300. package/dist/src/sources/cypher/descriptor.js.map +1 -0
  301. package/dist/src/sources/cypher/flatten.d.ts +72 -0
  302. package/dist/src/sources/cypher/flatten.d.ts.map +1 -0
  303. package/dist/src/sources/cypher/flatten.js +279 -0
  304. package/dist/src/sources/cypher/flatten.js.map +1 -0
  305. package/dist/src/sources/cypher/index.d.ts +33 -0
  306. package/dist/src/sources/cypher/index.d.ts.map +1 -0
  307. package/dist/src/sources/cypher/index.js +511 -0
  308. package/dist/src/sources/cypher/index.js.map +1 -0
  309. package/dist/src/sources/cypher/params.d.ts +40 -0
  310. package/dist/src/sources/cypher/params.d.ts.map +1 -0
  311. package/dist/src/sources/cypher/params.js +6 -0
  312. package/dist/src/sources/cypher/params.js.map +1 -0
  313. package/dist/src/sources/cypher/profiles.d.ts +44 -0
  314. package/dist/src/sources/cypher/profiles.d.ts.map +1 -0
  315. package/dist/src/sources/cypher/profiles.js +61 -0
  316. package/dist/src/sources/cypher/profiles.js.map +1 -0
  317. package/dist/src/sources/cypher/query-builder.d.ts +123 -0
  318. package/dist/src/sources/cypher/query-builder.d.ts.map +1 -0
  319. package/dist/src/sources/cypher/query-builder.js +267 -0
  320. package/dist/src/sources/cypher/query-builder.js.map +1 -0
  321. package/dist/src/sources/cypher/transports/bolt.d.ts +21 -0
  322. package/dist/src/sources/cypher/transports/bolt.d.ts.map +1 -0
  323. package/dist/src/sources/cypher/transports/bolt.js +185 -0
  324. package/dist/src/sources/cypher/transports/bolt.js.map +1 -0
  325. package/dist/src/sources/cypher/transports/neptune-analytics.d.ts +15 -0
  326. package/dist/src/sources/cypher/transports/neptune-analytics.d.ts.map +1 -0
  327. package/dist/src/sources/cypher/transports/neptune-analytics.js +38 -0
  328. package/dist/src/sources/cypher/transports/neptune-analytics.js.map +1 -0
  329. package/dist/src/sources/cypher/transports/neptune-https.d.ts +31 -0
  330. package/dist/src/sources/cypher/transports/neptune-https.d.ts.map +1 -0
  331. package/dist/src/sources/cypher/transports/neptune-https.js +104 -0
  332. package/dist/src/sources/cypher/transports/neptune-https.js.map +1 -0
  333. package/dist/src/sources/cypher/transports/query-api.d.ts +14 -0
  334. package/dist/src/sources/cypher/transports/query-api.d.ts.map +1 -0
  335. package/dist/src/sources/cypher/transports/query-api.js +80 -0
  336. package/dist/src/sources/cypher/transports/query-api.js.map +1 -0
  337. package/dist/src/sources/cypher/transports/transport.d.ts +78 -0
  338. package/dist/src/sources/cypher/transports/transport.d.ts.map +1 -0
  339. package/dist/src/sources/cypher/transports/transport.js +77 -0
  340. package/dist/src/sources/cypher/transports/transport.js.map +1 -0
  341. package/dist/src/sources/cypher/types-map.d.ts +91 -0
  342. package/dist/src/sources/cypher/types-map.d.ts.map +1 -0
  343. package/dist/src/sources/cypher/types-map.js +306 -0
  344. package/dist/src/sources/cypher/types-map.js.map +1 -0
  345. package/dist/src/sources/define.d.ts +47 -0
  346. package/dist/src/sources/define.d.ts.map +1 -0
  347. package/dist/src/sources/define.js +173 -0
  348. package/dist/src/sources/define.js.map +1 -0
  349. package/dist/src/sources/fail.d.ts +33 -0
  350. package/dist/src/sources/fail.d.ts.map +1 -0
  351. package/dist/src/sources/fail.js +49 -0
  352. package/dist/src/sources/fail.js.map +1 -0
  353. package/dist/src/sources/ids.d.ts +69 -0
  354. package/dist/src/sources/ids.d.ts.map +1 -0
  355. package/dist/src/sources/ids.js +69 -0
  356. package/dist/src/sources/ids.js.map +1 -0
  357. package/dist/src/sources/ndex/client.d.ts +31 -0
  358. package/dist/src/sources/ndex/client.d.ts.map +1 -0
  359. package/dist/src/sources/ndex/client.js +90 -0
  360. package/dist/src/sources/ndex/client.js.map +1 -0
  361. package/dist/src/sources/ndex/descriptor.d.ts +10 -0
  362. package/dist/src/sources/ndex/descriptor.d.ts.map +1 -0
  363. package/dist/src/sources/ndex/descriptor.js +55 -0
  364. package/dist/src/sources/ndex/descriptor.js.map +1 -0
  365. package/dist/src/sources/ndex/expand.d.ts +20 -0
  366. package/dist/src/sources/ndex/expand.d.ts.map +1 -0
  367. package/dist/src/sources/ndex/expand.js +87 -0
  368. package/dist/src/sources/ndex/expand.js.map +1 -0
  369. package/dist/src/sources/ndex/index.d.ts +25 -0
  370. package/dist/src/sources/ndex/index.d.ts.map +1 -0
  371. package/dist/src/sources/ndex/index.js +191 -0
  372. package/dist/src/sources/ndex/index.js.map +1 -0
  373. package/dist/src/sources/ndex/params.d.ts +17 -0
  374. package/dist/src/sources/ndex/params.d.ts.map +1 -0
  375. package/dist/src/sources/ndex/params.js +6 -0
  376. package/dist/src/sources/ndex/params.js.map +1 -0
  377. package/dist/src/sources/ndex/share-url.d.ts +40 -0
  378. package/dist/src/sources/ndex/share-url.d.ts.map +1 -0
  379. package/dist/src/sources/ndex/share-url.js +64 -0
  380. package/dist/src/sources/ndex/share-url.js.map +1 -0
  381. package/dist/src/sources/params.d.ts +31 -0
  382. package/dist/src/sources/params.d.ts.map +1 -0
  383. package/dist/src/sources/params.js +6 -0
  384. package/dist/src/sources/params.js.map +1 -0
  385. package/dist/src/sources/registry.d.ts +118 -0
  386. package/dist/src/sources/registry.d.ts.map +1 -0
  387. package/dist/src/sources/registry.js +193 -0
  388. package/dist/src/sources/registry.js.map +1 -0
  389. package/dist/src/sources/rest/descriptor.d.ts +16 -0
  390. package/dist/src/sources/rest/descriptor.d.ts.map +1 -0
  391. package/dist/src/sources/rest/descriptor.js +76 -0
  392. package/dist/src/sources/rest/descriptor.js.map +1 -0
  393. package/dist/src/sources/rest/index.d.ts +14 -0
  394. package/dist/src/sources/rest/index.d.ts.map +1 -0
  395. package/dist/src/sources/rest/index.js +13 -0
  396. package/dist/src/sources/rest/index.js.map +1 -0
  397. package/dist/src/sources/rest/mapping.d.ts +30 -0
  398. package/dist/src/sources/rest/mapping.d.ts.map +1 -0
  399. package/dist/src/sources/rest/mapping.js +362 -0
  400. package/dist/src/sources/rest/mapping.js.map +1 -0
  401. package/dist/src/sources/rest/openalex.d.ts +27 -0
  402. package/dist/src/sources/rest/openalex.d.ts.map +1 -0
  403. package/dist/src/sources/rest/openalex.js +269 -0
  404. package/dist/src/sources/rest/openalex.js.map +1 -0
  405. package/dist/src/sources/rest/paging.d.ts +53 -0
  406. package/dist/src/sources/rest/paging.d.ts.map +1 -0
  407. package/dist/src/sources/rest/paging.js +138 -0
  408. package/dist/src/sources/rest/paging.js.map +1 -0
  409. package/dist/src/sources/rest/params.d.ts +216 -0
  410. package/dist/src/sources/rest/params.d.ts.map +1 -0
  411. package/dist/src/sources/rest/params.js +7 -0
  412. package/dist/src/sources/rest/params.js.map +1 -0
  413. package/dist/src/sources/rest/records.d.ts +41 -0
  414. package/dist/src/sources/rest/records.d.ts.map +1 -0
  415. package/dist/src/sources/rest/records.js +240 -0
  416. package/dist/src/sources/rest/records.js.map +1 -0
  417. package/dist/src/sources/rest/samples/deps-dev.d.ts +10 -0
  418. package/dist/src/sources/rest/samples/deps-dev.d.ts.map +1 -0
  419. package/dist/src/sources/rest/samples/deps-dev.js +47 -0
  420. package/dist/src/sources/rest/samples/deps-dev.js.map +1 -0
  421. package/dist/src/sources/rest/samples/string.d.ts +9 -0
  422. package/dist/src/sources/rest/samples/string.d.ts.map +1 -0
  423. package/dist/src/sources/rest/samples/string.js +42 -0
  424. package/dist/src/sources/rest/samples/string.js.map +1 -0
  425. package/dist/src/sources/rest/selectors.d.ts +41 -0
  426. package/dist/src/sources/rest/selectors.d.ts.map +1 -0
  427. package/dist/src/sources/rest/selectors.js +81 -0
  428. package/dist/src/sources/rest/selectors.js.map +1 -0
  429. package/dist/src/sources/rest/source.d.ts +67 -0
  430. package/dist/src/sources/rest/source.d.ts.map +1 -0
  431. package/dist/src/sources/rest/source.js +650 -0
  432. package/dist/src/sources/rest/source.js.map +1 -0
  433. package/dist/src/sources/rest/templates.d.ts +38 -0
  434. package/dist/src/sources/rest/templates.d.ts.map +1 -0
  435. package/dist/src/sources/rest/templates.js +125 -0
  436. package/dist/src/sources/rest/templates.js.map +1 -0
  437. package/dist/src/sources/run.d.ts +93 -0
  438. package/dist/src/sources/run.d.ts.map +1 -0
  439. package/dist/src/sources/run.js +673 -0
  440. package/dist/src/sources/run.js.map +1 -0
  441. package/dist/src/sources/sha256.d.ts +12 -0
  442. package/dist/src/sources/sha256.d.ts.map +1 -0
  443. package/dist/src/sources/sha256.js +95 -0
  444. package/dist/src/sources/sha256.js.map +1 -0
  445. package/dist/src/sources/sigv4.d.ts +26 -0
  446. package/dist/src/sources/sigv4.d.ts.map +1 -0
  447. package/dist/src/sources/sigv4.js +184 -0
  448. package/dist/src/sources/sigv4.js.map +1 -0
  449. package/dist/src/sources/sparql/descriptor.d.ts +10 -0
  450. package/dist/src/sources/sparql/descriptor.d.ts.map +1 -0
  451. package/dist/src/sources/sparql/descriptor.js +64 -0
  452. package/dist/src/sources/sparql/descriptor.js.map +1 -0
  453. package/dist/src/sources/sparql/index.d.ts +28 -0
  454. package/dist/src/sources/sparql/index.d.ts.map +1 -0
  455. package/dist/src/sources/sparql/index.js +314 -0
  456. package/dist/src/sources/sparql/index.js.map +1 -0
  457. package/dist/src/sources/sparql/ntriples.d.ts +69 -0
  458. package/dist/src/sources/sparql/ntriples.d.ts.map +1 -0
  459. package/dist/src/sources/sparql/ntriples.js +180 -0
  460. package/dist/src/sources/sparql/ntriples.js.map +1 -0
  461. package/dist/src/sources/sparql/params.d.ts +27 -0
  462. package/dist/src/sources/sparql/params.d.ts.map +1 -0
  463. package/dist/src/sources/sparql/params.js +5 -0
  464. package/dist/src/sources/sparql/params.js.map +1 -0
  465. package/dist/src/sources/sparql/query.d.ts +72 -0
  466. package/dist/src/sources/sparql/query.d.ts.map +1 -0
  467. package/dist/src/sources/sparql/query.js +113 -0
  468. package/dist/src/sources/sparql/query.js.map +1 -0
  469. package/dist/src/sources/testing.d.ts +96 -0
  470. package/dist/src/sources/testing.d.ts.map +1 -0
  471. package/dist/src/sources/testing.js +138 -0
  472. package/dist/src/sources/testing.js.map +1 -0
  473. package/dist/src/sources/types.d.ts +696 -0
  474. package/dist/src/sources/types.d.ts.map +1 -0
  475. package/dist/src/sources/types.js +10 -0
  476. package/dist/src/sources/types.js.map +1 -0
  477. package/dist/src/styles/annotations.d.ts +61 -5
  478. package/dist/src/styles/annotations.d.ts.map +1 -1
  479. package/dist/src/styles/annotations.js +130 -22
  480. package/dist/src/styles/annotations.js.map +1 -1
  481. package/dist/src/styles/canon/bends.d.ts +46 -0
  482. package/dist/src/styles/canon/bends.d.ts.map +1 -1
  483. package/dist/src/styles/canon/bends.js +123 -0
  484. package/dist/src/styles/canon/bends.js.map +1 -1
  485. package/dist/src/styles/canon/color-tables.d.ts +6 -1
  486. package/dist/src/styles/canon/color-tables.d.ts.map +1 -1
  487. package/dist/src/styles/canon/color-tables.js +10 -4
  488. package/dist/src/styles/canon/color-tables.js.map +1 -1
  489. package/dist/src/styles/canon/color.d.ts +3 -1
  490. package/dist/src/styles/canon/color.d.ts.map +1 -1
  491. package/dist/src/styles/canon/color.js +103 -3
  492. package/dist/src/styles/canon/color.js.map +1 -1
  493. package/dist/src/styles/canon/enums.d.ts +2 -2
  494. package/dist/src/styles/canon/enums.d.ts.map +1 -1
  495. package/dist/src/styles/canon/enums.js +82 -7
  496. package/dist/src/styles/canon/enums.js.map +1 -1
  497. package/dist/src/styles/canon/graphviz.d.ts +12 -3
  498. package/dist/src/styles/canon/graphviz.d.ts.map +1 -1
  499. package/dist/src/styles/canon/graphviz.js +57 -9
  500. package/dist/src/styles/canon/graphviz.js.map +1 -1
  501. package/dist/src/styles/canon/number.d.ts +8 -0
  502. package/dist/src/styles/canon/number.d.ts.map +1 -1
  503. package/dist/src/styles/canon/number.js +18 -0
  504. package/dist/src/styles/canon/number.js.map +1 -1
  505. package/dist/src/styles/canon/text.d.ts +29 -2
  506. package/dist/src/styles/canon/text.d.ts.map +1 -1
  507. package/dist/src/styles/canon/text.js +156 -16
  508. package/dist/src/styles/canon/text.js.map +1 -1
  509. package/dist/src/styles/codes.d.ts +2 -0
  510. package/dist/src/styles/codes.d.ts.map +1 -1
  511. package/dist/src/styles/codes.js +2 -0
  512. package/dist/src/styles/codes.js.map +1 -1
  513. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
  514. package/dist/src/styles/cyjs/cascade.js +1037 -35
  515. package/dist/src/styles/cyjs/cascade.js.map +1 -1
  516. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
  517. package/dist/src/styles/cyjs/defaults-table.js +5 -0
  518. package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
  519. package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
  520. package/dist/src/styles/cyjs/emit.js +216 -12
  521. package/dist/src/styles/cyjs/emit.js.map +1 -1
  522. package/dist/src/styles/cyjs/face.d.ts +20 -0
  523. package/dist/src/styles/cyjs/face.d.ts.map +1 -0
  524. package/dist/src/styles/cyjs/face.js +332 -0
  525. package/dist/src/styles/cyjs/face.js.map +1 -0
  526. package/dist/src/styles/cyjs/mappers.d.ts +48 -2
  527. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
  528. package/dist/src/styles/cyjs/mappers.js +463 -26
  529. package/dist/src/styles/cyjs/mappers.js.map +1 -1
  530. package/dist/src/styles/cyjs/routes.d.ts +53 -0
  531. package/dist/src/styles/cyjs/routes.d.ts.map +1 -0
  532. package/dist/src/styles/cyjs/routes.js +360 -0
  533. package/dist/src/styles/cyjs/routes.js.map +1 -0
  534. package/dist/src/styles/cytoscape/charts.d.ts +23 -0
  535. package/dist/src/styles/cytoscape/charts.d.ts.map +1 -0
  536. package/dist/src/styles/cytoscape/charts.js +560 -0
  537. package/dist/src/styles/cytoscape/charts.js.map +1 -0
  538. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -1
  539. package/dist/src/styles/cytoscape/emit.js +204 -44
  540. package/dist/src/styles/cytoscape/emit.js.map +1 -1
  541. package/dist/src/styles/cytoscape/lexicon.d.ts +97 -3
  542. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -1
  543. package/dist/src/styles/cytoscape/lexicon.js +413 -79
  544. package/dist/src/styles/cytoscape/lexicon.js.map +1 -1
  545. package/dist/src/styles/cytoscape/mappings.d.ts +30 -0
  546. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -1
  547. package/dist/src/styles/cytoscape/mappings.js +92 -0
  548. package/dist/src/styles/cytoscape/mappings.js.map +1 -1
  549. package/dist/src/styles/cytoscape/resolve.d.ts +46 -3
  550. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -1
  551. package/dist/src/styles/cytoscape/resolve.js +632 -22
  552. package/dist/src/styles/cytoscape/resolve.js.map +1 -1
  553. package/dist/src/styles/cytoscape/translate.d.ts.map +1 -1
  554. package/dist/src/styles/cytoscape/translate.js +10 -10
  555. package/dist/src/styles/cytoscape/translate.js.map +1 -1
  556. package/dist/src/styles/defaults.d.ts +50 -0
  557. package/dist/src/styles/defaults.d.ts.map +1 -1
  558. package/dist/src/styles/defaults.js +315 -37
  559. package/dist/src/styles/defaults.js.map +1 -1
  560. package/dist/src/styles/index.d.ts +4 -3
  561. package/dist/src/styles/index.d.ts.map +1 -1
  562. package/dist/src/styles/index.js +3 -2
  563. package/dist/src/styles/index.js.map +1 -1
  564. package/dist/src/styles/internal.d.ts +1 -0
  565. package/dist/src/styles/internal.d.ts.map +1 -1
  566. package/dist/src/styles/internal.js +1 -0
  567. package/dist/src/styles/internal.js.map +1 -1
  568. package/dist/src/styles/io/block.d.ts.map +1 -1
  569. package/dist/src/styles/io/block.js +9 -0
  570. package/dist/src/styles/io/block.js.map +1 -1
  571. package/dist/src/styles/io/generic.d.ts.map +1 -1
  572. package/dist/src/styles/io/generic.js +56 -16
  573. package/dist/src/styles/io/generic.js.map +1 -1
  574. package/dist/src/styles/io/round-trip.d.ts +56 -0
  575. package/dist/src/styles/io/round-trip.d.ts.map +1 -0
  576. package/dist/src/styles/io/round-trip.js +138 -0
  577. package/dist/src/styles/io/round-trip.js.map +1 -0
  578. package/dist/src/styles/io/writer.d.ts.map +1 -1
  579. package/dist/src/styles/io/writer.js +13 -4
  580. package/dist/src/styles/io/writer.js.map +1 -1
  581. package/dist/src/styles/notes.d.ts.map +1 -1
  582. package/dist/src/styles/notes.js +1 -0
  583. package/dist/src/styles/notes.js.map +1 -1
  584. package/dist/src/styles/plan/features.d.ts +1 -1
  585. package/dist/src/styles/plan/features.d.ts.map +1 -1
  586. package/dist/src/styles/plan/features.js +22 -2
  587. package/dist/src/styles/plan/features.js.map +1 -1
  588. package/dist/src/styles/plan/index.d.ts +1 -1
  589. package/dist/src/styles/plan/index.d.ts.map +1 -1
  590. package/dist/src/styles/plan/index.js +1 -1
  591. package/dist/src/styles/plan/index.js.map +1 -1
  592. package/dist/src/styles/plan/plan.d.ts.map +1 -1
  593. package/dist/src/styles/plan/plan.js +61 -4
  594. package/dist/src/styles/plan/plan.js.map +1 -1
  595. package/dist/src/styles/plan/resolve.d.ts +13 -1
  596. package/dist/src/styles/plan/resolve.d.ts.map +1 -1
  597. package/dist/src/styles/plan/resolve.js +53 -6
  598. package/dist/src/styles/plan/resolve.js.map +1 -1
  599. package/dist/src/styles/slots.d.ts +14 -0
  600. package/dist/src/styles/slots.d.ts.map +1 -0
  601. package/dist/src/styles/slots.js +20 -0
  602. package/dist/src/styles/slots.js.map +1 -0
  603. package/dist/src/styles/types.d.ts +638 -5
  604. package/dist/src/styles/types.d.ts.map +1 -1
  605. package/dist/src/styles/vocabulary.d.ts +3 -2
  606. package/dist/src/styles/vocabulary.d.ts.map +1 -1
  607. package/dist/src/styles/vocabulary.js +218 -1
  608. package/dist/src/styles/vocabulary.js.map +1 -1
  609. package/dist/src/types.d.ts +19 -5
  610. package/dist/src/types.d.ts.map +1 -1
  611. package/dist/src/types.js.map +1 -1
  612. package/dist/styles.js +29 -238
  613. package/dist/styles.js.map +1 -1
  614. package/dist/xgmml.js +3 -3
  615. package/docs/samples/people.json +6 -0
  616. package/package.json +53 -8
  617. package/src/common/report.ts +27 -7
  618. package/src/formats/cx/importer.ts +6 -1
  619. package/src/formats/cx/styles.ts +144 -9
  620. package/src/formats/cx2/importer.ts +1 -0
  621. package/src/formats/cx2/styles.ts +3 -1
  622. package/src/formats/cys/importer.ts +1 -0
  623. package/src/formats/cys/styles.ts +27 -5
  624. package/src/formats/dot/drawing.ts +799 -0
  625. package/src/formats/dot/importer.ts +120 -35
  626. package/src/formats/dot/label-places.ts +228 -0
  627. package/src/formats/dot/record.ts +336 -0
  628. package/src/formats/dot/styles.ts +1657 -154
  629. package/src/formats/gexf/dialog-metrics.ts +61 -0
  630. package/src/formats/gexf/importer.ts +50 -5
  631. package/src/formats/gexf/preview.ts +217 -0
  632. package/src/formats/gexf/schema.ts +2 -1
  633. package/src/formats/gexf/styles.ts +1144 -142
  634. package/src/formats/gml/importer.ts +291 -76
  635. package/src/formats/gml/index.ts +1 -1
  636. package/src/formats/gml/positions.ts +29 -7
  637. package/src/formats/gml/styles.ts +854 -111
  638. package/src/formats/graphml/styles-plain.ts +8 -23
  639. package/src/formats/graphml/styles-yed.ts +821 -159
  640. package/src/formats/graphml/styles-yfiles3.ts +485 -63
  641. package/src/formats/graphml/yfiles.ts +19 -6
  642. package/src/formats/json/dialect.ts +41 -5
  643. package/src/formats/json/importer.ts +59 -7
  644. package/src/formats/json/index.ts +1 -0
  645. package/src/formats/json/positions.ts +22 -5
  646. package/src/formats/json/styles.ts +2041 -185
  647. package/src/formats/pajek/draw.ts +458 -0
  648. package/src/formats/pajek/exporter.ts +81 -49
  649. package/src/formats/pajek/importer.ts +278 -103
  650. package/src/formats/pajek/index.ts +1 -0
  651. package/src/formats/pajek/styles.ts +1382 -133
  652. package/src/formats/pajek/syntax.ts +27 -2
  653. package/src/formats/xgmml/emit.ts +73 -5
  654. package/src/formats/xgmml/styles.ts +331 -71
  655. package/src/index.ts +94 -0
  656. package/src/positions/convert.ts +12 -1
  657. package/src/positions/extract.ts +55 -1
  658. package/src/positions/index.ts +1 -0
  659. package/src/positions/types.ts +60 -0
  660. package/src/registry.ts +326 -1
  661. package/src/sources/builtins.ts +63 -0
  662. package/src/sources/codes.ts +235 -0
  663. package/src/sources/context.ts +894 -0
  664. package/src/sources/credentials.ts +409 -0
  665. package/src/sources/cypher/descriptor.ts +131 -0
  666. package/src/sources/cypher/flatten.ts +323 -0
  667. package/src/sources/cypher/index.ts +628 -0
  668. package/src/sources/cypher/params.ts +41 -0
  669. package/src/sources/cypher/profiles.ts +78 -0
  670. package/src/sources/cypher/query-builder.ts +327 -0
  671. package/src/sources/cypher/transports/bolt.ts +228 -0
  672. package/src/sources/cypher/transports/neptune-analytics.ts +41 -0
  673. package/src/sources/cypher/transports/neptune-https.ts +122 -0
  674. package/src/sources/cypher/transports/query-api.ts +100 -0
  675. package/src/sources/cypher/transports/transport.ts +139 -0
  676. package/src/sources/cypher/types-map.ts +418 -0
  677. package/src/sources/define.ts +195 -0
  678. package/src/sources/fail.ts +70 -0
  679. package/src/sources/ids.ts +80 -0
  680. package/src/sources/ndex/client.ts +102 -0
  681. package/src/sources/ndex/descriptor.ts +64 -0
  682. package/src/sources/ndex/expand.ts +100 -0
  683. package/src/sources/ndex/index.ts +235 -0
  684. package/src/sources/ndex/params.ts +17 -0
  685. package/src/sources/ndex/share-url.ts +84 -0
  686. package/src/sources/params.ts +32 -0
  687. package/src/sources/registry.ts +325 -0
  688. package/src/sources/rest/descriptor.ts +79 -0
  689. package/src/sources/rest/index.ts +22 -0
  690. package/src/sources/rest/mapping.ts +401 -0
  691. package/src/sources/rest/openalex.ts +288 -0
  692. package/src/sources/rest/paging.ts +180 -0
  693. package/src/sources/rest/params.ts +243 -0
  694. package/src/sources/rest/records.ts +289 -0
  695. package/src/sources/rest/samples/deps-dev.ts +49 -0
  696. package/src/sources/rest/samples/string.ts +44 -0
  697. package/src/sources/rest/selectors.ts +91 -0
  698. package/src/sources/rest/source.ts +776 -0
  699. package/src/sources/rest/templates.ts +151 -0
  700. package/src/sources/run.ts +793 -0
  701. package/src/sources/sha256.ts +99 -0
  702. package/src/sources/sigv4.ts +209 -0
  703. package/src/sources/sparql/descriptor.ts +67 -0
  704. package/src/sources/sparql/index.ts +356 -0
  705. package/src/sources/sparql/ntriples.ts +219 -0
  706. package/src/sources/sparql/params.ts +28 -0
  707. package/src/sources/sparql/query.ts +162 -0
  708. package/src/sources/testing.ts +210 -0
  709. package/src/sources/types.ts +776 -0
  710. package/src/styles/annotations.ts +201 -27
  711. package/src/styles/canon/bends.ts +137 -0
  712. package/src/styles/canon/color-tables.ts +10 -4
  713. package/src/styles/canon/color.ts +105 -3
  714. package/src/styles/canon/enums.ts +99 -8
  715. package/src/styles/canon/graphviz.ts +71 -10
  716. package/src/styles/canon/number.ts +19 -0
  717. package/src/styles/canon/text.ts +187 -16
  718. package/src/styles/codes.ts +2 -0
  719. package/src/styles/cyjs/cascade.ts +1193 -38
  720. package/src/styles/cyjs/defaults-table.ts +5 -0
  721. package/src/styles/cyjs/emit.ts +247 -16
  722. package/src/styles/cyjs/face.ts +372 -0
  723. package/src/styles/cyjs/mappers.ts +498 -28
  724. package/src/styles/cyjs/routes.ts +432 -0
  725. package/src/styles/cytoscape/charts.ts +684 -0
  726. package/src/styles/cytoscape/emit.ts +220 -44
  727. package/src/styles/cytoscape/lexicon.ts +459 -80
  728. package/src/styles/cytoscape/mappings.ts +108 -0
  729. package/src/styles/cytoscape/resolve.ts +763 -23
  730. package/src/styles/cytoscape/translate.ts +11 -11
  731. package/src/styles/defaults.ts +436 -38
  732. package/src/styles/index.ts +5 -0
  733. package/src/styles/internal.ts +1 -0
  734. package/src/styles/io/block.ts +10 -0
  735. package/src/styles/io/generic.ts +79 -23
  736. package/src/styles/io/round-trip.ts +202 -0
  737. package/src/styles/io/writer.ts +14 -4
  738. package/src/styles/notes.ts +1 -0
  739. package/src/styles/plan/features.ts +23 -2
  740. package/src/styles/plan/index.ts +1 -1
  741. package/src/styles/plan/plan.ts +69 -5
  742. package/src/styles/plan/resolve.ts +61 -6
  743. package/src/styles/slots.ts +25 -0
  744. package/src/styles/types.ts +658 -4
  745. package/src/styles/vocabulary.ts +226 -3
  746. package/src/types.ts +19 -5
  747. package/dist/chunks/block-DiGq58c3.js.map +0 -1
  748. package/dist/chunks/choose--jM0yYCo.js +0 -57
  749. package/dist/chunks/choose--jM0yYCo.js.map +0 -1
  750. package/dist/chunks/convert-BcQ8_jMo.js.map +0 -1
  751. package/dist/chunks/emit-CwU8cUmG.js.map +0 -1
  752. package/dist/chunks/export-BvqAXCKh.js.map +0 -1
  753. package/dist/chunks/exporter-CrDWIyiq.js.map +0 -1
  754. package/dist/chunks/format-BpQBx8MZ.js.map +0 -1
  755. package/dist/chunks/generic-Dq1ntt9q.js.map +0 -1
  756. package/dist/chunks/importer-BSvfrX_h.js.map +0 -1
  757. package/dist/chunks/importer-CMmeMWlC.js.map +0 -1
  758. package/dist/chunks/importer-COTK9Tsl.js.map +0 -1
  759. package/dist/chunks/importer-DiZRmOQG.js.map +0 -1
  760. package/dist/chunks/importer-e6LGpqHB.js.map +0 -1
  761. package/dist/chunks/importer-iFYPEKaJ.js.map +0 -1
  762. package/dist/chunks/importer-yh_aBCU6.js.map +0 -1
  763. package/dist/chunks/lexicon-CT69Kd87.js.map +0 -1
  764. package/dist/chunks/styles-BQnsga7p.js.map +0 -1
  765. package/dist/chunks/styles-D3fRphrO.js +0 -690
  766. package/dist/chunks/styles-D3fRphrO.js.map +0 -1
  767. package/dist/chunks/styles-yed-BF5x3_Y5.js +0 -842
  768. package/dist/chunks/styles-yed-BF5x3_Y5.js.map +0 -1
  769. package/dist/chunks/writer-DMKvthQE.js +0 -448
  770. package/dist/chunks/writer-DMKvthQE.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
@@ -53,6 +57,7 @@ export { STYLE_BLOCK_NAME as DOT_STYLE_BLOCK_ATTRIBUTE } from "../../styles/io/b
53
57
  * @category Built-in formats
54
58
  */
55
59
  export const DOT_STYLE_CAPABILITIES = Object.freeze({
60
+ tool: "graphviz",
56
61
  node: Object.freeze([
57
62
  "fill",
58
63
  "fillOpacity",
@@ -66,6 +71,14 @@ export const DOT_STYLE_CAPABILITIES = Object.freeze({
66
71
  "labelColor",
67
72
  "labelSize",
68
73
  "labelFont",
74
+ "labelWeight",
75
+ "labelItalic",
76
+ "labelPosition",
77
+ "shapePoints",
78
+ "shapePointsStretch",
79
+ "shapeParts",
80
+ "shapeLines",
81
+ "shapeLineWidth",
69
82
  "tooltip",
70
83
  "visible",
71
84
  ]),
@@ -76,6 +89,8 @@ export const DOT_STYLE_CAPABILITIES = Object.freeze({
76
89
  "lineStyle",
77
90
  "sourceArrow",
78
91
  "targetArrow",
92
+ "sourceArrowColor",
93
+ "targetArrowColor",
79
94
  "arrowSize",
80
95
  "label",
81
96
  "sourceLabel",
@@ -83,6 +98,8 @@ export const DOT_STYLE_CAPABILITIES = Object.freeze({
83
98
  "labelColor",
84
99
  "labelSize",
85
100
  "labelFont",
101
+ "labelWeight",
102
+ "labelItalic",
86
103
  "tooltip",
87
104
  "visible",
88
105
  ]),
@@ -108,7 +125,12 @@ const NODE_ATTRIBUTES = [
108
125
  "height",
109
126
  "fixedsize",
110
127
  "regular",
128
+ "sides",
129
+ "distortion",
130
+ "skew",
131
+ "orientation",
111
132
  "penwidth",
133
+ "gradientangle",
112
134
  "fontname",
113
135
  "fontsize",
114
136
  "fontcolor",
@@ -129,8 +151,10 @@ const EDGE_ATTRIBUTES = [
129
151
  "arrowtail",
130
152
  "dir",
131
153
  "arrowsize",
154
+ "fillcolor",
132
155
  "headlabel",
133
156
  "taillabel",
157
+ "xlabel",
134
158
  ];
135
159
  /** Attributes the reader notes as not carried. */
136
160
  const NOTED_ATTRIBUTES = ["peripheries", "xlabel"];
@@ -260,13 +284,234 @@ function labelText(raw, names, html, record, at) {
260
284
  }
261
285
  return dotLabelText(record ? recordLabelText(raw, at) : raw, names, at);
262
286
  }
287
+ /**
288
+ * Reads how a plain label's lines line up (`\l`, `\r`); an HTML-like or a record label lines up its own way.
289
+ * @param raw - the label as the file gives it
290
+ * @param names - what the label's escapes stand for
291
+ * @param html - the file's HTML strings
292
+ * @param record - whether the node is a record
293
+ * @param put - receives the value
294
+ */
295
+ function readLabelAlign(raw, names, html, record, put) {
296
+ if (isHtmlString(raw) || html.has(`<${raw}>`)) {
297
+ // each <BR ALIGN> lines up the line before it: one alignment on every break lines up the label
298
+ const aligns = new Set([...raw.matchAll(/<br\b([^>]*)>/gi)].map((m) => /\balign\s*=\s*["']?(left|right|center)/i.exec(m[1])?.[1].toLowerCase() ?? "center"));
299
+ // words after the last break are a line of their own, centered
300
+ const body = isHtmlString(raw) ? raw.slice(1, -1) : raw;
301
+ const last = /<br\b[^>]*>(?![\s\S]*<br\b)/i.exec(body);
302
+ const tail = last === null ? "" : body.slice(last.index + last[0].length);
303
+ if (stripTags(tail).trim() !== "") {
304
+ aligns.add("center");
305
+ }
306
+ const [only] = aligns;
307
+ if (aligns.size === 1 && only !== "center") {
308
+ put("labelTextAlign", only);
309
+ }
310
+ return;
311
+ }
312
+ if (record) {
313
+ return;
314
+ }
315
+ const align = dotLabelAlign(raw, names);
316
+ if (align !== null) {
317
+ put("labelTextAlign", align);
318
+ }
319
+ }
320
+ /**
321
+ * Text with its tags (`<` to the next `>`) dropped; a `<` with no `>` after it, and the rest, kept.
322
+ * @param text - the text
323
+ * @returns the text without tags
324
+ */
325
+ function stripTags(text) {
326
+ let out = "";
327
+ let i = 0;
328
+ while (i < text.length) {
329
+ const open = text.indexOf("<", i);
330
+ const close = open < 0 ? -1 : text.indexOf(">", open + 1);
331
+ if (close < 0) {
332
+ return out + text.slice(i);
333
+ }
334
+ out += text.slice(i, open);
335
+ i = close + 1;
336
+ }
337
+ return out;
338
+ }
339
+ /**
340
+ * Reads the font runs of an HTML-like label (`<B>`, `<I>`, `<FONT COLOR FACE POINT-SIZE>`) as Graphviz draws them:
341
+ * one run in a font of its own sets the label's own font (color, size, family, weight, slant), several are
342
+ * `labelRuns`. Underline, overline, strike-through, `<SUB>` and `<SUP>` have no neutral slot and draw plain.
343
+ * @param raw - the label as the file gives it
344
+ * @param html - the file's HTML strings
345
+ * @param scheme - the element's color scheme
346
+ * @param put - receives the values
347
+ * @param at - the sites
348
+ */
349
+ function readHtmlRuns(raw, html, scheme, put, at) {
350
+ if (!isHtmlString(raw) && !html.has(`<${raw}>`)) {
351
+ return;
352
+ }
353
+ const markup = isHtmlString(raw) ? raw : `<${raw}>`;
354
+ const runs = [];
355
+ for (const span of htmlLabelSpans(markup)) {
356
+ const run = { text: span.text, ...htmlRunFont(span.tags, scheme, at("labelRuns")) };
357
+ const last = runs.at(-1);
358
+ if (last !== undefined && JSON.stringify({ ...last, text: "" }) === JSON.stringify({ ...run, text: "" })) {
359
+ last.text = `${String(last.text)}${run.text}`;
360
+ }
361
+ else {
362
+ runs.push(run);
363
+ }
364
+ }
365
+ trimBlankRuns(runs);
366
+ if (runs.every((r) => Object.keys(r).length === 1)) {
367
+ return;
368
+ }
369
+ if (runs.length === 1) {
370
+ const { text: _text, ...font } = runs[0];
371
+ for (const [property, value] of Object.entries(font)) {
372
+ put(RUN_PROPERTY[property], value);
373
+ }
374
+ return;
375
+ }
376
+ runs[0].text = String(runs[0].text).trimStart();
377
+ const end = runs.at(-1);
378
+ if (end !== undefined) {
379
+ end.text = String(end.text).trimEnd();
380
+ }
381
+ put("labelRuns", JSON.stringify(runs));
382
+ }
383
+ /**
384
+ * Drops the blank runs at a label's ends, as htmlLabelText trims its blanks.
385
+ * @param runs - the runs, trimmed in place
386
+ */
387
+ function trimBlankRuns(runs) {
388
+ while (runs.length > 0 && String(runs[0].text).trim() === "") {
389
+ runs.shift();
390
+ }
391
+ while (runs.length > 0 && String(runs.at(-1)?.text).trim() === "") {
392
+ runs.pop();
393
+ }
394
+ }
395
+ /** The label property a run's own font field sets when the whole label is one run. */
396
+ const RUN_PROPERTY = {
397
+ weight: "labelWeight",
398
+ italic: "labelItalic",
399
+ font: "labelFont",
400
+ size: "labelSize",
401
+ color: "labelColor",
402
+ };
403
+ /**
404
+ * The font of a run inside HTML font tags, as `labelRuns` fields: bold, italic, and the innermost `FONT`'s face,
405
+ * point size and color.
406
+ * @param tags - the open tags, outermost first
407
+ * @param scheme - the element's color scheme
408
+ * @param site - where a bad color is noted
409
+ * @returns the fields that differ from the label's own font
410
+ */
411
+ function htmlRunFont(tags, scheme, site) {
412
+ const out = {};
413
+ for (const tag of tags) {
414
+ const name = /^<(\w+)/.exec(tag)?.[1].toLowerCase();
415
+ if (name === "b") {
416
+ out.weight = "bold";
417
+ }
418
+ else if (name === "i") {
419
+ out.italic = true;
420
+ }
421
+ else if (name === "font") {
422
+ for (const [attr, value] of tagAttributes(tag)) {
423
+ readFontAttribute(attr.toLowerCase(), value, scheme, site, out);
424
+ }
425
+ }
426
+ }
427
+ return out;
428
+ }
429
+ /**
430
+ * Reads one attribute of a `FONT` tag into a run's font fields: its face, point size or color.
431
+ * @param attr - the attribute's name, lowercase
432
+ * @param value - its value
433
+ * @param scheme - the element's color scheme
434
+ * @param site - where a bad color is noted
435
+ * @param out - the run's font fields, set
436
+ */
437
+ function readFontAttribute(attr, value, scheme, site, out) {
438
+ if (attr === "face" && value.trim() !== "" && value.length <= MAX_STYLE_VALUE_LENGTH) {
439
+ const drawn = graphvizFont(value);
440
+ out.font = drawn.family;
441
+ if (drawn.bold) {
442
+ out.weight = "bold";
443
+ }
444
+ if (drawn.italic) {
445
+ out.italic = true;
446
+ }
447
+ }
448
+ else if (attr === "point-size" && Number(value) > 0) {
449
+ out.size = Number(value);
450
+ }
451
+ else if (attr === "color") {
452
+ const c = canonColor(value, "graphviz", site, { scheme, use: "text" });
453
+ if (c !== null) {
454
+ out.color = c.color;
455
+ }
456
+ }
457
+ }
458
+ /** Whether a character can be part of an HTML attribute's name (a word character or `-`). */
459
+ const ATTRIBUTE_NAME_CHAR = /^[\w-]$/;
460
+ /**
461
+ * The quoted attributes of a tag, `name = "value"` or `name = 'value'`, in order; an unquoted or unclosed value is
462
+ * skipped. Scans the tag once (an attribute regex would backtrack on a long name with no `=`).
463
+ * @param tag - the tag
464
+ * @returns each attribute's name and value
465
+ */
466
+ function tagAttributes(tag) {
467
+ const attributes = [];
468
+ let i = 0;
469
+ while (i < tag.length) {
470
+ if (!ATTRIBUTE_NAME_CHAR.test(tag[i])) {
471
+ i++;
472
+ continue;
473
+ }
474
+ let end = i;
475
+ while (end < tag.length && ATTRIBUTE_NAME_CHAR.test(tag[end])) {
476
+ end++;
477
+ }
478
+ const value = quotedValueAt(tag, end);
479
+ if (value !== null) {
480
+ attributes.push([tag.slice(i, end), value.text]);
481
+ }
482
+ i = value === null ? end : value.next;
483
+ }
484
+ return attributes;
485
+ }
486
+ /**
487
+ * The quoted value of an attribute whose name ends at `at`: blanks, `=`, blanks, then text between matching quotes.
488
+ * @param tag - the tag
489
+ * @param at - where the name ends
490
+ * @returns the value and where reading goes on after its closing quote, or null when there is none
491
+ */
492
+ function quotedValueAt(tag, at) {
493
+ let i = at;
494
+ while (i < tag.length && /\s/.test(tag[i])) {
495
+ i++;
496
+ }
497
+ if (tag[i] !== "=") {
498
+ return null;
499
+ }
500
+ i++;
501
+ while (i < tag.length && /\s/.test(tag[i])) {
502
+ i++;
503
+ }
504
+ const quote = tag[i];
505
+ const close = quote === '"' || quote === "'" ? tag.indexOf(quote, i + 1) : -1;
506
+ return close < 0 ? null : { text: tag.slice(i + 1, close), next: close + 1 };
507
+ }
263
508
  /**
264
509
  * Reads a color into a property and its alpha into the property's opacity.
265
510
  * @param raw - the color
266
511
  * @param scheme - the object's color scheme
267
512
  * @param use - how Graphviz draws it
268
513
  * @param property - the color property
269
- * @param opacity - its opacity property, or null when the alpha is dropped
514
+ * @param opacity - its opacity property, or null when the alpha is dropped (an outline's is its borderOpacity)
270
515
  * @param put - receives the values
271
516
  * @param at - the sites
272
517
  * @returns the alpha, or null when the color is absent
@@ -280,11 +525,15 @@ function readColor(raw, scheme, use, property, opacity, put, at) {
280
525
  return null;
281
526
  }
282
527
  put(property, c.color);
283
- if (opacity !== null) {
528
+ if (property === "borderColor") {
529
+ // a transparent outline is no outline (the caller sets its width to 0); a part-transparent one is drawn so
530
+ put("borderOpacity", c.alpha > 0 && c.alpha < 1 ? c.alpha : null);
531
+ }
532
+ else if (opacity !== null) {
284
533
  put(opacity, c.alpha);
285
534
  }
286
- else if (c.alpha > 0 || property !== "borderColor") {
287
- // a transparent outline is no outline (the caller sets its width to 0); any other alpha is dropped, noted
535
+ else {
536
+ // any other alpha is dropped, noted
288
537
  colorOpacity(c, null, at(property), false);
289
538
  }
290
539
  return c.alpha;
@@ -331,21 +580,142 @@ function inches(raw, at) {
331
580
  * @param at - the sites
332
581
  */
333
582
  function readCommon(get, tokens, scheme, names, put, at) {
334
- if (tokens.includes("invis")) {
583
+ // Graphviz takes `invisible` for `invis` too (emit.c `parse_style`)
584
+ if (tokens.includes("invis") || tokens.includes("invisible")) {
335
585
  put("visible", false);
336
586
  }
337
- readColor(nonEmpty(get("fontcolor")), scheme, "line", "labelColor", null, put, at);
587
+ // a part-transparent font color draws its text so (`#377eb880`); an opaque one needs no opacity
588
+ const putFont = (property, value) => {
589
+ put(property, property === "labelOpacity" && value === 1 ? null : value);
590
+ };
591
+ readColor(nonEmpty(get("fontcolor")), scheme, "text", "labelColor", "labelOpacity", putFont, at);
338
592
  const size = get("fontsize");
339
593
  put("labelSize", size === null ? null : canonNumber(size, at("labelSize")));
340
594
  const font = get("fontname");
341
595
  if (font !== null && font.length <= MAX_STYLE_VALUE_LENGTH) {
342
- put("labelFont", font);
596
+ const drawn = graphvizFont(font);
597
+ put("labelFont", drawn.family);
598
+ put("labelWeight", drawn.bold ? "bold" : null);
599
+ put("labelItalic", drawn.italic ? true : null);
343
600
  }
344
601
  const tip = get("tooltip") ?? get("edgetooltip");
345
602
  if (tip !== null) {
346
603
  put("tooltip", dotLabelText(tip, names, at("tooltip")));
347
604
  }
348
605
  }
606
+ /**
607
+ * Reads a node's label and, for a node that is not a cluster, its size and how it grows to hold the label.
608
+ * @param n - the node
609
+ * @param names - what the label's escapes stand for
610
+ */
611
+ function readNodeLabelAndSize(n, names) {
612
+ const { get, cx, put, at, shape } = n;
613
+ // label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
614
+ const label = n.point ? null : get(LABEL_ATTRIBUTE);
615
+ const shown = label === null ? cx.name : labelText(label, names, cx.html, RECORD.has(shape), at("label"));
616
+ readNodeLabel(n, label, shown, names);
617
+ // plain is its label alone at size 0: with an empty label Graphviz draws nothing, filled or not; an HTML label
618
+ // with no words (a table holding an IMG) still draws its cells
619
+ const htmlLabel = label !== null && (isHtmlString(label) || cx.html.has(`<${label}>`));
620
+ if (shape === "plain" && shown === "" && !htmlLabel) {
621
+ put("visible", false);
622
+ }
623
+ if (cx.cluster) {
624
+ readClusterBox(get, put);
625
+ }
626
+ else {
627
+ readNodeSize(n, label, shown, htmlLabel, names);
628
+ }
629
+ }
630
+ /**
631
+ * Reads a node's label, its alignment and runs, and where it sits.
632
+ * @param n - the node
633
+ * @param label - its label, or null when it has none
634
+ * @param shown - the text it shows
635
+ * @param names - what the label's escapes stand for
636
+ */
637
+ function readNodeLabel(n, label, shown, names) {
638
+ const { get, cx, put, at, shape } = n;
639
+ // a cluster has no label of its own unless one is given, so an empty one is none
640
+ if (label !== null && !(cx.cluster && label === "")) {
641
+ put("label", shown);
642
+ readLabelAlign(label, names, cx.html, RECORD.has(shape), put);
643
+ readHtmlRuns(label, cx.html, n.scheme, put, at);
644
+ if (cx.cluster) {
645
+ put("labelPosition", clusterLabelPosition(get));
646
+ readClusterLabelPlace(get, put);
647
+ }
648
+ }
649
+ if (!cx.cluster && !n.point) {
650
+ const position = nodeLabelPosition(get);
651
+ put("labelPosition", position === "center" ? null : position);
652
+ }
653
+ }
654
+ /**
655
+ * 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
656
+ * label's fit).
657
+ * @param n - the node
658
+ * @param label - its label, or null when it has none
659
+ * @param shown - the text it shows
660
+ * @param htmlLabel - whether the label is HTML-like
661
+ * @param names - what the label's escapes stand for
662
+ */
663
+ function readNodeSize(n, label, shown, htmlLabel, names) {
664
+ const { get, put, at, shape } = n;
665
+ readSize(get, shape, n.point, put, at);
666
+ if (RECORD.has(shape) && label !== null && !htmlLabel) {
667
+ readRecord(n, label, names);
668
+ }
669
+ else if (!n.point) {
670
+ readLabelFit(get, shape, label, shown, put);
671
+ readQuantum(n, label);
672
+ }
673
+ }
674
+ /** Graphviz's default node width and height, in points (0.75 and 0.5 inch). */
675
+ const DEFAULT_NODE_SIZE = { w: 54, h: 36 };
676
+ /**
677
+ * Reads a record node as Graphviz lays it out: its size (at least its fields' unless `fixedsize`), the dividers
678
+ * between its fields as lines over the node, and each field's text lines where Graphviz centers or justifies them,
679
+ * in place of one label.
680
+ * @param n - the node
681
+ * @param label - its record label
682
+ * @param names - what the label's escapes stand for
683
+ */
684
+ function readRecord(n, label, names) {
685
+ const { get, put, at, cx } = n;
686
+ const pt = (attr, fallback) => {
687
+ const v = inches(get(attr), at(attr === "width" ? "width" : "height"));
688
+ return v === null ? fallback : v * DOT_POINTS_PER_INCH;
689
+ };
690
+ const size = Number(get("fontsize") ?? 14);
691
+ 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));
692
+ const hw = lay.width / 2;
693
+ const hh = lay.height / 2;
694
+ const fx = (x) => String(num6(hw > 0 ? x / hw : 0));
695
+ const fy = (y) => String(num6(hh > 0 ? y / hh : 0));
696
+ put("width", num6(lay.width));
697
+ put("height", num6(lay.height));
698
+ put("label", "");
699
+ put("shapeLines", lay.dividers.length === 0
700
+ ? null
701
+ : lay.dividers.map(([x0, y0, x1, y1]) => `${fx(x0)},${fy(y0)} ${fx(x1)},${fy(y1)}`).join("; "));
702
+ put("shapeTexts", lay.texts.length === 0
703
+ ? null
704
+ : JSON.stringify(lay.texts.map((t) => [Number(fx(t.x)), Number(fy(t.y)), t.text])));
705
+ }
706
+ /**
707
+ * Reads a laid-out cluster's size from its `bb` ("llx,lly,urx,ury" in points), the box Graphviz draws round its
708
+ * members and its label; a cluster with no `bb` is drawn round its members.
709
+ * @param get - the cluster's attributes
710
+ * @param put - receives the values
711
+ */
712
+ function readClusterBox(get, put) {
713
+ const parts = (get("bb") ?? "").split(",").map(Number);
714
+ if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] >= parts[0] && parts[3] >= parts[1]) {
715
+ put("width", num6(parts[2] - parts[0]));
716
+ put("height", num6(parts[3] - parts[1]));
717
+ }
718
+ }
349
719
  /**
350
720
  * Reads one node, or one cluster's container, the way Graphviz draws it.
351
721
  * @param get - its attributes
@@ -378,19 +748,65 @@ function readNode(get, cx, put, at) {
378
748
  else {
379
749
  readNodeFill(n);
380
750
  }
751
+ readNodeBands(n);
381
752
  readNodeOutline(n);
382
753
  readNodeShape(n);
383
- // label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
384
- const label = n.point ? null : get(LABEL_ATTRIBUTE);
385
- if (label !== null) {
386
- put("label", labelText(label, names, cx.html, RECORD.has(shape), at("label")));
754
+ readNodeLabelAndSize(n, names);
755
+ if (nonEmpty(get("xlabel")) !== null) {
756
+ noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
387
757
  }
388
- if (!cx.cluster) {
389
- readSize(get, shape, n.point, label ?? (n.point ? null : cx.name), put, at);
758
+ }
759
+ /**
760
+ * Where a node's label sits: its `labelloc` puts it against the top or bottom of the node's box, inside it; it is
761
+ * centered by default.
762
+ * @param get - the node's attributes
763
+ * @returns the position
764
+ */
765
+ function nodeLabelPosition(get) {
766
+ const loc = (get("labelloc") ?? "").trim().toLowerCase();
767
+ if (loc.startsWith("t")) {
768
+ return "inside-top";
769
+ }
770
+ return loc.startsWith("b") ? "inside-bottom" : "center";
771
+ }
772
+ /** How far in from the cluster's side Graphviz draws a label justified left or right: half its label's padding. */
773
+ const CLUSTER_LABEL_INSET = 8;
774
+ /**
775
+ * Where Graphviz draws a cluster's label beyond its position: at a stored `lp` (its center) inside the cluster's
776
+ * `bb`, else `CLUSTER_LABEL_INSET` points in from the side `labeljust` puts it against.
777
+ * @param get - the cluster's attributes
778
+ * @param put - receives the values
779
+ */
780
+ function readClusterLabelPlace(get, put) {
781
+ const lp = (get("lp") ?? "").split(",").map(Number);
782
+ const bb = (get("bb") ?? "").split(",").map(Number);
783
+ if (lp.length === 2 && bb.length === 4 && [...lp, ...bb].every(Number.isFinite)) {
784
+ put("labelPosition", "center");
785
+ put("labelOffsetX", num6(lp[0] - (bb[0] + bb[2]) / 2));
786
+ put("labelOffsetY", num6((bb[1] + bb[3]) / 2 - lp[1]));
787
+ return;
788
+ }
789
+ const just = (get("labeljust") ?? "").trim().toLowerCase();
790
+ if (just.startsWith("l")) {
791
+ put("labelOffsetX", CLUSTER_LABEL_INSET);
792
+ }
793
+ else if (just.startsWith("r")) {
794
+ put("labelOffsetX", -CLUSTER_LABEL_INSET);
390
795
  }
391
- if (Number(get("peripheries")) > 1 || nonEmpty(get("xlabel")) !== null) {
392
- noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, nonEmpty(get("xlabel")) === null ? "peripheries" : "xlabel");
796
+ }
797
+ /**
798
+ * Where a cluster's label sits: inside the cluster's box (Graphviz grows the box to hold it), at the top unless
799
+ * `labelloc` is `b`, centered unless `labeljust` is `l` or `r`.
800
+ * @param get - the cluster's attributes
801
+ * @returns the position
802
+ */
803
+ function clusterLabelPosition(get) {
804
+ const loc = (get("labelloc") ?? "").trim().toLowerCase().startsWith("b") ? "inside-bottom" : "inside-top";
805
+ const just = (get("labeljust") ?? "").trim().toLowerCase();
806
+ if (just.startsWith("l")) {
807
+ return `${loc}-left`;
393
808
  }
809
+ return just.startsWith("r") ? `${loc}-right` : loc;
394
810
  }
395
811
  /**
396
812
  * Reads a cluster's fill: its fill color when filled, else its background.
@@ -402,27 +818,154 @@ function readClusterFill(n) {
402
818
  const bg = nonEmpty(get("bgcolor"));
403
819
  const raw = filled ? (nonEmpty(get("fillcolor")) ?? n.color ?? bg ?? "lightgrey") : bg;
404
820
  readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
821
+ if (!tokens.includes("striped")) {
822
+ readFillGradient(n, raw);
823
+ }
824
+ }
825
+ /**
826
+ * Reads a gradient fill (`fillcolor="a:b"`, `gradientangle`): Graphviz runs it from the list's first color to
827
+ * its second, `gradientangle` degrees counter-clockwise from left to right (0 by default), which is the neutral
828
+ * angle's clockwise turn negated, from 0 up to 360. `style=radial` runs it out from the middle instead. A second color
829
+ * left out is black; colors after the second are not drawn. A first color's `;fraction` makes Graphviz switch colors
830
+ * at that fraction, which its SVG writes as stops at the fraction less 0.001 and at the fraction.
831
+ * @param n - the node or cluster
832
+ * @param raw - its fill color value
833
+ */
834
+ function readFillGradient(n, raw) {
835
+ const colon = raw === null ? -1 : raw.indexOf(":");
836
+ if (raw === null || colon < 0) {
837
+ return;
838
+ }
839
+ const radial = n.tokens.includes("radial");
840
+ if (radial) {
841
+ // ponytail: a radial gradientangle moves Graphviz's focus off the middle; graphty's radial ramp has no focus
842
+ n.put("fillGradientKind", "radial");
843
+ }
844
+ const frac = Number(raw.slice(0, colon).split(";")[1]);
845
+ if (frac > 0 && frac < 1) {
846
+ n.put("fillGradientStops", `${String(num6(frac - 0.001))} ${String(num6(frac))}`);
847
+ }
848
+ const second = raw
849
+ .slice(colon + 1)
850
+ .split(":")[0]
851
+ .split(";")[0];
852
+ const c = canonColor(second === "" ? "black" : second, "graphviz", n.at("fillGradient"), {
853
+ scheme: n.scheme,
854
+ use: "fill",
855
+ });
856
+ if (c === null) {
857
+ return;
858
+ }
859
+ n.put("fillGradient", c.color);
860
+ if (radial) {
861
+ return;
862
+ }
863
+ const angle = Number(nonEmpty(n.get("gradientangle")) ?? "0");
864
+ // 0 up to 360: the neutral angle turns clockwise on screen, Graphviz's counter-clockwise
865
+ const turned = Number.isFinite(angle) ? ((-angle % 360) + 360) % 360 : 0;
866
+ n.put("fillGradientAngle", num6(turned) + 0);
405
867
  }
406
868
  /**
407
869
  * Reads a node's fill: a point is always filled; a striped fill fills a box and a wedged one an ellipse only.
408
870
  * @param n - the node
409
871
  */
410
872
  function readNodeFill(n) {
411
- const { shape, point } = n;
873
+ const { point } = n;
412
874
  const fillcolor = nonEmpty(n.get("fillcolor"));
413
- const filled = point ||
414
- n.tokens.some((t) => t === "filled" ||
415
- t === "radial" ||
416
- (t === "striped" && BOX_LIKE.has(shape)) ||
417
- (t === "wedged" && ELLIPSE_LIKE.has(shape)));
418
- if (filled) {
875
+ if (isFilled(n)) {
419
876
  const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
420
877
  readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
878
+ // stripes on a box and wedges on an ellipse take the color list in place of a gradient
879
+ const banded = (n.tokens.includes("striped") && BOX_LIKE.has(n.shape)) ||
880
+ (n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape));
881
+ if ((n.tokens.includes("filled") || n.tokens.includes("radial")) && !banded) {
882
+ readFillGradient(n, raw);
883
+ }
421
884
  }
422
885
  else if (fillcolor !== null) {
423
886
  noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
424
887
  }
425
888
  }
889
+ /**
890
+ * Reads a node's striped or wedged fill (`style=striped` on a box, `style=wedged` on an ellipse) from its color
891
+ * list, `color;fraction` separated by `:`: a color with no fraction shares what the others leave, the last color takes
892
+ * what sized colors leave when none is unsized, and the shares stop at the whole
893
+ * (https://graphviz.org/docs/attr-types/colorList/). Graphviz draws stripes left to right and
894
+ * wedges counter-clockwise from 3 o'clock, which is the same pie as the wedges in reverse order clockwise from there.
895
+ * @param n - the node
896
+ */
897
+ function readNodeBands(n) {
898
+ // a cluster is a box, so it takes stripes too (and never wedges)
899
+ const striped = n.tokens.includes("striped") && (n.cx.cluster || BOX_LIKE.has(n.shape));
900
+ const wedged = !n.cx.cluster && n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape);
901
+ const list = nonEmpty(n.get("fillcolor")) ?? n.color;
902
+ if ((!striped && !wedged) || !list?.includes(":")) {
903
+ return;
904
+ }
905
+ const bands = colorBands(list, n.scheme, "fill", n.at(striped ? "stripeColors" : "pieColors"));
906
+ if (bands.length === 0) {
907
+ return;
908
+ }
909
+ const order = striped ? bands : [...bands].reverse();
910
+ const prefix = striped ? "stripe" : "pie";
911
+ n.put(`${prefix}Colors`, order.map((b) => b.color).join(" "));
912
+ n.put(`${prefix}Sizes`, order.map((b) => String(b.size)).join(" "));
913
+ if (striped) {
914
+ n.put("stripeDirection", "right");
915
+ }
916
+ else {
917
+ n.put("pieFitBox", true);
918
+ n.put("pieRotation", 90);
919
+ }
920
+ }
921
+ /**
922
+ * The bands of a Graphviz color list (`color;fraction` separated by `:`), in order: a color with no fraction shares
923
+ * what the others leave, the last color takes what sized colors leave when none is unsized, and the shares stop at
924
+ * the whole (https://graphviz.org/docs/attr-types/colorList/, emit.c `parseSegs`).
925
+ * @param list - the color list
926
+ * @param scheme - the color scheme
927
+ * @param use - how Graphviz draws the colors
928
+ * @param site - where it was read
929
+ * @returns each band's color and share of the whole
930
+ */
931
+ function colorBands(list, scheme, use, site) {
932
+ const parts = list.split(":").map((part) => {
933
+ const [color, fraction] = part.split(";");
934
+ const c = canonColor(color.trim(), "graphviz", site, { scheme, use });
935
+ const f = fraction === undefined ? Number.NaN : Number(fraction);
936
+ return { color: c?.color ?? null, fraction: Number.isFinite(f) && f >= 0 ? f : null };
937
+ });
938
+ const kept = parts.flatMap((p) => (p.color === null ? [] : [{ color: p.color, fraction: p.fraction }]));
939
+ const given = kept.reduce((sum, p) => sum + (p.fraction ?? 0), 0);
940
+ const unsized = kept.filter((p) => p.fraction === null).length;
941
+ const share = unsized > 0 ? Math.max(0, 1 - given) / unsized : 0;
942
+ let used = 0;
943
+ const bands = kept.flatMap((p) => {
944
+ const size = Math.min(p.fraction ?? share, 1 - used);
945
+ used += Math.max(size, 0);
946
+ return size > 0 ? [{ color: p.color, size: num6(size) }] : [];
947
+ });
948
+ // every color sized and short of the whole: Graphviz gives what is left to the last one (emit.c parseSegs)
949
+ const last = bands.at(-1);
950
+ if (last !== undefined && unsized === 0 && used < 1) {
951
+ last.size = num6(last.size + 1 - used);
952
+ }
953
+ return bands;
954
+ }
955
+ /**
956
+ * Whether Graphviz fills a node: a point always; `filled` and `radial` any shape, `striped` a box and `wedged` an
957
+ * ellipse only.
958
+ * @param n - the node
959
+ * @returns true when its box or shape is filled
960
+ */
961
+ function isFilled(n) {
962
+ const { shape, point } = n;
963
+ return (point ||
964
+ n.tokens.some((t) => t === "filled" ||
965
+ t === "radial" ||
966
+ (t === "striped" && BOX_LIKE.has(shape)) ||
967
+ (t === "wedged" && ELLIPSE_LIKE.has(shape))));
968
+ }
426
969
  /**
427
970
  * Reads a node's or cluster's outline: its color, width and dashes.
428
971
  * @param n - the node
@@ -436,7 +979,8 @@ function readNodeOutline(n) {
436
979
  const outline = cx.cluster ? (nonEmpty(get("pencolor")) ?? n.color) : n.color;
437
980
  const alpha = readColor(outline, n.scheme, "line", "borderColor", null, put, at);
438
981
  const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
439
- if (alpha === 0) {
982
+ // peripheries=0 draws no outline, on a node and on a cluster alike
983
+ if (alpha === 0 || Number(nonEmpty(get("peripheries")) ?? "1") === 0) {
440
984
  put("borderWidth", 0);
441
985
  }
442
986
  else if (width !== null) {
@@ -452,21 +996,293 @@ function readNodeOutline(n) {
452
996
  else if (tokens.includes("dotted")) {
453
997
  put("borderStyle", "dotted");
454
998
  }
999
+ if (!cx.cluster) {
1000
+ readNodeRings(n);
1001
+ }
1002
+ }
1003
+ /** The outlines Graphviz draws round a node of these shapes when `peripheries` is not set. */
1004
+ export const SHAPE_PERIPHERIES = new Map([
1005
+ ["doublecircle", 2],
1006
+ ["doubleoctagon", 2],
1007
+ ["tripleoctagon", 3],
1008
+ ]);
1009
+ /** How far apart Graphviz draws a node's outlines, center to center (`GAP`, lib/common/shapes.c). */
1010
+ export const PERIPHERY_GAP = 4;
1011
+ /**
1012
+ * Reads a node's second outline (`peripheries=2`, `doublecircle`): Graphviz draws it `PERIPHERY_GAP` points outside
1013
+ * the first, in the same color, width and dashes. graphty draws one outer ring, so a third and later outline is
1014
+ * noted as not carried.
1015
+ * @param n - the node
1016
+ */
1017
+ function readNodeRings(n) {
1018
+ const raw = nonEmpty(n.get("peripheries"));
1019
+ const count = raw === null ? (SHAPE_PERIPHERIES.get(n.shape) ?? 1) : Math.trunc(Number(raw));
1020
+ if (Number.isNaN(count) || count <= 1 || NO_OUTLINE.has(n.shape)) {
1021
+ return;
1022
+ }
1023
+ const c = n.color === null
1024
+ ? null
1025
+ : canonColor(n.color, "graphviz", n.at("outerBorderColor"), {
1026
+ scheme: n.scheme,
1027
+ use: "line",
1028
+ });
1029
+ if (c !== null && c.alpha === 0) {
1030
+ return;
1031
+ }
1032
+ const widthRaw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
1033
+ const parsed = widthRaw === null ? Number.NaN : Number(widthRaw);
1034
+ const unset = n.tokens.includes("bold") ? 2 : 1;
1035
+ const pen = Number.isFinite(parsed) ? Math.max(0, parsed) : unset;
1036
+ n.put("outerBorderColor", c?.color ?? "#000000");
1037
+ n.put("outerBorderOpacity", c !== null && c.alpha < 1 ? c.alpha : null);
1038
+ n.put("outerBorderWidth", num6(pen));
1039
+ // the gap between the outline's outer edge and the ring's inner edge
1040
+ n.put("outerBorderOffset", num6(Math.max(0, PERIPHERY_GAP - pen)));
1041
+ if (n.tokens.includes("dashed") || n.tokens.includes("dotted")) {
1042
+ n.put("outerBorderStyle", n.tokens.includes("dashed") ? "dashed" : "dotted");
1043
+ }
1044
+ if (count > 2) {
1045
+ noteAt(n.at("outerBorderWidth"), STYLE_ISSUE.NOT_CARRIED, `peripheries=${String(count)}`);
1046
+ }
455
1047
  }
456
1048
  /**
457
- * Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is.
1049
+ * Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is. Graphviz draws
1050
+ * a striped box with square corners whatever `rounded` says.
458
1051
  * @param n - the node
459
1052
  */
460
1053
  function readNodeShape(n) {
461
- const rounded = n.tokens.includes("rounded");
1054
+ const rounded = n.tokens.includes("rounded") && !n.tokens.includes("striped");
1055
+ const turned = turnedPolygon(n);
462
1056
  if (n.cx.cluster) {
463
1057
  n.put("shape", rounded ? "round-rectangle" : "rectangle");
464
1058
  }
1059
+ else if (n.shape === "polygon") {
1060
+ readPolygon(n, rounded);
1061
+ }
1062
+ else if (turned !== null) {
1063
+ n.put("shape", "polygon");
1064
+ n.put("shapePoints", cornersText(graphvizDrawnCorners(turned)));
1065
+ n.put("shapePointsStretch", false);
1066
+ }
1067
+ else if (NAMED_OUTLINES.has(n.shape)) {
1068
+ n.put("shape", "polygon");
1069
+ n.put("shapePoints", NAMED_OUTLINES.get(n.shape) ?? null);
1070
+ // a polygon shape's corners are where Graphviz draws them, one reaching past its box too
1071
+ n.put("shapePointsStretch", false);
1072
+ }
1073
+ else if (GRAPHVIZ_GLYPH_SHAPES.has(n.shape)) {
1074
+ const size = (name) => {
1075
+ const v = inches(n.get(name), n.at(name));
1076
+ return v === null ? null : v * DOT_POINTS_PER_INCH;
1077
+ };
1078
+ putGlyph(n.shape, size("width"), size("height"), n.put, () => {
1079
+ const raw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
1080
+ const pen = raw === null ? Number.NaN : Number(raw);
1081
+ if (Number.isFinite(pen)) {
1082
+ return Math.max(0, pen);
1083
+ }
1084
+ return n.tokens.includes("bold") ? 2 : 1;
1085
+ }, () => n.color === null
1086
+ ? null
1087
+ : (canonColor(n.color, "graphviz", n.at("outerBorderColor"), { scheme: n.scheme, use: "line" })
1088
+ ?.color ?? null));
1089
+ }
465
1090
  else if (n.shapeRaw !== null) {
466
- n.put("shape", rounded && BOX_LIKE.has(n.shape)
1091
+ const shape = rounded && BOX_LIKE.has(n.shape)
467
1092
  ? "round-rectangle"
468
- : canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape")));
1093
+ : canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape"));
1094
+ // a filled shape=none is its box filled, with no outline (its border is 0): a rectangle, not a node with no body
1095
+ n.put("shape", shape === "none" && isFilled(n) ? "rectangle" : shape);
1096
+ }
1097
+ }
1098
+ /** The polygon shapes `orientation` turns, by their own sides and orientation (`shapes.c`). */
1099
+ const TURNABLE_POLYGONS = new Map([
1100
+ ...[...BOX_LIKE].map((name) => [name, { sides: 4, orientation: 0 }]),
1101
+ ["diamond", { sides: 4, orientation: 45 }],
1102
+ ["hexagon", { sides: 6, orientation: 0 }],
1103
+ ["octagon", { sides: 8, orientation: 0 }],
1104
+ ]);
1105
+ /**
1106
+ * The polygon a named polygon shape is when its `orientation` turns it off its own axes (a box turned 30 degrees),
1107
+ * or null when it is drawn as its shape: Graphviz adds the attribute to the shape's own orientation.
1108
+ * @param n - the node
1109
+ * @returns the polygon, or null
1110
+ */
1111
+ function turnedPolygon(n) {
1112
+ const turn = Number(nonEmpty(n.get("orientation")) ?? "0");
1113
+ const own = TURNABLE_POLYGONS.get(n.shape) ?? GRAPHVIZ_POLYGON_SHAPES.get(n.shape);
1114
+ if (own === undefined || !Number.isFinite(turn) || turn % 360 === 0) {
1115
+ return null;
1116
+ }
1117
+ const polygon = {
1118
+ distortion: 0,
1119
+ skew: 0,
1120
+ ...own,
1121
+ orientation: own.orientation + Math.max(-360, turn),
1122
+ };
1123
+ // a quarter turn of a box is a box again
1124
+ return isGraphvizBox(polygon) ? null : polygon;
1125
+ }
1126
+ /**
1127
+ * Reads a `shape=polygon` node: a box when it is one (four sides, no distortion or skew, square to the axes), an
1128
+ * ellipse below three sides, else its corners as Graphviz places them.
1129
+ * @param n - the node
1130
+ * @param rounded - whether its style rounds a box's corners
1131
+ */
1132
+ function readPolygon(n, rounded) {
1133
+ const number = (name, fallback, least) => {
1134
+ const raw = nonEmpty(n.get(name));
1135
+ const v = raw === null ? Number.NaN : Number(raw);
1136
+ return Number.isFinite(v) ? Math.max(least, v) : fallback;
1137
+ };
1138
+ const polygon = {
1139
+ sides: Math.trunc(number("sides", 4, 0)),
1140
+ distortion: number("distortion", 0, -100),
1141
+ skew: number("skew", 0, -100),
1142
+ orientation: number("orientation", 0, -360),
1143
+ };
1144
+ if (isGraphvizBox(polygon)) {
1145
+ n.put("shape", rounded ? "round-rectangle" : "rectangle");
1146
+ }
1147
+ else if (polygon.sides < 3) {
1148
+ n.put("shape", "ellipse");
1149
+ }
1150
+ else {
1151
+ n.put("shape", "polygon");
1152
+ n.put("shapePoints", cornersText(graphvizDrawnCorners(polygon)));
1153
+ n.put("shapePointsStretch", false);
1154
+ }
1155
+ }
1156
+ /**
1157
+ * A Graphviz polygon's corners where Graphviz draws them, as fractions of its box's half width and half height: it
1158
+ * grows a polygon that falls short of the box along an axis until it reaches the box's sides, and keeps one that
1159
+ * reaches past them (a distortion, a skew, an egg) as it is (shapes.c `poly_init`: `scalex = bb.x / xmax`).
1160
+ * @param polygon - the polygon's attributes
1161
+ * @returns the corners
1162
+ */
1163
+ function graphvizDrawnCorners(polygon) {
1164
+ const corners = graphvizPolygonCorners(polygon);
1165
+ const grow = (axis) => Math.max(1, 1 / Math.max(...corners.map((c) => Math.abs(c[axis]))));
1166
+ const [gx, gy] = [grow(0), grow(1)];
1167
+ return corners.map(([x, y]) => [x * gx, y * gy]);
1168
+ }
1169
+ /**
1170
+ * Corners as a `shapePoints` value.
1171
+ * @param corners - the corners
1172
+ * @returns `x,y x,y ...`
1173
+ */
1174
+ function cornersText(corners) {
1175
+ return corners.map(([x, y]) => `${num6(x)},${num6(y)}`).join(" ");
1176
+ }
1177
+ /** The outline of each Graphviz polygon shape graphty draws as a polygon of its corners, by shape name. */
1178
+ const NAMED_OUTLINES = new Map([...GRAPHVIZ_POLYGON_SHAPES].map(([name, polygon]) => [name, cornersText(graphvizDrawnCorners(polygon))]));
1179
+ /**
1180
+ * A Graphviz shape drawn as pieces of its own (`graphvizGlyph`) at a node's size, as the neutral values that draw it:
1181
+ * a polygon of its outline drawn where it says, its further pieces (`shapeParts`) and lines (`shapeLines`), each as
1182
+ * fractions of the node's half width and half height. An `underline` is filled with no line round it, its line alone
1183
+ * drawn in the pen's width.
1184
+ * @param shape - the shape name, lowercase
1185
+ * @param width - the node's width in points, or null for Graphviz's default
1186
+ * @param height - its height, or null
1187
+ * @param put - receives the values
1188
+ * @param pen - the node's pen width, for an outline drawn as a line alone and a ring
1189
+ * @param color - the node's pen color, for a ring
1190
+ */
1191
+ export function putGlyph(shape, width, height, put, pen, color) {
1192
+ const text = glyphTexts(shape, width, height);
1193
+ if (text === null) {
1194
+ return;
1195
+ }
1196
+ put("shape", "polygon");
1197
+ put("shapePoints", text.outline);
1198
+ put("shapePointsStretch", false);
1199
+ put("shapeParts", text.parts);
1200
+ put("shapeLines", text.lines);
1201
+ if (text.unstroked) {
1202
+ put("shapeLineWidth", pen());
1203
+ put("borderWidth", 0);
1204
+ }
1205
+ if (text.ring !== null) {
1206
+ // drawn in the pen, its gap the distance less a pen's width (the outline's half and its own)
1207
+ put("outerBorderColor", color() ?? "#000000");
1208
+ put("outerBorderWidth", num6(pen()));
1209
+ put("outerBorderOffset", num6(Math.max(0, text.ring - pen())));
1210
+ }
1211
+ }
1212
+ /**
1213
+ * A Graphviz glyph's pieces as `shapePoints`, `shapeParts` and `shapeLines` text at a node's size, or null for a shape
1214
+ * that is not one.
1215
+ * @param shape - the shape name, lowercase
1216
+ * @param width - the node's width in points, or null for Graphviz's default
1217
+ * @param height - its height, or null
1218
+ * @returns the texts (parts and lines null when there are none)
1219
+ */
1220
+ function glyphTexts(shape, width, height) {
1221
+ const glyph = graphvizGlyph(shape, width, height);
1222
+ if (glyph === null) {
1223
+ return null;
1224
+ }
1225
+ // fractions of the half width and half height the glyph was laid out at
1226
+ const half = (v, fallback) => (v !== null && v > 0 ? v : fallback) / 2;
1227
+ const [W, H] = [half(width, 54), half(height, 36)];
1228
+ const list = (points) => cornersText(points.map(([x, y]) => [x / W, y / H]));
1229
+ const lists = (all) => all.length === 0 ? null : all.map(list).join("; ");
1230
+ return {
1231
+ outline: list(glyph.outline),
1232
+ parts: lists(glyph.parts),
1233
+ lines: lists(glyph.lines),
1234
+ unstroked: glyph.unstroked === true,
1235
+ ring: glyph.ring ?? null,
1236
+ };
1237
+ }
1238
+ /**
1239
+ * The corners of a Graphviz shape graphty draws as a polygon (`house`, `cylinder`, `note` ...), as a `shapePoints`
1240
+ * value, or undefined for a shape graphty draws by name.
1241
+ * @param shape - the shape name, lowercase
1242
+ * @returns the corners, or undefined
1243
+ */
1244
+ export function graphvizOutline(shape) {
1245
+ return NAMED_OUTLINES.get(shape);
1246
+ }
1247
+ /**
1248
+ * Whether Graphviz draws a shape with no outline (`plain`, `plaintext`, `none`).
1249
+ * @param shape - the shape name, lowercase
1250
+ * @returns true for a shape with no outline
1251
+ */
1252
+ export function graphvizHasNoOutline(shape) {
1253
+ return NO_OUTLINE.has(shape);
1254
+ }
1255
+ /** The shape name of each outline in `NAMED_OUTLINES`, so a written node keeps its Graphviz shape. */
1256
+ const OUTLINE_NAMES = new Map([...NAMED_OUTLINES].map(([name, text]) => [text, name]));
1257
+ /**
1258
+ * The Graphviz glyph shape a polygon is at a node's size, its outline, parts and lines alike, so a written node keeps
1259
+ * its shape (a ribosome site and a protease site share their outline and differ in their stem).
1260
+ * @param get - the node's values
1261
+ * @param width - the node's width in points, or null
1262
+ * @param height - its height, or null
1263
+ * @returns the shape name and whether it is filled with no line round it, or undefined
1264
+ */
1265
+ function glyphOf(get, width, height) {
1266
+ const text = (property) => {
1267
+ const v = get(property);
1268
+ return typeof v === "string" ? v.trim() : null;
1269
+ };
1270
+ const outline = text("shapePoints");
1271
+ if (outline === null) {
1272
+ return undefined;
1273
+ }
1274
+ // a glyph with another number of corners cannot match: skip writing its texts, which is most of the work
1275
+ const corners = outline === "" ? 0 : outline.split(/\s+/).length;
1276
+ for (const name of GRAPHVIZ_GLYPH_SHAPES) {
1277
+ if (graphvizGlyph(name, width, height)?.outline.length !== corners) {
1278
+ continue;
1279
+ }
1280
+ const g = glyphTexts(name, width, height);
1281
+ if (g !== null && g.outline === outline && g.parts === text("shapeParts") && g.lines === text("shapeLines")) {
1282
+ return { name, unstroked: g.unstroked };
1283
+ }
469
1284
  }
1285
+ return undefined;
470
1286
  }
471
1287
  /**
472
1288
  * A pen width as Graphviz draws it: a negative `penwidth` is raised to 0, its minimum, and noted.
@@ -485,15 +1301,14 @@ function penWidth(raw, attribute, site) {
485
1301
  }
486
1302
  /**
487
1303
  * Reads a node's width and height: exact where Graphviz draws them exactly (`fixedsize`, an empty label that is not
488
- * a record's, a point), otherwise minimums it grows to fit the label, noted.
1304
+ * a record's, a point), otherwise minimums it grows to fit the label (`readLabelFit`).
489
1305
  * @param get - the node's attributes
490
1306
  * @param shape - its shape, lowercase
491
1307
  * @param point - whether it is a point
492
- * @param label - the label it shows, or null
493
1308
  * @param put - receives the values
494
1309
  * @param at - the sites
495
1310
  */
496
- function readSize(get, shape, point, label, put, at) {
1311
+ function readSize(get, shape, point, put, at) {
497
1312
  let w = inches(get("width"), at("width"));
498
1313
  let h = inches(get("height"), at("height"));
499
1314
  const same = point || CIRCLE_LIKE.has(shape) || isTrue(get("regular"));
@@ -521,13 +1336,80 @@ function readSize(get, shape, point, label, put, at) {
521
1336
  else if (same) {
522
1337
  ww = hh = Math.max(w ?? 0, h ?? 0);
523
1338
  }
524
- const exact = point || isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape));
525
- if (!exact) {
526
- noteAt(at("width"), STYLE_ISSUE.SIZE_MINIMUM, "grows to fit its label");
527
- }
528
1339
  put("width", ww === null ? null : canonNumber(ww, at("width"), DOT_POINTS_PER_INCH));
529
1340
  put("height", hh === null ? null : canonNumber(hh, at("height"), DOT_POINTS_PER_INCH));
530
1341
  }
1342
+ /**
1343
+ * Reads the graph's `quantum` for a node that grows to hold its label: Graphviz rounds the padded label box up to a
1344
+ * multiple of it before fitting the outline round it (`poly_init`), so the node is at least one quantum across, an
1345
+ * ellipse's box times the square root of 2.
1346
+ * @param n - the node
1347
+ * @param label - its `label` attribute, or null
1348
+ */
1349
+ function readQuantum(n, label) {
1350
+ const { get, put, shape } = n;
1351
+ let scale = BOX_LIKE.has(shape) ? 1 : null;
1352
+ if (ELLIPSE_LIKE.has(shape)) {
1353
+ scale = Math.SQRT2;
1354
+ }
1355
+ const w = Number(get("width") ?? Number.NaN);
1356
+ const h = Number(get("height") ?? Number.NaN);
1357
+ // ponytail: one quantum is the least size; a label box over one quantum is not rounded up to the next, and only
1358
+ // boxes and ellipses with their size given are read (other polygons fit their box by factors of their own)
1359
+ if (n.cx.quantum === 0 || scale === null || label === "" || isTrue(get("fixedsize")) || !(w >= 0 && h >= 0)) {
1360
+ return;
1361
+ }
1362
+ const least = n.cx.quantum * scale;
1363
+ put("width", num6(Math.max(w * DOT_POINTS_PER_INCH, least)));
1364
+ put("height", num6(Math.max(h * DOT_POINTS_PER_INCH, least)));
1365
+ }
1366
+ /** Graphviz's padding around a node's label when it has no `margin`: 8 points left and right, 4 above and below. */
1367
+ const LABEL_PAD = { x: 8, y: 4 };
1368
+ /** Graphviz's line height, as a multiple of the font size (`LINESPACING`, lib/common/const.h). */
1369
+ const LINE_SPACING = 1.2;
1370
+ /**
1371
+ * Reads how a node that is not drawn at a fixed size grows to hold its label (`poly_init`, lib/common/shapes.c):
1372
+ * its padded label box fits inside its outline, the `margin` (else 8 points by 4) on each side, and a circle, a
1373
+ * square or a `regular` node stays as wide as it is tall. The neutral label box is one font size per line, so the
1374
+ * rest of Graphviz's line height (1.2 font sizes) goes into the vertical padding.
1375
+ * @param get - the node's attributes
1376
+ * @param shape - its shape, lowercase
1377
+ * @param label - its `label` attribute, or null
1378
+ * @param shown - the label it shows
1379
+ * @param put - receives the values
1380
+ */
1381
+ function readLabelFit(get, shape, label, shown, put) {
1382
+ if (isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape))) {
1383
+ return;
1384
+ }
1385
+ const lines = Math.max(1, shown.replace(/\n$/, "").split("\n").length);
1386
+ const size = Number(get("fontsize") ?? 14);
1387
+ const fontSize = Number.isFinite(size) && size > 0 ? size : 14;
1388
+ const margin = (get("margin") ?? "")
1389
+ .split(",")
1390
+ .filter((text) => text.trim() !== "")
1391
+ .map(Number)
1392
+ .filter(isLength);
1393
+ // plain is plaintext with a margin of 0
1394
+ const pad = shape === "plain" ? { x: 0, y: 0 } : LABEL_PAD;
1395
+ const padX = margin.length > 0 ? margin[0] * DOT_POINTS_PER_INCH : pad.x;
1396
+ const padY = margin.length > 0 ? (margin[1] ?? margin[0]) * DOT_POINTS_PER_INCH : pad.y;
1397
+ // a decorated box holds its label as a box does, exactly
1398
+ put("labelFit", GRAPHVIZ_DECORATED_BOXES.has(shape) ? "bounds" : "inside");
1399
+ put("labelPaddingX", num6(padX));
1400
+ put("labelPaddingY", num6(padY + ((LINE_SPACING - 1) * fontSize * lines) / 2));
1401
+ if (CIRCLE_LIKE.has(shape) || isTrue(get("regular"))) {
1402
+ put("labelFitSquare", true);
1403
+ }
1404
+ }
1405
+ /**
1406
+ * Whether a `margin` part is a usable length: a number at least 0.
1407
+ * @param value - the part
1408
+ * @returns whether it is
1409
+ */
1410
+ function isLength(value) {
1411
+ return Number.isFinite(value) && value >= 0;
1412
+ }
531
1413
  /**
532
1414
  * Reads one edge the way Graphviz draws it.
533
1415
  * @param get - its attributes
@@ -540,6 +1422,7 @@ function readEdge(get, cx, put, at) {
540
1422
  const scheme = get("colorscheme");
541
1423
  readCommon(get, tokens, scheme, cx.names, put, at);
542
1424
  readColor(nonEmpty(get("color")), scheme, "edge", "color", "opacity", put, at);
1425
+ readEdgeSegments(nonEmpty(get("color")), scheme, put, at);
543
1426
  readEdgeLine(get, tokens, put, at);
544
1427
  readEdgeArrows(get, cx, put, at);
545
1428
  for (const [attribute, property] of [
@@ -547,15 +1430,199 @@ function readEdge(get, cx, put, at) {
547
1430
  ["headlabel", "targetLabel"],
548
1431
  ["taillabel", "sourceLabel"],
549
1432
  ]) {
550
- const raw = get(attribute);
1433
+ // an edge's xlabel is drawn beside its middle, as its label is: it is the label when there is no other
1434
+ const raw = attribute === LABEL_ATTRIBUTE ? (get(attribute) ?? nonEmpty(get("xlabel"))) : get(attribute);
551
1435
  if (raw !== null) {
552
1436
  put(property, labelText(raw, cx.names, cx.html, false, at(property)));
1437
+ if (property === "label") {
1438
+ readLabelAlign(raw, cx.names, cx.html, false, put);
1439
+ readHtmlRuns(raw, cx.html, scheme, put, at);
1440
+ }
553
1441
  }
554
1442
  }
555
- if (nonEmpty(get("xlabel")) !== null) {
1443
+ if (get(LABEL_ATTRIBUTE) !== null && nonEmpty(get("xlabel")) !== null) {
556
1444
  noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
557
1445
  }
558
1446
  }
1447
+ /**
1448
+ * Reads an edge's colored segments: a color list with a `;fraction` in it (`red;0.3:blue`) draws the line in one piece
1449
+ * per color along its length, which is a linear gradient with a hard switch at each band's end. A list with no
1450
+ * fraction draws parallel lines, which graphty does not draw (the line is in the first color).
1451
+ * @param list - the edge's `color`
1452
+ * @param scheme - its color scheme
1453
+ * @param put - receives the values
1454
+ * @param at - the sites
1455
+ */
1456
+ function readEdgeSegments(list, scheme, put, at) {
1457
+ if (list === null || !list.includes(":") || !list.includes(";")) {
1458
+ return;
1459
+ }
1460
+ const bands = colorBands(list, scheme, "edge", at("gradientColors"));
1461
+ if (bands.length < 2) {
1462
+ return;
1463
+ }
1464
+ const colors = [];
1465
+ const stops = [];
1466
+ let start = 0;
1467
+ for (const band of bands) {
1468
+ colors.push(band.color, band.color);
1469
+ stops.push(num6(start), num6(Math.min(1, start + band.size)));
1470
+ start += band.size;
1471
+ }
1472
+ put("gradientKind", "linear");
1473
+ put("gradientColors", colors.join(" "));
1474
+ put("gradientStops", stops.join(" "));
1475
+ }
1476
+ /**
1477
+ * What an edge's label places are read against: the nodes' centers and sizes.
1478
+ * @param snapshot - the graph
1479
+ * @returns the context
1480
+ */
1481
+ function labelPlaceContext(snapshot) {
1482
+ const column = snapshot.nodes.byRole("position");
1483
+ return {
1484
+ center: (i) => {
1485
+ const v = column?.isSet(i) === true ? column.value(i) : null;
1486
+ return v === null ? null : [v[0], v[1]];
1487
+ },
1488
+ node: (i, name) => nonEmpty(cellText(snapshot.nodes, name, i)),
1489
+ };
1490
+ }
1491
+ /**
1492
+ * 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`
1493
+ * control points (y up), flattened into bends between the two nodes' centers. Null for an edge Graphviz drew
1494
+ * straight, a `pos` that is not a spline, or nodes without positions.
1495
+ * @param snapshot - the graph
1496
+ * @param e - the edge
1497
+ * @param pos - its `pos` text
1498
+ * @returns the `bends` value, or null
1499
+ */
1500
+ function splineBends(snapshot, e, pos) {
1501
+ if (pos === null) {
1502
+ return null;
1503
+ }
1504
+ const column = snapshot.nodes.byRole("position");
1505
+ const center = (i) => {
1506
+ const v = column?.isSet(i) === true ? column.value(i) : null;
1507
+ return v === null ? null : [v[0], v[1]];
1508
+ };
1509
+ const [source, target] = [center(snapshot.edgeSource(e)), center(snapshot.edgeTarget(e))];
1510
+ if (source === null || target === null) {
1511
+ return null;
1512
+ }
1513
+ // the arrow tips of the first spline's start and the last spline's end
1514
+ const tip = (spline, flag) => {
1515
+ const token = (spline ?? "")
1516
+ .trim()
1517
+ .split(/\s+/)
1518
+ .find((t) => t.startsWith(`${flag},`));
1519
+ const [x, y] = (token ?? "").slice(2).split(",").map(Number);
1520
+ return token !== undefined && Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
1521
+ };
1522
+ const parts = pos.split(";");
1523
+ const tips = { start: tip(parts[0], "s"), end: tip(parts.at(-1), "e") };
1524
+ // ponytail: the splines of a concentrated edge (";"-separated) are joined end to end
1525
+ const splines = parts.map((spline) => spline
1526
+ .trim()
1527
+ .split(/\s+/)
1528
+ .filter((t) => !/^[es],/.test(t))
1529
+ .map((t) => {
1530
+ const [x, y] = t.split(",").map(Number);
1531
+ return [x, y];
1532
+ }));
1533
+ return graphvizRouteBends(source, target, splines, tips);
1534
+ }
1535
+ /** How far apart Graphviz's neato lays the loops on one node, across (x) and along (y) its side, in points. */
1536
+ const GRAPHVIZ_LOOP_STEP = { x: 18, y: 9 };
1537
+ /**
1538
+ * The self-loops Graphviz lays itself, with no `pos` of their own and no ports: neato's `selfRight` (splines.c), on
1539
+ * 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
1540
+ * 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
1541
+ * drawings of nodes from 0.33 to 1 inch).
1542
+ * @param snapshot - the graph
1543
+ * @returns the bends of an edge that is such a loop, or null
1544
+ */
1545
+ function graphvizLoops(snapshot) {
1546
+ const portless = (get) => nonEmpty(get("tailport")) === null &&
1547
+ nonEmpty(get("headport")) === null &&
1548
+ nonEmpty(get(POS_ATTRIBUTE)) === null;
1549
+ const onNode = new Map();
1550
+ // ponytail: ortho routes a loop round the node's corners and none draws no edge; both keep the element's loop.
1551
+ const splines = (nonEmpty(cellText(snapshot.graph, "splines", 0)) ?? "").toLowerCase();
1552
+ // a file graphty wrote keeps its loops' look in its own block
1553
+ const laid = splines !== "ortho" && splines !== "none" && nonEmpty(cellText(snapshot.graph, STYLE_BLOCK_NAME, 0)) === null;
1554
+ for (let e = 0; e < snapshot.edgeCount && laid; e++) {
1555
+ const n = snapshot.edgeSource(e);
1556
+ if (n === snapshot.edgeTarget(e) && portless((name) => cellText(snapshot.edges, name, e))) {
1557
+ onNode.set(n, [...(onNode.get(n) ?? []), e]);
1558
+ }
1559
+ }
1560
+ const inches = (n, name, fallback) => {
1561
+ const v = Number(nonEmpty(cellText(snapshot.nodes, name, n)) ?? Number.NaN);
1562
+ return (Number.isFinite(v) && v > 0 ? v : fallback) * DOT_POINTS_PER_INCH;
1563
+ };
1564
+ return {
1565
+ bendsOf: (e, get) => {
1566
+ const n = snapshot.edgeSource(e);
1567
+ const list = onNode.get(n);
1568
+ if (n !== snapshot.edgeTarget(e) || list === undefined || !portless(get)) {
1569
+ return null;
1570
+ }
1571
+ // ponytail: a node Graphviz grows to fit its label is taken at its stated width and height
1572
+ const [w, h] = [inches(n, "width", 0.75), inches(n, "height", 0.5)];
1573
+ const k = list.indexOf(e) + 1;
1574
+ const dx = w / 2 + GRAPHVIZ_LOOP_STEP.x * k;
1575
+ const dy = (GRAPHVIZ_LOOP_STEP.y * k) / list.length;
1576
+ const curve = flattenCubic([
1577
+ [0, 0],
1578
+ [dx / 3, dy],
1579
+ [dx, dy],
1580
+ [dx, 0],
1581
+ [dx, -dy],
1582
+ [dx / 3, -dy],
1583
+ [0, 0],
1584
+ ]);
1585
+ return curve === null ? null : canonNodeOffsets([0, 0], [w, h], curve.slice(1, -1), false);
1586
+ },
1587
+ };
1588
+ }
1589
+ /**
1590
+ * The bends of a route Graphviz laid: B-splines of `3n + 1` control points each (y up), flattened into bends between
1591
+ * the two nodes' centers. Null for a route on the line between the centers, which is the straight edge it looks like,
1592
+ * or for control points that are not a spline. Also reads the `Line` of a GML file Graphviz's gv2gml wrote.
1593
+ * @param source - the source node's center
1594
+ * @param target - the target node's center
1595
+ * @param splines - the control points of each spline, joined end to end
1596
+ * @param tips - the arrow tips `pos` stores (`s,x,y` and `e,x,y`): the line runs on to each, along its arrow
1597
+ * @param tips.start - the tail arrow's tip, or null
1598
+ * @param tips.end - the head arrow's tip, or null
1599
+ * @returns the `bends` value, or null
1600
+ */
1601
+ export function graphvizRouteBends(source, target, splines, tips) {
1602
+ const points = [];
1603
+ for (const controls of splines) {
1604
+ const flat = flattenCubic(controls);
1605
+ if (flat === null || flat.some(([x, y]) => !Number.isFinite(x) || !Number.isFinite(y))) {
1606
+ return null;
1607
+ }
1608
+ points.push(...flat);
1609
+ }
1610
+ // a tip where the spline already ends adds no point
1611
+ const apart = (a, b) => b === undefined || Math.hypot(a[0] - b[0], a[1] - b[1]) > 1e-6;
1612
+ const start = tips?.start ?? null;
1613
+ const end = tips?.end ?? null;
1614
+ if (start !== null && apart(start, points[0])) {
1615
+ points.unshift(start);
1616
+ }
1617
+ if (end !== null && apart(end, points.at(-1))) {
1618
+ points.push(end);
1619
+ }
1620
+ const [dx, dy] = [target[0] - source[0], target[1] - source[1]];
1621
+ const length = Math.hypot(dx, dy);
1622
+ const off = Math.max(...points.map(([x, y]) => Math.abs((x - source[0]) * dy - (y - source[1]) * dx)));
1623
+ // a route on the line between the centers is the straight edge it looks like
1624
+ return length > 0 && off / (length * length) > 1e-3 ? canonBends(source, target, points, false) : null;
1625
+ }
559
1626
  /**
560
1627
  * Reads an edge's line: its width and dashes.
561
1628
  * @param get - its attributes
@@ -598,15 +1665,88 @@ function readEdgeArrows(get, cx, put, at) {
598
1665
  // with none of these the tool-default rule draws the arrow of the graph's kind
599
1666
  if (dirRaw !== null || head !== null || tail !== null) {
600
1667
  const dir = (dirRaw ?? (cx.directed ? "forward" : "none")).trim().toLowerCase();
601
- const forward = dir === "forward" || dir === "both";
602
- const back = dir === "back" || dir === "both";
603
- put("targetArrow", forward ? canonEnum("arrow", head ?? "normal", "graphviz", at("targetArrow")) : "none");
604
- put("sourceArrow", back ? canonEnum("arrow", tail ?? "normal", "graphviz", at("sourceArrow")) : "none");
1668
+ readEdgeArrow("target", head, dir === "forward" || dir === "both", put, at);
1669
+ readEdgeArrow("source", tail, dir === "back" || dir === "both", put, at);
605
1670
  }
606
1671
  const size = get("arrowsize");
607
1672
  if (size !== null) {
608
1673
  put("arrowSize", canonNumber(size, at("arrowSize")));
609
1674
  }
1675
+ // an edge's `fillcolor` fills its arrowheads (their outline stays the edge's color)
1676
+ const fill = nonEmpty(get("fillcolor"));
1677
+ for (const property of ["targetArrowColor", "sourceArrowColor"]) {
1678
+ readColor(fill, get("colorscheme"), "fill", property, null, put, at);
1679
+ }
1680
+ }
1681
+ /**
1682
+ * Reads one end's arrow: its shape, or none when the edge's direction draws no arrow there, and the side it keeps.
1683
+ * @param end - which end
1684
+ * @param raw - its `arrowhead` or `arrowtail`, or null
1685
+ * @param on - whether the edge's direction draws an arrow there
1686
+ * @param put - receives the values
1687
+ * @param at - the sites
1688
+ */
1689
+ function readEdgeArrow(end, raw, on, put, at) {
1690
+ const arrow = on ? graphvizArrowValue(raw ?? "normal", at(`${end}Arrow`)) : null;
1691
+ put(`${end}Arrow`, arrow === null ? "none" : arrow.value);
1692
+ if (arrow?.side !== null && arrow?.side !== undefined) {
1693
+ put(`${end}ArrowSide`, arrow.side);
1694
+ }
1695
+ }
1696
+ /** The sides an `l` or `r` arrow modifier keeps, by their first letters. */
1697
+ const ARROW_SIDES = ["left", "right"];
1698
+ /** The open forms of Graphviz arrows that a neutral shape draws; the others are drawn filled. */
1699
+ const GRAPHVIZ_OPEN_ARROWS = new Set(["normal", "diamond", "dot", "box", "inv"]);
1700
+ /** The open Graphviz arrows the neutral vocabulary has no name for, by the value graphty draws them as. */
1701
+ const GRAPHVIZ_OTHER_ARROWS = new Map([
1702
+ ["obox", "other:open-square"],
1703
+ ["oinv", "other:open-inverted"],
1704
+ ]);
1705
+ /**
1706
+ * The neutral arrow value Graphviz draws for an arrow name (`onormal`, `lbox`, `invempty`), with an open shape the
1707
+ * neutral vocabulary has no name for (`obox`, `oinv`) as the value graphty draws it as, and the side an `l` or `r`
1708
+ * keeps. Read so by the DOT reader and by the GML reader for gv2gml's `targetArrow` / `sourceArrow`.
1709
+ * @param raw - the arrow name
1710
+ * @param site - where it was read
1711
+ * @returns the value and its side (null for the whole shape)
1712
+ */
1713
+ export function graphvizArrowValue(raw, site) {
1714
+ const first = graphvizArrow(raw);
1715
+ const other = first === null ? undefined : GRAPHVIZ_OTHER_ARROWS.get(first.name);
1716
+ return {
1717
+ value: other ?? canonEnum("arrow", first?.name ?? raw, "graphviz", site),
1718
+ side: first?.side ?? null,
1719
+ };
1720
+ }
1721
+ /**
1722
+ * The arrow Graphviz draws at an edge's very end, from an arrow name of up to four shapes (`dotodot`,
1723
+ * `lteeoldiamond`): the first shape that is not `none`, with its `o` (open) kept where a neutral shape draws it, and
1724
+ * the side an `l` or `r` keeps alone ("clip the shape, leaving only the part to the left of the edge", looking along
1725
+ * the edge toward the arrow's tip).
1726
+ * @param raw - the arrow name
1727
+ * @returns the shape's own name and its side (null for the whole shape), or null for a name outside the grammar (a
1728
+ * synonym such as `empty`)
1729
+ */
1730
+ function graphvizArrow(raw) {
1731
+ let rest = raw.trim().toLowerCase();
1732
+ // the one synonym the grammar would misread (as `inv`): arrows.c's `invempty` is an open `inv`
1733
+ if (rest === "invempty") {
1734
+ rest = "oinv";
1735
+ }
1736
+ while (rest.length > 0) {
1737
+ const m = /^([olr]{0,3})(normal|crow|tee|box|diamond|dot|none|inv|vee|icurve|curve)/.exec(rest);
1738
+ if (m === null) {
1739
+ return null;
1740
+ }
1741
+ rest = rest.slice(m[0].length);
1742
+ const [, mods, shape] = m;
1743
+ if (shape !== "none") {
1744
+ const side = ARROW_SIDES.find((s) => mods.includes(s[0])) ?? null;
1745
+ const open = mods.includes("o") && GRAPHVIZ_OPEN_ARROWS.has(shape) ? "o" : "";
1746
+ return { name: open + shape, side };
1747
+ }
1748
+ }
1749
+ return null;
610
1750
  }
611
1751
  /**
612
1752
  * The HTML strings of a file: its HTML node names and labels, for Graphviz's shared string pool.
@@ -682,12 +1822,16 @@ function isCluster(snapshot, i) {
682
1822
  function contexts(snapshot, written = null) {
683
1823
  const html = htmlStrings(snapshot);
684
1824
  const graph = snapshot.meta.name ?? "";
1825
+ const rankdir = (cellText(snapshot.graph, "rankdir", 0) ?? "").trim().toUpperCase();
1826
+ const flip = rankdir === "LR" || rankdir === "RL";
1827
+ const q = Number(cellText(snapshot.graph, "quantum", 0) ?? Number.NaN);
1828
+ const quantum = Number.isFinite(q) && q > 0 ? q * DOT_POINTS_PER_INCH : 0;
685
1829
  const own = directedEdges(snapshot);
686
1830
  // a graph mixing both directions is written with one, every edge then drawn as that one
687
1831
  const directed = written === null || own.every((d) => d === snapshot.directed) ? own : own.map(() => written);
688
1832
  const name = (i) => String(snapshot.ids.idOf(i));
689
1833
  return {
690
- node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html }),
1834
+ node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html, flip, quantum }),
691
1835
  edge: (e) => {
692
1836
  const tail = name(snapshot.edgeSource(e));
693
1837
  const head = name(snapshot.edgeTarget(e));
@@ -726,6 +1870,25 @@ function styleColumns(table, attributes) {
726
1870
  const own = new Set([...attributes, ...NOTED_ATTRIBUTES]);
727
1871
  return [...table.names()].filter((c) => own.has(c) || c.startsWith("graphty.style."));
728
1872
  }
1873
+ /**
1874
+ * The graph's own styles: its `bgcolor` as its background.
1875
+ * @param snapshot - the graph
1876
+ * @param notes - where a bad color is noted
1877
+ * @returns the graph's values
1878
+ */
1879
+ function graphBackground(snapshot, notes) {
1880
+ const graph = {};
1881
+ const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
1882
+ if (!snapshot.graph.has("bgcolor") || bg === null) {
1883
+ return graph;
1884
+ }
1885
+ readColor(bg, cellText(snapshot.graph, "colorscheme", 0), "fill", "background", null, (p, v) => {
1886
+ if (v !== null) {
1887
+ graph[p] = v;
1888
+ }
1889
+ }, (property) => ({ notes, target: "graph", property }));
1890
+ return graph;
1891
+ }
729
1892
  /**
730
1893
  * The styles of a DOT file, read the way Graphviz draws it, with Graphviz's defaults beneath: an unfilled black
731
1894
  * ellipse 54 x 36 labeled with its name, black edges with a target arrow in a directed graph. A file graphty wrote
@@ -761,6 +1924,11 @@ export function dotStyles(snapshot, context) {
761
1924
  }
762
1925
  }
763
1926
  const edge = valueColumns(snapshot.edgeCount);
1927
+ const loops = graphvizLoops(snapshot);
1928
+ // `splines=none` and `splines=""` draw no edges at all (https://graphviz.org/docs/attrs/splines/)
1929
+ const splines = cellText(snapshot.graph, "splines", 0)?.trim().toLowerCase();
1930
+ const noEdges = splines === "" || splines === "none";
1931
+ const places = labelPlaceContext(snapshot);
764
1932
  for (let e = 0; e < snapshot.edgeCount; e++) {
765
1933
  context.budget.spend("edge", EDGE_ATTRIBUTES.length);
766
1934
  const get = (name) => cellText(snapshot.edges, name, e);
@@ -768,16 +1936,18 @@ export function dotStyles(snapshot, context) {
768
1936
  readEdge(get, cx.edge(e), (p, v) => {
769
1937
  edge.put(e, p, v);
770
1938
  }, at);
1939
+ if (noEdges) {
1940
+ edge.put(e, "visible", false);
1941
+ }
1942
+ const bends = splineBends(snapshot, e, get(POS_ATTRIBUTE)) ?? loops.bendsOf(e, get);
1943
+ if (bends !== null) {
1944
+ edge.put(e, "bends", bends);
1945
+ }
1946
+ readLabelPlaces(snapshot, places, e, get, (p, v) => {
1947
+ edge.put(e, p, v);
1948
+ });
771
1949
  }
772
- const graph = {};
773
- const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
774
- if (snapshot.graph.has("bgcolor") && bg !== null) {
775
- readColor(bg, cellText(snapshot.graph, "colorscheme", 0), "fill", "background", null, (p, v) => {
776
- if (v !== null) {
777
- graph[p] = v;
778
- }
779
- }, (property) => ({ notes, target: "graph", property }));
780
- }
1950
+ const graph = graphBackground(snapshot, notes);
781
1951
  const blockText = cellText(snapshot.graph, STYLE_BLOCK_NAME, 0);
782
1952
  if (blockText !== null) {
783
1953
  exactFromBlock(snapshot, blockText, { node: node.values, edge: edge.values }, cx);
@@ -855,8 +2025,9 @@ const ARROWS = new Map([
855
2025
  ["circle-open", "odot"],
856
2026
  ["square", "box"],
857
2027
  ["crow", "crow"],
858
- ["half", "lnormal"],
859
2028
  ["inverted", "inv"],
2029
+ ["other:open-square", "obox"],
2030
+ ["other:open-inverted", "oinv"],
860
2031
  ]);
861
2032
  /** Line and border styles Graphviz draws exactly; the others are written as the nearest. */
862
2033
  const STROKES = new Map([
@@ -883,7 +2054,10 @@ function graphvizDefaults(target) {
883
2054
  };
884
2055
  add("fontcolor", "labelColor");
885
2056
  add("fontsize", "labelSize");
886
- add("fontname", "labelFont");
2057
+ const font = t.labelFont?.value;
2058
+ if (typeof font === "string") {
2059
+ out.set("fontname", graphvizFontName(font, false, false));
2060
+ }
887
2061
  if (target === "node") {
888
2062
  add("color", "borderColor");
889
2063
  add("pencolor", "borderColor");
@@ -934,12 +2108,16 @@ function gvColor(color, opacity) {
934
2108
  return color + a.toString(16).padStart(2, "0");
935
2109
  }
936
2110
  /**
937
- * A label's text as a DOT label: backslashes and line breaks escaped, so Graphviz's escape passes give the text back.
2111
+ * A label's text as a DOT label: backslashes, line breaks and `&` escaped, so Graphviz's escape and entity passes
2112
+ * give the text back.
938
2113
  * @param text - the text
939
2114
  * @returns the label
940
2115
  */
941
2116
  function escapeLabel(text) {
942
- return text.replaceAll("\\", String.raw `\\`).replaceAll("\n", String.raw `\n`);
2117
+ return text
2118
+ .replaceAll("&", "&amp;")
2119
+ .replaceAll("\\", String.raw `\\`)
2120
+ .replaceAll("\n", String.raw `\n`);
943
2121
  }
944
2122
  /**
945
2123
  * A shape, line or arrow value in Graphviz's spelling.
@@ -964,10 +2142,11 @@ function spelled(table, value, nearest, property, approximate) {
964
2142
  /**
965
2143
  * The font, tooltip and visibility attributes every element shares.
966
2144
  * @param get - the element's values
2145
+ * @param approximate - records approximations
967
2146
  * @param out - the attributes, added to
968
2147
  * @param tokens - the style tokens, added to
969
2148
  */
970
- function writeCommon(get, out, tokens) {
2149
+ function writeCommon(get, approximate, out, tokens) {
971
2150
  const color = get("labelColor");
972
2151
  if (typeof color === "string") {
973
2152
  out.set("fontcolor", color);
@@ -977,8 +2156,21 @@ function writeCommon(get, out, tokens) {
977
2156
  out.set("fontsize", size);
978
2157
  }
979
2158
  const font = get("labelFont");
2159
+ const bold = get("labelWeight") === "bold";
2160
+ const italic = get("labelItalic") === true;
980
2161
  if (typeof font === "string") {
981
- out.set("fontname", font);
2162
+ const name = graphvizFontName(font, bold, italic);
2163
+ out.set("fontname", name);
2164
+ const drawn = graphvizFont(name);
2165
+ if (drawn.bold !== bold) {
2166
+ approximate("labelWeight", get("labelWeight") ?? "normal");
2167
+ }
2168
+ if (drawn.italic !== italic) {
2169
+ approximate("labelItalic", italic);
2170
+ }
2171
+ }
2172
+ else if (bold || italic) {
2173
+ approximate(bold ? "labelWeight" : "labelItalic", bold ? "bold" : true);
982
2174
  }
983
2175
  const tip = get("tooltip");
984
2176
  if (typeof tip === "string") {
@@ -992,14 +2184,15 @@ function writeCommon(get, out, tokens) {
992
2184
  * A node's or cluster's Graphviz attributes from its values.
993
2185
  * @param get - the node's values
994
2186
  * @param cx - the node's name and kind
2187
+ * @param raw - its attributes as data
995
2188
  * @param approximate - records approximations
996
2189
  * @param notes - where an HTML name is noted
997
2190
  * @returns the attributes
998
2191
  */
999
- function nodeAttributes(get, cx, approximate, notes) {
2192
+ function nodeAttributes(get, cx, raw, approximate, notes) {
1000
2193
  const out = new Map();
1001
2194
  const tokens = [];
1002
- writeCommon(get, out, tokens);
2195
+ writeCommon(get, approximate, out, tokens);
1003
2196
  writeNodeOutline(get, cx, approximate, out, tokens);
1004
2197
  const gvShape = writeNodeShape(get, cx, approximate, out, tokens);
1005
2198
  if (!cx.cluster) {
@@ -1014,11 +2207,26 @@ function nodeAttributes(get, cx, approximate, notes) {
1014
2207
  else if (label === null && shown !== null) {
1015
2208
  out.set(LABEL_ATTRIBUTE, "");
1016
2209
  }
2210
+ noteLabelPosition(get("labelPosition"), cx, raw, approximate);
1017
2211
  if (tokens.length > 0) {
1018
2212
  out.set("style", tokens.join(","));
1019
2213
  }
1020
2214
  return withoutDefaults(out, "node");
1021
2215
  }
2216
+ /**
2217
+ * Notes a label position Graphviz does not draw: a node's label sits where its own `labelloc` (kept as data) puts
2218
+ * it, and a cluster's where its `labelloc` and `labeljust` do.
2219
+ * @param position - the label's position
2220
+ * @param cx - the node's name and kind
2221
+ * @param raw - the node's attributes as data
2222
+ * @param approximate - records approximations
2223
+ */
2224
+ function noteLabelPosition(position, cx, raw, approximate) {
2225
+ if (typeof position === "string" &&
2226
+ position !== (cx.cluster ? clusterLabelPosition(raw) : nodeLabelPosition(raw))) {
2227
+ approximate("labelPosition", position);
2228
+ }
2229
+ }
1022
2230
  /**
1023
2231
  * A node's or cluster's fill and outline attributes.
1024
2232
  * @param get - the node's values
@@ -1071,19 +2279,91 @@ function writeNodeShape(get, cx, approximate, out, tokens) {
1071
2279
  }
1072
2280
  return "";
1073
2281
  }
2282
+ const size = (name) => {
2283
+ const v = get(name);
2284
+ return typeof v === "number" ? v : null;
2285
+ };
2286
+ const glyph = shape === "polygon" ? glyphOf(get, size("width"), size("height")) : undefined;
2287
+ if (glyph !== undefined) {
2288
+ return writeGlyph(get, glyph, out);
2289
+ }
2290
+ approximateShapeLines(get, approximate);
2291
+ const polygon = shape === "polygon" ? writePolygon(get("shapePoints"), approximate, out) : null;
2292
+ if (polygon !== null) {
2293
+ // Graphviz draws its polygon's corners where it lays them, which read back unstretched
2294
+ if (get("shapePointsStretch") !== false) {
2295
+ approximate("shapePointsStretch", true);
2296
+ }
2297
+ return polygon;
2298
+ }
1074
2299
  const gvShape = spelled(SHAPES, shape, shape === "vee" ? "invtriangle" : "box", "shape", approximate);
1075
2300
  out.set("shape", gvShape);
1076
2301
  return gvShape;
1077
2302
  }
1078
2303
  /**
1079
- * A node's width and height, fixed so Graphviz does not grow them to fit the label.
2304
+ * A glyph's shape attribute, and its line's width as its pen's when it draws no outline.
2305
+ * @param get - the node's values
2306
+ * @param glyph - the glyph
2307
+ * @param out - the attributes, added to
2308
+ * @returns the Graphviz shape written
2309
+ */
2310
+ function writeGlyph(get, glyph, out) {
2311
+ out.set("shape", glyph.name);
2312
+ // an underline's line is drawn in its pen, which draws no outline round it
2313
+ const line = get("shapeLineWidth");
2314
+ if (glyph.unstroked && typeof line === "number") {
2315
+ out.set("penwidth", line);
2316
+ }
2317
+ return glyph.name;
2318
+ }
2319
+ /**
2320
+ * Notes a node's pieces and lines of no Graphviz shape as approximated: Graphviz has no attribute for them.
2321
+ * @param get - the node's values
2322
+ * @param approximate - records approximations
2323
+ */
2324
+ function approximateShapeLines(get, approximate) {
2325
+ for (const property of ["shapeParts", "shapeLines", "shapeLineWidth"]) {
2326
+ const v = get(property);
2327
+ if (v !== null && v !== undefined) {
2328
+ approximate(property, v);
2329
+ }
2330
+ }
2331
+ }
2332
+ /**
2333
+ * A polygon's shape attribute: the Graphviz shape whose outline it is, else `shape=polygon` with its number of sides.
2334
+ * @param points - its `shapePoints`
2335
+ * @param approximate - records approximations
2336
+ * @param out - the attributes, added to
2337
+ * @returns the Graphviz shape written, or null for corners too few to be a polygon
2338
+ */
2339
+ function writePolygon(points, approximate, out) {
2340
+ if (typeof points !== "string") {
2341
+ return null;
2342
+ }
2343
+ const named = OUTLINE_NAMES.get(points.trim());
2344
+ if (named !== undefined) {
2345
+ out.set("shape", named);
2346
+ return named;
2347
+ }
2348
+ // the corners themselves are not Graphviz attributes: the polygon keeps its number of sides
2349
+ const sides = points.trim().split(/\s+/).length;
2350
+ if (sides < 3) {
2351
+ return null;
2352
+ }
2353
+ out.set("shape", "polygon");
2354
+ out.set("sides", sides);
2355
+ approximate("shapePoints", points);
2356
+ return "polygon";
2357
+ }
2358
+ /**
2359
+ * A node's width and height, fixed so Graphviz does not grow them to fit the label, unless the node grows to fit it.
1080
2360
  * @param get - the node's values
1081
2361
  * @param out - the attributes, added to
1082
2362
  */
1083
2363
  function writeNodeSize(get, out) {
1084
2364
  const width = get("width");
1085
2365
  const height = get("height");
1086
- if (typeof width === "number" || typeof height === "number") {
2366
+ if ((typeof width === "number" || typeof height === "number") && typeof get("labelFit") !== "string") {
1087
2367
  out.set("fixedsize", "true");
1088
2368
  }
1089
2369
  if (typeof width === "number") {
@@ -1103,7 +2383,7 @@ function writeNodeSize(get, out) {
1103
2383
  function edgeAttributes(get, directed, approximate) {
1104
2384
  const out = new Map();
1105
2385
  const tokens = [];
1106
- writeCommon(get, out, tokens);
2386
+ writeCommon(get, approximate, out, tokens);
1107
2387
  const color = get("color");
1108
2388
  if (typeof color === "string") {
1109
2389
  out.set("color", gvColor(color, get("opacity")));
@@ -1117,6 +2397,16 @@ function edgeAttributes(get, directed, approximate) {
1117
2397
  tokens.push(spelled(STROKES, line, line.includes("dash") ? "dashed" : "solid", "lineStyle", approximate));
1118
2398
  }
1119
2399
  writeEdgeArrows(get, directed, approximate, out);
2400
+ // Graphviz fills both arrowheads with the one `fillcolor`
2401
+ const head = get("targetArrowColor");
2402
+ const tail = get("sourceArrowColor");
2403
+ const fill = head ?? tail;
2404
+ if (typeof fill === "string") {
2405
+ out.set("fillcolor", fill);
2406
+ if (head !== null && tail !== null && head !== tail) {
2407
+ approximate("sourceArrowColor", tail);
2408
+ }
2409
+ }
1120
2410
  const arrowSize = get("arrowSize");
1121
2411
  if (typeof arrowSize === "number") {
1122
2412
  out.set("arrowsize", arrowSize);
@@ -1148,7 +2438,7 @@ function writeEdgeArrows(get, directed, approximate, out) {
1148
2438
  const target = get("targetArrow");
1149
2439
  const tail = typeof source === "string" && source !== "none";
1150
2440
  const head = typeof target === "string" && target !== "none";
1151
- const plain = directed ? head && !tail && target === "triangle" : !head && !tail;
2441
+ const plain = directed ? head && !tail && target === "triangle" && get("targetArrowSide") === null : !head && !tail;
1152
2442
  if (plain || (source === null && target === null)) {
1153
2443
  return;
1154
2444
  }
@@ -1163,26 +2453,24 @@ function writeEdgeArrows(get, directed, approximate, out) {
1163
2453
  dir = "back";
1164
2454
  }
1165
2455
  out.set("dir", dir);
2456
+ // a one-sided arrow is its shape with an `l` or `r` before it; the neutral half arrow, one barb, is nearest to the
2457
+ // left half of Graphviz's triangle, which reads back as that
2458
+ const arrow = (value, side, property) => {
2459
+ if (value === "half") {
2460
+ approximate(property, value);
2461
+ approximate(`${property}Side`, value);
2462
+ return "lnormal";
2463
+ }
2464
+ const name = spelled(ARROWS, value, "normal", property, approximate);
2465
+ return side === "left" || side === "right" ? `${side[0]}${name}` : name;
2466
+ };
1166
2467
  if (head) {
1167
- out.set("arrowhead", spelled(ARROWS, target, "normal", "targetArrow", approximate));
2468
+ out.set("arrowhead", arrow(target, get("targetArrowSide"), "targetArrow"));
1168
2469
  }
1169
2470
  if (tail) {
1170
- out.set("arrowtail", spelled(ARROWS, source, "normal", "sourceArrow", approximate));
2471
+ out.set("arrowtail", arrow(source, get("sourceArrowSide"), "sourceArrow"));
1171
2472
  }
1172
2473
  }
1173
- /**
1174
- * Each element's values of the properties the writer writes, as a getter per element.
1175
- * @param picture - each element's value of a property
1176
- * @param target - nodes or edges
1177
- * @returns the getter
1178
- */
1179
- function getter(picture, target) {
1180
- const columns = new Map();
1181
- for (const p of DOT_STYLE_CAPABILITIES[target]) {
1182
- columns.set(p, picture(target, p));
1183
- }
1184
- return (i) => (property) => columns.get(property)?.[i] ?? null;
1185
- }
1186
2474
  /**
1187
2475
  * For a file graphty wrote: where a value read from the attributes is what the writer writes for the block's own
1188
2476
  * value (a hexagon written as a hexagon, a pentagon-like vee as an inverted triangle, an opacity rounded to 255 steps,
@@ -1196,40 +2484,31 @@ function getter(picture, target) {
1196
2484
  * @param cx - the elements' contexts
1197
2485
  */
1198
2486
  function exactFromBlock(snapshot, text, read, cx) {
1199
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
1200
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "dot") : null;
1201
- if (ex === null) {
1202
- return;
1203
- }
1204
2487
  const notes = new StyleNotes();
1205
- for (const target of ["node", "edge"]) {
1206
- const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1207
- const get = getter((t, p) => resolveStyles(ex, t, p, snapshot), target);
1208
- const values = read[target];
1209
- for (let i = 0; i < n; i++) {
1210
- const want = get(i);
1211
- const back = writtenAndReadBack(target, i, want, cx, notes);
1212
- for (const property of DOT_STYLE_CAPABILITIES[target]) {
1213
- const native = values[property]?.[i] ?? null;
1214
- const w = want(property);
1215
- if (native !== w && native === (back.get(property) ?? null)) {
1216
- values[property] ?? (values[property] = new Array(n).fill(null));
1217
- values[property][i] = w;
1218
- }
1219
- }
1220
- }
1221
- }
2488
+ takeFromBlock(snapshot, text, "dot", (target) => ({
2489
+ values: read[target],
2490
+ properties: DOT_STYLE_CAPABILITIES[target],
2491
+ writes: DOT_STYLE_CAPABILITIES[target],
2492
+ readBack: (i, want) => {
2493
+ const raw = (name) => cellText(target === "node" ? snapshot.nodes : snapshot.edges, name, i);
2494
+ const back = writtenAndReadBack(target, i, want, { cx, raw }, notes);
2495
+ return (property) => back.get(property) ?? null;
2496
+ },
2497
+ }));
1222
2498
  }
1223
2499
  /**
1224
2500
  * An element's values written as the writer writes them and read back as the reader reads them.
1225
2501
  * @param target - nodes or edges
1226
2502
  * @param i - the element
1227
2503
  * @param want - its values
1228
- * @param cx - the elements' contexts
2504
+ * @param data - the elements' contexts, and the element's attributes as data (what the writer leaves as it is)
2505
+ * @param data.cx - the elements' contexts
2506
+ * @param data.raw - the element's attributes as data
1229
2507
  * @param notes - a scratch for the notes the round trip makes
1230
2508
  * @returns the values read back, by property
1231
2509
  */
1232
- function writtenAndReadBack(target, i, want, cx, notes) {
2510
+ function writtenAndReadBack(target, i, want, data, notes) {
2511
+ const { cx, raw } = data;
1233
2512
  const ignore = () => undefined;
1234
2513
  const back = new Map();
1235
2514
  const put = (property, value) => {
@@ -1240,8 +2519,10 @@ function writtenAndReadBack(target, i, want, cx, notes) {
1240
2519
  const at = (property) => ({ notes, target, property });
1241
2520
  if (target === "node") {
1242
2521
  const ncx = cx.node(i);
1243
- const attrs = nodeAttributes(want, ncx, ignore, notes);
1244
- readNode((name) => textOf(attrs.get(name)), ncx, put, at);
2522
+ const attrs = nodeAttributes(want, ncx, raw, ignore, notes);
2523
+ // a cluster's label position stays in its data (`labelloc`, `labeljust`), which the writer leaves alone
2524
+ const kept = (name) => (name === "labelloc" || name === "labeljust" ? raw(name) : null);
2525
+ readNode((name) => (attrs.has(name) ? textOf(attrs.get(name)) : kept(name)), ncx, put, at);
1245
2526
  }
1246
2527
  else {
1247
2528
  const ecx = cx.edge(i);
@@ -1332,7 +2613,8 @@ function styledExport(snapshot, input, onMixedDirection) {
1332
2613
  if (out.graph.has("bgcolor")) {
1333
2614
  out.graph.remove("bgcolor");
1334
2615
  }
1335
- if (typeof background === "string") {
2616
+ // Graphviz paints a graph without bgcolor white, so the writer leaves that default out like the others
2617
+ if (typeof background === "string" && background !== toolDefaults("graphviz")?.graph.background?.value) {
1336
2618
  out.graph.set("bgcolor", [background], { dtype: "string" });
1337
2619
  }
1338
2620
  const inline = [...input.tags.node.values(), ...input.tags.edge.values()].some((col) => !readsBack(col));
@@ -1357,28 +2639,83 @@ function styledExport(snapshot, input, onMixedDirection) {
1357
2639
  */
1358
2640
  function attributeColumns(snapshot, input, target, w) {
1359
2641
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1360
- const get = getter((t, p) => input.picture(t, p), target);
2642
+ const get = elementValues((t, p) => input.picture(t, p), target, DOT_STYLE_CAPABILITIES[target]);
1361
2643
  const columns = new Map();
1362
2644
  for (let i = 0; i < n; i++) {
1363
2645
  const attrs = target === "node"
1364
- ? nodeAttributes(get(i), w.cx.node(i), w.approximate, w.notes)
2646
+ ? nodeAttributes(get(i), w.cx.node(i), (name) => cellText(snapshot.nodes, name, i), w.approximate, w.notes)
1365
2647
  : edgeAttributes(get(i), w.cx.edge(i).directed, w.approximate);
2648
+ // a self-loop's route is not written: Graphviz lays the loop itself, which is what reads back
2649
+ if (target === "edge" && snapshot.edgeSource(i) === snapshot.edgeTarget(i)) {
2650
+ w.approximate("bends", "self-loop");
2651
+ }
1366
2652
  // no label is written label="" (Graphviz would show the name), which a rule reading the label column
1367
2653
  // reads back as the empty label
1368
2654
  if (w.read.has(LABEL_ATTRIBUTE) && attrs.get(LABEL_ATTRIBUTE) === "" && get(i)("label") === null) {
1369
2655
  w.approximate("label", "");
1370
2656
  }
1371
- for (const [name, value] of attrs) {
1372
- let col = columns.get(name);
1373
- if (col === undefined) {
1374
- col = new Array(n).fill(null);
1375
- columns.set(name, col);
1376
- }
1377
- col[i] = value;
1378
- }
2657
+ setColumnValues(columns, attrs, i, n);
2658
+ }
2659
+ if (target === "node") {
2660
+ stopInheritance(snapshot, w.cx, columns, input.rules.styles.graph.background);
1379
2661
  }
1380
2662
  return columns;
1381
2663
  }
2664
+ /**
2665
+ * Puts one element's attributes into the columns, adding a column for an attribute not seen before.
2666
+ * @param columns - the columns, by name
2667
+ * @param attrs - the element's attributes
2668
+ * @param i - the element
2669
+ * @param n - how many elements there are
2670
+ */
2671
+ function setColumnValues(columns, attrs, i, n) {
2672
+ for (const [name, value] of attrs) {
2673
+ let col = columns.get(name);
2674
+ if (col === undefined) {
2675
+ col = new Array(n).fill(null);
2676
+ columns.set(name, col);
2677
+ }
2678
+ col[i] = value;
2679
+ }
2680
+ }
2681
+ /** What a cluster draws when neither it nor a graph around it sets the attribute: Graphviz's defaults. */
2682
+ const CLUSTER_UNSET = new Map([
2683
+ ["style", ""],
2684
+ ["penwidth", 1],
2685
+ ["color", ""],
2686
+ ["pencolor", ""],
2687
+ ["fillcolor", ""],
2688
+ ["bgcolor", ""],
2689
+ ["fontname", "Times-Roman"],
2690
+ ["fontsize", 14],
2691
+ ["fontcolor", "black"],
2692
+ [LABEL_ATTRIBUTE, ""],
2693
+ ]);
2694
+ /**
2695
+ * Gives every cluster its own value of each attribute a graph around it sets, Graphviz's default where the cluster
2696
+ * has none, since Graphviz gives a cluster the attributes of the graphs around it (the root's are kept as data).
2697
+ * @param snapshot - the graph handed to the exporter
2698
+ * @param cx - the elements' contexts
2699
+ * @param columns - the node attributes written, by name, changed in place
2700
+ * @param background - the background the writer gives the root graph (its `bgcolor`), which a cluster would inherit
2701
+ */
2702
+ function stopInheritance(snapshot, cx, columns, background) {
2703
+ const clusters = Array.from({ length: snapshot.nodeCount }, (_, i) => i).filter((i) => cx.node(i).cluster);
2704
+ for (const [name, unset] of CLUSTER_UNSET) {
2705
+ const col = columns.get(name);
2706
+ const enclosing = cellText(snapshot.graph, name, 0) !== null ||
2707
+ (name === "bgcolor" && typeof background === "string") ||
2708
+ clusters.some((i) => (col?.[i] ?? null) !== null);
2709
+ if (!enclosing) {
2710
+ continue;
2711
+ }
2712
+ const filled = col ?? new Array(snapshot.nodeCount).fill(null);
2713
+ for (const i of clusters) {
2714
+ filled[i] ?? (filled[i] = unset);
2715
+ }
2716
+ columns.set(name, filled);
2717
+ }
2718
+ }
1382
2719
  /**
1383
2720
  * Removes every column Graphviz reads as how to draw: the picture writes them, so a data column of the same name (a
1384
2721
  * `label` attribute graphty does not draw) would draw something graphty does not. One a rule reads, whose values the