feat: zig rewrite
All checks were successful
ci/crow/push/release Pipeline was successful
ci/crow/tag/build Pipeline was successful

This commit is contained in:
Grace Yoder 2026-07-17 19:54:15 -06:00
commit 0213276406
Signed by: grace
SSH key fingerprint: SHA256:oG9v0ZlohsoMAg083HAwBGOcaPLF3nIuOlAW4MtASMo
19 changed files with 865 additions and 0 deletions

79
src/cgi.zig Executable file
View file

@ -0,0 +1,79 @@
const std = @import("std");
const Io = std.Io;
const File = Io.File;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ArrayList = std.ArrayList;
const StringArrayHashMap = std.StringArrayHashMapUnmanaged;
const HTMLua = @import("htmlua");
const Config = struct {
component_path: []const u8,
template_path: []const u8,
};
var component_dir: Io.Dir = undefined;
pub fn main(init: std.process.Init) !void {
const io = init.io;
var arena = std.heap.ArenaAllocator.init(init.gpa);
defer arena.deinit();
const alloc = arena.allocator();
var args = init.minimal.args.iterate();
const cwd = std.Io.Dir.cwd();
if (!args.skip()) unreachable;
const config_file_path = args.next().?;
const config_buf = try cwd.readFileAllocOptions(io, config_file_path, alloc, .unlimited, std.mem.Alignment.@"1", '\x00');
const config = try std.zon.parse.fromSliceAlloc(
Config,
alloc,
config_buf,
null,
.{},
);
component_dir = try cwd.openDir(io, config.component_path, .{});
const template_dir = try cwd.openDir(io, config.template_path, .{});
var state: HTMLua.Parser = .{
.tags = &[_]HTMLua.ParserTag{
HTMLua.tags.Include,
HTMLua.tags.Export,
HTMLua.tags.Use,
HTMLua.tags.Syntax,
},
.tag_stack = try ArrayList([]const u8).initCapacity(alloc, 256),
.exported_parts = try StringArrayHashMap([]const u8).init(alloc, &[0][]const u8{}, &[0][]const u8{}),
.component_resolver = struct {
pub fn resolve(arena_inner: *ArenaAllocator, io_inner: Io, path: []const u8) anyerror!*Io.Reader {
const read_buff = try arena_inner.allocator().alloc(u8, 16384);
const clean_path = try HTMLua.utils.sanitizePath(arena_inner.allocator(), path);
var file = try component_dir.openFile(io_inner, clean_path, .{ .mode = .read_only });
const reader = try arena_inner.allocator().create(File.Reader);
reader.* = file.reader(io_inner, read_buff);
return &reader.interface;
}
}.resolve,
};
const request_path = init.environ_map.get("PATH_INFO").?;
const clean_request_path = try HTMLua.utils.sanitizePath(alloc, request_path);
const file = try template_dir.openFile(io, clean_request_path, .{ .mode = .read_only });
defer file.close(io);
const in_buf = try alloc.alloc(u8, 16384);
var in = file.reader(io, in_buf);
const out_buf = try alloc.alloc(u8, 16384);
var out = File.stdout().writer(io, out_buf);
try out.interface.writeAll("Content-type: text/html\n");
try HTMLua.parseTemplate(&arena, io, &state, &in.interface, &out.interface);
try out.flush();
}

59
src/main.zig Executable file
View file

@ -0,0 +1,59 @@
const std = @import("std");
const Io = std.Io;
const File = Io.File;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ArrayList = std.ArrayList;
const StringArrayHashMap = std.StringArrayHashMapUnmanaged;
const HTMLua = @import("root.zig");
pub fn main(init: std.process.Init) !void {
const io = init.io;
var arena = std.heap.ArenaAllocator.init(init.gpa);
defer arena.deinit();
const alloc = arena.allocator();
var state: HTMLua.Parser = .{
.component_resolver = &resolver,
.tags = &[_]HTMLua.ParserTag{
HTMLua.tags.Include,
HTMLua.tags.Export,
HTMLua.tags.Use,
},
.tag_stack = try ArrayList([]const u8).initCapacity(alloc, 256),
.exported_parts = try StringArrayHashMap([]const u8).init(alloc, &[0][]const u8{}, &[0][]const u8{}),
};
// I dont like this but this is temporary so its ok
const args = try init.minimal.args.toSlice(alloc);
const file_path = if (args.len != 1) args[1] else null;
const file = if (file_path) |fp|
try Io.Dir.cwd().openFile(io, fp, .{ .mode = .read_only })
else
null;
defer if (file) |f| f.close(io);
const in_buf = try alloc.alloc(u8, 1024);
var in =
if (file) |f| f.reader(io, in_buf) else File.stdin().reader(io, in_buf);
const out_buf = try alloc.alloc(u8, 1024);
var out = File.stdout().writer(io, out_buf);
try HTMLua.parseTemplate(&arena, io, &state, &in.interface, &out.interface);
try out.flush();
}
// https://ziggit.dev/t/zig-0-15-1-reader-writer-dont-make-copies-of-fieldparentptr-based-interfaces/11719
fn resolver(arena: *ArenaAllocator, io: Io, path: []const u8) anyerror!*Io.Reader {
const read_buff = try arena.allocator().alloc(u8, 1024);
const cwd = std.Io.Dir.cwd();
var file = try cwd.openFile(io, path, .{ .mode = .read_only });
const reader = try arena.allocator().create(Io.File.Reader);
reader.* = file.reader(io, read_buff);
return &reader.interface;
}

