aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.crow/build.yml59
-rwxr-xr-x.gitignore19
-rwxr-xr-xbuild.zig112
-rwxr-xr-xbuild.zig.zon52
-rwxr-xr-xexamples/a.htmlua3
-rwxr-xr-xexamples/config.zon4
-rwxr-xr-xexamples/main.htmlua21
-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
16 files changed, 838 insertions, 0 deletions
diff --git a/.crow/build.yml b/.crow/build.yml
new file mode 100644
index 0000000..c2e1c94
--- /dev/null
+++ b/.crow/build.yml
@@ -0,0 +1,59 @@
+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
+ - 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
+ - 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
+ - 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
+ - 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/.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/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..56f58dc
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,52 @@
+.{
+ // This is the default name used by packages depending on this one. For
+ // example, when a user runs `zig fetch --save <url>`, this field is used
+ // as the key in the `dependencies` table. Although the user can choose a
+ // different name, most users will stick with this provided value.
+ //
+ // It is redundant to include "zig" in this name because it is already
+ // within the Zig package namespace.
+ .name = .htmlua,
+ // This is a [Semantic Version](https://semver.org/).
+ // In a future version of Zig it will be used for package deduplication.
+ .version = "0.0.0",
+ // Together with name, this represents a globally unique package
+ // identifier. This field is generated by the Zig toolchain when the
+ // package is first created, and then *never changes*. This allows
+ // unambiguous detection of one package being an updated version of
+ // another.
+ //
+ // When forking a Zig project, this id should be regenerated (delete the
+ // field and run `zig build`) if the upstream project is still maintained.
+ // Otherwise, the fork is *hostile*, attempting to take control over the
+ // original project's identity. Thus it is recommended to leave the comment
+ // on the following line intact, so that it shows up in code reviews that
+ // modify the field.
+ .fingerprint = 0xb94bab7ca04adb0c, // Changing this has security and trust implications.
+ // Tracks the earliest Zig version that the package considers to be a
+ // supported use case.
+ .minimum_zig_version = "0.16.0",
+ // This field is optional.
+ // Each dependency must either provide a `url` and `hash`, or a `path`.
+ // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
+ // Once all dependencies are fetched, `zig build` no longer requires
+ // internet connectivity.
+ .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",
+ // For example...
+ //"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 @@
+<p>included file!</p>
+<use part=first></use>
+<use part=second></use>
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 @@
+<!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>
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];
+}