fix(runtime): border edge SGR nests dim outermost, matching Ink (#97)
A border edge combining dim with a foreground and/or background color emitted a
different SGR nesting order than Ink (vue: dim innermost via the shared Text
applyChalk; Ink: dim OUTERMOST). Visually identical, but the byte stream diverged.
Give colorizeEdge its own stylePiece ordering matching render-border.ts (fg, then
bg, then chalk.dim last/outermost) instead of routing border edges through
applyChalk — whose dim-innermost order is correct for <Text> and is left unchanged.
colorizeEdge is the single shared path for all four edges.
Exact-byte tests lock the top edge, a side rail, and the fg+bg (no-dim) subset;
the previously-lax `.toContain('[31m')` tests (duplicated across borders and
background-color) are upgraded.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -592,8 +592,12 @@ test("foreground, background and dim combine correctly", async ({ expect }) => {
|
||||
{ columns: 100 },
|
||||
);
|
||||
const output = lastFrame()!;
|
||||
// red FG (31), cyan BG (46), dim (2)
|
||||
expect(output).toContain("[31m");
|
||||
expect(output).toContain("[46m");
|
||||
expect(output).toContain("[2m");
|
||||
// EXACT-byte parity with Ink's render-border.ts stylePiece (commit 40b3a75,
|
||||
// lines 7-20): fg innermost, then bg, then dim outermost. Open codes nest
|
||||
// dim(2) -> bg(46) -> fg(31); close fg(39) -> bg(49) -> dim(22). The lax
|
||||
// `.toContain('[31m')` form this replaces would pass even with the wrong
|
||||
// Text-style (dim-innermost) nesting.
|
||||
const topLine = output.split("\n")[0]!;
|
||||
expect(topLine).toContain("\x1b[2m\x1b[46m\x1b[31m");
|
||||
expect(topLine).toContain("\x1b[39m\x1b[49m\x1b[22m");
|
||||
});
|
||||
|
||||
@@ -1199,10 +1199,68 @@ test("foreground, background and dim combine correctly", async ({ expect }) => {
|
||||
{ columns: 100 },
|
||||
);
|
||||
const frame = lastFrame()!;
|
||||
// red FG=31, cyan BG=46, dim=2
|
||||
expect(frame).toContain("[31m");
|
||||
expect(frame).toContain("[46m");
|
||||
expect(frame).toContain("[2m");
|
||||
// EXACT-byte parity with Ink's render-border.ts stylePiece (commit 40b3a75,
|
||||
// lines 7-20): fg INNERMOST, then bg, then dim OUTERMOST —
|
||||
// chalk.dim(chalk.bgCyan(chalk.red(glyphs))). With chalk level 3 the open
|
||||
// codes nest dim(2) -> bg(46) -> fg(31) and close fg(39) -> bg(49) -> dim(22).
|
||||
// A lax `.toContain('[31m')` etc. would pass even with the wrong (Text-style,
|
||||
// dim-innermost) nesting, so assert the precise byte windows.
|
||||
const topLine = frame.split("\n")[0]!;
|
||||
expect(topLine).toContain("\x1b[2m\x1b[46m\x1b[31m");
|
||||
expect(topLine).toContain("\x1b[39m\x1b[49m\x1b[22m");
|
||||
});
|
||||
|
||||
test("border side rails: foreground, background and dim combine in Ink byte order", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// Exercises a vertical edge (left rail) — colorizeEdge is the single shared
|
||||
// path for all four edges, so this guards the same SGR ordering on the rails.
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box
|
||||
borderLeftDimColor
|
||||
borderStyle="single"
|
||||
borderLeftColor="red"
|
||||
borderLeftBackgroundColor="cyan"
|
||||
alignSelf="flex-start"
|
||||
>
|
||||
<Text>Hi</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
const frame = lastFrame()!;
|
||||
// The left rail glyph │ lives on the content row (row index 1). Same Ink
|
||||
// stylePiece order as the top edge: dim(2) -> bg(46) -> fg(31), close
|
||||
// fg(39) -> bg(49) -> dim(22).
|
||||
const railRow = frame.split("\n")[1]!;
|
||||
expect(railRow).toContain("\x1b[2m\x1b[46m\x1b[31m");
|
||||
expect(railRow).toContain("\x1b[39m\x1b[49m\x1b[22m");
|
||||
});
|
||||
|
||||
test("border foreground + background (no dim) nests bg outer, fg inner like Ink", async ({
|
||||
expect,
|
||||
}) => {
|
||||
// The no-dim subset must also match Ink's stylePiece: colorize(colorize(glyph,
|
||||
// fg,'foreground'), bg,'background') => bg(46) outer, fg(31) inner, no dim wrap.
|
||||
const { lastFrame } = await render(
|
||||
defineComponent(() => () => (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderTopColor="red"
|
||||
borderTopBackgroundColor="cyan"
|
||||
alignSelf="flex-start"
|
||||
>
|
||||
<Text>Hi</Text>
|
||||
</Box>
|
||||
)),
|
||||
{ columns: 100 },
|
||||
);
|
||||
const topLine = lastFrame()!.split("\n")[0]!;
|
||||
expect(topLine).toContain("\x1b[46m\x1b[31m");
|
||||
expect(topLine).toContain("\x1b[39m\x1b[49m");
|
||||
// and NO dim code on this edge
|
||||
expect(topLine).not.toContain("\x1b[2m");
|
||||
});
|
||||
|
||||
// G05 — height-1 box with side-only borders renders rails (Ink parity)
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
styledCharsToString,
|
||||
tokenize,
|
||||
} from "@alcalzone/ansi-tokenize";
|
||||
import { applyChalk } from "./text-style.ts";
|
||||
import chalk from "chalk";
|
||||
import { applyChalk, applyColor } from "./text-style.ts";
|
||||
import { sanitizeAnsi } from "./sanitize-ansi.ts";
|
||||
import Yoga from "yoga-layout";
|
||||
import type {
|
||||
@@ -494,11 +495,18 @@ function drawBorder(
|
||||
const edgeBg =
|
||||
(props[`border${capEdge}BackgroundColor`] as string | undefined) ??
|
||||
(props["borderBackgroundColor"] as string | undefined);
|
||||
const p: TextProps = {};
|
||||
if (edgeColor) p.color = edgeColor;
|
||||
if (edgeBg) p.backgroundColor = edgeBg;
|
||||
if (edgeDim) p.dimColor = true;
|
||||
return Object.keys(p).length > 0 ? applyChalk(s, p) : s;
|
||||
// Border SGR nesting deliberately differs from <Text>'s. Mirror Ink's
|
||||
// render-border.ts stylePiece (commit 40b3a75, lines 7-20) EXACTLY:
|
||||
// foreground innermost, then background, then `chalk.dim` OUTERMOST —
|
||||
// i.e. chalk.dim(bg(fg(glyphs))). This is NOT applyChalk's order, whose
|
||||
// dim-innermost nesting is correct for <Text> (Ink Text.tsx) and must
|
||||
// stay unchanged. Routing edges through applyChalk would emit the bytes
|
||||
// in the wrong order (bg, fg, dim) versus Ink's (dim, bg, fg).
|
||||
let styled = s;
|
||||
if (edgeColor) styled = applyColor(chalk, edgeColor as never, false)(styled);
|
||||
if (edgeBg) styled = applyColor(chalk, edgeBg as never, true)(styled);
|
||||
if (edgeDim) styled = chalk.dim(styled);
|
||||
return styled;
|
||||
}
|
||||
|
||||
if (top) {
|
||||
|
||||
Reference in New Issue
Block a user