initial version of template renderer
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Mon, 31 Oct 2022 19:38:35 +0000 (14:38 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Mon, 31 Oct 2022 19:38:35 +0000 (14:38 -0500)
13 files changed:
.gitignore [new file with mode: 0644]
build.onyx [new file with mode: 0644]
onyx-pkg.ini [new file with mode: 0644]
src/app.onyx [new file with mode: 0644]
src/html-templates/TODO.md [new file with mode: 0644]
src/html-templates/module.onyx [new file with mode: 0644]
src/html-templates/src/otmp.onyx [new file with mode: 0644]
src/html-templates/src/parser.onyx [new file with mode: 0644]
src/html-templates/src/render.onyx [new file with mode: 0644]
src/html-templates/src/types.onyx [new file with mode: 0644]
www/static/css/style.css [new file with mode: 0644]
www/templates/base.html [new file with mode: 0644]
www/templates/index.html [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..3640de3
--- /dev/null
@@ -0,0 +1,6 @@
+bin/
+lib/
+*.sublime-project
+*.sublime-workspace
+.vscode/
+*.wasm
diff --git a/build.onyx b/build.onyx
new file mode 100644 (file)
index 0000000..68a9c01
--- /dev/null
@@ -0,0 +1,8 @@
+#load "./lib/http-server/module"
+#load "./lib/postgres/module"
+#load "./lib/postgres-orm/module"
+#library_path "./bin"
+
+#load_all "./src"
+#load "./src/html-templates/module"
+
diff --git a/onyx-pkg.ini b/onyx-pkg.ini
new file mode 100644 (file)
index 0000000..c6f22aa
--- /dev/null
@@ -0,0 +1,26 @@
+[metadata]
+name=onyxlang-io
+description=The onyxlang.io website
+url=https://onyxlang.io
+author=Brendan Hansen
+version=0.0.1
+
+[config]
+lib_source_directory=./lib
+lib_bin_directory=./bin
+
+[native_library]
+build_cmd=
+library=
+
+[dependencies]
+git://onyxlang.io/repo/http-server=0.0.17
+git://onyxlang.io/repo/postgres-orm=0.0.18
+
+[dependency_folders]
+git://onyxlang.io/repo/http-server=http-server
+git://onyxlang.io/repo/postgres-orm=postgres-orm
+git://onyxlang.io/repo/json=json
+git://onyxlang.io/repo/base64=base64
+git://onyxlang.io/repo/postgres=postgres
+
diff --git a/src/app.onyx b/src/app.onyx
new file mode 100644 (file)
index 0000000..4c52d97
--- /dev/null
@@ -0,0 +1,49 @@
+#inject runtime.vars {
+    Enable_Heap_Debug :: true
+    Debug :: true
+}
+
+use core
+use http {Req :: Request, Res :: Response}
+
+reg: otmp.TemplateRegistry;
+
+@http.route.{.GET, "/index"}
+(req: ^Req, res: ^Res) {
+    reg->render_template("index", ^res.writer, ^.{
+        x = 123,
+        y = "123123",
+        numbers = .[1, 2, 3, 4],
+        names   = .["joe", "jim", "john"],
+    });
+    res->status(200);
+}
+
+main :: () {
+    reg = otmp.registry();
+    reg->load_directory("./www/templates", ".html");
+
+    app := http.application();
+
+    files := http.static("/static/", "./www/static/");
+    app->pipe(^files);
+
+    #if #defined(runtime.vars.Debug) {
+        app->pipe((req, res) => {
+            printf("Before: {}\n", alloc.heap.get_watermark());
+            reg->refresh_templates();
+            printf("After: {}\n", alloc.heap.get_watermark());
+        });
+    }
+
+    router := http.router();
+    router->collect_routes();
+    app->pipe(^router);
+
+    logger := http.logger();
+    app->pipe(^logger);
+
+    app->serve(8080);
+}
+
+
diff --git a/src/html-templates/TODO.md b/src/html-templates/TODO.md
new file mode 100644 (file)
index 0000000..3c71e3b
--- /dev/null
@@ -0,0 +1,4 @@
+
+
+- [ ] // :EscapedStrings
+
diff --git a/src/html-templates/module.onyx b/src/html-templates/module.onyx
new file mode 100644 (file)
index 0000000..e3b0da5
--- /dev/null
@@ -0,0 +1,7 @@
+package otmp
+
+
+#load "core/std"
+#load_all "./src"
+
+
diff --git a/src/html-templates/src/otmp.onyx b/src/html-templates/src/otmp.onyx
new file mode 100644 (file)
index 0000000..e40d4f3
--- /dev/null
@@ -0,0 +1,153 @@
+package otmp
+
+use core.alloc {as_allocator, arena}
+use core {array, io, os, string, tprintf, printf}
+
+//
+// Template Registry
+// 
+
+registry :: () -> TemplateRegistry {
+    t: TemplateRegistry;
+    t.arena = arena.make(context.allocator, 64 * 1024);
+    t.templates->init();
+
+    return t;
+}
+
+#overload
+delete :: (t: ^TemplateRegistry) {
+    for ^temp: t.templates.entries {
+        delete(temp.value);
+    }
+
+    delete(^t.templates);
+    arena.free(^t.arena);
+}
+
+#inject TemplateRegistry {
+    load_directory :: (self: ^TemplateRegistry, dir: str, extension: str) {
+        for os.list_directory(dir) {
+            if string.ends_with(it->name(), extension) {
+                name := it->name();
+                name = name[0 .. string.last_index_of(name, #char ".")];
+
+                self->load_template(name, tprintf("{}/{}", dir, it->name()));
+            }
+
+            if it.type == .Directory {
+                self->load_directory(tprintf("{}/{}", dir, it->name()), extension);
+            }
+        }
+    }
+
+    load_template :: (self: ^TemplateRegistry, name: str, filename: str) -> Error {
+        printf("Loading template {} from {}.\n", name, filename);
+        if self.templates->has(name) {
+            printf("[ERROR] Template '{}' already exists.\n");
+            return .Duplicate_Template;
+        }
+
+        if !os.is_file(filename) {
+            return .Template_Not_Found;
+        }
+
+        contents := os.get_contents(filename);
+        stored_contents := contents;
+        defer delete(^stored_contents);
+
+        permanent_name := string.alloc_copy(name, as_allocator(^self.arena));
+
+        temp          := template_make(as_allocator(^self.arena));
+        temp.filepath  = filename |> string.alloc_copy(as_allocator(^self.arena));
+        temp.name      = permanent_name;
+
+        parse_template(temp, ^contents);
+
+        self.templates[permanent_name] = temp;
+        return .None;
+    }
+
+    refresh_templates :: (self: ^TemplateRegistry) {
+        for ^temp: self.templates.entries {
+            array.clear(^temp.value.instructions);
+            arena.clear(^temp.value.node_storage);
+
+            contents := os.get_contents(temp.value.filepath);
+            stored_contents := contents;
+            defer delete(^stored_contents);
+
+            parse_template(temp.value, ^contents);
+        }
+    }
+
+    get_template :: (self: ^TemplateRegistry, name: str) -> ^Template {
+        return self.templates[name];
+    }
+
+    render_template :: (self: ^TemplateRegistry, name: str, output: ^io.Writer, scope: any) -> Error {
+        temp := self.templates[name];
+        if temp == null {
+            return .Template_Not_Found;
+        }
+
+        tscope, err := core.misc.any_to_map(scope);
+        defer delete(^tscope);
+
+        return temp->render(self, ^tscope, output);
+    }
+}
+
+
+
+
+//
+// Template
+//
+#package
+template_make :: (alloc: Allocator) -> ^Template {
+    t := new(Template, alloc);
+    t.node_storage = arena.make(context.allocator, 64 * 1024);
+    return t;
+}
+
+#overload
+delete :: (t: ^Template) {
+    delete(^t.instructions);
+    arena.free(^t.node_storage);
+}
+
+#inject Template {
+    render :: (self: ^Template, reg: ^TemplateRegistry, scope: ^TemplateScope, output: ^io.Writer) -> Error {
+        r := ^TemplateRenderer.{
+            t = self,
+            w = output,
+            reg = reg,
+            scope = scope
+        };
+
+        err := render_template(r);
+
+        if err != .None {
+            core.printf("Template Error: {}\n", r.error);
+        }
+
+        return err;
+    }
+}
+
+
+#package
+make_node :: macro (t: ^Template, $T: type_expr) -> ^T where IsTNode(T) {
+    r := new(T, allocator=as_allocator(^t.node_storage));
+    r.type = T;
+    return r;
+}
+
+#package
+make_expr :: macro (t: ^Template, $T: type_expr) -> ^T where IsTExpr(T) {
+    r := new(T, allocator=as_allocator(^t.node_storage));
+    r.type = T;
+    return r;
+}
+
diff --git a/src/html-templates/src/parser.onyx b/src/html-templates/src/parser.onyx
new file mode 100644 (file)
index 0000000..7ed451e
--- /dev/null
@@ -0,0 +1,390 @@
+package otmp
+
+
+use core {string, array, iter}
+use core.alloc {as_allocator}
+
+ParseError :: enum {
+    None;
+    Unexpected_Token;
+    Expected_Token;
+
+    Nested_Command;
+
+    Cannot_Nest_Blocks;
+}
+
+
+#package
+TemplateLexer :: struct {
+    // The input string
+    s: ^str;
+    line: u32;
+    col:  u32;
+
+    hit_eof := false;
+
+    inside_command := false;
+    inside_expression := false;
+    error: ParseError;
+
+    token_buffer: [..] TemplateToken;
+}
+
+TemplateToken :: struct {
+    Type :: enum {
+        Error;
+        EOF;
+
+        Text;   // Raw Text
+
+        Command_Start; Command_End;
+        Expression_Start; Expression_End;
+
+        Keyword_Block;
+        Keyword_EndBlock;
+        Keyword_Foreach;
+        Keyword_EndForeach;
+        Keyword_In;
+        Keyword_Extends;
+
+        String_Literal;
+
+        Variable;
+    }
+
+    type: Type;
+    text: str;
+    line: u32;
+    col:  u32;
+}
+
+#inject TemplateLexer {
+    peek :: (self: ^TemplateLexer, n := 0) -> TemplateToken {
+        while n >= self.token_buffer.length {
+            next := self->eat_next_token();
+            if next.type == .EOF do return next;
+
+            self.token_buffer << next;
+        }
+
+        return self.token_buffer[n];
+    }
+
+    consume :: (self: ^TemplateLexer) -> TemplateToken {
+        if !array.empty(self.token_buffer) {
+            tkn := self.token_buffer[0];
+            array.delete(^self.token_buffer, 0);
+            return tkn;
+        }
+
+        return self->eat_next_token();
+    }
+
+    eat_next_token :: (self: ^TemplateLexer) -> TemplateToken {
+        tkn: TemplateToken; 
+        tkn.line = self.line;
+        tkn.col  = self.col;
+        
+        string.strip_leading_whitespace(self.s);
+
+        if string.empty(*self.s) {
+            self.hit_eof = true;
+            yield_token(.EOF);
+        }
+
+        token_match("{{") {
+            if self.inside_command {
+                self.error = .Nested_Command;
+                yield_token(.Error);
+            }
+
+            self.inside_command = true;
+            yield_token(.Command_Start);
+        }
+
+        token_match("}}") {
+            if self.inside_command {
+                self.inside_command = false;
+            }
+
+            yield_token(.Command_End);
+        }
+
+        token_match("{%") {
+            if self.inside_expression {
+                self.error = .Nested_Command;
+                yield_token(.Error);
+            }
+
+            self.inside_expression = true;
+            yield_token(.Expression_Start);
+        }
+
+        token_match("%}") {
+            if self.inside_expression {
+                self.inside_expression = false;
+            }
+
+            yield_token(.Expression_End);
+        }
+
+        if self.inside_command || self.inside_expression {
+            string.strip_leading_whitespace(self.s);
+
+            token_consume("block",      .Keyword_Block);            
+            token_consume("endblock",   .Keyword_EndBlock);            
+            token_consume("foreach",    .Keyword_Foreach);            
+            token_consume("endforeach", .Keyword_EndForeach);            
+            token_consume("in",         .Keyword_In);
+            token_consume("extends",    .Keyword_Extends);
+
+            if self.s.data[0] == #char "\"" {
+                // :TODO add escaped strings
+                string.advance(self.s, 1);
+
+                tkn.text, *self.s = string.bisect(*self.s, #char "\"");
+                
+                yield_token(.String_Literal);
+            }
+
+            if self.s.data[0] == #char "$" {
+                string.advance(self.s, 1);
+
+                tkn.text = string.read_alphanum(self.s);
+
+                yield_token(.Variable);
+            }
+
+        } else {
+            length := 1;
+            while self.s.data[length] != #char "{" && length < self.s.length {
+                length += 1;
+            }
+
+            tkn.text = self.s.data[0 .. length];
+            string.advance(self.s, length);
+            yield_token(.Text);
+        }
+
+        yield_token :: macro (kind: TemplateToken.Type) {
+            tkn.type = kind;
+            return tkn;
+        }
+
+        token_match :: macro (t: str, body: Code) {
+            if string.starts_with(*self.s, t) {
+                string.advance(self.s, t.length);
+
+                #unquote body;
+            }
+        }
+        
+        token_consume :: macro (t: str, kind: TemplateToken.Type) {
+            if string.starts_with(*self.s, t) {
+                tkn.text = self.s.data[0 .. t.length];
+                string.advance(self.s, t.length);
+                yield_token(kind);
+            }
+        }
+    }
+}
+
+#overload
+delete :: (tl: ^TemplateLexer) {
+    delete(^tl.token_buffer);
+}
+
+#overload
+iter.as_iterator :: (tl: ^TemplateLexer) => {
+    return iter.generator(
+        ^.{ tl = tl, hit_error = false },
+        
+        (ctx) => {
+            if !ctx.hit_error {
+                tkn := ctx.tl->consume();
+                if tkn.type == .Error || tkn.type == .EOF {
+                    ctx.hit_error = true;
+                }
+
+                return tkn, true;
+            }
+
+            return .{}, false;
+        }
+    );
+}
+
+
+#package
+TemplateParser :: struct {
+    t: ^Template;
+    l: ^TemplateLexer;
+
+    instruction_targets: [..] ^[..] ^TNode;
+}
+
+#package
+parse_template :: (t: ^Template, s: ^str) -> ParseError {
+    l := TemplateLexer.{s};
+    p := TemplateParser.{t, ^l};
+    p.instruction_targets << ^t.instructions;
+    defer delete(^p.instruction_targets);
+
+    return parse_statements(^p);
+}
+
+#local
+parse_statements :: (use p: ^TemplateParser) -> ParseError {
+    while !p.l.hit_eof {
+        switch tkn := p.l->consume(); tkn.type {
+            case .Command_Start {
+                if err := parse_statement(p); err != .None {
+                    return err;
+                }
+
+                expect_token(p, .Command_End);
+            }
+
+            case .Expression_Start {
+                if node, err := parse_expression(p); err != .None {
+                    return err;
+                } else {
+                    *array.get(instruction_targets, -1) << node;
+                }
+
+                expect_token(p, .Expression_End);
+            }
+
+            case .Text {
+                text_node := make_node(t, TNodeText);
+                text_node.text = string.alloc_copy(tkn.text, as_allocator(^t.node_storage));
+                *array.get(instruction_targets, -1) << text_node;
+            }
+        }
+    }
+
+    return .None;
+}
+
+#local
+parse_statement :: (use p: ^TemplateParser) -> ParseError {
+    switch tkn := p.l->consume(); tkn.type {
+        case .Keyword_Extends {
+            text, err := parse_string(p);
+            if err != .None do return err;
+
+            extend_node := make_node(t, TNodeExtend);
+            extend_node.template_name = text;
+
+            *array.get(instruction_targets, -1) << extend_node;
+        }
+
+        case .Keyword_Block {
+            text, err := parse_string(p);
+            if err != .None do return err;
+
+            block_node := make_node(t, TNodeBlock);
+            block_node.block_name = text;
+
+            *array.get(instruction_targets, -1) << block_node;
+
+            instruction_targets << ^block_node.contents;
+        }
+
+        case .Keyword_EndBlock {
+            array.pop(^instruction_targets);
+        }
+
+        case .Keyword_Foreach {
+            var_tkn: TemplateToken;
+            expect_token(p, .Variable, #(var_tkn));
+
+            expect_token(p, .Keyword_In);
+
+            iter_tkn: TemplateToken;
+            expect_token(p, .Variable, #(iter_tkn));
+
+            var_expr := do {
+                name := string.alloc_copy(iter_tkn.text, as_allocator(^t.node_storage));
+
+                var_expr := make_expr(t, TExprVar);
+                var_expr.var_name = name;
+
+                return var_expr;
+            };
+
+            for_node := make_node(t, TNodeForeach);
+            for_node.var_name = string.alloc_copy(var_tkn.text, as_allocator(^t.node_storage));
+            for_node.list = var_expr;
+
+            *array.get(instruction_targets, -1) << for_node;
+            instruction_targets << ^for_node.body;
+        }
+
+        case .Keyword_EndForeach {
+            array.pop(^instruction_targets);
+        }
+    }
+
+    return .None;
+}
+
+#local
+parse_expression :: (use p: ^TemplateParser) -> (^TExpr, ParseError) {
+    switch tkn := p.l->consume(); tkn.type {
+        case .Keyword_Block {
+            name, err := parse_string(p);
+            if err != .None do return null, err;
+
+            block_expr := make_expr(t, TExprBlock);
+            block_expr.block_name = name;
+
+            return block_expr, .None;
+        }
+
+        case .Variable {
+            name := tkn.text |> string.alloc_copy(as_allocator(^t.node_storage));
+
+            var_expr := make_expr(t, TExprVar);
+            var_expr.var_name = name;
+
+            return var_expr, .None;
+        }
+    }
+
+    return null, .Unexpected_Token;
+}
+
+#local
+parse_string :: (use p: ^TemplateParser) -> (str, ParseError) {
+    str_tkn := p.l->consume();
+    if str_tkn.type != .String_Literal {
+        return "", .Unexpected_Token;
+    }
+
+    value := str_tkn.text |> string.alloc_copy(as_allocator(^t.node_storage));
+
+    return value, .None;
+}
+
+#local
+expect_token :: #match #local {}
+
+#overload
+expect_token :: macro (p: ^TemplateParser, type: TemplateToken.Type, out: Code) {
+    if (p.l->peek()).type != type {
+        return .Expected_Token;
+    }
+
+    (#unquote out) = p.l->consume();
+}
+
+#overload
+expect_token :: macro (p: ^TemplateParser, type: TemplateToken.Type) {
+    if (p.l->peek()).type != type {
+        return .Expected_Token;
+    }
+
+    p.l->consume();
+}
diff --git a/src/html-templates/src/render.onyx b/src/html-templates/src/render.onyx
new file mode 100644 (file)
index 0000000..cc3b985
--- /dev/null
@@ -0,0 +1,90 @@
+package otmp
+
+use core {io, tprintf}
+
+#package
+TemplateRenderer :: struct {
+    t: ^Template;
+    w: ^io.Writer;
+    reg: ^TemplateRegistry;
+
+    scope: ^TemplateScope;
+    blocks: Map(str, ^TNodeBlock);
+
+    error: str;
+}
+
+#package
+render_template :: (use r: ^TemplateRenderer) -> Error {
+    core.printf("{*}\n", scope);
+
+    return render_instructions(r, t.instructions);
+}
+
+#local
+render_instructions :: (use r: ^TemplateRenderer, instrs: [..] ^TNode) -> Error {
+    for instrs {
+        switch it.type {
+            case TNodeText {
+                text := cast(^TNodeText) it;
+                io.write_str(w, text.text);
+            }    
+
+            case TNodeBlock {
+                block := cast(^TNodeBlock) it;
+                if blocks->has(block.block_name) {
+                    error = tprintf("Block '{}' defined multiple times.");
+                    return .Render_Error;
+                }
+
+                blocks[block.block_name] = block;
+            }
+
+            case TNodeExtend {
+                extend := cast(^TNodeExtend) it;
+                template := reg->get_template(extend.template_name);
+                if template == null {
+                    error = tprintf("Template '{}' not found.");
+                    return .Render_Error;
+                }
+
+                if err := render_instructions(r, template.instructions); err != .None {
+                    return err;
+                }
+            }
+
+            case TNodeForeach {
+            }
+
+
+
+            case TExprBlock {
+                block := cast(^TExprBlock) it;
+                if !(blocks->has(block.block_name)) {
+                    continue;
+                }
+
+                if err := render_instructions(r, blocks[block.block_name].contents); err != .None {
+                    return err;
+                }
+            }
+
+            case TExprVar {
+                // :Temporary :TemplateVariables
+                var := cast(^TExprVar) it;
+                if !(scope->has(var.var_name)) {
+                    continue;
+                }
+
+                io.write_format_va(w, "{}", .[scope->get(var.var_name)]);
+            }
+
+            case #default {
+                error = tprintf("Unhandled node type '{}'", it.type);
+                return .Render_Error;
+            }
+        }
+    }
+
+    return .None;
+}
diff --git a/src/html-templates/src/types.onyx b/src/html-templates/src/types.onyx
new file mode 100644 (file)
index 0000000..8e1388f
--- /dev/null
@@ -0,0 +1,97 @@
+package otmp
+
+use core.alloc.arena {Arena}
+
+TemplateRegistry :: struct {
+    templates: Map(str, ^Template);
+
+    arena: Arena;
+}
+
+Template :: struct {
+    name: str;
+    filepath: str;
+
+    instructions: [..] ^TNode;
+
+    node_storage: Arena;
+}
+
+
+TNode :: struct {
+    type: type_expr;
+}
+
+TNodeText :: struct {
+    use node: TNode;
+
+    text: str;     // storage: template.node_storage
+}
+
+TNodeExtend :: struct {
+    use node: TNode;
+
+    template_name: str; // storage: template.node_storage
+}
+
+TNodeBlock :: struct {
+    use node: TNode;
+
+    block_name: str;   // storage: template.node_storage
+    contents: [..] ^TNode;
+}
+
+TNodeForeach :: struct {
+    use node: TNode;
+
+    var_name: str;     // storage: template.node_storage
+    list: ^TExpr;
+    body: [..] ^TNode;
+}
+
+
+TExpr :: struct {
+    use node: TNode;
+}
+
+TExprBlock :: struct {
+    use expr: TExpr;
+    
+    block_name: str; // storage: template.node_storage
+}
+
+TExprVar :: struct {
+    use expr: TExpr;
+
+    var_name: str;   // storage: template.node_storage
+}
+
+TExprSelector :: struct {
+    use expr: TExpr;
+
+    var: ^TExpr;
+    field: str;     // storage: template.node_storage
+}
+
+
+Error :: enum {
+    None;
+    Template_Not_Found;
+    Duplicate_Template;
+
+    Render_Error;
+}
+
+
+TemplateScope :: #type Map(str, any);
+
+
+#package {
+    IsTNode :: interface (t: $T) {
+        { ^t } -> ^TNode;
+    }
+
+    IsTExpr :: interface (t: $T) {
+        { ^t } -> ^TExpr;
+    }
+}
diff --git a/www/static/css/style.css b/www/static/css/style.css
new file mode 100644 (file)
index 0000000..8000e72
--- /dev/null
@@ -0,0 +1,8 @@
+* {
+    padding: 0;
+    margin:  0;
+    box-sizing: border-box;
+}
+
+
+
diff --git a/www/templates/base.html b/www/templates/base.html
new file mode 100644 (file)
index 0000000..a1ae781
--- /dev/null
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>{% block "title" %}</title>
+
+        <link rel="stylesheet" href="/static/css/style.css">
+
+        {% block "styles" %}
+    </head>
+
+    <body>
+        {% block "content" %}
+    </body>
+</html>
\ No newline at end of file
diff --git a/www/templates/index.html b/www/templates/index.html
new file mode 100644 (file)
index 0000000..1ef7886
--- /dev/null
@@ -0,0 +1,19 @@
+{{block "title"}}
+    Wow, such title!
+{{endblock}}
+
+{{block "content"}}
+    <h1>Hi!</h1>
+    <h2>{% $x %}</h2>
+    <h2>{% $y %}</h2>
+
+    <ul>
+    {{foreach $name in $names}}
+        <li>{% $name %}</li>
+    {{endforeach}}
+    </ul>
+{{endblock}}
+
+{{extends "base"}}
+
+