srctree

Gregory Mullen parent 96595a4d 9dd7110c
Verse is now it's own libraray

build.zig added: 206, removed: 4403, total 0
@@ -1,5 +1,7 @@
const std = @import("std");
 
const Compiler = @import("verse").Compiler;
 
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
@@ -11,7 +13,16 @@ pub fn build(b: *std.Build) void {
var bins = std.ArrayList(*std.Build.Step.Compile).init(b.allocator);
defer bins.clearAndFree();
 
const comptime_templates = try compileTemplates(b);
const verse = b.dependency("verse", .{
.target = target,
.optimize = optimize,
});
 
const comptime_templates = Compiler.buildTemplates(b, "templates") catch unreachable;
const comptime_structs = Compiler.buildStructs(b, "templates") catch unreachable;
const verse_module = verse.module("verse");
verse_module.addImport("comptime_templates", comptime_templates);
verse_module.addImport("comptime_structs", comptime_structs);
 
const exe = b.addExecutable(.{
.name = "srctree",
@@ -22,6 +33,9 @@ pub fn build(b: *std.Build) void {
b.installArtifact(exe);
bins.append(exe) catch unreachable;
 
exe.root_module.addImport("verse", verse_module);
//exe.linkLibrary(verse.artifact("verse"));
 
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
@@ -35,10 +49,12 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
unit_tests.root_module.addImport("verse", verse_module);
bins.append(unit_tests) catch unreachable;
 
for (bins.items) |ex| {
ex.root_module.addImport("comptime_templates", comptime_templates);
ex.root_module.addImport("comptime_structs", comptime_templates);
ex.root_module.addOptions("config", options);
if (enable_libcurl) {
ex.linkSystemLibrary2("curl", .{ .preferred_link_mode = .static });
@@ -51,22 +67,10 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
 
const t_compiler = b.addExecutable(.{
.name = "template-compiler",
.root_source_file = b.path("src/template-compiler.zig"),
.target = target,
});
 
t_compiler.root_module.addImport("comptime_templates", comptime_templates);
const tc_build_run = b.addRunArtifact(t_compiler);
const tc_structs = tc_build_run.addOutputFileArg("compiled-structs.zig");
const tc_build_step = b.step("templates", "Compile templates down into struct");
tc_build_step.dependOn(&tc_build_run.step);
 
for (bins.items) |bin| bin.root_module.addImport("comptime_template_structs", b.addModule(
"comptime_template_structs",
.{ .root_source_file = tc_structs },
));
//for (bins.items) |bin| bin.root_module.addImport("comptime_template_structs", b.addModule(
// "comptime_template_structs",
// .{ .root_source_file = tc_structs },
//));
 
// Partner Binaries
const mailer = b.addExecutable(.{
@@ -85,43 +89,3 @@ pub fn build(b: *std.Build) void {
send_email.addArgs(args);
}
}
 
fn compileTemplates(b: *std.Build) !*std.Build.Module {
const compiled = b.addModule("comptime_templates", .{
.root_source_file = b.path("src/template/comptime.zig"),
});
 
const list = buildSrcTemplates(b) catch @panic("unable to build src files");
const found = b.addOptions();
found.addOption([]const []const u8, "names", list);
compiled.addOptions("config", found);
 
for (list) |file| {
_ = compiled.addAnonymousImport(file, .{
.root_source_file = b.path(file),
});
}
 
return compiled;
}
 
var template_list: ?[][]const u8 = null;
 
fn buildSrcTemplates(b: *std.Build) ![][]const u8 {
if (template_list) |tl| return tl;
 
const tmplsrcdir = "templates";
var cwd = std.fs.cwd();
var idir = cwd.openDir(tmplsrcdir, .{ .iterate = true }) catch |err| {
std.debug.print("template build error {}", .{err});
return err;
};
var arrlist = std.ArrayList([]const u8).init(b.allocator);
var itr = idir.iterate();
while (try itr.next()) |file| {
if (!std.mem.endsWith(u8, file.name, ".html")) continue;
try arrlist.append(b.pathJoin(&[2][]const u8{ tmplsrcdir, file.name }));
}
template_list = try arrlist.toOwnedSlice();
return template_list.?;
}
 
filename was Deleted added: 206, removed: 4403, total 0
@@ -0,0 +1,17 @@
.{
.name = "srctree",
.version = "0.0.0",
.minimum_zig_version = "0.13.0",
.dependencies = .{
.verse = .{
//.url = "git+https://srctree.gr.ht/repo/verse",
.path = "../verse",
},
},
 
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}
 
src/api.zig added: 206, removed: 4403, total 0
@@ -1,14 +1,14 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
 
pub const Routes = @import("routes.zig");
pub const Verse = @import("verse.zig");
pub const Verse = @import("verse");
pub const Router = Verse.Router;
 
const ROUTE = Routes.ROUTE;
const ROUTE = Router.ROUTE;
 
const Repo = @import("api/repo.zig");
 
const endpoints = [_]Routes.Match{
const endpoints = [_]Router.Match{
ROUTE("v0", router),
ROUTE("v1", router),
 
@@ -34,20 +34,20 @@ const APIRouteData = struct {
}
};
 
pub fn router(ctx: *Verse) Routes.Error!Routes.Callable {
pub fn router(ctx: *Verse) Router.Error!Router.Callable {
const uri_api = ctx.uri.next() orelse return heartbeat;
if (!std.mem.eql(u8, uri_api, "api")) return heartbeat;
const rd = try APIRouteData.init(ctx.alloc);
ctx.route_ctx = rd;
 
return Routes.router(ctx, &endpoints);
return Router.router(ctx, &endpoints);
}
 
const Diff = struct {
sha: []const u8,
};
 
fn diff(ctx: *Verse) Routes.Error!void {
fn diff(ctx: *Verse) Router.Error!void {
return try ctx.sendJSON([0]Diff{});
}
 
@@ -55,7 +55,7 @@ const HeartBeat = struct {
nice: usize = 0,
};
 
fn heartbeat(ctx: *Verse) Routes.Error!void {
fn heartbeat(ctx: *Verse) Router.Error!void {
return try ctx.sendJSON(HeartBeat{ .nice = 69 });
}
 
@@ -63,7 +63,7 @@ const Issue = struct {
index: usize,
};
 
fn issue(ctx: *Verse) Routes.Error!void {
fn issue(ctx: *Verse) Router.Error!void {
return try ctx.sendJSON([0]Issue{});
}
 
@@ -82,7 +82,7 @@ const Network = struct {
networks: []RemotePeer,
};
 
fn network(ctx: *Verse) Routes.Error!void {
fn network(ctx: *Verse) Router.Error!void {
return try ctx.sendJSON(Network{ .networks = [0].{} });
}
 
@@ -90,7 +90,7 @@ const Patch = struct {
patch: []const u8,
};
 
fn patch(ctx: *Verse) Routes.Error!void {
fn patch(ctx: *Verse) Router.Error!void {
return try ctx.sendJSON(Patch{ .patch = [0].{} });
}
 
@@ -102,7 +102,7 @@ const Flex = struct {
};
};
 
fn flex(ctx: *Verse) Routes.Error!void {
fn flex(ctx: *Verse) Router.Error!void {
return try ctx.sendJSON([0]Flex{});
}
 
@@ -111,6 +111,6 @@ const User = struct {
email: []const u8,
};
 
fn user(ctx: *Verse) Routes.Error!void {
fn user(ctx: *Verse) Router.Error!void {
return try ctx.sendJSON([0]User{});
}
 
src/api/repo.zig added: 206, removed: 4403, total 0
@@ -4,21 +4,21 @@ const Allocator = std.mem.Allocator;
const API = @import("../api.zig");
const Bleach = @import("../bleach.zig");
const Git = @import("../git.zig");
const Routes = @import("../routes.zig");
const Router = API.Router;
 
const ROUTE = Routes.ROUTE;
const ROUTE = Router.ROUTE;
 
const endpoints = [_]Routes.Match{
const endpoints = [_]Router.Match{
ROUTE("", repo),
ROUTE("branches", repoBranches),
ROUTE("tags", repoTags),
};
 
pub fn router(ctx: *API.Verse) Routes.Error!Routes.Callable {
pub fn router(ctx: *API.Verse) Router.Error!Router.Callable {
const uri_api = ctx.uri.next() orelse return repo;
if (!std.mem.eql(u8, uri_api, "repo")) return repo;
 
return Routes.router(ctx, &endpoints);
return Router.router(ctx, &endpoints);
}
 
pub const Repo = struct {
@@ -45,7 +45,7 @@ fn openRepo(a: Allocator, raw_name: []const u8) !Git.Repo {
return gitrepo;
}
 
pub fn repo(ctx: *API.Verse) API.Routes.Error!void {
pub fn repo(ctx: *API.Verse) API.Router.Error!void {
const req = try ctx.reqdata.validate(RepoRequest);
 
var gitrepo = openRepo(ctx.alloc, req.name) catch |err| switch (err) {
@@ -84,7 +84,7 @@ pub const RepoBranches = struct {
branches: []const Branch,
};
 
pub fn repoBranches(ctx: *API.Verse) API.Routes.Error!void {
pub fn repoBranches(ctx: *API.Verse) API.Router.Error!void {
const req = try ctx.reqdata.validate(RepoRequest);
 
var gitrepo = openRepo(ctx.alloc, req.name) catch |err| switch (err) {
@@ -121,7 +121,7 @@ pub const RepoTags = struct {
tags: []const []const u8,
};
 
pub fn repoTags(ctx: *API.Verse) API.Routes.Error!void {
pub fn repoTags(ctx: *API.Verse) API.Router.Error!void {
const req = try ctx.reqdata.validate(RepoRequest);
 
var gitrepo = openRepo(ctx.alloc, req.name) catch |err| switch (err) {
 
ev/null added: 206, removed: 4403, total 0
@@ -1,89 +0,0 @@
const std = @import("std");
 
const Allocator = std.mem.Allocator;
 
const HTML = @import("html.zig");
 
const DOM = @This();
 
alloc: Allocator,
elems: std.ArrayList(HTML.E),
parent: ?*DOM = null,
child: ?*DOM = null,
next: ?HTML.E = null,
 
pub fn new(a: Allocator) *DOM {
const self = a.create(DOM) catch unreachable;
self.* = DOM{
.alloc = a,
.elems = std.ArrayList(HTML.E).init(a),
};
return self;
}
 
pub fn open(self: *DOM, elem: HTML.E) *DOM {
if (self.child) |_| @panic("DOM Already Open");
self.child = new(self.alloc);
self.child.?.parent = self;
self.child.?.next = elem;
return self.child.?;
}
 
pub fn pushSlice(self: *DOM, elems: []HTML.E) void {
for (elems) |elem| self.push(elem);
}
 
pub fn push(self: *DOM, elem: HTML.E) void {
self.elems.append(elem) catch unreachable;
}
 
pub fn dupe(self: *DOM, elem: HTML.E) void {
self.elems.append(HTML.E{
.name = elem.name,
.text = elem.text,
.children = if (elem.children) |c| self.alloc.dupe(HTML.E, c) catch null else null,
.attrs = if (elem.attrs) |a| self.alloc.dupe(HTML.Attribute, a) catch null else null,
}) catch unreachable;
}
 
pub fn close(self: *DOM) *DOM {
if (self.parent) |p| {
self.next.?.children = self.elems.toOwnedSlice() catch unreachable;
if (self.next.?.attrs) |attr| {
self.next.?.attrs = self.alloc.dupe(HTML.Attribute, attr) catch unreachable;
}
p.push(self.next.?);
p.child = null;
defer self.alloc.destroy(self);
return p;
} else @panic("DOM ISN'T OPEN");
unreachable;
}
 
pub fn done(self: *DOM) []HTML.E {
if (self.child) |_| @panic("INVALID STATE DOM STILL HAS OPEN CHILDREN");
defer self.alloc.destroy(self);
return self.elems.toOwnedSlice() catch unreachable;
}
 
test "basic" {
const a = std.testing.allocator;
var dom = new(a);
try std.testing.expect(dom.child == null);
_ = dom.done();
}
 
test "open close" {
var a = std.testing.allocator;
var dom = new(a);
try std.testing.expect(dom.child == null);
 
var new_dom = dom.open(HTML.div(null, null));
try std.testing.expect(new_dom.child == null);
try std.testing.expect(dom.child == new_dom);
const closed = new_dom.close();
try std.testing.expect(dom == closed);
try std.testing.expect(dom.child == null);
 
a.free(dom.done());
}
 
src/endpoints/admin.zig added: 206, removed: 4403, total 0
@@ -2,11 +2,11 @@ const std = @import("std");
 
const Allocator = std.mem.Allocator;
 
const Verse = @import("../verse.zig");
const Route = @import("../routes.zig");
const HTML = @import("../html.zig");
const DOM = @import("../dom.zig");
const Template = @import("../template.zig");
const Verse = @import("verse");
const Route = Verse.Router;
const HTML = Verse.HTML;
const DOM = Verse.DOM;
const Template = Verse.Template;
 
const Error = Route.Error;
const UriIter = Route.UriIter;
@@ -40,7 +40,7 @@ const AdminPage = Template.PageData("admin.html");
const btns = [1]Template.Structs.NavButtons{.{ .name = "inbox", .extra = 0, .url = "/inbox" }};
 
fn default(ctx: *Verse) Error!void {
try ctx.request.auth.validOrError();
try ctx.auth.validOrError();
var dom = DOM.new(ctx.alloc);
const action = "/admin/post";
dom = dom.open(HTML.form(null, &[_]HTML.Attr{
@@ -70,7 +70,7 @@ fn default(ctx: *Verse) Error!void {
}
 
fn cloneUpstream(ctx: *Verse) Error!void {
try ctx.request.auth.validOrError();
try ctx.auth.validOrError();
var dom = DOM.new(ctx.alloc);
const action = "/admin/clone-upstream";
dom = dom.open(HTML.form(null, &[_]HTML.Attr{
@@ -105,7 +105,7 @@ const CloneUpstreamReq = struct {
};
 
fn postCloneUpstream(ctx: *Verse) Error!void {
try ctx.request.auth.validOrError();
try ctx.auth.validOrError();
 
const udata = ctx.reqdata.post.?.validate(CloneUpstreamReq) catch return error.BadData;
std.debug.print("repo uri {s}\n", .{udata.repo_uri});
@@ -152,7 +152,7 @@ fn postCloneUpstream(ctx: *Verse) Error!void {
}
 
fn postNewRepo(ctx: *Verse) Error!void {
try ctx.request.auth.validOrError();
try ctx.auth.validOrError();
// TODO ini repo dir
var valid = if (ctx.reqdata.post) |p|
p.validator()
@@ -206,7 +206,7 @@ fn postNewRepo(ctx: *Verse) Error!void {
}
 
fn newRepo(ctx: *Verse) Error!void {
try ctx.request.auth.validOrError();
try ctx.auth.validOrError();
var dom = DOM.new(ctx.alloc);
const action = "/admin/new-repo";
dom = dom.open(HTML.form(null, &[_]HTML.Attr{
@@ -234,7 +234,7 @@ fn newRepo(ctx: *Verse) Error!void {
}
 
fn view(ctx: *Verse) Error!void {
try ctx.request.auth.validOrError();
try ctx.auth.validOrError();
if (ctx.reqdata.post) |pd| {
std.debug.print("{any}\n", .{pd.items});
return newRepo(ctx);
 
src/endpoints/commit-flex.zig added: 206, removed: 4403, total 0
@@ -6,13 +6,13 @@ const Bleach = @import("../bleach.zig");
const DateTime = @import("../datetime.zig");
const Git = @import("../git.zig");
 
const DOM = @import("../dom.zig");
const HTML = @import("../html.zig");
const Verse = @import("../verse.zig");
const Template = @import("../template.zig");
const Verse = @import("verse");
const DOM = Verse.DOM;
const HTML = Verse.HTML;
const Template = Verse.Template;
const S = Template.Structs;
 
const Route = @import("../routes.zig");
const Route = Verse.Router;
const Error = Route.Error;
 
const Scribe = struct {
 
src/endpoints/gist.zig added: 206, removed: 4403, total 0
@@ -1,23 +1,23 @@
const std = @import("std");
const allocPrint = std.fmt.allocPrint;
 
const Verse = @import("../verse.zig");
const Template = @import("../template.zig");
const RequestData = @import("../request_data.zig").RequestData;
const Verse = @import("verse");
const Template = Verse.Template;
const RequestData = Verse.RequestData.RequestData;
const Bleach = @import("../bleach.zig");
const Allocator = std.mem.Allocator;
 
const Gist = @import("../types.zig").Gist;
 
const Route = @import("../routes.zig");
const Error = Route.Error;
const POST = Route.POST;
const GET = Route.GET;
const Routes = Verse.Router;
const Error = Routes.Error;
const POST = Routes.POST;
const GET = Routes.GET;
 
const GistPage = Template.PageData("gist.html");
const GistNewPage = Template.PageData("gist_new.html");
 
const endpoints = [_]Route.Match{
const endpoints = [_]Routes.Match{
GET("", new),
GET("gist", view),
GET("new", new),
@@ -25,7 +25,7 @@ const endpoints = [_]Route.Match{
POST("post", post),
};
 
pub fn router(ctx: *Verse) Error!Route.Callable {
pub fn router(ctx: *Verse) Error!Routes.Callable {
if (!std.mem.eql(u8, ctx.uri.next() orelse "", "gist")) return error.Unrouteable;
 
if (ctx.uri.peek()) |peek| {
@@ -41,7 +41,7 @@ pub fn router(ctx: *Verse) Error!Route.Callable {
}
} else return new;
 
return Route.router(ctx, &endpoints);
return Routes.router(ctx, &endpoints);
}
 
const GistPost = struct {
@@ -51,7 +51,7 @@ const GistPost = struct {
};
 
fn post(ctx: *Verse) Error!void {
try ctx.request.auth.validOrError();
try ctx.auth.validOrError();
 
const udata = RequestData(GistPost).initMap(ctx.alloc, ctx.reqdata) catch return error.BadData;
 
 
src/endpoints/network.zig added: 206, removed: 4403, total 0
@@ -1,14 +1,14 @@
const std = @import("std");
const allocPrint = std.fmt.allocPrint;
 
const DOM = @import("../dom.zig");
const Verse = @import("../verse.zig");
const Template = @import("../template.zig");
const Verse = @import("verse");
const Template = Verse.Template;
const DOM = Verse.DOM;
const HTML = Verse.HTML;
 
const Route = @import("../routes.zig");
const Route = Verse.Router;
const Error = Route.Error;
const UriIter = Route.UriIter;
const HTML = @import("../html.zig");
const Repos = @import("../repos.zig");
const Ini = @import("../ini.zig");
const Git = @import("../git.zig");
 
src/endpoints/repos.zig added: 206, removed: 4403, total 0
@@ -6,21 +6,21 @@ const eql = std.mem.eql;
const startsWith = std.mem.startsWith;
const splitScalar = std.mem.splitScalar;
 
const Verse = @import("../verse.zig");
const Response = @import("../response.zig");
const Request = @import("../request.zig");
const HTML = @import("../html.zig");
const elm = HTML.element;
const DOM = @import("../dom.zig");
const Template = @import("../template.zig");
const Verse = @import("verse");
const Response = Verse.Response;
const Request = Verse.Request;
const HTML = Verse.HTML;
const DOM = Verse.DOM;
const Template = Verse.Template;
const Route = Verse.Router;
const S = Template.Structs;
const Route = @import("../routes.zig");
const elm = HTML.element;
const Error = Route.Error;
const UriIter = Route.UriIter;
const ROUTE = Route.ROUTE;
const POST = Route.POST;
const GET = Route.GET;
const RequestData = @import("../request_data.zig").RequestData;
const RequestData = Verse.RequestData.RequestData;
 
const Bleach = @import("../bleach.zig");
const Humanize = @import("../humanize.zig");
@@ -295,7 +295,7 @@ fn list(ctx: *Verse) Error!void {
}, repoSorterNew);
 
var repo_buttons: []const u8 = "";
if (ctx.request.auth.valid()) {
if (ctx.auth.valid()) {
repo_buttons =
\\<div class="act-btns"><a class="btn" href="/admin/clone-upstream">New Upstream</a></div>
;
 
src/endpoints/repos/commits.zig added: 206, removed: 4403, total 0
@@ -5,22 +5,23 @@ const bufPrint = std.fmt.bufPrint;
const endsWith = std.mem.endsWith;
const eql = std.mem.eql;
 
const Verse = @import("verse");
const DOM = Verse.DOM;
const HTML = Verse.HTML;
const Response = Verse.Response;
const Route = Verse.Router;
const Template = Verse.Template;
const RequestData = Verse.RequestData;
 
const Diffs = @import("diffs.zig");
 
const Repos = @import("../repos.zig");
 
const Bleach = @import("../../bleach.zig");
const Verse = @import("../../verse.zig");
const DOM = @import("../../dom.zig");
const Git = @import("../../git.zig");
const HTML = @import("../../html.zig");
const Bleach = @import("../../bleach.zig");
const Humanize = @import("../../humanize.zig");
const Patch = @import("../../patch.zig");
const Response = @import("../../response.zig");
const Route = @import("../../routes.zig");
const Template = @import("../../template.zig");
const Types = @import("../../types.zig");
const RequestData = @import("../../request_data.zig").RequestData;
 
const S = Template.Structs;
const CommitMap = Types.CommitMap;
 
src/endpoints/repos/diffs.zig added: 206, removed: 4403, total 0
@@ -16,15 +16,16 @@ const Commits = @import("commits.zig");
 
const Repos = @import("../repos.zig");
 
const Verse = @import("verse");
 
const Git = @import("../../git.zig");
const Bleach = @import("../../bleach.zig");
const Verse = @import("../../verse.zig");
const DOM = @import("../../dom.zig");
const HTML = @import("../../html.zig");
const DOM = Verse.DOM;
const HTML = Verse.HTML;
const Humanize = @import("../../humanize.zig");
const Patch = @import("../../patch.zig");
const Route = @import("../../routes.zig");
const Template = @import("../../template.zig");
const Route = Verse.Router;
const Template = Verse.Template;
const Types = @import("../../types.zig");
const Highlighting = @import("../../syntax-highlight.zig");
 
 
src/endpoints/repos/issues.zig added: 206, removed: 4403, total 0
@@ -4,11 +4,11 @@ const allocPrint = std.fmt.allocPrint;
const bufPrint = std.fmt.bufPrint;
const fmtSliceHexLower = std.fmt.fmtSliceHexLower;
 
const Route = @import("../../routes.zig");
const DOM = @import("../../dom.zig");
const HTML = @import("../../html.zig");
const Verse = @import("../../verse.zig");
const Template = @import("../../template.zig");
const Verse = @import("verse");
const Route = Verse.Router;
const DOM = Verse.DOM;
const HTML = Verse.HTML;
const Template = Verse.Template;
const Error = Route.Error;
const UriIter = Route.Error;
const ROUTE = Route.ROUTE;
 
src/endpoints/search.zig added: 206, removed: 4403, total 0
@@ -3,24 +3,24 @@ const Allocator = std.mem.Allocator;
const splitScalar = std.mem.splitScalar;
const allocPrint = std.fmt.allocPrint;
 
const Verse = @import("../verse.zig");
const Verse = @import("verse");
const Delta = @import("../types.zig").Delta;
const Template = @import("../template.zig");
const Route = @import("../routes.zig");
const Error = Route.Error;
const ROUTE = Route.ROUTE;
const Template = Verse.Template;
const Routes = Verse.Router;
const Error = Routes.Error;
const ROUTE = Routes.ROUTE;
const S = Template.Structs;
 
const Bleach = @import("../bleach.zig");
 
pub const routes = [_]Route.Match{
pub const routes = [_]Routes.Match{
ROUTE("", search),
ROUTE("search", search),
ROUTE("inbox", inbox),
};
 
pub fn router(ctx: *Verse) Error!Route.Callable {
return Route.router(ctx, &routes);
pub fn router(ctx: *Verse) Error!Routes.Callable {
return Routes.router(ctx, &routes);
}
 
const SearchReq = struct {
 
src/endpoints/settings.zig added: 206, removed: 4403, total 0
@@ -1,18 +1,18 @@
const std = @import("std");
pub const Template = @import("../template.zig");
const Verse = @import("../verse.zig");
const Route = @import("../routes.zig");
const RequestData = @import("../request_data.zig").RequestData;
const Verse = @import("verse");
const Template = Verse.Template;
const Router = Verse.Router;
const RequestData = Verse.RequestData.RequestData;
 
pub const endpoints = [_]Route.Match{
Route.GET("", default),
Route.POST("post", post),
pub const endpoints = [_]Router.Match{
Router.GET("", default),
Router.POST("post", post),
};
 
const SettingsPage = Template.PageData("settings.html");
 
fn default(ctx: *Verse) Route.Error!void {
try ctx.request.auth.validOrError();
fn default(ctx: *Verse) Router.Error!void {
try ctx.auth.validOrError();
 
var blocks = try ctx.alloc.alloc(Template.Structs.ConfigBlocks, ctx.cfg.?.ns.len);
for (ctx.cfg.?.ns, 0..) |ns, i| {
@@ -42,8 +42,8 @@ const SettingsReq = struct {
block_text: [][]const u8,
};
 
fn post(ctx: *Verse) Route.Error!void {
try ctx.request.auth.validOrError();
fn post(ctx: *Verse) Router.Error!void {
try ctx.auth.validOrError();
 
const udata = RequestData(SettingsReq).initMap(ctx.alloc, ctx.reqdata) catch return error.BadData;
 
 
src/endpoints/users.zig added: 206, removed: 4403, total 0
@@ -2,7 +2,7 @@ const std = @import("std");
 
const DOM = @import("../dom.zig");
const HTML = @import("../html.zig");
const Verse = @import("../verse.zig");
const Verse = @import("../verse");
const Template = @import("../template.zig");
const Route = @import("../routes.zig");
const UriIter = Route.UriIter;
 
src/gitweb.zig added: 206, removed: 4403, total 0
@@ -2,15 +2,15 @@ const std = @import("std");
 
const Allocator = std.mem.Allocator;
 
const Verse = @import("verse.zig");
const Response = @import("response.zig");
const Request = @import("request.zig");
const HTML = @import("html.zig");
const Verse = @import("verse");
const Response = Verse.Response;
const Request = Verse.Request;
const HTML = Verse.HTML;
const elm = HTML.element;
const DOM = @import("dom.zig");
const Template = @import("template.zig");
const DOM = Verse.DOM;
const Template = Verse.Template;
 
const Route = @import("routes.zig");
const Route = Verse.Router;
const Error = Route.Error;
const UriIter = Route.UriIter;
 
 
ev/null added: 206, removed: 4403, total 0
@@ -1,58 +0,0 @@
const std = @import("std");
 
const Allocator = std.mem.Allocator;
 
pub const HIndex = std.StringHashMap(Value);
 
const Value = struct {
str: []const u8,
next: ?*Value = null,
};
 
pub const Headers = @This();
 
alloc: Allocator,
index: HIndex,
 
pub fn init(a: Allocator) Headers {
return .{
.alloc = a,
.index = HIndex.init(a),
};
}
 
pub fn raze(h: *Headers) void {
h.index.deinit(h.alloc);
h.* = undefined;
}
 
/// TODO actually normalize to thing
/// TODO are we gonna normilize comptime?
fn normilize(name: []const u8) !void {
if (name.len == 0) return;
}
 
pub fn add(h: *Headers, comptime name: []const u8, value: []const u8) !void {
try normilize(name);
const res = try h.index.getOrPut(name);
if (res.found_existing) {
res.value_ptr.* = Value{
.str = value,
.next = res.value_ptr,
};
} else {
res.value_ptr.* = Value{
.str = value,
};
}
}
 
pub fn format(h: Headers, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
_ = h;
_ = out;
unreachable;
}
 
pub fn clearAndFree(h: *Headers) void {
h.index.clearAndFree(h.alloc);
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,404 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
 
pub usingnamespace @import("html/extra.zig");
 
pub const Attribute = struct {
key: []const u8,
value: ?[]const u8,
 
/// Helper function
pub fn class(val: ?[]const u8) [1]Attribute {
return [_]Attr{
.{ .key = "class", .value = val },
};
}
 
pub fn alloc(a: Allocator, keys: []const []const u8, vals: []const ?[]const u8) ![]Attribute {
const all = try a.alloc(Attribute, @max(keys.len, vals.len));
for (all, keys, vals) |*dst, k, v| {
dst.* = Attribute{
.key = try a.dupe(u8, k),
.value = if (v) |va| try a.dupe(u8, va) else null,
};
}
return all;
}
 
pub fn create(a: Allocator, k: []const u8, v: ?[]const u8) ![]Attribute {
return alloc(a, &[_][]const u8{k}, &[_]?[]const u8{v});
}
 
pub fn format(self: Attribute, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
if (self.value) |value| {
try std.fmt.format(out, " {s}=\"{s}\"", .{ self.key, value });
} else {
try std.fmt.format(out, " {s}", .{self.key});
}
}
};
 
pub const Attr = Attribute;
 
pub const Element = struct {
name: []const u8,
text: ?[]const u8 = null,
attrs: ?[]const Attribute = null,
children: ?[]const Element = null,
self_close: bool = false,
 
pub fn format(self: Element, comptime fmt: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
const pretty = std.mem.eql(u8, fmt, "pretty");
 
if (self.name[0] == '_') {
if (self.text) |txt| {
return try std.fmt.format(out, "{s}", .{txt});
}
}
 
try out.print("<{s}", .{self.name});
if (self.attrs) |attrs| {
for (attrs) |attr| try out.print("{}", .{attr});
}
if (self.self_close) {
return try out.print(" />", .{});
} else try out.print(">", .{});
 
if (self.children) |children| {
for (children) |child| {
if (pretty) {
try out.print("\n{pretty}", .{child});
} else {
try out.print("{}", .{child});
}
} else if (pretty) try out.writeAll("\n");
} else if (self.text) |txt| {
try out.print("{s}", .{txt});
} else if (self.self_close) {
return try out.print(" />", .{});
}
try out.print("</{s}>", .{self.name});
}
};
 
pub const E = Element;
 
/// TODO this desperately needs to return a type instead
pub fn element(comptime name: []const u8, children: anytype, attrs: ?[]const Attribute) Element {
const ChildrenType = @TypeOf(children);
if (ChildrenType == @TypeOf(null)) return .{ .name = name, .attrs = attrs };
const child_type_info = @typeInfo(ChildrenType);
switch (child_type_info) {
.Pointer => |ptr| switch (ptr.size) {
.One => switch (@typeInfo(ptr.child)) {
.Array => |arr| switch (arr.child) {
u8 => return .{
.name = name,
.text = children,
.attrs = attrs,
},
Element => {
return .{
.name = name,
.children = children,
.attrs = attrs,
};
},
else => @compileError("Unknown type given to element"),
},
.Pointer => @compileError("Pointer to a pointer, (perhaps &[]u8) did you mistakenly add a &?"),
else => {
@compileLog(ptr);
@compileLog(ptr.child);
@compileLog(@typeInfo(ptr.child));
@compileLog(ChildrenType);
},
},
.Slice => switch (ptr.child) {
u8 => return .{
.name = name,
.text = children,
.attrs = attrs,
},
Element => return .{
.name = name,
.children = children,
.attrs = attrs,
},
else => {
@compileLog(ptr);
@compileLog(ptr.child);
@compileLog(ptr.size);
@compileLog(ChildrenType);
@compileError("Invalid pointer children given");
},
},
else => {
@compileLog(ptr);
@compileLog(ptr.size);
@compileLog(ChildrenType);
},
},
.Struct => @compileError("Raw structs aren't allowed, element must be a slice"),
.Array => |arr| switch (arr.child) {
// TODO, this is probably a compiler error, prefix with &
Element => return .{
.name = name,
.children = children.ptr,
.attrs = attrs,
},
else => {
@compileLog(ChildrenType);
@compileLog(@typeInfo(ChildrenType));
@compileError("children must be either Element, or []Element or .{}");
},
},
else => {
@compileLog(ChildrenType);
@compileLog(@typeInfo(ChildrenType));
@compileError("children must be either Element, or []Element or .{}");
},
}
@compileLog(ChildrenType);
@compileLog(@typeInfo(ChildrenType));
@compileError("Invalid type given for children when calling element");
}
 
pub fn elementClosed(comptime name: []const u8, attrs: ?[]const Attribute) Element {
var e = element(name, null, attrs);
e.self_close = true;
return e;
}
 
pub fn text(c: []const u8) Element {
return element("_text", c, null);
}
 
pub fn html(c: anytype) Element {
return element("html", c, null);
}
 
pub fn head(c: anytype) Element {
return element("head", c, null);
}
 
pub fn body(c: anytype) Element {
return element("body", c, null);
}
 
pub fn div(c: anytype, a: ?[]const Attribute) Element {
return element("div", c, a);
}
 
pub fn h1(c: anytype, a: ?[]const Attribute) Element {
return element("h1", c, a);
}
 
pub fn h2(c: anytype, a: ?[]const Attribute) Element {
return element("h2", c, a);
}
 
pub fn h3(c: anytype, a: ?[]const Attribute) Element {
return element("h3", c, a);
}
 
pub fn p(c: anytype, a: ?[]const Attribute) Element {
return element("p", c, a);
}
 
pub fn br() Element {
return elementClosed("br", null);
}
 
pub fn span(c: anytype, a: ?[]const Attribute) Element {
return element("span", c, a);
}
 
pub fn strong(c: anytype) Element {
return element("strong", c, null);
}
 
pub fn anch(c: anytype, attr: ?[]const Attribute) Element {
return element("a", c, attr);
}
 
pub fn aHrefAlloc(a: Allocator, txt: []const u8, href: []const u8) !Element {
var attr = try a.alloc(Attribute, 1);
attr[0] = Attribute{
.key = "href",
.value = href,
};
return anch(txt, attr);
}
 
pub fn form(c: anytype, attr: ?[]const Attribute) Element {
return element("form", c, attr);
}
 
/// Written to work with DOM
pub fn formAlloc(a: Allocator, action: []const u8, o: struct { method: ?[]const u8 = null }) !Element {
var attr = try a.alloc(Attribute, if (o.method == null) 1 else 2);
attr[0] = Attribute{
.key = "action",
.value = action,
};
if (o.method != null)
attr[1] = Attribute{
.key = "method",
.value = o.method,
};
return element("form", null, attr);
}
 
pub fn textarea(c: anytype, attr: ?[]const Attribute) Element {
return element("textarea", c, attr);
}
 
pub fn textareaAlloc(
a: Allocator,
name: []const u8,
o: struct { placeholder: ?[]const u8 = null },
) !Element {
var attr = try a.alloc(Attribute, if (o.placeholder == null) 1 else 2);
attr[0] = Attribute{
.key = "name",
.value = name,
};
if (o.placeholder != null)
attr[1] = Attribute{
.key = "placeholder",
.value = o.placeholder,
};
 
return element("textarea", null, attr);
}
 
pub fn input(attr: ?[]const Attribute) Element {
return elementClosed("input", attr);
}
 
pub fn inputAlloc(
a: Allocator,
name: []const u8,
o: struct { placeholder: ?[]const u8 = null },
) !Element {
var attr = try a.alloc(Attribute, if (o.placeholder == null) 1 else 2);
attr[0] = Attribute{
.key = "name",
.value = name,
};
if (o.placeholder != null)
attr[1] = Attribute{
.key = "placeholder",
.value = o.placeholder,
};
 
return elementClosed("input", attr);
}
 
pub fn btn(c: anytype, attr: ?[]const Attribute) Element {
return element("button", c, attr);
}
 
pub fn btnDupe(txt: []const u8, name: []const u8) Element {
return element("button", txt, &[_]Attr{.{ .key = "name", .value = name }});
}
 
pub fn linkBtnAlloc(a: Allocator, txt: []const u8, href: []const u8) !Element {
const attr = [2]Attr{
Attr.class("btn")[0],
Attr{
.key = "href",
.value = href,
},
};
return element(
"a",
try a.dupe(u8, txt),
try a.dupe(Attr, &attr),
);
}
 
pub fn li(c: anytype, attr: ?[]const Attribute) Element {
return element("li", c, attr);
}
 
test "html" {
var a = std.testing.allocator;
 
const str = try std.fmt.allocPrint(a, "{}", .{html(null)});
defer a.free(str);
try std.testing.expectEqualStrings("<html></html>", str);
 
const str2 = try std.fmt.allocPrint(a, "{pretty}", .{html(&[_]E{body(null)})});
defer a.free(str2);
try std.testing.expectEqualStrings("<html>\n<body></body>\n</html>", str2);
}
 
test "nested" {
var a = std.testing.allocator;
const str = try std.fmt.allocPrint(a, "{pretty}", .{
html(&[_]E{
head(null),
body(
&[_]E{div(&[_]E{p(null, null)}, null)},
),
}),
});
defer a.free(str);
 
const example =
\\<html>
\\<head></head>
\\<body>
\\<div>
\\<p></p>
\\</div>
\\</body>
\\</html>
;
try std.testing.expectEqualStrings(example, str);
}
 
test "text" {
var a = std.testing.allocator;
 
const str = try std.fmt.allocPrint(a, "{}", .{text("this is text")});
defer a.free(str);
try std.testing.expectEqualStrings("this is text", str);
 
const pt = try std.fmt.allocPrint(a, "{}", .{p("this is text", null)});
defer a.free(pt);
try std.testing.expectEqualStrings("<p>this is text</p>", pt);
 
const p_txt = try std.fmt.allocPrint(a, "{}", .{p(&[_]E{text("this is text")}, null)});
defer a.free(p_txt);
try std.testing.expectEqualStrings("<p>this is text</p>", p_txt);
}
 
test "attrs" {
var a = std.testing.allocator;
const str = try std.fmt.allocPrint(a, "{pretty}", .{
html(&[_]E{
head(null),
body(
&[_]E{div(&[_]E{p(null, null)}, &[_]Attribute{
Attribute{ .key = "class", .value = "something" },
})},
),
}),
});
defer a.free(str);
 
const example =
\\<html>
\\<head></head>
\\<body>
\\<div class="something">
\\<p></p>
\\</div>
\\</body>
\\</html>
;
try std.testing.expectEqualStrings(example, str);
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,20 +0,0 @@
const E = @import("../html.zig").E;
const Attr = @import("../html.zig").Attr;
 
const element = @import("../html.zig").element;
 
pub fn repo() E {
return element("repo", null, null);
}
 
pub fn diff() E {
return element("diff", null, null);
}
 
pub fn patch() E {
return element("patch", null, null);
}
 
pub fn commit(c: anytype, attr: ?[]const Attr) E {
return element("commit", c, attr);
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,59 +0,0 @@
const std = @import("std");
 
const Allocator = std.mem.Allocator;
const Server = std.http.Server;
 
const Verse = @import("verse.zig");
const Request = @import("request.zig");
const Response = @import("response.zig");
const Router = @import("routes.zig");
const RequestData = @import("request_data.zig");
 
const MAX_HEADER_SIZE = 1 <<| 13;
 
pub fn serve(a: Allocator, srv: *Server) !void {
connection: while (true) {
var arena = std.heap.ArenaAllocator.init(a);
defer arena.deinit();
const alloc = arena.allocator();
 
var http_resp = try srv.accept(.{
.allocator = alloc,
.header_strategy = .{ .dynamic = MAX_HEADER_SIZE },
});
defer http_resp.deinit();
//const request = response.request;
 
while (http_resp.reset() != .closing) {
http_resp.wait() catch |err| switch (err) {
error.HttpHeadersInvalid => continue :connection,
error.EndOfStream => continue,
else => return err,
};
std.log.info("{s} {s} {s}", .{
@tagName(http_resp.request.method),
@tagName(http_resp.request.version),
http_resp.request.target,
});
//const body = try http_resp.reader().readAllAlloc(alloc, 8192);
//defer a.free(body);
 
try http_resp.headers.append("Server", "Source Tree WebServer");
try http_resp.headers.append("connection", "close");
 
var request = try Request.init(alloc, http_resp);
const response = Response.init(alloc, &request);
 
var ctx = try Verse.init(
alloc,
request,
response,
undefined, // :<
);
Router.baseRouterHtml(&ctx) catch |e| switch (e) {
error.AndExit => break :connection,
else => return e,
};
}
}
}
 
src/main.zig added: 206, removed: 4403, total 0
@@ -1,24 +1,27 @@
const std = @import("std");
const Verse = @import("verse");
 
const Allocator = std.mem.Allocator;
const Thread = std.Thread;
const Server = std.http.Server;
 
const Database = @import("database.zig");
const HTML = @import("html.zig");
const Template = @import("template.zig");
const Route = @import("routes.zig");
//const HTML = Verse.HTML;
const Route = Verse.Router;
const Repos = @import("repos.zig");
const HTTP = @import("http.zig");
const zWSGI = @import("zwsgi.zig");
const Ini = @import("ini.zig");
const zWSGI = Verse.zWSGI;
 
// TODO FIXME revert to internal config instead of Verse version
// but I don't want to lose track of origin, and that's where the
// primary version (read: usage) lives currently.
//const Ini = @import("ini.zig");
const Ini = Verse.Ini;
const Cache = @import("cache.zig");
 
const Srctree = @import("srctree.zig");
 
test "main" {
std.testing.refAllDecls(@This());
_ = HTML.html(&[0]HTML.Element{});
std.testing.refAllDecls(@import("git.zig"));
}
 
@@ -71,9 +74,6 @@ pub fn main() !void {
defer _ = gpa.deinit();
const a = gpa.allocator();
 
Template.init(a);
defer Template.raze(a);
 
var runmode: zWSGI.RunMode = .unix;
 
var args = std.process.args();
 
src/patch.zig added: 206, removed: 4403, total 0
@@ -10,10 +10,10 @@ const splitScalar = std.mem.splitScalar;
 
const CURL = @import("curl.zig");
const Bleach = @import("bleach.zig");
const Response = @import("response.zig");
const HTML = @import("html.zig");
const DOM = @import("dom.zig");
const Verse = @import("template.zig").Verse;
const Verse = @import("verse");
const Response = Verse.Response;
const HTML = Verse.HTML;
const DOM = Verse.DOM;
 
pub const Patch = @This();
 
 
src/repos.zig added: 206, removed: 4403, total 0
@@ -3,7 +3,8 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
 
const Git = @import("git.zig");
const Ini = @import("ini.zig");
/// TODO FIXME
const Ini = @import("verse").Ini;
 
const Repos = @This();
 
 
ev/null added: 206, removed: 4403, total 0
@@ -1,109 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const indexOf = std.mem.indexOf;
const eql = std.mem.eql;
 
const zWSGIRequest = @import("zwsgi.zig").zWSGIRequest;
const Auth = @import("auth.zig");
 
pub const Request = @This();
 
pub const RawRequests = union(enum) {
zwsgi: zWSGIRequest,
http: *std.http.Server.Request,
};
 
const Pair = struct {
name: []const u8,
val: []const u8,
};
 
pub const HeaderList = std.ArrayList(Pair);
 
pub const Methods = enum(u8) {
GET = 1,
HEAD = 2,
POST = 4,
PUT = 8,
DELETE = 16,
CONNECT = 32,
OPTIONS = 64,
TRACE = 128,
 
pub fn fromStr(s: []const u8) !Methods {
inline for (std.meta.fields(Methods)) |field| {
if (std.mem.startsWith(u8, s, field.name)) {
return @enumFromInt(field.value);
}
}
return error.UnknownMethod;
}
};
 
/// TODO this is unstable and likely to be removed
raw_request: RawRequests,
 
headers: HeaderList,
uri: []const u8,
method: Methods,
auth: Auth,
 
pub fn init(a: Allocator, raw_req: anytype) !Request {
switch (@TypeOf(raw_req)) {
zWSGIRequest => {
var req = Request{
.raw_request = .{ .zwsgi = raw_req },
.headers = HeaderList.init(a),
.uri = undefined,
.method = Methods.GET,
.auth = undefined,
};
for (raw_req.vars) |v| {
try req.addHeader(v.key, v.val);
if (std.mem.eql(u8, v.key, "PATH_INFO")) {
req.uri = v.val;
}
if (std.mem.eql(u8, v.key, "REQUEST_METHOD")) {
req.method = Methods.fromStr(v.val) catch Methods.GET;
}
}
req.auth = Auth.init(req.headers);
return req;
},
*std.http.Server.Request => {
var req = Request{
.raw_request = .{ .http = raw_req },
.headers = HeaderList.init(a),
.uri = raw_req.head.target,
.method = switch (raw_req.head.method) {
.GET => .GET,
.POST => .POST,
else => @panic("not implemented"),
},
.auth = undefined,
};
var itr = raw_req.iterateHeaders();
while (itr.next()) |head| {
try req.addHeader(head.name, head.value);
}
req.auth = Auth.init(req.headers);
return req;
},
else => @compileError("rawish of " ++ @typeName(raw_req) ++ " isn't a support request type"),
}
@compileError("unreachable");
}
 
pub fn addHeader(self: *Request, name: []const u8, val: []const u8) !void {
try self.headers.append(.{ .name = name, .val = val });
}
 
pub fn getHeader(self: Request, key: []const u8) ?[]const u8 {
for (self.headers.items) |itm| {
if (std.mem.eql(u8, itm.name, key)) {
return itm.val;
}
} else {
return null;
}
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,494 +0,0 @@
const std = @import("std");
const Type = @import("builtin").Type;
const Allocator = std.mem.Allocator;
const eql = std.mem.eql;
const splitScalar = std.mem.splitScalar;
const splitSequence = std.mem.splitSequence;
 
const Data = @This();
 
post: ?PostData,
query: QueryData,
 
pub fn validate(data: Data, comptime T: type) !T {
return RequestData(T).init(data);
}
 
/// This is the preferred api to use... once it actually exists :D
pub fn Validator(comptime T: type) type {
return struct {
data: T,
 
const Self = @This();
 
pub fn init(data: T) Self {
return Self{
.data = data,
};
}
 
pub fn count(v: *Self, name: []const u8) usize {
var i: usize = 0;
for (v.data.items) |item| {
if (eql(u8, item.name, name)) i += 1;
}
return i;
}
 
pub fn require(v: *Self, name: []const u8) !DataItem {
return v.optional(name) orelse error.DataMissing;
}
 
pub fn requirePos(v: *Self, name: []const u8, skip: usize) !DataItem {
var skipped: usize = skip;
for (v.data.items) |item| {
if (eql(u8, item.name, name)) {
if (skipped > 0) {
skipped -= 1;
continue;
}
return item;
}
}
return error.DataMissing;
}
 
pub fn optional(v: *Self, name: []const u8) ?DataItem {
for (v.data.items) |item| {
if (eql(u8, item.name, name)) return item;
}
return null;
}
 
pub fn optionalBool(v: *Self, name: []const u8) ?bool {
if (v.optional(name)) |boolish| {
if (eql(u8, boolish.value, "0") or eql(u8, boolish.value, "false")) {
return false;
}
return true;
}
return null;
}
 
pub fn files(_: *Self, _: []const u8) !void {
return error.NotImplemented;
}
};
}
 
pub fn validator(data: anytype) Validator(@TypeOf(data)) {
return Validator(@TypeOf(data)).init(data);
}
 
pub const DataKind = enum {
@"form-data",
};
 
pub const DataItem = struct {
data: []const u8,
headers: ?[]const u8 = null,
body: ?[]const u8 = null,
 
kind: DataKind = .@"form-data",
name: []const u8,
value: []const u8,
};
 
pub const PostData = struct {
rawpost: []u8,
items: []DataItem,
 
pub fn validate(pdata: PostData, comptime T: type) !T {
return RequestData(T).initPost(pdata);
}
 
pub fn validator(self: PostData) Validator(PostData) {
return Validator(PostData).init(self);
}
};
 
pub const QueryData = struct {
alloc: Allocator,
rawquery: []const u8,
items: []DataItem,
 
/// TODO leaks on error
pub fn init(a: Allocator, query: []const u8) !QueryData {
var itr = splitScalar(u8, query, '&');
const count = std.mem.count(u8, query, "&") + 1;
const items = try a.alloc(DataItem, count);
for (items) |*item| {
item.* = try parseSegment(a, itr.next().?);
}
 
return QueryData{
.alloc = a,
.rawquery = query,
.items = items,
};
}
 
pub fn validate(qdata: QueryData, comptime T: type) !T {
return RequestData(T).initQuery(qdata);
}
 
/// segments name=value&name2=otherval
/// segment in name=%22dquote%22
/// segment out name="dquote"
fn parseSegment(a: Allocator, seg: []const u8) !DataItem {
if (std.mem.indexOf(u8, seg, "=")) |i| {
const alen = seg.len - i - 1;
const input = seg[i + 1 .. seg.len];
var value: []u8 = @constCast(input);
if (alen > 0) {
value = try a.alloc(u8, alen);
value = try normalizeUrlEncoded(input, value);
}
return .{
.data = seg,
.name = seg[0..i],
.value = value,
};
} else {
return .{
.data = seg,
.name = seg,
.value = seg[seg.len..seg.len],
};
}
}
 
pub fn validator(self: QueryData) Validator(QueryData) {
return Validator(QueryData).init(self);
}
};
 
pub const ContentType = union(enum) {
const Application = enum {
@"x-www-form-urlencoded",
@"x-git-upload-pack-request",
};
const MultiPart = enum {
mixed,
@"form-data",
};
multipart: MultiPart,
application: Application,
 
fn subWrap(comptime Kind: type, str: []const u8) !Kind {
inline for (std.meta.fields(Kind)) |field| {
if (std.mem.startsWith(u8, str, field.name)) {
return @enumFromInt(field.value);
}
}
return error.UnknownContentType;
}
 
fn wrap(comptime kind: type, val: anytype) !ContentType {
return switch (kind) {
MultiPart => .{ .multipart = try subWrap(kind, val) },
Application => .{ .application = try subWrap(kind, val) },
else => @compileError("not implemented type"),
};
}
 
pub fn fromStr(str: []const u8) !ContentType {
inline for (std.meta.fields(ContentType)) |field| {
if (std.mem.startsWith(u8, str, field.name)) {
return wrap(field.type, str[field.name.len + 1 ..]);
}
}
return error.UnknownContentType;
}
};
 
pub fn RequestData(comptime T: type) type {
return struct {
req: T,
 
const Self = @This();
 
pub fn init(data: Data) !T {
var query_valid = data.query.validator();
var mpost_valid = if (data.post) |post| post.validator() else null;
var req: T = undefined;
inline for (std.meta.fields(T)) |field| {
if (mpost_valid) |*post_valid| {
@field(req, field.name) = get(field.type, field.name, post_valid, field.default_value) catch try get(field.type, field.name, &query_valid, field.default_value);
} else {
@field(req, field.name) = try get(field.type, field.name, &query_valid, field.default_value);
}
}
return req;
}
 
pub fn initMap(a: Allocator, data: Data) !T {
if (data.post) |post| return initPostMap(a, post);
 
// Only post is implemented
return error.NotImplemented;
}
 
fn get(FieldType: type, comptime name: []const u8, valid: anytype, default: ?*const anyopaque) !FieldType {
return switch (@typeInfo(FieldType)) {
.Optional => |opt| switch (opt.child) {
bool => if (valid.optionalBool(name)) |b|
b
else if (default != null)
@as(*const ?bool, @ptrCast(default.?)).*
else
null,
else => if (valid.optional(name)) |o| o.value else null,
},
.Pointer => (try valid.require(name)).value,
else => comptime unreachable,
};
}
 
fn initQuery(query: QueryData) !T {
var valid = query.validator();
var req: T = undefined;
inline for (std.meta.fields(T)) |field| {
@field(req, field.name) = try get(field.type, field.name, &valid, field.default_value);
}
return req;
}
 
fn initPost(data: PostData) !T {
var valid = data.validator();
 
var req: T = undefined;
inline for (std.meta.fields(T)) |field| {
@field(req, field.name) = try get(field.type, field.name, &valid, field.default_value);
}
return req;
}
 
fn initPostMap(a: Allocator, data: PostData) !T {
var valid = data.validator();
 
var req: T = undefined;
inline for (std.meta.fields(T)) |field| {
@field(req, field.name) = switch (@typeInfo(field.type)) {
.Optional => if (valid.optional(field.name)) |o| o.value else null,
.Pointer => |fptr| switch (fptr.child) {
u8 => (try valid.require(field.name)).value,
[]const u8 => arr: {
const count = valid.count(field.name);
var map = try a.alloc([]const u8, count);
for (0..count) |i| {
map[i] = (try valid.requirePos(field.name, i)).value;
}
break :arr map;
},
else => comptime unreachable,
},
else => unreachable,
};
}
return req;
}
};
}
 
fn normalizeUrlEncoded(in: []const u8, out: []u8) ![]u8 {
var len: usize = 0;
var i: usize = 0;
while (i < in.len) {
const c = &in[i];
var char: u8 = 0xff;
switch (c.*) {
'+' => char = ' ',
'%' => {
if (i + 2 >= in.len) {
char = c.*;
continue;
}
char = std.fmt.parseInt(u8, in[i + 1 ..][0..2], 16) catch '%';
i += 2;
},
else => |o| char = o,
}
out[len] = char;
len += 1;
i += 1;
}
return out[0..len];
}
 
fn parseApplication(a: Allocator, ap: ContentType.Application, data: []u8, htype: []const u8) ![]DataItem {
switch (ap) {
.@"x-www-form-urlencoded" => {
std.debug.assert(std.mem.startsWith(u8, htype, "application/x-www-form-urlencoded"));
 
var itr = splitScalar(u8, data, '&');
const count = std.mem.count(u8, data, "&") +| 1;
const items = try a.alloc(DataItem, count);
for (items) |*itm| {
const idata = itr.next().?;
var odata = try a.dupe(u8, idata);
var name = odata;
var value = odata;
if (std.mem.indexOf(u8, idata, "=")) |i| {
name = try normalizeUrlEncoded(idata[0..i], odata[0..i]);
value = try normalizeUrlEncoded(idata[i + 1 ..], odata[i + 1 ..]);
}
itm.* = .{
.data = odata,
.name = name,
.value = value,
};
}
return items;
},
.@"x-git-upload-pack-request" => {
// Git just uses the raw data instead, no need to preprocess
return &[0]DataItem{};
},
}
}
 
const DataHeader = enum {
@"Content-Disposition",
@"Content-Type",
 
pub fn fromStr(str: []const u8) !DataHeader {
inline for (std.meta.fields(DataHeader)) |field| {
if (std.mem.startsWith(u8, str, field.name)) {
return @enumFromInt(field.value);
}
}
std.log.info("'{s}'", .{str});
return error.UnknownHeader;
}
};
 
const MultiData = struct {
header: DataHeader,
str: []const u8,
name: ?[]const u8 = null,
filename: ?[]const u8 = null,
 
fn update(md: *MultiData, str: []const u8) void {
var trimmed = std.mem.trim(u8, str, " \t\n\r");
if (std.mem.indexOf(u8, trimmed, "=")) |i| {
if (std.mem.eql(u8, trimmed[0..i], "name")) {
md.name = trimmed[i + 1 ..];
} else if (std.mem.eql(u8, trimmed[0..i], "filename")) {
md.filename = trimmed[i + 1 ..];
}
}
}
};
 
fn parseMultiData(data: []const u8) !MultiData {
var extra = splitScalar(u8, data, ';');
const first = extra.first();
const header = try DataHeader.fromStr(first);
var mdata: MultiData = .{
.header = header,
.str = first[@tagName(header).len + 1 ..],
};
 
while (extra.next()) |each| {
mdata.update(each);
}
 
return mdata;
}
 
fn parseMultiFormData(a: Allocator, data: []const u8) !DataItem {
_ = a;
std.debug.assert(std.mem.startsWith(u8, data, "\r\n"));
if (std.mem.indexOf(u8, data, "\r\n\r\n")) |i| {
var post_item = DataItem{
.data = data,
.name = undefined,
.value = data[i + 4 ..],
};
 
post_item.headers = data[0..i];
var headeritr = splitSequence(u8, post_item.headers.?, "\r\n");
while (headeritr.next()) |header| {
if (header.len == 0) continue;
const md = try parseMultiData(header);
if (md.name) |name| post_item.name = name;
// TODO look for other headers or other data
}
return post_item;
}
return error.UnableToParseFormData;
}
 
/// Pretends to follow RFC2046
fn parseMulti(a: Allocator, mp: ContentType.MultiPart, data: []const u8, htype: []const u8) ![]DataItem {
var boundry_buffer = [_]u8{'-'} ** 74;
switch (mp) {
.mixed => {
return error.NotImplemented;
},
.@"form-data" => {
std.debug.assert(std.mem.startsWith(u8, htype, "multipart/form-data; boundary="));
std.debug.assert(htype.len > 30);
const bound_given = htype[30..];
@memcpy(boundry_buffer[2 .. bound_given.len + 2], bound_given);
 
const boundry = boundry_buffer[0 .. bound_given.len + 2];
const count = std.mem.count(u8, data, boundry) -| 1;
const items = try a.alloc(DataItem, count);
var itr = splitSequence(u8, data, boundry);
_ = itr.first(); // the RFC says I'm supposed to ignore the preamble :<
for (items) |*itm| {
itm.* = try parseMultiFormData(a, itr.next().?);
}
std.debug.assert(std.mem.eql(u8, itr.rest(), "--\r\n"));
return items;
},
}
}
 
pub fn readBody(
a: Allocator,
reader: *std.io.AnyReader,
size: usize,
htype: []const u8,
) !PostData {
const post_buf: []u8 = try a.alloc(u8, size);
const read_size = try reader.read(post_buf);
if (read_size != size) return error.UnexpectedHttpBodySize;
 
const items = switch (try ContentType.fromStr(htype)) {
.application => |ap| try parseApplication(a, ap, post_buf, htype),
.multipart => |mp| try parseMulti(a, mp, post_buf, htype),
};
 
return .{
.rawpost = post_buf,
.items = items,
};
}
 
pub fn readQuery(a: Allocator, query: []const u8) !QueryData {
return QueryData.init(a, query);
}
 
pub fn parseRequestData(
a: Allocator,
query: []const u8,
acpt: std.net.StreamServer.Connection,
size: usize,
htype: []const u8,
) !RequestData {
return RequestData{
.post_data = try readBody(a, acpt, size, htype),
.query_data = try readQuery(a, query),
};
}
 
test "multipart/mixed" {}
 
test "multipart/form-data" {}
 
test "multipart/multipart" {}
 
test "application/x-www-form-urlencoded" {}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,219 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const AnyWriter = std.io.AnyWriter;
 
const Request = @import("request.zig");
const Headers = @import("headers.zig");
 
const Response = @This();
 
const ONESHOT_SIZE = 14720;
 
const Pair = struct {
name: []const u8,
val: []const u8,
};
 
pub const TransferMode = enum {
static,
streaming,
proxy,
proxy_streaming,
};
 
const Downstream = enum {
buffer,
zwsgi,
http,
};
 
const Error = error{
WrongPhase,
HeadersFinished,
ResponseClosed,
UnknownStatus,
};
 
pub const Writer = std.io.Writer(*Response, Error, write);
 
//request: *Request,
headers: ?Headers = null,
tranfer_mode: TransferMode = .static,
// This is just bad code, but I need to give the sane implementation more thought
http_response: ?std.http.Server.Response = null,
downstream: union(Downstream) {
buffer: std.io.BufferedWriter(ONESHOT_SIZE, std.net.Stream.Writer),
zwsgi: std.net.Stream.Writer,
http: std.io.AnyWriter,
},
status: ?std.http.Status = null,
 
pub fn init(a: Allocator, req: *Request) !Response {
var res = Response{
//.alloc = a,
.headers = Headers.init(a),
.http_response = switch (req.raw_request) {
.zwsgi => null,
.http => |h| h.*.respondStreaming(.{
.send_buffer = try a.alloc(u8, 0xffff),
.respond_options = .{
.transfer_encoding = .chunked,
.keep_alive = false,
},
}),
},
.downstream = switch (req.raw_request) {
.zwsgi => |*z| .{ .zwsgi = z.acpt.stream.writer() },
.http => .{ .http = undefined },
},
};
if (res.http_response) |*h| res.downstream.http = h.writer();
res.headersInit() catch @panic("unable to create Response obj");
return res;
}
 
fn headersInit(res: *Response) !void {
try res.headersAdd("Server", "zwsgi/0.0.0");
try res.headersAdd("Content-Type", "text/html; charset=utf-8"); // Firefox is trash
}
 
pub fn headersAdd(res: *Response, comptime name: []const u8, value: []const u8) !void {
if (res.headers) |*headers| {
try headers.add(name, value);
} else return Error.HeadersFinished;
}
 
pub fn start(res: *Response) !void {
if (res.headers == null) return Error.WrongPhase;
if (res.status == null) res.status = .ok;
switch (res.downstream) {
.http => {
// I don't know why/where the writer goes invalid, but I'll probably
// fix it later?
if (res.http_response) |*h| res.downstream.http = h.writer();
try res.sendHeaders();
},
else => {
try res.sendHeaders();
_ = try res.write("\r\n");
},
}
}
 
fn sendHTTPHeader(res: *Response) !void {
if (res.status == null) res.status = .ok;
_ = switch (res.status.?) {
.ok => try res.write("HTTP/1.1 200 OK\r\n"),
.found => try res.write("HTTP/1.1 302 Found\r\n"),
.forbidden => try res.write("HTTP/1.1 403 Forbidden\r\n"),
.not_found => try res.write("HTTP/1.1 404 Not Found\r\n"),
.internal_server_error => try res.write("HTTP/1.1 500 Internal Server Error\r\n"),
else => return Error.UnknownStatus,
};
}
 
pub fn sendHeaders(res: *Response) !void {
switch (res.downstream) {
.http => try res.http_response.?.flush(),
.zwsgi, .buffer => {
if (res.headers) |*headers| {
try res.sendHTTPHeader();
var itr = headers.index.iterator();
while (itr.next()) |header| {
var buf: [512]u8 = undefined;
const b = try std.fmt.bufPrint(&buf, "{s}: {s}\r\n", .{
header.key_ptr.*,
header.value_ptr.str,
});
_ = try res.write(b);
}
_ = try res.write("Transfer-Encoding: chunked\r\n");
} else return error.WrongPhase;
},
}
res.headers = null;
}
 
pub fn redirect(res: *Response, loc: []const u8, see_other: bool) !void {
if (res.headers == null) return error.WrongPhase;
 
try res.writeAll("HTTP/1.1 ");
if (see_other) {
try res.writeAll("303 See Other\r\n");
} else {
try res.writeAll("302 Found\r\n");
}
 
try res.writeAll("Location: ");
try res.writeAll(loc);
try res.writeAll("\r\n\r\n");
}
 
/// Do not use
/// TODO remove
pub fn send(res: *Response, data: []const u8) !void {
if (res.headers != null) try res.start();
try res.writeAll(data);
return res.finish();
}
 
pub fn writer(res: *const Response) AnyWriter {
return .{
.writeFn = typeErasedWrite,
.context = res,
};
}
 
pub fn writeChunk(res: *const Response, data: []const u8) !void {
comptime unreachable;
var size: [0xff]u8 = undefined;
const chunk = try std.fmt.bufPrint(&size, "{x}\r\n", .{data.len});
try res.writeAll(chunk);
try res.writeAll(data);
try res.writeAll("\r\n");
}
 
pub fn writeAll(res: *const Response, data: []const u8) !void {
var index: usize = 0;
while (index < data.len) {
index += try write(res, data[index..]);
}
}
 
pub fn typeErasedWrite(opq: *const anyopaque, data: []const u8) anyerror!usize {
const cast: *const Response = @alignCast(@ptrCast(opq));
return try write(cast, data);
}
 
/// Raw writer, use with caution! To use phase checking, use send();
pub fn write(res: *const Response, data: []const u8) !usize {
return switch (res.downstream) {
.zwsgi => |*w| try w.write(data),
.http => |*w| return try w.write(data),
.buffer => {
var bff: *Response = @constCast(res);
return try bff.write(data);
},
};
}
 
fn flush(res: *Response) !void {
switch (res.downstream) {
.buffer => |*w| try w.flush(),
.http => |*h| h.flush(),
else => {},
}
}
 
pub fn finish(res: *Response) !void {
switch (res.downstream) {
.http => {
if (res.http_response) |*h| try h.endChunked(.{});
},
//.zwsgi => |*w| _ = try w.write("0\r\n\r\n"),
else => {},
}
//return res.flush() catch |e| {
// std.debug.print("Error on flush :< {}\n", .{e});
//};
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,197 +0,0 @@
const std = @import("std");
const eql = std.mem.eql;
 
const Allocator = std.mem.Allocator;
 
const api = @import("api.zig");
const Verse = @import("verse.zig");
const Response = @import("response.zig");
const Request = @import("request.zig");
const HTML = @import("html.zig");
const StaticFile = @import("static-file.zig");
 
pub const Errors = @import("errors.zig");
pub const Error = Errors.ServerError || Errors.ClientError || Errors.NetworkError;
 
pub const UriIter = std.mem.SplitIterator(u8, .scalar);
 
pub const Router = *const fn (*Verse) Error!Callable;
pub const Callable = *const fn (*Verse) Error!void;
 
pub const DEBUG: bool = false;
 
/// Methods is a struct so bitwise or will work as expected
pub const Methods = packed struct {
GET: bool = false,
HEAD: bool = false,
POST: bool = false,
PUT: bool = false,
DELETE: bool = false,
CONNECT: bool = false,
OPTIONS: bool = false,
TRACE: bool = false,
 
pub fn matchMethod(self: Methods, req: Request.Methods) bool {
return switch (req) {
.GET => self.GET,
.HEAD => self.HEAD,
.POST => self.POST,
.PUT => self.PUT,
.DELETE => self.DELETE,
.CONNECT => self.CONNECT,
.OPTIONS => self.OPTIONS,
.TRACE => self.TRACE,
};
}
};
 
pub const Endpoint = struct {
callable: Callable,
methods: Methods = .{ .GET = true },
};
 
pub const Match = struct {
name: []const u8,
match: union(enum) {
call: Callable,
route: Router,
simple: []const Match,
},
methods: Methods = .{ .GET = true },
};
 
pub fn ROUTE(comptime name: []const u8, comptime match: anytype) Match {
return comptime Match{
.name = name,
.match = switch (@typeInfo(@TypeOf(match))) {
.Pointer => |ptr| switch (@typeInfo(ptr.child)) {
.Fn => |fnc| switch (fnc.return_type orelse null) {
Error!void => .{ .call = match },
Error!Callable => .{ .route = match },
else => @compileError("unknown function return type"),
},
else => .{ .simple = match },
},
.Fn => |fnc| switch (fnc.return_type orelse null) {
Error!void => .{ .call = match },
Error!Callable => .{ .route = match },
else => @compileError("unknown function return type"),
},
else => |el| @compileError("match type not supported, for provided type [" ++
@typeName(@TypeOf(el)) ++
"]"),
},
 
.methods = .{ .GET = true, .POST = true },
};
}
 
pub fn any(comptime name: []const u8, comptime match: Callable) Match {
var mr = ROUTE(name, match);
mr.methods = .{ .GET = true, .POST = true };
return mr;
}
 
pub fn GET(comptime name: []const u8, comptime match: Callable) Match {
var mr = ROUTE(name, match);
mr.methods = .{ .GET = true };
return mr;
}
 
pub fn POST(comptime name: []const u8, comptime match: Callable) Match {
var mr = ROUTE(name, match);
mr.methods = .{ .POST = true };
return mr;
}
 
pub fn STATIC(comptime name: []const u8) Match {
var mr = ROUTE(name, StaticFile.fileOnDisk);
mr.methods = .{ .GET = true };
return mr;
}
 
pub fn defaultResponse(comptime code: std.http.Status) Callable {
return switch (code) {
.not_found => notFound,
.internal_server_error => internalServerError,
else => default,
};
}
 
fn notFound(ctx: *Verse) Error!void {
ctx.response.status = .not_found;
const E4XX = @embedFile("../templates/4XX.html");
return ctx.sendRawSlice(E4XX);
}
 
fn internalServerError(ctx: *Verse) Error!void {
ctx.response.status = .internal_server_error;
const E5XX = @embedFile("../templates/5XX.html");
return ctx.sendRawSlice(E5XX);
}
 
fn default(ctx: *Verse) Error!void {
const index = @embedFile("../templates/index.html");
return ctx.sendRawSlice(index);
}
 
pub fn router(ctx: *Verse, comptime routes: []const Match) Callable {
const search = ctx.uri.peek() orelse {
if (DEBUG) std.debug.print("No endpoint found: URI is empty.\n", .{});
return notFound;
};
inline for (routes) |ep| {
if (eql(u8, search, ep.name)) {
switch (ep.match) {
.call => |call| {
if (ep.methods.matchMethod(ctx.request.method))
return call;
},
.route => |route| {
return route(ctx) catch |err| switch (err) {
error.Unrouteable => return notFound,
else => unreachable,
};
},
.simple => |simple| {
_ = ctx.uri.next();
if (ctx.uri.peek() == null and
eql(u8, simple[0].name, "") and
simple[0].match == .call)
return simple[0].match.call;
return router(ctx, simple);
},
}
}
}
return notFound;
}
 
const root = [_]Match{
ROUTE("", default),
};
 
pub fn baseRouter(ctx: *Verse) Error!void {
if (DEBUG) std.debug.print("baserouter {s}\n", .{ctx.uri.peek().?});
if (ctx.uri.peek()) |first| {
if (first.len > 0) {
const route: Callable = router(ctx, &root);
return route(ctx);
}
}
return default(ctx);
}
 
const root_with_static = root ++
[_]Match{.{ .name = "static", .match = .{ .call = StaticFile.file } }};
 
pub fn baseRouterHtml(ctx: *Verse) Error!void {
if (DEBUG) std.debug.print("baserouter {s}\n", .{ctx.uri.peek().?});
if (ctx.uri.peek()) |first| {
if (first.len > 0) {
const route: Callable = router(ctx, &root_with_static);
return route(ctx);
}
}
return default(ctx);
}
 
src/srctree.zig added: 206, removed: 4403, total 0
@@ -1,6 +1,6 @@
const Routes = @import("routes.zig");
const Verse = @import("verse.zig");
const Template = @import("template.zig");
const Verse = @import("verse");
const Routes = Verse.Router;
const Template = Verse.Template;
const Api = @import("api.zig");
//const Types = @import("types.zig");
 
 
ev/null added: 206, removed: 4403, total 0
@@ -1,22 +0,0 @@
const std = @import("std");
 
const Verse = @import("verse.zig");
const Route = @import("routes.zig");
 
pub fn fileOnDisk(ctx: *Verse) Route.Error!void {
_ = ctx.uri.next(); // clear /static
const fname = ctx.uri.next() orelse return error.Unrouteable;
if (fname.len == 0) return error.Unrouteable;
for (fname) |c| switch (c) {
'A'...'Z', 'a'...'z', '-', '_', '.' => continue,
else => return error.Abusive,
};
if (std.mem.indexOf(u8, fname, "..")) |_| return error.Abusive;
 
const static = std.fs.cwd().openDir("static", .{}) catch return error.Unrouteable;
const fdata = static.readFileAlloc(ctx.alloc, fname, 0xFFFFFF) catch return error.Unknown;
 
ctx.response.start() catch return error.Unknown;
ctx.response.writeAll(fdata) catch return error.Unknown;
ctx.response.finish() catch return error.Unknown;
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,297 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const eql = std.mem.eql;
const bufPrint = std.fmt.bufPrint;
const compiled = @import("comptime_templates");
const Template = @import("template.zig");
 
const AbstTree = struct {
pub const Member = struct {
name: []u8,
kind: []u8,
 
pub fn format(self: Member, comptime _: []const u8, _: std.fmt.FormatOptions, w: anytype) !void {
try w.writeAll(" ");
try w.writeAll(self.name);
try w.writeAll(self.kind);
}
};
 
parent: ?*AbstTree,
alloc: Allocator,
name: []u8,
children: []Member,
child_cap: usize = 0,
 
pub fn init(a: Allocator, name: []const u8, parent: ?*AbstTree) !*AbstTree {
const self = try a.create(AbstTree);
self.* = .{
.parent = parent,
.alloc = a,
.name = try a.dupe(u8, name),
.children = try a.alloc(Member, 50),
.child_cap = 50,
};
self.children.len = 0;
return self;
}
 
pub fn exists(self: *AbstTree, name: []const u8) bool {
for (self.children) |child| {
if (eql(u8, child.name, name)) return true;
}
return false;
}
 
pub fn append(self: *AbstTree, name: []const u8, kind: []const u8) !void {
if (self.children.len >= self.child_cap) @panic("large structs not implemented");
 
for (self.children) |child| {
if (std.mem.eql(u8, child.name, name)) {
if (!std.mem.eql(u8, child.kind, kind)) {
std.debug.print("Error: kind mismatch while building ", .{});
var par = self.parent;
while (par != null) {
par = par.?.parent;
std.debug.print("{s}.", .{par.?.name});
}
 
std.debug.print(
"{s}.{s}\n {s} != {s}\n",
.{ self.name, name, child.kind, kind },
);
return error.KindMismatch;
}
return;
}
}
 
self.children.len += 1;
self.children[self.children.len - 1] = .{
.name = try self.alloc.dupe(u8, name),
.kind = try self.alloc.dupe(u8, kind),
};
}
 
pub fn format(self: AbstTree, comptime fmt: []const u8, _: std.fmt.FormatOptions, w: anytype) !void {
if (comptime std.mem.eql(u8, fmt, "")) {
try w.writeAll("pub const ");
try w.writeAll(self.name);
try w.writeAll(" = struct {\n");
for (self.children) |child| {
try w.print("{}", .{child});
}
try w.writeAll("};\n");
} else {
comptime unreachable;
}
}
};
 
var tree: std.StringHashMap(*AbstTree) = undefined;
 
pub fn main() !void {
var args = std.process.args();
 
var wout_path: ?[]const u8 = null;
while (args.next()) |arg| {
wout_path = arg;
}
 
const a = std.heap.page_allocator;
 
const wout_dname = std.fs.path.dirname(wout_path.?) orelse return error.InvalidPath;
const wout_dir = try std.fs.cwd().openDir(wout_dname, .{});
var wfile = try wout_dir.createFile(std.fs.path.basename(wout_path.?), .{});
defer wfile.close();
try wfile.writeAll(
\\// Generated by srctree template compiler
\\
);
var wout = wfile.writer();
 
tree = std.StringHashMap(*AbstTree).init(a);
 
for (compiled.data) |tplt| {
const fdata = try std.fs.cwd().readFileAlloc(a, tplt.path, 0xffff);
defer a.free(fdata);
 
const name = makeStructName(tplt.path);
const this = try AbstTree.init(a, name, null);
const gop = try tree.getOrPut(this.name);
if (!gop.found_existing) {
gop.value_ptr.* = this;
}
try emitVars(a, fdata, this);
}
 
var itr = tree.iterator();
while (itr.next()) |each| {
//std.debug.print("tree: {}\n", .{each.value_ptr.*});
try wout.print("{}\n", .{each.value_ptr.*});
}
}
 
fn emitVars(a: Allocator, fdata: []const u8, current: *AbstTree) !void {
var data = fdata;
while (data.len > 0) {
if (std.mem.indexOf(u8, data, "<")) |offset| {
data = data[offset..];
if (Template.Directive.init(data)) |drct| {
data = data[drct.tag_block.len..];
const s_name = makeStructName(drct.noun);
var f_name = makeFieldName(drct.noun);
switch (drct.verb) {
.variable => |_| {
var buffer: [0xFF]u8 = undefined;
var kind = try bufPrint(&buffer, ": []const u8,\n", .{});
 
switch (drct.otherwise) {
.required, .ignore => {},
.default => |str| {
kind = try bufPrint(&buffer, ": []const u8 = \"{s}\",\n", .{str});
},
.delete => {
kind = try bufPrint(&buffer, ": ?[]const u8 = null,\n", .{});
},
.template => |_| {
kind = try bufPrint(&buffer, ": ?{s},\n", .{s_name});
f_name = makeFieldName(drct.noun[1 .. drct.noun.len - 5]);
},
.blob => unreachable,
}
if (drct.known_type) |kt| {
kind = try bufPrint(&buffer, ": {s},\n", .{@tagName(kt)});
}
try current.append(f_name, kind);
},
else => |verb| {
var this = try AbstTree.init(a, s_name, current);
const gop = try tree.getOrPut(this.name);
if (!gop.found_existing) {
gop.value_ptr.* = this;
} else {
this = gop.value_ptr.*;
}
 
switch (verb) {
.variable => unreachable,
.foreach => {
var buffer: [0xFF]u8 = undefined;
const kind = try bufPrint(&buffer, ": []const {s},\n", .{s_name});
try current.append(f_name, kind);
try emitVars(a, drct.otherwise.blob, this);
},
.split => {
var buffer: [0xFF]u8 = undefined;
const kind = try bufPrint(&buffer, ": []const []const u8,\n", .{});
try current.append(f_name, kind);
},
.with => {
var buffer: [0xFF]u8 = undefined;
const kind = try bufPrint(&buffer, ": ?{s},\n", .{s_name});
try current.append(f_name, kind);
try emitVars(a, drct.otherwise.blob, this);
},
.build => {
var buffer: [0xFF]u8 = undefined;
const tmpl_name = makeStructName(drct.otherwise.template.name);
const kind = try bufPrint(&buffer, ": {s},\n", .{tmpl_name});
try current.append(f_name, kind);
//try emitVars(a, drct.otherwise.template.blob, this);
},
}
},
}
} else if (std.mem.indexOfPos(u8, data, 1, "<")) |next| {
data = data[next..];
} else return;
} else return;
}
return;
}
 
pub fn makeFieldName(in: []const u8) []const u8 {
const local = struct {
var name: [0xFFFF]u8 = undefined;
};
 
var i: usize = 0;
for (in) |chr| {
switch (chr) {
'a'...'z' => {
local.name[i] = chr;
i += 1;
},
'A'...'Z' => {
if (i != 0) {
local.name[i] = '_';
i += 1;
}
local.name[i] = std.ascii.toLower(chr);
i += 1;
},
'0'...'9' => {
for (intToWord(chr)) |cchr| {
local.name[i] = cchr;
i += 1;
}
},
'-', '_', '.' => {
local.name[i] = '_';
i += 1;
},
else => {},
}
}
 
return local.name[0..i];
}
 
pub fn makeStructName(in: []const u8) []const u8 {
const local = struct {
var name: [0xFFFF]u8 = undefined;
};
 
var tail = in;
 
if (std.mem.lastIndexOf(u8, in, "/")) |i| {
tail = tail[i..];
}
 
var i: usize = 0;
var next_upper = true;
for (tail) |chr| {
switch (chr) {
'a'...'z', 'A'...'Z' => {
if (next_upper) {
local.name[i] = std.ascii.toUpper(chr);
} else {
local.name[i] = chr;
}
next_upper = false;
i += 1;
},
'0'...'9' => {
for (intToWord(chr)) |cchr| {
local.name[i] = cchr;
i += 1;
}
},
'-', '_', '.' => {
next_upper = true;
},
else => {},
}
}
 
return local.name[0..i];
}
 
fn intToWord(in: u8) []const u8 {
return switch (in) {
'4' => "Four",
'5' => "Five",
else => unreachable,
};
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,814 +0,0 @@
const std = @import("std");
const build_mode = @import("builtin").mode;
const compiled = @import("comptime_templates");
pub const Structs = @import("comptime_template_structs");
const Allocator = std.mem.Allocator;
const allocPrint = std.fmt.allocPrint;
 
const HTML = @import("html.zig");
const Pages = @import("template/page.zig");
pub const Directive = @import("template/directive.zig");
 
pub const Page = Pages.Page;
pub const PageRuntime = Pages.PageRuntime;
 
const MAX_BYTES = 2 <<| 15;
const TEMPLATE_PATH = "templates/";
 
const DEBUG = false;
 
pub const Template = struct {
// path: []const u8,
name: []const u8 = "undefined",
blob: []const u8,
parent: ?*const Template = null,
 
pub fn initWith(self: *Template, a: Allocator, data: []struct { name: []const u8, val: []const u8 }) !void {
self.init(a);
for (data) |d| {
try self.ctx.putSlice(d.name, d.value);
}
}
 
pub fn pageOf(self: Template, comptime Kind: type, data: Kind) PageRuntime(Kind) {
return PageRuntime(Kind).init(.{ .name = self.name, .blob = self.blob }, data);
}
 
pub fn format(_: Template, comptime _: []const u8, _: std.fmt.FormatOptions, _: anytype) !void {
comptime unreachable;
}
};
 
fn tailPath(path: []const u8) []const u8 {
if (std.mem.indexOf(u8, path, "/")) |i| {
return path[i + 1 ..];
}
return path[0..0];
}
 
pub const builtin: [compiled.data.len]Template = blk: {
@setEvalBranchQuota(5000);
var t: [compiled.data.len]Template = undefined;
for (compiled.data, &t) |filedata, *dst| {
dst.* = Template{
//.path = filedata.path,
.name = tailPath(filedata.path),
.blob = filedata.blob,
};
}
break :blk t;
};
 
pub var dynamic: []const Template = undefined;
 
fn loadTemplates(a: Allocator) !void {
var cwd = std.fs.cwd();
var idir = cwd.openDir(TEMPLATE_PATH, .{ .iterate = true }) catch |err| {
std.debug.print("Unable to build dynamic templates ({})\n", .{err});
return;
};
defer idir.close();
var itr = idir.iterate();
var list = std.ArrayList(Template).init(a);
errdefer list.clearAndFree();
while (try itr.next()) |file| {
if (file.kind != .file) continue;
const name = try std.mem.join(a, "/", &[2][]const u8{
TEMPLATE_PATH,
file.name,
});
defer a.free(name);
const tail = tailPath(file.name);
const name_ = try a.dupe(u8, tail);
try list.append(.{
//.path = path,
.name = name_,
.blob = try cwd.readFileAlloc(a, name, MAX_BYTES),
});
}
dynamic = try list.toOwnedSlice();
}
 
pub fn init(a: Allocator) void {
loadTemplates(a) catch unreachable;
}
 
pub fn raze(a: Allocator) void {
for (dynamic) |t| {
// leaks?
a.free(t.name);
a.free(t.blob);
}
a.free(dynamic);
}
 
pub fn findWhenever(name: []const u8) Template {
for (dynamic) |d| {
if (std.mem.eql(u8, d.name, name)) {
return d;
}
}
unreachable;
}
 
pub fn load(a: Allocator, comptime name: []const u8) Template {
var t = findTemplate(name);
t.init(a);
return t;
}
 
pub fn findTemplate(comptime name: []const u8) Template {
inline for (builtin) |bi| {
if (comptime std.mem.eql(u8, bi.name, name)) {
return bi;
}
}
@compileError("template " ++ name ++ " not found!");
}
 
pub fn PageData(comptime name: []const u8) type {
const template = findTemplate(name);
const page_data = comptime findPageType(name);
return Page(template, page_data);
}
 
fn intToWord(in: u8) []const u8 {
return switch (in) {
'4' => "Four",
'5' => "Five",
else => unreachable,
};
}
 
pub fn makeStructName(comptime in: []const u8, comptime out: []u8) usize {
var ltail = in;
if (comptime std.mem.lastIndexOf(u8, in, "/")) |i| {
ltail = ltail[i..];
}
 
var i = 0;
var next_upper = true;
inline for (ltail) |chr| {
switch (chr) {
'a'...'z', 'A'...'Z' => {
if (next_upper) {
out[i] = std.ascii.toUpper(chr);
} else {
out[i] = chr;
}
next_upper = false;
i += 1;
},
'0'...'9' => {
for (intToWord(chr)) |cchr| {
out[i] = cchr;
i += 1;
}
},
'-', '_', '.' => {
next_upper = true;
},
else => {},
}
}
 
return i;
}
 
pub fn makeFieldName(in: []const u8, out: []u8) usize {
var i: usize = 0;
for (in) |chr| {
switch (chr) {
'a'...'z' => {
out[i] = chr;
i += 1;
},
'A'...'Z' => {
if (i != 0) {
out[i] = '_';
i += 1;
}
out[i] = std.ascii.toLower(chr);
i += 1;
},
'0'...'9' => {
for (intToWord(chr)) |cchr| {
out[i] = cchr;
i += 1;
}
},
'-', '_', '.' => {
out[i] = '_';
i += 1;
},
else => {},
}
}
 
return i;
}
 
pub fn findPageType(comptime name: []const u8) type {
var local: [0xFFFF]u8 = undefined;
const llen = comptime makeStructName(name, &local);
return @field(Structs, local[0..llen]);
}
 
test "build.zig included templates" {
const names = [_][]const u8{
"templates/4XX.html",
"templates/5XX.html",
"templates/index.html",
"templates/code.html",
};
 
names: for (names) |name| {
for (compiled.data) |bld| {
if (std.mem.eql(u8, name, bld.path)) continue :names;
} else return error.TemplateMissing;
}
}
 
test "load templates" {
const a = std.testing.allocator;
init(a);
defer raze(a);
 
//try std.testing.expectEqual(3, builtin.len);
for (builtin) |bi| {
if (std.mem.eql(u8, bi.name, "index.html")) {
try std.testing.expectEqualStrings("index.html", bi.name);
try std.testing.expectEqualStrings("<!DOCTYPE html>", bi.blob[0..15]);
break;
}
} else {
return error.TemplateNotFound;
}
}
 
test findTemplate {
const tmpl = findTemplate("user_commits.html");
try std.testing.expectEqualStrings("user_commits.html", tmpl.name);
}
 
test "directive something" {
const a = std.testing.allocator;
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<Something>",
};
 
const ctx = .{
.something = @as([]const u8, "Some Text Here"),
};
const pg = Page(t, @TypeOf(ctx)).init(ctx);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings("Some Text Here", p);
 
const t2 = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<Something />",
};
 
const ctx2 = .{
.something = @as([]const u8, "Some Text Here"),
};
const pg2 = Page(t2, @TypeOf(ctx2)).init(ctx2);
const p2 = try allocPrint(a, "{}", .{pg2});
defer a.free(p2);
try std.testing.expectEqualStrings("Some Text Here", p2);
}
 
test "directive typed something" {
var a = std.testing.allocator;
 
const Something = struct {
something: []const u8,
};
 
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<Something>",
};
 
const page = Page(t, Something);
 
const pg = page.init(.{
.something = "Some Text Here",
});
 
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings("Some Text Here", p);
}
 
test "directive typed something /" {
var a = std.testing.allocator;
 
const Something = struct {
something: []const u8,
};
 
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<Something />",
};
 
const page = Page(t, Something);
 
const p = page.init(.{
.something = "Some Text Here",
});
 
const pg = try allocPrint(a, "{}", .{p});
defer a.free(pg);
try std.testing.expectEqualStrings("Some Text Here", pg);
}
 
test "directive nothing" {
var a = std.testing.allocator;
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<!-- nothing -->",
};
 
const ctx = .{};
const page = Page(t, @TypeOf(ctx));
 
const pg = page.init(ctx);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings("<!-- nothing -->", p);
}
 
test "directive nothing new" {
const a = std.testing.allocator;
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<Nothing>",
};
 
const ctx = .{};
 
// TODO is this still the expected behavior
//const p = Page(t, @TypeOf(ctx)).init(.{});
//try std.testing.expectError(error.VariableMissing, p);
 
const pg = Page(t, @TypeOf(ctx)).init(.{});
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings("<Nothing>", p);
}
 
test "directive ORELSE" {
var a = std.testing.allocator;
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<This default='string until end'>",
};
 
const ctx = .{
.this = @as(?[]const u8, null),
};
 
const pg = Page(t, @TypeOf(ctx)).init(ctx);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings("string until end", p);
}
 
test "directive ORNULL" {
var a = std.testing.allocator;
const t = Template{
//.path = "/dev/null",
.name = "test",
// Invalid because 'string until end' is known to be unreachable
.blob = "<This ornull string until end>",
};
 
const ctx = .{
.this = @as(?[]const u8, null),
};
 
const pg = Page(t, @TypeOf(ctx)).init(ctx);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings("", p);
 
const t2 = Template{
//.path = "/dev/null",
.name = "test",
.blob = "<This ornull>",
};
 
const nullpage = Page(t2, @TypeOf(ctx)).init(ctx);
const p2 = try allocPrint(a, "{}", .{nullpage});
defer a.free(p2);
try std.testing.expectEqualStrings("", p2);
}
 
test "directive For 0..n" {}
 
test "directive For" {
var a = std.testing.allocator;
 
const blob =
\\<div><For Loop><span><Name></span></For></div>
;
 
const expected: []const u8 =
\\<div><span>not that</span></div>
;
 
const dbl_expected: []const u8 =
\\<div><span>first</span><span>second</span></div>
;
 
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = blob,
};
 
var ctx: struct { loop: []const struct { name: []const u8 } } = .{
.loop = &.{
.{ .name = "not that" },
},
};
 
const pg = Page(t, @TypeOf(ctx)).init(ctx);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
 
ctx = .{
.loop = &.{
.{ .name = "first" },
.{ .name = "second" },
},
};
 
const dbl_page = Page(t, @TypeOf(ctx)).init(ctx);
const pg2 = try allocPrint(a, "{}", .{dbl_page});
defer a.free(pg2);
try std.testing.expectEqualStrings(dbl_expected, pg2);
}
 
test "directive For & For" {
var a = std.testing.allocator;
 
const blob =
\\<div>
\\ <For Loop>
\\ <span><Name></span>
\\ <For Numbers>
\\ <Number>
\\ </For>
\\ </For>
\\</div>
;
 
const expected: []const u8 =
\\<div>
\\ <span>Alice</span>
\\ A0
\\ A1
\\ A2
++ "\n \n" ++
\\ <span>Bob</span>
\\ B0
\\ B1
\\ B2
++ "\n \n \n" ++
\\</div>
;
 
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = blob,
};
 
const ctx: struct {
loop: []const struct {
name: []const u8,
numbers: []const struct {
number: []const u8,
},
},
} = .{
.loop = &.{
.{
.name = "Alice",
.numbers = &.{
.{ .number = "A0" },
.{ .number = "A1" },
.{ .number = "A2" },
},
},
.{
.name = "Bob",
.numbers = &.{
.{ .number = "B0" },
.{ .number = "B1" },
.{ .number = "B2" },
},
},
},
};
 
const pg = Page(t, @TypeOf(ctx)).init(ctx);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
test "directive for then for" {
var a = std.testing.allocator;
 
const blob =
\\<div>
\\ <For Loop>
\\ <span><Name></span>
\\ </For>
\\ <For Numbers>
\\ <Number>
\\ </For>
\\</div>
;
 
const expected: []const u8 =
\\<div>
\\ <span>Alice</span>
\\ <span>Bob</span>
++ "\n \n" ++
\\ A0
\\ A1
\\ A2
++ "\n \n" ++
\\</div>
;
 
const FTF = struct {
const Loop = struct {
name: []const u8,
};
const Numbers = struct {
number: []const u8,
};
 
loop: []const Loop,
numbers: []const Numbers,
};
 
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = blob,
};
const page = Page(t, FTF);
 
const loop = [2]FTF.Loop{
.{ .name = "Alice" },
.{ .name = "Bob" },
};
const numbers = [3]FTF.Numbers{
.{ .number = "A0" },
.{ .number = "A1" },
.{ .number = "A2" },
};
const pg = page.init(.{
.loop = loop[0..],
.numbers = numbers[0..],
});
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
test "directive With" {
const a = std.testing.allocator;
 
const blob =
\\<div>
\\ <With Thing>
\\ <span><Thing></span>
\\ </With>
\\</div>
;
 
const expected_empty: []const u8 =
\\<div>
++ "\n \n" ++
\\</div>
;
// trailing spaces expected and required
try std.testing.expect(std.mem.count(u8, expected_empty, " \n") == 1);
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = blob,
};
 
var ctx: struct {
thing: ?struct {
thing: []const u8,
},
} = .{
.thing = null,
};
 
const page = Page(t, @TypeOf(ctx));
const pg = page.init(ctx);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected_empty, p);
 
ctx = .{
.thing = .{ .thing = "THING" },
};
 
const expected_thing: []const u8 =
\\<div>
\\ <span>THING</span>
\\</div>
;
 
const pg2 = page.init(ctx);
const p2 = try allocPrint(a, "{}", .{pg2});
defer a.free(p2);
try std.testing.expectEqualStrings(expected_thing, p2);
}
 
test "directive Split" {
var a = std.testing.allocator;
 
const blob =
\\<div>
\\ <Split Slice />
\\</div>
\\
;
 
const expected: []const u8 =
\\<div>
\\ Alice
\\Bob
\\Charlie
\\Eve
++ "\n\n" ++
\\</div>
\\
;
 
const FE = struct {
slice: []const []const u8,
};
 
const t = Template{
//.path = "/dev/null",
.name = "test",
.blob = blob,
};
const page = Page(t, FE);
 
const slice = FE{
.slice = &[_][]const u8{
"Alice",
"Bob",
"Charlie",
"Eve",
},
};
const pg = page.init(slice);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
test "directive Build" {
var a = std.testing.allocator;
 
const blob =
\\<Build Name _template.html />
;
 
const expected: []const u8 =
\\<div>
\\AliceBobCharlieEve
\\</div>
;
 
const FE = struct {
const This = struct {
this: []const u8,
};
name: struct {
slice: []const This,
},
};
 
const t = Template{
.name = "test",
.blob = blob,
};
const page = Page(t, FE);
 
dynamic = &[1]Template{
.{
.name = "_template.html",
.blob = "<div>\n<For Slice><This></For>\n</div>",
},
};
 
const slice = FE{
.name = .{
.slice = &[4]FE.This{
.{ .this = "Alice" },
.{ .this = "Bob" },
.{ .this = "Charlie" },
.{ .this = "Eve" },
},
},
};
const pg = page.init(slice);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
test "directive typed usize" {
var a = std.testing.allocator;
const blob = "<Number type=\"usize\" />";
const expected: []const u8 = "420";
 
const FE = struct { number: usize };
 
const t = Template{ .name = "test", .blob = blob };
const page = Page(t, FE);
 
const slice = FE{ .number = 420 };
const pg = page.init(slice);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
test "directive typed ?usize" {
var a = std.testing.allocator;
const blob = "<Number type=\"?usize\" />";
const expected: []const u8 = "420";
 
const FE = struct { number: ?usize };
 
const t = Template{ .name = "test", .blob = blob };
const page = Page(t, FE);
 
const slice = FE{ .number = 420 };
const pg = page.init(slice);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
test "directive typed ?usize null" {
var a = std.testing.allocator;
const blob = "<Number type=\"?usize\" />";
const expected: []const u8 = "";
 
const FE = struct { number: ?usize };
 
const t = Template{ .name = "test", .blob = blob };
const page = Page(t, FE);
 
const slice = FE{ .number = null };
const pg = page.init(slice);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
test "directive typed isize" {
var a = std.testing.allocator;
const blob = "<Number type=\"isize\" />";
const expected: []const u8 = "-420";
 
const FE = struct { number: isize };
 
const t = Template{ .name = "test", .blob = blob };
const page = Page(t, FE);
 
const slice = FE{ .number = -420 };
const pg = page.init(slice);
const p = try allocPrint(a, "{}", .{pg});
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,18 +0,0 @@
const Found = @import("config");
 
pub const FileData = struct {
path: []const u8,
blob: []const u8,
};
 
pub const data: [Found.names.len]FileData = blk: {
@setEvalBranchQuota(5000);
var t: [Found.names.len]FileData = undefined;
for (Found.names, &t) |file, *dst| {
dst.* = FileData{
.path = file,
.blob = @embedFile(file),
};
}
break :blk t;
};
 
ev/null added: 206, removed: 4403, total 0
@@ -1,281 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_mode = @import("builtin").mode;
 
// TODO rename this to... uhhh Map maybe?
pub const DataMap = @This();
 
pub const Pair = struct {
name: []const u8,
value: []const u8,
};
 
pub const Data = union(enum) {
slice: []const u8,
block: []DataMap,
reader: std.io.AnyReader,
};
 
pub const HashMap = std.StringHashMap(Data);
 
ctx: HashMap,
 
pub fn Builder(comptime T: type) type {
return struct {
from: T,
 
pub const Self = @This();
 
pub fn init(from: T) Self {
return .{
.from = from,
};
}
 
pub fn buildUnsanitized(self: Self, a: Allocator, ctx: *DataMap) !void {
if (comptime @import("builtin").zig_version.minor >= 12) {
if (std.meta.hasMethod(T, "contextBuilderUnsanitized")) {
return self.from.contextBuilderUnsanitized(a, ctx);
}
}
 
inline for (std.meta.fields(T)) |field| {
if (field.type == []const u8) {
try ctx.put(field.name, @field(self.from, field.name));
}
}
}
 
pub fn build(self: Self, a: Allocator, ctx: *DataMap) !void {
if (comptime @import("builtin").zig_version.minor >= 12) {
if (std.meta.hasMethod(T, "contextBuilder")) {
return self.from.contextBuilder(a, ctx);
}
}
 
return self.buildUnsanitized(a, ctx);
}
};
}
 
pub fn init(a: Allocator) DataMap {
return DataMap{
.ctx = HashMap.init(a),
};
}
 
pub fn initWith(a: Allocator, data: []const Pair) !DataMap {
var ctx = DataMap.init(a);
for (data) |d| {
ctx.putSlice(d.name, d.value) catch |err| switch (err) {
error.OutOfMemory => return err,
else => unreachable,
};
}
return ctx;
}
 
pub fn initBuildable(a: Allocator, buildable: anytype) !DataMap {
var ctx = DataMap.init(a);
const builder = buildable.builder();
try builder.build(a, &ctx);
return ctx;
}
 
pub fn raze(self: *DataMap) void {
var itr = self.ctx.iterator();
while (itr.next()) |*n| {
switch (n.value_ptr.*) {
.slice, .reader => continue,
.block => |*block| for (block.*) |*b| b.raze(),
}
}
self.ctx.deinit();
}
 
pub fn put(self: *DataMap, name: []const u8, value: Data) !void {
try self.ctx.put(name, value);
}
 
pub fn get(self: DataMap, name: []const u8) ?Data {
return self.ctx.get(name);
}
 
pub fn putSlice(self: *DataMap, name: []const u8, value: []const u8) !void {
if (comptime build_mode == .Debug)
if (!std.ascii.isUpper(name[0]))
std.debug.print("Warning Template can't resolve {s}\n", .{name});
try self.ctx.put(name, .{ .slice = value });
}
 
pub fn getSlice(self: DataMap, name: []const u8) ?[]const u8 {
return switch (self.getNext(name) orelse return null) {
.slice => |s| s,
.block => unreachable,
.reader => unreachable,
};
}
 
/// Memory of block is managed by the caller. Calling raze will not free the
/// memory from within.
pub fn putBlock(self: *DataMap, name: []const u8, block: []DataMap) !void {
try self.ctx.put(name, .{ .block = block });
}
 
pub fn getBlock(self: DataMap, name: []const u8) !?[]const DataMap {
return switch (self.ctx.get(name) orelse return null) {
// I'm sure this hack will live forever, I'm abusing With to be
// an IF here, without actually implementing IF... sorry!
//std.debug.print("Error: get [{s}] required Block, found slice\n", .{name});
.slice, .reader => return error.NotABlock,
.block => |b| b,
};
}
 
pub fn putReader(self: *DataMap, name: []const u8, value: []const u8) !void {
try self.putSlice(name, value);
}
 
pub fn getReader(self: DataMap, name: []const u8) ?std.io.AnyReader {
switch (self.ctx.get(name) orelse return null) {
.slice, .block => return error.NotAReader,
.reader => |r| return r,
}
comptime unreachable;
}
 
test "directive For" {
var a = std.testing.allocator;
 
const blob =
\\<div><For Loop><span><Name></span></For></div>
;
 
const expected: []const u8 =
\\<div><span>not that</span></div>
;
 
const dbl_expected: []const u8 =
\\<div><span>first</span><span>second</span></div>
;
 
var t = Template{
//.path = "/dev/null",
.name = "test",
.blob = blob,
};
 
var ctx = DataMap.init(a);
defer ctx.raze();
var blocks: [1]DataMap = [1]DataMap{
DataMap.init(a),
};
try blocks[0].putSlice("Name", "not that");
// We have to raze because it will be over written
defer blocks[0].raze();
try ctx.putBlock("Loop", &blocks);
 
const p = try t.page(ctx).build(a);
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
 
// many
var many_blocks: [2]DataMap = [_]DataMap{
DataMap.init(a),
DataMap.init(a),
};
// what... 2 is many
 
try many_blocks[0].putSlice("Name", "first");
try many_blocks[1].putSlice("Name", "second");
 
try ctx.putBlock("Loop", &many_blocks);
 
const dbl_page = try t.page(ctx).build(a);
defer a.free(dbl_page);
try std.testing.expectEqualStrings(dbl_expected, dbl_page);
 
//many_blocks[0].raze();
//many_blocks[1].raze();
}
 
test "directive For & For" {
var a = std.testing.allocator;
 
const blob =
\\<div>
\\ <For Loop>
\\ <span><Name></span>
\\ <For Numbers>
\\ <Number>
\\ </For>
\\ </For>
\\</div>
;
 
const expected: []const u8 =
\\<div>
\\ <span>Alice</span>
\\ A0
\\ A1
\\ A2
++ "\n \n" ++
\\ <span>Bob</span>
\\ B0
\\ B1
\\ B2
++ "\n \n \n" ++
\\</div>
;
 
var t = Template{
//.path = "/dev/null",
.name = "test",
.blob = blob,
};
 
var ctx = DataMap.init(a);
defer ctx.raze();
var outer = [2]DataMap{
DataMap.init(a),
DataMap.init(a),
};
 
try outer[0].putSlice("Name", "Alice");
//defer outer[0].raze();
 
var arena = std.heap.ArenaAllocator.init(a);
defer arena.deinit();
const aa = arena.allocator();
 
const lput = "Number";
 
var alice_inner: [3]DataMap = undefined;
try outer[0].putBlock("Numbers", &alice_inner);
for (0..3) |i| {
alice_inner[i] = DataMap.init(a);
try alice_inner[i].putSlice(
lput,
try std.fmt.allocPrint(aa, "A{}", .{i}),
);
}
 
try outer[1].putSlice("Name", "Bob");
//defer outer[1].raze();
 
var bob_inner: [3]DataMap = undefined;
try outer[1].putBlock("Numbers", &bob_inner);
for (0..3) |i| {
bob_inner[i] = DataMap.init(a);
try bob_inner[i].putSlice(
lput,
try std.fmt.allocPrint(aa, "B{}", .{i}),
);
}
 
try ctx.putBlock("Loop", &outer);
 
const p = try t.page(ctx).build(a);
defer a.free(p);
try std.testing.expectEqualStrings(expected, p);
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,509 +0,0 @@
const std = @import("std");
const eql = std.mem.eql;
const startsWith = std.mem.startsWith;
const indexOf = std.mem.indexOf;
const indexOfPos = std.mem.indexOfPos;
const indexOfAnyPos = std.mem.indexOfAnyPos;
const indexOfScalar = std.mem.indexOfScalar;
const indexOfScalarPos = std.mem.indexOfScalarPos;
const isUpper = std.ascii.isUpper;
const count = std.mem.count;
const isWhitespace = std.ascii.isWhitespace;
const trim = std.mem.trim;
const trimLeft = std.mem.trimLeft;
const whitespace = std.ascii.whitespace[0..];
 
pub const Directive = @This();
 
const PageRuntime = @import("page.zig").PageRuntime;
 
const Template = @import("../template.zig");
 
const dynamic = &Template.dynamic;
const builtin = Template.builtin;
const makeFieldName = Template.makeFieldName;
 
verb: Verb,
noun: []const u8,
otherwise: Otherwise,
known_type: ?KnownType = null,
tag_block: []const u8,
 
pub const Otherwise = union(enum) {
required: void,
ignore: void,
delete: void,
default: []const u8,
template: *const Template.Template,
blob: []const u8,
};
 
pub const Verb = enum {
variable,
foreach,
split,
with,
build,
};
 
pub const KnownType = enum {
usize,
isize,
@"?usize",
 
pub fn nullable(kt: KnownType) bool {
return switch (kt) {
.usize, .isize => false,
.@"?usize" => true,
};
}
};
 
const Positions = struct {
start: usize,
start_ws: usize,
end: usize,
end_ws: usize,
width: usize,
};
 
pub fn init(str: []const u8) ?Directive {
if (str.len < 2) return null;
if (!isUpper(str[1]) and str[1] != '_') return null;
const end = findTag(str) catch return null;
const tag = str[0..end];
const verb = tag[1 .. indexOfAnyPos(u8, tag, 1, " /") orelse tag.len - 1];
 
if (verb.len == tag.len - 2) {
if (initNoun(verb, tag)) |noun| {
return noun;
} else unreachable;
}
 
const noun = tag[verb.len + 1 .. tag.len - 1];
if (initVerb(verb, noun, str)) |kind| {
return kind;
}
 
if (initNoun(verb, tag)) |kind| {
return kind;
}
return null;
}
 
fn initNoun(noun: []const u8, tag: []const u8) ?Directive {
//std.debug.print("init noun {s}\n", .{noun});
if (noun[0] == '_') if (getBuiltin(noun)) |bi| {
return Directive{
.noun = noun,
.verb = .variable,
.otherwise = .{ .template = bi },
.tag_block = tag,
};
};
 
var default_str: ?[]const u8 = null;
var knownt: ?KnownType = null;
var rem_attr = tag[noun.len + 1 .. tag.len - 1];
while (indexOfScalar(u8, rem_attr, '=') != null) {
if (findAttribute(rem_attr)) |attr| {
if (eql(u8, attr.name, "type")) {
inline for (std.meta.fields(KnownType)) |kt| {
if (eql(u8, attr.value, kt.name)) {
knownt = @enumFromInt(kt.value);
break;
}
} else {
std.debug.print("Unable to resolve requested type '{s}'\n", .{attr.value});
unreachable;
}
} else if (eql(u8, attr.name, "default")) {
default_str = attr.value;
}
rem_attr = rem_attr[attr.len..];
} else |err| switch (err) {
error.AttrInvalid => break,
else => unreachable,
}
}
 
return Directive{
.verb = .variable,
.noun = noun,
.otherwise = if (default_str) |str|
.{ .default = str }
else if (indexOf(u8, tag, " ornull")) |_|
.delete
else if (knownt) |kn|
if (kn.nullable())
.delete
else
.required
else
.required,
.known_type = knownt,
.tag_block = tag,
};
}
 
pub fn initVerb(verb: []const u8, noun: []const u8, blob: []const u8) ?Directive {
var otherw: struct { Directive.Otherwise, usize } = undefined;
var word: Verb = undefined;
if (eql(u8, verb, "For")) {
otherw = calcBody("For", noun, blob) orelse return null;
word = .foreach;
} else if (eql(u8, verb, "Split")) {
otherw = calcBody("Split", noun, blob) orelse return null;
word = .split;
} else if (eql(u8, verb, "With")) {
otherw = calcBody("With", noun, blob) orelse return null;
word = .with;
} else if (eql(u8, verb, "With")) {
otherw = calcBody("With", noun, blob) orelse return null;
word = .with;
} else if (eql(u8, verb, "Build")) {
const b_noun = noun[1..(indexOfScalarPos(u8, noun, 1, ' ') orelse return null)];
const tail = noun[b_noun.len + 1 ..];
const b_html = tail[1..(indexOfScalarPos(u8, tail, 2, ' ') orelse return null)];
if (getBuiltin(b_html)) |bi| {
return Directive{
.verb = .build,
.noun = b_noun,
.otherwise = .{ .template = bi },
.tag_block = blob[0 .. verb.len + 2 + noun.len],
};
} else if (getDynamic(b_html)) |bi| {
return Directive{
.verb = .build,
.noun = b_noun,
.otherwise = .{ .template = bi },
.tag_block = blob[0 .. verb.len + 2 + noun.len],
};
} else return null;
} else return null;
 
// TODO convert to while
//inline for (Word) |tag_name| {
// if (eql(u8, noun, @tagName(tag_name))) {
// pos = calcPos(@tagName(tag_name), blob, verb) orelse return null;
// word = tag_name;
// break;
// }
//} else return null;
 
var end = (indexOf(u8, noun, ">") orelse noun.len);
if (noun[end - 1] == '/') end -= 1;
return .{
.verb = word,
.noun = noun[1..end],
.otherwise = otherw[0],
.tag_block = blob[0..otherw[1]],
};
}
 
fn findTag(blob: []const u8) !usize {
return 1 + (indexOf(u8, blob, ">") orelse return error.TagInvalid);
}
 
const TAttr = struct {
name: []const u8,
value: []const u8,
len: usize,
};
 
fn findAttribute(tag: []const u8) !TAttr {
const equi = indexOfScalar(u8, tag, '=') orelse return error.AttrInvalid;
const name = trim(u8, tag[0..equi], whitespace);
var value = trim(u8, tag[equi + 1 ..], whitespace);
 
var end: usize = equi + 1;
while (end < tag.len and isWhitespace(tag[end])) end += 1;
while (end < tag.len) {
// TODO rewrite with tagged switch syntax
switch (tag[end]) {
'\n', '\r', '\t', ' ' => end += 1,
'\'', '"' => |qut| {
end += 1;
while (end <= tag.len and tag[end] != qut) end += 1;
if (end == tag.len) return error.AttrInvalid;
if (tag[end] != qut) return error.AttrInvalid else end += 1;
value = trim(u8, tag[equi + 1 .. end], whitespace.* ++ &[_]u8{ qut, '=', '<', '>', '/' });
break;
},
else => {
while (end < tag.len and !isWhitespace(tag[end])) end += 1;
},
}
}
return .{
.name = name,
.value = value,
.len = end,
};
}
 
test findAttribute {
var attr = try findAttribute("type=\"usize\"");
try std.testing.expectEqualDeep(TAttr{ .name = "type", .value = "usize", .len = 12 }, attr);
attr = try findAttribute("type=\"isize\"");
try std.testing.expectEqualDeep(TAttr{ .name = "type", .value = "isize", .len = 12 }, attr);
attr = try findAttribute("type=\"?usize\"");
try std.testing.expectEqualDeep(TAttr{ .name = "type", .value = "?usize", .len = 13 }, attr);
attr = try findAttribute("default=\"text\"");
try std.testing.expectEqualDeep(TAttr{ .name = "default", .value = "text", .len = 14 }, attr);
attr = try findAttribute("default=\"text\" />");
try std.testing.expectEqualDeep(TAttr{ .name = "default", .value = "text", .len = 14 }, attr);
}
 
fn validChar(c: u8) bool {
return switch (c) {
'A'...'Z', 'a'...'z' => true,
'-', '_', '.', ':' => true,
else => false,
};
}
 
fn calcBodyS(comptime _: []const u8, _: []const u8, blob: []const u8, end: usize) ?struct { Otherwise, usize } {
if (blob.len <= end) return null;
return .{
.{ .ignore = {} },
end + 1,
};
}
 
fn calcBody(comptime keyword: []const u8, noun: []const u8, blob: []const u8) ?struct { Otherwise, usize } {
const open: *const [keyword.len + 2]u8 = "<" ++ keyword ++ " ";
const close: *const [keyword.len + 3]u8 = "</" ++ keyword ++ ">";
 
if (!startsWith(u8, blob, open)) @panic("error compiling template");
var shape_i: usize = open.len;
while (shape_i < blob.len and blob[shape_i] != '/' and blob[shape_i] != '>')
shape_i += 1;
switch (blob[shape_i]) {
'/' => return calcBodyS(keyword, noun, blob, shape_i + 1),
'>' => {},
else => return null,
}
 
var start = 1 + (indexOf(u8, blob, ">") orelse return null);
var close_pos: usize = indexOfPos(u8, blob, 0, close) orelse return null;
var skip = count(u8, blob[start..close_pos], open);
while (skip > 0) : (skip -= 1) {
close_pos = indexOfPos(u8, blob, close_pos + 1, close) orelse close_pos;
}
 
const end = close_pos + close.len;
const end_ws = end - close.len;
const start_ws = start;
while (start < end and isWhitespace(blob[start])) : (start +|= 1) {}
 
//while (endws > start and isWhitespace(blob[endws])) : (endws -|= 1) {}
//endws += 1;
 
var width: usize = 1;
while (width < noun.len and validChar(noun[width])) {
width += 1;
}
return .{
.{ .blob = blob[start_ws..end_ws] },
end,
};
}
 
fn isStringish(t: type) bool {
return switch (t) {
[]const u8, ?[]const u8 => true,
else => false,
};
}
 
pub fn doTyped(self: Directive, T: type, ctx: anytype, out: anytype) anyerror!void {
//@compileLog(T);
var local: [0xff]u8 = undefined;
const realname = local[0..makeFieldName(self.noun, &local)];
switch (@typeInfo(T)) {
.Struct => {
inline for (std.meta.fields(T)) |field| {
if (comptime isStringish(field.type)) continue;
switch (@typeInfo(field.type)) {
.Pointer => {
if (eql(u8, field.name, realname)) {
const child = @field(ctx, field.name);
for (child) |each| {
switch (field.type) {
[]const []const u8 => {
std.debug.assert(self.verb == .split);
try out.writeAll(each);
try out.writeAll("\n");
//try out.writeAll( self.otherwise.blob.whitespace);
},
else => {
std.debug.assert(self.verb == .foreach);
try self.forEachTyped(@TypeOf(each), each, out);
},
}
}
}
},
.Optional => {
if (eql(u8, field.name, realname)) {
//@compileLog("optional for {s}\n", field.name, field.type, T);
const child = @field(ctx, field.name);
if (child) |exists| {
if (self.verb == .with)
try self.withTyped(@TypeOf(exists), exists, out)
else
try self.doTyped(@TypeOf(exists), exists, out);
}
}
},
.Struct => {
if (eql(u8, field.name, realname)) {
const child = @field(ctx, field.name);
std.debug.assert(self.verb == .build);
try self.withTyped(@TypeOf(child), child, out);
}
},
.Int => |int| {
if (eql(u8, field.name, realname)) {
std.debug.assert(int.bits == 64);
try std.fmt.formatInt(@field(ctx, field.name), 10, .lower, .{}, out);
}
},
else => comptime unreachable,
}
}
},
.Int => {
//std.debug.assert(int.bits == 64);
try std.fmt.formatInt(ctx, 10, .lower, .{}, out);
},
else => comptime unreachable,
}
}
 
pub fn forEachTyped(self: Directive, T: type, data: T, out: anytype) anyerror!void {
var p = PageRuntime(T){
.data = data,
.template = .{
.name = self.noun,
.blob = trimLeft(u8, self.otherwise.blob, whitespace),
},
};
try p.format("", .{}, out);
}
 
pub fn withTyped(self: Directive, T: type, block: T, out: anytype) anyerror!void {
var p = PageRuntime(T){
.data = block,
.template = if (self.otherwise == .template) self.otherwise.template.* else .{
.name = self.noun,
.blob = trim(u8, self.otherwise.blob, whitespace),
},
};
try p.format("", .{}, out);
}
 
fn getDynamic(name: []const u8) ?*const Template.Template {
for (0..dynamic.*.len) |i| {
if (eql(u8, dynamic.*[i].name, name)) {
return &dynamic.*[i];
}
}
return null;
}
 
fn getBuiltin(name: []const u8) ?*const Template.Template {
for (0..builtin.len) |i| {
if (eql(u8, builtin[i].name, name)) {
return &builtin[i];
}
}
return null;
}
 
fn typeField(T: type, name: []const u8, data: T) ?[]const u8 {
if (@typeInfo(T) != .Struct) return null;
var local: [0xff]u8 = undefined;
const realname = local[0..makeFieldName(name, &local)];
inline for (std.meta.fields(T)) |field| {
if (eql(u8, field.name, realname)) {
switch (field.type) {
[]const u8,
?[]const u8,
=> return @field(data, field.name),
 
else => return null,
}
}
}
return null;
}
 
pub fn format(d: Directive, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
_ = d;
_ = out;
unreachable;
}
 
pub fn formatTyped(d: Directive, comptime T: type, ctx: T, out: anytype) !void {
switch (d.verb) {
.variable => {
if (d.known_type) |_| return d.doTyped(T, ctx, out);
const noun = d.noun;
const var_name = typeField(T, noun, ctx);
if (var_name) |data_blob| {
try out.writeAll(data_blob);
} else {
//if (DEBUG) std.debug.print("[missing var {s}]\n", .{noun.vari});
switch (d.otherwise) {
.default => |str| try out.writeAll(str),
// Not really an error, just instruct caller to print original text
.ignore => return error.IgnoreDirective,
.required => return error.VariableMissing,
.delete => {},
.template => |template| {
if (T == usize) unreachable;
if (@typeInfo(T) != .Struct) unreachable;
inline for (std.meta.fields(T)) |field| {
switch (@typeInfo(field.type)) {
.Optional => |otype| {
if (otype.child == []const u8) continue;
 
var local: [0xff]u8 = undefined;
const realname = local[0..makeFieldName(noun[1 .. noun.len - 5], &local)];
if (std.mem.eql(u8, field.name, realname)) {
if (@field(ctx, field.name)) |subdata| {
var subpage = template.pageOf(otype.child, subdata);
try subpage.format("{}", .{}, out);
} else std.debug.print(
"sub template data was null for {s}\n",
.{field.name},
);
}
},
.Struct => {
if (std.mem.eql(u8, field.name, noun)) {
const subdata = @field(ctx, field.name);
var subpage = template.pageOf(@TypeOf(subdata), subdata);
try subpage.format("{}", .{}, out);
}
},
else => {}, //@compileLog(field.type),
}
}
},
//inline for (std.meta.fields(T)) |field| {
// if (eql(u8, field.name, noun)) {
// const subdata = @field(ctx, field.name);
// var page = template.pageOf(@TypeOf(subdata), subdata);
// try page.format("{}", .{}, out);
// }
//}
.blob => unreachable,
}
}
},
else => d.doTyped(T, ctx, out) catch unreachable,
}
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,105 +0,0 @@
const std = @import("std");
const is_test = @import("builtin").is_test;
const Allocator = std.mem.Allocator;
const AnyWriter = std.io.AnyWriter;
const eql = std.mem.eql;
const indexOfScalar = std.mem.indexOfScalar;
 
const Templates = @import("../template.zig");
const Template = Templates.Template;
const Directive = Templates.Directive;
 
pub fn PageRuntime(comptime PageDataType: type) type {
return struct {
pub const Self = @This();
pub const Kind = PageDataType;
template: Template,
data: PageDataType,
 
pub fn init(t: Template, d: PageDataType) PageRuntime(PageDataType) {
return .{
.template = t,
.data = d,
};
}
 
pub fn format(self: Self, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
//var ctx = self.data;
var blob = self.template.blob;
while (blob.len > 0) {
if (indexOfScalar(u8, blob, '<')) |offset| {
try out.writeAll(blob[0..offset]);
blob = blob[offset..];
if (Directive.init(blob)) |drct| {
const end = drct.tag_block.len;
drct.formatTyped(PageDataType, self.data, out) catch |err| switch (err) {
error.IgnoreDirective => try out.writeAll(blob[0..end]),
error.VariableMissing => {
if (!is_test) std.debug.print("Template Error, variable missing {{{s}}}\n", .{blob[0..end]});
try out.writeAll(blob[0..end]);
},
else => return err,
};
 
blob = blob[end..];
} else {
if (std.mem.indexOfPos(u8, blob, 1, "<")) |next| {
try out.writeAll(blob[0..next]);
blob = blob[next..];
} else {
return try out.writeAll(blob);
}
}
continue;
}
return try out.writeAll(blob);
}
}
};
}
 
pub fn Page(comptime template: Template, comptime PageDataType: type) type {
return struct {
pub const Self = @This();
pub const Kind = PageDataType;
pub const PageTemplate = template;
data: PageDataType,
 
pub fn init(d: PageDataType) Page(template, PageDataType) {
return .{ .data = d };
}
 
pub fn format(self: Self, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
var blob = Self.PageTemplate.blob;
while (blob.len > 0) {
if (indexOfScalar(u8, blob, '<')) |offset| {
try out.writeAll(blob[0..offset]);
blob = blob[offset..];
 
if (Directive.init(blob)) |drct| {
const end = drct.tag_block.len;
drct.formatTyped(PageDataType, self.data, out) catch |err| switch (err) {
error.IgnoreDirective => try out.writeAll(blob[0..end]),
error.VariableMissing => {
if (!is_test) std.debug.print("Template Error, variable missing {{{s}}}\n", .{blob[0..end]});
try out.writeAll(blob[0..end]);
},
else => return err,
};
 
blob = blob[end..];
} else {
if (std.mem.indexOfPos(u8, blob, 1, "<")) |next| {
try out.writeAll(blob[0..next]);
blob = blob[next..];
} else {
return try out.writeAll(blob);
}
}
continue;
}
return try out.writeAll(blob);
}
}
};
}
 
src/types/delta.zig added: 206, removed: 4403, total 0
@@ -8,7 +8,6 @@ const Bleach = @import("../bleach.zig");
const Types = @import("../types.zig");
const Thread = Types.Thread;
const Message = Thread.Message;
const Template = @import("../template.zig");
 
pub const Delta = @This();
 
 
ev/null added: 206, removed: 4403, total 0
@@ -1,91 +0,0 @@
pub const std = @import("std");
const Allocator = std.mem.Allocator;
const splitScalar = std.mem.splitScalar;
 
const zWSGI = @import("zwsgi.zig");
const Auth = @import("auth.zig");
 
const zWSGIRequest = zWSGI.zWSGIRequest;
 
pub const Request = @import("request.zig");
pub const Response = @import("response.zig");
pub const RequestData = @import("request_data.zig");
pub const Template = @import("template.zig");
pub const Routes = @import("routes.zig");
pub const UriIter = Routes.UriIter;
const Config = @import("ini.zig").Config;
 
const Error = @import("errors.zig").Error;
 
pub const Verse = @This();
 
alloc: Allocator,
request: Request,
response: Response,
reqdata: RequestData,
uri: UriIter,
cfg: ?Config,
 
// TODO fix this unstable API
auth: Auth,
route_ctx: ?*const anyopaque = null,
 
const VarPair = struct {
[]const u8,
[]const u8,
};
 
pub fn init(a: Allocator, cfg: ?Config, req: Request, res: Response, reqdata: RequestData) !Verse {
std.debug.assert(req.uri[0] == '/');
//const reqheader = req.headers
return .{
.alloc = a,
.request = req,
.response = res,
.reqdata = reqdata,
.uri = splitScalar(u8, req.uri[1..], '/'),
.cfg = cfg,
.auth = Auth.init(req.headers),
};
}
 
pub fn sendPage(ctx: *Verse, page: anytype) Error!void {
ctx.response.start() catch |err| switch (err) {
error.BrokenPipe => return error.NetworkCrash,
else => unreachable,
};
const loggedin = if (ctx.request.auth.valid()) "<a href=\"#\">Logged In</a>" else "Public";
const T = @TypeOf(page.*);
if (@hasField(T, "data") and @hasField(@TypeOf(page.data), "body_header")) {
page.data.body_header.?.nav.?.nav_auth = loggedin;
}
 
const writer = ctx.response.writer();
page.format("{}", .{}, writer) catch |err| switch (err) {
else => std.debug.print("Page Build Error {}\n", .{err}),
};
}
 
pub fn sendRawSlice(ctx: *Verse, slice: []const u8) Error!void {
ctx.response.send(slice) catch unreachable;
}
 
pub fn sendError(ctx: *Verse, comptime code: std.http.Status) Error!void {
return Routes.defaultResponse(code)(ctx);
}
 
pub fn sendJSON(ctx: *Verse, json: anytype) Error!void {
ctx.response.start() catch |err| switch (err) {
error.BrokenPipe => return error.NetworkCrash,
else => unreachable,
};
 
const data = std.json.stringifyAlloc(ctx.alloc, json, .{
.emit_null_optional_fields = false,
}) catch |err| {
std.debug.print("Error trying to print json {}\n", .{err});
return error.Unknown;
};
ctx.response.writeAll(data) catch unreachable;
ctx.response.finish() catch unreachable;
}
 
ev/null added: 206, removed: 4403, total 0
@@ -1,394 +0,0 @@
const std = @import("std");
 
const Allocator = std.mem.Allocator;
const Server = std.net.Server;
 
const Verse = @import("verse.zig");
const Request = @import("request.zig");
const Response = @import("response.zig");
const Router = @import("routes.zig");
const RequestData = @import("request_data.zig");
const Config = @import("ini.zig").Config;
 
const ZWSGI = @This();
 
alloc: Allocator,
config: Config,
routefn: RouterFn,
buildfn: BuildFn,
runmode: RunMode = .unix,
 
pub const RouterFn = *const fn (*Verse) Router.Callable;
// TODO provide default for this?
pub const BuildFn = *const fn (*Verse, Router.Callable) Router.Error!void;
 
const uProtoHeader = packed struct {
mod1: u8 = 0,
size: u16 = 0,
mod2: u8 = 0,
};
 
const uWSGIVar = struct {
key: []const u8,
val: []const u8,
 
pub fn read(_: []u8) uWSGIVar {
return uWSGIVar{ .key = "", .val = "" };
}
 
pub fn format(self: uWSGIVar, comptime _: []const u8, _: std.fmt.FormatOptions, out: anytype) !void {
try std.fmt.format(out, "\"{s}\" = \"{s}\"", .{
self.key,
if (self.val.len > 0) self.val else "[Empty]",
});
}
};
 
pub const zWSGIRequest = struct {
header: uProtoHeader,
acpt: Server.Connection,
vars: []uWSGIVar,
body: ?[]u8 = null,
};
 
pub fn init(a: Allocator, config: Config, route_fn: RouterFn, build_fn: BuildFn) ZWSGI {
return .{
.alloc = a,
.config = config,
.routefn = route_fn,
.buildfn = build_fn,
};
}
 
const HOST = "127.0.0.1";
const PORT = 2000;
const FILE = "./srctree.sock";
 
fn serveUnix(zwsgi: *ZWSGI) !void {
var cwd = std.fs.cwd();
if (cwd.access(FILE, .{})) {
try cwd.deleteFile(FILE);
} else |_| {}
 
const uaddr = try std.net.Address.initUnix(FILE);
var server = try uaddr.listen(.{});
defer server.deinit();
 
const path = try std.fs.cwd().realpathAlloc(zwsgi.alloc, FILE);
defer zwsgi.alloc.free(path);
const zpath = try zwsgi.alloc.dupeZ(u8, path);
defer zwsgi.alloc.free(zpath);
const mode = std.os.linux.chmod(zpath, 0o777);
if (false) std.debug.print("mode {o}\n", .{mode});
std.debug.print("Unix server listening\n", .{});
 
while (true) {
var acpt = try server.accept();
defer acpt.stream.close();
var timer = try std.time.Timer.start();
 
var arena = std.heap.ArenaAllocator.init(zwsgi.alloc);
defer arena.deinit();
const a = arena.allocator();
 
var ctx = try zwsgi.buildVerseuWSGI(a, &acpt);
 
defer {
std.log.err("zWSGI: [{d:.3}] {s} - {s}: {s} -- \"{s}\"", .{
@as(f64, @floatFromInt(timer.lap())) / 1000000.0,
findOr(ctx.request.raw_request.zwsgi.vars, "REMOTE_ADDR"),
findOr(ctx.request.raw_request.zwsgi.vars, "REQUEST_METHOD"),
findOr(ctx.request.raw_request.zwsgi.vars, "REQUEST_URI"),
findOr(ctx.request.raw_request.zwsgi.vars, "HTTP_USER_AGENT"),
});
}
 
const callable = zwsgi.routefn(&ctx);
zwsgi.buildfn(&ctx, callable) catch |err| {
switch (err) {
error.NetworkCrash => std.debug.print("client disconnect'\n", .{}),
error.Unrouteable => {
std.debug.print("Unrouteable'\n", .{});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
},
error.NotImplemented,
error.Unknown,
error.ReqResInvalid,
error.AndExit,
error.NoSpaceLeft,
=> {
std.debug.print("Unexpected error '{}'\n", .{err});
return err;
},
error.InvalidURI => unreachable,
error.OutOfMemory => {
std.debug.print("Out of memory at '{}'\n", .{arena.queryCapacity()});
return err;
},
error.Abusive,
error.Unauthenticated,
error.BadData,
error.DataMissing,
=> {
std.debug.print("Abusive {} because {}\n", .{ ctx.request, err });
for (ctx.request.raw_request.zwsgi.vars) |vars| {
std.debug.print("Abusive var '{s}' => '''{s}'''\n", .{ vars.key, vars.val });
}
if (ctx.reqdata.post) |post_data| {
std.debug.print("post data => '''{s}'''\n", .{post_data.rawpost});
}
},
}
};
}
}
 
fn serveHttp(zwsgi: *ZWSGI) !void {
const addr = std.net.Address.parseIp(HOST, PORT) catch unreachable;
var srv = try addr.listen(.{ .reuse_address = true });
defer srv.deinit();
std.debug.print("HTTP Server listening\n", .{});
 
const path = try std.fs.cwd().realpathAlloc(zwsgi.alloc, FILE);
defer zwsgi.alloc.free(path);
const zpath = try zwsgi.alloc.dupeZ(u8, path);
defer zwsgi.alloc.free(zpath);
 
const request_buffer: []u8 = try zwsgi.alloc.alloc(u8, 0xffff);
defer zwsgi.alloc.free(request_buffer);
 
while (true) {
var conn = try srv.accept();
defer conn.stream.close();
std.debug.print("HTTP conn from {}\n", .{conn.address});
var hsrv = std.http.Server.init(conn, request_buffer);
var arena = std.heap.ArenaAllocator.init(zwsgi.alloc);
defer arena.deinit();
const a = arena.allocator();
 
var hreq = try hsrv.receiveHead();
 
var ctx = try zwsgi.buildVerseHttp(a, &hreq);
var ipbuf: [0x20]u8 = undefined;
const ipport = try std.fmt.bufPrint(&ipbuf, "{}", .{conn.address});
if (std.mem.indexOf(u8, ipport, ":")) |i| {
try ctx.request.addHeader("REMOTE_ADDR", ipport[0..i]);
try ctx.request.addHeader("REMOTE_PORT", ipport[i + 1 ..]);
} else unreachable;
 
const callable = zwsgi.routefn(&ctx);
zwsgi.buildfn(&ctx, callable) catch |err| {
switch (err) {
error.NetworkCrash => std.debug.print("client disconnect'\n", .{}),
error.Unrouteable => {
std.debug.print("Unrouteable'\n", .{});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
},
error.NotImplemented,
error.Unknown,
error.ReqResInvalid,
error.AndExit,
error.NoSpaceLeft,
=> {
std.debug.print("Unexpected error '{}'\n", .{err});
return err;
},
error.InvalidURI => unreachable,
error.OutOfMemory => {
std.debug.print("Out of memory at '{}'\n", .{arena.queryCapacity()});
return err;
},
error.Abusive,
error.Unauthenticated,
error.BadData,
error.DataMissing,
=> {
std.debug.print("Abusive {} because {}\n", .{ ctx.request, err });
for (ctx.request.raw_request.zwsgi.vars) |vars| {
std.debug.print("Abusive var '{s}' => '''{s}'''\n", .{ vars.key, vars.val });
}
},
}
};
}
unreachable;
}
 
pub const RunMode = enum {
unix,
http,
other,
stop,
};
 
pub fn serve(zwsgi: *ZWSGI) !void {
switch (zwsgi.runmode) {
.unix => try zwsgi.serveUnix(),
.http => try zwsgi.serveHttp(),
else => {},
}
}
 
fn readU16(b: *const [2]u8) u16 {
std.debug.assert(b.len >= 2);
return @as(u16, @bitCast(b[0..2].*));
}
 
test "readu16" {
const buffer = [2]u8{ 238, 1 };
const size: u16 = 494;
try std.testing.expectEqual(size, readU16(&buffer));
}
 
fn readVars(a: Allocator, b: []const u8) ![]uWSGIVar {
var list = std.ArrayList(uWSGIVar).init(a);
var buf = b;
while (buf.len > 0) {
const keysize = readU16(buf[0..2]);
buf = buf[2..];
const key = try a.dupe(u8, buf[0..keysize]);
buf = buf[keysize..];
 
const valsize = readU16(buf[0..2]);
buf = buf[2..];
const val = try a.dupe(u8, if (valsize == 0) "" else buf[0..valsize]);
buf = buf[valsize..];
 
try list.append(uWSGIVar{
.key = key,
.val = val,
});
}
return try list.toOwnedSlice();
}
 
const dump_vars = false;
 
fn find(list: []uWSGIVar, search: []const u8) ?[]const u8 {
for (list) |each| {
if (std.mem.eql(u8, each.key, search)) return each.val;
}
return null;
}
 
fn findOr(list: []uWSGIVar, search: []const u8) []const u8 {
return find(list, search) orelse "[missing]";
}
 
fn buildVerse(z: ZWSGI, a: Allocator, request: *Request) !Verse {
var post_data: ?RequestData.PostData = null;
var reqdata: RequestData = undefined;
switch (request.raw_request) {
.zwsgi => |zreq| {
if (find(zreq.vars, "HTTP_CONTENT_LENGTH")) |h_len| {
const h_type = findOr(zreq.vars, "HTTP_CONTENT_TYPE");
 
const post_size = try std.fmt.parseInt(usize, h_len, 10);
if (post_size > 0) {
var reader = zreq.acpt.stream.reader().any();
post_data = try RequestData.readBody(a, &reader, post_size, h_type);
if (dump_vars) std.log.info(
"post data \"{s}\" {{{any}}}",
.{ post_data.?.rawpost, post_data.?.rawpost },
);
 
for (post_data.?.items) |itm| {
if (dump_vars) std.log.info("{}", .{itm});
}
}
}
 
var query: RequestData.QueryData = undefined;
if (find(zreq.vars, "QUERY_STRING")) |qs| {
query = try RequestData.readQuery(a, qs);
}
reqdata = RequestData{
.post = post_data,
.query = query,
};
},
.http => |hreq| {
if (hreq.head.content_length) |h_len| {
if (h_len > 0) {
const h_type = hreq.head.content_type orelse "text/plain";
var reader = try hreq.reader();
post_data = try RequestData.readBody(a, &reader, h_len, h_type);
if (dump_vars) std.log.info(
"post data \"{s}\" {{{any}}}",
.{ post_data.?.rawpost, post_data.?.rawpost },
);
 
for (post_data.?.items) |itm| {
if (dump_vars) std.log.info("{}", .{itm});
}
}
}
 
var query_data: RequestData.QueryData = undefined;
if (std.mem.indexOf(u8, hreq.head.target, "/")) |i| {
query_data = try RequestData.readQuery(a, hreq.head.target[i..]);
}
reqdata = RequestData{
.post = post_data,
.query = query_data,
};
},
}
 
const response = try Response.init(a, request);
return Verse.init(a, z.config, request.*, response, reqdata);
}
 
fn readHttpHeaders(a: Allocator, req: *std.http.Server.Request) !Request {
//const vars = try readVars(a, buf);
 
var itr_headers = req.iterateHeaders();
while (itr_headers.next()) |header| {
std.debug.print("http header => {s} -> {s}\n", .{ header.name, header.value });
if (dump_vars) std.log.info("{}", .{header});
}
 
return try Request.init(a, req);
}
 
fn buildVerseHttp(z: ZWSGI, a: Allocator, req: *std.http.Server.Request) !Verse {
var request = try readHttpHeaders(a, req);
std.debug.print("http target -> {s}\n", .{request.uri});
return z.buildVerse(a, &request);
}
 
fn readuWSGIHeader(a: Allocator, acpt: Server.Connection) !Request {
var uwsgi_header = uProtoHeader{};
var ptr: [*]u8 = @ptrCast(&uwsgi_header);
_ = try acpt.stream.read(@alignCast(ptr[0..4]));
 
const buf: []u8 = try a.alloc(u8, uwsgi_header.size);
const read = try acpt.stream.read(buf);
if (read != uwsgi_header.size) {
std.log.err("unexpected read size {} {}", .{ read, uwsgi_header.size });
}
 
const vars = try readVars(a, buf);
for (vars) |v| {
if (dump_vars) std.log.info("{}", .{v});
}
 
return try Request.init(
a,
zWSGIRequest{
.header = uwsgi_header,
.acpt = acpt,
.vars = vars,
},
);
}
 
fn buildVerseuWSGI(z: ZWSGI, a: Allocator, conn: *Server.Connection) !Verse {
var request = try readuWSGIHeader(a, conn.*);
 
return z.buildVerse(a, &request);
}