| ... | ... | @@ -23,7 +23,7 @@ pub const FormatOptions = struct { |
| 23 | 23 | precision: ?usize = null, |
| 24 | 24 | width: ?usize = null, |
| 25 | 25 | alignment: Alignment = .right, |
| 26 | | fill: u8 = ' ', |
| 26 | fill: u21 = ' ', |
| 27 | 27 | }; |
| 28 | 28 | |
| 29 | 29 | /// Renders fmt string with args, calling `writer` with slices of bytes. |
| ... | ... | @@ -211,14 +211,18 @@ fn cacheString(str: anytype) []const u8 { |
| 211 | 211 | |
| 212 | 212 | pub const Placeholder = struct { |
| 213 | 213 | specifier_arg: []const u8, |
| 214 | | fill: u8, |
| 214 | fill: u21, |
| 215 | 215 | alignment: Alignment, |
| 216 | 216 | arg: Specifier, |
| 217 | 217 | width: Specifier, |
| 218 | 218 | precision: Specifier, |
| 219 | 219 | |
| 220 | 220 | pub fn parse(comptime str: anytype) Placeholder { |
| 221 | | comptime var parser = Parser{ .buf = &str }; |
| 221 | const view = std.unicode.Utf8View.initComptime(&str); |
| 222 | comptime var parser = Parser{ |
| 223 | .buf = &str, |
| 224 | .iter = view.iterator(), |
| 225 | }; |
| 222 | 226 | |
| 223 | 227 | // Parse the positional argument number |
| 224 | 228 | const arg = comptime parser.specifier() catch |err| |
| ... | ... | @@ -230,7 +234,7 @@ pub const Placeholder = struct { |
| 230 | 234 | // Skip the colon, if present |
| 231 | 235 | if (comptime parser.char()) |ch| { |
| 232 | 236 | if (ch != ':') { |
| 233 | | @compileError("expected : or }, found '" ++ [1]u8{ch} ++ "'"); |
| 237 | @compileError("expected : or }, found '" ++ unicode.utf8EncodeComptime(ch) ++ "'"); |
| 234 | 238 | } |
| 235 | 239 | } |
| 236 | 240 | |
| ... | ... | @@ -265,7 +269,7 @@ pub const Placeholder = struct { |
| 265 | 269 | // Skip the dot, if present |
| 266 | 270 | if (comptime parser.char()) |ch| { |
| 267 | 271 | if (ch != '.') { |
| 268 | | @compileError("expected . or }, found '" ++ [1]u8{ch} ++ "'"); |
| 272 | @compileError("expected . or }, found '" ++ unicode.utf8EncodeComptime(ch) ++ "'"); |
| 269 | 273 | } |
| 270 | 274 | } |
| 271 | 275 | |
| ... | ... | @@ -274,7 +278,7 @@ pub const Placeholder = struct { |
| 274 | 278 | @compileError(@errorName(err)); |
| 275 | 279 | |
| 276 | 280 | if (comptime parser.char()) |ch| { |
| 277 | | @compileError("extraneous trailing character '" ++ [1]u8{ch} ++ "'"); |
| 281 | @compileError("extraneous trailing character '" ++ unicode.utf8EncodeComptime(ch) ++ "'"); |
| 278 | 282 | } |
| 279 | 283 | |
| 280 | 284 | return Placeholder{ |
| ... | ... | @@ -297,21 +301,23 @@ pub const Specifier = union(enum) { |
| 297 | 301 | pub const Parser = struct { |
| 298 | 302 | buf: []const u8, |
| 299 | 303 | pos: usize = 0, |
| 304 | iter: std.unicode.Utf8Iterator = undefined, |
| 300 | 305 | |
| 301 | 306 | // Returns a decimal number or null if the current character is not a |
| 302 | 307 | // digit |
| 303 | 308 | pub fn number(self: *@This()) ?usize { |
| 304 | 309 | var r: ?usize = null; |
| 305 | 310 | |
| 306 | | while (self.pos < self.buf.len) : (self.pos += 1) { |
| 307 | | switch (self.buf[self.pos]) { |
| 311 | while (self.peek(0)) |code_point| { |
| 312 | switch (code_point) { |
| 308 | 313 | '0'...'9' => { |
| 309 | 314 | if (r == null) r = 0; |
| 310 | 315 | r.? *= 10; |
| 311 | | r.? += self.buf[self.pos] - '0'; |
| 316 | r.? += code_point - '0'; |
| 312 | 317 | }, |
| 313 | 318 | else => break, |
| 314 | 319 | } |
| 320 | _ = self.iter.nextCodepoint(); |
| 315 | 321 | } |
| 316 | 322 | |
| 317 | 323 | return r; |
| ... | ... | @@ -319,31 +325,27 @@ pub const Parser = struct { |
| 319 | 325 | |
| 320 | 326 | // Returns a substring of the input starting from the current position |
| 321 | 327 | // and ending where `ch` is found or until the end if not found |
| 322 | | pub fn until(self: *@This(), ch: u8) []const u8 { |
| 323 | | const start = self.pos; |
| 324 | | |
| 325 | | if (start >= self.buf.len) |
| 326 | | return &[_]u8{}; |
| 327 | | |
| 328 | | while (self.pos < self.buf.len) : (self.pos += 1) { |
| 329 | | if (self.buf[self.pos] == ch) break; |
| 328 | pub fn until(self: *@This(), ch: u21) []const u8 { |
| 329 | var result: []const u8 = &[_]u8{}; |
| 330 | while (self.peek(0)) |code_point| { |
| 331 | if (code_point == ch) |
| 332 | break; |
| 333 | result = result ++ (self.iter.nextCodepointSlice() orelse &[_]u8{}); |
| 330 | 334 | } |
| 331 | | return self.buf[start..self.pos]; |
| 335 | return result; |
| 332 | 336 | } |
| 333 | 337 | |
| 334 | 338 | // Returns one character, if available |
| 335 | | pub fn char(self: *@This()) ?u8 { |
| 336 | | if (self.pos < self.buf.len) { |
| 337 | | const ch = self.buf[self.pos]; |
| 338 | | self.pos += 1; |
| 339 | | return ch; |
| 339 | pub fn char(self: *@This()) ?u21 { |
| 340 | if (self.iter.nextCodepoint()) |code_point| { |
| 341 | return code_point; |
| 340 | 342 | } |
| 341 | 343 | return null; |
| 342 | 344 | } |
| 343 | 345 | |
| 344 | | pub fn maybe(self: *@This(), val: u8) bool { |
| 345 | | if (self.pos < self.buf.len and self.buf[self.pos] == val) { |
| 346 | | self.pos += 1; |
| 346 | pub fn maybe(self: *@This(), val: u21) bool { |
| 347 | if (self.peek(0) == val) { |
| 348 | _ = self.iter.nextCodepoint(); |
| 347 | 349 | return true; |
| 348 | 350 | } |
| 349 | 351 | return false; |
| ... | ... | @@ -367,8 +369,17 @@ pub const Parser = struct { |
| 367 | 369 | } |
| 368 | 370 | |
| 369 | 371 | // Returns the n-th next character or null if that's past the end |
| 370 | | pub fn peek(self: *@This(), n: usize) ?u8 { |
| 371 | | return if (self.pos + n < self.buf.len) self.buf[self.pos + n] else null; |
| 372 | pub fn peek(self: *@This(), n: usize) ?u21 { |
| 373 | const original_i = self.iter.i; |
| 374 | defer self.iter.i = original_i; |
| 375 | |
| 376 | var i = 0; |
| 377 | var code_point: ?u21 = null; |
| 378 | while (i <= n) : (i += 1) { |
| 379 | code_point = self.iter.nextCodepoint(); |
| 380 | if (code_point == null) return null; |
| 381 | } |
| 382 | return code_point; |
| 372 | 383 | } |
| 373 | 384 | }; |
| 374 | 385 | |
| ... | ... | @@ -965,8 +976,7 @@ pub fn formatUnicodeCodepoint( |
| 965 | 976 | var buf: [4]u8 = undefined; |
| 966 | 977 | const len = unicode.utf8Encode(c, &buf) catch |err| switch (err) { |
| 967 | 978 | error.Utf8CannotEncodeSurrogateHalf, error.CodepointTooLarge => { |
| 968 | | const len = unicode.utf8Encode(unicode.replacement_character, &buf) catch unreachable; |
| 969 | | return formatBuf(buf[0..len], options, writer); |
| 979 | return formatBuf(&unicode.utf8EncodeComptime(unicode.replacement_character), options, writer); |
| 970 | 980 | }, |
| 971 | 981 | }; |
| 972 | 982 | return formatBuf(buf[0..len], options, writer); |
| ... | ... | @@ -985,20 +995,28 @@ pub fn formatBuf( |
| 985 | 995 | if (padding == 0) |
| 986 | 996 | return writer.writeAll(buf); |
| 987 | 997 | |
| 998 | var fill_buffer: [4]u8 = undefined; |
| 999 | const fill_utf8 = if (unicode.utf8Encode(options.fill, &fill_buffer)) |len| |
| 1000 | fill_buffer[0..len] |
| 1001 | else |err| switch (err) { |
| 1002 | error.Utf8CannotEncodeSurrogateHalf, |
| 1003 | error.CodepointTooLarge, |
| 1004 | => &unicode.utf8EncodeComptime(unicode.replacement_character), |
| 1005 | }; |
| 988 | 1006 | switch (options.alignment) { |
| 989 | 1007 | .left => { |
| 990 | 1008 | try writer.writeAll(buf); |
| 991 | | try writer.writeByteNTimes(options.fill, padding); |
| 1009 | try writer.writeBytesNTimes(fill_utf8, padding); |
| 992 | 1010 | }, |
| 993 | 1011 | .center => { |
| 994 | 1012 | const left_padding = padding / 2; |
| 995 | 1013 | const right_padding = (padding + 1) / 2; |
| 996 | | try writer.writeByteNTimes(options.fill, left_padding); |
| 1014 | try writer.writeBytesNTimes(fill_utf8, left_padding); |
| 997 | 1015 | try writer.writeAll(buf); |
| 998 | | try writer.writeByteNTimes(options.fill, right_padding); |
| 1016 | try writer.writeBytesNTimes(fill_utf8, right_padding); |
| 999 | 1017 | }, |
| 1000 | 1018 | .right => { |
| 1001 | | try writer.writeByteNTimes(options.fill, padding); |
| 1019 | try writer.writeBytesNTimes(fill_utf8, padding); |
| 1002 | 1020 | try writer.writeAll(buf); |
| 1003 | 1021 | }, |
| 1004 | 1022 | } |
| ... | ... | @@ -2793,6 +2811,15 @@ test "padding" { |
| 2793 | 2811 | try expectFmt("a====", "{c:=<5}", .{'a'}); |
| 2794 | 2812 | } |
| 2795 | 2813 | |
| 2814 | test "padding fill char utf" { |
| 2815 | try expectFmt("──crêpe───", "{s:─^10}", .{"crêpe"}); |
| 2816 | try expectFmt("─────crêpe", "{s:─>10}", .{"crêpe"}); |
| 2817 | try expectFmt("crêpe─────", "{s:─<10}", .{"crêpe"}); |
| 2818 | try expectFmt("────a", "{c:─>5}", .{'a'}); |
| 2819 | try expectFmt("──a──", "{c:─^5}", .{'a'}); |
| 2820 | try expectFmt("a────", "{c:─<5}", .{'a'}); |
| 2821 | } |
| 2822 | |
| 2796 | 2823 | test "decimal float padding" { |
| 2797 | 2824 | const number: f32 = 3.1415; |
| 2798 | 2825 | try expectFmt("left-pad: **3.141\n", "left-pad: {d:*>7.3}\n", .{number}); |