From 55a1697e7175c4c82989c0633ef2227fb0295b8e Mon Sep 17 00:00:00 2001 From: Grace Yoder Date: Fri, 17 Jul 2026 19:54:15 -0600 Subject: --- src/cgi.zig | 79 ++++++++++++++++++++++++ src/main.zig | 59 ++++++++++++++++++ src/root.zig | 76 +++++++++++++++++++++++ src/tags.zig | 4 ++ src/tags/Export.zig | 52 ++++++++++++++++ src/tags/Include.zig | 58 ++++++++++++++++++ src/tags/Syntax.zig | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/tags/Use.zig | 48 +++++++++++++++ src/utils.zig | 23 +++++++ 9 files changed, 568 insertions(+) create mode 100755 src/cgi.zig create mode 100755 src/main.zig create mode 100755 src/root.zig create mode 100755 src/tags.zig create mode 100755 src/tags/Export.zig create mode 100755 src/tags/Include.zig create mode 100755 src/tags/Syntax.zig create mode 100755 src/tags/Use.zig create mode 100755 src/utils.zig (limited to 'src') 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("') 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 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, "")) |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("
");
+
+    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("");
+                _ = end_stack.pop();
+                continue;
+            } else if (last < next_idx) {
+                next_idx = last;
+            }
+        }
+        if (next_hl) |hl| {
+            if (hl.start == code_idx) {
+                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("
"); +} + +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]; +} -- cgit v1.3.1