76
src/root.zig Executable file
View file

@ -0,0 +1,76 @@
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const SplitIterator = std.mem.SplitIterator;
const DelimiterType = std.mem.DelimiterType;
const ArrayList = std.ArrayList;
const StringArrayHashMap = std.StringArrayHashMapUnmanaged;
pub const tags = @import("tags.zig");
pub const utils = @import("utils.zig");
pub const ParserFn =
fn (
arena: *ArenaAllocator,
io: Io,
state: *Parser,
reader: *Io.Reader,
writer: *Io.Writer,
attrs: *SplitIterator(u8, DelimiterType.scalar),
) anyerror!void;
pub const ParserTag = struct {
name: []const u8,
parse_fn: *const ParserFn,
};
pub const Parser = struct {
component_resolver: *const fn (alloc: *ArenaAllocator, io: Io, path: []const u8) anyerror!*Io.Reader,
exported_parts: StringArrayHashMap([]const u8),
tags: []const ParserTag,
tag_stack: ArrayList([]const u8),
};
pub fn parseTemplate(arena: *ArenaAllocator, io: Io, state: *Parser, reader: *Io.Reader, writer: *Io.Writer) !void {
outer: while (true) {
_ = reader.streamDelimiter(writer, '<') catch |err| {
if (err == Io.Reader.Error.EndOfStream) break else return err;
};
// consume '<'
_ = try reader.takeByte();
if (try reader.peekByte() == '/') {
// consume '/'
_ = try reader.takeByte();
const last_tag = state.tag_stack.getLastOrNull() orelse "";
// if reached closing tag, send execution back up the stack
if (std.mem.eql(u8, last_tag, try reader.peekDelimiterExclusive('>'))) {
// consume closing tag
_ = try reader.takeDelimiterInclusive('>');
return;
} else {
try writer.writeAll("</");
continue :outer;
}
}
const inside = reader.takeDelimiterExclusive('>') catch |err| {
if (err == error.StreamTooLong) {
try writer.writeByte('<');
break :outer;
} else return err;
};
var it = std.mem.splitScalar(u8, inside, ' ');
const tag_name = it.first();
for (state.tags) |tag| {
if (std.mem.eql(u8, tag.name, tag_name) == true) {
// consume '>'
_ = try reader.takeByte();
try tag.parse_fn(arena, io, state, reader, writer, &it);
continue :outer;
}
}
try writer.writeByte('<');
try writer.writeAll(inside);
}
}

4
src/tags.zig Executable file
View file

@ -0,0 +1,4 @@
pub const Include = @import("tags/Include.zig").Include;
pub const Export = @import("tags/Export.zig").Export;
pub const Use = @import("tags/Use.zig").Use;
pub const Syntax = @import("tags/Syntax.zig").Syntax;

52
src/tags/Export.zig Executable file
View file

@ -0,0 +1,52 @@
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const SplitIterator = std.mem.SplitIterator;
const DelimiterType = std.mem.DelimiterType;
const parser = @import("../root.zig");
const Parser = parser.Parser;
pub const ExportTagError = error{
NoPartAttribute,
};
pub const Export: parser.ParserTag = .{
.name = "export",
.parse_fn = &parse,
};
fn parse(
arena: *ArenaAllocator,
io: Io,
state: *Parser,
reader: *Io.Reader,
writer: *Io.Writer,
attrs: *SplitIterator(u8, DelimiterType.scalar),
) anyerror!void {
_ = writer;
const raw_part_attr: []const u8 = while (attrs.next()) |attr| {
var it = std.mem.splitScalar(u8, attr, '=');
if (std.mem.eql(u8, it.first(), "part")) {
break it.next() orelse return ExportTagError.NoPartAttribute;
}
} else return ExportTagError.NoPartAttribute;
const part = try arena.allocator().dupe(u8, raw_part_attr);
var part_buffer = try Io.Writer.Allocating.initCapacity(arena.allocator(), 1024);
try state.tag_stack.append(arena.allocator(), "export");
try parser.parseTemplate(
arena,
io,
state,
reader,
&part_buffer.writer,
);
_ = state.tag_stack.pop();
try state.exported_parts.put(arena.allocator(), part, try part_buffer.toOwnedSlice());
}

