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
This commit is contained in:
@@ -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
|
||||
|
||||
+6
-6
@@ -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))
|
||||
|
||||
+2
-2
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user