Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
12 kB · 303 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304const std = @import("std");const languages = @import("code_highlight_languages.zig");
const Allocator = std.mem.Allocator;
const reset_style = "\x1b[39m";
pub const Theme = enum { dark, light,};
const Palette = struct { keyword_style: []const u8, string_style: []const u8, number_style: []const u8, comment_style: []const u8,};
const dark_palette: Palette = .{ .keyword_style = "\x1b[38;5;252m", .string_style = "\x1b[38;5;250m", .number_style = "\x1b[38;5;250m", .comment_style = "\x1b[38;5;245m",};
const light_palette: Palette = .{ .keyword_style = "\x1b[38;5;238m", .string_style = "\x1b[38;5;241m", .number_style = "\x1b[38;5;241m", .comment_style = "\x1b[38;5;243m",};
fn paletteForTheme(theme: Theme) Palette { return switch (theme) { .dark => dark_palette, .light => light_palette, };}
pub fn highlight( alloc: Allocator, source: []const u8, profile: *const languages.Profile, theme: Theme,) ![]u8 { var styled: std.ArrayList(u8) = .empty; errdefer styled.deinit(alloc); const palette = paletteForTheme(theme);
var index: usize = 0; while (index < source.len) { if (source[index] == '\n') { try styled.append(alloc, '\n'); index += 1; continue; } if (blockCommentEnd(source, index, profile.block_comment)) |end| { try appendStyled(alloc, &styled, palette.comment_style, source[index..end]); index = end; continue; } if (lineCommentEnd(source, index, profile.line_comments)) |end| { try appendStyled(alloc, &styled, palette.comment_style, source[index..end]); index = end; continue; } if (isQuote(source[index], profile.quotes)) { const end = quotedEnd(source, index); try appendStyled(alloc, &styled, palette.string_style, source[index..end]); index = end; continue; } if (isNumberStart(source, index)) { const end = numberEnd(source, index); try appendStyled(alloc, &styled, palette.number_style, source[index..end]); index = end; continue; } if (isIdentifierStart(source[index])) { const end = identifierEnd(source, index); const token = source[index..end]; if (inList(token, profile.keywords, profile.keyword_case)) { try appendStyled(alloc, &styled, palette.keyword_style, token); } else if (inList(token, profile.literals, profile.keyword_case)) { try appendStyled(alloc, &styled, palette.number_style, token); } else { try styled.appendSlice(alloc, token); } index = end; continue; } try styled.append(alloc, source[index]); index += 1; }
return styled.toOwnedSlice(alloc);}
fn appendStyled(alloc: Allocator, out: *std.ArrayList(u8), style: []const u8, text: []const u8) !void { try out.appendSlice(alloc, style); try out.appendSlice(alloc, text); try out.appendSlice(alloc, reset_style);}
fn blockCommentEnd(source: []const u8, index: usize, block_comment: ?languages.BlockComment) ?usize { const comment = block_comment orelse return null; if (!std.mem.startsWith(u8, source[index..], comment.start)) return null; const content_start = index + comment.start.len; const close_start = std.mem.indexOfPos(u8, source, content_start, comment.end) orelse return source.len; return close_start + comment.end.len;}
fn lineCommentEnd(source: []const u8, index: usize, prefixes: []const []const u8) ?usize { for (prefixes) |prefix| { if (std.mem.startsWith(u8, source[index..], prefix)) return lineEnd(source, index); } return null;}
fn lineEnd(source: []const u8, start: usize) usize { return std.mem.indexOfScalarPos(u8, source, start, '\n') orelse source.len;}
fn isQuote(byte: u8, quotes: []const u8) bool { return std.mem.indexOfScalar(u8, quotes, byte) != null;}
fn quotedEnd(source: []const u8, start: usize) usize { const quote = source[start]; var index = start + 1; while (index < source.len) : (index += 1) { if (source[index] == '\n') return index; if (source[index] == '\\' and index + 1 < source.len) { index += 1; continue; } if (source[index] == quote) return index + 1; } return source.len;}
fn isNumberStart(source: []const u8, index: usize) bool { return std.ascii.isDigit(source[index]) and (index == 0 or !isIdentifierContinue(source[index - 1]));}
fn numberEnd(source: []const u8, start: usize) usize { var index = start; while (index < source.len and (std.ascii.isAlphanumeric(source[index]) or source[index] == '.' or source[index] == '_')) : (index += 1) {} return index;}
fn isIdentifierStart(byte: u8) bool { return std.ascii.isAlphabetic(byte) or byte == '_' or byte == '$';}
fn isIdentifierContinue(byte: u8) bool { return isIdentifierStart(byte) or std.ascii.isDigit(byte);}
fn identifierEnd(source: []const u8, start: usize) usize { var index = start + 1; while (index < source.len and isIdentifierContinue(source[index])) : (index += 1) {} return index;}
fn inList(token: []const u8, options: []const []const u8, keyword_case: languages.KeywordCase) bool { for (options) |option| { const matches = switch (keyword_case) { .sensitive => std.mem.eql(u8, token, option), .ascii_insensitive => std.ascii.eqlIgnoreCase(token, option), }; if (matches) return true; } return false;}
fn ansiSequenceEnd(text: []const u8, start: usize) usize { if (start + 2 > text.len or text[start] != 0x1b or text[start + 1] != '[') return start; var index = start + 2; while (index < text.len) : (index += 1) { if (text[index] >= '@' and text[index] <= '~') return index + 1; } return start;}
fn stripAnsi(alloc: std.mem.Allocator, text: []const u8) ![]u8 { var plain: std.ArrayList(u8) = .empty; errdefer plain.deinit(alloc); var index: usize = 0; while (index < text.len) { if (text[index] == 0x1b) { const end = ansiSequenceEnd(text, index); if (end > index) { index = end; continue; } } try plain.append(alloc, text[index]); index += 1; } return plain.toOwnedSlice(alloc);}
fn count(text: []const u8, needle: []const u8) usize { var result: usize = 0; var start: usize = 0; while (std.mem.indexOfPos(u8, text, start, needle)) |index| { result += 1; start = index + needle.len; } return result;}
test "supported source gains balanced colors without changing code bytes" { const alloc = std.testing.allocator; const source = "const value = \"const\"; // return\n"; const styled = try highlight(alloc, source, languages.resolve("zig").?, .dark); defer alloc.free(styled);
const plain = try stripAnsi(alloc, styled); defer alloc.free(plain); try std.testing.expectEqualStrings(source, plain); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;") != null); try std.testing.expectEqual(count(styled, "\x1b[38;5;"), count(styled, "\x1b[39m")); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;252mconst") != null); try std.testing.expectEqual(@as(usize, 1), count(styled, "\x1b[38;5;252mconst")); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;250m\"const\"\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;252mreturn") == null);}
test "light theme uses a readable syntax palette without changing code bytes" { const alloc = std.testing.allocator; const source = "const value = \"ready\"; // comment\n"; const styled = try highlight(alloc, source, languages.resolve("zig").?, .light); defer alloc.free(styled);
const plain = try stripAnsi(alloc, styled); defer alloc.free(plain); try std.testing.expectEqualStrings(source, plain); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;238mconst\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;241m\"ready\"\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;243m// comment\x1b[39m") != null); try std.testing.expectEqual(count(styled, "\x1b[38;5;"), count(styled, "\x1b[39m"));}
test "every registered profile highlights representative source" { const alloc = std.testing.allocator; const cases = [_]struct { label: []const u8, source: []const u8, }{ .{ .label = "zig", .source = "pub fn main() void { return; }" }, .{ .label = "ts", .source = "const ready = true;" }, .{ .label = "json", .source = "{\"ready\": true}" }, .{ .label = "sh", .source = "if true; then echo \"ready\"; fi" }, .{ .label = "python", .source = "def ready(): return True" }, .{ .label = "yaml", .source = "ready: true # comment" }, .{ .label = "toml", .source = "ready = true # comment" }, .{ .label = "sql", .source = "SELECT id FROM users" }, .{ .label = "dockerfile", .source = "FROM alpine:3.20" }, .{ .label = "rust", .source = "fn main() { let ready = true; }" }, .{ .label = "go", .source = "package main\nfunc main() {}" }, .{ .label = "c", .source = "int main(void) { return 0; }" }, .{ .label = "cpp", .source = "class Ready { public: bool value = true; };" }, .{ .label = "csharp", .source = "public class Ready { }" }, .{ .label = "java", .source = "public class Ready { }" }, .{ .label = "kotlin", .source = "fun ready(): Boolean = true" }, .{ .label = "php", .source = "<?php function ready() { return true; }" }, .{ .label = "ruby", .source = "def ready\n true\nend" }, .{ .label = "swift", .source = "func ready() -> Bool { true }" }, .{ .label = "powershell", .source = "Function Ready { return $true }" }, .{ .label = "lua", .source = "local ready = true" }, .{ .label = "html", .source = "<main class=\"ready\"></main>" }, .{ .label = "xml", .source = "<?xml version=\"1.0\"?>" }, .{ .label = "css", .source = ".ready { color: red; }" }, .{ .label = "hcl", .source = "resource \"ready\" \"main\" {}" }, };
for (cases) |case| { const styled = try highlight(alloc, case.source, languages.resolve(case.label).?, .dark); defer alloc.free(styled); const plain = try stripAnsi(alloc, styled); defer alloc.free(plain); try std.testing.expectEqualStrings(case.source, plain); try std.testing.expect(std.mem.indexOf(u8, styled, "\x1b[38;5;") != null); }}
test "profiles use configured block comments and case-insensitive keywords" { const alloc = std.testing.allocator; const source = "/* comment */\nSELECT id FROM users\n<!-- note -->"; const css = try highlight(alloc, source[0..13], languages.resolve("css").?, .dark); defer alloc.free(css); const sql = try highlight(alloc, source[14..34], languages.resolve("sql").?, .dark); defer alloc.free(sql); const html = try highlight(alloc, source[35..], languages.resolve("html").?, .dark); defer alloc.free(html);
try std.testing.expect(std.mem.indexOf(u8, css, "\x1b[38;5;245m/* comment */\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, sql, "\x1b[38;5;252mSELECT\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, html, "\x1b[38;5;245m<!-- note -->\x1b[39m") != null);}