diff options
| author | 2025-04-30 11:58:19 -0400 | |
|---|---|---|
| committer | 2025-04-30 11:58:19 -0400 | |
| commit | 1be5e46490e061761b4b97dff5c6acb2181d6fe9 (patch) | |
| tree | 77a1edcdedd7afae7428e92feba37d2bb1035b22 /src/DisplayWidth.zig | |
| parent | Add general tests step (diff) | |
| download | zg-1be5e46490e061761b4b97dff5c6acb2181d6fe9.tar.gz zg-1be5e46490e061761b4b97dff5c6acb2181d6fe9.tar.xz zg-1be5e46490e061761b4b97dff5c6acb2181d6fe9.zip | |
Factor out 'Data' for grapheme and DisplayWidth
In the process of refactoring the whole library, so that it doesn't
expose anything called "Data" separately from user functionality.
Diffstat (limited to 'src/DisplayWidth.zig')
| -rw-r--r-- | src/DisplayWidth.zig | 240 |
1 files changed, 164 insertions, 76 deletions
diff --git a/src/DisplayWidth.zig b/src/DisplayWidth.zig index 8631bd4..11ec59e 100644 --- a/src/DisplayWidth.zig +++ b/src/DisplayWidth.zig | |||
| @@ -2,38 +2,131 @@ const std = @import("std"); | |||
| 2 | const builtin = @import("builtin"); | 2 | const builtin = @import("builtin"); |
| 3 | const options = @import("options"); | 3 | const options = @import("options"); |
| 4 | const ArrayList = std.ArrayList; | 4 | const ArrayList = std.ArrayList; |
| 5 | const compress = std.compress; | ||
| 5 | const mem = std.mem; | 6 | const mem = std.mem; |
| 6 | const simd = std.simd; | 7 | const simd = std.simd; |
| 7 | const testing = std.testing; | 8 | const testing = std.testing; |
| 8 | 9 | ||
| 9 | const ascii = @import("ascii"); | 10 | const ascii = @import("ascii"); |
| 10 | const CodePointIterator = @import("code_point").Iterator; | 11 | const CodePointIterator = @import("code_point").Iterator; |
| 11 | const GraphemeIterator = @import("grapheme").Iterator; | ||
| 12 | pub const DisplayWidthData = @import("DisplayWidthData"); | 12 | pub const DisplayWidthData = @import("DisplayWidthData"); |
| 13 | 13 | ||
| 14 | data: *const DisplayWidthData, | 14 | const Graphemes = @import("Graphemes"); |
| 15 | 15 | ||
| 16 | const Self = @This(); | 16 | g_data: Graphemes, |
| 17 | s1: []u16 = undefined, | ||
| 18 | s2: []i4 = undefined, | ||
| 19 | owns_gdata: bool, | ||
| 20 | |||
| 21 | const DisplayWidth = @This(); | ||
| 22 | |||
| 23 | pub fn init(allocator: mem.Allocator) mem.Allocator.Error!DisplayWidth { | ||
| 24 | var dw: DisplayWidth = try DisplayWidth.setup(allocator); | ||
| 25 | errdefer { | ||
| 26 | allocator.free(dw.s1); | ||
| 27 | allocator.free(dw.s2); | ||
| 28 | } | ||
| 29 | dw.owns_gdata = true; | ||
| 30 | dw.g_data = try Graphemes.init(allocator); | ||
| 31 | errdefer dw.g_data.deinit(allocator); | ||
| 32 | return dw; | ||
| 33 | } | ||
| 34 | |||
| 35 | pub fn initWithGraphemeData(allocator: mem.Allocator, g_data: Graphemes) mem.Allocator.Error!DisplayWidth { | ||
| 36 | var dw = try DisplayWidth.setup(allocator); | ||
| 37 | dw.g_data = g_data; | ||
| 38 | dw.owns_gdata = false; | ||
| 39 | return dw; | ||
| 40 | } | ||
| 41 | |||
| 42 | // Sets up the DisplayWidthData, leaving the GraphemeData undefined. | ||
| 43 | fn setup(allocator: mem.Allocator) mem.Allocator.Error!DisplayWidth { | ||
| 44 | const decompressor = compress.flate.inflate.decompressor; | ||
| 45 | const in_bytes = @embedFile("dwp"); | ||
| 46 | var in_fbs = std.io.fixedBufferStream(in_bytes); | ||
| 47 | var in_decomp = decompressor(.raw, in_fbs.reader()); | ||
| 48 | var reader = in_decomp.reader(); | ||
| 49 | |||
| 50 | const endian = builtin.cpu.arch.endian(); | ||
| 51 | |||
| 52 | var dw: DisplayWidth = undefined; | ||
| 53 | |||
| 54 | const stage_1_len: u16 = reader.readInt(u16, endian) catch unreachable; | ||
| 55 | dw.s1 = try allocator.alloc(u16, stage_1_len); | ||
| 56 | errdefer allocator.free(dw.s1); | ||
| 57 | for (0..stage_1_len) |i| dw.s1[i] = reader.readInt(u16, endian) catch unreachable; | ||
| 58 | |||
| 59 | const stage_2_len: u16 = reader.readInt(u16, endian) catch unreachable; | ||
| 60 | dw.s2 = try allocator.alloc(i4, stage_2_len); | ||
| 61 | errdefer allocator.free(dw.s2); | ||
| 62 | for (0..stage_2_len) |i| dw.s2[i] = @intCast(reader.readInt(i8, endian) catch unreachable); | ||
| 63 | |||
| 64 | return dw; | ||
| 65 | } | ||
| 66 | |||
| 67 | pub fn deinit(dw: *const DisplayWidth, allocator: mem.Allocator) void { | ||
| 68 | allocator.free(dw.s1); | ||
| 69 | allocator.free(dw.s2); | ||
| 70 | if (dw.owns_gdata) dw.g_data.deinit(allocator); | ||
| 71 | } | ||
| 72 | |||
| 73 | /// codePointWidth returns the number of cells `cp` requires when rendered | ||
| 74 | /// in a fixed-pitch font (i.e. a terminal screen). This can range from -1 to | ||
| 75 | /// 3, where BACKSPACE and DELETE return -1 and 3-em-dash returns 3. C0/C1 | ||
| 76 | /// control codes return 0. If `cjk` is true, ambiguous code points return 2, | ||
| 77 | /// otherwise they return 1. | ||
| 78 | pub fn codePointWidth(dw: DisplayWidth, cp: u21) i4 { | ||
| 79 | return dw.s2[dw.s1[cp >> 8] + (cp & 0xff)]; | ||
| 80 | } | ||
| 81 | |||
| 82 | test "codePointWidth" { | ||
| 83 | const dw = try DisplayWidth.init(std.testing.allocator); | ||
| 84 | defer dw.deinit(std.testing.allocator); | ||
| 85 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0000)); // null | ||
| 86 | try testing.expectEqual(@as(i4, -1), dw.codePointWidth(0x8)); // \b | ||
| 87 | try testing.expectEqual(@as(i4, -1), dw.codePointWidth(0x7f)); // DEL | ||
| 88 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0005)); // Cf | ||
| 89 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0007)); // \a BEL | ||
| 90 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000A)); // \n LF | ||
| 91 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000B)); // \v VT | ||
| 92 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000C)); // \f FF | ||
| 93 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000D)); // \r CR | ||
| 94 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000E)); // SQ | ||
| 95 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000F)); // SI | ||
| 96 | |||
| 97 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x070F)); // Cf | ||
| 98 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x0603)); // Cf Arabic | ||
| 99 | |||
| 100 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x00AD)); // soft-hyphen | ||
| 101 | try testing.expectEqual(@as(i4, 2), dw.codePointWidth(0x2E3A)); // two-em dash | ||
| 102 | try testing.expectEqual(@as(i4, 3), dw.codePointWidth(0x2E3B)); // three-em dash | ||
| 103 | |||
| 104 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x00BD)); // ambiguous halfwidth | ||
| 105 | |||
| 106 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth('é')); | ||
| 107 | try testing.expectEqual(@as(i4, 2), dw.codePointWidth('😊')); | ||
| 108 | try testing.expectEqual(@as(i4, 2), dw.codePointWidth('统')); | ||
| 109 | } | ||
| 17 | 110 | ||
| 18 | /// strWidth returns the total display width of `str` as the number of cells | 111 | /// strWidth returns the total display width of `str` as the number of cells |
| 19 | /// required in a fixed-pitch font (i.e. a terminal screen). | 112 | /// required in a fixed-pitch font (i.e. a terminal screen). |
| 20 | pub fn strWidth(self: Self, str: []const u8) usize { | 113 | pub fn strWidth(dw: DisplayWidth, str: []const u8) usize { |
| 21 | var total: isize = 0; | 114 | var total: isize = 0; |
| 22 | 115 | ||
| 23 | // ASCII fast path | 116 | // ASCII fast path |
| 24 | if (ascii.isAsciiOnly(str)) { | 117 | if (ascii.isAsciiOnly(str)) { |
| 25 | for (str) |b| total += self.data.codePointWidth(b); | 118 | for (str) |b| total += dw.codePointWidth(b); |
| 26 | return @intCast(@max(0, total)); | 119 | return @intCast(@max(0, total)); |
| 27 | } | 120 | } |
| 28 | 121 | ||
| 29 | var giter = GraphemeIterator.init(str, &self.data.g_data); | 122 | var giter = dw.g_data.iterator(str); |
| 30 | 123 | ||
| 31 | while (giter.next()) |gc| { | 124 | while (giter.next()) |gc| { |
| 32 | var cp_iter = CodePointIterator{ .bytes = gc.bytes(str) }; | 125 | var cp_iter = CodePointIterator{ .bytes = gc.bytes(str) }; |
| 33 | var gc_total: isize = 0; | 126 | var gc_total: isize = 0; |
| 34 | 127 | ||
| 35 | while (cp_iter.next()) |cp| { | 128 | while (cp_iter.next()) |cp| { |
| 36 | var w = self.data.codePointWidth(cp.code); | 129 | var w = dw.codePointWidth(cp.code); |
| 37 | 130 | ||
| 38 | if (w != 0) { | 131 | if (w != 0) { |
| 39 | // Handle text emoji sequence. | 132 | // Handle text emoji sequence. |
| @@ -58,41 +151,40 @@ pub fn strWidth(self: Self, str: []const u8) usize { | |||
| 58 | } | 151 | } |
| 59 | 152 | ||
| 60 | test "strWidth" { | 153 | test "strWidth" { |
| 61 | const data = try DisplayWidthData.init(testing.allocator); | 154 | const dw = try DisplayWidth.init(testing.allocator); |
| 62 | defer data.deinit(testing.allocator); | 155 | defer dw.deinit(testing.allocator); |
| 63 | const self = Self{ .data = &data }; | ||
| 64 | const c0 = options.c0_width orelse 0; | 156 | const c0 = options.c0_width orelse 0; |
| 65 | 157 | ||
| 66 | try testing.expectEqual(@as(usize, 5), self.strWidth("Hello\r\n")); | 158 | try testing.expectEqual(@as(usize, 5), dw.strWidth("Hello\r\n")); |
| 67 | try testing.expectEqual(@as(usize, 1), self.strWidth("\u{0065}\u{0301}")); | 159 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{0065}\u{0301}")); |
| 68 | try testing.expectEqual(@as(usize, 2), self.strWidth("\u{1F476}\u{1F3FF}\u{0308}\u{200D}\u{1F476}\u{1F3FF}")); | 160 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{1F476}\u{1F3FF}\u{0308}\u{200D}\u{1F476}\u{1F3FF}")); |
| 69 | try testing.expectEqual(@as(usize, 8), self.strWidth("Hello 😊")); | 161 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Hello 😊")); |
| 70 | try testing.expectEqual(@as(usize, 8), self.strWidth("Héllo 😊")); | 162 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo 😊")); |
| 71 | try testing.expectEqual(@as(usize, 8), self.strWidth("Héllo :)")); | 163 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo :)")); |
| 72 | try testing.expectEqual(@as(usize, 8), self.strWidth("Héllo 🇪🇸")); | 164 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo 🇪🇸")); |
| 73 | try testing.expectEqual(@as(usize, 2), self.strWidth("\u{26A1}")); // Lone emoji | 165 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{26A1}")); // Lone emoji |
| 74 | try testing.expectEqual(@as(usize, 1), self.strWidth("\u{26A1}\u{FE0E}")); // Text sequence | 166 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{26A1}\u{FE0E}")); // Text sequence |
| 75 | try testing.expectEqual(@as(usize, 2), self.strWidth("\u{26A1}\u{FE0F}")); // Presentation sequence | 167 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{26A1}\u{FE0F}")); // Presentation sequence |
| 76 | try testing.expectEqual(@as(usize, 1), self.strWidth("\u{2764}")); // Default text presentation | 168 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{2764}")); // Default text presentation |
| 77 | try testing.expectEqual(@as(usize, 1), self.strWidth("\u{2764}\u{FE0E}")); // Default text presentation with VS15 selector | 169 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{2764}\u{FE0E}")); // Default text presentation with VS15 selector |
| 78 | try testing.expectEqual(@as(usize, 2), self.strWidth("\u{2764}\u{FE0F}")); // Default text presentation with VS16 selector | 170 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{2764}\u{FE0F}")); // Default text presentation with VS16 selector |
| 79 | const expect_bs: usize = if (c0 == 0) 0 else 1 + c0; | 171 | const expect_bs: usize = if (c0 == 0) 0 else 1 + c0; |
| 80 | try testing.expectEqual(expect_bs, self.strWidth("A\x08")); // Backspace | 172 | try testing.expectEqual(expect_bs, dw.strWidth("A\x08")); // Backspace |
| 81 | try testing.expectEqual(expect_bs, self.strWidth("\x7FA")); // DEL | 173 | try testing.expectEqual(expect_bs, dw.strWidth("\x7FA")); // DEL |
| 82 | const expect_long_del: usize = if (c0 == 0) 0 else 1 + (c0 * 3); | 174 | const expect_long_del: usize = if (c0 == 0) 0 else 1 + (c0 * 3); |
| 83 | try testing.expectEqual(expect_long_del, self.strWidth("\x7FA\x08\x08")); // never less than 0 | 175 | try testing.expectEqual(expect_long_del, dw.strWidth("\x7FA\x08\x08")); // never less than 0 |
| 84 | 176 | ||
| 85 | // wcwidth Python lib tests. See: https://github.com/jquast/wcwidth/blob/master/tests/test_core.py | 177 | // wcwidth Python lib tests. See: https://github.com/jquast/wcwidth/blob/master/tests/test_core.py |
| 86 | const empty = ""; | 178 | const empty = ""; |
| 87 | try testing.expectEqual(@as(usize, 0), self.strWidth(empty)); | 179 | try testing.expectEqual(@as(usize, 0), dw.strWidth(empty)); |
| 88 | const with_null = "hello\x00world"; | 180 | const with_null = "hello\x00world"; |
| 89 | try testing.expectEqual(@as(usize, 10 + c0), self.strWidth(with_null)); | 181 | try testing.expectEqual(@as(usize, 10 + c0), dw.strWidth(with_null)); |
| 90 | const hello_jp = "コンニチハ, セカイ!"; | 182 | const hello_jp = "コンニチハ, セカイ!"; |
| 91 | try testing.expectEqual(@as(usize, 19), self.strWidth(hello_jp)); | 183 | try testing.expectEqual(@as(usize, 19), dw.strWidth(hello_jp)); |
| 92 | const control = "\x1b[0m"; | 184 | const control = "\x1b[0m"; |
| 93 | try testing.expectEqual(@as(usize, 3 + c0), self.strWidth(control)); | 185 | try testing.expectEqual(@as(usize, 3 + c0), dw.strWidth(control)); |
| 94 | const balinese = "\u{1B13}\u{1B28}\u{1B2E}\u{1B44}"; | 186 | const balinese = "\u{1B13}\u{1B28}\u{1B2E}\u{1B44}"; |
| 95 | try testing.expectEqual(@as(usize, 3), self.strWidth(balinese)); | 187 | try testing.expectEqual(@as(usize, 3), dw.strWidth(balinese)); |
| 96 | 188 | ||
| 97 | // These commented out tests require a new specification for complex scripts. | 189 | // These commented out tests require a new specification for complex scripts. |
| 98 | // See: https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdf | 190 | // See: https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdf |
| @@ -106,17 +198,17 @@ test "strWidth" { | |||
| 106 | // try testing.expectEqual(@as(usize, 3), strWidth(kannada_1)); | 198 | // try testing.expectEqual(@as(usize, 3), strWidth(kannada_1)); |
| 107 | // The following passes but as a mere coincidence. | 199 | // The following passes but as a mere coincidence. |
| 108 | const kannada_2 = "\u{0cb0}\u{0cbc}\u{0ccd}\u{0c9a}"; | 200 | const kannada_2 = "\u{0cb0}\u{0cbc}\u{0ccd}\u{0c9a}"; |
| 109 | try testing.expectEqual(@as(usize, 2), self.strWidth(kannada_2)); | 201 | try testing.expectEqual(@as(usize, 2), dw.strWidth(kannada_2)); |
| 110 | 202 | ||
| 111 | // From Rust https://github.com/jameslanska/unicode-display-width | 203 | // From Rust https://github.com/jameslanska/unicode-display-width |
| 112 | try testing.expectEqual(@as(usize, 15), self.strWidth("🔥🗡🍩👩🏻🚀⏰💃🏼🔦👍🏻")); | 204 | try testing.expectEqual(@as(usize, 15), dw.strWidth("🔥🗡🍩👩🏻🚀⏰💃🏼🔦👍🏻")); |
| 113 | try testing.expectEqual(@as(usize, 2), self.strWidth("🦀")); | 205 | try testing.expectEqual(@as(usize, 2), dw.strWidth("🦀")); |
| 114 | try testing.expectEqual(@as(usize, 2), self.strWidth("👨👩👧👧")); | 206 | try testing.expectEqual(@as(usize, 2), dw.strWidth("👨👩👧👧")); |
| 115 | try testing.expectEqual(@as(usize, 2), self.strWidth("👩🔬")); | 207 | try testing.expectEqual(@as(usize, 2), dw.strWidth("👩🔬")); |
| 116 | try testing.expectEqual(@as(usize, 9), self.strWidth("sane text")); | 208 | try testing.expectEqual(@as(usize, 9), dw.strWidth("sane text")); |
| 117 | try testing.expectEqual(@as(usize, 9), self.strWidth("Ẓ̌á̲l͔̝̞̄̑͌g̖̘̘̔̔͢͞͝o̪̔T̢̙̫̈̍͞e̬͈͕͌̏͑x̺̍ṭ̓̓ͅ")); | 209 | try testing.expectEqual(@as(usize, 9), dw.strWidth("Ẓ̌á̲l͔̝̞̄̑͌g̖̘̘̔̔͢͞͝o̪̔T̢̙̫̈̍͞e̬͈͕͌̏͑x̺̍ṭ̓̓ͅ")); |
| 118 | try testing.expectEqual(@as(usize, 17), self.strWidth("슬라바 우크라이나")); | 210 | try testing.expectEqual(@as(usize, 17), dw.strWidth("슬라바 우크라이나")); |
| 119 | try testing.expectEqual(@as(usize, 1), self.strWidth("\u{378}")); | 211 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{378}")); |
| 120 | } | 212 | } |
| 121 | 213 | ||
| 122 | /// centers `str` in a new string of width `total_width` (in display cells) using `pad` as padding. | 214 | /// centers `str` in a new string of width `total_width` (in display cells) using `pad` as padding. |
| @@ -124,17 +216,17 @@ test "strWidth" { | |||
| 124 | /// receive one additional pad. This makes sure the returned string fills the requested width. | 216 | /// receive one additional pad. This makes sure the returned string fills the requested width. |
| 125 | /// Caller must free returned bytes with `allocator`. | 217 | /// Caller must free returned bytes with `allocator`. |
| 126 | pub fn center( | 218 | pub fn center( |
| 127 | self: Self, | 219 | dw: DisplayWidth, |
| 128 | allocator: mem.Allocator, | 220 | allocator: mem.Allocator, |
| 129 | str: []const u8, | 221 | str: []const u8, |
| 130 | total_width: usize, | 222 | total_width: usize, |
| 131 | pad: []const u8, | 223 | pad: []const u8, |
| 132 | ) ![]u8 { | 224 | ) ![]u8 { |
| 133 | const str_width = self.strWidth(str); | 225 | const str_width = dw.strWidth(str); |
| 134 | if (str_width > total_width) return error.StrTooLong; | 226 | if (str_width > total_width) return error.StrTooLong; |
| 135 | if (str_width == total_width) return try allocator.dupe(u8, str); | 227 | if (str_width == total_width) return try allocator.dupe(u8, str); |
| 136 | 228 | ||
| 137 | const pad_width = self.strWidth(pad); | 229 | const pad_width = dw.strWidth(pad); |
| 138 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; | 230 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; |
| 139 | 231 | ||
| 140 | const margin_width = @divFloor((total_width - str_width), 2); | 232 | const margin_width = @divFloor((total_width - str_width), 2); |
| @@ -165,62 +257,61 @@ pub fn center( | |||
| 165 | 257 | ||
| 166 | test "center" { | 258 | test "center" { |
| 167 | const allocator = testing.allocator; | 259 | const allocator = testing.allocator; |
| 168 | const data = try DisplayWidthData.init(allocator); | 260 | const dw = try DisplayWidth.init(allocator); |
| 169 | defer data.deinit(allocator); | 261 | defer dw.deinit(allocator); |
| 170 | const self = Self{ .data = &data }; | ||
| 171 | 262 | ||
| 172 | // Input and width both have odd length | 263 | // Input and width both have odd length |
| 173 | var centered = try self.center(allocator, "abc", 9, "*"); | 264 | var centered = try dw.center(allocator, "abc", 9, "*"); |
| 174 | try testing.expectEqualSlices(u8, "***abc***", centered); | 265 | try testing.expectEqualSlices(u8, "***abc***", centered); |
| 175 | 266 | ||
| 176 | // Input and width both have even length | 267 | // Input and width both have even length |
| 177 | testing.allocator.free(centered); | 268 | testing.allocator.free(centered); |
| 178 | centered = try self.center(allocator, "w😊w", 10, "-"); | 269 | centered = try dw.center(allocator, "w😊w", 10, "-"); |
| 179 | try testing.expectEqualSlices(u8, "---w😊w---", centered); | 270 | try testing.expectEqualSlices(u8, "---w😊w---", centered); |
| 180 | 271 | ||
| 181 | // Input has even length, width has odd length | 272 | // Input has even length, width has odd length |
| 182 | testing.allocator.free(centered); | 273 | testing.allocator.free(centered); |
| 183 | centered = try self.center(allocator, "1234", 9, "-"); | 274 | centered = try dw.center(allocator, "1234", 9, "-"); |
| 184 | try testing.expectEqualSlices(u8, "--1234---", centered); | 275 | try testing.expectEqualSlices(u8, "--1234---", centered); |
| 185 | 276 | ||
| 186 | // Input has odd length, width has even length | 277 | // Input has odd length, width has even length |
| 187 | testing.allocator.free(centered); | 278 | testing.allocator.free(centered); |
| 188 | centered = try self.center(allocator, "123", 8, "-"); | 279 | centered = try dw.center(allocator, "123", 8, "-"); |
| 189 | try testing.expectEqualSlices(u8, "--123---", centered); | 280 | try testing.expectEqualSlices(u8, "--123---", centered); |
| 190 | 281 | ||
| 191 | // Input is the same length as the width | 282 | // Input is the same length as the width |
| 192 | testing.allocator.free(centered); | 283 | testing.allocator.free(centered); |
| 193 | centered = try self.center(allocator, "123", 3, "-"); | 284 | centered = try dw.center(allocator, "123", 3, "-"); |
| 194 | try testing.expectEqualSlices(u8, "123", centered); | 285 | try testing.expectEqualSlices(u8, "123", centered); |
| 195 | 286 | ||
| 196 | // Input is empty | 287 | // Input is empty |
| 197 | testing.allocator.free(centered); | 288 | testing.allocator.free(centered); |
| 198 | centered = try self.center(allocator, "", 3, "-"); | 289 | centered = try dw.center(allocator, "", 3, "-"); |
| 199 | try testing.expectEqualSlices(u8, "---", centered); | 290 | try testing.expectEqualSlices(u8, "---", centered); |
| 200 | 291 | ||
| 201 | // Input is empty and width is zero | 292 | // Input is empty and width is zero |
| 202 | testing.allocator.free(centered); | 293 | testing.allocator.free(centered); |
| 203 | centered = try self.center(allocator, "", 0, "-"); | 294 | centered = try dw.center(allocator, "", 0, "-"); |
| 204 | try testing.expectEqualSlices(u8, "", centered); | 295 | try testing.expectEqualSlices(u8, "", centered); |
| 205 | 296 | ||
| 206 | // Input is longer than the width, which is an error | 297 | // Input is longer than the width, which is an error |
| 207 | testing.allocator.free(centered); | 298 | testing.allocator.free(centered); |
| 208 | try testing.expectError(error.StrTooLong, self.center(allocator, "123", 2, "-")); | 299 | try testing.expectError(error.StrTooLong, dw.center(allocator, "123", 2, "-")); |
| 209 | } | 300 | } |
| 210 | 301 | ||
| 211 | /// padLeft returns a new string of width `total_width` (in display cells) using `pad` as padding | 302 | /// padLeft returns a new string of width `total_width` (in display cells) using `pad` as padding |
| 212 | /// on the left side. Caller must free returned bytes with `allocator`. | 303 | /// on the left side. Caller must free returned bytes with `allocator`. |
| 213 | pub fn padLeft( | 304 | pub fn padLeft( |
| 214 | self: Self, | 305 | dw: DisplayWidth, |
| 215 | allocator: mem.Allocator, | 306 | allocator: mem.Allocator, |
| 216 | str: []const u8, | 307 | str: []const u8, |
| 217 | total_width: usize, | 308 | total_width: usize, |
| 218 | pad: []const u8, | 309 | pad: []const u8, |
| 219 | ) ![]u8 { | 310 | ) ![]u8 { |
| 220 | const str_width = self.strWidth(str); | 311 | const str_width = dw.strWidth(str); |
| 221 | if (str_width > total_width) return error.StrTooLong; | 312 | if (str_width > total_width) return error.StrTooLong; |
| 222 | 313 | ||
| 223 | const pad_width = self.strWidth(pad); | 314 | const pad_width = dw.strWidth(pad); |
| 224 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; | 315 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; |
| 225 | 316 | ||
| 226 | const margin_width = total_width - str_width; | 317 | const margin_width = total_width - str_width; |
| @@ -244,32 +335,31 @@ pub fn padLeft( | |||
| 244 | 335 | ||
| 245 | test "padLeft" { | 336 | test "padLeft" { |
| 246 | const allocator = testing.allocator; | 337 | const allocator = testing.allocator; |
| 247 | const data = try DisplayWidthData.init(allocator); | 338 | const dw = try DisplayWidth.init(allocator); |
| 248 | defer data.deinit(allocator); | 339 | defer dw.deinit(allocator); |
| 249 | const self = Self{ .data = &data }; | ||
| 250 | 340 | ||
| 251 | var right_aligned = try self.padLeft(allocator, "abc", 9, "*"); | 341 | var right_aligned = try dw.padLeft(allocator, "abc", 9, "*"); |
| 252 | defer testing.allocator.free(right_aligned); | 342 | defer testing.allocator.free(right_aligned); |
| 253 | try testing.expectEqualSlices(u8, "******abc", right_aligned); | 343 | try testing.expectEqualSlices(u8, "******abc", right_aligned); |
| 254 | 344 | ||
| 255 | testing.allocator.free(right_aligned); | 345 | testing.allocator.free(right_aligned); |
| 256 | right_aligned = try self.padLeft(allocator, "w😊w", 10, "-"); | 346 | right_aligned = try dw.padLeft(allocator, "w😊w", 10, "-"); |
| 257 | try testing.expectEqualSlices(u8, "------w😊w", right_aligned); | 347 | try testing.expectEqualSlices(u8, "------w😊w", right_aligned); |
| 258 | } | 348 | } |
| 259 | 349 | ||
| 260 | /// padRight returns a new string of width `total_width` (in display cells) using `pad` as padding | 350 | /// padRight returns a new string of width `total_width` (in display cells) using `pad` as padding |
| 261 | /// on the right side. Caller must free returned bytes with `allocator`. | 351 | /// on the right side. Caller must free returned bytes with `allocator`. |
| 262 | pub fn padRight( | 352 | pub fn padRight( |
| 263 | self: Self, | 353 | dw: DisplayWidth, |
| 264 | allocator: mem.Allocator, | 354 | allocator: mem.Allocator, |
| 265 | str: []const u8, | 355 | str: []const u8, |
| 266 | total_width: usize, | 356 | total_width: usize, |
| 267 | pad: []const u8, | 357 | pad: []const u8, |
| 268 | ) ![]u8 { | 358 | ) ![]u8 { |
| 269 | const str_width = self.strWidth(str); | 359 | const str_width = dw.strWidth(str); |
| 270 | if (str_width > total_width) return error.StrTooLong; | 360 | if (str_width > total_width) return error.StrTooLong; |
| 271 | 361 | ||
| 272 | const pad_width = self.strWidth(pad); | 362 | const pad_width = dw.strWidth(pad); |
| 273 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; | 363 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; |
| 274 | 364 | ||
| 275 | const margin_width = total_width - str_width; | 365 | const margin_width = total_width - str_width; |
| @@ -294,16 +384,15 @@ pub fn padRight( | |||
| 294 | 384 | ||
| 295 | test "padRight" { | 385 | test "padRight" { |
| 296 | const allocator = testing.allocator; | 386 | const allocator = testing.allocator; |
| 297 | const data = try DisplayWidthData.init(allocator); | 387 | const dw = try DisplayWidth.init(allocator); |
| 298 | defer data.deinit(allocator); | 388 | defer dw.deinit(allocator); |
| 299 | const self = Self{ .data = &data }; | ||
| 300 | 389 | ||
| 301 | var left_aligned = try self.padRight(allocator, "abc", 9, "*"); | 390 | var left_aligned = try dw.padRight(allocator, "abc", 9, "*"); |
| 302 | defer testing.allocator.free(left_aligned); | 391 | defer testing.allocator.free(left_aligned); |
| 303 | try testing.expectEqualSlices(u8, "abc******", left_aligned); | 392 | try testing.expectEqualSlices(u8, "abc******", left_aligned); |
| 304 | 393 | ||
| 305 | testing.allocator.free(left_aligned); | 394 | testing.allocator.free(left_aligned); |
| 306 | left_aligned = try self.padRight(allocator, "w😊w", 10, "-"); | 395 | left_aligned = try dw.padRight(allocator, "w😊w", 10, "-"); |
| 307 | try testing.expectEqualSlices(u8, "w😊w------", left_aligned); | 396 | try testing.expectEqualSlices(u8, "w😊w------", left_aligned); |
| 308 | } | 397 | } |
| 309 | 398 | ||
| @@ -311,7 +400,7 @@ test "padRight" { | |||
| 311 | /// `threshold` defines how far the last column of the last word can be | 400 | /// `threshold` defines how far the last column of the last word can be |
| 312 | /// from the edge. Caller must free returned bytes with `allocator`. | 401 | /// from the edge. Caller must free returned bytes with `allocator`. |
| 313 | pub fn wrap( | 402 | pub fn wrap( |
| 314 | self: Self, | 403 | dw: DisplayWidth, |
| 315 | allocator: mem.Allocator, | 404 | allocator: mem.Allocator, |
| 316 | str: []const u8, | 405 | str: []const u8, |
| 317 | columns: usize, | 406 | columns: usize, |
| @@ -329,7 +418,7 @@ pub fn wrap( | |||
| 329 | while (word_iter.next()) |word| { | 418 | while (word_iter.next()) |word| { |
| 330 | try result.appendSlice(word); | 419 | try result.appendSlice(word); |
| 331 | try result.append(' '); | 420 | try result.append(' '); |
| 332 | line_width += self.strWidth(word) + 1; | 421 | line_width += dw.strWidth(word) + 1; |
| 333 | 422 | ||
| 334 | if (line_width > columns or columns - line_width <= threshold) { | 423 | if (line_width > columns or columns - line_width <= threshold) { |
| 335 | try result.append('\n'); | 424 | try result.append('\n'); |
| @@ -347,12 +436,11 @@ pub fn wrap( | |||
| 347 | 436 | ||
| 348 | test "wrap" { | 437 | test "wrap" { |
| 349 | const allocator = testing.allocator; | 438 | const allocator = testing.allocator; |
| 350 | const data = try DisplayWidthData.init(allocator); | 439 | const dw = try DisplayWidth.init(allocator); |
| 351 | defer data.deinit(allocator); | 440 | defer dw.deinit(allocator); |
| 352 | const self = Self{ .data = &data }; | ||
| 353 | 441 | ||
| 354 | const input = "The quick brown fox\r\njumped over the lazy dog!"; | 442 | const input = "The quick brown fox\r\njumped over the lazy dog!"; |
| 355 | const got = try self.wrap(allocator, input, 10, 3); | 443 | const got = try dw.wrap(allocator, input, 10, 3); |
| 356 | defer testing.allocator.free(got); | 444 | defer testing.allocator.free(got); |
| 357 | const want = "The quick \nbrown fox \njumped \nover the \nlazy dog!"; | 445 | const want = "The quick \nbrown fox \njumped \nover the \nlazy dog!"; |
| 358 | try testing.expectEqualStrings(want, got); | 446 | try testing.expectEqualStrings(want, got); |