From 002887e3e76ec2bd07e0e29fe5f3da7294e5b790 Mon Sep 17 00:00:00 2001 From: Joshua Reusch Date: Tue, 1 Oct 2024 18:49:13 +0200 Subject: [PATCH] mode_2027 vs mode_2027_ext --- src/string_width.gleam | 85 +++++++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/src/string_width.gleam b/src/string_width.gleam index 244d1a0..3c06d67 100644 --- a/src/string_width.gleam +++ b/src/string_width.gleam @@ -23,16 +23,22 @@ pub opaque type Options { Options( count_ansi_codes: Bool, ambiguous_as_wide: Bool, - mode_2027: Bool, + mode: Mode, tab_width: Int, tab_offset: Int, ) } +pub opaque type Mode { + ModeWcwidth + Mode2027 + Mode2027Ext +} + const default_options = Options( count_ansi_codes: False, ambiguous_as_wide: False, - mode_2027: False, + mode: ModeWcwidth, tab_width: 8, tab_offset: 0, ) @@ -59,7 +65,21 @@ pub fn new() -> Options { /// See also [Grapheme Clusters and Terminal Emulators](https://mitchellh.com/writing/grapheme-clusters-in-terminals) /// for a better explanation on how terminals behave. pub fn mode_2027(options: Options) -> Options { - Options(..options, mode_2027: True) + Options(..options, mode: Mode2027) +} + +/// Measure grapheme clusters instead of individual code points, and always +/// treat grapheme clusters with more than one non-modifier character as wide. +/// +/// This is a custom extension to Unicode Core. Many single grapheme clusters +/// would currently still render as wide glyphs in many contexts, since font +/// shaping would not be able to collapse them into a single narrow "ligature". +/// +/// This mode is an additional heuristic that tries to handle more such cases. +/// +/// See `mode_2027` for more explanation on the implications. +pub fn mode_2027_ext(options: Options) -> Options { + Options(..options, mode: Mode2027Ext) } /// Some characters are marked by Unicode as "ambiguous", meaning they may @@ -130,7 +150,7 @@ pub fn line(str: String) -> Int { /// ```gleam /// let options = /// new() -/// |> mode_2027 +/// |> mode /// /// line_with("👩‍👩‍👦‍👦", options) /// // --> 2 @@ -176,9 +196,12 @@ pub fn dimensions_with(str: String, options: Options) -> #(Int, Int) { fun(state, ansi, range_width * length) } - let on_chars = case options.mode_2027 { - True -> fn(state, str) { fold_chars_2027(options, str, state, fun) } - False -> fn(state, str) { fold_chars_raw(options, str, state, fun) } + let on_chars = case options.mode { + ModeWcwidth -> fn(state, str) { fold_chars_raw(options, str, state, fun) } + Mode2027 -> fn(state, str) { fold_chars_2027(options, str, state, fun) } + Mode2027Ext -> fn(state, str) { + fold_chars_2027_ext(options, str, state, fun) + } } let #(rows, cols_max, cols_curr) = @@ -221,7 +244,7 @@ fn prepare_measure( // if one component is followed by another zero-width component treat them as one? // maybe even in fold? -// mode_2027_ext +// mode_ext // mode_wcwidth // mixed: pass grapheme clusters but measure codepoints @@ -286,9 +309,14 @@ pub fn fold_with( } } - let on_chars = case options.mode_2027 { - True -> fn(state, str) { fold_chars_2027(options, str, state, fun) } - False -> fn(state, str) { fold_chars_wcwidth(options, str, state, fun) } + let on_chars = case options.mode { + ModeWcwidth -> fn(state, str) { + fold_chars_wcwidth(options, str, state, fun) + } + Mode2027 -> fn(state, str) { fold_chars_2027(options, str, state, fun) } + Mode2027Ext -> fn(state, str) { + fold_chars_2027_ext(options, str, state, fun) + } } let ansi_ranges = case options.count_ansi_codes { @@ -306,7 +334,7 @@ pub fn fold_with( } /// Iterate over the measured components of a string. Components are either -/// graphemes or codepoints, depending on the `mode_2027` option, +/// graphemes or codepoints, depending on the `mode` option, /// or other undivisible sequences, like ANSI escape codes. /// /// This is a lower-level utility compared to the others in this package. It does @@ -347,9 +375,12 @@ pub fn fold_raw( False -> ansi.match(string) } - let on_chars = case options.mode_2027 { - True -> fn(state, str) { fold_chars_2027(options, str, state, fun) } - False -> fn(state, str) { fold_chars_raw(options, str, state, fun) } + let on_chars = case options.mode { + ModeWcwidth -> fn(state, str) { fold_chars_raw(options, str, state, fun) } + Mode2027 -> fn(state, str) { fold_chars_2027(options, str, state, fun) } + Mode2027Ext -> fn(state, str) { + fold_chars_2027_ext(options, str, state, fun) + } } let on_range = fn(state, ansi, _) { fun(state, ansi, 0) } @@ -425,11 +456,31 @@ fn fold_chars_2027( fun: fn(state, String, Int) -> state, ) -> state { use state, grapheme <- fold_graphemes(string, state) - let width = do_grapheme_cluster(options, grapheme) + let width = measure_mode_2027(options, grapheme) fun(state, grapheme, width) } -fn do_grapheme_cluster(options: Options, string: String) -> Int { +fn fold_chars_2027_ext( + options: Options, + string: String, + state: state, + fun: fn(state, String, Int) -> state, +) -> state { + use state, grapheme <- fold_graphemes(string, state) + let width = measure_mode_2027_ext(options, grapheme) + fun(state, grapheme, width) +} + +fn measure_mode_2027(options: Options, string: String) -> Int { + use max, cp <- fold_codepoints(string, 0) + case cp { + // unicode core: VS16 forces a cluster to be wide + 0xfe0f -> 2 + _ -> int.max(max, wcwidth(options, cp)) + } +} + +fn measure_mode_2027_ext(options: Options, string: String) -> Int { // The Unicode Core spec (https://github.com/contour-terminal/terminal-unicode-core) // proposing Mode 2027 says the following: // -- 2.51.2