@knowvah/dot-engine 2.6.11 → 2.6.12
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/async/render-async.d.ts.map +1 -1
- package/dist/common/agerr.d.ts +37 -0
- package/dist/common/agerr.d.ts.map +1 -0
- package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
- package/dist/common/color.d.ts.map +1 -1
- package/dist/common/crand.d.ts +0 -20
- package/dist/common/crand.d.ts.map +1 -1
- package/dist/common/emit-once.d.ts +6 -0
- package/dist/common/emit-once.d.ts.map +1 -0
- package/dist/common/graph-init.d.ts.map +1 -1
- package/dist/common/htmltable-attr-warn.d.ts.map +1 -1
- package/dist/common/htmltable-emit-fill.d.ts +9 -0
- package/dist/common/htmltable-emit-fill.d.ts.map +1 -1
- package/dist/common/htmltable-emit-rules.d.ts.map +1 -1
- package/dist/common/htmltable-lex-expat.d.ts +1 -6
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +1 -7
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/poly-gencode.d.ts.map +1 -1
- package/dist/common/poly-init.d.ts.map +1 -1
- package/dist/common/postproc.d.ts.map +1 -1
- package/dist/common/random.d.ts.map +1 -1
- package/dist/common/render-state.d.ts +8 -0
- package/dist/common/render-state.d.ts.map +1 -0
- package/dist/common/splines-geom.d.ts +8 -0
- package/dist/common/splines-geom.d.ts.map +1 -1
- package/dist/common/splines-routespl.d.ts.map +1 -1
- package/dist/common/textmeasure-lookup.d.ts.map +1 -1
- package/dist/common/textmeasure.d.ts.map +1 -1
- package/dist/gvc/anchor.d.ts.map +1 -1
- package/dist/gvc/device-cluster.d.ts.map +1 -1
- package/dist/gvc/device-labels.d.ts +14 -0
- package/dist/gvc/device-labels.d.ts.map +1 -1
- package/dist/gvc/device.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +765 -451
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/cluster.d.ts.map +1 -1
- package/dist/layout/dot/conc.d.ts.map +1 -1
- package/dist/layout/dot/decomp.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts +18 -10
- package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
- package/dist/layout/dot/mincross-build.d.ts.map +1 -1
- package/dist/layout/dot/mincross-cross.d.ts.map +1 -1
- package/dist/layout/dot/rank.d.ts.map +1 -1
- package/dist/layout/dot/splines.d.ts.map +1 -1
- package/dist/layout/fdp/index.d.ts.map +1 -1
- package/dist/layout/fdp/tlayout-parms.d.ts.map +1 -1
- package/dist/layout/fdp/xlayout.d.ts.map +1 -1
- package/dist/layout/neato/edge-len.d.ts.map +1 -1
- package/dist/layout/neato/fdp-adjust.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +4 -10
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/kk.d.ts.map +1 -1
- package/dist/layout/neato/multispline-router.d.ts.map +1 -1
- package/dist/layout/neato/multispline.d.ts.map +1 -1
- package/dist/layout/neato/poly.d.ts.map +1 -1
- package/dist/layout/neato/sgd.d.ts.map +1 -1
- package/dist/layout/neato/start.d.ts.map +1 -1
- package/dist/ortho/index.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/render/color-resolve.d.ts +49 -1
- package/dist/render/color-resolve.d.ts.map +1 -1
- package/dist/render/public.d.ts.map +1 -1
- package/dist/render/svg-edge-split.d.ts +2 -0
- package/dist/render/svg-edge-split.d.ts.map +1 -1
- package/dist/render/xdot-public.d.ts.map +1 -1
- package/dist/render.js +513 -200
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/async/render-async.ts +6 -0
- package/src/common/agerr.ts +96 -0
- package/src/common/arrows-shapes-poly.ts +19 -5
- package/src/common/color.ts +38 -10
- package/src/common/crand.ts +8 -2
- package/src/common/emit-once.ts +27 -0
- package/src/common/graph-init.ts +2 -1
- package/src/common/htmltable-attr-warn.ts +3 -4
- package/src/common/htmltable-emit-fill.ts +48 -0
- package/src/common/htmltable-emit-rules.ts +2 -0
- package/src/common/htmltable-lex-expat.ts +78 -23
- package/src/common/htmltable.ts +15 -16
- package/src/common/poly-gencode.ts +14 -2
- package/src/common/poly-init.ts +2 -1
- package/src/common/postproc.ts +4 -1
- package/src/common/random.ts +11 -1
- package/src/common/render-state.ts +36 -0
- package/src/common/splines-geom.ts +8 -4
- package/src/common/splines-routespl.ts +2 -1
- package/src/common/textmeasure-lookup.ts +10 -3
- package/src/common/textmeasure.ts +45 -2
- package/src/gvc/anchor.ts +1 -12
- package/src/gvc/device-cluster.ts +37 -9
- package/src/gvc/device-labels.ts +27 -0
- package/src/gvc/device.ts +18 -6
- package/src/index.ts +3 -0
- package/src/layout/dot/cluster.ts +2 -1
- package/src/layout/dot/conc.ts +6 -5
- package/src/layout/dot/decomp.ts +3 -0
- package/src/layout/dot/edge-route-clip.ts +27 -21
- package/src/layout/dot/mincross-build.ts +2 -0
- package/src/layout/dot/mincross-cross.ts +2 -0
- package/src/layout/dot/rank.ts +2 -0
- package/src/layout/dot/splines.ts +2 -1
- package/src/layout/fdp/index.ts +2 -1
- package/src/layout/fdp/tlayout-parms.ts +9 -2
- package/src/layout/fdp/xlayout.ts +10 -2
- package/src/layout/neato/cdt-surface.ts +6 -6
- package/src/layout/neato/edge-len.ts +2 -1
- package/src/layout/neato/fdp-adjust.ts +3 -2
- package/src/layout/neato/init.ts +11 -11
- package/src/layout/neato/kk.ts +2 -1
- package/src/layout/neato/multispline-router.ts +3 -3
- package/src/layout/neato/multispline.ts +3 -2
- package/src/layout/neato/poly.ts +3 -2
- package/src/layout/neato/sgd.ts +5 -2
- package/src/layout/neato/start.ts +14 -10
- package/src/ortho/index.ts +3 -1
- package/src/parser/dot.js +4 -1
- package/src/parser/dot.pegjs +5 -1
- package/src/parser/index.ts +4 -3
- package/src/render/color-resolve.ts +193 -4
- package/src/render/public.ts +2 -0
- package/src/render/svg-edge-split.ts +4 -2
- package/src/render/xdot-public.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.12",
|
|
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",
|
|
@@ -18,6 +18,7 @@ import type { EngineName } from '../gvc/context.js';
|
|
|
18
18
|
import { createDefaultContext } from '../gvc/default-context.js';
|
|
19
19
|
import { checkRenderArgs } from '../render/public.js';
|
|
20
20
|
import { render as deviceRender } from '../gvc/device.js';
|
|
21
|
+
import { resetRenderState } from '../common/render-state.js';
|
|
21
22
|
import type { ImageSizer } from '../common/htmltable-types.js';
|
|
22
23
|
import type { ImageResolver } from '../gvc/image-resolver.js';
|
|
23
24
|
import type { OutputFormat, RenderOptions } from '../render/public.js';
|
|
@@ -148,6 +149,11 @@ export async function renderAsync(
|
|
|
148
149
|
const engine: EngineName = opts?.engine ?? 'dot';
|
|
149
150
|
try {
|
|
150
151
|
const { fontIssues, sizes, bytes } = await prefetchAll(g, opts);
|
|
152
|
+
// AD-4: reset after the last await, so the synchronous layout + render
|
|
153
|
+
// below starts from C's initial process state even when other renders ran
|
|
154
|
+
// while the prefetch was pending. renderSvgAsync and renderSvgInto reach
|
|
155
|
+
// this through renderAsync.
|
|
156
|
+
resetRenderState();
|
|
151
157
|
// Created after fonts settle so the measurer sees the loaded faces.
|
|
152
158
|
const ctx = createDefaultContext();
|
|
153
159
|
if (opts?.imageSizer !== undefined) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one stderr writer for messages C prints through cgraph's agerr family
|
|
5
|
+
* (`agerr`, `agerrorf`, `agwarningf`, `AGPREV` continuations). Sites whose C
|
|
6
|
+
* counterpart is `fprintf(stderr)`/`fputs`/`printf` do not use it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { registerRenderState } from './render-state.js';
|
|
10
|
+
|
|
11
|
+
/** `agerrlevel_t` as the port uses it: AGERR, AGWARN, AGPREV. */
|
|
12
|
+
export type AgerrLevel = 'error' | 'warning' | 'prev';
|
|
13
|
+
|
|
14
|
+
/** The console channel a line goes to. */
|
|
15
|
+
export type AgerrChannel = 'warn' | 'error';
|
|
16
|
+
|
|
17
|
+
const PREFIX_ERROR = 'Error';
|
|
18
|
+
const PREFIX_WARNING = 'Warning';
|
|
19
|
+
const PREFIX_SEP = ': ';
|
|
20
|
+
const DEL = 0x7f;
|
|
21
|
+
const LAST_CNTRL = 0x1f;
|
|
22
|
+
const WHITESPACE = new Set([0x09, 0x0a, 0x0b, 0x0c, 0x0d]);
|
|
23
|
+
const OCTAL_WIDTH = 3;
|
|
24
|
+
const OCTAL_RADIX = 8;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The level of the last non-AGPREV message, which AGPREV continues: C's
|
|
28
|
+
* `agerrno`, zero-initialised, i.e. AGWARN. Reset per render (AD-4).
|
|
29
|
+
* @see lib/cgraph/agerror.c:agerrno
|
|
30
|
+
*/
|
|
31
|
+
let lastLevel: 'error' | 'warning' = 'warning';
|
|
32
|
+
|
|
33
|
+
registerRenderState(() => {
|
|
34
|
+
lastLevel = 'warning';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `default_usererrf`: every character that `gv_iscntrl(c) && !gv_isspace(c)`
|
|
39
|
+
* (0x00-0x1F except `\t\n\v\f\r`, and 0x7F) is written as `\ooo`.
|
|
40
|
+
* @see lib/cgraph/agerror.c:default_usererrf
|
|
41
|
+
*/
|
|
42
|
+
export function escapeControl(message: string): string {
|
|
43
|
+
let out = '';
|
|
44
|
+
for (const ch of message) {
|
|
45
|
+
const c = ch.codePointAt(0)!;
|
|
46
|
+
if ((c <= LAST_CNTRL || c === DEL) && !WHITESPACE.has(c)) {
|
|
47
|
+
out += `\\${c.toString(OCTAL_RADIX).padStart(OCTAL_WIDTH, '0')}`;
|
|
48
|
+
} else out += ch;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Write one already-assembled stderr line (prefix included) on a console
|
|
55
|
+
* channel, escaped as `default_usererrf` would.
|
|
56
|
+
*/
|
|
57
|
+
export function agerrLineOn(channel: AgerrChannel, line: string): void {
|
|
58
|
+
if (channel === 'warn') console.warn(escapeControl(line));
|
|
59
|
+
else console.error(escapeControl(line));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The part of C's `agerr(level, ...)` that is not formatting: the `Error: `/
|
|
64
|
+
* `Warning: ` prefix (none for AGPREV), escaping, and the console channel
|
|
65
|
+
* (`console.warn` for warnings, `console.error` otherwise; AGPREV continues
|
|
66
|
+
* the previous level's channel).
|
|
67
|
+
* @see lib/cgraph/agerror.c:out
|
|
68
|
+
*/
|
|
69
|
+
export function agerr(level: AgerrLevel, msg: string, channel?: AgerrChannel): void {
|
|
70
|
+
if (level === 'prev') {
|
|
71
|
+
agerrLineOn(channel ?? (lastLevel === 'warning' ? 'warn' : 'error'), msg);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
lastLevel = level;
|
|
75
|
+
const prefix = level === 'error' ? PREFIX_ERROR : PREFIX_WARNING;
|
|
76
|
+
agerrLineOn(channel ?? (level === 'warning' ? 'warn' : 'error'), `${prefix}${PREFIX_SEP}${msg}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @see lib/cgraph/agerror.c:agerrorf */
|
|
80
|
+
export const agerrorf = (msg: string, channel?: AgerrChannel): void => agerr('error', msg, channel);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @see lib/cgraph/agerror.c:agwarningf
|
|
84
|
+
* `channel` overrides the console channel for sites that have always written
|
|
85
|
+
* their warning on `console.error`.
|
|
86
|
+
*/
|
|
87
|
+
export const agwarningf = (msg: string, channel?: AgerrChannel): void =>
|
|
88
|
+
agerr('warning', msg, channel);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A finished line whose prefix is already part of the text. The channel
|
|
92
|
+
* follows the prefix (`Warning` -> `console.warn`) unless `channel` is given.
|
|
93
|
+
*/
|
|
94
|
+
export function agerrFinished(line: string, channel?: AgerrChannel): void {
|
|
95
|
+
agerrLineOn(channel ?? (line.startsWith(PREFIX_WARNING) ? 'warn' : 'error'), line);
|
|
96
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { Point } from '../model/geom.js';
|
|
12
12
|
import { ccwrotatepf, cwrotatepf } from '../model/geom.js';
|
|
13
13
|
import { bezier } from './bezier.js';
|
|
14
|
+
import { fma } from './fma.js';
|
|
14
15
|
import type { GenResult } from './arrows-shapes-util.js';
|
|
15
16
|
import {
|
|
16
17
|
vsub, vadd, miterShape, negUnit, backwardDelta, axialProjection,
|
|
@@ -206,14 +207,27 @@ function cubicHalf(c: Point[], rightHalf: boolean): Point[] {
|
|
|
206
207
|
return half;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Build the four Bézier control points for the curve arms.
|
|
212
|
+
*
|
|
213
|
+
* The oracle (libgvc 16.1 `_arrow_type_curve`) contracts only the
|
|
214
|
+
* `p.x +/- 0.95 * v.x` term into `fmadd(v.x, +/-0.95, p.x)`; every other add
|
|
215
|
+
* is a plain fadd, and `w.x * 4.0 / 3.0` is `(w.x * 4) / 3` (fmul, fdiv).
|
|
216
|
+
* Only the x arms carry the 0.95 term, so AF[1].y / AF[2].y stay unfused.
|
|
217
|
+
* @see lib/common/arrows.c:arrow_type_curve
|
|
218
|
+
*/
|
|
210
219
|
function curveControlPoints(p: Point, v: Point, w: Point, flag: number): Point[] {
|
|
211
220
|
const AF0: Point = { x: p.x + v.x + w.x, y: p.y + v.y + w.y };
|
|
212
221
|
const AF3: Point = { x: p.x - v.x + w.x, y: p.y - v.y + w.y };
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
222
|
+
const bulgeX = (w.x * 4.0) / 3.0;
|
|
223
|
+
const bulgeY = (w.y * 4.0) / 3.0;
|
|
224
|
+
const x1 = fma(v.x, 0.95, p.x) + w.x;
|
|
225
|
+
const x2 = fma(v.x, -0.95, p.x) + w.x;
|
|
226
|
+
if (flag & ARR_MOD_INV) { /* ----(-| */
|
|
227
|
+
return [AF0, { x: x1 + bulgeX, y: AF0.y + bulgeY }, { x: x2 + bulgeX, y: AF3.y + bulgeY }, AF3];
|
|
228
|
+
}
|
|
229
|
+
/* ----)-| */
|
|
230
|
+
return [AF0, { x: x1 - bulgeX, y: AF0.y - bulgeY }, { x: x2 - bulgeX, y: AF3.y - bulgeY }, AF3];
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
/** @see lib/common/arrows.c:arrow_type_curve */
|
package/src/common/color.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { isExactlyEqual } from '../util/math.js';
|
|
9
9
|
import { COLOR_LIB } from './colorData.js';
|
|
10
|
+
import { registerRenderState } from './render-state.js';
|
|
10
11
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Public types
|
|
@@ -37,6 +38,7 @@ interface HsvaTuple { H: number; S: number; V: number; A: number }
|
|
|
37
38
|
// ---------------------------------------------------------------------------
|
|
38
39
|
|
|
39
40
|
let colorScheme: string = '';
|
|
41
|
+
registerRenderState(() => { colorScheme = ''; });
|
|
40
42
|
|
|
41
43
|
/** @see lib/common/colxlate.c:setColorScheme */
|
|
42
44
|
export function setColorScheme(scheme: string): void {
|
|
@@ -256,17 +258,43 @@ function setBlackOpaque(color: GVColor, targetType: ColorType): void {
|
|
|
256
258
|
// Hex and HSV string parsers
|
|
257
259
|
// ---------------------------------------------------------------------------
|
|
258
260
|
|
|
259
|
-
/**
|
|
261
|
+
/** C isspace in the "C" locale. */
|
|
262
|
+
const SSCANF_SPACE = new Set([' ', '\t', '\n', '\v', '\f', '\r']);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* `sscanf(p, "#" + "%<width>x" * max)`: the values converted before the first
|
|
266
|
+
* failure. Each `%x` skips leading white space then takes up to `width` hex
|
|
267
|
+
* digits, so a short or odd-length run still converts ("#12345" gives 0x12,
|
|
268
|
+
* 0x34, 0x5). Sign and `0x` prefixes are not modelled.
|
|
269
|
+
* @see lib/common/colxlate.c:colorxlate
|
|
270
|
+
*/
|
|
271
|
+
function scanHexGroups(p: string, width: number, max: number): number[] {
|
|
272
|
+
const out: number[] = [];
|
|
273
|
+
if (p.charAt(0) !== '#') return out;
|
|
274
|
+
let i = 1;
|
|
275
|
+
while (out.length < max) {
|
|
276
|
+
while (SSCANF_SPACE.has(p.charAt(i))) i++;
|
|
277
|
+
let j = i;
|
|
278
|
+
while (j < p.length && j - i < width && /[0-9a-fA-F]/.test(p.charAt(j))) j++;
|
|
279
|
+
if (j === i) break;
|
|
280
|
+
out.push(parseInt(p.slice(i, j), 16));
|
|
281
|
+
i = j;
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse "#rrggbb", "#rrggbbaa" (any run of at least three two-digit groups),
|
|
288
|
+
* or the 4-character "#rgb" form, as colorxlate's sscanf calls do. Returns
|
|
289
|
+
* null on failure.
|
|
290
|
+
*/
|
|
260
291
|
function parseHexColor(p: string): RgbaTuple | null {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!m3) return null;
|
|
268
|
-
const r = parseInt(m3[1], 16); const g = parseInt(m3[2], 16); const b = parseInt(m3[3], 16);
|
|
269
|
-
return { r: r | (r << 4), g: g | (g << 4), b: b | (b << 4), a: 255 };
|
|
292
|
+
const g = scanHexGroups(p, 2, 4);
|
|
293
|
+
if (g.length >= 3) return { r: g[0]!, g: g[1]!, b: g[2]!, a: g[3] ?? 255 };
|
|
294
|
+
const short = p.length === 4 ? scanHexGroups(p, 1, 3) : [];
|
|
295
|
+
if (short.length !== 3) return null;
|
|
296
|
+
const [r, gg, b] = short as [number, number, number];
|
|
297
|
+
return { r: r | (r << 4), g: gg | (gg << 4), b: b | (b << 4), a: 255 };
|
|
270
298
|
}
|
|
271
299
|
|
|
272
300
|
/** Clamp a number to [0, 1]. */
|
package/src/common/crand.ts
CHANGED
|
@@ -21,11 +21,17 @@
|
|
|
21
21
|
* @see lib/util/random.c:gv_random / gv_permutation (15.0.0)
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { registerRenderState } from './render-state.js';
|
|
25
|
+
|
|
24
26
|
/** RAND_MAX on the reference platform. */
|
|
25
27
|
export const C_RAND_MAX = 2147483647;
|
|
26
28
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
+
/** Unseeded C rand() behaves as srand(1). */
|
|
30
|
+
const UNSEEDED_STATE = 1;
|
|
31
|
+
|
|
32
|
+
/** Current stream state. */
|
|
33
|
+
let state = UNSEEDED_STATE;
|
|
34
|
+
registerRenderState(() => { state = UNSEEDED_STATE; });
|
|
29
35
|
|
|
30
36
|
/** srand(3). */
|
|
31
37
|
export function csrand(seed: number): void {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Once-per-process message gate. C keeps the seen strings in a file static
|
|
5
|
+
* dictionary that is cleared only by `emit_once_reset` in `gvFreeContext`; one
|
|
6
|
+
* native run is one process, so the port clears it per render (AD-4).
|
|
7
|
+
*
|
|
8
|
+
* @see lib/common/emit.c:emit_once
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { registerRenderState } from './render-state.js';
|
|
12
|
+
|
|
13
|
+
const seen = new Set<string>();
|
|
14
|
+
|
|
15
|
+
registerRenderState(() => {
|
|
16
|
+
seen.clear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* True the first time `str` is offered since the last reset, false after.
|
|
21
|
+
* @see lib/common/emit.c:emit_once
|
|
22
|
+
*/
|
|
23
|
+
export function emitOnce(str: string): boolean {
|
|
24
|
+
if (seen.has(str)) return false;
|
|
25
|
+
seen.add(str);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
package/src/common/graph-init.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type { RatioKind } from '../model/layoutParams.js';
|
|
|
29
29
|
import { doGraphLabel } from '../layout/dot/graph-label.js';
|
|
30
30
|
import { mapbool, setClType, LOCAL, GLOBAL, NOCLUST } from '../layout/dot/rank.js';
|
|
31
31
|
import { cround } from './arith.js';
|
|
32
|
+
import { agwarningf } from './agerr.js';
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Constants — @see lib/common/const.h
|
|
@@ -88,7 +89,7 @@ export function findCharset(g: Graph): number {
|
|
|
88
89
|
}
|
|
89
90
|
if (p === 'big-5' || p === 'big5') return CHAR_BIG5;
|
|
90
91
|
if (p === 'utf-8' || p === 'utf8') return CHAR_UTF8;
|
|
91
|
-
|
|
92
|
+
agwarningf(`Unsupported charset "${raw}" - assuming utf-8`, 'error');
|
|
92
93
|
return CHAR_UTF8;
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* @see lib/common/htmllex.c:doAttrs
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { agerrFinished } from './agerr.js';
|
|
15
|
+
|
|
14
16
|
/** An attribute as startElement gets it: name in source case, value decoded by expat. */
|
|
15
17
|
export type AttrEntry = readonly [name: string, value: string];
|
|
16
18
|
|
|
@@ -272,10 +274,7 @@ export class StderrLines {
|
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
/** A finished stderr line on the console channel the port uses for it. */
|
|
275
|
-
export const consoleLine = (line: string): void =>
|
|
276
|
-
if (line.startsWith('Warning')) console.warn(line);
|
|
277
|
-
else console.error(line);
|
|
278
|
-
};
|
|
277
|
+
export const consoleLine = (line: string): void => agerrFinished(line);
|
|
279
278
|
|
|
280
279
|
/** One label's stderr: what make_html_label and make_label print, in order. */
|
|
281
280
|
export const labelStderr = (): StderrLines => new StderrLines(consoleLine);
|
|
@@ -18,6 +18,7 @@ import { FillType, PenType } from '../gvc/context.js';
|
|
|
18
18
|
import { transformPoint } from '../gvc/device.js';
|
|
19
19
|
import { resolveRenderColor } from '../render/color-resolve.js';
|
|
20
20
|
import { emitRoundedBezier } from './poly-shapes.js';
|
|
21
|
+
import type { PlacedHtml, PlacedCell } from './htmltable-pos.js';
|
|
21
22
|
import { htmlStyle } from './htmltable-style.js';
|
|
22
23
|
import {
|
|
23
24
|
BORDER_LEFT,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
BORDER_BOTTOM,
|
|
27
28
|
BORDER_MASK,
|
|
28
29
|
} from './htmltable-types.js';
|
|
30
|
+
import { registerRenderState } from './render-state.js';
|
|
29
31
|
|
|
30
32
|
// ---------------------------------------------------------------------------
|
|
31
33
|
// Gradient spec parser
|
|
@@ -136,6 +138,7 @@ export function htmlFillPenWidth(): number {
|
|
|
136
138
|
export function resetHtmlFillPenWidth(): void {
|
|
137
139
|
htmlFillPen = 1.0;
|
|
138
140
|
}
|
|
141
|
+
registerRenderState(resetHtmlFillPenWidth);
|
|
139
142
|
|
|
140
143
|
/** Map an HTML STYLE to the C pen type: dashed wins over dotted. @see lib/common/htmltable.c:doBorder */
|
|
141
144
|
function penTypeOf(style: string | undefined): PenType {
|
|
@@ -366,3 +369,48 @@ function emitSidedPolylines(
|
|
|
366
369
|
renderer.polyline(seq.map((i) => corners[i]!), job);
|
|
367
370
|
}
|
|
368
371
|
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// htmlLabelColors — the colour names C resolves while emitting an HTML label
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
/** @see lib/common/const.h DEFAULT_COLOR */
|
|
378
|
+
const HTML_DEFAULT_COLOR = 'black';
|
|
379
|
+
|
|
380
|
+
/** setFill's names: the fill (and a gradient's stop). @see htmltable.c:setFill */
|
|
381
|
+
function bgNames(bgcolor: string): string[] {
|
|
382
|
+
const spec = parseGradientSpec(bgcolor);
|
|
383
|
+
return spec === null ? [bgcolor] : [spec[0], spec[1]];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** One cell in emit_html_cell order: fill, border, then the child. */
|
|
387
|
+
function cellNames(cell: PlacedCell): string[] {
|
|
388
|
+
if (htmlStyle(cell.style).invisible) return [];
|
|
389
|
+
const out = cell.bgcolor !== undefined ? bgNames(cell.bgcolor) : [];
|
|
390
|
+
if (cell.border > 0) out.push(cell.color ?? HTML_DEFAULT_COLOR);
|
|
391
|
+
if (cell.nested !== undefined) out.push(...tableNames(cell.nested));
|
|
392
|
+
else for (const line of cell.lines) if (line.fontColor !== null) out.push(line.fontColor);
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** One table in emit_html_tbl order: fill, rules, border, cells. */
|
|
397
|
+
function tableNames(tbl: PlacedHtml): string[] {
|
|
398
|
+
if (htmlStyle(tbl.style).invisible) return [];
|
|
399
|
+
const out = tbl.bgcolor !== undefined ? bgNames(tbl.bgcolor) : [];
|
|
400
|
+
const color = tbl.color ?? HTML_DEFAULT_COLOR;
|
|
401
|
+
if (tbl.cells.some((c) => c.vruled === true || c.hruled === true)) out.push(color);
|
|
402
|
+
if (tbl.border > 0) out.push(color);
|
|
403
|
+
for (const cell of tbl.cells) out.push(...cellNames(cell));
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The names an HTML label hands to gvrender_resolve_color, in C's order: the
|
|
409
|
+
* root table's pen first (emit_html_label), then emit_html_tbl's walk.
|
|
410
|
+
* @see lib/common/htmltable.c:emit_html_label
|
|
411
|
+
* @see lib/common/htmltable.c:emit_html_tbl
|
|
412
|
+
* @see lib/common/htmltable.c:emit_html_cell
|
|
413
|
+
*/
|
|
414
|
+
export function htmlLabelColors(label: PlacedHtml): string[] {
|
|
415
|
+
return [label.color ?? HTML_DEFAULT_COLOR, ...tableNames(label)];
|
|
416
|
+
}
|
|
@@ -14,6 +14,7 @@ import type { RenderJob } from '../gvc/job.js';
|
|
|
14
14
|
import type { RendererPlugin } from '../gvc/context.js';
|
|
15
15
|
import type { PlacedCell, PlacedHtml } from './htmltable-pos.js';
|
|
16
16
|
import { doSide, withHtmlPaint } from './htmltable-emit-fill.js';
|
|
17
|
+
import { registerRenderState } from './render-state.js';
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Rule context
|
|
@@ -217,6 +218,7 @@ export function resetHtmlAnchorIds(): void {
|
|
|
217
218
|
anchorEnv.objLabel = null;
|
|
218
219
|
anchorEnv.imgscale = 'false';
|
|
219
220
|
}
|
|
221
|
+
registerRenderState(resetHtmlAnchorIds);
|
|
220
222
|
|
|
221
223
|
/**
|
|
222
224
|
* Open an anchor when the data carries href/title/target.
|
|
@@ -270,12 +270,19 @@ function scanCdata(text: string, p: number, base: number, dataOf: CdataText, tok
|
|
|
270
270
|
return close < 0 ? -1 : close + CDATA_CLOSE.length;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
/**
|
|
274
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Character data at `p` up to the next `<`: one `text` token, then the index it
|
|
275
|
+
* ends at. A reference cut off by the end of the data is not part of the token
|
|
276
|
+
* (`held`): expat waits for the characters that settle it.
|
|
277
|
+
* @see expat lib/xmltok_impl.c:PREFIX(contentTok) (XML_TOK_PARTIAL)
|
|
278
|
+
*/
|
|
279
|
+
function scanText(text: string, p: number, base: number, tokens: ExpatToken[]): { at: number; held: boolean } {
|
|
275
280
|
const e = text.indexOf('<', p);
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
const slice = text.slice(p, e < 0 ? text.length : e);
|
|
282
|
+
const from = e < 0 ? heldRefStart(slice) : undefined;
|
|
283
|
+
const end = from === undefined ? p + slice.length : p + from;
|
|
284
|
+
if (end > p) tokens.push({ kind: 'text', start: base + p, end: base + end });
|
|
285
|
+
return { at: end, held: from !== undefined };
|
|
279
286
|
}
|
|
280
287
|
|
|
281
288
|
/** The fed text of the label source between two offsets. */
|
|
@@ -295,7 +302,9 @@ function scanContent(text: string, base: number, inCdata: boolean, dataOf: Cdata
|
|
|
295
302
|
continue;
|
|
296
303
|
}
|
|
297
304
|
if (text[p] !== '<') {
|
|
298
|
-
|
|
305
|
+
const scanned = scanText(text, p, base, tokens);
|
|
306
|
+
if (scanned.held) return { tokens, consumed: scanned.at, inCdata: false };
|
|
307
|
+
p = scanned.at;
|
|
299
308
|
continue;
|
|
300
309
|
}
|
|
301
310
|
const { scan, kind } = scanMarkup(text, p);
|
|
@@ -408,33 +417,79 @@ export class ExpatFeed {
|
|
|
408
417
|
/** Entities expat knows without a DTD; `scanEntity` leaves them as written. */
|
|
409
418
|
const XML_PREDEFINED_ENTITIES = new Set(['lt', 'gt', 'amp', 'quot', 'apos']);
|
|
410
419
|
|
|
411
|
-
|
|
420
|
+
/** An `&name;` that `scanEntity` rewrites: 2 to 8 characters (alphanumeric) that name an HTML entity. */
|
|
421
|
+
const SCANNED_ENTITY = /^&([^;&<]*);/;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The reference the content tokenizer reads at the `&` at `t`: the index after
|
|
425
|
+
* its `;`, or the index of the character that makes it an invalid token (the
|
|
426
|
+
* end of `text` when the reference runs out: `</HTML>` or another tag follows).
|
|
427
|
+
* Runs the attribute value scanner over the reference alone, closed by a quote,
|
|
428
|
+
* which is scanRef on the same bytes.
|
|
429
|
+
* @see expat lib/xmltok_impl.c:PREFIX(contentTok) (BT_AMP)
|
|
430
|
+
* @see expat lib/xmltok_impl.c:PREFIX(scanRef)
|
|
431
|
+
* @see expat lib/xmltok_impl.c:PREFIX(scanCharRef)
|
|
432
|
+
* @see expat lib/xmltok_impl.c:PREFIX(scanHexCharRef)
|
|
433
|
+
*/
|
|
434
|
+
function contentRef(text: string, t: number): { end: number } | { invalid: number } {
|
|
435
|
+
const semi = text.indexOf(';', t);
|
|
436
|
+
const ref = text.slice(t, semi < 0 ? text.length : semi + 1);
|
|
437
|
+
const scan = scanAttValue(`"${ref}"`, 0);
|
|
438
|
+
if ('close' in scan) return { end: t + scan.close - 1 };
|
|
439
|
+
return { invalid: 'invalid' in scan ? t + scan.invalid - 1 : text.length };
|
|
440
|
+
}
|
|
412
441
|
|
|
413
|
-
|
|
442
|
+
/** A reference problem of character data: expat's error, where its event pointer is, and the `&` it began at. */
|
|
443
|
+
interface RefProblem {
|
|
444
|
+
message: string;
|
|
445
|
+
offset: number;
|
|
446
|
+
from: number;
|
|
447
|
+
}
|
|
414
448
|
|
|
415
449
|
/**
|
|
416
|
-
* The first reference of character data that expat rejects
|
|
417
|
-
*
|
|
418
|
-
* a character reference XML
|
|
419
|
-
*
|
|
450
|
+
* The first reference of character data that expat rejects: an invalid token
|
|
451
|
+
* (a bare `&`, `&#;`, `A`, `Aq`, ..), a `&name;` scanEntity leaves
|
|
452
|
+
* alone (neither an HTML entity nor predefined), or a character reference XML
|
|
453
|
+
* does not allow. An invalid token at the very end of `text` is a reference
|
|
454
|
+
* expat is still waiting on.
|
|
455
|
+
* @see lib/common/utils.c:scanEntity
|
|
456
|
+
* @see expat lib/xmlparse.c:doContent (XML_TOK_INVALID, XML_TOK_ENTITY_REF, XML_TOK_CHAR_REF)
|
|
420
457
|
*/
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
458
|
+
function firstRefProblem(text: string): RefProblem | undefined {
|
|
459
|
+
let t = text.indexOf('&');
|
|
460
|
+
while (t >= 0) {
|
|
461
|
+
if (text[t + 1] !== '#') {
|
|
462
|
+
const name = SCANNED_ENTITY.exec(text.slice(t))?.[1];
|
|
463
|
+
if (name !== undefined && name.length >= 2 && Object.hasOwn(HTML_ENTITIES, name)) {
|
|
464
|
+
t = text.indexOf('&', t + name.length + 2);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
427
467
|
}
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
430
|
-
const key = text.slice(t + 1,
|
|
431
|
-
if (
|
|
432
|
-
return { message:
|
|
468
|
+
const ref = contentRef(text, t);
|
|
469
|
+
if ('invalid' in ref) return { message: XML_ERROR_INVALID_TOKEN, offset: ref.invalid, from: t };
|
|
470
|
+
const key = text.slice(t + 1, ref.end - 1);
|
|
471
|
+
if (text[t + 1] === '#') {
|
|
472
|
+
if (charRefNumber(`&${key};`) < 0) return { message: XML_ERROR_BAD_CHAR_REF, offset: t, from: t };
|
|
473
|
+
} else if (!XML_PREDEFINED_ENTITIES.has(key)) {
|
|
474
|
+
return { message: XML_ERROR_UNDEFINED_ENTITY, offset: t, from: t };
|
|
433
475
|
}
|
|
476
|
+
t = text.indexOf('&', ref.end);
|
|
434
477
|
}
|
|
435
478
|
return undefined;
|
|
436
479
|
}
|
|
437
480
|
|
|
481
|
+
/** The first reference of character data that expat rejects, and the offset of its event pointer. */
|
|
482
|
+
export function textRefError(text: string): { message: string; offset: number } | undefined {
|
|
483
|
+
const problem = firstRefProblem(text);
|
|
484
|
+
return problem === undefined ? undefined : { message: problem.message, offset: problem.offset };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Where a reference that runs to the end of `text` begins: expat holds it until more data arrives. */
|
|
488
|
+
function heldRefStart(text: string): number | undefined {
|
|
489
|
+
const problem = firstRefProblem(text);
|
|
490
|
+
return problem?.message === XML_ERROR_INVALID_TOKEN && problem.offset === text.length ? problem.from : undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
438
493
|
export interface TagChunk {
|
|
439
494
|
close: boolean;
|
|
440
495
|
selfClose: boolean;
|
package/src/common/htmltable.ts
CHANGED
|
@@ -169,25 +169,24 @@ const itemHasFlags = (i: HtmlTextItem): boolean =>
|
|
|
169
169
|
i.subscript === true || i.superscript === true;
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
|
-
* C's "simple" test: one item per line, no font
|
|
173
|
-
* resolved face and size across all lines.
|
|
172
|
+
* C's "simple" test over the line list (C spans): one item per line, no font
|
|
173
|
+
* flags, uniform resolved face and size across all lines. It reads each
|
|
174
|
+
* line's first item, so the "" item appendFLineList gives an empty <BR/> line
|
|
175
|
+
* counts with its font: `k<U><BR/><BR/></U>` is not simple.
|
|
174
176
|
* @see lib/common/htmltable.c:size_html_txt (lines 946-986)
|
|
175
177
|
*/
|
|
176
|
-
|
|
178
|
+
const htmlTextIsSimple = (lines: SizedLine[], env: HtmlSizeEnv): boolean => {
|
|
177
179
|
let prevFs = -1;
|
|
178
180
|
let prevFace: string | null = null;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (prevFace === null) prevFace = face;
|
|
189
|
-
else if (face !== prevFace) return false;
|
|
190
|
-
}
|
|
181
|
+
for (const ln of lines) {
|
|
182
|
+
// splitSizedLines never yields an itemless line (C: nitems >= 1).
|
|
183
|
+
const item = ln.items[0]!;
|
|
184
|
+
if (ln.items.length > 1 || itemHasFlags(item)) return false;
|
|
185
|
+
const { fs, face } = itemFont(item, env);
|
|
186
|
+
if (prevFs < 0) prevFs = fs;
|
|
187
|
+
else if (fs !== prevFs) return false;
|
|
188
|
+
if (prevFace === null) prevFace = face;
|
|
189
|
+
else if (face !== prevFace) return false;
|
|
191
190
|
}
|
|
192
191
|
return true;
|
|
193
192
|
};
|
|
@@ -203,7 +202,7 @@ const sizeTextContent = (
|
|
|
203
202
|
env: HtmlSizeEnv = defaultSizeEnv(),
|
|
204
203
|
): { w: number; h: number } => {
|
|
205
204
|
const lines = splitSizedLines(texts, measurer, env);
|
|
206
|
-
const simple = htmlTextIsSimple(
|
|
205
|
+
const simple = htmlTextIsSimple(lines, env);
|
|
207
206
|
let w = 0;
|
|
208
207
|
let h = 0;
|
|
209
208
|
for (const ln of lines) {
|
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
styleTokens,
|
|
34
34
|
} from './style-resolve.js';
|
|
35
35
|
import { stripedBox, wedgedEllipse } from '../render/svg-multicolor.js';
|
|
36
|
-
import { resolveRenderColor } from '../render/color-resolve.js';
|
|
36
|
+
import { resolveRenderColor, reportColorNames } from '../render/color-resolve.js';
|
|
37
|
+
import { htmlLabelColors } from './htmltable-emit-fill.js';
|
|
37
38
|
import { svgNodeId } from '../render/svg-id.js';
|
|
38
39
|
import { drawRoundCorners, mcircleHack, underlineDraw } from './poly-shapes.js';
|
|
39
40
|
|
|
@@ -191,6 +192,8 @@ export function renderLabel(
|
|
|
191
192
|
job: RenderJob,
|
|
192
193
|
): void {
|
|
193
194
|
if (label.u.kind !== 'txt' || label.u.nspans < 1) return;
|
|
195
|
+
// gvrender_set_pencolor(job, lp->fontcolor), once, before the spans.
|
|
196
|
+
reportColorNames([label.fontcolor]);
|
|
194
197
|
label.pos = coord;
|
|
195
198
|
label.set = true;
|
|
196
199
|
// First-span baseline per valign (labelloc). @see lib/common/labels.c:240-251
|
|
@@ -227,6 +230,7 @@ function renderNodeLabel(
|
|
|
227
230
|
// Route the HTML label's box/fill/text ops into the node LABEL emit-state
|
|
228
231
|
// (→ _ldraw_ for xdot), not the node DRAW state. SVG ignores emit_state.
|
|
229
232
|
const html = label.u.html as PlacedHtml;
|
|
233
|
+
reportColorNames(htmlLabelColors(html));
|
|
230
234
|
withLabelEmitState(job, () => emitHtmlLabel(html, coord, renderer, job));
|
|
231
235
|
} else {
|
|
232
236
|
renderLabel(label, coord, renderer, job);
|
|
@@ -367,8 +371,16 @@ export function applyNodeStyle(obj: ObjState, n: Node, job: RenderJob): boolean
|
|
|
367
371
|
// the whole node emission (emit_begin_node..emit_end_node), which covers both
|
|
368
372
|
// this color block and the label textspans. Both callers of this function are
|
|
369
373
|
// codefns, so they already run inside that window. @see gvc/device.ts
|
|
374
|
+
const penwidth = nodeAttr(n, n.root, 'penwidth');
|
|
375
|
+
// record_gencode resolves the pen (penColor) before the fill; poly_gencode
|
|
376
|
+
// and point_gencode resolve the fill first. Only the order in which an
|
|
377
|
+
// unknown colour is reported differs. @see shapes.c:3890-3892 vs 2985-3004
|
|
378
|
+
if ((n.info.shape as ShapeDesc | undefined)?.kind === ShapeKind.SH_RECORD) {
|
|
379
|
+
applyPenState(obj, styleAttr, colorAttr, penwidth);
|
|
380
|
+
return applyFillState(obj, n, job);
|
|
381
|
+
}
|
|
370
382
|
const filled = applyFillState(obj, n, job);
|
|
371
|
-
applyPenState(obj, styleAttr, colorAttr,
|
|
383
|
+
applyPenState(obj, styleAttr, colorAttr, penwidth);
|
|
372
384
|
return filled;
|
|
373
385
|
}
|
|
374
386
|
|