@knowvah/dot-engine 1.9.0 → 2.0.1

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 (184) 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/htmltable-types.d.ts +3 -3
  21. package/dist/common/htmltable-types.d.ts.map +1 -1
  22. package/dist/common/make-label.d.ts.map +1 -1
  23. package/dist/common/poly-shapes.d.ts.map +1 -1
  24. package/dist/common/textmeasure-factory.d.ts +2 -0
  25. package/dist/common/textmeasure-factory.d.ts.map +1 -1
  26. package/dist/common/utils-inputscale.d.ts +19 -0
  27. package/dist/common/utils-inputscale.d.ts.map +1 -0
  28. package/dist/errors.d.ts +61 -5
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/gvc/context.d.ts +36 -4
  31. package/dist/gvc/context.d.ts.map +1 -1
  32. package/dist/gvc/device.d.ts +5 -2
  33. package/dist/gvc/device.d.ts.map +1 -1
  34. package/dist/gvc/image-resolver.d.ts +16 -16
  35. package/dist/gvc/image-resolver.d.ts.map +1 -1
  36. package/dist/gvc/job.d.ts +1 -9
  37. package/dist/gvc/job.d.ts.map +1 -1
  38. package/dist/gvc/usershape.d.ts +2 -12
  39. package/dist/gvc/usershape.d.ts.map +1 -1
  40. package/dist/index.d.ts +27 -8
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7682 -5535
  43. package/dist/index.js.map +4 -4
  44. package/dist/label/index.d.ts.map +1 -1
  45. package/dist/label/node.d.ts +0 -6
  46. package/dist/label/node.d.ts.map +1 -1
  47. package/dist/label/rectangle.d.ts +1 -7
  48. package/dist/label/rectangle.d.ts.map +1 -1
  49. package/dist/layout/circo/circular.d.ts +8 -5
  50. package/dist/layout/circo/circular.d.ts.map +1 -1
  51. package/dist/layout/dot/pack-components.d.ts +0 -19
  52. package/dist/layout/dot/pack-components.d.ts.map +1 -1
  53. package/dist/layout/dot/position.d.ts +7 -2
  54. package/dist/layout/dot/position.d.ts.map +1 -1
  55. package/dist/layout/fdp/derive.d.ts.map +1 -1
  56. package/dist/layout/fdp/index.d.ts.map +1 -1
  57. package/dist/layout/fdp/init.d.ts.map +1 -1
  58. package/dist/layout/fdp/layout.d.ts.map +1 -1
  59. package/dist/layout/fdp/normalize.d.ts +2 -1
  60. package/dist/layout/fdp/normalize.d.ts.map +1 -1
  61. package/dist/layout/fdp/ports.d.ts +0 -10
  62. package/dist/layout/fdp/ports.d.ts.map +1 -1
  63. package/dist/layout/fdp/xlayout.d.ts +0 -16
  64. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  65. package/dist/layout/neato/adjust-info.d.ts +117 -0
  66. package/dist/layout/neato/adjust-info.d.ts.map +1 -0
  67. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  68. package/dist/layout/neato/constraint-adjust.d.ts +40 -0
  69. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
  70. package/dist/layout/neato/edge-len.d.ts +20 -0
  71. package/dist/layout/neato/edge-len.d.ts.map +1 -0
  72. package/dist/layout/neato/fdp-adjust.d.ts +32 -4
  73. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  74. package/dist/layout/neato/index.d.ts +9 -6
  75. package/dist/layout/neato/index.d.ts.map +1 -1
  76. package/dist/layout/neato/init.d.ts +4 -11
  77. package/dist/layout/neato/init.d.ts.map +1 -1
  78. package/dist/layout/neato/kk-paths.d.ts +50 -0
  79. package/dist/layout/neato/kk-paths.d.ts.map +1 -0
  80. package/dist/layout/neato/kk-solve.d.ts +14 -0
  81. package/dist/layout/neato/kk-solve.d.ts.map +1 -0
  82. package/dist/layout/neato/kk.d.ts +44 -0
  83. package/dist/layout/neato/kk.d.ts.map +1 -0
  84. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  85. package/dist/layout/neato/poly.d.ts +50 -0
  86. package/dist/layout/neato/poly.d.ts.map +1 -0
  87. package/dist/layout/neato/sc-adjust.d.ts +2 -9
  88. package/dist/layout/neato/sc-adjust.d.ts.map +1 -1
  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/simple-scale.d.ts +15 -0
  94. package/dist/layout/neato/simple-scale.d.ts.map +1 -0
  95. package/dist/layout/neato/start.d.ts +51 -0
  96. package/dist/layout/neato/start.d.ts.map +1 -0
  97. package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
  98. package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
  99. package/dist/layout/sfdp/index.d.ts.map +1 -1
  100. package/dist/layout/sfdp/init.d.ts +0 -9
  101. package/dist/layout/sfdp/init.d.ts.map +1 -1
  102. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  103. package/dist/layout/twopi/circle.d.ts +0 -7
  104. package/dist/layout/twopi/circle.d.ts.map +1 -1
  105. package/dist/ortho/ortho-parallel.d.ts.map +1 -1
  106. package/dist/ortho/trap-query.d.ts.map +1 -1
  107. package/dist/parser/index.d.ts +9 -5
  108. package/dist/parser/index.d.ts.map +1 -1
  109. package/dist/render/index.d.ts +2 -0
  110. package/dist/render/index.d.ts.map +1 -1
  111. package/dist/render/public.d.ts +9 -3
  112. package/dist/render/public.d.ts.map +1 -1
  113. package/dist/render/xdot-public.d.ts +7 -2
  114. package/dist/render/xdot-public.d.ts.map +1 -1
  115. package/dist/render.js +7468 -5556
  116. package/dist/render.js.map +4 -4
  117. package/dist/util/xml.d.ts.map +1 -1
  118. package/dist/vpsc/Solver.d.ts +1 -0
  119. package/dist/vpsc/Solver.d.ts.map +1 -1
  120. package/package.json +1 -1
  121. package/src/api/builder.ts +73 -5
  122. package/src/api/edge-ops.ts +19 -0
  123. package/src/api/geometry.ts +22 -5
  124. package/src/async/collect.ts +204 -0
  125. package/src/async/fonts.ts +61 -0
  126. package/src/async/render-async.ts +205 -0
  127. package/src/async/render-into.ts +115 -0
  128. package/src/async/sanitize.ts +150 -0
  129. package/src/common/htmltable-types.ts +4 -5
  130. package/src/common/make-label.ts +10 -1
  131. package/src/common/poly-shapes.ts +4 -1
  132. package/src/common/textmeasure-factory.ts +11 -0
  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/normalize.ts +12 -8
  152. package/src/layout/fdp/ports.ts +3 -2
  153. package/src/layout/fdp/xlayout.ts +8 -5
  154. package/src/layout/neato/adjust-info.ts +332 -0
  155. package/src/layout/neato/cdt-surface.ts +50 -30
  156. package/src/layout/neato/constraint-adjust.ts +466 -0
  157. package/src/layout/neato/edge-len.ts +36 -0
  158. package/src/layout/neato/fdp-adjust.ts +120 -12
  159. package/src/layout/neato/index.ts +36 -54
  160. package/src/layout/neato/init.ts +21 -36
  161. package/src/layout/neato/kk-paths.ts +146 -0
  162. package/src/layout/neato/kk-solve.ts +64 -0
  163. package/src/layout/neato/kk.ts +322 -0
  164. package/src/layout/neato/multispline-router.ts +4 -3
  165. package/src/layout/neato/poly.ts +493 -0
  166. package/src/layout/neato/sc-adjust.ts +2 -16
  167. package/src/layout/neato/sgd-dijkstra.ts +125 -0
  168. package/src/layout/neato/sgd.ts +63 -40
  169. package/src/layout/neato/simple-scale.ts +54 -0
  170. package/src/layout/neato/start.ts +222 -0
  171. package/src/layout/neato/vpsc-adjust.ts +93 -0
  172. package/src/layout/sfdp/index.ts +4 -5
  173. package/src/layout/sfdp/init.ts +40 -4
  174. package/src/layout/sfdp/spring-driver.ts +30 -1
  175. package/src/layout/twopi/circle.ts +2 -1
  176. package/src/ortho/ortho-parallel.ts +2 -1
  177. package/src/ortho/trap-query.ts +2 -1
  178. package/src/parser/index.ts +11 -11
  179. package/src/render/index.ts +5 -0
  180. package/src/render/public.ts +24 -24
  181. package/src/render/svg.ts +1 -1
  182. package/src/render/xdot-public.ts +19 -18
  183. package/src/util/xml.ts +24 -30
  184. package/src/vpsc/Solver.ts +5 -3
