From 5d92336e198a7d2317bf758ced06961f0b38564c Mon Sep 17 00:00:00 2001 From: roboomp Date: Sun, 9 Aug 2026 19:03:32 +0000 Subject: [PATCH] fix(utils): masked routed lines beneath edge labels Added opaque label-space cells so Mermaid ASCII edge labels erase routed paths while preserving spaces during serialization. Added horizontal and vertical regression coverage. Fixes #8098 --- packages/utils/CHANGELOG.md | 4 ++++ .../src/vendor/mermaid-ascii/ascii/canvas.ts | 12 ++++++------ .../src/vendor/mermaid-ascii/ascii/draw.ts | 4 ++-- .../src/vendor/mermaid-ascii/text-metrics.ts | 3 +++ packages/utils/test/mermaid-ascii.test.ts | 18 ++++++++++++++++++ 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 498d25ff2..9e84c0aa0 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Mermaid ASCII multi-word edge labels allowing routed lines to show through spaces ([#8098](https://github.com/can1357/oh-my-pi/issues/8098)). + ## [17.2.11] - 2026-08-07 ### Added diff --git a/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts b/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts index 2db2d19c6..1499e18fd 100644 --- a/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts +++ b/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts @@ -8,7 +8,7 @@ import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types' import { colorizeLine, DEFAULT_ASCII_THEME } from './ansi' -import { displayWidth, toCells, WIDE_PAD } from '../text-metrics' +import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics' /** * Create a blank canvas filled with spaces. @@ -189,7 +189,7 @@ export function isJunctionChar(c: string): boolean { * letter/digit test misses. */ function isLabelChar(c: string): boolean { - return c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c) + return c === LABEL_SPACE || c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c) } /** @@ -268,7 +268,7 @@ export function mergeCanvases( for (let x = 0; x < overlay.length; x++) { for (let y = 0; y < overlay[0]!.length; y++) { const c = overlay[x]![y]! - // WIDE_PAD cells are written atomically with their lead below + // Spaces are transparent; WIDE_PAD cells are written atomically with their lead below if (c === ' ' || c === WIDE_PAD) continue const mx = x + offset.x const my = y + offset.y @@ -327,8 +327,8 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions): let line = '' for (let x = 0; x <= maxX; x++) { const c = canvas[x]![y]! - // Skip wide-glyph continuation cells: the glyph itself spans 2 columns - if (c !== WIDE_PAD) line += c + // Skip wide-glyph continuation cells and restore opaque label spaces. + if (c !== WIDE_PAD) line += c === LABEL_SPACE ? ' ' : c } lines.push(line) } else { @@ -338,7 +338,7 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions): for (let x = 0; x <= maxX; x++) { const c = canvas[x]![y]! if (c === WIDE_PAD) continue - chars.push(c) + chars.push(c === LABEL_SPACE ? ' ' : c) roles.push(roleCanvas[x]?.[y] ?? null) } lines.push(colorizeLine(chars, roles, theme, colorMode)) diff --git a/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts b/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts index 7d7fdc821..310d56e2d 100644 --- a/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts +++ b/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts @@ -21,7 +21,7 @@ import { gridToDrawingCoord, lineToDrawing } from './grid' import { splitLines } from './multiline-utils' import { getCorners } from './shapes/corners' import { getShapeAttachmentPoint } from './shapes/index' -import { displayWidth, toCells, WIDE_PAD } from '../text-metrics' +import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics' // ============================================================================ // Node drawing — renders a node using shape-aware rendering @@ -677,7 +677,7 @@ function drawTextOnLine(canvas: Canvas, line: DrawingCoord[], label: string, isU for (let i = 0; i < lines.length; i++) { const lineText = lines[i]! const startX = middleX - Math.floor(displayWidth(lineText) / 2) - drawText(canvas, { x: startX, y: startY + i }, lineText) + drawText(canvas, { x: startX, y: startY + i }, lineText.replaceAll(' ', LABEL_SPACE)) } } diff --git a/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts b/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts index 77c89e7b0..ebe805f3d 100644 --- a/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts +++ b/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts @@ -29,6 +29,9 @@ */ export const WIDE_PAD = '\u0000' +/** Opaque label-space placeholder that serializes back to a regular space. */ +export const LABEL_SPACE = '\u0001' + const graphemeSegmenter = new Intl.Segmenter() /** diff --git a/packages/utils/test/mermaid-ascii.test.ts b/packages/utils/test/mermaid-ascii.test.ts index ef9d44f7c..0a61922af 100644 --- a/packages/utils/test/mermaid-ascii.test.ts +++ b/packages/utils/test/mermaid-ascii.test.ts @@ -13,6 +13,24 @@ describe("renderMermaidAscii", () => { expect(rendered).not.toContain("──A─"); }); + it("masks routed lines behind spaces in edge labels", () => { + const horizontal = renderMermaidAscii( + ["graph LR", " A[Agent] -->|on Mac| B[Server]", " A -->|on Linux| C[Cluster]"].join("\n"), + { colorMode: "none", useAscii: false }, + ); + const vertical = renderMermaidAscii("graph TD\n A[Agent] -->|to c| C[Cluster]", { + colorMode: "none", + useAscii: false, + }); + + expect(horizontal).toContain("on Mac"); + expect(horizontal).toContain("on Linux"); + expect(horizontal).not.toContain("on─Mac"); + expect(horizontal).not.toContain("on─Linux"); + expect(vertical).toContain("to c"); + expect(vertical).not.toContain("to│c"); + }); + it("returns a bounded fallback for declaration orders that make a clean route unreachable", () => { const rendered = renderMermaidAsciiSafe( [