@knowvah/dot-engine 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowvah/dot-engine",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
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",
@@ -76,10 +76,12 @@
76
76
  "docs:brand": "node docs-site/copy-brand-assets.mjs",
77
77
  "docs:copy-reports": "node docs-site/copy-reports.mjs",
78
78
  "docs:copy-goldens": "node docs-site/copy-goldens.mjs",
79
+ "docs:i18n-status": "node scripts/docs-i18n-status.mjs",
80
+ "docs:i18n-hashes": "node scripts/docs-i18n-status.mjs --write-hashes",
79
81
  "docs:api": "typedoc",
80
- "docs:dev": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:api && vitepress dev docs-site",
82
+ "docs:dev": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:i18n-hashes && npm run docs:api && vitepress dev docs-site",
81
83
  "predocs:build": "npm run parser:regen",
82
- "docs:build": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:api && vitepress build docs-site",
84
+ "docs:build": "npm run docs:brand && npm run docs:copy-reports && npm run docs:copy-goldens && npm run docs:i18n-hashes && npm run docs:api && vitepress build docs-site",
83
85
  "docs:preview": "vitepress preview docs-site",
84
86
  "release": "semantic-release",
85
87
  "publish:check": "npm run typecheck && npm run build && npm pack --dry-run",
@@ -33,8 +33,8 @@ import { ShapeKind, type ShapeDesc } from '../../common/types.js';
33
33
  import {
34
34
  collectNonAdjacentFlatGroup, routeFlatEdgeGroupFaithful,
35
35
  } from './splines-flat-multi.js';
36
- import { makeFlatLabeledEdge, makeAdjFlatNoPortEdge } from './splines-flat-labeled.js';
37
- import { EDGETYPE_SPLINE, swapEndsP, swapEdgeSpline, getMainEdge } from './splines.js';
36
+ import { makeFlatLabeledEdge, makeAdjFlatNoPortEdge, isAdjFlatCandidate } from './splines-flat-labeled.js';
37
+ import { edgeType, swapEndsP, swapEdgeSpline, getMainEdge } from './splines.js';
38
38
  import { FLATORDER } from './fastgr.js';
39
39
  import { IGNORED } from './rank.js';
40
40
  import { buildDotSinfo } from './self-loop.js';
@@ -312,9 +312,18 @@ function isAdjacentPortFlat(e: GraphEdge, g: Graph): boolean {
312
312
  && isFlatAdjacent(g, e) && hasDefinedPort(e);
313
313
  }
314
314
 
315
- /** True when x is an unrouted adjacent same-rank flat edge with a port. */
315
+ /** True when x is an unrouted adjacent same-rank flat edge (port or not): C
316
+ * groups by main edge only, so a port-less sibling joins the aux call. */
316
317
  function isGroupableFlat(x: GraphEdge, g: Graph): boolean {
317
- return x.info.spl === undefined && isAdjacentPortFlat(x, g);
318
+ return x.info.spl === undefined && x.tail.info.rank !== undefined
319
+ && x.tail.info.rank === x.head.info.rank && isFlatAdjacent(g, x);
320
+ }
321
+
322
+ /** True when a port-less adjacent flat edge shares its group with a ported one:
323
+ * C's `ports` flag covers the whole group, so it routes via the aux graph. */
324
+ function joinsPortedFlatGroup(e: GraphEdge, g: Graph): boolean {
325
+ return isAdjFlatCandidate(e) && isGroupableFlat(e, g)
326
+ && collectAdjacentFlatGroup(e, g).some(hasDefinedPort);
318
327
  }
319
328
 
