@knowvah/dot-engine 2.6.11 → 2.7.0

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 (270) hide show
  1. package/README.md +1 -1
  2. package/dist/api.js +18 -4
  3. package/dist/api.js.map +3 -3
  4. package/dist/async/render-async.d.ts.map +1 -1
  5. package/dist/common/agerr.d.ts +37 -0
  6. package/dist/common/agerr.d.ts.map +1 -0
  7. package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
  8. package/dist/common/color.d.ts.map +1 -1
  9. package/dist/common/crand.d.ts +0 -20
  10. package/dist/common/crand.d.ts.map +1 -1
  11. package/dist/common/emit-once.d.ts +6 -0
  12. package/dist/common/emit-once.d.ts.map +1 -0
  13. package/dist/common/graph-init.d.ts.map +1 -1
  14. package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
  15. package/dist/common/htmltable-emit-fill.d.ts +29 -2
  16. package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
  17. package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
  18. package/dist/common/htmltable-emit.d.ts.map +1 -1
  19. package/dist/common/htmltable-lex-expat.d.ts +1 -6
  20. package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
  21. package/dist/common/htmltable-pos-runs.d.ts +7 -4
  22. package/dist/common/htmltable-pos-runs.d.ts.map +1 -1
  23. package/dist/common/htmltable.d.ts +1 -7
  24. package/dist/common/htmltable.d.ts.map +1 -1
  25. package/dist/common/libm-trig.d.ts +5 -0
  26. package/dist/common/libm-trig.d.ts.map +1 -0
  27. package/dist/common/multicolor.d.ts +21 -13
  28. package/dist/common/multicolor.d.ts.map +1 -1
  29. package/dist/common/poly-gencode.d.ts +1 -2
  30. package/dist/common/poly-gencode.d.ts.map +1 -1
  31. package/dist/common/poly-init.d.ts.map +1 -1
  32. package/dist/common/postproc.d.ts.map +1 -1
  33. package/dist/common/random.d.ts.map +1 -1
  34. package/dist/common/render-state.d.ts +8 -0
  35. package/dist/common/render-state.d.ts.map +1 -0
  36. package/dist/common/splines-clip.d.ts +7 -0
  37. package/dist/common/splines-clip.d.ts.map +1 -1
  38. package/dist/common/splines-geom.d.ts +8 -0
  39. package/dist/common/splines-geom.d.ts.map +1 -1
  40. package/dist/common/splines-routespl.d.ts.map +1 -1
  41. package/dist/common/textmeasure-lookup.d.ts.map +1 -1
  42. package/dist/common/textmeasure.d.ts.map +1 -1
  43. package/dist/gvc/anchor.d.ts.map +1 -1
  44. package/dist/gvc/context.d.ts +6 -0
  45. package/dist/gvc/context.d.ts.map +1 -1
  46. package/dist/gvc/device-cluster.d.ts +15 -0
  47. package/dist/gvc/device-cluster.d.ts.map +1 -1
  48. package/dist/gvc/device-labels.d.ts +14 -0
  49. package/dist/gvc/device-labels.d.ts.map +1 -1
  50. package/dist/gvc/device.d.ts.map +1 -1
  51. package/dist/gvc/first-layer.d.ts +9 -0
  52. package/dist/gvc/first-layer.d.ts.map +1 -0
  53. package/dist/gvc/gvrender-set-style.d.ts +28 -0
  54. package/dist/gvc/gvrender-set-style.d.ts.map +1 -0
  55. package/dist/gvc/usershape.d.ts +18 -0
  56. package/dist/gvc/usershape.d.ts.map +1 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +3169 -1296
  59. package/dist/index.js.map +4 -4
  60. package/dist/layout/dot/cluster.d.ts.map +1 -1
  61. package/dist/layout/dot/compound.d.ts.map +1 -1
  62. package/dist/layout/dot/conc.d.ts.map +1 -1
  63. package/dist/layout/dot/decomp.d.ts.map +1 -1
  64. package/dist/layout/dot/edge-route-clip.d.ts +32 -14
  65. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  66. package/dist/layout/dot/edge-route-routing.d.ts.map +1 -1
  67. package/dist/layout/dot/mincross-build.d.ts.map +1 -1
  68. package/dist/layout/dot/mincross-cross.d.ts.map +1 -1
  69. package/dist/layout/dot/rank.d.ts.map +1 -1
  70. package/dist/layout/dot/splines.d.ts.map +1 -1
  71. package/dist/layout/fdp/index.d.ts.map +1 -1
  72. package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
  73. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  74. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts +11 -0
  75. package/dist/layout/neato/__fixtures__/dijkstra-f.d.ts.map +1 -0
  76. package/dist/layout/neato/__fixtures__/hier-imds.d.ts +36 -0
  77. package/dist/layout/neato/__fixtures__/hier-imds.d.ts.map +1 -0
  78. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts +32 -0
  79. package/dist/layout/neato/__fixtures__/hier-majorization.d.ts.map +1 -0
  80. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  81. package/dist/layout/neato/compute-hierarchy.d.ts +24 -0
  82. package/dist/layout/neato/compute-hierarchy.d.ts.map +1 -0
  83. package/dist/layout/neato/conjgrad-mkernel.d.ts +20 -0
  84. package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -0
  85. package/dist/layout/neato/conjgrad.d.ts +19 -0
  86. package/dist/layout/neato/conjgrad.d.ts.map +1 -1
  87. package/dist/layout/neato/constrained-majorization.d.ts +26 -0
  88. package/dist/layout/neato/constrained-majorization.d.ts.map +1 -0
  89. package/dist/layout/neato/dijkstra.d.ts +33 -16
  90. package/dist/layout/neato/dijkstra.d.ts.map +1 -1
  91. package/dist/layout/neato/edge-len.d.ts.map +1 -1
  92. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  93. package/dist/layout/neato/gts-triangle.d.ts +14 -0
  94. package/dist/layout/neato/gts-triangle.d.ts.map +1 -0
  95. package/dist/layout/neato/init.d.ts +6 -22
  96. package/dist/layout/neato/init.d.ts.map +1 -1
  97. package/dist/layout/neato/kk.d.ts.map +1 -1
  98. package/dist/layout/neato/kkutils.d.ts +20 -0
  99. package/dist/layout/neato/kkutils.d.ts.map +1 -0
  100. package/dist/layout/neato/make-graph-data.d.ts +43 -0
  101. package/dist/layout/neato/make-graph-data.d.ts.map +1 -0
  102. package/dist/layout/neato/matrix-ops.d.ts +46 -0
  103. package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
  104. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  105. package/dist/layout/neato/multispline.d.ts.map +1 -1
  106. package/dist/layout/neato/opt-arrangement.d.ts +20 -0
  107. package/dist/layout/neato/opt-arrangement.d.ts.map +1 -0
  108. package/dist/layout/neato/poly.d.ts.map +1 -1
  109. package/dist/layout/neato/quad-prog-solve.d.ts +44 -0
  110. package/dist/layout/neato/quad-prog-solve.d.ts.map +1 -0
  111. package/dist/layout/neato/sgd.d.ts.map +1 -1
  112. package/dist/layout/neato/smart-ini-x.d.ts +9 -0
  113. package/dist/layout/neato/smart-ini-x.d.ts.map +1 -0
  114. package/dist/layout/neato/splines.d.ts +1 -1
  115. package/dist/layout/neato/splines.d.ts.map +1 -1
  116. package/dist/layout/neato/start.d.ts +4 -1
  117. package/dist/layout/neato/start.d.ts.map +1 -1
  118. package/dist/layout/neato/stress.d.ts +8 -0
  119. package/dist/layout/neato/stress.d.ts.map +1 -1
  120. package/dist/model/edge.d.ts +1 -1
  121. package/dist/model/edge.d.ts.map +1 -1
  122. package/dist/model/lazy-attrs.d.ts +10 -0
  123. package/dist/model/lazy-attrs.d.ts.map +1 -0
  124. package/dist/model/node.d.ts +1 -1
  125. package/dist/model/node.d.ts.map +1 -1
  126. package/dist/ortho/index.d.ts.map +1 -1
  127. package/dist/parser/index.d.ts.map +1 -1
  128. package/dist/render/color-resolve.d.ts +49 -1
  129. package/dist/render/color-resolve.d.ts.map +1 -1
  130. package/dist/render/dot/edge-draw.d.ts +1 -1
  131. package/dist/render/dot/edge-draw.d.ts.map +1 -1
  132. package/dist/render/public.d.ts.map +1 -1
  133. package/dist/render/svg-edge-split.d.ts +2 -0
  134. package/dist/render/svg-edge-split.d.ts.map +1 -1
  135. package/dist/render/svg-multicolor.d.ts +2 -2
  136. package/dist/render/svg-multicolor.d.ts.map +1 -1
  137. package/dist/render/xdot-public.d.ts.map +1 -1
  138. package/dist/render.js +2799 -927
  139. package/dist/render.js.map +4 -4
  140. package/package.json +1 -1
  141. package/src/async/render-async.ts +6 -0
  142. package/src/common/agerr.ts +96 -0
  143. package/src/common/arrows-shapes-poly.ts +19 -5
  144. package/src/common/color.ts +38 -10
  145. package/src/common/crand.ts +8 -2
  146. package/src/common/emit-once.ts +27 -0
  147. package/src/common/graph-init.ts +2 -1
  148. package/src/common/htmltable-attr-warn.ts +3 -4
  149. package/src/common/htmltable-emit-fill.ts +104 -11
  150. package/src/common/htmltable-emit-rules.ts +2 -0
  151. package/src/common/htmltable-emit.ts +4 -3
  152. package/src/common/htmltable-lex-expat.ts +96 -23
  153. package/src/common/htmltable-pos-runs.ts +11 -6
  154. package/src/common/htmltable.ts +28 -117
  155. package/src/common/libm-trig.ts +43 -0
  156. package/src/common/multicolor.ts +88 -24
  157. package/src/common/poly-gencode.ts +30 -10
  158. package/src/common/poly-init.ts +20 -1
  159. package/src/common/postproc.ts +4 -1
  160. package/src/common/random.ts +11 -1
  161. package/src/common/render-state.ts +36 -0
  162. package/src/common/splines-clip.ts +15 -12
  163. package/src/common/splines-geom.ts +8 -4
  164. package/src/common/splines-routespl.ts +2 -1
  165. package/src/common/textmeasure-lookup.ts +9 -2
  166. package/src/common/textmeasure.ts +45 -2
  167. package/src/gvc/anchor.ts +1 -12
  168. package/src/gvc/context.ts +8 -0
  169. package/src/gvc/device-cluster.ts +93 -10
  170. package/src/gvc/device-labels.ts +27 -0
  171. package/src/gvc/device.ts +34 -8
  172. package/src/gvc/first-layer.ts +26 -0
  173. package/src/gvc/gvrender-set-style.ts +114 -0
  174. package/src/gvc/usershape.ts +26 -0
  175. package/src/index.ts +3 -0
  176. package/src/layout/dot/cluster.ts +2 -1
  177. package/src/layout/dot/compound.ts +3 -4
  178. package/src/layout/dot/conc.ts +6 -5
  179. package/src/layout/dot/decomp.ts +3 -0
  180. package/src/layout/dot/edge-route-clip.ts +46 -28
  181. package/src/layout/dot/edge-route-routing.ts +11 -3
  182. package/src/layout/dot/fastgr.ts +2 -2
  183. package/src/layout/dot/mincross-build.ts +2 -0
  184. package/src/layout/dot/mincross-cross.ts +2 -0
  185. package/src/layout/dot/rank.ts +2 -0
  186. package/src/layout/dot/splines.ts +2 -1
  187. package/src/layout/fdp/index.ts +2 -1
  188. package/src/layout/fdp/tlayout-parms.ts +9 -2
  189. package/src/layout/fdp/xlayout.ts +10 -2
  190. package/src/layout/neato/__fixtures__/dijkstra-f.ts +17 -0
  191. package/src/layout/neato/__fixtures__/hier/c1554.gv +33 -0
  192. package/src/layout/neato/__fixtures__/hier/c1554.plain +26 -0
  193. package/src/layout/neato/__fixtures__/hier/c1554.xdot +161 -0
  194. package/src/layout/neato/__fixtures__/hier/cases/cycle.gv +1 -0
  195. package/src/layout/neato/__fixtures__/hier/cases/cycle.plain +10 -0
  196. package/src/layout/neato/__fixtures__/hier/cases/cycle.stderr +0 -0
  197. package/src/layout/neato/__fixtures__/hier/cases/dirnone.gv +1 -0
  198. package/src/layout/neato/__fixtures__/hier/cases/dirnone.plain +11 -0
  199. package/src/layout/neato/__fixtures__/hier/cases/dirnone.stderr +0 -0
  200. package/src/layout/neato/__fixtures__/hier/cases/epsilon.gv +1 -0
  201. package/src/layout/neato/__fixtures__/hier/cases/epsilon.plain +13 -0
  202. package/src/layout/neato/__fixtures__/hier/cases/epsilon.stderr +0 -0
  203. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.gv +1 -0
  204. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.plain +8 -0
  205. package/src/layout/neato/__fixtures__/hier/cases/levelsgap.stderr +0 -0
  206. package/src/layout/neato/__fixtures__/hier/cases/mds.gv +1 -0
  207. package/src/layout/neato/__fixtures__/hier/cases/mds.plain +10 -0
  208. package/src/layout/neato/__fixtures__/hier/cases/mds.stderr +0 -0
  209. package/src/layout/neato/__fixtures__/hier/cases/multiedge.gv +1 -0
  210. package/src/layout/neato/__fixtures__/hier/cases/multiedge.plain +10 -0
  211. package/src/layout/neato/__fixtures__/hier/cases/multiedge.stderr +0 -0
  212. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.gv +1 -0
  213. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.plain +8 -0
  214. package/src/layout/neato/__fixtures__/hier/cases/neglevelsgap.stderr +0 -0
  215. package/src/layout/neato/__fixtures__/hier/cases/startrandom.gv +1 -0
  216. package/src/layout/neato/__fixtures__/hier/cases/startrandom.plain +10 -0
  217. package/src/layout/neato/__fixtures__/hier/cases/startrandom.stderr +0 -0
  218. package/src/layout/neato/__fixtures__/hier/cases/startregular.gv +1 -0
  219. package/src/layout/neato/__fixtures__/hier/cases/startregular.plain +10 -0
  220. package/src/layout/neato/__fixtures__/hier/cases/startregular.stderr +0 -0
  221. package/src/layout/neato/__fixtures__/hier/cases/tdist.gv +1 -0
  222. package/src/layout/neato/__fixtures__/hier/cases/tdist.plain +13 -0
  223. package/src/layout/neato/__fixtures__/hier/cases/tdist.stderr +0 -0
  224. package/src/layout/neato/__fixtures__/hier/cases/undirected.gv +1 -0
  225. package/src/layout/neato/__fixtures__/hier/cases/undirected.plain +10 -0
  226. package/src/layout/neato/__fixtures__/hier/cases/undirected.stderr +0 -0
  227. package/src/layout/neato/__fixtures__/hier/hier-probe.gv +1 -0
  228. package/src/layout/neato/__fixtures__/hier/hier-probe.plain +16 -0
  229. package/src/layout/neato/__fixtures__/hier/hier-probe.xdot +62 -0
  230. package/src/layout/neato/__fixtures__/hier-imds.ts +151 -0
  231. package/src/layout/neato/__fixtures__/hier-majorization.ts +145 -0
  232. package/src/layout/neato/cdt-surface.ts +12 -22
  233. package/src/layout/neato/compute-hierarchy.ts +90 -0
  234. package/src/layout/neato/conjgrad-mkernel.ts +141 -0
  235. package/src/layout/neato/conjgrad.ts +208 -0
  236. package/src/layout/neato/constrained-majorization.ts +479 -0
  237. package/src/layout/neato/dijkstra.ts +218 -92
  238. package/src/layout/neato/edge-len.ts +2 -1
  239. package/src/layout/neato/fdp-adjust.ts +3 -2
  240. package/src/layout/neato/gts-triangle.ts +42 -0
  241. package/src/layout/neato/init.ts +68 -68
  242. package/src/layout/neato/kk.ts +2 -1
  243. package/src/layout/neato/kkutils.ts +96 -0
  244. package/src/layout/neato/make-graph-data.ts +166 -0
  245. package/src/layout/neato/matrix-ops.ts +138 -0
  246. package/src/layout/neato/multispline-router.ts +3 -3
  247. package/src/layout/neato/multispline.ts +3 -2
  248. package/src/layout/neato/opt-arrangement.ts +86 -0
  249. package/src/layout/neato/poly.ts +3 -2
  250. package/src/layout/neato/quad-prog-solve.ts +489 -0
  251. package/src/layout/neato/sgd.ts +5 -2
  252. package/src/layout/neato/smart-ini-x.ts +503 -0
  253. package/src/layout/neato/splines.ts +4 -4
  254. package/src/layout/neato/start.ts +19 -14
  255. package/src/layout/neato/stress.ts +28 -0
  256. package/src/model/edge.ts +3 -2
  257. package/src/model/lazy-attrs.ts +36 -0
  258. package/src/model/node.ts +3 -2
  259. package/src/ortho/index.ts +2 -1
  260. package/src/parser/dot.js +4 -1
  261. package/src/parser/dot.pegjs +5 -1
  262. package/src/parser/index.ts +4 -3
  263. package/src/render/color-resolve.ts +193 -4
  264. package/src/render/dot/edge-draw.ts +6 -3
  265. package/src/render/dot.ts +1 -1
  266. package/src/render/public.ts +2 -0
  267. package/src/render/svg-edge-split.ts +6 -4
  268. package/src/render/svg-multicolor.ts +4 -3
  269. package/src/render/svg-parallel-edge.ts +2 -2
  270. package/src/render/xdot-public.ts +2 -0
