@rse/gradia 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dst/gradia-api-config.d.ts +25 -0
- package/dst/gradia-api-config.js +63 -0
- package/dst/gradia-api-model.d.ts +19 -0
- package/dst/gradia-api-model.js +6 -0
- package/dst/gradia-api-parser.d.ts +2 -0
- package/dst/gradia-api-parser.js +164 -0
- package/dst/gradia-api-render-base.d.ts +20 -0
- package/dst/gradia-api-render-base.js +25 -0
- package/dst/gradia-api-render-edge.d.ts +25 -0
- package/dst/gradia-api-render-edge.js +211 -0
- package/dst/gradia-api-render-group.d.ts +9 -0
- package/dst/gradia-api-render-group.js +103 -0
- package/dst/gradia-api-render-node.d.ts +18 -0
- package/dst/gradia-api-render-node.js +74 -0
- package/dst/gradia-api-render-svg.d.ts +23 -0
- package/dst/gradia-api-render-svg.js +199 -0
- package/dst/gradia-api-type-graph.d.ts +4 -0
- package/dst/gradia-api-type-graph.js +329 -0
- package/dst/gradia-api-type-grid.d.ts +4 -0
- package/dst/gradia-api-type-grid.js +52 -0
- package/dst/gradia-api-type-hub.d.ts +4 -0
- package/dst/gradia-api-type-hub.js +180 -0
- package/dst/gradia-api.d.ts +26 -0
- package/dst/gradia-api.js +71 -0
- package/dst/gradia-cli.d.ts +7 -0
- package/dst/gradia-cli.js +52 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
|
|
2
|
+
Gradia
|
|
3
|
+
======
|
|
4
|
+
|
|
5
|
+
**Object Graph Diagram Rendering**
|
|
6
|
+
|
|
7
|
+
<p/>
|
|
8
|
+
<img src="https://nodei.co/npm/@rse/gradia.png?downloads=true&stars=true" alt=""/>
|
|
9
|
+
|
|
10
|
+
<p/>
|
|
11
|
+
[](https://github.com/rse)
|
|
12
|
+
[](https://github.com/rse)
|
|
13
|
+
|
|
14
|
+
About
|
|
15
|
+
-----
|
|
16
|
+
|
|
17
|
+
**Gradia** is a small API and CLI for rendering directed graphs,
|
|
18
|
+
described in a concise textual input language, as SVG diagrams. Three
|
|
19
|
+
diagram types are supported which fit the scenario of visualizing object
|
|
20
|
+
graphs:
|
|
21
|
+
|
|
22
|
+
- `graph`: a loosly grid-snapped layered layout of the whole graph,
|
|
23
|
+
based on the Dagre algorithm of [@antv/layout](https://github.com/antvis/layout).
|
|
24
|
+
- `hub`: a hub graph, placing one primary hub node between its input and
|
|
25
|
+
output nodes.
|
|
26
|
+
- `grid`: a compact grid of tiles for an edge-less graph.
|
|
27
|
+
|
|
28
|
+
Installation
|
|
29
|
+
------------
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
$ npm install @rse/gradia
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Usage
|
|
36
|
+
-----
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
$ gradia [-t graph|hub|grid] [-f <format>] [-c <name>=<value>] -o <file>.svg <graph>.txt
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The output format is `svg:standalone` (a standalone SVG/XML document,
|
|
43
|
+
the default), `svg:embedded` (the SVG without the `<?xml?>` declaration,
|
|
44
|
+
for direct embedding into HTML), `url:xml` (a `data:image/svg+xml` URL
|
|
45
|
+
with URL-encoded XML), or `url:base64` (a `data:image/svg+xml` URL with
|
|
46
|
+
Base64-encoded XML).
|
|
47
|
+
|
|
48
|
+
Input Syntax
|
|
49
|
+
------------
|
|
50
|
+
|
|
51
|
+
The input is a plain-text graph description: one node/edge chain per
|
|
52
|
+
line, plus optional comments and rendering directives.
|
|
53
|
+
|
|
54
|
+
### Grammar
|
|
55
|
+
|
|
56
|
+
The following BNF grammar describes the input syntax. Non-terminals are
|
|
57
|
+
written as `<name>`, terminals as `"..."`, optional parts as `[ ... ]`,
|
|
58
|
+
repeatable parts as `{ ... }` (zero or more times), and alternatives are
|
|
59
|
+
separated by `|`.
|
|
60
|
+
|
|
61
|
+
```bnf
|
|
62
|
+
<graph> ::= { <statement> | <newline> }
|
|
63
|
+
<statement> ::= <node> { <edge> <node> } ( <newline> | <eof> )
|
|
64
|
+
<node> ::= <atom> [ ":" <atom> ] [ <attributes> ]
|
|
65
|
+
<attributes> ::= "[" <attribute> { "," <attribute> } "]"
|
|
66
|
+
<attribute> ::= <atom> ":" <atom>
|
|
67
|
+
<edge> ::= "--" [ "(" <edge-name> ")" "--" ] ">" [ "[" <edge-arity> "]" ]
|
|
68
|
+
<edge-name> ::= <string> | <name-word>
|
|
69
|
+
<edge-arity> ::= <string> | <arity-word>
|
|
70
|
+
<atom> ::= <bareword> | <string>
|
|
71
|
+
<bareword> ::= <bareword-char> { <bareword-char> }
|
|
72
|
+
<bareword-char> ::= <char> - ( <whitespace> | "[" | "]" | "(" | ")" | "," | ":" | "\"" | ">" | "#" )
|
|
73
|
+
<name-word> ::= <name-char> { <name-char> }
|
|
74
|
+
<name-char> ::= <char> - ( <whitespace> | "(" | ")" | "\"" )
|
|
75
|
+
<arity-word> ::= <arity-char> { <arity-char> }
|
|
76
|
+
<arity-char> ::= <char> - ( <whitespace> | "[" | "]" | "\"" )
|
|
77
|
+
<string> ::= "\"" { <string-char> } "\""
|
|
78
|
+
<string-char> ::= ( <char> - ( "\"" | "\\" | <newline> ) ) | ( "\\" ( <char> - <newline> ) )
|
|
79
|
+
<comment> ::= "#" { <char> - <newline> }
|
|
80
|
+
<directive> ::= "#config" <whitespace> { <whitespace> } <option> <whitespace> { <whitespace> } ( <string> | <word> )
|
|
81
|
+
<option> ::= <letter> { <letter> | <digit> | "-" }
|
|
82
|
+
<word> ::= <char> - <whitespace> { <char> - <whitespace> }
|
|
83
|
+
<newline> ::= "\n"
|
|
84
|
+
<whitespace> ::= " " | "\t" | "\r"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Lexical Rules
|
|
88
|
+
|
|
89
|
+
- A `<bareword>` must not contain the character sequence `--` at any
|
|
90
|
+
position, as `--` always starts an edge operator.
|
|
91
|
+
|
|
92
|
+
- An `<edge>` is a single lexical token: no whitespace and no comment is
|
|
93
|
+
allowed anywhere inside it, especially not between `>` and `[`.
|
|
94
|
+
|
|
95
|
+
- Whitespace (except `<newline>`) is insignificant between all other
|
|
96
|
+
tokens and never part of a token.
|
|
97
|
+
|
|
98
|
+
- A `<newline>` terminates a `<statement>`: statements cannot span
|
|
99
|
+
multiple lines.
|
|
100
|
+
|
|
101
|
+
- A `<comment>` starts at an unquoted `#` and extends to the end of the
|
|
102
|
+
line. Inside a `<string>`, `#` is an ordinary character.
|
|
103
|
+
|
|
104
|
+
- A `<string>` cannot span lines. Inside it, `\` escapes the following
|
|
105
|
+
character (especially `\"` and `\\`).
|
|
106
|
+
|
|
107
|
+
- A `<directive>` is a `<comment>` which occupies its entire line,
|
|
108
|
+
starts with the keyword `#config`, and whose `<option>` is one of the
|
|
109
|
+
recognized rendering options (see below). All other comments are
|
|
110
|
+
ignored.
|
|
111
|
+
|
|
112
|
+
### Semantic Rules
|
|
113
|
+
|
|
114
|
+
- In `<node>`, the first `<atom>` is the node id and the optional
|
|
115
|
+
`<atom>` after `:` is the displayed node label. Without a label, the
|
|
116
|
+
id is displayed.
|
|
117
|
+
|
|
118
|
+
- A node can be referenced multiple times. All references address the
|
|
119
|
+
same node and merge their label and attributes into it, with later
|
|
120
|
+
values overriding earlier ones for the same attribute key.
|
|
121
|
+
|
|
122
|
+
- Nodes which are never part of an `<edge>` are rendered free-standing.
|
|
123
|
+
|
|
124
|
+
- In a chain `a --> b --> c`, each `<edge>` connects its immediately
|
|
125
|
+
preceding and following node, i.e., the chain is equivalent to the
|
|
126
|
+
two statements `a --> b` and `b --> c`.
|
|
127
|
+
|
|
128
|
+
- The `<edge-name>` is rendered as the label of the edge and the
|
|
129
|
+
`<edge-arity>` as its cardinality label.
|
|
130
|
+
|
|
131
|
+
- Four attribute keys are reserved and consumed by the renderer instead
|
|
132
|
+
of being displayed as a regular `<key>: <val>` line:
|
|
133
|
+
|
|
134
|
+
- `url: <url>` renders the node box as a hyperlink,
|
|
135
|
+
- `type: <name>` renders `<name>` as a smaller text above the node
|
|
136
|
+
label inside the node box,
|
|
137
|
+
- `primary: true` renders the node in the primary node colors
|
|
138
|
+
(and, for diagram type `hub`, marks the single central hub node),
|
|
139
|
+
- `group: <name>` places the node into the surrounding group box
|
|
140
|
+
`<name>`.
|
|
141
|
+
|
|
142
|
+
- A node cannot be a member of more than one group. As soon as at least
|
|
143
|
+
one node carries a `group` attribute, all nodes without one implicitly
|
|
144
|
+
belong to the group `default`, and every edge has to stay within a
|
|
145
|
+
single group.
|
|
146
|
+
|
|
147
|
+
- All remaining attributes are displayed as `<key>: <val>` lines inside
|
|
148
|
+
the node box.
|
|
149
|
+
|
|
150
|
+
### Directives
|
|
151
|
+
|
|
152
|
+
A `<directive>` sets one of the rendering options, which otherwise are
|
|
153
|
+
settable through the corresponding `--config <option>=<value>`
|
|
154
|
+
command-line options. The recognized options are:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
font-family color-node-ghost-name color-group-name
|
|
158
|
+
color-node-ghost-box color-group-box
|
|
159
|
+
color-node-regular-name color-node-ghost-border color-group-border
|
|
160
|
+
color-node-regular-box
|
|
161
|
+
color-node-regular-border color-node-primary-name color-edge-line
|
|
162
|
+
color-node-primary-box color-edge-name
|
|
163
|
+
color-node-primary-border color-edge-arity
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The option `font-embed`, and a `font-family` value pointing to a WOFF2
|
|
167
|
+
file, are intentionally rejected in directives, as the input is treated
|
|
168
|
+
as untrusted. Both are available on the command-line only.
|
|
169
|
+
|
|
170
|
+
### Example
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
#config color-node-primary-box #336699
|
|
174
|
+
#config color-edge-line #999999
|
|
175
|
+
|
|
176
|
+
Animal: animal [ type: "abstract class", kind: base, url: "#animal", primary: true ]
|
|
177
|
+
Dog: dog
|
|
178
|
+
Cat: cat
|
|
179
|
+
|
|
180
|
+
Dog --(isa)--> Animal
|
|
181
|
+
Cat --(isa)--> Animal
|
|
182
|
+
"Pet Owner" --(owns)-->[0..*] Dog --(chases)-->[0..n] Cat
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
License
|
|
186
|
+
-------
|
|
187
|
+
|
|
188
|
+
Copyright © 2026 Dr. Ralf S. Engelschall (http://engelschall.com/)<br/>
|
|
189
|
+
Distributed under MIT license (https://spdx.org/licenses/MIT.html)
|
|
190
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const configDefaults: {
|
|
2
|
+
"font-family": string;
|
|
3
|
+
"font-embed": boolean;
|
|
4
|
+
"color-node-regular-name": string;
|
|
5
|
+
"color-node-regular-box": string;
|
|
6
|
+
"color-node-regular-border": string;
|
|
7
|
+
"color-node-primary-name": string;
|
|
8
|
+
"color-node-primary-box": string;
|
|
9
|
+
"color-node-primary-border": string;
|
|
10
|
+
"color-node-ghost-name": string;
|
|
11
|
+
"color-node-ghost-box": string;
|
|
12
|
+
"color-node-ghost-border": string;
|
|
13
|
+
"color-group-name": string;
|
|
14
|
+
"color-group-box": string;
|
|
15
|
+
"color-group-border": string;
|
|
16
|
+
"color-edge-line": string;
|
|
17
|
+
"color-edge-name": string;
|
|
18
|
+
"color-edge-arity": string;
|
|
19
|
+
};
|
|
20
|
+
export type Config = typeof configDefaults;
|
|
21
|
+
export declare const parseDirectives: (input: string) => Partial<Config>;
|
|
22
|
+
export declare const resolveFont: (config: Config) => {
|
|
23
|
+
family: string;
|
|
24
|
+
embed?: string;
|
|
25
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Gradia -- Object Graph Diagram Rendering
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Distributed under MIT license <https://spdx.org/licenses/MIT.html>
|
|
5
|
+
*/
|
|
6
|
+
/* built-in dependencies */
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
/* the rendering configuration and its default values */
|
|
10
|
+
export const configDefaults = {
|
|
11
|
+
"font-family": "Helvetica",
|
|
12
|
+
"font-embed": false,
|
|
13
|
+
"color-node-regular-name": "#336699",
|
|
14
|
+
"color-node-regular-box": "#e0f0ff",
|
|
15
|
+
"color-node-regular-border": "#c0d0e0",
|
|
16
|
+
"color-node-primary-name": "#ffffff",
|
|
17
|
+
"color-node-primary-box": "#336699",
|
|
18
|
+
"color-node-primary-border": "#003366",
|
|
19
|
+
"color-node-ghost-name": "#666666",
|
|
20
|
+
"color-node-ghost-box": "#f0f0f0",
|
|
21
|
+
"color-node-ghost-border": "#a0a0a0",
|
|
22
|
+
"color-group-name": "#6699cc",
|
|
23
|
+
"color-group-box": "#f4f8fc",
|
|
24
|
+
"color-group-border": "#c0d0e0",
|
|
25
|
+
"color-edge-line": "#999999",
|
|
26
|
+
"color-edge-name": "#333333",
|
|
27
|
+
"color-edge-arity": "#333333"
|
|
28
|
+
};
|
|
29
|
+
/* parse "#config <option> <value>" configuration directives from a graph
|
|
30
|
+
description (lines which are otherwise treated as plain comments) */
|
|
31
|
+
export const parseDirectives = (input) => {
|
|
32
|
+
const partial = {};
|
|
33
|
+
for (const line of input.split(/\r?\n/)) {
|
|
34
|
+
const m = /^[ \t]*#config[ \t]+([a-z][a-z0-9-]*)[ \t]+(?:"((?:[^"\\]|\\.)*)"|(\S+))[ \t]*$/.exec(line);
|
|
35
|
+
if (m === null || !Object.hasOwn(configDefaults, m[1]))
|
|
36
|
+
continue;
|
|
37
|
+
const key = m[1];
|
|
38
|
+
const val = m[2] !== undefined ? m[2].replace(/\\(.)/g, "$1") : m[3];
|
|
39
|
+
/* reject font file paths and font embedding from the untrusted graph
|
|
40
|
+
description, as they would let it read arbitrary local WOFF2 files */
|
|
41
|
+
if (key === "font-embed" || (key === "font-family" && val.endsWith(".woff2")))
|
|
42
|
+
continue;
|
|
43
|
+
partial[key] = typeof configDefaults[key] === "boolean" ?
|
|
44
|
+
val === "true" : val;
|
|
45
|
+
}
|
|
46
|
+
return partial;
|
|
47
|
+
};
|
|
48
|
+
/* resolve the configured font family: either a plain family name or a
|
|
49
|
+
path to a WOFF2 file (its content optionally embedded as base64) */
|
|
50
|
+
export const resolveFont = (config) => {
|
|
51
|
+
const spec = config["font-family"];
|
|
52
|
+
if (spec.endsWith(".woff2")) {
|
|
53
|
+
const family = path.basename(spec, ".woff2");
|
|
54
|
+
if (config["font-embed"])
|
|
55
|
+
return { family, embed: fs.readFileSync(spec).toString("base64") };
|
|
56
|
+
else
|
|
57
|
+
return { family };
|
|
58
|
+
}
|
|
59
|
+
else if (config["font-embed"])
|
|
60
|
+
throw new Error("option \"font-embed\" requires option \"font-family\" to be the path to a WOFF2 file");
|
|
61
|
+
else
|
|
62
|
+
return { family: spec };
|
|
63
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Attr {
|
|
2
|
+
key: string;
|
|
3
|
+
val: string;
|
|
4
|
+
}
|
|
5
|
+
export interface Node {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
attrs: Attr[];
|
|
9
|
+
}
|
|
10
|
+
export interface Edge {
|
|
11
|
+
source: string;
|
|
12
|
+
target: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
arity?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Graph {
|
|
17
|
+
nodes: Map<string, Node>;
|
|
18
|
+
edges: Edge[];
|
|
19
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Gradia -- Object Graph Diagram Rendering
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Distributed under MIT license <https://spdx.org/licenses/MIT.html>
|
|
5
|
+
*/
|
|
6
|
+
/* tokenize the graph description language */
|
|
7
|
+
const tokenize = (input) => {
|
|
8
|
+
const tokens = [];
|
|
9
|
+
let line = 1;
|
|
10
|
+
let col = 1;
|
|
11
|
+
let i = 0;
|
|
12
|
+
/* reusable regular expressions */
|
|
13
|
+
const reEdge = /--(?:\((?:"((?:[^"\\\n]|\\.)*)"|([^()"\s]+))\)--)?>(?:\[(?:"((?:[^"\\\n]|\\.)*)"|([^[\]"\s]+))\])?/y;
|
|
14
|
+
const reString = /"((?:[^"\\\n]|\\.)*)"/y;
|
|
15
|
+
const reWord = /(?:(?!--)[^\s[\](),:">#])+/y;
|
|
16
|
+
/* helper functions */
|
|
17
|
+
const match = (re) => {
|
|
18
|
+
re.lastIndex = i;
|
|
19
|
+
return re.exec(input);
|
|
20
|
+
};
|
|
21
|
+
const unquote = (s) => s.replace(/\\(.)/g, "$1");
|
|
22
|
+
/* iterate over input */
|
|
23
|
+
while (i < input.length) {
|
|
24
|
+
const ch = input[i];
|
|
25
|
+
let m;
|
|
26
|
+
/* whitespace, line breaks, and comments */
|
|
27
|
+
if (ch === "\n") {
|
|
28
|
+
tokens.push({ type: "newline", text: "\n", line, col });
|
|
29
|
+
line++;
|
|
30
|
+
col = 1;
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
else if (ch === " " || ch === "\t" || ch === "\r") {
|
|
34
|
+
col++;
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
else if (ch === "#") {
|
|
38
|
+
while (i < input.length && input[i] !== "\n") {
|
|
39
|
+
col++;
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/* edge operators, quoted strings, punctuation, and barewords */
|
|
44
|
+
else if ((m = match(reEdge)) !== null) {
|
|
45
|
+
const name = m[1] !== undefined ? unquote(m[1]) : m[2];
|
|
46
|
+
const arity = m[3] !== undefined ? unquote(m[3]) : m[4];
|
|
47
|
+
tokens.push({ type: "edge", text: m[0], name, arity, line, col });
|
|
48
|
+
col += m[0].length;
|
|
49
|
+
i += m[0].length;
|
|
50
|
+
}
|
|
51
|
+
else if ((m = match(reString)) !== null) {
|
|
52
|
+
tokens.push({ type: "string", text: unquote(m[1]), line, col });
|
|
53
|
+
col += m[0].length;
|
|
54
|
+
i += m[0].length;
|
|
55
|
+
}
|
|
56
|
+
else if (ch === "\"")
|
|
57
|
+
throw new Error(`unterminated string at line ${line}, column ${col}`);
|
|
58
|
+
else if ("[](),:".includes(ch)) {
|
|
59
|
+
tokens.push({ type: "punct", text: ch, line, col });
|
|
60
|
+
col++;
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
else if ((m = match(reWord)) !== null) {
|
|
64
|
+
tokens.push({ type: "word", text: m[0], line, col });
|
|
65
|
+
col += m[0].length;
|
|
66
|
+
i += m[0].length;
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
throw new Error(`invalid character ${JSON.stringify(ch)} at line ${line}, column ${col}`);
|
|
70
|
+
}
|
|
71
|
+
return tokens;
|
|
72
|
+
};
|
|
73
|
+
/* parse the token stream into the graph model */
|
|
74
|
+
export const parse = (input) => {
|
|
75
|
+
const tokens = tokenize(input);
|
|
76
|
+
const graph = { nodes: new Map(), edges: [] };
|
|
77
|
+
let pos = 0;
|
|
78
|
+
/* helper functions */
|
|
79
|
+
const peek = () => pos < tokens.length ? tokens[pos] : null;
|
|
80
|
+
const accept = (text) => {
|
|
81
|
+
const t = peek();
|
|
82
|
+
if (t === null || t.type !== "punct" || t.text !== text)
|
|
83
|
+
return false;
|
|
84
|
+
pos++;
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
const bail = (what) => {
|
|
88
|
+
const t = peek();
|
|
89
|
+
const loc = t !== null ? `line ${t.line}, column ${t.col}` : "end of input";
|
|
90
|
+
throw new Error(`expected ${what} at ${loc}`);
|
|
91
|
+
};
|
|
92
|
+
/* parse a single atom (bareword or quoted string) */
|
|
93
|
+
const parseAtom = () => {
|
|
94
|
+
const t = peek();
|
|
95
|
+
if (t === null || (t.type !== "word" && t.type !== "string"))
|
|
96
|
+
return bail("bareword or quoted string");
|
|
97
|
+
pos++;
|
|
98
|
+
return t.text;
|
|
99
|
+
};
|
|
100
|
+
/* parse a node reference with optional label and attributes,
|
|
101
|
+
merging the information into the already known node (if any) */
|
|
102
|
+
const parseNodeRef = () => {
|
|
103
|
+
const start = tokens[pos];
|
|
104
|
+
const id = parseAtom();
|
|
105
|
+
/* parse the optional node label */
|
|
106
|
+
let name = null;
|
|
107
|
+
if (accept(":"))
|
|
108
|
+
name = parseAtom();
|
|
109
|
+
/* parse the optional node attributes */
|
|
110
|
+
const attrs = [];
|
|
111
|
+
if (accept("[")) {
|
|
112
|
+
while (true) {
|
|
113
|
+
const key = parseAtom();
|
|
114
|
+
if (!accept(":"))
|
|
115
|
+
bail("\":\" in key/value attribute");
|
|
116
|
+
const val = parseAtom();
|
|
117
|
+
attrs.push({ key, val });
|
|
118
|
+
if (!accept(","))
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (!accept("]"))
|
|
122
|
+
bail("\"]\" after key/value attributes");
|
|
123
|
+
}
|
|
124
|
+
/* merge the parsed information into the graph node */
|
|
125
|
+
const node = graph.nodes.get(id) ?? { id, name: id, attrs: [] };
|
|
126
|
+
if (name !== null)
|
|
127
|
+
node.name = name;
|
|
128
|
+
for (const attr of attrs) {
|
|
129
|
+
const k = node.attrs.findIndex((a) => a.key === attr.key);
|
|
130
|
+
if (k === -1)
|
|
131
|
+
node.attrs.push(attr);
|
|
132
|
+
else {
|
|
133
|
+
/* a node has to stay in a single group, so reject any
|
|
134
|
+
attempt to re-assign it to a different group */
|
|
135
|
+
if (attr.key === "group" && node.attrs[k].val !== attr.val)
|
|
136
|
+
throw new Error(`node "${id}" cannot be member of more than one group ` +
|
|
137
|
+
`("${node.attrs[k].val}" vs "${attr.val}") ` +
|
|
138
|
+
`at line ${start.line}, column ${start.col}`);
|
|
139
|
+
node.attrs[k] = attr;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
graph.nodes.set(id, node);
|
|
143
|
+
return id;
|
|
144
|
+
};
|
|
145
|
+
/* parse the statements (one node/edge chain per line) */
|
|
146
|
+
while (pos < tokens.length) {
|
|
147
|
+
if (tokens[pos].type === "newline") {
|
|
148
|
+
pos++;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
let source = parseNodeRef();
|
|
152
|
+
let t = peek();
|
|
153
|
+
while (t !== null && t.type === "edge") {
|
|
154
|
+
pos++;
|
|
155
|
+
const target = parseNodeRef();
|
|
156
|
+
graph.edges.push({ source, target, name: t.name, arity: t.arity });
|
|
157
|
+
source = target;
|
|
158
|
+
t = peek();
|
|
159
|
+
}
|
|
160
|
+
if (t !== null && t.type !== "newline")
|
|
161
|
+
bail("edge operator or end of line");
|
|
162
|
+
}
|
|
163
|
+
return graph;
|
|
164
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const FS_NAME = 30;
|
|
2
|
+
export declare const FS_TYPE = 16;
|
|
3
|
+
export declare const FS_ATTR = 22;
|
|
4
|
+
export declare const FS_EDGE = 16;
|
|
5
|
+
export declare const FS_ARITY = 16;
|
|
6
|
+
export declare const FS_GROUP = 24;
|
|
7
|
+
export declare const MARGIN = 40;
|
|
8
|
+
export declare const SLOT = 12;
|
|
9
|
+
export declare const RADIUS = 20;
|
|
10
|
+
export declare const HOP = 8;
|
|
11
|
+
export declare const SCALE_H = 2.25;
|
|
12
|
+
export declare const textWidth: (text: string, size: number) => number;
|
|
13
|
+
export declare const escapeXML: (text: string) => string;
|
|
14
|
+
export type Poly = [number, number][];
|
|
15
|
+
export interface NodeStyle {
|
|
16
|
+
fill: string;
|
|
17
|
+
stroke: string;
|
|
18
|
+
text: string;
|
|
19
|
+
dash?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Gradia -- Object Graph Diagram Rendering
|
|
3
|
+
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Distributed under MIT license <https://spdx.org/licenses/MIT.html>
|
|
5
|
+
*/
|
|
6
|
+
/* rendering geometry constants */
|
|
7
|
+
export const FS_NAME = 30; /* font size of node names */
|
|
8
|
+
export const FS_TYPE = 16; /* font size of node types */
|
|
9
|
+
export const FS_ATTR = 22; /* font size of node attributes */
|
|
10
|
+
export const FS_EDGE = 16; /* font size of edge labels */
|
|
11
|
+
export const FS_ARITY = 16; /* font size of edge arities */
|
|
12
|
+
export const FS_GROUP = 24; /* font size of group tags */
|
|
13
|
+
export const MARGIN = 40; /* outer margin of the canvas */
|
|
14
|
+
export const SLOT = 12; /* offset between parallel routes */
|
|
15
|
+
export const RADIUS = 20; /* corner rounding radius of edges */
|
|
16
|
+
export const HOP = 8; /* radius of edge crossing hops */
|
|
17
|
+
export const SCALE_H = 2.25; /* height scale of node boxes */
|
|
18
|
+
/* estimate rendered text width (no canvas available under Node,
|
|
19
|
+
factor tuned for the average glyph advance of Source Sans 3) */
|
|
20
|
+
export const textWidth = (text, size) => text.length * size * 0.52;
|
|
21
|
+
/* escape a string for use in XML/SVG content */
|
|
22
|
+
export const escapeXML = (text) => text.replace(/&/g, "&")
|
|
23
|
+
.replace(/</g, "<")
|
|
24
|
+
.replace(/>/g, ">")
|
|
25
|
+
.replace(/"/g, """);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Edge } from "./gradia-api-model.js";
|
|
2
|
+
import { Poly } from "./gradia-api-render-base.js";
|
|
3
|
+
export declare const simplifyPoly: (pts: Poly) => Poly;
|
|
4
|
+
export type Side = "e" | "w";
|
|
5
|
+
export declare const assignPorts: (edges: Edge[], sides: {
|
|
6
|
+
s: Side;
|
|
7
|
+
t: Side;
|
|
8
|
+
}[], cx: (id: string) => number, cy: (id: string) => number, boxW: Map<string, number>, boxH: Map<string, number>) => Map<string, {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
}>;
|
|
12
|
+
export interface TrackUser {
|
|
13
|
+
edge: number;
|
|
14
|
+
posIn: number;
|
|
15
|
+
posOut: number;
|
|
16
|
+
mirror: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare const assignTracks: (users: TrackUser[], width: number, pad: number) => Map<number, number>;
|
|
19
|
+
export declare const computeHops: (polys: Poly[]) => Map<number, number[]>[];
|
|
20
|
+
export declare const pathOf: (poly: Poly, hop: Map<number, number[]>) => string;
|
|
21
|
+
export declare const pointAt: (poly: Poly, fraction: number) => {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
horizontal: boolean;
|
|
25
|
+
};
|