mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Add Bun.wrapAnsi() for text wrapping with ANSI escape code preservation (#26061)
## Summary Adds `Bun.wrapAnsi()`, a native implementation of the popular [wrap-ansi](https://www.npmjs.com/package/wrap-ansi) npm package for wrapping text with ANSI escape codes. ## API ```typescript Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string interface WrapAnsiOptions { hard?: boolean; // default: false - Break words longer than columns wordWrap?: boolean; // default: true - Wrap at word boundaries trim?: boolean; // default: true - Trim leading/trailing whitespace ambiguousIsNarrow?: boolean; // default: true - Treat ambiguous-width chars as narrow } ``` ## Features - Wraps text to fit within specified column width - Preserves ANSI escape codes (SGR colors/styles) - Supports OSC 8 hyperlinks - Respects Unicode display widths (full-width characters, emoji) - Normalizes `\r\n` to `\n` ## Implementation Details The implementation closes and reopens ANSI codes around line breaks for robust terminal compatibility. This differs slightly from the npm package in edge cases but produces visually equivalent output. ### Behavioral Differences from npm wrap-ansi 1. **ANSI code preservation**: Bun always maintains complete ANSI escape sequences. The npm version can output malformed codes (missing ESC character) in certain edge cases with `wordWrap: false, trim: false`. 2. **Newline ANSI handling**: Bun closes and reopens ANSI codes around newlines for robustness. The npm version sometimes keeps them spanning across newlines. The visual output is equivalent. ## Tests - 27 custom tests covering basic functionality, ANSI codes, Unicode, and options - 23 tests ported from the npm package (MIT licensed, credited in file header) - All 50 tests pass ## Benchmark <!-- Benchmark results will be added --> ``` $ cd /Users/sosuke/code/bun/bench && ../build/release/bun snippets/wrap-ansi.js clk: ~3.82 GHz cpu: Apple M4 Max runtime: bun 1.3.7 (arm64-darwin) benchmark avg (min … max) p75 p99 (min … top 1%) -------------------------------------------- ------------------------------- Short text (45 chars) - npm 25.81 µs/iter 21.71 µs █ (16.79 µs … 447.38 µs) 110.96 µs ▆█▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Short text (45 chars) - Bun 685.55 ns/iter 667.00 ns █ (459.00 ns … 2.16 ms) 1.42 µs ▁▁▁█▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁ summary Short text (45 chars) - Bun 37.65x faster than Short text (45 chars) - npm -------------------------------------------- ------------------------------- Medium text (810 chars) - npm 568.12 µs/iter 578.00 µs ▄▅█▆▆▃ (525.25 µs … 944.71 µs) 700.75 µs ▄██████▆▅▄▃▃▂▂▂▁▁▁▁▁▁ Medium text (810 chars) - Bun 11.22 µs/iter 11.28 µs █ (11.04 µs … 11.46 µs) 11.33 µs █▁▁▁██▁█▁▁▁▁█▁█▁▁█▁▁█ summary Medium text (810 chars) - Bun 50.62x faster than Medium text (810 chars) - npm -------------------------------------------- ------------------------------- Long text (8100 chars) - npm 7.66 ms/iter 7.76 ms ▂▂▅█ ▅ (7.31 ms … 8.10 ms) 8.06 ms ▃▃▄▃█████▇▇███▃▆▆▆▄▁▃ Long text (8100 chars) - Bun 112.14 µs/iter 113.50 µs █ (102.50 µs … 146.04 µs) 124.92 µs ▁▁▁▁▁▁██▇▅█▃▂▂▂▂▁▁▁▁▁ summary Long text (8100 chars) - Bun 68.27x faster than Long text (8100 chars) - npm -------------------------------------------- ------------------------------- Colored short - npm 28.46 µs/iter 28.56 µs █ (27.90 µs … 29.34 µs) 28.93 µs ▆▁▆▁▁▆▁▁▆▆▆▁▆█▁▁▁▁▁▁▆ Colored short - Bun 861.64 ns/iter 867.54 ns ▂ ▇█▄▂ (839.68 ns … 891.12 ns) 882.04 ns ▃▅▄▅▆▆▇▆██▇████▆▃▅▅▅▂ summary Colored short - Bun 33.03x faster than Colored short - npm -------------------------------------------- ------------------------------- Colored medium - npm 557.84 µs/iter 562.63 µs ▂▃█▄ (508.08 µs … 911.92 µs) 637.96 µs ▁▁▁▂▄█████▅▂▂▁▁▁▁▁▁▁▁ Colored medium - Bun 14.91 µs/iter 14.94 µs ██ ████ ██ █ ██ (14.77 µs … 15.17 µs) 15.06 µs ██▁▁████▁██▁█▁▁▁▁▁▁██ summary Colored medium - Bun 37.41x faster than Colored medium - npm -------------------------------------------- ------------------------------- Colored long - npm 7.84 ms/iter 7.90 ms █ ▅ (7.53 ms … 8.38 ms) 8.19 ms ▂▂▂▄▃▆██▇██▇▃▂▃▃▃▄▆▂▂ Colored long - Bun 176.73 µs/iter 175.42 µs █ (162.50 µs … 1.37 ms) 204.46 µs ▁▁▂▄▇██▅▂▂▂▁▁▁▁▁▁▁▁▁▁ summary Colored long - Bun 44.37x faster than Colored long - npm -------------------------------------------- ------------------------------- Hard wrap long - npm 8.05 ms/iter 8.12 ms ▃ ▇█ (7.67 ms … 8.53 ms) 8.50 ms ▄▁▁▁▃▄█████▄▃▂▆▄▃▂▂▂▂ Hard wrap long - Bun 111.85 µs/iter 112.33 µs ▇█ (101.42 µs … 145.42 µs) 123.88 µs ▁▁▁▁▁▁▁████▄▃▂▂▂▁▁▁▁▁ summary Hard wrap long - Bun 72.01x faster than Hard wrap long - npm -------------------------------------------- ------------------------------- Hard wrap colored - npm 8.82 ms/iter 8.92 ms ▆ ██ (8.55 ms … 9.47 ms) 9.32 ms ▆▆████▆▆▄▆█▄▆▄▄▁▃▁▃▄▃ Hard wrap colored - Bun 174.38 µs/iter 175.54 µs █ ▂ (165.75 µs … 210.25 µs) 199.50 µs ▁▃█▆███▃▂▃▂▂▂▂▂▁▁▁▁▁▁ summary Hard wrap colored - Bun 50.56x faster than Hard wrap colored - npm -------------------------------------------- ------------------------------- Japanese (full-width) - npm 51.00 µs/iter 52.67 µs █▂ █▄ (40.71 µs … 344.88 µs) 66.13 µs ▁▁▃██▄▃▅██▇▄▃▄▃▂▂▁▁▁▁ Japanese (full-width) - Bun 7.46 µs/iter 7.46 µs █ (6.50 µs … 34.92 µs) 9.38 µs ▁▁▁▁▁██▆▂▁▂▁▁▁▁▁▁▁▁▁▁ summary Japanese (full-width) - Bun 6.84x faster than Japanese (full-width) - npm -------------------------------------------- ------------------------------- Emoji text - npm 173.63 µs/iter 222.17 µs █ (129.42 µs … 527.25 µs) 249.58 µs ▁▃█▆▃▃▃▁▁▁▁▁▁▁▂▄▆▄▂▂▁ Emoji text - Bun 9.42 µs/iter 9.47 µs ██ (9.32 µs … 9.52 µs) 9.50 µs █▁▁███▁▁█▁██▁▁▁▁██▁▁█ summary Emoji text - Bun 18.44x faster than Emoji text - npm -------------------------------------------- ------------------------------- Hyperlink (OSC 8) - npm 208.00 µs/iter 254.25 µs █ (169.58 µs … 542.17 µs) 281.00 µs ▁▇█▃▃▂▂▂▁▁▁▁▁▁▁▃▃▅▃▂▁ Hyperlink (OSC 8) - Bun 6.00 µs/iter 6.06 µs █ ▄ (5.88 µs … 6.11 µs) 6.10 µs ▅▅▅▁▅█▅▁▅▁█▁▁▅▅▅▅█▅▁█ summary Hyperlink (OSC 8) - Bun 34.69x faster than Hyperlink (OSC 8) - npm -------------------------------------------- ------------------------------- No trim long - npm 8.32 ms/iter 8.38 ms █▇ (7.61 ms … 13.67 ms) 11.74 ms ▃████▄▂▃▂▂▃▁▁▁▁▁▁▁▁▁▂ No trim long - Bun 93.92 µs/iter 94.42 µs █▂ (82.75 µs … 162.38 µs) 103.83 µs ▁▁▁▁▁▁▁▁▄███▄▃▂▂▁▁▁▁▁ summary No trim long - Bun 88.62x faster than No trim long - npm ``` --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tar": "^7.4.3",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"wrap-ansi": "^9.0.0",
|
||||
"zx": "^7.2.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -169,7 +170,7 @@
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.0.1", "", {}, "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||
|
||||
@@ -493,6 +494,8 @@
|
||||
|
||||
"which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yaml": ["yaml@2.3.4", "", {}, "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA=="],
|
||||
@@ -503,8 +506,6 @@
|
||||
|
||||
"@babel/highlight/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||
|
||||
"ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"avvio/fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
@@ -517,6 +518,10 @@
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
"@babel/highlight/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"@babel/highlight/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"@babel/highlight/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"string-width": "7.1.0",
|
||||
"wrap-ansi": "^9.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tar": "^7.4.3",
|
||||
"tinycolor2": "^1.6.0",
|
||||
|
||||
@@ -14,3 +14,4 @@ export function run(opts = {}) {
|
||||
|
||||
export const bench = Mitata.bench;
|
||||
export const group = Mitata.group;
|
||||
export const summary = Mitata.summary;
|
||||
|
||||
103
bench/snippets/wrap-ansi.js
Normal file
103
bench/snippets/wrap-ansi.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import wrapAnsi from "wrap-ansi";
|
||||
import { bench, run, summary } from "../runner.mjs";
|
||||
|
||||
// Test fixtures
|
||||
const shortText = "The quick brown fox jumped over the lazy dog.";
|
||||
const mediumText = "The quick brown fox jumped over the lazy dog and then ran away with the unicorn. ".repeat(10);
|
||||
const longText = "The quick brown fox jumped over the lazy dog and then ran away with the unicorn. ".repeat(100);
|
||||
|
||||
// ANSI colored text
|
||||
const red = s => `\u001B[31m${s}\u001B[39m`;
|
||||
const green = s => `\u001B[32m${s}\u001B[39m`;
|
||||
const blue = s => `\u001B[34m${s}\u001B[39m`;
|
||||
|
||||
const coloredShort = `The quick ${red("brown fox")} jumped over the ${green("lazy dog")}.`;
|
||||
const coloredMedium =
|
||||
`The quick ${red("brown fox jumped over")} the ${green("lazy dog and then ran away")} with the ${blue("unicorn")}. `.repeat(
|
||||
10,
|
||||
);
|
||||
const coloredLong =
|
||||
`The quick ${red("brown fox jumped over")} the ${green("lazy dog and then ran away")} with the ${blue("unicorn")}. `.repeat(
|
||||
100,
|
||||
);
|
||||
|
||||
// Full-width characters (Japanese)
|
||||
const japaneseText = "日本語のテキストを折り返すテストです。全角文字は幅2としてカウントされます。".repeat(5);
|
||||
|
||||
// Emoji text
|
||||
const emojiText = "Hello 👋 World 🌍! Let's test 🧪 some emoji 😀 wrapping 📦!".repeat(5);
|
||||
|
||||
// Hyperlink text
|
||||
const hyperlinkText = "Check out \u001B]8;;https://bun.sh\u0007Bun\u001B]8;;\u0007, it's fast! ".repeat(10);
|
||||
|
||||
// Options
|
||||
const hardOpts = { hard: true };
|
||||
const noTrimOpts = { trim: false };
|
||||
|
||||
// Basic text benchmarks
|
||||
summary(() => {
|
||||
bench("Short text (45 chars) - npm", () => wrapAnsi(shortText, 20));
|
||||
bench("Short text (45 chars) - Bun", () => Bun.wrapAnsi(shortText, 20));
|
||||
});
|
||||
|
||||
summary(() => {
|
||||
bench("Medium text (810 chars) - npm", () => wrapAnsi(mediumText, 40));
|
||||
bench("Medium text (810 chars) - Bun", () => Bun.wrapAnsi(mediumText, 40));
|
||||
});
|
||||
|
||||
summary(() => {
|
||||
bench("Long text (8100 chars) - npm", () => wrapAnsi(longText, 80));
|
||||
bench("Long text (8100 chars) - Bun", () => Bun.wrapAnsi(longText, 80));
|
||||
});
|
||||
|
||||
// ANSI colored text benchmarks
|
||||
summary(() => {
|
||||
bench("Colored short - npm", () => wrapAnsi(coloredShort, 20));
|
||||
bench("Colored short - Bun", () => Bun.wrapAnsi(coloredShort, 20));
|
||||
});
|
||||
|
||||
summary(() => {
|
||||
bench("Colored medium - npm", () => wrapAnsi(coloredMedium, 40));
|
||||
bench("Colored medium - Bun", () => Bun.wrapAnsi(coloredMedium, 40));
|
||||
});
|
||||
|
||||
summary(() => {
|
||||
bench("Colored long - npm", () => wrapAnsi(coloredLong, 80));
|
||||
bench("Colored long - Bun", () => Bun.wrapAnsi(coloredLong, 80));
|
||||
});
|
||||
|
||||
// Hard wrap benchmarks
|
||||
summary(() => {
|
||||
bench("Hard wrap long - npm", () => wrapAnsi(longText, 80, hardOpts));
|
||||
bench("Hard wrap long - Bun", () => Bun.wrapAnsi(longText, 80, hardOpts));
|
||||
});
|
||||
|
||||
summary(() => {
|
||||
bench("Hard wrap colored - npm", () => wrapAnsi(coloredLong, 80, hardOpts));
|
||||
bench("Hard wrap colored - Bun", () => Bun.wrapAnsi(coloredLong, 80, hardOpts));
|
||||
});
|
||||
|
||||
// Unicode benchmarks
|
||||
summary(() => {
|
||||
bench("Japanese (full-width) - npm", () => wrapAnsi(japaneseText, 40));
|
||||
bench("Japanese (full-width) - Bun", () => Bun.wrapAnsi(japaneseText, 40));
|
||||
});
|
||||
|
||||
summary(() => {
|
||||
bench("Emoji text - npm", () => wrapAnsi(emojiText, 30));
|
||||
bench("Emoji text - Bun", () => Bun.wrapAnsi(emojiText, 30));
|
||||
});
|
||||
|
||||
// Hyperlink benchmarks
|
||||
summary(() => {
|
||||
bench("Hyperlink (OSC 8) - npm", () => wrapAnsi(hyperlinkText, 40));
|
||||
bench("Hyperlink (OSC 8) - Bun", () => Bun.wrapAnsi(hyperlinkText, 40));
|
||||
});
|
||||
|
||||
// No trim option
|
||||
summary(() => {
|
||||
bench("No trim long - npm", () => wrapAnsi(longText, 80, noTrimOpts));
|
||||
bench("No trim long - Bun", () => Bun.wrapAnsi(longText, 80, noTrimOpts));
|
||||
});
|
||||
|
||||
await run();
|
||||
Reference in New Issue
Block a user