summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig1
-rw-r--r--src/DisplayWidth.zig289
2 files changed, 106 insertions, 184 deletions
diff --git a/build.zig b/build.zig
index aab8516..0e3d5aa 100644
--- a/build.zig
+++ b/build.zig
@@ -516,6 +516,7 @@ pub fn build(b: *std.Build) void {
516 516
517 const test_unicode_step = b.step("unicode", "Rune unicode tests"); 517 const test_unicode_step = b.step("unicode", "Rune unicode tests");
518 test_unicode_step.dependOn(&run_unicode_tests.step); 518 test_unicode_step.dependOn(&run_unicode_tests.step);
519 test_unicode_step.dependOn(&display_width_tr.step);
519 520
520 const test_step = b.step("test", "Run all module tests"); 521 const test_step = b.step("test", "Run all module tests");
521 test_step.dependOn(&run_unicode_tests.step); 522 test_step.dependOn(&run_unicode_tests.step);
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
6graphemes: Graphemes = undefined, 6const Data = struct {
7s1: []u16 = undefined, 7 s1: []const u16 = undefined,
8s2: []i4 = undefined, 8 s2: []const i4 = undefined,
9owns_graphemes: bool = true, 9};
10
11const 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
11const DisplayWidth = @This(); 19const DisplayWidth = @This();
12 20
13pub 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
26pub 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
34pub 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.
41pub 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
59pub 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.
70pub fn codePointWidth(dw: DisplayWidth, cp: u21) i4 { 26pub 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
74test "codePointWidth" { 30test "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.
106pub fn graphemeClusterWidth(dw: DisplayWidth, gc: []const u8) isize { 60pub 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).
133pub fn strWidth(dw: DisplayWidth, str: []const u8) usize { 87pub 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
151test "strWidth" { 105test "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`.
219pub fn center( 171pub 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
259test "center" { 210test "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`.
305pub fn padLeft( 254pub 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
337test "padLeft" { 285test "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`.
353pub fn padRight( 299pub 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
386test "padRight" { 331test "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`.
403pub fn wrap( 346pub 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
440test "wrap" { 382test "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
464fn 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
477test "allocation test" {
478 try testing.checkAllAllocationFailures(testing.allocator, testAllocation, .{});
479}
480
481const std = @import("std"); 402const std = @import("std");
482const builtin = @import("builtin"); 403const builtin = @import("builtin");
483const options = @import("options"); 404const options = @import("options");