diff --git a/src/string_width.gleam b/src/string_width.gleam index 419727e..061e216 100644 --- a/src/string_width.gleam +++ b/src/string_width.gleam @@ -855,6 +855,75 @@ fn div_up(numerator: Int, denom: Int) { { numerator + denom - 1 } / denom } +/// 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 +/// resulting string, without affecting the styling anymore. +/// +/// It does so by collecting all SGR sequences since the last reset, and +/// re-applying them on each line break. +/// +/// **Warning:** This function assumes it is safe to emit reset commands, +/// and that no ambient styling is in effect (that is, the terminal would be in +/// its default state when printing the resulting string!) +/// +/// ### Examples +/// +/// ```gleam +/// // "\u{1b}[31m": red text color +/// // "\u{1b}[m": reset/normal mode +/// inline_styles("\u{1b}[31mhello\nworld") +/// // --> "\u{1b}[31mhello" <> "\u{1b}[m\n" <> "\u{1b}[31mworld" <> "\u{1b}[m" +/// ``` +/// +///
+/// +/// Back to top ↑ +/// +///
+pub fn inline_styles(str: String) -> String { + let ansi_ranges = ansi.match(str) + + let on_range = fn(state, ansi, _) { + let #(buf, sgr) = state + case ansi { + "\u{1b}[0m" | "\u{1b}[m" -> #(buf <> ansi, "") + _ -> + case string.ends_with(ansi, "m") { + True -> #(buf <> ansi, sgr <> ansi) + False -> #(buf <> ansi, sgr) + } + } + } + + let on_chars = fn(state, chars) { + case state { + #(buf, "") -> #(buf <> chars, "") + #(buf, sgr) -> { + let #(rest, _, buf) = { + use #(ptr, pos, buf), byte <- fold_bytes(chars, #(chars, 0, buf)) + case byte { + 0xA -> { + let assert #(line, "\n" <> ptr) = unsafe_split(ptr, pos) + #(ptr, 0, buf <> line <> "\u{1b}[m\n" <> sgr) + } + _ -> #(ptr, pos + 1, buf) + } + } + + #(buf <> rest, sgr) + } + } + } + + let state = fold_parts(str, 0, ansi_ranges, #("", ""), on_chars, on_range) + + case state { + #(buf, "") -> buf + #(buf, _sgr) -> buf <> "\u{1b}[m" + } +} + // -- FOLD --------------------------------------------------------------------- /// Returns true if a given component string recieved in `fold` is an ANSI diff --git a/test/layout_test.gleam b/test/layout_test.gleam index 80b9025..a2efb2f 100644 --- a/test/layout_test.gleam +++ b/test/layout_test.gleam @@ -131,6 +131,9 @@ pub fn position_test() { with: ".", ) |> should.equal("..........\n...XXX....\n..........") + + position("Trans\nrights\nare\nhuman\nrights", Size(0, 7), Right, Top, " ") + |> should.equal(" Trans \n rights\n are \n human \n rights") } pub fn align_test() { @@ -142,6 +145,26 @@ pub fn align_test() { align(" Welcome ", to: 20, align: Center, with: "==") |> should.equal("====== Welcome ======") + align("Trans\nrights\nare\nhuman\nrights", to: 7, align: Right, with: " ") |> should.equal(" Trans\n rights\n are\n human\n rights") } + +pub fn inline_styles_test() { + string_width.inline_styles("") + |> should.equal("") + + string_width.inline_styles("hello") + |> should.equal("hello") + + string_width.inline_styles("hel\u{1b}[mlo") + |> should.equal("hel\u{1b}[mlo") + + string_width.inline_styles("\u{1b}[31mhello\nworld") + |> should.equal( + "\u{1b}[31mhello" <> "\u{1b}[m\n" <> "\u{1b}[31mworld" <> "\u{1b}[m", + ) + + string_width.inline_styles("\u{1b}[31mhell\u{1b}[0mo,\nworld!") + |> should.equal("\u{1b}[31mhell\u{1b}[0mo,\nworld!") +}