diff --git a/src/string_width.gleam b/src/string_width.gleam index a776a35..e47d0d4 100644 --- a/src/string_width.gleam +++ b/src/string_width.gleam @@ -11,8 +11,12 @@ //// [padding](#padding)[[_with](#padding_with)], //// [position](#position)[[_with](#position_with)], //// [scroll](#scroll)[[_with](#scroll_with)], -//// [tabs_to_spaces](#tabs_to_spaces)[[_with](#tabs_to_spaces_with)], -//// [inline_styles](#inline_styles) +//// [tabs_to_spaces](#tabs_to_spaces)[[_with](#tabs_to_spaces_with)] +//// +//// #### ANSI escape sequence helpers +//// [inline_styles](#inline_styles), +//// [is_ansi_component](#is_ansi_component), +//// [strip_ansi](#strip_ansi) //// //// #### Options Builder //// [new](#new), @@ -23,7 +27,7 @@ //// #### Advanced //// [fold](#fold)[[_with](#fold_with)], //// [fold_raw](#fold_raw), [fold_raw_pieces](#fold_raw_pieces), -//// [is_ansi_component](#is_ansi_component) +//// //// import gleam/int @@ -48,14 +52,16 @@ import string_width/internal/tables // v3.2.0: // x collect/reset SGR codes like reflow does -// - make align trim lines? // x padding layout, switch position to use padding internally // x remove do_measure -// - make sure position -> padding only measures the string once +// x make sure position -> padding only measures the string once // - rewrite align (think about trimming) +// - make align trim lines? // - join/columns layout (grid? table?) +// - join_vertical might also be intereseting for alignment/spacing +// x ansi module // x drop_left/drop_right - padding with negative margins? -// - cut/viewort/scroll(!!) function as an alternative to position with overflow? +// x cut/viewort/scroll(!!) function as an alternative to position with overflow? // provide the area you want to view and we will compute the padding for that. // x change the module header to not list _with functions separately // x add "Back to top" links to all functions @@ -70,6 +76,7 @@ import string_width/internal/tables // - think _again_ about changing fold to be called with an EOS marker at the end // - remove max_width from align // - position with overflow-hidden +// - limit -> reflow/fit that doesn't respect newlines in the original string // - split stuff into 2/3 packages? // -- OPTIONS ------------------------------------------------------------------ @@ -1263,6 +1270,8 @@ fn spacer_width(options: Options, str: String) -> Int { } } +// -- ANSI HELPERS ------------------------------------------------------------- + /// Make sure [SGR ansi codes](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) /// (these are the ones you'd use for colors and styling!) never wrap /// around a line. This makes it save to use other layout functions on the @@ -1332,7 +1341,32 @@ pub fn inline_styles(str: String) -> String { } } -// -- FOLD --------------------------------------------------------------------- +/// Strip _all_ ansi sequences from a string, using the same matching algorithm +/// that this library uses internally. +/// +/// This makes it safe to enable `count_ansi_codes`, or print a string without +/// having to worry that it might mess with the terminal and/or colors. +/// +/// Unexpected characters inside of escape sequences are ignored (swallowed). +/// +/// ### Examples +/// +/// ```gleam +/// strip_ansi("\u{1b}[0;33;49;3;9;4mhi~\u{1b}[0m") +/// // --> "hi~" +/// +/// strip_ansi("check out \u{1b}]8;;https://gleam.run\u{7}Gleam!\u{1b}]8;;\u{7}") +/// // --> "check out Gleam!" +/// ``` +/// +///
+/// +/// Back to top ↑ +/// +///
+pub fn strip_ansi(str: String) -> String { + ansi.strip(str) +} /// Returns true if a given component string recieved in `fold` is an ANSI /// escape sequence. @@ -1353,6 +1387,8 @@ pub fn is_ansi_component(str: String, options: Options) -> Bool { } } +// -- FOLD --------------------------------------------------------------------- + /// The measured pieces of a string, passed to you by `fold` and `fold_with` pub type Piece { Piece(piece: String, row: Int, column: Int, width: Int) diff --git a/src/string_width/internal/ansi.gleam b/src/string_width/internal/ansi.gleam index 578838c..00570ff 100644 --- a/src/string_width/internal/ansi.gleam +++ b/src/string_width/internal/ansi.gleam @@ -39,20 +39,36 @@ pub fn match(str: String) -> List(#(Int, Int)) { Escape(start) -> case byte { - // '[' + // '[' - CSI 0x5b -> #(pos + 1, CSI(start), matches) - // ']' + // ']' - OSC 0x5d -> #(pos + 1, OSC(start), matches) - // a..z single character end - _ if byte >= 0x61 && byte <= 0x7a -> { + + // Fp + _ if byte >= 0x30 && byte <= 0x3f -> { #(pos + 1, Text, [#(start, pos - start + 1), ..matches]) } - // A..Z single character end - _ if byte >= 0x41 && byte <= 0x5a -> { + + // other Fe/C1 sequences + // P - DCS + 0x50 -> #(pos + 1, OSC(start), matches) + // X - SOS + 0x58 -> #(pos + 1, OSC(start), matches) + // '^' - PM + 0x5e -> #(pos + 1, OSC(start), matches) + // '_' - APC + 0x5f -> #(pos + 1, OSC(start), matches) + _ if byte >= 0x40 && byte <= 0x5f -> { #(pos + 1, Text, [#(start, pos - start + 1), ..matches]) } - // ignore (use OSC state) - _ -> #(pos + 1, OSC(start), matches) + + // Fs + _ if byte >= 0x60 && byte <= 0x7e -> { + #(pos + 1, Text, [#(start, pos - start + 1), ..matches]) + } + + // unexpected - nF maybe? or another control character. + _ -> #(pos + 1, Text, matches) } CSI(start) -> diff --git a/src/string_width_ffi.mjs b/src/string_width_ffi.mjs index 7988a43..f64a3f2 100644 --- a/src/string_width_ffi.mjs +++ b/src/string_width_ffi.mjs @@ -1,6 +1,6 @@ import { toList, Ok, Error } from './gleam.mjs' -const ANSI_RE = /\x1b(?:[a-zA-Z]|(?:\[[\x20-\x3f]*?[\x40-\x7e])|(?:.*?(?:\x07|\x1b\\)))/mg +const ANSI_RE = /\x1b(?:[\x30-\x4f\x51-\x57\x59-\x5a\x60-\x7e]|(?:\[[\x20-\x3f]*?[\x40-\x7e])|(?:[\x50\x58\x5d\x5e\x5f].*?(?:\x07|\x1b\\)))/mg const ASCII_RE = /[\x20-\x7e]+(?=[\x20-\x7e]|$)/g; const SEGMENTER = new Intl.Segmenter(); diff --git a/test/ansi_test.gleam b/test/ansi_test.gleam index 49cca0d..f74186d 100644 --- a/test/ansi_test.gleam +++ b/test/ansi_test.gleam @@ -101,3 +101,11 @@ pub fn emoji_test() { ) |> should.equal("πŸ‘©β€πŸ‘©β€πŸ‘¦β€πŸ‘¦helloπŸ‘©β€πŸ‘©β€πŸ‘¦β€πŸ‘¦") } + +pub fn docs_test() { + ansi.strip("\u{1b}[0;33;49;3;9;4mhi~\u{1b}[0m") + |> should.equal("hi~") + + ansi.strip("check out \u{1b}]8;;https://gleam.run\u{7}Gleam!\u{1b}]8;;\u{7}") + |> should.equal("check out Gleam!") +}