Kaynağa Gözat

fix(tui): optimize Mermaid terminal rendering (#42595)

Kit Langton 2 gün önce
ebeveyn
işleme
1cf97fac5c

+ 4 - 2
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,

+ 22 - 0
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", () => {

+ 11 - 0
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 [
           {

+ 23 - 0
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