diff options
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); |