@knowvah/dot-engine 1.2.2 → 1.2.4

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.
Files changed (42) hide show
  1. package/dist/cdt/bag.d.ts +6 -4
  2. package/dist/cdt/bag.d.ts.map +1 -1
  3. package/dist/common/arrows-shapes-poly.d.ts +1 -1
  4. package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
  5. package/dist/common/arrows-shapes.d.ts +2 -2
  6. package/dist/common/arrows-shapes.d.ts.map +1 -1
  7. package/dist/common/htmltable-lex.d.ts.map +1 -1
  8. package/dist/common/record.d.ts.map +1 -1
  9. package/dist/index.js +2013 -2008
  10. package/dist/index.js.map +4 -4
  11. package/dist/layout/dot/edge-route-arrow.d.ts +8 -0
  12. package/dist/layout/dot/edge-route-arrow.d.ts.map +1 -1
  13. package/dist/model/geom.d.ts +5 -0
  14. package/dist/model/geom.d.ts.map +1 -1
  15. package/dist/render.js +95 -90
  16. package/dist/render.js.map +4 -4
  17. package/dist/xdot/parse.d.ts.map +1 -1
  18. package/package.json +2 -1
  19. package/src/cdt/bag.ts +30 -52
  20. package/src/common/arrows-shapes-poly.ts +20 -1
  21. package/src/common/arrows-shapes.ts +6 -2
  22. package/src/common/htmltable-lex.ts +17 -4
  23. package/src/common/htmltable-parse.ts +1 -1
  24. package/src/common/record.ts +3 -5
  25. package/src/layout/dot/edge-route-arrow.ts +7 -2
  26. package/src/model/geom.ts +14 -0
  27. package/src/xdot/parse.ts +3 -1
  28. package/dist/gvc/textlayout.d.ts +0 -33
  29. package/dist/gvc/textlayout.d.ts.map +0 -1
  30. package/dist/layout/osage/layout.d.ts +0 -49
  31. package/dist/layout/osage/layout.d.ts.map +0 -1
  32. package/dist/layout/pack/test-helpers.d.ts +0 -7
  33. package/dist/layout/pack/test-helpers.d.ts.map +0 -1
  34. package/dist/layout/patchwork/tree-node.d.ts +0 -46
  35. package/dist/layout/patchwork/tree-node.d.ts.map +0 -1
  36. package/dist/xdot/lex.d.ts +0 -22
  37. package/dist/xdot/lex.d.ts.map +0 -1
  38. package/src/gvc/textlayout.ts +0 -44
  39. package/src/layout/osage/layout.ts +0 -254
  40. package/src/layout/pack/test-helpers.ts +0 -46
  41. package/src/layout/patchwork/tree-node.ts +0 -120
  42. package/src/xdot/lex.ts +0 -151
