diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..71969716e3e9ca87fdafa1839c25c6daf7da7560 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +zig-* +.zigmod +deps.zig diff --git a/build.zig b/build.zig new file mode 100644 index 0000000000000000000000000000000000000000..dc747cb928ca1ef06197be80815205eff95ceca4 --- /dev/null +++ b/build.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const deps = @import("./deps.zig"); + +pub fn build(b: *std.build.Builder) void { + const target = b.standardTargetOptions(.{}); + + const mode = b.standardReleaseOptions(); + + const exe = b.addExecutable("zig-pek", "src/main.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); + deps.addAllTo(exe); + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/src/astgen.zig b/src/astgen.zig new file mode 100644 index 0000000000000000000000000000000000000000..501d0c595c9460d49dd0e70629cf9ecc434ea6d7 --- /dev/null +++ b/src/astgen.zig @@ -0,0 +1,113 @@ +const std = @import("std"); +const Token = @import("./tokenize.zig").Token; + +// +// + +pub const Value = union(enum) { + element: Element, + attr: Attr, + string: []const u8, +}; + +pub const Element = struct { + name: []const u8, + attrs: []const Attr, + children: []const Value, +}; + +pub const Attr = struct { + key: []const u8, + value: []const u8, +}; + +// +// + +pub fn do(comptime tokens: []const Token) Element { + var parser = Parser{ .tokens = tokens, .index = 0 }; + return parser.doElement(); +} + +// +// + +const Parser = struct { + tokens: []const Token, + index: usize, + + pub fn doElement(comptime self: *Parser) Element { + return Element{ + .name = self.eat(.word), + .attrs = self.doAttrs(), + .children = self.doChildren(), + }; + } + + pub fn doAttrs(comptime self: *Parser) []const Attr { + var ret: []const Attr = &[_]Attr{}; + + if (self.tryEatSymbol("[")) { + inline while (true) { + if (self.tryEatSymbol("]")) break; + ret = ret ++ &[_]Attr{self.doAttr()}; + } + } + + return ret; + } + + fn tryEatSymbol(comptime self: *Parser, comptime needle: []const u8) bool { + switch (self.tokens[self.index]) { + .symbol => |sym| { + if (std.mem.eql(u8, sym, needle)) { + self.index += 1; + return true; + } + return false; + }, + else => { + return false; + }, + } + } + + pub fn doAttr(comptime self: *Parser) Attr { + const k = self.eat(.word); + self.eatSymbol("="); + const v = self.eat(.string); + return Attr{ + .key = k, + .value = v, + }; + } + + pub fn eatSymbol(comptime self: *Parser, comptime needle: []const u8) void { + std.debug.assert(std.mem.eql(u8, self.eat(.symbol), needle)); + } + + pub fn doChildren(comptime self: *Parser) []const Value { + var ret: []const Value = &[_]Value{}; + + if (self.tryEatSymbol("(")) { + inline while (true) { + if (self.tryEatSymbol(")")) break; + ret = ret ++ &[_]Value{self.doValue()}; + } + } + + return ret; + } + + pub fn doValue(comptime self: *Parser) Value { + if (self.tokens[self.index] == .string) { + return Value{ .string = self.eat(.string) }; + } + return Value{ .element = self.doElement() }; + } + + fn eat(comptime self: *Parser, comptime typ: std.meta.Tag(Token)) []const u8 { + defer self.index += 1; + return @field(self.tokens[self.index], @tagName(typ)); + } +}; diff --git a/src/lib.zig b/src/lib.zig new file mode 100644 index 0000000000000000000000000000000000000000..b4cb006111594b0483f572c0dacb98eb72f2d8e2 --- /dev/null +++ b/src/lib.zig @@ -0,0 +1,69 @@ +//! Pek HTML Preprocessor Language +//! +//! Shortening of Pekingese +//! https://en.wikipedia.org/wiki/Pekingese +//! +//! Loosely inspired by Pug +//! https://pugjs.org/api/getting-started.html + +// // document.corgi +// doctype html +// html[lang="en"]( +// head( +// title("Corgi Example") +// meta[charset="UTF-8"] +// meta[name="viewport",content="width=device-width,initial-scale=1"] +// ) +// body( +// h1("Corgi Example") +// hr +// p("This is an example HTML document written in "a[href="https://github.com/corgi-lang/corgi"]("Corgi")".") +// p("Follow Nektro on Twitter @Nektro") +// ) +// ) + +const std = @import("std"); +const range = @import("range").range; + +const tokenize = @import("./tokenize.zig"); +const astgen = @import("./astgen.zig"); + +pub fn parse(comptime input: []const u8) astgen.Value { + return astgen.Value{ .element = astgen.do(tokenize.do(input, &.{ '[', '=', ']', '(', ')', '{', '}' })) }; +} + +pub fn compile(writer: anytype, comptime value: astgen.Value, data: anytype, indent: usize, flag1: bool) anyerror!void { + switch (value) { + .element => |v| { + const hastext = for (v.children) |x| { + if (x == .string) break true; + } else false; + + if (flag1) for (range(indent)) |_| try writer.writeAll(" "); + try writer.writeAll("<"); + try writer.writeAll(v.name); + + for (v.attrs) |it| { + try writer.print(" {s}=\"{}\"", .{ it.key, std.zig.fmtEscapes(it.value[1 .. it.value.len - 1]) }); + } + + if (v.children.len == 0) { + try writer.writeAll(" />\n"); + } else { + try writer.writeAll(">"); + + if (!hastext) try writer.writeAll("\n"); + inline for (v.children) |it| { + try compile(writer, it, data, indent + 1, !hastext); + } + if (!hastext) for (range(indent)) |_| try writer.writeAll(" "); + try writer.print("", .{v.name}); + if (flag1) try writer.writeAll("\n"); + } + }, + .string => |v| { + try writer.writeAll(v[1 .. v.len - 1]); + }, + else => unreachable, + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000000000000000000000000000000000000..f91f776d5e547097eeb1cd6073d32e834374ce78 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,26 @@ +const std = @import("std"); +const pek = @import("pek"); + +const example_document = + \\html[lang="en"]( + \\ head( + \\ title("Pek Example") + \\ meta[charset="UTF-8"] + \\ meta[name="viewport" content="width=device-width,initial-scale=1"] + \\ ) + \\ body( + \\ h1("Pek Example") + \\ hr + \\ p("This is an example HTML document written in "a[href="https://github.com/nektro/zig-pek"]("Pek")".") + \\ ) + \\) +; + +pub fn main() !void { + std.log.info("All your codebase are belong to us.", .{}); + std.debug.print("\n", .{}); + + const doc = comptime pek.parse(example_document); + try pek.compile(std.io.getStdErr().writer(), doc, .{}, 0, false); + std.debug.print("\n", .{}); +} diff --git a/src/tokenize.zig b/src/tokenize.zig new file mode 100644 index 0000000000000000000000000000000000000000..e01b36ae68dd4bf7f4f689c754d19d70c55ddfaa --- /dev/null +++ b/src/tokenize.zig @@ -0,0 +1,110 @@ +const std = @import("std"); +const string = []const u8; + +// +// + +pub const Token = union(enum) { + word: string, + symbol: string, + string: string, + + pub const skippedChars = &[_]u8{ ' ', '\n', '\t', '\r' }; +}; + +// +// + +pub fn do(comptime input: string, comptime symbols: []const u8) []const Token { + var ret: []const Token = &[_]Token{}; + + var line = 1; + var pos = 1; + + var start = 0; + var end = 0; + var mode = 0; + + @setEvalBranchQuota(10001000); + + inline for (input) |c, i| { + const s = &[_]u8{c}; + + var shouldFlush: bool = undefined; + + blk: { + if (mode == 0) { + if (c == '#') { + mode = 1; + shouldFlush = false; + break :blk; + } + if (c == '"') { + mode = 2; + shouldFlush = false; + break :blk; + } + } + if (mode == 1) { + if (c == '\n') { + // skip comments + // f(v.handle(TTCom, in[s:i])) + start = i; + end = i; + mode = 0; + } + shouldFlush = c == '\n'; + break :blk; + } + if (mode == 2) { + if (c == input[start]) { + ret = ret ++ &[_]Token{.{ .string = input[start .. i + 1] }}; + start = i + 1; + end = i; + mode = 0; + } + shouldFlush = false; + break :blk; + } + if (std.mem.indexOf(u8, Token.skippedChars, s)) |_| { + shouldFlush = true; + break :blk; + } + if (std.mem.indexOf(u8, symbols, s)) |_| { + shouldFlush = true; + break :blk; + } + shouldFlush = false; + break :blk; + } + + if (!shouldFlush) { + end += 1; + } + if (shouldFlush) { + if (mode == 0) { + if (end - start > 0) { + ret = ret ++ &[_]Token{.{ .word = input[start..end] }}; + start = i; + end = i; + } + if (std.mem.indexOf(u8, Token.skippedChars, s)) |_| { + start += 1; + end += 1; + } + if (std.mem.indexOf(u8, symbols, s)) |_| { + ret = ret ++ &[_]Token{.{ .symbol = s }}; + start += 1; + end += 1; + } + } + } + + pos += 1; + if (c != '\n') continue; + line += 1; + pos = 1; + } + + return ret; +} diff --git a/zig.mod b/zig.mod new file mode 100644 index 0000000000000000000000000000000000000000..83b771ae9c9397286ace2a44d93ca90b9da3b2a5 --- /dev/null +++ b/zig.mod @@ -0,0 +1,7 @@ +id: gllspcgd4fa2jblyp17dspjm6cww2s6eaelmr7dme5gyv3fn +name: pek +main: src/lib.zig +license: AGPL-3.0 +description: An HTML preprocessor with a builtin template engine. +dependencies: + - src: git https://github.com/nektro/zig-range diff --git a/zigmod.lock b/zigmod.lock new file mode 100644 index 0000000000000000000000000000000000000000..4abcde2af0172615d652f2b48a13934c6821abf0 --- /dev/null +++ b/zigmod.lock @@ -0,0 +1,2 @@ +2 +git https://github.com/nektro/zig-range commit-890ca308fe09b3d5c866d5cfb3b3d7a95dbf939f