@graphty/graph-io 0.3.43 → 0.3.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (674) hide show
  1. package/dist/chunks/approximations-DJMHdDvv.js +40 -0
  2. package/dist/chunks/approximations-DJMHdDvv.js.map +1 -0
  3. package/dist/chunks/block-CPCHkX42.js +2777 -0
  4. package/dist/chunks/block-CPCHkX42.js.map +1 -0
  5. package/dist/chunks/{choose-DvpbdRuz.js → choose--jM0yYCo.js} +3 -2
  6. package/dist/chunks/{choose-DvpbdRuz.js.map → choose--jM0yYCo.js.map} +1 -1
  7. package/dist/chunks/{constants-fT3E8JYY.js → constants-BKwqelYD.js} +6 -2
  8. package/dist/chunks/constants-BKwqelYD.js.map +1 -0
  9. package/dist/chunks/convert-BcQ8_jMo.js +59 -0
  10. package/dist/chunks/convert-BcQ8_jMo.js.map +1 -0
  11. package/dist/chunks/emit-DZy6JgdA.js +3456 -0
  12. package/dist/chunks/emit-DZy6JgdA.js.map +1 -0
  13. package/dist/chunks/{escape-B62ZGM_a.js → escape-CgzxiYPg.js} +44 -21
  14. package/dist/chunks/escape-CgzxiYPg.js.map +1 -0
  15. package/dist/chunks/{export-rKLeGQMo.js → export-BvqAXCKh.js} +196 -792
  16. package/dist/chunks/export-BvqAXCKh.js.map +1 -0
  17. package/dist/chunks/{exporter-yLxKBvLE.js → exporter--Mk9gXCJ.js} +4 -3
  18. package/dist/chunks/{exporter-yLxKBvLE.js.map → exporter--Mk9gXCJ.js.map} +1 -1
  19. package/dist/chunks/{exporter-RVtXVetw.js → exporter-C-9cgZKa.js} +1805 -287
  20. package/dist/chunks/exporter-C-9cgZKa.js.map +1 -0
  21. package/dist/chunks/{exporter-8hLNemFk.js → exporter-DFqJhBnj.js} +133 -74
  22. package/dist/chunks/exporter-DFqJhBnj.js.map +1 -0
  23. package/dist/chunks/{exporter-BWwEuOPT.js → exporter-DteVeJki.js} +22 -12
  24. package/dist/chunks/exporter-DteVeJki.js.map +1 -0
  25. package/dist/chunks/format-BpQBx8MZ.js +676 -0
  26. package/dist/chunks/format-BpQBx8MZ.js.map +1 -0
  27. package/dist/chunks/generic-BGO71QBv.js +1347 -0
  28. package/dist/chunks/generic-BGO71QBv.js.map +1 -0
  29. package/dist/chunks/{importer-C98dpSH4.js → importer-B-9Rc1zO.js} +141 -82
  30. package/dist/chunks/importer-B-9Rc1zO.js.map +1 -0
  31. package/dist/chunks/{importer-D794XUTe.js → importer-BVVR740x.js} +31 -8
  32. package/dist/chunks/importer-BVVR740x.js.map +1 -0
  33. package/dist/chunks/{importer-WrGRZb0N.js → importer-C0AjA3Ax.js} +449 -102
  34. package/dist/chunks/importer-C0AjA3Ax.js.map +1 -0
  35. package/dist/chunks/{importer-BbNb7eNg.js → importer-C7pvJe1L.js} +1398 -129
  36. package/dist/chunks/importer-C7pvJe1L.js.map +1 -0
  37. package/dist/chunks/{importer-xZ-3suY1.js → importer-ClwoVx3O.js} +791 -135
  38. package/dist/chunks/importer-ClwoVx3O.js.map +1 -0
  39. package/dist/chunks/{importer-Ht7LNg8L.js → importer-D0sC_6bO.js} +116 -296
  40. package/dist/chunks/importer-D0sC_6bO.js.map +1 -0
  41. package/dist/chunks/{importer-Dz7tI8Sa.js → importer-EHQk1jbM.js} +617 -115
  42. package/dist/chunks/importer-EHQk1jbM.js.map +1 -0
  43. package/dist/chunks/{json-elements-DKSqpNW1.js → json-elements-DJZRZ6jd.js} +123 -32
  44. package/dist/chunks/json-elements-DJZRZ6jd.js.map +1 -0
  45. package/dist/chunks/lexicon-WMsNddyU.js +509 -0
  46. package/dist/chunks/lexicon-WMsNddyU.js.map +1 -0
  47. package/dist/chunks/{ontology-export-CtRXjg_i.js → ontology-export-G4MqRdux.js} +3 -2
  48. package/dist/chunks/{ontology-export-CtRXjg_i.js.map → ontology-export-G4MqRdux.js.map} +1 -1
  49. package/dist/chunks/{records-Cu4bxYOm.js → records-CiaWZrFH.js} +2 -2
  50. package/dist/chunks/{records-Cu4bxYOm.js.map → records-CiaWZrFH.js.map} +1 -1
  51. package/dist/chunks/styles-DvoFBKCK.js +690 -0
  52. package/dist/chunks/styles-DvoFBKCK.js.map +1 -0
  53. package/dist/chunks/styles-yed-Cz9rLBCP.js +842 -0
  54. package/dist/chunks/styles-yed-Cz9rLBCP.js.map +1 -0
  55. package/dist/chunks/styles-zNTmz5R7.js +2102 -0
  56. package/dist/chunks/styles-zNTmz5R7.js.map +1 -0
  57. package/dist/chunks/{text-DHOJXnED.js → text-CzJNFOS9.js} +2 -2
  58. package/dist/chunks/{text-DHOJXnED.js.map → text-CzJNFOS9.js.map} +1 -1
  59. package/dist/chunks/units-XMZFIFe1.js +19 -0
  60. package/dist/chunks/units-XMZFIFe1.js.map +1 -0
  61. package/dist/chunks/writer-gQ8kx99H.js +448 -0
  62. package/dist/chunks/writer-gQ8kx99H.js.map +1 -0
  63. package/dist/csv.js +5 -4
  64. package/dist/csv.js.map +1 -1
  65. package/dist/cx.js +1 -1
  66. package/dist/cx2.js +2 -2
  67. package/dist/cys.js +1 -1
  68. package/dist/dot.js +5 -2
  69. package/dist/dot.js.map +1 -1
  70. package/dist/gexf.js +292 -412
  71. package/dist/gexf.js.map +1 -1
  72. package/dist/gml.js +927 -74
  73. package/dist/gml.js.map +1 -1
  74. package/dist/graph-io.js +351 -236
  75. package/dist/graph-io.js.map +1 -1
  76. package/dist/graphml.js +4 -2
  77. package/dist/graphml.js.map +1 -1
  78. package/dist/json.js +1 -1
  79. package/dist/neo4j.js +4 -3
  80. package/dist/neo4j.js.map +1 -1
  81. package/dist/obo.js +1 -1
  82. package/dist/pajek.js +4 -2
  83. package/dist/positions.d.ts +1 -0
  84. package/dist/positions.js +427 -0
  85. package/dist/positions.js.map +1 -0
  86. package/dist/src/common/codes.d.ts +6 -0
  87. package/dist/src/common/codes.d.ts.map +1 -1
  88. package/dist/src/common/codes.js +6 -0
  89. package/dist/src/common/codes.js.map +1 -1
  90. package/dist/src/common/escape.d.ts.map +1 -1
  91. package/dist/src/common/escape.js +4 -2
  92. package/dist/src/common/escape.js.map +1 -1
  93. package/dist/src/common/export.d.ts +10 -0
  94. package/dist/src/common/export.d.ts.map +1 -1
  95. package/dist/src/common/export.js +123 -100
  96. package/dist/src/common/export.js.map +1 -1
  97. package/dist/src/common/input.d.ts.map +1 -1
  98. package/dist/src/common/input.js +8 -1
  99. package/dist/src/common/input.js.map +1 -1
  100. package/dist/src/common/json-elements.d.ts +12 -9
  101. package/dist/src/common/json-elements.d.ts.map +1 -1
  102. package/dist/src/common/json-elements.js +160 -37
  103. package/dist/src/common/json-elements.js.map +1 -1
  104. package/dist/src/common/notes.d.ts +42 -0
  105. package/dist/src/common/notes.d.ts.map +1 -0
  106. package/dist/src/common/notes.js +72 -0
  107. package/dist/src/common/notes.js.map +1 -0
  108. package/dist/src/common/report.d.ts +18 -0
  109. package/dist/src/common/report.d.ts.map +1 -1
  110. package/dist/src/common/report.js +36 -1
  111. package/dist/src/common/report.js.map +1 -1
  112. package/dist/src/common/units.d.ts +24 -0
  113. package/dist/src/common/units.d.ts.map +1 -0
  114. package/dist/src/common/units.js +26 -0
  115. package/dist/src/common/units.js.map +1 -0
  116. package/dist/src/common/xml.d.ts +18 -0
  117. package/dist/src/common/xml.d.ts.map +1 -1
  118. package/dist/src/common/xml.js +42 -5
  119. package/dist/src/common/xml.js.map +1 -1
  120. package/dist/src/formats/cx/exporter.d.ts +6 -0
  121. package/dist/src/formats/cx/exporter.d.ts.map +1 -1
  122. package/dist/src/formats/cx/exporter.js +71 -8
  123. package/dist/src/formats/cx/exporter.js.map +1 -1
  124. package/dist/src/formats/cx/importer.d.ts +5 -2
  125. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  126. package/dist/src/formats/cx/importer.js +102 -72
  127. package/dist/src/formats/cx/importer.js.map +1 -1
  128. package/dist/src/formats/cx/styles.d.ts +102 -0
  129. package/dist/src/formats/cx/styles.d.ts.map +1 -0
  130. package/dist/src/formats/cx/styles.js +225 -0
  131. package/dist/src/formats/cx/styles.js.map +1 -0
  132. package/dist/src/formats/cx2/exporter.d.ts +5 -0
  133. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  134. package/dist/src/formats/cx2/exporter.js +181 -71
  135. package/dist/src/formats/cx2/exporter.js.map +1 -1
  136. package/dist/src/formats/cx2/importer.d.ts +4 -1
  137. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  138. package/dist/src/formats/cx2/importer.js +19 -1
  139. package/dist/src/formats/cx2/importer.js.map +1 -1
  140. package/dist/src/formats/cx2/styles.d.ts +18 -0
  141. package/dist/src/formats/cx2/styles.d.ts.map +1 -0
  142. package/dist/src/formats/cx2/styles.js +24 -0
  143. package/dist/src/formats/cx2/styles.js.map +1 -0
  144. package/dist/src/formats/cys/constants.d.ts +2 -0
  145. package/dist/src/formats/cys/constants.d.ts.map +1 -1
  146. package/dist/src/formats/cys/constants.js +2 -0
  147. package/dist/src/formats/cys/constants.js.map +1 -1
  148. package/dist/src/formats/cys/exporter.d.ts +6 -2
  149. package/dist/src/formats/cys/exporter.d.ts.map +1 -1
  150. package/dist/src/formats/cys/exporter.js +215 -53
  151. package/dist/src/formats/cys/exporter.js.map +1 -1
  152. package/dist/src/formats/cys/importer.d.ts +2 -1
  153. package/dist/src/formats/cys/importer.d.ts.map +1 -1
  154. package/dist/src/formats/cys/importer.js +119 -6
  155. package/dist/src/formats/cys/importer.js.map +1 -1
  156. package/dist/src/formats/cys/session.d.ts +2 -0
  157. package/dist/src/formats/cys/session.d.ts.map +1 -1
  158. package/dist/src/formats/cys/session.js +5 -0
  159. package/dist/src/formats/cys/session.js.map +1 -1
  160. package/dist/src/formats/cys/styles.d.ts +38 -0
  161. package/dist/src/formats/cys/styles.d.ts.map +1 -0
  162. package/dist/src/formats/cys/styles.js +213 -0
  163. package/dist/src/formats/cys/styles.js.map +1 -0
  164. package/dist/src/formats/cys/tables.d.ts +8 -3
  165. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  166. package/dist/src/formats/cys/tables.js +88 -34
  167. package/dist/src/formats/cys/tables.js.map +1 -1
  168. package/dist/src/formats/dot/exporter.d.ts +2 -0
  169. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  170. package/dist/src/formats/dot/exporter.js +195 -81
  171. package/dist/src/formats/dot/exporter.js.map +1 -1
  172. package/dist/src/formats/dot/importer.d.ts.map +1 -1
  173. package/dist/src/formats/dot/importer.js +100 -20
  174. package/dist/src/formats/dot/importer.js.map +1 -1
  175. package/dist/src/formats/dot/index.d.ts +1 -0
  176. package/dist/src/formats/dot/index.d.ts.map +1 -1
  177. package/dist/src/formats/dot/index.js +1 -0
  178. package/dist/src/formats/dot/index.js.map +1 -1
  179. package/dist/src/formats/dot/positions.d.ts +14 -0
  180. package/dist/src/formats/dot/positions.d.ts.map +1 -0
  181. package/dist/src/formats/dot/positions.js +70 -0
  182. package/dist/src/formats/dot/positions.js.map +1 -0
  183. package/dist/src/formats/dot/styles.d.ts +82 -0
  184. package/dist/src/formats/dot/styles.d.ts.map +1 -0
  185. package/dist/src/formats/dot/styles.js +1496 -0
  186. package/dist/src/formats/dot/styles.js.map +1 -0
  187. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  188. package/dist/src/formats/gexf/exporter.js +20 -5
  189. package/dist/src/formats/gexf/exporter.js.map +1 -1
  190. package/dist/src/formats/gexf/importer.d.ts +8 -1
  191. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  192. package/dist/src/formats/gexf/importer.js +251 -131
  193. package/dist/src/formats/gexf/importer.js.map +1 -1
  194. package/dist/src/formats/gexf/index.d.ts +8 -0
  195. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  196. package/dist/src/formats/gexf/index.js +10 -2
  197. package/dist/src/formats/gexf/index.js.map +1 -1
  198. package/dist/src/formats/gexf/schema.d.ts +5 -0
  199. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  200. package/dist/src/formats/gexf/schema.js +5 -0
  201. package/dist/src/formats/gexf/schema.js.map +1 -1
  202. package/dist/src/formats/gexf/styles.d.ts +74 -0
  203. package/dist/src/formats/gexf/styles.d.ts.map +1 -0
  204. package/dist/src/formats/gexf/styles.js +604 -0
  205. package/dist/src/formats/gexf/styles.js.map +1 -0
  206. package/dist/src/formats/gml/exporter.d.ts +1 -5
  207. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  208. package/dist/src/formats/gml/exporter.js +19 -8
  209. package/dist/src/formats/gml/exporter.js.map +1 -1
  210. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  211. package/dist/src/formats/gml/importer.js +4 -0
  212. package/dist/src/formats/gml/importer.js.map +1 -1
  213. package/dist/src/formats/gml/index.d.ts +2 -1
  214. package/dist/src/formats/gml/index.d.ts.map +1 -1
  215. package/dist/src/formats/gml/index.js +2 -1
  216. package/dist/src/formats/gml/index.js.map +1 -1
  217. package/dist/src/formats/gml/positions.d.ts +14 -0
  218. package/dist/src/formats/gml/positions.d.ts.map +1 -0
  219. package/dist/src/formats/gml/positions.js +46 -0
  220. package/dist/src/formats/gml/positions.js.map +1 -0
  221. package/dist/src/formats/gml/styles.d.ts +59 -0
  222. package/dist/src/formats/gml/styles.d.ts.map +1 -0
  223. package/dist/src/formats/gml/styles.js +1056 -0
  224. package/dist/src/formats/gml/styles.js.map +1 -0
  225. package/dist/src/formats/gml/syntax.d.ts +11 -0
  226. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  227. package/dist/src/formats/gml/syntax.js +151 -62
  228. package/dist/src/formats/gml/syntax.js.map +1 -1
  229. package/dist/src/formats/graphml/constants.d.ts +4 -0
  230. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  231. package/dist/src/formats/graphml/constants.js +5 -1
  232. package/dist/src/formats/graphml/constants.js.map +1 -1
  233. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  234. package/dist/src/formats/graphml/exporter.js +133 -65
  235. package/dist/src/formats/graphml/exporter.js.map +1 -1
  236. package/dist/src/formats/graphml/importer.d.ts +4 -1
  237. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  238. package/dist/src/formats/graphml/importer.js +100 -44
  239. package/dist/src/formats/graphml/importer.js.map +1 -1
  240. package/dist/src/formats/graphml/index.d.ts +1 -0
  241. package/dist/src/formats/graphml/index.d.ts.map +1 -1
  242. package/dist/src/formats/graphml/index.js +1 -0
  243. package/dist/src/formats/graphml/index.js.map +1 -1
  244. package/dist/src/formats/graphml/styles-plain.d.ts +19 -0
  245. package/dist/src/formats/graphml/styles-plain.d.ts.map +1 -0
  246. package/dist/src/formats/graphml/styles-plain.js +187 -0
  247. package/dist/src/formats/graphml/styles-plain.js.map +1 -0
  248. package/dist/src/formats/graphml/styles-yed.d.ts +179 -0
  249. package/dist/src/formats/graphml/styles-yed.d.ts.map +1 -0
  250. package/dist/src/formats/graphml/styles-yed.js +1086 -0
  251. package/dist/src/formats/graphml/styles-yed.js.map +1 -0
  252. package/dist/src/formats/graphml/styles-yfiles3.d.ts +19 -0
  253. package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -0
  254. package/dist/src/formats/graphml/styles-yfiles3.js +508 -0
  255. package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -0
  256. package/dist/src/formats/graphml/tree.d.ts.map +1 -1
  257. package/dist/src/formats/graphml/tree.js +34 -19
  258. package/dist/src/formats/graphml/tree.js.map +1 -1
  259. package/dist/src/formats/graphml/yfiles.d.ts +16 -2
  260. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  261. package/dist/src/formats/graphml/yfiles.js +83 -12
  262. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  263. package/dist/src/formats/json/exporter.d.ts +5 -0
  264. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  265. package/dist/src/formats/json/exporter.js +68 -25
  266. package/dist/src/formats/json/exporter.js.map +1 -1
  267. package/dist/src/formats/json/importer.d.ts +22 -3
  268. package/dist/src/formats/json/importer.d.ts.map +1 -1
  269. package/dist/src/formats/json/importer.js +439 -264
  270. package/dist/src/formats/json/importer.js.map +1 -1
  271. package/dist/src/formats/json/positions.d.ts +16 -0
  272. package/dist/src/formats/json/positions.d.ts.map +1 -0
  273. package/dist/src/formats/json/positions.js +44 -0
  274. package/dist/src/formats/json/positions.js.map +1 -0
  275. package/dist/src/formats/json/styles.d.ts +102 -0
  276. package/dist/src/formats/json/styles.d.ts.map +1 -0
  277. package/dist/src/formats/json/styles.js +1753 -0
  278. package/dist/src/formats/json/styles.js.map +1 -0
  279. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  280. package/dist/src/formats/neo4j/exporter.js +2 -1
  281. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  282. package/dist/src/formats/pajek/exporter.d.ts +2 -0
  283. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  284. package/dist/src/formats/pajek/exporter.js +142 -76
  285. package/dist/src/formats/pajek/exporter.js.map +1 -1
  286. package/dist/src/formats/pajek/importer.d.ts +4 -0
  287. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  288. package/dist/src/formats/pajek/importer.js +60 -6
  289. package/dist/src/formats/pajek/importer.js.map +1 -1
  290. package/dist/src/formats/pajek/index.d.ts +2 -0
  291. package/dist/src/formats/pajek/index.d.ts.map +1 -1
  292. package/dist/src/formats/pajek/index.js +2 -0
  293. package/dist/src/formats/pajek/index.js.map +1 -1
  294. package/dist/src/formats/pajek/styles.d.ts +50 -0
  295. package/dist/src/formats/pajek/styles.d.ts.map +1 -0
  296. package/dist/src/formats/pajek/styles.js +569 -0
  297. package/dist/src/formats/pajek/styles.js.map +1 -0
  298. package/dist/src/formats/pajek/syntax.d.ts +6 -0
  299. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  300. package/dist/src/formats/pajek/syntax.js +6 -0
  301. package/dist/src/formats/pajek/syntax.js.map +1 -1
  302. package/dist/src/formats/xgmml/columns.d.ts.map +1 -1
  303. package/dist/src/formats/xgmml/columns.js +34 -9
  304. package/dist/src/formats/xgmml/columns.js.map +1 -1
  305. package/dist/src/formats/xgmml/constants.d.ts +2 -0
  306. package/dist/src/formats/xgmml/constants.d.ts.map +1 -1
  307. package/dist/src/formats/xgmml/constants.js +3 -1
  308. package/dist/src/formats/xgmml/constants.js.map +1 -1
  309. package/dist/src/formats/xgmml/document.d.ts +12 -8
  310. package/dist/src/formats/xgmml/document.d.ts.map +1 -1
  311. package/dist/src/formats/xgmml/document.js +56 -13
  312. package/dist/src/formats/xgmml/document.js.map +1 -1
  313. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  314. package/dist/src/formats/xgmml/emit.js +12 -6
  315. package/dist/src/formats/xgmml/emit.js.map +1 -1
  316. package/dist/src/formats/xgmml/exporter.d.ts +3 -1
  317. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  318. package/dist/src/formats/xgmml/exporter.js +22 -9
  319. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  320. package/dist/src/formats/xgmml/importer.d.ts +2 -1
  321. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  322. package/dist/src/formats/xgmml/importer.js +31 -24
  323. package/dist/src/formats/xgmml/importer.js.map +1 -1
  324. package/dist/src/formats/xgmml/styles.d.ts +104 -0
  325. package/dist/src/formats/xgmml/styles.d.ts.map +1 -0
  326. package/dist/src/formats/xgmml/styles.js +310 -0
  327. package/dist/src/formats/xgmml/styles.js.map +1 -0
  328. package/dist/src/index.d.ts +9 -7
  329. package/dist/src/index.d.ts.map +1 -1
  330. package/dist/src/index.js +7 -7
  331. package/dist/src/index.js.map +1 -1
  332. package/dist/src/positions/codes.d.ts +31 -0
  333. package/dist/src/positions/codes.d.ts.map +1 -0
  334. package/dist/src/positions/codes.js +26 -0
  335. package/dist/src/positions/codes.js.map +1 -0
  336. package/dist/src/positions/convert.d.ts +58 -0
  337. package/dist/src/positions/convert.d.ts.map +1 -0
  338. package/dist/src/positions/convert.js +107 -0
  339. package/dist/src/positions/convert.js.map +1 -0
  340. package/dist/src/positions/extract.d.ts +27 -0
  341. package/dist/src/positions/extract.d.ts.map +1 -0
  342. package/dist/src/positions/extract.js +526 -0
  343. package/dist/src/positions/extract.js.map +1 -0
  344. package/dist/src/positions/index.d.ts +10 -0
  345. package/dist/src/positions/index.d.ts.map +1 -0
  346. package/dist/src/positions/index.js +9 -0
  347. package/dist/src/positions/index.js.map +1 -0
  348. package/dist/src/positions/types.d.ts +208 -0
  349. package/dist/src/positions/types.d.ts.map +1 -0
  350. package/dist/src/positions/types.js +10 -0
  351. package/dist/src/positions/types.js.map +1 -0
  352. package/dist/src/registry.d.ts +30 -0
  353. package/dist/src/registry.d.ts.map +1 -1
  354. package/dist/src/registry.js +130 -9
  355. package/dist/src/registry.js.map +1 -1
  356. package/dist/src/styles/annotations.d.ts +132 -0
  357. package/dist/src/styles/annotations.d.ts.map +1 -0
  358. package/dist/src/styles/annotations.js +320 -0
  359. package/dist/src/styles/annotations.js.map +1 -0
  360. package/dist/src/styles/budget.d.ts +45 -0
  361. package/dist/src/styles/budget.d.ts.map +1 -0
  362. package/dist/src/styles/budget.js +58 -0
  363. package/dist/src/styles/budget.js.map +1 -0
  364. package/dist/src/styles/canon/bends.d.ts +32 -0
  365. package/dist/src/styles/canon/bends.d.ts.map +1 -0
  366. package/dist/src/styles/canon/bends.js +74 -0
  367. package/dist/src/styles/canon/bends.js.map +1 -0
  368. package/dist/src/styles/canon/color-tables.d.ts +22 -0
  369. package/dist/src/styles/canon/color-tables.d.ts.map +1 -0
  370. package/dist/src/styles/canon/color-tables.js +369 -0
  371. package/dist/src/styles/canon/color-tables.js.map +1 -0
  372. package/dist/src/styles/canon/color.d.ts +66 -0
  373. package/dist/src/styles/canon/color.d.ts.map +1 -0
  374. package/dist/src/styles/canon/color.js +548 -0
  375. package/dist/src/styles/canon/color.js.map +1 -0
  376. package/dist/src/styles/canon/enums.d.ts +25 -0
  377. package/dist/src/styles/canon/enums.d.ts.map +1 -0
  378. package/dist/src/styles/canon/enums.js +187 -0
  379. package/dist/src/styles/canon/enums.js.map +1 -0
  380. package/dist/src/styles/canon/graphviz.d.ts +48 -0
  381. package/dist/src/styles/canon/graphviz.d.ts.map +1 -0
  382. package/dist/src/styles/canon/graphviz.js +160 -0
  383. package/dist/src/styles/canon/graphviz.js.map +1 -0
  384. package/dist/src/styles/canon/index.d.ts +13 -0
  385. package/dist/src/styles/canon/index.d.ts.map +1 -0
  386. package/dist/src/styles/canon/index.js +13 -0
  387. package/dist/src/styles/canon/index.js.map +1 -0
  388. package/dist/src/styles/canon/number.d.ts +59 -0
  389. package/dist/src/styles/canon/number.d.ts.map +1 -0
  390. package/dist/src/styles/canon/number.js +142 -0
  391. package/dist/src/styles/canon/number.js.map +1 -0
  392. package/dist/src/styles/canon/site.d.ts +51 -0
  393. package/dist/src/styles/canon/site.d.ts.map +1 -0
  394. package/dist/src/styles/canon/site.js +63 -0
  395. package/dist/src/styles/canon/site.js.map +1 -0
  396. package/dist/src/styles/canon/text.d.ts +61 -0
  397. package/dist/src/styles/canon/text.d.ts.map +1 -0
  398. package/dist/src/styles/canon/text.js +199 -0
  399. package/dist/src/styles/canon/text.js.map +1 -0
  400. package/dist/src/styles/codes.d.ts +95 -0
  401. package/dist/src/styles/codes.d.ts.map +1 -0
  402. package/dist/src/styles/codes.js +97 -0
  403. package/dist/src/styles/codes.js.map +1 -0
  404. package/dist/src/styles/cyjs/cascade.d.ts +134 -0
  405. package/dist/src/styles/cyjs/cascade.d.ts.map +1 -0
  406. package/dist/src/styles/cyjs/cascade.js +2252 -0
  407. package/dist/src/styles/cyjs/cascade.js.map +1 -0
  408. package/dist/src/styles/cyjs/defaults-table.d.ts +25 -0
  409. package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -0
  410. package/dist/src/styles/cyjs/defaults-table.js +76 -0
  411. package/dist/src/styles/cyjs/defaults-table.js.map +1 -0
  412. package/dist/src/styles/cyjs/emit.d.ts +82 -0
  413. package/dist/src/styles/cyjs/emit.d.ts.map +1 -0
  414. package/dist/src/styles/cyjs/emit.js +791 -0
  415. package/dist/src/styles/cyjs/emit.js.map +1 -0
  416. package/dist/src/styles/cyjs/index.d.ts +13 -0
  417. package/dist/src/styles/cyjs/index.d.ts.map +1 -0
  418. package/dist/src/styles/cyjs/index.js +13 -0
  419. package/dist/src/styles/cyjs/index.js.map +1 -0
  420. package/dist/src/styles/cyjs/live.d.ts +46 -0
  421. package/dist/src/styles/cyjs/live.d.ts.map +1 -0
  422. package/dist/src/styles/cyjs/live.js +108 -0
  423. package/dist/src/styles/cyjs/live.js.map +1 -0
  424. package/dist/src/styles/cyjs/mappers.d.ts +147 -0
  425. package/dist/src/styles/cyjs/mappers.d.ts.map +1 -0
  426. package/dist/src/styles/cyjs/mappers.js +744 -0
  427. package/dist/src/styles/cyjs/mappers.js.map +1 -0
  428. package/dist/src/styles/cyjs/selector.d.ts +113 -0
  429. package/dist/src/styles/cyjs/selector.d.ts.map +1 -0
  430. package/dist/src/styles/cyjs/selector.js +611 -0
  431. package/dist/src/styles/cyjs/selector.js.map +1 -0
  432. package/dist/src/styles/cyjs/stylesheet-text.d.ts +37 -0
  433. package/dist/src/styles/cyjs/stylesheet-text.d.ts.map +1 -0
  434. package/dist/src/styles/cyjs/stylesheet-text.js +181 -0
  435. package/dist/src/styles/cyjs/stylesheet-text.js.map +1 -0
  436. package/dist/src/styles/cytoscape/emit.d.ts +125 -0
  437. package/dist/src/styles/cytoscape/emit.d.ts.map +1 -0
  438. package/dist/src/styles/cytoscape/emit.js +1025 -0
  439. package/dist/src/styles/cytoscape/emit.js.map +1 -0
  440. package/dist/src/styles/cytoscape/index.d.ts +10 -0
  441. package/dist/src/styles/cytoscape/index.d.ts.map +1 -0
  442. package/dist/src/styles/cytoscape/index.js +10 -0
  443. package/dist/src/styles/cytoscape/index.js.map +1 -0
  444. package/dist/src/styles/cytoscape/lexicon.d.ts +49 -0
  445. package/dist/src/styles/cytoscape/lexicon.d.ts.map +1 -0
  446. package/dist/src/styles/cytoscape/lexicon.js +605 -0
  447. package/dist/src/styles/cytoscape/lexicon.js.map +1 -0
  448. package/dist/src/styles/cytoscape/mappings.d.ts +171 -0
  449. package/dist/src/styles/cytoscape/mappings.d.ts.map +1 -0
  450. package/dist/src/styles/cytoscape/mappings.js +460 -0
  451. package/dist/src/styles/cytoscape/mappings.js.map +1 -0
  452. package/dist/src/styles/cytoscape/resolve.d.ts +137 -0
  453. package/dist/src/styles/cytoscape/resolve.d.ts.map +1 -0
  454. package/dist/src/styles/cytoscape/resolve.js +381 -0
  455. package/dist/src/styles/cytoscape/resolve.js.map +1 -0
  456. package/dist/src/styles/cytoscape/translate.d.ts +50 -0
  457. package/dist/src/styles/cytoscape/translate.d.ts.map +1 -0
  458. package/dist/src/styles/cytoscape/translate.js +156 -0
  459. package/dist/src/styles/cytoscape/translate.js.map +1 -0
  460. package/dist/src/styles/defaults.d.ts +90 -0
  461. package/dist/src/styles/defaults.d.ts.map +1 -0
  462. package/dist/src/styles/defaults.js +414 -0
  463. package/dist/src/styles/defaults.js.map +1 -0
  464. package/dist/src/styles/index.d.ts +15 -0
  465. package/dist/src/styles/index.d.ts.map +1 -0
  466. package/dist/src/styles/index.js +14 -0
  467. package/dist/src/styles/index.js.map +1 -0
  468. package/dist/src/styles/internal.d.ts +17 -0
  469. package/dist/src/styles/internal.d.ts.map +1 -0
  470. package/dist/src/styles/internal.js +16 -0
  471. package/dist/src/styles/internal.js.map +1 -0
  472. package/dist/src/styles/io/approximations.d.ts +24 -0
  473. package/dist/src/styles/io/approximations.d.ts.map +1 -0
  474. package/dist/src/styles/io/approximations.js +51 -0
  475. package/dist/src/styles/io/approximations.js.map +1 -0
  476. package/dist/src/styles/io/block.d.ts +166 -0
  477. package/dist/src/styles/io/block.d.ts.map +1 -0
  478. package/dist/src/styles/io/block.js +930 -0
  479. package/dist/src/styles/io/block.js.map +1 -0
  480. package/dist/src/styles/io/generic.d.ts +88 -0
  481. package/dist/src/styles/io/generic.d.ts.map +1 -0
  482. package/dist/src/styles/io/generic.js +516 -0
  483. package/dist/src/styles/io/generic.js.map +1 -0
  484. package/dist/src/styles/io/index.d.ts +8 -0
  485. package/dist/src/styles/io/index.d.ts.map +1 -0
  486. package/dist/src/styles/io/index.js +7 -0
  487. package/dist/src/styles/io/index.js.map +1 -0
  488. package/dist/src/styles/io/writer.d.ts +89 -0
  489. package/dist/src/styles/io/writer.d.ts.map +1 -0
  490. package/dist/src/styles/io/writer.js +626 -0
  491. package/dist/src/styles/io/writer.js.map +1 -0
  492. package/dist/src/styles/notes.d.ts +47 -0
  493. package/dist/src/styles/notes.d.ts.map +1 -0
  494. package/dist/src/styles/notes.js +140 -0
  495. package/dist/src/styles/notes.js.map +1 -0
  496. package/dist/src/styles/plan/classes.d.ts +31 -0
  497. package/dist/src/styles/plan/classes.d.ts.map +1 -0
  498. package/dist/src/styles/plan/classes.js +139 -0
  499. package/dist/src/styles/plan/classes.js.map +1 -0
  500. package/dist/src/styles/plan/continuous.d.ts +32 -0
  501. package/dist/src/styles/plan/continuous.d.ts.map +1 -0
  502. package/dist/src/styles/plan/continuous.js +165 -0
  503. package/dist/src/styles/plan/continuous.js.map +1 -0
  504. package/dist/src/styles/plan/discrete.d.ts +39 -0
  505. package/dist/src/styles/plan/discrete.d.ts.map +1 -0
  506. package/dist/src/styles/plan/discrete.js +127 -0
  507. package/dist/src/styles/plan/discrete.js.map +1 -0
  508. package/dist/src/styles/plan/families.d.ts +31 -0
  509. package/dist/src/styles/plan/families.d.ts.map +1 -0
  510. package/dist/src/styles/plan/families.js +95 -0
  511. package/dist/src/styles/plan/families.js.map +1 -0
  512. package/dist/src/styles/plan/features.d.ts +58 -0
  513. package/dist/src/styles/plan/features.d.ts.map +1 -0
  514. package/dist/src/styles/plan/features.js +185 -0
  515. package/dist/src/styles/plan/features.js.map +1 -0
  516. package/dist/src/styles/plan/index.d.ts +7 -0
  517. package/dist/src/styles/plan/index.d.ts.map +1 -0
  518. package/dist/src/styles/plan/index.js +7 -0
  519. package/dist/src/styles/plan/index.js.map +1 -0
  520. package/dist/src/styles/plan/plan.d.ts +71 -0
  521. package/dist/src/styles/plan/plan.d.ts.map +1 -0
  522. package/dist/src/styles/plan/plan.js +893 -0
  523. package/dist/src/styles/plan/plan.js.map +1 -0
  524. package/dist/src/styles/plan/resolve.d.ts +137 -0
  525. package/dist/src/styles/plan/resolve.d.ts.map +1 -0
  526. package/dist/src/styles/plan/resolve.js +587 -0
  527. package/dist/src/styles/plan/resolve.js.map +1 -0
  528. package/dist/src/styles/types.d.ts +702 -0
  529. package/dist/src/styles/types.d.ts.map +1 -0
  530. package/dist/src/styles/types.js +10 -0
  531. package/dist/src/styles/types.js.map +1 -0
  532. package/dist/src/styles/vocabulary.d.ts +41 -0
  533. package/dist/src/styles/vocabulary.d.ts.map +1 -0
  534. package/dist/src/styles/vocabulary.js +140 -0
  535. package/dist/src/styles/vocabulary.js.map +1 -0
  536. package/dist/src/types.d.ts +64 -1
  537. package/dist/src/types.d.ts.map +1 -1
  538. package/dist/src/types.js.map +1 -1
  539. package/dist/styles.d.ts +1 -0
  540. package/dist/styles.js +322 -0
  541. package/dist/styles.js.map +1 -0
  542. package/dist/xgmml.js +5 -4
  543. package/dist/xgmml.js.map +1 -1
  544. package/package.json +13 -2
  545. package/src/common/codes.ts +7 -0
  546. package/src/common/escape.ts +5 -2
  547. package/src/common/export.ts +179 -140
  548. package/src/common/input.ts +8 -4
  549. package/src/common/json-elements.ts +169 -38
  550. package/src/common/notes.ts +94 -0
  551. package/src/common/report.ts +41 -1
  552. package/src/common/units.ts +42 -0
  553. package/src/common/xml.ts +45 -5
  554. package/src/formats/cx/exporter.ts +95 -11
  555. package/src/formats/cx/importer.ts +126 -90
  556. package/src/formats/cx/styles.ts +304 -0
  557. package/src/formats/cx2/exporter.ts +234 -77
  558. package/src/formats/cx2/importer.ts +19 -1
  559. package/src/formats/cx2/styles.ts +29 -0
  560. package/src/formats/cys/constants.ts +3 -0
  561. package/src/formats/cys/exporter.ts +246 -60
  562. package/src/formats/cys/importer.ts +125 -9
  563. package/src/formats/cys/session.ts +6 -0
  564. package/src/formats/cys/styles.ts +254 -0
  565. package/src/formats/cys/tables.ts +117 -48
  566. package/src/formats/dot/exporter.ts +262 -126
  567. package/src/formats/dot/importer.ts +111 -20
  568. package/src/formats/dot/index.ts +1 -0
  569. package/src/formats/dot/positions.ts +77 -0
  570. package/src/formats/dot/styles.ts +1755 -0
  571. package/src/formats/gexf/exporter.ts +27 -11
  572. package/src/formats/gexf/importer.ts +308 -158
  573. package/src/formats/gexf/index.ts +10 -0
  574. package/src/formats/gexf/schema.ts +5 -0
  575. package/src/formats/gexf/styles.ts +719 -0
  576. package/src/formats/gml/exporter.ts +33 -14
  577. package/src/formats/gml/importer.ts +4 -0
  578. package/src/formats/gml/index.ts +2 -1
  579. package/src/formats/gml/positions.ts +52 -0
  580. package/src/formats/gml/styles.ts +1248 -0
  581. package/src/formats/gml/syntax.ts +173 -62
  582. package/src/formats/graphml/constants.ts +5 -0
  583. package/src/formats/graphml/exporter.ts +163 -71
  584. package/src/formats/graphml/importer.ts +119 -45
  585. package/src/formats/graphml/index.ts +1 -0
  586. package/src/formats/graphml/styles-plain.ts +236 -0
  587. package/src/formats/graphml/styles-yed.ts +1221 -0
  588. package/src/formats/graphml/styles-yfiles3.ts +573 -0
  589. package/src/formats/graphml/tree.ts +36 -20
  590. package/src/formats/graphml/yfiles.ts +89 -13
  591. package/src/formats/json/exporter.ts +76 -26
  592. package/src/formats/json/importer.ts +541 -322
  593. package/src/formats/json/positions.ts +50 -0
  594. package/src/formats/json/styles.ts +2082 -0
  595. package/src/formats/neo4j/exporter.ts +2 -1
  596. package/src/formats/pajek/exporter.ts +195 -106
  597. package/src/formats/pajek/importer.ts +78 -9
  598. package/src/formats/pajek/index.ts +2 -0
  599. package/src/formats/pajek/styles.ts +685 -0
  600. package/src/formats/pajek/syntax.ts +7 -0
  601. package/src/formats/xgmml/columns.ts +39 -9
  602. package/src/formats/xgmml/constants.ts +3 -0
  603. package/src/formats/xgmml/document.ts +54 -12
  604. package/src/formats/xgmml/emit.ts +11 -6
  605. package/src/formats/xgmml/exporter.ts +32 -13
  606. package/src/formats/xgmml/importer.ts +43 -29
  607. package/src/formats/xgmml/styles.ts +400 -0
  608. package/src/index.ts +16 -0
  609. package/src/positions/codes.ts +32 -0
  610. package/src/positions/convert.ts +123 -0
  611. package/src/positions/extract.ts +619 -0
  612. package/src/positions/index.ts +23 -0
  613. package/src/positions/types.ts +221 -0
  614. package/src/registry.ts +179 -8
  615. package/src/styles/annotations.ts +478 -0
  616. package/src/styles/budget.ts +60 -0
  617. package/src/styles/canon/bends.ts +84 -0
  618. package/src/styles/canon/color-tables.ts +386 -0
  619. package/src/styles/canon/color.ts +624 -0
  620. package/src/styles/canon/enums.ts +211 -0
  621. package/src/styles/canon/graphviz.ts +189 -0
  622. package/src/styles/canon/index.ts +13 -0
  623. package/src/styles/canon/number.ts +152 -0
  624. package/src/styles/canon/site.ts +87 -0
  625. package/src/styles/canon/text.ts +221 -0
  626. package/src/styles/codes.ts +104 -0
  627. package/src/styles/cyjs/cascade.ts +2670 -0
  628. package/src/styles/cyjs/defaults-table.ts +82 -0
  629. package/src/styles/cyjs/emit.ts +952 -0
  630. package/src/styles/cyjs/index.ts +37 -0
  631. package/src/styles/cyjs/live.ts +147 -0
  632. package/src/styles/cyjs/mappers.ts +879 -0
  633. package/src/styles/cyjs/selector.ts +778 -0
  634. package/src/styles/cyjs/stylesheet-text.ts +214 -0
  635. package/src/styles/cytoscape/emit.ts +1264 -0
  636. package/src/styles/cytoscape/index.ts +40 -0
  637. package/src/styles/cytoscape/lexicon.ts +658 -0
  638. package/src/styles/cytoscape/mappings.ts +603 -0
  639. package/src/styles/cytoscape/resolve.ts +521 -0
  640. package/src/styles/cytoscape/translate.ts +237 -0
  641. package/src/styles/defaults.ts +504 -0
  642. package/src/styles/index.ts +57 -0
  643. package/src/styles/internal.ts +17 -0
  644. package/src/styles/io/approximations.ts +47 -0
  645. package/src/styles/io/block.ts +1138 -0
  646. package/src/styles/io/generic.ts +687 -0
  647. package/src/styles/io/index.ts +18 -0
  648. package/src/styles/io/writer.ts +780 -0
  649. package/src/styles/notes.ts +165 -0
  650. package/src/styles/plan/classes.ts +166 -0
  651. package/src/styles/plan/continuous.ts +218 -0
  652. package/src/styles/plan/discrete.ts +165 -0
  653. package/src/styles/plan/families.ts +115 -0
  654. package/src/styles/plan/features.ts +234 -0
  655. package/src/styles/plan/index.ts +7 -0
  656. package/src/styles/plan/plan.ts +1129 -0
  657. package/src/styles/plan/resolve.ts +685 -0
  658. package/src/styles/types.ts +814 -0
  659. package/src/styles/vocabulary.ts +172 -0
  660. package/src/types.ts +76 -1
  661. package/dist/chunks/constants-fT3E8JYY.js.map +0 -1
  662. package/dist/chunks/escape-B62ZGM_a.js.map +0 -1
  663. package/dist/chunks/export-rKLeGQMo.js.map +0 -1
  664. package/dist/chunks/exporter-8hLNemFk.js.map +0 -1
  665. package/dist/chunks/exporter-BWwEuOPT.js.map +0 -1
  666. package/dist/chunks/exporter-RVtXVetw.js.map +0 -1
  667. package/dist/chunks/importer-BbNb7eNg.js.map +0 -1
  668. package/dist/chunks/importer-C98dpSH4.js.map +0 -1
  669. package/dist/chunks/importer-D794XUTe.js.map +0 -1
  670. package/dist/chunks/importer-Dz7tI8Sa.js.map +0 -1
  671. package/dist/chunks/importer-Ht7LNg8L.js.map +0 -1
  672. package/dist/chunks/importer-WrGRZb0N.js.map +0 -1
  673. package/dist/chunks/importer-xZ-3suY1.js.map +0 -1
  674. package/dist/chunks/json-elements-DKSqpNW1.js.map +0 -1
