@rse/gradia 1.1.9 → 1.2.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 +78 -36
- package/dst/gradia-api-config.d.ts +7 -0
- package/dst/gradia-api-config.js +54 -24
- package/dst/gradia-api-parser.js +10 -4
- package/dst/gradia-api-render-base.d.ts +28 -0
- package/dst/gradia-api-render-base.js +3 -2
- package/dst/gradia-api-render-container.d.ts +31 -0
- package/dst/gradia-api-render-container.js +300 -0
- package/dst/gradia-api-render-edge.d.ts +1 -1
- package/dst/gradia-api-render-edge.js +106 -37
- package/dst/gradia-api-render-group.d.ts +3 -2
- package/dst/gradia-api-render-group.js +16 -27
- package/dst/gradia-api-render-node.d.ts +9 -1
- package/dst/gradia-api-render-node.js +28 -14
- package/dst/gradia-api-render-svg.d.ts +1 -21
- package/dst/gradia-api-render-svg.js +82 -20
- package/dst/gradia-api-type-graph.d.ts +3 -2
- package/dst/gradia-api-type-graph.js +233 -122
- package/dst/gradia-api-type-grid.d.ts +3 -2
- package/dst/gradia-api-type-grid.js +5 -3
- package/dst/gradia-api-type-hub.d.ts +3 -2
- package/dst/gradia-api-type-hub.js +20 -10
- package/dst/gradia-api.d.ts +12 -4
- package/dst/gradia-api.js +33 -13
- package/dst/gradia-api.umd.js +2 -2
- package/dst/gradia-cli.js +4 -21
- package/dst/gradia-mcp.js +47 -53
- package/package.json +1 -1
package/dst/gradia-cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
/* external dependencies */
|
|
12
12
|
import { Command, Option } from "commander";
|
|
13
13
|
/* internal dependencies */
|
|
14
|
-
import {
|
|
14
|
+
import { validateConfig } from "./gradia-api-config.js";
|
|
15
15
|
import { Gradia, diagramTypes, diagramTypeDefault, diagramFormats, diagramFormatDefault } from "./gradia-api.js";
|
|
16
16
|
/* internal package meta-information */
|
|
17
17
|
const pkg = JSON.parse(fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
@@ -30,31 +30,14 @@ program.name("gradia")
|
|
|
30
30
|
.argument("[input]", "input graph description file (omitted in MCP service mode)")
|
|
31
31
|
.action(async (input, options) => {
|
|
32
32
|
/* parse and validate the rendering configuration options */
|
|
33
|
-
const
|
|
34
|
-
const store = config;
|
|
33
|
+
const store = {};
|
|
35
34
|
for (const nv of options.config) {
|
|
36
35
|
const m = /^([a-z][a-z0-9-]*)=(.*)$/.exec(nv);
|
|
37
36
|
if (m === null)
|
|
38
37
|
throw new Error(`invalid configuration option "${nv}" (expected "<name>=<value>")`);
|
|
39
|
-
|
|
40
|
-
throw new Error(`unknown configuration option "${m[1]}"`);
|
|
41
|
-
const key = m[1];
|
|
42
|
-
if (typeof configDefaults[key] === "boolean") {
|
|
43
|
-
if (m[2] !== "true" && m[2] !== "false")
|
|
44
|
-
throw new Error(`invalid value "${m[2]}" for boolean configuration option "${key}" (expected "true" or "false")`);
|
|
45
|
-
store[key] = m[2] === "true";
|
|
46
|
-
}
|
|
47
|
-
else if (typeof configDefaults[key] === "number") {
|
|
48
|
-
const num = Number(m[2]);
|
|
49
|
-
if (m[2] === "" || !Number.isFinite(num) || num < 0)
|
|
50
|
-
throw new Error(`invalid value "${m[2]}" for numeric configuration option "${key}" (expected a non-negative number)`);
|
|
51
|
-
store[key] = num;
|
|
52
|
-
}
|
|
53
|
-
else if (m[2] === "")
|
|
54
|
-
throw new Error(`invalid empty value for string configuration option "${key}"`);
|
|
55
|
-
else
|
|
56
|
-
store[key] = m[2];
|
|
38
|
+
store[m[1]] = m[2];
|
|
57
39
|
}
|
|
40
|
+
const config = validateConfig(store, { trusted: true });
|
|
58
41
|
/* optionally run as an MCP service instead of rendering once,
|
|
59
42
|
with the command-line options acting as the defaults of the
|
|
60
43
|
tool calls (and hence being the only trusted source of the
|
package/dst/gradia-mcp.js
CHANGED
|
@@ -8,8 +8,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
/* internal dependencies */
|
|
11
|
-
import { configDefaults } from "./gradia-api-config.js";
|
|
12
|
-
import { Gradia, diagramTypes, diagramFormats, diagramFormatDefault } from "./gradia-api.js";
|
|
11
|
+
import { configDefaults, validateConfig } from "./gradia-api-config.js";
|
|
12
|
+
import { Gradia, diagramTypes, diagramTypeDefault, diagramFormats, diagramFormatDefault } from "./gradia-api.js";
|
|
13
13
|
/* the rendering configuration options offered to the MCP client
|
|
14
14
|
("font-embed" is intentionally omitted, as the arguments of a tool
|
|
15
15
|
call are treated as untrusted and it would allow the reading of
|
|
@@ -128,7 +128,7 @@ as <name>, terminals as "...", optional parts as [ ... ], repeatable parts as
|
|
|
128
128
|
following node, i.e., the chain is equivalent to "a --> b" and "b --> c".
|
|
129
129
|
- The <edge-name> is rendered as the label of the edge and the <edge-arity> as
|
|
130
130
|
its cardinality label.
|
|
131
|
-
-
|
|
131
|
+
- Six attribute keys are reserved and consumed by the renderer instead of being
|
|
132
132
|
displayed as a regular "<key>: <val>" line:
|
|
133
133
|
- "url: <url>" renders the node box as a hyperlink. Only relative URLs and
|
|
134
134
|
the schemes "http", "https", and "mailto" are honored; any other URL is
|
|
@@ -137,13 +137,37 @@ as <name>, terminals as "...", optional parts as [ ... ], repeatable parts as
|
|
|
137
137
|
- "primary: true" renders the node in the primary node colors (and, for the
|
|
138
138
|
diagram type "hub", marks the single central hub node). The value has to
|
|
139
139
|
be the literal string "true",
|
|
140
|
-
- "group: <name>" places the node into the surrounding group box <name
|
|
140
|
+
- "group: <name>" places the node into the surrounding group box <name>,
|
|
141
|
+
- "parent: <id>" nests the node into the container node <id> (a node which
|
|
142
|
+
is implicitly declared if never referenced otherwise),
|
|
143
|
+
- "container: <type>" marks the node as a container whose members are laid
|
|
144
|
+
out with the diagram type <type> ("graph", "hub", or "grid"), even if it
|
|
145
|
+
has no members at all.
|
|
141
146
|
- A node cannot be a member of more than one group. As soon as at least one node
|
|
142
147
|
carries a "group" attribute, all nodes without one implicitly belong to the
|
|
143
|
-
group "default", and every edge has to stay within a single group.
|
|
148
|
+
group "default", and every edge has to stay within a single group. A nested
|
|
149
|
+
node belongs to the group of its outermost container.
|
|
144
150
|
- The groups are laid out individually with the selected diagram type and then
|
|
145
151
|
stacked vertically as decorated group boxes. For the type "hub" this means
|
|
146
152
|
that every single group needs its own primary node.
|
|
153
|
+
- A node referenced as the "parent" of another node is a container: it is
|
|
154
|
+
rendered as a dashed grey box surrounding its members instead of as a node
|
|
155
|
+
box, with its label, "type", and "url" in the box head (its remaining
|
|
156
|
+
attributes are not rendered). Containers nest to any depth, but a node cannot
|
|
157
|
+
be a member of more than one container, and cannot be nested into itself.
|
|
158
|
+
- The members of a container are laid out as a diagram of their own, with the
|
|
159
|
+
diagram type of the enclosing level unless overridden by "container: <type>",
|
|
160
|
+
and the container then takes part in the layout of the enclosing level as a
|
|
161
|
+
single box. Edges may connect any two nodes, across container boundaries and
|
|
162
|
+
to container nodes themselves, except a container with one of its own
|
|
163
|
+
members. An edge crossing the boundary of a container laid out as "graph" is
|
|
164
|
+
routed through the box border to the inner node (entering on the west and
|
|
165
|
+
leaving on the east side), while an edge crossing the boundary of a container
|
|
166
|
+
laid out as "hub" or "grid" ends at the box border instead. The diagram type
|
|
167
|
+
constraints apply to every container level (e.g., a "hub" container needs its
|
|
168
|
+
own primary node, and a "grid" container permits no edges among its members).
|
|
169
|
+
- Use containers (not groups) to visualize zones, tiers, deployment units, or
|
|
170
|
+
packages whose members are connected to the outside world.
|
|
147
171
|
- All remaining attributes are displayed as "<key>: <val>" lines inside the node
|
|
148
172
|
box.
|
|
149
173
|
|
|
@@ -155,8 +179,9 @@ as <name>, terminals as "...", optional parts as [ ... ], repeatable parts as
|
|
|
155
179
|
#config color-edge-line #999999
|
|
156
180
|
|
|
157
181
|
Animal: animal [ type: "abstract class", kind: base, url: "#animal", primary: true ]
|
|
158
|
-
Dog: dog
|
|
159
|
-
Cat: cat
|
|
182
|
+
Dog: dog [ parent: Pets ]
|
|
183
|
+
Cat: cat [ parent: Pets ]
|
|
184
|
+
Pets: "Domestic Pets" [ type: package ]
|
|
160
185
|
|
|
161
186
|
Dog --(isa)--> Animal
|
|
162
187
|
Cat --(isa)--> Animal
|
|
@@ -170,11 +195,11 @@ over them. The recognized options, with their types and built-in defaults, are:
|
|
|
170
195
|
|
|
171
196
|
${configOptions}
|
|
172
197
|
|
|
173
|
-
The "size-*", "group-*", "graph-*", "hub-*", and "grid-*" options
|
|
174
|
-
rendering geometry (canvas margin, node box sizing, edge routing,
|
|
175
|
-
spacing, and the per-diagram-type layout) and take
|
|
176
|
-
the boolean "grid-node-width-equal", which forces
|
|
177
|
-
diagram to the width of the widest one.
|
|
198
|
+
The "size-*", "group-*", "container-*", "graph-*", "hub-*", and "grid-*" options
|
|
199
|
+
control the rendering geometry (canvas margin, node box sizing, edge routing,
|
|
200
|
+
group and container box spacing, and the per-diagram-type layout) and take
|
|
201
|
+
non-negative numbers, except the boolean "grid-node-width-equal", which forces
|
|
202
|
+
all node boxes of a "grid" diagram to the width of the widest one.
|
|
178
203
|
|
|
179
204
|
The "size-node-width-max" option additionally enables the word-wrapping of the
|
|
180
205
|
node box texts: given a positive value, the node name, its type, and its
|
|
@@ -215,57 +240,26 @@ On a malformed input, a violated diagram type constraint, or an invalid "config"
|
|
|
215
240
|
argument, the tool instead returns an error result whose text starts with
|
|
216
241
|
"gradia: ERROR: ", followed by the reason.
|
|
217
242
|
`.trim();
|
|
218
|
-
/* the arguments of the "gradia_render" tool
|
|
219
|
-
|
|
243
|
+
/* the arguments of the "gradia_render" tool (with their descriptions
|
|
244
|
+
reflecting the service defaults underlying the arguments) */
|
|
245
|
+
const toolArgumentsOf = (defaults) => ({
|
|
220
246
|
input: z.string()
|
|
221
247
|
.describe("The graph description in the Gradia input language (see the tool description for its grammar)."),
|
|
222
248
|
type: z.enum(diagramTypes).optional()
|
|
223
249
|
.describe("The diagram type. Takes precedence over a \"#type\" directive inside the input. " +
|
|
224
|
-
|
|
250
|
+
(defaults.type !== undefined ?
|
|
251
|
+
`Defaults to "${defaults.type}".` :
|
|
252
|
+
`Defaults to the "#type" directive of the input, else to "${diagramTypeDefault}".`)),
|
|
225
253
|
format: z.enum(diagramFormats).optional()
|
|
226
254
|
.describe("The output format: \"svg:standalone\" (a standalone SVG/XML document), " +
|
|
227
255
|
"\"svg:embedded\" (the SVG without the \"<?xml?>\" declaration, for embedding into HTML), " +
|
|
228
256
|
"\"url:xml\" (a \"data:image/svg+xml\" URL with URL-encoded XML), or " +
|
|
229
257
|
"\"url:base64\" (a \"data:image/svg+xml\" URL with Base64-encoded XML). " +
|
|
230
|
-
`Defaults to "${diagramFormatDefault}".`),
|
|
258
|
+
`Defaults to "${defaults.format ?? diagramFormatDefault}".`),
|
|
231
259
|
config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional()
|
|
232
260
|
.describe("The rendering configuration options (see the tool description for their names, " +
|
|
233
261
|
"types, and defaults). Takes precedence over the \"#config\" directives inside the input.")
|
|
234
|
-
};
|
|
235
|
-
/* validate and coerce the rendering configuration options of a tool
|
|
236
|
-
call (the arguments are treated as untrusted, so an unknown option
|
|
237
|
-
or an invalid value is rejected instead of being silently applied) */
|
|
238
|
-
const validateConfig = (raw) => {
|
|
239
|
-
const config = {};
|
|
240
|
-
for (const [key, val] of Object.entries(raw)) {
|
|
241
|
-
if (!Object.hasOwn(configDefaults, key))
|
|
242
|
-
throw new Error(`unknown configuration option "${key}"`);
|
|
243
|
-
const k = key;
|
|
244
|
-
if (k === "font-embed")
|
|
245
|
-
throw new Error(`configuration option "${key}" is not available in this context`);
|
|
246
|
-
else if (typeof configDefaults[k] === "boolean") {
|
|
247
|
-
if (typeof val !== "boolean" && val !== "true" && val !== "false")
|
|
248
|
-
throw new Error(`invalid value "${String(val)}" for boolean configuration option "${key}" ` +
|
|
249
|
-
"(expected \"true\" or \"false\")");
|
|
250
|
-
config[k] = typeof val === "boolean" ? val : val === "true";
|
|
251
|
-
}
|
|
252
|
-
else if (typeof configDefaults[k] === "number") {
|
|
253
|
-
const num = Number(val);
|
|
254
|
-
if (typeof val === "boolean" || val === "" || !Number.isFinite(num) || num < 0)
|
|
255
|
-
throw new Error(`invalid value "${String(val)}" for numeric configuration option "${key}" ` +
|
|
256
|
-
"(expected a non-negative number)");
|
|
257
|
-
config[k] = num;
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
if (typeof val !== "string")
|
|
261
|
-
throw new Error(`invalid value "${String(val)}" for string configuration option "${key}" (expected a string)`);
|
|
262
|
-
if (k === "font-family" && val.endsWith(".woff2"))
|
|
263
|
-
throw new Error("configuration option \"font-family\" must not be the path to a WOFF2 file in this context");
|
|
264
|
-
config[k] = val;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return config;
|
|
268
|
-
};
|
|
262
|
+
});
|
|
269
263
|
/* run the MCP service on stdio */
|
|
270
264
|
export const serve = async (meta, defaults = {}) => {
|
|
271
265
|
/* establish the MCP server and its single tool */
|
|
@@ -277,11 +271,11 @@ export const serve = async (meta, defaults = {}) => {
|
|
|
277
271
|
server.registerTool("gradia_render", {
|
|
278
272
|
title: "Render an object graph as an SVG diagram",
|
|
279
273
|
description: toolDescription,
|
|
280
|
-
inputSchema:
|
|
274
|
+
inputSchema: toolArgumentsOf(defaults),
|
|
281
275
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
|
|
282
276
|
}, async (args) => {
|
|
283
277
|
try {
|
|
284
|
-
const config = { ...defaults.config, ...validateConfig(args.config ?? {}) };
|
|
278
|
+
const config = { ...defaults.config, ...validateConfig(args.config ?? {}, { trusted: false }) };
|
|
285
279
|
const out = await Gradia.render(args.input, {
|
|
286
280
|
type: args.type ?? defaults.type,
|
|
287
281
|
format: args.format ?? defaults.format,
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Object Graph Diagram Rendering",
|
|
4
4
|
"keywords": [ "object", "graph", "diagram", "rendering", "svg", "layout", "api", "cli" ],
|
|
5
5
|
"publishConfig": { "access": "public" },
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.2.0",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Dr. Ralf S. Engelschall",
|