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"
+/// ```
+///
+///
+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!")
+}