@knowvah/dot-engine 1.1.1 → 1.2.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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowvah/dot-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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/arrows.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
ARR_MOD_OPEN, ARR_MOD_INV, ARR_MOD_LEFT, ARR_MOD_RIGHT,
|
|
15
15
|
ARR_TYPE_NORM, ARR_TYPE_CROW, ARR_TYPE_TEE, ARR_TYPE_BOX,
|
|
16
16
|
ARR_TYPE_DIAMOND, ARR_TYPE_DOT, ARR_TYPE_CURVE, ARR_TYPE_GAP,
|
|
17
|
-
ARR_TYPE_MASK, ARR_LENFACT_BY_TYPE,
|
|
17
|
+
ARR_TYPE_MASK, ARR_LENFACT_BY_TYPE, NUMB_OF_ARROW_HEADS,
|
|
18
18
|
} from './arrows-constants.js';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -125,13 +125,26 @@ const parseOne = (s: string): { comp: ArrowComponent; rest: string } | null => {
|
|
|
125
125
|
export function parseArrow(str: string): ArrowComponent[] {
|
|
126
126
|
const result: ArrowComponent[] = [];
|
|
127
127
|
let s = str;
|
|
128
|
-
|
|
128
|
+
// C warns and returns on an unparseable fragment, leaving *flag as whatever
|
|
129
|
+
// was accumulated; only a name that fails on the FIRST fragment yields the
|
|
130
|
+
// "no arrow spec at all" case the caller's `normal` default covers.
|
|
131
|
+
let parsedAny = false;
|
|
132
|
+
// C's loop counter `i` advances only when a component is STORED, so a gap
|
|
133
|
+
// dropped by the rules below does not consume one of the four slots.
|
|
134
|
+
while (s.length > 0 && result.length < NUMB_OF_ARROW_HEADS) {
|
|
129
135
|
const m = parseOne(s);
|
|
130
136
|
if (!m) break;
|
|
131
|
-
|
|
137
|
+
parsedAny = true;
|
|
132
138
|
s = m.rest;
|
|
139
|
+
const isGap = m.comp.name === 'none';
|
|
140
|
+
// A GAP in the LAST slot is dropped — `arrowhead="teeteeteenone"` draws
|
|
141
|
+
// three arrows, not three plus a trailing spacer.
|
|
142
|
+
if (isGap && result.length === NUMB_OF_ARROW_HEADS - 1) continue;
|
|
143
|
+
// A GAP that is the entire spec is dropped (`arrowhead=none` → no arrow).
|
|
144
|
+
if (isGap && result.length === 0 && s.length === 0) continue;
|
|
145
|
+
result.push(m.comp);
|
|
133
146
|
}
|
|
134
|
-
if (
|
|
147
|
+
if (!parsedAny) result.push({ name: 'normal', open: false, left: false, right: false });
|
|
135
148
|
return result;
|
|
136
149
|
}
|
|
137
150
|
|
|
@@ -145,8 +145,15 @@ function applyLabel(e: Edge, g: Graph, fi: FontInfo, measurer: TextMeasurer): vo
|
|
|
145
145
|
e.info.label = makeAnyLabel(content, isHtml, fi, measurer, e);
|
|
146
146
|
g.root.info.has_labels |= EDGE_LABEL;
|
|
147
147
|
// ED_label_ontop: mapbool(late_string(e, E_label_float, "false"))
|
|
148
|
-
//
|
|
149
|
-
|
|
148
|
+
//
|
|
149
|
+
// The user-facing attribute is `labelfloat`, with no underscore — that is the
|
|
150
|
+
// name E_label_float is BOUND from (input.c:770). `label_float` (underscore)
|
|
151
|
+
// appears only at dotsplines.c:738, where it is rebound against the internal
|
|
152
|
+
// aux graph. Reading the underscored name here honoured an attribute C
|
|
153
|
+
// ignores and ignored the one it honours; no corpus graph declares either, so
|
|
154
|
+
// no parity track could see it (test/corpus/attr-frequency.ts).
|
|
155
|
+
// @see lib/common/utils.c:522 · lib/common/input.c:770
|
|
156
|
+
e.info.label_ontop = mapbool(e.attrs.get('labelfloat')) ? 1 : 0;
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
// ---------------------------------------------------------------------------
|
|
@@ -258,6 +258,23 @@ export function emitHtmlImg(
|
|
|
258
258
|
*
|
|
259
259
|
* @see lib/common/htmltable.c:emit_html_cell
|
|
260
260
|
*/
|
|
261
|
+
/**
|
|
262
|
+
* True when an HTML element's STYLE declares it invisible.
|
|
263
|
+
*
|
|
264
|
+
* HTML STYLE is lexed by its OWN parser, not the node/edge one: tokens are
|
|
265
|
+
* split on space or comma, compared case-insensitively, and BOTH "INVIS" and
|
|
266
|
+
* "INVISIBLE" set the flag. (The node/edge parser splits on comma only and
|
|
267
|
+
* matches exact-case, so it cannot be reused here.)
|
|
268
|
+
* @see lib/common/htmllex.c:168 (DELIM " ,", strview_case_str_eq)
|
|
269
|
+
*/
|
|
270
|
+
function htmlStyleInvisible(style: string | undefined): boolean {
|
|
271
|
+
if (style === undefined) return false;
|
|
272
|
+
return style.split(/[ ,]+/).some((tok) => {
|
|
273
|
+
const t = tok.toUpperCase();
|
|
274
|
+
return t === 'INVIS' || t === 'INVISIBLE';
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
261
278
|
export function emitHtmlCell(
|
|
262
279
|
cell: PlacedCell,
|
|
263
280
|
pos: Point,
|
|
@@ -268,6 +285,12 @@ export function emitHtmlCell(
|
|
|
268
285
|
{ href: cell.href, title: cell.title, target: cell.target, id: cell.id },
|
|
269
286
|
absBox(cell.box, pos), renderer, job,
|
|
270
287
|
);
|
|
288
|
+
// An invisible cell emits no fill, no border and no content — only the
|
|
289
|
+
// anchor, which C opens before the gate. @see htmltable.c:emit_html_cell:643
|
|
290
|
+
if (htmlStyleInvisible(cell.style)) {
|
|
291
|
+
if (inAnchor) endHtmlAnchor(renderer, job);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
271
294
|
emitCellDecoration(cell, pos, renderer, job);
|
|
272
295
|
if (cell.nested !== undefined) {
|
|
273
296
|
// C: emit_html_cell recurses emit_html_tbl for an HTML_TBL child.
|
|
@@ -329,14 +352,19 @@ export function emitHtmlLabel(
|
|
|
329
352
|
// @see lib/common/htmltable.c:emit_html_tbl (537, 583-588)
|
|
330
353
|
const clustersLast = (job.flags & EMIT_CLUSTERS_LAST) !== 0;
|
|
331
354
|
const inAnchor = clustersLast ? false : initHtmlAnchor(anchorData, tblBox, renderer, job);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
355
|
+
// An invisible TABLE suppresses its fill, ALL its cells, and its rules and
|
|
356
|
+
// border — the whole block C guards at emit_html_tbl:542.
|
|
357
|
+
const tblInvisible = htmlStyleInvisible(placed.style);
|
|
358
|
+
if (!tblInvisible) {
|
|
359
|
+
if (placed.bgcolor !== undefined) {
|
|
360
|
+
emitBgFill({ bgcolor: placed.bgcolor, box: placed.box, pos, border: placed.border,
|
|
361
|
+
renderer, job, gradientangle: placed.gradientangle, style: placed.style });
|
|
362
|
+
}
|
|
363
|
+
for (const cell of placed.cells) emitHtmlCell(cell, pos, renderer, job);
|
|
364
|
+
emitTableRules(placed, pos, renderer, job);
|
|
365
|
+
if (placed.border > 0) {
|
|
366
|
+
emitBorder({ box: placed.box, pos, border: placed.border, color: placed.color, sides: placed.sides, style: placed.style }, renderer, job);
|
|
367
|
+
}
|
|
340
368
|
}
|
|
341
369
|
if (inAnchor) endHtmlAnchor(renderer, job);
|
|
342
370
|
if (clustersLast && initHtmlAnchor(anchorData, tblBox, renderer, job)) {
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
styleTokens,
|
|
34
34
|
} from './style-resolve.js';
|
|
35
35
|
import { stripedBox, wedgedEllipse } from '../render/svg-multicolor.js';
|
|
36
|
-
import { resolveRenderColor
|
|
36
|
+
import { resolveRenderColor } from '../render/color-resolve.js';
|
|
37
37
|
import { svgNodeId } from '../render/svg-id.js';
|
|
38
38
|
import { drawRoundCorners, mcircleHack, underlineDraw } from './poly-shapes.js';
|
|
39
39
|
|
|
@@ -349,13 +349,13 @@ function applyPenState(obj: ObjState, styleAttr: string | undefined,
|
|
|
349
349
|
export function applyNodeStyle(obj: ObjState, n: Node, job: RenderJob): boolean {
|
|
350
350
|
const styleAttr = nodeAttr(n, n.root, 'style');
|
|
351
351
|
const colorAttr = nodeAttr(n, n.root, 'color');
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
352
|
+
// The node's `colorscheme` window is NOT opened here: C opens it once around
|
|
353
|
+
// the whole node emission (emit_begin_node..emit_end_node), which covers both
|
|
354
|
+
// this color block and the label textspans. Both callers of this function are
|
|
355
|
+
// codefns, so they already run inside that window. @see gvc/device.ts
|
|
356
|
+
const filled = applyFillState(obj, n, job);
|
|
357
|
+
applyPenState(obj, styleAttr, colorAttr, nodeAttr(n, n.root, 'penwidth'));
|
|
358
|
+
return filled;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
/**
|
|
@@ -15,66 +15,77 @@
|
|
|
15
15
|
// "Times New Roman" on _WIN32. The oracle and goldens are non-Windows, so the
|
|
16
16
|
// non-Windows family strings are used here.
|
|
17
17
|
|
|
18
|
+
import { FontnameKind } from '../model/layoutParams.js';
|
|
19
|
+
|
|
18
20
|
/**
|
|
19
|
-
* One PostScript-font alias entry
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* One PostScript-font alias entry. `xfig_code` is omitted (xfig output is a
|
|
22
|
+
* non-goal); every other field is carried, because the `fontnames` graph
|
|
23
|
+
* attribute selects WHICH of them the SVG emitter uses.
|
|
22
24
|
* @see lib/common/textspan.h _PostscriptAlias
|
|
23
25
|
*/
|
|
24
26
|
export interface PostscriptAlias {
|
|
27
|
+
/** The PostScript name itself — the family under PSFONTS. */
|
|
28
|
+
name: string;
|
|
25
29
|
family: string;
|
|
26
30
|
weight: string | null;
|
|
27
31
|
stretch: string | null;
|
|
28
32
|
style: string | null;
|
|
29
33
|
svgFontFamily: string;
|
|
34
|
+
svgFontWeight: string | null;
|
|
35
|
+
svgFontStyle: string | null;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
// Rows ported verbatim from ps_font_equiv.h (macros resolved, non-Windows):
|
|
33
|
-
// [name, family, weight, stretch, style, svgFontFamily
|
|
39
|
+
// [name, family, weight, stretch, style, svgFontFamily, svgFontWeight,
|
|
40
|
+
// svgFontStyle].
|
|
34
41
|
const ALIAS_ROWS: readonly (readonly [
|
|
35
42
|
string, string, string | null, string | null, string | null, string,
|
|
43
|
+
string | null, string | null,
|
|
36
44
|
])[] = [
|
|
37
|
-
['AvantGarde-Book', 'URW Gothic L', 'book', null, null, 'sans-Serif'],
|
|
38
|
-
['AvantGarde-BookOblique', 'URW Gothic L', 'book', null, 'oblique', 'sans-Serif'],
|
|
39
|
-
['AvantGarde-Demi', 'URW Gothic L', 'demi', null, null, 'sans-Serif'],
|
|
40
|
-
['AvantGarde-DemiOblique', 'URW Gothic L', 'demi', null, 'oblique', 'sans-Serif'],
|
|
41
|
-
['Bookman-Demi', 'URW Bookman L', 'demi', null, null, 'serif'],
|
|
42
|
-
['Bookman-DemiItalic', 'URW Bookman L', 'demi', null, 'italic', 'serif'],
|
|
43
|
-
['Bookman-Light', 'URW Bookman L', 'light', null, null, 'serif'],
|
|
44
|
-
['Bookman-LightItalic', 'URW Bookman L', 'light', null, 'italic', 'serif'],
|
|
45
|
-
['Courier', 'Courier', null, null, null, 'monospace'],
|
|
46
|
-
['Courier-Bold', 'Courier', 'bold', null, null, 'monospace'],
|
|
47
|
-
['Courier-BoldOblique', 'Courier', 'bold', null, 'oblique', 'monospace'],
|
|
48
|
-
['Courier-Oblique', 'Courier', null, null, 'oblique', 'monospace'],
|
|
49
|
-
['Helvetica', 'Helvetica', null, null, null, 'sans-Serif'],
|
|
50
|
-
['Helvetica-Bold', 'Helvetica', 'bold', null, null, 'sans-Serif'],
|
|
51
|
-
['Helvetica-BoldOblique', 'Helvetica', 'bold', null, 'oblique', 'sans-Serif'],
|
|
52
|
-
['Helvetica-Narrow', 'Helvetica', null, 'condensed', null, 'sans-Serif'],
|
|
53
|
-
['Helvetica-Narrow-Bold', 'Helvetica', 'bold', 'condensed', null, 'sans-Serif'],
|
|
54
|
-
['Helvetica-Narrow-BoldOblique', 'Helvetica', 'bold', 'condensed', 'oblique', 'sans-Serif'],
|
|
55
|
-
['Helvetica-Narrow-Oblique', 'Helvetica', null, 'condensed', 'oblique', 'sans-Serif'],
|
|
56
|
-
['Helvetica-Oblique', 'Helvetica', null, null, 'oblique', 'sans-Serif'],
|
|
57
|
-
['NewCenturySchlbk-Bold', 'Century Schoolbook L', 'bold', null, null, 'serif'],
|
|
58
|
-
['NewCenturySchlbk-BoldItalic', 'Century Schoolbook L', 'bold', null, 'italic', 'serif'],
|
|
59
|
-
['NewCenturySchlbk-Italic', 'Century Schoolbook L', null, null, 'italic', 'serif'],
|
|
60
|
-
['NewCenturySchlbk-Roman', 'Century Schoolbook L', 'roman', null, null, 'serif'],
|
|
61
|
-
['Palatino-Bold', 'Palatino Linotype', 'bold', null, null, 'serif'],
|
|
62
|
-
['Palatino-BoldItalic', 'Palatino Linotype', 'bold', null, 'italic', 'serif'],
|
|
63
|
-
['Palatino-Italic', 'Palatino Linotype', null, null, 'italic', 'serif'],
|
|
64
|
-
['Palatino-Roman', 'Palatino Linotype', 'roman', null, null, 'serif'],
|
|
65
|
-
['Symbol', 'Symbol', null, null, null, 'fantasy'],
|
|
66
|
-
['Times-Bold', 'Times', 'bold', null, null, 'serif'],
|
|
67
|
-
['Times-BoldItalic', 'Times', 'bold', null, 'italic', 'serif'],
|
|
68
|
-
['Times-Italic', 'Times', null, null, 'italic', 'serif'],
|
|
69
|
-
['Times-Roman', 'Times', null, null, null, 'serif'],
|
|
70
|
-
['ZapfChancery-MediumItalic', 'URW Chancery L', 'medium', null, 'italic', 'serif'],
|
|
71
|
-
['ZapfDingbats', 'Dingbats', null, null, null, 'fantasy'],
|
|
45
|
+
['AvantGarde-Book', 'URW Gothic L', 'book', null, null, 'sans-Serif', null, null],
|
|
46
|
+
['AvantGarde-BookOblique', 'URW Gothic L', 'book', null, 'oblique', 'sans-Serif', null, 'italic'],
|
|
47
|
+
['AvantGarde-Demi', 'URW Gothic L', 'demi', null, null, 'sans-Serif', 'bold', null],
|
|
48
|
+
['AvantGarde-DemiOblique', 'URW Gothic L', 'demi', null, 'oblique', 'sans-Serif', 'bold', 'italic'],
|
|
49
|
+
['Bookman-Demi', 'URW Bookman L', 'demi', null, null, 'serif', 'bold', null],
|
|
50
|
+
['Bookman-DemiItalic', 'URW Bookman L', 'demi', null, 'italic', 'serif', 'bold', 'italic'],
|
|
51
|
+
['Bookman-Light', 'URW Bookman L', 'light', null, null, 'serif', null, null],
|
|
52
|
+
['Bookman-LightItalic', 'URW Bookman L', 'light', null, 'italic', 'serif', null, 'italic'],
|
|
53
|
+
['Courier', 'Courier', null, null, null, 'monospace', null, null],
|
|
54
|
+
['Courier-Bold', 'Courier', 'bold', null, null, 'monospace', 'bold', null],
|
|
55
|
+
['Courier-BoldOblique', 'Courier', 'bold', null, 'oblique', 'monospace', 'bold', 'italic'],
|
|
56
|
+
['Courier-Oblique', 'Courier', null, null, 'oblique', 'monospace', null, 'italic'],
|
|
57
|
+
['Helvetica', 'Helvetica', null, null, null, 'sans-Serif', null, null],
|
|
58
|
+
['Helvetica-Bold', 'Helvetica', 'bold', null, null, 'sans-Serif', 'bold', null],
|
|
59
|
+
['Helvetica-BoldOblique', 'Helvetica', 'bold', null, 'oblique', 'sans-Serif', 'bold', 'italic'],
|
|
60
|
+
['Helvetica-Narrow', 'Helvetica', null, 'condensed', null, 'sans-Serif', null, null],
|
|
61
|
+
['Helvetica-Narrow-Bold', 'Helvetica', 'bold', 'condensed', null, 'sans-Serif', 'bold', null],
|
|
62
|
+
['Helvetica-Narrow-BoldOblique', 'Helvetica', 'bold', 'condensed', 'oblique', 'sans-Serif', 'bold', 'italic'],
|
|
63
|
+
['Helvetica-Narrow-Oblique', 'Helvetica', null, 'condensed', 'oblique', 'sans-Serif', null, 'italic'],
|
|
64
|
+
['Helvetica-Oblique', 'Helvetica', null, null, 'oblique', 'sans-Serif', null, 'italic'],
|
|
65
|
+
['NewCenturySchlbk-Bold', 'Century Schoolbook L', 'bold', null, null, 'serif', 'bold', null],
|
|
66
|
+
['NewCenturySchlbk-BoldItalic', 'Century Schoolbook L', 'bold', null, 'italic', 'serif', 'bold', 'italic'],
|
|
67
|
+
['NewCenturySchlbk-Italic', 'Century Schoolbook L', null, null, 'italic', 'serif', null, 'italic'],
|
|
68
|
+
['NewCenturySchlbk-Roman', 'Century Schoolbook L', 'roman', null, null, 'serif', null, null],
|
|
69
|
+
['Palatino-Bold', 'Palatino Linotype', 'bold', null, null, 'serif', 'bold', null],
|
|
70
|
+
['Palatino-BoldItalic', 'Palatino Linotype', 'bold', null, 'italic', 'serif', 'bold', 'italic'],
|
|
71
|
+
['Palatino-Italic', 'Palatino Linotype', null, null, 'italic', 'serif', null, 'italic'],
|
|
72
|
+
['Palatino-Roman', 'Palatino Linotype', 'roman', null, null, 'serif', null, null],
|
|
73
|
+
['Symbol', 'Symbol', null, null, null, 'fantasy', null, null],
|
|
74
|
+
['Times-Bold', 'Times', 'bold', null, null, 'serif', 'bold', null],
|
|
75
|
+
['Times-BoldItalic', 'Times', 'bold', null, 'italic', 'serif', 'bold', 'italic'],
|
|
76
|
+
['Times-Italic', 'Times', null, null, 'italic', 'serif', null, 'italic'],
|
|
77
|
+
['Times-Roman', 'Times', null, null, null, 'serif', null, null],
|
|
78
|
+
['ZapfChancery-MediumItalic', 'URW Chancery L', 'medium', null, 'italic', 'serif', null, 'italic'],
|
|
79
|
+
['ZapfDingbats', 'Dingbats', null, null, null, 'fantasy', null, null],
|
|
72
80
|
];
|
|
73
81
|
|
|
74
82
|
const ALIAS_BY_NAME: ReadonlyMap<string, PostscriptAlias> = new Map(
|
|
75
83
|
ALIAS_ROWS.map((r) => [
|
|
76
84
|
r[0].toLowerCase(),
|
|
77
|
-
{
|
|
85
|
+
{
|
|
86
|
+
name: r[0], family: r[1], weight: r[2], stretch: r[3], style: r[4],
|
|
87
|
+
svgFontFamily: r[5], svgFontWeight: r[6], svgFontStyle: r[7],
|
|
88
|
+
},
|
|
78
89
|
]),
|
|
79
90
|
);
|
|
80
91
|
|
|
@@ -89,24 +100,44 @@ export function translatePostscriptFontname(name: string): PostscriptAlias | nul
|
|
|
89
100
|
|
|
90
101
|
/**
|
|
91
102
|
* Build the SVG font attribute string for a span's font name when it resolves
|
|
92
|
-
* to a PostScript alias
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
103
|
+
* to a PostScript alias, honouring the graph's `fontnames` mode. Returns null
|
|
104
|
+
* when there is no alias (the caller emits the name verbatim).
|
|
105
|
+
* `weight`/`style` report whether the alias set them, so the HTML-flag block
|
|
106
|
+
* can avoid duplicating bold/italic.
|
|
107
|
+
*
|
|
108
|
+
* C selects family/weight/style as a THREE-WAY switch on GD_fontnames, and the
|
|
109
|
+
* SVGFONTS branch reads different struct fields (svg_font_weight /
|
|
110
|
+
* svg_font_style) rather than reusing the native ones — so the mode changes
|
|
111
|
+
* more than the family string. The `,svgFontFamily` suffix is appended by the
|
|
112
|
+
* same rule in every mode: only when it differs from the chosen family, which
|
|
113
|
+
* is exactly why SVGFONTS emits a bare `sans-Serif` and NATIVEFONTS emits
|
|
114
|
+
* `Helvetica,sans-Serif`.
|
|
96
115
|
*
|
|
97
116
|
* Family strings contain no XML-special characters, so (like C's raw `%s`) no
|
|
98
117
|
* escaping is applied here.
|
|
99
|
-
* @see plugin/core/gvrender_core_svg.c:462-495 svg_textspan
|
|
118
|
+
* @see plugin/core/gvrender_core_svg.c:462-495 svg_textspan
|
|
100
119
|
*/
|
|
101
120
|
export function fontFamilyAttrs(
|
|
102
121
|
fontName: string | null,
|
|
122
|
+
mode: FontnameKind = FontnameKind.NativeFonts,
|
|
103
123
|
): { attrs: string; weight: boolean; style: boolean } | null {
|
|
104
124
|
const a = fontName !== null ? translatePostscriptFontname(fontName) : null;
|
|
105
125
|
if (a === null) return null;
|
|
106
|
-
|
|
126
|
+
let family: string;
|
|
127
|
+
let weight: string | null;
|
|
128
|
+
let style: string | null;
|
|
129
|
+
switch (mode) {
|
|
130
|
+
case FontnameKind.PsFonts:
|
|
131
|
+
family = a.name; weight = a.weight; style = a.style; break;
|
|
132
|
+
case FontnameKind.SvgFonts:
|
|
133
|
+
family = a.svgFontFamily; weight = a.svgFontWeight; style = a.svgFontStyle; break;
|
|
134
|
+
default:
|
|
135
|
+
family = a.family; weight = a.weight; style = a.style; break;
|
|
136
|
+
}
|
|
137
|
+
const fam = a.svgFontFamily !== family ? family + ',' + a.svgFontFamily : family;
|
|
107
138
|
let attrs = ' font-family="' + fam + '"';
|
|
108
|
-
if (
|
|
139
|
+
if (weight !== null) attrs += ' font-weight="' + weight + '"';
|
|
109
140
|
if (a.stretch !== null) attrs += ' font-stretch="' + a.stretch + '"';
|
|
110
|
-
if (
|
|
111
|
-
return { attrs, weight:
|
|
141
|
+
if (style !== null) attrs += ' font-style="' + style + '"';
|
|
142
|
+
return { attrs, weight: weight !== null, style: style !== null };
|
|
112
143
|
}
|
|
@@ -97,6 +97,22 @@ class SplineClipHelper {
|
|
|
97
97
|
return [sflag, eflag];
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The ORIGINAL edge's arrow end whose TYPE applies at a geometric spline end.
|
|
102
|
+
*
|
|
103
|
+
* C resolves both arrow types once from the original edge (arrow_flags) and
|
|
104
|
+
* encodes each type in its flag, so swapping the two flags when swapEnds is
|
|
105
|
+
* true carries the type along with it. This port keeps only the presence bit
|
|
106
|
+
* in the flag and re-reads the type from the `arrowhead`/`arrowtail`
|
|
107
|
+
* attribute, so the same swap has to be applied to the NAME lookup — without
|
|
108
|
+
* it a reversed back edge (`C->A [arrowhead=tee]`, head ranked above tail)
|
|
109
|
+
* draws its `arrowtail` type at the end where C draws the arrowhead.
|
|
110
|
+
* @see lib/common/splines.c:arrow_clip (if (j) SWAP(sflag, eflag))
|
|
111
|
+
*/
|
|
112
|
+
static arrowAttrEnd(isTail: boolean, j: boolean): 'head' | 'tail' {
|
|
113
|
+
return isTail !== j ? 'tail' : 'head';
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
static resolveArrowFlags(info: SplineInfo, hn: Node, fe: Edge, orig: Edge, j: boolean): [number, number] {
|
|
101
117
|
let [sflag, eflag] = SplineClipHelper.arrowFlags(orig);
|
|
102
118
|
if (info.splineMerge != null) {
|
|
@@ -147,9 +163,10 @@ class SplineClipHelper {
|
|
|
147
163
|
*/
|
|
148
164
|
static arrowEndClip(
|
|
149
165
|
e: Edge, ps: Point[], startp: number, endpIn: number, spl: Bezier, eflag: number,
|
|
166
|
+
attrEnd: 'head' | 'tail' = 'head',
|
|
150
167
|
): number {
|
|
151
168
|
let endp = endpIn;
|
|
152
|
-
const elen = arrowClipLength(edgeArrowName(e,
|
|
169
|
+
const elen = arrowClipLength(edgeArrowName(e, attrEnd),
|
|
153
170
|
SplineClipHelper.clipArrowsize(e), SplineClipHelper.clipPenwidth(e));
|
|
154
171
|
spl.eflag = eflag;
|
|
155
172
|
spl.ep = ps[endp + 3]!;
|
|
@@ -174,9 +191,10 @@ class SplineClipHelper {
|
|
|
174
191
|
*/
|
|
175
192
|
static arrowStartClip(
|
|
176
193
|
e: Edge, ps: Point[], startpIn: number, endp: number, spl: Bezier, sflag: number,
|
|
194
|
+
attrEnd: 'head' | 'tail' = 'tail',
|
|
177
195
|
): number {
|
|
178
196
|
let startp = startpIn;
|
|
179
|
-
const slen = arrowClipLength(edgeArrowName(e,
|
|
197
|
+
const slen = arrowClipLength(edgeArrowName(e, attrEnd),
|
|
180
198
|
SplineClipHelper.clipArrowsize(e), SplineClipHelper.clipPenwidth(e));
|
|
181
199
|
spl.sflag = sflag;
|
|
182
200
|
spl.sp = ps[startp]!;
|
|
@@ -231,24 +249,29 @@ class SplineClipHelper {
|
|
|
231
249
|
newspl.eflag = eflag;
|
|
232
250
|
// C: ortho edges use arrowOrthoClip (keeps segments axis-aligned), not the
|
|
233
251
|
// De-Casteljau start/end clips. @see lib/common/splines.c:90
|
|
252
|
+
// The arrow TYPE follows the flag swap, not the geometric end: see
|
|
253
|
+
// arrowAttrEnd. (Under ortho, ignoreSwap forces j=false, so these are the
|
|
254
|
+
// plain tail/head ends.)
|
|
255
|
+
const startEnd = SplineClipHelper.arrowAttrEnd(true, j);
|
|
256
|
+
const endEnd = SplineClipHelper.arrowAttrEnd(false, j);
|
|
234
257
|
if (info.isOrtho) {
|
|
235
258
|
if (sflag || eflag) {
|
|
236
259
|
SplineClipHelper.arrowOrthoClip(
|
|
237
260
|
orig, ps, bounds.start, bounds.end, newspl, sflag, eflag);
|
|
238
|
-
if (sflag) SplineClipHelper.stashArrow(orig, newspl.sp, ps[bounds.start]!, true);
|
|
239
|
-
if (eflag) SplineClipHelper.stashArrow(orig, newspl.ep, ps[bounds.end + 3]!, false);
|
|
261
|
+
if (sflag) SplineClipHelper.stashArrow(orig, newspl.sp, ps[bounds.start]!, true, startEnd);
|
|
262
|
+
if (eflag) SplineClipHelper.stashArrow(orig, newspl.ep, ps[bounds.end + 3]!, false, endEnd);
|
|
240
263
|
}
|
|
241
264
|
return;
|
|
242
265
|
}
|
|
243
266
|
if (sflag) {
|
|
244
267
|
bounds.start = SplineClipHelper.arrowStartClip(
|
|
245
|
-
orig, ps, bounds.start, bounds.end, newspl, sflag);
|
|
246
|
-
SplineClipHelper.stashArrow(orig, newspl.sp, ps[bounds.start]!, true);
|
|
268
|
+
orig, ps, bounds.start, bounds.end, newspl, sflag, startEnd);
|
|
269
|
+
SplineClipHelper.stashArrow(orig, newspl.sp, ps[bounds.start]!, true, startEnd);
|
|
247
270
|
}
|
|
248
271
|
if (eflag) {
|
|
249
272
|
bounds.end = SplineClipHelper.arrowEndClip(
|
|
250
|
-
orig, ps, bounds.start, bounds.end, newspl, eflag);
|
|
251
|
-
SplineClipHelper.stashArrow(orig, newspl.ep, ps[bounds.end + 3]!, false);
|
|
273
|
+
orig, ps, bounds.start, bounds.end, newspl, eflag, endEnd);
|
|
274
|
+
SplineClipHelper.stashArrow(orig, newspl.ep, ps[bounds.end + 3]!, false, endEnd);
|
|
252
275
|
}
|
|
253
276
|
}
|
|
254
277
|
|
|
@@ -314,9 +337,12 @@ class SplineClipHelper {
|
|
|
314
337
|
}
|
|
315
338
|
|
|
316
339
|
/** Record an arrow polygon on the edge for svgArrowPolygons. */
|
|
317
|
-
static stashArrow(
|
|
340
|
+
static stashArrow(
|
|
341
|
+
e: Edge, tip: Point, base: Point, isTail: boolean, attrEnd?: 'head' | 'tail',
|
|
342
|
+
): void {
|
|
318
343
|
const dir = { x: base.x - tip.x, y: base.y - tip.y };
|
|
319
|
-
const
|
|
344
|
+
const end = attrEnd ?? (isTail ? 'tail' : 'head');
|
|
345
|
+
const ops = arrowDrawOpsForEnd(e, end, tip, dir, SplineClipHelper.renderPenwidth(e));
|
|
320
346
|
if (isTail) e.info.tailArrowOps = ops;
|
|
321
347
|
else e.info.headArrowOps = ops;
|
|
322
348
|
}
|
package/src/gvc/device.ts
CHANGED
|
@@ -173,11 +173,19 @@ function nodeInBox(n: Node, job: RenderJob): boolean {
|
|
|
173
173
|
function emitNodeBody(n: Node, renderer: RendererPlugin, job: RenderJob): void {
|
|
174
174
|
setHtmlAnchorObj(svgNodeId(n, job), labelTextOf(n.info.label), job.obj ?? undefined);
|
|
175
175
|
setHtmlObjImgscale(nodeAttr(n, n.root, 'imagescale'));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
// C sets the node's colorscheme at the tail of emit_begin_node and restores
|
|
177
|
+
// it at the head of emit_end_node, so the window spans codefn AND the xlabel
|
|
178
|
+
// — not just the color block. Bare scheme indices in the node's LABEL (e.g.
|
|
179
|
+
// `fontcolor=3` under `colorscheme=rdbu5`) resolve inside this window; with
|
|
180
|
+
// the window scoped to the fill/pen block they fell back to black.
|
|
181
|
+
// @see lib/common/emit.c:1781/1789 · emit_node:1826-1831
|
|
182
|
+
withColorScheme(nodeAttr(n, n.root, 'colorscheme'), () => {
|
|
183
|
+
renderer.beginNode(n, job);
|
|
184
|
+
const shape = n.info.shape as ShapeDesc | undefined;
|
|
185
|
+
if (shape?.fns?.codefn) shape.fns.codefn(job, n);
|
|
186
|
+
renderNodeXLabel(n, renderer, job);
|
|
187
|
+
renderer.endNode(n, job);
|
|
188
|
+
});
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
/**
|
|
@@ -193,8 +201,12 @@ export function renderNode(n: Node, renderer: RendererPlugin, job: RenderJob, do
|
|
|
193
201
|
// "and is in page/view" gate. @see lib/common/emit.c:1808 node_in_box
|
|
194
202
|
if (!nodeInBox(n, job)) return;
|
|
195
203
|
// Shortcircuit invisible nodes: C omits the whole node before emit_begin_node.
|
|
204
|
+
// C reads late_string(n, N_style, ""), i.e. agxget — which resolves the
|
|
205
|
+
// node's DECLARATION-SCOPE default, so `{ node [style=invis] p q }` hides p
|
|
206
|
+
// and q. Reading n.attrs directly sees only a style set on the node itself
|
|
207
|
+
// and drew every subgraph-scoped invisible node (switch.gv: 16 of them).
|
|
196
208
|
// @see lib/common/emit.c:emit_node (style "invis" return)
|
|
197
|
-
const nodeStyle = n.
|
|
209
|
+
const nodeStyle = nodeAttr(n, n.root, 'style');
|
|
198
210
|
if (parseStyleFlags(nodeStyle).invis) return;
|
|
199
211
|
// The "invisible" alias also suppresses drawing (gvrender_set_style →
|
|
200
212
|
// PEN_NONE) for shapes that pass their raw style through — but NOT the point
|
package/src/gvc/job.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Graph } from '../model/graph.js';
|
|
14
|
+
import { FontnameKind } from '../model/layoutParams.js';
|
|
14
15
|
import type { Node } from '../model/node.js';
|
|
15
16
|
import type { Edge } from '../model/edge.js';
|
|
16
17
|
import type { Point, Box } from '../model/geom.js';
|
|
@@ -297,6 +298,9 @@ export class RenderJob {
|
|
|
297
298
|
radialGradId: number = 0;
|
|
298
299
|
/** Whether the graph being rendered is directed (edge titles use -> vs --). */
|
|
299
300
|
directed: boolean = true;
|
|
301
|
+
/** GD_fontnames of the root graph — selects which PostScript-alias fields the
|
|
302
|
+
* SVG emitter uses. @see plugin/core/gvrender_core_svg.c:464 */
|
|
303
|
+
fontnames: FontnameKind = FontnameKind.NativeFonts;
|
|
300
304
|
/** Root graph's `id` attribute (GD_drawing(root)->id), '' if unset. Used as
|
|
301
305
|
* the `<gid>_` prefix for non-root object ids. @see lib/common/emit.c:getObjId */
|
|
302
306
|
drawingId: string = '';
|
package/src/layout/dot/index.ts
CHANGED
|
@@ -164,10 +164,37 @@ export function dotPhasePost(g: Graph): void {
|
|
|
164
164
|
// dotLayoutPipeline — the static dotLayout function from dotinit.c
|
|
165
165
|
// ---------------------------------------------------------------------------
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* How many pipeline phases to run: C's
|
|
169
|
+
* `late_int(g, agfindgraphattr(g, "phase"), -1, 1)`.
|
|
170
|
+
*
|
|
171
|
+
* The DOT attribute is `phase`; `maxphase` is only the name of C's local
|
|
172
|
+
* variable. The port read `maxphase` as the attribute name, so it responded to
|
|
173
|
+
* a name C ignores and ignored the one C honours — invisible to every parity
|
|
174
|
+
* track, because no corpus graph declares either (found via the attribute
|
|
175
|
+
* blind-spot scan, test/corpus/attr-frequency.ts).
|
|
176
|
+
*
|
|
177
|
+
* late_int's `minimum` argument is 1, and it clamps rather than rejects, so a
|
|
178
|
+
* present-but-smaller value becomes 1 — only an ABSENT or unparseable value
|
|
179
|
+
* yields the -1 "run everything" default.
|
|
180
|
+
* @see lib/dotgen/dotinit.c:297 · lib/common/utils.c:40 late_int
|
|
181
|
+
*/
|
|
182
|
+
function graphPhase(g: Graph): number {
|
|
183
|
+
const v = g.attrs.get('phase');
|
|
184
|
+
if (v === undefined || v === '') return -1;
|
|
185
|
+
// strtol parses a numeric PREFIX, so "3abc" is 3, and parseInt agrees. The
|
|
186
|
+
// two failure exits return defaultValue WITHOUT clamping, so `phase=abc`
|
|
187
|
+
// runs the whole pipeline while `phase=0` clamps up to 1 — the distinction
|
|
188
|
+
// getAttrInt cannot express, since it collapses both onto defaultVal.
|
|
189
|
+
const n = parseInt(v, 10);
|
|
190
|
+
if (isNaN(n) || n > 2147483647) return -1;
|
|
191
|
+
return n < 1 ? 1 : n;
|
|
192
|
+
}
|
|
193
|
+
|
|
167
194
|
/**
|
|
168
195
|
* Runs the full dot layout pipeline on graph g.
|
|
169
196
|
*
|
|
170
|
-
* The optional `
|
|
197
|
+
* The optional `phase` attribute (read from g.attrs) controls how many
|
|
171
198
|
* phases run (1=rank only, 2=+mincross, 3=+position, ≥4 or absent=all).
|
|
172
199
|
*
|
|
173
200
|
* C's `dotLayout` checks `dot_mincross`'s return code and returns immediately
|
|
@@ -198,16 +225,21 @@ export function dotPhasePost(g: Graph): void {
|
|
|
198
225
|
* where C produces near-empty output.
|
|
199
226
|
*/
|
|
200
227
|
export function dotLayoutPipeline(g: Graph): void {
|
|
201
|
-
const maxphase =
|
|
228
|
+
const maxphase = graphPhase(g);
|
|
202
229
|
dotPhaseInit(g);
|
|
203
230
|
dotRank(g);
|
|
204
|
-
|
|
231
|
+
// A phase stop is a SUCCESS return in C (dotLayout returns 0), and
|
|
232
|
+
// `dot_layout` skips dotneato_postprocess only when doDot reports an ERROR
|
|
233
|
+
// (dotinit.c:512-517). So the postprocess still runs on every phase stop —
|
|
234
|
+
// without it the drawing keeps raw layout coordinates and is translated off
|
|
235
|
+
// the origin. The rc/posRc paths below are the error paths and stay bare.
|
|
236
|
+
if (maxphase === 1) { gvPostprocess(g); return; }
|
|
205
237
|
const rc = dotMincross(g);
|
|
206
238
|
if (rc !== 0) return;
|
|
207
|
-
if (maxphase === 2) return;
|
|
239
|
+
if (maxphase === 2) { gvPostprocess(g); return; }
|
|
208
240
|
const posRc = dotPosition(g);
|
|
209
241
|
if (posRc !== 0) return;
|
|
210
|
-
if (maxphase === 3) return;
|
|
242
|
+
if (maxphase === 3) { gvPostprocess(g); return; }
|
|
211
243
|
dotPhasePost(g);
|
|
212
244
|
}
|
|
213
245
|
|
package/src/layout/sfdp/init.ts
CHANGED
|
@@ -140,6 +140,17 @@ export function tuneControl(g: Graph, ctrl: SpringElectricalControl): void {
|
|
|
140
140
|
ctrl.beautifyLeaves = mapBool(aggetGraph(g, 'beautify'), false);
|
|
141
141
|
ctrl.doShrinking = mapBool(aggetGraph(g, 'overlap_shrink'), true);
|
|
142
142
|
ctrl.rotation = lateDouble(aggetGraph(g, 'rotation'), 0.0, -Number.MAX_VALUE);
|
|
143
|
+
// C rotates the layout before overlap removal (spring_electrical.c:1181).
|
|
144
|
+
// `rotate()` is not ported, and nothing reads ctrl.rotation, so a non-zero
|
|
145
|
+
// value silently produced an UNROTATED layout — native moves nodes ~320pt on
|
|
146
|
+
// a 6-node graph at rotation=45. Fail the way `smoothing` above does: a loud
|
|
147
|
+
// unported-feature error beats silently wrong geometry.
|
|
148
|
+
// Found by the attribute blind-spot scan; no corpus graph sets it.
|
|
149
|
+
if (ctrl.rotation !== 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`sfdp rotation="${ctrl.rotation}": rotate() is not ported ` +
|
|
152
|
+
'(unreachable at sfdp defaults); see plans/port-catalog/README.md');
|
|
153
|
+
}
|
|
143
154
|
ctrl.edgeLabelingScheme = lateInt(aggetGraph(g, 'label_scheme'), 0, 0);
|
|
144
155
|
if (ctrl.edgeLabelingScheme > 4) ctrl.edgeLabelingScheme = 0;
|
|
145
156
|
}
|
package/src/model/graph.ts
CHANGED
|
@@ -105,6 +105,32 @@ export class Graph {
|
|
|
105
105
|
*/
|
|
106
106
|
graphDefaultsSnapshot?: Map<string, string>;
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Keys in `attrs` the BUILDER seeded from `graphDefaultsSnapshot` rather than
|
|
110
|
+
* the input declaring them in this scope (see builder.ts GRAPH_LABEL_INHERIT_
|
|
111
|
+
* KEYS). cgraph distinguishes these structurally — a seeded value is inherited
|
|
112
|
+
* through the dict view and has no local symbol, while a local declaration
|
|
113
|
+
* creates one even when its value equals the inherited value — so the
|
|
114
|
+
* serializer needs the provenance to decide what write_dict would print.
|
|
115
|
+
* Undefined on the root graph and on any scope that seeded nothing.
|
|
116
|
+
* @see lib/cgraph/write.c:262 write_dict
|
|
117
|
+
*/
|
|
118
|
+
seededAttrs?: Set<string>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Graph-attribute keys whose declaration in THIS scope was the first anywhere
|
|
122
|
+
* in the graph. cgraph's `setattr` splits on exactly this: a key with no
|
|
123
|
+
* existing symbol takes the global-declaration branch, which ends up seeding
|
|
124
|
+
* every direct subgraph that already exists with a local empty value (see
|
|
125
|
+
* `eagerEmptyParts`); a key some earlier scope already declared takes the
|
|
126
|
+
* "new local definition" branch and touches no subgraph at all. So an
|
|
127
|
+
* identical `graph [fill=x]` statement makes a child print `fill=""` or print
|
|
128
|
+
* nothing purely according to whether a SIBLING declared `fill` first.
|
|
129
|
+
* Undefined on any scope that declared nothing new.
|
|
130
|
+
* @see lib/cgraph/attr.c:257 setattr (lsym / psym / global branches)
|
|
131
|
+
*/
|
|
132
|
+
firstGraphDecl?: Set<string>;
|
|
133
|
+
|
|
108
134
|
/**
|
|
109
135
|
* Layout-engine info; replaces GD_* macros via agbindrec (AD-1).
|
|
110
136
|
* @see lib/cgraph/cgraph.h:agbindrec
|