@liminis/editor 0.3.0 → 0.4.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.
Files changed (36) hide show
  1. package/LICENSE +0 -13
  2. package/README.md +122 -75
  3. package/dist/app/editor/CorrectionPanelPlugin.js +10 -11
  4. package/dist/app/editor/DragHandlePlugin.js +1 -1
  5. package/dist/app/editor/SelectionContextMenuPlugin.js +4 -4
  6. package/dist/app/editor/nodes/C4Component.js +8 -10
  7. package/dist/app/editor/nodes/C4Node.d.ts +1 -1
  8. package/dist/app/editor/nodes/DiagramContextMenu.js +5 -5
  9. package/dist/headless.d.ts +3 -5
  10. package/dist/headless.js +2 -4
  11. package/dist/index.d.ts +1 -1
  12. package/dist/styles.css +425 -454
  13. package/docs/architecture.md +80 -0
  14. package/docs/decisions/adr-93-liminis-editor-defined-aliases.md +28 -1
  15. package/docs/decisions/adr-98-invert-token-direction.md +309 -0
  16. package/docs/diagrams/architecture-1-dark.svg +1 -0
  17. package/docs/diagrams/architecture-1.svg +1 -0
  18. package/docs/diagrams/architecture-2-dark.svg +1 -0
  19. package/docs/diagrams/architecture-2.svg +1 -0
  20. package/package.json +2 -2
  21. package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +0 -35
  22. package/dist/app/editor/c4/C4InteractiveRenderer.js +0 -299
  23. package/dist/app/editor/c4/edge-clipping.d.ts +0 -24
  24. package/dist/app/editor/c4/edge-clipping.js +0 -139
  25. package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +0 -38
  26. package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +0 -112
  27. package/dist/app/editor/c4/layout.d.ts +0 -25
  28. package/dist/app/editor/c4/layout.js +0 -839
  29. package/dist/app/editor/c4/parser.d.ts +0 -19
  30. package/dist/app/editor/c4/parser.js +0 -410
  31. package/dist/app/editor/c4/render-to-string.d.ts +0 -24
  32. package/dist/app/editor/c4/render-to-string.js +0 -34
  33. package/dist/app/editor/c4/renderer.d.ts +0 -64
  34. package/dist/app/editor/c4/renderer.js +0 -569
  35. package/dist/app/editor/c4/types.d.ts +0 -203
  36. package/dist/app/editor/c4/types.js +0 -43
