@knowvah/dot-engine 1.1.1 → 1.2.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 +32 -17
- package/dist/api.js +24 -0
- package/dist/api.js.map +2 -2
- package/dist/common/arrows.d.ts.map +1 -1
- package/dist/common/edge-label-init.d.ts.map +1 -1
- package/dist/common/htmltable-emit.d.ts +0 -8
- package/dist/common/htmltable-emit.d.ts.map +1 -1
- package/dist/common/ps-fontalias.d.ts +22 -9
- package/dist/common/ps-fontalias.d.ts.map +1 -1
- package/dist/common/splines-clip.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/gvc/job.d.ts +4 -0
- package/dist/gvc/job.d.ts.map +1 -1
- package/dist/index.js +1981 -1533
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/index.d.ts +1 -1
- package/dist/layout/dot/index.d.ts.map +1 -1
- package/dist/layout/sfdp/init.d.ts.map +1 -1
- package/dist/model/graph.d.ts +24 -0
- package/dist/model/graph.d.ts.map +1 -1
- package/dist/parser/builder.d.ts +32 -0
- package/dist/parser/builder.d.ts.map +1 -1
- package/dist/render/dot/agwrite.d.ts +186 -0
- package/dist/render/dot/agwrite.d.ts.map +1 -0
- package/dist/render/dot/attrs.d.ts +220 -0
- package/dist/render/dot/attrs.d.ts.map +1 -0
- package/dist/render/dot/edge-draw.d.ts +60 -0
- package/dist/render/dot/edge-draw.d.ts.map +1 -0
- package/dist/render/dot/types.d.ts +28 -0
- package/dist/render/dot/types.d.ts.map +1 -0
- package/dist/render/dot/xdot-ops.d.ts +206 -0
- package/dist/render/dot/xdot-ops.d.ts.map +1 -0
- package/dist/render/dot.d.ts +58 -277
- package/dist/render/dot.d.ts.map +1 -1
- package/dist/render/map.d.ts +14 -1
- package/dist/render/map.d.ts.map +1 -1
- package/dist/render/svg-graph.d.ts.map +1 -1
- package/dist/render.js +1942 -1494
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/arrows.ts +17 -4
- package/src/common/edge-label-init.ts +9 -2
- package/src/common/htmltable-emit.ts +36 -8
- package/src/common/poly-gencode.ts +8 -8
- package/src/common/ps-fontalias.ts +80 -49
- package/src/common/splines-clip.ts +36 -10
- package/src/gvc/device.ts +18 -6
- package/src/gvc/job.ts +4 -0
- package/src/layout/dot/index.ts +37 -5
- package/src/layout/sfdp/init.ts +11 -0
- package/src/model/graph.ts +26 -0
- package/src/parser/builder.ts +90 -5
- package/src/render/dot/agwrite.ts +506 -0
- package/src/render/dot/attrs.ts +437 -0
- package/src/render/dot/edge-draw.ts +203 -0
- package/src/render/dot/types.ts +32 -0
- package/src/render/dot/xdot-ops.ts +432 -0
- package/src/render/dot.ts +114 -1131
- package/src/render/map.ts +18 -1
- package/src/render/svg-graph.ts +4 -0
- package/src/render/svg-helpers.ts +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* xdot output primitives — number, string, colour and geometry formatting for
|
|
5
|
+
* the xdot draw-string language, plus the shared `%.5g`/`%.2f` helpers.
|
|
6
|
+
*
|
|
7
|
+
* @see plugin/core/gvrender_core_dot.c
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Point } from '../../model/geom.js';
|
|
11
|
+
import type { GVColor } from '../../common/color.js';
|
|
12
|
+
import { colorxlate } from '../../common/color.js';
|
|
13
|
+
import { getGradientPoints } from '../svg-gradient.js';
|
|
14
|
+
import { EmitState, toFixed2HalfEven } from '../../gvc/job.js';
|
|
15
|
+
import { printfSig, printfFixed } from '../../util/printf-round.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export const XDOT_VERSION = '1.7';
|
|
22
|
+
|
|
23
|
+
/** Max magnitude cap — matches maxnegnum in C source. */
|
|
24
|
+
export const MAX_NEGNUM = 999999999999999.99;
|
|
25
|
+
|
|
26
|
+
/** Near-zero suppression threshold (same as gvprintdouble). */
|
|
27
|
+
export const NEAR_ZERO = 0.005;
|
|
28
|
+
|
|
29
|
+
/** Indices 8/9 alias 1; 10/11 alias 5. NUM_XBUFS covers 0–7. */
|
|
30
|
+
export const NUM_XBUFS = 8;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Style tokens that never become an xdot `S` op: `filled`/`bold`/`setlinewidth`
|
|
34
|
+
* are filtered by xdot_style itself; the polygon/fill styles are consumed by the
|
|
35
|
+
* shape/fill code before gvrender_set_style, so they never reach the render
|
|
36
|
+
* style. Only line styles (solid/dashed/dotted + unknown) emit an `S` op.
|
|
37
|
+
* @see plugin/core/gvrender_core_dot.c:184 xdot_style · lib/common/shapes.c
|
|
38
|
+
*/
|
|
39
|
+
export const NON_LINE_STYLES: ReadonlySet<string> = new Set([
|
|
40
|
+
'filled', 'bold', 'rounded', 'diagonals', 'striped', 'wedged',
|
|
41
|
+
'invis', 'invisible', 'radial',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// printNum — @see lib/gvc/gvdevice.c:gvprintnum
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Strip trailing zeros (and the decimal point) from a toFixed(3) string,
|
|
50
|
+
* then collapse a leading "0." or "-0." prefix.
|
|
51
|
+
*/
|
|
52
|
+
export function trimAndStrip(s: string): string {
|
|
53
|
+
const dot = s.indexOf('.');
|
|
54
|
+
if (dot < 0) return s;
|
|
55
|
+
let end = s.length;
|
|
56
|
+
while (end > dot + 1 && s[end - 1] === '0') end--;
|
|
57
|
+
if (s[end - 1] === '.') end--;
|
|
58
|
+
const t = s.slice(0, end);
|
|
59
|
+
if (t.startsWith('0.')) return t.slice(1);
|
|
60
|
+
if (t.startsWith('-0.')) return '-' + t.slice(2);
|
|
61
|
+
return t;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert a double to compact string form for DOT/XDOT output.
|
|
66
|
+
*
|
|
67
|
+
* Rules (porting gvprintnum from lib/gvc/gvdevice.c):
|
|
68
|
+
* - |n| > MAX_NEGNUM → clamped to ±MAX_NEGNUM
|
|
69
|
+
* - |n| < NEAR_ZERO → "0" (suppresses -0)
|
|
70
|
+
* - 3 decimal places, trailing zeros and point stripped
|
|
71
|
+
* - Leading "0." collapsed to "." (e.g. 0.5 → ".5")
|
|
72
|
+
*
|
|
73
|
+
* Exported for json.ts (T30) and map.ts (T31).
|
|
74
|
+
*
|
|
75
|
+
* @see lib/gvc/gvdevice.c:gvprintnum
|
|
76
|
+
*/
|
|
77
|
+
export function printNum(n: number): string {
|
|
78
|
+
if (Math.abs(n) > MAX_NEGNUM) {
|
|
79
|
+
return n < 0 ? String(-MAX_NEGNUM) : String(MAX_NEGNUM);
|
|
80
|
+
}
|
|
81
|
+
if (Math.abs(n) < NEAR_ZERO) return '0';
|
|
82
|
+
return trimAndStrip(n.toFixed(3));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// XDot buffer management
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a 12-slot xbufs array with the canonical aliasing.
|
|
91
|
+
*
|
|
92
|
+
* Indices 8–9 (NDraw, EDraw) alias index 1 (CDraw).
|
|
93
|
+
* Indices 10–11 (NLabel, ELabel) alias index 5 (CLabel).
|
|
94
|
+
*
|
|
95
|
+
* @see plugin/core/gvrender_core_dot.c:xbufs
|
|
96
|
+
*/
|
|
97
|
+
export function makeXbufs(): string[][] {
|
|
98
|
+
const bufs: string[][] = Array.from({ length: NUM_XBUFS + 4 }, () => []);
|
|
99
|
+
bufs[EmitState.NDraw] = bufs[EmitState.CDraw]!;
|
|
100
|
+
bufs[EmitState.EDraw] = bufs[EmitState.CDraw]!;
|
|
101
|
+
bufs[EmitState.NLabel] = bufs[EmitState.CLabel]!;
|
|
102
|
+
bufs[EmitState.ELabel] = bufs[EmitState.CLabel]!;
|
|
103
|
+
return bufs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// XDOT op helpers — @see plugin/core/gvrender_core_dot.c
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format one xdot draw-op number: 2 decimals, trailing zeros and point trimmed
|
|
112
|
+
* — mirroring xdot_fmt_num ("%.02f" + agxbuf_trim_zeros). Distinct from
|
|
113
|
+
* `printNum` (used for the DOT `pos`/`bb`/`width`/`height` attributes), which
|
|
114
|
+
* keeps more precision; xdot's DRAW ops are emitted at 2 dp by the C engine.
|
|
115
|
+
* @see plugin/core/gvrender_core_dot.c:126 xdot_fmt_num
|
|
116
|
+
*/
|
|
117
|
+
export function xdotNum(v: number): string {
|
|
118
|
+
// Round half-to-even like C's printf %.02f (FE_TONEAREST); JS toFixed rounds
|
|
119
|
+
// half-away-from-zero, which diverges at exact .xx5 ties (2323.125 → native
|
|
120
|
+
// 2323.12, not 2323.13). @see lib/gvc/gvdevice.c gvprintdouble
|
|
121
|
+
let s = toFixed2HalfEven(v);
|
|
122
|
+
if (s.indexOf('.') >= 0) {
|
|
123
|
+
let end = s.length;
|
|
124
|
+
while (end > 0 && s[end - 1] === '0') end--;
|
|
125
|
+
if (s[end - 1] === '.') end--;
|
|
126
|
+
s = s.slice(0, end);
|
|
127
|
+
}
|
|
128
|
+
return s === '-0' ? '0' : s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format a single xdot point "x y ". xdot is y-up: `Y_invert` defaults false, so
|
|
133
|
+
* `yDir(y, yOff)` returns `y` unchanged for xdot (only `-Ty` plain/dot invert).
|
|
134
|
+
* The layout coordinate passes through with NO inversion — unlike the SVG path.
|
|
135
|
+
* @see lib/common/output.c:36 yDir · plugin/core/gvrender_core_dot.c:132 xdot_point
|
|
136
|
+
*/
|
|
137
|
+
export function xdotPoint(p: Point): string {
|
|
138
|
+
return xdotNum(p.x) + ' ' + xdotNum(p.y) + ' ';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Format N points preceded by opcode and count: "<c> <n> x0 y0 x1 y1 …". */
|
|
142
|
+
export function xdotPoints(c: string, pts: Point[]): string {
|
|
143
|
+
let s = c + ' ' + String(pts.length) + ' ';
|
|
144
|
+
for (const p of pts) s += xdotPoint(p);
|
|
145
|
+
return s;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* UTF-8 byte length of a string — the value C's `xdot_str` writes as the length
|
|
150
|
+
* prefix (`strlen(s)` over the UTF-8 bytes), NOT the JS UTF-16 code-unit count.
|
|
151
|
+
* A label like `ÿ` (U+00FF) is 2 UTF-8 bytes, so its `T`/`F` op prefix is 2.
|
|
152
|
+
* @see plugin/core/gvrender_core_dot.c:83 xdot_str_xbuf (`%zu`, strlen)
|
|
153
|
+
*/
|
|
154
|
+
export function utf8Len(s: string): number {
|
|
155
|
+
let n = 0;
|
|
156
|
+
for (let i = 0; i < s.length; i++) {
|
|
157
|
+
const c = s.charCodeAt(i);
|
|
158
|
+
if (c < 0x80) n += 1;
|
|
159
|
+
else if (c < 0x800) n += 2;
|
|
160
|
+
else if (c >= 0xd800 && c <= 0xdbff) { n += 4; i++; } // surrogate pair → 4 bytes
|
|
161
|
+
else n += 3;
|
|
162
|
+
}
|
|
163
|
+
return n;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Clamp a normalized [0,1] float channel to a 0-255 byte (round-to-nearest). */
|
|
167
|
+
export function chanByte(v: number): number {
|
|
168
|
+
return Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve a GVColor to RGBA bytes — the value the C xdot callbacks read from
|
|
173
|
+
* `job->obj->pencolor.u.rgba` (already resolved by gvrender_set_pencolor). A
|
|
174
|
+
* plain `rgba` passes through; named/hex/HSV specs run through colorxlate; a
|
|
175
|
+
* `none` color is fully transparent black (callers gate on PEN_NONE).
|
|
176
|
+
*/
|
|
177
|
+
export function gvColorRgba(c: GVColor): [number, number, number, number] {
|
|
178
|
+
if (c.type === 'rgba') return [chanByte(c.r), chanByte(c.g), chanByte(c.b), chanByte(c.a)];
|
|
179
|
+
// A `none`/transparent paint emits graphviz's "transparent" bytes (ff ff fe 00),
|
|
180
|
+
// matching native's `#fffffe00` — not fully-zero black. @see colxlate.c transparent
|
|
181
|
+
if (c.type === 'none') return [0xff, 0xff, 0xfe, 0x00];
|
|
182
|
+
const out: GVColor = { type: 'rgba', r: 0, g: 0, b: 0, a: 0 };
|
|
183
|
+
colorxlate(c.type === 'string' ? c.s : '', out, 'rgba');
|
|
184
|
+
return out.type === 'rgba'
|
|
185
|
+
? [chanByte(out.r), chanByte(out.g), chanByte(out.b), chanByte(out.a)]
|
|
186
|
+
: [0, 0, 0, 255];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Bare xdot color body (no `c `/`C ` prefix): the CONSTANT length prefix
|
|
191
|
+
* (7 for `#rrggbb`, 9 for `#rrggbbaa`) plus `-#hex`, the alpha byte present only
|
|
192
|
+
* when not fully opaque. Used both by the color ops and by gradient color stops.
|
|
193
|
+
* @see plugin/core/gvrender_core_dot.c:99 xdot_str_color_xbuf
|
|
194
|
+
*/
|
|
195
|
+
export function xdotColorBody(rgba: [number, number, number, number]): string {
|
|
196
|
+
const hx = (n: number): string => n.toString(16).padStart(2, '0');
|
|
197
|
+
const body = '#' + hx(rgba[0]) + hx(rgba[1]) + hx(rgba[2]);
|
|
198
|
+
return rgba[3] === 0xff ? '7 -' + body : '9 -' + body + hx(rgba[3]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Format an xdot color op ("c "/"C ") from RGBA bytes.
|
|
203
|
+
* @see plugin/core/gvrender_core_dot.c:99 xdot_str_color_xbuf
|
|
204
|
+
*/
|
|
205
|
+
export function xdotColorOp(prefix: 'c ' | 'C ', rgba: [number, number, number, number]): string {
|
|
206
|
+
return prefix + xdotColorBody(rgba) + ' ';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build the linear-gradient `C len -[x0 y0 x1 y1 2 <stops>]` fill op. Endpoints
|
|
211
|
+
* come from getGradientPoints (the same geometry the SVG gradient uses); stops
|
|
212
|
+
* are (frac,fill)/(frac,stop) when frac>0, else (0,fill)/(1,stop). Shared by
|
|
213
|
+
* node/cluster gradient fills and the graph-background gradient.
|
|
214
|
+
* @see plugin/core/gvrender_core_dot.c:544-598 xdot_gradient_fillcolor
|
|
215
|
+
*/
|
|
216
|
+
export function linearGradientOp(
|
|
217
|
+
pts: Point[],
|
|
218
|
+
fillColor: GVColor,
|
|
219
|
+
stopColor: GVColor,
|
|
220
|
+
frac: number,
|
|
221
|
+
angleDeg: number,
|
|
222
|
+
): string {
|
|
223
|
+
// isRHS=true: native y-up coords, matching C's get_gradient_points(A,G,n,angle,2)
|
|
224
|
+
// for the xdot device path (the SVG path uses isRHS=false + a container flip).
|
|
225
|
+
const { g0, g1 } = getGradientPoints(pts, (angleDeg * Math.PI) / 180, false, true);
|
|
226
|
+
const inner =
|
|
227
|
+
'[' + xdotNum(g0.x) + ' ' + xdotNum(g0.y) + ' ' + xdotNum(g1.x) + ' ' + xdotNum(g1.y) +
|
|
228
|
+
' 2 ' + gradientStops(fillColor, stopColor, frac) + ']';
|
|
229
|
+
return 'C ' + String(utf8Len(inner)) + ' -' + inner + ' ';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The `<frac> <colorbody>` stop pairs for a gradient (frac>0 vs the 0/1 form). */
|
|
233
|
+
export function gradientStops(fillColor: GVColor, stopColor: GVColor, frac: number): string {
|
|
234
|
+
const fill = gvColorRgba(fillColor);
|
|
235
|
+
const stop = gvColorRgba(stopColor);
|
|
236
|
+
const stops: Array<[number, [number, number, number, number]]> =
|
|
237
|
+
frac > 0 ? [[frac, fill], [frac, stop]] : [[0, fill], [1, stop]];
|
|
238
|
+
return stops.map(([f, c]) => trimFixed3(f) + ' ' + xdotColorBody(c)).join(' ');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build the radial-gradient `C len -(c1x c1y r1 c2x c2y r2 2 <stops>)` fill op.
|
|
243
|
+
* Reuses getGradientPoints (radial) for the center/radii, un-negating its SVG
|
|
244
|
+
* y. r1 = outerR/4, r2 = outerR; c2 is the center, c1 the center offset by r1
|
|
245
|
+
* along the gradient angle (== center when angle 0).
|
|
246
|
+
* @see plugin/core/gvrender_core_dot.c:562-585 xdot_gradient_fillcolor (radial)
|
|
247
|
+
*/
|
|
248
|
+
export function radialGradientOp(
|
|
249
|
+
pts: Point[],
|
|
250
|
+
fillColor: GVColor,
|
|
251
|
+
stopColor: GVColor,
|
|
252
|
+
frac: number,
|
|
253
|
+
angleDeg: number,
|
|
254
|
+
): string {
|
|
255
|
+
const rad = (angleDeg * Math.PI) / 180;
|
|
256
|
+
// isRHS=true: native y-up coords, matching C's get_gradient_points(A,G,n,0,3).
|
|
257
|
+
const gp = getGradientPoints(pts, rad, true, true);
|
|
258
|
+
const cx = gp.g0.x;
|
|
259
|
+
const cy = gp.g0.y;
|
|
260
|
+
const r1 = gp.g1.x;
|
|
261
|
+
const r2 = gp.g1.y;
|
|
262
|
+
const c1x = angleDeg === 0 ? cx : cx + r1 * Math.cos(rad);
|
|
263
|
+
const c1y = angleDeg === 0 ? cy : cy + r1 * Math.sin(rad);
|
|
264
|
+
const inner =
|
|
265
|
+
'(' + xdotNum(c1x) + ' ' + xdotNum(c1y) + ' ' + xdotNum(r1) + ' ' +
|
|
266
|
+
xdotNum(cx) + ' ' + xdotNum(cy) + ' ' + xdotNum(r2) + ' 2 ' +
|
|
267
|
+
gradientStops(fillColor, stopColor, frac) + ')';
|
|
268
|
+
return 'C ' + String(utf8Len(inner)) + ' -' + inner + ' ';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Pen ("c ") color op from a resolved GVColor. */
|
|
272
|
+
export function xdotPenColor(c: GVColor): string {
|
|
273
|
+
return xdotColorOp('c ', gvColorRgba(c));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Fill ("C ") color op from a resolved GVColor. */
|
|
277
|
+
export function xdotFillColor(c: GVColor): string {
|
|
278
|
+
return xdotColorOp('C ', gvColorRgba(c));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Build the xdot "F size len -name " font op. Mirrors xdot_textspan's `F` +
|
|
283
|
+
* `xdot_str(job, "", font->name)` — the length prefix is the byte length of the
|
|
284
|
+
* face name. @see plugin/core/gvrender_core_dot.c:498 xdot_textspan
|
|
285
|
+
*/
|
|
286
|
+
export function xdotFont(size: number, name: string): string {
|
|
287
|
+
return 'F ' + xdotNum(size > 0 ? size : 0) + ' ' + String(utf8Len(name)) + ' -' + name + ' ';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build an xdot length-prefixed string op ("S "/"" prefix): "<pfx><len> -<s> ".
|
|
292
|
+
* @see plugin/core/gvrender_core_dot.c:83 xdot_str_xbuf
|
|
293
|
+
*/
|
|
294
|
+
export function xdotStrOp(prefix: string, s: string): string {
|
|
295
|
+
return prefix + String(utf8Len(s)) + ' -' + s + ' ';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Quote a DOT identifier unless it is a bare id or numeral, mirroring agwrite's
|
|
300
|
+
* agcanonStr so the serialized graph reparses (the comparator reparses both
|
|
301
|
+
* sides). Only `"` is escaped (→ `\"`); a `\` is left as-is — it is already the
|
|
302
|
+
* start of a stored escape like `\n`/`\l` that agcanonStr keeps verbatim, so
|
|
303
|
+
* doubling it (`\\n`) would change the name (`a\n(b\n"c")` must stay `a\n…`, not
|
|
304
|
+
* `a\\n…`). Over-quoting a value native leaves bare is harmless: both parse to
|
|
305
|
+
* the same name. @see lib/cgraph/write.c:_agstrcanon (escapes '"', keeps '\')
|
|
306
|
+
*
|
|
307
|
+
* ONE DELIBERATE DIVERGENCE from _agstrcanon: an ODD trailing backslash run is
|
|
308
|
+
* padded to even. C copies it verbatim and appends the closing quote, so a name
|
|
309
|
+
* ending in a single `\` serializes as `"a\"` — the backslash escapes the quote
|
|
310
|
+
* and the output does not reparse. The DOT lexer cannot produce such a name
|
|
311
|
+
* (source `"a\"` never terminates), so this is unreachable from any file and no
|
|
312
|
+
* corpus id changes; it is reachable only through the programmatic API, where a
|
|
313
|
+
* caller supplies the string directly. Padding costs a name that round-trips to
|
|
314
|
+
* `a\\` instead of `a\`, which beats emitting a document that cannot be parsed.
|
|
315
|
+
* @see CodeQL "Incomplete string escaping or encoding" — the alert's own remedy
|
|
316
|
+
* (escape every `\`) is wrong here and breaks `\n`/`\l` parity.
|
|
317
|
+
*/
|
|
318
|
+
export function xdotId(s: string): string {
|
|
319
|
+
if (/^[A-Za-z_][A-Za-z_0-9]*$/.test(s)) return s;
|
|
320
|
+
if (/^-?(\.[0-9]+|[0-9]+(\.[0-9]*)?)$/.test(s)) return s;
|
|
321
|
+
const body = s.replace(/"/g, '\\"');
|
|
322
|
+
const trailingBackslashes = /\\*$/.exec(body)![0].length;
|
|
323
|
+
return '"' + body + (trailingBackslashes % 2 === 1 ? '\\' : '') + '"';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Format a number as C's `%.5g` — 5 significant figures, trailing zeros and
|
|
328
|
+
* point trimmed, switching to `e±NN` (min 2 exponent digits) when the exponent
|
|
329
|
+
* is < -4 or ≥ 5. Native writes the DOT `pos`/`bb`/`width`/`height` attributes
|
|
330
|
+
* with `%.5g` (output.c:71/294/302), so large coordinates round to 5 sig figs
|
|
331
|
+
* (`2219962` → `2.2201e+06`); `printNum`'s fixed 3 dp keeps too much precision.
|
|
332
|
+
*
|
|
333
|
+
* Uses {@link printfSig} rather than `Number.prototype.toPrecision(5)` so
|
|
334
|
+
* exact-halfway values (e.g. 1399.25) round half-to-even like C's snprintf,
|
|
335
|
+
* not half-away-from-zero like `toPrecision`. @see docs proven case
|
|
336
|
+
* graphs-b786 (circo): pos coordinate 1399.25 → native "1399.2", `toPrecision`
|
|
337
|
+
* gave "1399.3".
|
|
338
|
+
* @see lib/common/output.c:71 (agxbprint "%.5g")
|
|
339
|
+
*/
|
|
340
|
+
export function gfmt5(v: number): string {
|
|
341
|
+
if (!Number.isFinite(v)) return String(v);
|
|
342
|
+
if (v === 0) return '0';
|
|
343
|
+
let s = printfSig(v, 5);
|
|
344
|
+
const e = s.indexOf('e');
|
|
345
|
+
if (e >= 0) {
|
|
346
|
+
let mant = s.slice(0, e);
|
|
347
|
+
if (mant.indexOf('.') >= 0) mant = mant.replace(/0+$/, '').replace(/\.$/, '');
|
|
348
|
+
const ei = parseInt(s.slice(e + 1), 10);
|
|
349
|
+
return mant + 'e' + (ei < 0 ? '-' : '+') + String(Math.abs(ei)).padStart(2, '0');
|
|
350
|
+
}
|
|
351
|
+
if (s.indexOf('.') >= 0) s = s.replace(/0+$/, '').replace(/\.$/, '');
|
|
352
|
+
return s;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Format a number as C's `%.2f` — fixed 2 decimals, half-to-even on exact ties.
|
|
357
|
+
* `attach_attrs` writes the graph-label size attributes `lwidth`/`lheight` in
|
|
358
|
+
* INCHES with `%.2f`, unlike every other computed attribute (which uses `%.5g`).
|
|
359
|
+
* @see lib/common/output.c:244-247 (agxbprint "%.2f", PS2INCH)
|
|
360
|
+
*/
|
|
361
|
+
export function gfmt2(v: number): string {
|
|
362
|
+
return printfFixed(v, 2);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Format a label position as the `x,y` pair every computed `*_lp` attribute
|
|
367
|
+
* uses: both coordinates at `%.5g`. C passes y through `yDir()`, which is the
|
|
368
|
+
* identity unless `Y_invert` — and `Y_invert` is only set by `-Ty` (plain/dot),
|
|
369
|
+
* never for xdot — so no inversion is applied here, exactly as the existing
|
|
370
|
+
* `pos`/`bb` emission does.
|
|
371
|
+
* @see lib/common/output.c:35 yDir · lib/common/output.c:241 (agxbprint "%.5g,%.5g")
|
|
372
|
+
*/
|
|
373
|
+
export function lpStr(p: Point): string {
|
|
374
|
+
return gfmt5(p.x) + ',' + gfmt5(p.y);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Escape backslashes in a LABEL draw string — C's put_escaping_backslashes,
|
|
379
|
+
* applied to the `_ldraw_`/`_hldraw_`/`_tldraw_` buffers (not the shape draws)
|
|
380
|
+
* before agset. A literal `\` in a label's text (e.g. `WXYZ\nabc`) becomes `\\`
|
|
381
|
+
* so it survives the DOT string round-trip; the T-op byte-length prefix stays on
|
|
382
|
+
* the UNescaped text. @see plugin/core/gvrender_core_dot.c:218 put_escaping_backslashes
|
|
383
|
+
*/
|
|
384
|
+
export function escBackslash(s: string): string {
|
|
385
|
+
return s.replace(/\\/g, '\\\\');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** True if s[i..] starts an escape sequence agcanonStr keeps verbatim: a `\`
|
|
389
|
+
* followed by one of E G H L N T l n r \ ". @see lib/cgraph/write.c:is_escape */
|
|
390
|
+
export function isEscapeSeq(s: string, i: number): boolean {
|
|
391
|
+
if (s[i] !== '\\') return false;
|
|
392
|
+
const n = s[i + 1];
|
|
393
|
+
return n === 'E' || n === 'G' || n === 'H' || n === 'L' || n === 'N' || n === 'T'
|
|
394
|
+
|| n === 'l' || n === 'n' || n === 'r' || n === '\\' || n === '"';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Escape a value for a DOT attribute exactly as agwrite's agcanonStr does: a `"`
|
|
399
|
+
* becomes `\"` ONLY when it is not already part of an escape sequence, so an
|
|
400
|
+
* existing `\"` or `\\` in the value is passed through verbatim rather than
|
|
401
|
+
* double-escaped. For an already-backslash-doubled value (node/edge/graph labels,
|
|
402
|
+
* post put_escaping_backslashes) this is identical to a naive `"`→`\"` — every
|
|
403
|
+
* `"` follows a doubled `\\`, so none are ever part_of_escape. For raw cluster
|
|
404
|
+
* labels (agset, no put_escaping) it preserves the source `\"`/`\\` unchanged.
|
|
405
|
+
* @see lib/cgraph/write.c:_agstrcanon (135-167)
|
|
406
|
+
*/
|
|
407
|
+
export function agcanonEscape(s: string): string {
|
|
408
|
+
let out = '';
|
|
409
|
+
let partOfEscape = false;
|
|
410
|
+
for (let i = 0; i < s.length; i++) {
|
|
411
|
+
const c = s[i]!;
|
|
412
|
+
if (c === '"' && !partOfEscape) {
|
|
413
|
+
out += '\\';
|
|
414
|
+
} else if (!partOfEscape && isEscapeSeq(s, i)) {
|
|
415
|
+
partOfEscape = true;
|
|
416
|
+
} else {
|
|
417
|
+
partOfEscape = false;
|
|
418
|
+
}
|
|
419
|
+
out += c;
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Trim a "%.3f" fixed string like C's agxbuf_trim_zeros (trailing 0s + dot). */
|
|
425
|
+
export function trimFixed3(v: number): string {
|
|
426
|
+
let s = v.toFixed(3);
|
|
427
|
+
if (s.indexOf('.') < 0) return s;
|
|
428
|
+
let end = s.length;
|
|
429
|
+
while (end > 0 && s[end - 1] === '0') end--;
|
|
430
|
+
if (s[end - 1] === '.') end--;
|
|
431
|
+
return s.slice(0, end);
|
|
432
|
+
}
|