320
329
  /**
@@ -362,7 +371,7 @@ function routeFaithfulSidePort(e: GraphEdge, g: Graph): boolean {
362
371
  // @see lib/dotgen/dotsplines.c:dot_splines_ (ED_adjacent edge-group loop)
363
372
  if (sameRank && isFlatAdjacent(g, e)) {
364
373
  const group = collectAdjacentFlatGroup(e, g);
365
- return makeFlatAdjEdges(g, group, group.length, EDGETYPE_SPLINE) === 0
374
+ return makeFlatAdjEdges(g, group, group.length, edgeType(g)) === 0
366
375
  && e.info.spl !== undefined;
367
376
  }
368
377
  // Non-adjacent flats: C groups identical-port siblings into ONE make_flat_edge
@@ -372,7 +381,7 @@ function routeFaithfulSidePort(e: GraphEdge, g: Graph): boolean {
372
381
  // @see lib/dotgen/dotsplines.c:make_flat_edge, make_flat_bottom_edges
373
382
  if (sameRank) {
374
383
  const group = collectNonAdjacentFlatGroup(e, g);
375
- return routeFlatEdgeGroupFaithful(g, group, group.length) && e.info.spl !== undefined;
384
+ return routeFlatEdgeGroupFaithful(g, group, group.length, edgeType(g)) && e.info.spl !== undefined;
376
385
  }
377
386
  const pts = routeRegularEdgeFaithful(g, e);
378
387
  if (pts === null) return false;
@@ -410,6 +419,7 @@ function routeFlatEdge(e: GraphEdge, g: Graph): boolean {
410
419
  // above (make_flat_adj_edges → makeSimpleFlatLabels). Both decline for every
411
420
  // other edge, so only labeled flats are diverted. @see dotsplines.c:1527-1533
412
421
  if (makeFlatLabeledEdge(g, e)) return true;
422
+ if (joinsPortedFlatGroup(e, g) && routeFaithfulSidePort(e, g)) return true;
413
423
  if (makeAdjFlatNoPortEdge(g, e)) return true;
414
424
  if ((hasSidePort(e) || isAdjacentPortFlat(e, g)) && routeFaithfulSidePort(e, g)) return true;
415
425
  // Non-adjacent unlabeled flat with no side port (e.g. a same-rank cross-cluster
@@ -419,7 +429,7 @@ function routeFlatEdge(e: GraphEdge, g: Graph): boolean {
419
429
  && e.info.label === undefined && !isFlatAdjacent(g, e)) {
420
430
  const group = collectNonAdjacentFlatGroup(e, g);
421
431
  if (group.length > 0
422
- && routeFlatEdgeGroupFaithful(g, group, group.length)
432
+ && routeFlatEdgeGroupFaithful(g, group, group.length, edgeType(g))
423
433
  && e.info.spl !== undefined) return true;
424
434
  }
425
435
  return false;
@@ -58,7 +58,9 @@ function buildNodes(nodeArr: Node[]): OrthoNodeArr {
58
58
  * parallel multi-edge) is skipped. This is ortho's own concentrate mechanism,
59
59
  * distinct from class2's `edge_type == IGNORED` marking, and the keep-first
60
60
  * decision depends on this iteration order matching C's.
61
- * Self-loops (tail === head) are routed elsewhere and excluded here.
61
+ * Self-loops (tail === head) are gathered like any other edge; orthoEdges
62
+ * routes them via addLoop. Only Concentrate dedup skips edges.
63
+ * @see lib/ortho/ortho.c:orthoEdges
62
64
  * @see lib/ortho/ortho.c:1207-1228 (orthoEdges edge gather + Concentrate PS)
63
65
  */