@@ -1,19 +0,0 @@
1
- /**
2
- * C4-PlantUML Parser
3
- *
4
- * Parses C4-PlantUML macro syntax into a C4 diagram AST.
5
- * Supports: Person, System, Container, Component (and _Ext, Db, Queue variants),
6
- * System_Boundary, Container_Boundary, Rel (and directional variants), BiRel.
7
- *
8
- * Strips: @startuml/@enduml, !include, SHOW_LEGEND(), LAYOUT_* directives.
9
- */
10
- import type { C4Diagram, ParseError, ParseResult } from './types.js';
11
- /**
12
- * Parse C4-PlantUML code into a C4 diagram AST.
13
- */
14
- export declare function parseC4(code: string): ParseResult;
15
- /**
16
- * Validate that a C4 diagram is well-formed.
17
- * Checks for duplicate IDs, missing relationship targets, etc.
18
- */
19
- export declare function validateC4(diagram: C4Diagram): ParseError[];
@@ -1,410 +0,0 @@
1
- /**
2
- * C4-PlantUML Parser
3
- *
4
- * Parses C4-PlantUML macro syntax into a C4 diagram AST.
5
- * Supports: Person, System, Container, Component (and _Ext, Db, Queue variants),
6
- * System_Boundary, Container_Boundary, Rel (and directional variants), BiRel.
7
- *
8
- * Strips: @startuml/@enduml, !include, SHOW_LEGEND(), LAYOUT_* directives.
9
- */
10
- const ELEMENT_MACROS = {
11
- // Person
12
- Person: { type: 'person', external: false, shape: 'rectangle', argStyle: 'system', boundary: false },
13
- Person_Ext: { type: 'person', external: true, shape: 'rectangle', argStyle: 'system', boundary: false },
14
- // System
15
- System: { type: 'system', external: false, shape: 'rectangle', argStyle: 'system', boundary: false },
16
- System_Ext: { type: 'system', external: true, shape: 'rectangle', argStyle: 'system', boundary: false },
17
- SystemDb: { type: 'system', external: false, shape: 'cylinder', argStyle: 'system', boundary: false },
18
- SystemDb_Ext: { type: 'system', external: true, shape: 'cylinder', argStyle: 'system', boundary: false },
19
- SystemQueue: { type: 'system', external: false, shape: 'queue', argStyle: 'system', boundary: false },
20
- SystemQueue_Ext: { type: 'system', external: true, shape: 'queue', argStyle: 'system', boundary: false },
21
- // Container
22
- Container: { type: 'container', external: false, shape: 'rectangle', argStyle: 'detail', boundary: false },
23
- Container_Ext: { type: 'container', external: true, shape: 'rectangle', argStyle: 'detail', boundary: false },
24
- ContainerDb: { type: 'container', external: false, shape: 'cylinder', argStyle: 'detail', boundary: false },
25
- ContainerDb_Ext: { type: 'container', external: true, shape: 'cylinder', argStyle: 'detail', boundary: false },
26
- ContainerQueue: { type: 'container', external: false, shape: 'queue', argStyle: 'detail', boundary: false },
27
- ContainerQueue_Ext: { type: 'container', external: true, shape: 'queue', argStyle: 'detail', boundary: false },
28
- // Component
29
- Component: { type: 'component', external: false, shape: 'rectangle', argStyle: 'detail', boundary: false },
30
- Component_Ext: { type: 'component', external: true, shape: 'rectangle', argStyle: 'detail', boundary: false },
31
- ComponentDb: { type: 'component', external: false, shape: 'cylinder', argStyle: 'detail', boundary: false },
32
- ComponentDb_Ext: { type: 'component', external: true, shape: 'cylinder', argStyle: 'detail', boundary: false },
33
- ComponentQueue: { type: 'component', external: false, shape: 'queue', argStyle: 'detail', boundary: false },
34
- ComponentQueue_Ext: { type: 'component', external: true, shape: 'queue', argStyle: 'detail', boundary: false },
35
- // Boundaries
36
- System_Boundary: { type: 'system', external: false, shape: 'rectangle', argStyle: 'system', boundary: true },
37
- Container_Boundary: { type: 'container', external: false, shape: 'rectangle', argStyle: 'system', boundary: true },
38
- Enterprise_Boundary: { type: 'system', external: false, shape: 'rectangle', argStyle: 'system', boundary: true },
39
- Boundary: { type: 'system', external: false, shape: 'rectangle', argStyle: 'system', boundary: true },
40
- // Deployment nodes (C4 deployment diagrams)
41
- Deployment_Node: { type: 'system', external: false, shape: 'rectangle', argStyle: 'detail', boundary: true },
42
- Deployment_Node_L: { type: 'system', external: false, shape: 'rectangle', argStyle: 'detail', boundary: true },
43
- Deployment_Node_R: { type: 'system', external: false, shape: 'rectangle', argStyle: 'detail', boundary: true },
44
- Node: { type: 'system', external: false, shape: 'rectangle', argStyle: 'detail', boundary: true },
45
- Node_L: { type: 'system', external: false, shape: 'rectangle', argStyle: 'detail', boundary: true },
46
- Node_R: { type: 'system', external: false, shape: 'rectangle', argStyle: 'detail', boundary: true },
47
- // Infrastructure nodes (C4 deployment diagrams)
48
- InfrastructureNode: { type: 'component', external: false, shape: 'rectangle', argStyle: 'detail', boundary: false },
49
- InfrastructureNode_Ext: { type: 'component', external: true, shape: 'rectangle', argStyle: 'detail', boundary: false },
50
- };
51
- const REL_MACROS = new Set([
52
- 'Rel', 'Rel_Back', 'Rel_Neighbor',
53
- 'Rel_D', 'Rel_U', 'Rel_L', 'Rel_R',
54
- 'BiRel', 'BiRel_D', 'BiRel_U', 'BiRel_L', 'BiRel_R',
55
- ]);
56
- /** Directives and macros to silently skip */
57
- const SKIP_PREFIXES = ['@startuml', '@enduml', '!include', '!define', 'SHOW_', 'HIDE_', 'title '];
58
- // =============================================================================
59
- // LINE-BASED PARSER
60
- // =============================================================================
61
- /**
62
- * Parse C4-PlantUML code into a C4 diagram AST.
63
- */
64
- export function parseC4(code) {
65
- const parser = new C4PlantUMLParser(code);
66
- return parser.parse();
67
- }
68
- /**
69
- * Validate that a C4 diagram is well-formed.
70
- * Checks for duplicate IDs, missing relationship targets, etc.
71
- */
72
- export function validateC4(diagram) {
73
- const errors = [];
74
- const elementIds = new Set();
75
- for (const element of diagram.elements) {
76
- if (elementIds.has(element.id)) {
77
- errors.push({
78
- message: `Duplicate element ID: '${element.id}'`,
79
- line: 0,
80
- column: 0,
81
- });
82
- }
83
- elementIds.add(element.id);
84
- }
85
- for (const rel of diagram.relationships) {
86
- if (!elementIds.has(rel.sourceId)) {
87
- errors.push({
88
- message: `Relationship source '${rel.sourceId}' not found`,
89
- line: 0,
90
- column: 0,
91
- });
92
- }
93
- if (!elementIds.has(rel.targetId)) {
94
- errors.push({
95
- message: `Relationship target '${rel.targetId}' not found`,
96
- line: 0,
97
- column: 0,
98
- });
99
- }
100
- }
101
- return errors;
102
- }
103
- class C4PlantUMLParser {
104
- lines;
105
- lineNum = 0;
106
- elements = [];
107
- relationships = [];
108
- errors = [];
109
- direction;
110
- constructor(code) {
111
- this.lines = code.split('\n');
112
- }
113
- parse() {
114
- this.parseLines(undefined);
115
- const diagram = {
116
- elements: this.elements,
117
- relationships: this.relationships,
118
- direction: this.direction,
119
- };
120
- return {
121
- diagram: this.errors.length === 0 ? diagram : null,
122
- errors: this.errors,
123
- };
124
- }
125
- /**
126
- * Parse lines at the current level. If parentId is set, we're inside a boundary.
127
- * Stops when we hit a closing brace or run out of lines.
128
- */
129
- parseLines(parentId) {
130
- while (this.lineNum < this.lines.length) {
131
- const rawLine = this.lines[this.lineNum];
132
- const line = this.stripComment(rawLine).trim();
133
- // Empty or comment-only line
134
- if (line === '') {
135
- this.lineNum++;
136
- continue;
137
- }
138
- // Closing brace — return to parent
139
- if (line === '}' || line.startsWith('}')) {
140
- this.lineNum++;
141
- return;
142
- }
143
- // Parse LAYOUT directives
144
- if (line.startsWith('LAYOUT_TOP_DOWN')) {
145
- this.direction = 'down';
146
- this.lineNum++;
147
- continue;
148
- }
149
- if (line.startsWith('LAYOUT_LEFT_RIGHT')) {
150
- this.direction = 'right';
151
- this.lineNum++;
152
- continue;
153
- }
154
- // Skip other directives
155
- if (this.isSkippable(line)) {
156
- this.lineNum++;
157
- continue;
158
- }
159
- // Extract the macro name (first word before '(')
160
- const macroMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(line);
161
- if (!macroMatch) {
162
- // Unknown line
163
- this.errors.push({
164
- message: `Unrecognized syntax: '${line.slice(0, 50)}'`,
165
- line: this.lineNum + 1,
166
- column: 1,
167
- });
168
- this.lineNum++;
169
- continue;
170
- }
171
- const macroName = macroMatch[1];
172
- // Element macro
173
- if (macroName in ELEMENT_MACROS) {
174
- this.parseElementMacro(macroName, line, parentId);
175
- continue;
176
- }
177
- // Relationship macro
178
- if (REL_MACROS.has(macroName)) {
179
- this.parseRelMacro(line);
180
- this.lineNum++;
181
- continue;
182
- }
183
- // Unknown macro — skip
184
- this.errors.push({
185
- message: `Unknown macro: '${macroName}'`,
186
- line: this.lineNum + 1,
187
- column: 1,
188
- });
189
- this.lineNum++;
190
- }
191
- }
192
- /**
193
- * Parse an element macro like Person(alias, "Label", "Description")
194
- * or a boundary like System_Boundary(alias, "Label") { ... }
195
- */
196
- parseElementMacro(macroName, line, parentId) {
197
- const info = ELEMENT_MACROS[macroName];
198
- const currentLine = this.lineNum + 1;
199
- // Extract arguments from parentheses
200
- const args = this.extractArgs(line, currentLine);
201
- if (args === null) {
202
- this.lineNum++;
203
- return;
204
- }
205
- // First arg is always the alias (unquoted identifier)
206
- const alias = args[0]?.replace(/^"|"$/g, '');
207
- if (!alias) {
208
- this.errors.push({
209
- message: `${macroName}: missing alias (first argument)`,
210
- line: currentLine,
211
- column: 1,
212
- });
213
- this.lineNum++;
214
- return;
215
- }
216
- // Build properties from remaining args
217
- const properties = {};
218
- let name;
219
- if (info.external)
220
- properties.external = true;
221
- if (info.shape !== 'rectangle')
222
- properties.shape = info.shape;
223
- if (info.boundary)
224
- properties.style = 'boundary';
225
- if (info.argStyle === 'system') {
226
- // (alias, "label", "description")
227
- name = this.unquote(args[1] ?? '') || alias;
228
- const desc = this.unquote(args[2] ?? '');
229
- if (desc)
230
- properties.description = desc;
231
- }
232
- else {
233
- // (alias, "label", "technology", "description")
234
- name = this.unquote(args[1] ?? '') || alias;
235
- const tech = this.unquote(args[2] ?? '');
236
- const desc = this.unquote(args[3] ?? '');
237
- if (tech)
238
- properties.tech = tech;
239
- if (desc)
240
- properties.description = desc;
241
- }
242
- // Check for $tags and other named params
243
- for (const arg of args) {
244
- if (arg.startsWith('$tags=')) {
245
- // Could be used for styling in the future
246
- }
247
- }
248
- const element = {
249
- type: info.type,
250
- id: alias,
251
- name,
252
- properties,
253
- children: [],
254
- parent: parentId,
255
- };
256
- this.elements.push(element);
257
- // Check if this line ends with { (boundary with children)
258
- const hasBrace = line.includes('{') && !line.includes('}');
259
- if (hasBrace) {
260
- this.lineNum++;
261
- // Parse children
262
- const childrenBefore = this.elements.length;
263
- this.parseLines(alias);
264
- // Link children
265
- for (let i = childrenBefore; i < this.elements.length; i++) {
266
- if (this.elements[i].parent === alias) {
267
- element.children.push(this.elements[i]);
268
- }
269
- }
270
- }
271
- else {
272
- this.lineNum++;
273
- }
274
- }
275
- /**
276
- * Parse a relationship macro like Rel(from, to, "label", "tech")
277
- */
278
- parseRelMacro(line) {
279
- const currentLine = this.lineNum + 1;
280
- const args = this.extractArgs(line, currentLine);
281
- if (args === null)
282
- return;
283
- if (args.length < 3) {
284
- this.errors.push({
285
- message: `Relationship requires at least 3 arguments (from, to, label)`,
286
- line: currentLine,
287
- column: 1,
288
- });
289
- return;
290
- }
291
- const sourceId = args[0].replace(/^"|"$/g, '');
292
- const targetId = args[1].replace(/^"|"$/g, '');
293
- const label = this.unquote(args[2]);
294
- const tech = this.unquote(args[3] ?? '');
295
- const fullLabel = tech ? `${label} [${tech}]` : label;
296
- this.relationships.push({
297
- sourceId,
298
- targetId,
299
- label: fullLabel,
300
- });
301
- }
302
- /**
303
- * Extract comma-separated arguments from a macro call.
304
- * Handles quoted strings with commas inside them.
305
- */
306
- extractArgs(line, lineNum) {
307
- // Find opening paren
308
- const openIdx = line.indexOf('(');
309
- if (openIdx === -1) {
310
- this.errors.push({
311
- message: `Expected '(' in macro call`,
312
- line: lineNum,
313
- column: 1,
314
- });
315
- return null;
316
- }
317
- // Find matching closing paren (handle nested parens in edge cases)
318
- let depth = 0;
319
- let closeIdx = -1;
320
- for (let i = openIdx; i < line.length; i++) {
321
- if (line[i] === '(')
322
- depth++;
323
- else if (line[i] === ')') {
324
- depth--;
325
- if (depth === 0) {
326
- closeIdx = i;
327
- break;
328
- }
329
- }
330
- }
331
- if (closeIdx === -1) {
332
- this.errors.push({
333
- message: `Unmatched '(' — missing closing ')'`,
334
- line: lineNum,
335
- column: openIdx + 1,
336
- });
337
- return null;
338
- }
339
- const inner = line.slice(openIdx + 1, closeIdx);
340
- return this.splitArgs(inner);
341
- }
342
- /**
343
- * Split a comma-separated argument string, respecting quoted strings.
344
- */
345
- splitArgs(inner) {
346
- const args = [];
347
- let current = '';
348
- let inQuote = false;
349
- let escaped = false;
350
- for (const char of inner) {
351
- if (escaped) {
352
- current += char;
353
- escaped = false;
354
- continue;
355
- }
356
- if (char === '\\') {
357
- escaped = true;
358
- current += char;
359
- continue;
360
- }
361
- if (char === '"') {
362
- inQuote = !inQuote;
363
- current += char;
364
- continue;
365
- }
366
- if (char === ',' && !inQuote) {
367
- const trimmed = current.trim();
368
- if (trimmed)
369
- args.push(trimmed);
370
- current = '';
371
- continue;
372
- }
373
- current += char;
374
- }
375
- const trimmed = current.trim();
376
- if (trimmed)
377
- args.push(trimmed);
378
- return args;
379
- }
380
- /**
381
- * Remove surrounding quotes from a string argument.
382
- */
383
- unquote(s) {
384
- s = s.trim();
385
- if (s.startsWith('"') && s.endsWith('"')) {
386
- return s.slice(1, -1);
387
- }
388
- return s;
389
- }
390
- /**
391
- * Strip single-line comment (C4-PlantUML uses ' for comments)
392
- */
393
- stripComment(line) {
394
- let inQuote = false;
395
- for (let i = 0; i < line.length; i++) {
396
- if (line[i] === '"')
397
- inQuote = !inQuote;
398
- if (line[i] === '\'' && !inQuote) {
399
- return line.slice(0, i);
400
- }
401
- }
402
- return line;
403
- }
404
- /**
405
- * Check if a line should be silently skipped (directives, etc.)
406
- */
407
- isSkippable(line) {
408
- return SKIP_PREFIXES.some((p) => line.startsWith(p));
409
- }
410
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Headless C4 SVG rendering — pure .ts wrapper (no JSX).
3
- *
4
- * Bridges the React C4Renderer to Node.js contexts (Electron main process)
5
- * by using renderToStaticMarkup + createElement instead of JSX.
6
- *
7
- * Used by the app_render_c4_diagram MCP tool for Confluence publishing.
8
- */
9
- import type { ParseError } from './types.js';
10
- /**
11
- * Parse C4 code and render to SVG string in one call.
12
- *
13
- * @param code - C4 DSL source code
14
- * @param isDarkMode - Whether to render in dark mode
15
- * @param manualPositions - Optional manual positions for elements (bypasses dagre auto-layout)
16
- * @returns svg string and any parse/validation errors
17
- */
18
- export declare function renderC4DiagramToSVG(code: string, isDarkMode?: boolean, manualPositions?: Record<string, {
19
- x: number;
20
- y: number;
21
- }>): {
22
- svg: string;
23
- errors: ParseError[];
24
- };
@@ -1,34 +0,0 @@
1
- /**
2
- * Headless C4 SVG rendering — pure .ts wrapper (no JSX).
3
- *
4
- * Bridges the React C4Renderer to Node.js contexts (Electron main process)
5
- * by using renderToStaticMarkup + createElement instead of JSX.
6
- *
7
- * Used by the app_render_c4_diagram MCP tool for Confluence publishing.
8
- */
9
- import { renderToStaticMarkup } from 'react-dom/server';
10
- import { createElement } from 'react';
11
- import { C4Renderer } from './renderer.js';
12
- import { parseC4, validateC4 } from './parser.js';
13
- import { layoutC4Diagram } from './layout.js';
14
- /**
15
- * Parse C4 code and render to SVG string in one call.
16
- *
17
- * @param code - C4 DSL source code
18
- * @param isDarkMode - Whether to render in dark mode
19
- * @param manualPositions - Optional manual positions for elements (bypasses dagre auto-layout)
20
- * @returns svg string and any parse/validation errors
21
- */
22
- export function renderC4DiagramToSVG(code, isDarkMode = false, manualPositions) {
23
- const parseResult = parseC4(code);
24
- if (parseResult.diagram) {
25
- const validationErrors = validateC4(parseResult.diagram);
26
- parseResult.errors.push(...validationErrors);
27
- }
28
- if (!parseResult.diagram || parseResult.errors.length > 0) {
29
- return { svg: '', errors: parseResult.errors };
30
- }
31
- const layout = layoutC4Diagram(parseResult.diagram, undefined, manualPositions);
32
- const svg = renderToStaticMarkup(createElement(C4Renderer, { layout, isDarkMode }));
33
- return { svg, errors: [] };
34
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * C4 SVG Renderer (Canonical)
3
- *
4
- * Pure React component that renders C4 architecture diagrams as SVG.
5
- * This is the single authoritative renderer — used in the editor (via C4Node)
6
- * and for headless SVG generation (via renderToStaticMarkup for publishing).
7
- *
8
- * Features:
9
- * - All C4 element types (person, system, container, component)
10
- * - All shapes (rectangle, cylinder, queue)
11
- * - Boundary nesting with alternating fills
12
- * - Edge labels with line clipping, rotation, and multi-line support
13
- * - Step number labels (circled) and legend ref labels (squared)
14
- * - Smart legend placement for long/parallel labels
15
- * - Light and dark theme support
16
- */
17
- import type { LayoutResult } from './types.js';
18
- /**
19
- * Compute legend placement and dimensions for a layout.
20
- * Exported for use by C4InteractiveRenderer to create a legend hit area.
21
- * Returns null if the layout has no legend entries.
22
- */
23
- export declare function computeLegendInfo(layout: LayoutResult): {
24
- x: number;
25
- y: number;
26
- width: number;
27
- height: number;
28
- } | null;
29
- export interface C4RendererProps {
30
- /** Layout result from the layout engine */
31
- layout: LayoutResult;
32
- /** Whether dark mode is enabled */
33
- isDarkMode: boolean;
34
- }
35
- /**
36
- * C4 Diagram SVG Renderer.
37
- *
38
- * Pure component: takes layout + theme, returns SVG.
39
- * Used directly in the editor and via renderToStaticMarkup for publishing.
40
- */
41
- export declare function C4Renderer({ layout, isDarkMode }: C4RendererProps): JSX.Element;
42
- /**
43
- * Renders C4 diagram content (nodes, edges, legend) as SVG group elements.
44
- * Exported for use by C4InteractiveRenderer to embed diagram content
45
- * inside a custom SVG wrapper without depending on C4Renderer's DOM structure.
46
- */
47
- export interface C4RendererContentProps extends C4RendererProps {
48
- /** Override legend position (for manual layout mode) */
49
- legendPositionOverride?: {
50
- x: number;
51
- y: number;
52
- } | null;
53
- }
54
- export declare function C4RendererContent({ layout, isDarkMode, legendPositionOverride }: C4RendererContentProps): JSX.Element;
55
- export interface C4ErrorDisplayProps {
56
- errors: {
57
- message: string;
58
- line: number;
59
- column: number;
60
- }[];
61
- isDarkMode: boolean;
62
- }
63
- export declare function C4ErrorDisplay({ errors, isDarkMode }: C4ErrorDisplayProps): JSX.Element;
64
- export default C4Renderer;