@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
@@ -30,25 +30,27 @@ import type { GraphSnapshot } from "@graphty/graph-format";
30
30
 
31
31
  import { isWritableDotText } from "../../common/escape.js";
32
32
  import { inferTextDtype } from "../../common/text.js";
33
+ import { type BendPoint, canonBends, canonNodeOffsets, flattenCubic } from "../../styles/canon/bends.js";
33
34
  import { canonColor, colorOpacity } from "../../styles/canon/color.js";
34
35
  import { canonEnum } from "../../styles/canon/enums.js";
35
36
  import { canonNumber, DOT_POINTS_PER_INCH, num6 } from "../../styles/canon/number.js";
36
37
  import { type CanonSite, MAX_STYLE_VALUE_LENGTH, noteAt } from "../../styles/canon/site.js";
37
- import { type DotEscapeNames, dotLabelText, htmlLabelText, recordLabelText } from "../../styles/canon/text.js";
38
+ import {
39
+ type DotEscapeNames,
40
+ dotLabelAlign,
41
+ dotLabelText,
42
+ htmlLabelSpans,
43
+ htmlLabelText,
44
+ recordLabelText,
45
+ } from "../../styles/canon/text.js";
38
46
  import { STYLE_ISSUE } from "../../styles/codes.js";
39
47
  import { toolDefaults } from "../../styles/defaults.js";
40
48
  import { StyleApproximations } from "../../styles/io/approximations.js";
41
- import {
42
- blockStyles,
43
- parseStyleBlock,
44
- STYLE_BLOCK_NAME,
45
- styleBlockLosses,
46
- writeStyleBlock,
47
- } from "../../styles/io/block.js";
49
+ import { STYLE_BLOCK_NAME, styleBlockLosses, writeStyleBlock } from "../../styles/io/block.js";
50
+ import { elementValues, takeFromBlock } from "../../styles/io/round-trip.js";
48
51
  import { type ExporterStyleInput, sharedInExportCall } from "../../styles/io/writer.js";
49
52
  import { StyleNotes } from "../../styles/notes.js";