58
src/tags/Include.zig Executable file
View file

@ -0,0 +1,58 @@
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const SplitIterator = std.mem.SplitIterator;
const DelimiterType = std.mem.DelimiterType;
const parser = @import("../root.zig");
const Parser = parser.Parser;
pub const IncludeTagError = error{
NoPathAttribute,
PathOpenFailure,
};
pub const Include: parser.ParserTag = .{
.name = "include",
.parse_fn = &parse,
};
fn parse(
arena: *ArenaAllocator,
io: Io,
state: *Parser,
reader: *Io.Reader,
writer: *Io.Writer,
attrs: *SplitIterator(u8, DelimiterType.scalar),
) anyerror!void {
const raw_path_attr: []const u8 = while (attrs.next()) |attr| {
var it = std.mem.splitScalar(u8, attr, '=');
if (std.mem.eql(u8, it.first(), "path")) {
const raw_path = it.next() orelse return IncludeTagError.NoPathAttribute;
if (raw_path.len == 0) return IncludeTagError.NoPathAttribute;
if ((raw_path[0] == '"' and raw_path[raw_path.len - 1] == '"') or
(raw_path[0] == '\'' and raw_path[raw_path.len - 1] == '\''))
{
if (raw_path.len <= 2) return IncludeTagError.NoPathAttribute;
break raw_path[1 .. raw_path.len - 1];
} else {
break raw_path;
}
}
} else return IncludeTagError.NoPathAttribute;
const path = try arena.allocator().dupe(u8, raw_path_attr);
// parse through to gather <export> tags
try state.tag_stack.append(arena.allocator(), "include");
try parser.parseTemplate(arena, io, state, reader, writer);
_ = state.tag_stack.pop();
const included_reader = state.component_resolver(arena, io, path) catch {
std.log.err("Include Tag: failed to open file {s}", .{path});
return IncludeTagError.PathOpenFailure;
};
try parser.parseTemplate(arena, io, state, included_reader, writer);
}

169
src/tags/Syntax.zig Executable file
View file

@ -0,0 +1,169 @@
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const SplitIterator = std.mem.SplitIterator;
const DelimiterType = std.mem.DelimiterType;
const ezts = @import("easy-zig-tree-sitter");
const ts = @import("tree-sitter");
const parser = @import("../root.zig");
const Parser = parser.Parser;
pub const SyntaxTagError = error{InvalidLanguage};
pub const Syntax: parser.ParserTag = .{
.name = "syntax",
.parse_fn = &parse,
};
fn parse(
arena: *ArenaAllocator,
io: Io,
state: *Parser,
reader: *Io.Reader,
writer: *Io.Writer,
attrs: *SplitIterator(u8, DelimiterType.scalar),
) anyerror!void {
_ = state;
_ = io;
const raw_lang_attr: []const u8 = while (attrs.next()) |attr| {
var it = std.mem.splitScalar(u8, attr, '=');
if (std.mem.eql(u8, it.first(), "lang")) {
break it.next() orelse "";
}
} else "";
const lang_key = try arena.allocator().dupe(u8, raw_lang_attr);
_ = try reader.takeDelimiterInclusive('\n');
const first_line = try reader.peekDelimiterInclusive('\n');
const whitespace_chr = for (first_line, 0..) |chr, idx| {
if (!(chr == ' ' or chr == '\t')) break idx;
} else 0;
var code = try std.ArrayList(u8).initCapacity(arena.allocator(), 1024);
while (true) {
const line = try reader.takeDelimiterInclusive('\n');
if (std.mem.indexOf(u8, line, "</syntax>")) |idx| {
try code.appendSlice(arena.allocator(), line[0..idx]);
// TODO: tags at the end will break the parser
break;
}
const whitespace_lem = @min(line.len, whitespace_chr);
if (std.mem.countScalar(u8, line[0..whitespace_lem], ' ') + std.mem.countScalar(u8, line[0..whitespace_lem], '\t') < whitespace_lem) {
try code.appendSlice(arena.allocator(), line);
} else {
try code.appendSlice(arena.allocator(), line[whitespace_lem..line.len]);
}
}
// Alignment issues?
// ts.setAllocator(arena.allocator());
// TODO: TS Thread Mutex
// TODO: null handeling
var hlp = HLParser.create(lang_key, code.items) orelse {
std.log.warn("Failed to make Parser for {s}", .{lang_key});
return;
};
defer hlp.destroy();
var next_hl = hlp.nextHL();
var end_stack = try std.ArrayList(u32).initCapacity(arena.allocator(), 1024);
var code_idx: u32 = 0;
try writer.writeAll("<pre class=\"hl\"><code>");
while (code_idx != code.items.len) {
var next_idx: u32 = @intCast(code.items.len);
if (end_stack.getLastOrNull()) |last| {
if (last == code_idx) {
try writer.writeAll("</span>");
_ = end_stack.pop();
continue;
} else if (last < next_idx) {
next_idx = last;
}
}
if (next_hl) |hl| {
if (hl.start == code_idx) {
try writer.writeAll("<span class=\"hl");
// no '.' in css class names
var it = std.mem.splitScalar(u8, hl.name, '.');
while (it.next()) |n| {
try writer.writeByte('-');
try writer.writeAll(n);
}
try writer.writeAll("\">");
try end_stack.append(arena.allocator(), hl.end);
next_hl = hlp.nextHL();
continue;
} else if (hl.start < next_idx) {
next_idx = hl.start;
}
}
try writer.writeAll(code.items[code_idx..next_idx]);
code_idx = next_idx;
}
try writer.writeAll("</code></pre>");
}
const HLGroup = struct {
name: []const u8,
start: u32,
end: u32,
};
const HLParser = struct {
lang: *ts.Language,
tree: *ts.Tree,
query: *ts.Query,
cursor: *ts.QueryCursor,
fn create(lang_key: []const u8, code: []const u8) ?HLParser {
const lang = ezts.getLang(lang_key) orelse return null;
const query_string = ezts.getQuery(lang_key, "highlights") orelse return null;
const ps = ts.Parser.create();
ps.setLanguage(lang) catch return null;
const tree = ps.parseString(code, null) orelse return null;
var error_offset: u32 = 0;
const query = ts.Query.create(lang, query_string, &error_offset) catch return null;
const cursor = ts.QueryCursor.create();
cursor.exec(query, tree.rootNode());
return .{
.lang = lang,
.tree = tree,
.query = query,
.cursor = cursor,
};
}
fn destroy(self: *HLParser) void {
self.cursor.destroy();
self.query.destroy();
self.tree.destroy();
self.lang.destroy();
}
fn nextHL(self: *HLParser) ?HLGroup {
if (self.cursor.nextCapture()) |data| {
const idx, const match = data;
const c = match.captures[idx];
return .{
.start = c.node.startByte(),
.end = c.node.endByte(),
.name = self.query.captureNameForId(c.index) orelse return null,
};
} else return null;
}
};

