@knowvah/dot-engine 1.8.1 → 2.0.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 (181) hide show
  1. package/README.md +232 -30
  2. package/dist/api/builder.d.ts +3 -0
  3. package/dist/api/builder.d.ts.map +1 -1
  4. package/dist/api/edge-ops.d.ts +7 -0
  5. package/dist/api/edge-ops.d.ts.map +1 -1
  6. package/dist/api/geometry.d.ts +5 -2
  7. package/dist/api/geometry.d.ts.map +1 -1
  8. package/dist/api.js +159 -31
  9. package/dist/api.js.map +3 -3
  10. package/dist/async/collect.d.ts +49 -0
  11. package/dist/async/collect.d.ts.map +1 -0
  12. package/dist/async/fonts.d.ts +20 -0
  13. package/dist/async/fonts.d.ts.map +1 -0
  14. package/dist/async/render-async.d.ts +91 -0
  15. package/dist/async/render-async.d.ts.map +1 -0
  16. package/dist/async/render-into.d.ts +38 -0
  17. package/dist/async/render-into.d.ts.map +1 -0
  18. package/dist/async/sanitize.d.ts +28 -0
  19. package/dist/async/sanitize.d.ts.map +1 -0
  20. package/dist/common/css-font.d.ts +9 -0
  21. package/dist/common/css-font.d.ts.map +1 -0
  22. package/dist/common/htmltable-types.d.ts +3 -3
  23. package/dist/common/htmltable-types.d.ts.map +1 -1
  24. package/dist/common/make-label.d.ts.map +1 -1
  25. package/dist/common/poly-shapes.d.ts.map +1 -1
  26. package/dist/common/textmeasure-factory.d.ts +2 -0
  27. package/dist/common/textmeasure-factory.d.ts.map +1 -1
  28. package/dist/common/textmeasure.d.ts +11 -1
  29. package/dist/common/textmeasure.d.ts.map +1 -1
  30. package/dist/common/utils-inputscale.d.ts +19 -0
  31. package/dist/common/utils-inputscale.d.ts.map +1 -0
  32. package/dist/errors.d.ts +61 -5
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/gvc/context.d.ts +36 -4
  35. package/dist/gvc/context.d.ts.map +1 -1
  36. package/dist/gvc/device.d.ts +5 -2
  37. package/dist/gvc/device.d.ts.map +1 -1
  38. package/dist/gvc/image-resolver.d.ts +16 -16
  39. package/dist/gvc/image-resolver.d.ts.map +1 -1
  40. package/dist/gvc/job.d.ts +1 -9
  41. package/dist/gvc/job.d.ts.map +1 -1
  42. package/dist/gvc/usershape.d.ts +2 -12
  43. package/dist/gvc/usershape.d.ts.map +1 -1
  44. package/dist/index.d.ts +27 -8
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +7745 -5550
  47. package/dist/index.js.map +4 -4
  48. package/dist/label/index.d.ts.map +1 -1
  49. package/dist/label/node.d.ts +0 -6
  50. package/dist/label/node.d.ts.map +1 -1
  51. package/dist/label/rectangle.d.ts +1 -7
  52. package/dist/label/rectangle.d.ts.map +1 -1
  53. package/dist/layout/circo/circular.d.ts +8 -5
  54. package/dist/layout/circo/circular.d.ts.map +1 -1
  55. package/dist/layout/dot/pack-components.d.ts +0 -19
  56. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  57. package/dist/layout/dot/position.d.ts +7 -2
  58. package/dist/layout/dot/position.d.ts.map +1 -1
  59. package/dist/layout/fdp/derive.d.ts.map +1 -1
  60. package/dist/layout/fdp/index.d.ts.map +1 -1
  61. package/dist/layout/fdp/init.d.ts.map +1 -1
  62. package/dist/layout/fdp/layout.d.ts.map +1 -1
  63. package/dist/layout/fdp/ports.d.ts +0 -10
  64. package/dist/layout/fdp/ports.d.ts.map +1 -1
  65. package/dist/layout/fdp/xlayout.d.ts +0 -16
  66. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  67. package/dist/layout/neato/adjust-info.d.ts +117 -0
  68. package/dist/layout/neato/adjust-info.d.ts.map +1 -0
  69. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  70. package/dist/layout/neato/constraint-adjust.d.ts +40 -0
  71. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
  72. package/dist/layout/neato/edge-len.d.ts +20 -0
  73. package/dist/layout/neato/edge-len.d.ts.map +1 -0
  74. package/dist/layout/neato/fdp-adjust.d.ts +32 -4
  75. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  76. package/dist/layout/neato/index.d.ts +9 -6
  77. package/dist/layout/neato/index.d.ts.map +1 -1
  78. package/dist/layout/neato/init.d.ts +4 -11
  79. package/dist/layout/neato/init.d.ts.map +1 -1
  80. package/dist/layout/neato/kk-paths.d.ts +50 -0
  81. package/dist/layout/neato/kk-paths.d.ts.map +1 -0
  82. package/dist/layout/neato/kk-solve.d.ts +14 -0
  83. package/dist/layout/neato/kk-solve.d.ts.map +1 -0
  84. package/dist/layout/neato/kk.d.ts +44 -0
  85. package/dist/layout/neato/kk.d.ts.map +1 -0
  86. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  87. package/dist/layout/neato/poly.d.ts +50 -0
  88. package/dist/layout/neato/poly.d.ts.map +1 -0
  89. package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
  90. package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
  91. package/dist/layout/neato/sgd.d.ts +14 -9
  92. package/dist/layout/neato/sgd.d.ts.map +1 -1
  93. package/dist/layout/neato/start.d.ts +51 -0
  94. package/dist/layout/neato/start.d.ts.map +1 -0
  95. package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
  96. package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
  97. package/dist/layout/sfdp/index.d.ts.map +1 -1
  98. package/dist/layout/sfdp/init.d.ts +0 -9
  99. package/dist/layout/sfdp/init.d.ts.map +1 -1
  100. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  101. package/dist/layout/twopi/circle.d.ts +0 -7
  102. package/dist/layout/twopi/circle.d.ts.map +1 -1
  103. package/dist/ortho/ortho-parallel.d.ts.map +1 -1
  104. package/dist/ortho/trap-query.d.ts.map +1 -1
  105. package/dist/parser/index.d.ts +9 -5
  106. package/dist/parser/index.d.ts.map +1 -1
  107. package/dist/render/index.d.ts +2 -0
  108. package/dist/render/index.d.ts.map +1 -1
  109. package/dist/render/public.d.ts +9 -3
  110. package/dist/render/public.d.ts.map +1 -1
  111. package/dist/render/xdot-public.d.ts +7 -2
  112. package/dist/render/xdot-public.d.ts.map +1 -1
  113. package/dist/render.js +7548 -5588
  114. package/dist/render.js.map +4 -4
  115. package/dist/util/xml.d.ts.map +1 -1
  116. package/dist/vpsc/Solver.d.ts +1 -0
  117. package/dist/vpsc/Solver.d.ts.map +1 -1
  118. package/package.json +1 -1
  119. package/src/api/builder.ts +73 -5
  120. package/src/api/edge-ops.ts +19 -0
  121. package/src/api/geometry.ts +22 -5
  122. package/src/async/collect.ts +204 -0
  123. package/src/async/fonts.ts +61 -0
  124. package/src/async/render-async.ts +205 -0
  125. package/src/async/render-into.ts +115 -0
  126. package/src/async/sanitize.ts +150 -0
  127. package/src/common/css-font.ts +109 -0
  128. package/src/common/htmltable-types.ts +4 -5
  129. package/src/common/make-label.ts +10 -1
  130. package/src/common/poly-shapes.ts +4 -1
  131. package/src/common/textmeasure-factory.ts +11 -0
  132. package/src/common/textmeasure.ts +41 -8
  133. package/src/common/utils-inputscale.ts +31 -0
  134. package/src/errors.ts +188 -5
  135. package/src/gvc/context.ts +96 -17
  136. package/src/gvc/device.ts +11 -6
  137. package/src/gvc/image-resolver.ts +25 -2
  138. package/src/gvc/job.ts +3 -1
  139. package/src/gvc/usershape.ts +6 -0
  140. package/src/index.ts +59 -43
  141. package/src/label/index.ts +6 -2
  142. package/src/label/node.ts +2 -1
  143. package/src/label/rectangle.ts +4 -2
  144. package/src/layout/circo/circular.ts +9 -6
  145. package/src/layout/dot/pack-components.ts +6 -2
  146. package/src/layout/dot/position.ts +21 -3
  147. package/src/layout/fdp/derive.ts +7 -6
  148. package/src/layout/fdp/index.ts +45 -5
  149. package/src/layout/fdp/init.ts +7 -6
  150. package/src/layout/fdp/layout.ts +2 -1
  151. package/src/layout/fdp/ports.ts +3 -2
  152. package/src/layout/fdp/xlayout.ts +3 -1
  153. package/src/layout/neato/adjust-info.ts +332 -0
  154. package/src/layout/neato/cdt-surface.ts +50 -30
  155. package/src/layout/neato/constraint-adjust.ts +466 -0
  156. package/src/layout/neato/edge-len.ts +36 -0
  157. package/src/layout/neato/fdp-adjust.ts +117 -12
  158. package/src/layout/neato/index.ts +36 -54
  159. package/src/layout/neato/init.ts +21 -36
  160. package/src/layout/neato/kk-paths.ts +146 -0
  161. package/src/layout/neato/kk-solve.ts +64 -0
  162. package/src/layout/neato/kk.ts +322 -0
  163. package/src/layout/neato/multispline-router.ts +4 -3
  164. package/src/layout/neato/poly.ts +493 -0
  165. package/src/layout/neato/sgd-dijkstra.ts +125 -0
  166. package/src/layout/neato/sgd.ts +63 -40
  167. package/src/layout/neato/start.ts +222 -0
  168. package/src/layout/neato/vpsc-adjust.ts +93 -0
  169. package/src/layout/sfdp/index.ts +4 -5
  170. package/src/layout/sfdp/init.ts +40 -4
  171. package/src/layout/sfdp/spring-driver.ts +30 -1
  172. package/src/layout/twopi/circle.ts +2 -1
  173. package/src/ortho/ortho-parallel.ts +2 -1
  174. package/src/ortho/trap-query.ts +2 -1
  175. package/src/parser/index.ts +11 -11
  176. package/src/render/index.ts +5 -0
  177. package/src/render/public.ts +24 -24
  178. package/src/render/svg.ts +1 -1
  179. package/src/render/xdot-public.ts +19 -18
  180. package/src/util/xml.ts +24 -30
  181. package/src/vpsc/Solver.ts +5 -3
