@knowvah/dot-engine 2.7.1 → 2.8.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.
- package/dist/api.js.map +2 -2
- package/dist/common/agerr.d.ts +8 -1
- package/dist/common/agerr.d.ts.map +1 -1
- package/dist/common/fma.d.ts.map +1 -1
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-label.d.ts +3 -0
- package/dist/common/htmltable-label.d.ts.map +1 -1
- package/dist/common/htmltable-lex-attvalue.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +5 -0
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/layers.d.ts +12 -0
- package/dist/common/layers.d.ts.map +1 -1
- package/dist/common/layout-state.d.ts +8 -0
- package/dist/common/layout-state.d.ts.map +1 -0
- package/dist/common/make-label.d.ts.map +1 -1
- package/dist/common/multicolor.d.ts +6 -3
- package/dist/common/multicolor.d.ts.map +1 -1
- package/dist/common/nodeinit.d.ts.map +1 -1
- package/dist/common/poly-init.d.ts +11 -0
- package/dist/common/poly-init.d.ts.map +1 -1
- package/dist/common/poly-sizing.d.ts +1 -0
- package/dist/common/poly-sizing.d.ts.map +1 -1
- package/dist/common/postproc.d.ts.map +1 -1
- package/dist/common/style-resolve.d.ts +11 -9
- package/dist/common/style-resolve.d.ts.map +1 -1
- package/dist/gvc/device-cluster.d.ts +5 -6
- package/dist/gvc/device-cluster.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/first-layer.d.ts +21 -5
- package/dist/gvc/first-layer.d.ts.map +1 -1
- package/dist/gvc/gvrender-set-style.d.ts.map +1 -1
- package/dist/gvc/usershape.d.ts +20 -0
- package/dist/gvc/usershape.d.ts.map +1 -1
- package/dist/index.js +494 -275
- package/dist/index.js.map +4 -4
- package/dist/layout/circo/index.d.ts.map +1 -1
- package/dist/layout/dot/init.d.ts +10 -0
- package/dist/layout/dot/init.d.ts.map +1 -1
- package/dist/layout/dot/splines.d.ts.map +1 -1
- package/dist/layout/fdp/init.d.ts.map +1 -1
- package/dist/layout/neato/conjgrad-mkernel.d.ts.map +1 -1
- package/dist/layout/neato/constrained-majorization.d.ts.map +1 -1
- package/dist/layout/neato/edge-len.d.ts +6 -2
- package/dist/layout/neato/edge-len.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk.d.ts.map +1 -1
- package/dist/layout/neato/make-graph-data.d.ts.map +1 -1
- package/dist/layout/neato/matrix-ops.d.ts +0 -6
- package/dist/layout/neato/matrix-ops.d.ts.map +1 -1
- package/dist/layout/neato/smart-ini-x.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts.map +1 -1
- package/dist/layout/neato/stress-kernel.d.ts +2 -5
- package/dist/layout/neato/stress-kernel.d.ts.map +1 -1
- package/dist/layout/neato/stress.d.ts +6 -0
- package/dist/layout/neato/stress.d.ts.map +1 -1
- package/dist/layout/osage/index.d.ts.map +1 -1
- package/dist/layout/patchwork/index.d.ts.map +1 -1
- package/dist/layout/sfdp/init.d.ts.map +1 -1
- package/dist/layout/twopi/init.d.ts +12 -6
- package/dist/layout/twopi/init.d.ts.map +1 -1
- package/dist/model/cgraph-ops.d.ts +9 -0
- package/dist/model/cgraph-ops.d.ts.map +1 -1
- package/dist/render/color-resolve.d.ts +7 -0
- package/dist/render/color-resolve.d.ts.map +1 -1
- package/dist/render/json.d.ts +17 -2
- package/dist/render/json.d.ts.map +1 -1
- package/dist/render/map.d.ts +3 -5
- package/dist/render/map.d.ts.map +1 -1
- package/dist/render.js +500 -281
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/agerr.ts +69 -8
- package/src/common/fma.ts +11 -1
- package/src/common/htmltable-emit-fill.ts +11 -7
- package/src/common/htmltable-label.ts +6 -3
- package/src/common/htmltable-lex-attvalue.ts +17 -1
- package/src/common/htmltable.ts +10 -5
- package/src/common/layers.ts +31 -2
- package/src/common/layout-state.ts +35 -0
- package/src/common/make-label.ts +6 -2
- package/src/common/multicolor.ts +12 -6
- package/src/common/nodeinit.ts +2 -1
- package/src/common/poly-init.ts +18 -1
- package/src/common/poly-sizing.ts +4 -4
- package/src/common/postproc.ts +4 -1
- package/src/common/style-resolve.ts +118 -50
- package/src/gvc/device-cluster.ts +31 -39
- package/src/gvc/device.ts +55 -43
- package/src/gvc/first-layer.ts +60 -11
- package/src/gvc/gvrender-set-style.ts +1 -6
- package/src/gvc/usershape.ts +40 -0
- package/src/layout/circo/index.ts +4 -0
- package/src/layout/dot/init.ts +32 -0
- package/src/layout/dot/splines.ts +3 -0
- package/src/layout/fdp/init.ts +17 -0
- package/src/layout/neato/conjgrad-mkernel.ts +2 -3
- package/src/layout/neato/constrained-majorization.ts +2 -1
- package/src/layout/neato/edge-len.ts +39 -7
- package/src/layout/neato/init.ts +8 -3
- package/src/layout/neato/kk.ts +2 -1
- package/src/layout/neato/make-graph-data.ts +1 -2
- package/src/layout/neato/matrix-ops.ts +1 -11
- package/src/layout/neato/smart-ini-x.ts +1 -1
- package/src/layout/neato/splines.ts +3 -1
- package/src/layout/neato/start.ts +3 -2
- package/src/layout/neato/stress-kernel.ts +45 -86
- package/src/layout/neato/stress.ts +19 -0
- package/src/layout/osage/index.ts +8 -1
- package/src/layout/patchwork/index.ts +4 -0
- package/src/layout/sfdp/init.ts +3 -0
- package/src/layout/twopi/init.ts +21 -9
- package/src/layout/twopi/pipeline.ts +3 -3
- package/src/model/cgraph-ops.ts +19 -0
- package/src/render/color-resolve.ts +17 -2
- package/src/render/json.ts +9 -10
- package/src/render/map.ts +5 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphviz",
|
package/src/common/agerr.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* The one stderr writer for messages C prints through cgraph's agerr family
|
|
5
5
|
* (`agerr`, `agerrorf`, `agwarningf`, `AGPREV` continuations). Sites whose C
|
|
6
6
|
* counterpart is `fprintf(stderr)`/`fputs`/`printf` do not use it.
|
|
7
|
+
*
|
|
8
|
+
* Partial lines (AD-7): the port strips C's trailing `\n` at every call site,
|
|
9
|
+
* so `agerr`/`agerrorf`/`agwarningf` always finish a line. A site whose C
|
|
10
|
+
* message has no trailing `\n` (`emit.c:parseSegs`) opts in with
|
|
11
|
+
* {@link agerrPartial}: the text is held, and the next `agerr*` message is
|
|
12
|
+
* appended to it (its own prefix included, as C's `out` writes it) and the
|
|
13
|
+
* joined line printed on the held line's channel. A held line is flushed on
|
|
14
|
+
* its own by {@link agerrLineOn}/{@link agerrFinished} and by render-state
|
|
15
|
+
* reset.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
import { registerRenderState } from './render-state.js';
|
|
@@ -30,7 +39,14 @@ const OCTAL_RADIX = 8;
|
|
|
30
39
|
*/
|
|
31
40
|
let lastLevel: 'error' | 'warning' = 'warning';
|
|
32
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The held partial line and its channel (one slot: stderr has one cursor).
|
|
44
|
+
* Cleared by {@link flushPending}; reset per render.
|
|
45
|
+
*/
|
|
46
|
+
const pending: { line: string | null; channel: AgerrChannel } = { line: null, channel: 'warn' };
|
|
47
|
+
|
|
33
48
|
registerRenderState(() => {
|
|
49
|
+
flushPending();
|
|
34
50
|
lastLevel = 'warning';
|
|
35
51
|
});
|
|
36
52
|
|
|
@@ -55,25 +71,70 @@ export function escapeControl(message: string): string {
|
|
|
55
71
|
* channel, escaped as `default_usererrf` would.
|
|
56
72
|
*/
|
|
57
73
|
export function agerrLineOn(channel: AgerrChannel, line: string): void {
|
|
74
|
+
flushPending();
|
|
75
|
+
writeLine(channel, line);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Print a held partial line, if any, as its own line. */
|
|
79
|
+
function flushPending(): void {
|
|
80
|
+
if (pending.line === null) return;
|
|
81
|
+
const { line, channel } = pending;
|
|
82
|
+
pending.line = null;
|
|
83
|
+
writeLine(channel, line);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeLine(channel: AgerrChannel, line: string): void {
|
|
58
87
|
if (channel === 'warn') console.warn(escapeControl(line));
|
|
59
88
|
else console.error(escapeControl(line));
|
|
60
89
|
}
|
|
61
90
|
|
|
91
|
+
/** The text `out` writes for one message: prefix (none for AGPREV) + body. */
|
|
92
|
+
function assemble(level: AgerrLevel, msg: string): string {
|
|
93
|
+
if (level === 'prev') return msg;
|
|
94
|
+
const prefix = level === 'error' ? PREFIX_ERROR : PREFIX_WARNING;
|
|
95
|
+
return `${prefix}${PREFIX_SEP}${msg}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Console channel of a message; AGPREV continues the previous level's. */
|
|
99
|
+
function channelOf(level: AgerrLevel): AgerrChannel {
|
|
100
|
+
const eff = level === 'prev' ? lastLevel : level;
|
|
101
|
+
return eff === 'warning' ? 'warn' : 'error';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Shared body of `agerr`/`agerrPartial`: record the level, assemble the text,
|
|
106
|
+
* join it onto a held line (which fixes the channel), then hold or print.
|
|
107
|
+
*/
|
|
108
|
+
function emit(level: AgerrLevel, msg: string, channel: AgerrChannel | undefined, hold: boolean): void {
|
|
109
|
+
const target = channel ?? channelOf(level);
|
|
110
|
+
if (level !== 'prev') lastLevel = level;
|
|
111
|
+
const text = assemble(level, msg);
|
|
112
|
+
if (pending.line === null) {
|
|
113
|
+
pending.channel = target;
|
|
114
|
+
pending.line = text;
|
|
115
|
+
} else pending.line += text;
|
|
116
|
+
if (!hold) flushPending();
|
|
117
|
+
}
|
|
118
|
+
|
|
62
119
|
/**
|
|
63
120
|
* The part of C's `agerr(level, ...)` that is not formatting: the `Error: `/
|
|
64
121
|
* `Warning: ` prefix (none for AGPREV), escaping, and the console channel
|
|
65
122
|
* (`console.warn` for warnings, `console.error` otherwise; AGPREV continues
|
|
66
|
-
* the previous level's channel).
|
|
123
|
+
* the previous level's channel). Finishes the line, joining a held partial.
|
|
67
124
|
* @see lib/cgraph/agerror.c:out
|
|
68
125
|
*/
|
|
69
126
|
export function agerr(level: AgerrLevel, msg: string, channel?: AgerrChannel): void {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
127
|
+
emit(level, msg, channel, false);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `agerr` for a C message with no trailing `\n`: the line stays open and the
|
|
132
|
+
* next `agerr*` message continues it. The channel follows this (first)
|
|
133
|
+
* message's level unless `channel` is given.
|
|
134
|
+
* @see lib/cgraph/agerror.c:out
|
|
135
|
+
*/
|
|
136
|
+
export function agerrPartial(level: AgerrLevel, msg: string, channel?: AgerrChannel): void {
|
|
137
|
+
emit(level, msg, channel, true);
|
|
77
138
|
}
|
|
78
139
|
|
|
79
140
|
/** @see lib/cgraph/agerror.c:agerrorf */
|
package/src/common/fma.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* half-ULP boundary of s — not observed for any supported input
|
|
14
14
|
* (validated against a full-precision C oracle).
|
|
15
15
|
*
|
|
16
|
+
* A non-finite product or addend short-circuits to `a * b + c` (or the
|
|
17
|
+
* non-finite `c` when a and b are finite) before the split: the split's `a·b - p` terms turn an
|
|
18
|
+
* infinite operand into Inf - Inf = NaN where hardware fmadd gives ±Inf.
|
|
19
|
+
*
|
|
16
20
|
* Per Boldo & Muller, "Some functions computable with a fused-mac"
|
|
17
21
|
* (IEEE Trans. Computers, 2011) — the exact emulation needs
|
|
18
22
|
* round-to-odd; this is the standard approximation without it.
|
|
@@ -25,8 +29,14 @@ const SPLIT = 134217729;
|
|
|
25
29
|
* round(a·b + c) with one rounding (fmadd).
|
|
26
30
|
*/
|
|
27
31
|
export function fma(a: number, b: number, c: number): number {
|
|
28
|
-
/* Dekker two-product: p + err == a·b exactly */
|
|
29
32
|
const p = a * b;
|
|
33
|
+
/* Non-finite product or addend: the IEEE fmadd result is p + c, except
|
|
34
|
+
* that a finite a·b which merely overflowed p yields the non-finite c. */
|
|
35
|
+
if (!Number.isFinite(p) || !Number.isFinite(c)) {
|
|
36
|
+
return Number.isFinite(a) && Number.isFinite(b) && !Number.isFinite(c) ? c : p + c;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Dekker two-product: p + err == a·b exactly */
|
|
30
40
|
const at = SPLIT * a;
|
|
31
41
|
const ahi = at - (at - a);
|
|
32
42
|
const alo = a - ahi;
|
|
@@ -16,7 +16,7 @@ import { ObjType, EmitState, MapShape } from '../gvc/job.js';
|
|
|
16
16
|
import type { RendererPlugin } from '../gvc/context.js';
|
|
17
17
|
import { FillType, PenType } from '../gvc/context.js';
|
|
18
18
|
import { transformPoint } from '../gvc/device.js';
|
|
19
|
-
import { firstColor, resolveRenderColor } from '../render/color-resolve.js';
|
|
19
|
+
import { firstColor, resolveFillColorName, resolveRenderColor } from '../render/color-resolve.js';
|
|
20
20
|
import { emitRoundedBezier } from './poly-shapes.js';
|
|
21
21
|
import type { PlacedHtml, PlacedCell } from './htmltable-pos.js';
|
|
22
22
|
import { htmlStyle } from './htmltable-style.js';
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
BORDER_BOTTOM,
|
|
29
29
|
BORDER_MASK,
|
|
30
30
|
} from './htmltable-types.js';
|
|
31
|
-
import { parseSegs, parseSegsSilent } from './multicolor.js';
|
|
31
|
+
import { MSG_MANY_COLORS, parseSegs, parseSegsSilent } from './multicolor.js';
|
|
32
32
|
import { agwarningf } from './agerr.js';
|
|
33
33
|
import { registerRenderState } from './render-state.js';
|
|
34
34
|
|
|
@@ -43,9 +43,6 @@ export interface HtmlStopColor {
|
|
|
43
43
|
frac: number;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/** @see lib/common/emit.c:findStopColor */
|
|
47
|
-
const MSG_MANY_COLORS = 'More than 2 colors specified for a gradient - ignoring remaining';
|
|
48
|
-
|
|
49
46
|
/**
|
|
50
47
|
* Port of C's findStopColor over parseSegs: null when parseSegs returns
|
|
51
48
|
* non-zero (including the rv 3 over-full warning), has < 2 segments or has no
|
|
@@ -197,8 +194,10 @@ function penTypeOf(style: string | undefined): PenType {
|
|
|
197
194
|
* @see lib/common/htmltable.c:setFill
|
|
198
195
|
*/
|
|
199
196
|
export function withHtmlPaint(paint: HtmlPaint, job: RenderJob, drawFn: () => void): void {
|
|
197
|
+
// gvrender_set_fillcolor hands the cut name to colorxlate even when empty
|
|
198
|
+
// (":red" -> ""), so the empty name warns and fills opaque black.
|
|
200
199
|
paintObj.fillColor = paint.fill !== undefined
|
|
201
|
-
?
|
|
200
|
+
? resolveFillColorName(firstColor(paint.fill)) : { type: 'none' };
|
|
202
201
|
paintObj.penColor = paint.pen !== undefined
|
|
203
202
|
? resolveRenderColor(paint.pen) : { type: 'string', s: 'transparent' };
|
|
204
203
|
applyHtmlFill(paint);
|
|
@@ -425,7 +424,12 @@ const HTML_DEFAULT_COLOR = 'black';
|
|
|
425
424
|
/** setFill's names: the fill (and a gradient's stop). @see htmltable.c:setFill */
|
|
426
425
|
function bgNames(bgcolor: string): string[] {
|
|
427
426
|
const spec = parseGradientSpec(bgcolor);
|
|
428
|
-
|
|
427
|
+
if (spec !== null) return [spec[0], spec[1]];
|
|
428
|
+
// A spec parseSegs rejects, or an empty cut name (":red"), is resolved by
|
|
429
|
+
// setFill itself, after findStopColor's own diagnostics: leave it to the
|
|
430
|
+
// paint. @see lib/common/htmltable.c:setFill
|
|
431
|
+
if (parseSegsSilent(bgcolor).error !== 0 || firstColor(bgcolor) === '') return [];
|
|
432
|
+
return [bgcolor];
|
|
429
433
|
}
|
|
430
434
|
|
|
431
435
|
/** One cell in emit_html_cell order: fill, border, then the child. */
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { TextlabelT } from './types.js';
|
|
13
13
|
import type { TextMeasurer } from './textmeasure.js';
|
|
14
|
+
import type { ImageResolver } from '../gvc/image-resolver.js';
|
|
14
15
|
import type { HtmlLabel, ImageSizer } from './htmltable-types.js';
|
|
15
16
|
import { sizeHtmlLabel, parseHtmlLabelRv, inheritTablePenColor } from './htmltable.js';
|
|
16
17
|
import { makeLabel } from './make-label.js';
|
|
@@ -28,7 +29,9 @@ export interface HtmlLabelResult {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/** The label's font, the object's pen color, and the `<IMG>` sizer. */
|
|
31
|
-
export type HtmlLabelInfo = HtmlFontInfo & {
|
|
32
|
+
export type HtmlLabelInfo = HtmlFontInfo & {
|
|
33
|
+
imageSizer?: ImageSizer; imageResolver?: ImageResolver; imageDpi?: number; pencolor?: string;
|
|
34
|
+
};
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* make_html_label: parse, size and position an HTML-like label. Returns C's
|
|
@@ -44,7 +47,7 @@ export function makeHtmlLabelTo(
|
|
|
44
47
|
measurer: TextMeasurer,
|
|
45
48
|
stderr: StderrLines,
|
|
46
49
|
): HtmlLabelResult {
|
|
47
|
-
const { fontname, fontsize, fontcolor, imageSizer, pencolor } = finfo;
|
|
50
|
+
const { fontname, fontsize, fontcolor, imageSizer, imageResolver, imageDpi, pencolor } = finfo;
|
|
48
51
|
let lbl: HtmlLabel, lexWarn: boolean; // lexWarn: htmllex.c's ctx->warn, rv bit 1
|
|
49
52
|
try {
|
|
50
53
|
({ label: lbl, warn: lexWarn } = parseHtmlLabelRv(content, stderr));
|
|
@@ -61,7 +64,7 @@ export function makeHtmlLabelTo(
|
|
|
61
64
|
// @see lib/common/htmltable.c:1911 (getPenColor), :1406/:1556 (inheritance)
|
|
62
65
|
if (lbl.kind === 'table') inheritTablePenColor(lbl.table, pencolor);
|
|
63
66
|
const rv = Number(lexWarn) | sizeHtmlLabel(lbl, measurer, {
|
|
64
|
-
fontsize, fontname, imageSizer, stderr,
|
|
67
|
+
fontsize, fontname, imageSizer, imageResolver, imageDpi, stderr,
|
|
65
68
|
});
|
|
66
69
|
const placed = posHtmlLabel(lbl, { fontname, fontsize, fontcolor }, measurer);
|
|
67
70
|
const dim = lbl.dimen ?? { w: 0, h: 0 };
|
|
@@ -33,6 +33,22 @@ const DEC_DIGIT = /[0-9]/;
|
|
|
33
33
|
const HEX_DIGIT = /[0-9A-Fa-f]/;
|
|
34
34
|
/** Bytes expat's tokenizer rejects anywhere (BT_NONXML): controls other than tab, LF and CR. */
|
|
35
35
|
const NON_XML = /[\x00-\x08\x0b\x0c\x0e-\x1f]/;
|
|
36
|
+
/** The same classification for U+FFFE, U+FFFF and a lone surrogate (the UTF-8 form of one is invalid). */
|
|
37
|
+
const NON_XML_BEYOND_ASCII = /[\ufffe\uffff]/;
|
|
38
|
+
const HIGH_SURROGATE = /[\ud800-\udbff]/;
|
|
39
|
+
const LOW_SURROGATE = /[\udc00-\udfff]/;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether the character at `k` is BT_NONXML in a value: what `CONTENT_NON_XML`
|
|
43
|
+
* (htmltable-lex-expat.ts) rejects in content, one tokenizer table for both.
|
|
44
|
+
* @see expat lib/asciitab.h
|
|
45
|
+
*/
|
|
46
|
+
function isNonXmlAt(text: string, k: number): boolean {
|
|
47
|
+
const c = text[k]!;
|
|
48
|
+
if (NON_XML.test(c) || NON_XML_BEYOND_ASCII.test(c)) return true;
|
|
49
|
+
if (HIGH_SURROGATE.test(c)) return !LOW_SURROGATE.test(text[k + 1] ?? '');
|
|
50
|
+
return LOW_SURROGATE.test(c) && !HIGH_SURROGATE.test(text[k - 1] ?? '');
|
|
51
|
+
}
|
|
36
52
|
|
|
37
53
|
/** A reference's digits from `i` up to `;`: the index after it, or where scanning stops. */
|
|
38
54
|
function scanDigits(text: string, i: number, digit: RegExp): number | AttValueScan {
|
|
@@ -74,7 +90,7 @@ export function scanAttValue(text: string, j: number): AttValueScan {
|
|
|
74
90
|
const c = text[k];
|
|
75
91
|
if (c === undefined) return { partial: true };
|
|
76
92
|
if (c === q) return { close: k };
|
|
77
|
-
if (c === '<' ||
|
|
93
|
+
if (c === '<' || isNonXmlAt(text, k)) return { invalid: k };
|
|
78
94
|
if (c !== '&') {
|
|
79
95
|
k++;
|
|
80
96
|
continue;
|
package/src/common/htmltable.ts
CHANGED
|
@@ -22,9 +22,10 @@ import type {
|
|
|
22
22
|
HtmlCell, HtmlCellContent, HtmlImage, HtmlLabel, HtmlTable, HtmlText,
|
|
23
23
|
ImageSizer,
|
|
24
24
|
} from './htmltable-types.js';
|
|
25
|
-
import {
|
|
25
|
+
import { DEFAULT_DPI, imageIsSupplied, imageOpenWarning, usershapeSizePts } from '../gvc/usershape.js';
|
|
26
26
|
import { registerRenderState } from './render-state.js';
|
|
27
27
|
import { PointSet } from './pointset.js';
|
|
28
|
+
import type { ImageResolver } from '../gvc/image-resolver.js';
|
|
28
29
|
import type { TextMeasurer } from './textmeasure.js';
|
|
29
30
|
import { buildLineRuns, runsAreSimple } from './htmltable-pos-runs.js';
|
|
30
31
|
import { withTableFont } from './htmltable-fontinfo.js';
|
|
@@ -87,6 +88,10 @@ export interface HtmlSizeEnv {
|
|
|
87
88
|
fontsize: number;
|
|
88
89
|
fontname: string;
|
|
89
90
|
imageSizer?: ImageSizer;
|
|
91
|
+
/** Per-context resolver: bytes it supplies count as a found file. */
|
|
92
|
+
imageResolver?: ImageResolver;
|
|
93
|
+
/** The graph's usershape dpi (`gvusershape_size` pixel to point); default 96. */
|
|
94
|
+
imageDpi?: number;
|
|
90
95
|
stderr?: StderrLines; // the label's stderr; none: straight to the console
|
|
91
96
|
}
|
|
92
97
|
|
|
@@ -132,20 +137,20 @@ const sizeTextContent = (
|
|
|
132
137
|
/** Images whose sizing failed: size_html_img's rv = 1 on every later call. */
|
|
133
138
|
const missingImages = { set: new WeakSet<HtmlImage>() };
|
|
134
139
|
|
|
135
|
-
const sizeHtmlImg = (img: HtmlImage, { imageSizer, stderr }: HtmlSizeEnv): number => {
|
|
140
|
+
const sizeHtmlImg = (img: HtmlImage, { imageSizer, imageResolver, imageDpi, stderr }: HtmlSizeEnv): number => {
|
|
136
141
|
// C sizes each image once (processTbl); the port's size and pos passes
|
|
137
142
|
// share layoutHtmlTable, so guard against re-sizing (and re-warning). The
|
|
138
143
|
// returned rv still follows C on every call.
|
|
139
144
|
if (img.width !== undefined && img.height !== undefined) return missingImages.set.has(img) ? 1 : 0;
|
|
140
|
-
const sz =
|
|
145
|
+
const sz = usershapeSizePts(img.src, imageDpi ?? DEFAULT_DPI, imageSizer);
|
|
141
146
|
if (sz !== null) {
|
|
142
|
-
img.width = sz.
|
|
147
|
+
img.width = sz.x; img.height = sz.y;
|
|
143
148
|
return 0;
|
|
144
149
|
}
|
|
145
150
|
// Missing-image: gvusershape_open's warning (unless bytes are supplied),
|
|
146
151
|
// then zero size + the Error.
|
|
147
152
|
// @see lib/gvc/gvusershape.c:gvusershape_file_access
|
|
148
|
-
if (!imageIsSupplied(img.src, imageSizer)) agerrLine(stderr, `Warning: ${imageOpenWarning(img.src)}`);
|
|
153
|
+
if (!imageIsSupplied(img.src, imageSizer, imageResolver)) agerrLine(stderr, `Warning: ${imageOpenWarning(img.src)}`);
|
|
149
154
|
// C: agerrorf("No or improper image file=\"%s\"\n", img->src) — agerr
|
|
150
155
|
// prefixes "Error: " on stderr.
|
|
151
156
|
agerrLine(stderr, `Error: No or improper image file="${img.src}"`);
|
package/src/common/layers.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// only in the layers it belongs to (its `layer` attribute, or all layers when
|
|
7
7
|
// it has none). All of this is inert unless numLayers > 1.
|
|
8
8
|
|
|
9
|
+
import { agwarningf } from './agerr.js';
|
|
9
10
|
import type { Graph } from '../model/graph.js';
|
|
10
11
|
import type { Node } from '../model/node.js';
|
|
11
12
|
import type { Edge } from '../model/edge.js';
|
|
@@ -21,6 +22,11 @@ export interface LayerInfo {
|
|
|
21
22
|
numLayers: number;
|
|
22
23
|
sep: string;
|
|
23
24
|
listSep: string;
|
|
25
|
+
/**
|
|
26
|
+
* C `gvc->layerlist`: `[count, l1, …, lcount, numLayers + 1]` from the
|
|
27
|
+
* `layerselect` attribute, or null when absent or matching no layer.
|
|
28
|
+
*/
|
|
29
|
+
layerList: number[] | null;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/** Split `s` on any character in the `delims` set, dropping empty tokens
|
|
@@ -39,10 +45,13 @@ export function parseLayers(g: Graph): LayerInfo {
|
|
|
39
45
|
// Conflict: a char in both sep sets → ignore listSep (C warns + clears it).
|
|
40
46
|
if ([...sep].some((c) => listSep.includes(c))) listSep = '';
|
|
41
47
|
if (p === undefined || p === '') {
|
|
42
|
-
return { layerIDs: [], numLayers: 1, sep, listSep };
|
|
48
|
+
return { layerIDs: [], numLayers: 1, sep, listSep, layerList: null };
|
|
43
49
|
}
|
|
44
50
|
const toks = splitOnSet(p, sep);
|
|
45
|
-
|
|
51
|
+
const info: LayerInfo = { layerIDs: [null, ...toks], numLayers: toks.length, sep, listSep, layerList: null };
|
|
52
|
+
const sel = g.attrs.get('layerselect');
|
|
53
|
+
if (sel !== undefined && sel !== '') info.layerList = parseLayerselect(info, sel);
|
|
54
|
+
return info;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
function isNaturalNumber(s: string): boolean {
|
|
@@ -111,3 +120,23 @@ export function edgeInLayer(info: LayerInfo, layerNum: number, e: Edge): boolean
|
|
|
111
120
|
return pn === '' || selectedLayer(info, layerNum, pn);
|
|
112
121
|
});
|
|
113
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Walk each layer index and keep those `selectedLayer` matches against `p`.
|
|
126
|
+
* Returns `[n, l1..ln, numLayers + 1]`, or null (after a warning) when nothing
|
|
127
|
+
* matches. The warning's "specifed" is native's spelling.
|
|
128
|
+
* @see lib/common/emit.c:parse_layerselect
|
|
129
|
+
*/
|
|
130
|
+
export function parseLayerselect(info: LayerInfo, p: string): number[] | null {
|
|
131
|
+
const picked: number[] = [];
|
|
132
|
+
for (let i = 1; i <= info.numLayers; i++) {
|
|
133
|
+
if (selectedLayer(info, i, p)) picked.push(i);
|
|
134
|
+
}
|
|
135
|
+
if (picked.length === 0) {
|
|
136
|
+
agwarningf(
|
|
137
|
+
`The layerselect attribute "${p}" does not match any layer specifed by the layers attribute - ignored.`,
|
|
138
|
+
);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return [picked.length, ...picked, info.numLayers + 1];
|
|
142
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The C global `State` ("last finished phase"). It is a per-process variable
|
|
5
|
+
* in C; the port resets it per render (AD-4) to its initial value.
|
|
6
|
+
*
|
|
7
|
+
* @see lib/common/globals.h:State
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { registerRenderState } from './render-state.js';
|
|
11
|
+
|
|
12
|
+
/** Phase values for `State`. @see lib/common/const.h:GVBEGIN, GVSPLINES */
|
|
13
|
+
export const GVBEGIN = 0;
|
|
14
|
+
export const GVSPLINES = 1;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Holder for the C global `State`. C's initial value is 0 (BSS) and
|
|
18
|
+
* graph_init assigns GVBEGIN, which is the same value.
|
|
19
|
+
* @see lib/common/input.c:graph_init (State = GVBEGIN)
|
|
20
|
+
*/
|
|
21
|
+
const layoutState = { state: GVBEGIN };
|
|
22
|
+
|
|
23
|
+
registerRenderState(() => {
|
|
24
|
+
layoutState.state = GVBEGIN;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Read `State`. @see lib/common/globals.h:State */
|
|
28
|
+
export function getState(): number {
|
|
29
|
+
return layoutState.state;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Assign `State`. @see lib/common/globals.h:State */
|
|
33
|
+
export function setState(value: number): void {
|
|
34
|
+
layoutState.state = value;
|
|
35
|
+
}
|
package/src/common/make-label.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { Node } from '../model/node.js';
|
|
|
20
20
|
import { nodeAttr, graphAttrInherited } from './attr-get.js';
|
|
21
21
|
import { Graph } from '../model/graph.js';
|
|
22
22
|
import { htmlEntityUTF8 } from './html-entities.js';
|
|
23
|
+
import { usershapeDpi } from '../gvc/usershape.js';
|
|
23
24
|
|
|
24
25
|
export const DEFAULT_FONTSIZE = 14.0;
|
|
25
26
|
/** C's default fontname (non-Windows); renderers alias it, e.g. SVG
|
|
@@ -217,11 +218,14 @@ export function makeAnyLabel(
|
|
|
217
218
|
// pen color so an HTML table/cell border with no explicit COLOR inherits
|
|
218
219
|
// it (htmltable.c:1911 getPenColor). @see getPenColor
|
|
219
220
|
// ADR-2: a per-context sizer (when set) wins over the global one.
|
|
220
|
-
const
|
|
221
|
+
const root = obj !== undefined ? rootOf(obj) : undefined;
|
|
222
|
+
const imageSizer = root?.info.gvc?.imageSizer;
|
|
223
|
+
const imageResolver = root?.info.gvc?.imageResolver;
|
|
224
|
+
const imageDpi = root !== undefined ? usershapeDpi(root.attrs) : undefined;
|
|
221
225
|
// One stderr for the label: doInt's unterminated warning runs into the
|
|
222
226
|
// next write, `in label of ...` included (agerr AGPREV).
|
|
223
227
|
const stderr = labelStderr();
|
|
224
|
-
const { label, rv } = makeHtmlLabelTo(content, { ...font, pencolor: getPenColor(obj), imageSizer }, measurer, stderr);
|
|
228
|
+
const { label, rv } = makeHtmlLabelTo(content, { ...font, pencolor: getPenColor(obj), imageSizer, imageResolver, imageDpi }, measurer, stderr);
|
|
225
229
|
if (rv !== 0 && obj !== undefined) stderr.write(`${inLabelOf(obj)}\n`);
|
|
226
230
|
stderr.flush();
|
|
227
231
|
return label;
|
package/src/common/multicolor.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @see lib/common/emit.c:470 parseSegs
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { agerr,
|
|
16
|
+
import { agerr, agerrPartial, agerrorf } from './agerr.js';
|
|
17
17
|
import type { Edge } from '../model/edge.js';
|
|
18
18
|
import { registerRenderState } from './render-state.js';
|
|
19
19
|
|
|
@@ -64,6 +64,9 @@ function claimWarned(): boolean {
|
|
|
64
64
|
/** A claim that never fires: parses without touching or consuming the flag. */
|
|
65
65
|
const claimNever = (): boolean => false;
|
|
66
66
|
|
|
67
|
+
/** @see lib/common/emit.c:findStopColor */
|
|
68
|
+
export const MSG_MANY_COLORS = 'More than 2 colors specified for a gradient - ignoring remaining';
|
|
69
|
+
|
|
67
70
|
/** @see lib/common/emit.c:451 EPS */
|
|
68
71
|
const EPS = 1e-5;
|
|
69
72
|
|
|
@@ -187,9 +190,10 @@ function scanSegs(
|
|
|
187
190
|
* On rv 1 or 2 the returned segs array is empty.
|
|
188
191
|
*
|
|
189
192
|
* The rv 2 error and rv 3 warning are printed here, once per process (the
|
|
190
|
-
* shared `warned` flag).
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
+
* shared `warned` flag). The warning is a held partial line (no `\n`, as in
|
|
194
|
+
* C); C's callers then append an AGPREV line for rv > 1; `context` supplies it
|
|
195
|
+
* (e.g. `in edge a -> b`) and completes the held line. Without `context` the
|
|
196
|
+
* next agerr message continues the line.
|
|
193
197
|
*
|
|
194
198
|
* @see lib/common/emit.c:470 parseSegs
|
|
195
199
|
*/
|
|
@@ -199,8 +203,10 @@ export function parseSegs(
|
|
|
199
203
|
): { segs: ColorSeg[]; error: 0 | 1 | 2 | 3 } {
|
|
200
204
|
const result = scanSegs(colorlist, claimWarned);
|
|
201
205
|
if (result.error === 3) {
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
// C's message has no newline: the line stays open for the next agerr*
|
|
207
|
+
// call (or the caller's AGPREV context line).
|
|
208
|
+
agerrPartial('warning', `Total size > 1 in "${colorlist}" color spec `);
|
|
209
|
+
if (context !== undefined) agerr('prev', context());
|
|
204
210
|
} else if (result.error === 2) {
|
|
205
211
|
agerrorf(`Illegal value in "${colorlist}" color attribute; float expected after ';'`);
|
|
206
212
|
if (context !== undefined) agerr('prev', context());
|
package/src/common/nodeinit.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { TextMeasurer } from './textmeasure.js';
|
|
|
15
15
|
import type { PolygonT, ShapeDesc, TextlabelT } from './types.js';
|
|
16
16
|
import { ShapeKind } from './types.js';
|
|
17
17
|
import { bindShape } from './shapes.js';
|
|
18
|
-
import { assignShapeInfo, buildNodeLabel, nodeAttr, readFontAttrs } from './poly-init.js';
|
|
18
|
+
import { assignShapeInfo, buildNodeLabel, nodeAttr, nodeImageSize, readFontAttrs } from './poly-init.js';
|
|
19
19
|
import { makeAnyLabel } from './make-label.js';
|
|
20
20
|
import { isHtmlValue, htmlValueContent } from './html-string.js';
|
|
21
21
|
import { NODE_XLABEL } from '../layout/dot/rank.js';
|
|
@@ -154,6 +154,7 @@ export function polySizeParamsFromNode(
|
|
|
154
154
|
isPlain: shape.name === 'plain',
|
|
155
155
|
optionShape: poly.option.shape, // size_gen discriminant (STAR/CYLINDER)
|
|
156
156
|
nojustify: mapbool(nodeAttr(n, g, 'nojustify')), // shapes.c:2133
|
|
157
|
+
imageSize: nodeImageSize(n, g), // shapes.c:2039-2051
|
|
157
158
|
flip,
|
|
158
159
|
};
|
|
159
160
|
}
|
package/src/common/poly-init.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from './make-label.js';
|
|
27
27
|
import { computeVertices } from './poly-vertices.js';
|
|
28
28
|
import { agwarningf } from './agerr.js';
|
|
29
|
-
import { imageIsSupplied, imageOpenWarning } from '../gvc/usershape.js';
|
|
29
|
+
import { imageIsSupplied, imageOpenWarning, usershapeDpi, usershapeSizePts } from '../gvc/usershape.js';
|
|
30
30
|
|
|
31
31
|
// Default half-dimensions in points (72 dpi, 0.75 in wide, 0.5 in tall)
|
|
32
32
|
const DEFAULT_LW = 27;
|
|
@@ -114,6 +114,23 @@ function warnMissingImage(n: Node, g: Graph, shapefile: string | undefined): voi
|
|
|
114
114
|
agwarningf(`No or improper image="${image}" for node "${n.name}"`);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* poly_init's imagesize for a non-custom shape's `image=`: gvusershape_size in
|
|
119
|
+
* points plus the fixed 2pt padding, and GD_has_images. Null (C's imagesize 0)
|
|
120
|
+
* when there is no image or the caller supplies no size; the missing-image
|
|
121
|
+
* warning stays in warnMissingImage.
|
|
122
|
+
* @see lib/common/shapes.c:2039-2051
|
|
123
|
+
*/
|
|
124
|
+
export function nodeImageSize(n: Node, g: Graph): { x: number; y: number } | undefined {
|
|
125
|
+
const shapefile = nodeAttr(n, g, 'shapefile');
|
|
126
|
+
if (shapefile !== undefined && shapefile !== '') return undefined;
|
|
127
|
+
const gvc = g.root.info.gvc;
|
|
128
|
+
const sz = usershapeSizePts(nodeAttr(n, g, 'image'), usershapeDpi(g.root.attrs), gvc?.imageSizer);
|
|
129
|
+
if (sz === null) return undefined;
|
|
130
|
+
g.root.info.has_images = true;
|
|
131
|
+
return { x: sz.x + 2, y: sz.y + 2 }; // some fixed padding
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
export function polyInit(n: Node, g: Graph, measurer: TextMeasurer): void {
|
|
118
135
|
const shapeName = nodeAttr(n, g, 'shape') ?? 'ellipse';
|
|
119
136
|
const shapefile = nodeAttr(n, g, 'shapefile');
|
|
@@ -89,6 +89,7 @@ export interface PolySizeParams {
|
|
|
89
89
|
* @see lib/common/shapes.c:2132-2145
|
|
90
90
|
*/
|
|
91
91
|
nojustify?: boolean;
|
|
92
|
+
imageSize?: Point; // padded image size in points, shapes.c:2020-2051 imagesize
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** Node geometry in points: left/right half-widths and height. */
|
|
@@ -550,7 +551,8 @@ export function polySize(p: PolySizeParams): PolySizeResult {
|
|
|
550
551
|
const init = initialSizePts(p);
|
|
551
552
|
const dimen = quantize(padLabelDimen(p.labelDimen, p.margin, p.isPlain), p.quantumIn);
|
|
552
553
|
const { sides, isBox } = effectiveShape(p);
|
|
553
|
-
|
|
554
|
+
const img = p.imageSize ?? { x: 0, y: 0 }; // shapes.c:2055: bb = max(dimen, imagesize)
|
|
555
|
+
let bb: Point = { x: Math.max(dimen.x, img.x), y: Math.max(dimen.y, img.y) };
|
|
554
556
|
// C shapes.c:2073-2085 dispatch: box → exact fit; poly_desc shapes (star,
|
|
555
557
|
// cylinder) → their size_gen; everything else → generic ellipse fit.
|
|
556
558
|
if (isBox) {
|
|
@@ -583,8 +585,6 @@ export function polySize(p: PolySizeParams): PolySizeResult {
|
|
|
583
585
|
* Compute the label's justification borders (ND_label->space) and vertical
|
|
584
586
|
* alignment — the "space available for label" block of poly_init. `bb` is the
|
|
585
587
|
* final (constrained) box; `minBb` is the pre-constraint shape-fit box.
|
|
586
|
-
* Image sizing is unported, so `imagesize` is 0 (which matches C for the
|
|
587
|
-
* missing-image repro cases); revisit when image loading lands.
|
|
588
588
|
* @see lib/common/shapes.c:poly_init (2132-2152)
|
|
589
589
|
*/
|
|
590
590
|
function labelSpace(
|
|
@@ -604,7 +604,7 @@ function labelSpace(
|
|
|
604
604
|
spaceX = dimen.x - spacex;
|
|
605
605
|
}
|
|
606
606
|
// space.y: !fixedshape → dimen.y + (bb.y - min_bb.y); fixedshape keeps dimen.y.
|
|
607
|
-
const spaceY = fixedshape ? dimen.y : dimen.y + (bb.y - minBb.y);
|
|
607
|
+
const spaceY = fixedshape ? dimen.y : dimen.y + ((bb.y - minBb.y) + Math.max(0, (p.imageSize?.y ?? 0) - dimen.y));
|
|
608
608
|
return { space: { x: spaceX, y: spaceY }, valign: labelValign(p) };
|
|
609
609
|
}
|
|
610
610
|
|
package/src/common/postproc.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from '../layout/dot/init.js';
|
|
28
28
|
import { addXLabels } from './xlabels-place.js';
|
|
29
29
|
import { registerRenderState } from './render-state.js';
|
|
30
|
+
import { getState, GVSPLINES } from './layout-state.js';
|
|
30
31
|
import { agerrorf } from './agerr.js';
|
|
31
32
|
|
|
32
33
|
/** Concentrated multi-edge duplicate — never drawn. @see lib/common/const.h:IGNORED */
|
|
@@ -221,7 +222,9 @@ function translateDrawing(g: Graph): void {
|
|
|
221
222
|
|
|
222
223
|
for (const v of g.nodes.values()) {
|
|
223
224
|
mapNode(v);
|
|
224
|
-
// State == GVSPLINES
|
|
225
|
+
// C: `if (State == GVSPLINES)` guards the whole edge loop; layouts that
|
|
226
|
+
// never route (patchwork, osage) leave State at GVBEGIN.
|
|
227
|
+
if (getState() !== GVSPLINES) continue;
|
|
225
228
|
for (const e of g.edges) {
|
|
226
229
|
if (e.tail !== v) continue;
|
|
227
230
|
// C map_edge: a spline-less (non-concentrate, non-IGNORED) edge was
|