From 957d30f43a5b6c4959362d3d271149ab1c4b43d4 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Tue, 26 May 2026 13:19:50 +0800 Subject: [PATCH] feat: rewrite Output class with StyledChar[][] grid for correct emoji/wide char handling Port Ink's output character model using @alcalzone/ansi-tokenize. Each cell is now a StyledChar with proper grapheme clustering. Fixes emoji alignment, wide char boundaries, and changes clipping to top-only (matching Ink). Upgraded ansi-tokenize to ^0.3.0. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/background-color.test.tsx | 56 ++-- .../integration/components/borders.test.tsx | 30 +-- .../integration/paint/output-emoji.test.tsx | 49 ++++ packages/runtime/src/paint/paint.ts | 242 +++++++++++------- pnpm-lock.yaml | 22 +- pnpm-workspace.yaml | 2 +- 6 files changed, 251 insertions(+), 150 deletions(-) create mode 100644 packages/runtime-tests/integration/paint/output-emoji.test.tsx diff --git a/packages/runtime-tests/integration/components/background-color.test.tsx b/packages/runtime-tests/integration/components/background-color.test.tsx index 6c3786e..fd3133b 100644 --- a/packages/runtime-tests/integration/components/background-color.test.tsx +++ b/packages/runtime-tests/integration/components/background-color.test.tsx @@ -63,7 +63,7 @@ test("Text inherits parent Box background color", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot(`"Hello World"`); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello World"`); }); test("Text explicit background color overrides inherited", async () => { @@ -75,7 +75,7 @@ test("Text explicit background color overrides inherited", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot(`"Hello World"`); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello World"`); }); test("Nested Box background inheritance", async () => { @@ -89,7 +89,7 @@ test("Nested Box background inheritance", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot(`"Hello World"`); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello World"`); }); test("Text without parent Box background has no inheritance", async () => { @@ -114,9 +114,7 @@ test("Multiple Text elements inherit same background", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot( - `"Hello World"`, - ); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello World"`); }); test("Mixed text with and without background inheritance", async () => { @@ -130,9 +128,7 @@ test("Mixed text with and without background inheritance", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot( - `"Inherited No BG Red BG"`, - ); + expect(lastFrame()).toMatchInlineSnapshot(`"Inherited No BG Red BG"`); }); test("Complex nested structure with background inheritance", async () => { @@ -150,9 +146,7 @@ test("Complex nested structure with background inheritance", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot( - `"Outer: Inner: Explicit"`, - ); + expect(lastFrame()).toMatchInlineSnapshot(`"Outer: Inner: Explicit"`); }); test("Box background with standard color", async () => { @@ -164,7 +158,7 @@ test("Box background with standard color", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); }); test("Box background with hex color", async () => { @@ -176,9 +170,7 @@ test("Box background with hex color", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot( - `"Hello"`, - ); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); }); test("Box background with rgb color", async () => { @@ -190,9 +182,7 @@ test("Box background with rgb color", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot( - `"Hello"`, - ); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); }); test("Box background with ansi256 color", async () => { @@ -204,7 +194,7 @@ test("Box background with ansi256 color", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); }); test("Box background with wide characters", async () => { @@ -216,7 +206,7 @@ test("Box background with wide characters", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot(`"こんにちは"`); + expect(lastFrame()).toMatchInlineSnapshot(`"こんにちは"`); }); test("Box background with emojis", async () => { @@ -228,7 +218,7 @@ test("Box background with emojis", async () => { )), { columns: 100 }, ); - expect(lastFrame()).toMatchInlineSnapshot(`"🎉🎊"`); + expect(lastFrame()).toMatchInlineSnapshot(`"🎉🎊"`); }); test("Box background fills entire area with standard color", async () => { @@ -241,7 +231,7 @@ test("Box background fills entire area with standard color", async () => { { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - "Hello  + "Hello     " `); @@ -312,9 +302,9 @@ test("Box background with border fills content area", async () => { ); expect(lastFrame()).toMatchInlineSnapshot(` "╭────────╮ - │Hi │ - │ │ - │ │ + │Hi │ + │ │ + │ │ ╰────────╯" `); }); @@ -330,7 +320,7 @@ test("Box background with padding fills entire padded area", async () => { ); expect(lastFrame()).toMatchInlineSnapshot(` "  -  Hi  +  Hi       " @@ -353,7 +343,7 @@ test("Box background with center alignment fills entire area", async () => { { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - " Hi  + " Hi     " `); @@ -376,8 +366,8 @@ test("Box background with column layout fills entire area", async () => { { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - "Line 1  - Line 2  + "Line 1  + Line 2       " @@ -400,7 +390,7 @@ test("Box background updates on rerender", async () => { bgColor.value = "green"; await nextTick(); - expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); + expect(lastFrame()).toMatchInlineSnapshot(`"Hello"`); bgColor.value = undefined; await nextTick(); @@ -417,8 +407,8 @@ test("Box backgroundColor fills full width on every line when text wraps", async { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - "Hello    - World!! " + "Hello  + World!! " `); }); diff --git a/packages/runtime-tests/integration/components/borders.test.tsx b/packages/runtime-tests/integration/components/borders.test.tsx index dd32ac5..1a01faf 100644 --- a/packages/runtime-tests/integration/components/borders.test.tsx +++ b/packages/runtime-tests/integration/components/borders.test.tsx @@ -31,9 +31,9 @@ test("single node - full width box with colorful border", async () => { { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ - │Hello World │ - ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `); }); @@ -100,7 +100,7 @@ test("single node - fit-content box with variation selector emojis", async () => ); expect(lastFrame()).toMatchInlineSnapshot(` "╭──────╮ - │🌡️⚠️✅ + │🌡️⚠️✅│ ╰──────╯" `); }); @@ -272,9 +272,9 @@ test("multiple nodes - full width box with colorful border", async () => { { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ - │Hello World │ - ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `); }); @@ -636,9 +636,9 @@ test("render border after update", async () => { borderColor.value = "green"; await nextTick(); expect(lastFrame()).toMatchInlineSnapshot(` - "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ - │Hello World │ - ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Hello World │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `); borderColor.value = undefined; @@ -947,9 +947,9 @@ test("custom border style", async () => { { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - "↘↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ + "↘↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↙ →Content ← - ↗↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" + ↗↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↖" `); }); @@ -964,9 +964,9 @@ test("dim border color", async () => { { columns: 100 }, ); expect(lastFrame()).toMatchInlineSnapshot(` - "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ - │Content │ - ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" + "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + │Content │ + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `); }); diff --git a/packages/runtime-tests/integration/paint/output-emoji.test.tsx b/packages/runtime-tests/integration/paint/output-emoji.test.tsx new file mode 100644 index 0000000..de67610 --- /dev/null +++ b/packages/runtime-tests/integration/paint/output-emoji.test.tsx @@ -0,0 +1,49 @@ +import { defineComponent } from "vue"; +import { describe, test, expect } from "vite-plus/test"; +import { render } from "@vue-tui/testing"; +import { Box, Text } from "@vue-tui/runtime"; + +describe("Output character model", () => { + test("variation selector emoji characters align correctly within borders", async () => { + const App = defineComponent(() => () => ( + + 🌡️⚠️✅ + + )); + const { lastFrame } = await render(App, { columns: 100 }); + const frame = lastFrame()!; + const lines = frame.split("\n"); + // The text line (line 1) must have a closing right border "│" + // With the old placeLine implementation, variation selector emojis + // miscount visual width and the right border is lost. + expect(lines[1]).toContain("│"); + expect(lines[1]!.endsWith("│")).toBe(true); + }); + + test("wide CJK characters occupy two cells", async () => { + const App = defineComponent(() => () => ( + + 你好世 + + )); + const { lastFrame } = await render(App, { columns: 100 }); + const frame = lastFrame()!; + const lines = frame.split("\n"); + expect(lines[1]!.endsWith("│")).toBe(true); + }); + + test("simple emoji characters align within borders", async () => { + const App = defineComponent(() => () => ( + + 🦾🌏😋 + + )); + const { lastFrame } = await render(App, { columns: 100 }); + const frame = lastFrame()!; + const lines = frame.split("\n"); + // All lines must end with the right border character + expect(lines[0]!.endsWith("╮")).toBe(true); + expect(lines[1]!.endsWith("│")).toBe(true); + expect(lines[2]!.endsWith("╯")).toBe(true); + }); +}); diff --git a/packages/runtime/src/paint/paint.ts b/packages/runtime/src/paint/paint.ts index e800ad6..af36e42 100644 --- a/packages/runtime/src/paint/paint.ts +++ b/packages/runtime/src/paint/paint.ts @@ -1,6 +1,12 @@ import stringWidth from "string-width"; import sliceAnsi from "slice-ansi"; import cliBoxes from "cli-boxes"; +import { + type StyledChar, + styledCharsFromTokens, + styledCharsToString, + tokenize, +} from "@alcalzone/ansi-tokenize"; import { applyChalk } from "./text-style.ts"; import Yoga from "yoga-layout"; import type { @@ -43,10 +49,48 @@ interface UnclipOp { type Op = WriteOp | ClipOp | UnclipOp; +class OutputCaches { + private widths = new Map(); + private blockWidths = new Map(); + private styledCharsCache = new Map(); + + getStyledChars(line: string): StyledChar[] { + let cached = this.styledCharsCache.get(line); + if (cached === undefined) { + cached = styledCharsFromTokens(tokenize(line)); + this.styledCharsCache.set(line, cached); + } + return cached; + } + + getStringWidth(text: string): number { + let cached = this.widths.get(text); + if (cached === undefined) { + cached = stringWidth(text); + this.widths.set(text, cached); + } + return cached; + } + + getWidestLine(text: string): number { + let cached = this.blockWidths.get(text); + if (cached === undefined) { + let lineWidth = 0; + for (const line of text.split("\n")) { + lineWidth = Math.max(lineWidth, this.getStringWidth(line)); + } + cached = lineWidth; + this.blockWidths.set(text, cached); + } + return cached; + } +} + class Output { readonly width: number; readonly height: number; private ops: Op[] = []; + private readonly caches: OutputCaches = new OutputCaches(); constructor(width: number, height: number) { this.width = width; @@ -65,11 +109,17 @@ class Output { this.ops.push({ type: "unclip" }); } - get(): string { - // Build a sparse grid of cells, write each op left-to-right. - const grid: string[][] = Array.from({ length: this.height }, () => - Array.from({ length: this.width }, () => " "), - ); + get(): { output: string; height: number } { + // Initialize output grid with StyledChar cells + const output: StyledChar[][] = []; + for (let y = 0; y < this.height; y++) { + const row: StyledChar[] = []; + for (let x = 0; x < this.width; x++) { + row.push({ type: "char", value: " ", fullWidth: false, styles: [] }); + } + output.push(row); + } + const clips: ClipRect[] = []; for (const op of this.ops) { @@ -83,48 +133,39 @@ class Output { } // op.type === "write" + const { transformers } = op; let { x, y } = op; let lines = op.lines; - // Apply transformers - lines = lines.map((line, idx) => { - let l = line; - for (const tf of op.transformers) l = tf(l, idx); - return l; - }); - - // Apply active clip rect — intersect ALL clips in the stack so nested - // overflow:hidden boxes correctly constrain children. - if (clips.length > 0) { - const clip = clips.reduce( - (acc, c) => ({ - x1: acc.x1 != null && c.x1 != null ? Math.max(acc.x1, c.x1) : (acc.x1 ?? c.x1), - x2: acc.x2 != null && c.x2 != null ? Math.min(acc.x2, c.x2) : (acc.x2 ?? c.x2), - y1: acc.y1 != null && c.y1 != null ? Math.max(acc.y1, c.y1) : (acc.y1 ?? c.y1), - y2: acc.y2 != null && c.y2 != null ? Math.min(acc.y2, c.y2) : (acc.y2 ?? c.y2), - }), - { x1: undefined, x2: undefined, y1: undefined, y2: undefined }, - ); + // Only the most recent clip applies (top-only, matching Ink behavior) + const clip = clips.at(-1); + if (clip) { const clipH = typeof clip.x1 === "number" && typeof clip.x2 === "number"; const clipV = typeof clip.y1 === "number" && typeof clip.y2 === "number"; - // If the intersection is empty, skip the write entirely - if (clipH && clip.x1! >= clip.x2!) continue; - if (clipV && clip.y1! >= clip.y2!) continue; + // If text is positioned outside of clipping area altogether, skip + if (clipH) { + const text = lines.join("\n"); + const width = this.caches.getWidestLine(text); + if (x + width < clip.x1! || x > clip.x2!) continue; + } - // Skip entirely out-of-bounds writes if (clipV) { const height = lines.length; - if (y + height <= clip.y1! || y >= clip.y2!) continue; - } - if (clipH) { - // Quick check: if write is entirely to the right of clip or entirely - // to the left, skip it - if (x >= clip.x2!) continue; + if (y + height < clip.y1! || y > clip.y2!) continue; + } + + if (clipH) { + lines = lines.map((line) => { + const from = x < clip.x1! ? clip.x1! - x : 0; + const lineWidth = this.caches.getStringWidth(line); + const to = x + lineWidth > clip.x2! ? clip.x2! - x : lineWidth; + return sliceAnsi(line, from, to); + }); + if (x < clip.x1!) x = clip.x1!; } - // Vertical clipping: slice lines array if (clipV) { const from = y < clip.y1! ? clip.y1! - y : 0; const height = lines.length; @@ -132,61 +173,88 @@ class Output { lines = lines.slice(from, to); if (y < clip.y1!) y = clip.y1!; } + } - // Horizontal clipping: slice each line - if (clipH) { - lines = lines.map((line) => { - const lineWidth = stringWidth(line); - const from = x < clip.x1! ? clip.x1! - x : 0; - const to = x + lineWidth > clip.x2! ? clip.x2! - x : lineWidth; - return sliceAnsi(line, from, to); - }); - if (x < clip.x1!) x = clip.x1!; + let offsetY = 0; + + for (let [index, line] of lines.entries()) { + const currentLine = output[y + offsetY]; + + // Line can be missing if text is taller than pre-initialized output + if (!currentLine) { + continue; } - } - for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { - placeLine(grid, x, y + lineIdx, lines[lineIdx]!); - } - } - return grid.map((row) => row.join("").trimEnd()).join("\n"); - } -} + for (const transformer of transformers) { + line = transformer(line, index); + } -function placeLine(grid: string[][], x: number, y: number, line: string): void { - if (y < 0 || y >= grid.length) return; - const row = grid[y]!; - // We walk the line as visual cells. ANSI sequences are kept attached to the - // cell that follows them; emoji/wide chars consume two cells. - let col = x; - let i = 0; - let pendingAnsi = ""; - while (i < line.length && col < row.length) { - const ch = line[i]!; - // ANSI CSI sequence: ESC[...m — accumulate as a prefix for the next cell. - if (ch === "\x1b" && line[i + 1] === "[") { - const end = line.indexOf("m", i); - if (end >= 0) { - pendingAnsi += line.slice(i, end + 1); - i = end + 1; - continue; + const characters = this.caches.getStyledChars(line); + let offsetX = x; + + // Nothing to write (e.g. line was clipped away) + if (characters.length === 0) { + offsetY++; + continue; + } + + const spaceCell: StyledChar = { + type: "char", + value: " ", + fullWidth: false, + styles: [], + }; + + // Wide characters (e.g. CJK) occupy two cells: a leading cell with + // the character and a trailing placeholder with value ''. When an + // overlapping write lands in the middle of a wide character, the + // boundary cells need cleanup so the terminal never renders a + // half-visible wide character. + if ( + currentLine[offsetX]?.value === "" && + offsetX > 0 && + this.caches.getStringWidth(currentLine[offsetX - 1]?.value ?? "") > 1 + ) { + currentLine[offsetX - 1] = spaceCell; + } + + for (const character of characters) { + currentLine[offsetX] = character; + + // Determine printed width using string-width to align with measurement + const characterWidth = Math.max(1, this.caches.getStringWidth(character.value)); + + // For multi-column characters, clear following cells to avoid stray spaces/artifacts + if (characterWidth > 1) { + for (let i = 1; i < characterWidth; i++) { + currentLine[offsetX + i] = { + type: "char", + value: "", + fullWidth: false, + styles: character.styles, + }; + } + } + + offsetX += characterWidth; + } + + if (currentLine[offsetX]?.value === "") { + currentLine[offsetX] = spaceCell; + } + + offsetY++; } } - const segment = ch; - const w = stringWidth(segment); - if (col >= 0 && col < row.length) { - row[col] = pendingAnsi + segment; - pendingAnsi = ""; - } - if (w === 2 && col + 1 < row.length) row[col + 1] = ""; - col += Math.max(1, w); - i++; - } - // If the line ended with a trailing escape (e.g. reset), attach it to the - // last written cell so it's not lost. - if (pendingAnsi && col > x) { - const lastCol = Math.min(col - 1, row.length - 1); - if (lastCol >= 0) row[lastCol] = (row[lastCol] ?? "") + pendingAnsi; + + const generatedOutput = output + .map((line) => { + const lineWithoutEmptyItems = line.filter((item) => item !== undefined); + return styledCharsToString(lineWithoutEmptyItems).trimEnd(); + }) + .join("\n"); + + return { output: generatedOutput, height: output.length }; } } @@ -304,9 +372,9 @@ export function paint(root: TuiNode): string { const layout = root.yoga.getComputedLayout(); const width = Math.max(1, Math.floor(layout.width)); const height = Math.max(1, Math.floor(layout.height)); - const output = new Output(width, height); - paintNode(root, output, 0, 0, []); - return output.get(); + const out = new Output(width, height); + paintNode(root, out, 0, 0, []); + return out.get().output; } function paintNode( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 910bd76..bcb81d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: '@alcalzone/ansi-tokenize': - specifier: ^0.1.3 - version: 0.1.3 + specifier: ^0.3.0 + version: 0.3.0 '@types/node': specifier: ^24.12.4 version: 24.12.4 @@ -145,7 +145,7 @@ importers: dependencies: '@alcalzone/ansi-tokenize': specifier: 'catalog:' - version: 0.1.3 + version: 0.3.0 '@vue/runtime-core': specifier: ^3.4.0 version: 3.5.34 @@ -241,9 +241,9 @@ importers: packages: - '@alcalzone/ansi-tokenize@0.1.3': - resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==} - engines: {node: '>=14.13.1'} + '@alcalzone/ansi-tokenize@0.3.0': + resolution: {integrity: sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==} + engines: {node: '>=18'} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -1354,10 +1354,6 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} @@ -1762,10 +1758,10 @@ packages: snapshots: - '@alcalzone/ansi-tokenize@0.1.3': + '@alcalzone/ansi-tokenize@0.3.0': dependencies: ansi-styles: 6.2.3 - is-fullwidth-code-point: 4.0.0 + is-fullwidth-code-point: 5.1.0 '@babel/code-frame@7.29.0': dependencies: @@ -2741,8 +2737,6 @@ snapshots: dependencies: ms: 2.1.3 - is-fullwidth-code-point@4.0.0: {} - is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.6.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6292678..61217bc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,7 +7,7 @@ packages: catalogMode: prefer catalog: - "@alcalzone/ansi-tokenize": ^0.1.3 + "@alcalzone/ansi-tokenize": ^0.3.0 "@types/node": ^24.12.4 typescript: ^5 vite: npm:@voidzero-dev/vite-plus-core@latest