diff --git a/packages/merman/src/markdown.ts b/packages/merman/src/markdown.ts index 5b4dc55e639..907688093f3 100644 --- a/packages/merman/src/markdown.ts +++ b/packages/merman/src/markdown.ts @@ -44,6 +44,7 @@ interface PreparedDiagram { } export interface MermaidMarkdownRendererOptions { + /** Use terminal-optimized diagram spacing. Defaults to true. */ compact?: boolean /** Fold horizontal flowcharts that exceed this width. Defaults to 120 columns. */ layoutMaxWidth?: number @@ -118,10 +119,11 @@ function prepareDiagram( layoutMaxWidth: number, ): PreparedDiagram { const colors = options.colors ?? {} + const compact = options.compact ?? true switch (kind) { case "flowchart": { const grid = drawFlowchartDiagramGrid(parseMermaidFlowchartDiagram(source), { - compact: options.compact, + compact, layoutMaxWidth, }) const size = grid.getTextSize({ trimTop: true, trimBottom: true }) @@ -161,7 +163,7 @@ function prepareDiagram( } } case "sequence": { - const grid = drawSequenceDiagramGrid(parseMermaidSequenceDiagram(source), { compact: options.compact }) + const grid = drawSequenceDiagramGrid(parseMermaidSequenceDiagram(source), { compact }) const size = grid.getTextSize() return { kind, diff --git a/packages/merman/src/state/routing.test.ts b/packages/merman/src/state/routing.test.ts index 7067e45f2a5..c832c05ca78 100644 --- a/packages/merman/src/state/routing.test.ts +++ b/packages/merman/src/state/routing.test.ts @@ -243,6 +243,28 @@ describe("createStateTransitionRenderPlans", () => { ).toBe(false) } }) + + test("keeps routes to offset end markers continuous", () => { + const diagram = prepareVisibleStateDiagram( + parseMermaidStateDiagram(`stateDiagram-v2 + [*] --> Pending + Pending --> Running + Running --> Idle + Running --> Interrupted + Interrupted --> [*]`), + ) + const layout = createStateDiagramLayout(diagram, { minStateGap: 12 }) + const plan = createStateTransitionRenderPlans(diagram, layout.bounds, 30).find( + (plan) => plan.route.transition.to === "__end", + )! + + expect( + plan.path.slice(1).every(([x, y], index) => { + const previous = plan.path[index]! + return Math.abs(x - previous[0]) + Math.abs(y - previous[1]) === 1 + }), + ).toBe(true) + }) }) describe("createStateTransitionJunctionPlans", () => { diff --git a/packages/merman/src/state/routing.ts b/packages/merman/src/state/routing.ts index 7b464c900e5..23f0644bbe6 100644 --- a/packages/merman/src/state/routing.ts +++ b/packages/merman/src/state/routing.ts @@ -216,6 +216,11 @@ function verticalCorridorCrossesUnrelatedState( }) } +function hasVerticalCorridor(from: BoxBounds, to: BoxBounds): boolean { + if (from.centerY < to.centerY) return from.top + from.height <= to.top - 1 + return from.top - 1 >= to.top + to.height +} + function horizontalCorridorCrossesUnrelatedState( diagram: StateVisibleDiagram, transition: StateVisibleTransition, @@ -388,6 +393,9 @@ export function createStateTransitionRoutePlans( } return [{ ...base, kind: "horizontal-forward", leftToRight: from.centerX <= to.centerX }] } + if (!hasVerticalCorridor(from, to)) { + return [{ ...base, kind: "side-parallel", railX: allocateSideRail(transition.label) }] + } if (from.centerX !== to.centerX) { return [{ ...base, kind: "vertical-elbow", hasReverse: false, offsetConnector: false }] } @@ -395,6 +403,9 @@ export function createStateTransitionRoutePlans( } if (from.centerY !== to.centerY) { + if (!hasVerticalCorridor(from, to)) { + return [{ ...base, kind: "side-parallel", railX: allocateSideRail(transition.label) }] + } if (from.centerY > to.centerY && feedback) return [ { diff --git a/packages/merman/src/test/markdown.test.ts b/packages/merman/src/test/markdown.test.ts index 380aa4034a6..7173b15a1b5 100644 --- a/packages/merman/src/test/markdown.test.ts +++ b/packages/merman/src/test/markdown.test.ts @@ -78,6 +78,29 @@ flowchart LR expect(markdown.getChildren()[0]?.marginTop).toBe(1) }) +test("uses compact terminal spacing for Mermaid diagrams by default", async () => { + const testRenderer = await createTestRenderer({ width: 80, height: 40 }) + renderer = testRenderer.renderer + const markdown = new MarkdownRenderable(renderer, { + id: "markdown-compact-mermaid", + content: `\`\`\`mermaid +flowchart TD + repo[Organization profile repo] --> build[Build] + build --> worker[Isolated organization Worker] + worker --> sessions[SessionDOs] + sessions --> opencode[OpenCode + native plugins] + opencode --> modal[Modal workspaces] +\`\`\``, + syntaxStyle, + renderNode: createMermaidMarkdownRenderer(renderer), + }) + + renderer.root.add(markdown) + await renderMarkdown(markdown, testRenderer.renderOnce) + + expect(markdown.getChildren()[0]?.height).toBe(28) +}) + test("recognizes normalized Mermaid fence info strings", async () => { const testRenderer = await createTestRenderer({ width: 80, height: 14 }) renderer = testRenderer.renderer