Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import {eastAsianWidth} from 'get-east-asian-width';
Logic:
- Segment graphemes to match how terminals render clusters.
- Width rules:
1. Skip non-printing clusters (Default_Ignorable, Control, pure Mark, lone Surrogates). Tabs are ignored by design.
1. Skip non-printing clusters (Default_Ignorable, Control, pure nonspacing/enclosing Mark, lone Surrogates). Tabs are ignored by design.
2. RGI emoji clusters (\p{RGI_Emoji}) are double-width.
3. Minimally-qualified/unqualified emoji clusters (ZWJ sequences with 2+ Extended_Pictographic, or keycap sequences) are double-width.
4. Hangul jamo collapse each standard modern Hangul L+V or L+V+T syllable piece to width 2.
Unmatched repeated leading/vowel/trailing jamo stay additive because that matches how the terminals we target render them.
5. Otherwise use East Asian Width of the cluster's first visible code point, and add widths for trailing Halfwidth/Fullwidth Forms within the same cluster (e.g., dakuten/handakuten/prolonged sound mark).
5. Otherwise use East Asian Width of the cluster's first visible code point, and add widths for trailing spacing marks and Halfwidth/Fullwidth Forms within the same cluster (e.g., dakuten/handakuten/prolonged sound mark).
*/

const segmenter = new Intl.Segmenter();

// Whole-cluster zero-width
const zeroWidthClusterRegex = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Mark}|\p{Surrogate})+$/v;
const zeroWidthClusterRegex = /^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Nonspacing_Mark}|\p{Enclosing_Mark}|\p{Surrogate})+$/v;

// Pick the base scalar if the cluster starts with Prepend/Format/Marks
const leadingNonPrintingRegex = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v;
const leadingNonPrintingRegex = /^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Nonspacing_Mark}\p{Enclosing_Mark}\p{Surrogate}]+/v;
const spacingMarkRegex = /\p{Spacing_Mark}/v;

// RGI emoji sequences
const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
Expand Down Expand Up @@ -125,7 +126,7 @@ function hangulClusterWidth(visibleSegment, eastAsianWidthOptions) {
return width;
}

function trailingHalfwidthWidth(visibleSegment, eastAsianWidthOptions) {
function trailingWidth(visibleSegment, eastAsianWidthOptions) {
let extra = 0;
let first = true;

Expand All @@ -135,7 +136,10 @@ function trailingHalfwidthWidth(visibleSegment, eastAsianWidthOptions) {
continue;
}

if (character >= '\uFF00' && character <= '\uFFEF') {
if (
spacingMarkRegex.test(character)
|| (character >= '\uFF00' && character <= '\uFFEF')
) {
extra += eastAsianWidth(character.codePointAt(0), eastAsianWidthOptions);
}
}
Expand Down Expand Up @@ -195,8 +199,8 @@ export default function stringWidth(input, options = {}) {
const codePoint = visibleSegment.codePointAt(0);
width += eastAsianWidth(codePoint, eastAsianWidthOptions);

// Add width for trailing Halfwidth and Fullwidth Forms (e.g., ゙, ゚, ー)
width += trailingHalfwidthWidth(visibleSegment, eastAsianWidthOptions);
// Add width for trailing spacing marks and Halfwidth/Fullwidth Forms (e.g., ゙, ゚, ー)
width += trailingWidth(visibleSegment, eastAsianWidthOptions);
}

return width;
Expand Down
4 changes: 4 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ test('Indic conjunct via ZWJ', macro, 'क्\u200Dष', 1);
test('combining diacritical mark', macro, 'e\u0301', 1);
test('multiple combining marks', macro, 'e\u0301\u0302', 1);
test('combining marks only', macro, '\u0301\u0302', 0);
test('Tibetan combining mark', macro, 'ཟླ', 1);
test('enclosing mark', macro, 'a\u20DD', 1);
test('spacing mark alone', macro, '\u093E', 1);
test('spacing mark after base character', macro, '\u0915\u093E', 2);

// Surrogate pairs and high code points
test('emoji surrogate pair', macro, '😀', 2);
Expand Down
Loading