@@ -0,0 +1,2670 @@
1
+ /**
2
+ * The Cytoscape.js style engine: a stylesheet applied to the raw element JSON the way Cytoscape.js 3.34 applies it,
3
+ * giving what Cytoscape.js draws for every node and edge, and the stylesheet's rules as neutral rules.
4
+ *
5
+ * - Elements are taken as `cy.add()` takes them: nodes first, ids made text, duplicates and edges to missing nodes
6
+ * dropped, compound parents linked without cycles.
7
+ * - Comparisons read the RAW element data (a field of mixed types is compared with `===`, not as graph-io's column
8
+ * holds it), and a dotted field reads a nested path.
9
+ * - Beneath the file's rules lie Cytoscape.js's own default rules (edges 3px wide, compound parents gray rectangles,
10
+ * selected elements blue); per property the last matching rule wins; a mapper that fails resets the property to
11
+ * its Cytoscape.js default; an element's own `style` is a bypass over every rule.
12
+ * - Every selector is evaluated through indexes (class, id, value) where one test decides, so a sheet of 20,000
13
+ * per-value rules or a selector of 100,000 class tests costs one pass over the elements it names. Every step spends
14
+ * from the extraction's one counted budget.
15
+ *
16
+ * The rules: a selector a short predicate expresses exactly over graph-io's columns becomes that predicate; any other
17
+ * becomes a membership style column (`member.<n>`) with a short `has` test. A run of three or more rules that each
18
+ * test one class, one id or one value of the same field and only set values becomes ONE mapping rule over a winner
19
+ * column. A `data()` or `mapData()` mapper becomes a passthrough or continuous mapping when graph-io's column gives
20
+ * exactly Cytoscape.js's values, else the mapper's per-element values as a style column.
21
+ */
22
+
23
+ import type { GraphSnapshot } from "@graphty/graph-format";
24
+
25
+ import { StyleBudget } from "../budget.js";
26
+ import type { CanonSite } from "../canon/site.js";
27
+ import { STYLE_ISSUE } from "../codes.js";
28
+ import { StyleNotes } from "../notes.js";
29
+ import { canonicalFor, degreeValues, evalContinuous, numberOf, plainCell } from "../plan/resolve.js";
30
+ import type {
31
+ ColumnRef,
32
+ ContinuousPoint,
33
+ ImporterStyles,
34
+ Mapping,
35
+ Predicate,
36
+ ReaderOutput,
37
+ Scalar,
38
+ StyleColumn,
39
+ StyleRule,
40
+ StyleTarget,
41
+ StyleValue,
42
+ StyleWorkBudget,
43
+ } from "../types.js";
44
+ import { CYJS_DEFAULT_STYLESHEET, CYJS_PROPERTY_DEFAULTS } from "./defaults-table.js";
45
+ import {
46
+ applyCyjsMapper,
47
+ CYJS_THROWS,
48
+ type CyjsFlat,
49
+ cyjsNeutral,
50
+ type CyjsParsed,
51
+ cyjsProperties,
52
+ cyjsPropertyName,
53
+ type CyjsPropertySpec,
54
+ cyjsRendered,
55
+ parseCyjsValue,
56
+ } from "./mappers.js";
57
+ import { type CyjsCheck, type CyjsQuery, type CyjsSelector, parseCyjsSelector } from "./selector.js";
58
+ import { type CyjsSheetEntry, parseCyjsStylesheetText } from "./stylesheet-text.js";
59
+
60
+ // ============================================================ elements
61
+
62
+ /** One element as a Cytoscape.js core holds it. */
63
+ export interface CyjsElement {
64
+ /** The element's JSON as given. */
65
+ readonly json: Readonly<Record<string, unknown>>;
66
+ /** Its `data` object as given. */
67
+ readonly data: Readonly<Record<string, unknown>>;
68
+ /** Fields Cytoscape.js rewrites on add: an id made text or generated, endpoints and parent made text. */
69
+ readonly overrides: ReadonlyMap<string, unknown>;
70
+ /** The id. */
71
+ readonly id: string;
72
+ /** The classes. */
73
+ readonly classes: ReadonlySet<unknown>;
74
+ /** `:selected`. */
75
+ readonly selected: boolean;
76
+ /** `:selectable`. */
77
+ readonly selectable: boolean;
78
+ /** `:locked`. */
79
+ readonly locked: boolean;
80
+ /** `:grabbable`. */
81
+ readonly grabbable: boolean;
82
+ }
83
+
84
+ /** The elements of a Cytoscape.js core, with their links. */
85
+ export interface CyjsGraph {
86
+ /** Nodes in the order the core holds them. */
87
+ readonly nodes: readonly CyjsElement[];
88
+ /** Edges in the order the core holds them. */
89
+ readonly edges: readonly CyjsElement[];
90
+ /** Each edge's source node. */
91
+ readonly source: Int32Array;
92
+ /** Each edge's target node. */
93
+ readonly target: Int32Array;
94
+ /** Each node's compound parent, or -1. */
95
+ readonly parent: Int32Array;
96
+ /** Each node's children. */
97
+ readonly children: readonly (readonly number[])[];
98
+ /** Each node's incident edges (a loop once). */
99
+ readonly incident: readonly (readonly number[])[];
100
+ }
101
+
102
+ /**
103
+ * Whether a value is a plain object (not an array).
104
+ * @param v - the value
105
+ * @returns true for an object
106
+ */
107
+ function isObject(v: unknown): v is Record<string, unknown> {
108
+ return v !== null && typeof v === "object" && !Array.isArray(v);
109
+ }
110
+
111
+ /**
112
+ * An id as Cytoscape.js keeps it: a finite number made text, a non-blank string as it is; null when Cytoscape.js
113
+ * refuses it.
114
+ * @param v - the value
115
+ * @returns the id, or null
116
+ */
117
+ function idText(v: unknown): string | null {
118
+ if (typeof v === "number" && !Number.isNaN(v)) {
119
+ return String(v);
120
+ }
121
+ if (typeof v === "string" && v !== "" && !/^\s+$/.test(v)) {
122
+ return v;
123
+ }
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * The flags of an element from its JSON.
129
+ * @param json - the element
130
+ * @param edge - whether it is an edge
131
+ * @returns the selection, lock and grab flags
132
+ */
133
+ function flags(
134
+ json: Readonly<Record<string, unknown>>,
135
+ edge: boolean,
136
+ ): Pick<CyjsElement, "selected" | "selectable" | "locked" | "grabbable"> {
137
+ const pannable = json.pannable === undefined ? edge : Boolean(json.pannable);
138
+ const grabbable = json.grabbable === undefined ? true : Boolean(json.grabbable);
139
+ return {
140
+ selected: Boolean(json.selected),
141
+ selectable: json.selectable === undefined ? true : Boolean(json.selectable),
142
+ locked: Boolean(json.locked),
143
+ grabbable: !pannable && grabbable,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * An element's classes as Cytoscape.js reads them: an array's truthy items, or a string split at whitespace.
149
+ * @param v - the `classes` field
150
+ * @returns the classes
151
+ */
152
+ function classSet(v: unknown): Set<unknown> {
153
+ const out = new Set<unknown>();
154
+ let items: unknown[] = [];
155
+ if (Array.isArray(v)) {
156
+ items = v;
157
+ } else if (typeof v === "string") {
158
+ items = v.split(/\s+/);
159
+ }
160
+ for (const c of items) {
161
+ if (c) {
162
+ out.add(c);
163
+ }
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /**
169
+ * Each node's compound parent, as Cytoscape.js keeps it: a parent naming no node, an edge, or a descendant of the
170
+ * node itself (which would make a cycle) is dropped. Near-linear in the nodes, however deep the hierarchy.
171
+ * @param nodes - the core's nodes, in order; a dropped parent is recorded in their overrides
172
+ * @param ids - the index of each node id (edge ids map to -1)
173
+ * @returns each node's parent index (-1 for none) and each node's children
174
+ */
175
+ function parentsOf(
176
+ nodes: readonly CyjsElement[],
177
+ ids: ReadonlyMap<string, number>,
178
+ ): { parent: Int32Array; children: number[][] } {
179
+ const parent = new Int32Array(nodes.length).fill(-1);
180
+ const children: number[][] = nodes.map(() => []);
181
+ // root[x] leads to the root of x's parent chain; paths are halved as they are walked
182
+ const root = Int32Array.from(nodes, (_n, i) => i);
183
+ const rootOf = (x: number): number => {
184
+ while (root[x] !== x) {
185
+ root[x] = root[root[x]];
186
+ x = root[x];
187
+ }
188
+ return x;
189
+ };
190
+ for (let i = 0; i < nodes.length; i++) {
191
+ const raw = nodes[i].data.parent;
192
+ if (raw === undefined || raw === null) {
193
+ continue;
194
+ }
195
+ const text = typeof raw === "number" && !Number.isNaN(raw) ? String(raw) : raw;
196
+ const p = typeof text === "string" ? ids.get(text) : undefined;
197
+ if (p === undefined || p < 0) {
198
+ (nodes[i].overrides as Map<string, unknown>).set("parent", undefined);
199
+ continue;
200
+ }
201
+ if (typeof raw === "number") {
202
+ (nodes[i].overrides as Map<string, unknown>).set("parent", text);
203
+ }
204
+ // a node never becomes its own ancestor: node i has no parent yet, so it is an ancestor of p exactly when it
205
+ // is the root of p's chain, found in near-constant time through the union-find roots
206
+ if (rootOf(p) === i) {
207
+ (nodes[i].overrides as Map<string, unknown>).set("parent", undefined);
208
+ continue;
209
+ }
210
+ parent[i] = p;
211
+ root[i] = p;
212
+ children[p].push(i);
213
+ }
214
+ return { parent, children };
215
+ }
216
+
217
+ /**
218
+ * The elements a Cytoscape.js core holds after `cy.add(elements)`.
219
+ * @param elements - an array of element JSON, or `{ nodes, edges }`
220
+ * @returns the elements and their links
221
+ */
222
+ export function cyjsGraph(elements: unknown): CyjsGraph {
223
+ const { nodeJson, edgeJson } = elementGroups(elements);
224
+ const ids = new Map<string, number>();
225
+ const make = elementMaker(ids);
226
+ const nodes: CyjsElement[] = [];
227
+ for (const json of nodeJson) {
228
+ const e = make(json, false, new Map());
229
+ if (e !== null) {
230
+ ids.set(e.id, nodes.length);
231
+ nodes.push(e);
232
+ }
233
+ }
234
+ const edges: CyjsElement[] = [];
235
+ const src: number[] = [];
236
+ const tgt: number[] = [];
237
+ const edgeIds = new Set<string>();
238
+ for (const json of edgeJson) {
239
+ const overrides = new Map<string, unknown>();
240
+ const e = make(json, true, overrides);
241
+ const ends = e === null || edgeIds.has(e.id) ? null : edgeEnds(e, ids, overrides);
242
+ if (e !== null && ends !== null) {
243
+ edgeIds.add(e.id);
244
+ edges.push(e);
245
+ src.push(ends[0]);
246
+ tgt.push(ends[1]);
247
+ }
248
+ }
249
+ for (const id of edgeIds) {
250
+ ids.set(id, -1);
251
+ }
252
+ const { parent, children } = parentsOf(nodes, ids);
253
+ const incident: number[][] = nodes.map(() => []);
254
+ for (let e = 0; e < edges.length; e++) {
255
+ incident[src[e]].push(e);
256
+ if (src[e] !== tgt[e]) {
257
+ incident[tgt[e]].push(e);
258
+ }
259
+ }
260
+ return {
261
+ nodes,
262
+ edges,
263
+ source: Int32Array.from(src),
264
+ target: Int32Array.from(tgt),
265
+ parent,
266
+ children,
267
+ incident,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * The node and edge JSON of `cy.add(elements)`: an array's items by their `group`, else an edge when they name a
273
+ * source and a target; a `{ nodes, edges }` object's by where they are.
274
+ * @param elements - an array of element JSON, or `{ nodes, edges }`
275
+ * @returns the node and edge JSON, in order
276
+ */
277
+ function elementGroups(elements: unknown): {
278
+ nodeJson: Record<string, unknown>[];
279
+ edgeJson: Record<string, unknown>[];
280
+ } {
281
+ const nodeJson: Record<string, unknown>[] = [];
282
+ const edgeJson: Record<string, unknown>[] = [];
283
+ const take = (item: unknown, group: unknown): void => {
284
+ const g = groupOf(item, group);
285
+ if (g === "nodes") {
286
+ nodeJson.push(item as Record<string, unknown>);
287
+ } else if (g === "edges") {
288
+ edgeJson.push(item as Record<string, unknown>);
289
+ }
290
+ };
291
+ if (Array.isArray(elements)) {
292
+ for (const item of elements) {
293
+ take(item, undefined);
294
+ }
295
+ } else if (isObject(elements)) {
296
+ for (const item of Array.isArray(elements.nodes) ? elements.nodes : []) {
297
+ take(item, "nodes");
298
+ }
299
+ for (const item of Array.isArray(elements.edges) ? elements.edges : []) {
300
+ take(item, "edges");
301
+ }
302
+ }
303
+ return { nodeJson, edgeJson };
304
+ }
305
+
306
+ /**
307
+ * The group an element JSON is added to: the one its place gives, else its own `group`, else an edge when its data
308
+ * names a source and a target.
309
+ * @param item - the element JSON
310
+ * @param group - the group its place gives, or undefined
311
+ * @returns the group, or undefined for no element
312
+ */
313
+ function groupOf(item: unknown, group: unknown): unknown {
314
+ if (!isObject(item)) {
315
+ return undefined;
316
+ }
317
+ const data = item.data ?? {};
318
+ if (!isObject(data)) {
319
+ return undefined;
320
+ }
321
+ const given = group ?? item.group;
322
+ if (given !== undefined && given !== null) {
323
+ return given;
324
+ }
325
+ const ends = data.source !== undefined && data.source !== null && data.target !== undefined && data.target !== null;
326
+ return ends ? "edges" : "nodes";
327
+ }
328
+
329
+ /**
330
+ * Makes the elements of one core: an element without an id gets a generated one; one whose id is taken or not an id
331
+ * is dropped.
332
+ * @param ids - the ids taken so far, by node index
333
+ * @returns the maker: the element of a JSON, or null when it is dropped
334
+ */
335
+ function elementMaker(
336
+ ids: ReadonlyMap<string, number>,
337
+ ): (json: Record<string, unknown>, edge: boolean, overrides: Map<string, unknown>) => CyjsElement | null {
338
+ let generated = 0;
339
+ return (json, edge, overrides) => {
340
+ const data = isObject(json.data) ? json.data : {};
341
+ let id: string | null;
342
+ if (data.id === undefined) {
343
+ id = `${String.fromCodePoint(0)}generated-${String(generated++)}`;
344
+ overrides.set("id", id);
345
+ } else {
346
+ id = idText(data.id);
347
+ if (id === null || ids.has(id)) {
348
+ return null;
349
+ }
350
+ if (typeof data.id === "number") {
351
+ overrides.set("id", id);
352
+ }
353
+ }
354
+ return { json, data, overrides, id, classes: classSet(json.classes), ...flags(json, edge) };
355
+ };
356
+ }
357
+
358
+ /**
359
+ * An edge's source and target nodes; a number endpoint is read as its text.
360
+ * @param e - the edge
361
+ * @param ids - the node ids
362
+ * @param overrides - the edge's data as Cytoscape.js rewrites it
363
+ * @returns the two node indexes, or null when an endpoint names no node
364
+ */
365
+ function edgeEnds(
366
+ e: CyjsElement,
367
+ ids: ReadonlyMap<string, number>,
368
+ overrides: Map<string, unknown>,
369
+ ): [number, number] | null {
370
+ const ends: number[] = [];
371
+ for (const field of ["source", "target"] as const) {
372
+ const raw = e.data[field];
373
+ const text = typeof raw === "number" && !Number.isNaN(raw) ? String(raw) : raw;
374
+ if (typeof raw === "number") {
375
+ overrides.set(field, text);
376
+ }
377
+ const at = typeof text === "string" ? ids.get(text) : undefined;
378
+ if (at !== undefined) {
379
+ ends.push(at);
380
+ }
381
+ }
382
+ return ends.length === 2 ? [ends[0], ends[1]] : null;
383
+ }
384
+
385
+ /**
386
+ * A data field as a selector reads it (`ele.data(field)`): the field itself, else a dotted path walked step by step.
387
+ * @param e - the element
388
+ * @param field - the field
389
+ * @returns the value, or undefined
390
+ */
391
+ function selectorField(e: CyjsElement, field: string): unknown {
392
+ if (e.overrides.has(field)) {
393
+ return e.overrides.get(field);
394
+ }
395
+ const direct = e.data[field];
396
+ if (direct !== undefined || !field.includes(".")) {
397
+ return direct;
398
+ }
399
+ let v: unknown = e.data;
400
+ const path = field.split(".");
401
+ let k = 0;
402
+ while (v !== null && v !== undefined && k < path.length) {
403
+ v = (v as Record<string, unknown>)[path[k++]];
404
+ }
405
+ return k === path.length ? v : undefined;
406
+ }
407
+
408
+ /**
409
+ * An element's data as a mapper reads it, with Cytoscape.js's rewrites of id, endpoints and parent.
410
+ * @param e - the element
411
+ * @returns an object to read fields from
412
+ */
413
+ function mapperData(e: CyjsElement): unknown {
414
+ if (e.overrides.size === 0) {
415
+ return e.data;
416
+ }
417
+ return new Proxy(e.data, {
418
+ get: (target, key) =>
419
+ typeof key === "string" && e.overrides.has(key)
420
+ ? e.overrides.get(key)
421
+ : (target as Record<string | symbol, unknown>)[key],
422
+ });
423
+ }
424
+
425
+ // ============================================================ the stylesheet
426
+
427
+ /** One stylesheet entry ready to apply. */
428
+ interface Entry {
429
+ /** The position in the sheet (default rules first, negative). */
430
+ readonly index: number;
431
+ /** Cytoscape.js's own default rule. */
432
+ readonly tool: boolean;
433
+ /** The selector as written. */
434
+ readonly text: string;
435
+ readonly selector: CyjsSelector;
436
+ readonly properties: readonly (readonly [string, unknown])[];
437
+ readonly dashOnly: boolean;
438
+ }
439
+
440
+ /**
441
+ * The entries of a stylesheet given as JSON (an array of `{ selector, style }`) or as text.
442
+ * @param style - the stylesheet
443
+ * @param notes - where problems are noted
444
+ * @returns the entries in order
445
+ */
446
+ function sheetEntries(style: unknown, notes: StyleNotes): CyjsSheetEntry[] {
447
+ if (typeof style === "string") {
448
+ const { entries, stop } = parseCyjsStylesheetText(style);
449
+ if (stop !== null) {
450
+ notes.add(STYLE_ISSUE.UNREADABLE, null, "stylesheet", { sample: stop });
451
+ }
452
+ return entries;
453
+ }
454
+ if (!Array.isArray(style)) {
455
+ return [];
456
+ }
457
+ const out: CyjsSheetEntry[] = [];
458
+ for (const item of style) {
459
+ if (!isObject(item)) {
460
+ continue;
461
+ }
462
+ const props = item.style ?? item.css;
463
+ const properties: [string, unknown][] = [];
464
+ if (isObject(props)) {
465
+ for (const name of Object.keys(props)) {
466
+ properties.push([name, props[name]]);
467
+ }
468
+ }
469
+ out.push({ selector: item.selector, properties, dashOnly: false });
470
+ }
471
+ return out;
472
+ }
473
+
474
+ // ============================================================ matching
475
+
476
+ /** The most nested combinators a selector may hold before it is noted and matched to nothing. */
477
+ const MAX_SELECTOR_NESTING = 64;
478
+
479
+ /**
480
+ * How deep a query nests.
481
+ * @param q - the query
482
+ * @param depth - the depth so far
483
+ * @returns the depth, capped just above the limit
484
+ */
485
+ function nesting(q: CyjsQuery, depth = 0): number {
486
+ if (depth > MAX_SELECTOR_NESTING) {
487
+ return depth;
488
+ }
489
+ let max = depth;
490
+ for (const c of q.checks) {
491
+ for (const sub of subQueries(c)) {
492
+ max = Math.max(max, nesting(sub, depth + 1));
493
+ if (max > MAX_SELECTOR_NESTING) {
494
+ return max;
495
+ }
496
+ }
497
+ }
498
+ return max;
499
+ }
500
+
501
+ /**
502
+ * The queries a check holds.
503
+ * @param c - the check
504
+ * @returns its sub-queries
505
+ */
506
+ function subQueries(c: CyjsCheck): CyjsQuery[] {
507
+ switch (c.type) {
508
+ case "child":
509
+ case "parent":
510
+ return [c.parent, c.child];
511
+ case "descendant":
512
+ case "ancestor":
513
+ return [c.ancestor, c.descendant];
514
+ case "split":
515
+ return [c.left, c.right, c.subject];
516
+ case "directedEdge":
517
+ case "nodeSource":
518
+ case "nodeTarget":
519
+ return [c.source, c.target];
520
+ case "undirectedEdge":
521
+ return [c.nodes[0], c.nodes[1]];
522
+ case "nodeNeighbor":
523
+ return [c.node, c.neighbor];
524
+ default:
525
+ return [];
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Cytoscape.js's comparison of a field with a selector's value.
531
+ * @param field - the element's value
532
+ * @param op - the base operator
533
+ * @param not - the `!` flag
534
+ * @param ci - the `@` flag
535
+ * @param value - the selector's value
536
+ * @returns whether it matches
537
+ */
538
+ function valCmp(field: unknown, op: string, not: boolean, ci: boolean, value: string | number): boolean {
539
+ let a: unknown = field;
540
+ let b: unknown = value;
541
+ const isFieldStr = typeof field === "string";
542
+ const isFieldNum = typeof field === "number" && !Number.isNaN(field);
543
+ let fs: string | null = null;
544
+ let vs: string | null = null;
545
+ if (isFieldStr || typeof value === "string" || ci) {
546
+ fs = !isFieldStr && !isFieldNum ? "" : String(field);
547
+ vs = String(value);
548
+ }
549
+ if (ci && fs !== null && vs !== null) {
550
+ fs = fs.toLowerCase();
551
+ vs = vs.toLowerCase();
552
+ a = fs;
553
+ b = vs;
554
+ }
555
+ let m = false;
556
+ let ineq = false;
557
+ switch (op) {
558
+ case "*=":
559
+ m = fs !== null && vs !== null && fs.includes(vs);
560
+ break;
561
+ case "$=":
562
+ m = fs !== null && vs !== null && fs.endsWith(vs);
563
+ break;
564
+ case "^=":
565
+ m = fs !== null && vs !== null && fs.startsWith(vs);
566
+ break;
567
+ case "=":
568
+ m = a === b;
569
+ break;
570
+ case ">":
571
+ ineq = true;
572
+ m = (a as number) > (b as number);
573
+ break;
574
+ case ">=":
575
+ ineq = true;
576
+ m = (a as number) >= (b as number);
577
+ break;
578
+ case "<":
579
+ ineq = true;
580
+ m = (a as number) < (b as number);
581
+ break;
582
+ case "<=":
583
+ ineq = true;
584
+ m = (a as number) <= (b as number);
585
+ break;
586
+ default:
587
+ break;
588
+ }
589
+ if (not && ((field !== null && field !== undefined) || !ineq)) {
590
+ m = !m;
591
+ }
592
+ return m;
593
+ }
594
+
595
+ /** The checks of an element's place in the graph: its parents, children and edges. */
596
+ const STRUCTURE_CHECKS: ReadonlySet<string> = new Set([
597
+ "child",
598
+ "parent",
599
+ "descendant",
600
+ "ancestor",
601
+ "split",
602
+ "directedEdge",
603
+ "undirectedEdge",
604
+ "nodeSource",
605
+ "nodeTarget",
606
+ "nodeNeighbor",
607
+ ]);
608
+
609
+ /** States whose value changes while Cytoscape.js runs; they are evaluated as at load and noted. */
610
+ const RUNTIME_STATES: ReadonlySet<string> = new Set([
611
+ "active",
612
+ "inactive",
613
+ "grabbed",
614
+ "free",
615
+ "animated",
616
+ "unanimated",
617
+ "visible",
618
+ "hidden",
619
+ "transparent",
620
+ "backgrounding",
621
+ "nonbackgrounding",
622
+ ]);
623
+
624
+ /** Thrown inside a selector match when the counted budget runs out partway through it. */
625
+ class BudgetStop extends Error {}
626
+
627
+ /** How many units of work a match does between checks of the budget and the signal. */
628
+ const CHECK_EVERY = 4096;
629
+
630
+ /** Evaluates selectors over one graph. */
631
+ class Matcher {
632
+ readonly degree: Int32Array;
633
+ readonly indegree: Int32Array;
634
+ readonly outdegree: Int32Array;
635
+ work = 0;
636
+ /** Where the work is charged while a match runs; null between matches. */
637
+ #charge: { kind: StyleTarget; budget: StyleWorkBudget; signal?: AbortSignal } | null = null;
638
+ #charged = 0;
639
+ /**
640
+ * The answers of nested sub-queries for one match, per element: 0 unknown, 1 true, 2 false. Each sub-query is
641
+ * then evaluated at most once per element, so a nested compound selector costs polynomial, not exponential, time.
642
+ */
643
+ readonly #memo = { node: new Map<CyjsQuery, Uint8Array>(), edge: new Map<CyjsQuery, Uint8Array>() };
644
+
645
+ constructor(
646
+ readonly g: CyjsGraph,
647
+ readonly notes: StyleNotes,
648
+ ) {
649
+ const n = g.nodes.length;
650
+ this.degree = new Int32Array(n);
651
+ this.indegree = new Int32Array(n);
652
+ this.outdegree = new Int32Array(n);
653
+ for (let e = 0; e < g.edges.length; e++) {
654
+ const s = g.source[e];
655
+ const t = g.target[e];
656
+ this.degree[s] += s === t ? 2 : 1;
657
+ if (s !== t) {
658
+ this.degree[t]++;
659
+ }
660
+ this.outdegree[s]++;
661
+ this.indegree[t]++;
662
+ }
663
+ }
664
+
665
+ /**
666
+ * Whether an element matches a query.
667
+ * @param q - the query
668
+ * @param kind - the element's kind
669
+ * @param i - the element
670
+ * @returns true when every check matches
671
+ */
672
+ query(q: CyjsQuery, kind: StyleTarget, i: number): boolean {
673
+ for (const c of q.checks) {
674
+ this.work++;
675
+ if (this.#charge !== null && this.work - this.#charged >= CHECK_EVERY) {
676
+ this.settle();
677
+ }
678
+ if (!this.check(c, kind, i)) {
679
+ return false;
680
+ }
681
+ }
682
+ return true;
683
+ }
684
+
685
+ /**
686
+ * Starts one selector match: its work is charged to the budget as it runs, and the sub-query answers are fresh.
687
+ * @param kind - the target the work is charged to
688
+ * @param budget - the counted budget
689
+ * @param signal - cancels the match
690
+ */
691
+ begin(kind: StyleTarget, budget: StyleWorkBudget, signal?: AbortSignal): void {
692
+ this.#charge = { kind, budget, signal };
693
+ this.#charged = this.work;
694
+ this.#memo.node.clear();
695
+ this.#memo.edge.clear();
696
+ }
697
+
698
+ /** Ends a match: its remaining work is charged (never throwing) and its sub-query answers are dropped. */
699
+ end(): void {
700
+ const charge = this.#charge;
701
+ this.#charge = null;
702
+ charge?.budget.spend(charge.kind, this.work - this.#charged);
703
+ this.#charged = this.work;
704
+ this.#memo.node.clear();
705
+ this.#memo.edge.clear();
706
+ }
707
+
708
+ /**
709
+ * Charges the work done since the last charge; throws BudgetStop once the budget is exhausted.
710
+ * @throws BudgetStop - when the target's budget is exhausted
711
+ */
712
+ settle(): void {
713
+ const charge = this.#charge;
714
+ if (charge === null) {
715
+ return;
716
+ }
717
+ charge.budget.spend(charge.kind, this.work - this.#charged);
718
+ this.#charged = this.work;
719
+ charge.signal?.throwIfAborted();
720
+ if (charge.budget.exhausted(charge.kind)) {
721
+ throw new BudgetStop();
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Whether an element matches a nested sub-query, remembered for the rest of the match.
727
+ * @param q - the sub-query
728
+ * @param kind - the element's kind
729
+ * @param i - the element
730
+ * @returns true when every check matches
731
+ */
732
+ sub(q: CyjsQuery, kind: StyleTarget, i: number): boolean {
733
+ const memo = this.#memo[kind];
734
+ let seen = memo.get(q);
735
+ if (seen === undefined) {
736
+ seen = new Uint8Array(kind === "node" ? this.g.nodes.length : this.g.edges.length);
737
+ memo.set(q, seen);
738
+ }
739
+ if (seen[i] !== 0) {
740
+ return seen[i] === 1;
741
+ }
742
+ const m = this.query(q, kind, i);
743
+ seen[i] = m ? 1 : 2;
744
+ return m;
745
+ }
746
+
747
+ /**
748
+ * Whether an element matches one check.
749
+ * @param c - the check
750
+ * @param kind - the element's kind
751
+ * @param i - the element
752
+ * @returns true when it matches
753
+ */
754
+ check(c: CyjsCheck, kind: StyleTarget, i: number): boolean {
755
+ return STRUCTURE_CHECKS.has(c.type) ? this.structureCheck(c, kind, i) : this.elementCheck(c, kind, i);
756
+ }
757
+
758
+ /**
759
+ * Whether an element matches a check of its own: its group, state, id, classes or data.
760
+ * @param c - the check
761
+ * @param kind - the element's kind
762
+ * @param i - the element
763
+ * @returns true when it matches
764
+ */
765
+ private elementCheck(c: CyjsCheck, kind: StyleTarget, i: number): boolean {
766
+ const { g } = this;
767
+ const e = kind === "node" ? g.nodes[i] : g.edges[i];
768
+ switch (c.type) {
769
+ case "group":
770
+ return c.value === "*" || c.value === `${kind}s`;
771
+ case "state":
772
+ return this.state(c.value, kind, i, e);
773
+ case "id":
774
+ return e.id === c.value;
775
+ case "class":
776
+ return e.classes.has(c.value);
777
+ case "exist":
778
+ return selectorField(e, c.field) !== undefined;
779
+ case "bool": {
780
+ const v = selectorField(e, c.field);
781
+ if (c.op === "?") {
782
+ return Boolean(v);
783
+ }
784
+ return c.op === "!" ? !v : v === undefined;
785
+ }
786
+ case "compare":
787
+ return valCmp(selectorField(e, c.field), c.op, c.not, c.ci, c.value);
788
+ case "meta": {
789
+ const v = kind === "node" ? this[c.field][i] : undefined;
790
+ return valCmp(v, c.op, c.not, c.ci, c.value);
791
+ }
792
+ case "true":
793
+ return true;
794
+ default:
795
+ return false;
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Whether an element matches a check of its place in the graph: its parents, children and edges.
801
+ * @param c - the check
802
+ * @param kind - the element's kind
803
+ * @param i - the element
804
+ * @returns true when it matches
805
+ */
806
+ private structureCheck(c: CyjsCheck, kind: StyleTarget, i: number): boolean {
807
+ const { g } = this;
808
+ switch (c.type) {
809
+ case "child":
810
+ return (
811
+ this.sub(c.child, kind, i) &&
812
+ kind === "node" &&
813
+ g.parent[i] >= 0 &&
814
+ this.sub(c.parent, "node", g.parent[i])
815
+ );
816
+ case "parent":
817
+ return (
818
+ this.sub(c.parent, kind, i) &&
819
+ kind === "node" &&
820
+ g.children[i].some((k) => this.sub(c.child, "node", k))
821
+ );
822
+ case "descendant":
823
+ return this.sub(c.descendant, kind, i) && kind === "node" && this.someAncestor(i, c.ancestor);
824
+ case "ancestor":
825
+ return this.sub(c.ancestor, kind, i) && kind === "node" && this.someDescendant(i, c.descendant);
826
+ case "split":
827
+ return this.sub(c.subject, kind, i) && this.sub(c.left, kind, i) && this.sub(c.right, kind, i);
828
+ case "directedEdge":
829
+ return (
830
+ kind === "edge" &&
831
+ this.sub(c.source, "node", g.source[i]) &&
832
+ this.sub(c.target, "node", g.target[i])
833
+ );
834
+ case "undirectedEdge": {
835
+ if (kind !== "edge") {
836
+ return false;
837
+ }
838
+ const [a, b] = c.nodes;
839
+ const s = g.source[i];
840
+ const t = g.target[i];
841
+ return (
842
+ (this.sub(a, "node", s) && this.sub(b, "node", t)) ||
843
+ (this.sub(b, "node", s) && this.sub(a, "node", t))
844
+ );
845
+ }
846
+ case "nodeSource":
847
+ return (
848
+ kind === "node" &&
849
+ this.sub(c.source, kind, i) &&
850
+ g.incident[i].some((ed) => g.source[ed] === i && this.sub(c.target, "node", g.target[ed]))
851
+ );
852
+ case "nodeTarget":
853
+ return (
854
+ kind === "node" &&
855
+ this.sub(c.target, kind, i) &&
856
+ g.incident[i].some((ed) => g.target[ed] === i && this.sub(c.source, "node", g.source[ed]))
857
+ );
858
+ case "nodeNeighbor":
859
+ return (
860
+ kind === "node" &&
861
+ this.sub(c.node, kind, i) &&
862
+ g.incident[i].some((ed) => {
863
+ const other = g.source[ed] === i ? g.target[ed] : g.source[ed];
864
+ return this.sub(c.neighbor, "node", other);
865
+ })
866
+ );
867
+ default:
868
+ return false;
869
+ }
870
+ }
871
+
872
+ /**
873
+ * Whether an ancestor of a node matches a query (cycle-safe: parents never form a cycle).
874
+ * @param i - the node
875
+ * @param q - the query
876
+ * @returns true when one does
877
+ */
878
+ someAncestor(i: number, q: CyjsQuery): boolean {
879
+ for (let a = this.g.parent[i]; a >= 0; a = this.g.parent[a]) {
880
+ if (this.sub(q, "node", a)) {
881
+ return true;
882
+ }
883
+ }
884
+ return false;
885
+ }
886
+
887
+ /**
888
+ * Whether a descendant of a node matches a query, walked without recursion.
889
+ * @param i - the node
890
+ * @param q - the query
891
+ * @returns true when one does
892
+ */
893
+ someDescendant(i: number, q: CyjsQuery): boolean {
894
+ const stack = [...this.g.children[i]];
895
+ while (stack.length > 0) {
896
+ const k = stack.pop() as number;
897
+ if (this.sub(q, "node", k)) {
898
+ return true;
899
+ }
900
+ for (const c of this.g.children[k]) {
901
+ stack.push(c);
902
+ }
903
+ }
904
+ return false;
905
+ }
906
+
907
+ /**
908
+ * A state selector, evaluated as at load.
909
+ * @param s - the state
910
+ * @param kind - the element's kind
911
+ * @param i - the element
912
+ * @param e - the element
913
+ * @returns whether the element is in the state
914
+ */
915
+ state(s: string, kind: StyleTarget, i: number, e: CyjsElement): boolean {
916
+ const { g } = this;
917
+ if (RUNTIME_STATES.has(s)) {
918
+ this.notes.add(STYLE_ISSUE.STATE_ASSUMED, kind, `:${s}`, { sample: `:${s}` });
919
+ }
920
+ const node = kind === "node";
921
+ switch (s) {
922
+ case "selected":
923
+ return e.selected;
924
+ case "unselected":
925
+ return !e.selected;
926
+ case "selectable":
927
+ return e.selectable;
928
+ case "unselectable":
929
+ return !e.selectable;
930
+ case "locked":
931
+ return e.locked;
932
+ case "unlocked":
933
+ return !e.locked;
934
+ case "grabbable":
935
+ return e.grabbable;
936
+ case "ungrabbable":
937
+ return !e.grabbable;
938
+ case "free":
939
+ case "inside":
940
+ case "unanimated":
941
+ case "inactive":
942
+ case "nonbackgrounding":
943
+ case "visible":
944
+ return true;
945
+ case "parent":
946
+ return node && g.children[i].length > 0;
947
+ case "childless":
948
+ return node && g.children[i].length === 0;
949
+ case "child":
950
+ case "nonorphan":
951
+ return node && g.parent[i] >= 0;
952
+ case "orphan":
953
+ return node && g.parent[i] < 0;
954
+ case "compound":
955
+ return node
956
+ ? g.children[i].length > 0
957
+ : g.children[g.source[i]].length > 0 || g.children[g.target[i]].length > 0;
958
+ case "loop":
959
+ return !node && g.source[i] === g.target[i];
960
+ case "simple":
961
+ return !node && g.source[i] !== g.target[i];
962
+ default:
963
+ return false;
964
+ }
965
+ }
966
+ }
967
+
968
+ /** Indexes of one target's elements: by class, by id, and by value per field (built on first use). */
969
+ class Indexes {
970
+ readonly byClass = new Map<unknown, number[]>();
971
+ readonly byId = new Map<string, number>();
972
+ readonly byValue = new Map<string, Map<string, number[]>>();
973
+ /** Per element, the stamp of the last selector that matched it. */
974
+ readonly marks: Int32Array;
975
+ /** The last stamp handed out. */
976
+ stamp = 0;
977
+
978
+ constructor(
979
+ readonly elements: readonly CyjsElement[],
980
+ readonly spend: (ops: number) => void,
981
+ ) {
982
+ this.marks = new Int32Array(elements.length);
983
+ for (let i = 0; i < elements.length; i++) {
984
+ for (const c of elements[i].classes) {
985
+ let list = this.byClass.get(c);
986
+ if (list === undefined) {
987
+ list = [];
988
+ this.byClass.set(c, list);
989
+ }
990
+ list.push(i);
991
+ }
992
+ if (!this.byId.has(elements[i].id)) {
993
+ this.byId.set(elements[i].id, i);
994
+ }
995
+ }
996
+ spend(elements.length);
997
+ }
998
+
999
+ /**
1000
+ * The elements whose field is `===` a value.
1001
+ * @param field - the field
1002
+ * @param value - the value
1003
+ * @returns the element indexes
1004
+ */
1005
+ equal(field: string, value: string | number): readonly number[] {
1006
+ let m = this.byValue.get(field);
1007
+ if (m === undefined) {
1008
+ m = new Map();
1009
+ for (let i = 0; i < this.elements.length; i++) {
1010
+ const v = selectorField(this.elements[i], field);
1011
+ if (typeof v !== "string" && typeof v !== "number") {
1012
+ continue;
1013
+ }
1014
+ const k = `${typeof v}:${String(v)}`;
1015
+ let list = m.get(k);
1016
+ if (list === undefined) {
1017
+ list = [];
1018
+ m.set(k, list);
1019
+ }
1020
+ list.push(i);
1021
+ }
1022
+ this.byValue.set(field, m);
1023
+ this.spend(this.elements.length);
1024
+ }
1025
+ if (typeof value === "number" && Number.isNaN(value)) {
1026
+ return [];
1027
+ }
1028
+ return m.get(`${typeof value}:${String(value)}`) ?? [];
1029
+ }
1030
+ }
1031
+
1032
+ /**
1033
+ * The candidates of one query for a target: the element list of its most selective indexed check, or null when
1034
+ * every element must be tested.
1035
+ * @param q - the query
1036
+ * @param kind - the target
1037
+ * @param ix - the target's indexes
1038
+ * @returns the candidates, or null
1039
+ */
1040
+ function candidates(q: CyjsQuery, kind: StyleTarget, ix: Indexes): readonly number[] | null {
1041
+ let best: readonly number[] | null = null;
1042
+ for (const c of q.checks) {
1043
+ let list: readonly number[] | null = null;
1044
+ if (c.type === "group" && c.value !== "*" && c.value !== `${kind}s`) {
1045
+ return [];
1046
+ }
1047
+ if (c.type === "class") {
1048
+ list = ix.byClass.get(c.value) ?? [];
1049
+ } else if (c.type === "id") {
1050
+ const at = ix.byId.get(c.value);
1051
+ list = at === undefined ? [] : [at];
1052
+ } else if (c.type === "compare" && c.op === "=" && !c.not && !c.ci) {
1053
+ list = ix.equal(c.field, c.value);
1054
+ }
1055
+ if (list !== null && (best === null || list.length < best.length)) {
1056
+ best = list;
1057
+ }
1058
+ }
1059
+ return best;
1060
+ }
1061
+
1062
+ // ============================================================ the engine
1063
+
1064
+ /** A cell of the cascade: a flat value, a failed mapper (the default), or a value Cytoscape.js cannot draw. */
1065
+ type Cell = CyjsFlat | "reset" | "fn" | typeof CYJS_THROWS;
1066
+
1067
+ /** One property of one entry, parsed. */
1068
+ interface EntryProperty {
1069
+ readonly name: string;
1070
+ readonly spec: CyjsPropertySpec;
1071
+ readonly parsed: CyjsParsed;
1072
+ }
1073
+
1074
+ /** One target's result. */
1075
+ export interface CyjsTargetStyles {
1076
+ /** The element count (in snapshot order when a snapshot was given). */
1077
+ readonly n: number;
1078
+ /** What Cytoscape.js draws, per neutral property, by element index. */
1079
+ readonly values: Partial<Record<string, StyleColumn>>;
1080
+ /** The stylesheet's rules, bottom to top. */
1081
+ readonly rules: StyleRule[];
1082
+ /** The style columns the rules read. */
1083
+ readonly columns: Map<string, StyleColumn>;
1084
+ /** The data columns the rules or the values were read from. */
1085
+ readonly rawColumns: string[];
1086
+ /** Each element's Cytoscape.js element, by index (null where the core holds none). */
1087
+ readonly elements: readonly (CyjsElement | null)[];
1088
+ }
1089
+
1090
+ /** The input of the engine. */
1091
+ export interface CyjsStyleInput {
1092
+ /** The elements: an array of element JSON, or `{ nodes, edges }`. */
1093
+ readonly elements: unknown;
1094
+ /** The stylesheet: an array of `{ selector, style }` (or `css`), or a stylesheet written as text. */
1095
+ readonly style: unknown;
1096
+ /**
1097
+ * The graph the styles are for. Elements are matched to its nodes by id and to its edges in order, and the
1098
+ * values come out indexed like it; its columns let a selector or a mapper become a predicate or a mapping over
1099
+ * graph-io's columns. Without it, values are indexed like the core holds the elements.
1100
+ */
1101
+ readonly snapshot?: GraphSnapshot;
1102
+ /** Whether Cytoscape.js's own default rules (edges 3px wide, gray compound parents) are applied; default true. */
1103
+ readonly toolDefaults?: boolean;
1104
+ /**
1105
+ * What a live core draws, per element JSON object (`ele.style()` beside `ele.json()`): taken as the truth over what
1106
+ * the stylesheet gives, and the only source for a property whose stylesheet value is a function.
1107
+ */
1108
+ readonly rendered?: ReadonlyMap<object, Readonly<Record<string, unknown>>>;
1109
+ /** Where problems are noted. */
1110
+ readonly notes?: StyleNotes;
1111
+ /** The counted work budget. */
1112
+ readonly budget?: StyleWorkBudget;
1113
+ /** Cancels the work. */
1114
+ readonly signal?: AbortSignal;
1115
+ }
1116
+
1117
+ /** The engine's result. */
1118
+ export interface CyjsStyles {
1119
+ /** Nodes. */
1120
+ readonly node: CyjsTargetStyles;
1121
+ /** Edges. */
1122
+ readonly edge: CyjsTargetStyles;
1123
+ /** The elements as the core holds them. */
1124
+ readonly graph: CyjsGraph;
1125
+ }
1126
+
1127
+ const POSITIONS: ReadonlyMap<string, string> = new Map([
1128
+ ["top,left", "top-left"],
1129
+ ["top,center", "top"],
1130
+ ["top,right", "top-right"],
1131
+ ["center,left", "left"],
1132
+ ["center,center", "center"],
1133
+ ["center,right", "right"],
1134
+ ["bottom,left", "bottom-left"],
1135
+ ["bottom,center", "bottom"],
1136
+ ["bottom,right", "bottom-right"],
1137
+ ]);
1138
+
1139
+ /**
1140
+ * Applies a Cytoscape.js stylesheet to its elements as Cytoscape.js 3.34 does.
1141
+ * @param input - the elements, the stylesheet, the graph and the options
1142
+ * @returns what is drawn per element and the stylesheet's rules, per target
1143
+ */
1144
+ export function evaluateCytoscapeJs(input: CyjsStyleInput): CyjsStyles {
1145
+ const notes = input.notes ?? new StyleNotes();
1146
+ const budget = input.budget ?? new StyleBudget();
1147
+ const graph = cyjsGraph(input.elements);
1148
+ const sheet: Entry[] = [];
1149
+ if (input.toolDefaults !== false) {
1150
+ CYJS_DEFAULT_STYLESHEET.forEach((d, k) => {
1151
+ sheet.push({
1152
+ index: k - CYJS_DEFAULT_STYLESHEET.length,
1153
+ tool: true,
1154
+ text: d.selector,
1155
+ selector: parseCyjsSelector(d.selector),
1156
+ properties: Object.entries(d.style),
1157
+ dashOnly: true,
1158
+ });
1159
+ });
1160
+ }
1161
+ const raw = sheetEntries(input.style, notes);
1162
+ let invalid = 0;
1163
+ let deep = 0;
1164
+ raw.forEach((r, k) => {
1165
+ let selector = parseCyjsSelector(r.selector);
1166
+ if (selector.invalid) {
1167
+ invalid++;
1168
+ }
1169
+ if (selector.queries.some((q) => nesting(q) > MAX_SELECTOR_NESTING)) {
1170
+ deep++;
1171
+ selector = { queries: [], invalid: true };
1172
+ }
1173
+ sheet.push({
1174
+ index: k,
1175
+ tool: false,
1176
+ text: typeof r.selector === "string" ? r.selector : "",
1177
+ selector,
1178
+ properties: r.properties,
1179
+ dashOnly: r.dashOnly,
1180
+ });
1181
+ });
1182
+ if (invalid > 0) {
1183
+ notes.add(STYLE_ISSUE.SELECTOR, null, "selector", { count: invalid, sample: "invalid" });
1184
+ }
1185
+ if (deep > 0) {
1186
+ notes.add(STYLE_ISSUE.SELECTOR, null, "selector", { count: deep, sample: "nested too deep" });
1187
+ }
1188
+ const matcher = new Matcher(graph, notes);
1189
+ const node = evaluateTarget("node", graph, sheet, matcher, input, notes, budget);
1190
+ const edge = evaluateTarget("edge", graph, sheet, matcher, input, notes, budget);
1191
+ return { node, edge, graph };
1192
+ }
1193
+
1194
+ /**
1195
+ * The order of the output: for each output index, the core's element (or -1).
1196
+ * @param kind - the target
1197
+ * @param graph - the core's elements
1198
+ * @param snapshot - the graph, when given
1199
+ * @param notes - where problems are noted
1200
+ * @returns the core element of each output index
1201
+ */
1202
+ function outputOrder(
1203
+ kind: StyleTarget,
1204
+ graph: CyjsGraph,
1205
+ snapshot: GraphSnapshot | undefined,
1206
+ notes: StyleNotes,
1207
+ ): Int32Array {
1208
+ const els = kind === "node" ? graph.nodes : graph.edges;
1209
+ if (snapshot === undefined) {
1210
+ return Int32Array.from(els, (_e, i) => i);
1211
+ }
1212
+ if (kind === "node") {
1213
+ const order = new Int32Array(snapshot.nodeCount).fill(-1);
1214
+ for (let i = 0; i < els.length; i++) {
1215
+ const at = snapshotIndex(snapshot, els[i].id);
1216
+ if (at >= 0 && order[at] < 0) {
1217
+ order[at] = i;
1218
+ }
1219
+ }
1220
+ return order;
1221
+ }
1222
+ const order = new Int32Array(snapshot.edgeCount).fill(-1);
1223
+ const nodeAt = outputOrder("node", graph, snapshot, notes);
1224
+ const coreToSnap = new Int32Array(graph.nodes.length).fill(-1);
1225
+ nodeAt.forEach((c, s) => {
1226
+ if (c >= 0) {
1227
+ coreToSnap[c] = s;
1228
+ }
1229
+ });
1230
+ let j = 0;
1231
+ for (let e = 0; e < els.length && j < snapshot.edgeCount; e++) {
1232
+ if (
1233
+ coreToSnap[graph.source[e]] === snapshot.edgeSource(j) &&
1234
+ coreToSnap[graph.target[e]] === snapshot.edgeTarget(j)
1235
+ ) {
1236
+ order[j++] = e;
1237
+ }
1238
+ }
1239
+ if (j < snapshot.edgeCount) {
1240
+ notes.add(STYLE_ISSUE.UNRESOLVED, "edge", "elements", { count: snapshot.edgeCount - j, sample: "edge order" });
1241
+ }
1242
+ return order;
1243
+ }
1244
+
1245
+ /**
1246
+ * A node's index in the graph by its Cytoscape.js id (text), trying the id as a number too.
1247
+ * @param snapshot - the graph
1248
+ * @param id - the id
1249
+ * @returns the index, or -1
1250
+ */
1251
+ function snapshotIndex(snapshot: GraphSnapshot, id: string): number {
1252
+ if (snapshot.ids.has(id)) {
1253
+ return snapshot.ids.indexOf(id);
1254
+ }
1255
+ const n = Number(id);
1256
+ return id.trim() !== "" && Number.isFinite(n) && snapshot.ids.has(n) ? snapshot.ids.indexOf(n) : -1;
1257
+ }
1258
+
1259
+ /** The cascade's winning cells, per Cytoscape.js property, by core element. */
1260
+ type Cells = Map<string, (Cell | undefined)[]>;
1261
+
1262
+ /** One entry applied to a target: its elements and its parsed properties. */
1263
+ interface Applied {
1264
+ readonly entry: Entry;
1265
+ readonly list: readonly number[];
1266
+ readonly props: EntryProperty[];
1267
+ }
1268
+
1269
+ /**
1270
+ * A property's cells, created on first use.
1271
+ * @param cells - the cells
1272
+ * @param name - the Cytoscape.js property
1273
+ * @param n - the element count
1274
+ * @returns the property's cells
1275
+ */
1276
+ function cellsOf(cells: Cells, name: string, n: number): (Cell | undefined)[] {
1277
+ let c = cells.get(name);
1278
+ if (c === undefined) {
1279
+ c = new Array<Cell | undefined>(n);
1280
+ cells.set(name, c);
1281
+ }
1282
+ return c;
1283
+ }
1284
+
1285
+ /**
1286
+ * Applies the sheet to one target.
1287
+ * @param kind - nodes or edges
1288
+ * @param graph - the core's elements
1289
+ * @param sheet - the entries
1290
+ * @param matcher - the selector evaluator
1291
+ * @param input - the input
1292
+ * @param notes - where problems are noted
1293
+ * @param budget - the counted budget
1294
+ * @returns the target's values and rules
1295
+ */
1296
+ function evaluateTarget(
1297
+ kind: StyleTarget,
1298
+ graph: CyjsGraph,
1299
+ sheet: readonly Entry[],
1300
+ matcher: Matcher,
1301
+ input: CyjsStyleInput,
1302
+ notes: StyleNotes,
1303
+ budget: StyleWorkBudget,
1304
+ ): CyjsTargetStyles {
1305
+ const els = kind === "node" ? graph.nodes : graph.edges;
1306
+ const spend = (ops: number): void => {
1307
+ budget.spend(kind, ops);
1308
+ };
1309
+ const { cells, applied } = cascade(kind, els, sheet, matcher, input, notes, budget);
1310
+ const order = outputOrder(kind, graph, input.snapshot, notes);
1311
+ const coreToOut = new Int32Array(els.length).fill(-1);
1312
+ order.forEach((c, k) => {
1313
+ if (c >= 0) {
1314
+ coreToOut[c] = k;
1315
+ }
1316
+ });
1317
+ applyBypasses(kind, els, cells, coreToOut, notes);
1318
+ if (kind === "edge") {
1319
+ forceLoopCurves(graph, cells.get("curve-style"));
1320
+ }
1321
+ const drawn = drawnValues(kind, els, cells, order, input.rendered, notes, spend);
1322
+ if (kind === "edge") {
1323
+ straightenUnpairedBeziers(graph, curveStyles(graph, cells, input.rendered), coreToOut, drawn.curved);
1324
+ }
1325
+ const built = buildRules(kind, applied, { els, order, coreToOut }, input, drawn, { matcher, notes, spend });
1326
+ return {
1327
+ n: order.length,
1328
+ values: drawn,
1329
+ rules: built.rules,
1330
+ columns: built.columns,
1331
+ rawColumns: built.rawColumns,
1332
+ elements: Array.from(order, (i) => (i >= 0 ? els[i] : null)),
1333
+ };
1334
+ }
1335
+
1336
+ /**
1337
+ * The cascade over one target: per property, the cell of the last rule that sets it, per element.
1338
+ * @param kind - nodes or edges
1339
+ * @param els - the core's elements of the target
1340
+ * @param sheet - the entries
1341
+ * @param matcher - the selector evaluator
1342
+ * @param input - the input
1343
+ * @param notes - where problems are noted
1344
+ * @param budget - the counted budget
1345
+ * @returns the cells and the entries applied
1346
+ */
1347
+ function cascade(
1348
+ kind: StyleTarget,
1349
+ els: readonly CyjsElement[],
1350
+ sheet: readonly Entry[],
1351
+ matcher: Matcher,
1352
+ input: CyjsStyleInput,
1353
+ notes: StyleNotes,
1354
+ budget: StyleWorkBudget,
1355
+ ): { cells: Cells; applied: Applied[] } {
1356
+ const n = els.length;
1357
+ const specs = cyjsProperties(kind);
1358
+ const spend = (ops: number): void => {
1359
+ budget.spend(kind, ops);
1360
+ };
1361
+ const ix = new Indexes(els, spend);
1362
+ const cells: Cells = new Map();
1363
+ const notCarried = new Map<string, number>();
1364
+ const applied: Applied[] = [];
1365
+ let stopped = false;
1366
+ for (const entry of sheet) {
1367
+ input.signal?.throwIfAborted();
1368
+ const props = entryProperties(entry, kind, specs, notes, notCarried);
1369
+ if (props.length === 0) {
1370
+ continue;
1371
+ }
1372
+ const list = budget.exhausted(kind)
1373
+ ? null
1374
+ : budgetedMatch(entry.selector, { kind, n }, matcher, ix, input, budget);
1375
+ if (list === null) {
1376
+ stopped = true;
1377
+ break;
1378
+ }
1379
+ spend(list.length);
1380
+ applied.push({ entry, list, props });
1381
+ for (const p of props) {
1382
+ const col = cellsOf(cells, p.name, n);
1383
+ for (const i of list) {
1384
+ col[i] = cellFor(p, els[i]);
1385
+ }
1386
+ spend(list.length);
1387
+ }
1388
+ }
1389
+ if (stopped) {
1390
+ notes.add(STYLE_ISSUE.BUDGET, kind, null, { sample: "stylesheet" });
1391
+ }
1392
+ for (const [name, count] of notCarried) {
1393
+ notes.add(STYLE_ISSUE.NOT_CARRIED, kind, name, { count, sample: name });
1394
+ }
1395
+ return { cells, applied };
1396
+ }
1397
+
1398
+ /**
1399
+ * The elements a selector matches, within the counted budget.
1400
+ * @param sel - the selector
1401
+ * @param target - the target and its element count
1402
+ * @param target.kind - nodes or edges
1403
+ * @param target.n - the element count
1404
+ * @param matcher - the evaluator
1405
+ * @param ix - the target's indexes
1406
+ * @param input - the input, whose signal cancels the match
1407
+ * @param budget - the counted budget
1408
+ * @returns the element indexes, or null when the budget ran out partway through
1409
+ */
1410
+ function budgetedMatch(
1411
+ sel: CyjsSelector,
1412
+ target: { readonly kind: StyleTarget; readonly n: number },
1413
+ matcher: Matcher,
1414
+ ix: Indexes,
1415
+ input: CyjsStyleInput,
1416
+ budget: StyleWorkBudget,
1417
+ ): readonly number[] | null {
1418
+ matcher.begin(target.kind, budget, input.signal);
1419
+ try {
1420
+ return matchList(sel, target.kind, target.n, matcher, ix);
1421
+ } catch (error) {
1422
+ if (!(error instanceof BudgetStop)) {
1423
+ throw error;
1424
+ }
1425
+ return null;
1426
+ } finally {
1427
+ matcher.end();
1428
+ }
1429
+ }
1430
+
1431
+ /**
1432
+ * Each element's own `style` (a bypass) over every rule; a mapper there is refused, as Cytoscape.js refuses it.
1433
+ * @param kind - nodes or edges
1434
+ * @param els - the core's elements of the target
1435
+ * @param cells - the cells; changed in place
1436
+ * @param coreToOut - the output index of each core element
1437
+ * @param notes - where problems are noted
1438
+ */
1439
+ function applyBypasses(
1440
+ kind: StyleTarget,
1441
+ els: readonly CyjsElement[],
1442
+ cells: Cells,
1443
+ coreToOut: Int32Array,
1444
+ notes: StyleNotes,
1445
+ ): void {
1446
+ const specs = cyjsProperties(kind);
1447
+ for (let i = 0; i < els.length; i++) {
1448
+ const { json } = els[i];
1449
+ const bypass = json.style ?? json.css;
1450
+ if (!isObject(bypass)) {
1451
+ continue;
1452
+ }
1453
+ for (const name0 of Object.keys(bypass)) {
1454
+ const name = cyjsPropertyName(name0);
1455
+ const spec = specs.get(name);
1456
+ const value = bypass[name0];
1457
+ if (spec === undefined || value === "" || value === null) {
1458
+ continue;
1459
+ }
1460
+ const parsed = parseCyjsValue(spec, value, "bypass");
1461
+ if (parsed === null || parsed === CYJS_THROWS || !isFlat(parsed)) {
1462
+ notes.add(STYLE_ISSUE.BAD_VALUE, kind, spec.property, { sample: value, element: coreToOut[i] });
1463
+ continue;
1464
+ }
1465
+ cellsOf(cells, name, els.length)[i] = parsed;
1466
+ }
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * What is drawn per neutral property, in output order: the cells as neutral values, overridden by what a live core
1472
+ * reports, with the label alignments and the visibility parts combined. Properties with no value are left out.
1473
+ * @param kind - nodes or edges
1474
+ * @param els - the core's elements of the target
1475
+ * @param cells - the cascade's cells
1476
+ * @param order - the core element of each output index
1477
+ * @param rendered - what a live core reports per element JSON, when given
1478
+ * @param notes - where problems are noted
1479
+ * @param spend - spends from the budget
1480
+ * @returns the values by property
1481
+ */
1482
+ function drawnValues(
1483
+ kind: StyleTarget,
1484
+ els: readonly CyjsElement[],
1485
+ cells: Cells,
1486
+ order: Int32Array,
1487
+ rendered: CyjsStyleInput["rendered"],
1488
+ notes: StyleNotes,
1489
+ spend: (ops: number) => void,
1490
+ ): Record<string, (StyleValue | null)[]> {
1491
+ const out = order.length;
1492
+ const values: Record<string, (StyleValue | null)[]> = {};
1493
+ const words = new Map<string, (string | null)[]>();
1494
+ const drawnOf: DrawnRead = { kind, els, cells, order, rendered, notes };
1495
+ for (const [name, spec] of cyjsProperties(kind)) {
1496
+ if (!cells.has(name) && rendered === undefined) {
1497
+ continue;
1498
+ }
1499
+ const col = drawnColumn(drawnOf, name, spec);
1500
+ spend(2 * out);
1501
+ if (spec.property.startsWith("_")) {
1502
+ words.set(
1503
+ spec.property,
1504
+ col.map((v) => (typeof v === "string" ? v : null)),
1505
+ );
1506
+ continue;
1507
+ }
1508
+ values[spec.property] ??= new Array<StyleValue | null>(out).fill(null);
1509
+ const target = values[spec.property];
1510
+ col.forEach((v, k) => {
1511
+ if (v !== null && v !== undefined) {
1512
+ target[k] = v;
1513
+ }
1514
+ });
1515
+ }
1516
+ combineParts(kind, out, words, values, notes);
1517
+ const drawn: Record<string, (StyleValue | null)[]> = {};
1518
+ for (const [p, col] of Object.entries(values)) {
1519
+ if (col.some((v) => v !== null)) {
1520
+ drawn[p] = col;
1521
+ }
1522
+ }
1523
+ return drawn;
1524
+ }
1525
+
1526
+ /** What drawnColumn() reads. */
1527
+ interface DrawnRead {
1528
+ readonly kind: StyleTarget;
1529
+ readonly els: readonly CyjsElement[];
1530
+ readonly cells: Cells;
1531
+ readonly order: Int32Array;
1532
+ readonly rendered: CyjsStyleInput["rendered"];
1533
+ readonly notes: StyleNotes;
1534
+ }
1535
+
1536
+ /**
1537
+ * One Cytoscape.js property's neutral value per output element: its cell, overridden by what a live core reports.
1538
+ * @param d - the target's cells and what a live core reports
1539
+ * @param name - the property
1540
+ * @param spec - its spec
1541
+ * @returns the values, in output order
1542
+ */
1543
+ function drawnColumn(d: DrawnRead, name: string, spec: CyjsPropertySpec): (StyleValue | null | undefined)[] {
1544
+ const { kind, order, notes, rendered } = d;
1545
+ const out = order.length;
1546
+ const col = new Array<StyleValue | null | undefined>(out).fill(null);
1547
+ const cellCol = d.cells.get(name);
1548
+ for (let k = 0; k < out && cellCol !== undefined; k++) {
1549
+ const c = order[k] >= 0 ? cellCol[order[k]] : undefined;
1550
+ if (c !== undefined) {
1551
+ col[k] = neutralOfCell(spec, name, c, { notes, target: kind, property: spec.property, element: k });
1552
+ }
1553
+ }
1554
+ if (rendered !== undefined) {
1555
+ overrideRendered(d, rendered, name, spec, col);
1556
+ }
1557
+ return col;
1558
+ }
1559
+
1560
+ /**
1561
+ * Overrides a property's values with what a live core reports.
1562
+ * @param d - the target's cells
1563
+ * @param rendered - what a live core reports
1564
+ * @param name - the property
1565
+ * @param spec - its spec
1566
+ * @param col - the values, in output order, changed in place
1567
+ */
1568
+ function overrideRendered(
1569
+ d: DrawnRead,
1570
+ rendered: NonNullable<CyjsStyleInput["rendered"]>,
1571
+ name: string,
1572
+ spec: CyjsPropertySpec,
1573
+ col: (StyleValue | null | undefined)[],
1574
+ ): void {
1575
+ const { kind, order, notes } = d;
1576
+ for (let k = 0; k < order.length; k++) {
1577
+ const r = order[k] >= 0 ? rendered.get(d.els[order[k]].json) : undefined;
1578
+ if (r !== undefined && Object.hasOwn(r, name)) {
1579
+ const site = { notes, target: kind, property: spec.property, element: k };
1580
+ const v = cyjsRendered(kind, name, r[name], site);
1581
+ // a live core reports "" for a label it does not draw
1582
+ col[k] = v === undefined || (v === "" && spec.type === "text") ? null : v;
1583
+ }
1584
+ }
1585
+ }
1586
+
1587
+ /**
1588
+ * Each edge's curve style as Cytoscape.js draws it: what a live core reports, else the winning cell.
1589
+ * @param graph - the core's elements
1590
+ * @param cells - the winning cells
1591
+ * @param rendered - what a live core reports, when given
1592
+ * @returns the curve style per edge, in core order
1593
+ */
1594
+ function curveStyles(graph: CyjsGraph, cells: Cells, rendered: CyjsStyleInput["rendered"]): unknown[] {
1595
+ const col = cells.get("curve-style");
1596
+ return graph.edges.map((el, e) => {
1597
+ const r = rendered?.get(el.json);
1598
+ if (r !== undefined && Object.hasOwn(r, "curve-style")) {
1599
+ return r["curve-style"];
1600
+ }
1601
+ const c = col?.[e];
1602
+ return typeof c === "object" && c.kind === "word" ? c.value : undefined;
1603
+ });
1604
+ }
1605
+
1606
+ /**
1607
+ * Cytoscape.js bows a `bezier` edge only to keep it apart from the other `bezier` edges joining the same two nodes:
1608
+ * it draws a lone one, and the middle one of an odd number, straight. Loops stay curved.
1609
+ * @param graph - the core's elements
1610
+ * @param styles - each edge's curve style, in core order
1611
+ * @param coreToOut - each edge's output index, or -1
1612
+ * @param curved - the drawn `curved` values in output order, changed in place
1613
+ */
1614
+ function straightenUnpairedBeziers(
1615
+ graph: CyjsGraph,
1616
+ styles: readonly unknown[],
1617
+ coreToOut: Int32Array,
1618
+ curved: (StyleValue | null)[] | undefined,
1619
+ ): void {
1620
+ if (curved === undefined) {
1621
+ return;
1622
+ }
1623
+ for (const bundle of bezierBundles(graph, styles)) {
1624
+ const k = bundle.length % 2 === 1 ? coreToOut[bundle[(bundle.length - 1) / 2]] : -1;
1625
+ if (k >= 0) {
1626
+ curved[k] = false;
1627
+ }
1628
+ }
1629
+ }
1630
+
1631
+ /**
1632
+ * The `bezier` edges that are not loops, grouped by the two nodes they join (either way round), in core order.
1633
+ * @param graph - the core's elements
1634
+ * @param styles - each edge's curve style, in core order
1635
+ * @returns the groups
1636
+ */
1637
+ function bezierBundles(graph: CyjsGraph, styles: readonly unknown[]): Iterable<number[]> {
1638
+ const bundles = new Map<string, number[]>();
1639
+ for (let e = 0; e < graph.edges.length; e++) {
1640
+ const s = graph.source[e];
1641
+ const t = graph.target[e];
1642
+ if (s === t || styles[e] !== "bezier") {
1643
+ continue;
1644
+ }
1645
+ const key = s < t ? `${s} ${t}` : `${t} ${s}`;
1646
+ const bundle = bundles.get(key);
1647
+ if (bundle === undefined) {
1648
+ bundles.set(key, [e]);
1649
+ } else {
1650
+ bundle.push(e);
1651
+ }
1652
+ }
1653
+ return bundles.values();
1654
+ }
1655
+
1656
+ /**
1657
+ * Cytoscape.js draws every loop curved: a loop whose curve style is anything but `bezier` becomes `bezier`, except
1658
+ * `haystack`, which Cytoscape.js leaves as it is because it is the property's own default.
1659
+ * @param graph - the core's elements
1660
+ * @param col - the winning curve style per edge, if any rule set one
1661
+ */
1662
+ function forceLoopCurves(graph: CyjsGraph, col: (Cell | undefined)[] | undefined): void {
1663
+ if (col === undefined) {
1664
+ return;
1665
+ }
1666
+ for (let e = 0; e < graph.edges.length; e++) {
1667
+ const c = col[e];
1668
+ if (
1669
+ graph.source[e] === graph.target[e] &&
1670
+ typeof c === "object" &&
1671
+ c.kind === "word" &&
1672
+ c.value !== "bezier" &&
1673
+ c.value !== "haystack"
1674
+ ) {
1675
+ col[e] = { kind: "word", value: "bezier", str: "bezier" };
1676
+ }
1677
+ }
1678
+ }
1679
+
1680
+ /**
1681
+ * The properties of one entry that apply to a target, parsed.
1682
+ * @param entry - the entry
1683
+ * @param kind - the target
1684
+ * @param specs - the target's carried properties
1685
+ * @param notes - where problems are noted
1686
+ * @param notCarried - counts of property names not carried
1687
+ * @returns the parsed properties, later duplicates last
1688
+ */
1689
+ function entryProperties(
1690
+ entry: Entry,
1691
+ kind: StyleTarget,
1692
+ specs: ReadonlyMap<string, CyjsPropertySpec>,
1693
+ notes: StyleNotes,
1694
+ notCarried: Map<string, number>,
1695
+ ): EntryProperty[] {
1696
+ const out: EntryProperty[] = [];
1697
+ for (const [name0, value] of entry.properties) {
1698
+ const name = entry.dashOnly ? dashOnlyName(name0) : cyjsPropertyName(name0);
1699
+ const spec = specs.get(name);
1700
+ if (spec === undefined) {
1701
+ if (kind === "node" && !CYJS_EDGE_ONLY.has(name) && !entry.tool) {
1702
+ notCarried.set(name, (notCarried.get(name) ?? 0) + 1);
1703
+ }
1704
+ continue;
1705
+ }
1706
+ const parsed = parseCyjsValue(spec, value, "rule");
1707
+ if (parsed === null || parsed === CYJS_THROWS) {
1708
+ // a rule's own value: noted for the target as a whole, so per-element notes keep their element lists
1709
+ if (!entry.tool) {
1710
+ notes.add(STYLE_ISSUE.BAD_VALUE, kind, null, { sample: `${name}: ${String(value)}` });
1711
+ }
1712
+ continue;
1713
+ }
1714
+ out.push({ name, spec, parsed });
1715
+ }
1716
+ return out;
1717
+ }
1718
+
1719
+ /**
1720
+ * A property name of an entry Cytoscape.js reads in dash form only: `content` is `label`, any other name as it is.
1721
+ * @param name - the name as written
1722
+ * @returns the name
1723
+ */
1724
+ function dashOnlyName(name: string): string {
1725
+ return name === "content" ? "label" : name;
1726
+ }
1727
+
1728
+ /** Edge properties a node never carries: not noted as dropped on nodes. */
1729
+ const CYJS_EDGE_ONLY: ReadonlySet<string> = new Set([
1730
+ "line-color",
1731
+ "line-style",
1732
+ "target-arrow-shape",
1733
+ "source-arrow-shape",
1734
+ "target-arrow-color",
1735
+ "source-arrow-color",
1736
+ "curve-style",
1737
+ "arrow-scale",
1738
+ "source-label",
1739
+ "target-label",
1740
+ ]);
1741
+
1742
+ /**
1743
+ * The elements of a target a selector matches, in order.
1744
+ * @param sel - the selector
1745
+ * @param kind - the target
1746
+ * @param n - the element count
1747
+ * @param matcher - the evaluator
1748
+ * @param ix - the target's indexes
1749
+ * @returns the element indexes
1750
+ */
1751
+ function matchList(sel: CyjsSelector, kind: StyleTarget, n: number, matcher: Matcher, ix: Indexes): readonly number[] {
1752
+ if (sel.queries.length === 0) {
1753
+ return [];
1754
+ }
1755
+ // one stamp per call marks the elements found, so a selector costs what its candidates cost, not the graph
1756
+ const stamp = ++ix.stamp;
1757
+ const marked = ix.marks;
1758
+ const hits: number[] = [];
1759
+ let scanned = false;
1760
+ const visit = (q: CyjsQuery, i: number): void => {
1761
+ if (marked[i] !== stamp && matcher.query(q, kind, i)) {
1762
+ marked[i] = stamp;
1763
+ hits.push(i);
1764
+ }
1765
+ };
1766
+ for (const q of sel.queries) {
1767
+ const cands = candidates(q, kind, ix);
1768
+ if (cands === null) {
1769
+ scanned = true;
1770
+ for (let i = 0; i < n; i++) {
1771
+ visit(q, i);
1772
+ }
1773
+ } else {
1774
+ matcher.work += cands.length;
1775
+ for (const i of cands) {
1776
+ visit(q, i);
1777
+ }
1778
+ }
1779
+ }
1780
+ if (scanned || sel.queries.length > 1) {
1781
+ hits.sort((a, b) => a - b);
1782
+ }
1783
+ matcher.work += hits.length;
1784
+ return hits;
1785
+ }
1786
+
1787
+ /**
1788
+ * The cell a property gives one element: its constant, or its mapper evaluated.
1789
+ * @param p - the property
1790
+ * @param e - the element
1791
+ * @returns the cell
1792
+ */
1793
+ function cellFor(p: EntryProperty, e: CyjsElement): Cell {
1794
+ const v = p.parsed;
1795
+ if (v.kind === "data" || v.kind === "mapData") {
1796
+ return applyCyjsMapper(p.spec, v, mapperData(e)) ?? "reset";
1797
+ }
1798
+ if (v.kind === "fn") {
1799
+ return "fn";
1800
+ }
1801
+ return v;
1802
+ }
1803
+
1804
+ /** Parsed property defaults, per property name. */
1805
+ const DEFAULT_CELLS = new Map<string, CyjsFlat | null>();
1806
+
1807
+ /**
1808
+ * A property's Cytoscape.js default as a flat value.
1809
+ * @param name - the property
1810
+ * @param spec - its spec
1811
+ * @returns the default
1812
+ */
1813
+ function defaultCell(name: string, spec: CyjsPropertySpec): CyjsFlat | null {
1814
+ const key = `${spec.type}|${name}`;
1815
+ if (!DEFAULT_CELLS.has(key)) {
1816
+ const raw = CYJS_PROPERTY_DEFAULTS.get(name);
1817
+ const parsed = raw === undefined ? null : parseCyjsValue(spec, raw, "flat");
1818
+ DEFAULT_CELLS.set(key, parsed === null || parsed === CYJS_THROWS || !isFlat(parsed) ? null : parsed);
1819
+ }
1820
+ return DEFAULT_CELLS.get(key) ?? null;
1821
+ }
1822
+
1823
+ /**
1824
+ * Whether a parsed value is flat.
1825
+ * @param v - the value
1826
+ * @returns true for a color, number or word
1827
+ */
1828
+ function isFlat(v: CyjsParsed): v is CyjsFlat {
1829
+ return v.kind === "color" || v.kind === "number" || v.kind === "word";
1830
+ }
1831
+
1832
+ /**
1833
+ * The neutral value of a cell.
1834
+ * @param spec - the property
1835
+ * @param name - its Cytoscape.js name
1836
+ * @param c - the cell
1837
+ * @param site - where it is read
1838
+ * @returns the value, null for none, or undefined when it cannot be evaluated (noted)
1839
+ */
1840
+ function neutralOfCell(spec: CyjsPropertySpec, name: string, c: Cell, site: CanonSite): StyleValue | null | undefined {
1841
+ if (c === CYJS_THROWS) {
1842
+ site.notes.add(STYLE_ISSUE.BAD_VALUE, site.target, site.property, {
1843
+ sample: "unstyleable",
1844
+ element: site.element,
1845
+ });
1846
+ return undefined;
1847
+ }
1848
+ if (c === "fn") {
1849
+ site.notes.add(STYLE_ISSUE.LAYER_FLATTENED, site.target, site.property, {
1850
+ sample: "FUNCTION",
1851
+ element: site.element,
1852
+ });
1853
+ return undefined;
1854
+ }
1855
+ const flat = c === "reset" ? defaultCell(name, spec) : c;
1856
+ if (flat === null) {
1857
+ return null;
1858
+ }
1859
+ return cyjsNeutral(spec, flat, site);
1860
+ }
1861
+
1862
+ /**
1863
+ * Combines the label alignments into `labelPosition` and `visibility` with `display` into `visible`.
1864
+ * @param kind - the target
1865
+ * @param n - the element count
1866
+ * @param words - the parts by internal name
1867
+ * @param values - the values; changed in place
1868
+ * @param notes - where problems are noted
1869
+ */
1870
+ function combineParts(
1871
+ kind: StyleTarget,
1872
+ n: number,
1873
+ words: ReadonlyMap<string, (string | null)[]>,
1874
+ values: Record<string, (StyleValue | null)[]>,
1875
+ notes: StyleNotes,
1876
+ ): void {
1877
+ const va = words.get("_valign");
1878
+ const ha = words.get("_halign");
1879
+ if (kind === "node" && (va !== undefined || ha !== undefined)) {
1880
+ values.labelPosition = Array.from({ length: n }, (_, i) =>
1881
+ labelPosition(va?.[i] ?? null, ha?.[i] ?? null, (sample) => {
1882
+ notes.add(STYLE_ISSUE.UNKNOWN_VALUE, kind, "labelPosition", { sample, element: i });
1883
+ }),
1884
+ );
1885
+ }
1886
+ const vis = words.get("_visibility");
1887
+ const disp = words.get("_display");
1888
+ if (vis !== undefined || disp !== undefined) {
1889
+ const col = new Array<StyleValue | null>(n).fill(null);
1890
+ for (let i = 0; i < n; i++) {
1891
+ const a = vis?.[i] ?? null;
1892
+ const b = disp?.[i] ?? null;
1893
+ if (a === null && b === null) {
1894
+ continue;
1895
+ }
1896
+ col[i] = a !== "hidden" && b !== "none";
1897
+ }
1898
+ values.visible = col;
1899
+ }
1900
+ }
1901
+
1902
+ /**
1903
+ * A node label's position from its vertical and horizontal alignments; an `-inside` alignment is read as its
1904
+ * outside one, noted.
1905
+ * @param v - the vertical alignment, or null
1906
+ * @param h - the horizontal alignment, or null
1907
+ * @param note - notes an alignment read as another
1908
+ * @returns the position, or null
1909
+ */
1910
+ function labelPosition(v: string | null, h: string | null, note: (sample: string) => void): StyleValue | null {
1911
+ if (v === null && h === null) {
1912
+ return null;
1913
+ }
1914
+ const vv = (v ?? "top").replace("-inside", "");
1915
+ const hh = (h ?? "center").replace("-inside", "");
1916
+ if ((v ?? "").endsWith("-inside") || (h ?? "").endsWith("-inside")) {
1917
+ note(`${v ?? ""} ${h ?? ""}`);
1918
+ }
1919
+ return POSITIONS.get(`${vv},${hh}`) ?? null;
1920
+ }
1921
+
1922
+ // ============================================================ rules
1923
+
1924
+ /**
1925
+ * The stylesheet as neutral rules over graph-io's columns and the style columns it adds.
1926
+ * @param kind - the target
1927
+ * @param applied - the entries with their matched elements and properties
1928
+ * @param core - the core's elements, the core element of each output index and the output index of each core element
1929
+ * (or -1)
1930
+ * @param core.els - the core's elements
1931
+ * @param core.order - the core element of each output index
1932
+ * @param core.coreToOut - the output index of each core element, or -1
1933
+ * @param input - the input (the graph, the rendered values)
1934
+ * @param truth - what is drawn per property, by output index
1935
+ * @param env - the evaluator (degrees), where problems are noted and how the budget is spent
1936
+ * @param env.matcher - the evaluator
1937
+ * @param env.notes - where problems are noted
1938
+ * @param env.spend - spends from the budget
1939
+ * @returns the rules, the style columns and the data columns they read
1940
+ */
1941
+ function buildRules(
1942
+ kind: StyleTarget,
1943
+ applied: readonly { entry: Entry; list: readonly number[]; props: EntryProperty[] }[],
1944
+ core: { readonly els: readonly CyjsElement[]; readonly order: Int32Array; readonly coreToOut: Int32Array },
1945
+ input: CyjsStyleInput,
1946
+ truth: Readonly<Record<string, readonly (StyleValue | null)[]>>,
1947
+ env: { readonly matcher: Matcher; readonly notes: StyleNotes; readonly spend: (ops: number) => void },
1948
+ ): { rules: StyleRule[]; columns: Map<string, StyleColumn>; rawColumns: string[] } {
1949
+ const { els, order, coreToOut } = core;
1950
+ const { matcher, notes, spend } = env;
1951
+ const out = order.length;
1952
+ const columns = new Map<string, StyleColumn>();
1953
+ const raw = new Set<string>();
1954
+ const ctx: RuleContext = {
1955
+ kind,
1956
+ els,
1957
+ coreToOut,
1958
+ out,
1959
+ snapshot: input.snapshot,
1960
+ truth: input.rendered === undefined ? null : truth,
1961
+ matcher,
1962
+ notes,
1963
+ spend,
1964
+ columns,
1965
+ raw,
1966
+ members: new Map(),
1967
+ faithful: new Map(),
1968
+ degrees: new Map(),
1969
+ };
1970
+ const drafts: Draft[] = [];
1971
+ for (const a of applied) {
1972
+ const props = a.props.filter((p) => !p.spec.property.startsWith("_"));
1973
+ if (props.length === 0 || a.list.length === 0) {
1974
+ continue;
1975
+ }
1976
+ drafts.push(draftOf(ctx, a.entry, a.list, props));
1977
+ }
1978
+ const rules = collapse(ctx, drafts);
1979
+ return { rules, columns, rawColumns: [...raw] };
1980
+ }
1981
+
1982
+ /** The state shared while rules are built. */
1983
+ interface RuleContext {
1984
+ readonly kind: StyleTarget;
1985
+ readonly els: readonly CyjsElement[];
1986
+ readonly coreToOut: Int32Array;
1987
+ readonly out: number;
1988
+ readonly snapshot: GraphSnapshot | undefined;
1989
+ /** What a live core drew, when given: the values a function-valued property is copied from. */
1990
+ readonly truth: Readonly<Record<string, readonly (StyleValue | null)[]>> | null;
1991
+ readonly matcher: Matcher;
1992
+ readonly notes: StyleNotes;
1993
+ readonly spend: (ops: number) => void;
1994
+ readonly columns: Map<string, StyleColumn>;
1995
+ readonly raw: Set<string>;
1996
+ readonly members: Map<string, string>;
1997
+ readonly faithful: Map<string, boolean>;
1998
+ readonly degrees: Map<string, boolean>;
1999
+ }
2000
+
2001
+ /** A rule with what collapsing needs. */
2002
+ interface Draft {
2003
+ /** Builds the rule (and its predicate's column) when it stays a rule of its own. */
2004
+ readonly rule: () => StyleRule;
2005
+ /** The rule's id. */
2006
+ readonly id: string;
2007
+ /** Its constants. */
2008
+ readonly set: Readonly<Record<string, StyleValue>>;
2009
+ /** Cytoscape.js's own default rule. */
2010
+ readonly tool: boolean;
2011
+ /** For a rule of one class, one id or one value test that only sets values: its kind and key. */
2012
+ readonly single: { readonly sig: string; readonly key: Scalar } | null;
2013
+ /** The matched core elements. */
2014
+ readonly list: readonly number[];
2015
+ }
2016
+
2017
+ /**
2018
+ * One entry as a rule.
2019
+ * @param ctx - the shared state
2020
+ * @param entry - the entry
2021
+ * @param list - the matched core elements
2022
+ * @param props - the carried properties
2023
+ * @returns the draft
2024
+ */
2025
+ function draftOf(ctx: RuleContext, entry: Entry, list: readonly number[], props: readonly EntryProperty[]): Draft {
2026
+ const setting = new Map<string, StyleValue>();
2027
+ const mapping = new Map<string, Mapping>();
2028
+ let flattened = false;
2029
+ const id = entry.tool
2030
+ ? `${ctx.kind}.cytoscape-js.${String(entry.index + CYJS_DEFAULT_STYLESHEET.length)}`
2031
+ : `${ctx.kind}.sheet.${String(entry.index)}`;
2032
+ for (const p of props) {
2033
+ // a later duplicate of a property replaces the earlier one
2034
+ setting.delete(p.spec.property);
2035
+ mapping.delete(p.spec.property);
2036
+ const v = p.parsed;
2037
+ if (v.kind === "fn") {
2038
+ const copied = functionCopy(ctx, entry, list, p.spec.property);
2039
+ if (copied !== null) {
2040
+ mapping.set(p.spec.property, copied);
2041
+ flattened = true;
2042
+ }
2043
+ } else if (isFlat(v)) {
2044
+ const neutral = cyjsNeutral(p.spec, v, {
2045
+ notes: new StyleNotes(),
2046
+ target: ctx.kind,
2047
+ property: p.spec.property,
2048
+ });
2049
+ if (neutral !== undefined) {
2050
+ setting.set(p.spec.property, neutral);
2051
+ }
2052
+ } else {
2053
+ mapping.set(p.spec.property, mapperMapping(ctx, entry, list, p));
2054
+ }
2055
+ }
2056
+ const set = Object.fromEntries(setting);
2057
+ const map = Object.fromEntries(mapping);
2058
+ const hasMap = mapping.size > 0;
2059
+ // the predicate is built only for a rule that is not collapsed into a run: a run's rules need no column each
2060
+ const rule = (): StyleRule => {
2061
+ const where = predicateOf(ctx, entry, list);
2062
+ return {
2063
+ id,
2064
+ target: ctx.kind,
2065
+ role: roleOf(hasMap, where),
2066
+ origin: entry.tool ? "tool" : "declared",
2067
+ where,
2068
+ ...(setting.size > 0 ? { set } : {}),
2069
+ ...(hasMap ? { map } : {}),
2070
+ facts: flattened ? { elements: list.length, reason: "FUNCTION" } : { elements: list.length },
2071
+ };
2072
+ };
2073
+ return {
2074
+ rule,
2075
+ id,
2076
+ set,
2077
+ tool: entry.tool,
2078
+ single: hasMap || entry.tool ? null : singleTest(entry.selector, ctx.kind),
2079
+ list,
2080
+ };
2081
+ }
2082
+
2083
+ /**
2084
+ * A function-valued property: its values as the live core drew them, copied per element into a style column.
2085
+ * @param ctx - the shared state
2086
+ * @param entry - the entry
2087
+ * @param list - the matched core elements
2088
+ * @param property - the neutral property
2089
+ * @returns the passthrough of the copy, or null without a live core's values
2090
+ */
2091
+ function functionCopy(ctx: RuleContext, entry: Entry, list: readonly number[], property: string): Mapping | null {
2092
+ const drawn = ctx.truth?.[property];
2093
+ if (drawn === undefined) {
2094
+ return null;
2095
+ }
2096
+ const key = `function.${String(entry.index)}.${property}`;
2097
+ const col = new Array<StyleValue | null>(ctx.out).fill(null);
2098
+ for (const i of list) {
2099
+ const k = ctx.coreToOut[i];
2100
+ if (k >= 0) {
2101
+ col[k] = drawn[k];
2102
+ }
2103
+ }
2104
+ ctx.columns.set(key, col);
2105
+ return { kind: "passthrough", by: { style: key } };
2106
+ }
2107
+
2108
+ /**
2109
+ * A rule's role from what it does.
2110
+ * @param hasMap - whether it maps values
2111
+ * @param where - its predicate
2112
+ * @returns "mapping", "defaults" (constants on every element) or "group"
2113
+ */
2114
+ function roleOf(hasMap: boolean, where: Predicate): string {
2115
+ if (hasMap) {
2116
+ return "mapping";
2117
+ }
2118
+ return "all" in where ? "defaults" : "group";
2119
+ }
2120
+
2121
+ /**
2122
+ * The varying test of a single-query selector, for collapsing runs: its last class, id or value test, with the other
2123
+ * tests (a shared scope such as `[graphty_import = "a"]`) in the signature so a run only joins rules that agree on them.
2124
+ * @param sel - the selector
2125
+ * @param kind - the target
2126
+ * @returns the run signature and the rule's key, or null
2127
+ */
2128
+ function singleTest(sel: CyjsSelector, kind: StyleTarget): { sig: string; key: Scalar } | null {
2129
+ if (sel.invalid || sel.queries.length !== 1) {
2130
+ return null;
2131
+ }
2132
+ const checks = sel.queries[0].checks.filter(
2133
+ (c) => !(c.type === "group" && (c.value === "*" || c.value === `${kind}s`)),
2134
+ );
2135
+ const c = checks.at(-1);
2136
+ if (c === undefined || checks.slice(0, -1).some((x) => x.type !== "compare" && x.type !== "class")) {
2137
+ return null;
2138
+ }
2139
+ const context = JSON.stringify(checks.slice(0, -1));
2140
+ if (c.type === "class" && typeof c.value === "string") {
2141
+ return { sig: `class\u0000${context}`, key: c.value };
2142
+ }
2143
+ if (c.type === "id") {
2144
+ return { sig: `id\u0000${context}`, key: c.value };
2145
+ }
2146
+ if (c.type === "compare" && c.op === "=" && !c.not && !c.ci) {
2147
+ return { sig: `eq\u0000${context}\u0000${c.field}`, key: c.value };
2148
+ }
2149
+ return null;
2150
+ }
2151
+
2152
+ /**
2153
+ * Whether graph-io's column holds exactly what a selector reads for every element: the same value (`===`) where the
2154
+ * raw data has a scalar, nothing where it has none, and no raw null or false (a selector sees those, a column does
2155
+ * not).
2156
+ * @param ctx - the shared state
2157
+ * @param field - the field
2158
+ * @returns true when a predicate over the column selects what the selector selects
2159
+ */
2160
+ function faithful(ctx: RuleContext, field: string): boolean {
2161
+ const known = ctx.faithful.get(field);
2162
+ if (known !== undefined) {
2163
+ return known;
2164
+ }
2165
+ let ok = ctx.snapshot !== undefined;
2166
+ const table = ctx.kind === "node" ? ctx.snapshot?.nodes : ctx.snapshot?.edges;
2167
+ const has = table?.names().includes(field) ?? false;
2168
+ for (let c = 0; ok && c < ctx.els.length; c++) {
2169
+ const k = ctx.coreToOut[c];
2170
+ const rv = selectorField(ctx.els[c], field);
2171
+ if (rv === null || rv === false) {
2172
+ ok = false;
2173
+ break;
2174
+ }
2175
+ const gv = k >= 0 && has && table !== undefined ? plainCell(table.value(field, k)) : null;
2176
+ if (rv === undefined ? gv !== null : rv !== gv) {
2177
+ ok = false;
2178
+ }
2179
+ }
2180
+ ctx.spend(ctx.els.length);
2181
+ ctx.faithful.set(field, ok);
2182
+ return ok;
2183
+ }
2184
+
2185
+ /**
2186
+ * Whether the graph's degree equals Cytoscape.js's for every node.
2187
+ * @param ctx - the shared state
2188
+ * @param which - the degree
2189
+ * @returns true when a predicate over the computed degree selects what the selector selects
2190
+ */
2191
+ function degreeAgrees(ctx: RuleContext, which: "degree" | "indegree" | "outdegree"): boolean {
2192
+ const known = ctx.degrees.get(which);
2193
+ if (known !== undefined) {
2194
+ return known;
2195
+ }
2196
+ let ok = ctx.snapshot !== undefined && ctx.kind === "node" && (which === "degree" || ctx.snapshot.directed);
2197
+ if (ok && ctx.snapshot !== undefined) {
2198
+ const mine = degreeValues(ctx.snapshot, which);
2199
+ for (let c = 0; c < ctx.els.length; c++) {
2200
+ const k = ctx.coreToOut[c];
2201
+ if (k < 0 || mine[k] !== ctx.matcher[which][c]) {
2202
+ ok = false;
2203
+ break;
2204
+ }
2205
+ }
2206
+ ok &&= ctx.els.length === ctx.out;
2207
+ }
2208
+ ctx.degrees.set(which, ok);
2209
+ return ok;
2210
+ }
2211
+
2212
+ /**
2213
+ * A selector as a short predicate when one expresses it exactly, else a membership style column.
2214
+ * @param ctx - the shared state
2215
+ * @param entry - the entry
2216
+ * @param list - the matched core elements
2217
+ * @returns the predicate
2218
+ */
2219
+ function predicateOf(ctx: RuleContext, entry: Entry, list: readonly number[]): Predicate {
2220
+ const short = shortPredicate(ctx, entry.selector);
2221
+ if (short !== null) {
2222
+ return short;
2223
+ }
2224
+ if (list.length === ctx.els.length && ctx.els.length === ctx.out) {
2225
+ return { all: true };
2226
+ }
2227
+ const sig = `${entry.tool ? "tool" : "file"}\u0000${entry.text}`;
2228
+ let key = ctx.members.get(sig);
2229
+ if (key === undefined) {
2230
+ key = `member.${String(ctx.members.size + 1)}`;
2231
+ ctx.members.set(sig, key);
2232
+ const col = new Array<StyleValue | null>(ctx.out).fill(null);
2233
+ for (const i of list) {
2234
+ const k = ctx.coreToOut[i];
2235
+ if (k >= 0) {
2236
+ col[k] = true;
2237
+ }
2238
+ }
2239
+ ctx.spend(ctx.out + list.length);
2240
+ ctx.columns.set(key, col);
2241
+ ctx.notes.add(STYLE_ISSUE.SELECTOR_AS_COLUMN, ctx.kind, key, { sample: entry.text });
2242
+ }
2243
+ return { has: { style: key } };
2244
+ }
2245
+
2246
+ /**
2247
+ * The short predicate of a selector, or null: every query of one alternative with at most 8 tests, each a group,
2248
+ * a value test or an existence test over a column graph-io holds exactly, or a degree test the graph computes the
2249
+ * same way; several alternatives only as equal tests of one field (`in`).
2250
+ * @param ctx - the shared state
2251
+ * @param sel - the selector
2252
+ * @returns the predicate, or null
2253
+ */
2254
+ function shortPredicate(ctx: RuleContext, sel: CyjsSelector): Predicate | null {
2255
+ if (sel.invalid) {
2256
+ return null;
2257
+ }
2258
+ const alts: Predicate[][] = [];
2259
+ for (const q of sel.queries) {
2260
+ const terms = queryTerms(ctx, q);
2261
+ if (terms === null) {
2262
+ return null;
2263
+ }
2264
+ if (terms !== "never") {
2265
+ alts.push(terms);
2266
+ }
2267
+ }
2268
+ if (alts.length === 0) {
2269
+ return null;
2270
+ }
2271
+ if (alts.some((t) => t.length === 0)) {
2272
+ return ctx.els.length === ctx.out ? { all: true } : null;
2273
+ }
2274
+ if (alts.length === 1) {
2275
+ const t = alts[0];
2276
+ if (t.length > 8) {
2277
+ return null;
2278
+ }
2279
+ return t.length === 1 ? t[0] : { and: t };
2280
+ }
2281
+ return equalTests(alts);
2282
+ }
2283
+
2284
+ /**
2285
+ * A query's checks as short predicate terms.
2286
+ * @param ctx - the shared state
2287
+ * @param q - the query
2288
+ * @returns the terms; "never" when its group test excludes the target; null when a check has no short term
2289
+ */
2290
+ function queryTerms(ctx: RuleContext, q: CyjsQuery): Predicate[] | "never" | null {
2291
+ const terms: Predicate[] = [];
2292
+ let never = false;
2293
+ for (const c of q.checks) {
2294
+ if (c.type === "group") {
2295
+ never ||= c.value !== "*" && c.value !== `${ctx.kind}s`;
2296
+ continue;
2297
+ }
2298
+ const t = termOf(ctx, c);
2299
+ if (t === null) {
2300
+ return null;
2301
+ }
2302
+ terms.push(t);
2303
+ }
2304
+ return never ? "never" : terms;
2305
+ }
2306
+
2307
+ /**
2308
+ * Several alternatives as one `in` predicate: only equal tests of one column, at most 8.
2309
+ * @param alts - the alternatives' terms
2310
+ * @returns the predicate, or null
2311
+ */
2312
+ function equalTests(alts: readonly Predicate[][]): Predicate | null {
2313
+ if (alts.length > 8 || !alts.every((t) => t.length === 1 && "eq" in t[0])) {
2314
+ return null;
2315
+ }
2316
+ const by = (alts[0][0] as { eq: readonly [ColumnRef, Scalar] }).eq[0];
2317
+ const values: Scalar[] = [];
2318
+ for (const t of alts) {
2319
+ const [r, v] = (t[0] as { eq: readonly [ColumnRef, Scalar] }).eq;
2320
+ if (JSON.stringify(r) !== JSON.stringify(by)) {
2321
+ return null;
2322
+ }
2323
+ values.push(v);
2324
+ }
2325
+ return { in: [by, values] };
2326
+ }
2327
+
2328
+ /**
2329
+ * One check as a short predicate term, or null.
2330
+ * @param ctx - the shared state
2331
+ * @param c - the check
2332
+ * @returns the term, or null
2333
+ */
2334
+ function termOf(ctx: RuleContext, c: CyjsCheck): Predicate | null {
2335
+ switch (c.type) {
2336
+ case "compare":
2337
+ if (c.op !== "=" || c.ci || !faithful(ctx, c.field)) {
2338
+ return null;
2339
+ }
2340
+ return c.not ? { not: { eq: [{ data: c.field }, c.value] } } : { eq: [{ data: c.field }, c.value] };
2341
+ case "exist":
2342
+ return faithful(ctx, c.field) ? { has: { data: c.field } } : null;
2343
+ case "bool":
2344
+ return c.op === "^" && faithful(ctx, c.field) ? { not: { has: { data: c.field } } } : null;
2345
+ case "meta":
2346
+ if (c.ci || !degreeAgrees(ctx, c.field)) {
2347
+ return null;
2348
+ }
2349
+ return degreeTerm(c.field, c.op, c.not, c.value);
2350
+ default:
2351
+ return null;
2352
+ }
2353
+ }
2354
+
2355
+ /**
2356
+ * A degree test as a predicate over the computed degree, so it follows later edits: `=` as `eq`, and an order test
2357
+ * as the degrees it keeps or the degrees it leaves out (degrees are whole numbers from 0), at most 8 of them.
2358
+ * @param field - the degree
2359
+ * @param op - the comparison
2360
+ * @param not - whether the test is negated
2361
+ * @param value - the number compared with
2362
+ * @returns the predicate, or null when it needs more than 8 values
2363
+ */
2364
+ function degreeTerm(
2365
+ field: "degree" | "indegree" | "outdegree",
2366
+ op: string,
2367
+ not: boolean,
2368
+ value: number,
2369
+ ): Predicate | null {
2370
+ const ref: ColumnRef = { computed: field };
2371
+ if (op === "=") {
2372
+ return not ? { not: { eq: [ref, value] } } : { eq: [ref, value] };
2373
+ }
2374
+ // The whole numbers from 0 the test leaves out (`>`, `>=`) or keeps (`<`, `<=`).
2375
+ let below: number;
2376
+ if (op === ">" || op === "<=") {
2377
+ below = Math.floor(value) + 1;
2378
+ } else if (op === ">=" || op === "<") {
2379
+ below = Math.ceil(value);
2380
+ } else {
2381
+ return null;
2382
+ }
2383
+ if (!Number.isFinite(below) || below < 1 || below > 8) {
2384
+ return null;
2385
+ }
2386
+ const values = Array.from({ length: below }, (_, k) => k);
2387
+ const keeps = op === "<" || op === "<=";
2388
+ const inSet: Predicate = { in: [ref, values] };
2389
+ return keeps !== not ? inSet : { not: inSet };
2390
+ }
2391
+
2392
+ /**
2393
+ * A mapper as a neutral mapping over graph-io's column when that gives Cytoscape.js's values for every element the
2394
+ * rule applies to, else the mapper's per-element values as a style column (`value.<rule>.<property>`).
2395
+ * @param ctx - the shared state
2396
+ * @param entry - the entry
2397
+ * @param list - the matched core elements
2398
+ * @param p - the property
2399
+ * @returns the mapping
2400
+ */
2401
+ function mapperMapping(ctx: RuleContext, entry: Entry, list: readonly number[], p: EntryProperty): Mapping {
2402
+ const v = p.parsed as CyjsParsed & { kind: "data" | "mapData" };
2403
+ const want = new Array<StyleValue | null>(list.length);
2404
+ const site: CanonSite = { notes: new StyleNotes(), target: ctx.kind, property: p.spec.property };
2405
+ for (let k = 0; k < list.length; k++) {
2406
+ const c = cellFor(p, ctx.els[list[k]]);
2407
+ const neutral = neutralOfCell(p.spec, p.name, c, site);
2408
+ want[k] = neutral ?? null;
2409
+ }
2410
+ ctx.spend(list.length * 2);
2411
+ const candidate = candidateMapping(ctx, v, p);
2412
+ if (candidate !== null && ctx.snapshot !== undefined) {
2413
+ const exact = givesWanted(
2414
+ ctx,
2415
+ ctx.snapshot,
2416
+ candidate,
2417
+ { field: v.field, property: p.spec.property },
2418
+ list,
2419
+ want,
2420
+ );
2421
+ ctx.spend(list.length);
2422
+ if (exact) {
2423
+ ctx.raw.add(v.field);
2424
+ return candidate;
2425
+ }
2426
+ }
2427
+ const key = `value.${entry.tool ? "tool" : String(entry.index)}.${p.spec.property}`;
2428
+ const col = new Array<StyleValue | null>(ctx.out).fill(null);
2429
+ for (let k = 0; k < list.length; k++) {
2430
+ const o = ctx.coreToOut[list[k]];
2431
+ if (o >= 0) {
2432
+ col[o] = want[k];
2433
+ }
2434
+ }
2435
+ ctx.columns.set(key, col);
2436
+ return { kind: "passthrough", by: { style: key } };
2437
+ }
2438
+
2439
+ /**
2440
+ * Whether a mapping over graph-io's column gives Cytoscape.js's value for every element a rule applies to.
2441
+ * @param ctx - the shared state
2442
+ * @param snapshot - the graph
2443
+ * @param candidate - the mapping
2444
+ * @param what - the column and the neutral property
2445
+ * @param what.field - the column
2446
+ * @param what.property - the neutral property
2447
+ * @param list - the matched core elements
2448
+ * @param want - Cytoscape.js's value of each, in list order
2449
+ * @returns true when it gives every one
2450
+ */
2451
+ function givesWanted(
2452
+ ctx: RuleContext,
2453
+ snapshot: GraphSnapshot,
2454
+ candidate: (Mapping & { kind: "passthrough" }) | (Mapping & { kind: "continuous" }),
2455
+ what: { readonly field: string; readonly property: string },
2456
+ list: readonly number[],
2457
+ want: readonly (StyleValue | null)[],
2458
+ ): boolean {
2459
+ const table = ctx.kind === "node" ? snapshot.nodes : snapshot.edges;
2460
+ if (!table.names().includes(what.field)) {
2461
+ return false;
2462
+ }
2463
+ const canon = canonicalFor(ctx.kind, what.property);
2464
+ for (let k = 0; k < list.length; k++) {
2465
+ const o = ctx.coreToOut[list[k]];
2466
+ const cell = o < 0 ? null : plainCell(table.value(what.field, o));
2467
+ let got: StyleValue | null = null;
2468
+ if (cell !== null && !Array.isArray(cell)) {
2469
+ got = candidate.kind === "passthrough" ? canon(cell) : evalContinuous(candidate.points, numberOf(cell));
2470
+ }
2471
+ if (o >= 0 && got !== want[k]) {
2472
+ return false;
2473
+ }
2474
+ }
2475
+ return true;
2476
+ }
2477
+
2478
+ /**
2479
+ * The neutral mapping a mapper would be over its field, before it is checked.
2480
+ * @param ctx - the shared state
2481
+ * @param v - the mapper
2482
+ * @param p - the property
2483
+ * @returns a passthrough for `data()`, a two-point continuous mapping for `mapData()`, or null
2484
+ */
2485
+ function candidateMapping(
2486
+ ctx: RuleContext,
2487
+ v: CyjsParsed & { kind: "data" | "mapData" },
2488
+ p: EntryProperty,
2489
+ ): (Mapping & { kind: "passthrough" }) | (Mapping & { kind: "continuous" }) | null {
2490
+ if (v.kind === "data") {
2491
+ return { kind: "passthrough", by: { data: v.field } };
2492
+ }
2493
+ if (v.fieldMin === v.fieldMax) {
2494
+ return null;
2495
+ }
2496
+ const site: CanonSite = { notes: new StyleNotes(), target: ctx.kind, property: p.spec.property };
2497
+ const a = cyjsNeutral(p.spec, v.min, site);
2498
+ const b = cyjsNeutral(p.spec, v.max, site);
2499
+ if (a === undefined || b === undefined) {
2500
+ return null;
2501
+ }
2502
+ const lo: ContinuousPoint = { at: v.fieldMin, below: a, value: a, above: a };
2503
+ const hi: ContinuousPoint = { at: v.fieldMax, below: b, value: b, above: b };
2504
+ return { kind: "continuous", by: { data: v.field }, points: v.fieldMin < v.fieldMax ? [lo, hi] : [hi, lo] };
2505
+ }
2506
+
2507
+ /**
2508
+ * Collapses runs of three or more rules that each test one class, one id or one value of the same field and only set
2509
+ * values into ONE mapping rule: per property, a winner column holds the key (class, id or value) of the last rule in
2510
+ * the run that matches the element and sets the property (the cascade's winner), and a discrete table maps each key
2511
+ * to that rule's value. A run keyed by a value whose column graph-io holds exactly reads that column instead.
2512
+ * @param ctx - the shared state
2513
+ * @param drafts - the rules in order
2514
+ * @returns the rules
2515
+ */
2516
+ function collapse(ctx: RuleContext, drafts: readonly Draft[]): StyleRule[] {
2517
+ const out: StyleRule[] = [];
2518
+ let run = 0;
2519
+ let i = 0;
2520
+ while (i < drafts.length) {
2521
+ const sig = drafts[i].single?.sig ?? null;
2522
+ let j = i + 1;
2523
+ while (sig !== null && j < drafts.length && drafts[j].single?.sig === sig) {
2524
+ j++;
2525
+ }
2526
+ if (sig === null || j - i < 3) {
2527
+ for (const d of drafts.slice(i, j)) {
2528
+ const rule = d.rule();
2529
+ // Cytoscape.js's own default rule over every element: the writing tool's constants already say it
2530
+ if (!(d.tool && "all" in rule.where)) {
2531
+ out.push(rule);
2532
+ }
2533
+ }
2534
+ } else {
2535
+ run++;
2536
+ out.push(runRule(ctx, drafts.slice(i, j), sig, run));
2537
+ }
2538
+ i = j;
2539
+ }
2540
+ return out;
2541
+ }
2542
+
2543
+ /**
2544
+ * A run of rules collapsed into one mapping rule.
2545
+ * @param ctx - the shared state
2546
+ * @param members - the run's rules, in order
2547
+ * @param sig - their shared signature
2548
+ * @param run - the run's number
2549
+ * @returns the rule
2550
+ */
2551
+ function runRule(ctx: RuleContext, members: readonly Draft[], sig: string, run: number): StyleRule {
2552
+ const props = new Set(members.flatMap((d) => Object.keys(d.set)));
2553
+ // a value run with no shared tests may read the field itself
2554
+ const parts = sig.split("\u0000");
2555
+ const field = parts[0] === "eq" && parts[1] === "[]" ? parts[2] : null;
2556
+ const map: Record<string, Mapping> = {};
2557
+ for (const p of props) {
2558
+ map[p] = runMapping(ctx, members, p, { field, key: `winner.${String(run)}.${p}` });
2559
+ }
2560
+ const first = members[0].id.split(".").pop() ?? "";
2561
+ return {
2562
+ id: `${ctx.kind}.sheet.by.${parts[0] === "eq" ? "value" : parts[0]}.${first}`,
2563
+ target: ctx.kind,
2564
+ role: "mapping",
2565
+ origin: "declared",
2566
+ where: { all: true },
2567
+ map,
2568
+ facts: { elements: members.reduce((sum, d) => sum + d.list.length, 0) },
2569
+ };
2570
+ }
2571
+
2572
+ /**
2573
+ * One property of a collapsed run: a winner column holds the key of the last rule in the run that matches the
2574
+ * element and sets the property (the cascade's winner), and a discrete table maps each key to that rule's value; a
2575
+ * run keyed by a value whose column graph-io holds exactly reads that column instead.
2576
+ * @param ctx - the shared state
2577
+ * @param members - the run's rules, in order
2578
+ * @param p - the property
2579
+ * @param names - the field a value run tests (null for another run) and the winner column's key
2580
+ * @param names.field - the field a value run tests, or null
2581
+ * @param names.key - the winner column's key
2582
+ * @returns the mapping
2583
+ */
2584
+ function runMapping(
2585
+ ctx: RuleContext,
2586
+ members: readonly Draft[],
2587
+ p: string,
2588
+ names: { readonly field: string | null; readonly key: string },
2589
+ ): Mapping {
2590
+ const winner = new Array<StyleValue | null>(ctx.out).fill(null);
2591
+ const table = new Map<string, [Scalar, StyleValue]>();
2592
+ for (const d of members) {
2593
+ const value = (d.set as Record<string, StyleValue | undefined>)[p];
2594
+ if (value === undefined || d.single === null) {
2595
+ continue;
2596
+ }
2597
+ const { key } = d.single;
2598
+ table.set(`${typeof key}:${String(key)}`, [key, value]);
2599
+ for (const c of d.list) {
2600
+ const k = ctx.coreToOut[c];
2601
+ if (k >= 0) {
2602
+ winner[k] = key;
2603
+ }
2604
+ }
2605
+ ctx.spend(d.list.length);
2606
+ }
2607
+ const entries = [...table.values()];
2608
+ // a table key that names a later rule's value: the table maps each key to the last rule's value for it
2609
+ const byKey = new Map(entries.map(([k, v]) => [`${typeof k}:${String(k)}`, v]));
2610
+ const { field } = names;
2611
+ if (field !== null && faithful(ctx, field) && sameByData(ctx, field, winner, byKey)) {
2612
+ return { kind: "discrete", by: { data: field }, table: entries };
2613
+ }
2614
+ ctx.columns.set(names.key, winner);
2615
+ return { kind: "discrete", by: { style: names.key }, table: entries };
2616
+ }
2617
+
2618
+ /**
2619
+ * Whether a discrete table keyed by a column's own values gives every element its winner's value.
2620
+ * @param ctx - the shared state
2621
+ * @param field - the column
2622
+ * @param winner - each element's winning key, or null
2623
+ * @param byKey - the table
2624
+ * @returns true when reading the column gives the same values
2625
+ */
2626
+ function sameByData(
2627
+ ctx: RuleContext,
2628
+ field: string,
2629
+ winner: readonly (StyleValue | null)[],
2630
+ byKey: ReadonlyMap<string, StyleValue>,
2631
+ ): boolean {
2632
+ const table = ctx.kind === "node" ? ctx.snapshot?.nodes : ctx.snapshot?.edges;
2633
+ if (!table?.names().includes(field)) {
2634
+ return false;
2635
+ }
2636
+ for (let k = 0; k < ctx.out; k++) {
2637
+ const v = plainCell(table.value(field, k));
2638
+ const viaData =
2639
+ typeof v === "string" || typeof v === "number" ? (byKey.get(`${typeof v}:${String(v)}`) ?? null) : null;
2640
+ const w = winner[k];
2641
+ const viaWinner = w === null ? null : (byKey.get(`${typeof w}:${String(w)}`) ?? null);
2642
+ if (viaData !== viaWinner) {
2643
+ return false;
2644
+ }
2645
+ }
2646
+ return true;
2647
+ }
2648
+
2649
+ // ============================================================ the importer path
2650
+
2651
+ /**
2652
+ * A Cytoscape.js file's styles as an importer's `styles()` returns them: what Cytoscape.js draws per element and the
2653
+ * stylesheet's rules, with Cytoscape.js as the writing tool whose defaults fill what the stylesheet leaves unset.
2654
+ * @param input - the elements, the stylesheet, the graph and the options
2655
+ * @returns the styles for graph-io's planner
2656
+ */
2657
+ export function cytoscapeJsStyles(input: CyjsStyleInput & { readonly snapshot: GraphSnapshot }): ImporterStyles {
2658
+ const notes = input.notes ?? new StyleNotes();
2659
+ const r = evaluateCytoscapeJs({ ...input, notes });
2660
+ const output = (t: CyjsTargetStyles): ReaderOutput => ({ values: t.values, rules: t.rules, columns: t.columns });
2661
+ return {
2662
+ canonical: true,
2663
+ dialect: "cytoscape-js",
2664
+ tool: "cytoscape-js",
2665
+ units: "px",
2666
+ node: output(r.node),
2667
+ edge: output(r.edge),
2668
+ notes: notes.list(),
2669
+ };
2670
+ }