@davidsouther/agathon 0.0.0-stage → 0.0.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/package.json +19 -4
- package/src/assert.ts +67 -0
- package/src/geometry/box.ts +76 -0
- package/src/geometry/circle.ts +296 -0
- package/src/geometry/geometry.ts +318 -0
- package/src/geometry/path.ts +315 -0
- package/src/geometry/quadtree.ts +321 -0
- package/src/geometry/rectangle.ts +129 -0
- package/src/geometry/rngon.ts +132 -0
- package/src/geometry/types.ts +102 -0
- package/src/geometry.ts +61 -0
- package/src/highlight/highlight.ts +85 -0
- package/src/index.ts +5 -0
- package/src/layout/grid/manager.ts +55 -0
- package/src/layout/grid.ts +1 -0
- package/src/layout/hierarchic/README.md +21 -0
- package/src/layout/hierarchic/acyclic.ts +249 -0
- package/src/layout/hierarchic/children.ts +169 -0
- package/src/layout/hierarchic/debug.ts +186 -0
- package/src/layout/hierarchic/layout.ts +266 -0
- package/src/layout/hierarchic/manager.ts +65 -0
- package/src/layout/hierarchic/normalize.ts +177 -0
- package/src/layout/hierarchic/order.ts +321 -0
- package/src/layout/hierarchic/position.ts +514 -0
- package/src/layout/hierarchic/rank.ts +629 -0
- package/src/layout/hierarchic.ts +2 -0
- package/src/layout/hulls/arrange.ts +94 -0
- package/src/layout/hulls/hulls.ts +243 -0
- package/src/layout/hulls/segments.ts +174 -0
- package/src/layout/hulls.ts +3 -0
- package/src/layout/manager/child_manager.ts +42 -0
- package/src/layout/manager/drag_manager.ts +112 -0
- package/src/layout/manager/manager.ts +425 -0
- package/src/layout/manager/pan_manager.ts +146 -0
- package/src/layout/manager/visibility_manager.ts +84 -0
- package/src/layout/manager/zoom_manager.ts +263 -0
- package/src/layout/manager.ts +6 -0
- package/src/layout/properties/defaults.ts +99 -0
- package/src/layout/properties/symbol.ts +99 -0
- package/src/layout/properties/weak_map.ts +73 -0
- package/src/layout/properties.ts +111 -0
- package/src/layout/types.ts +470 -0
- package/src/layout/util.ts +444 -0
- package/src/layout.ts +6 -0
- package/src/samples/diff_graph.ts +15 -0
- package/src/samples/edge_cases.ts +168 -0
- package/src/samples/gansner_with_states.ts +67 -0
- package/src/samples/load_generator.ts +213 -0
- package/src/samples/search_graph.ts +35 -0
- package/src/tsabre/algo.ts +126 -0
- package/src/tsabre/builder.ts +97 -0
- package/src/tsabre/compress.ts +15 -0
- package/src/tsabre/dijkstra/dijkstra.ts +76 -0
- package/src/tsabre/dijkstra/heap.ts +83 -0
- package/src/tsabre/graph.ts +808 -0
- package/src/tsabre/parse.ts +634 -0
- package/src/tsabre/print.ts +188 -0
- package/src/tsabre/sourcebuilder/code_content_buffer.ts +275 -0
- package/src/tsabre/sourcebuilder/printable.ts +33 -0
- package/src/tsabre.ts +6 -0
- package/src/types.ts +424 -0
- package/README.md +0 -3
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { CircleGeometry, type GeometryParams, parseGeometry } from "../geometry.ts";
|
|
2
|
+
import type {
|
|
3
|
+
EdgeConfig,
|
|
4
|
+
GraphConfig,
|
|
5
|
+
LayoutEdge,
|
|
6
|
+
LayoutGraph,
|
|
7
|
+
LayoutNode,
|
|
8
|
+
NodeConfig,
|
|
9
|
+
} from "../layout/types.ts";
|
|
10
|
+
import { type AttributeSet, ElementDiffPresence, type Node } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
import { TsabreGraph } from "./graph.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse a dot digraph block. The subset of DOT we support is LR(1), so it's a
|
|
16
|
+
* pretty simple recursive descent parser.
|
|
17
|
+
*/
|
|
18
|
+
export function parse(text: string): [LayoutGraph, Array<{}>] {
|
|
19
|
+
const g = new TsabreGraph<LayoutNode, LayoutEdge>();
|
|
20
|
+
|
|
21
|
+
const t = tokenizer(text);
|
|
22
|
+
const d = Digraph.from(t);
|
|
23
|
+
d.fill(g);
|
|
24
|
+
|
|
25
|
+
return [g, t.errors];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class Digraph {
|
|
29
|
+
private readonly statements: StatementList;
|
|
30
|
+
private constructor(statements: StatementList) {
|
|
31
|
+
this.statements = statements;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fill(g: LayoutGraph) {
|
|
35
|
+
this.statements.fill(g);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static from(this: { name: string }, tokens: Tokenizer): Digraph {
|
|
39
|
+
let token = tokens.peek();
|
|
40
|
+
if (token.type !== TokenType.Digraph) {
|
|
41
|
+
tokens.parseError(Digraph.name, "digraph", token);
|
|
42
|
+
} else {
|
|
43
|
+
tokens.next();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const statements = StatementList.from(tokens);
|
|
47
|
+
|
|
48
|
+
token = tokens.peek();
|
|
49
|
+
if (token.type !== TokenType.End) {
|
|
50
|
+
tokens.parseError(Digraph.name, "End", token);
|
|
51
|
+
}
|
|
52
|
+
return new Digraph(statements);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class StatementList {
|
|
57
|
+
private readonly statements: Array<EdgeStatement | NodeStatement>;
|
|
58
|
+
private readonly attributes: AttributeList;
|
|
59
|
+
private constructor(statements: Array<EdgeStatement | NodeStatement>, attributes: AttributeList) {
|
|
60
|
+
this.statements = statements;
|
|
61
|
+
this.attributes = attributes;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fill(g: LayoutGraph, parent?: Node) {
|
|
65
|
+
for (const statement of this.statements) {
|
|
66
|
+
statement.fill(g);
|
|
67
|
+
if (parent && statement instanceof NodeStatement) {
|
|
68
|
+
statement.setParent(g, parent);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
(
|
|
72
|
+
g as {
|
|
73
|
+
// Reopen for a quick assignment here.
|
|
74
|
+
layout?: Partial<GraphConfig>;
|
|
75
|
+
}
|
|
76
|
+
).layout = this.attributes.graphAttributes;
|
|
77
|
+
this.attributes.fill(g);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// tslint:disable-next-line:enforce-name-casing
|
|
81
|
+
static Empty() {
|
|
82
|
+
return new StatementList([], AttributeList.Empty());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static from(this: { name: string }, tokens: Tokenizer) {
|
|
86
|
+
const open = tokens.peek();
|
|
87
|
+
if (open.type !== TokenType.OpenBlock) {
|
|
88
|
+
tokens.parseError(StatementList.name, "{", open);
|
|
89
|
+
} else {
|
|
90
|
+
tokens.next();
|
|
91
|
+
}
|
|
92
|
+
let attributes = AttributeList.Empty();
|
|
93
|
+
const statements: Array<EdgeStatement | NodeStatement> = [];
|
|
94
|
+
|
|
95
|
+
while (tokens.hasNext) {
|
|
96
|
+
const token = tokens.peek();
|
|
97
|
+
let statement: EdgeStatement | NodeStatement | undefined;
|
|
98
|
+
switch (token.type) {
|
|
99
|
+
case TokenType.OpenAttr:
|
|
100
|
+
attributes = AttributeList.from(tokens);
|
|
101
|
+
break;
|
|
102
|
+
case TokenType.ID:
|
|
103
|
+
statement = Statement.from(tokens);
|
|
104
|
+
if (statement) statements.push(statement);
|
|
105
|
+
break;
|
|
106
|
+
case TokenType.CloseBlock:
|
|
107
|
+
tokens.next();
|
|
108
|
+
return new StatementList(statements, attributes);
|
|
109
|
+
case TokenType.End:
|
|
110
|
+
tokens.parseError(StatementList.name, "[ } ID", token);
|
|
111
|
+
return new StatementList(statements, attributes);
|
|
112
|
+
default:
|
|
113
|
+
tokens.next();
|
|
114
|
+
tokens.parseError(StatementList.name, "[ } ID", token);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
tokens.parseError(StatementList.name, "}", open);
|
|
118
|
+
if (statements.length > 0) {
|
|
119
|
+
return new StatementList(statements, attributes);
|
|
120
|
+
} else {
|
|
121
|
+
return StatementList.Empty();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// tslint:disable-next-line:class-as-namespace keeping static to fit the others.
|
|
127
|
+
class Statement {
|
|
128
|
+
static from(this: { name: string }, tokens: Tokenizer) {
|
|
129
|
+
const id = tokens.next();
|
|
130
|
+
if (id.type !== TokenType.ID) {
|
|
131
|
+
tokens.parseError(Statement.name, "id", id);
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const token = tokens.peek();
|
|
135
|
+
switch (token.type) {
|
|
136
|
+
case TokenType.Edge:
|
|
137
|
+
tokens.next();
|
|
138
|
+
return EdgeStatement.from(tokens, id as Token & { type: typeof TokenType.ID });
|
|
139
|
+
case TokenType.OpenAttr:
|
|
140
|
+
case TokenType.OpenBlock:
|
|
141
|
+
case TokenType.CloseBlock:
|
|
142
|
+
case TokenType.Semi:
|
|
143
|
+
case TokenType.End:
|
|
144
|
+
return NodeStatement.from(tokens, id as Token & { type: typeof TokenType.ID });
|
|
145
|
+
default:
|
|
146
|
+
// Advance past the bad token
|
|
147
|
+
tokens.next();
|
|
148
|
+
tokens.parseError(Statement.name, "-> [ { ;", token);
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class EdgeStatement {
|
|
155
|
+
private readonly edge: LayoutEdge;
|
|
156
|
+
|
|
157
|
+
private readonly attributes: AttributeList;
|
|
158
|
+
private constructor(
|
|
159
|
+
s: Token & { type: typeof TokenType.ID },
|
|
160
|
+
t: Token & { type: typeof TokenType.ID },
|
|
161
|
+
n: (Token & { type: typeof TokenType.ID }) | undefined,
|
|
162
|
+
attributes: AttributeList,
|
|
163
|
+
) {
|
|
164
|
+
this.attributes = attributes;
|
|
165
|
+
this.edge = {
|
|
166
|
+
source: s.value,
|
|
167
|
+
target: t.value,
|
|
168
|
+
presence: attributes.presence(),
|
|
169
|
+
layout: this.attributes.edgeAttributes,
|
|
170
|
+
};
|
|
171
|
+
if (n !== undefined) {
|
|
172
|
+
this.edge.name = n.value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fill(g: LayoutGraph) {
|
|
177
|
+
const source = this.edge.source;
|
|
178
|
+
const target = this.edge.target;
|
|
179
|
+
if (!g.hasNode(source)) g.setNode(source, { id: source });
|
|
180
|
+
if (!g.hasNode(target)) g.setNode(target, { id: target });
|
|
181
|
+
g.setEdge(this.edge, this.edge);
|
|
182
|
+
this.attributes.fill(this.edge);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static from(
|
|
186
|
+
this: { name: string },
|
|
187
|
+
tokens: Tokenizer,
|
|
188
|
+
id: Token & {
|
|
189
|
+
type: typeof TokenType.ID;
|
|
190
|
+
},
|
|
191
|
+
) {
|
|
192
|
+
const id2 = tokens.next();
|
|
193
|
+
if (id2.type !== TokenType.ID) {
|
|
194
|
+
tokens.parseError(EdgeStatement.name, "ID", id2);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let multi: (Token & { type: typeof TokenType.ID }) | undefined;
|
|
198
|
+
let token = tokens.peek();
|
|
199
|
+
|
|
200
|
+
if (token.type === TokenType.ID) {
|
|
201
|
+
multi = token as Token & { type: typeof TokenType.ID };
|
|
202
|
+
tokens.next();
|
|
203
|
+
token = tokens.peek();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let attributes = AttributeList.Empty();
|
|
207
|
+
if (token.type === TokenType.OpenAttr) {
|
|
208
|
+
attributes = AttributeList.from(tokens);
|
|
209
|
+
token = tokens.peek();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (token.type === TokenType.Semi) {
|
|
213
|
+
tokens.next();
|
|
214
|
+
return new EdgeStatement(id, id2 as Token & { type: typeof TokenType.ID }, multi, attributes);
|
|
215
|
+
} else if (token.type === TokenType.CloseBlock) {
|
|
216
|
+
return new EdgeStatement(id, id2 as Token & { type: typeof TokenType.ID }, multi, attributes);
|
|
217
|
+
} else {
|
|
218
|
+
tokens.parseError(EdgeStatement.name, "; or }", token);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (id2.type === TokenType.ID) {
|
|
222
|
+
return new EdgeStatement(id, id2 as Token & { type: typeof TokenType.ID }, multi, attributes);
|
|
223
|
+
} else {
|
|
224
|
+
tokens.parseError(EdgeStatement.name, "ID", id);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class NodeStatement {
|
|
232
|
+
private readonly node: LayoutNode;
|
|
233
|
+
private readonly attributeList: AttributeList;
|
|
234
|
+
private readonly statementList: StatementList;
|
|
235
|
+
private constructor(
|
|
236
|
+
id: Token & { type: typeof TokenType.ID },
|
|
237
|
+
attributeList: AttributeList,
|
|
238
|
+
statementList: StatementList,
|
|
239
|
+
) {
|
|
240
|
+
this.attributeList = attributeList;
|
|
241
|
+
this.statementList = statementList;
|
|
242
|
+
this.node = {
|
|
243
|
+
id: id.value,
|
|
244
|
+
presence: attributeList.presence(),
|
|
245
|
+
layout: attributeList.nodeAttributes,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fill(g: LayoutGraph) {
|
|
250
|
+
g.setNode(this.node.id, this.node);
|
|
251
|
+
this.attributeList.fill(this.node);
|
|
252
|
+
this.statementList.fill(g, this.node);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setParent(g: LayoutGraph, parent: Node) {
|
|
256
|
+
g.setParent(this.node.id, parent.id);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static from(
|
|
260
|
+
this: { name: string },
|
|
261
|
+
tokens: Tokenizer,
|
|
262
|
+
id: Token & {
|
|
263
|
+
type: typeof TokenType.ID;
|
|
264
|
+
},
|
|
265
|
+
) {
|
|
266
|
+
let attributes = AttributeList.Empty();
|
|
267
|
+
let token = tokens.peek();
|
|
268
|
+
if (token.type === TokenType.OpenAttr) {
|
|
269
|
+
attributes = AttributeList.from(tokens);
|
|
270
|
+
token = tokens.peek();
|
|
271
|
+
if (token.type === TokenType.CloseBlock) {
|
|
272
|
+
return new NodeStatement(id, attributes, StatementList.Empty());
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (token.type === TokenType.OpenBlock) {
|
|
276
|
+
const statements = StatementList.from(tokens);
|
|
277
|
+
token = tokens.peek();
|
|
278
|
+
if (token.type === TokenType.Semi) {
|
|
279
|
+
tokens.next();
|
|
280
|
+
}
|
|
281
|
+
return new NodeStatement(id, attributes, statements);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (token.type === TokenType.Semi) {
|
|
285
|
+
tokens.next();
|
|
286
|
+
return new NodeStatement(id, attributes, StatementList.Empty());
|
|
287
|
+
}
|
|
288
|
+
tokens.parseError(NodeStatement.name, ";", token);
|
|
289
|
+
return new NodeStatement(id, attributes, StatementList.Empty());
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class AttributeList implements AttributeSet {
|
|
294
|
+
private readonly attributes = new Map<string, Attribute>();
|
|
295
|
+
private constructor(attributes: Attribute[]) {
|
|
296
|
+
for (const attribute of attributes) {
|
|
297
|
+
this.attributes.set(attribute.id, attribute);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
presence() {
|
|
302
|
+
const presence = this.asString("presence").toLowerCase();
|
|
303
|
+
switch (presence) {
|
|
304
|
+
case "left":
|
|
305
|
+
return ElementDiffPresence.LEFT;
|
|
306
|
+
case "right":
|
|
307
|
+
return ElementDiffPresence.RIGHT;
|
|
308
|
+
case "both":
|
|
309
|
+
default:
|
|
310
|
+
return ElementDiffPresence.BOTH;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
asJson<T>(attr: string, def?: T): T | undefined {
|
|
315
|
+
try {
|
|
316
|
+
const attribute = this.attributes.get(attr.toLowerCase());
|
|
317
|
+
if (attribute) return JSON.parse(attribute.value) as T;
|
|
318
|
+
} catch {
|
|
319
|
+
// JSON parse error, ignore.
|
|
320
|
+
}
|
|
321
|
+
return def;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
asNumber(attr: string, def = 0): number {
|
|
325
|
+
const attribute = this.attributes.get(attr.toLowerCase());
|
|
326
|
+
if (attribute) return Number(attribute.value);
|
|
327
|
+
return def;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
asBoolean(attr: string, def = false): boolean {
|
|
331
|
+
const attribute = this.attributes.get(attr.toLowerCase());
|
|
332
|
+
if (attribute) {
|
|
333
|
+
const value = attribute.value.toLowerCase();
|
|
334
|
+
return value !== "0" && value !== "false";
|
|
335
|
+
}
|
|
336
|
+
return def;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
asString(attr: string, def = ""): string {
|
|
340
|
+
const attribute = this.attributes.get(attr.toLowerCase());
|
|
341
|
+
if (attribute) return String(attribute.value);
|
|
342
|
+
return def;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
hasAttribute(attr: string): boolean {
|
|
346
|
+
return this.attributes.has(attr.toLowerCase());
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
get nodeAttributes(): Partial<NodeConfig> {
|
|
350
|
+
const rank = this.asNumber("rank", 0);
|
|
351
|
+
const order = this.asNumber("order", -1);
|
|
352
|
+
const geometryParams = this.asJson<GeometryParams>("geometry");
|
|
353
|
+
const geometry = geometryParams ? parseGeometry(geometryParams) : new CircleGeometry();
|
|
354
|
+
const fixedRank = this.asBoolean("fixedRank", false);
|
|
355
|
+
return { rank, order, fixedRank, geometry };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get edgeAttributes(): Partial<EdgeConfig> {
|
|
359
|
+
const minLength = this.asNumber("minLength", 1);
|
|
360
|
+
const weight = this.asNumber("weight", 1);
|
|
361
|
+
const cutValue = this.asNumber("cutValue", 0);
|
|
362
|
+
return { minLength, weight, cutValue };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
get graphAttributes(): Partial<GraphConfig> {
|
|
366
|
+
const edgeSep = this.asNumber("edgeSep", 10);
|
|
367
|
+
const nodeSep = this.asNumber("nodeSep", 50);
|
|
368
|
+
const rankSep = this.asNumber("rankSep", 50);
|
|
369
|
+
const timing = this.asBoolean("timing", false);
|
|
370
|
+
const printDebug = this.asBoolean("printDebug", false);
|
|
371
|
+
const hullRotation = this.asNumber("hullRotation", 25);
|
|
372
|
+
const bendLongEdges = this.asBoolean("bendLongEdges", true);
|
|
373
|
+
const childrenRelative = this.asBoolean("childrenRelative", true);
|
|
374
|
+
return {
|
|
375
|
+
edgeSep,
|
|
376
|
+
nodeSep,
|
|
377
|
+
rankSep,
|
|
378
|
+
timing,
|
|
379
|
+
printDebug,
|
|
380
|
+
hullRotation,
|
|
381
|
+
bendLongEdges,
|
|
382
|
+
childrenRelative,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// tslint:disable-next-line:enforce-name-casing
|
|
387
|
+
static Empty() {
|
|
388
|
+
return new AttributeList([]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static from(this: { name: string }, tokens: Tokenizer): AttributeList {
|
|
392
|
+
const open = tokens.next();
|
|
393
|
+
if (open.type !== TokenType.OpenAttr) {
|
|
394
|
+
tokens.parseError(AttributeList.name, "[", open);
|
|
395
|
+
}
|
|
396
|
+
const attributes: Attribute[] = [];
|
|
397
|
+
while (tokens.hasNext) {
|
|
398
|
+
const token = tokens.peek();
|
|
399
|
+
if (token.type === TokenType.ID) {
|
|
400
|
+
const attribute = Attribute.from(tokens);
|
|
401
|
+
if (attribute) attributes.push(attribute);
|
|
402
|
+
} else if (token.type === TokenType.CloseAttr) {
|
|
403
|
+
tokens.next();
|
|
404
|
+
return new AttributeList(attributes);
|
|
405
|
+
} else {
|
|
406
|
+
tokens.parseError(AttributeList.name, "ID", token);
|
|
407
|
+
tokens.next();
|
|
408
|
+
return new AttributeList(attributes);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
tokens.parseError(AttributeList.name, "]", open);
|
|
412
|
+
return AttributeList.Empty();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fill(o: unknown) {
|
|
416
|
+
if (this.attributes.size === 0) return;
|
|
417
|
+
(o as { data?: AttributeList }).data = this;
|
|
418
|
+
if (this.asBoolean("expanded")) {
|
|
419
|
+
(o as { expanded?: boolean }).expanded = true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
class Attribute {
|
|
425
|
+
readonly id: string;
|
|
426
|
+
readonly value: string;
|
|
427
|
+
private constructor(
|
|
428
|
+
id: Token & { type: typeof TokenType.ID },
|
|
429
|
+
value: Token & { type: typeof TokenType.Value | typeof TokenType.ID },
|
|
430
|
+
) {
|
|
431
|
+
this.id = id.value.toLowerCase();
|
|
432
|
+
this.value = value.value;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
static from(this: { name: string }, tokens: Tokenizer): Attribute | undefined {
|
|
436
|
+
const id = tokens.next();
|
|
437
|
+
if (id.type !== TokenType.ID) {
|
|
438
|
+
tokens.parseError(Attribute.name, "id", id);
|
|
439
|
+
}
|
|
440
|
+
const eq = tokens.next();
|
|
441
|
+
if (eq.type !== TokenType.Assign) {
|
|
442
|
+
tokens.parseError(Attribute.name, "=", eq);
|
|
443
|
+
}
|
|
444
|
+
const value = tokens.next();
|
|
445
|
+
if (
|
|
446
|
+
value.type === TokenType.Semi ||
|
|
447
|
+
value.type === TokenType.CloseAttr ||
|
|
448
|
+
value.type === TokenType.CloseBlock
|
|
449
|
+
) {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
if (value.type !== TokenType.Value && value.type !== TokenType.ID) {
|
|
453
|
+
tokens.parseError(Attribute.name, "value or id", value);
|
|
454
|
+
tokens.next();
|
|
455
|
+
}
|
|
456
|
+
const close = tokens.peek();
|
|
457
|
+
if (close.type === TokenType.Semi) {
|
|
458
|
+
tokens.next();
|
|
459
|
+
} else if (close.type !== TokenType.CloseAttr) {
|
|
460
|
+
tokens.parseError(Attribute.name, "; or ]", close);
|
|
461
|
+
}
|
|
462
|
+
return new Attribute(
|
|
463
|
+
id as Token & { type: typeof TokenType.ID },
|
|
464
|
+
value as Token & { type: typeof TokenType.Value | typeof TokenType.ID },
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// tslint:disable:enforce-name-casing
|
|
470
|
+
const TokenType = {
|
|
471
|
+
End: "END",
|
|
472
|
+
Digraph: "DIGRAPH",
|
|
473
|
+
OpenBlock: "OPEN_BLOCK",
|
|
474
|
+
CloseBlock: "CLOSE_BLOCK",
|
|
475
|
+
OpenAttr: "OPEN_ATTR",
|
|
476
|
+
CloseAttr: "CLOSE_ATTR",
|
|
477
|
+
Edge: "EDGE",
|
|
478
|
+
IncompleteEdge: "INCOMPLETE_EDGE",
|
|
479
|
+
Assign: "ASSIGN",
|
|
480
|
+
ID: "ID",
|
|
481
|
+
Value: "VALUE",
|
|
482
|
+
Semi: "SEMI",
|
|
483
|
+
} as const;
|
|
484
|
+
type TokenType = (typeof TokenType)[keyof typeof TokenType];
|
|
485
|
+
// tslint:enable:enforce-name-casing
|
|
486
|
+
|
|
487
|
+
class Token {
|
|
488
|
+
readonly type: TokenType;
|
|
489
|
+
readonly value: string;
|
|
490
|
+
readonly line: number;
|
|
491
|
+
readonly column: number;
|
|
492
|
+
readonly length: number;
|
|
493
|
+
constructor(type: TokenType, value: string, line: number, column: number, length = value.length) {
|
|
494
|
+
this.type = type;
|
|
495
|
+
this.value = value;
|
|
496
|
+
this.line = line;
|
|
497
|
+
this.column = column;
|
|
498
|
+
this.length = length;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
interface Tokenizer {
|
|
503
|
+
readonly hasNext: boolean;
|
|
504
|
+
peek(): Token;
|
|
505
|
+
next(): Token;
|
|
506
|
+
readonly errors: Array<{}>;
|
|
507
|
+
parseError(node: string, expected: string, token: Token): void;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function tokenizer(text: string): Tokenizer {
|
|
511
|
+
const errors: Array<{}> = [];
|
|
512
|
+
let pos = 0;
|
|
513
|
+
let line = 1;
|
|
514
|
+
let column = 1;
|
|
515
|
+
|
|
516
|
+
function advance(n: number) {
|
|
517
|
+
column += n;
|
|
518
|
+
pos += n;
|
|
519
|
+
whitespace();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function whitespace() {
|
|
523
|
+
while (pos < text.length) {
|
|
524
|
+
if (text[pos] === "#") {
|
|
525
|
+
while (text[pos] !== "\n" && pos < text.length) {
|
|
526
|
+
increment();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (text[pos].match(/\s/)) {
|
|
530
|
+
increment();
|
|
531
|
+
} else {
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function increment() {
|
|
538
|
+
if (text[pos] === "\n") {
|
|
539
|
+
line++;
|
|
540
|
+
column = 1;
|
|
541
|
+
} else {
|
|
542
|
+
column++;
|
|
543
|
+
}
|
|
544
|
+
pos++;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function token(type: TokenType, value: string, length = value.length) {
|
|
548
|
+
return new Token(type, value, line, column, length);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
whitespace();
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
get errors() {
|
|
555
|
+
return errors;
|
|
556
|
+
},
|
|
557
|
+
get hasNext() {
|
|
558
|
+
return pos < text.length;
|
|
559
|
+
},
|
|
560
|
+
peek() {
|
|
561
|
+
if (pos >= text.length) {
|
|
562
|
+
return token(TokenType.End, "");
|
|
563
|
+
}
|
|
564
|
+
const c = text.charAt(pos);
|
|
565
|
+
switch (c) {
|
|
566
|
+
case "{":
|
|
567
|
+
return token(TokenType.OpenBlock, c);
|
|
568
|
+
case "}":
|
|
569
|
+
return token(TokenType.CloseBlock, c);
|
|
570
|
+
case "[":
|
|
571
|
+
return token(TokenType.OpenAttr, c);
|
|
572
|
+
case "]":
|
|
573
|
+
return token(TokenType.CloseAttr, c);
|
|
574
|
+
case "=":
|
|
575
|
+
return token(TokenType.Assign, c);
|
|
576
|
+
case ";":
|
|
577
|
+
return token(TokenType.Semi, c);
|
|
578
|
+
case "-":
|
|
579
|
+
if (text.charAt(pos + 1) === ">") {
|
|
580
|
+
return token(TokenType.Edge, "->");
|
|
581
|
+
} else {
|
|
582
|
+
// A negative number, for a value ID.
|
|
583
|
+
return findEndId();
|
|
584
|
+
}
|
|
585
|
+
case '"':
|
|
586
|
+
return findEndQuote('"');
|
|
587
|
+
case "'":
|
|
588
|
+
return findEndQuote("'");
|
|
589
|
+
case "d":
|
|
590
|
+
if (text.substr(pos, pos + 7) === "digraph") {
|
|
591
|
+
return token(TokenType.Digraph, "digraph");
|
|
592
|
+
}
|
|
593
|
+
return findEndId();
|
|
594
|
+
default:
|
|
595
|
+
return findEndId();
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
next() {
|
|
599
|
+
const next = this.hasNext ? this.peek() : token(TokenType.End, "");
|
|
600
|
+
advance(next.length);
|
|
601
|
+
return next;
|
|
602
|
+
},
|
|
603
|
+
parseError(node: string, expected: string, got: Token) {
|
|
604
|
+
errors.push({ node, expected, got });
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
function findEndQuote(quoteType: '"' | "'"): Token {
|
|
609
|
+
let offset = 1;
|
|
610
|
+
while (offset + pos < text.length) {
|
|
611
|
+
// Find the closing quote, unless it is escaped
|
|
612
|
+
if (text[offset + pos] === quoteType && text[offset + pos - 1] !== "\\") {
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
offset++;
|
|
616
|
+
}
|
|
617
|
+
const value = text
|
|
618
|
+
.slice(pos + 1, pos + offset)
|
|
619
|
+
.replace(new RegExp("\\\\" + quoteType, "g"), quoteType);
|
|
620
|
+
return token(TokenType.Value, value, offset + 1);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function findEndId() {
|
|
624
|
+
let offset = 1;
|
|
625
|
+
while (offset + pos < text.length) {
|
|
626
|
+
// Space and all the special characters above.
|
|
627
|
+
if (text.substr(offset + pos, 2) === "->") break;
|
|
628
|
+
if (/[\s=;"[\]{}]/.test(text.charAt(offset + pos))) break;
|
|
629
|
+
offset++;
|
|
630
|
+
}
|
|
631
|
+
const value = text.slice(pos, pos + offset);
|
|
632
|
+
return token(TokenType.ID, value);
|
|
633
|
+
}
|
|
634
|
+
}
|