@@ -1 +1 @@
1
- {"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../../src/util/xml.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,sFAAsF;AACtF,MAAM,WAAW,QAAQ;IACvB,uDAAuD;IACvD,GAAG,EAAE,OAAO,CAAC;IACb,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,gDAAgD;IAChD,IAAI,EAAE,OAAO,CAAC;IACd,oCAAoC;IACpC,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,CAE9D"}
1
+ {"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../../src/util/xml.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAIH,sFAAsF;AACtF,MAAM,WAAW,QAAQ;IACvB,uDAAuD;IACvD,GAAG,EAAE,OAAO,CAAC;IACb,2BAA2B;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,gDAAgD;IAChD,IAAI,EAAE,OAAO,CAAC;IACd,oCAAoC;IACpC,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,CAE9D"}
@@ -57,6 +57,7 @@ export declare class VPSC {
57
57
  /**
58
58
  * Throw if any constraint has slack < -1e-7.
59
59
  * Shared between satisfy() and refine().
60
+ * @see lib/vpsc/solve_VPSC.cpp:VPSC::satisfy (uncaught std::runtime_error)
60
61
  */
61
62
  private verifyConstraints;
62
63
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Solver.d.ts","sourceRoot":"","sources":["../../src/vpsc/Solver.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOrC;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;gBAET,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;IAOtD,OAAO,IAAI,MAAM;IACjB,OAAO,IAAI,MAAM;IACjB,OAAO,IAAI,MAAM;IACjB,OAAO,IAAI,MAAM;IACjB,UAAU,IAAI,MAAM;IACpB,UAAU,IAAI,MAAM;IACpB,KAAK,IAAI,MAAM;IACf,MAAM,IAAI,MAAM;IAEhB,sFAAsF;IACtF,QAAQ,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM;IAQ9B,sFAAsF;IACtF,QAAQ,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM;CAO/B;AAQD;;;;;GAKG;AACH,qBAAa,IAAI;IACf,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC;IAE3B,oDAAoD;gBACxC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE;IAK5C;;;;OAIG;IACH,OAAO,IAAI,IAAI;IASf,mDAAmD;IACnD,OAAO,CAAC,MAAM;IAoBd;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAMzB;;;OAGG;IACH,KAAK,IAAI,IAAI;CAId;AAQD;;;;;GAKG;AACH,qBAAa,OAAQ,SAAQ,IAAI;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,QAAQ,CAAe;IAE/B,0DAA0D;gBAC9C,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE;IAO5C;;;;OAIG;IACM,OAAO,IAAI,IAAI;IAwBxB;;;OAGG;IACH,UAAU,IAAI,IAAI;IAOlB;;;OAGG;IACH,WAAW,IAAI,IAAI;IAqBnB;;;OAGG;IACM,KAAK,IAAI,IAAI;IAWtB;;;OAGG;IACH,OAAO,CAAC,YAAY;CAkBrB;AAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC"}
1
+ {"version":3,"file":"Solver.d.ts","sourceRoot":"","sources":["../../src/vpsc/Solver.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQrC;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;gBAET,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;IAOtD,OAAO,IAAI,MAAM;IACjB,OAAO,IAAI,MAAM;IACjB,OAAO,IAAI,MAAM;IACjB,OAAO,IAAI,MAAM;IACjB,UAAU,IAAI,MAAM;IACpB,UAAU,IAAI,MAAM;IACpB,KAAK,IAAI,MAAM;IACf,MAAM,IAAI,MAAM;IAEhB,sFAAsF;IACtF,QAAQ,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM;IAQ9B,sFAAsF;IACtF,QAAQ,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM;CAO/B;AAQD;;;;;GAKG;AACH,qBAAa,IAAI;IACf,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,CAAC;IAE3B,oDAAoD;gBACxC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE;IAK5C;;;;OAIG;IACH,OAAO,IAAI,IAAI;IASf,mDAAmD;IACnD,OAAO,CAAC,MAAM;IAoBd;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAMzB;;;OAGG;IACH,KAAK,IAAI,IAAI;CAId;AAQD;;;;;GAKG;AACH,qBAAa,OAAQ,SAAQ,IAAI;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,QAAQ,CAAe;IAE/B,0DAA0D;gBAC9C,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE;IAO5C;;;;OAIG;IACM,OAAO,IAAI,IAAI;IAwBxB;;;OAGG;IACH,UAAU,IAAI,IAAI;IAOlB;;;OAGG;IACH,WAAW,IAAI,IAAI;IAqBnB;;;OAGG;IACM,KAAK,IAAI,IAAI;IAWtB;;;OAGG;IACH,OAAO,CAAC,YAAY;CAkBrB;AAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "1.8.1",
3
+ "version": "2.0.0",
4
4
  "description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
5
5
  "keywords": [
6
6
  "graphviz",
@@ -16,8 +16,8 @@ import { Graph, type GraphKind } from '../model/graph.js';
16
16
  import type { Node } from '../model/node.js';
17
17
  import type { Edge } from '../model/edge.js';
18
18
  import { agnode, agsubg, agsubnode } from '../model/cgraph-ops.js';
19
- import { addEdge as cgraphAddEdge } from './edge-ops.js';
20
- import { RenderError } from '../errors.js';
19
+ import { addEdge as cgraphAddEdge, requireObject } from './edge-ops.js';
20
+ import { InternalError, invalidArgType } from '../errors.js';
21
21
  import { HTML_STRING_MARK } from '../common/html-string.js';
22
22
 
23
23
  // ── Public interfaces ──────────────────────────────────────────────────────────
@@ -101,14 +101,17 @@ class NodeHandle implements GvNode {
101
101
  }
102
102
 
103
103
  setAttr(k: string, v: string): void {
104
+ requireKeyValue(k, v);
104
105
  this._node.attrs.set(k, v);
105
106
  }
106
107
 
107
108
  setHtmlAttr(k: string, v: string): void {
109
+ requireKeyValue(k, v);
108
110
  this._node.attrs.set(k, HTML_STRING_MARK + v);
109
111
  }
110
112
 
111
113
  getAttr(k: string): string | undefined {
114
+ requireString('k', k);
112
115
  return this._node.attrs.get(k);
113
116
  }
114
117
  }
@@ -130,18 +133,67 @@ class EdgeHandle implements GvEdge {
130
133
  }
131
134
 
132
135
  setAttr(k: string, v: string): void {
136
+ requireKeyValue(k, v);
133
137
  this._edge.attrs.set(k, v);
134
138
  }
135
139
 
136
140
  setHtmlAttr(k: string, v: string): void {
141
+ requireKeyValue(k, v);
137
142
  this._edge.attrs.set(k, HTML_STRING_MARK + v);
138
143
  }
139
144
 
140
145
  getAttr(k: string): string | undefined {
146
+ requireString('k', k);
141
147
  return this._edge.attrs.get(k);
142
148
  }
143
149
  }
144
150
 
151
+ // ── Argument checks (ADR-3: typeof / null only) ───────────────────────────────
152
+
153
+ /** `name`, `k`, `v`: must be a string. */
154
+ function requireString(param: string, v: unknown): void {
155
+ if (typeof v !== 'string') throw invalidArgType(param, 'string', v);
156
+ }
157
+
158
+ /** `attrs`: `undefined` or a plain object whose values are strings. */
159
+ function requireAttrs(param: string, attrs: unknown): void {
160
+ if (attrs === undefined) return;
161
+ requireObject(param, attrs);
162
+ if (Array.isArray(attrs)) throw invalidArgType(param, 'plain object', attrs);
163
+ for (const [k, v] of Object.entries(attrs as Record<string, unknown>)) {
164
+ if (typeof v !== 'string') {
165
+ throw invalidArgType(`${param}.${k}`, 'string', v);
166
+ }
167
+ }
168
+ }
169
+
170
+ /** Node ref: a string or a handle created by this library. */
171
+ function requireNodeRef(param: string, ref: unknown): void {
172
+ if (typeof ref === 'string' || ref instanceof NodeHandle) return;
173
+ throw invalidArgType(param, 'string or GvNode', ref);
174
+ }
175
+
176
+ /** Key/value pair of setAttr/setHtmlAttr. */
177
+ function requireKeyValue(k: unknown, v: unknown): void {
178
+ requireString('k', k);
179
+ requireString('v', v);
180
+ }
181
+
182
+ /** `createGraph` options: `undefined` or an object with typed fields. */
183
+ function requireGraphOptions(opts: unknown): void {
184
+ if (opts === undefined) return;
185
+ requireObject('opts', opts);
186
+ const o = opts as Record<string, unknown>;
187
+ for (const key of ['directed', 'strict'] as const) {
188
+ if (o[key] !== undefined && typeof o[key] !== 'boolean') {
189
+ throw invalidArgType(`opts.${key}`, 'boolean or undefined', o[key]);
190
+ }
191
+ }
192
+ if (o['name'] !== undefined && typeof o['name'] !== 'string') {
193
+ throw invalidArgType('opts.name', 'string or undefined', o['name']);
194
+ }
195
+ }
196
+
145
197
  // ── Builder implementation ─────────────────────────────────────────────────────
146
198
 
147
199
  /** Resolve a GvNode handle or a name string to an internal Node. */
@@ -149,7 +201,9 @@ function resolveNode(g: Graph, ref: GvNode | string): Node {
149
201
  if (typeof ref === 'string') {
150
202
  const node = agnode(g, ref, true);
151
203
  if (node === null) {
152
- throw new RenderError(`Failed to resolve node '${ref}' in graph '${g.name}'`, 'GENERIC_ERROR');
204
+ throw new InternalError(
205
+ `Failed to resolve node '${ref}' in graph '${g.name}'`,
206
+ );
153
207
  }
154
208
  return node;
155
209
  }
@@ -194,6 +248,9 @@ class GraphBuilder implements GvGraphBuilder {
194
248
  head: GvNode | string,
195
249
  attrs?: Record<string, string>,
196
250
  ): GvEdge {
251
+ requireNodeRef('tail', tail);
252
+ requireNodeRef('head', head);
253
+ requireAttrs('attrs', attrs);
197
254
  const tailNode = resolveNode(this._graph, tail);
198
255
  const headNode = resolveNode(this._graph, head);
199
256
  const edge = cgraphAddEdge(this._graph, tailNode, headNode);
@@ -202,23 +259,28 @@ class GraphBuilder implements GvGraphBuilder {
202
259
  }
203
260
 
204
261
  addSubgraph(name: string, attrs?: Record<string, string>): GvGraphBuilder {
262
+ requireString('name', name);
263
+ requireAttrs('attrs', attrs);
205
264
  const sg = agsubg(this._context, name, true);
206
265
  if (sg === null) {
207
- throw new RenderError(`Failed to create subgraph '${name}'`, 'GENERIC_ERROR');
266
+ throw new InternalError(`Failed to create subgraph '${name}'`);
208
267
  }
209
268
  applyAttrs(sg.attrs, attrs);
210
269
  return new GraphBuilder(this._graph, sg);
211
270
  }
212
271
 
213
272
  setAttr(k: string, v: string): void {
273
+ requireKeyValue(k, v);
214
274
  this._context.attrs.set(k, v);
215
275
  }
216
276
 
217
277
  setHtmlAttr(k: string, v: string): void {
278
+ requireKeyValue(k, v);
218
279
  this._context.attrs.set(k, HTML_STRING_MARK + v);
219
280
  }
220
281
 
221
282
  getAttr(k: string): string | undefined {
283
+ requireString('k', k);
222
284
  return this._context.attrs.get(k);
223
285
  }
224
286
  }
@@ -233,8 +295,10 @@ function addNodeToContext(
233
295
  name: string,
234
296
  attrs: Record<string, string> | undefined,
235
297
  ): GvNode {
298
+ requireString('name', name);
299
+ requireAttrs('attrs', attrs);
236
300
  const node = agnode(root, name, true);
237
- if (node === null) throw new RenderError(`Failed to create node '${name}'`, 'GENERIC_ERROR');
301
+ if (node === null) throw new InternalError(`Failed to create node '${name}'`);
238
302
  applyAttrs(node.attrs, attrs);
239
303
  if (context !== root) agsubnode(context, node, true);
240
304
  return new NodeHandle(node);
@@ -269,9 +333,13 @@ function resolveKind(opts: CreateGraphOptions | undefined): GraphKind {
269
333
  * The builder's `.graph` is a fresh Graph ready for handoff to layout/render.
270
334
  * Defaults: directed=true, strict=false, name=''.
271
335
  *
336
+ * @throws TypeError `ERR_INVALID_ARG_TYPE` if `opts` or one of its fields has
337
+ * the wrong type. Builder and handle methods throw the same for wrong
338
+ * argument types, and `InternalError` if node/subgraph creation fails.
272
339
  * @see lib/cgraph/graph.c:agopen
273
340
  */
274
341
  export function createGraph(opts?: CreateGraphOptions): GvGraphBuilder {
342
+ requireGraphOptions(opts);
275
343
  const g = new Graph(opts?.name ?? '', resolveKind(opts));
276
344
  return new GraphBuilder(g, g);
277
345
  }
@@ -11,6 +11,17 @@
11
11
  import type { Graph } from '../model/graph.js';
12
12
  import type { Node } from '../model/node.js';
13
13
  import { Edge } from '../model/edge.js';
14
+ import { invalidArgType } from '../errors.js';
15
+
16
+ /**
17
+ * Argument check shared by the /api entry points: a non-null object.
18
+ * @internal
19
+ */
20
+ export function requireObject(param: string, v: unknown): void {
21
+ if (typeof v !== 'object' || v === null) {
22
+ throw invalidArgType(param, 'object', v);
23
+ }
24
+ }
14
25
 
15
26
  /** @see lib/cgraph/cgraph.h:agisundirected */
16
27
  function isUndirected(g: Graph): boolean {
@@ -91,6 +102,8 @@ function insertEdge(g: Graph, root: Graph, edge: Edge): void {
91
102
  * // edge.tail === a, edge.head === b
92
103
  * ```
93
104
  *
105
+ * @throws TypeError `ERR_INVALID_ARG_TYPE` if `g`, `tail` or `head` is not an
106
+ * object, or `name` is neither undefined nor a string
94
107
  * @see lib/cgraph/edge.c:agedge
95
108
  */
96
109
  export function addEdge(
@@ -99,6 +112,12 @@ export function addEdge(
99
112
  head: Node,
100
113
  name?: string,
101
114
  ): Edge {
115
+ requireObject('g', g);
116
+ requireObject('tail', tail);
117
+ requireObject('head', head);
118
+ if (name !== undefined && typeof name !== 'string') {
119
+ throw invalidArgType('name', 'string or undefined', name);
120
+ }
102
121
  const root = g.root;
103
122
  const undirected = isUndirected(g);
104
123
 
@@ -34,7 +34,8 @@ import type { Graph } from '../model/graph.js';
34
34
  import type { Node } from '../model/node.js';
35
35
  import type { Edge } from '../model/edge.js';
36
36
  import type { TextlabelT } from '../common/types.js';
37
- import { RenderError } from '../errors.js';
37
+ import { invalidArgValue, invalidState } from '../errors.js';
38
+ import { requireObject } from './edge-ops.js';
38
39
 
39
40
  // ---------------------------------------------------------------------------
40
41
  // Public coordinate types (canonical home — T5 imports GeometryOptions here)
@@ -399,6 +400,18 @@ function collectClusters(
399
400
  // Public API
400
401
  // ---------------------------------------------------------------------------
401
402
 
403
+ const Y_AXES: readonly YAxis[] = ['up', 'down'];
404
+
405
+ /** Argument check for getLayout's options (cheap typeof checks only). */
406
+ function checkGeometryOptions(opts: GeometryOptions | undefined): void {
407
+ if (opts === undefined) return;
408
+ requireObject('opts', opts);
409
+ const y: unknown = opts.yAxis;
410
+ if (y !== undefined && y !== 'up' && y !== 'down') {
411
+ throw invalidArgValue('opts.yAxis', y, Y_AXES);
412
+ }
413
+ }
414
+
402
415
  /**
403
416
  * Returns a plain, JSON-serializable snapshot of the computed geometry for
404
417
  * all nodes and edges in graph `g`.
@@ -406,20 +419,24 @@ function collectClusters(
406
419
  * Must be called **after** `ctx.layout(g, engine)` (or `render`) has run.
407
420
  * Before layout the geometry fields hold calloc-zero defaults (every node at
408
421
  * the origin, an empty bounding box), so a not-yet-laid-out graph is rejected
409
- * with a `RenderError` rather than returning that all-zero snapshot as if it
422
+ * with an `ERR_INVALID_STATE` error rather than returning that all-zero snapshot as if it
410
423
  * were real geometry.
411
424
  *
412
425
  * @param g - Laid-out graph (internal model; not mutated by this function).
413
426
  * @param opts - Coordinate options; defaults to `{ yAxis: 'down' }`.
414
- * @throws RenderError if `g` has not been laid out.
427
+ * @throws TypeError (`ERR_INVALID_ARG_TYPE`) if `g` is not an object or `opts`
428
+ * is not an object; (`ERR_INVALID_ARG_VALUE`) if `opts.yAxis` is not
429
+ * `'up'` or `'down'`.
430
+ * @throws Error (`ERR_INVALID_STATE`) if `g` has not been laid out.
415
431
  *
416
432
  * @see lib/common/types.h:GD_bb, ND_coord, ED_spl
417
433
  */
418
434
  export function getLayout(g: Graph, opts?: GeometryOptions): LayoutSnapshot {
435
+ requireObject('g', g);
436
+ checkGeometryOptions(opts);
419
437
  if (g.info?.laidOut !== true) {
420
- throw new RenderError(
438
+ throw invalidState(
421
439
  'getLayout requires a laid-out graph; run ctx.layout(g, engine) or render() first',
422
- 'GENERIC_ERROR',
423
440
  );
424
441
  }
425
442
  const yAxis: YAxis = opts?.yAxis ?? 'down';
@@ -0,0 +1,204 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+
3
+ /**
4
+ * Resource collector: the fonts and image sources the synchronous pipeline
5
+ * will ask for, gathered from the parsed graph before any layout runs.
6
+ *
7
+ * Each font request mirrors a measurer call site exactly (same attribute
8
+ * lookup, same defaults and fallbacks), so a prefetch loads the faces the
9
+ * measurer will actually measure.
10
+ *
11
+ * @see src/common/nodeinit.ts:initNodeXLabel / src/common/poly-init.ts:buildNodeLabel
12
+ * @see src/common/edge-label-init.ts (edge, xlabel, head/tail labels)
13
+ * @see src/layout/dot/graph-label.ts:doGraphLabel (root and cluster labels)
14
+ * @see src/common/htmltable.ts / htmltable-pos-runs.ts (HTML item fonts)
15
+ */
16
+
17
+ import type { Graph } from '../model/graph.js';
18
+ import type { Edge } from '../model/edge.js';
19
+ import type { Node } from '../model/node.js';
20
+ import type { TextVariantFlags } from '../common/textmeasure.js';
21
+ import { canvasFont } from '../common/css-font.js';
22
+ import { DEFAULT_FONTNAME, DEFAULT_FONTSIZE } from '../common/make-label.js';
23
+ import { isHtmlValue, htmlValueContent } from '../common/html-string.js';
24
+ import { nodeAttr, readFontAttrs } from '../common/poly-init.js';
25
+ import { initFontEdgeAttr, initFontLabelEdgeAttr } from '../common/edge-label-init.js';
26
+ import { parseHtmlLabel } from '../common/htmltable-parse.js';
27
+ import { isACluster } from '../layout/dot/rank.js';
28
+ import type {
29
+ HtmlCellContent, HtmlLabel, HtmlTable, HtmlTextItem,
30
+ } from '../common/htmltable-types.js';
31
+
32
+ /** One font face the measurer will be asked for. */
33
+ export interface FontRequest {
34
+ /** Graphviz `fontname` as the measurer receives it. */
35
+ readonly fontname: string | null;
36
+ /** Font size in points. */
37
+ readonly fontsize: number;
38
+ /** Bold/italic variant flags; absent for the regular face. */
39
+ readonly flags?: TextVariantFlags;
40
+ }
41
+
42
+ /** Everything the sync pipeline needs prefetched. */
43
+ export interface Resources {
44
+ /** Distinct font requests, deduped by their `canvasFont` string. */
45
+ readonly fonts: FontRequest[];
46
+ /** Distinct HTML `<IMG SRC>` values (dimension lookups). */
47
+ readonly sizeSrcs: string[];
48
+ /** Distinct image sources to fetch as bytes; empty unless `inlineImages`. */
49
+ readonly bytesSrcs: string[];
50
+ }
51
+
52
+ /** Options for {@link collectResources}. */
53
+ export interface CollectOptions {
54
+ /** Also collect `image=` and HTML `<IMG SRC>` values as bytes sources. */
55
+ readonly inlineImages: boolean;
56
+ }
57
+
58
+ interface Acc {
59
+ readonly fonts: Map<string, FontRequest>;
60
+ readonly sizeSrcs: Set<string>;
61
+ readonly bytesSrcs: Set<string>;
62
+ readonly inlineImages: boolean;
63
+ }
64
+
65
+ interface LabelFont { readonly fontname: string; readonly fontsize: number }
66
+
67
+ function addFont(acc: Acc, fontname: string | null, fontsize: number, flags?: TextVariantFlags): void {
68
+ const key = canvasFont(fontname, fontsize, flags);
69
+ if (acc.fonts.has(key)) return;
70
+ const req: FontRequest = flags === undefined
71
+ ? { fontname, fontsize }
72
+ : { fontname, fontsize, flags };
73
+ acc.fonts.set(key, req);
74
+ }
75
+
76
+ function addImage(acc: Acc, src: string | undefined, sized: boolean): void {
77
+ if (src === undefined || src === '') return;
78
+ if (sized) acc.sizeSrcs.add(src);
79
+ if (acc.inlineImages) acc.bytesSrcs.add(src);
80
+ }
81
+
82
+ /** Variant flags as the measurer receives them (undefined when regular). */
83
+ function variantFlags(item: HtmlTextItem): TextVariantFlags | undefined {
84
+ if (item.bold !== true && item.italic !== true) return undefined;
85
+ return { bold: item.bold === true, italic: item.italic === true };
86
+ }
87
+
88
+ function addItemFont(acc: Acc, item: HtmlTextItem, font: LabelFont): void {
89
+ if (item.text === undefined && item.br !== true) return;
90
+ addFont(acc, item.fontFace ?? font.fontname, item.fontSize ?? font.fontsize, variantFlags(item));
91
+ }
92
+
93
+ function walkContent(acc: Acc, c: HtmlCellContent, font: LabelFont): void {
94
+ switch (c.kind) {
95
+ case 'text':
96
+ for (const item of c.items) addItemFont(acc, item, font);
97
+ break;
98
+ case 'table':
99
+ walkTable(acc, c, font);
100
+ break;
101
+ case 'image':
102
+ addImage(acc, c.src, true);
103
+ break;
104
+ case 'hr':
105
+ break;
106
+ }
107
+ }
108
+
109
+ function walkTable(acc: Acc, t: HtmlTable, font: LabelFont): void {
110
+ for (const row of t.rows) {
111
+ for (const cell of row.cells) {
112
+ for (const c of cell.content) walkContent(acc, c, font);
113
+ }
114
+ }
115
+ }
116
+
117
+ function walkHtmlLabel(acc: Acc, label: HtmlLabel, font: LabelFont): void {
118
+ if (label.kind === 'table') {
119
+ walkTable(acc, label.table, font);
120
+ return;
121
+ }
122
+ for (const t of label.texts) walkContent(acc, t, font);
123
+ }
124
+
125
+ /** Collect the fonts/images one label string leads the sync pipeline to use. */
126
+ function addLabel(acc: Acc, str: string | undefined, font: LabelFont): void {
127
+ if (!str) return;
128
+ if (!isHtmlValue(str)) {
129
+ addFont(acc, font.fontname, font.fontsize);
130
+ return;
131
+ }
132
+ const content = htmlValueContent(str);
133
+ if (content === '') return; // labels.c:119 — empty HTML is no label
134
+ let label: HtmlLabel;
135
+ try {
136
+ label = parseHtmlLabel(content);
137
+ } catch {
138
+ // Parse failure: the sync path reverts to a plain-text label (htmltable.c:1892).
139
+ addFont(acc, font.fontname, font.fontsize);
140
+ return;
141
+ }
142
+ walkHtmlLabel(acc, label, font);
143
+ }
144
+
145
+ function collectNode(acc: Acc, n: Node, root: Graph): void {
146
+ const { fontname, fontsize } = readFontAttrs(n, root);
147
+ const label = nodeAttr(n, root, 'label');
148
+ if (label === undefined) addFont(acc, fontname, fontsize); // label defaults to \N
149
+ else addLabel(acc, label, { fontname, fontsize });
150
+ addLabel(acc, nodeAttr(n, root, 'xlabel'), { fontname, fontsize });
151
+ addImage(acc, nodeAttr(n, root, 'image'), false);
152
+ }
153
+
154
+ function collectEdge(acc: Acc, e: Edge): void {
155
+ const fi = initFontEdgeAttr(e);
156
+ const lfi = initFontLabelEdgeAttr(e, fi);
157
+ addLabel(acc, e.attrs.get('label'), fi);
158
+ addLabel(acc, e.attrs.get('xlabel'), fi);
159
+ addLabel(acc, e.attrs.get('headlabel'), lfi);
160
+ addLabel(acc, e.attrs.get('taillabel'), lfi);
161
+ }
162
+
163
+ /**
164
+ * Root/cluster label font. A subgraph reads through its parse-time defaults
165
+ * snapshot, as doGraphLabel does. @see src/layout/dot/graph-label.ts:readFontParams
166
+ */
167
+ function graphLabelFont(sg: Graph): LabelFont {
168
+ const get = (k: string): string | undefined => sg.attrs.get(k) ?? sg.graphDefaultsSnapshot?.get(k);
169
+ return {
170
+ fontname: get('fontname') ?? DEFAULT_FONTNAME,
171
+ fontsize: parseFloat(get('fontsize') ?? '') || DEFAULT_FONTSIZE,
172
+ };
173
+ }
174
+
175
+ function collectGraphLabels(acc: Acc, sg: Graph, root: Graph): void {
176
+ if (isACluster(sg)) {
177
+ addLabel(acc, sg.attrs.get('label') ?? sg.graphDefaultsSnapshot?.get('label'), graphLabelFont(sg));
178
+ }
179
+ for (const child of sg.subgraphs.values()) collectGraphLabels(acc, child, root);
180
+ }
181
+
182
+ /**
183
+ * Gather every font face and image source the synchronous pipeline will ask
184
+ * for. Pure: reads the parsed graph only; a malformed HTML label is skipped
185
+ * (the sync path falls back to plain text).
186
+ *
187
+ * @param g - A parsed graph (before layout).
188
+ * @param opts - Collection options.
189
+ * @returns The deduped fonts, size sources and bytes sources.
190
+ */
191
+ export function collectResources(g: Graph, opts: CollectOptions): Resources {
192
+ const acc: Acc = {
193
+ fonts: new Map(), sizeSrcs: new Set(), bytesSrcs: new Set(), inlineImages: opts.inlineImages,
194
+ };
195
+ const root = g.root;
196
+ collectGraphLabels(acc, root, root);
197
+ for (const n of root.nodes.values()) collectNode(acc, n, root);
198
+ for (const e of root.edges) collectEdge(acc, e);
199
+ return {
200
+ fonts: [...acc.fonts.values()],
201
+ sizeSrcs: [...acc.sizeSrcs],
202
+ bytesSrcs: [...acc.bytesSrcs],
203
+ };
204
+ }
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ import { canvasFont } from '../common/css-font.js';
3
+ import type { FontRequest } from './collect.js';
4
+
5
+ /** A font that could not be made available before measuring. */
6
+ export type FontIssue = { face: string; reason: 'failed' | 'timeout' };
7
+
8
+ /** Structural subset of `FontFaceSet`; the real object satisfies it. */
9
+ export interface FontSetLike {
10
+ load(font: string): Promise<readonly { status?: string }[]>;
11
+ }
12
+
13
+ type Outcome = FontIssue['reason'] | 'ok';
14
+
15
+ async function settle(set: FontSetLike, face: string): Promise<Outcome> {
16
+ try {
17
+ const faces = await set.load(face);
18
+ return faces.some((f) => f.status === 'error') ? 'failed' : 'ok';
19
+ } catch {
20
+ // A rejected load is reported as a `failed` issue, not rethrown.
21
+ return 'failed';
22
+ }
23
+ }
24
+
25
+ function toIssues(faces: readonly string[], outcomes: readonly Outcome[]): FontIssue[] {
26
+ const issues: FontIssue[] = [];
27
+ faces.forEach((face, i) => {
28
+ const reason = outcomes[i];
29
+ if (reason === undefined || reason === 'ok') return;
30
+ console.warn(`dot-engine: font "${face}" ${reason}`);
31
+ issues.push({ face, reason });
32
+ });
33
+ return issues;
34
+ }
35
+
36
+ /**
37
+ * Loads every distinct CSS font the requests map to (in parallel, sharing one
38
+ * deadline) so canvas measurement sees real faces rather than fallbacks.
39
+ * Resolves with one {@link FontIssue} per face that failed or timed out;
40
+ * never rejects. An undefined `fontSet` yields `[]` immediately.
41
+ */
42
+ export async function loadFonts(
43
+ fontSet: FontSetLike | undefined,
44
+ fonts: readonly FontRequest[],
45
+ timeoutMs: number,
46
+ ): Promise<FontIssue[]> {
47
+ if (fontSet === undefined) return [];
48
+ const faces = [...new Set(fonts.map((f) => canvasFont(f.fontname, f.fontsize, f.flags)))];
49
+ let timer: ReturnType<typeof setTimeout> | undefined;
50
+ const deadline = new Promise<Outcome>((resolve) => {
51
+ timer = setTimeout(() => resolve('timeout'), timeoutMs);
52
+ });
53
+ try {
54
+ const outcomes = await Promise.all(
55
+ faces.map((face) => Promise.race([settle(fontSet, face), deadline])),
56
+ );
57
+ return toIssues(faces, outcomes);
58
+ } finally {
59
+ clearTimeout(timer);
60
+ }
61
+ }