Compare commits
No commits in common. "main" and "httpstdlib" have entirely different histories.
main
...
httpstdlib
40 changed files with 3428 additions and 864 deletions
|
|
@ -1,63 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# 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
|
||||
38
.gitignore
vendored
Executable file → Normal file
38
.gitignore
vendored
Executable file → Normal file
|
|
@ -1,19 +1,21 @@
|
|||
# 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
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug
|
||||
target
|
||||
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
zig-pkg/
|
||||
/release/
|
||||
/debug/
|
||||
/build/
|
||||
/build-*/
|
||||
/docgen_tmp/
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Generated by cargo mutants
|
||||
# Contains mutation testing data
|
||||
**/mutants.out*/
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
|
|
|||
11
.rustfmt.toml
Normal file
11
.rustfmt.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
unstable_features = true
|
||||
|
||||
max_width = 120
|
||||
fn_params_layout = "Compressed"
|
||||
fn_single_line = true
|
||||
fn_call_width = 120
|
||||
where_single_line = true
|
||||
brace_style = "PreferSameLine"
|
||||
blank_lines_upper_bound = 5
|
||||
combine_control_expr = false
|
||||
wrap_comments = false
|
||||
2691
Cargo.lock
generated
Normal file
2691
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[workspace]
|
||||
members = [ "htmlua-apache-cgi","htmlua-parser", "htmlua-server"]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
19
LICENSE
19
LICENSE
|
|
@ -1,19 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# 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
112
build.zig
|
|
@ -1,112 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
.{
|
||||
.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",
|
||||
},
|
||||
}
|
||||
10
cspell.json
Normal file
10
cspell.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"words": [
|
||||
"htmlua",
|
||||
"headermap",
|
||||
"reqwest"
|
||||
],
|
||||
"userWords": [
|
||||
"mlua"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<p>included file!</p>
|
||||
<use part=first></use>
|
||||
<use part=second></use>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.{
|
||||
.component_path = "./examples",
|
||||
.template_path = "./examples",
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<!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>
|
||||
8
htmlua-apache-cgi/Cargo.toml
Normal file
8
htmlua-apache-cgi/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "htmlua-apache-cgi"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
htmlua-parser = { path = "../htmlua-parser" }
|
||||
kuchikiki = "0.8.2"
|
||||
11
htmlua-apache-cgi/src/main.rs
Normal file
11
htmlua-apache-cgi/src/main.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
use htmlua_parser::serve::serve_content;
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
println!("Content-Type: text/html\n");
|
||||
|
||||
let request_uri = env::var("PATH_INFO").unwrap_or_else(|_| "".to_string());
|
||||
let page = serve_content(request_uri.as_str()).unwrap();
|
||||
|
||||
println!("{page}");
|
||||
}
|
||||
21
htmlua-parser/Cargo.toml
Normal file
21
htmlua-parser/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "htmlua-parser"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
html5ever = "0.35.0"
|
||||
kuchikiki = "0.8.2"
|
||||
markup5ever = "0.11.0"
|
||||
mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize", "anyhow"] }
|
||||
pulldown-cmark = "0.13.0"
|
||||
reqwest = { version = "0.12.22", features = ["blocking", "json"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
syntect = "5.2.0"
|
||||
tendril = "0.4.3"
|
||||
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
32
htmlua-parser/src/helpers.rs
Normal file
32
htmlua-parser/src/helpers.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use std::{fs::File, io::{BufRead, BufReader, Read, Seek, SeekFrom}, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use kuchikiki::NodeRef;
|
||||
use markup5ever::{LocalName, Namespace, QualName};
|
||||
use tendril::TendrilSink;
|
||||
|
||||
|
||||
|
||||
|
||||
pub fn read_doc_from_file(path: PathBuf) -> Result<NodeRef> {
|
||||
|
||||
let mut file = File::open(path)?;
|
||||
let mut reader = BufReader::new(&mut file);
|
||||
|
||||
let mut first_line = String::new();
|
||||
reader.read_line(&mut first_line)?;
|
||||
let first_line_trimmed = first_line.trim();
|
||||
|
||||
let is_whole_doc = first_line_trimmed.eq_ignore_ascii_case("<!DOCTYPE html>");
|
||||
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
let mut page_string = String::new();
|
||||
file.read_to_string(&mut page_string)?;
|
||||
|
||||
if is_whole_doc {
|
||||
Ok(kuchikiki::parse_html().one(page_string))
|
||||
} else {
|
||||
let ctx_name = QualName::new(None, Namespace::from("http://www.w3.org/1999/xhtml"), LocalName::from("div"));
|
||||
Ok(kuchikiki::parse_fragment(ctx_name, Vec::new()).one(page_string))
|
||||
}
|
||||
}
|
||||
151
htmlua-parser/src/htmlua_stdlib.rs
Normal file
151
htmlua-parser/src/htmlua_stdlib.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
use std::{cell::{LazyCell, RefCell}, collections::HashMap, fmt::Write, rc::Rc};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use mlua::{prelude::*, Error, Lua, Table};
|
||||
use reqwest::{blocking::{Client, Response}, header::HeaderMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_value;
|
||||
|
||||
|
||||
pub fn create_htmlua_stdlib(l: &Lua, stdout: &Rc<RefCell<String>>) -> mlua::Result<Table> {
|
||||
let t = l.create_table()?;
|
||||
|
||||
// This cannot be the best way to do this
|
||||
|
||||
let stdout_println = stdout.clone();
|
||||
t.set(
|
||||
"println",
|
||||
l.create_function(move |_, text: String| {
|
||||
let mut stdout_ref = stdout_println.borrow_mut();
|
||||
writeln!(stdout_ref, "{text}").map_err(mlua::Error::external)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
let stdout_print = stdout.clone();
|
||||
t.set(
|
||||
"print",
|
||||
l.create_function(move |_, text: String| {
|
||||
let mut stdout_ref = stdout_print.borrow_mut();
|
||||
write!(stdout_ref, "{text}").map_err(mlua::Error::external)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
t.set("http", create_http_lib(l)?)?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
fn create_http_lib(l: &Lua) -> mlua::Result<Table> {
|
||||
let t = l.create_table()?;
|
||||
let http_client: Rc<LazyCell<Client>> = Rc::new(LazyCell::new(|| Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(3))
|
||||
.user_agent("htmlua/0.1.0")
|
||||
.build().unwrap()
|
||||
));
|
||||
|
||||
|
||||
let client = http_client.clone();
|
||||
t.set(
|
||||
"get",
|
||||
l.create_function(move |_, url: String| {
|
||||
let res = client.get(url).send().map_err(|e| Error::RuntimeError(e.to_string()))?;
|
||||
let lua_res: LuaHttpResponse = TryFrom::try_from(res)?;
|
||||
Ok(lua_res)
|
||||
})?
|
||||
)?;
|
||||
|
||||
let client = http_client.clone();
|
||||
t.set(
|
||||
"post",
|
||||
l.create_function(move |_, url: String| {
|
||||
let res = client.post(url).send().map_err(|e| Error::RuntimeError(e.to_string()))?;
|
||||
let lua_res: LuaHttpResponse = TryFrom::try_from(res)?;
|
||||
Ok(lua_res)
|
||||
})?
|
||||
)?;
|
||||
|
||||
let client = http_client.clone();
|
||||
t.set(
|
||||
"get_with_data",
|
||||
l.create_function(move |_, (url, data): (String, HashMap<String, String>)| {
|
||||
let res = client.get(url).query(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?;
|
||||
let lua_res: LuaHttpResponse = TryFrom::try_from(res)?;
|
||||
Ok(lua_res)
|
||||
})?
|
||||
)?;
|
||||
|
||||
let client = http_client.clone();
|
||||
t.set(
|
||||
"post_with_data_form",
|
||||
l.create_function(move |_, (url, data): (String, HashMap<String, String>)| {
|
||||
let res = client.post(url).form(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?;
|
||||
let lua_res: LuaHttpResponse = TryFrom::try_from(res)?;
|
||||
Ok(lua_res)
|
||||
})?
|
||||
)?;
|
||||
|
||||
let client = http_client.clone();
|
||||
t.set(
|
||||
"post_with_data_json",
|
||||
l.create_function(move |_, (url, data): (String, HashMap<String, String>)| {
|
||||
let res = client.post(url).json(&data).send().map_err(|e| Error::RuntimeError(e.to_string()))?;
|
||||
let lua_res: LuaHttpResponse = TryFrom::try_from(res)?;
|
||||
Ok(lua_res)
|
||||
})?
|
||||
)?;
|
||||
//TODO: all http
|
||||
|
||||
t.set(
|
||||
"decode_json",
|
||||
l.create_function(move |l, text: String| {
|
||||
let table: serde_json::Value = serde_json::from_str(&text).map_err(|e| Error::RuntimeError(e.to_string()))?;
|
||||
Ok(l.to_value(&table))
|
||||
})?
|
||||
)?;
|
||||
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct LuaHttpResponse {
|
||||
headers: HashMap<String, String>,
|
||||
body: String,
|
||||
status: u16,
|
||||
}
|
||||
|
||||
impl TryFrom<Response> for LuaHttpResponse {
|
||||
fn try_from(value: Response) -> Result<Self, Self::Error> {
|
||||
Ok(LuaHttpResponse { headers: headermap_to_hashmap(value.headers()), status: value.status().as_u16(), body: value.text().map_err(|e| Error::RuntimeError(e.to_string()))?, })
|
||||
}
|
||||
|
||||
type Error = Error;
|
||||
}
|
||||
|
||||
fn headermap_to_hashmap(headers: &HeaderMap) -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for (name, value) in headers {
|
||||
if let Ok(value_str) = value.to_str() {
|
||||
map.insert(name.as_str().to_string(), value_str.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
impl IntoLua for LuaHttpResponse {
|
||||
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
|
||||
let table = lua.create_table()?;
|
||||
|
||||
table.set("status", self.status)?;
|
||||
table.set("body", self.body)?;
|
||||
|
||||
let headers_table = lua.create_table()?;
|
||||
for (key, value) in self.headers {
|
||||
headers_table.set(key, value)?;
|
||||
}
|
||||
table.set("headers", headers_table)?;
|
||||
|
||||
Ok(LuaValue::Table(table))
|
||||
}
|
||||
}
|
||||
4
htmlua-parser/src/lib.rs
Normal file
4
htmlua-parser/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod render;
|
||||
pub mod serve;
|
||||
pub mod helpers;
|
||||
pub mod htmlua_stdlib;
|
||||
416
htmlua-parser/src/render.rs
Normal file
416
htmlua-parser/src/render.rs
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
use std::{cell::RefCell, fmt::Write, fs::read_to_string, path::PathBuf, rc::Rc};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use kuchikiki::{NodeRef, traits::TendrilSink};
|
||||
use mlua::{Lua, Table};
|
||||
use pulldown_cmark::{Options, Parser, html};
|
||||
use syntect::{
|
||||
easy::HighlightLines,
|
||||
highlighting::{Style, ThemeSet},
|
||||
html::{IncludeBackground, styled_line_to_highlighted_html},
|
||||
parsing::SyntaxSet,
|
||||
util::LinesWithEndings,
|
||||
};
|
||||
|
||||
use crate::{helpers::read_doc_from_file, htmlua_stdlib::create_htmlua_stdlib};
|
||||
|
||||
|
||||
|
||||
pub fn execute_lua(document: NodeRef) -> Result<NodeRef> {
|
||||
let lua = Lua::new();
|
||||
let globals = lua.globals();
|
||||
|
||||
let stdout: Rc<RefCell<String>> = Rc::new(RefCell::new(String::new()));
|
||||
|
||||
let htmlua_table =
|
||||
create_htmlua_stdlib(&lua, &stdout).map_err(|e| anyhow!("Failed to create Lua stdlib: {}", e))?;
|
||||
|
||||
globals
|
||||
.set("htmlua", htmlua_table)
|
||||
.map_err(|e| anyhow!("Failed to set global: {}", e))?;
|
||||
|
||||
let lua_elements: Vec<_> = match document.select("lua") {
|
||||
Ok(e) => e.collect(),
|
||||
Err(()) => return Err(anyhow!("Unable to find Lua")),
|
||||
};
|
||||
|
||||
for node in lua_elements {
|
||||
if let Some(text_node) = node.as_node().first_child() {
|
||||
if let Some(lua_code) = text_node.as_text() {
|
||||
stdout.borrow_mut().clear();
|
||||
lua.load(lua_code.borrow().as_str())
|
||||
.exec()
|
||||
.map_err(|e| anyhow!("Failed to execute Lua: {}", e))?;
|
||||
node.as_node()
|
||||
.insert_before(NodeRef::new_text(stdout.borrow().as_str()));
|
||||
node.as_node().detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
pub fn process_markdown(document: NodeRef) -> Result<NodeRef> {
|
||||
let markdown_elements: Vec<_> = match document.select("markdown") {
|
||||
Ok(e) => e.collect(),
|
||||
Err(()) => return Err(anyhow!("Unable to find markdown elements")),
|
||||
};
|
||||
for node in markdown_elements {
|
||||
if let Some(text_node) = node.as_node().first_child() {
|
||||
if let Some(markdown_text) = text_node.as_text() {
|
||||
let borrowed_text = markdown_text.borrow();
|
||||
let parser = Parser::new_ext(&borrowed_text, Options::all());
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser);
|
||||
let html_fragment = kuchikiki::parse_html().one(html_output);
|
||||
for child in html_fragment.children() {
|
||||
node.as_node().insert_before(child);
|
||||
}
|
||||
// Remove the original markdown node.
|
||||
node.as_node().detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from: Option<&NodeRef>) -> Result<NodeRef> {
|
||||
if let Some(from_node) = include_from {
|
||||
for i in document
|
||||
.select("includeelement")
|
||||
.map_err(|()| anyhow!("Error finding includeelement"))?
|
||||
{
|
||||
if let Some(name) = i.attributes.borrow().get("name") {
|
||||
let exported = from_node
|
||||
.select_first(format!("exportelement.{name}").as_str())
|
||||
.map_err(|()| anyhow!("Error finding exportelement"))?;
|
||||
exported.as_node().children().rev().for_each(|c| i.as_node().insert_after(c));
|
||||
}
|
||||
}
|
||||
while let Ok(i) = document.select_first("includeelement") {
|
||||
i.as_node().detach();
|
||||
}
|
||||
}
|
||||
|
||||
for i in document
|
||||
.select("include")
|
||||
.map_err(|()| anyhow!("Error finding include"))?
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
let attrs = match i.as_node().as_element() {
|
||||
Some(e) => e.attributes.borrow(),
|
||||
None => continue,
|
||||
};
|
||||
if let Some(include_path) = attrs.get("path") {
|
||||
let mut item_path = component_path.clone();
|
||||
item_path.push(include_path);
|
||||
let new_node = read_doc_from_file(item_path)?;
|
||||
let replaced_node = expand_template(new_node, component_path, Some(i.as_node()))?;
|
||||
replaced_node.select_first("html").map_err(|()| anyhow!("Error finding html"))?.as_node().children().rev().for_each(|c| i.as_node().insert_after(c));
|
||||
|
||||
}
|
||||
}
|
||||
while let Ok(i) = document.select_first("include") {
|
||||
i.as_node().detach();
|
||||
}
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
pub fn process_syntax_highlighting(document: NodeRef) -> Result<NodeRef> {
|
||||
let ps = SyntaxSet::load_defaults_newlines();
|
||||
let mut ts = ThemeSet::load_defaults();
|
||||
let syntax_elements: Vec<_> = match document.select("syntaxhighlight") {
|
||||
Ok(e) => e.collect(),
|
||||
Err(()) => return Err(anyhow!("Unable to find syntaxhighlight elements")),
|
||||
};
|
||||
if !syntax_elements.is_empty() {
|
||||
// TODO: read from config
|
||||
let themes = PathBuf::from("/var/www/htmlua/themes");
|
||||
let _ = ts.add_from_folder(themes);
|
||||
}
|
||||
for node in syntax_elements {
|
||||
let attrs = match node.as_node().as_element() {
|
||||
Some(e) => e.attributes.borrow(),
|
||||
None => continue,
|
||||
};
|
||||
let language = attrs.get("lang").unwrap_or("text");
|
||||
let theme_name = attrs.get("theme").unwrap_or("base16-ocean.dark");
|
||||
if let Some(text_node) = node.as_node().first_child() {
|
||||
if let Some(code_text) = text_node.as_text() {
|
||||
let syntax = ps
|
||||
.find_syntax_by_extension(language)
|
||||
.or_else(|| ps.find_syntax_by_name(language))
|
||||
.unwrap_or_else(|| ps.find_syntax_plain_text());
|
||||
let theme = &ts.themes[theme_name];
|
||||
let mut h = HighlightLines::new(syntax, theme);
|
||||
let mut html_output = String::new();
|
||||
write!(html_output, r#"<pre class="syntax-highlight" data-lang="{language}">"#)?;
|
||||
html_output.push_str("<code>");
|
||||
for line in LinesWithEndings::from(&code_text.borrow()) {
|
||||
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps)?;
|
||||
let escaped = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No)?;
|
||||
html_output.push_str(&escaped);
|
||||
}
|
||||
html_output.push_str("</code></pre>");
|
||||
let html_fragment = kuchikiki::parse_html().one(html_output);
|
||||
for child in html_fragment.children() {
|
||||
node.as_node().insert_before(child);
|
||||
}
|
||||
// Remove the original syntaxhighlight node.
|
||||
node.as_node().detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn basic_lua() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic HTML Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a paragraph.</p>
|
||||
<span><lua>
|
||||
htmlua.println("Test from Lua!")
|
||||
</lua></span>
|
||||
</body>
|
||||
</html>"#;
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = execute_lua(document).unwrap();
|
||||
let text = d.select_first("span").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "Test from Lua!\n");
|
||||
assert!(d.select_first("lua").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lua() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic HTML Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a paragraph.</p>
|
||||
<span id="ta"><lua>
|
||||
htmlua.println("Test from Lua!")
|
||||
</lua></span>
|
||||
<div>
|
||||
<span id="tb"><lua>htmlua.println("test 2")</lua></span>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#;
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = execute_lua(document).unwrap();
|
||||
let text = d.select_first("#ta").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "Test from Lua!\n");
|
||||
let text = d.select_first("#tb").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "test 2\n");
|
||||
assert!(d.select_first("lua").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_include() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic HTML Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a paragraph.</p>
|
||||
<include path="comp1.html" />
|
||||
<span><lua>
|
||||
htmlua.println("Test from Lua!")
|
||||
</lua></span>
|
||||
</body>
|
||||
</html>"#;
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("tests/components");
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = expand_template(document, &p, None).unwrap();
|
||||
let text = d.select_first("span").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "included1");
|
||||
assert!(d.select_first("include").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_include() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic HTML Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a paragraph.</p>
|
||||
<include path="multi1_1.html"></include>
|
||||
<include path="multi1_2.html"></include>
|
||||
<include path="multi1_3.html"></include>
|
||||
</body>
|
||||
</html>"#;
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("tests/components");
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = expand_template(document, &p, None).unwrap();
|
||||
let text = d.select_first("#inc1").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "included1");
|
||||
let text = d.select_first("#inc2").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "included2");
|
||||
let text = d.select_first("#inc3").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "included3");
|
||||
assert!(d.select_first("include").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_lua_include() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic HTML Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<p>This is a paragraph.</p>
|
||||
<span><lua>
|
||||
htmlua.println("Test from Lua!")
|
||||
</lua></span>
|
||||
<include path="comp1.html" />
|
||||
</body>
|
||||
</html>"#;
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("tests/components");
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = execute_lua(expand_template(document, &p, None).unwrap()).unwrap();
|
||||
let text = d.select_first("span").unwrap().as_node().text_contents();
|
||||
println!("{}", d.to_string());
|
||||
assert_eq!(text, "Test from Lua!\n");
|
||||
assert!(d.select_first("lua").is_err());
|
||||
let text = d.select_first("#inc1").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "included1");
|
||||
assert!(d.select_first("include").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_include() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic HTML Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<include path="comp2_level1.html" />
|
||||
</body>
|
||||
</html>"#;
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("tests/components");
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = expand_template(document, &p, None).unwrap();
|
||||
let text = d.select_first("span").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "included_twice");
|
||||
assert!(d.select_first("include").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_element_include() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Basic HTML Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<include path="with_include1.html">
|
||||
<exportelement class="el1"><span id="ta">element 1</span></exportelement>
|
||||
<exportelement class="el2"><span id="tb">element 2</span></exportelement>
|
||||
</include>
|
||||
</body>
|
||||
</html>"#;
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("tests/components");
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = expand_template(document, &p, None).unwrap();
|
||||
let text = d.select_first("#ta").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "element 1");
|
||||
let text = d.select_first("#tb").unwrap().as_node().text_contents();
|
||||
assert_eq!(text, "element 2");
|
||||
assert!(d.select_first("exportelement").is_err());
|
||||
assert!(d.select_first("includeelement").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_markdown() {
|
||||
let page = r"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Markdown Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<div>
|
||||
<markdown>
|
||||
# This is a heading
|
||||
This is a paragraph with **bold text** and *italic text*.
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
</markdown>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = process_markdown(document).unwrap();
|
||||
assert!(d.select_first("markdown").is_err());
|
||||
assert!(d.select_first("p").is_ok());
|
||||
assert!(d.select_first("ul").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_syntax_highlighting() {
|
||||
let page = r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syntax Highlighting Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Code Example</h1>
|
||||
<syntaxhighlight lang="rust">
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
let x = 42;
|
||||
let y = "test";
|
||||
}
|
||||
</syntaxhighlight>
|
||||
</body>
|
||||
</html>"#;
|
||||
let document = kuchikiki::parse_html().one(page);
|
||||
let d = process_syntax_highlighting(document).unwrap();
|
||||
assert!(d.select_first("syntaxhighlight").is_err());
|
||||
assert!(d.select_first("pre").is_ok());
|
||||
assert!(d.select_first("code").is_ok());
|
||||
let pre = d.select_first("pre").unwrap();
|
||||
let attrs = pre.attributes.borrow();
|
||||
assert_eq!(attrs.get("data-lang"), Some("rust"));
|
||||
assert!(attrs.get("class").unwrap().contains("syntax-highlight"));
|
||||
}
|
||||
}
|
||||
22
htmlua-parser/src/serve.rs
Normal file
22
htmlua-parser/src/serve.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use crate::helpers::read_doc_from_file;
|
||||
|
||||
use super::render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting};
|
||||
use anyhow::Result;
|
||||
use std::path::{Path, PathBuf}
|
||||
;
|
||||
|
||||
pub fn serve_content(request_uri: &str) -> Result<String> {
|
||||
// TODO: read from config
|
||||
let pages = PathBuf::from("/var/www/htmlua/pages");
|
||||
let components = PathBuf::from("/var/www/htmlua/components");
|
||||
let safe_path = Path::new(request_uri).strip_prefix("/")?;
|
||||
let page_path = pages.join(safe_path);
|
||||
|
||||
let doc = read_doc_from_file(page_path)?;
|
||||
|
||||
let full_doc = expand_template(doc, &components, None)?;
|
||||
let markdown_doc = process_markdown(full_doc)?;
|
||||
let highlighted_doc = process_syntax_highlighting(markdown_doc)?;
|
||||
let executed_doc = execute_lua(highlighted_doc)?;
|
||||
Ok(executed_doc.to_string())
|
||||
}
|
||||
1
htmlua-parser/tests/components/comp1.html
Normal file
1
htmlua-parser/tests/components/comp1.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<span id="inc1">included1</span>
|
||||
4
htmlua-parser/tests/components/comp2_level1.html
Normal file
4
htmlua-parser/tests/components/comp2_level1.html
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div>
|
||||
<p>test</p>
|
||||
<include path="comp2_level2.html"/>
|
||||
</div>
|
||||
1
htmlua-parser/tests/components/comp2_level2.html
Normal file
1
htmlua-parser/tests/components/comp2_level2.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<span>included_twice</span>
|
||||
1
htmlua-parser/tests/components/multi1_1.html
Normal file
1
htmlua-parser/tests/components/multi1_1.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<span id="inc1">included1</span>
|
||||
2
htmlua-parser/tests/components/multi1_2.html
Normal file
2
htmlua-parser/tests/components/multi1_2.html
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
<span id="inc2">included2</span>
|
||||
1
htmlua-parser/tests/components/multi1_3.html
Normal file
1
htmlua-parser/tests/components/multi1_3.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<span id="inc3">included3</span>
|
||||
7
htmlua-parser/tests/components/with_include1.html
Normal file
7
htmlua-parser/tests/components/with_include1.html
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<div>
|
||||
<includeelement name="el1"></includeelement>
|
||||
<br>
|
||||
<div>
|
||||
<includeelement name="el2"></includeelement>
|
||||
</div>
|
||||
</div>
|
||||
6
htmlua-server/Cargo.toml
Normal file
6
htmlua-server/Cargo.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "htmlua-server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
3
htmlua-server/src/main.rs
Normal file
3
htmlua-server/src/main.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
79
src/cgi.zig
79
src/cgi.zig
|
|
@ -1,79 +0,0 @@
|
|||
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
59
src/main.zig
|
|
@ -1,59 +0,0 @@
|
|||
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
76
src/root.zig
|
|
@ -1,76 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
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());
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
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;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
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