@rse/gradia 1.1.9 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 { configDefaults } from "./gradia-api-config.js";
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 config = {};
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
- if (!Object.hasOwn(configDefaults, m[1]))
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,8 +128,8 @@ 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
- - Four attribute keys are reserved and consumed by the renderer instead of being
132
- displayed as a regular "<key>: <val>" line:
131
+ - Seven attribute keys are reserved and consumed by the renderer instead of
132
+ being 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
135
135
  silently dropped and the node box then simply stays unlinked,
@@ -137,13 +137,47 @@ 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,
146
+ - "order: <number>" places the node explicitly (see below).
147
+ - A "graph" (a "graph" container level, too) with at least one node carrying an
148
+ "order" attribute is laid out in that explicit order instead of by the layered
149
+ algorithm: the distinct order values become the rows, top-down in ascending
150
+ order, the nodes of one value fill their row left-to-right in declaration
151
+ order (wrapped into further rows beyond "graph-columns-max" nodes), and the
152
+ nodes without an order follow in the trailing rows. A "grid" places its tiles
153
+ in the order of the attribute, too, the tiles without one trailing. Use it to
154
+ impose a reading order (e.g. the order of a document), which lets every
155
+ reference to an earlier item point upward or leftward.
141
156
  - A node cannot be a member of more than one group. As soon as at least one node
142
157
  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.
158
+ group "default", and every edge has to stay within a single group. A nested
159
+ node belongs to the group of its outermost container.
144
160
  - The groups are laid out individually with the selected diagram type and then
145
161
  stacked vertically as decorated group boxes. For the type "hub" this means
146
162
  that every single group needs its own primary node.
163
+ - A node referenced as the "parent" of another node is a container: it is
164
+ rendered as a dashed grey box surrounding its members instead of as a node
165
+ box, with its label, "type", and "url" in the box head (its remaining
166
+ attributes are not rendered). Containers nest to any depth, but a node cannot
167
+ be a member of more than one container, and cannot be nested into itself.
168
+ - The members of a container are laid out as a diagram of their own, with the
169
+ diagram type of the enclosing level unless overridden by "container: <type>",
170
+ and the container then takes part in the layout of the enclosing level as a
171
+ single box. Edges may connect any two nodes, across container boundaries and
172
+ to container nodes themselves, except a container with one of its own
173
+ members. An edge crossing the boundary of a container laid out as "graph" is
174
+ routed through the box border to the inner node (entering on the west and
175
+ leaving on the east side), while an edge crossing the boundary of a container
176
+ laid out as "hub" or "grid" ends at the box border instead. The diagram type
177
+ constraints apply to every container level (e.g., a "hub" container needs its
178
+ own primary node, and a "grid" container permits no edges among its members).
179
+ - Use containers (not groups) to visualize zones, tiers, deployment units, or
180
+ packages whose members are connected to the outside world.
147
181
  - All remaining attributes are displayed as "<key>: <val>" lines inside the node
148
182
  box.
149
183
 
@@ -155,8 +189,9 @@ as <name>, terminals as "...", optional parts as [ ... ], repeatable parts as
155
189
  #config color-edge-line #999999
156
190
 
157
191
  Animal: animal [ type: "abstract class", kind: base, url: "#animal", primary: true ]
158
- Dog: dog
159
- Cat: cat
192
+ Dog: dog [ parent: Pets ]
193
+ Cat: cat [ parent: Pets ]
194
+ Pets: "Domestic Pets" [ type: package ]
160
195
 
161
196
  Dog --(isa)--> Animal
162
197
  Cat --(isa)--> Animal
@@ -170,11 +205,11 @@ over them. The recognized options, with their types and built-in defaults, are:
170
205
 
171
206
  ${configOptions}
172
207
 
173
- The "size-*", "group-*", "graph-*", "hub-*", and "grid-*" options control the
174
- rendering geometry (canvas margin, node box sizing, edge routing, group box
175
- spacing, and the per-diagram-type layout) and take non-negative numbers, except
176
- the boolean "grid-node-width-equal", which forces all node boxes of a "grid"
177
- diagram to the width of the widest one.
208
+ The "size-*", "group-*", "container-*", "graph-*", "hub-*", and "grid-*" options
209
+ control the rendering geometry (canvas margin, node box sizing, edge routing,
210
+ group and container box spacing, and the per-diagram-type layout) and take
211
+ non-negative numbers, except the boolean "grid-node-width-equal", which forces
212
+ all node boxes of a "grid" diagram to the width of the widest one.
178
213
 
179
214
  The "size-node-width-max" option additionally enables the word-wrapping of the
180
215
  node box texts: given a positive value, the node name, its type, and its
@@ -215,57 +250,26 @@ On a malformed input, a violated diagram type constraint, or an invalid "config"
215
250
  argument, the tool instead returns an error result whose text starts with
216
251
  "gradia: ERROR: ", followed by the reason.
217
252
  `.trim();
218
- /* the arguments of the "gradia_render" tool */
219
- const toolArguments = {
253
+ /* the arguments of the "gradia_render" tool (with their descriptions
254
+ reflecting the service defaults underlying the arguments) */
255
+ const toolArgumentsOf = (defaults) => ({
220
256
  input: z.string()
221
257
  .describe("The graph description in the Gradia input language (see the tool description for its grammar)."),
222
258
  type: z.enum(diagramTypes).optional()
223
259
  .describe("The diagram type. Takes precedence over a \"#type\" directive inside the input. " +
224
- "Defaults to the \"#type\" directive of the input, else to \"graph\"."),
260
+ (defaults.type !== undefined ?
261
+ `Defaults to "${defaults.type}".` :
262
+ `Defaults to the "#type" directive of the input, else to "${diagramTypeDefault}".`)),
225
263
  format: z.enum(diagramFormats).optional()
226
264
  .describe("The output format: \"svg:standalone\" (a standalone SVG/XML document), " +
227
265
  "\"svg:embedded\" (the SVG without the \"<?xml?>\" declaration, for embedding into HTML), " +
228
266
  "\"url:xml\" (a \"data:image/svg+xml\" URL with URL-encoded XML), or " +
229
267
  "\"url:base64\" (a \"data:image/svg+xml\" URL with Base64-encoded XML). " +
230
- `Defaults to "${diagramFormatDefault}".`),
268
+ `Defaults to "${defaults.format ?? diagramFormatDefault}".`),
231
269
  config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional()
232
270
  .describe("The rendering configuration options (see the tool description for their names, " +
233
271
  "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
- };
272
+ });
269
273
  /* run the MCP service on stdio */
270
274
  export const serve = async (meta, defaults = {}) => {
271
275
  /* establish the MCP server and its single tool */
@@ -277,11 +281,11 @@ export const serve = async (meta, defaults = {}) => {
277
281
  server.registerTool("gradia_render", {
278
282
  title: "Render an object graph as an SVG diagram",
279
283
  description: toolDescription,
280
- inputSchema: toolArguments,
284
+ inputSchema: toolArgumentsOf(defaults),
281
285
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
282
286
  }, async (args) => {
283
287
  try {
284
- const config = { ...defaults.config, ...validateConfig(args.config ?? {}) };
288
+ const config = { ...defaults.config, ...validateConfig(args.config ?? {}, { trusted: false }) };
285
289
  const out = await Gradia.render(args.input, {
286
290
  type: args.type ?? defaults.type,
287
291
  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.1.9",
6
+ "version": "1.2.1",
7
7
  "license": "MIT",
8
8
  "author": {
9
9
  "name": "Dr. Ralf S. Engelschall",