lib/pretty/core/src/compact/format.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const types = @import("types.zig");
  3 
  4 const Column = types.Column;
  5 pub const gutter = "  ";
  6 const ellipsis = "\xE2\x80\xA6";
  7 const footer_dash = "\xE2\x80\x94";
  8 pub const footer_tail = " " ++ footer_dash ++
  9     " use --limit 0 for all, --json for exact records)\n";
 10 
 11 pub fn maximumLineBytes(
 12     columns: []const Column,
 13     rows: []const []const []const u8,
 14 ) error{CapacityOverflow}!usize {
 15     var content_bytes: usize = 0;
 16     for (columns, 0..) |column, column_index| {
 17         var width = rawTruncatedLength(column.header, column.max_width);
 18         for (rows) |row| {
 19             const value = if (column_index < row.len) row[column_index] else "";
 20             width = @max(width, normalizedTruncatedLength(value, column.max_width));
 21         }
 22         content_bytes = std.math.add(usize, content_bytes, width) catch
 23             return error.CapacityOverflow;
 24     }
 25     const gutters = if (columns.len == 0) 0 else std.math.mul(
 26         usize,
 27         columns.len - 1,
 28         gutter.len,
 29     ) catch return error.CapacityOverflow;
 30     const with_gutters = std.math.add(usize, content_bytes, gutters) catch
 31         return error.CapacityOverflow;
 32     return std.math.add(usize, with_gutters, 1) catch error.CapacityOverflow;
 33 }
 34 
 35 pub fn footerLength(displayed: usize, total: ?usize) error{CapacityOverflow}!usize {
 36     const count = total orelse return 0;
 37     if (count <= displayed) return 0;
 38     var bytes = "(showing ".len;
 39     bytes = std.math.add(usize, bytes, decimalLength(displayed)) catch
 40         return error.CapacityOverflow;
 41     bytes = std.math.add(usize, bytes, " of ".len) catch
 42         return error.CapacityOverflow;
 43     bytes = std.math.add(usize, bytes, decimalLength(count)) catch
 44         return error.CapacityOverflow;
 45     return std.math.add(usize, bytes, footer_tail.len) catch error.CapacityOverflow;
 46 }
 47 
 48 pub fn measureWidths(
 49     widths: []usize,
 50     columns: []const Column,
 51     rows: []const []const []const u8,
 52 ) void {
 53     std.debug.assert(widths.len == columns.len);
 54     for (columns, widths, 0..) |column, *width, column_index| {
 55         width.* = rawTruncatedLength(column.header, column.max_width);
 56         for (rows) |row| {
 57             const value = if (column_index < row.len) row[column_index] else "";
 58             width.* = @max(
 59                 width.*,
 60                 normalizedTruncatedLength(value, column.max_width),
 61             );
 62         }
 63     }
 64 }
 65 
 66 pub fn renderInto(
 67     writer: *std.Io.Writer,
 68     widths: []const usize,
 69     columns: []const Column,
 70     rows: []const []const []const u8,
 71     total: ?usize,
 72 ) std.Io.Writer.Error!void {
 73     try writeHeaderRow(writer, widths, columns);
 74     try writeSeparator(writer, widths);
 75     for (rows) |row| try writeDataRow(writer, widths, columns, row);
 76     if (total) |count| {
 77         if (count > rows.len) {
 78             try writer.print(
 79                 "(showing {d} of {d}{s}",
 80                 .{ rows.len, count, footer_tail },
 81             );
 82         }
 83     }
 84 }
 85 
 86 fn writeHeaderRow(
 87     writer: *std.Io.Writer,
 88     widths: []const usize,
 89     columns: []const Column,
 90 ) std.Io.Writer.Error!void {
 91     std.debug.assert(widths.len == columns.len);
 92     for (columns, 0..) |column, index| {
 93         if (index != 0) try writer.writeAll(gutter);
 94         const length = rawTruncatedLength(column.header, column.max_width);
 95         if (column.alignment == .right) try writeSpaces(writer, widths[index] - length);
 96         try writeLowerTruncated(writer, column.header, column.max_width);
 97         if (column.alignment == .left and index + 1 != columns.len) {
 98             try writeSpaces(writer, widths[index] - length);
 99         }
100     }
101     try writer.writeByte('\n');
102 }
103 
104 fn writeDataRow(
105     writer: *std.Io.Writer,
106     widths: []const usize,
107     columns: []const Column,
108     row: []const []const u8,
109 ) std.Io.Writer.Error!void {
110     std.debug.assert(widths.len == columns.len);
111     for (columns, 0..) |column, index| {
112         if (index != 0) try writer.writeAll(gutter);
113         const value = if (index < row.len) row[index] else "";
114         const length = normalizedTruncatedLength(value, column.max_width);
115         if (column.alignment == .right) try writeSpaces(writer, widths[index] - length);
116         try writeNormalizedTruncated(writer, value, column.max_width);
117         if (column.alignment == .left and index + 1 != columns.len) {
118             try writeSpaces(writer, widths[index] - length);
119         }
120     }
121     try writer.writeByte('\n');
122 }
123 
124 fn writeSeparator(
125     writer: *std.Io.Writer,
126     widths: []const usize,
127 ) std.Io.Writer.Error!void {
128     for (widths, 0..) |width, index| {
129         if (index != 0) try writer.writeAll(gutter);
130         for (0..width) |_| try writer.writeByte('-');
131     }
132     try writer.writeByte('\n');
133 }
134 
135 fn writeSpaces(writer: *std.Io.Writer, count: usize) std.Io.Writer.Error!void {
136     for (0..count) |_| try writer.writeByte(' ');
137 }
138 
139 fn rawTruncatedLength(value: []const u8, maximum: usize) usize {
140     if (value.len <= maximum) return value.len;
141     if (maximum <= ellipsis.len) return @min(maximum, ellipsis.len);
142     var cut = maximum - ellipsis.len;
143     while (cut > 0 and isContinuation(value[cut])) cut -= 1;
144     return cut + ellipsis.len;
145 }
146 
147 fn normalizedTruncatedLength(value: []const u8, maximum: usize) usize {
148     const length = normalizedLength(value);
149     if (length <= maximum) return length;
150     if (maximum <= ellipsis.len) return @min(maximum, ellipsis.len);
151     var cut = maximum - ellipsis.len;
152     while (cut > 0 and isContinuation(normalizedByteAt(value, cut))) cut -= 1;
153     return cut + ellipsis.len;
154 }
155 
156 fn normalizedLength(value: []const u8) usize {
157     var length: usize = 0;
158     for (value) |byte| {
159         if (normalizedByte(byte) != null) length += 1;
160     }
161     return length;
162 }
163 
164 fn normalizedByteAt(value: []const u8, target: usize) u8 {
165     var index: usize = 0;
166     for (value) |byte| {
167         const normalized = normalizedByte(byte) orelse continue;
168         if (index == target) return normalized;
169         index += 1;
170     }
171     unreachable;
172 }
173 
174 fn normalizedByte(byte: u8) ?u8 {
175     return switch (byte) {
176         '\t', '\r', '\n' => ' ',
177         0...8, 11...12, 14...31, 127 => null,
178         else => byte,
179     };
180 }
181 
182 fn writeLowerTruncated(
183     writer: *std.Io.Writer,
184     value: []const u8,
185     maximum: usize,
186 ) std.Io.Writer.Error!void {
187     const length = rawTruncatedLength(value, maximum);
188     if (value.len <= maximum) {
189         for (value) |byte| try writer.writeByte(std.ascii.toLower(byte));
190         return;
191     }
192     if (maximum <= ellipsis.len) return writer.writeAll(ellipsis[0..length]);
193     const prefix = length - ellipsis.len;
194     for (value[0..prefix]) |byte| try writer.writeByte(std.ascii.toLower(byte));
195     try writer.writeAll(ellipsis);
196 }
197 
198 fn writeNormalizedTruncated(
199     writer: *std.Io.Writer,
200     value: []const u8,
201     maximum: usize,
202 ) std.Io.Writer.Error!void {
203     const length = normalizedTruncatedLength(value, maximum);
204     const normalized_length = normalizedLength(value);
205     if (normalized_length <= maximum) return writeNormalizedPrefix(writer, value, length);
206     if (maximum <= ellipsis.len) return writer.writeAll(ellipsis[0..length]);
207     try writeNormalizedPrefix(writer, value, length - ellipsis.len);
208     try writer.writeAll(ellipsis);
209 }
210 
211 fn writeNormalizedPrefix(
212     writer: *std.Io.Writer,
213     value: []const u8,
214     count: usize,
215 ) std.Io.Writer.Error!void {
216     var written: usize = 0;
217     for (value) |byte| {
218         if (written == count) break;
219         const normalized = normalizedByte(byte) orelse continue;
220         try writer.writeByte(normalized);
221         written += 1;
222     }
223     std.debug.assert(written == count);
224 }
225 
226 fn isContinuation(byte: u8) bool {
227     return byte & 0xc0 == 0x80;
228 }
229 
230 fn decimalLength(value: usize) usize {
231     var remaining = value;
232     var digits: usize = 1;
233     while (remaining >= 10) : (remaining /= 10) digits += 1;
234     return digits;
235 }