sindresorhus

string-width

Get the visual width of a string - the number of columns required to display it

by sindresorhus

JavaScript

JavaScript98.7%
TypeScript1.3%

Commit activity

Last 12 weeks · 3 commits

Full graph →

Community health

4 of 6 standards met

Community profile →
85
✓README✓License✓Contributing✓Code of Conduct○Issue Template○PR Template

Recent PRs & issues

Active · 1 in progress · Last activity 3w ago
See all on GitHub →
Zelys-DFKH

Fixes #62 Replaces with the built-in , following up on the earlier attempts in #61 by @RobinTail and #73 by @dulmandakh, which were blocked because mishandled OSC 8 hyperlinks at the time. That was fixed in nodejs/node#54865 and shipped in Node.js 22.10.0, so engines is bumped to rather than plain to keep v22.0 through v22.9 from silently getting the old behavior. Node.js 20 is dropped from the CI matrix to match. I checked the output of both functions against CSI sequences and OSC 8 hyperlinks (BEL and ST terminated) and they match exactly, and the existing hyperlink test in test.js covers the regression case. All 224 tests pass. Happy to adjust the engines range or split out the CI matrix change if you'd prefer to handle those separately.

Zelys-DFKH · 3mo ago
ranihorev

What changed and why has a fast path for strings that are entirely printable ASCII, but everything else goes through and, per cluster, the zero-width regex, the regex, the Hangul jamo scan and the East Asian Width lookup. Profiling a workload of CLI table cells and log lines on Node 22 () put 60% of self time in that segmenter loop and another 14% in the RGI emoji regex, almost all of it spent on clusters that are a single ordinary character. A 73 character ASCII line took 0.14 us, the same line with one trailing emoji 75 us, a 23 character CJK cell 47 us and a line of accented French prose 51 us. The branch removes that work in four steps, each its own commit: 1. Inside segmented text, return early for a cluster that is a single printable ASCII character instead of running the cluster checks on it. 2. Count runs of printable ASCII directly and hand only the text around other characters to the segmenter. Two printable ASCII characters never share a grapheme cluster, so every character of a run except the first and last is a cluster of width 1 on its own; the edge characters stay with the segmenter because they may join a neighbour (combining marks, ZWJ, keycaps, prepends). Runs in the middle of a string are only skipped when they are at least 16 characters long, because a fresh call costs about as much as classifying a dozen clusters. The existing whole-string ASCII check is kept, so pure ASCII input takes exactly the same path as before. 3. Cache the width of single code point clusters per ambiguous width option, in a bounded map that is cleared when full, since the same ideographs, accented letters and Hangul syllables repeat constantly. 4. Extend the run fast path from printable ASCII to every character that provably never joins a neighbouring cluster. Under UAX #29 a boundary between two characters can only be suppressed by an Extend (marks, , ), ZWJ, SpacingMark, Prepend, Regional_Indicator, Hangul jamo (GCB L, V, T), Control, CR, LF or an Indic conjunct consonant or linker (GB9c). Between two characters from a set that contains none of those, GB999 always applies. characters qualify because GB11 needs a ZWJ, and precomposed Hangul syllables qualify because GB6 to GB8 need a jamo. The set is given as explicit ranges (printable ASCII, Latin-1 and Latin Extended, IPA, Greek, Cyrillic, general punctuation, currency, letterlike symbols, arrows, math, box drawing, shapes, dingbats, CJK symbols, kana, Bopomofo, Hangul compatibility jamo, enclosed CJK, CJK Unified Ideographs and all extensions, Hangul syllables, CJK compatibility ideographs, fullwidth and halfwidth forms), with the holes for combining marks, the soft hyphen, the Hangul filler, format characters and the halfwidth voiced sound marks carved out. A run's width is the sum of its code points' cached widths; the first and last character of a run stay with the segmenter exactly as in step 2. A test enumerates all 118,272 code points of the ranges and asserts, with escapes from the same ICU data the segmenter uses, that none is a mark, , , , , Control (Cc, Cf, Cs, Zl, Zp, default ignorable), Hangul jamo, a code point, or from a script with Prepend or Indic conjunct characters, and that every one is Latin, Greek, Coptic, Cyrillic, Han, Hiragana, Katakana, Hangul, Bopomofo, Braille, Common or unassigned. The same test then asks itself to confirm that each code point does not merge with ASCII, an character, a Hangul syllable, a CJK ideograph or a copy of itself, so a future Unicode version that assigned a joining character inside one of the ranges would fail CI rather than change results. The ranges are exported as so the test can enumerate them, following the private underscore export that uses for the same purpose. No API change otherwise, no new dependencies. Results are unchanged: the existing suite passes and a differential check of 26,267 inputs (see test plan) gives byte-identical output against the base commit. Benchmark Node 22.22.2, Linux x64, run alone on an otherwise idle 4 core box. Each cell is the median over 7 interleaved runs of a fresh process per side, each run taking the best of 7 timed repeats. The first six rows are the average per call over 200 generated strings of that kind; the rest are single strings or small groups. The two rows that still go through the segmenter for every cluster are untouched by the branch: 100 flags 273.0 us to 283.9 us (0.96x) and 100 decomposed Hangul syllables 215.4 us to 210.8 us (1.02x); they pay one class regex scan per string and one cache lookup attempt per cluster. Pure ASCII and ANSI rows run byte-identical code before and after and are within 5%. To reproduce: check out the base commit and this branch side by side, in both, and time on the same inputs from each checkout in separate processes (best of 7 repeats per case, median over 7 runs). The harness used here is in the notes directory: it imports from each worktree and prints the table above. Test plan (runs ): passes on the branch with 285 tests (229 existing plus 56 new). New tests cover the run boundaries for ASCII (emoji, combining marks, ZWJ, prepends, keycaps, CJK, ambiguous width, tabs and ANSI codes at the start, middle and end of long runs), the same boundaries for the wider class (accented letters next to combining marks, CJK next to variation selectors and emoji modifiers, precomposed syllables next to jamo, halfwidth kana next to voiced sound marks, hiragana next to combining dakuten, Cyrillic and Greek next to their combining marks, soft hyphen and Hangul filler inside runs, CJK Extension B runs next to marks and inside surrogate-pair edges, prepend and ZWJ and flags next to CJK runs, mixed script prose, box drawing rows, both ambiguous width settings), cache eviction (3,000 distinct ideographs and the Greek capitals checked twice through both the segmenter path and the run path, with both option values), the enumeration of the standalone ranges described above, and a structural check that the ranges are sorted, disjoint and exclude the known special cases. and report no issues. on the base commit: passes with 229 tests. Differential check against the base commit: 26,267 inputs, 26,267 identical, 0 differ. Outputs are compared as serialized JSON (the returned number, or error class plus message). Corpus: the 279 inline cases harvested from plus 8 non-string inputs; 6,000 seeded fuzz strings (up to 30 pieces each) built from a 333 piece alphabet covering printable ASCII, control characters, ANSI CSI and OSC sequences and lone ESC and CSI bytes, Latin-1 and ambiguous width characters, Latin Extended, IPA, Greek, Cyrillic and their combining marks, combining and enclosing marks, spacing marks and Indic conjuncts, format and default ignorable characters, separators, variation selectors, emoji and pieces of ZWJ, keycap, skin tone, flag and tag sequences, emoji modifiers, CJK, kana with combining and spacing dakuten, halfwidth kana and voiced marks, fullwidth forms, fillers, precomposed and decomposed Hangul including extended jamo, CJK Extensions B, G and H and compatibility ideographs, lone surrogates, box drawing and punctuation, the first and last code point of every standalone range and the code points just outside them, and runs of ASCII, CJK and accented letters of lengths 15 to 18 around the run threshold; and 19,980 structured cases placing each alphabet piece at the start, middle and end of ASCII, CJK or accented runs of lengths 0 to 33. Every case uses one of the four combinations of and . No inputs were excluded. An earlier revision of the last commit stepped one code unit instead of one code point at run edges, which the fuzz corpus caught on CJK Extension B characters (138 differences); the fix and regression tests are in the commit. 🤖 Generated with Claude Code https://claude.ai/code/session_015B7oH2ZYikMUumRhNmMQmE _Generated by Claude Code_

ranihorev · 1mo ago
TheAlexLichter

At the moment, spacing marks () characters are considered zero-width while they are combining but spacing and occupying horizontal space. They should not be considered zero-width given their nature. Related: https://github.com/sindresorhus/string-width/issues/6, https://github.com/prettier/prettier/issues/19535, https://github.com/oxc-project/oxc/issues/23863#issuecomment-4881454798

TheAlexLichter · 3mo ago
TheAlexLichter

Fixes https://github.com/sindresorhus/string-width/issues/79 by handling spacing mark as 1-width (and not zero)

TheAlexLichter · 3mo ago
Structured data for AI agents

Repository: sindresorhus/string-width. Description: Get the visual width of a string - the number of columns required to display it Stars: 530, Forks: 44. Primary language: JavaScript. Languages: JavaScript (98.7%), TypeScript (1.3%). License: MIT. Latest release: v8.3.0 (3w ago). Open PRs: 1, open issues: 1. Last activity: 3w ago. Community health: 85%. Top contributors: sindresorhus, fisker, coreyfarrell, BendingBender, adam2k, TheAlexLichter, privatenumber, LitoMore, Richienb, seachicken and others.