summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGrace Yoder <grace_yoder@outlook.com>2026-07-17 19:54:15 -0600
committerGrace Yoder <grace_yoder@outlook.com>2026-07-17 21:47:26 -0600
commit02132764068aa51ac98628d988dff15255308df0 (patch)
tree24455db4fc2d6f2c8941499cd0f586eedc2302e2 /src
feat: zig rewrite
Diffstat (limited to 'src')
-rwxr-xr-xsrc/cgi.zig79
-rwxr-xr-xsrc/main.zig59
-rwxr-xr-xsrc/root.zig76
-rwxr-xr-xsrc/tags.zig4
-rwxr-xr-xsrc/tags/Export.zig52
-rwxr-xr-xsrc/tags/Include.zig58
-rwxr-xr-xsrc/tags/Syntax.zig169
-rwxr-xr-xsrc/tags/Use.zig48
-rwxr-xr-xsrc/utils.zig23
9 files changed, 568 insertions, 0 deletions
diff --git a/src/cgi.zig b/src/cgi.zig
new file mode 100755
index 0000000..849ef8c
--- /dev/null
+++ b/src/cgi.zig
@@ -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();
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100755
index 0000000..18b8872
--- /dev/null
+++ b/src/main.zig
@@ -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;
+}
diff --git a/src/root.zig b/src/root.zig
new file mode 100755
index 0000000..3ddfe78
--- /dev/null
+++ b/src/root.zig
@@ -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);
+ }
+}
diff --git a/src/tags.zig b/src/tags.zig
new file mode 100755
index 0000000..3f4169e
--- /dev/null
+++ b/src/tags.zig
@@ -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;
diff --git a/src/tags/Export.zig b/src/tags/Export.zig
new file mode 100755
index 0000000..4b64048
--- /dev/null
+++ b/src/tags/Export.zig
@@ -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());
+}
diff --git a/src/tags/Include.zig b/src/tags/Include.zig
new file mode 100755
index 0000000..87f0b3f
--- /dev/null
+++ b/src/tags/Include.zig
@@ -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);
+}
diff --git a/src/tags/Syntax.zig b/src/tags/Syntax.zig
new file mode 100755
index 0000000..03930b1
--- /dev/null
+++ b/src/tags/Syntax.zig
@@ -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;
+ }
+};
diff --git a/src/tags/Use.zig b/src/tags/Use.zig
new file mode 100755
index 0000000..6ba1b1b
--- /dev/null
+++ b/src/tags/Use.zig
@@ -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;
+ }
+}
diff --git a/src/utils.zig b/src/utils.zig
new file mode 100755
index 0000000..ae03d76
--- /dev/null
+++ b/src/utils.zig
@@ -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];
+}