@knowvah/dot-engine 2.6.3 → 2.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
- package/dist/common/bezier.d.ts +12 -0
- package/dist/common/bezier.d.ts.map +1 -0
- package/dist/common/htmltable-attr-warn.d.ts +41 -0
- package/dist/common/htmltable-attr-warn.d.ts.map +1 -0
- package/dist/common/htmltable-fontinfo.d.ts +42 -0
- package/dist/common/htmltable-fontinfo.d.ts.map +1 -0
- package/dist/common/htmltable-lex-expat.d.ts +63 -0
- package/dist/common/htmltable-lex-expat.d.ts.map +1 -0
- package/dist/common/htmltable-lex-units.d.ts +32 -0
- package/dist/common/htmltable-lex-units.d.ts.map +1 -0
- package/dist/common/htmltable-lex.d.ts +10 -2
- package/dist/common/htmltable-lex.d.ts.map +1 -1
- package/dist/common/htmltable-parse-attrs.d.ts +7 -5
- package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
- package/dist/common/htmltable-parse-font.d.ts +33 -0
- package/dist/common/htmltable-parse-font.d.ts.map +1 -0
- package/dist/common/htmltable-parse-state.d.ts +39 -0
- package/dist/common/htmltable-parse-state.d.ts.map +1 -0
- package/dist/common/htmltable-parse.d.ts +18 -23
- package/dist/common/htmltable-parse.d.ts.map +1 -1
- package/dist/common/htmltable-pos.d.ts.map +1 -1
- package/dist/common/htmltable-types.d.ts +33 -3
- package/dist/common/htmltable-types.d.ts.map +1 -1
- package/dist/common/htmltable.d.ts +3 -2
- package/dist/common/htmltable.d.ts.map +1 -1
- package/dist/common/spline-midpoint.d.ts.map +1 -1
- package/dist/common/splines-geom.d.ts +5 -6
- package/dist/common/splines-geom.d.ts.map +1 -1
- package/dist/common/splines-routespl.d.ts.map +1 -1
- package/dist/common/taper.d.ts.map +1 -1
- package/dist/gvc/anchor.d.ts.map +1 -1
- package/dist/index.js +1366 -648
- package/dist/index.js.map +4 -4
- package/dist/layout/dot/compound-clip.d.ts +5 -5
- package/dist/layout/dot/compound-clip.d.ts.map +1 -1
- package/dist/layout/dot/compound-geom.d.ts +2 -7
- package/dist/layout/dot/compound-geom.d.ts.map +1 -1
- package/dist/layout/dot/compound.d.ts +1 -1
- package/dist/layout/dot/compound.d.ts.map +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts +1 -1
- package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
- package/dist/layout/dot/ortho-adapter.d.ts +13 -4
- package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
- package/dist/layout/dot/splines-flat-labeled.d.ts.map +1 -1
- package/dist/layout/dot/splines-label.d.ts.map +1 -1
- package/dist/layout/neato/bfs.d.ts +10 -3
- package/dist/layout/neato/bfs.d.ts.map +1 -1
- package/dist/layout/neato/init.d.ts +6 -11
- package/dist/layout/neato/init.d.ts.map +1 -1
- package/dist/layout/neato/splines.d.ts.map +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/scan-token.d.ts +40 -0
- package/dist/parser/scan-token.d.ts.map +1 -0
- package/dist/render/svg-edge-split.d.ts.map +1 -1
- package/dist/render.js +1388 -670
- package/dist/render.js.map +4 -4
- package/package.json +1 -1
- package/src/common/arrows-shapes-poly.ts +9 -10
- package/src/common/bezier.ts +33 -0
- package/src/common/htmltable-attr-warn.ts +270 -0
- package/src/common/htmltable-fontinfo.ts +83 -0
- package/src/common/htmltable-lex-expat.ts +357 -0
- package/src/common/htmltable-lex-units.ts +75 -0
- package/src/common/htmltable-lex.ts +221 -160
- package/src/common/htmltable-parse-attrs.ts +10 -14
- package/src/common/htmltable-parse-font.ts +92 -0
- package/src/common/htmltable-parse-state.ts +64 -0
- package/src/common/htmltable-parse.ts +138 -98
- package/src/common/htmltable-pos.ts +9 -8
- package/src/common/htmltable-types.ts +35 -3
- package/src/common/htmltable.ts +14 -7
- package/src/common/spline-midpoint.ts +2 -22
- package/src/common/splines-geom.ts +5 -25
- package/src/common/splines-routespl.ts +21 -13
- package/src/common/taper.ts +2 -14
- package/src/gvc/anchor.ts +5 -14
- package/src/layout/dot/__fixtures__/2796-flat-labeled-lost.dot +161 -0
- package/src/layout/dot/compound-clip.ts +9 -19
- package/src/layout/dot/compound-geom.ts +6 -23
- package/src/layout/dot/compound.ts +1 -1
- package/src/layout/dot/edge-route-clip.ts +6 -29
- package/src/layout/dot/ortho-adapter.ts +5 -5
- package/src/layout/dot/splines-flat-labeled.ts +3 -1
- package/src/layout/dot/splines-label.ts +3 -29
- package/src/layout/neato/bfs.ts +13 -4
- package/src/layout/neato/init.ts +15 -15
- package/src/layout/neato/splines.ts +4 -8
- package/src/parser/index.ts +13 -1
- package/src/parser/scan-token.ts +289 -0
- package/src/render/svg-edge-split.ts +5 -13
package/src/layout/neato/init.ts
CHANGED
|
@@ -321,21 +321,20 @@ export function mapModelToStress(model: number): number {
|
|
|
321
321
|
// solveModel helpers
|
|
322
322
|
// ---------------------------------------------------------------------------
|
|
323
323
|
|
|
324
|
-
/**
|
|
325
|
-
* Run stress majorization on the graph and write results back to node positions.
|
|
326
|
-
*
|
|
327
|
-
* @see lib/neatogen/neatoinit.c:majorization
|
|
328
|
-
*/
|
|
324
|
+
/** Run stress majorization and write positions back. @see lib/neatogen/neatoinit.c:majorization */
|
|
329
325
|
export function runMajorization(g: Graph, mode: number, model: number): void {
|
|
330
326
|
void model; // only MODEL_SHORTPATH distances are exercised by the suite
|
|
331
327
|
const nodeList = assignNodeIds(g);
|
|
332
328
|
const n = nodeList.length;
|
|
333
329
|
const haveLen = graphHasLen(g);
|
|
334
|
-
const vtx = makeGraphDataC(g, nodeList, haveLen);
|
|
330
|
+
const vtx = makeGraphDataC(g, nodeList, haveLen, mode === MODE_HIER || mode === MODE_IPSEP);
|
|
335
331
|
checkStart(g); // C: srand48(seed) immediately before initLayout draws
|
|
336
332
|
const maxi = lateDouble(g.attrs.get('maxiter'), DFLT_ITERATIONS, 0);
|
|
337
333
|
const epsilon = lateDouble(g.attrs.get('epsilon'), 1e-4, 0); // DFLT_TOLERANCE
|
|
338
|
-
|
|
334
|
+
// C ipsep (stress_majorization_cola, constrained_majorization_ipsep.c:150) always uses
|
|
335
|
+
// compute_apsp_packed; kD_mkernel uses the weighted APSP iff graph->ewgts (stress.c:855).
|
|
336
|
+
const weighted = haveLen && mode !== MODE_IPSEP;
|
|
337
|
+
const Dij = weighted ? computeWeightedApspPacked(vtx, n) : computeApspPacked(vtx, n);
|
|
339
338
|
const dCoords = [new Float64Array(n), new Float64Array(n)];
|
|
340
339
|
stressMajorizationKDMkernel(Dij, n, dCoords, nodeList, {
|
|
341
340
|
dim: DFLT_DIM, opts: OPT_EXP_FLAG, maxi, epsilon,
|
|
@@ -354,17 +353,18 @@ export function graphHasLen(g: Graph): boolean {
|
|
|
354
353
|
}
|
|
355
354
|
|
|
356
355
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* APSP uses BFS hop counts.
|
|
356
|
+
* C makeGraphData's vtx_data: per node out-edges then in-edges (agfstedge order), self at
|
|
357
|
+
* index 0, duplicate neighbours merged (len keeps the max). ewgts: lens (default 1.0) if any
|
|
358
|
+
* len attr, else 1.0 under haveDir (hier/ipsep), else absent (C NULL). eweights (haveWt)
|
|
359
|
+
* and edists are not built: nothing on a supported path reads them.
|
|
362
360
|
* @see lib/neatogen/neatoinit.c:makeGraphData
|
|
363
361
|
* @see lib/neatogen/stuff.c:setEdgeLen
|
|
364
362
|
*/
|
|
365
|
-
export function makeGraphDataC(
|
|
363
|
+
export function makeGraphDataC(
|
|
364
|
+
g: Graph, nodeList: Node[], haveLen: boolean, haveDir: boolean,
|
|
365
|
+
): VtxData[] {
|
|
366
366
|
const vtx: VtxData[] = nodeList.map((_, i) => ({
|
|
367
|
-
nedges: 1, edges: [i], ewgts: haveLen ? [0] : [],
|
|
367
|
+
nedges: 1, edges: [i], ewgts: haveLen || haveDir ? [0] : [],
|
|
368
368
|
}));
|
|
369
369
|
for (const np of nodeList) {
|
|
370
370
|
const i = np.info.id!;
|
|
@@ -382,7 +382,7 @@ export function makeGraphDataC(g: Graph, nodeList: Node[], haveLen: boolean): Vt
|
|
|
382
382
|
}
|
|
383
383
|
seen.set(vid, entry.nedges);
|
|
384
384
|
entry.edges.push(vid);
|
|
385
|
-
if (haveLen) entry.ewgts.push(dist);
|
|
385
|
+
if (haveLen || haveDir) entry.ewgts.push(dist); // C neatoinit.c:827: haveDir 1.0
|
|
386
386
|
entry.nedges++;
|
|
387
387
|
}
|
|
388
388
|
}
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
import { makeSelfEdge } from '../../common/splines.js';
|
|
33
33
|
import { newSpline, clipAndInstall } from '../../common/splines-clip.js';
|
|
34
34
|
import { orthoEdges } from '../../ortho/index.js';
|
|
35
|
+
import { PointSet } from '../../common/pointset.js';
|
|
36
|
+
import { isConcentrateDup } from '../dot/ortho-adapter.js';
|
|
35
37
|
import type { OrthoGraph, OrthoEdge, OrthoPoint } from '../../ortho/index.js';
|
|
36
38
|
import {
|
|
37
39
|
EDGETYPE_NONE, EDGETYPE_LINE, EDGETYPE_ORTHO,
|
|
@@ -595,17 +597,11 @@ class OrthoHelper {
|
|
|
595
597
|
// pair is routed (ortho.c:1223-1233). Self-loops are NOT filtered — the
|
|
596
598
|
// maze routes them via addLoop.
|
|
597
599
|
const concentrate = g.root.info.concentrate ?? false;
|
|
598
|
-
const
|
|
600
|
+
const ps = concentrate ? new PointSet() : null;
|
|
599
601
|
const edges: TaggedOrthoEdge[] = [];
|
|
600
602
|
for (const n of nodesInSeq(g)) {
|
|
601
603
|
for (const e of n.outEdges(g)) {
|
|
602
|
-
if (
|
|
603
|
-
const ti = e.tail.id;
|
|
604
|
-
const hi = e.head.id;
|
|
605
|
-
const key = ti <= hi ? `${ti}|${hi}` : `${hi}|${ti}`;
|
|
606
|
-
if (seen.has(key)) continue;
|
|
607
|
-
seen.add(key);
|
|
608
|
-
}
|
|
604
|
+
if (ps && isConcentrateDup(ps, e)) continue;
|
|
609
605
|
// C attachOrthoEdges endpoints: p1 = ND_coord(tail) + ED_tail_port.p,
|
|
610
606
|
// q1 = ND_coord(head) + ED_head_port.p (ortho.c:1075-1076). Plumb the
|
|
611
607
|
// port offset so compass/record ports (tailport=s, headport=n, …) exit
|
package/src/parser/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { Edge } from '../model/edge.js';
|
|
|
14
14
|
import { isHtmlValue, htmlValueContent } from '../common/html-string.js';
|
|
15
15
|
import { buildFromAst } from './builder.js';
|
|
16
16
|
import { scanNumberAmbiguities, numberAmbiguityMessage } from './number-scan.js';
|
|
17
|
+
import { syntaxErrorMessage } from './scan-token.js';
|
|
17
18
|
import type { ParsedGraph } from './ast.js';
|
|
18
19
|
import type { GvErrorCode, GvExpectation } from '../errors.js';
|
|
19
20
|
import { DotEngineError, friendlyMessageFor, invalidArgType } from '../errors.js';
|
|
@@ -249,6 +250,7 @@ export function parse(src: string): Graph {
|
|
|
249
250
|
if (isPeggyError(err)) {
|
|
250
251
|
const start = err.location.start;
|
|
251
252
|
warnNumberAmbiguities(src, start.offset);
|
|
253
|
+
console.error(syntaxErrorMessage(src, start.offset));
|
|
252
254
|
const code: GvErrorCode =
|
|
253
255
|
err.found === null ? 'SYNTAX_UNEXPECTED_EOF' : 'SYNTAX_ERROR';
|
|
254
256
|
throw new ParseError(
|
|
@@ -270,8 +272,18 @@ export function parse(src: string): Graph {
|
|
|
270
272
|
}
|
|
271
273
|
throw err;
|
|
272
274
|
}
|
|
275
|
+
try {
|
|
276
|
+
validateEdgeOperators(src, ast.directed);
|
|
277
|
+
} catch (err: unknown) {
|
|
278
|
+
// C's lexer hands the wrong operator to bison as '-': a syntax error there.
|
|
279
|
+
const offset = err instanceof ParseError ? err.location.offset : undefined;
|
|
280
|
+
if (offset !== undefined) {
|
|
281
|
+
warnNumberAmbiguities(src, offset);
|
|
282
|
+
console.error(syntaxErrorMessage(src, offset));
|
|
283
|
+
}
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
273
286
|
warnNumberAmbiguities(src, src.length);
|
|
274
|
-
validateEdgeOperators(src, ast.directed);
|
|
275
287
|
return buildFromAst(ast);
|
|
276
288
|
}
|
|
277
289
|
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What scan.l's flex scanner holds when bison reports a syntax error: the
|
|
5
|
+
* start condition, `yytext`, `line_num` and the string buffer. `aagerror`
|
|
6
|
+
* prints these, so a Peggy syntax error (which knows only an offset) is
|
|
7
|
+
* re-lexed here with scan.l's rules up to the offending token.
|
|
8
|
+
*
|
|
9
|
+
* The walk lexes the whole source from the start, the way flex does, because
|
|
10
|
+
* `line_num` depends on every newline consumed (comments, strings, `#line`
|
|
11
|
+
* directives) and an unterminated string or comment swallows the rest of the
|
|
12
|
+
* input. It stops at the first token that ends after the error offset.
|
|
13
|
+
*
|
|
14
|
+
* @see lib/cgraph/scan.l:aagerror, :118-146 (rules)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** flex start conditions of scan.l (`%x comment qstring hstring`). */
|
|
18
|
+
export type ScanState = 'INITIAL' | 'qstring' | 'hstring' | 'comment';
|
|
19
|
+
|
|
20
|
+
/** The scanner state `aagerror` reads. */
|
|
21
|
+
export interface ScanAtError {
|
|
22
|
+
state: ScanState;
|
|
23
|
+
/** `yytext` of the last token scanned; empty at EOF. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** `yyextra->line_num`. */
|
|
26
|
+
line: number;
|
|
27
|
+
/** `yyextra->Sbuf`: processed string text pending in qstring/hstring. */
|
|
28
|
+
buffer: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** scan.l's YY_BUF_SIZE as printed by aagerror. @see lib/cgraph/scan.l:aagerror */
|
|
32
|
+
const YY_BUF_SIZE = 16384;
|
|
33
|
+
/** `%.80s` in aagerror's "String starting:" line. */
|
|
34
|
+
const STRING_STARTING_BYTES = 80;
|
|
35
|
+
const ASCII_DEL = 0x7f;
|
|
36
|
+
|
|
37
|
+
/** One flex match: `end` is the next scan position. */
|
|
38
|
+
type Step =
|
|
39
|
+
| { kind: 'skip'; end: number; line?: number }
|
|
40
|
+
| { kind: 'token'; end: number; text: string }
|
|
41
|
+
| { kind: 'open'; state: ScanState; buffer: string };
|
|
42
|
+
|
|
43
|
+
/** scan.l LETTER `[A-Za-z_\200-\377]`: any non-ASCII character counts. */
|
|
44
|
+
function isLetter(c: string | undefined): boolean {
|
|
45
|
+
if (c === undefined) return false;
|
|
46
|
+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_'
|
|
47
|
+
|| c.charCodeAt(0) > ASCII_DEL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isDigit(c: string | undefined): boolean {
|
|
51
|
+
return c !== undefined && c >= '0' && c <= '9';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when a NUMBER starts at i (sign, digits, or a dot before a digit). */
|
|
55
|
+
function startsNumber(s: string, i: number): boolean {
|
|
56
|
+
const c = s[i];
|
|
57
|
+
if (isDigit(c)) return true;
|
|
58
|
+
if (c === '.') return isDigit(s[i + 1]);
|
|
59
|
+
if (c !== '-') return false;
|
|
60
|
+
return isDigit(s[i + 1]) || (s[i + 1] === '.' && isDigit(s[i + 2]));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** End index of the longest NUMBER match starting at i (scan.l:109). */
|
|
64
|
+
function matchNumber(s: string, i: number): number {
|
|
65
|
+
let j = s[i] === '-' ? i + 1 : i;
|
|
66
|
+
if (s[j] === '.') {
|
|
67
|
+
j++;
|
|
68
|
+
while (isDigit(s[j])) j++;
|
|
69
|
+
} else {
|
|
70
|
+
while (isDigit(s[j])) j++;
|
|
71
|
+
if (s[j] === '.') {
|
|
72
|
+
j++;
|
|
73
|
+
while (isDigit(s[j])) j++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (s[j] === '.' || isLetter(s[j])) j++;
|
|
77
|
+
return j;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** scan.l chkNum: the last character is a letter, or a second '.'. */
|
|
81
|
+
function badlyDelimited(text: string): boolean {
|
|
82
|
+
const c = text[text.length - 1]!;
|
|
83
|
+
const dot = text.indexOf('.');
|
|
84
|
+
return (!isDigit(c) && c !== '.') || (c === '.' && dot !== text.length - 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** End of the line holding i (the position of its '\n', or the length). */
|
|
88
|
+
function lineEnd(s: string, i: number): number {
|
|
89
|
+
const nl = s.indexOf('\n', i);
|
|
90
|
+
return nl === -1 ? s.length : nl;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* scan.l ppDirective on `#.*`: `#line N "file"` / `# N` resets line_num to
|
|
95
|
+
* N-1 (the line's own '\n' then counts), ignoring N <= 0.
|
|
96
|
+
* @see lib/cgraph/scan.l:ppDirective
|
|
97
|
+
*/
|
|
98
|
+
function directiveLine(text: string): number | undefined {
|
|
99
|
+
const rest = text.slice(1).replace(/^line/, '');
|
|
100
|
+
const m = /^\s*([+-]?\d+)/.exec(rest);
|
|
101
|
+
if (m === null) return undefined;
|
|
102
|
+
const lineno = Number.parseInt(m[1]!, 10);
|
|
103
|
+
return lineno <= 0 ? undefined : lineno - 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `<qstring>` rules: `\"` adds `"`, an escaped newline vanishes. */
|
|
107
|
+
function qstringBuffer(body: string): string {
|
|
108
|
+
return body.replace(/\\\\|\\"|\\\n/g, (m) => (m === '\\"' ? '"' : m === '\\\n' ? '' : m));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Lex a `"` string opened at i: closes, or runs to EOF still open. */
|
|
112
|
+
function lexQuoted(s: string, i: number): Step {
|
|
113
|
+
for (let j = i + 1; j < s.length; j++) {
|
|
114
|
+
if (s[j] === '\\' && j + 1 < s.length) j++;
|
|
115
|
+
else if (s[j] === '"') return { kind: 'token', end: j + 1, text: '"' };
|
|
116
|
+
}
|
|
117
|
+
return { kind: 'open', state: 'qstring', buffer: qstringBuffer(s.slice(i + 1)) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Lex a `<...>` HTML string opened at i, tracking nesting (scan.l:143-145). */
|
|
121
|
+
function lexHtml(s: string, i: number): Step {
|
|
122
|
+
let nest = 1;
|
|
123
|
+
for (let j = i + 1; j < s.length; j++) {
|
|
124
|
+
if (s[j] === '<') nest++;
|
|
125
|
+
else if (s[j] === '>' && --nest === 0) return { kind: 'token', end: j + 1, text: '>' };
|
|
126
|
+
}
|
|
127
|
+
return { kind: 'open', state: 'hstring', buffer: s.slice(i + 1) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Lex a `/*` comment opened at i. */
|
|
131
|
+
function lexComment(s: string, i: number): Step {
|
|
132
|
+
const close = s.indexOf('*/', i + 2);
|
|
133
|
+
if (close === -1) return { kind: 'open', state: 'comment', buffer: '' };
|
|
134
|
+
return { kind: 'skip', end: close + 2 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Lex `#...`: a line directive at the start of a line, else a comment. */
|
|
138
|
+
function lexHash(s: string, i: number): Step {
|
|
139
|
+
const end = lineEnd(s, i);
|
|
140
|
+
const atLineStart = i === 0 || s[i - 1] === '\n';
|
|
141
|
+
const line = atLineStart ? directiveLine(s.slice(i, end)) : undefined;
|
|
142
|
+
return line === undefined ? { kind: 'skip', end } : { kind: 'skip', end, line };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Lex NAME / NUMBER / edge operator / single character at i. */
|
|
146
|
+
function lexAtom(s: string, i: number): Step {
|
|
147
|
+
if (isLetter(s[i])) {
|
|
148
|
+
let j = i + 1;
|
|
149
|
+
while (isLetter(s[j]) || isDigit(s[j])) j++;
|
|
150
|
+
const text = s.slice(i, j);
|
|
151
|
+
// "\xEF\xBB\xBF" (a BOM) is ignored, not a NAME.
|
|
152
|
+
return text === '' ? { kind: 'skip', end: j } : { kind: 'token', end: j, text };
|
|
153
|
+
}
|
|
154
|
+
if (s[i] === '-' && (s[i + 1] === '-' || s[i + 1] === '>')) {
|
|
155
|
+
return { kind: 'token', end: i + 2, text: s.slice(i, i + 2) };
|
|
156
|
+
}
|
|
157
|
+
if (!startsNumber(s, i)) return { kind: 'token', end: i + 1, text: s[i]! };
|
|
158
|
+
const j = matchNumber(s, i);
|
|
159
|
+
const end = badlyDelimited(s.slice(i, j)) ? j - 1 : j; // yyless(leng-1)
|
|
160
|
+
return { kind: 'token', end, text: s.slice(i, end) };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Whitespace, comments and `#` lines at i; undefined for any other start. */
|
|
164
|
+
function lexTrivia(s: string, i: number): Step | undefined {
|
|
165
|
+
const c = s[i]!;
|
|
166
|
+
if (c === '\n' || c === ' ' || c === '\t' || c === '\r') return { kind: 'skip', end: i + 1 };
|
|
167
|
+
if (c === '#') return lexHash(s, i);
|
|
168
|
+
if (c !== '/') return undefined;
|
|
169
|
+
if (s[i + 1] === '*') return lexComment(s, i);
|
|
170
|
+
if (s[i + 1] === '/') return { kind: 'skip', end: lineEnd(s, i) };
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One INITIAL-state flex match at i. */
|
|
175
|
+
function lexStep(s: string, i: number): Step {
|
|
176
|
+
const trivia = lexTrivia(s, i);
|
|
177
|
+
if (trivia !== undefined) return trivia;
|
|
178
|
+
if (s[i] === '"') return lexQuoted(s, i);
|
|
179
|
+
if (s[i] === '<') return lexHtml(s, i);
|
|
180
|
+
return lexAtom(s, i);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Number of '\n' in s[from, to). */
|
|
184
|
+
function countNewlines(s: string, from: number, to: number): number {
|
|
185
|
+
let n = 0;
|
|
186
|
+
for (let i = s.indexOf('\n', from); i !== -1 && i < to; i = s.indexOf('\n', i + 1)) n++;
|
|
187
|
+
return n;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const HEADER_KEYWORDS = ['strict', 'graph', 'digraph'];
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The PEG matches the header keywords letter by letter, so a source ending in
|
|
194
|
+
* a proper prefix of one (`digra`) fails at EOF, past the NAME flex returned;
|
|
195
|
+
* C's parser rejects that NAME itself. Only the first token, or the one after
|
|
196
|
+
* `strict`, is a header position.
|
|
197
|
+
*/
|
|
198
|
+
function isTruncatedHeaderKeyword(
|
|
199
|
+
src: string,
|
|
200
|
+
step: { end: number; text: string },
|
|
201
|
+
offset: number,
|
|
202
|
+
seen: readonly string[],
|
|
203
|
+
): boolean {
|
|
204
|
+
if (offset !== src.length || step.end !== offset || seen.length > 1) return false;
|
|
205
|
+
if (seen.length === 1 && seen[0]!.toLowerCase() !== 'strict') return false;
|
|
206
|
+
const word = step.text.toLowerCase();
|
|
207
|
+
return HEADER_KEYWORDS.some((k) => k.length > word.length && k.startsWith(word));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* True when the parser's rejection at `offset` belongs to this token: the
|
|
212
|
+
* offset falls inside it or at its start. A stray '-' or '.' is the exception:
|
|
213
|
+
* the PEG's failed NUMBER attempt consumed it (and a following '.' after '-')
|
|
214
|
+
* before failing, so its offset lies just past the token flex returned.
|
|
215
|
+
*/
|
|
216
|
+
function reachesOffset(
|
|
217
|
+
src: string,
|
|
218
|
+
step: { end: number; text: string },
|
|
219
|
+
offset: number,
|
|
220
|
+
seen: readonly string[],
|
|
221
|
+
): boolean {
|
|
222
|
+
if (step.end > offset) return true;
|
|
223
|
+
if (isTruncatedHeaderKeyword(src, step, offset, seen)) return true;
|
|
224
|
+
if (step.text !== '-' && step.text !== '.') return false;
|
|
225
|
+
return offset <= step.end + (step.text === '-' && src[step.end] === '.' ? 1 : 0);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Scanner state when the parser rejects the token at `offset`: lex from the
|
|
230
|
+
* start and stop at the first token ending after `offset` (or at an
|
|
231
|
+
* unterminated string/comment, which runs to EOF). Past the last token the
|
|
232
|
+
* scanner is at EOF with an empty `yytext`.
|
|
233
|
+
* @see lib/cgraph/scan.l:aagerror
|
|
234
|
+
*/
|
|
235
|
+
export function scanAtError(src: string, offset: number): ScanAtError {
|
|
236
|
+
let line = 1;
|
|
237
|
+
let i = 0;
|
|
238
|
+
const seen: string[] = [];
|
|
239
|
+
while (i < src.length) {
|
|
240
|
+
const step = lexStep(src, i);
|
|
241
|
+
if (step.kind === 'open') {
|
|
242
|
+
line += countNewlines(src, i, src.length);
|
|
243
|
+
return { state: step.state, text: '', line, buffer: step.buffer };
|
|
244
|
+
}
|
|
245
|
+
if (step.kind === 'skip' && step.line !== undefined) line = step.line;
|
|
246
|
+
line += countNewlines(src, i, step.end);
|
|
247
|
+
if (step.kind === 'token' && reachesOffset(src, step, offset, seen)) {
|
|
248
|
+
return { state: 'INITIAL', text: step.text, line, buffer: '' };
|
|
249
|
+
}
|
|
250
|
+
if (step.kind === 'token') seen.push(step.text);
|
|
251
|
+
i = step.end;
|
|
252
|
+
}
|
|
253
|
+
return { state: 'INITIAL', text: '', line, buffer: '' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** `%.80s`: the first 80 bytes (UTF-8) of the string buffer. */
|
|
257
|
+
function firstBytes(s: string): string {
|
|
258
|
+
const bytes = new TextEncoder().encode(s);
|
|
259
|
+
if (bytes.length <= STRING_STARTING_BYTES) return s;
|
|
260
|
+
return new TextDecoder().decode(bytes.subarray(0, STRING_STARTING_BYTES));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The tail aagerror adds for an empty `yytext`, by start condition. */
|
|
264
|
+
function stateNote(at: ScanAtError): string {
|
|
265
|
+
const start = at.buffer.length > 0 ? firstBytes(at.buffer) : '';
|
|
266
|
+
switch (at.state) {
|
|
267
|
+
case 'qstring':
|
|
268
|
+
return ` scanning a quoted string (missing endquote? longer than ${YY_BUF_SIZE}?)`
|
|
269
|
+
+ (start === '' ? '' : `\nString starting:"${start}`);
|
|
270
|
+
case 'hstring':
|
|
271
|
+
return ` scanning a HTML string (missing '>'? bad nesting? longer than ${YY_BUF_SIZE}?)`
|
|
272
|
+
+ (start === '' ? '' : `\nString starting:<${start}`);
|
|
273
|
+
case 'comment':
|
|
274
|
+
return ` scanning a /*...*/ comment (missing '*/? longer than ${YY_BUF_SIZE}?)`;
|
|
275
|
+
case 'INITIAL':
|
|
276
|
+
return '';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The line `aagerror` hands to agerrorf for a syntax error at `offset`, as
|
|
282
|
+
* stderr shows it without the trailing newline and without the `<file>: `
|
|
283
|
+
* prefix. @see lib/cgraph/scan.l:aagerror
|
|
284
|
+
*/
|
|
285
|
+
export function syntaxErrorMessage(src: string, offset: number): string {
|
|
286
|
+
const at = scanAtError(src, offset);
|
|
287
|
+
const where = `Error: syntax error in line ${at.line}`;
|
|
288
|
+
return at.text === '' ? where + stateNote(at) : `${where} near '${at.text}'`;
|
|
289
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { Edge } from '../model/edge.js';
|
|
20
20
|
import type { RenderJob } from '../gvc/job.js';
|
|
21
21
|
import type { Point, Bezier } from '../model/geom.js';
|
|
22
|
+
import { bezier } from '../common/bezier.js';
|
|
22
23
|
import { parseSegs } from '../common/multicolor.js';
|
|
23
24
|
import { transformPoint } from '../gvc/device.js';
|
|
24
25
|
import { emitBezierPath, emitDash, emitPenWidth } from './svg-helpers.js';
|
|
@@ -43,21 +44,12 @@ function approxLen(list: Point[], off: number): number {
|
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* de Casteljau split of a single cubic (control points v[0..3]) at parameter t,
|
|
46
|
-
* returning the left and right 4-point sub-curves. Ports C's Bezier().
|
|
47
|
+
* returning the left and right 4-point sub-curves. Ports C's Bezier() via the shared fused bezier().
|
|
47
48
|
*/
|
|
48
49
|
function splitCubic(v: Point[], t: number): { left: Point[]; right: Point[] } {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const right: Point[] = new Array<Point>(4);
|
|
53
|
-
right[3] = row[3]!;
|
|
54
|
-
for (let i = 1; i <= 3; i++) {
|
|
55
|
-
for (let j = 0; j <= 3 - i; j++) {
|
|
56
|
-
row[j] = { x: u * row[j]!.x + t * row[j + 1]!.x, y: u * row[j]!.y + t * row[j + 1]!.y };
|
|
57
|
-
}
|
|
58
|
-
left.push(row[0]!);
|
|
59
|
-
right[3 - i] = row[3 - i]!;
|
|
60
|
-
}
|
|
50
|
+
const left: Point[] = [];
|
|
51
|
+
const right: Point[] = [];
|
|
52
|
+
bezier(v, t, left, right);
|
|
61
53
|
return { left, right };
|
|
62
54
|
}
|
|
63
55
|
|