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

63
.crow/build.yml Normal file
View file

@ -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

23
.crow/release.yml Normal file
View file

@ -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

19
.gitignore vendored Executable file
View file

@ -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/

19
LICENSE Normal file
View file

@ -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.

9
README.md Normal file
View file

@ -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.

112
build.zig Executable file
View file

@ -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);
}

24
build.zig.zon Executable file
View file

@ -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",
},
}

3
examples/a.htmlua Executable file
View file

@ -0,0 +1,3 @@
<p>included file!</p>
<use part=first></use>
<use part=second></use>

4
examples/config.zon Executable file
View file

@ -0,0 +1,4 @@
.{
.component_path = "./examples",
.template_path = "./examples",
}

21
examples/main.htmlua Executable file
View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>main</title>
</head>
<body>
<h1>hello</h1>
<include path="a.htmlua">
<export part=first>
<p><b>exported from main!</b></p>
</export>
<export part=second>
<p><b>exported from main but again!</b></p>
</export>
</include>
</body>
</html>

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