@@ -10,6 +10,7 @@
10
10
  * @see lib/neatogen/adjust.c:makeMatrix / getSizes (15.0.0)
11
11
  */
12
12
 
13
+ import { RenderError } from '../../errors.js';
13
14
  import type { Graph } from '../../model/graph.js';
14
15
  import { setEdgeTypeFromAttr } from '../dot/index.js';
15
16
  import { EDGETYPE_LINE } from '../neato/splines.js';
@@ -19,6 +20,7 @@ import {
19
20
  } from '../../common/nodeinit.js';
20
21
  import { initEdgeLabels } from '../../common/edge-label-init.js';
21
22
  import { aggetGraph } from '../fdp/fdp-model.js';
23
+ import { overlapPrismTries } from '../neato/fdp-adjust.js';
22
24
  import {
23
25
  type SpMatrix,
24
26
  smFromCoordinateArrays,
@@ -37,6 +39,11 @@ export const SMOOTHING_NONE = 0;
37
39
 
38
40
  const INT_MAX = 2147483647;
39
41
 
42
+ /** @see lib/neatogen/overlap.h:ELSCHEME_PENALTY2 (schemes 3, 4 are STRAIGHTLINE) */
43
+ const ELSCHEME_PENALTY2 = 2;
44
+ /** @see lib/neatogen/adjust.c:ELS */
45
+ const EDGE_LABEL_NODE_PREFIX = '|edgelabel|';
46
+
40
47
  /**
41
48
  * Graph-level init: line edges, 2-D, per-node neato init.
42
49
  * GD_ndim handling is fixed at 2 — the "dim"/"dimen" 3D+ modes are not
@@ -131,9 +138,10 @@ export function tuneControl(g: Graph, ctrl: SpringElectricalControl): void {
131
138
  const smoothing = aggetGraph(g, 'smoothing');
132
139
  if (smoothing !== undefined && smoothing.toLowerCase() !== 'none' &&
133
140
  smoothing !== String(SMOOTHING_NONE)) {
134
- throw new Error(
141
+ throw new RenderError(
135
142
  `sfdp smoothing="${smoothing}": post_process_smoothing is not ` +
136
- 'ported (unreachable at sfdp defaults); see mission 8 journal');
143
+ 'ported (unreachable at sfdp defaults); see mission 8 journal',
144
+ 'UNSUPPORTED_FEATURE');
137
145
  }
138
146
  ctrl.smoothing = SMOOTHING_NONE;
139
147
  ctrl.tscheme = lateQuadtreeScheme(aggetGraph(g, 'quadtree'), QUAD_TREE_NORMAL);
@@ -147,12 +155,40 @@ export function tuneControl(g: Graph, ctrl: SpringElectricalControl): void {
147
155
  // unported-feature error beats silently wrong geometry.
148
156
  // Found by the attribute blind-spot scan; no corpus graph sets it.
149
157
  if (ctrl.rotation !== 0) {
150
- throw new Error(
158
+ throw new RenderError(
151
159
  `sfdp rotation="${ctrl.rotation}": rotate() is not ported ` +
152
- '(unreachable at sfdp defaults); see plans/port-catalog/README.md');
160
+ '(unreachable at sfdp defaults); see plans/port-catalog/README.md',
161
+ 'UNSUPPORTED_FEATURE');
153
162
  }
154
163
  ctrl.edgeLabelingScheme = lateInt(aggetGraph(g, 'label_scheme'), 0, 0);
155
164
  if (ctrl.edgeLabelingScheme > 4) ctrl.edgeLabelingScheme = 0;
165
+ assertEdgeLabelSchemeSupported(g, ctrl.edgeLabelingScheme);
166
+ }
167
+
168
+ /**
169
+ * Fail loudly where C would take the unported edge-label path. C reaches it
170
+ * only when sfdp removes overlap itself (sfdp_layout: AM_PRISM, ctrl.overlap
171
+ * >= 0, so getSizes collects the "|edgelabel|" nodes) and at least one such
172
+ * node exists. Schemes 1-2 then act inside remove_overlap, which returns
173
+ * early when ntry == 0 (prism0, the default); schemes 3-4 shorten the
174
+ * label nodes in multilevel_spring_electrical_embedding regardless of ntry.
175
+ * Ordinary edge labels never trigger it: only nodes named "|edgelabel|...".
176
+ * @see lib/sfdpgen/sfdpinit.c:sfdpLayout
177
+ * @see lib/neatogen/adjust.c:getSizes (IS_LNODE)
178
+ * @see lib/sfdpgen/spring_electrical.c:multilevel_spring_electrical_embedding
179
+ * @see lib/neatogen/overlap.c:remove_overlap
180
+ */
181
+ function assertEdgeLabelSchemeSupported(g: Graph, scheme: number): void {
182
+ if (scheme <= 0) return;
183
+ const ntry = overlapPrismTries(aggetGraph(g, 'overlap') ?? 'prism0');
184
+ if (ntry === null) return; // ctrl.overlap = -1: getSizes gets no elabels
185
+ if (scheme <= ELSCHEME_PENALTY2 && ntry === 0) return;
186
+ for (const n of g.nodes.values()) {
187
+ if (!n.name.startsWith(EDGE_LABEL_NODE_PREFIX)) continue;
188
+ throw new RenderError(
189
+ `label_scheme=${scheme}: edge-label node handling is not supported yet`,
190
+ 'UNSUPPORTED_FEATURE');
191
+ }
156
192
  }
157
193
 
158
194
  /**
@@ -11,6 +11,7 @@
11
11
  * @see lib/neatogen/overlap.c:remove_overlap (15.0.0)
12
12
  */
13
13
 
14
+ import { RenderError } from '../../errors.js';
14
15
  import { fma } from '../../common/fma.js';
15
16
  import { cdrand } from '../../common/crand.js';
16
17
  import {
@@ -30,10 +31,38 @@ import {
30
31
  import {
31
32
  type SpringElectricalControl,
32
33
  AUTOP,
34
+ QUAD_TREE_NONE,
35
+ QUAD_TREE_FAST,
33
36
  springElectricalEmbedding,
34
37
  } from './spring-electrical.js';
35
38
  import { removeOverlapPrism } from '../neato/overlap-prism.js';
36
39
 
40
+ // ---------------------------------------------------------------------------
41
+ // Quadtree scheme dispatch (loud for the unported embeddings)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Mirror of the per-level dispatch: NONE -> _slow, FAST (or HYBRID above
46
+ * QUAD_TREE_HYBRID_SIZE) -> _fast, else the NORMAL embedding. The first two
47
+ * are not ported. HYBRID is unreachable here: sfdp's quadtree parser only
48
+ * yields NONE/NORMAL/FAST (sfdpinit.c:late_quadtree_scheme).
49
+ * @see lib/sfdpgen/spring_electrical.c:multilevel_spring_electrical_embedding (1140-1148)
50
+ */
51
+ function embedLevel(
52
+ dim: number, A: SpMatrix, ctrl: SpringElectricalControl, xc: number[],
53
+ ): void {
54
+ if (ctrl.tscheme === QUAD_TREE_NONE || ctrl.tscheme === QUAD_TREE_FAST) {
55
+ // Name the resolved scheme: "0"/"false" select none, "2" selects fast.
56
+ const [value, what] = ctrl.tscheme === QUAD_TREE_NONE
57
+ ? ['none', 'spring_electrical_embedding_slow']
58
+ : ['fast', 'spring_electrical_embedding_fast'];
59
+ throw new RenderError(
60
+ `quadtree=${value}: ${what} is not supported yet`,
61
+ 'UNSUPPORTED_FEATURE');
62
+ }
63
+ springElectricalEmbedding(dim, A, ctrl, xc);
64
+ }
65
+
37
66
  // ---------------------------------------------------------------------------
38
67
  // Multilevel driver helpers
39
68
  // ---------------------------------------------------------------------------
@@ -178,7 +207,7 @@ export function multilevelSpringElectricalEmbedding(
178
207
  }
179
208
 
180
209
  for (;;) {
181
- springElectricalEmbedding(dim, grid!.A, ctrl, xc);
210
+ embedLevel(dim, grid!.A, ctrl, xc);
182
211
  if (multilevelIsFinest(grid!)) break;
183
212
  const P = grid!.P!;
184
213
  grid = grid!.prev;
@@ -8,6 +8,7 @@
8
8
  * @see lib/twopigen/circle.h
9
9
  */
10
10
 
11
+ import { InternalError } from '../../errors.js';
11
12
  import type { Graph } from '../../model/graph.js';
12
13
  import type { Node } from '../../model/node.js';
13
14
  import type { Edge } from '../../model/edge.js';
@@ -23,7 +24,7 @@ export const MIN_RANKSEP = 0.02;
23
24
  /** Get the TwopiAlgData record for a node; throws if absent. */
24
25
  function rdata(n: Node): TwopiAlgData {
25
26
  const d = n.info.alg;
26
- if (!d || d.kind !== 'twopi') throw new Error(`twopi: missing rdata on node ${n.name}`);
27
+ if (!d || d.kind !== 'twopi') throw new InternalError(`twopi: missing rdata on node ${n.name}`);
27
28
  return d;
28
29
  }
29
30
 
@@ -27,6 +27,7 @@ import { Bend } from "./types.js";
27
27
  import { chanSearch, chansInOrder } from "./maze-channels.js";
28
28
  import { insertEdge, edgeExists, removeRedge } from "./rawgraph.js";
29
29
  import { segCmp } from "./ortho-route.js";
30
+ import { InternalError } from "../errors.js";
30
31
 
31
32
  /** @see lib/ortho/ortho.c:next_seg */
32
33
  function nextSeg(seg: OrthoSegment, dir: number): OrthoSegment | null {
@@ -81,7 +82,7 @@ function decidePoint(
81
82
  sj = np2; // eslint-disable-line no-param-reassign
82
83
  }
83
84
  if (np1 === null) prec = 0;
84
- else if (np2 === null) throw new Error("decide_point: np2 null (C assert(0))");
85
+ else if (np2 === null) throw new InternalError("decide_point: np2 null (C assert(0))");
85
86
  else {
86
87
  const temp = segCmp(np1, np2);
87
88
  if (temp === -2) return -1;
@@ -13,6 +13,7 @@ import {
13
13
  cross, TRAP_MAX,
14
14
  } from "./trap-types.js";
15
15
  import type { SegPoint, SegmentT, TrapT, QNode } from "./trap-types.js";
16
+ import { InternalError } from "../errors.js";
16
17
 
17
18
  /**
18
19
  * Test whether point v is to the left of segment segnum.
@@ -96,7 +97,7 @@ export function locateEndpoint(
96
97
  }
97
98
  return locateEndpoint(v, vo, rptr.left, seg, qs);
98
99
  case T_X: return locateX(v, vo, rptr, seg, qs);
99
- default: throw new Error("locateEndpoint: unreachable");
100
+ default: throw new InternalError("locateEndpoint: unreachable");
100
101
  }
101
102
  }
102
103
 
@@ -14,18 +14,18 @@ import type { Edge } from '../model/edge.js';
14
14
  import { isHtmlValue, htmlValueContent } from '../common/html-string.js';
15
15
  import { buildFromAst } from './builder.js';
16
16
  import type { ParsedGraph } from './ast.js';
17
- import type { GvError, GvErrorCode, GvExpectation } from '../errors.js';
18
- import { friendlyMessageFor } from '../errors.js';
17
+ import type { GvErrorCode, GvExpectation } from '../errors.js';
18
+ import { DotEngineError, friendlyMessageFor, invalidArgType } from '../errors.js';
19
19
 
20
20
  // ── ParseError ────────────────────────────────────────────────────────────────
21
21
 
22
22
  /**
23
23
  * Thrown for syntax errors or edge-direction violations.
24
24
  *
25
- * Implements the structured {@link GvError} contract: `location` is primary;
25
+ * Implements the structured {@link DotEngineError} / `GvError` contract: `location` is primary;
26
26
  * `line`/`column` are convenience getters that delegate to it.
27
27
  */
28
- export class ParseError extends Error implements GvError {
28
+ export class ParseError extends DotEngineError {
29
29
  readonly type = 'syntax';
30
30
  readonly code: GvErrorCode;
31
31
  readonly friendlyMessage: string;
@@ -37,8 +37,9 @@ export class ParseError extends Error implements GvError {
37
37
  code: GvErrorCode,
38
38
  location: { line: number; column: number; offset?: number },
39
39
  expected?: GvExpectation[],
40
+ options?: ErrorOptions,
40
41
  ) {
41
- super(message);
42
+ super(message, options);
42
43
  this.name = 'ParseError';
43
44
  this.code = code;
44
45
  this.location = location;
@@ -217,17 +218,16 @@ export function isPeggyError(err: unknown): err is {
217
218
 
218
219
  /**
219
220
  * Parse a DOT-language string and return a Graph model.
220
- * @throws ParseError for syntax errors or edge-direction violations.
221
+ * @throws ParseError `SYNTAX_ERROR`, `SYNTAX_UNEXPECTED_EOF`, `EDGE_OP_*` for
222
+ * syntax errors or edge-direction violations; `GENERIC_ERROR` when nesting
223
+ * is too deep
224
+ * @throws TypeError `ERR_INVALID_ARG_TYPE` if `src` is not a string
221
225
  */
222
226
  export function parse(src: string): Graph {
223
227
  if (typeof src !== 'string') {
224
228
  // Runtime guard for JS callers (the TS signature already forbids this):
225
229
  // a non-string argument must not surface as an opaque internal TypeError.
226
- throw new ParseError('DOT source must be a string', 'GENERIC_ERROR', {
227
- line: 1,
228
- column: 1,
229
- offset: 0,
230
- });
230
+ throw invalidArgType('dotSource', 'string', src);
231
231
  }
232
232
  let ast: ParsedGraph;
233
233
  try {
@@ -22,6 +22,11 @@
22
22
  // --- Multi-format render entry (T5) ---------------------------------------
23
23
  export { render } from './public.js';
24
24
  export type { OutputFormat, RenderOptions } from './public.js';
25
+ export { renderAsync } from '../async/render-async.js';
26
+ export type {
27
+ AsyncRenderOptions, AsyncRenderResult, AsyncImageSize, AsyncImageBytes,
28
+ FontIssue, FontSetLike,
29
+ } from '../async/render-async.js';
25
30
 
26
31
  // --- Structured xdot draw-ops (T6) ----------------------------------------
27
32
  export { getDrawOps, DEFAULT_DRAW_ENGINE } from './xdot-public.js';
@@ -13,8 +13,7 @@
13
13
  import type { Graph } from '../model/graph.js';
14
14
  import { createDefaultContext } from '../gvc/default-context.js';
15
15
  import { render as deviceRender } from '../gvc/device.js';
16
- import { RenderError } from '../errors.js';
17
- import type { GvError } from '../errors.js';
16
+ import { invalidArgType, rethrowAtBoundary } from '../errors.js';
18
17
  import type { EngineName } from '../gvc/context.js';
19
18
 
20
19
  // ---------------------------------------------------------------------------
@@ -64,23 +63,19 @@ export interface RenderOptions {
64
63
  // Private helpers (mirrors index.ts)
65
64
  // ---------------------------------------------------------------------------
66
65
 
67
- /**
68
- * Duck-type a thrown value as a {@link GvError}: object with string `type`
69
- * and string `code`. Re-implemented here (not imported) so this module has
70
- * no circular dependency on `src/index.ts`.
71
- */
72
- function isGvErrorLike(err: unknown): err is GvError {
73
- return (
74
- typeof err === 'object' &&
75
- err !== null &&
76
- typeof (err as { type?: unknown }).type === 'string' &&
77
- typeof (err as { code?: unknown }).code === 'string'
78
- );
79
- }
80
66
 
81
- /* v8 ignore next -- defensive normalizer; unreachable via public API */
82
- function messageOf(err: unknown): string {
83
- return err instanceof Error ? err.message : String(err);
67
+
68
+ /** Reject a bad `g` / `format` / `opts` before any work starts. */
69
+ function checkRenderArgs(g: unknown, format: unknown, opts: unknown): void {
70
+ if (typeof g !== 'object' || g === null) {
71
+ throw invalidArgType('g', 'object', g);
72
+ }
73
+ if (typeof format !== 'string') {
74
+ throw invalidArgType('format', 'string', format);
75
+ }
76
+ if (opts !== undefined && (typeof opts !== 'object' || opts === null)) {
77
+ throw invalidArgType('opts', 'object or undefined', opts);
78
+ }
84
79
  }
85
80
 
86
81
  // ---------------------------------------------------------------------------
@@ -91,8 +86,8 @@ function messageOf(err: unknown): string {
91
86
  * Render a (parsed or built) graph to the requested format string.
92
87
  *
93
88
  * Lifecycle: createDefaultContext → layout → deviceRender → freeLayout.
94
- * Error handling mirrors `renderSvg`: GvError-like throws re-surface
95
- * unchanged; unknown throws become `RenderError('RENDER_ERROR')`.
89
+ * Error handling mirrors `renderSvg`: usage errors and GvErrors re-surface
90
+ * unchanged; any other throw becomes an `InternalError` with `cause` set.
96
91
  *
97
92
  * @remarks
98
93
  * Security: for the markup formats (`svg`, `cmapx`, `imap`), treat the output
@@ -106,7 +101,13 @@ function messageOf(err: unknown): string {
106
101
  * @param format - target output format
107
102
  * @param opts - optional engine override (default: `'dot'`)
108
103
  * @returns rendered string in the requested format
109
- * @throws RenderError on layout or render failure
104
+ * @throws TypeError `ERR_INVALID_ARG_TYPE` if `g` is not an object, `format`
105
+ * is not a string, or `opts` is neither undefined nor an object
106
+ * @throws TypeError `ERR_INVALID_ARG_VALUE` if the engine or format is not
107
+ * registered
108
+ * @throws RenderError `RENDER_ERROR`, `UNKNOWN_LAYOUT` or `UNSUPPORTED_FEATURE`
109
+ * on layout or render failure
110
+ * @throws InternalError `INTERNAL_ERROR` on a dot-engine bug
110
111
  *
111
112
  * @see lib/gvc/gvc.c:gvRender
112
113
  */
@@ -115,6 +116,7 @@ export function render(
115
116
  format: OutputFormat,
116
117
  opts?: RenderOptions,
117
118
  ): string {
119
+ checkRenderArgs(g, format, opts);
118
120
  const engine: EngineName = opts?.engine ?? 'dot';
119
121
  const inlineImages = opts?.inlineImages ?? false;
120
122
  const ctx = createDefaultContext();
@@ -124,8 +126,6 @@ export function render(
124
126
  ctx.freeLayout(g, engine);
125
127
  return result;
126
128
  } catch (err: unknown) {
127
- /* v8 ignore next -- current engines don't throw a GvError-like value here */
128
- if (isGvErrorLike(err)) throw err;
129
- throw new RenderError(messageOf(err), 'RENDER_ERROR');
129
+ return rethrowAtBoundary(err);
130
130
  }
131
131
  }
package/src/render/svg.ts CHANGED
@@ -282,7 +282,7 @@ export class SvgRenderer implements RendererPlugin {
282
282
  const originy = (b.ur.y + b.ll.y + height) / 2;
283
283
  let href = src;
284
284
  if (job.inlineImages === true) {
285
- const found = findImageBytes(src);
285
+ const found = findImageBytes(src, job.imageResolver);
286
286
  if (found !== null) href = toDataUri(found.bytes, found.mime);
287
287
  }
288
288
  job.write('<image xlink:href="' + escapeXml(href) + '" width="' + g(width)
@@ -34,8 +34,7 @@ import { render as gvcRender } from '../gvc/device.js';
34
34
  import { createDefaultContext } from '../gvc/default-context.js';
35
35
  import { parseXDot } from '../xdot/index.js';
36
36
  import type { EngineName } from '../gvc/context.js';
37
- import { RenderError } from '../errors.js';
38
- import type { GvError } from '../errors.js';
37
+ import { invalidArgType, rethrowAtBoundary } from '../errors.js';
39
38
 
40
39
  /**
41
40
  * `Xdot` — the parsed result of one xdot attribute stream: `ops` (the
@@ -76,20 +75,16 @@ const XDOT_DRAW_ATTRS = [
76
75
  '_draw_', '_ldraw_', '_hdraw_', '_tdraw_', '_hldraw_', '_tldraw_',
77
76
  ] as const;
78
77
 
79
- function isGvErrorLike(err: unknown): err is GvError {
80
- return (
81
- typeof err === 'object' &&
82
- err !== null &&
83
- typeof (err as { type?: unknown }).type === 'string' &&
84
- typeof (err as { code?: unknown }).code === 'string'
85
- );
86
- }
87
78
 
88
- /** Re-throw GvError-like values; otherwise wrap as RENDER_ERROR. */
89
- function rethrowAsRender(err: unknown): never {
90
- if (isGvErrorLike(err)) throw err;
91
- const msg = err instanceof Error ? err.message : String(err);
92
- throw new RenderError(msg, 'RENDER_ERROR');
79
+
80
+ /** Reject a bad `g` / `opts` before any work starts. */
81
+ function checkDrawOpsArgs(g: unknown, opts: unknown): void {
82
+ if (typeof g !== 'object' || g === null) {
83
+ throw invalidArgType('g', 'object', g);
84
+ }
85
+ if (opts !== undefined && (typeof opts !== 'object' || opts === null)) {
86
+ throw invalidArgType('opts', 'object or undefined', opts);
87
+ }
93
88
  }
94
89
 
95
90
  /** Append XdotOps found in `attrs` for each xdot draw key into `out`. */
@@ -118,7 +113,7 @@ function layoutAndRenderXdot(g: Graph, engine: EngineName): string {
118
113
  ctx.freeLayout(g, engine);
119
114
  return src;
120
115
  } catch (err: unknown) {
121
- rethrowAsRender(err);
116
+ return rethrowAtBoundary(err);
122
117
  }
123
118
  }
124
119
 
@@ -132,8 +127,13 @@ function layoutAndRenderXdot(g: Graph, engine: EngineName): string {
132
127
  * @param g - A Graph from `parse()` or the builder API.
133
128
  * @param opts - Optional: `{ engine }` overrides the default `'dot'`.
134
129
  * @returns Flat typed draw-op array covering the full graph.
135
- * @throws ParseError if the xdot DOT output cannot be re-parsed.
136
- * @throws RenderError if layout or rendering fails.
130
+ * @throws TypeError `ERR_INVALID_ARG_TYPE` if `g` is not an object or `opts`
131
+ * is neither undefined nor an object
132
+ * @throws TypeError `ERR_INVALID_ARG_VALUE` if `opts.engine` is not registered
133
+ * @throws RenderError `RENDER_ERROR`, `UNKNOWN_LAYOUT` or `UNSUPPORTED_FEATURE`
134
+ * if layout or rendering fails
135
+ * @throws ParseError if the xdot output cannot be re-parsed (a dot-engine bug)
136
+ * @throws InternalError `INTERNAL_ERROR` on any other dot-engine bug
137
137
  *
138
138
  * @example
139
139
  * ```ts
@@ -159,6 +159,7 @@ function layoutAndRenderXdot(g: Graph, engine: EngineName): string {
159
159
  * @see lib/gvc/gvc.h:gvRender
160
160
  */
161
161
  export function getDrawOps(g: Graph, opts?: DrawOpsOptions): XdotOp[] {
162
+ checkDrawOpsArgs(g, opts);
162
163
  const engine = opts?.engine ?? DEFAULT_DRAW_ENGINE;
163
164
  return collectGraphOps(parse(layoutAndRenderXdot(g, engine)));
164
165
  }
package/src/util/xml.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  * @see lib/util/xml.c
7
7
  */
8
8
 
9
+ import { RenderError } from '../errors.js';
10
+
9
11
  /** Options to tweak the behaviour of XML escaping. @see lib/util/xml.h:xml_flags_t */
10
12
  export interface XmlFlags {
11
13
  /** Escape & unconditionally; also escape \n and \r. */
@@ -125,45 +127,37 @@ class XmlEscaper {
125
127
  }
126
128
 
127
129
  /**
128
- * Decode a multi-byte UTF-8 sequence at s[pos] and emit &#xNNNN;.
130
+ * Emit &#xNNNN; for the Unicode code point at s[pos]. JS strings are
131
+ * UTF-16, so the code point (what C decodes from UTF-8) comes from
132
+ * codePointAt, and an astral character consumes two units.
129
133
  * @see lib/util/xml.c:xml_core (UTF-8 block)
130
134
  */
131
135
  static utf8Entity(s: string, pos: number): [string, number] {
132
- const b0 = s.charCodeAt(pos) & 0xff;
133
- const cp = XmlEscaper.decodeUtf8(s, pos, b0);
134
- const consumed = XmlEscaper.utf8ByteLen(b0);
136
+ const cp = XmlEscaper.decodeUtf8(s, pos);
137
+ const consumed = XmlEscaper.utf8ByteLen(cp);
135
138
  return [`&#x${cp.toString(16)};`, consumed];
136
139
  }
137
140
 
138
- /** Decode the codepoint value from a multi-byte UTF-8 sequence. */
139
- static decodeUtf8(s: string, pos: number, b0: number): number {
140
- if ((b0 >> 5) === 6) {
141
- return ((b0 & 0x1f) << 6) | (s.charCodeAt(pos + 1) & 0x3f);
142
- }
143
- if ((b0 >> 4) === 14) {
144
- return (
145
- ((b0 & 0x0f) << 12) |
146
- ((s.charCodeAt(pos + 1) & 0x3f) << 6) |
147
- (s.charCodeAt(pos + 2) & 0x3f)
148
- );
149
- }
150
- if ((b0 >> 3) === 30) {
151
- return (
152
- ((b0 & 0x07) << 18) |
153
- ((s.charCodeAt(pos + 1) & 0x3f) << 12) |
154
- ((s.charCodeAt(pos + 2) & 0x3f) << 6) |
155
- (s.charCodeAt(pos + 3) & 0x3f)
156
- );
141
+ /**
142
+ * Return the code point at s[pos]. A lone surrogate is the analogue of
143
+ * malformed UTF-8, where C reports an error and exits (xml.c:135).
144
+ * @see lib/util/xml.c:xml_core (fprintf + graphviz_exit at xml.c:135)
145
+ */
146
+ static decodeUtf8(s: string, pos: number): number {
147
+ const cp = s.codePointAt(pos)!; // caller: pos < s.length
148
+ if (cp >= 0xd800 && cp <= 0xdfff) {
149
+ throw new RenderError(`gvXmlEscape: malformed UTF-8 at position ${pos}`, 'RENDER_ERROR');
157
150
  }
158
- throw new Error(`gvXmlEscape: malformed UTF-8 at position ${pos}`);
151
+ return cp;
159
152
  }
160
153
 
161
- /** Return the byte-length of a UTF-8 sequence from its leading byte. */
162
- static utf8ByteLen(b0: number): number {
163
- if ((b0 >> 5) === 6) return 2;
164
- if ((b0 >> 4) === 14) return 3;
165
- if ((b0 >> 3) === 30) return 4;
166
- throw new Error(`gvXmlEscape: invalid UTF-8 leading byte 0x${b0.toString(16)}`);
154
+ /**
155
+ * Return how many UTF-16 units encode the code point (C: the UTF-8 byte
156
+ * length from the leading byte).
157
+ * @see lib/util/xml.c:xml_core (length computation)
158
+ */
159
+ static utf8ByteLen(cp: number): number {
160
+ return cp > 0xffff ? 2 : 1;
167
161
  }
168
162
 
169
163
  static isDecDigit(c: string): boolean {
@@ -13,6 +13,7 @@ import { Variable } from "./Variable.js";
13
13
  import { Constraint } from "./Constraint.js";
14
14
  import { Block } from "./Block.js";
15
15
  import { Blocks } from "./Blocks.js";
16
+ import { InternalError } from "../errors.js";
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // Rectangle
@@ -126,10 +127,11 @@ export class VPSC {
126
127
  /**
127
128
  * Throw if any constraint has slack < -1e-7.
128
129
  * Shared between satisfy() and refine().
130
+ * @see lib/vpsc/solve_VPSC.cpp:VPSC::satisfy (uncaught std::runtime_error)
129
131
  */
130
132
  private verifyConstraints(): void {
131
133
  for (const c of this.cs) {
132
- if (c.slack() < -0.0000001) throw new Error("Unsatisfied constraint");
134
+ if (c.slack() < -0.0000001) throw new InternalError("Unsatisfied constraint");
133
135
  }
134
136
  }
135
137
 
@@ -183,7 +185,7 @@ export class IncVPSC extends VPSC {
183
185
  if (lb !== rb) {
184
186
  lb.mergeTwoArg(rb, vc);
185
187
  } else {
186
- if (++splitCtr > 10000) throw new Error("Cycle Error!");
188
+ if (++splitCtr > 10000) throw new InternalError("Cycle Error!");
187
189
  const [splitC, newLb, newRb] = lb.splitBetween(vc.left, vc.right);
188
190
  this.inactive.push(splitC);
189
191
  newLb.mergeTwoArg(newRb, vc);
@@ -192,7 +194,7 @@ export class IncVPSC extends VPSC {
192
194
  }
193
195
  this.bs.cleanup();
194
196
  for (const c of this.cs) {
195
- if (c.slack() < -0.0000001) throw new Error("Unsatisfied constraint");
197
+ if (c.slack() < -0.0000001) throw new InternalError("Unsatisfied constraint");
196
198
  }
197
199
  }
198
200