@@ -270,12 +270,36 @@ function scanCdata(text: string, p: number, base: number, dataOf: CdataText, tok
270
270
  return close < 0 ? -1 : close + CDATA_CLOSE.length;
271
271
  }
272
272
 
273
- /** Character data at `p` up to the next `<`: one `text` token, then the index it ends at. */
274
- function scanText(text: string, p: number, base: number, tokens: ExpatToken[]): number {
273
+ /**
274
+ * A character expat's tokenizer rejects in character data (BT_NONXML in the
275
+ * UTF-8 table): controls other than tab, LF and CR, U+FFFE and U+FFFF, and a
276
+ * lone surrogate (the UTF-8 form of one is invalid). DEL is BT_OTHER, valid.
277
+ * @see expat lib/asciitab.h
278
+ * @see expat lib/xmltok_impl.c:PREFIX(contentTok) (BT_NONXML)
279
+ */
280
+ const CONTENT_NON_XML = /[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
281
+
282
+ /**
283
+ * Character data at `p` up to the next `<`: one `text` token, then the index it
284
+ * ends at. A reference cut off by the end of the data is not part of the token
285
+ * (`held`): expat waits for the characters that settle it. A BT_NONXML character
286
+ * ends the data token; the next call finds it an invalid token (`invalid`).
287
+ * @see expat lib/xmltok_impl.c:PREFIX(contentTok) (XML_TOK_PARTIAL, BT_NONXML)
288
+ */
289
+ function scanText(
290
+ text: string, p: number, base: number, tokens: ExpatToken[],
291
+ ): { at: number; held: boolean; invalid?: number } {
275
292
  const e = text.indexOf('<', p);
276
- const end = e < 0 ? text.length : e;
277
- tokens.push({ kind: 'text', start: base + p, end: base + end });
278
- return end;
293
+ const slice = text.slice(p, e < 0 ? text.length : e);
294
+ const bad = CONTENT_NON_XML.exec(slice)?.index;
295
+ if (bad !== undefined) {
296
+ if (bad > 0) tokens.push({ kind: 'text', start: base + p, end: base + p + bad });
297
+ return { at: p + bad, held: false, invalid: p + bad };
298
+ }
299
+ const from = e < 0 ? heldRefStart(slice) : undefined;
300
+ const end = from === undefined ? p + slice.length : p + from;
301
+ if (end > p) tokens.push({ kind: 'text', start: base + p, end: base + end });
302
+ return { at: end, held: from !== undefined };
279
303
  }
280
304
 
281
305
  /** The fed text of the label source between two offsets. */
@@ -295,7 +319,10 @@ function scanContent(text: string, base: number, inCdata: boolean, dataOf: Cdata
295
319
  continue;
296
320
  }
297
321
  if (text[p] !== '<') {
298
- p = scanText(text, p, base, tokens);
322
+ const scanned = scanText(text, p, base, tokens);
323
+ if (scanned.invalid !== undefined) return { tokens, consumed: scanned.at, invalid: base + scanned.invalid, inCdata: false };
324
+ if (scanned.held) return { tokens, consumed: scanned.at, inCdata: false };
325
+ p = scanned.at;
299
326
  continue;
300
327
  }
301
328
  const { scan, kind } = scanMarkup(text, p);
@@ -408,33 +435,79 @@ export class ExpatFeed {
408
435
  /** Entities expat knows without a DTD; `scanEntity` leaves them as written. */
409
436
  const XML_PREDEFINED_ENTITIES = new Set(['lt', 'gt', 'amp', 'quot', 'apos']);
410
437
 
411
- const ENTITY_NAME = /^[A-Za-z_:][\w.:-]*$/;
438
+ /** An `&name;` that `scanEntity` rewrites: 2 to 8 characters (alphanumeric) that name an HTML entity. */
439
+ const SCANNED_ENTITY = /^&([^;&<]*);/;
412
440
 
413
- const CHAR_REF = /^&#(?:x[0-9A-Fa-f]+|[0-9]+);/;
441
+ /**
442
+ * The reference the content tokenizer reads at the `&` at `t`: the index after
443
+ * its `;`, or the index of the character that makes it an invalid token (the
444
+ * end of `text` when the reference runs out: `</HTML>` or another tag follows).
445
+ * Runs the attribute value scanner over the reference alone, closed by a quote,
446
+ * which is scanRef on the same bytes.
447
+ * @see expat lib/xmltok_impl.c:PREFIX(contentTok) (BT_AMP)
448
+ * @see expat lib/xmltok_impl.c:PREFIX(scanRef)
449
+ * @see expat lib/xmltok_impl.c:PREFIX(scanCharRef)
450
+ * @see expat lib/xmltok_impl.c:PREFIX(scanHexCharRef)
451
+ */
452
+ function contentRef(text: string, t: number): { end: number } | { invalid: number } {
453
+ const semi = text.indexOf(';', t);
454
+ const ref = text.slice(t, semi < 0 ? text.length : semi + 1);
455
+ const scan = scanAttValue(`"${ref}"`, 0);
456
+ if ('close' in scan) return { end: t + scan.close - 1 };
457
+ return { invalid: 'invalid' in scan ? t + scan.invalid - 1 : text.length };
458
+ }
459
+
460
+ /** A reference problem of character data: expat's error, where its event pointer is, and the `&` it began at. */
461
+ interface RefProblem {
462
+ message: string;
463
+ offset: number;
464
+ from: number;
465
+ }
414
466
 
415
467
  /**
416
- * The first reference of character data that expat rejects, at its offset: a
417
- * `&name;` scanEntity leaves alone (neither an HTML entity nor predefined), or
418
- * a character reference XML does not allow.
419
- * @see expat lib/xmlparse.c:doContent (XML_TOK_ENTITY_REF, XML_TOK_CHAR_REF)
468
+ * The first reference of character data that expat rejects: an invalid token
469
+ * (a bare `&`, `&#;`, `&#X41;`, `&#65q`, ..), a `&name;` scanEntity leaves
470
+ * alone (neither an HTML entity nor predefined), or a character reference XML
471
+ * does not allow. An invalid token at the very end of `text` is a reference
472
+ * expat is still waiting on.
473
+ * @see lib/common/utils.c:scanEntity
474
+ * @see expat lib/xmlparse.c:doContent (XML_TOK_INVALID, XML_TOK_ENTITY_REF, XML_TOK_CHAR_REF)
420
475
  */
421
- export function textRefError(text: string): { message: string; offset: number } | undefined {
422
- for (let t = text.indexOf('&'); t >= 0; t = text.indexOf('&', t + 1)) {
423
- if (text[t + 1] === '#') {
424
- const ref = CHAR_REF.exec(text.slice(t))?.[0];
425
- if (ref !== undefined && charRefNumber(ref) < 0) return { message: XML_ERROR_BAD_CHAR_REF, offset: t };
426
- continue;
476
+ function firstRefProblem(text: string): RefProblem | undefined {
477
+ let t = text.indexOf('&');
478
+ while (t >= 0) {
479
+ if (text[t + 1] !== '#') {
480
+ const name = SCANNED_ENTITY.exec(text.slice(t))?.[1];
481
+ if (name !== undefined && name.length >= 2 && Object.hasOwn(HTML_ENTITIES, name)) {
482
+ t = text.indexOf('&', t + name.length + 2);
483
+ continue;
484
+ }
427
485
  }
428
- const semi = text.indexOf(';', t + 1);
429
- if (semi < 0) continue;
430
- const key = text.slice(t + 1, semi);
431
- if (ENTITY_NAME.test(key) && !Object.hasOwn(HTML_ENTITIES, key) && !XML_PREDEFINED_ENTITIES.has(key)) {
432
- return { message: XML_ERROR_UNDEFINED_ENTITY, offset: t };
486
+ const ref = contentRef(text, t);
487
+ if ('invalid' in ref) return { message: XML_ERROR_INVALID_TOKEN, offset: ref.invalid, from: t };
488
+ const key = text.slice(t + 1, ref.end - 1);
489
+ if (text[t + 1] === '#') {
490
+ if (charRefNumber(`&${key};`) < 0) return { message: XML_ERROR_BAD_CHAR_REF, offset: t, from: t };
491
+ } else if (!XML_PREDEFINED_ENTITIES.has(key)) {
492
+ return { message: XML_ERROR_UNDEFINED_ENTITY, offset: t, from: t };
433
493
  }
494
+ t = text.indexOf('&', ref.end);
434
495
  }
435
496
  return undefined;
436
497
  }
437
498
 
499
+ /** The first reference of character data that expat rejects, and the offset of its event pointer. */
500
+ export function textRefError(text: string): { message: string; offset: number } | undefined {
501
+ const problem = firstRefProblem(text);
502
+ return problem === undefined ? undefined : { message: problem.message, offset: problem.offset };
503
+ }
504
+
505
+ /** Where a reference that runs to the end of `text` begins: expat holds it until more data arrives. */
506
+ function heldRefStart(text: string): number | undefined {
507
+ const problem = firstRefProblem(text);
508
+ return problem?.message === XML_ERROR_INVALID_TOKEN && problem.offset === text.length ? problem.from : undefined;
509
+ }
510
+
438
511
  export interface TagChunk {
439
512
  close: boolean;
440
513
  selfClose: boolean;
@@ -48,7 +48,10 @@ export interface HtmlFontInfo {
48
48
  fontcolor: string;
49
49
  }
50
50
 
51
- interface LineRun {
51
+ /** The label font defaults the line builder needs (sizing env or placement finfo). */
52
+ export type LabelFont = Pick<HtmlFontInfo, 'fontname' | 'fontsize'>;
53
+
54
+ export interface LineRun {
52
55
  items: HtmlTextItem[];
53
56
  width: number;
54
57
  /** Max raw font size over items (C mxfsize). */
@@ -84,7 +87,7 @@ export function itemFontFlags(item: HtmlTextItem): number {
84
87
  }
85
88
 
86
89
  /** Resolved face/size for an item, defaulting to the inherited finfo. */
87
- function itemFace(item: HtmlTextItem, finfo: HtmlFontInfo): { fs: number; face: string } {
90
+ function itemFace(item: HtmlTextItem, finfo: LabelFont): { fs: number; face: string } {
88
91
  return {
89
92
  fs: item.fontSize !== undefined ? item.fontSize : finfo.fontsize,
90
93
  face: item.fontFace !== undefined ? item.fontFace : finfo.fontname,
@@ -95,7 +98,7 @@ function itemFace(item: HtmlTextItem, finfo: HtmlFontInfo): { fs: number; face:
95
98
  interface ItemMetric { w: number; h: number; fs: number; yoffsetCenterline?: number; yoffsetLayout?: number; }
96
99
 
97
100
  /** Measure one text item; null when the item carries no text. */
98
- function measureItem(item: HtmlTextItem, finfo: HtmlFontInfo, measurer: TextMeasurer): ItemMetric | null {
101
+ function measureItem(item: HtmlTextItem, finfo: LabelFont, measurer: TextMeasurer): ItemMetric | null {
99
102
  if (item.text === undefined) return null;
100
103
  const { fs, face } = itemFace(item, finfo);
101
104
  const flags = itemFontFlags(item);
@@ -142,11 +145,13 @@ function accumulateItem(cur: LineRun, item: HtmlTextItem, m: ItemMetric): void {
142
145
  * in the current font (consecutive BRs = a real blank line with height);
143
146
  * - at end of text a line is added ONLY if items remain — a trailing BR does
144
147
  * NOT create a phantom empty line (this over-sized e.g. html2's node c).
148
+ * The single builder: size_html_txt (sizing) and emit_htextspans (placement)
149
+ * both read these runs, as C reads one htextspan_t list.
145
150
  * @see lib/common/htmlparse.y:appendFLineList / mkText
146
151
  */
147
152
  export function buildLineRuns(
148
153
  texts: HtmlText[],
149
- finfo: HtmlFontInfo,
154
+ finfo: LabelFont,
150
155
  measurer: TextMeasurer,
151
156
  ): LineRun[] {
152
157
  const runs: LineRun[] = [];
@@ -181,7 +186,7 @@ export function buildLineRuns(
181
186
  */
182
187
  function runSimpleKey(
183
188
  run: LineRun,
184
- finfo: HtmlFontInfo,
189
+ finfo: LabelFont,
185
190
  ): { fs: number; face: string } | null | undefined {
186
191
  if (run.items.length > 1) return null;
187
192
  const item = run.items[0];
@@ -195,7 +200,7 @@ function runSimpleKey(
195
200
  * uniform resolved face/size across lines.
196
201
  * @see lib/common/htmltable.c:size_html_txt (lines 946-986)
197
202
  */
198
- export function runsAreSimple(runs: LineRun[], finfo: HtmlFontInfo): boolean {
203
+ export function runsAreSimple(runs: LineRun[], finfo: LabelFont): boolean {
199
204
  let prevFs = -1;
200
205
  let prevFace: string | null = null;
201
206
  for (const run of runs) {
@@ -20,11 +20,13 @@ export type { TextMeasurer } from './textmeasure.js';
20
20
 
21
21
  import type {
22
22
  HtmlCell, HtmlCellContent, HtmlImage, HtmlLabel, HtmlTable, HtmlText,
23
- HtmlTextItem, ImageSizer,
23
+ ImageSizer,
24
24
  } from './htmltable-types.js';
25
- import { findImageSize } from '../gvc/usershape.js';
25
+ import { findImageSize, imageIsSupplied, imageOpenWarning } from '../gvc/usershape.js';
26
+ import { registerRenderState } from './render-state.js';
26
27
  import { PointSet } from './pointset.js';
27
28
  import type { TextMeasurer } from './textmeasure.js';
29
+ import { buildLineRuns, runsAreSimple } from './htmltable-pos-runs.js';
28
30
  import { withTableFont } from './htmltable-fontinfo.js';
29
31
  import { agerrLine, type StderrLines } from './htmltable-attr-warn.js';
30
32
 
@@ -90,108 +92,6 @@ export interface HtmlSizeEnv {
90
92
 
91
93
  const defaultSizeEnv = (): HtmlSizeEnv => ({ fontsize: DEFAULT_FONTSIZE, fontname: '' });
92
94
 
93
- /** One measured text line (a C "span"). @see size_html_txt */
94
- interface SizedLine {
95
- items: HtmlTextItem[];
96
- width: number;
97
- /** Max metric line height over items (C mxysize). */
98
- mxysize: number;
99
- /** Max raw font size over items (C mxfsize). */
100
- mxfsize: number;
101
- }
102
-
103
- const itemFont = (item: HtmlTextItem, env: HtmlSizeEnv): { fs: number; face: string } => ({
104
- fs: item.fontSize !== undefined ? item.fontSize : env.fontsize,
105
- face: item.fontFace !== undefined ? item.fontFace : env.fontname,
106
- });
107
-
108
- const measureItem = (
109
- item: HtmlTextItem,
110
- measurer: TextMeasurer,
111
- env: HtmlSizeEnv,
112
- ): { w: number; h: number } => {
113
- const { fs, face } = itemFont(item, env);
114
- // Variant widths per run flags. @see lib/common/htmltable.c:size_html_txt
115
- const flags = (item.bold === true || item.italic === true)
116
- ? { bold: item.bold === true, italic: item.italic === true }
117
- : undefined;
118
- return measurer.measure(item.text ?? '', face, fs, flags);
119
- };
120
-
121
- /**
122
- * Split items into measured lines at <BR/>, mirroring C's parser exactly
123
- * (htmlparse.y appendFLineList/mkText): a BR with no accumulated items yields
124
- * a line with one empty-string item in the current font (consecutive BRs = a
125
- * real blank line with height); at end of text a line is added ONLY if items
126
- * remain — a trailing BR does NOT create a phantom empty line. Must stay in
127
- * lockstep with buildLineRuns (htmltable-pos-runs.ts), the placement twin.
128
- * @see lib/common/htmlparse.y:appendFLineList / mkText
129
- * @see lib/common/htmltable.c:size_html_txt
130
- */
131
- const splitSizedLines = (
132
- texts: HtmlText[],
133
- measurer: TextMeasurer,
134
- env: HtmlSizeEnv,
135
- ): SizedLine[] => {
136
- const lines: SizedLine[] = [];
137
- let cur: SizedLine = { items: [], width: 0, mxysize: 0, mxfsize: 0 };
138
- const accumulate = (item: HtmlTextItem): void => {
139
- const sz = measureItem(item, measurer, env);
140
- cur.items.push(item);
141
- cur.width += sz.w;
142
- cur.mxysize = Math.max(cur.mxysize, sz.h);
143
- cur.mxfsize = Math.max(cur.mxfsize, itemFont(item, env).fs);
144
- };
145
- const flush = (br?: HtmlTextItem): void => {
146
- if (cur.items.length === 0 && br !== undefined) {
147
- // C: empty fitemList on a BR → one ""-item span in the current font.
148
- const { br: _b, brAlign: _a, ...fontProps } = br;
149
- accumulate({ text: '', ...fontProps });
150
- }
151
- lines.push(cur);
152
- cur = { items: [], width: 0, mxysize: 0, mxfsize: 0 };
153
- };
154
- for (const txt of texts) {
155
- for (const item of txt.items) {
156
- if (item.br) { flush(item); continue; }
157
- if (item.text === undefined) continue;
158
- accumulate(item);
159
- }
160
- if (cur.items.length > 0) flush(); // C mkText: only when items remain
161
- }
162
- return lines;
163
- };
164
-
165
- /** Does an item carry any font flag (C textfont_t.flags)? */
166
- const itemHasFlags = (i: HtmlTextItem): boolean =>
167
- i.bold === true || i.italic === true || i.underline === true ||
168
- i.strikethrough === true || i.overline === true ||
169
- i.subscript === true || i.superscript === true;
170
-
171
- /**
172
- * C's "simple" test: one item per line, no font flags, uniform
173
- * resolved face and size across all lines.
174
- * @see lib/common/htmltable.c:size_html_txt (lines 946-986)
175
- */
176
- export const htmlTextIsSimple = (texts: HtmlText[], env: HtmlSizeEnv): boolean => {
177
- let prevFs = -1;
178
- let prevFace: string | null = null;
179
- let count = 0;
180
- for (const txt of texts) {
181
- for (const item of txt.items) {
182
- if (item.br) { count = 0; continue; }
183
- if (item.text === undefined) continue;
184
- if (++count > 1 || itemHasFlags(item)) return false;
185
- const { fs, face } = itemFont(item, env);
186
- if (prevFs < 0) prevFs = fs;
187
- else if (fs !== prevFs) return false;
188
- if (prevFace === null) prevFace = face;
189
- else if (face !== prevFace) return false;
190
- }
191
- }
192
- return true;
193
- };
194
-
195
95
  /**
196
96
  * Size a text block. C: per line lsize = mxysize when simple, else the
197
97
  * raw max font size (mxfsize); a single-line block always uses mxysize.
@@ -202,16 +102,17 @@ const sizeTextContent = (
202
102
  measurer: TextMeasurer,
203
103
  env: HtmlSizeEnv = defaultSizeEnv(),
204
104
  ): { w: number; h: number } => {
205
- const lines = splitSizedLines(texts, measurer, env);
206
- const simple = htmlTextIsSimple(texts, env);
105
+ // The one line builder, shared with placement (htmltable-pos-runs.ts).
106
+ const lines = buildLineRuns(texts, env, measurer);
107
+ const simple = runsAreSimple(lines, env);
207
108
  let w = 0;
208
109
  let h = 0;
209
110
  for (const ln of lines) {
210
111
  w = Math.max(w, ln.width);
211
- const lsize = simple ? ln.mxysize : ln.mxfsize;
112
+ const lsize = simple ? ln.height : ln.fontSize;
212
113
  h += ln.items.length > 0 ? lsize : DEFAULT_FONTSIZE;
213
114
  }
214
- if (lines.length === 1) h = lines[0]!.items.length > 0 ? lines[0]!.mxysize : DEFAULT_FONTSIZE;
115
+ if (lines.length === 1) h = lines[0]!.items.length > 0 ? lines[0]!.height : DEFAULT_FONTSIZE;
215
116
  return { w, h };
216
117
  };
217
118
 
@@ -229,25 +130,28 @@ const sizeTextContent = (
229
130
  * @see lib/common/htmltable.c:size_html_img (line 1080)
230
131
  */
231
132
  /** Images whose sizing failed: size_html_img's rv = 1 on every later call. */
232
- const missingImages = new WeakSet<HtmlImage>();
133
+ const missingImages = { set: new WeakSet<HtmlImage>() };
233
134
 
234
135
  const sizeHtmlImg = (img: HtmlImage, { imageSizer, stderr }: HtmlSizeEnv): number => {
235
136
  // C sizes each image once (processTbl); the port's size and pos passes
236
137
  // share layoutHtmlTable, so guard against re-sizing (and re-warning). The
237
138
  // returned rv still follows C on every call.
238
- if (img.width !== undefined && img.height !== undefined) return missingImages.has(img) ? 1 : 0;
139
+ if (img.width !== undefined && img.height !== undefined) return missingImages.set.has(img) ? 1 : 0;
239
140
  const sz = imageSizer !== undefined ? imageSizer(img.src) : findImageSize(img.src);
240
141
  if (sz !== null) {
241
142
  img.width = sz.w; img.height = sz.h;
242
143
  return 0;
243
144
  }
244
- // Missing-image: zero size + warning.
145
+ // Missing-image: gvusershape_open's warning (unless bytes are supplied),
146
+ // then zero size + the Error.
147
+ // @see lib/gvc/gvusershape.c:gvusershape_file_access
148
+ if (!imageIsSupplied(img.src, imageSizer)) agerrLine(stderr, `Warning: ${imageOpenWarning(img.src)}`);
245
149
  // C: agerrorf("No or improper image file=\"%s\"\n", img->src) — agerr
246
150
  // prefixes "Error: " on stderr.
247
151
  agerrLine(stderr, `Error: No or improper image file="${img.src}"`);
248
152
  img.width = 0;
249
153
  img.height = 0;
250
- missingImages.add(img);
154
+ missingImages.set.add(img);
251
155
  return 1;
252
156
  };
253
157
 
@@ -318,7 +222,14 @@ const WARN_TABLE_UNSPECIFIED = 'Warning: fixed table size with unspecified width
318
222
  * C sizes each cell/table exactly once; the port's size and pos passes both
319
223
  * run layoutHtmlTable, so the size warnings are emitted on first sight only.
320
224
  */
321
- const sizeWarned = new WeakSet<object>();
225
+ const sizeWarned = { set: new WeakSet<object>() };
226
+
227
+ // Native re-parses per run, so each render starts with both sets empty (AD-7);
228
+ // a WeakSet cannot be cleared, so the holder's field is swapped.
229
+ registerRenderState(() => {
230
+ missingImages.set = new WeakSet<HtmlImage>();
231
+ sizeWarned.set = new WeakSet<object>();
232
+ });
322
233
 
323
234
  /**
324
235
  * FIXED_FLAG branch of size_html_cell: C's rv (set at the warning sites; the
@@ -358,8 +269,8 @@ const getCellSize = (
358
269
  // @see lib/common/htmltable.c:size_html_cell (FIXED_FLAG branch + box.UR MAX)
359
270
  let szx = cw + margin;
360
271
  let szy = ch + margin;
361
- const warn = !sizeWarned.has(cell);
362
- sizeWarned.add(cell);
272
+ const warn = !sizeWarned.set.has(cell);
273
+ sizeWarned.set.add(cell);
363
274
  const fixed = fixedCellCheck(cell, { w: szx, h: szy }, warn, env.stderr);
364
275
  rv |= fixed.rv;
365
276
  if (fixed.zero) { szx = 0; szy = 0; }
@@ -505,8 +416,8 @@ export const layoutHtmlTable = (
505
416
  * @see lib/common/htmltable.c:size_html_tbl (:1683, :1689)
506
417
  */
507
418
  const warnFixedTable = (tbl: HtmlTable, dims: { wd: number; ht: number }, out: StderrLines | undefined): number => {
508
- const warn = !sizeWarned.has(tbl);
509
- sizeWarned.add(tbl);
419
+ const warn = !sizeWarned.set.has(tbl);
420
+ sizeWarned.set.add(tbl);
510
421
  if (tbl.fixedsize !== true) return 0;
511
422
  if (tbl.width && tbl.height) {
512
423
  if (tbl.width >= dims.wd && tbl.height >= dims.ht) return 0;
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * `cos`/`sin` as the native build host's libm computes them, at the only
5
+ * inputs Graphviz feeds with constant arguments: the corner angles
6
+ * 2π·(i ∓ 0.5)/8 of neatosplines.c's circumscribed ellipse 8-gon.
7
+ *
8
+ * V8's Math.sin/Math.cos are not correctly rounded everywhere, and the
9
+ * result depends on the CPU: at 9π/8 Node 22-26 on arm64 return
10
+ * -0.3826834323650896 (x64 and Node 20 return the correctly rounded value)
11
+ * where the reference libm (macOS arm64 libSystem) returns -0.38268343236508967.
12
+ * The ulp moves an obstacle corner off its node's centre line, which flips
13
+ * knife-edge triangulations in the multispline router. The table holds the
14
+ * host values for those nine angles (printed with `%a` by a clang -O2
15
+ * harness of the same expression, which also reproduces the oracle's
16
+ * makeObstacle vertices); any other input falls back to Math.cos/Math.sin.
17
+ *
18
+ * @see lib/neatogen/neatosplines.c:circumscribed_polygon_corner_about_ellipse
19
+ * @see docs/known-divergences.md (host libm)
20
+ */
21
+
22
+ /** angle → [cos, sin] from the host libm, angles 2π·(k + 0.5)/8, k = -1..7. */
23
+ const OCTAGON_TRIG: ReadonlyMap<number, readonly [number, number]> = new Map([
24
+ [-0.39269908169872414, [0.9238795325112867, -0.3826834323650898]],
25
+ [0.39269908169872414, [0.9238795325112867, 0.3826834323650898]],
26
+ [1.1780972450961724, [0.38268343236508984, 0.9238795325112867]],
27
+ [1.9634954084936207, [-0.3826834323650897, 0.9238795325112867]],
28
+ [2.748893571891069, [-0.9238795325112867, 0.3826834323650899]],
29
+ [3.5342917352885173, [-0.9238795325112868, -0.38268343236508967]],
30
+ [4.319689898685965, [-0.38268343236509034, -0.9238795325112865]],
31
+ [5.105088062083414, [0.38268343236509, -0.9238795325112866]],
32
+ [5.890486225480862, [0.9238795325112865, -0.3826834323650904]],
33
+ ] as const);
34
+
35
+ /** libm `cos(x)` of the native build host (table above, else Math.cos). */
36
+ export function libmCos(x: number): number {
37
+ return OCTAGON_TRIG.get(x)?.[0] ?? Math.cos(x);
38
+ }
39
+
40
+ /** libm `sin(x)` of the native build host (table above, else Math.sin). */
41
+ export function libmSin(x: number): number {
42
+ return OCTAGON_TRIG.get(x)?.[1] ?? Math.sin(x);
43
+ }
@@ -13,6 +13,10 @@
13
13
  * @see lib/common/emit.c:470 parseSegs
14
14
  */
15
15
 
16
+ import { agerr, agerrorf, agwarningf } from './agerr.js';
17
+ import type { Edge } from '../model/edge.js';
18
+ import { registerRenderState } from './render-state.js';
19
+
16
20
  // ---------------------------------------------------------------------------
17
21
  // Types
18
22
  // ---------------------------------------------------------------------------
@@ -38,6 +42,28 @@ export interface ColorSeg {
38
42
  // Constants
39
43
  // ---------------------------------------------------------------------------
40
44
 
45
+ /**
46
+ * C's `static atomic_flag warned` in parseSegs: set by the first Total-size
47
+ * warning or Illegal-value error in the process, silencing both afterwards.
48
+ * Reset per render (AD-4).
49
+ * @see lib/common/emit.c:470 parseSegs
50
+ */
51
+ const parseSegsState = { warned: false };
52
+
53
+ registerRenderState(() => {
54
+ parseSegsState.warned = false;
55
+ });
56
+
57
+ /** `atomic_flag_test_and_set(&warned)` negated: true when this call claimed it. */
58
+ function claimWarned(): boolean {
59
+ if (parseSegsState.warned) return false;
60
+ parseSegsState.warned = true;
61
+ return true;
62
+ }
63
+
64
+ /** A claim that never fires: parses without touching or consuming the flag. */
65
+ const claimNever = (): boolean => false;
66
+
41
67
  /** @see lib/common/emit.c:451 EPS */
42
68
  const EPS = 1e-5;
43
69
 
@@ -76,7 +102,7 @@ function appendSeg(
76
102
  token: string,
77
103
  left: number,
78
104
  segs: ColorSeg[],
79
- warnedExceeded: { v: boolean },
105
+ claim: () => boolean,
80
106
  ): { left: number; rval: 0 | 3 } | null {
81
107
  const { color, v } = getSegLen(token);
82
108
  if (v < 0) return null;
@@ -85,10 +111,7 @@ function appendSeg(
85
111
  let rval: 0 | 3 = 0;
86
112
  const del = t - left;
87
113
  if (del > 0) {
88
- if (!aeq0(del) && !warnedExceeded.v) {
89
- warnedExceeded.v = true;
90
- rval = 3;
91
- }
114
+ if (!aeq0(del) && claim()) rval = 3;
92
115
  t = left;
93
116
  }
94
117
  left -= t;
@@ -128,34 +151,20 @@ function trimTrailing(segs: ColorSeg[]): void {
128
151
  // ---------------------------------------------------------------------------
129
152
 
130
153
  /**
131
- * Parse `"c1;f1:c2;f2:…"` into segments.
132
- *
133
- * Fractions are optional and non-negative; their sum must not exceed 1.
134
- * Segments without an explicit fraction share the remaining weight equally;
135
- * if none exist the remainder is added to the last segment.
136
- *
137
- * Return codes (mirror C):
138
- * 0 → ok; 1 → silent error; 2 → error with message; 3 → warning (sum > 1).
139
- * On rv 1 or 2 the returned segs array is empty.
140
- *
154
+ * The parse of `parseSegs` with the once-per-process flag abstracted as `claim`.
141
155
  * @see lib/common/emit.c:470 parseSegs
142
156
  */
143
- export function parseSegs(
157
+ function scanSegs(
144
158
  colorlist: string,
159
+ claim: () => boolean,
145
160
  ): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
146
161
  const segs: ColorSeg[] = [];
147
162
  let left = 1;
148
163
  let rval: 0 | 3 = 0;
149
- let badFloatCount = 0;
150
- const warnedExceeded = { v: false };
151
164
 
152
165
  for (const token of colorlist.split(':')) {
153
- const result = appendSeg(token, left, segs, warnedExceeded);
154
- if (result === null) {
155
- const errCode: 1 | 2 = badFloatCount === 0 ? 2 : 1;
156
- badFloatCount++;
157
- return { segs: [], error: errCode };
158
- }
166
+ const result = appendSeg(token, left, segs, claim);
167
+ if (result === null) return { segs: [], error: claim() ? 2 : 1 };
159
168
  if (result.rval === 3) rval = 3;
160
169
  left = result.left;
161
170
  if (aeq0(left)) { left = 0; break; }
@@ -165,3 +174,58 @@ export function parseSegs(
165
174
  trimTrailing(segs);
166
175
  return { segs, error: rval };
167
176
  }
177
+
178
+ /**
179
+ * Parse `"c1;f1:c2;f2:…"` into segments.
180
+ *
181
+ * Fractions are optional and non-negative; their sum must not exceed 1.
182
+ * Segments without an explicit fraction share the remaining weight equally;
183
+ * if none exist the remainder is added to the last segment.
184
+ *
185
+ * Return codes (mirror C):
186
+ * 0 → ok; 1 → silent error; 2 → error with message; 3 → warning (sum > 1).
187
+ * On rv 1 or 2 the returned segs array is empty.
188
+ *
189
+ * The rv 2 error and rv 3 warning are printed here, once per process (the
190
+ * shared `warned` flag). C's callers then append an AGPREV line for rv > 1;
191
+ * `context` supplies it (e.g. `in edge a -> b`): the warning continues the
192
+ * same line as C's AGPREV does, the error's context follows on its own line.
193
+ *
194
+ * @see lib/common/emit.c:470 parseSegs
195
+ */
196
+ export function parseSegs(
197
+ colorlist: string,
198
+ context?: () => string,
199
+ ): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
200
+ const result = scanSegs(colorlist, claimWarned);
201
+ if (result.error === 3) {
202
+ const tail = context === undefined ? '' : context();
203
+ agwarningf(`Total size > 1 in "${colorlist}" color spec ${tail}`);
204
+ } else if (result.error === 2) {
205
+ agerrorf(`Illegal value in "${colorlist}" color attribute; float expected after ';'`);
206
+ if (context !== undefined) agerr('prev', context());
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /**
212
+ * {@link parseSegs} for port-internal re-parses that have no C counterpart
213
+ * call (pre-passes, parallel edges): same segments and error class, but never
214
+ * prints and never consumes the once-per-process flag.
215
+ */
216
+ export function parseSegsSilent(
217
+ colorlist: string,
218
+ ): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
219
+ return scanSegs(colorlist, claimNever);
220
+ }
221
+
222
+ /**
223
+ * The AGPREV line C's `multicolor` appends for an rv > 1 parseSegs.
224
+ * @see lib/common/emit.c:1985 multicolor
225
+ */
226
+ export function edgeSegsContext(e: Edge): () => string {
227
+ return () => {
228
+ const directed = !e.tail.root.kind.endsWith('undirected');
229
+ return `in edge ${e.tail.name}${directed ? ' -> ' : ' -- '}${e.head.name}`;
230
+ };
231
+ }