feat: zig rewrite
This commit is contained in:
commit
0213276406
19 changed files with 865 additions and 0 deletions
63
.crow/build.yml
Normal file
63
.crow/build.yml
Normal 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
23
.crow/release.yml
Normal 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
19
.gitignore
vendored
Executable 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
19
LICENSE
Normal 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
9
README.md
Normal 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
112
build.zig
Executable 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
24
build.zig.zon
Executable 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
3
examples/a.htmlua
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<p>included file!</p>
|
||||||
|
<use part=first></use>
|
||||||
|
<use part=second></use>
|
||||||
4
examples/config.zon
Executable file
4
examples/config.zon
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.{
|
||||||
|
.component_path = "./examples",
|
||||||
|
.template_path = "./examples",
|
||||||
|
}
|
||||||
21
examples/main.htmlua
Executable file
21
examples/main.htmlua
Executable 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
79
src/cgi.zig
Executable 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
59
src/main.zig
Executable 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
76
src/root.zig
Executable 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
4
src/tags.zig
Executable 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
52
src/tags/Export.zig
Executable 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
58
src/tags/Include.zig
Executable 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
169
src/tags/Syntax.zig
Executable 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
48
src/tags/Use.zig
Executable 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
23
src/utils.zig
Executable 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];
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue