const measure = @import("measure.zig"); const util = @import("util.zig"); const LineSlicer = @This(); const std = @import("std"); data: []const u8, /// Byte offset where the next segment starts, or `null` once iteration is /// done. A non-null value (including `data.len`) means one more segment is /// pending, so a trailing `\n` yields a final empty segment. next_index: ?usize = 0, single: bool = false, consumed_single: bool = false, pub fn init(data: []const u8) LineSlicer { return .{ .data = data }; } pub fn singleLine(data: []const u8) LineSlicer { return .{ .data = data, .single = true }; } /// Returns the next line. Splits on `\n` like lipgloss `getLines`: the result /// has exactly `count('\n') + 1` segments, so a trailing `\n` produces a final /// empty segment and empty input produces one empty segment. pub fn next(self: *LineSlicer) ?[]const u8 { if (self.single) { if (self.consumed_single) return null; self.consumed_single = true; return self.data; } const start = self.next_index orelse return null; if (std.mem.indexOfScalarPos(u8, self.data, start, '\n')) |nl| { self.next_index = nl + 1; return self.data[start..nl]; } self.next_index = null; return self.data[start..]; } pub fn count(self: *LineSlicer) u16 { const saved = self.*; defer self.* = saved; var n: u16 = 0; while (self.next()) |_| n = util.satAdd(n, 1); return n; } pub fn maxWidth(self: *LineSlicer) u16 { const saved = self.*; defer self.* = saved; var max: u16 = 0; while (self.next()) |line| { const w = measure.cellWidth(line); if (w > max) max = w; } return max; } test "next splits on newline" { var it: LineSlicer = .init("a\nb\nc"); try std.testing.expectEqualStrings("a", it.next().?); try std.testing.expectEqualStrings("b", it.next().?); try std.testing.expectEqualStrings("c", it.next().?); try std.testing.expectEqual(@as(?[]const u8, null), it.next()); } test "next yields trailing empty line" { var it: LineSlicer = .init("hi\n"); try std.testing.expectEqualStrings("hi", it.next().?); try std.testing.expectEqualStrings("", it.next().?); try std.testing.expectEqual(@as(?[]const u8, null), it.next()); } test "next yields one empty segment for empty input" { var it: LineSlicer = .init(""); try std.testing.expectEqualStrings("", it.next().?); try std.testing.expectEqual(@as(?[]const u8, null), it.next()); } test "count matches lineCount semantics" { var a: LineSlicer = .init(""); try std.testing.expectEqual(@as(u16, 1), a.count()); var b: LineSlicer = .init("a\nb\n"); try std.testing.expectEqual(@as(u16, 3), b.count()); var c: LineSlicer = .init("a\n\nb"); try std.testing.expectEqual(@as(u16, 3), c.count()); } test "singleLine yields whole input once" { var it: LineSlicer = .singleLine("a\nb"); try std.testing.expectEqualStrings("a\nb", it.next().?); try std.testing.expectEqual(@as(?[]const u8, null), it.next()); }