@@ -1,254 +0,0 @@
1
- // SPDX-License-Identifier: EPL-2.0
2
-
3
- /**
4
- * Osage recursive layout helpers.
5
- *
6
- * @see lib/osage/osageinit.c:layout
7
- * @see lib/osage/osageinit.c:reposition
8
- */
9
-
10
- import type { Graph } from '../../model/graph.js';
11
- import type { Node } from '../../model/node.js';
12
- import type { Box, Point } from '../../model/geom.js';
13
- import type { PackInfo } from '../pack/index.js';
14
- import type { ChildLists } from './types.js';
15
- import { PackMode, getPackInfo, putRects, PK_USER_VALS } from '../pack/index.js';
16
- import { PARENT } from './index.js';
17
-
18
- export type { ChildLists };
19
-
20
- const DFLT_SZ = 18;
21
- const DFLT_MARGIN = 8;
22
-
23
- // ---------------------------------------------------------------------------
24
- // Box helpers
25
- // ---------------------------------------------------------------------------
26
-
27
- const makeBox = (llx: number, lly: number, urx: number, ury: number): Box =>
28
- ({ ll: { x: llx, y: lly }, ur: { x: urx, y: ury } });
29
-
30
- const copyBox = (b: Box): Box =>
31
- makeBox(b.ll.x, b.ll.y, b.ur.x, b.ur.y);
32
-
33
- const shiftBox = (b: Box, p: Point): Box =>
34
- makeBox(b.ll.x + p.x, b.ll.y + p.y, b.ur.x + p.x, b.ur.y + p.y);
35
-
36
- // ---------------------------------------------------------------------------
37
- // PackInfo builder
38
- // ---------------------------------------------------------------------------
39
-
40
- /** Build and populate a PackInfo from graph attributes. @see lib/osage/osageinit.c:layout */
41
- export const buildPackInfo = (g: Graph): PackInfo => {
42
- const p: PackInfo = { aspect: 1, sz: 0, margin: DFLT_MARGIN,
43
- doSplines: false, mode: PackMode.Array, fixed: null, vals: null, flags: 0 };
44
- getPackInfo(g, PackMode.Array, DFLT_MARGIN, p);
45
- if (p.mode < PackMode.Graph) p.mode = PackMode.Graph;
46
- return p;
47
- };
48
-
49
- // ---------------------------------------------------------------------------
50
- // ChildLists builders
51
- // ---------------------------------------------------------------------------
52
-
53
- const newChildLists = (): ChildLists =>
54
- ({ gs: [], childGraphs: [], childNodes: [], vals: [] });
55
-
56
- const addCluster = (lists: ChildLists, sg: Graph, useVals: boolean): void => {
57
- lists.gs.push(copyBox(sg.info.bb));
58
- lists.childGraphs.push(sg);
59
- if (useVals) lists.vals.push(0);
60
- };
61
-
62
- const addNode = (lists: ChildLists, n: Node, owner: Graph, useVals: boolean): void => {
63
- n.info.alg = { kind: 'osage', ownerCluster: owner };
64
- lists.gs.push(makeBox(0, 0, n.info.lw + n.info.rw, n.info.ht));
65
- lists.childNodes.push(n);
66
- if (useVals) lists.vals.push(0);
67
- };
68
-
69
- const fillClusters = (g: Graph, nClust: number, lists: ChildLists, uv: boolean): void => {
70
- for (let i = 1; i <= nClust; i++) addCluster(lists, g.info.clust![i - 1], uv);
71
- };
72
-
73
- const fillNodes = (g: Graph, nvs: number, lists: ChildLists, uv: boolean): void => {
74
- if (g.nodes.size <= nvs) return;
75
- for (const n of g.nodes.values()) {
76
- if (n.info.alg === undefined) addNode(lists, n, g, uv);
77
- }
78
- };
79
-
80
- /**
81
- * Build child arrays for putRects.
82
- * @see lib/osage/osageinit.c:layout (child list construction)
83
- */
84
- export const buildChildLists = (g: Graph, nClust: number, nvs: number, p: PackInfo): ChildLists => {
85
- const useVals = p.mode === PackMode.Array && (p.flags & PK_USER_VALS) !== 0;
86
- const lists = newChildLists();
87
- fillClusters(g, nClust, lists, useVals);
88
- fillNodes(g, nvs, lists, useVals);
89
- return lists;
90
- };
91
-
92
- // ---------------------------------------------------------------------------
93
- // Bounding-box union accumulator
94
- // ---------------------------------------------------------------------------
95
-
96
- const emptyUnion = (): Box => makeBox(Infinity, Infinity, -Infinity, -Infinity);
97
-
98
- const expandUnion = (u: Box, b: Box): void => {
99
- if (b.ll.x < u.ll.x) u.ll.x = b.ll.x;
100
- if (b.ll.y < u.ll.y) u.ll.y = b.ll.y;
101
- if (b.ur.x > u.ur.x) u.ur.x = b.ur.x;
102
- if (b.ur.y > u.ur.y) u.ur.y = b.ur.y;
103
- };
104
-
105
- const placeChild = (lists: ChildLists, i: number, tb: Box): void => {
106
- const ng = lists.childGraphs.length;
107
- if (i < ng) {
108
- lists.childGraphs[i]!.info.bb = tb;
109
- } else {
110
- const nd = lists.childNodes[i - ng]!;
111
- nd.info.coord = { x: (tb.ll.x + tb.ur.x) / 2, y: (tb.ll.y + tb.ur.y) / 2 };
112
- }
113
- };
114
-
115
- /**
116
- * Apply putRects offsets to child bbs/coords; return union bounding box.
117
- * @see lib/osage/osageinit.c:layout (offset application loop)
118
- */
119
- export const applyOffsets = (lists: ChildLists, pts: Point[]): Box => {
120
- const u = emptyUnion();
121
- for (let i = 0; i < lists.gs.length; i++) {
122
- const tb = shiftBox(lists.gs[i]!, pts[i]!);
123
- expandUnion(u, tb);
124
- placeChild(lists, i, tb);
125
- }
126
- return u;
127
- };
128
-
129
- // ---------------------------------------------------------------------------
130
- // Label + margin adjustments
131
- // ---------------------------------------------------------------------------
132
-
133
- /**
134
- * Widen union bb if the graph label is wider than packed content.
135
- * @see lib/osage/osageinit.c:layout (GD_label block)
136
- */
137
- export const applyLabelExpansion = (g: Graph, total: number, u: Box): Box => {
138
- const dim = (g.info.label as { dimen?: Point } | undefined)?.dimen;
139
- if (!dim) return u;
140
- if (total === 0) return makeBox(0, 0, dim.x, dim.y);
141
- const d = dim.x - (u.ur.x - u.ll.x);
142
- if (d > 0) { u.ll.x -= d / 2; u.ur.x += d / 2; }
143
- return u;
144
- };
145
-
146
- const borderY = (g: Graph, idx: 2 | 3): number =>
147
- g.info.border ? g.info.border[idx].y : 0;
148
-
149
- /**
150
- * Add margin and label-border padding to the union bounding box.
151
- * @see lib/osage/osageinit.c:layout (margin/border block)
152
- */
153
- export const addMarginAndBorder = (g: Graph, margin: number, u: Box): Box =>
154
- makeBox(
155
- u.ll.x - margin, u.ll.y - margin - borderY(g, 3),
156
- u.ur.x + margin, u.ur.y + margin + borderY(g, 2),
157
- );
158
-
159
- // ---------------------------------------------------------------------------
160
- // Origin translation
161
- // ---------------------------------------------------------------------------
162
-
163
- const shiftCluster = (sg: Graph, ox: number, oy: number): void => {
164
- const b = sg.info.bb;
165
- sg.info.bb = makeBox(b.ll.x - ox, b.ll.y - oy, b.ur.x - ox, b.ur.y - oy);
166
- };
167
-
168
- const shiftNode = (nd: Node, ox: number, oy: number): void => {
169
- nd.info.coord = { x: nd.info.coord.x - ox, y: nd.info.coord.y - oy };
170
- };
171
-
172
- /**
173
- * Subtract union.ll from all child positions so this cluster's LL = (0,0).
174
- * @see lib/osage/osageinit.c:layout (translate-to-origin loop)
175
- */
176
- export const translateToOrigin = (lists: ChildLists, u: Box): void => {
177
- for (const sg of lists.childGraphs) shiftCluster(sg, u.ll.x, u.ll.y);
178
- for (const nd of lists.childNodes) shiftNode(nd, u.ll.x, u.ll.y);
179
- };
180
-
181
- // ---------------------------------------------------------------------------
182
- // osageLayoutRec
183
- // ---------------------------------------------------------------------------
184
-
185
- const recurseSubclusters = (g: Graph, nClust: number, depth: number): number => {
186
- let nvs = 0;
187
- for (let i = 1; i <= nClust; i++) {
188
- const sg = g.info.clust![i - 1];
189
- osageLayoutRec(sg, depth + 1);
190
- nvs += sg.nodes.size;
191
- }
192
- return nvs;
193
- };
194
-
195
- const packAndPlace = (g: Graph, nClust: number, nvs: number, depth: number): void => {
196
- const p = buildPackInfo(g);
197
- const lists = buildChildLists(g, nClust, nvs, p);
198
- if (p.vals === null && lists.vals.length > 0) p.vals = lists.vals;
199
- const pts = putRects(lists.gs.length, lists.gs, p);
200
- if (!pts) return;
201
- let u = applyOffsets(lists, pts);
202
- u = applyLabelExpansion(g, g.nodes.size - nvs + nClust, u);
203
- u = addMarginAndBorder(g, depth > 0 ? p.margin / 2 : 0, u);
204
- translateToOrigin(lists, u);
205
- g.info.bb = makeBox(0, 0, u.ur.x - u.ll.x, u.ur.y - u.ll.y);
206
- };
207
-
208
- /**
209
- * Recursively pack all direct children (subclusters + loose nodes) of g.
210
- * @see lib/osage/osageinit.c:layout
211
- */
212
- export const osageLayoutRec = (g: Graph, depth: number): void => {
213
- const nClust = g.info.n_cluster ?? 0;
214
- const nvs = recurseSubclusters(g, nClust, depth);
215
- if (g.nodes.size - nvs + nClust === 0 && !g.info.label) {
216
- g.info.bb = makeBox(0, 0, DFLT_SZ, DFLT_SZ);
217
- return;
218
- }
219
- packAndPlace(g, nClust, nvs, depth);
220
- };
221
-
222
- // ---------------------------------------------------------------------------
223
- // osageReposition
224
- // ---------------------------------------------------------------------------
225
-
226
- const repositionNodes = (g: Graph, bb: Box): void => {
227
- for (const n of g.nodes.values()) {
228
- if (PARENT(n) !== g) continue;
229
- n.info.coord = { x: n.info.coord.x + bb.ll.x, y: n.info.coord.y + bb.ll.y };
230
- }
231
- };
232
-
233
- const shiftSubclusterBb = (sg: Graph, parentBb: Box): void => {
234
- const s = sg.info.bb;
235
- sg.info.bb = makeBox(
236
- s.ll.x + parentBb.ll.x, s.ll.y + parentBb.ll.y,
237
- s.ur.x + parentBb.ll.x, s.ur.y + parentBb.ll.y,
238
- );
239
- };
240
-
241
- /**
242
- * Convert cluster-relative coordinates to absolute root-relative coordinates.
243
- * @see lib/osage/osageinit.c:reposition
244
- */
245
- export const osageReposition = (g: Graph, depth: number): void => {
246
- const bb = g.info.bb;
247
- const nClust = g.info.n_cluster ?? 0;
248
- if (depth > 0) repositionNodes(g, bb);
249
- for (let i = 1; i <= nClust; i++) {
250
- const sg = g.info.clust![i - 1];
251
- if (depth > 0) shiftSubclusterBb(sg, bb);
252
- osageReposition(sg, depth + 1);
253
- }
254
- };
@@ -1,46 +0,0 @@
1
- // SPDX-License-Identifier: EPL-2.0
2
-
3
- /**
4
- * Shared test helpers for pack module tests.
5
- * Isolated in their own file so Lizard counts each function independently.
6
- */
7
-
8
- import { expect } from 'vitest';
9
- import type { Box, Point } from '../../model/geom.js';
10
- import { type PackInfo, PackMode } from './types.js';
11
-
12
- export function box(llx: number, lly: number, urx: number, ury: number): Box {
13
- return { ll: { x: llx, y: lly }, ur: { x: urx, y: ury } };
14
- }
15
-
16
- export function defaultPackInfo(overrides?: Partial<PackInfo>): PackInfo {
17
- return {
18
- aspect: 1, sz: 0, margin: 0, doSplines: false,
19
- mode: PackMode.Graph, fixed: null, vals: null, flags: 0,
20
- ...overrides,
21
- };
22
- }
23
-
24
- export function applyPlaces(bbs: Box[], places: Point[]): Box[] {
25
- return bbs.map((bb, i) => {
26
- const p = places[i] ?? { x: 0, y: 0 };
27
- return box(bb.ll.x + p.x, bb.ll.y + p.y, bb.ur.x + p.x, bb.ur.y + p.y);
28
- });
29
- }
30
-
31
- function overlaps(a: Box, b: Box): boolean {
32
- return a.ur.x > b.ll.x && b.ur.x > a.ll.x &&
33
- a.ur.y > b.ll.y && b.ur.y > a.ll.y;
34
- }
35
-
36
- export function assertNoOverlap(placed: Box[]): void {
37
- for (let i = 0; i < placed.length; i++) {
38
- for (let j = i + 1; j < placed.length; j++) {
39
- const a = placed[i];
40
- const b = placed[j];
41
- if (a !== undefined && b !== undefined) {
42
- expect(overlaps(a, b), `boxes ${i} and ${j} overlap`).toBe(false);
43
- }
44
- }
45
- }
46
- }
@@ -1,120 +0,0 @@
1
- // SPDX-License-Identifier: EPL-2.0
2
-
3
- /**
4
- * Internal tree node and construction helpers for the patchwork layout.
5
- *
6
- * Mirrors treenode_t and mkTree from lib/patchwork/patchwork.c.
7
- *
8
- * @see lib/patchwork/patchwork.c:treenode_t
9
- * @see lib/patchwork/patchwork.c:mkTree
10
- */
11
-
12
- import type { Graph } from '../../model/graph.js';
13
- import type { Node } from '../../model/node.js';
14
- import type { Rectangle } from './tree-map.js';
15
-
16
- /** @see lib/patchwork/patchwork.c:treenode_t */
17
- export interface TreeNode {
18
- area: number;
19
- childArea: number;
20
- r: Rectangle;
21
- leftChild: TreeNode | null;
22
- rightSib: TreeNode | null;
23
- kind: 'graph' | 'node';
24
- ref: Graph | Node;
25
- nChildren: number;
26
- }
27
-
28
- /** Default area when attribute is missing or zero. @see lib/patchwork/patchwork.c */
29
- const DFLT_SZ = 1.0;
30
-
31
- /** Area multiplier for numerical stability. @see lib/patchwork/patchwork.c */
32
- const SCALE = 1000.0;
33
-
34
- /** @see lib/patchwork/patchwork.c:getArea */
35
- export function getArea(attrs: Map<string, string>): number {
36
- const raw = attrs.get('area');
37
- let area = raw !== undefined ? parseFloat(raw) : DFLT_SZ;
38
- if (!isFinite(area) || area === 0) area = DFLT_SZ;
39
- return area * SCALE;
40
- }
41
-
42
- /** @see lib/patchwork/patchwork.c:fullArea (mp parameter) */
43
- export function getInset(attrs: Map<string, string>): number {
44
- const raw = attrs.get('inset');
45
- if (raw === undefined) return 0;
46
- const v = parseFloat(raw);
47
- return isFinite(v) ? v : 0;
48
- }
49
-
50
- /** wid = 2*inset + sqrt(childArea), area = wid². @see lib/patchwork/patchwork.c:fullArea */
51
- export function fullArea(childArea: number, inset: number): number {
52
- const wid = 2.0 * inset + Math.sqrt(childArea);
53
- return wid * wid;
54
- }
55
-
56
- /** Mutable accumulator used during tree construction. */
57
- export interface ChildAccum {
58
- first: TreeNode | null;
59
- prev: TreeNode | null;
60
- area: number;
61
- nChildren: number;
62
- }
63
-
64
- /** Append all cluster children of g to accum. @see lib/patchwork/patchwork.c:mkTree */
65
- export function appendClusters(g: Graph, sparent: Map<Node, Graph>, accum: ChildAccum): void {
66
- for (const subg of g.info.clust ?? []) {
67
- const cp = mkTree(subg, sparent);
68
- accum.nChildren++;
69
- accum.area += cp.area;
70
- if (!accum.first) accum.first = cp;
71
- if (accum.prev) accum.prev.rightSib = cp;
72
- accum.prev = cp;
73
- }
74
- }
75
-
76
- /** Append unclaimed direct nodes of g to accum. @see lib/patchwork/patchwork.c:mkTree */
77
- export function appendNodes(g: Graph, sparent: Map<Node, Graph>, accum: ChildAccum): void {
78
- for (const n of g.nodes.values()) {
79
- if (sparent.has(n)) continue;
80
- const cp: TreeNode = {
81
- area: getArea(n.attrs),
82
- childArea: 0,
83
- r: { x: [0, 0], size: [0, 0] },
84
- leftChild: null, rightSib: null,
85
- kind: 'node', ref: n, nChildren: 0,
86
- };
87
- accum.nChildren++;
88
- accum.area += cp.area;
89
- if (!accum.first) accum.first = cp;
90
- if (accum.prev) accum.prev.rightSib = cp;
91
- accum.prev = cp;
92
- sparent.set(n, g);
93
- }
94
- }
95
-
96
- /**
97
- * Recursively build the internal TreeNode tree from a Graph hierarchy.
98
- * Nodes already owned by a cluster (sparent set) are skipped.
99
- * @see lib/patchwork/patchwork.c:mkTree
100
- */
101
- export function mkTree(g: Graph, sparent: Map<Node, Graph>): TreeNode {
102
- const p: TreeNode = {
103
- area: 0, childArea: 0,
104
- r: { x: [0, 0], size: [0, 0] },
105
- leftChild: null, rightSib: null,
106
- kind: 'graph', ref: g, nChildren: 0,
107
- };
108
- const accum: ChildAccum = { first: null, prev: null, area: 0, nChildren: 0 };
109
- appendClusters(g, sparent, accum);
110
- appendNodes(g, sparent, accum);
111
- p.nChildren = accum.nChildren;
112
- if (accum.nChildren > 0) {
113
- p.childArea = accum.area;
114
- p.area = fullArea(accum.area, getInset(g.attrs));
115
- } else {
116
- p.area = getArea(g.attrs);
117
- }
118
- p.leftChild = accum.first;
119
- return p;
120
- }
package/src/xdot/lex.ts DELETED
@@ -1,151 +0,0 @@
1
- // SPDX-License-Identifier: EPL-2.0
2
- /**
3
- * Low-level lexer primitives for xdot attribute string parsing.
4
- * Ported from ~/git/graphviz/lib/xdot/xdot.c helper functions.
5
- */
6
-
7
- import type { XdotAlign, XdotPoint, XdotRect, XdotPolyline } from "./types.js";
8
-
9
- export type PR<T> = { val: T; pos: number } | null;
10
-
11
- // ---------------------------------------------------------------------------
12
- // Character helpers
13
- // ---------------------------------------------------------------------------
14
- export function isWs(c: string): boolean {
15
- return c === " " || c === "\t" || c === "\n" || c === "\r";
16
- }
17
-
18
- export function isDigit(c: string): boolean {
19
- return c >= "0" && c <= "9";
20
- }
21
-
22
- export function isAlnum(c: string): boolean {
23
- return isDigit(c) || (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
24
- }
25
-
26
- export function skipWs(s: string, pos: number): number {
27
- while (pos < s.length && isWs(s[pos])) pos++;
28
- return pos;
29
- }
30
-
31
- // ---------------------------------------------------------------------------
32
- // Digit / exponent consumers
33
- // ---------------------------------------------------------------------------
34
- export function consumeDigits(s: string, i: number): number {
35
- while (i < s.length && isDigit(s[i])) i++;
36
- return i;
37
- }
38
-
39
- function consumeExponent(s: string, i: number): number {
40
- if (i >= s.length) return i;
41
- if (s[i] !== "e" && s[i] !== "E") return i;
42
- i++;
43
- if (i < s.length && (s[i] === "+" || s[i] === "-")) i++;
44
- return consumeDigits(s, i);
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // parseReal — @see xdot.c:parseReal
49
- // ---------------------------------------------------------------------------
50
- export function parseReal(s: string, pos: number): PR<number> {
51
- let i = skipWs(s, pos);
52
- const start = i;
53
- if (i < s.length && (s[i] === "-" || s[i] === "+")) i++;
54
- const digStart = i;
55
- i = consumeDigits(s, i);
56
- if (i < s.length && s[i] === ".") { i++; i = consumeDigits(s, i); }
57
- i = consumeExponent(s, i);
58
- if (i === start || i === digStart) return null;
59
- return { val: parseFloat(s.slice(start, i)), pos: i };
60
- }
61
-
62
- // ---------------------------------------------------------------------------
63
- // parseInt_ — @see xdot.c:parseInt
64
- // ---------------------------------------------------------------------------
65
- export function parseInt_(s: string, pos: number): PR<number> {
66
- let i = skipWs(s, pos);
67
- const start = i;
68
- if (i < s.length && (s[i] === "-" || s[i] === "+")) i++;
69
- const digStart = i;
70
- i = consumeDigits(s, i);
71
- if (i === digStart) return null;
72
- return { val: parseInt(s.slice(start, i), 10), pos: i };
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // parseUInt_ — @see xdot.c:parseUInt
77
- // ---------------------------------------------------------------------------
78
- export function parseUInt_(s: string, pos: number): PR<number> {
79
- const i = skipWs(s, pos);
80
- const end = consumeDigits(s, i);
81
- if (end === i) return null;
82
- return { val: parseInt(s.slice(i, end), 10), pos: end };
83
- }
84
-
85
- // ---------------------------------------------------------------------------
86
- // parseRect — @see xdot.c:parseRect
87
- // ---------------------------------------------------------------------------
88
- export function parseRect(s: string, pos: number): PR<XdotRect> {
89
- const rx = parseReal(s, pos); if (!rx) return null;
90
- const ry = parseReal(s, rx.pos); if (!ry) return null;
91
- const rw = parseReal(s, ry.pos); if (!rw) return null;
92
- const rh = parseReal(s, rw.pos); if (!rh) return null;
93
- return { val: { x: rx.val, y: ry.val, w: rw.val, h: rh.val }, pos: rh.pos };
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // parsePoint / parsePolyline — @see xdot.c:parsePolyline
98
- // ---------------------------------------------------------------------------
99
- function parsePoint(s: string, pos: number): PR<XdotPoint> {
100
- const rx = parseReal(s, pos); if (!rx) return null;
101
- const ry = parseReal(s, rx.pos); if (!ry) return null;
102
- return { val: { x: rx.val, y: ry.val, z: 0 }, pos: ry.pos };
103
- }
104
-
105
- export function parsePolyline(s: string, pos: number): PR<XdotPolyline> {
106
- const ri = parseUInt_(s, pos); if (!ri) return null;
107
- let cur = ri.pos;
108
- const pts: XdotPoint[] = [];
109
- for (let i = 0; i < ri.val; i++) {
110
- const rp = parsePoint(s, cur); if (!rp) return null;
111
- pts.push(rp.val);
112
- cur = rp.pos;
113
- }
114
- return { val: { pts }, pos: cur };
115
- }
116
-
117
- // ---------------------------------------------------------------------------
118
- // parseString — @see xdot.c:parseString
119
- // len counts source characters; a lone `\` is an escape prefix (not counted).
120
- // ---------------------------------------------------------------------------
121
- function nextAccounted(ch: string, prev: string, n: number): number {
122
- return (ch !== "\\" || prev === "\\") ? n + 1 : n;
123
- }
124
-
125
- export function parseString(s: string, pos: number): PR<string> {
126
- const ri = parseInt_(s, pos);
127
- if (!ri || ri.val <= 0) return null;
128
- const len = ri.val;
129
- let cur = skipWs(s, ri.pos);
130
- if (s[cur] !== "-") return null;
131
- cur++;
132
- let out = "";
133
- let accounted = 0;
134
- for (let j = 0; accounted < len; j++) {
135
- if (cur + j >= s.length) return null;
136
- const ch = s[cur + j];
137
- out += ch;
138
- accounted = nextAccounted(ch, j > 0 ? s[cur + j - 1] : "", accounted);
139
- if (accounted >= len) return { val: out, pos: cur + j + 1 };
140
- }
141
- return null;
142
- }
143
-
144
- // ---------------------------------------------------------------------------
145
- // parseAlign — @see xdot.c:parseAlign
146
- // ---------------------------------------------------------------------------
147
- export function parseAlign(s: string, pos: number): PR<XdotAlign> {
148
- const ri = parseInt_(s, pos); if (!ri) return null;
149
- const align: XdotAlign = ri.val < 0 ? "left" : ri.val > 0 ? "right" : "center";
150
- return { val: align, pos: ri.pos };
151
- }