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