64
66
  function buildEdges(
@@ -74,7 +76,6 @@ function buildEdges(
74
76
  const oes = outIndex.get(n);
75
77
  if (oes === undefined) continue;
76
78
  for (const e of oes) {
77
- if (e.tail === e.head) continue;
78
79
  if (concentrate) {
79
80
  const ti = e.tail.id;
80
81
  const hi = e.head.id;
@@ -103,7 +104,7 @@ function buildEdges(
103
104
  return result;
104
105
  }
105
106
 
106
- /** Build an OrthoGraph from the dot Graph (tail≠head edges only). */
107
+ /** Build an OrthoGraph from the dot Graph (every out-edge, self-loops included). */
107
108
  export function buildOrthoGraph(g: Graph): OrthoGraph {
108
109
  const nodeArr = nodesInSeq(g);
109
110
  const orthoNodes = buildNodes(nodeArr);
@@ -22,10 +22,12 @@ import type { Edge } from '../../model/edge.js';
22
22
  import type { Node } from '../../model/node.js';
23
23
  import type { Port, Box } from '../../model/geom.js';
24
24
  import type { Path, PathendT } from '../../common/types.js';
25
- import { routeSplines } from '../../common/splines-routespl.js';
25
+ import { routeSplines, routePolylines } from '../../common/splines-routespl.js';
26
26
  import { clipAndInstall } from '../../common/splines-clip.js';
27
27
  import { graphRanksep } from './position-aux.js';
28
28
  import { buildDotSinfo } from './self-loop.js';
29
+ import { EDGETYPE_LINE, EDGETYPE_SPLINE } from './splines.js';
30
+ import { makeSimpleFlat } from './splines-flat-labeled.js';
29
31
  import {
30
32
  isFlatAdjacent, makeFlatEndBox, topBoxes, bottomBoxes, flatSide, flatVspace,
31
33
  freshFlatPath, assembleFlatPath, flatBboxCtx,
@@ -41,6 +43,8 @@ interface GroupRouteCtx {
41
43
  tlast: Box;
42
44
  hlast: Box;
43
45
  bottom: boolean;
46
+ /** Edge type: SPLINE routes with routesplines, anything else routepolylines. */
47
+ et: number;
44
48
  stepx: number;
45
49
  stepy: number;
46
50
  /** Geometric left end node of the channel (lower order). routeSplines yields
@@ -132,7 +136,8 @@ function routeGroupEdge(e: Edge, i: number, c: GroupRouteCtx): boolean {
132
136
  ? bottomBoxes(c.tlast, c.hlast, endStepX, endStepY, c.stepy)
133
137
  : topBoxes(c.tlast, c.hlast, endStepX, endStepY, c.stepy);
134
138
  assembleFlatPath(c.P, copyPathEnd(c.tend), copyPathEnd(c.hend), mid);
135
- const pts = routeSplines(c.P);
139
+ // @see lib/dotgen/dotsplines.c:1603-1607 (et == EDGETYPE_SPLINE ? routesplines : routepolylines)
140
+ const pts = c.et === EDGETYPE_SPLINE ? routeSplines(c.P) : routePolylines(c.P);
136
141
  if (pts === null || pts.length === 0) return false;
137
142
  // routeSplines returns points left→right (tend = leftNode). Reverse for an edge
138
143
  // whose tail is the right node so the points run tail→head; pass ignoreSwap so
@@ -149,12 +154,20 @@ function routeGroupEdge(e: Edge, i: number, c: GroupRouteCtx): boolean {
149
154
  * makeFlatEnd tail+head; for i in 0..cnt-1 route edge i nested by (i+1)*step.
150
155
  * Installs spl on every edges[i]; returns true iff all installed. cnt=1 reduces
151
156
  * to routeFlatEdgeFaithful (AD-1). `edges` must be group-ordered (collect...).
157
+ * `et == EDGETYPE_LINE` short-circuits to makeSimpleFlat (C 1535-1537); the
158
+ * corridor router is routesplines only for EDGETYPE_SPLINE, else routepolylines.
152
159
  * @see lib/dotgen/dotsplines.c:make_flat_edge, make_flat_bottom_edges
153
160
  */
154
- export function routeFlatEdgeGroupFaithful(g: Graph, edges: Edge[], cnt: number): boolean {
161
+ export function routeFlatEdgeGroupFaithful(
162
+ g: Graph, edges: Edge[], cnt: number, et: number,
163
+ ): boolean {
155
164
  const lead = edges[0];
156
165
  if (g.info.rank === undefined || lead.tail.info.rank === undefined
157
166
  || lead.head.info.rank !== lead.tail.info.rank) return false;
167
+ if (et === EDGETYPE_LINE) {
168
+ makeSimpleFlat(edges, et);
169
+ return true;
170
+ }
158
171
  // C makefwdedge: build the channel left→right by node order, else the middle
159
172
  // box inverts for a right-to-left (tail right of head) flat edge.
160
173
  const tailIsLeft = (lead.tail.info.order ?? 0) <= (lead.head.info.order ?? 0);
@@ -179,7 +192,7 @@ export function routeFlatEdgeGroupFaithful(g: Graph, edges: Edge[], cnt: number)
179
192
  const stepy = flatVspace(g, leftNode, !bottom) / (cnt + 1);
180
193
  const rc: GroupRouteCtx = {
181
194
  P, tend, hend, tlast: tend.boxes[tend.boxn - 1], hlast: hend.boxes[hend.boxn - 1],
182
- bottom, stepx, stepy, leftNode,
195
+ bottom, et, stepx, stepy, leftNode,
183
196
  };
184
197
  for (let i = 0; i < cnt; i++) {
185
198
  if (!routeGroupEdge(edges[i], i, rc)) return false;
@@ -21,11 +21,11 @@ import { dotRank } from './rank.js';
21
21
  import { dotMincross } from './mincross.js';
22
22
  import { dotPosition } from './position.js';
23
23
  import { dotSameports } from './sameport.js';
24
- import { dotSplines_, swapEndsP, swapEdgeSpline } from './splines.js';
24
+ import { dotSplines_, swapEndsP, swapEdgeSpline, EDGETYPE_SPLINE } from './splines.js';
25
25
  import { makePort } from '../../model/edgeInfo.js';
26
26
  import { beginPath } from '../../common/splines-path-begin.js';
27
27
  import { endPath } from '../../common/splines-path-end.js';
28
- import { routeSplines } from '../../common/splines-routespl.js';
28
+ import { routeSplines, routePolylines } from '../../common/splines-routespl.js';
29
29
  import { addBox, resolvePort } from '../../common/splines-path-shared.js';
30
30
  import { TOP, BOTTOM, FLATEDGE } from '../../common/splines-constants.js';
31
31
  import {
@@ -363,8 +363,11 @@ function normalizeCopiedFlatSpline(orig: Edge, auxe: Edge): void {
363
363
  * makeSimpleFlat branch are out of scope (callers gate on a side port).
364
364
  * @see lib/dotgen/dotsplines.c:make_flat_adj_edges
365
365
  */
366
- export function makeFlatAdjEdges(g: Graph, edges: Edge[], cnt: number, _et: number): number {
366
+ export function makeFlatAdjEdges(g: Graph, edges: Edge[], cnt: number, et: number): number {
367
367
  const aux = buildFlatAux(g, edges, cnt);
368
+ // C setEdgeType(auxg, et); inlined (index.ts's setEdgeType would be an import cycle).
369
+ // @see lib/dotgen/dotsplines.c:make_flat_adj_edges
370
+ aux.auxg.info.flags = (aux.auxg.info.flags & ~0xf) | (et & 0xf);
368
371
  dotInitNodeEdge(aux.auxg);
369
372
  dotRank(aux.auxg);
370
373
  if (dotMincross(aux.auxg) !== 0) return 1;
@@ -571,12 +574,11 @@ export function assembleFlatPath(P: Path, tend: PathendT, hend: PathendT, mid: B
571
574
  * faithful `beginPath → routeSplines → endPath → clipAndInstall` pipeline.
572
575
  * Returns spline control points in graphviz-internal (y-up) coordinates, or
573
576
  * null when the edge is not a same-rank edge. Does NOT clip or install — the
574
- * caller (routeOneEdge) does that via clipAndInstall, mirroring how SR3 wires
575
- * the regular faithful path.
577
+ * caller (routeOneEdge) does that via clipAndInstall (as the regular path).
576
578
  *
577
579
  * @see lib/dotgen/dotsplines.c:make_flat_edge (cnt=1, non-adjacent spline)
578
580
  */
579
- export function routeFlatEdgeFaithful(g: Graph, e: Edge): Point[] | null {
581
+ export function routeFlatEdgeFaithful(g: Graph, e: Edge, et: number): Point[] | null {
580
582
  const tn = e.tail;
581
583
  const r = tn.info.rank;
582
584
  if (g.info.rank === undefined || r === undefined || e.head.info.rank !== r) return null;
@@ -588,11 +590,9 @@ export function routeFlatEdgeFaithful(g: Graph, e: Edge): Point[] | null {
588
590
  const hend = makeFlatEndBox({ ctx, P, e, n: e.head, side, ranksep, isBegin: false });
589
591
  const stepx = (g.info.nodesep ?? 18) / 2;
590
592
  const stepy = flatVspace(g, tn, !bottom) / 2;
591
- const tlast = tend.boxes[tend.boxn - 1];
592
- const hlast = hend.boxes[hend.boxn - 1];
593
- const mid = bottom
594
- ? bottomBoxes(tlast, hlast, stepx, stepy, stepy)
595
- : topBoxes(tlast, hlast, stepx, stepy, stepy);
593
+ const [tlast, hlast] = [tend.boxes[tend.boxn - 1], hend.boxes[hend.boxn - 1]];
594
+ const mid = (bottom ? bottomBoxes : topBoxes)(tlast, hlast, stepx, stepy, stepy);
596
595
  assembleFlatPath(P, tend, hend, mid);
597
- return routeSplines(P);
596
+ // @see lib/dotgen/dotsplines.c:1603-1607
597
+ return et === EDGETYPE_SPLINE ? routeSplines(P) : routePolylines(P);
598
598
  }
package/src/parser/dot.js CHANGED
@@ -291,12 +291,11 @@ function peg$parse(input, options) {
291
291
  function peg$f15() { return "node"; }
292
292
  function peg$f16() { return "edge"; }
293
293
  function peg$f17(defs, rest) { return defs.concat(rest); }
294
- function peg$f18(defs) { return defs; }
295
- function peg$f19() { return []; }
296
- function peg$f20(a) { return a; }
297
- function peg$f21(pairs) { return pairs; }
298
- function peg$f22(k, v) { return { key: k, value: v }; }
299
- function peg$f23(head, chain, attrs) {
294
+ function peg$f18() { return []; }
295
+ function peg$f19(a) { return a; }
296
+ function peg$f20(pairs) { return pairs; }
297
+ function peg$f21(k, v) { return { key: k, value: v }; }
298
+ function peg$f22(head, chain, attrs) {
300
299
  if (chain.length > 0) {
301
300
  const nodes = [head].concat(chain);
302
301
  return { type: "edge", nodes: nodes, attrs: attrs };
@@ -310,28 +309,28 @@ function peg$parse(input, options) {
310
309
  return { type: "node", id: head, attrs: attrs };
311
310
  }
312
311
  }
313
- function peg$f24(s) { return s; }
314
- function peg$f25(n) { return n; }
315
- function peg$f26(id, port, compass) { return { id: id, port: port, compass: compass }; }
316
- function peg$f27(id, port) { return { id: id, port: port, compass: null }; }
317
- function peg$f28(id) { return { id: id, port: null, compass: null }; }
318
- function peg$f29(s, rest) { return [s].concat(rest); }
319
- function peg$f30() { return []; }
320
- function peg$f31(id, stmts) { return { type: "subgraph", id: id, stmts: stmts }; }
312
+ function peg$f23(s) { return s; }
313
+ function peg$f24(n) { return n; }
314
+ function peg$f25(id, port, compass) { return { id: id, port: port, compass: compass }; }
315
+ function peg$f26(id, port) { return { id: id, port: port, compass: null }; }
316
+ function peg$f27(id) { return { id: id, port: null, compass: null }; }
317
+ function peg$f28(s, rest) { return [s].concat(rest); }
318
+ function peg$f29() { return []; }
319
+ function peg$f30(id, stmts) { return { type: "subgraph", id: id, stmts: stmts }; }
320
+ function peg$f31(stmts) { return { type: "subgraph", id: null, stmts: stmts }; }
321
321
  function peg$f32(stmts) { return { type: "subgraph", id: null, stmts: stmts }; }
322
- function peg$f33(stmts) { return { type: "subgraph", id: null, stmts: stmts }; }
323
- function peg$f34(first, s) { return s; }
324
- function peg$f35(first, rest) { return rest.length > 0 ? first + rest.join("") : first; }
325
- function peg$f36(chars) { return chars.join(""); }
326
- function peg$f37() { return '"'; }
327
- function peg$f38() { return ''; }
328
- function peg$f39(c) { return '\\' + c; }
329
- function peg$f40(content) { return "\u0001" + content; }
330
- function peg$f41(chars) { return chars.join(""); }
331
- function peg$f42(nested, content) { return "<" + content + ">"; }
332
- function peg$f43(n) { return n; }
333
- function peg$f44(first, rest) { return first + rest.join(""); }
334
- function peg$f45(s) { return Array.isArray(s) ? s.flat(Infinity).filter(x => x !== null && x !== undefined).join("") : String(s); }
322
+ function peg$f33(first, s) { return s; }
323
+ function peg$f34(first, rest) { return rest.length > 0 ? first + rest.join("") : first; }
324
+ function peg$f35(chars) { return chars.join(""); }
325
+ function peg$f36() { return '"'; }
326
+ function peg$f37() { return ''; }
327
+ function peg$f38(c) { return '\\' + c; }
328
+ function peg$f39(content) { return "\u0001" + content; }
329
+ function peg$f40(chars) { return chars.join(""); }
330
+ function peg$f41(nested, content) { return "<" + content + ">"; }
331
+ function peg$f42(n) { return n; }
332
+ function peg$f43(first, rest) { return first + rest.join(""); }
333
+ function peg$f44(s) { return Array.isArray(s) ? s.flat(Infinity).filter(x => x !== null && x !== undefined).join("") : String(s); }
335
334
  let peg$currPos = options.peg$currPos | 0;
336
335
  let peg$savedPos = peg$currPos;
337
336
  const peg$posDetailsCache = [{ line: 1, column: 1 }];
@@ -749,7 +748,7 @@ function peg$parse(input, options) {
749
748
  s1 = peg$parseAttrType();
750
749
  if (s1 !== peg$FAILED) {
751
750
  s2 = peg$parse_();
752
- s3 = peg$parseAttrList();
751
+ s3 = peg$parseAttrListReq();
753
752
  if (s3 !== peg$FAILED) {
754
753
  peg$savedPos = s0;
755
754
  s0 = peg$f12(s1, s3);
@@ -806,7 +805,7 @@ function peg$parse(input, options) {
806
805
  return s0;
807
806
  }
808
807
 
809
- function peg$parseAttrList() {
808
+ function peg$parseAttrListReq() {
810
809
  let s0, s1, s2, s3, s4, s5, s6, s7;
811
810
 
812
811
  s0 = peg$currPos;
@@ -846,45 +845,20 @@ function peg$parse(input, options) {
846
845
  peg$currPos = s0;
847
846
  s0 = peg$FAILED;
848
847
  }
848
+
849
+ return s0;
850
+ }
851
+
852
+ function peg$parseAttrList() {
853
+ let s0, s1;
854
+
855
+ s0 = peg$parseAttrListReq();
849
856
  if (s0 === peg$FAILED) {
850
857
  s0 = peg$currPos;
851
- s1 = peg$parse_();
852
- if (input.charCodeAt(peg$currPos) === 91) {
853
- s2 = peg$c3;
854
- peg$currPos++;
855
- } else {
856
- s2 = peg$FAILED;
857
- if (peg$silentFails === 0) { peg$fail(peg$e3); }
858
- }
859
- if (s2 !== peg$FAILED) {
860
- s3 = peg$parse_();
861
- s4 = peg$parseAttrDefs();
862
- s5 = peg$parse_();
863
- if (input.charCodeAt(peg$currPos) === 93) {
864
- s6 = peg$c4;
865
- peg$currPos++;
866
- } else {
867
- s6 = peg$FAILED;
868
- if (peg$silentFails === 0) { peg$fail(peg$e4); }
869
- }
870
- if (s6 !== peg$FAILED) {
871
- peg$savedPos = s0;
872
- s0 = peg$f18(s4);
873
- } else {
874
- peg$currPos = s0;
875
- s0 = peg$FAILED;
876
- }
877
- } else {
878
- peg$currPos = s0;
879
- s0 = peg$FAILED;
880
- }
881
- if (s0 === peg$FAILED) {
882
- s0 = peg$currPos;
883
- s1 = '';
884
- peg$savedPos = s0;
885
- s1 = peg$f19();
886
- s0 = s1;
887
- }
858
+ s1 = '';
859
+ peg$savedPos = s0;
860
+ s1 = peg$f18();
861
+ s0 = s1;
888
862
  }
889
863
 
890
864
  return s0;
@@ -919,7 +893,7 @@ function peg$parse(input, options) {
919
893
  s5 = null;
920
894
  }
921
895
  peg$savedPos = s2;
922
- s2 = peg$f20(s3);
896
+ s2 = peg$f19(s3);
923
897
  } else {
924
898
  peg$currPos = s2;
925
899
  s2 = peg$FAILED;
@@ -950,14 +924,14 @@ function peg$parse(input, options) {
950
924
  s5 = null;
951
925
  }
952
926
  peg$savedPos = s2;
953
- s2 = peg$f20(s3);
927
+ s2 = peg$f19(s3);
954
928
  } else {
955
929
  peg$currPos = s2;
956
930
  s2 = peg$FAILED;
957
931
  }
958
932
  }
959
933
  peg$savedPos = s0;
960
- s1 = peg$f21(s1);
934
+ s1 = peg$f20(s1);
961
935
  s0 = s1;
962
936
 
963
937
  return s0;
@@ -982,7 +956,7 @@ function peg$parse(input, options) {
982
956
  s5 = peg$parseAtom();
983
957
  if (s5 !== peg$FAILED) {
984
958
  peg$savedPos = s0;
985
- s0 = peg$f22(s1, s5);
959
+ s0 = peg$f21(s1, s5);
986
960
  } else {
987
961
  peg$currPos = s0;
988
962
  s0 = peg$FAILED;
@@ -1012,7 +986,7 @@ function peg$parse(input, options) {
1012
986
  s5 = peg$parseAttrList();
1013
987
  if (s5 !== peg$FAILED) {
1014
988
  peg$savedPos = s0;
1015
- s0 = peg$f23(s1, s3, s5);
989
+ s0 = peg$f22(s1, s3, s5);
1016
990
  } else {
1017
991
  peg$currPos = s0;
1018
992
  s0 = peg$FAILED;
@@ -1036,7 +1010,7 @@ function peg$parse(input, options) {
1036
1010
  s1 = peg$parseSubgraph();
1037
1011
  if (s1 !== peg$FAILED) {
1038
1012
  peg$savedPos = s0;
1039
- s1 = peg$f24(s1);
1013
+ s1 = peg$f23(s1);
1040
1014
  }
1041
1015
  s0 = s1;
1042
1016
  if (s0 === peg$FAILED) {
@@ -1044,7 +1018,7 @@ function peg$parse(input, options) {
1044
1018
  s1 = peg$parseNode();
1045
1019
  if (s1 !== peg$FAILED) {
1046
1020
  peg$savedPos = s0;
1047
- s1 = peg$f25(s1);
1021
+ s1 = peg$f24(s1);
1048
1022
  }
1049
1023
  s0 = s1;
1050
1024
  }
@@ -1083,7 +1057,7 @@ function peg$parse(input, options) {
1083
1057
  s9 = peg$parseAtom();
1084
1058
  if (s9 !== peg$FAILED) {
1085
1059
  peg$savedPos = s0;
1086
- s0 = peg$f26(s1, s5, s9);
1060
+ s0 = peg$f25(s1, s5, s9);
1087
1061
  } else {
1088
1062
  peg$currPos = s0;
1089
1063
  s0 = peg$FAILED;
@@ -1121,7 +1095,7 @@ function peg$parse(input, options) {
1121
1095
  s5 = peg$parseAtom();
1122
1096
  if (s5 !== peg$FAILED) {
1123
1097
  peg$savedPos = s0;
1124
- s0 = peg$f27(s1, s5);
1098
+ s0 = peg$f26(s1, s5);
1125
1099
  } else {
1126
1100
  peg$currPos = s0;
1127
1101
  s0 = peg$FAILED;
@@ -1139,7 +1113,7 @@ function peg$parse(input, options) {
1139
1113
  s1 = peg$parseAtom();
1140
1114
  if (s1 !== peg$FAILED) {
1141
1115
  peg$savedPos = s0;
1142
- s1 = peg$f28(s1);
1116
+ s1 = peg$f27(s1);
1143
1117
  }
1144
1118
  s0 = s1;
1145
1119
  }
@@ -1161,7 +1135,7 @@ function peg$parse(input, options) {
1161
1135
  s5 = peg$parseEdgeChain();
1162
1136
  if (s5 !== peg$FAILED) {
1163
1137
  peg$savedPos = s0;
1164
- s0 = peg$f29(s3, s5);
1138
+ s0 = peg$f28(s3, s5);
1165
1139
  } else {
1166
1140
  peg$currPos = s0;
1167
1141
  s0 = peg$FAILED;
@@ -1178,7 +1152,7 @@ function peg$parse(input, options) {
1178
1152
  s0 = peg$currPos;
1179
1153
  s1 = '';
1180
1154
  peg$savedPos = s0;
1181
- s1 = peg$f30();
1155
+ s1 = peg$f29();
1182
1156
  s0 = s1;
1183
1157
  }
1184
1158
 
@@ -1245,7 +1219,7 @@ function peg$parse(input, options) {
1245
1219
  }
1246
1220
  if (s9 !== peg$FAILED) {
1247
1221
  peg$savedPos = s0;
1248
- s0 = peg$f31(s3, s7);
1222
+ s0 = peg$f30(s3, s7);
1249
1223
  } else {
1250
1224
  peg$currPos = s0;
1251
1225
  s0 = peg$FAILED;
@@ -1292,7 +1266,7 @@ function peg$parse(input, options) {
1292
1266
  }
1293
1267
  if (s7 !== peg$FAILED) {
1294
1268
  peg$savedPos = s0;
1295
- s0 = peg$f32(s5);
1269
+ s0 = peg$f31(s5);
1296
1270
  } else {
1297
1271
  peg$currPos = s0;
1298
1272
  s0 = peg$FAILED;
@@ -1332,7 +1306,7 @@ function peg$parse(input, options) {
1332
1306
  }
1333
1307
  if (s5 !== peg$FAILED) {
1334
1308
  peg$savedPos = s0;
1335
- s0 = peg$f33(s3);
1309
+ s0 = peg$f32(s3);
1336
1310
  } else {
1337
1311
  peg$currPos = s0;
1338
1312
  s0 = peg$FAILED;
@@ -1372,7 +1346,7 @@ function peg$parse(input, options) {
1372
1346
  s7 = peg$parseQAtom();
1373
1347
  if (s7 !== peg$FAILED) {
1374
1348
  peg$savedPos = s3;
1375
- s3 = peg$f34(s1, s7);
1349
+ s3 = peg$f33(s1, s7);
1376
1350
  } else {
1377
1351
  peg$currPos = s3;
1378
1352
  s3 = peg$FAILED;
@@ -1397,7 +1371,7 @@ function peg$parse(input, options) {
1397
1371
  s7 = peg$parseQAtom();
1398
1372
  if (s7 !== peg$FAILED) {
1399
1373
  peg$savedPos = s3;
1400
- s3 = peg$f34(s1, s7);
1374
+ s3 = peg$f33(s1, s7);
1401
1375
  } else {
1402
1376
  peg$currPos = s3;
1403
1377
  s3 = peg$FAILED;
@@ -1408,7 +1382,7 @@ function peg$parse(input, options) {
1408
1382
  }
1409
1383
  }
1410
1384
  peg$savedPos = s0;
1411
- s0 = peg$f35(s1, s2);
1385
+ s0 = peg$f34(s1, s2);
1412
1386
  } else {
1413
1387
  peg$currPos = s0;
1414
1388
  s0 = peg$FAILED;
@@ -1458,7 +1432,7 @@ function peg$parse(input, options) {
1458
1432
  }
1459
1433
  if (s3 !== peg$FAILED) {
1460
1434
  peg$savedPos = s0;
1461
- s0 = peg$f36(s2);
1435
+ s0 = peg$f35(s2);
1462
1436
  } else {
1463
1437
  peg$currPos = s0;
1464
1438
  s0 = peg$FAILED;
@@ -1484,7 +1458,7 @@ function peg$parse(input, options) {
1484
1458
  }
1485
1459
  if (s1 !== peg$FAILED) {
1486
1460
  peg$savedPos = s0;
1487
- s1 = peg$f37();
1461
+ s1 = peg$f36();
1488
1462
  }
1489
1463
  s0 = s1;
1490
1464
  if (s0 === peg$FAILED) {
@@ -1498,7 +1472,7 @@ function peg$parse(input, options) {
1498
1472
  }
1499
1473
  if (s1 !== peg$FAILED) {
1500
1474
  peg$savedPos = s0;
1501
- s1 = peg$f38();
1475
+ s1 = peg$f37();
1502
1476
  }
1503
1477
  s0 = s1;
1504
1478
  if (s0 === peg$FAILED) {
@@ -1520,7 +1494,7 @@ function peg$parse(input, options) {
1520
1494
  }
1521
1495
  if (s2 !== peg$FAILED) {
1522
1496
  peg$savedPos = s0;
1523
- s0 = peg$f39(s2);
1497
+ s0 = peg$f38(s2);
1524
1498
  } else {
1525
1499
  peg$currPos = s0;
1526
1500
  s0 = peg$FAILED;
@@ -1566,7 +1540,7 @@ function peg$parse(input, options) {
1566
1540
  }
1567
1541
  if (s3 !== peg$FAILED) {
1568
1542
  peg$savedPos = s0;
1569
- s0 = peg$f40(s2);
1543
+ s0 = peg$f39(s2);
1570
1544
  } else {
1571
1545
  peg$currPos = s0;
1572
1546
  s0 = peg$FAILED;
@@ -1590,7 +1564,7 @@ function peg$parse(input, options) {
1590
1564
  s2 = peg$parseHtmlChar();
1591
1565
  }
1592
1566
  peg$savedPos = s0;
1593
- s1 = peg$f41(s1);
1567
+ s1 = peg$f40(s1);
1594
1568
  s0 = s1;
1595
1569
 
1596
1570
  return s0;
@@ -1619,7 +1593,7 @@ function peg$parse(input, options) {
1619
1593
  }
1620
1594
  if (s3 !== peg$FAILED) {
1621
1595
  peg$savedPos = s0;
1622
- s0 = peg$f42(s1, s2);
1596
+ s0 = peg$f41(s1, s2);
1623
1597
  } else {
1624
1598
  peg$currPos = s0;
1625
1599
  s0 = peg$FAILED;
@@ -1665,7 +1639,7 @@ function peg$parse(input, options) {
1665
1639
  s2 = peg$parseName();
1666
1640
  if (s2 !== peg$FAILED) {
1667
1641
  peg$savedPos = s0;
1668
- s0 = peg$f43(s2);
1642
+ s0 = peg$f42(s2);
1669
1643
  } else {
1670
1644
  peg$currPos = s0;
1671
1645
  s0 = peg$FAILED;
@@ -1710,7 +1684,7 @@ function peg$parse(input, options) {
1710
1684
  }
1711
1685
  }
1712
1686
  peg$savedPos = s0;
1713
- s0 = peg$f44(s1, s2);
1687
+ s0 = peg$f43(s1, s2);
1714
1688
  } else {
1715
1689
  peg$currPos = s0;
1716
1690
  s0 = peg$FAILED;
@@ -1908,7 +1882,7 @@ function peg$parse(input, options) {
1908
1882
  }
1909
1883
  if (s1 !== peg$FAILED) {
1910
1884
  peg$savedPos = s0;
1911
- s1 = peg$f45(s1);
1885
+ s1 = peg$f44(s1);
1912
1886
  }
1913
1887
  s0 = s1;
1914
1888
 
@@ -57,7 +57,7 @@ Stmt
57
57
  // grammar.y: attrtype : T_graph | T_node | T_edge
58
58
  // grammar.y: graphattrdefs : attrassignment (bare key=value at graph level)
59
59
  AttrStmt
60
- = t:AttrType _ attrs:AttrList
60
+ = t:AttrType _ attrs:AttrListReq
61
61
  { return { type: "attr", target: t, attrs: attrs }; }
62
62
  / a:AttrAssignment
63
63
  { return { type: "assign", key: a.key, value: a.value }; }
@@ -68,13 +68,14 @@ AttrType
68
68
  / NodeKw { return "node"; }
69
69
  / EdgeKw { return "edge"; }
70
70
 
71
- // grammar.y: optattr : attrlist | /* empty */
72
- // grammar.y: attrlist : optattr '[' optattrdefs ']'
73
- AttrList
71
+ // grammar.y: attrlist : optattr '[' optattrdefs ']' (one or more bracket groups)
72
+ AttrListReq
74
73
  = _ "[" _ defs:AttrDefs _ "]" rest:AttrList
75
74
  { return defs.concat(rest); }
76
- / _ "[" _ defs:AttrDefs _ "]"
77
- { return defs; }
75
+
76
+ // grammar.y: optattr : attrlist | /* empty */
77
+ AttrList
78
+ = AttrListReq
78
79
  / "" { return []; }
79
80
 
80
81
  // grammar.y: optattrdefs : optattrdefs attrdefs | /* empty */