commit 02132764068aa51ac98628d988dff15255308df0 Author: Grace Yoder Date: Fri Jul 17 19:54:15 2026 -0600 feat: zig rewrite diff --git a/.crow/build.yml b/.crow/build.yml new file mode 100644 index 0000000..9163a4d --- /dev/null +++ b/.crow/build.yml @@ -0,0 +1,63 @@ +when: + - event: tag + +clone: + git: + image: codefloe.com/crow-plugins/clone:1.1.0 + settings: + tags: true + +variables: + - &zig_image 'ghcr.io/rust-cross/cargo-zigbuild:0.23' + +steps: + - name: build-linux-x86_64 + image: *zig_image + commands: + # - zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe --prefix zig-out-linux-x86_64 + - zig build -Dtarget=x86_64-linux-musl --prefix zig-out-linux-x86_64 + - cp zig-out-linux-x86_64/bin/htmlua htmlua-linux-x86_64 + - cp zig-out-linux-x86_64/bin/htmlua-cgi htmlua-cgi-linux-x86_64 + + - name: build-linux-aarch64 + image: *zig_image + commands: + # - zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe --prefix zig-out-linux-aarch64 + - zig build -Dtarget=aarch64-linux-musl --prefix zig-out-linux-aarch64 + - cp zig-out-linux-aarch64/bin/htmlua htmlua-linux-aarch64 + - cp zig-out-linux-aarch64/bin/htmlua-cgi htmlua-cgi-linux-aarch64 + + - name: build-macos-x86_64 + image: *zig_image + commands: + # - zig build -Dtarget=x86_64-macos -Doptimize=ReleaseSafe --prefix zig-out-macos-x86_64 --sysroot /opt/MacOSX11.3.sdk + - zig build -Dtarget=x86_64-macos --prefix zig-out-macos-x86_64 --sysroot /opt/MacOSX11.3.sdk + - cp zig-out-macos-x86_64/bin/htmlua htmlua-macos-x86_64 + - cp zig-out-macos-x86_64/bin/htmlua-cgi htmlua-cgi-macos-x86_64 + + - name: build-macos-aarch64 + image: *zig_image + commands: + # - zig build -Dtarget=aarch64-macos -Doptimize=ReleaseSafe --prefix zig-out-macos-aarch64 --sysroot /opt/MacOSX11.3.sdk + - zig build -Dtarget=aarch64-macos --prefix zig-out-macos-aarch64 --sysroot /opt/MacOSX11.3.sdk + - cp zig-out-macos-aarch64/bin/htmlua htmlua-macos-aarch64 + - cp zig-out-macos-aarch64/bin/htmlua-cgi htmlua-cgi-macos-aarch64 + + + - name: release + image: woodpeckerci/plugin-release + depends_on: [build-linux-x86_64, build-linux-aarch64, build-macos-x86_64, build-macos-aarch64] + settings: + api_key: + from_secret: forgejo_token + files: + - htmlua-linux-x86_64 + - htmlua-linux-aarch64 + - htmlua-macos-x86_64 + - htmlua-macos-aarch64 + - htmlua-cgi-linux-x86_64 + - htmlua-cgi-linux-aarch64 + - htmlua-cgi-macos-x86_64 + - htmlua-cgi-macos-aarch64 + checksum: sha256 + file_exists: overwrite diff --git a/.crow/release.yml b/.crow/release.yml new file mode 100644 index 0000000..3770f56 --- /dev/null +++ b/.crow/release.yml @@ -0,0 +1,23 @@ +# This file is a bit of a hack in order to run the real release CI +when: + - event: push + branch: ${CI_REPO_DEFAULT_BRANCH} + +clone: + git: + image: codefloe.com/crow-plugins/clone:1.1.0 + settings: + tags: true + + +steps: + - name: retag-latest + image: alpine:3.20 + environment: + FORGEJO_TOKEN: + from_secret: forgejo_token + commands: + - apk add --no-cache git + - git remote set-url origin "https://oauth2:$${FORGEJO_TOKEN}@$${CI_FORGE_URL#https://}/$${CI_REPO}.git" + - git tag -f latest + - git push -f origin latest diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..8493d22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# This file is for zig-specific build artifacts. +# If you have OS-specific or editor-specific files to ignore, +# such as *.swp or .DS_Store, put those in your global +# ~/.gitignore and put this in your ~/.gitconfig: +# +# [core] +# excludesfile = ~/.gitignore +# +# Cheers! +# -andrewrk + +.zig-cache/ +zig-out/ +zig-pkg/ +/release/ +/debug/ +/build/ +/build-*/ +/docgen_tmp/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c5954d --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2026 Grace Yoder + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9232026 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# HTMLua + +### An HTML templating engine with (not yet reimplemented) Lua scripting abilities + +See in action at [grace.pink](https://grace.pink/index.htmlua) + +This is currently an incomplete rewrite. If you want to have the old rust +version you are able to checkout the `rust` tag. + diff --git a/build.zig b/build.zig new file mode 100755 index 0000000..42d6c20 --- /dev/null +++ b/build.zig @@ -0,0 +1,112 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const ezts = b.dependency("eazy_zig_tree_sitter", .{ + .target = target, + .optimize = optimize, + .use_all_langs = true, + .use_third_party = true, + }); + + const ts = b.dependency("tree_sitter", .{ + .target = target, + .optimize = optimize, + }); + + const mod = b.addModule("htmlua", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .imports = &.{ + .{ .name = "easy-zig-tree-sitter", .module = ezts.module("eazy_zig_tree_sitter") }, + .{ .name = "tree-sitter", .module = ts.module("tree_sitter") }, + }, + }); + + // Debug bin + const exe = b.addExecutable(.{ + .name = "htmlua", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // CGI bin + const cgi = b.addExecutable(.{ + .name = "htmlua-cgi", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cgi.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + + b.installArtifact(cgi); + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + + const run_mod_tests = b.addRunArtifact(mod_tests); + + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + const run_exe_tests = b.addRunArtifact(exe_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); + test_step.dependOn(&run_exe_tests.step); + + // https://zigtools.org/zls/guides/build-on-save/ + const exe_check = b.addExecutable(.{ + .name = "htmlua", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + const cgi_check = b.addExecutable(.{ + .name = "htmlua-cgi", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cgi.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "htmlua", .module = mod }, + }, + }), + }); + + const check = b.step("check", "Check if htmlua compiles"); + check.dependOn(&exe_check.step); + check.dependOn(&cgi_check.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100755 index 0000000..297ddc1 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .htmlua, + .version = "1.0.0-alpha", + .fingerprint = 0xb94bab7ca04adb0c, + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .tree_sitter = .{ + .url = "git+https://github.com/tree-sitter/zig-tree-sitter.git#b562510b5563dcf3b7fcac4a5fe9d60834c84667", + .hash = "tree_sitter-0.26.0-8heIf-vDAQDh0HhAAUVwVZFmLrwSKMZl5gRaVFqtas0W", + }, + .eazy_zig_tree_sitter = .{ + .url = "git+https://git.gae.moe/grace/eazy-zig-tree-sitter.git#8d1fa80a0e225282f607a1e2b2504cfdbf427cfa", + .hash = "eazy_zig_tree_sitter-0.0.0-QEABqToEAwCwlPVt4G7QS6E6vicz4SISg8N93xRr8t8K", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "examples", + "LICENSE", + "README.md", + }, +} diff --git a/examples/a.htmlua b/examples/a.htmlua new file mode 100755 index 0000000..4857b50 --- /dev/null +++ b/examples/a.htmlua @@ -0,0 +1,3 @@ +

included file!

+ + diff --git a/examples/config.zon b/examples/config.zon new file mode 100755 index 0000000..2335fbe --- /dev/null +++ b/examples/config.zon @@ -0,0 +1,4 @@ +.{ + .component_path = "./examples", + .template_path = "./examples", +} diff --git a/examples/main.htmlua b/examples/main.htmlua new file mode 100755 index 0000000..d7827c2 --- /dev/null +++ b/examples/main.htmlua @@ -0,0 +1,21 @@ + + + + + main + + + +

hello

+ + +

exported from main!

+
+ +

exported from main but again!

+
+
+ + + + 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]; +}