//! Per-rune styling. Mirrors lipgloss `StyleRunes`. const std = @import("std"); const Allocator = std.mem.Allocator; const Writer = std.Io.Writer; const Style = @import("Style.zig"); /// Applies `matched` to the code points of `str` at the given indices and /// `unmatched` to the rest, mirroring lipgloss `StyleRunes`. Indices are /// code-point positions; out-of-range indices are ignored. Consecutive runes /// with the same state are rendered as one group. The caller owns the result. pub fn styleRunes( allocator: Allocator, str: []const u8, indices: []const usize, matched: Style, unmatched: Style, ) Style.RenderError![]u8 { var buf: Writer.Allocating = .init(allocator); defer buf.deinit(); const w = &buf.writer; var group_start: usize = 0; var group_matches = false; var have_group = false; var rune_idx: usize = 0; var i: usize = 0; while (i < str.len) { const len = std.unicode.utf8ByteSequenceLength(str[i]) catch 1; const this_matches = contains(indices, rune_idx); if (!have_group) { group_start = i; group_matches = this_matches; have_group = true; } else if (this_matches != group_matches) { const style = if (group_matches) matched else unmatched; try style.render(w, str[group_start..i]); group_start = i; group_matches = this_matches; } i += len; rune_idx += 1; } if (have_group) { const style = if (group_matches) matched else unmatched; try style.render(w, str[group_start..]); } return allocator.dupe(u8, buf.written()); } fn contains(indices: []const usize, idx: usize) bool { for (indices) |x| { if (x == idx) return true; } return false; } test "styleRunes styles matched indices" { const bold = Style.init(.{}).attr(.bold, true); const plain = Style.init(.{}); const out = try styleRunes(std.testing.allocator, "abc", &.{1}, bold, plain); defer std.testing.allocator.free(out); try std.testing.expectEqualStrings("a\x1b[1mb\x1b[0mc", out); } test "styleRunes groups adjacent matches" { const bold = Style.init(.{}).attr(.bold, true); const plain = Style.init(.{}); const out = try styleRunes(std.testing.allocator, "abcd", &.{ 1, 2 }, bold, plain); defer std.testing.allocator.free(out); try std.testing.expectEqualStrings("a\x1b[1mbc\x1b[0md", out); } test "styleRunes empty string" { const plain = Style.init(.{}); const out = try styleRunes(std.testing.allocator, "", &.{}, plain, plain); defer std.testing.allocator.free(out); try std.testing.expectEqualStrings("", out); }