50
- import { styleFingerprint } from "../../styles/plan/features.js";
51
- import { plainCell, resolveStyles } from "../../styles/plan/resolve.js";
53
+ import { plainCell } from "../../styles/plan/resolve.js";
52
54
  import type {
53
55
  ImporterStyles,
54
56
  StyleColumn,
@@ -61,7 +63,19 @@ import type {
61
63
  } from "../../styles/types.js";
62
64
  import type { LossNote } from "../../types.js";
63
65
  import { type ValueColumns, valueColumns } from "../graphml/styles-yed.js";
64
- import { CLUSTER_COLUMN, LABEL_ATTRIBUTE } from "./names.js";
66
+ import {
67
+ GRAPHVIZ_DECORATED_BOXES,
68
+ GRAPHVIZ_GLYPH_SHAPES,
69
+ GRAPHVIZ_POLYGON_SHAPES,
70
+ graphvizFont,
71
+ graphvizFontName,
72
+ graphvizGlyph,
73
+ graphvizPolygonCorners,
74
+ isGraphvizBox,
75
+ } from "./drawing.js";
76
+ import { type LabelPlaceContext, readLabelPlaces } from "./label-places.js";
77
+ import { CLUSTER_COLUMN, LABEL_ATTRIBUTE, POS_ATTRIBUTE } from "./names.js";
78
+ import { layoutRecord } from "./record.js";
65
79
 
66
80
  /**
67
81
  * The graph attribute holding the `graphty.styles` block of a DOT file graphty wrote.
@@ -75,6 +89,7 @@ export { STYLE_BLOCK_NAME as DOT_STYLE_BLOCK_ATTRIBUTE } from "../../styles/io/b
75
89
  * @category Built-in formats
76
90
  */
77
91
  export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
92
+ tool: "graphviz",
78
93
  node: Object.freeze([
79
94
  "fill",
80
95
  "fillOpacity",
@@ -88,6 +103,14 @@ export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
88
103
  "labelColor",
89
104
  "labelSize",
90
105
  "labelFont",
106
+ "labelWeight",
107
+ "labelItalic",
108
+ "labelPosition",
109
+ "shapePoints",
110
+ "shapePointsStretch",
111
+ "shapeParts",
112
+ "shapeLines",
113
+ "shapeLineWidth",
91
114
  "tooltip",
92
115
  "visible",
93
116
  ] as const),
@@ -98,6 +121,8 @@ export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
98
121
  "lineStyle",
99
122
  "sourceArrow",
100
123
  "targetArrow",
124
+ "sourceArrowColor",
125
+ "targetArrowColor",
101
126
  "arrowSize",
102
127
  "label",
103
128
  "sourceLabel",
@@ -105,6 +130,8 @@ export const DOT_STYLE_CAPABILITIES: StyleWriteCapabilities = Object.freeze({
105
130
  "labelColor",
106
131
  "labelSize",
107
132
  "labelFont",
133
+ "labelWeight",
134
+ "labelItalic",
108
135
  "tooltip",
109
136
  "visible",
110
137
  ] as const),
@@ -134,7 +161,12 @@ const NODE_ATTRIBUTES: readonly string[] = [
134
161
  "height",
135
162
  "fixedsize",
136
163
  "regular",
164
+ "sides",
165
+ "distortion",
166
+ "skew",
167
+ "orientation",
137
168
  "penwidth",
169
+ "gradientangle",
138
170
  "fontname",
139
171
  "fontsize",
140
172
  "fontcolor",
@@ -156,8 +188,10 @@ const EDGE_ATTRIBUTES: readonly string[] = [
156
188
  "arrowtail",
157
189
  "dir",
158
190
  "arrowsize",
191
+ "fillcolor",
159
192
  "headlabel",
160
193
  "taillabel",
194
+ "xlabel",
161
195
  ];
162
196
 
163
197
  /** Attributes the reader notes as not carried. */
@@ -195,6 +229,10 @@ interface NodeContext {
195
229
  readonly cluster: boolean;
196
230
  /** The HTML strings of the file, for Graphviz's shared string pool. */
197
231
  readonly html: ReadonlySet<string>;
232
+ /** Whether the graph's `rankdir` is LR or RL, which turns a record's fields top to bottom. */
233
+ readonly flip: boolean;
234
+ /** The graph's `quantum` in points (a node's padded label box is rounded up to a multiple of it), 0 for none. */
235
+ readonly quantum: number;
198
236
  }
199
237
 
200
238
  /** What an edge's label escapes depend on. */
@@ -335,13 +373,255 @@ function labelText(
335
373
  return dotLabelText(record ? recordLabelText(raw, at) : raw, names, at);
336
374
  }
337
375
 
376
+ /**
377
+ * Reads how a plain label's lines line up (`\l`, `\r`); an HTML-like or a record label lines up its own way.
378
+ * @param raw - the label as the file gives it
379
+ * @param names - what the label's escapes stand for
380
+ * @param html - the file's HTML strings
381
+ * @param record - whether the node is a record
382
+ * @param put - receives the value
383
+ */
384
+ function readLabelAlign(
385
+ raw: string,
386
+ names: DotEscapeNames,
387
+ html: ReadonlySet<string>,
388
+ record: boolean,
389
+ put: Put,
390
+ ): void {
391
+ if (isHtmlString(raw) || html.has(`<${raw}>`)) {
392
+ // each <BR ALIGN> lines up the line before it: one alignment on every break lines up the label
393
+ const aligns = new Set(
394
+ [...raw.matchAll(/<br\b([^>]*)>/gi)].map(
395
+ (m) => /\balign\s*=\s*["']?(left|right|center)/i.exec(m[1])?.[1].toLowerCase() ?? "center",
396
+ ),
397
+ );
398
+ // words after the last break are a line of their own, centered
399
+ const body = isHtmlString(raw) ? raw.slice(1, -1) : raw;
400
+ const last = /<br\b[^>]*>(?![\s\S]*<br\b)/i.exec(body);
401
+ const tail = last === null ? "" : body.slice(last.index + last[0].length);
402
+ if (stripTags(tail).trim() !== "") {
403
+ aligns.add("center");
404
+ }
405
+ const [only] = aligns;
406
+ if (aligns.size === 1 && only !== "center") {
407
+ put("labelTextAlign", only);
408
+ }
409
+ return;
410
+ }
411
+ if (record) {
412
+ return;
413
+ }
414
+ const align = dotLabelAlign(raw, names);
415
+ if (align !== null) {
416
+ put("labelTextAlign", align);
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Text with its tags (`<` to the next `>`) dropped; a `<` with no `>` after it, and the rest, kept.
422
+ * @param text - the text
423
+ * @returns the text without tags
424
+ */
425
+ function stripTags(text: string): string {
426
+ let out = "";
427
+ let i = 0;
428
+ while (i < text.length) {
429
+ const open = text.indexOf("<", i);
430
+ const close = open < 0 ? -1 : text.indexOf(">", open + 1);
431
+ if (close < 0) {
432
+ return out + text.slice(i);
433
+ }
434
+ out += text.slice(i, open);
435
+ i = close + 1;
436
+ }
437
+ return out;
438
+ }
439
+
440
+ /**
441
+ * Reads the font runs of an HTML-like label (`<B>`, `<I>`, `<FONT COLOR FACE POINT-SIZE>`) as Graphviz draws them:
442
+ * one run in a font of its own sets the label's own font (color, size, family, weight, slant), several are
443
+ * `labelRuns`. Underline, overline, strike-through, `<SUB>` and `<SUP>` have no neutral slot and draw plain.
444
+ * @param raw - the label as the file gives it
445
+ * @param html - the file's HTML strings
446
+ * @param scheme - the element's color scheme
447
+ * @param put - receives the values
448
+ * @param at - the sites
449
+ */
450
+ function readHtmlRuns(raw: string, html: ReadonlySet<string>, scheme: string | null, put: Put, at: At): void {
451
+ if (!isHtmlString(raw) && !html.has(`<${raw}>`)) {
452
+ return;
453
+ }
454
+ const markup = isHtmlString(raw) ? raw : `<${raw}>`;
455
+ const runs: Record<string, StyleValue>[] = [];
456
+ for (const span of htmlLabelSpans(markup)) {
457
+ const run = { text: span.text, ...htmlRunFont(span.tags, scheme, at("labelRuns")) };
458
+ const last = runs.at(-1);
459
+ if (last !== undefined && JSON.stringify({ ...last, text: "" }) === JSON.stringify({ ...run, text: "" })) {
460
+ last.text = `${String(last.text)}${run.text}`;
461
+ } else {
462
+ runs.push(run);
463
+ }
464
+ }
465
+ trimBlankRuns(runs);
466
+ if (runs.every((r) => Object.keys(r).length === 1)) {
467
+ return;
468
+ }
469
+ if (runs.length === 1) {
470
+ const { text: _text, ...font } = runs[0];
471
+ for (const [property, value] of Object.entries(font)) {
472
+ put(RUN_PROPERTY[property], value);
473
+ }
474
+ return;
475
+ }
476
+ runs[0].text = String(runs[0].text).trimStart();
477
+ const end = runs.at(-1);
478
+ if (end !== undefined) {
479
+ end.text = String(end.text).trimEnd();
480
+ }
481
+ put("labelRuns", JSON.stringify(runs));
482
+ }
483
+
484
+ /**
485
+ * Drops the blank runs at a label's ends, as htmlLabelText trims its blanks.
486
+ * @param runs - the runs, trimmed in place
487
+ */
488
+ function trimBlankRuns(runs: Record<string, StyleValue>[]): void {
489
+ while (runs.length > 0 && String(runs[0].text).trim() === "") {
490
+ runs.shift();
491
+ }
492
+ while (runs.length > 0 && String(runs.at(-1)?.text).trim() === "") {
493
+ runs.pop();
494
+ }
495
+ }
496
+
497
+ /** The label property a run's own font field sets when the whole label is one run. */
498
+ const RUN_PROPERTY: Readonly<Record<string, string>> = {
499
+ weight: "labelWeight",
500
+ italic: "labelItalic",
501
+ font: "labelFont",
502
+ size: "labelSize",
503
+ color: "labelColor",
504
+ };
505
+
506
+ /**
507
+ * The font of a run inside HTML font tags, as `labelRuns` fields: bold, italic, and the innermost `FONT`'s face,
508
+ * point size and color.
509
+ * @param tags - the open tags, outermost first
510
+ * @param scheme - the element's color scheme
511
+ * @param site - where a bad color is noted
512
+ * @returns the fields that differ from the label's own font
513
+ */
514
+ function htmlRunFont(tags: readonly string[], scheme: string | null, site: CanonSite): Record<string, StyleValue> {
515
+ const out: Record<string, StyleValue> = {};
516
+ for (const tag of tags) {
517
+ const name = /^<(\w+)/.exec(tag)?.[1].toLowerCase();
518
+ if (name === "b") {
519
+ out.weight = "bold";
520
+ } else if (name === "i") {
521
+ out.italic = true;
522
+ } else if (name === "font") {
523
+ for (const [attr, value] of tagAttributes(tag)) {
524
+ readFontAttribute(attr.toLowerCase(), value, scheme, site, out);
525
+ }
526
+ }
527
+ }
528
+ return out;
529
+ }
530
+
531
+ /**
532
+ * Reads one attribute of a `FONT` tag into a run's font fields: its face, point size or color.
533
+ * @param attr - the attribute's name, lowercase
534
+ * @param value - its value
535
+ * @param scheme - the element's color scheme
536
+ * @param site - where a bad color is noted
537
+ * @param out - the run's font fields, set
538
+ */
539
+ function readFontAttribute(
540
+ attr: string,
541
+ value: string,
542
+ scheme: string | null,
543
+ site: CanonSite,
544
+ out: Record<string, StyleValue>,
545
+ ): void {
546
+ if (attr === "face" && value.trim() !== "" && value.length <= MAX_STYLE_VALUE_LENGTH) {
547
+ const drawn = graphvizFont(value);
548
+ out.font = drawn.family;
549
+ if (drawn.bold) {
550
+ out.weight = "bold";
551
+ }
552
+ if (drawn.italic) {
553
+ out.italic = true;
554
+ }
555
+ } else if (attr === "point-size" && Number(value) > 0) {
556
+ out.size = Number(value);
557
+ } else if (attr === "color") {
558
+ const c = canonColor(value, "graphviz", site, { scheme, use: "text" });
559
+ if (c !== null) {
560
+ out.color = c.color;
561
+ }
562
+ }
563
+ }
564
+
565
+ /** Whether a character can be part of an HTML attribute's name (a word character or `-`). */
566
+ const ATTRIBUTE_NAME_CHAR = /^[\w-]$/;
567
+
568
+ /**
569
+ * The quoted attributes of a tag, `name = "value"` or `name = 'value'`, in order; an unquoted or unclosed value is
570
+ * skipped. Scans the tag once (an attribute regex would backtrack on a long name with no `=`).
571
+ * @param tag - the tag
572
+ * @returns each attribute's name and value
573
+ */
574
+ function tagAttributes(tag: string): [string, string][] {
575
+ const attributes: [string, string][] = [];
576
+ let i = 0;
577
+ while (i < tag.length) {
578
+ if (!ATTRIBUTE_NAME_CHAR.test(tag[i])) {
579
+ i++;
580
+ continue;
581
+ }
582
+ let end = i;
583
+ while (end < tag.length && ATTRIBUTE_NAME_CHAR.test(tag[end])) {
584
+ end++;
585
+ }
586
+ const value = quotedValueAt(tag, end);
587
+ if (value !== null) {
588
+ attributes.push([tag.slice(i, end), value.text]);
589
+ }
590
+ i = value === null ? end : value.next;
591
+ }
592
+ return attributes;
593
+ }
594
+
595
+ /**
596
+ * The quoted value of an attribute whose name ends at `at`: blanks, `=`, blanks, then text between matching quotes.
597
+ * @param tag - the tag
598
+ * @param at - where the name ends
599
+ * @returns the value and where reading goes on after its closing quote, or null when there is none
600
+ */
601
+ function quotedValueAt(tag: string, at: number): { text: string; next: number } | null {
602
+ let i = at;
603
+ while (i < tag.length && /\s/.test(tag[i])) {
604
+ i++;
605
+ }
606
+ if (tag[i] !== "=") {
607
+ return null;
608
+ }
609
+ i++;
610
+ while (i < tag.length && /\s/.test(tag[i])) {
611
+ i++;
612
+ }
613
+ const quote = tag[i];
614
+ const close = quote === '"' || quote === "'" ? tag.indexOf(quote, i + 1) : -1;
615
+ return close < 0 ? null : { text: tag.slice(i + 1, close), next: close + 1 };
616
+ }
617
+
338
618
  /**
339
619
  * Reads a color into a property and its alpha into the property's opacity.
340
620
  * @param raw - the color
341
621
  * @param scheme - the object's color scheme
342
622
  * @param use - how Graphviz draws it
343
623
  * @param property - the color property
344
- * @param opacity - its opacity property, or null when the alpha is dropped
624
+ * @param opacity - its opacity property, or null when the alpha is dropped (an outline's is its borderOpacity)
345
625
  * @param put - receives the values
346
626
  * @param at - the sites
347
627
  * @returns the alpha, or null when the color is absent
@@ -349,7 +629,7 @@ function labelText(
349
629
  function readColor(
350
630
  raw: string | null,
351
631
  scheme: string | null,
352
- use: "line" | "fill" | "edge",
632
+ use: "line" | "text" | "fill" | "edge",
353
633
  property: string,
354
634
  opacity: string | null,
355
635
  put: Put,
@@ -363,10 +643,13 @@ function readColor(
363
643
  return null;
364
644
  }
365
645
  put(property, c.color);
366
- if (opacity !== null) {
646
+ if (property === "borderColor") {
647
+ // a transparent outline is no outline (the caller sets its width to 0); a part-transparent one is drawn so
648
+ put("borderOpacity", c.alpha > 0 && c.alpha < 1 ? c.alpha : null);
649
+ } else if (opacity !== null) {
367
650
  put(opacity, c.alpha);
368
- } else if (c.alpha > 0 || property !== "borderColor") {
369
- // a transparent outline is no outline (the caller sets its width to 0); any other alpha is dropped, noted
651
+ } else {
652
+ // any other alpha is dropped, noted
370
653
  colorOpacity(c, null, at(property), false);
371
654
  }
372
655
  return c.alpha;
@@ -423,15 +706,23 @@ function readCommon(
423
706
  put: Put,
424
707
  at: At,
425
708
  ): void {
426
- if (tokens.includes("invis")) {
709
+ // Graphviz takes `invisible` for `invis` too (emit.c `parse_style`)
710
+ if (tokens.includes("invis") || tokens.includes("invisible")) {
427
711
  put("visible", false);
428
712
  }
429
- readColor(nonEmpty(get("fontcolor")), scheme, "line", "labelColor", null, put, at);
713
+ // a part-transparent font color draws its text so (`#377eb880`); an opaque one needs no opacity
714
+ const putFont: Put = (property, value) => {
715
+ put(property, property === "labelOpacity" && value === 1 ? null : value);
716
+ };
717
+ readColor(nonEmpty(get("fontcolor")), scheme, "text", "labelColor", "labelOpacity", putFont, at);
430
718
  const size = get("fontsize");
431
719
  put("labelSize", size === null ? null : canonNumber(size, at("labelSize")));
432
720
  const font = get("fontname");
433
721
  if (font !== null && font.length <= MAX_STYLE_VALUE_LENGTH) {
434
- put("labelFont", font);
722
+ const drawn = graphvizFont(font);
723
+ put("labelFont", drawn.family);
724
+ put("labelWeight", drawn.bold ? "bold" : null);
725
+ put("labelItalic", drawn.italic ? true : null);
435
726
  }
436
727
  const tip = get("tooltip") ?? get("edgetooltip");
437
728
  if (tip !== null) {
@@ -456,6 +747,143 @@ interface NodeRead {
456
747
  readonly color: string | null;
457
748
  }
458
749
 
750
+ /**
751
+ * Reads a node's label and, for a node that is not a cluster, its size and how it grows to hold the label.
752
+ * @param n - the node
753
+ * @param names - what the label's escapes stand for
754
+ */
755
+ function readNodeLabelAndSize(n: NodeRead, names: DotEscapeNames): void {
756
+ const { get, cx, put, at, shape } = n;
757
+ // label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
758
+ const label = n.point ? null : get(LABEL_ATTRIBUTE);
759
+ const shown = label === null ? cx.name : labelText(label, names, cx.html, RECORD.has(shape), at("label"));
760
+ readNodeLabel(n, label, shown, names);
761
+ // plain is its label alone at size 0: with an empty label Graphviz draws nothing, filled or not; an HTML label
762
+ // with no words (a table holding an IMG) still draws its cells
763
+ const htmlLabel = label !== null && (isHtmlString(label) || cx.html.has(`<${label}>`));
764
+ if (shape === "plain" && shown === "" && !htmlLabel) {
765
+ put("visible", false);
766
+ }
767
+ if (cx.cluster) {
768
+ readClusterBox(get, put);
769
+ } else {
770
+ readNodeSize(n, label, shown, htmlLabel, names);
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Reads a node's label, its alignment and runs, and where it sits.
776
+ * @param n - the node
777
+ * @param label - its label, or null when it has none
778
+ * @param shown - the text it shows
779
+ * @param names - what the label's escapes stand for
780
+ */
781
+ function readNodeLabel(n: NodeRead, label: string | null, shown: string, names: DotEscapeNames): void {
782
+ const { get, cx, put, at, shape } = n;
783
+ // a cluster has no label of its own unless one is given, so an empty one is none
784
+ if (label !== null && !(cx.cluster && label === "")) {
785
+ put("label", shown);
786
+ readLabelAlign(label, names, cx.html, RECORD.has(shape), put);
787
+ readHtmlRuns(label, cx.html, n.scheme, put, at);
788
+ if (cx.cluster) {
789
+ put("labelPosition", clusterLabelPosition(get));
790
+ readClusterLabelPlace(get, put);
791
+ }
792
+ }
793
+ if (!cx.cluster && !n.point) {
794
+ const position = nodeLabelPosition(get);
795
+ put("labelPosition", position === "center" ? null : position);
796
+ }
797
+ }
798
+
799
+ /**
800
+ * 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
801
+ * label's fit).
802
+ * @param n - the node
803
+ * @param label - its label, or null when it has none
804
+ * @param shown - the text it shows
805
+ * @param htmlLabel - whether the label is HTML-like
806
+ * @param names - what the label's escapes stand for
807
+ */
808
+ function readNodeSize(
809
+ n: NodeRead,
810
+ label: string | null,
811
+ shown: string,
812
+ htmlLabel: boolean,
813
+ names: DotEscapeNames,
814
+ ): void {
815
+ const { get, put, at, shape } = n;
816
+ readSize(get, shape, n.point, put, at);
817
+ if (RECORD.has(shape) && label !== null && !htmlLabel) {
818
+ readRecord(n, label, names);
819
+ } else if (!n.point) {
820
+ readLabelFit(get, shape, label, shown, put);
821
+ readQuantum(n, label);
822
+ }
823
+ }
824
+
825
+ /** Graphviz's default node width and height, in points (0.75 and 0.5 inch). */
826
+ const DEFAULT_NODE_SIZE = { w: 54, h: 36 } as const;
827
+
828
+ /**
829
+ * Reads a record node as Graphviz lays it out: its size (at least its fields' unless `fixedsize`), the dividers
830
+ * between its fields as lines over the node, and each field's text lines where Graphviz centers or justifies them,
831
+ * in place of one label.
832
+ * @param n - the node
833
+ * @param label - its record label
834
+ * @param names - what the label's escapes stand for
835
+ */
836
+ function readRecord(n: NodeRead, label: string, names: DotEscapeNames): void {
837
+ const { get, put, at, cx } = n;
838
+ const pt = (attr: string, fallback: number): number => {
839
+ const v = inches(get(attr), at(attr === "width" ? "width" : "height"));
840
+ return v === null ? fallback : v * DOT_POINTS_PER_INCH;
841
+ };
842
+ const size = Number(get("fontsize") ?? 14);
843
+ const lay = layoutRecord(
844
+ label,
845
+ !cx.flip,
846
+ pt("width", DEFAULT_NODE_SIZE.w),
847
+ pt("height", DEFAULT_NODE_SIZE.h),
848
+ isTrue(get("fixedsize")),
849
+ Number.isFinite(size) && size > 0 ? size : 14,
850
+ (line) => dotLabelText(line, names),
851
+ );
852
+ const hw = lay.width / 2;
853
+ const hh = lay.height / 2;
854
+ const fx = (x: number): string => String(num6(hw > 0 ? x / hw : 0));
855
+ const fy = (y: number): string => String(num6(hh > 0 ? y / hh : 0));
856
+ put("width", num6(lay.width));
857
+ put("height", num6(lay.height));
858
+ put("label", "");
859
+ put(
860
+ "shapeLines",
861
+ lay.dividers.length === 0
862
+ ? null
863
+ : lay.dividers.map(([x0, y0, x1, y1]) => `${fx(x0)},${fy(y0)} ${fx(x1)},${fy(y1)}`).join("; "),
864
+ );
865
+ put(
866
+ "shapeTexts",
867
+ lay.texts.length === 0
868
+ ? null
869
+ : JSON.stringify(lay.texts.map((t) => [Number(fx(t.x)), Number(fy(t.y)), t.text])),
870
+ );
871
+ }
872
+
873
+ /**
874
+ * Reads a laid-out cluster's size from its `bb` ("llx,lly,urx,ury" in points), the box Graphviz draws round its
875
+ * members and its label; a cluster with no `bb` is drawn round its members.
876
+ * @param get - the cluster's attributes
877
+ * @param put - receives the values
878
+ */
879
+ function readClusterBox(get: Attr, put: Put): void {
880
+ const parts = (get("bb") ?? "").split(",").map(Number);
881
+ if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] >= parts[0] && parts[3] >= parts[1]) {
882
+ put("width", num6(parts[2] - parts[0]));
883
+ put("height", num6(parts[3] - parts[1]));
884
+ }
885
+ }
886
+
459
887
  /**
460
888
  * Reads one node, or one cluster's container, the way Graphviz draws it.
461
889
  * @param get - its attributes
@@ -487,21 +915,72 @@ function readNode(get: Attr, cx: NodeContext, put: Put, at: At): void {
487
915
  } else {
488
916
  readNodeFill(n);
489
917
  }
918
+ readNodeBands(n);
490
919
  readNodeOutline(n);
491
920
  readNodeShape(n);
492
- // label: a node shows its name unless it has a label (the tool-default rule); a cluster and a point show none
493
- const label = n.point ? null : get(LABEL_ATTRIBUTE);
494
- if (label !== null) {
495
- put("label", labelText(label, names, cx.html, RECORD.has(shape), at("label")));
921
+ readNodeLabelAndSize(n, names);
922
+ if (nonEmpty(get("xlabel")) !== null) {
923
+ noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
496
924
  }
497
- if (!cx.cluster) {
498
- readSize(get, shape, n.point, label ?? (n.point ? null : cx.name), put, at);
925
+ }
926
+
927
+ /**
928
+ * Where a node's label sits: its `labelloc` puts it against the top or bottom of the node's box, inside it; it is
929
+ * centered by default.
930
+ * @param get - the node's attributes
931
+ * @returns the position
932
+ */
933
+ function nodeLabelPosition(get: Attr): "inside-top" | "inside-bottom" | "center" {
934
+ const loc = (get("labelloc") ?? "").trim().toLowerCase();
935
+ if (loc.startsWith("t")) {
936
+ return "inside-top";
499
937
  }
500
- if (Number(get("peripheries")) > 1 || nonEmpty(get("xlabel")) !== null) {
501
- noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, nonEmpty(get("xlabel")) === null ? "peripheries" : "xlabel");
938
+ return loc.startsWith("b") ? "inside-bottom" : "center";
939
+ }
940
+
941
+ /** How far in from the cluster's side Graphviz draws a label justified left or right: half its label's padding. */
942
+ const CLUSTER_LABEL_INSET = 8;
943
+
944
+ /**
945
+ * Where Graphviz draws a cluster's label beyond its position: at a stored `lp` (its center) inside the cluster's
946
+ * `bb`, else `CLUSTER_LABEL_INSET` points in from the side `labeljust` puts it against.
947
+ * @param get - the cluster's attributes
948
+ * @param put - receives the values
949
+ */
950
+ function readClusterLabelPlace(get: Attr, put: Put): void {
951
+ const lp = (get("lp") ?? "").split(",").map(Number);
952
+ const bb = (get("bb") ?? "").split(",").map(Number);
953
+ if (lp.length === 2 && bb.length === 4 && [...lp, ...bb].every(Number.isFinite)) {
954
+ put("labelPosition", "center");
955
+ put("labelOffsetX", num6(lp[0] - (bb[0] + bb[2]) / 2));
956
+ put("labelOffsetY", num6((bb[1] + bb[3]) / 2 - lp[1]));
957
+ return;
958
+ }
959
+ const just = (get("labeljust") ?? "").trim().toLowerCase();
960
+ if (just.startsWith("l")) {
961
+ put("labelOffsetX", CLUSTER_LABEL_INSET);
962
+ } else if (just.startsWith("r")) {
963
+ put("labelOffsetX", -CLUSTER_LABEL_INSET);
502
964
  }
503
965
  }
504
966
 
967
+ /**
968
+ * Where a cluster's label sits: inside the cluster's box (Graphviz grows the box to hold it), at the top unless
969
+ * `labelloc` is `b`, centered unless `labeljust` is `l` or `r`.
970
+ * @param get - the cluster's attributes
971
+ * @returns the position
972
+ */
973
+ function clusterLabelPosition(
974
+ get: Attr,
975
+ ): `inside-${"top" | "top-left" | "top-right" | "bottom" | "bottom-left" | "bottom-right"}` {
976
+ const loc = (get("labelloc") ?? "").trim().toLowerCase().startsWith("b") ? "inside-bottom" : "inside-top";
977
+ const just = (get("labeljust") ?? "").trim().toLowerCase();
978
+ if (just.startsWith("l")) {
979
+ return `${loc}-left`;
980
+ }
981
+ return just.startsWith("r") ? `${loc}-right` : loc;
982
+ }
983
+
505
984
  /**
506
985
  * Reads a cluster's fill: its fill color when filled, else its background.
507
986
  * @param n - the cluster
@@ -512,6 +991,53 @@ function readClusterFill(n: NodeRead): void {
512
991
  const bg = nonEmpty(get("bgcolor"));
513
992
  const raw = filled ? (nonEmpty(get("fillcolor")) ?? n.color ?? bg ?? "lightgrey") : bg;
514
993
  readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
994
+ if (!tokens.includes("striped")) {
995
+ readFillGradient(n, raw);
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * Reads a gradient fill (`fillcolor="a:b"`, `gradientangle`): Graphviz runs it from the list's first color to
1001
+ * its second, `gradientangle` degrees counter-clockwise from left to right (0 by default), which is the neutral
1002
+ * angle's clockwise turn negated, from 0 up to 360. `style=radial` runs it out from the middle instead. A second color
1003
+ * left out is black; colors after the second are not drawn. A first color's `;fraction` makes Graphviz switch colors
1004
+ * at that fraction, which its SVG writes as stops at the fraction less 0.001 and at the fraction.
1005
+ * @param n - the node or cluster
1006
+ * @param raw - its fill color value
1007
+ */
1008
+ function readFillGradient(n: NodeRead, raw: string | null): void {
1009
+ const colon = raw === null ? -1 : raw.indexOf(":");
1010
+ if (raw === null || colon < 0) {
1011
+ return;
1012
+ }
1013
+ const radial = n.tokens.includes("radial");
1014
+ if (radial) {
1015
+ // ponytail: a radial gradientangle moves Graphviz's focus off the middle; graphty's radial ramp has no focus
1016
+ n.put("fillGradientKind", "radial");
1017
+ }
1018
+ const frac = Number(raw.slice(0, colon).split(";")[1]);
1019
+ if (frac > 0 && frac < 1) {
1020
+ n.put("fillGradientStops", `${String(num6(frac - 0.001))} ${String(num6(frac))}`);
1021
+ }
1022
+ const second = raw
1023
+ .slice(colon + 1)
1024
+ .split(":")[0]
1025
+ .split(";")[0];
1026
+ const c = canonColor(second === "" ? "black" : second, "graphviz", n.at("fillGradient"), {
1027
+ scheme: n.scheme,
1028
+ use: "fill",
1029
+ });
1030
+ if (c === null) {
1031
+ return;
1032
+ }
1033
+ n.put("fillGradient", c.color);
1034
+ if (radial) {
1035
+ return;
1036
+ }
1037
+ const angle = Number(nonEmpty(n.get("gradientangle")) ?? "0");
1038
+ // 0 up to 360: the neutral angle turns clockwise on screen, Graphviz's counter-clockwise
1039
+ const turned = Number.isFinite(angle) ? ((-angle % 360) + 360) % 360 : 0;
1040
+ n.put("fillGradientAngle", num6(turned) + 0);
515
1041
  }
516
1042
 
517
1043
  /**
@@ -519,9 +1045,104 @@ function readClusterFill(n: NodeRead): void {
519
1045
  * @param n - the node
520
1046
  */
521
1047
  function readNodeFill(n: NodeRead): void {
522
- const { shape, point } = n;
1048
+ const { point } = n;
523
1049
  const fillcolor = nonEmpty(n.get("fillcolor"));
524
- const filled =
1050
+ if (isFilled(n)) {
1051
+ const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
1052
+ readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
1053
+ // stripes on a box and wedges on an ellipse take the color list in place of a gradient
1054
+ const banded =
1055
+ (n.tokens.includes("striped") && BOX_LIKE.has(n.shape)) ||
1056
+ (n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape));
1057
+ if ((n.tokens.includes("filled") || n.tokens.includes("radial")) && !banded) {
1058
+ readFillGradient(n, raw);
1059
+ }
1060
+ } else if (fillcolor !== null) {
1061
+ noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
1062
+ }
1063
+ }
1064
+
1065
+ /**
1066
+ * Reads a node's striped or wedged fill (`style=striped` on a box, `style=wedged` on an ellipse) from its color
1067
+ * list, `color;fraction` separated by `:`: a color with no fraction shares what the others leave, the last color takes
1068
+ * what sized colors leave when none is unsized, and the shares stop at the whole
1069
+ * (https://graphviz.org/docs/attr-types/colorList/). Graphviz draws stripes left to right and
1070
+ * wedges counter-clockwise from 3 o'clock, which is the same pie as the wedges in reverse order clockwise from there.
1071
+ * @param n - the node
1072
+ */
1073
+ function readNodeBands(n: NodeRead): void {
1074
+ // a cluster is a box, so it takes stripes too (and never wedges)
1075
+ const striped = n.tokens.includes("striped") && (n.cx.cluster || BOX_LIKE.has(n.shape));
1076
+ const wedged = !n.cx.cluster && n.tokens.includes("wedged") && ELLIPSE_LIKE.has(n.shape);
1077
+ const list = nonEmpty(n.get("fillcolor")) ?? n.color;
1078
+ if ((!striped && !wedged) || !list?.includes(":")) {
1079
+ return;
1080
+ }
1081
+ const bands = colorBands(list, n.scheme, "fill", n.at(striped ? "stripeColors" : "pieColors"));
1082
+ if (bands.length === 0) {
1083
+ return;
1084
+ }
1085
+ const order = striped ? bands : [...bands].reverse();
1086
+ const prefix = striped ? "stripe" : "pie";
1087
+ n.put(`${prefix}Colors`, order.map((b) => b.color).join(" "));
1088
+ n.put(`${prefix}Sizes`, order.map((b) => String(b.size)).join(" "));
1089
+ if (striped) {
1090
+ n.put("stripeDirection", "right");
1091
+ } else {
1092
+ n.put("pieFitBox", true);
1093
+ n.put("pieRotation", 90);
1094
+ }
1095
+ }
1096
+
1097
+ /**
1098
+ * The bands of a Graphviz color list (`color;fraction` separated by `:`), in order: a color with no fraction shares
1099
+ * what the others leave, the last color takes what sized colors leave when none is unsized, and the shares stop at
1100
+ * the whole (https://graphviz.org/docs/attr-types/colorList/, emit.c `parseSegs`).
1101
+ * @param list - the color list
1102
+ * @param scheme - the color scheme
1103
+ * @param use - how Graphviz draws the colors
1104
+ * @param site - where it was read
1105
+ * @returns each band's color and share of the whole
1106
+ */
1107
+ function colorBands(
1108
+ list: string,
1109
+ scheme: string | null,
1110
+ use: "fill" | "edge",
1111
+ site: CanonSite,
1112
+ ): { color: string; size: number }[] {
1113
+ const parts = list.split(":").map((part) => {
1114
+ const [color, fraction] = part.split(";");
1115
+ const c = canonColor(color.trim(), "graphviz", site, { scheme, use });
1116
+ const f = fraction === undefined ? Number.NaN : Number(fraction);
1117
+ return { color: c?.color ?? null, fraction: Number.isFinite(f) && f >= 0 ? f : null };
1118
+ });
1119
+ const kept = parts.flatMap((p) => (p.color === null ? [] : [{ color: p.color, fraction: p.fraction }]));
1120
+ const given = kept.reduce((sum, p) => sum + (p.fraction ?? 0), 0);
1121
+ const unsized = kept.filter((p) => p.fraction === null).length;
1122
+ const share = unsized > 0 ? Math.max(0, 1 - given) / unsized : 0;
1123
+ let used = 0;
1124
+ const bands = kept.flatMap((p) => {
1125
+ const size = Math.min(p.fraction ?? share, 1 - used);
1126
+ used += Math.max(size, 0);
1127
+ return size > 0 ? [{ color: p.color, size: num6(size) }] : [];
1128
+ });
1129
+ // every color sized and short of the whole: Graphviz gives what is left to the last one (emit.c parseSegs)
1130
+ const last = bands.at(-1);
1131
+ if (last !== undefined && unsized === 0 && used < 1) {
1132
+ last.size = num6(last.size + 1 - used);
1133
+ }
1134
+ return bands;
1135
+ }
1136
+
1137
+ /**
1138
+ * Whether Graphviz fills a node: a point always; `filled` and `radial` any shape, `striped` a box and `wedged` an
1139
+ * ellipse only.
1140
+ * @param n - the node
1141
+ * @returns true when its box or shape is filled
1142
+ */
1143
+ function isFilled(n: NodeRead): boolean {
1144
+ const { shape, point } = n;
1145
+ return (
525
1146
  point ||
526
1147
  n.tokens.some(
527
1148
  (t) =>
@@ -529,13 +1150,8 @@ function readNodeFill(n: NodeRead): void {
529
1150
  t === "radial" ||
530
1151
  (t === "striped" && BOX_LIKE.has(shape)) ||
531
1152
  (t === "wedged" && ELLIPSE_LIKE.has(shape)),
532
- );
533
- if (filled) {
534
- const raw = fillcolor ?? n.color ?? (point ? "black" : "lightgrey");
535
- readColor(raw, n.scheme, "fill", "fill", "fillOpacity", n.put, n.at);
536
- } else if (fillcolor !== null) {
537
- noteAt(n.at("fill"), STYLE_ISSUE.UNFILLED, fillcolor.slice(0, MAX_STYLE_VALUE_LENGTH));
538
- }
1153
+ )
1154
+ );
539
1155
  }
540
1156
 
541
1157
  /**
@@ -550,7 +1166,8 @@ function readNodeOutline(n: NodeRead): void {
550
1166
  const outline = cx.cluster ? (nonEmpty(get("pencolor")) ?? n.color) : n.color;
551
1167
  const alpha = readColor(outline, n.scheme, "line", "borderColor", null, put, at);
552
1168
  const width = nonEmpty(get("penwidth")) ?? setLineWidth(tokens);
553
- if (alpha === 0) {
1169
+ // peripheries=0 draws no outline, on a node and on a cluster alike
1170
+ if (alpha === 0 || Number(nonEmpty(get("peripheries")) ?? "1") === 0) {
554
1171
  put("borderWidth", 0);
555
1172
  } else if (width !== null) {
556
1173
  put("borderWidth", penWidth(width, get("penwidth") !== null, at("borderWidth")));
@@ -558,29 +1175,336 @@ function readNodeOutline(n: NodeRead): void {
558
1175
  put("borderWidth", 2);
559
1176
  }
560
1177
  }
561
- if (tokens.includes("dashed")) {
562
- put("borderStyle", "dashed");
563
- } else if (tokens.includes("dotted")) {
564
- put("borderStyle", "dotted");
1178
+ if (tokens.includes("dashed")) {
1179
+ put("borderStyle", "dashed");
1180
+ } else if (tokens.includes("dotted")) {
1181
+ put("borderStyle", "dotted");
1182
+ }
1183
+ if (!cx.cluster) {
1184
+ readNodeRings(n);
1185
+ }
1186
+ }
1187
+
1188
+ /** The outlines Graphviz draws round a node of these shapes when `peripheries` is not set. */
1189
+ export const SHAPE_PERIPHERIES: ReadonlyMap<string, number> = new Map([
1190
+ ["doublecircle", 2],
1191
+ ["doubleoctagon", 2],
1192
+ ["tripleoctagon", 3],
1193
+ ]);
1194
+
1195
+ /** How far apart Graphviz draws a node's outlines, center to center (`GAP`, lib/common/shapes.c). */
1196
+ export const PERIPHERY_GAP = 4;
1197
+
1198
+ /**
1199
+ * Reads a node's second outline (`peripheries=2`, `doublecircle`): Graphviz draws it `PERIPHERY_GAP` points outside
1200
+ * the first, in the same color, width and dashes. graphty draws one outer ring, so a third and later outline is
1201
+ * noted as not carried.
1202
+ * @param n - the node
1203
+ */
1204
+ function readNodeRings(n: NodeRead): void {
1205
+ const raw = nonEmpty(n.get("peripheries"));
1206
+ const count = raw === null ? (SHAPE_PERIPHERIES.get(n.shape) ?? 1) : Math.trunc(Number(raw));
1207
+ if (Number.isNaN(count) || count <= 1 || NO_OUTLINE.has(n.shape)) {
1208
+ return;
1209
+ }
1210
+ const c =
1211
+ n.color === null
1212
+ ? null
1213
+ : canonColor(n.color, "graphviz", n.at("outerBorderColor"), {
1214
+ scheme: n.scheme,
1215
+ use: "line",
1216
+ });
1217
+ if (c !== null && c.alpha === 0) {
1218
+ return;
1219
+ }
1220
+ const widthRaw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
1221
+ const parsed = widthRaw === null ? Number.NaN : Number(widthRaw);
1222
+ const unset = n.tokens.includes("bold") ? 2 : 1;
1223
+ const pen = Number.isFinite(parsed) ? Math.max(0, parsed) : unset;
1224
+ n.put("outerBorderColor", c?.color ?? "#000000");
1225
+ n.put("outerBorderOpacity", c !== null && c.alpha < 1 ? c.alpha : null);
1226
+ n.put("outerBorderWidth", num6(pen));
1227
+ // the gap between the outline's outer edge and the ring's inner edge
1228
+ n.put("outerBorderOffset", num6(Math.max(0, PERIPHERY_GAP - pen)));
1229
+ if (n.tokens.includes("dashed") || n.tokens.includes("dotted")) {
1230
+ n.put("outerBorderStyle", n.tokens.includes("dashed") ? "dashed" : "dotted");
1231
+ }
1232
+ if (count > 2) {
1233
+ noteAt(n.at("outerBorderWidth"), STYLE_ISSUE.NOT_CARRIED, `peripheries=${String(count)}`);
1234
+ }
1235
+ }
1236
+
1237
+ /**
1238
+ * Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is. Graphviz draws
1239
+ * a striped box with square corners whatever `rounded` says.
1240
+ * @param n - the node
1241
+ */
1242
+ function readNodeShape(n: NodeRead): void {
1243
+ const rounded = n.tokens.includes("rounded") && !n.tokens.includes("striped");
1244
+ const turned = turnedPolygon(n);
1245
+ if (n.cx.cluster) {
1246
+ n.put("shape", rounded ? "round-rectangle" : "rectangle");
1247
+ } else if (n.shape === "polygon") {
1248
+ readPolygon(n, rounded);
1249
+ } else if (turned !== null) {
1250
+ n.put("shape", "polygon");
1251
+ n.put("shapePoints", cornersText(graphvizDrawnCorners(turned)));
1252
+ n.put("shapePointsStretch", false);
1253
+ } else if (NAMED_OUTLINES.has(n.shape)) {
1254
+ n.put("shape", "polygon");
1255
+ n.put("shapePoints", NAMED_OUTLINES.get(n.shape) ?? null);
1256
+ // a polygon shape's corners are where Graphviz draws them, one reaching past its box too
1257
+ n.put("shapePointsStretch", false);
1258
+ } else if (GRAPHVIZ_GLYPH_SHAPES.has(n.shape)) {
1259
+ const size = (name: string): number | null => {
1260
+ const v = inches(n.get(name), n.at(name));
1261
+ return v === null ? null : v * DOT_POINTS_PER_INCH;
1262
+ };
1263
+ putGlyph(
1264
+ n.shape,
1265
+ size("width"),
1266
+ size("height"),
1267
+ n.put,
1268
+ () => {
1269
+ const raw = nonEmpty(n.get("penwidth")) ?? setLineWidth(n.tokens);
1270
+ const pen = raw === null ? Number.NaN : Number(raw);
1271
+ if (Number.isFinite(pen)) {
1272
+ return Math.max(0, pen);
1273
+ }
1274
+ return n.tokens.includes("bold") ? 2 : 1;
1275
+ },
1276
+ () =>
1277
+ n.color === null
1278
+ ? null
1279
+ : (canonColor(n.color, "graphviz", n.at("outerBorderColor"), { scheme: n.scheme, use: "line" })
1280
+ ?.color ?? null),
1281
+ );
1282
+ } else if (n.shapeRaw !== null) {
1283
+ const shape =
1284
+ rounded && BOX_LIKE.has(n.shape)
1285
+ ? "round-rectangle"
1286
+ : canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape"));
1287
+ // a filled shape=none is its box filled, with no outline (its border is 0): a rectangle, not a node with no body
1288
+ n.put("shape", shape === "none" && isFilled(n) ? "rectangle" : shape);
1289
+ }
1290
+ }
1291
+
1292
+ /** The polygon shapes `orientation` turns, by their own sides and orientation (`shapes.c`). */
1293
+ const TURNABLE_POLYGONS: ReadonlyMap<string, { readonly sides: number; readonly orientation: number }> = new Map([
1294
+ ...[...BOX_LIKE].map((name) => [name, { sides: 4, orientation: 0 }] as const),
1295
+ ["diamond", { sides: 4, orientation: 45 }],
1296
+ ["hexagon", { sides: 6, orientation: 0 }],
1297
+ ["octagon", { sides: 8, orientation: 0 }],
1298
+ ]);
1299
+
1300
+ /**
1301
+ * The polygon a named polygon shape is when its `orientation` turns it off its own axes (a box turned 30 degrees),
1302
+ * or null when it is drawn as its shape: Graphviz adds the attribute to the shape's own orientation.
1303
+ * @param n - the node
1304
+ * @returns the polygon, or null
1305
+ */
1306
+ function turnedPolygon(n: NodeRead): Parameters<typeof graphvizPolygonCorners>[0] | null {
1307
+ const turn = Number(nonEmpty(n.get("orientation")) ?? "0");
1308
+ const own = TURNABLE_POLYGONS.get(n.shape) ?? GRAPHVIZ_POLYGON_SHAPES.get(n.shape);
1309
+ if (own === undefined || !Number.isFinite(turn) || turn % 360 === 0) {
1310
+ return null;
1311
+ }
1312
+ const polygon = {
1313
+ distortion: 0,
1314
+ skew: 0,
1315
+ ...own,
1316
+ orientation: own.orientation + Math.max(-360, turn),
1317
+ };
1318
+ // a quarter turn of a box is a box again
1319
+ return isGraphvizBox(polygon) ? null : polygon;
1320
+ }
1321
+
1322
+ /**
1323
+ * Reads a `shape=polygon` node: a box when it is one (four sides, no distortion or skew, square to the axes), an
1324
+ * ellipse below three sides, else its corners as Graphviz places them.
1325
+ * @param n - the node
1326
+ * @param rounded - whether its style rounds a box's corners
1327
+ */
1328
+ function readPolygon(n: NodeRead, rounded: boolean): void {
1329
+ const number = (name: string, fallback: number, least: number): number => {
1330
+ const raw = nonEmpty(n.get(name));
1331
+ const v = raw === null ? Number.NaN : Number(raw);
1332
+ return Number.isFinite(v) ? Math.max(least, v) : fallback;
1333
+ };
1334
+ const polygon = {
1335
+ sides: Math.trunc(number("sides", 4, 0)),
1336
+ distortion: number("distortion", 0, -100),
1337
+ skew: number("skew", 0, -100),
1338
+ orientation: number("orientation", 0, -360),
1339
+ };
1340
+ if (isGraphvizBox(polygon)) {
1341
+ n.put("shape", rounded ? "round-rectangle" : "rectangle");
1342
+ } else if (polygon.sides < 3) {
1343
+ n.put("shape", "ellipse");
1344
+ } else {
1345
+ n.put("shape", "polygon");
1346
+ n.put("shapePoints", cornersText(graphvizDrawnCorners(polygon)));
1347
+ n.put("shapePointsStretch", false);
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * A Graphviz polygon's corners where Graphviz draws them, as fractions of its box's half width and half height: it
1353
+ * grows a polygon that falls short of the box along an axis until it reaches the box's sides, and keeps one that
1354
+ * reaches past them (a distortion, a skew, an egg) as it is (shapes.c `poly_init`: `scalex = bb.x / xmax`).
1355
+ * @param polygon - the polygon's attributes
1356
+ * @returns the corners
1357
+ */
1358
+ function graphvizDrawnCorners(polygon: Parameters<typeof graphvizPolygonCorners>[0]): [number, number][] {
1359
+ const corners = graphvizPolygonCorners(polygon);
1360
+ const grow = (axis: 0 | 1): number => Math.max(1, 1 / Math.max(...corners.map((c) => Math.abs(c[axis]))));
1361
+ const [gx, gy] = [grow(0), grow(1)];
1362
+ return corners.map(([x, y]) => [x * gx, y * gy]);
1363
+ }
1364
+
1365
+ /**
1366
+ * Corners as a `shapePoints` value.
1367
+ * @param corners - the corners
1368
+ * @returns `x,y x,y ...`
1369
+ */
1370
+ function cornersText(corners: readonly (readonly [number, number])[]): string {
1371
+ return corners.map(([x, y]) => `${num6(x)},${num6(y)}`).join(" ");
1372
+ }
1373
+
1374
+ /** The outline of each Graphviz polygon shape graphty draws as a polygon of its corners, by shape name. */
1375
+ const NAMED_OUTLINES: ReadonlyMap<string, string> = new Map(
1376
+ [...GRAPHVIZ_POLYGON_SHAPES].map(([name, polygon]) => [name, cornersText(graphvizDrawnCorners(polygon))] as const),
1377
+ );
1378
+
1379
+ /**
1380
+ * A Graphviz shape drawn as pieces of its own (`graphvizGlyph`) at a node's size, as the neutral values that draw it:
1381
+ * a polygon of its outline drawn where it says, its further pieces (`shapeParts`) and lines (`shapeLines`), each as
1382
+ * fractions of the node's half width and half height. An `underline` is filled with no line round it, its line alone
1383
+ * drawn in the pen's width.
1384
+ * @param shape - the shape name, lowercase
1385
+ * @param width - the node's width in points, or null for Graphviz's default
1386
+ * @param height - its height, or null
1387
+ * @param put - receives the values
1388
+ * @param pen - the node's pen width, for an outline drawn as a line alone and a ring
1389
+ * @param color - the node's pen color, for a ring
1390
+ */
1391
+ export function putGlyph(
1392
+ shape: string,
1393
+ width: number | null,
1394
+ height: number | null,
1395
+ put: (property: string, value: StyleValue | null) => void,
1396
+ pen: () => number,
1397
+ color: () => string | null,
1398
+ ): void {
1399
+ const text = glyphTexts(shape, width, height);
1400
+ if (text === null) {
1401
+ return;
1402
+ }
1403
+ put("shape", "polygon");
1404
+ put("shapePoints", text.outline);
1405
+ put("shapePointsStretch", false);
1406
+ put("shapeParts", text.parts);
1407
+ put("shapeLines", text.lines);
1408
+ if (text.unstroked) {
1409
+ put("shapeLineWidth", pen());
1410
+ put("borderWidth", 0);
1411
+ }
1412
+ if (text.ring !== null) {
1413
+ // drawn in the pen, its gap the distance less a pen's width (the outline's half and its own)
1414
+ put("outerBorderColor", color() ?? "#000000");
1415
+ put("outerBorderWidth", num6(pen()));
1416
+ put("outerBorderOffset", num6(Math.max(0, text.ring - pen())));
1417
+ }
1418
+ }
1419
+
1420
+ /**
1421
+ * A Graphviz glyph's pieces as `shapePoints`, `shapeParts` and `shapeLines` text at a node's size, or null for a shape
1422
+ * that is not one.
1423
+ * @param shape - the shape name, lowercase
1424
+ * @param width - the node's width in points, or null for Graphviz's default
1425
+ * @param height - its height, or null
1426
+ * @returns the texts (parts and lines null when there are none)
1427
+ */
1428
+ function glyphTexts(
1429
+ shape: string,
1430
+ width: number | null,
1431
+ height: number | null,
1432
+ ): { outline: string; parts: string | null; lines: string | null; unstroked: boolean; ring: number | null } | null {
1433
+ const glyph = graphvizGlyph(shape, width, height);
1434
+ if (glyph === null) {
1435
+ return null;
565
1436
  }
1437
+ // fractions of the half width and half height the glyph was laid out at
1438
+ const half = (v: number | null, fallback: number): number => (v !== null && v > 0 ? v : fallback) / 2;
1439
+ const [W, H] = [half(width, 54), half(height, 36)];
1440
+ const list = (points: readonly (readonly [number, number])[]): string =>
1441
+ cornersText(points.map(([x, y]) => [x / W, y / H] as const));
1442
+ const lists = (all: readonly (readonly (readonly [number, number])[])[]): string | null =>
1443
+ all.length === 0 ? null : all.map(list).join("; ");
1444
+ return {
1445
+ outline: list(glyph.outline),
1446
+ parts: lists(glyph.parts),
1447
+ lines: lists(glyph.lines),
1448
+ unstroked: glyph.unstroked === true,
1449
+ ring: glyph.ring ?? null,
1450
+ };
566
1451
  }
567
1452
 
568
1453
  /**
569
- * Reads a node's or cluster's shape: a cluster is a rectangle, rounded by the `rounded` style as a box is.
570
- * @param n - the node
1454
+ * The corners of a Graphviz shape graphty draws as a polygon (`house`, `cylinder`, `note` ...), as a `shapePoints`
1455
+ * value, or undefined for a shape graphty draws by name.
1456
+ * @param shape - the shape name, lowercase
1457
+ * @returns the corners, or undefined
571
1458
  */
572
- function readNodeShape(n: NodeRead): void {
573
- const rounded = n.tokens.includes("rounded");
574
- if (n.cx.cluster) {
575
- n.put("shape", rounded ? "round-rectangle" : "rectangle");
576
- } else if (n.shapeRaw !== null) {
577
- n.put(
578
- "shape",
579
- rounded && BOX_LIKE.has(n.shape)
580
- ? "round-rectangle"
581
- : canonEnum("shape", n.shapeRaw, "graphviz", n.at("shape")),
582
- );
1459
+ export function graphvizOutline(shape: string): string | undefined {
1460
+ return NAMED_OUTLINES.get(shape);
1461
+ }
1462
+
1463
+ /**
1464
+ * Whether Graphviz draws a shape with no outline (`plain`, `plaintext`, `none`).
1465
+ * @param shape - the shape name, lowercase
1466
+ * @returns true for a shape with no outline
1467
+ */
1468
+ export function graphvizHasNoOutline(shape: string): boolean {
1469
+ return NO_OUTLINE.has(shape);
1470
+ }
1471
+
1472
+ /** The shape name of each outline in `NAMED_OUTLINES`, so a written node keeps its Graphviz shape. */
1473
+ const OUTLINE_NAMES: ReadonlyMap<string, string> = new Map([...NAMED_OUTLINES].map(([name, text]) => [text, name]));
1474
+
1475
+ /**
1476
+ * The Graphviz glyph shape a polygon is at a node's size, its outline, parts and lines alike, so a written node keeps
1477
+ * its shape (a ribosome site and a protease site share their outline and differ in their stem).
1478
+ * @param get - the node's values
1479
+ * @param width - the node's width in points, or null
1480
+ * @param height - its height, or null
1481
+ * @returns the shape name and whether it is filled with no line round it, or undefined
1482
+ */
1483
+ function glyphOf(
1484
+ get: Get,
1485
+ width: number | null,
1486
+ height: number | null,
1487
+ ): { name: string; unstroked: boolean } | undefined {
1488
+ const text = (property: string): string | null => {
1489
+ const v = get(property);
1490
+ return typeof v === "string" ? v.trim() : null;
1491
+ };
1492
+ const outline = text("shapePoints");
1493
+ if (outline === null) {
1494
+ return undefined;
1495
+ }
1496
+ // a glyph with another number of corners cannot match: skip writing its texts, which is most of the work
1497
+ const corners = outline === "" ? 0 : outline.split(/\s+/).length;
1498
+ for (const name of GRAPHVIZ_GLYPH_SHAPES) {
1499
+ if (graphvizGlyph(name, width, height)?.outline.length !== corners) {
1500
+ continue;
1501
+ }
1502
+ const g = glyphTexts(name, width, height);
1503
+ if (g !== null && g.outline === outline && g.parts === text("shapeParts") && g.lines === text("shapeLines")) {
1504
+ return { name, unstroked: g.unstroked };
1505
+ }
583
1506
  }
1507
+ return undefined;
584
1508
  }
585
1509
 
586
1510
  /**
@@ -601,15 +1525,14 @@ function penWidth(raw: string, attribute: boolean, site: CanonSite): number | nu
601
1525
 
602
1526
  /**
603
1527
  * Reads a node's width and height: exact where Graphviz draws them exactly (`fixedsize`, an empty label that is not
604
- * a record's, a point), otherwise minimums it grows to fit the label, noted.
1528
+ * a record's, a point), otherwise minimums it grows to fit the label (`readLabelFit`).
605
1529
  * @param get - the node's attributes
606
1530
  * @param shape - its shape, lowercase
607
1531
  * @param point - whether it is a point
608
- * @param label - the label it shows, or null
609
1532
  * @param put - receives the values
610
1533
  * @param at - the sites
611
1534
  */
612
- function readSize(get: Attr, shape: string, point: boolean, label: string | null, put: Put, at: At): void {
1535
+ function readSize(get: Attr, shape: string, point: boolean, put: Put, at: At): void {
613
1536
  let w = inches(get("width"), at("width"));
614
1537
  let h = inches(get("height"), at("height"));
615
1538
  const same = point || CIRCLE_LIKE.has(shape) || isTrue(get("regular"));
@@ -635,14 +1558,86 @@ function readSize(get: Attr, shape: string, point: boolean, label: string | null
635
1558
  } else if (same) {
636
1559
  ww = hh = Math.max(w ?? 0, h ?? 0);
637
1560
  }
638
- const exact = point || isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape));
639
- if (!exact) {
640
- noteAt(at("width"), STYLE_ISSUE.SIZE_MINIMUM, "grows to fit its label");
641
- }
642
1561
  put("width", ww === null ? null : canonNumber(ww, at("width"), DOT_POINTS_PER_INCH));
643
1562
  put("height", hh === null ? null : canonNumber(hh, at("height"), DOT_POINTS_PER_INCH));
644
1563
  }
645
1564
 
1565
+ /**
1566
+ * Reads the graph's `quantum` for a node that grows to hold its label: Graphviz rounds the padded label box up to a
1567
+ * multiple of it before fitting the outline round it (`poly_init`), so the node is at least one quantum across, an
1568
+ * ellipse's box times the square root of 2.
1569
+ * @param n - the node
1570
+ * @param label - its `label` attribute, or null
1571
+ */
1572
+ function readQuantum(n: NodeRead, label: string | null): void {
1573
+ const { get, put, shape } = n;
1574
+ let scale: number | null = BOX_LIKE.has(shape) ? 1 : null;
1575
+ if (ELLIPSE_LIKE.has(shape)) {
1576
+ scale = Math.SQRT2;
1577
+ }
1578
+ const w = Number(get("width") ?? Number.NaN);
1579
+ const h = Number(get("height") ?? Number.NaN);
1580
+ // ponytail: one quantum is the least size; a label box over one quantum is not rounded up to the next, and only
1581
+ // boxes and ellipses with their size given are read (other polygons fit their box by factors of their own)
1582
+ if (n.cx.quantum === 0 || scale === null || label === "" || isTrue(get("fixedsize")) || !(w >= 0 && h >= 0)) {
1583
+ return;
1584
+ }
1585
+ const least = n.cx.quantum * scale;
1586
+ put("width", num6(Math.max(w * DOT_POINTS_PER_INCH, least)));
1587
+ put("height", num6(Math.max(h * DOT_POINTS_PER_INCH, least)));
1588
+ }
1589
+
1590
+ /** Graphviz's padding around a node's label when it has no `margin`: 8 points left and right, 4 above and below. */
1591
+ const LABEL_PAD = { x: 8, y: 4 } as const;
1592
+
1593
+ /** Graphviz's line height, as a multiple of the font size (`LINESPACING`, lib/common/const.h). */
1594
+ const LINE_SPACING = 1.2;
1595
+
1596
+ /**
1597
+ * Reads how a node that is not drawn at a fixed size grows to hold its label (`poly_init`, lib/common/shapes.c):
1598
+ * its padded label box fits inside its outline, the `margin` (else 8 points by 4) on each side, and a circle, a
1599
+ * square or a `regular` node stays as wide as it is tall. The neutral label box is one font size per line, so the
1600
+ * rest of Graphviz's line height (1.2 font sizes) goes into the vertical padding.
1601
+ * @param get - the node's attributes
1602
+ * @param shape - its shape, lowercase
1603
+ * @param label - its `label` attribute, or null
1604
+ * @param shown - the label it shows
1605
+ * @param put - receives the values
1606
+ */
1607
+ function readLabelFit(get: Attr, shape: string, label: string | null, shown: string, put: Put): void {
1608
+ if (isTrue(get("fixedsize")) || (label === "" && !RECORD.has(shape))) {
1609
+ return;
1610
+ }
1611
+ const lines = Math.max(1, shown.replace(/\n$/, "").split("\n").length);
1612
+ const size = Number(get("fontsize") ?? 14);
1613
+ const fontSize = Number.isFinite(size) && size > 0 ? size : 14;
1614
+ const margin = (get("margin") ?? "")
1615
+ .split(",")
1616
+ .filter((text) => text.trim() !== "")
1617
+ .map(Number)
1618
+ .filter(isLength);
1619
+ // plain is plaintext with a margin of 0
1620
+ const pad = shape === "plain" ? { x: 0, y: 0 } : LABEL_PAD;
1621
+ const padX = margin.length > 0 ? margin[0] * DOT_POINTS_PER_INCH : pad.x;
1622
+ const padY = margin.length > 0 ? (margin[1] ?? margin[0]) * DOT_POINTS_PER_INCH : pad.y;
1623
+ // a decorated box holds its label as a box does, exactly
1624
+ put("labelFit", GRAPHVIZ_DECORATED_BOXES.has(shape) ? "bounds" : "inside");
1625
+ put("labelPaddingX", num6(padX));
1626
+ put("labelPaddingY", num6(padY + ((LINE_SPACING - 1) * fontSize * lines) / 2));
1627
+ if (CIRCLE_LIKE.has(shape) || isTrue(get("regular"))) {
1628
+ put("labelFitSquare", true);
1629
+ }
1630
+ }
1631
+
1632
+ /**
1633
+ * Whether a `margin` part is a usable length: a number at least 0.
1634
+ * @param value - the part
1635
+ * @returns whether it is
1636
+ */
1637
+ function isLength(value: number): boolean {
1638
+ return Number.isFinite(value) && value >= 0;
1639
+ }
1640
+
646
1641
  /**
647
1642
  * Reads one edge the way Graphviz draws it.
648
1643
  * @param get - its attributes
@@ -655,6 +1650,7 @@ function readEdge(get: Attr, cx: EdgeContext, put: Put, at: At): void {
655
1650
  const scheme = get("colorscheme");
656
1651
  readCommon(get, tokens, scheme, cx.names, put, at);
657
1652
  readColor(nonEmpty(get("color")), scheme, "edge", "color", "opacity", put, at);
1653
+ readEdgeSegments(nonEmpty(get("color")), scheme, put, at);
658
1654
  readEdgeLine(get, tokens, put, at);
659
1655
  readEdgeArrows(get, cx, put, at);
660
1656
  for (const [attribute, property] of [
@@ -662,16 +1658,216 @@ function readEdge(get: Attr, cx: EdgeContext, put: Put, at: At): void {
662
1658
  ["headlabel", "targetLabel"],
663
1659
  ["taillabel", "sourceLabel"],
664
1660
  ] as const) {
665
- const raw = get(attribute);
1661
+ // an edge's xlabel is drawn beside its middle, as its label is: it is the label when there is no other
1662
+ const raw = attribute === LABEL_ATTRIBUTE ? (get(attribute) ?? nonEmpty(get("xlabel"))) : get(attribute);
666
1663
  if (raw !== null) {
667
1664
  put(property, labelText(raw, cx.names, cx.html, false, at(property)));
1665
+ if (property === "label") {
1666
+ readLabelAlign(raw, cx.names, cx.html, false, put);
1667
+ readHtmlRuns(raw, cx.html, scheme, put, at);
1668
+ }
668
1669
  }
669
1670
  }
670
- if (nonEmpty(get("xlabel")) !== null) {
1671
+ if (get(LABEL_ATTRIBUTE) !== null && nonEmpty(get("xlabel")) !== null) {
671
1672
  noteAt(at("label"), STYLE_ISSUE.NOT_CARRIED, "xlabel");
672
1673
  }
673
1674
  }
674
1675
 
1676
+ /**
1677
+ * Reads an edge's colored segments: a color list with a `;fraction` in it (`red;0.3:blue`) draws the line in one piece
1678
+ * per color along its length, which is a linear gradient with a hard switch at each band's end. A list with no
1679
+ * fraction draws parallel lines, which graphty does not draw (the line is in the first color).
1680
+ * @param list - the edge's `color`
1681
+ * @param scheme - its color scheme
1682
+ * @param put - receives the values
1683
+ * @param at - the sites
1684
+ */
1685
+ function readEdgeSegments(list: string | null, scheme: string | null, put: Put, at: At): void {
1686
+ if (list === null || !list.includes(":") || !list.includes(";")) {
1687
+ return;
1688
+ }
1689
+ const bands = colorBands(list, scheme, "edge", at("gradientColors"));
1690
+ if (bands.length < 2) {
1691
+ return;
1692
+ }
1693
+ const colors: string[] = [];
1694
+ const stops: number[] = [];
1695
+ let start = 0;
1696
+ for (const band of bands) {
1697
+ colors.push(band.color, band.color);
1698
+ stops.push(num6(start), num6(Math.min(1, start + band.size)));
1699
+ start += band.size;
1700
+ }
1701
+ put("gradientKind", "linear");
1702
+ put("gradientColors", colors.join(" "));
1703
+ put("gradientStops", stops.join(" "));
1704
+ }
1705
+
1706
+ /**
1707
+ * What an edge's label places are read against: the nodes' centers and sizes.
1708
+ * @param snapshot - the graph
1709
+ * @returns the context
1710
+ */
1711
+ function labelPlaceContext(snapshot: GraphSnapshot): LabelPlaceContext {
1712
+ const column = snapshot.nodes.byRole("position");
1713
+ return {
1714
+ center: (i) => {
1715
+ const v = column?.isSet(i) === true ? (column.value(i) as ArrayLike<number>) : null;
1716
+ return v === null ? null : [v[0], v[1]];
1717
+ },
1718
+ node: (i, name) => nonEmpty(cellText(snapshot.nodes, name, i)),
1719
+ };
1720
+ }
1721
+
1722
+ /**
1723
+ * 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`
1724
+ * control points (y up), flattened into bends between the two nodes' centers. Null for an edge Graphviz drew
1725
+ * straight, a `pos` that is not a spline, or nodes without positions.
1726
+ * @param snapshot - the graph
1727
+ * @param e - the edge
1728
+ * @param pos - its `pos` text
1729
+ * @returns the `bends` value, or null
1730
+ */
1731
+ function splineBends(snapshot: GraphSnapshot, e: number, pos: string | null): string | null {
1732
+ if (pos === null) {
1733
+ return null;
1734
+ }
1735
+ const column = snapshot.nodes.byRole("position");
1736
+ const center = (i: number): BendPoint | null => {
1737
+ const v = column?.isSet(i) === true ? (column.value(i) as ArrayLike<number>) : null;
1738
+ return v === null ? null : [v[0], v[1]];
1739
+ };
1740
+ const [source, target] = [center(snapshot.edgeSource(e)), center(snapshot.edgeTarget(e))];
1741
+ if (source === null || target === null) {
1742
+ return null;
1743
+ }
1744
+ // the arrow tips of the first spline's start and the last spline's end
1745
+ const tip = (spline: string | undefined, flag: "s" | "e"): BendPoint | null => {
1746
+ const token = (spline ?? "")
1747
+ .trim()
1748
+ .split(/\s+/)
1749
+ .find((t) => t.startsWith(`${flag},`));
1750
+ const [x, y] = (token ?? "").slice(2).split(",").map(Number);
1751
+ return token !== undefined && Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
1752
+ };
1753
+ const parts = pos.split(";");
1754
+ const tips = { start: tip(parts[0], "s"), end: tip(parts.at(-1), "e") };
1755
+ // ponytail: the splines of a concentrated edge (";"-separated) are joined end to end
1756
+ const splines = parts.map((spline) =>
1757
+ spline
1758
+ .trim()
1759
+ .split(/\s+/)
1760
+ .filter((t) => !/^[es],/.test(t))
1761
+ .map((t): BendPoint => {
1762
+ const [x, y] = t.split(",").map(Number);
1763
+ return [x, y];
1764
+ }),
1765
+ );
1766
+ return graphvizRouteBends(source, target, splines, tips);
1767
+ }
1768
+
1769
+ /** How far apart Graphviz's neato lays the loops on one node, across (x) and along (y) its side, in points. */
1770
+ const GRAPHVIZ_LOOP_STEP = { x: 18, y: 9 } as const;
1771
+
1772
+ /**
1773
+ * The self-loops Graphviz lays itself, with no `pos` of their own and no ports: neato's `selfRight` (splines.c), on
1774
+ * 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
1775
+ * 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
1776
+ * drawings of nodes from 0.33 to 1 inch).
1777
+ * @param snapshot - the graph
1778
+ * @returns the bends of an edge that is such a loop, or null
1779
+ */
1780
+ function graphvizLoops(snapshot: GraphSnapshot): { bendsOf: (e: number, get: Attr) => string | null } {
1781
+ const portless = (get: Attr): boolean =>
1782
+ nonEmpty(get("tailport")) === null &&
1783
+ nonEmpty(get("headport")) === null &&
1784
+ nonEmpty(get(POS_ATTRIBUTE)) === null;
1785
+ const onNode = new Map<number, number[]>();
1786
+ // ponytail: ortho routes a loop round the node's corners and none draws no edge; both keep the element's loop.
1787
+ const splines = (nonEmpty(cellText(snapshot.graph, "splines", 0)) ?? "").toLowerCase();
1788
+ // a file graphty wrote keeps its loops' look in its own block
1789
+ const laid =
1790
+ splines !== "ortho" && splines !== "none" && nonEmpty(cellText(snapshot.graph, STYLE_BLOCK_NAME, 0)) === null;
1791
+ for (let e = 0; e < snapshot.edgeCount && laid; e++) {
1792
+ const n = snapshot.edgeSource(e);
1793
+ if (n === snapshot.edgeTarget(e) && portless((name) => cellText(snapshot.edges, name, e))) {
1794
+ onNode.set(n, [...(onNode.get(n) ?? []), e]);
1795
+ }
1796
+ }
1797
+ const inches = (n: number, name: string, fallback: number): number => {
1798
+ const v = Number(nonEmpty(cellText(snapshot.nodes, name, n)) ?? Number.NaN);
1799
+ return (Number.isFinite(v) && v > 0 ? v : fallback) * DOT_POINTS_PER_INCH;
1800
+ };
1801
+ return {
1802
+ bendsOf: (e, get) => {
1803
+ const n = snapshot.edgeSource(e);
1804
+ const list = onNode.get(n);
1805
+ if (n !== snapshot.edgeTarget(e) || list === undefined || !portless(get)) {
1806
+ return null;
1807
+ }
1808
+ // ponytail: a node Graphviz grows to fit its label is taken at its stated width and height
1809
+ const [w, h] = [inches(n, "width", 0.75), inches(n, "height", 0.5)];
1810
+ const k = list.indexOf(e) + 1;
1811
+ const dx = w / 2 + GRAPHVIZ_LOOP_STEP.x * k;
1812
+ const dy = (GRAPHVIZ_LOOP_STEP.y * k) / list.length;
1813
+ const curve = flattenCubic([
1814
+ [0, 0],
1815
+ [dx / 3, dy],
1816
+ [dx, dy],
1817
+ [dx, 0],
1818
+ [dx, -dy],
1819
+ [dx / 3, -dy],
1820
+ [0, 0],
1821
+ ]);
1822
+ return curve === null ? null : canonNodeOffsets([0, 0], [w, h], curve.slice(1, -1), false);
1823
+ },
1824
+ };
1825
+ }
1826
+
1827
+ /**
1828
+ * The bends of a route Graphviz laid: B-splines of `3n + 1` control points each (y up), flattened into bends between
1829
+ * the two nodes' centers. Null for a route on the line between the centers, which is the straight edge it looks like,
1830
+ * or for control points that are not a spline. Also reads the `Line` of a GML file Graphviz's gv2gml wrote.
1831
+ * @param source - the source node's center
1832
+ * @param target - the target node's center
1833
+ * @param splines - the control points of each spline, joined end to end
1834
+ * @param tips - the arrow tips `pos` stores (`s,x,y` and `e,x,y`): the line runs on to each, along its arrow
1835
+ * @param tips.start - the tail arrow's tip, or null
1836
+ * @param tips.end - the head arrow's tip, or null
1837
+ * @returns the `bends` value, or null
1838
+ */
1839
+ export function graphvizRouteBends(
1840
+ source: BendPoint,
1841
+ target: BendPoint,
1842
+ splines: readonly (readonly BendPoint[])[],
1843
+ tips?: { readonly start: BendPoint | null; readonly end: BendPoint | null },
1844
+ ): string | null {
1845
+ const points: BendPoint[] = [];
1846
+ for (const controls of splines) {
1847
+ const flat = flattenCubic(controls);
1848
+ if (flat === null || flat.some(([x, y]) => !Number.isFinite(x) || !Number.isFinite(y))) {
1849
+ return null;
1850
+ }
1851
+ points.push(...flat);
1852
+ }
1853
+ // a tip where the spline already ends adds no point
1854
+ const apart = (a: BendPoint, b: BendPoint | undefined): boolean =>
1855
+ b === undefined || Math.hypot(a[0] - b[0], a[1] - b[1]) > 1e-6;
1856
+ const start = tips?.start ?? null;
1857
+ const end = tips?.end ?? null;
1858
+ if (start !== null && apart(start, points[0])) {
1859
+ points.unshift(start);
1860
+ }
1861
+ if (end !== null && apart(end, points.at(-1))) {
1862
+ points.push(end);
1863
+ }
1864
+ const [dx, dy] = [target[0] - source[0], target[1] - source[1]];
1865
+ const length = Math.hypot(dx, dy);
1866
+ const off = Math.max(...points.map(([x, y]) => Math.abs((x - source[0]) * dy - (y - source[1]) * dx)));
1867
+ // a route on the line between the centers is the straight edge it looks like
1868
+ return length > 0 && off / (length * length) > 1e-3 ? canonBends(source, target, points, false) : null;
1869
+ }
1870
+
675
1871
  /**
676
1872
  * Reads an edge's line: its width and dashes.
677
1873
  * @param get - its attributes
@@ -711,15 +1907,97 @@ function readEdgeArrows(get: Attr, cx: EdgeContext, put: Put, at: At): void {
711
1907
  // with none of these the tool-default rule draws the arrow of the graph's kind
712
1908
  if (dirRaw !== null || head !== null || tail !== null) {
713
1909
  const dir = (dirRaw ?? (cx.directed ? "forward" : "none")).trim().toLowerCase();
714
- const forward = dir === "forward" || dir === "both";
715
- const back = dir === "back" || dir === "both";
716
- put("targetArrow", forward ? canonEnum("arrow", head ?? "normal", "graphviz", at("targetArrow")) : "none");
717
- put("sourceArrow", back ? canonEnum("arrow", tail ?? "normal", "graphviz", at("sourceArrow")) : "none");
1910
+ readEdgeArrow("target", head, dir === "forward" || dir === "both", put, at);
1911
+ readEdgeArrow("source", tail, dir === "back" || dir === "both", put, at);
718
1912
  }
719
1913
  const size = get("arrowsize");
720
1914
  if (size !== null) {
721
1915
  put("arrowSize", canonNumber(size, at("arrowSize")));
722
1916
  }
1917
+ // an edge's `fillcolor` fills its arrowheads (their outline stays the edge's color)
1918
+ const fill = nonEmpty(get("fillcolor"));
1919
+ for (const property of ["targetArrowColor", "sourceArrowColor"]) {
1920
+ readColor(fill, get("colorscheme"), "fill", property, null, put, at);
1921
+ }
1922
+ }
1923
+
1924
+ /**
1925
+ * Reads one end's arrow: its shape, or none when the edge's direction draws no arrow there, and the side it keeps.
1926
+ * @param end - which end
1927
+ * @param raw - its `arrowhead` or `arrowtail`, or null
1928
+ * @param on - whether the edge's direction draws an arrow there
1929
+ * @param put - receives the values
1930
+ * @param at - the sites
1931
+ */
1932
+ function readEdgeArrow(end: "target" | "source", raw: string | null, on: boolean, put: Put, at: At): void {
1933
+ const arrow = on ? graphvizArrowValue(raw ?? "normal", at(`${end}Arrow`)) : null;
1934
+ put(`${end}Arrow`, arrow === null ? "none" : arrow.value);
1935
+ if (arrow?.side !== null && arrow?.side !== undefined) {
1936
+ put(`${end}ArrowSide`, arrow.side);
1937
+ }
1938
+ }
1939
+
1940
+ /** The sides an `l` or `r` arrow modifier keeps, by their first letters. */
1941
+ const ARROW_SIDES = ["left", "right"] as const;
1942
+
1943
+ /** The open forms of Graphviz arrows that a neutral shape draws; the others are drawn filled. */
1944
+ const GRAPHVIZ_OPEN_ARROWS: ReadonlySet<string> = new Set(["normal", "diamond", "dot", "box", "inv"]);
1945
+
1946
+ /** The open Graphviz arrows the neutral vocabulary has no name for, by the value graphty draws them as. */
1947
+ const GRAPHVIZ_OTHER_ARROWS: ReadonlyMap<string, string> = new Map([
1948
+ ["obox", "other:open-square"],
1949
+ ["oinv", "other:open-inverted"],
1950
+ ]);
1951
+
1952
+ /**
1953
+ * The neutral arrow value Graphviz draws for an arrow name (`onormal`, `lbox`, `invempty`), with an open shape the
1954
+ * neutral vocabulary has no name for (`obox`, `oinv`) as the value graphty draws it as, and the side an `l` or `r`
1955
+ * keeps. Read so by the DOT reader and by the GML reader for gv2gml's `targetArrow` / `sourceArrow`.
1956
+ * @param raw - the arrow name
1957
+ * @param site - where it was read
1958
+ * @returns the value and its side (null for the whole shape)
1959
+ */
1960
+ export function graphvizArrowValue(
1961
+ raw: string,
1962
+ site: CanonSite,
1963
+ ): { readonly value: string | null; readonly side: "left" | "right" | null } {
1964
+ const first = graphvizArrow(raw);
1965
+ const other = first === null ? undefined : GRAPHVIZ_OTHER_ARROWS.get(first.name);
1966
+ return {
1967
+ value: other ?? canonEnum("arrow", first?.name ?? raw, "graphviz", site),
1968
+ side: first?.side ?? null,
1969
+ };
1970
+ }
1971
+
1972
+ /**
1973
+ * The arrow Graphviz draws at an edge's very end, from an arrow name of up to four shapes (`dotodot`,
1974
+ * `lteeoldiamond`): the first shape that is not `none`, with its `o` (open) kept where a neutral shape draws it, and
1975
+ * the side an `l` or `r` keeps alone ("clip the shape, leaving only the part to the left of the edge", looking along
1976
+ * the edge toward the arrow's tip).
1977
+ * @param raw - the arrow name
1978
+ * @returns the shape's own name and its side (null for the whole shape), or null for a name outside the grammar (a
1979
+ * synonym such as `empty`)
1980
+ */
1981
+ function graphvizArrow(raw: string): { readonly name: string; readonly side: "left" | "right" | null } | null {
1982
+ let rest = raw.trim().toLowerCase();
1983
+ // the one synonym the grammar would misread (as `inv`): arrows.c's `invempty` is an open `inv`
1984
+ if (rest === "invempty") {
1985
+ rest = "oinv";
1986
+ }
1987
+ while (rest.length > 0) {
1988
+ const m = /^([olr]{0,3})(normal|crow|tee|box|diamond|dot|none|inv|vee|icurve|curve)/.exec(rest);
1989
+ if (m === null) {
1990
+ return null;
1991
+ }
1992
+ rest = rest.slice(m[0].length);
1993
+ const [, mods, shape] = m;
1994
+ if (shape !== "none") {
1995
+ const side = ARROW_SIDES.find((s) => mods.includes(s[0])) ?? null;
1996
+ const open = mods.includes("o") && GRAPHVIZ_OPEN_ARROWS.has(shape) ? "o" : "";
1997
+ return { name: open + shape, side };
1998
+ }
1999
+ }
2000
+ return null;
723
2001
  }
724
2002
 
725
2003
  /**
@@ -806,12 +2084,16 @@ function contexts(
806
2084
  } {
807
2085
  const html = htmlStrings(snapshot);
808
2086
  const graph = snapshot.meta.name ?? "";
2087
+ const rankdir = (cellText(snapshot.graph, "rankdir", 0) ?? "").trim().toUpperCase();
2088
+ const flip = rankdir === "LR" || rankdir === "RL";
2089
+ const q = Number(cellText(snapshot.graph, "quantum", 0) ?? Number.NaN);
2090
+ const quantum = Number.isFinite(q) && q > 0 ? q * DOT_POINTS_PER_INCH : 0;
809
2091
  const own = directedEdges(snapshot);
810
2092
  // a graph mixing both directions is written with one, every edge then drawn as that one
811
2093
  const directed = written === null || own.every((d) => d === snapshot.directed) ? own : own.map(() => written);
812
2094
  const name = (i: number): string => String(snapshot.ids.idOf(i));
813
2095
  return {
814
- node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html }),
2096
+ node: (i) => ({ name: name(i), graph, cluster: isCluster(snapshot, i), html, flip, quantum }),
815
2097
  edge: (e) => {
816
2098
  const tail = name(snapshot.edgeSource(e));
817
2099
  const head = name(snapshot.edgeTarget(e));
@@ -853,6 +2135,34 @@ function styleColumns(table: GraphSnapshot["nodes"], attributes: readonly string
853
2135
  return [...table.names()].filter((c) => own.has(c) || c.startsWith("graphty.style."));
854
2136
  }
855
2137
 
2138
+ /**
2139
+ * The graph's own styles: its `bgcolor` as its background.
2140
+ * @param snapshot - the graph
2141
+ * @param notes - where a bad color is noted
2142
+ * @returns the graph's values
2143
+ */
2144
+ function graphBackground(snapshot: GraphSnapshot, notes: StyleNotes): Record<string, StyleValue> {
2145
+ const graph: Record<string, StyleValue> = {};
2146
+ const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
2147
+ if (!snapshot.graph.has("bgcolor") || bg === null) {
2148
+ return graph;
2149
+ }
2150
+ readColor(
2151
+ bg,
2152
+ cellText(snapshot.graph, "colorscheme", 0),
2153
+ "fill",
2154
+ "background",
2155
+ null,
2156
+ (p, v) => {
2157
+ if (v !== null) {
2158
+ graph[p] = v;
2159
+ }
2160
+ },
2161
+ (property) => ({ notes, target: "graph", property }),
2162
+ );
2163
+ return graph;
2164
+ }
2165
+
856
2166
  /**
857
2167
  * The styles of a DOT file, read the way Graphviz draws it, with Graphviz's defaults beneath: an unfilled black
858
2168
  * ellipse 54 x 36 labeled with its name, black edges with a target arrow in a directed graph. A file graphty wrote
@@ -893,6 +2203,11 @@ export function dotStyles(snapshot: GraphSnapshot, context: StyleReadContext): I
893
2203
  }
894
2204
  }
895
2205
  const edge = valueColumns(snapshot.edgeCount);
2206
+ const loops = graphvizLoops(snapshot);
2207
+ // `splines=none` and `splines=""` draw no edges at all (https://graphviz.org/docs/attrs/splines/)
2208
+ const splines = cellText(snapshot.graph, "splines", 0)?.trim().toLowerCase();
2209
+ const noEdges = splines === "" || splines === "none";
2210
+ const places = labelPlaceContext(snapshot);
896
2211
  for (let e = 0; e < snapshot.edgeCount; e++) {
897
2212
  context.budget.spend("edge", EDGE_ATTRIBUTES.length);
898
2213
  const get: Attr = (name) => cellText(snapshot.edges, name, e);
@@ -905,24 +2220,18 @@ export function dotStyles(snapshot: GraphSnapshot, context: StyleReadContext): I
905
2220
  },
906
2221
  at,
907
2222
  );
2223
+ if (noEdges) {
2224
+ edge.put(e, "visible", false);
2225
+ }
2226
+ const bends = splineBends(snapshot, e, get(POS_ATTRIBUTE)) ?? loops.bendsOf(e, get);
2227
+ if (bends !== null) {
2228
+ edge.put(e, "bends", bends);
2229
+ }
2230
+ readLabelPlaces(snapshot, places, e, get, (p, v) => {
2231
+ edge.put(e, p, v);
2232
+ });
908
2233
  }
909
- const graph: Record<string, StyleValue> = {};
910
- const bg = nonEmpty(cellText(snapshot.graph, "bgcolor", 0));
911
- if (snapshot.graph.has("bgcolor") && bg !== null) {
912
- readColor(
913
- bg,
914
- cellText(snapshot.graph, "colorscheme", 0),
915
- "fill",
916
- "background",
917
- null,
918
- (p, v) => {
919
- if (v !== null) {
920
- graph[p] = v;
921
- }
922
- },
923
- (property) => ({ notes, target: "graph", property }),
924
- );
925
- }
2234
+ const graph = graphBackground(snapshot, notes);
926
2235
  const blockText = cellText(snapshot.graph, STYLE_BLOCK_NAME, 0);
927
2236
  if (blockText !== null) {
928
2237
  exactFromBlock(snapshot, blockText, { node: node.values, edge: edge.values }, cx);
@@ -1016,8 +2325,9 @@ const ARROWS: ReadonlyMap<string, string> = new Map([
1016
2325
  ["circle-open", "odot"],
1017
2326
  ["square", "box"],
1018
2327
  ["crow", "crow"],
1019
- ["half", "lnormal"],
1020
2328
  ["inverted", "inv"],
2329
+ ["other:open-square", "obox"],
2330
+ ["other:open-inverted", "oinv"],
1021
2331
  ]);
1022
2332
 
1023
2333
  /** Line and border styles Graphviz draws exactly; the others are written as the nearest. */
@@ -1045,7 +2355,10 @@ function graphvizDefaults(target: StyleTarget): ReadonlyMap<string, Written> {
1045
2355
  };
1046
2356
  add("fontcolor", "labelColor");
1047
2357
  add("fontsize", "labelSize");
1048
- add("fontname", "labelFont");
2358
+ const font = t.labelFont?.value;
2359
+ if (typeof font === "string") {
2360
+ out.set("fontname", graphvizFontName(font, false, false));
2361
+ }
1049
2362
  if (target === "node") {
1050
2363
  add("color", "borderColor");
1051
2364
  add("pencolor", "borderColor");
@@ -1099,12 +2412,16 @@ function gvColor(color: string, opacity: StyleValue | null): string {
1099
2412
  }
1100
2413
 
1101
2414
  /**
1102
- * A label's text as a DOT label: backslashes and line breaks escaped, so Graphviz's escape passes give the text back.
2415
+ * A label's text as a DOT label: backslashes, line breaks and `&` escaped, so Graphviz's escape and entity passes
2416
+ * give the text back.
1103
2417
  * @param text - the text
1104
2418
  * @returns the label
1105
2419
  */
1106
2420
  function escapeLabel(text: string): string {
1107
- return text.replaceAll("\\", String.raw`\\`).replaceAll("\n", String.raw`\n`);
2421
+ return text
2422
+ .replaceAll("&", "&amp;")
2423
+ .replaceAll("\\", String.raw`\\`)
2424
+ .replaceAll("\n", String.raw`\n`);
1108
2425
  }
1109
2426
 
1110
2427
  /**
@@ -1137,10 +2454,11 @@ function spelled(
1137
2454
  /**
1138
2455
  * The font, tooltip and visibility attributes every element shares.
1139
2456
  * @param get - the element's values
2457
+ * @param approximate - records approximations
1140
2458
  * @param out - the attributes, added to
1141
2459
  * @param tokens - the style tokens, added to
1142
2460
  */
1143
- function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): void {
2461
+ function writeCommon(get: Get, approximate: Approximate, out: Map<string, Written>, tokens: string[]): void {
1144
2462
  const color = get("labelColor");
1145
2463
  if (typeof color === "string") {
1146
2464
  out.set("fontcolor", color);
@@ -1150,8 +2468,20 @@ function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): voi
1150
2468
  out.set("fontsize", size);
1151
2469
  }
1152
2470
  const font = get("labelFont");
2471
+ const bold = get("labelWeight") === "bold";
2472
+ const italic = get("labelItalic") === true;
1153
2473
  if (typeof font === "string") {
1154
- out.set("fontname", font);
2474
+ const name = graphvizFontName(font, bold, italic);
2475
+ out.set("fontname", name);
2476
+ const drawn = graphvizFont(name);
2477
+ if (drawn.bold !== bold) {
2478
+ approximate("labelWeight", get("labelWeight") ?? "normal");
2479
+ }
2480
+ if (drawn.italic !== italic) {
2481
+ approximate("labelItalic", italic);
2482
+ }
2483
+ } else if (bold || italic) {
2484
+ approximate(bold ? "labelWeight" : "labelItalic", bold ? "bold" : true);
1155
2485
  }
1156
2486
  const tip = get("tooltip");
1157
2487
  if (typeof tip === "string") {
@@ -1166,14 +2496,21 @@ function writeCommon(get: Get, out: Map<string, Written>, tokens: string[]): voi
1166
2496
  * A node's or cluster's Graphviz attributes from its values.
1167
2497
  * @param get - the node's values
1168
2498
  * @param cx - the node's name and kind
2499
+ * @param raw - its attributes as data
1169
2500
  * @param approximate - records approximations
1170
2501
  * @param notes - where an HTML name is noted
1171
2502
  * @returns the attributes
1172
2503
  */
1173
- function nodeAttributes(get: Get, cx: NodeContext, approximate: Approximate, notes: StyleNotes): Map<string, Written> {
2504
+ function nodeAttributes(
2505
+ get: Get,
2506
+ cx: NodeContext,
2507
+ raw: Attr,
2508
+ approximate: Approximate,
2509
+ notes: StyleNotes,
2510
+ ): Map<string, Written> {
1174
2511
  const out = new Map<string, Written>();
1175
2512
  const tokens: string[] = [];
1176
- writeCommon(get, out, tokens);
2513
+ writeCommon(get, approximate, out, tokens);
1177
2514
  writeNodeOutline(get, cx, approximate, out, tokens);
1178
2515
  const gvShape = writeNodeShape(get, cx, approximate, out, tokens);
1179
2516
  if (!cx.cluster) {
@@ -1187,12 +2524,30 @@ function nodeAttributes(get: Get, cx: NodeContext, approximate: Approximate, not
1187
2524
  } else if (label === null && shown !== null) {
1188
2525
  out.set(LABEL_ATTRIBUTE, "");
1189
2526
  }
2527
+ noteLabelPosition(get("labelPosition"), cx, raw, approximate);
1190
2528
  if (tokens.length > 0) {
1191
2529
  out.set("style", tokens.join(","));
1192
2530
  }
1193
2531
  return withoutDefaults(out, "node");
1194
2532
  }
1195
2533
 
2534
+ /**
2535
+ * Notes a label position Graphviz does not draw: a node's label sits where its own `labelloc` (kept as data) puts
2536
+ * it, and a cluster's where its `labelloc` and `labeljust` do.
2537
+ * @param position - the label's position
2538
+ * @param cx - the node's name and kind
2539
+ * @param raw - the node's attributes as data
2540
+ * @param approximate - records approximations
2541
+ */
2542
+ function noteLabelPosition(position: StyleValue | null, cx: NodeContext, raw: Attr, approximate: Approximate): void {
2543
+ if (
2544
+ typeof position === "string" &&
2545
+ position !== (cx.cluster ? clusterLabelPosition(raw) : nodeLabelPosition(raw))
2546
+ ) {
2547
+ approximate("labelPosition", position);
2548
+ }
2549
+ }
2550
+
1196
2551
  /**
1197
2552
  * A node's or cluster's fill and outline attributes.
1198
2553
  * @param get - the node's values
@@ -1258,20 +2613,95 @@ function writeNodeShape(
1258
2613
  }
1259
2614
  return "";
1260
2615
  }
2616
+ const size = (name: string): number | null => {
2617
+ const v = get(name);
2618
+ return typeof v === "number" ? v : null;
2619
+ };
2620
+ const glyph = shape === "polygon" ? glyphOf(get, size("width"), size("height")) : undefined;
2621
+ if (glyph !== undefined) {
2622
+ return writeGlyph(get, glyph, out);
2623
+ }
2624
+ approximateShapeLines(get, approximate);
2625
+ const polygon = shape === "polygon" ? writePolygon(get("shapePoints"), approximate, out) : null;
2626
+ if (polygon !== null) {
2627
+ // Graphviz draws its polygon's corners where it lays them, which read back unstretched
2628
+ if (get("shapePointsStretch") !== false) {
2629
+ approximate("shapePointsStretch", true);
2630
+ }
2631
+ return polygon;
2632
+ }
1261
2633
  const gvShape = spelled(SHAPES, shape, shape === "vee" ? "invtriangle" : "box", "shape", approximate);
1262
2634
  out.set("shape", gvShape);
1263
2635
  return gvShape;
1264
2636
  }
1265
2637
 
1266
2638
  /**
1267
- * A node's width and height, fixed so Graphviz does not grow them to fit the label.
2639
+ * A glyph's shape attribute, and its line's width as its pen's when it draws no outline.
2640
+ * @param get - the node's values
2641
+ * @param glyph - the glyph
2642
+ * @param out - the attributes, added to
2643
+ * @returns the Graphviz shape written
2644
+ */
2645
+ function writeGlyph(get: Get, glyph: NonNullable<ReturnType<typeof glyphOf>>, out: Map<string, Written>): string {
2646
+ out.set("shape", glyph.name);
2647
+ // an underline's line is drawn in its pen, which draws no outline round it
2648
+ const line = get("shapeLineWidth");
2649
+ if (glyph.unstroked && typeof line === "number") {
2650
+ out.set("penwidth", line);
2651
+ }
2652
+ return glyph.name;
2653
+ }
2654
+
2655
+ /**
2656
+ * Notes a node's pieces and lines of no Graphviz shape as approximated: Graphviz has no attribute for them.
2657
+ * @param get - the node's values
2658
+ * @param approximate - records approximations
2659
+ */
2660
+ function approximateShapeLines(get: Get, approximate: Approximate): void {
2661
+ for (const property of ["shapeParts", "shapeLines", "shapeLineWidth"]) {
2662
+ const v = get(property);
2663
+ if (v !== null && v !== undefined) {
2664
+ approximate(property, v);
2665
+ }
2666
+ }
2667
+ }
2668
+
2669
+ /**
2670
+ * A polygon's shape attribute: the Graphviz shape whose outline it is, else `shape=polygon` with its number of sides.
2671
+ * @param points - its `shapePoints`
2672
+ * @param approximate - records approximations
2673
+ * @param out - the attributes, added to
2674
+ * @returns the Graphviz shape written, or null for corners too few to be a polygon
2675
+ */
2676
+ function writePolygon(points: unknown, approximate: Approximate, out: Map<string, Written>): string | null {
2677
+ if (typeof points !== "string") {
2678
+ return null;
2679
+ }
2680
+ const named = OUTLINE_NAMES.get(points.trim());
2681
+ if (named !== undefined) {
2682
+ out.set("shape", named);
2683
+ return named;
2684
+ }
2685
+ // the corners themselves are not Graphviz attributes: the polygon keeps its number of sides
2686
+ const sides = points.trim().split(/\s+/).length;
2687
+ if (sides < 3) {
2688
+ return null;
2689
+ }
2690
+ out.set("shape", "polygon");
2691
+ out.set("sides", sides);
2692
+ approximate("shapePoints", points);
2693
+ return "polygon";
2694
+ }
2695
+
2696
+ /**
2697
+ * A node's width and height, fixed so Graphviz does not grow them to fit the label, unless the node grows to fit it.
1268
2698
  * @param get - the node's values
1269
2699
  * @param out - the attributes, added to
1270
2700
  */
1271
2701
  function writeNodeSize(get: Get, out: Map<string, Written>): void {
1272
2702
  const width = get("width");
1273
2703
  const height = get("height");
1274
- if (typeof width === "number" || typeof height === "number") {
2704
+ if ((typeof width === "number" || typeof height === "number") && typeof get("labelFit") !== "string") {
1275
2705
  out.set("fixedsize", "true");
1276
2706
  }
1277
2707
  if (typeof width === "number") {
@@ -1292,7 +2722,7 @@ function writeNodeSize(get: Get, out: Map<string, Written>): void {
1292
2722
  function edgeAttributes(get: Get, directed: boolean, approximate: Approximate): Map<string, Written> {
1293
2723
  const out = new Map<string, Written>();
1294
2724
  const tokens: string[] = [];
1295
- writeCommon(get, out, tokens);
2725
+ writeCommon(get, approximate, out, tokens);
1296
2726
  const color = get("color");
1297
2727
  if (typeof color === "string") {
1298
2728
  out.set("color", gvColor(color, get("opacity")));
@@ -1306,6 +2736,16 @@ function edgeAttributes(get: Get, directed: boolean, approximate: Approximate):
1306
2736
  tokens.push(spelled(STROKES, line, line.includes("dash") ? "dashed" : "solid", "lineStyle", approximate));
1307
2737
  }
1308
2738
  writeEdgeArrows(get, directed, approximate, out);
2739
+ // Graphviz fills both arrowheads with the one `fillcolor`
2740
+ const head = get("targetArrowColor");
2741
+ const tail = get("sourceArrowColor");
2742
+ const fill = head ?? tail;
2743
+ if (typeof fill === "string") {
2744
+ out.set("fillcolor", fill);
2745
+ if (head !== null && tail !== null && head !== tail) {
2746
+ approximate("sourceArrowColor", tail);
2747
+ }
2748
+ }
1309
2749
  const arrowSize = get("arrowSize");
1310
2750
  if (typeof arrowSize === "number") {
1311
2751
  out.set("arrowsize", arrowSize);
@@ -1338,7 +2778,7 @@ function writeEdgeArrows(get: Get, directed: boolean, approximate: Approximate,
1338
2778
  const target = get("targetArrow");
1339
2779
  const tail = typeof source === "string" && source !== "none";
1340
2780
  const head = typeof target === "string" && target !== "none";
1341
- const plain = directed ? head && !tail && target === "triangle" : !head && !tail;
2781
+ const plain = directed ? head && !tail && target === "triangle" && get("targetArrowSide") === null : !head && !tail;
1342
2782
  if (plain || (source === null && target === null)) {
1343
2783
  return;
1344
2784
  }
@@ -1351,29 +2791,23 @@ function writeEdgeArrows(get: Get, directed: boolean, approximate: Approximate,
1351
2791
  dir = "back";
1352
2792
  }
1353
2793
  out.set("dir", dir);
2794
+ // a one-sided arrow is its shape with an `l` or `r` before it; the neutral half arrow, one barb, is nearest to the
2795
+ // left half of Graphviz's triangle, which reads back as that
2796
+ const arrow = (value: string, side: StyleValue | null, property: string): string => {
2797
+ if (value === "half") {
2798
+ approximate(property, value);
2799
+ approximate(`${property}Side`, value);
2800
+ return "lnormal";
2801
+ }
2802
+ const name = spelled(ARROWS, value, "normal", property, approximate);
2803
+ return side === "left" || side === "right" ? `${side[0]}${name}` : name;
2804
+ };
1354
2805
  if (head) {
1355
- out.set("arrowhead", spelled(ARROWS, target, "normal", "targetArrow", approximate));
2806
+ out.set("arrowhead", arrow(target, get("targetArrowSide"), "targetArrow"));
1356
2807
  }
1357
2808
  if (tail) {
1358
- out.set("arrowtail", spelled(ARROWS, source, "normal", "sourceArrow", approximate));
1359
- }
1360
- }
1361
-
1362
- /**
1363
- * Each element's values of the properties the writer writes, as a getter per element.
1364
- * @param picture - each element's value of a property
1365
- * @param target - nodes or edges
1366
- * @returns the getter
1367
- */
1368
- function getter(
1369
- picture: (target: StyleTarget, property: string) => readonly (StyleValue | null)[],
1370
- target: StyleTarget,
1371
- ): (i: number) => Get {
1372
- const columns = new Map<string, readonly (StyleValue | null)[]>();
1373
- for (const p of DOT_STYLE_CAPABILITIES[target]) {
1374
- columns.set(p, picture(target, p));
2809
+ out.set("arrowtail", arrow(source, get("sourceArrowSide"), "sourceArrow"));
1375
2810
  }
1376
- return (i) => (property) => columns.get(property)?.[i] ?? null;
1377
2811
  }
1378
2812
 
1379
2813
  /**
@@ -1394,29 +2828,17 @@ function exactFromBlock(
1394
2828
  read: { readonly node: ValueColumns; readonly edge: ValueColumns },
1395
2829
  cx: ReturnType<typeof contexts>,
1396
2830
  ): void {
1397
- const parsed = parseStyleBlock(text, styleFingerprint(snapshot));
1398
- const ex = parsed.ok ? blockStyles(parsed.block, snapshot, "dot") : null;
1399
- if (ex === null) {
1400
- return;
1401
- }
1402
2831
  const notes = new StyleNotes();
1403
- for (const target of ["node", "edge"] as const) {
1404
- const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1405
- const get = getter((t, p) => resolveStyles(ex, t, p as never, snapshot), target);
1406
- const values = read[target];
1407
- for (let i = 0; i < n; i++) {
1408
- const want = get(i);
1409
- const back = writtenAndReadBack(target, i, want, cx, notes);
1410
- for (const property of DOT_STYLE_CAPABILITIES[target]) {
1411
- const native = values[property]?.[i] ?? null;
1412
- const w = want(property);
1413
- if (native !== w && native === (back.get(property) ?? null)) {
1414
- values[property] ??= new Array<StyleValue | null>(n).fill(null);
1415
- values[property][i] = w;
1416
- }
1417
- }
1418
- }
1419
- }
2832
+ takeFromBlock(snapshot, text, "dot", (target) => ({
2833
+ values: read[target],
2834
+ properties: DOT_STYLE_CAPABILITIES[target],
2835
+ writes: DOT_STYLE_CAPABILITIES[target],
2836
+ readBack: (i, want) => {
2837
+ const raw: Attr = (name) => cellText(target === "node" ? snapshot.nodes : snapshot.edges, name, i);
2838
+ const back = writtenAndReadBack(target, i, want, { cx, raw }, notes);
2839
+ return (property) => back.get(property) ?? null;
2840
+ },
2841
+ }));
1420
2842
  }
1421
2843
 
1422
2844
  /**
@@ -1424,7 +2846,9 @@ function exactFromBlock(
1424
2846
  * @param target - nodes or edges
1425
2847
  * @param i - the element
1426
2848
  * @param want - its values
1427
- * @param cx - the elements' contexts
2849
+ * @param data - the elements' contexts, and the element's attributes as data (what the writer leaves as it is)
2850
+ * @param data.cx - the elements' contexts
2851
+ * @param data.raw - the element's attributes as data
1428
2852
  * @param notes - a scratch for the notes the round trip makes
1429
2853
  * @returns the values read back, by property
1430
2854
  */
@@ -1432,9 +2856,10 @@ function writtenAndReadBack(
1432
2856
  target: StyleTarget,
1433
2857
  i: number,
1434
2858
  want: Get,
1435
- cx: ReturnType<typeof contexts>,
2859
+ data: { readonly cx: ReturnType<typeof contexts>; readonly raw: Attr },
1436
2860
  notes: StyleNotes,
1437
2861
  ): Map<string, StyleValue> {
2862
+ const { cx, raw } = data;
1438
2863
  const ignore: Approximate = () => undefined;
1439
2864
  const back = new Map<string, StyleValue>();
1440
2865
  const put: Put = (property, value) => {
@@ -1445,8 +2870,10 @@ function writtenAndReadBack(
1445
2870
  const at: At = (property) => ({ notes, target, property });
1446
2871
  if (target === "node") {
1447
2872
  const ncx = cx.node(i);
1448
- const attrs = nodeAttributes(want, ncx, ignore, notes);
1449
- readNode((name) => textOf(attrs.get(name)), ncx, put, at);
2873
+ const attrs = nodeAttributes(want, ncx, raw, ignore, notes);
2874
+ // a cluster's label position stays in its data (`labelloc`, `labeljust`), which the writer leaves alone
2875
+ const kept = (name: string): string | null => (name === "labelloc" || name === "labeljust" ? raw(name) : null);
2876
+ readNode((name) => (attrs.has(name) ? textOf(attrs.get(name)) : kept(name)), ncx, put, at);
1450
2877
  } else {
1451
2878
  const ecx = cx.edge(i);
1452
2879
  const attrs = edgeAttributes(want, ecx.directed, ignore);
@@ -1555,7 +2982,8 @@ function styledExport(snapshot: GraphSnapshot, input: ExporterStyleInput, onMixe
1555
2982
  if (out.graph.has("bgcolor")) {
1556
2983
  out.graph.remove("bgcolor");
1557
2984
  }
1558
- if (typeof background === "string") {
2985
+ // Graphviz paints a graph without bgcolor white, so the writer leaves that default out like the others
2986
+ if (typeof background === "string" && background !== toolDefaults("graphviz")?.graph.background?.value) {
1559
2987
  out.graph.set("bgcolor", [background], { dtype: "string" });
1560
2988
  }
1561
2989
  const inline = [...input.tags.node.values(), ...input.tags.edge.values()].some((col) => !readsBack(col));
@@ -1598,30 +3026,105 @@ function attributeColumns(
1598
3026
  w: AttributeWrite,
1599
3027
  ): Map<string, (Written | null)[]> {
1600
3028
  const n = target === "node" ? snapshot.nodeCount : snapshot.edgeCount;
1601
- const get = getter((t, p) => input.picture(t, p as never), target);
3029
+ const get = elementValues((t, p) => input.picture(t, p as never), target, DOT_STYLE_CAPABILITIES[target]);
1602
3030
  const columns = new Map<string, (Written | null)[]>();
1603
3031
  for (let i = 0; i < n; i++) {
1604
3032
  const attrs =
1605
3033
  target === "node"
1606
- ? nodeAttributes(get(i), w.cx.node(i), w.approximate, w.notes)
3034
+ ? nodeAttributes(
3035
+ get(i),
3036
+ w.cx.node(i),
3037
+ (name) => cellText(snapshot.nodes, name, i),
3038
+ w.approximate,
3039
+ w.notes,
3040
+ )
1607
3041
  : edgeAttributes(get(i), w.cx.edge(i).directed, w.approximate);
3042
+ // a self-loop's route is not written: Graphviz lays the loop itself, which is what reads back
3043
+ if (target === "edge" && snapshot.edgeSource(i) === snapshot.edgeTarget(i)) {
3044
+ w.approximate("bends", "self-loop");
3045
+ }
1608
3046
  // no label is written label="" (Graphviz would show the name), which a rule reading the label column
1609
3047
  // reads back as the empty label
1610
3048
  if (w.read.has(LABEL_ATTRIBUTE) && attrs.get(LABEL_ATTRIBUTE) === "" && get(i)("label") === null) {
1611
3049
  w.approximate("label", "");
1612
3050
  }
1613
- for (const [name, value] of attrs) {
1614
- let col = columns.get(name);
1615
- if (col === undefined) {
1616
- col = new Array<Written | null>(n).fill(null);
1617
- columns.set(name, col);
1618
- }
1619
- col[i] = value;
1620
- }
3051
+ setColumnValues(columns, attrs, i, n);
3052
+ }
3053
+ if (target === "node") {
3054
+ stopInheritance(snapshot, w.cx, columns, input.rules.styles.graph.background);
1621
3055
  }
1622
3056
  return columns;
1623
3057
  }
1624
3058
 
3059
+ /**
3060
+ * Puts one element's attributes into the columns, adding a column for an attribute not seen before.
3061
+ * @param columns - the columns, by name
3062
+ * @param attrs - the element's attributes
3063
+ * @param i - the element
3064
+ * @param n - how many elements there are
3065
+ */
3066
+ function setColumnValues(
3067
+ columns: Map<string, (Written | null)[]>,
3068
+ attrs: ReadonlyMap<string, Written>,
3069
+ i: number,
3070
+ n: number,
3071
+ ): void {
3072
+ for (const [name, value] of attrs) {
3073
+ let col = columns.get(name);
3074
+ if (col === undefined) {
3075
+ col = new Array<Written | null>(n).fill(null);
3076
+ columns.set(name, col);
3077
+ }
3078
+ col[i] = value;
3079
+ }
3080
+ }
3081
+
3082
+ /** What a cluster draws when neither it nor a graph around it sets the attribute: Graphviz's defaults. */
3083
+ const CLUSTER_UNSET: ReadonlyMap<string, Written> = new Map<string, Written>([
3084
+ ["style", ""],
3085
+ ["penwidth", 1],
3086
+ ["color", ""],
3087
+ ["pencolor", ""],
3088
+ ["fillcolor", ""],
3089
+ ["bgcolor", ""],
3090
+ ["fontname", "Times-Roman"],
3091
+ ["fontsize", 14],
3092
+ ["fontcolor", "black"],
3093
+ [LABEL_ATTRIBUTE, ""],
3094
+ ]);
3095
+
3096
+ /**
3097
+ * Gives every cluster its own value of each attribute a graph around it sets, Graphviz's default where the cluster
3098
+ * has none, since Graphviz gives a cluster the attributes of the graphs around it (the root's are kept as data).
3099
+ * @param snapshot - the graph handed to the exporter
3100
+ * @param cx - the elements' contexts
3101
+ * @param columns - the node attributes written, by name, changed in place
3102
+ * @param background - the background the writer gives the root graph (its `bgcolor`), which a cluster would inherit
3103
+ */
3104
+ function stopInheritance(
3105
+ snapshot: GraphSnapshot,
3106
+ cx: ReturnType<typeof contexts>,
3107
+ columns: Map<string, (Written | null)[]>,
3108
+ background: unknown,
3109
+ ): void {
3110
+ const clusters = Array.from({ length: snapshot.nodeCount }, (_, i) => i).filter((i) => cx.node(i).cluster);
3111
+ for (const [name, unset] of CLUSTER_UNSET) {
3112
+ const col = columns.get(name);
3113
+ const enclosing =
3114
+ cellText(snapshot.graph, name, 0) !== null ||
3115
+ (name === "bgcolor" && typeof background === "string") ||
3116
+ clusters.some((i) => (col?.[i] ?? null) !== null);
3117
+ if (!enclosing) {
3118
+ continue;
3119
+ }
3120
+ const filled = col ?? new Array<Written | null>(snapshot.nodeCount).fill(null);
3121
+ for (const i of clusters) {
3122
+ filled[i] ??= unset;
3123
+ }
3124
+ columns.set(name, filled);
3125
+ }
3126
+ }
3127
+
1625
3128
  /**
1626
3129
  * Removes every column Graphviz reads as how to draw: the picture writes them, so a data column of the same name (a
1627
3130
  * `label` attribute graphty does not draw) would draw something graphty does not. One a rule reads, whose values the