diff options
Diffstat (limited to 'src/DisplayWidth.zig')
| -rw-r--r-- | src/DisplayWidth.zig | 289 |
1 files changed, 105 insertions, 184 deletions
diff --git a/src/DisplayWidth.zig b/src/DisplayWidth.zig index dee7ebd..767c985 100644 --- a/src/DisplayWidth.zig +++ b/src/DisplayWidth.zig | |||
| @@ -3,112 +3,66 @@ | |||
| 3 | //! Answers questions about the printable width in monospaced fonts of the | 3 | //! Answers questions about the printable width in monospaced fonts of the |
| 4 | //! string of interest. | 4 | //! string of interest. |
| 5 | 5 | ||
| 6 | graphemes: Graphemes = undefined, | 6 | const Data = struct { |
| 7 | s1: []u16 = undefined, | 7 | s1: []const u16 = undefined, |
| 8 | s2: []i4 = undefined, | 8 | s2: []const i4 = undefined, |
| 9 | owns_graphemes: bool = true, | 9 | }; |
| 10 | |||
| 11 | const display_width = display_width: { | ||
| 12 | const data = @import("dwp"); | ||
| 13 | break :display_width Data{ | ||
| 14 | .s1 = &data.s1, | ||
| 15 | .s2 = &data.s2, | ||
| 16 | }; | ||
| 17 | }; | ||
| 10 | 18 | ||
| 11 | const DisplayWidth = @This(); | 19 | const DisplayWidth = @This(); |
| 12 | 20 | ||
| 13 | pub fn init(allocator: Allocator) Allocator.Error!DisplayWidth { | ||
| 14 | var dw = DisplayWidth{}; | ||
| 15 | try dw.setup(allocator); | ||
| 16 | errdefer { | ||
| 17 | allocator.free(dw.s1); | ||
| 18 | allocator.free(dw.s2); | ||
| 19 | } | ||
| 20 | dw.owns_graphemes = true; | ||
| 21 | dw.graphemes = try Graphemes.init(allocator); | ||
| 22 | errdefer dw.graphemes.deinit(allocator); | ||
| 23 | return dw; | ||
| 24 | } | ||
| 25 | |||
| 26 | pub fn initWithGraphemes(allocator: Allocator, graphemes: Graphemes) Allocator.Error!DisplayWidth { | ||
| 27 | var dw = DisplayWidth{}; | ||
| 28 | try dw.setup(allocator); | ||
| 29 | dw.graphemes = graphemes; | ||
| 30 | dw.owns_graphemes = false; | ||
| 31 | return dw; | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn setupWithGraphemes(dw: *DisplayWidth, allocator: Allocator, graphemes: Graphemes) Allocator.Error!void { | ||
| 35 | try dw.setup(allocator); | ||
| 36 | dw.graphemes = graphemes; | ||
| 37 | dw.owns_graphemes = false; | ||
| 38 | } | ||
| 39 | |||
| 40 | // Sets up the DisplayWidthData, leaving the GraphemeData undefined. | ||
| 41 | pub fn setup(dw: *DisplayWidth, allocator: Allocator) Allocator.Error!void { | ||
| 42 | const in_bytes = @embedFile("dwp"); | ||
| 43 | var in_fbs = std.io.fixedBufferStream(in_bytes); | ||
| 44 | var reader = in_fbs.reader(); | ||
| 45 | |||
| 46 | const endian = builtin.cpu.arch.endian(); | ||
| 47 | |||
| 48 | const stage_1_len: u16 = reader.readInt(u16, endian) catch unreachable; | ||
| 49 | dw.s1 = try allocator.alloc(u16, stage_1_len); | ||
| 50 | errdefer allocator.free(dw.s1); | ||
| 51 | for (0..stage_1_len) |i| dw.s1[i] = reader.readInt(u16, endian) catch unreachable; | ||
| 52 | |||
| 53 | const stage_2_len: u16 = reader.readInt(u16, endian) catch unreachable; | ||
| 54 | dw.s2 = try allocator.alloc(i4, stage_2_len); | ||
| 55 | errdefer allocator.free(dw.s2); | ||
| 56 | for (0..stage_2_len) |i| dw.s2[i] = @intCast(reader.readInt(i8, endian) catch unreachable); | ||
| 57 | } | ||
| 58 | |||
| 59 | pub fn deinit(dw: *const DisplayWidth, allocator: Allocator) void { | ||
| 60 | allocator.free(dw.s1); | ||
| 61 | allocator.free(dw.s2); | ||
| 62 | if (dw.owns_graphemes) dw.graphemes.deinit(allocator); | ||
| 63 | } | ||
| 64 | |||
| 65 | /// codePointWidth returns the number of cells `cp` requires when rendered | 21 | /// codePointWidth returns the number of cells `cp` requires when rendered |
| 66 | /// in a fixed-pitch font (i.e. a terminal screen). This can range from -1 to | 22 | /// in a fixed-pitch font (i.e. a terminal screen). This can range from -1 to |
| 67 | /// 3, where BACKSPACE and DELETE return -1 and 3-em-dash returns 3. C0/C1 | 23 | /// 3, where BACKSPACE and DELETE return -1 and 3-em-dash returns 3. C0/C1 |
| 68 | /// control codes return 0. If `cjk` is true, ambiguous code points return 2, | 24 | /// control codes return 0. If `cjk` is true, ambiguous code points return 2, |
| 69 | /// otherwise they return 1. | 25 | /// otherwise they return 1. |
| 70 | pub fn codePointWidth(dw: DisplayWidth, cp: u21) i4 { | 26 | pub fn codePointWidth(cp: u21) i4 { |
| 71 | return dw.s2[dw.s1[cp >> 8] + (cp & 0xff)]; | 27 | return display_width.s2[display_width.s1[cp >> 8] + (cp & 0xff)]; |
| 72 | } | 28 | } |
| 73 | 29 | ||
| 74 | test "codePointWidth" { | 30 | test "codePointWidth" { |
| 75 | const dw = try DisplayWidth.init(std.testing.allocator); | 31 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x0000)); // null |
| 76 | defer dw.deinit(std.testing.allocator); | 32 | try testing.expectEqual(@as(i4, -1), DisplayWidth.codePointWidth(0x8)); // \b |
| 77 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0000)); // null | 33 | try testing.expectEqual(@as(i4, -1), DisplayWidth.codePointWidth(0x7f)); // DEL |
| 78 | try testing.expectEqual(@as(i4, -1), dw.codePointWidth(0x8)); // \b | 34 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x0005)); // Cf |
| 79 | try testing.expectEqual(@as(i4, -1), dw.codePointWidth(0x7f)); // DEL | 35 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x0007)); // \a BEL |
| 80 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0005)); // Cf | 36 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x000A)); // \n LF |
| 81 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0007)); // \a BEL | 37 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x000B)); // \v VT |
| 82 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000A)); // \n LF | 38 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x000C)); // \f FF |
| 83 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000B)); // \v VT | 39 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x000D)); // \r CR |
| 84 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000C)); // \f FF | 40 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x000E)); // SQ |
| 85 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000D)); // \r CR | 41 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x000F)); // SI |
| 86 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000E)); // SQ | 42 | |
| 87 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000F)); // SI | 43 | try testing.expectEqual(@as(i4, 0), DisplayWidth.codePointWidth(0x070F)); // Cf |
| 88 | 44 | try testing.expectEqual(@as(i4, 1), DisplayWidth.codePointWidth(0x0603)); // Cf Arabic | |
| 89 | try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x070F)); // Cf | 45 | |
| 90 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x0603)); // Cf Arabic | 46 | try testing.expectEqual(@as(i4, 1), DisplayWidth.codePointWidth(0x00AD)); // soft-hyphen |
| 91 | 47 | try testing.expectEqual(@as(i4, 2), DisplayWidth.codePointWidth(0x2E3A)); // two-em dash | |
| 92 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x00AD)); // soft-hyphen | 48 | try testing.expectEqual(@as(i4, 3), DisplayWidth.codePointWidth(0x2E3B)); // three-em dash |
| 93 | try testing.expectEqual(@as(i4, 2), dw.codePointWidth(0x2E3A)); // two-em dash | 49 | |
| 94 | try testing.expectEqual(@as(i4, 3), dw.codePointWidth(0x2E3B)); // three-em dash | 50 | try testing.expectEqual(@as(i4, 1), DisplayWidth.codePointWidth(0x00BD)); // ambiguous halfwidth |
| 95 | 51 | ||
| 96 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x00BD)); // ambiguous halfwidth | 52 | try testing.expectEqual(@as(i4, 1), DisplayWidth.codePointWidth('é')); |
| 97 | 53 | try testing.expectEqual(@as(i4, 2), DisplayWidth.codePointWidth('😊')); | |
| 98 | try testing.expectEqual(@as(i4, 1), dw.codePointWidth('é')); | 54 | try testing.expectEqual(@as(i4, 2), DisplayWidth.codePointWidth('统')); |
| 99 | try testing.expectEqual(@as(i4, 2), dw.codePointWidth('😊')); | ||
| 100 | try testing.expectEqual(@as(i4, 2), dw.codePointWidth('统')); | ||
| 101 | } | 55 | } |
| 102 | 56 | ||
| 103 | /// graphemeClusterWidth returns the total display width of `gc` as the number | 57 | /// graphemeClusterWidth returns the total display width of `gc` as the number |
| 104 | /// of cells required in a fixed-pitch font (i.e. a terminal screen). | 58 | /// of cells required in a fixed-pitch font (i.e. a terminal screen). |
| 105 | /// `gc` is a slice corresponding to one grapheme cluster. | 59 | /// `gc` is a slice corresponding to one grapheme cluster. |
| 106 | pub fn graphemeClusterWidth(dw: DisplayWidth, gc: []const u8) isize { | 60 | pub fn graphemeClusterWidth(gc: []const u8) isize { |
| 107 | var cp_iter = CodePointIterator{ .bytes = gc }; | 61 | var cp_iter = CodePointIterator{ .bytes = gc }; |
| 108 | var gc_total: isize = 0; | 62 | var gc_total: isize = 0; |
| 109 | 63 | ||
| 110 | while (cp_iter.next()) |cp| { | 64 | while (cp_iter.next()) |cp| { |
| 111 | var w = dw.codePointWidth(cp.code); | 65 | var w = DisplayWidth.codePointWidth(cp.code); |
| 112 | 66 | ||
| 113 | if (w != 0) { | 67 | if (w != 0) { |
| 114 | // Handle text emoji sequence. | 68 | // Handle text emoji sequence. |
| @@ -130,59 +84,57 @@ pub fn graphemeClusterWidth(dw: DisplayWidth, gc: []const u8) isize { | |||
| 130 | 84 | ||
| 131 | /// strWidth returns the total display width of `str` as the number of cells | 85 | /// strWidth returns the total display width of `str` as the number of cells |
| 132 | /// required in a fixed-pitch font (i.e. a terminal screen). | 86 | /// required in a fixed-pitch font (i.e. a terminal screen). |
| 133 | pub fn strWidth(dw: DisplayWidth, str: []const u8) usize { | 87 | pub fn strWidth(str: []const u8) usize { |
| 134 | var total: isize = 0; | 88 | var total: isize = 0; |
| 135 | 89 | ||
| 136 | // ASCII fast path | 90 | // ASCII fast path |
| 137 | if (ascii.isAsciiOnly(str)) { | 91 | if (ascii.isAsciiOnly(str)) { |
| 138 | for (str) |b| total += dw.codePointWidth(b); | 92 | for (str) |b| total += DisplayWidth.codePointWidth(b); |
| 139 | return @intCast(@max(0, total)); | 93 | return @intCast(@max(0, total)); |
| 140 | } | 94 | } |
| 141 | 95 | ||
| 142 | var giter = dw.graphemes.iterator(str); | 96 | var giter = Graphemes.iterator(str); |
| 143 | 97 | ||
| 144 | while (giter.next()) |gc| { | 98 | while (giter.next()) |gc| { |
| 145 | total += dw.graphemeClusterWidth(gc.bytes(str)); | 99 | total += DisplayWidth.graphemeClusterWidth(gc.bytes(str)); |
| 146 | } | 100 | } |
| 147 | 101 | ||
| 148 | return @intCast(@max(0, total)); | 102 | return @intCast(@max(0, total)); |
| 149 | } | 103 | } |
| 150 | 104 | ||
| 151 | test "strWidth" { | 105 | test "strWidth" { |
| 152 | const dw = try DisplayWidth.init(testing.allocator); | ||
| 153 | defer dw.deinit(testing.allocator); | ||
| 154 | const c0 = options.c0_width orelse 0; | 106 | const c0 = options.c0_width orelse 0; |
| 155 | 107 | ||
| 156 | try testing.expectEqual(@as(usize, 5), dw.strWidth("Hello\r\n")); | 108 | try testing.expectEqual(@as(usize, 5), DisplayWidth.strWidth("Hello\r\n")); |
| 157 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{0065}\u{0301}")); | 109 | try testing.expectEqual(@as(usize, 1), DisplayWidth.strWidth("\u{0065}\u{0301}")); |
| 158 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{1F476}\u{1F3FF}\u{0308}\u{200D}\u{1F476}\u{1F3FF}")); | 110 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth("\u{1F476}\u{1F3FF}\u{0308}\u{200D}\u{1F476}\u{1F3FF}")); |
| 159 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Hello 😊")); | 111 | try testing.expectEqual(@as(usize, 8), DisplayWidth.strWidth("Hello 😊")); |
| 160 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo 😊")); | 112 | try testing.expectEqual(@as(usize, 8), DisplayWidth.strWidth("Héllo 😊")); |
| 161 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo :)")); | 113 | try testing.expectEqual(@as(usize, 8), DisplayWidth.strWidth("Héllo :)")); |
| 162 | try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo 🇪🇸")); | 114 | try testing.expectEqual(@as(usize, 8), DisplayWidth.strWidth("Héllo 🇪🇸")); |
| 163 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{26A1}")); // Lone emoji | 115 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth("\u{26A1}")); // Lone emoji |
| 164 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{26A1}\u{FE0E}")); // Text sequence | 116 | try testing.expectEqual(@as(usize, 1), DisplayWidth.strWidth("\u{26A1}\u{FE0E}")); // Text sequence |
| 165 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{26A1}\u{FE0F}")); // Presentation sequence | 117 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth("\u{26A1}\u{FE0F}")); // Presentation sequence |
| 166 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{2764}")); // Default text presentation | 118 | try testing.expectEqual(@as(usize, 1), DisplayWidth.strWidth("\u{2764}")); // Default text presentation |
| 167 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{2764}\u{FE0E}")); // Default text presentation with VS15 selector | 119 | try testing.expectEqual(@as(usize, 1), DisplayWidth.strWidth("\u{2764}\u{FE0E}")); // Default text presentation with VS15 selector |
| 168 | try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{2764}\u{FE0F}")); // Default text presentation with VS16 selector | 120 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth("\u{2764}\u{FE0F}")); // Default text presentation with VS16 selector |
| 169 | const expect_bs: usize = if (c0 == 0) 0 else 1 + c0; | 121 | const expect_bs: usize = if (c0 == 0) 0 else 1 + c0; |
| 170 | try testing.expectEqual(expect_bs, dw.strWidth("A\x08")); // Backspace | 122 | try testing.expectEqual(expect_bs, DisplayWidth.strWidth("A\x08")); // Backspace |
| 171 | try testing.expectEqual(expect_bs, dw.strWidth("\x7FA")); // DEL | 123 | try testing.expectEqual(expect_bs, DisplayWidth.strWidth("\x7FA")); // DEL |
| 172 | const expect_long_del: usize = if (c0 == 0) 0 else 1 + (c0 * 3); | 124 | const expect_long_del: usize = if (c0 == 0) 0 else 1 + (c0 * 3); |
| 173 | try testing.expectEqual(expect_long_del, dw.strWidth("\x7FA\x08\x08")); // never less than 0 | 125 | try testing.expectEqual(expect_long_del, DisplayWidth.strWidth("\x7FA\x08\x08")); // never less than 0 |
| 174 | 126 | ||
| 175 | // wcwidth Python lib tests. See: https://github.com/jquast/wcwidth/blob/master/tests/test_core.py | 127 | // wcwidth Python lib tests. See: https://github.com/jquast/wcwidth/blob/master/tests/test_core.py |
| 176 | const empty = ""; | 128 | const empty = ""; |
| 177 | try testing.expectEqual(@as(usize, 0), dw.strWidth(empty)); | 129 | try testing.expectEqual(@as(usize, 0), DisplayWidth.strWidth(empty)); |
| 178 | const with_null = "hello\x00world"; | 130 | const with_null = "hello\x00world"; |
| 179 | try testing.expectEqual(@as(usize, 10 + c0), dw.strWidth(with_null)); | 131 | try testing.expectEqual(@as(usize, 10 + c0), DisplayWidth.strWidth(with_null)); |
| 180 | const hello_jp = "コンニチハ, セカイ!"; | 132 | const hello_jp = "コンニチハ, セカイ!"; |
| 181 | try testing.expectEqual(@as(usize, 19), dw.strWidth(hello_jp)); | 133 | try testing.expectEqual(@as(usize, 19), DisplayWidth.strWidth(hello_jp)); |
| 182 | const control = "\x1b[0m"; | 134 | const control = "\x1b[0m"; |
| 183 | try testing.expectEqual(@as(usize, 3 + c0), dw.strWidth(control)); | 135 | try testing.expectEqual(@as(usize, 3 + c0), DisplayWidth.strWidth(control)); |
| 184 | const balinese = "\u{1B13}\u{1B28}\u{1B2E}\u{1B44}"; | 136 | const balinese = "\u{1B13}\u{1B28}\u{1B2E}\u{1B44}"; |
| 185 | try testing.expectEqual(@as(usize, 3), dw.strWidth(balinese)); | 137 | try testing.expectEqual(@as(usize, 3), DisplayWidth.strWidth(balinese)); |
| 186 | 138 | ||
| 187 | // These commented out tests require a new specification for complex scripts. | 139 | // These commented out tests require a new specification for complex scripts. |
| 188 | // See: https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdf | 140 | // See: https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdf |
| @@ -196,20 +148,20 @@ test "strWidth" { | |||
| 196 | // try testing.expectEqual(@as(usize, 3), strWidth(kannada_1)); | 148 | // try testing.expectEqual(@as(usize, 3), strWidth(kannada_1)); |
| 197 | // The following passes but as a mere coincidence. | 149 | // The following passes but as a mere coincidence. |
| 198 | const kannada_2 = "\u{0cb0}\u{0cbc}\u{0ccd}\u{0c9a}"; | 150 | const kannada_2 = "\u{0cb0}\u{0cbc}\u{0ccd}\u{0c9a}"; |
| 199 | try testing.expectEqual(@as(usize, 2), dw.strWidth(kannada_2)); | 151 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth(kannada_2)); |
| 200 | 152 | ||
| 201 | // From Rust https://github.com/jameslanska/unicode-display-width | 153 | // From Rust https://github.com/jameslanska/unicode-display-width |
| 202 | try testing.expectEqual(@as(usize, 15), dw.strWidth("🔥🗡🍩👩🏻🚀⏰💃🏼🔦👍🏻")); | 154 | try testing.expectEqual(@as(usize, 15), DisplayWidth.strWidth("🔥🗡🍩👩🏻🚀⏰💃🏼🔦👍🏻")); |
| 203 | try testing.expectEqual(@as(usize, 2), dw.strWidth("🦀")); | 155 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth("🦀")); |
| 204 | try testing.expectEqual(@as(usize, 2), dw.strWidth("👨👩👧👧")); | 156 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth("👨👩👧👧")); |
| 205 | try testing.expectEqual(@as(usize, 2), dw.strWidth("👩🔬")); | 157 | try testing.expectEqual(@as(usize, 2), DisplayWidth.strWidth("👩🔬")); |
| 206 | try testing.expectEqual(@as(usize, 9), dw.strWidth("sane text")); | 158 | try testing.expectEqual(@as(usize, 9), DisplayWidth.strWidth("sane text")); |
| 207 | try testing.expectEqual(@as(usize, 9), dw.strWidth("Ẓ̌á̲l͔̝̞̄̑͌g̖̘̘̔̔͢͞͝o̪̔T̢̙̫̈̍͞e̬͈͕͌̏͑x̺̍ṭ̓̓ͅ")); | 159 | try testing.expectEqual(@as(usize, 9), DisplayWidth.strWidth("Ẓ̌á̲l͔̝̞̄̑͌g̖̘̘̔̔͢͞͝o̪̔T̢̙̫̈̍͞e̬͈͕͌̏͑x̺̍ṭ̓̓ͅ")); |
| 208 | try testing.expectEqual(@as(usize, 17), dw.strWidth("슬라바 우크라이나")); | 160 | try testing.expectEqual(@as(usize, 17), DisplayWidth.strWidth("슬라바 우크라이나")); |
| 209 | try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{378}")); | 161 | try testing.expectEqual(@as(usize, 1), DisplayWidth.strWidth("\u{378}")); |
| 210 | 162 | ||
| 211 | // https://codeberg.org/atman/zg/issues/82 | 163 | // https://codeberg.org/atman/zg/issues/82 |
| 212 | try testing.expectEqual(@as(usize, 12), dw.strWidth("✍️✍🏻✍🏼✍🏽✍🏾✍🏿")); | 164 | try testing.expectEqual(@as(usize, 12), DisplayWidth.strWidth("✍️✍🏻✍🏼✍🏽✍🏾✍🏿")); |
| 213 | } | 165 | } |
| 214 | 166 | ||
| 215 | /// centers `str` in a new string of width `total_width` (in display cells) using `pad` as padding. | 167 | /// centers `str` in a new string of width `total_width` (in display cells) using `pad` as padding. |
| @@ -217,17 +169,16 @@ test "strWidth" { | |||
| 217 | /// receive one additional pad. This makes sure the returned string fills the requested width. | 169 | /// receive one additional pad. This makes sure the returned string fills the requested width. |
| 218 | /// Caller must free returned bytes with `allocator`. | 170 | /// Caller must free returned bytes with `allocator`. |
| 219 | pub fn center( | 171 | pub fn center( |
| 220 | dw: DisplayWidth, | ||
| 221 | allocator: mem.Allocator, | 172 | allocator: mem.Allocator, |
| 222 | str: []const u8, | 173 | str: []const u8, |
| 223 | total_width: usize, | 174 | total_width: usize, |
| 224 | pad: []const u8, | 175 | pad: []const u8, |
| 225 | ) ![]u8 { | 176 | ) ![]u8 { |
| 226 | const str_width = dw.strWidth(str); | 177 | const str_width = DisplayWidth.strWidth(str); |
| 227 | if (str_width > total_width) return error.StrTooLong; | 178 | if (str_width > total_width) return error.StrTooLong; |
| 228 | if (str_width == total_width) return try allocator.dupe(u8, str); | 179 | if (str_width == total_width) return try allocator.dupe(u8, str); |
| 229 | 180 | ||
| 230 | const pad_width = dw.strWidth(pad); | 181 | const pad_width = DisplayWidth.strWidth(pad); |
| 231 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; | 182 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; |
| 232 | 183 | ||
| 233 | const margin_width = @divFloor((total_width - str_width), 2); | 184 | const margin_width = @divFloor((total_width - str_width), 2); |
| @@ -258,61 +209,58 @@ pub fn center( | |||
| 258 | 209 | ||
| 259 | test "center" { | 210 | test "center" { |
| 260 | const allocator = testing.allocator; | 211 | const allocator = testing.allocator; |
| 261 | const dw = try DisplayWidth.init(allocator); | ||
| 262 | defer dw.deinit(allocator); | ||
| 263 | 212 | ||
| 264 | // Input and width both have odd length | 213 | // Input and width both have odd length |
| 265 | var centered = try dw.center(allocator, "abc", 9, "*"); | 214 | var centered = try DisplayWidth.center(allocator, "abc", 9, "*"); |
| 266 | try testing.expectEqualSlices(u8, "***abc***", centered); | 215 | try testing.expectEqualSlices(u8, "***abc***", centered); |
| 267 | 216 | ||
| 268 | // Input and width both have even length | 217 | // Input and width both have even length |
| 269 | testing.allocator.free(centered); | 218 | allocator.free(centered); |
| 270 | centered = try dw.center(allocator, "w😊w", 10, "-"); | 219 | centered = try DisplayWidth.center(allocator, "w😊w", 10, "-"); |
| 271 | try testing.expectEqualSlices(u8, "---w😊w---", centered); | 220 | try testing.expectEqualSlices(u8, "---w😊w---", centered); |
| 272 | 221 | ||
| 273 | // Input has even length, width has odd length | 222 | // Input has even length, width has odd length |
| 274 | testing.allocator.free(centered); | 223 | allocator.free(centered); |
| 275 | centered = try dw.center(allocator, "1234", 9, "-"); | 224 | centered = try DisplayWidth.center(allocator, "1234", 9, "-"); |
| 276 | try testing.expectEqualSlices(u8, "--1234---", centered); | 225 | try testing.expectEqualSlices(u8, "--1234---", centered); |
| 277 | 226 | ||
| 278 | // Input has odd length, width has even length | 227 | // Input has odd length, width has even length |
| 279 | testing.allocator.free(centered); | 228 | allocator.free(centered); |
| 280 | centered = try dw.center(allocator, "123", 8, "-"); | 229 | centered = try DisplayWidth.center(allocator, "123", 8, "-"); |
| 281 | try testing.expectEqualSlices(u8, "--123---", centered); | 230 | try testing.expectEqualSlices(u8, "--123---", centered); |
| 282 | 231 | ||
| 283 | // Input is the same length as the width | 232 | // Input is the same length as the width |
| 284 | testing.allocator.free(centered); | 233 | allocator.free(centered); |
| 285 | centered = try dw.center(allocator, "123", 3, "-"); | 234 | centered = try DisplayWidth.center(allocator, "123", 3, "-"); |
| 286 | try testing.expectEqualSlices(u8, "123", centered); | 235 | try testing.expectEqualSlices(u8, "123", centered); |
| 287 | 236 | ||
| 288 | // Input is empty | 237 | // Input is empty |
| 289 | testing.allocator.free(centered); | 238 | allocator.free(centered); |
| 290 | centered = try dw.center(allocator, "", 3, "-"); | 239 | centered = try DisplayWidth.center(allocator, "", 3, "-"); |
| 291 | try testing.expectEqualSlices(u8, "---", centered); | 240 | try testing.expectEqualSlices(u8, "---", centered); |
| 292 | 241 | ||
| 293 | // Input is empty and width is zero | 242 | // Input is empty and width is zero |
| 294 | testing.allocator.free(centered); | 243 | allocator.free(centered); |
| 295 | centered = try dw.center(allocator, "", 0, "-"); | 244 | centered = try DisplayWidth.center(allocator, "", 0, "-"); |
| 296 | try testing.expectEqualSlices(u8, "", centered); | 245 | try testing.expectEqualSlices(u8, "", centered); |
| 297 | 246 | ||
| 298 | // Input is longer than the width, which is an error | 247 | // Input is longer than the width, which is an error |
| 299 | testing.allocator.free(centered); | 248 | allocator.free(centered); |
| 300 | try testing.expectError(error.StrTooLong, dw.center(allocator, "123", 2, "-")); | 249 | try testing.expectError(error.StrTooLong, DisplayWidth.center(allocator, "123", 2, "-")); |
| 301 | } | 250 | } |
| 302 | 251 | ||
| 303 | /// padLeft returns a new string of width `total_width` (in display cells) using `pad` as padding | 252 | /// padLeft returns a new string of width `total_width` (in display cells) using `pad` as padding |
| 304 | /// on the left side. Caller must free returned bytes with `allocator`. | 253 | /// on the left side. Caller must free returned bytes with `allocator`. |
| 305 | pub fn padLeft( | 254 | pub fn padLeft( |
| 306 | dw: DisplayWidth, | ||
| 307 | allocator: mem.Allocator, | 255 | allocator: mem.Allocator, |
| 308 | str: []const u8, | 256 | str: []const u8, |
| 309 | total_width: usize, | 257 | total_width: usize, |
| 310 | pad: []const u8, | 258 | pad: []const u8, |
| 311 | ) ![]u8 { | 259 | ) ![]u8 { |
| 312 | const str_width = dw.strWidth(str); | 260 | const str_width = DisplayWidth.strWidth(str); |
| 313 | if (str_width > total_width) return error.StrTooLong; | 261 | if (str_width > total_width) return error.StrTooLong; |
| 314 | 262 | ||
| 315 | const pad_width = dw.strWidth(pad); | 263 | const pad_width = DisplayWidth.strWidth(pad); |
| 316 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; | 264 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; |
| 317 | 265 | ||
| 318 | const margin_width = total_width - str_width; | 266 | const margin_width = total_width - str_width; |
| @@ -336,31 +284,28 @@ pub fn padLeft( | |||
| 336 | 284 | ||
| 337 | test "padLeft" { | 285 | test "padLeft" { |
| 338 | const allocator = testing.allocator; | 286 | const allocator = testing.allocator; |
| 339 | const dw = try DisplayWidth.init(allocator); | ||
| 340 | defer dw.deinit(allocator); | ||
| 341 | 287 | ||
| 342 | var right_aligned = try dw.padLeft(allocator, "abc", 9, "*"); | 288 | var right_aligned = try DisplayWidth.padLeft(allocator, "abc", 9, "*"); |
| 343 | defer testing.allocator.free(right_aligned); | 289 | defer allocator.free(right_aligned); |
| 344 | try testing.expectEqualSlices(u8, "******abc", right_aligned); | 290 | try testing.expectEqualSlices(u8, "******abc", right_aligned); |
| 345 | 291 | ||
| 346 | testing.allocator.free(right_aligned); | 292 | allocator.free(right_aligned); |
| 347 | right_aligned = try dw.padLeft(allocator, "w😊w", 10, "-"); | 293 | right_aligned = try DisplayWidth.padLeft(allocator, "w😊w", 10, "-"); |
| 348 | try testing.expectEqualSlices(u8, "------w😊w", right_aligned); | 294 | try testing.expectEqualSlices(u8, "------w😊w", right_aligned); |
| 349 | } | 295 | } |
| 350 | 296 | ||
| 351 | /// padRight returns a new string of width `total_width` (in display cells) using `pad` as padding | 297 | /// padRight returns a new string of width `total_width` (in display cells) using `pad` as padding |
| 352 | /// on the right side. Caller must free returned bytes with `allocator`. | 298 | /// on the right side. Caller must free returned bytes with `allocator`. |
| 353 | pub fn padRight( | 299 | pub fn padRight( |
| 354 | dw: DisplayWidth, | ||
| 355 | allocator: mem.Allocator, | 300 | allocator: mem.Allocator, |
| 356 | str: []const u8, | 301 | str: []const u8, |
| 357 | total_width: usize, | 302 | total_width: usize, |
| 358 | pad: []const u8, | 303 | pad: []const u8, |
| 359 | ) ![]u8 { | 304 | ) ![]u8 { |
| 360 | const str_width = dw.strWidth(str); | 305 | const str_width = DisplayWidth.strWidth(str); |
| 361 | if (str_width > total_width) return error.StrTooLong; | 306 | if (str_width > total_width) return error.StrTooLong; |
| 362 | 307 | ||
| 363 | const pad_width = dw.strWidth(pad); | 308 | const pad_width = DisplayWidth.strWidth(pad); |
| 364 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; | 309 | if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; |
| 365 | 310 | ||
| 366 | const margin_width = total_width - str_width; | 311 | const margin_width = total_width - str_width; |
| @@ -385,15 +330,13 @@ pub fn padRight( | |||
| 385 | 330 | ||
| 386 | test "padRight" { | 331 | test "padRight" { |
| 387 | const allocator = testing.allocator; | 332 | const allocator = testing.allocator; |
| 388 | const dw = try DisplayWidth.init(allocator); | ||
| 389 | defer dw.deinit(allocator); | ||
| 390 | 333 | ||
| 391 | var left_aligned = try dw.padRight(allocator, "abc", 9, "*"); | 334 | var left_aligned = try DisplayWidth.padRight(allocator, "abc", 9, "*"); |
| 392 | defer testing.allocator.free(left_aligned); | 335 | defer testing.allocator.free(left_aligned); |
| 393 | try testing.expectEqualSlices(u8, "abc******", left_aligned); | 336 | try testing.expectEqualSlices(u8, "abc******", left_aligned); |
| 394 | 337 | ||
| 395 | testing.allocator.free(left_aligned); | 338 | testing.allocator.free(left_aligned); |
| 396 | left_aligned = try dw.padRight(allocator, "w😊w", 10, "-"); | 339 | left_aligned = try DisplayWidth.padRight(allocator, "w😊w", 10, "-"); |
| 397 | try testing.expectEqualSlices(u8, "w😊w------", left_aligned); | 340 | try testing.expectEqualSlices(u8, "w😊w------", left_aligned); |
| 398 | } | 341 | } |
| 399 | 342 | ||
| @@ -401,7 +344,6 @@ test "padRight" { | |||
| 401 | /// `threshold` defines how far the last column of the last word can be | 344 | /// `threshold` defines how far the last column of the last word can be |
| 402 | /// from the edge. Caller must free returned bytes with `allocator`. | 345 | /// from the edge. Caller must free returned bytes with `allocator`. |
| 403 | pub fn wrap( | 346 | pub fn wrap( |
| 404 | dw: DisplayWidth, | ||
| 405 | allocator: mem.Allocator, | 347 | allocator: mem.Allocator, |
| 406 | str: []const u8, | 348 | str: []const u8, |
| 407 | columns: usize, | 349 | columns: usize, |
| @@ -419,7 +361,7 @@ pub fn wrap( | |||
| 419 | while (word_iter.next()) |word| { | 361 | while (word_iter.next()) |word| { |
| 420 | try result.appendSlice(word); | 362 | try result.appendSlice(word); |
| 421 | try result.append(' '); | 363 | try result.append(' '); |
| 422 | line_width += dw.strWidth(word) + 1; | 364 | line_width += DisplayWidth.strWidth(word) + 1; |
| 423 | 365 | ||
| 424 | if (line_width > columns or columns - line_width <= threshold) { | 366 | if (line_width > columns or columns - line_width <= threshold) { |
| 425 | try result.append('\n'); | 367 | try result.append('\n'); |
| @@ -439,11 +381,9 @@ pub fn wrap( | |||
| 439 | 381 | ||
| 440 | test "wrap" { | 382 | test "wrap" { |
| 441 | const allocator = testing.allocator; | 383 | const allocator = testing.allocator; |
| 442 | const dw = try DisplayWidth.init(allocator); | ||
| 443 | defer dw.deinit(allocator); | ||
| 444 | 384 | ||
| 445 | const input = "The quick brown fox\r\njumped over the lazy dog!"; | 385 | const input = "The quick brown fox\r\njumped over the lazy dog!"; |
| 446 | const got = try dw.wrap(allocator, input, 10, 3); | 386 | const got = try DisplayWidth.wrap(allocator, input, 10, 3); |
| 447 | defer testing.allocator.free(got); | 387 | defer testing.allocator.free(got); |
| 448 | const want = "The quick \nbrown fox \njumped \nover the \nlazy dog!"; | 388 | const want = "The quick \nbrown fox \njumped \nover the \nlazy dog!"; |
| 449 | try testing.expectEqualStrings(want, got); | 389 | try testing.expectEqualStrings(want, got); |
| @@ -453,31 +393,12 @@ test "zg/74" { | |||
| 453 | var debug_alloc = std.heap.DebugAllocator(.{}).init; | 393 | var debug_alloc = std.heap.DebugAllocator(.{}).init; |
| 454 | const allocator = debug_alloc.allocator(); | 394 | const allocator = debug_alloc.allocator(); |
| 455 | defer _ = debug_alloc.deinit(); | 395 | defer _ = debug_alloc.deinit(); |
| 456 | const dw = try DisplayWidth.init(allocator); | 396 | const wrapped = try DisplayWidth.wrap(allocator, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pellentesque pulvinar felis, sit amet commodo ligula feugiat sed. Sed quis malesuada elit, nec eleifend lectus. Sed tincidunt finibus aliquet. Praesent consectetur nibh libero, tempus imperdiet lorem congue eget.", 16, 1); |
| 457 | defer dw.deinit(allocator); | ||
| 458 | const wrapped = try dw.wrap(allocator, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pellentesque pulvinar felis, sit amet commodo ligula feugiat sed. Sed quis malesuada elit, nec eleifend lectus. Sed tincidunt finibus aliquet. Praesent consectetur nibh libero, tempus imperdiet lorem congue eget.", 16, 1); | ||
| 459 | defer allocator.free(wrapped); | 397 | defer allocator.free(wrapped); |
| 460 | const expected_wrap = "Lorem ipsum dolor \nsit amet, consectetur \nadipiscing elit. \nNullam pellentesque \npulvinar felis, \nsit amet commodo \nligula feugiat \nsed. Sed quis malesuada \nelit, nec eleifend \nlectus. Sed tincidunt \nfinibus aliquet. \nPraesent consectetur \nnibh libero, tempus \nimperdiet lorem \ncongue eget."; | 398 | const expected_wrap = "Lorem ipsum dolor \nsit amet, consectetur \nadipiscing elit. \nNullam pellentesque \npulvinar felis, \nsit amet commodo \nligula feugiat \nsed. Sed quis malesuada \nelit, nec eleifend \nlectus. Sed tincidunt \nfinibus aliquet. \nPraesent consectetur \nnibh libero, tempus \nimperdiet lorem \ncongue eget."; |
| 461 | try std.testing.expectEqualStrings(expected_wrap, wrapped); | 399 | try std.testing.expectEqualStrings(expected_wrap, wrapped); |
| 462 | } | 400 | } |
| 463 | 401 | ||
| 464 | fn testAllocation(allocator: Allocator) !void { | ||
| 465 | { | ||
| 466 | var dw = try DisplayWidth.init(allocator); | ||
| 467 | dw.deinit(allocator); | ||
| 468 | } | ||
| 469 | { | ||
| 470 | var graph = try Graphemes.init(allocator); | ||
| 471 | defer graph.deinit(allocator); | ||
| 472 | var dw = try DisplayWidth.initWithGraphemes(allocator, graph); | ||
| 473 | dw.deinit(allocator); | ||
| 474 | } | ||
| 475 | } | ||
| 476 | |||
| 477 | test "allocation test" { | ||
| 478 | try testing.checkAllAllocationFailures(testing.allocator, testAllocation, .{}); | ||
| 479 | } | ||
| 480 | |||
| 481 | const std = @import("std"); | 402 | const std = @import("std"); |
| 482 | const builtin = @import("builtin"); | 403 | const builtin = @import("builtin"); |
| 483 | const options = @import("options"); | 404 | const options = @import("options"); |