From 9f802781568d657ec53fcda1fb21488dd65cd1c4 Mon Sep 17 00:00:00 2001 From: Joshua Reusch Date: Sat, 12 Oct 2024 12:04:45 +0200 Subject: [PATCH] doc improvements, skip align in position if every line is wider, fix a bug with dimensions+tab_offset on empty strings --- src/string_width.gleam | 74 +++++++++++++++++++++++++----------- test/generate.gleam | 2 + test/layout_test.gleam | 4 +- test/string_width_test.gleam | 8 ++++ 4 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/string_width.gleam b/src/string_width.gleam index b2d4365..e105424 100644 --- a/src/string_width.gleam +++ b/src/string_width.gleam @@ -194,7 +194,7 @@ pub fn line(str: String) -> Int { /// // --> 2 /// ``` pub fn line_with(str: String, options: Options) -> Int { - let Size(columns:, rows: _) = dimensions_with(str, options) + let #(_, columns, _) = do_measure(str, options) columns } @@ -213,20 +213,31 @@ pub fn line_with(str: String, options: Options) -> Int { /// // --> Size(rows: 2, columns: 10) /// ``` pub fn dimensions(str: String) -> Size { - dimensions_with(str, default_options) + let #(rows, columns, _) = do_measure(str, default_options) + Size(rows:, columns:) } /// Like `dimensions`, but use custom options. pub fn dimensions_with(str: String, options: Options) -> Size { + let #(rows, columns, _) = do_measure(str, options) + Size(rows:, columns:) +} + +fn do_measure(str: String, options: Options) -> #(Int, Int, Int) { let #(str, ranges, range_width) = prepare_measure(options, str) let fun = fn(state, chr, width) { - let #(rows, cols_max, cols_curr) = state + let #(rows, cols_max, cols_min, cols_curr) = state case chr { - "\n" -> #(rows + 1, int.max(cols_max, cols_curr), options.tab_offset) - "\t" -> #(rows, cols_max, tab(options, cols_curr)) - _ -> #(rows, cols_max, cols_curr + width) + "\n" -> #( + rows + 1, + int.max(cols_max, cols_curr), + int.min(cols_max, cols_curr), + options.tab_offset, + ) + "\t" -> #(rows, cols_max, cols_min, tab(options, cols_curr)) + _ -> #(rows, cols_max, cols_min, cols_curr + width) } } @@ -242,14 +253,21 @@ pub fn dimensions_with(str: String, options: Options) -> Size { } } - let #(rows, cols_max, cols_curr) = - fold_parts(str, 0, ranges, #(0, 0, options.tab_offset), on_chars, on_range) + let initial = #(0, 0, max_safe_integer, options.tab_offset) + let #(rows, cols_max, cols_min, cols_curr) = + fold_parts(str, 0, ranges, initial, on_chars, on_range) - let columns = int.max(cols_max, cols_curr) - options.tab_offset - case cols_curr > 0 { - True -> Size(rows: rows + 1, columns:) - False -> Size(rows:, columns:) + let cols_max = int.max(cols_max, cols_curr) - options.tab_offset + let rows = case cols_curr > options.tab_offset { + True -> rows + 1 + False -> rows } + let cols_min = case rows > 0 { + True -> int.min(cols_min, cols_curr) - options.tab_offset + False -> 0 + } + + #(rows, cols_max, cols_min) } /// Round up to the next tab boundary. @@ -291,7 +309,7 @@ fn prepare_measure( /// limit("Hello World", Size(rows: 1, columns: 10), ellipsis: "...") /// // --> "Hello W..." /// -/// limit("Hello World", Size(rows: 2, columns: 5), ellipsis: "...") +/// limit("Hello World", Size(rows: 2, columns: 10), ellipsis: "...") /// // --> "Hello\nWorld" /// ``` pub fn limit( @@ -627,9 +645,9 @@ pub type Placement { /// Position the string area inside a bigger box without changing text alignment. /// The box will be filled with the space character. /// -/// If a line is already bigger than the maximum width, it will not be changed. -/// If there are more lines than the maximum amount of rows, the extra lines -/// will still be kept. +/// **Tip:** If you only want to align a string on one axis, you can provide +/// `0` for the orthogonal one! Extra characters and lines will be kept. +/// /// If the space strings' width does not evenly divide the missing amount of /// columns, the extra spacer will overflow the max width. /// @@ -658,9 +676,13 @@ pub fn position_with( using options: Options, with space: String, ) -> String { + let #(str_rows, str_cols_max, str_cols_min) = do_measure(str, options) let space_width = line_with(space, options) let size = dimensions_with(str, options) + // horizontal align - we can skip this if every line is at least columns wide + let str = case bounding_box.columns > str_cols_min { + True -> { let align = case alignment { Left -> fn(line, line_width) { let missing = div_up(bounding_box.columns - line_width, space_width) @@ -669,7 +691,7 @@ pub fn position_with( Right -> { let missing_left = - div_up(bounding_box.columns - size.columns, space_width) + div_up(bounding_box.columns - str_cols_max, space_width) let space_left = string.repeat(space, missing_left) fn(line, line_width) { @@ -682,23 +704,28 @@ pub fn position_with( Center -> { let missing_total = - div_up(bounding_box.columns - size.columns, space_width) + div_up(bounding_box.columns - str_cols_max, space_width) let missing_left = missing_total / 2 let space_left = string.repeat(space, missing_left) let space_right = string.repeat(space, missing_total - missing_left) fn(line, line_width) { let missing = - { bounding_box.columns - line_width } / space_width - missing_total + { bounding_box.columns - line_width } + / space_width + - missing_total space_left <> line <> string.repeat(space, missing) <> space_right } } + } + do_align(str, options, align) + } + False -> str } - let str = do_align(str, options, align) - let missing_rows = bounding_box.rows - size.rows - + // vertical align - we can also skip this if the string has more lines already + let missing_rows = bounding_box.rows - str_rows case missing_rows > 0 { True -> { let space_row = @@ -738,6 +765,9 @@ pub fn position_with( /// /// align(" Welcome ", to: 20, align: Center, with: "==") /// // --> "====== Welcome ======" // (len = 21) +/// +/// align("Trans\nrights\nare\nhuman\nrights", to: 7, align: Right, with: " ") +/// // --> " Trans\n rights\n are\n human\n rights" /// ``` pub fn align( str: String, diff --git a/test/generate.gleam b/test/generate.gleam index 5d872cf..d4418bd 100644 --- a/test/generate.gleam +++ b/test/generate.gleam @@ -24,6 +24,7 @@ type Property { pub fn main() { // TODO: look into this: https://github.com/erlang/otp/blob/master/lib/stdlib/uc_spec/gen_unicode_mod.escript#L198 + // TODO: ranges can be compressed further if all codepoints in between are unassigned let east_asian_width = parse_unicode_data("./data/EastAsianWidth.txt", fn(property) { case property { @@ -41,6 +42,7 @@ pub fn main() { }) // emoji presentation characters are always also considered wide, regardless of EAW + // we get this data already in EAW, but I wanna be explicit let emoji_presentation = parse_unicode_data("./data/emoji-data.txt", fn(prop) { case prop { diff --git a/test/layout_test.gleam b/test/layout_test.gleam index 5190be4..52a4a2e 100644 --- a/test/layout_test.gleam +++ b/test/layout_test.gleam @@ -8,7 +8,7 @@ pub fn limit_test() { limit("Hello World", Size(1, 10), "...") |> should.equal("Hello W...") - limit("Hello World", Size(2, 5), "...") + limit("Hello World", Size(2, 10), "...") |> should.equal("Hello\nWorld") limit("VeryLongIdentifier", Size(2, 8), "...") @@ -86,4 +86,6 @@ 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") } diff --git a/test/string_width_test.gleam b/test/string_width_test.gleam index 5c06c7f..0480a27 100644 --- a/test/string_width_test.gleam +++ b/test/string_width_test.gleam @@ -54,6 +54,14 @@ pub fn dimensions_test() { dimensions("hello,\n안녕하세요\n") |> should.equal(Size(2, 10)) } +pub fn dimensions_with_tab_offset_test() { + let options = + string_width.new() + |> string_width.at_tab_offset(3) + + string_width.dimensions_with("", options) |> should.equal(Size(0, 0)) +} + pub fn pedantic_test() { // these are the test cases I had for v3. // I wanna document what other programs do, even if I think they are buggy. -- 2.51.2