48
src/tags/Use.zig Executable file
View file

@ -0,0 +1,48 @@
const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const SplitIterator = std.mem.SplitIterator;
const DelimiterType = std.mem.DelimiterType;
const parser = @import("../root.zig");
const Parser = parser.Parser;
pub const UseTagError = error{ NoPartAttribute, NoPartExported };
pub const Use: parser.ParserTag = .{
.name = "use",
.parse_fn = &parse,
};
fn parse(
arena: *ArenaAllocator,
io: Io,
state: *Parser,
reader: *Io.Reader,
writer: *Io.Writer,
attrs: *SplitIterator(u8, DelimiterType.scalar),
) anyerror!void {
_ = io;
const raw_part_attr: []const u8 = while (attrs.next()) |attr| {
var it = std.mem.splitScalar(u8, attr, '=');
if (std.mem.eql(u8, it.first(), "part")) {
break it.next() orelse return UseTagError.NoPartAttribute;
}
} else return UseTagError.NoPartAttribute;
const part = try arena.allocator().dupe(u8, raw_part_attr);
const part_text = state.exported_parts.get(part) orelse return UseTagError.NoPartExported;
var part_reader = Io.Reader.fixed(part_text);
_ = try part_reader.streamRemaining(writer);
while (true) {
_ = try reader.discardDelimiterInclusive('<');
const inside = try reader.takeDelimiterInclusive('>');
if (std.mem.eql(u8, "/use>", inside)) return;
}
}

23
src/utils.zig Executable file
View file

@ -0,0 +1,23 @@
const std = @import("std");
const Io = std.Io;
const File = Io.File;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ArrayList = std.ArrayList;
const StringArrayHashMap = std.StringArrayHashMapUnmanaged;
pub fn sanitizePath(alloc: Allocator, path: []const u8) ![]const u8 {
const clean = try alloc.alloc(u8, path.len);
var list = ArrayList(u8).initBuffer(clean);
var pit = std.mem.splitScalar(u8, path, '/');
while (pit.next()) |part| {
if (part.len == 0) continue;
if (std.mem.eql(u8, part, "..")) continue;
// This should never actually allocate
try list.appendSlice(alloc, part);
try list.append(alloc, '/');
}
const new_len = list.items.len - 1;
_ = alloc.resize(clean, new_len);
return clean[0..new_len];
}