const std = @import("std"); // Source-patched syntax profile registry for v1 coding feedback. // req: coding/002, coding/003, ui/002, governance/002 test { _ = spansAlloc; } pub const Error = error{ InvalidLanguage, }; pub const Class = enum { keyword, builtin, string, comment, number, pub fn text(self: Class) []const u8 { return switch (self) { .keyword => "keyword", .builtin => "builtin", .string => "string", .comment => "comment", .number => "number", }; } }; pub const Span = struct { start: usize, end: usize, class: Class, }; pub const Profile = struct { language: []const u8, source_patched: bool, }; pub fn profile(language: []const u8) ?Profile { if (std.mem.eql(u8, language, "zig")) return .{ .language = "zig", .source_patched = true }; if (std.mem.eql(u8, language, "text")) return .{ .language = "text", .source_patched = false }; return null; } pub fn spansAlloc(allocator: std.mem.Allocator, language: []const u8, source: []const u8) ![]Span { const resolved = profile(language) orelse return Error.InvalidLanguage; if (!resolved.source_patched) return allocator.alloc(Span, 0); var spans = std.ArrayList(Span).empty; errdefer spans.deinit(allocator); var i: usize = 0; while (i < source.len) { if (source[i] == '/' and i + 1 < source.len and source[i + 1] == '/') { const start = i; i += 2; while (i < source.len and source[i] != '\n') : (i += 1) {} try spans.append(allocator, .{ .start = start, .end = i, .class = .comment }); continue; } if (source[i] == '"') { const start = i; i += 1; while (i < source.len) : (i += 1) { if (source[i] == '\\') { i += @intFromBool(i + 1 < source.len); continue; } if (source[i] == '"') { i += 1; break; } } try spans.append(allocator, .{ .start = start, .end = i, .class = .string }); continue; } if (isDigit(source[i])) { const start = i; while (i < source.len and isDigit(source[i])) : (i += 1) {} try spans.append(allocator, .{ .start = start, .end = i, .class = .number }); continue; } if (isIdentStart(source[i])) { const start = i; i += 1; while (i < source.len and isIdentContinue(source[i])) : (i += 1) {} const token = source[start..i]; if (keywordClass(token)) |class| try spans.append(allocator, .{ .start = start, .end = i, .class = class }); continue; } i += 1; } return spans.toOwnedSlice(allocator); } pub fn rowsAlloc(allocator: std.mem.Allocator, language: []const u8, source: []const u8) ![][]const u8 { const spans = try spansAlloc(allocator, language, source); defer allocator.free(spans); var rows = std.ArrayList([]const u8).empty; errdefer { for (rows.items) |row| allocator.free(row); rows.deinit(allocator); } const resolved = profile(language) orelse return Error.InvalidLanguage; if (!resolved.source_patched) { try rows.append(allocator, try allocator.dupe(u8, "syntax:text:fallback:no_spans")); return rows.toOwnedSlice(allocator); } if (spans.len == 0) { try rows.append(allocator, try std.fmt.allocPrint(allocator, "syntax:{s}:no_spans", .{resolved.language})); return rows.toOwnedSlice(allocator); } for (spans) |span| { const preview = try previewAlloc(allocator, source[span.start..span.end]); defer allocator.free(preview); try rows.append(allocator, try std.fmt.allocPrint( allocator, "syntax:{s}:{s}:{d}:{d}:{s}", .{ resolved.language, span.class.text(), span.start, span.end, preview }, )); } return rows.toOwnedSlice(allocator); } fn keywordClass(token: []const u8) ?Class { const keywords = [_][]const u8{ "pub", "const", "var", "fn", "return", "if", "else", "while", "for", "switch", "try", "catch", "defer", "test", "struct", "enum", "error", "union", "comptime", "inline", "break", "continue" }; for (keywords) |keyword| if (std.mem.eql(u8, token, keyword)) return .keyword; const builtins = [_][]const u8{ "true", "false", "null", "undefined", "usize", "u8", "i32", "void", "bool", "anyerror" }; for (builtins) |builtin| if (std.mem.eql(u8, token, builtin)) return .builtin; return null; } fn previewAlloc(allocator: std.mem.Allocator, token: []const u8) ![]u8 { var out = std.ArrayList(u8).empty; errdefer out.deinit(allocator); var i: usize = 0; while (i < token.len and out.items.len < 48) { const len = std.unicode.utf8ByteSequenceLength(token[i]) catch 1; const end = @min(token.len, i + len); const slice = token[i..end]; if (slice.len == 1 and (slice[0] <= 0x20 or slice[0] == ':' or slice[0] == '|')) { try out.append(allocator, '_'); } else { try out.appendSlice(allocator, slice); } i = end; } if (out.items.len == 0) try out.append(allocator, '_'); return out.toOwnedSlice(allocator); } fn isDigit(byte: u8) bool { return byte >= '0' and byte <= '9'; } fn isIdentStart(byte: u8) bool { return (byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') or byte == '_'; } fn isIdentContinue(byte: u8) bool { return isIdentStart(byte) or isDigit(byte); } fn freeRows(allocator: std.mem.Allocator, rows: []const []const u8) void { for (rows) |row| allocator.free(row); allocator.free(rows); } test "regular: zig source profile produces deterministic highlight spans" { const source = "pub const answer = 42; // ok\n"; const spans = try spansAlloc(std.testing.allocator, "zig", source); defer std.testing.allocator.free(spans); try std.testing.expectEqual(@as(usize, 4), spans.len); try std.testing.expectEqualDeep(Span{ .start = 0, .end = 3, .class = .keyword }, spans[0]); try std.testing.expectEqualDeep(Span{ .start = 4, .end = 9, .class = .keyword }, spans[1]); try std.testing.expectEqualDeep(Span{ .start = 19, .end = 21, .class = .number }, spans[2]); try std.testing.expectEqualDeep(Span{ .start = 23, .end = 28, .class = .comment }, spans[3]); } test "regular: zig syntax rows include class byte ranges and previews" { const rows = try rowsAlloc(std.testing.allocator, "zig", "fn main() void { return; }"); defer freeRows(std.testing.allocator, rows); try std.testing.expectEqualStrings("syntax:zig:keyword:0:2:fn", rows[0]); try std.testing.expectEqualStrings("syntax:zig:builtin:10:14:void", rows[1]); try std.testing.expectEqualStrings("syntax:zig:keyword:17:23:return", rows[2]); } test "adversarial: plain text falls back and unknown languages fail" { const rows = try rowsAlloc(std.testing.allocator, "text", "pub const not highlighted"); defer freeRows(std.testing.allocator, rows); try std.testing.expectEqual(@as(usize, 1), rows.len); try std.testing.expectEqualStrings("syntax:text:fallback:no_spans", rows[0]); try std.testing.expectError(Error.InvalidLanguage, rowsAlloc(std.testing.allocator, "python", "def x(): pass")); }