@knowvah/dot-engine 1.9.0 → 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 (175) 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 +7560 -5446
  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/ports.d.ts +0 -10
  60. package/dist/layout/fdp/ports.d.ts.map +1 -1
  61. package/dist/layout/fdp/xlayout.d.ts +0 -16
  62. package/dist/layout/fdp/xlayout.d.ts.map +1 -1
  63. package/dist/layout/neato/adjust-info.d.ts +117 -0
  64. package/dist/layout/neato/adjust-info.d.ts.map +1 -0
  65. package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
  66. package/dist/layout/neato/constraint-adjust.d.ts +40 -0
  67. package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
  68. package/dist/layout/neato/edge-len.d.ts +20 -0
  69. package/dist/layout/neato/edge-len.d.ts.map +1 -0
  70. package/dist/layout/neato/fdp-adjust.d.ts +32 -4
  71. package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
  72. package/dist/layout/neato/index.d.ts +9 -6
  73. package/dist/layout/neato/index.d.ts.map +1 -1
  74. package/dist/layout/neato/init.d.ts +4 -11
  75. package/dist/layout/neato/init.d.ts.map +1 -1
  76. package/dist/layout/neato/kk-paths.d.ts +50 -0
  77. package/dist/layout/neato/kk-paths.d.ts.map +1 -0
  78. package/dist/layout/neato/kk-solve.d.ts +14 -0
  79. package/dist/layout/neato/kk-solve.d.ts.map +1 -0
  80. package/dist/layout/neato/kk.d.ts +44 -0
  81. package/dist/layout/neato/kk.d.ts.map +1 -0
  82. package/dist/layout/neato/multispline-router.d.ts.map +1 -1
  83. package/dist/layout/neato/poly.d.ts +50 -0
  84. package/dist/layout/neato/poly.d.ts.map +1 -0
  85. package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
  86. package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
  87. package/dist/layout/neato/sgd.d.ts +14 -9
  88. package/dist/layout/neato/sgd.d.ts.map +1 -1
  89. package/dist/layout/neato/start.d.ts +51 -0
  90. package/dist/layout/neato/start.d.ts.map +1 -0
  91. package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
  92. package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
  93. package/dist/layout/sfdp/index.d.ts.map +1 -1
  94. package/dist/layout/sfdp/init.d.ts +0 -9
  95. package/dist/layout/sfdp/init.d.ts.map +1 -1
  96. package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
  97. package/dist/layout/twopi/circle.d.ts +0 -7
  98. package/dist/layout/twopi/circle.d.ts.map +1 -1
  99. package/dist/ortho/ortho-parallel.d.ts.map +1 -1
  100. package/dist/ortho/trap-query.d.ts.map +1 -1
  101. package/dist/parser/index.d.ts +9 -5
  102. package/dist/parser/index.d.ts.map +1 -1
  103. package/dist/render/index.d.ts +2 -0
  104. package/dist/render/index.d.ts.map +1 -1
  105. package/dist/render/public.d.ts +9 -3
  106. package/dist/render/public.d.ts.map +1 -1
  107. package/dist/render/xdot-public.d.ts +7 -2
  108. package/dist/render/xdot-public.d.ts.map +1 -1
  109. package/dist/render.js +7355 -5476
  110. package/dist/render.js.map +4 -4
  111. package/dist/util/xml.d.ts.map +1 -1
  112. package/dist/vpsc/Solver.d.ts +1 -0
  113. package/dist/vpsc/Solver.d.ts.map +1 -1
  114. package/package.json +1 -1
  115. package/src/api/builder.ts +73 -5
  116. package/src/api/edge-ops.ts +19 -0
  117. package/src/api/geometry.ts +22 -5
  118. package/src/async/collect.ts +204 -0
  119. package/src/async/fonts.ts +61 -0
  120. package/src/async/render-async.ts +205 -0
  121. package/src/async/render-into.ts +115 -0
  122. package/src/async/sanitize.ts +150 -0
  123. package/src/common/htmltable-types.ts +4 -5
  124. package/src/common/make-label.ts +10 -1
  125. package/src/common/poly-shapes.ts +4 -1
  126. package/src/common/textmeasure-factory.ts +11 -0
  127. package/src/common/utils-inputscale.ts +31 -0
  128. package/src/errors.ts +188 -5
  129. package/src/gvc/context.ts +96 -17
  130. package/src/gvc/device.ts +11 -6
  131. package/src/gvc/image-resolver.ts +25 -2
  132. package/src/gvc/job.ts +3 -1
  133. package/src/gvc/usershape.ts +6 -0
  134. package/src/index.ts +59 -43
  135. package/src/label/index.ts +6 -2
  136. package/src/label/node.ts +2 -1
  137. package/src/label/rectangle.ts +4 -2
  138. package/src/layout/circo/circular.ts +9 -6
  139. package/src/layout/dot/pack-components.ts +6 -2
  140. package/src/layout/dot/position.ts +21 -3
  141. package/src/layout/fdp/derive.ts +7 -6
  142. package/src/layout/fdp/index.ts +45 -5
  143. package/src/layout/fdp/init.ts +7 -6
  144. package/src/layout/fdp/layout.ts +2 -1
  145. package/src/layout/fdp/ports.ts +3 -2
  146. package/src/layout/fdp/xlayout.ts +3 -1
  147. package/src/layout/neato/adjust-info.ts +332 -0
  148. package/src/layout/neato/cdt-surface.ts +50 -30
  149. package/src/layout/neato/constraint-adjust.ts +466 -0
  150. package/src/layout/neato/edge-len.ts +36 -0
  151. package/src/layout/neato/fdp-adjust.ts +117 -12
  152. package/src/layout/neato/index.ts +36 -54
  153. package/src/layout/neato/init.ts +21 -36
  154. package/src/layout/neato/kk-paths.ts +146 -0
  155. package/src/layout/neato/kk-solve.ts +64 -0
  156. package/src/layout/neato/kk.ts +322 -0
  157. package/src/layout/neato/multispline-router.ts +4 -3
  158. package/src/layout/neato/poly.ts +493 -0
  159. package/src/layout/neato/sgd-dijkstra.ts +125 -0
  160. package/src/layout/neato/sgd.ts +63 -40
  161. package/src/layout/neato/start.ts +222 -0
  162. package/src/layout/neato/vpsc-adjust.ts +93 -0
  163. package/src/layout/sfdp/index.ts +4 -5
  164. package/src/layout/sfdp/init.ts +40 -4
  165. package/src/layout/sfdp/spring-driver.ts +30 -1
  166. package/src/layout/twopi/circle.ts +2 -1
  167. package/src/ortho/ortho-parallel.ts +2 -1
  168. package/src/ortho/trap-query.ts +2 -1
  169. package/src/parser/index.ts +11 -11
  170. package/src/render/index.ts +5 -0
  171. package/src/render/public.ts +24 -24
  172. package/src/render/svg.ts +1 -1
  173. package/src/render/xdot-public.ts +19 -18
  174. package/src/util/xml.ts +24 -30
  175. package/src/vpsc/Solver.ts +5 -3
@@ -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