@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.
- package/README.md +232 -30
- package/dist/api/builder.d.ts +3 -0
- package/dist/api/builder.d.ts.map +1 -1
- package/dist/api/edge-ops.d.ts +7 -0
- package/dist/api/edge-ops.d.ts.map +1 -1
- package/dist/api/geometry.d.ts +5 -2
- package/dist/api/geometry.d.ts.map +1 -1
- package/dist/api.js +159 -31
- package/dist/api.js.map +3 -3
- package/dist/async/collect.d.ts +49 -0
- package/dist/async/collect.d.ts.map +1 -0
- package/dist/async/fonts.d.ts +20 -0
- package/dist/async/fonts.d.ts.map +1 -0
- package/dist/async/render-async.d.ts +91 -0
- package/dist/async/render-async.d.ts.map +1 -0
- package/dist/async/render-into.d.ts +38 -0
- package/dist/async/render-into.d.ts.map +1 -0
- package/dist/async/sanitize.d.ts +28 -0
- package/dist/async/sanitize.d.ts.map +1 -0
- package/dist/common/htmltable-types.d.ts +3 -3
- package/dist/common/htmltable-types.d.ts.map +1 -1
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/poly-shapes.d.ts.map +1 -1
- package/dist/common/textmeasure-factory.d.ts +2 -0
- package/dist/common/textmeasure-factory.d.ts.map +1 -1
- package/dist/common/utils-inputscale.d.ts +19 -0
- package/dist/common/utils-inputscale.d.ts.map +1 -0
- package/dist/errors.d.ts +61 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/gvc/context.d.ts +36 -4
- package/dist/gvc/context.d.ts.map +1 -1
- package/dist/gvc/device.d.ts +5 -2
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/image-resolver.d.ts +16 -16
- package/dist/gvc/image-resolver.d.ts.map +1 -1
- package/dist/gvc/job.d.ts +1 -9
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/gvc/usershape.d.ts +2 -12
- package/dist/gvc/usershape.d.ts.map +1 -1
- package/dist/index.d.ts +27 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7682 -5535
- package/dist/index.js.map +4 -4
- package/dist/label/index.d.ts.map +1 -1
- package/dist/label/node.d.ts +0 -6
- package/dist/label/node.d.ts.map +1 -1
- package/dist/label/rectangle.d.ts +1 -7
- package/dist/label/rectangle.d.ts.map +1 -1
- package/dist/layout/circo/circular.d.ts +8 -5
- package/dist/layout/circo/circular.d.ts.map +1 -1
- package/dist/layout/dot/pack-components.d.ts +0 -19
- package/dist/layout/dot/pack-components.d.ts.map +1 -1
- package/dist/layout/dot/position.d.ts +7 -2
- package/dist/layout/dot/position.d.ts.map +1 -1
- package/dist/layout/fdp/derive.d.ts.map +1 -1
- package/dist/layout/fdp/index.d.ts.map +1 -1
- package/dist/layout/fdp/init.d.ts.map +1 -1
- package/dist/layout/fdp/layout.d.ts.map +1 -1
- package/dist/layout/fdp/normalize.d.ts +2 -1
- package/dist/layout/fdp/normalize.d.ts.map +1 -1
- package/dist/layout/fdp/ports.d.ts +0 -10
- package/dist/layout/fdp/ports.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts +0 -16
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/layout/neato/adjust-info.d.ts +117 -0
- package/dist/layout/neato/adjust-info.d.ts.map +1 -0
- package/dist/layout/neato/cdt-surface.d.ts.map +1 -1
- package/dist/layout/neato/constraint-adjust.d.ts +40 -0
- package/dist/layout/neato/constraint-adjust.d.ts.map +1 -0
- package/dist/layout/neato/edge-len.d.ts +20 -0
- package/dist/layout/neato/edge-len.d.ts.map +1 -0
- package/dist/layout/neato/fdp-adjust.d.ts +32 -4
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- package/dist/layout/neato/index.d.ts +9 -6
- package/dist/layout/neato/index.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +4 -11
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk-paths.d.ts +50 -0
- package/dist/layout/neato/kk-paths.d.ts.map +1 -0
- package/dist/layout/neato/kk-solve.d.ts +14 -0
- package/dist/layout/neato/kk-solve.d.ts.map +1 -0
- package/dist/layout/neato/kk.d.ts +44 -0
- package/dist/layout/neato/kk.d.ts.map +1 -0
- package/dist/layout/neato/multispline-router.d.ts.map +1 -1
- package/dist/layout/neato/poly.d.ts +50 -0
- package/dist/layout/neato/poly.d.ts.map +1 -0
- package/dist/layout/neato/sc-adjust.d.ts +2 -9
- package/dist/layout/neato/sc-adjust.d.ts.map +1 -1
- package/dist/layout/neato/sgd-dijkstra.d.ts +20 -0
- package/dist/layout/neato/sgd-dijkstra.d.ts.map +1 -0
- package/dist/layout/neato/sgd.d.ts +14 -9
- package/dist/layout/neato/sgd.d.ts.map +1 -1
- package/dist/layout/neato/simple-scale.d.ts +15 -0
- package/dist/layout/neato/simple-scale.d.ts.map +1 -0
- package/dist/layout/neato/start.d.ts +51 -0
- package/dist/layout/neato/start.d.ts.map +1 -0
- package/dist/layout/neato/vpsc-adjust.d.ts +21 -0
- package/dist/layout/neato/vpsc-adjust.d.ts.map +1 -0
- package/dist/layout/sfdp/index.d.ts.map +1 -1
- package/dist/layout/sfdp/init.d.ts +0 -9
- package/dist/layout/sfdp/init.d.ts.map +1 -1
- package/dist/layout/sfdp/spring-driver.d.ts.map +1 -1
- package/dist/layout/twopi/circle.d.ts +0 -7
- package/dist/layout/twopi/circle.d.ts.map +1 -1
- package/dist/ortho/ortho-parallel.d.ts.map +1 -1
- package/dist/ortho/trap-query.d.ts.map +1 -1
- package/dist/parser/index.d.ts +9 -5
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/render/index.d.ts +2 -0
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/public.d.ts +9 -3
- package/dist/render/public.d.ts.map +1 -1
- package/dist/render/xdot-public.d.ts +7 -2
- package/dist/render/xdot-public.d.ts.map +1 -1
- package/dist/render.js +7468 -5556
- package/dist/render.js.map +4 -4
- package/dist/util/xml.d.ts.map +1 -1
- package/dist/vpsc/Solver.d.ts +1 -0
- package/dist/vpsc/Solver.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/builder.ts +73 -5
- package/src/api/edge-ops.ts +19 -0
- package/src/api/geometry.ts +22 -5
- package/src/async/collect.ts +204 -0
- package/src/async/fonts.ts +61 -0
- package/src/async/render-async.ts +205 -0
- package/src/async/render-into.ts +115 -0
- package/src/async/sanitize.ts +150 -0
- package/src/common/htmltable-types.ts +4 -5
- package/src/common/make-label.ts +10 -1
- package/src/common/poly-shapes.ts +4 -1
- package/src/common/textmeasure-factory.ts +11 -0
- package/src/common/utils-inputscale.ts +31 -0
- package/src/errors.ts +188 -5
- package/src/gvc/context.ts +96 -17
- package/src/gvc/device.ts +11 -6
- package/src/gvc/image-resolver.ts +25 -2
- package/src/gvc/job.ts +3 -1
- package/src/gvc/usershape.ts +6 -0
- package/src/index.ts +59 -43
- package/src/label/index.ts +6 -2
- package/src/label/node.ts +2 -1
- package/src/label/rectangle.ts +4 -2
- package/src/layout/circo/circular.ts +9 -6
- package/src/layout/dot/pack-components.ts +6 -2
- package/src/layout/dot/position.ts +21 -3
- package/src/layout/fdp/derive.ts +7 -6
- package/src/layout/fdp/index.ts +45 -5
- package/src/layout/fdp/init.ts +7 -6
- package/src/layout/fdp/layout.ts +2 -1
- package/src/layout/fdp/normalize.ts +12 -8
- package/src/layout/fdp/ports.ts +3 -2
- package/src/layout/fdp/xlayout.ts +8 -5
- package/src/layout/neato/adjust-info.ts +332 -0
- package/src/layout/neato/cdt-surface.ts +50 -30
- package/src/layout/neato/constraint-adjust.ts +466 -0
- package/src/layout/neato/edge-len.ts +36 -0
- package/src/layout/neato/fdp-adjust.ts +120 -12
- package/src/layout/neato/index.ts +36 -54
- package/src/layout/neato/init.ts +21 -36
- package/src/layout/neato/kk-paths.ts +146 -0
- package/src/layout/neato/kk-solve.ts +64 -0
- package/src/layout/neato/kk.ts +322 -0
- package/src/layout/neato/multispline-router.ts +4 -3
- package/src/layout/neato/poly.ts +493 -0
- package/src/layout/neato/sc-adjust.ts +2 -16
- package/src/layout/neato/sgd-dijkstra.ts +125 -0
- package/src/layout/neato/sgd.ts +63 -40
- package/src/layout/neato/simple-scale.ts +54 -0
- package/src/layout/neato/start.ts +222 -0
- package/src/layout/neato/vpsc-adjust.ts +93 -0
- package/src/layout/sfdp/index.ts +4 -5
- package/src/layout/sfdp/init.ts +40 -4
- package/src/layout/sfdp/spring-driver.ts +30 -1
- package/src/layout/twopi/circle.ts +2 -1
- package/src/ortho/ortho-parallel.ts +2 -1
- package/src/ortho/trap-query.ts +2 -1
- package/src/parser/index.ts +11 -11
- package/src/render/index.ts +5 -0
- package/src/render/public.ts +24 -24
- package/src/render/svg.ts +1 -1
- package/src/render/xdot-public.ts +19 -18
- package/src/util/xml.ts +24 -30
- package/src/vpsc/Solver.ts +5 -3
package/src/errors.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type GvErrorCode =
|
|
|
27
27
|
| 'EDGE_OP_UNDIRECTED_IN_DIRECTED' // '--' used in a digraph
|
|
28
28
|
| 'HTML_PARSE_ERROR' // HTML-like label parse failure
|
|
29
29
|
| 'RENDER_ERROR' // known layout/render-stage failure
|
|
30
|
+
| 'INTERNAL_ERROR' // dot-engine bug (assert / invariant / foreign throw)
|
|
31
|
+
| 'UNKNOWN_LAYOUT' // graph layout="..." names no registered engine
|
|
32
|
+
| 'UNSUPPORTED_FEATURE' // graph uses a Graphviz feature not yet ported
|
|
30
33
|
| 'GENERIC_ERROR'; // catch-all fallback
|
|
31
34
|
|
|
32
35
|
/** Structured error contract shared by every error source. */
|
|
@@ -66,6 +69,11 @@ export const FRIENDLY_MESSAGES: Record<GvErrorCode, string> = {
|
|
|
66
69
|
"An undirected edge '--' was used in a directed graph; use '->' instead.",
|
|
67
70
|
HTML_PARSE_ERROR: 'An HTML-like label could not be parsed.',
|
|
68
71
|
RENDER_ERROR: 'The graph could not be laid out or rendered.',
|
|
72
|
+
INTERNAL_ERROR:
|
|
73
|
+
'dot-engine hit an internal bug while processing the graph. Please report it with the DOT source that triggered it.',
|
|
74
|
+
UNKNOWN_LAYOUT: 'The graph names a layout engine that is not available.',
|
|
75
|
+
UNSUPPORTED_FEATURE:
|
|
76
|
+
'The graph uses a Graphviz feature that dot-engine does not support yet.',
|
|
69
77
|
GENERIC_ERROR: 'An unexpected error occurred while rendering the graph.',
|
|
70
78
|
};
|
|
71
79
|
|
|
@@ -77,19 +85,194 @@ export function friendlyMessageFor(code: GvErrorCode): string {
|
|
|
77
85
|
return FRIENDLY_MESSAGES[code];
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
/** Codes a {@link RenderError} types as `semantic` rather than `render`. */
|
|
89
|
+
const SEMANTIC_RENDER_CODES: ReadonlySet<GvErrorCode> = new Set<GvErrorCode>([
|
|
90
|
+
'UNKNOWN_LAYOUT',
|
|
91
|
+
'UNSUPPORTED_FEATURE',
|
|
92
|
+
]);
|
|
93
|
+
|
|
80
94
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
95
|
+
* Abstract base of every library-originated error: `instanceof
|
|
96
|
+
* DotEngineError` means dot-engine failed on this input (bad input, a fatal
|
|
97
|
+
* error Graphviz itself would report, or a dot-engine bug). Each subclass
|
|
98
|
+
* sets a hard-coded `name`.
|
|
83
99
|
*/
|
|
84
|
-
export class
|
|
100
|
+
export abstract class DotEngineError extends Error implements GvError {
|
|
101
|
+
abstract readonly type: GvErrorType;
|
|
102
|
+
abstract readonly code: GvErrorCode;
|
|
103
|
+
abstract readonly friendlyMessage: string;
|
|
104
|
+
|
|
105
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
106
|
+
super(message, options);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A dot-engine bug: failed assertion, broken invariant or foreign throw. */
|
|
111
|
+
export class InternalError extends DotEngineError {
|
|
85
112
|
readonly type = 'render';
|
|
113
|
+
readonly code = 'INTERNAL_ERROR';
|
|
114
|
+
readonly friendlyMessage = friendlyMessageFor('INTERNAL_ERROR');
|
|
115
|
+
|
|
116
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
117
|
+
super(message, options);
|
|
118
|
+
this.name = 'InternalError';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Error thrown for known layout/render-stage failures. `type` is `semantic`
|
|
124
|
+
* for `UNKNOWN_LAYOUT` / `UNSUPPORTED_FEATURE`, otherwise `render`.
|
|
125
|
+
*/
|
|
126
|
+
export class RenderError extends DotEngineError {
|
|
127
|
+
readonly type: GvErrorType;
|
|
86
128
|
readonly code: GvErrorCode;
|
|
87
129
|
readonly friendlyMessage: string;
|
|
88
130
|
|
|
89
|
-
constructor(
|
|
90
|
-
|
|
131
|
+
constructor(
|
|
132
|
+
message: string,
|
|
133
|
+
code: GvErrorCode = 'RENDER_ERROR',
|
|
134
|
+
options?: ErrorOptions,
|
|
135
|
+
) {
|
|
136
|
+
super(message, options);
|
|
91
137
|
this.name = 'RenderError';
|
|
92
138
|
this.code = code;
|
|
139
|
+
this.type = SEMANTIC_RENDER_CODES.has(code) ? 'semantic' : 'render';
|
|
93
140
|
this.friendlyMessage = friendlyMessageFor(code);
|
|
94
141
|
}
|
|
95
142
|
}
|
|
143
|
+
|
|
144
|
+
/** Guard shared by every boundary: structural check, works across bundles. */
|
|
145
|
+
export function isGvError(e: unknown): e is GvError {
|
|
146
|
+
if (typeof e !== 'object' || e === null) return false;
|
|
147
|
+
const c = e as { type?: unknown; code?: unknown };
|
|
148
|
+
return typeof c.type === 'string' && typeof c.code === 'string';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Usage errors (caller mistakes; not DotEngineError, not GvError) ─────────
|
|
152
|
+
|
|
153
|
+
/** Node-style codes carried by caller-mistake errors. */
|
|
154
|
+
export type UsageErrorCode =
|
|
155
|
+
| 'ERR_INVALID_ARG_TYPE'
|
|
156
|
+
| 'ERR_INVALID_ARG_VALUE'
|
|
157
|
+
| 'ERR_OUT_OF_RANGE'
|
|
158
|
+
| 'ERR_INVALID_STATE';
|
|
159
|
+
|
|
160
|
+
/** TypeError carrying a usage code; `name` stays `TypeError`. */
|
|
161
|
+
export class UsageTypeError extends TypeError {
|
|
162
|
+
readonly code: UsageErrorCode;
|
|
163
|
+
|
|
164
|
+
constructor(message: string, code: UsageErrorCode) {
|
|
165
|
+
super(message);
|
|
166
|
+
this.code = code;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** RangeError carrying a usage code; `name` stays `RangeError`. */
|
|
171
|
+
export class UsageRangeError extends RangeError {
|
|
172
|
+
readonly code: UsageErrorCode;
|
|
173
|
+
|
|
174
|
+
constructor(message: string, code: UsageErrorCode) {
|
|
175
|
+
super(message);
|
|
176
|
+
this.code = code;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Error carrying a usage code; `name` stays `Error`. */
|
|
181
|
+
export class UsageStateError extends Error {
|
|
182
|
+
readonly code: UsageErrorCode;
|
|
183
|
+
|
|
184
|
+
constructor(message: string, code: UsageErrorCode) {
|
|
185
|
+
super(message);
|
|
186
|
+
this.code = code;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Short description of a received value for usage-error messages. */
|
|
191
|
+
function describeReceived(v: unknown): string {
|
|
192
|
+
if (v === null || v === undefined) return String(v);
|
|
193
|
+
switch (typeof v) {
|
|
194
|
+
case 'string':
|
|
195
|
+
return `string ${JSON.stringify(v)}`;
|
|
196
|
+
case 'number':
|
|
197
|
+
case 'boolean':
|
|
198
|
+
case 'bigint':
|
|
199
|
+
return `${typeof v} ${String(v)}`;
|
|
200
|
+
case 'function':
|
|
201
|
+
return 'a function';
|
|
202
|
+
case 'symbol':
|
|
203
|
+
return 'a symbol';
|
|
204
|
+
default: {
|
|
205
|
+
const ctor = (v as { constructor?: { name?: unknown } }).constructor;
|
|
206
|
+
return typeof ctor?.name === 'string' && ctor.name !== ''
|
|
207
|
+
? `an instance of ${ctor.name}`
|
|
208
|
+
: 'an object';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Wrong type, `null` or a missing required argument. */
|
|
214
|
+
export function invalidArgType(
|
|
215
|
+
param: string,
|
|
216
|
+
expected: string,
|
|
217
|
+
actual: unknown,
|
|
218
|
+
): TypeError {
|
|
219
|
+
return new UsageTypeError(
|
|
220
|
+
`The "${param}" argument must be of type ${expected}. Received ${describeReceived(actual)}`,
|
|
221
|
+
'ERR_INVALID_ARG_TYPE',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Unknown engine/format name (or other enum-like value) as an argument. */
|
|
226
|
+
export function invalidArgValue(
|
|
227
|
+
param: string,
|
|
228
|
+
value: unknown,
|
|
229
|
+
allowed: readonly string[],
|
|
230
|
+
): TypeError {
|
|
231
|
+
return new UsageTypeError(
|
|
232
|
+
`The argument "${param}" is invalid. Received ${JSON.stringify(value)}; allowed: ${allowed.join(', ')}`,
|
|
233
|
+
'ERR_INVALID_ARG_VALUE',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Numeric argument outside its range. */
|
|
238
|
+
export function outOfRange(
|
|
239
|
+
param: string,
|
|
240
|
+
range: string,
|
|
241
|
+
value: number,
|
|
242
|
+
): RangeError {
|
|
243
|
+
return new UsageRangeError(
|
|
244
|
+
`The value of "${param}" is out of range. It must be ${range}. Received ${String(value)}`,
|
|
245
|
+
'ERR_OUT_OF_RANGE',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Call made in the wrong state (e.g. `getLayout` before layout). */
|
|
250
|
+
export function invalidState(message: string): Error {
|
|
251
|
+
return new UsageStateError(message, 'ERR_INVALID_STATE');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** True for errors built by the usage-error factories. */
|
|
255
|
+
export function isUsageError(e: unknown): boolean {
|
|
256
|
+
return (
|
|
257
|
+
e instanceof UsageTypeError ||
|
|
258
|
+
e instanceof UsageRangeError ||
|
|
259
|
+
e instanceof UsageStateError
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Public-boundary catch (shared by every entry point; not re-exported) ────
|
|
264
|
+
|
|
265
|
+
/** Message of any thrown value. */
|
|
266
|
+
export function messageOf(err: unknown): string {
|
|
267
|
+
return err instanceof Error ? err.message : String(err);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Boundary `catch` (ADR-5): usage errors and {@link GvError}s re-surface
|
|
272
|
+
* unchanged; anything else is a dot-engine bug and becomes an
|
|
273
|
+
* {@link InternalError} keeping the original as `cause`.
|
|
274
|
+
*/
|
|
275
|
+
export function rethrowAtBoundary(err: unknown): never {
|
|
276
|
+
if (isUsageError(err) || isGvError(err)) throw err;
|
|
277
|
+
throw new InternalError(messageOf(err), { cause: err });
|
|
278
|
+
}
|
package/src/gvc/context.ts
CHANGED
|
@@ -23,7 +23,10 @@ import type { Point, Box } from '../model/geom.js';
|
|
|
23
23
|
import type { TextSpan } from '../common/emit-types.js';
|
|
24
24
|
import type { TextMeasurer } from '../common/textmeasure.js';
|
|
25
25
|
import type { DebugOptions } from '../debug.js';
|
|
26
|
+
import type { ImageSizer } from '../common/htmltable-types.js';
|
|
27
|
+
import type { ImageResolver } from './image-resolver.js';
|
|
26
28
|
import type { RenderJob } from './job.js'; // scaffold in T25; full class in T26
|
|
29
|
+
import { RenderError, invalidArgType, invalidArgValue } from '../errors.js';
|
|
27
30
|
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
29
32
|
// Enums — match C definitions in lib/gvc/gvcjob.h exactly
|
|
@@ -165,6 +168,24 @@ class PluginRegistry {
|
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
/** Argument check for a TextMeasurer parameter (ADR-3). */
|
|
172
|
+
function checkMeasurer(m: unknown): void {
|
|
173
|
+
if (typeof m !== 'object' || m === null
|
|
174
|
+
|| typeof (m as { measure?: unknown }).measure !== 'function') {
|
|
175
|
+
throw invalidArgType('measurer', 'TextMeasurer', m);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Argument check for `register` (ADR-3): shape of a plugin, not its behaviour. */
|
|
180
|
+
function checkPlugin(p: unknown): void {
|
|
181
|
+
const o = p as { type?: unknown; quality?: unknown; layout?: unknown; cleanup?: unknown };
|
|
182
|
+
const ok = typeof p === 'object' && p !== null && typeof o.type === 'string'
|
|
183
|
+
&& ('quality' in o
|
|
184
|
+
? typeof o.quality === 'number'
|
|
185
|
+
: typeof o.layout === 'function' && typeof o.cleanup === 'function');
|
|
186
|
+
if (!ok) throw invalidArgType('p', 'RendererPlugin or LayoutEngine', p);
|
|
187
|
+
}
|
|
188
|
+
|
|
168
189
|
/**
|
|
169
190
|
* Root Graphviz context. Owns the plugin registry, text measurer, and
|
|
170
191
|
* the layout-engine dispatch.
|
|
@@ -180,15 +201,40 @@ export class GvcContext {
|
|
|
180
201
|
textMeasurer: TextMeasurer;
|
|
181
202
|
readonly debug: DebugOptions | undefined;
|
|
182
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Per-context HTML `<IMG>` sizer; takes precedence over the global
|
|
206
|
+
* `setImageSizer`. Undefined by default. @see ADR-2 (async-api)
|
|
207
|
+
*/
|
|
208
|
+
imageSizer?: ImageSizer;
|
|
209
|
+
/**
|
|
210
|
+
* Per-context image resolver for `inlineImages`; takes precedence over the
|
|
211
|
+
* global `setImageResolver`. Undefined by default. @see ADR-2 (async-api)
|
|
212
|
+
*/
|
|
213
|
+
imageResolver?: ImageResolver;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @throws TypeError `ERR_INVALID_ARG_TYPE` if `measurer` has no `measure`
|
|
217
|
+
* function or `options` is neither undefined nor an object
|
|
218
|
+
*/
|
|
183
219
|
constructor(measurer: TextMeasurer, options?: { debug?: DebugOptions }) {
|
|
220
|
+
checkMeasurer(measurer);
|
|
221
|
+
if (options !== undefined && (typeof options !== 'object' || options === null)) {
|
|
222
|
+
throw invalidArgType('options', 'object or undefined', options);
|
|
223
|
+
}
|
|
184
224
|
this.textMeasurer = measurer;
|
|
185
225
|
this.debug = options?.debug;
|
|
186
226
|
}
|
|
187
227
|
|
|
188
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Register a renderer or layout engine; overload discriminated by `quality`.
|
|
230
|
+
* @throws TypeError `ERR_INVALID_ARG_TYPE` if `p` is not a renderer plugin
|
|
231
|
+
* (string `type`, numeric `quality`) or a layout engine (string `type`,
|
|
232
|
+
* `layout` and `cleanup` functions)
|
|
233
|
+
*/
|
|
189
234
|
register(p: RendererPlugin): void;
|
|
190
235
|
register(p: LayoutEngine): void;
|
|
191
236
|
register(p: RendererPlugin | LayoutEngine): void {
|
|
237
|
+
checkPlugin(p);
|
|
192
238
|
if ('quality' in p) {
|
|
193
239
|
const idx = PluginRegistry.insertionIdx(this.renderers, p);
|
|
194
240
|
this.renderers.splice(idx, 0, p);
|
|
@@ -202,14 +248,35 @@ export class GvcContext {
|
|
|
202
248
|
* Because renderers are sorted quality-descending within a prefix, the first
|
|
203
249
|
* match is always the highest-quality one (last-registered wins on tie).
|
|
204
250
|
*
|
|
205
|
-
* @throws
|
|
251
|
+
* @throws TypeError (ERR_INVALID_ARG_TYPE) if format is not a string
|
|
252
|
+
* @throws TypeError (ERR_INVALID_ARG_VALUE) if no renderer is registered
|
|
253
|
+
* for format
|
|
206
254
|
* @see lib/gvc/gvplugin.c:gvplugin_find
|
|
207
255
|
*/
|
|
208
256
|
bestRenderer(format: string): RendererPlugin {
|
|
257
|
+
if (typeof format !== 'string') {
|
|
258
|
+
throw invalidArgType('format', 'string', format);
|
|
259
|
+
}
|
|
209
260
|
for (const r of this.renderers) {
|
|
210
261
|
if (r.type.split(':')[0] === format) return r;
|
|
211
262
|
}
|
|
212
|
-
|
|
263
|
+
const prefixes = new Set(this.renderers.map((r) => r.type.split(':')[0]!));
|
|
264
|
+
throw invalidArgValue('format', format, [...prefixes]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Validate the `(g, engineName)` arguments shared by layout/freeLayout. */
|
|
268
|
+
private checkLayoutArgs(g: Graph, engineName: EngineName): LayoutEngine {
|
|
269
|
+
if (typeof g !== 'object' || g === null) {
|
|
270
|
+
throw invalidArgType('g', 'object', g);
|
|
271
|
+
}
|
|
272
|
+
if (typeof engineName !== 'string') {
|
|
273
|
+
throw invalidArgType('engine', 'string', engineName);
|
|
274
|
+
}
|
|
275
|
+
const engine = this.layouts.get(engineName);
|
|
276
|
+
if (engine === undefined) {
|
|
277
|
+
throw invalidArgValue('engine', engineName, [...this.layouts.keys()]);
|
|
278
|
+
}
|
|
279
|
+
return engine;
|
|
213
280
|
}
|
|
214
281
|
|
|
215
282
|
/**
|
|
@@ -218,27 +285,37 @@ export class GvcContext {
|
|
|
218
285
|
* rendering happens in between and must see the layout state
|
|
219
286
|
* (e.g. cluster arrays).
|
|
220
287
|
*
|
|
221
|
-
* @throws
|
|
288
|
+
* @throws TypeError (ERR_INVALID_ARG_TYPE / ERR_INVALID_ARG_VALUE) for a
|
|
289
|
+
* bad `g`, or an engine argument that is not registered
|
|
290
|
+
* @throws RenderError (UNKNOWN_LAYOUT) if the `layout` attribute names no
|
|
291
|
+
* registered engine
|
|
292
|
+
* @throws RenderError (RENDER_ERROR / UNSUPPORTED_FEATURE) or InternalError
|
|
293
|
+
* from the engine itself, unwrapped; a foreign throw from an engine bug
|
|
294
|
+
* also propagates unwrapped (only the public render functions wrap it)
|
|
222
295
|
* @see lib/gvc/gvlayout.c:gvLayoutJobs
|
|
223
296
|
*/
|
|
224
297
|
layout(g: Graph, engineName: EngineName): void {
|
|
298
|
+
const selected = this.checkLayoutArgs(g, engineName);
|
|
225
299
|
// C gvLayoutJobs: the graph's `layout` ATTRIBUTE unconditionally
|
|
226
300
|
// overrides the selected engine (-K / API choice); an unrecognized
|
|
227
301
|
// value is an error, not a fallback. @see lib/gvc/gvlayout.c:66-73
|
|
228
302
|
const attr = g.attrs?.get('layout'); // test doubles may lack attrs
|
|
229
|
-
let
|
|
303
|
+
let engine = selected;
|
|
230
304
|
if (attr !== undefined && attr !== '') {
|
|
231
|
-
|
|
232
|
-
|
|
305
|
+
const override = this.layouts.get(attr);
|
|
306
|
+
if (override === undefined) {
|
|
307
|
+
throw new RenderError(
|
|
308
|
+
`Layout type: "${attr}" not recognized`,
|
|
309
|
+
'UNKNOWN_LAYOUT',
|
|
310
|
+
);
|
|
233
311
|
}
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
const engine = this.layouts.get(name);
|
|
237
|
-
if (engine === undefined) {
|
|
238
|
-
throw new Error(`no layout engine registered: ${name}`);
|
|
312
|
+
engine = override;
|
|
239
313
|
}
|
|
240
314
|
if (g.info) g.info.gvc = this;
|
|
241
315
|
engine.layout(g);
|
|
316
|
+
// C: GD_cleanup(g) = gvle->cleanup — freeLayout must use the engine that
|
|
317
|
+
// actually ran, not its argument. @see lib/gvc/gvlayout.c:88
|
|
318
|
+
if (g.info) g.info.cleanup = (x: Graph): void => engine.cleanup(x);
|
|
242
319
|
// Mark the graph laid-out so the public getLayout can reject a graph that
|
|
243
320
|
// still carries calloc-zero geometry defaults.
|
|
244
321
|
if (g.info) g.info.laidOut = true;
|
|
@@ -247,14 +324,16 @@ export class GvcContext {
|
|
|
247
324
|
/**
|
|
248
325
|
* Release engine layout state after rendering.
|
|
249
326
|
*
|
|
250
|
-
* @throws
|
|
327
|
+
* @throws TypeError (ERR_INVALID_ARG_TYPE / ERR_INVALID_ARG_VALUE) for a
|
|
328
|
+
* bad `g`, or an engine argument that is not registered
|
|
251
329
|
* @see lib/gvc/gvlayout.c:gvFreeLayout
|
|
252
330
|
*/
|
|
253
331
|
freeLayout(g: Graph, engineName: EngineName): void {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
332
|
+
this.checkLayoutArgs(g, engineName); // argument validation only
|
|
333
|
+
const cleanup = g.info?.cleanup;
|
|
334
|
+
if (cleanup) {
|
|
335
|
+
cleanup(g);
|
|
336
|
+
g.info.cleanup = undefined;
|
|
257
337
|
}
|
|
258
|
-
engine.cleanup(g);
|
|
259
338
|
}
|
|
260
339
|
}
|
package/src/gvc/device.ts
CHANGED
|
@@ -18,8 +18,9 @@ import { parseDrawingSize, initJobViewportZoom, parseLandscape, parseGraphPad, p
|
|
|
18
18
|
import type { Graph } from '../model/graph.js';
|
|
19
19
|
import type { Node } from '../model/node.js';
|
|
20
20
|
import type { Edge } from '../model/edge.js';
|
|
21
|
-
import type { RendererPlugin
|
|
22
|
-
import { PenType } from './context.js';
|
|
21
|
+
import type { RendererPlugin } from './context.js';
|
|
22
|
+
import { GvcContext, PenType } from './context.js';
|
|
23
|
+
import { invalidArgType } from '../errors.js';
|
|
23
24
|
import { gvrenderTextspan, withLabelEmitState } from './textspan-emit.js';
|
|
24
25
|
import { resolveEdgeAnchor, resolveObjAnchor, beginAnchorIf } from './anchor.js';
|
|
25
26
|
import type { ShapeDesc, TextlabelT } from '../common/types.js';
|
|
@@ -57,9 +58,8 @@ import { svgNodeId, svgEdgeId, svgClusterId, svgGraphId } from '../render/svg-id
|
|
|
57
58
|
// ---------------------------------------------------------------------------
|
|
58
59
|
// AD-1 (image-api): `inlineImages` on RenderJob via module augmentation
|
|
59
60
|
//
|
|
60
|
-
// Declared here (not
|
|
61
|
-
//
|
|
62
|
-
// (svg.ts usershape()). Purely additive: an unset field reads `undefined`,
|
|
61
|
+
// Declared here (not in job.ts), beside render() which sets it; svg.ts
|
|
62
|
+
// usershape() reads it. Purely additive: an unset field reads `undefined`,
|
|
63
63
|
// which is falsy, so any RenderJob built without going through this render()
|
|
64
64
|
// (tests, other call sites) keeps today's raw-src passthrough unchanged.
|
|
65
65
|
// @see src/render/public.ts:RenderOptions.inlineImages
|
|
@@ -536,13 +536,18 @@ function renderPage(g: Graph, renderer: RendererPlugin, job: RenderJob, info: La
|
|
|
536
536
|
* (`setImageResolver`) and inlines a `data:` URI on a hit instead of the
|
|
537
537
|
* raw src passthrough. Unset/false reproduces byte-identical pre-AD-1
|
|
538
538
|
* output. @see src/render/public.ts:RenderOptions.inlineImages
|
|
539
|
-
* @throws
|
|
539
|
+
* @throws TypeError `ERR_INVALID_ARG_TYPE` if `ctx` is not a GvcContext, `g`
|
|
540
|
+
* is not an object, or `format` is not a string
|
|
541
|
+
* @throws TypeError `ERR_INVALID_ARG_VALUE` if no renderer is registered for format
|
|
540
542
|
* @see lib/gvc/gvrender.c:gvrender_select
|
|
541
543
|
*/
|
|
542
544
|
export function render(ctx: GvcContext, g: Graph, format: string, inlineImages = false): string {
|
|
545
|
+
if (!(ctx instanceof GvcContext)) throw invalidArgType('ctx', 'GvcContext', ctx);
|
|
546
|
+
if (typeof g !== 'object' || g === null) throw invalidArgType('g', 'object', g);
|
|
543
547
|
const renderer = ctx.bestRenderer(format);
|
|
544
548
|
const job = new RenderJob(format, ctx.textMeasurer);
|
|
545
549
|
job.inlineImages = inlineImages;
|
|
550
|
+
if (ctx.imageResolver !== undefined) job.imageResolver = ctx.imageResolver;
|
|
546
551
|
// gvc->bb = GD_bb(g) verbatim -- no recompute fallback. Every layout engine
|
|
547
552
|
// sets g.info.bb itself before render() runs (set_aspect for dot,
|
|
548
553
|
// compute_bb-equivalent computeSubgraphBB calls in neato/circo/sfdp/fdp/
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* @see lib/gvc/gvusershape.c (ImageDict, gvusershape_find/size)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { invalidArgType } from '../errors.js';
|
|
20
|
+
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
// Resolver registry
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
@@ -29,14 +31,30 @@ export type ImageResolver = (
|
|
|
29
31
|
src: string,
|
|
30
32
|
) => { bytes: Uint8Array; mime?: string } | Uint8Array | null;
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Per-render resolver on the render job (async-api ADR-2): device.ts render()
|
|
36
|
+
* copies `GvcContext.imageResolver` here; svg.ts usershape() passes it to
|
|
37
|
+
* findImageBytes, which then skips the global resolver.
|
|
38
|
+
*/
|
|
39
|
+
declare module './job.js' {
|
|
40
|
+
interface RenderJob {
|
|
41
|
+
imageResolver?: ImageResolver;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
let activeResolver: ImageResolver | null = null;
|
|
33
46
|
|
|
34
47
|
/**
|
|
35
48
|
* Register (or clear, with null) the global image resolver consulted when
|
|
36
49
|
* `RenderOptions.inlineImages` is set. Mirrors gvusershape's process-global
|
|
37
50
|
* dictionary and `setImageSizer`'s registration shape.
|
|
51
|
+
* @throws TypeError `ERR_INVALID_ARG_TYPE` if `fn` is neither a function nor
|
|
52
|
+
* null
|
|
38
53
|
*/
|
|
39
54
|
export function setImageResolver(fn: ImageResolver | null): void {
|
|
55
|
+
if (fn !== null && typeof fn !== 'function') {
|
|
56
|
+
throw invalidArgType('resolver', 'function or null', fn);
|
|
57
|
+
}
|
|
40
58
|
activeResolver = fn;
|
|
41
59
|
}
|
|
42
60
|
|
|
@@ -73,12 +91,17 @@ function inferMimeFromSrc(src: string): string {
|
|
|
73
91
|
* when the resolver omits it). Returns `null` when no resolver is set or the
|
|
74
92
|
* resolver itself returns `null` — the graceful-miss path that keeps the raw
|
|
75
93
|
* `src` passthrough in `usershape()`.
|
|
94
|
+
*
|
|
95
|
+
* @param resolver - per-context resolver (ADR-2); when given it is consulted
|
|
96
|
+
* instead of the global one, with the same normalization.
|
|
76
97
|
*/
|
|
77
98
|
export function findImageBytes(
|
|
78
99
|
src: string,
|
|
100
|
+
resolver?: ImageResolver,
|
|
79
101
|
): { bytes: Uint8Array; mime: string } | null {
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
const active = resolver ?? activeResolver;
|
|
103
|
+
if (active === null) return null;
|
|
104
|
+
const result = active(src);
|
|
82
105
|
if (result === null) return null;
|
|
83
106
|
if (result instanceof Uint8Array) {
|
|
84
107
|
return { bytes: result, mime: inferMimeFromSrc(src) };
|
package/src/gvc/job.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* @see lib/gvc/gvdevice.c:gvprintdouble
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { InternalError } from '../errors.js';
|
|
13
14
|
import type { Graph } from '../model/graph.js';
|
|
14
15
|
import { FontnameKind } from '../model/layoutParams.js';
|
|
15
16
|
import type { Node } from '../model/node.js';
|
|
@@ -361,11 +362,12 @@ export class RenderJob {
|
|
|
361
362
|
|
|
362
363
|
/**
|
|
363
364
|
* Pop the top object state from the stack.
|
|
365
|
+
* @see lib/common/emit.c:pop_obj_state (assert(obj) at emit.c:135)
|
|
364
366
|
* @throws Error if the stack is empty.
|
|
365
367
|
*/
|
|
366
368
|
popObj(): void {
|
|
367
369
|
if (this.objStack.length === 0) {
|
|
368
|
-
throw new
|
|
370
|
+
throw new InternalError('RenderJob.popObj: stack is empty');
|
|
369
371
|
}
|
|
370
372
|
this.objStack.pop();
|
|
371
373
|
}
|
package/src/gvc/usershape.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* @see lib/gvc/gvusershape.c (ImageDict, gvusershape_find/size)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { invalidArgType } from '../errors.js';
|
|
16
17
|
import type { ImageSizer } from '../common/htmltable-types.js';
|
|
17
18
|
|
|
18
19
|
export type { ImageSizer } from '../common/htmltable-types.js';
|
|
@@ -22,8 +23,13 @@ let activeSizer: ImageSizer | null = null;
|
|
|
22
23
|
/**
|
|
23
24
|
* Register (or clear, with null) the global image sizer consulted by
|
|
24
25
|
* HTML <IMG> sizing. Mirrors gvusershape's process-global dictionary.
|
|
26
|
+
* @throws TypeError `ERR_INVALID_ARG_TYPE` if `sizer` is neither a function
|
|
27
|
+
* nor null
|
|
25
28
|
*/
|
|
26
29
|
export function setImageSizer(sizer: ImageSizer | null): void {
|
|
30
|
+
if (sizer !== null && typeof sizer !== 'function') {
|
|
31
|
+
throw invalidArgType('sizer', 'function or null', sizer);
|
|
32
|
+
}
|
|
27
33
|
activeSizer = sizer;
|
|
28
34
|
}
|
|
29
35
|
|