feat: add template inheritance (extends/block) support
- ViewEngine now supports extends and named blocks - Each route gets exclusive cached AST (no shared parent layouts) - Fix iteration over struct arrays in each loops - Add demo app with full e-commerce layout using extends - Serve static files from public folder - Bump version to 0.3.0
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
p
|
||||
| Route no found
|
||||
@@ -1,336 +0,0 @@
|
||||
//! Auto-generated by pugz.compileTemplates()
|
||||
//! Do not edit manually - regenerate by running: zig build
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArrayList = std.ArrayList(u8);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const esc_lut: [256]?[]const u8 = blk: {
|
||||
var t: [256]?[]const u8 = .{null} ** 256;
|
||||
t['&'] = "&";
|
||||
t['<'] = "<";
|
||||
t['>'] = ">";
|
||||
t['"'] = """;
|
||||
t['\''] = "'";
|
||||
break :blk t;
|
||||
};
|
||||
|
||||
fn esc(o: *ArrayList, a: Allocator, s: []const u8) Allocator.Error!void {
|
||||
var i: usize = 0;
|
||||
for (s, 0..) |c, j| {
|
||||
if (esc_lut[c]) |e| {
|
||||
if (j > i) try o.appendSlice(a, s[i..j]);
|
||||
try o.appendSlice(a, e);
|
||||
i = j + 1;
|
||||
}
|
||||
}
|
||||
if (i < s.len) try o.appendSlice(a, s[i..]);
|
||||
}
|
||||
|
||||
fn truthy(v: anytype) bool {
|
||||
return switch (@typeInfo(@TypeOf(v))) {
|
||||
.bool => v,
|
||||
.optional => v != null,
|
||||
.pointer => |p| if (p.size == .slice) v.len > 0 else true,
|
||||
.int, .comptime_int => v != 0,
|
||||
else => true,
|
||||
};
|
||||
}
|
||||
|
||||
var int_buf: [32]u8 = undefined;
|
||||
|
||||
fn strVal(v: anytype) []const u8 {
|
||||
const T = @TypeOf(v);
|
||||
switch (@typeInfo(T)) {
|
||||
.pointer => |p| switch (p.size) {
|
||||
.slice => return v,
|
||||
.one => {
|
||||
// For pointer-to-array, slice it
|
||||
const child_info = @typeInfo(p.child);
|
||||
if (child_info == .array) {
|
||||
const arr_info = child_info.array;
|
||||
const ptr: [*]const arr_info.child = @ptrCast(v);
|
||||
return ptr[0..arr_info.len];
|
||||
}
|
||||
return strVal(v.*);
|
||||
},
|
||||
else => @compileError("unsupported pointer type"),
|
||||
},
|
||||
.array => @compileError("arrays must be passed by pointer"),
|
||||
.int, .comptime_int => return std.fmt.bufPrint(&int_buf, "{d}", .{v}) catch "0",
|
||||
.optional => return if (v) |val| strVal(val) else "",
|
||||
else => @compileError("strVal: unsupported type " ++ @typeName(T)),
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Templates
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn index(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>hello</title></head><body><p>some thing</p>ballahballah");
|
||||
{
|
||||
const text = "click me ";
|
||||
const @"type" = "secondary";
|
||||
try o.appendSlice(a, "<button");
|
||||
try o.appendSlice(a, " class=\"");
|
||||
try o.appendSlice(a, "btn btn-");
|
||||
try o.appendSlice(a, strVal(@"type"));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, ">");
|
||||
try esc(&o, a, strVal(text));
|
||||
try o.appendSlice(a, "</button>");
|
||||
}
|
||||
try o.appendSlice(a, "</body><br /><a href=\"//google.com\" target=\"_blank\">Google 1</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 2</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 3</a></html>");
|
||||
_ = d;
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn sub_layout(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<html><head><title>My Site - ");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div class=\"sidebar\"><p>nothing</p></div><div class=\"primary\"><p>nothing</p></div><div id=\"footer\"><p>some footer content</p></div></body></html>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn _404(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "<p>Route no found</p>";
|
||||
}
|
||||
|
||||
pub fn mixins_alert(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn mixins_buttons(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn mixins_cards(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn mixins_alert_error(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn mixins_input_text(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn home(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</title><link rel=\"stylesheet\" href=\"/style.css\" /></head><body><header><h1>");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</h1>");
|
||||
if (@hasField(@TypeOf(d), "authenticated") and truthy(@field(d, "authenticated"))) {
|
||||
try o.appendSlice(a, "<span class=\"user\">Welcome back!</span>");
|
||||
}
|
||||
try o.appendSlice(a, "</header><main><p>This page is rendered using a compiled template.</p><p>Compiled templates are 3x faster than Pug.js!</p></main><footer><p>© 2024 Pugz Demo</p></footer></body></html>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<html><head><title>My Site - ");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script><script src=\"/pets.js\"></script></head><body><h1>");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</h1><p>Welcome to the pets page!</p><ul><li>Cat</li><li>Dog</li></ul><ul>");
|
||||
for (@field(d, "items")) |val| {
|
||||
try o.appendSlice(a, "<li>");
|
||||
try esc(&o, a, strVal(val));
|
||||
try o.appendSlice(a, "</li>");
|
||||
}
|
||||
try o.appendSlice(a, "</ul><input data-json=\"{ "very-long": "piece of ", "data": true }\" /><br /><div class=\"div-class\" (click)=\"play()\">one</div><div class=\"div-class\" (click)=\"play()\">two</div><a style=\"color:red;background:green;\">sdfsdfs</a><a class=\"button\">btn</a><br /><form method=\"post\">");
|
||||
{
|
||||
const name = "firstName";
|
||||
const label = "First Name";
|
||||
const placeholder = "first name";
|
||||
try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">");
|
||||
try esc(&o, a, strVal(label));
|
||||
try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\"");
|
||||
try o.appendSlice(a, " name=\"");
|
||||
try o.appendSlice(a, strVal(name));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, " placeholder=\"");
|
||||
try o.appendSlice(a, strVal(placeholder));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, " /></fieldset>");
|
||||
}
|
||||
try o.appendSlice(a, "<br />");
|
||||
{
|
||||
const name = "lastName";
|
||||
const label = "Last Name";
|
||||
const placeholder = "last name";
|
||||
try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">");
|
||||
try esc(&o, a, strVal(label));
|
||||
try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\"");
|
||||
try o.appendSlice(a, " name=\"");
|
||||
try o.appendSlice(a, strVal(name));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, " placeholder=\"");
|
||||
try o.appendSlice(a, strVal(placeholder));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, " /></fieldset>");
|
||||
}
|
||||
try o.appendSlice(a, "<submit>sumit</submit>");
|
||||
if (@hasField(@TypeOf(d), "error") and truthy(@field(d, "error"))) {
|
||||
{
|
||||
const message = @field(d, "error");
|
||||
{
|
||||
const mixin_attrs_1: struct {
|
||||
class: []const u8 = "",
|
||||
id: []const u8 = "",
|
||||
style: []const u8 = "",
|
||||
} = .{
|
||||
.class = "alert-error",
|
||||
};
|
||||
try o.appendSlice(a, "<div");
|
||||
try o.appendSlice(a, " class=\"");
|
||||
try o.appendSlice(a, "alert ");
|
||||
try o.appendSlice(a, strVal(mixin_attrs_1.class));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, " role=\"alert\"><svg class=\"h-6 w-6 shrink-0 stroke-current\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z\"></path></svg><span>");
|
||||
try esc(&o, a, strVal(message));
|
||||
try o.appendSlice(a, "</span></div>");
|
||||
}
|
||||
}
|
||||
}
|
||||
try o.appendSlice(a, "</form><div id=\"footer\"><p>some footer content</p></div></body></html>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn mixin_test(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>Mixin Test</title></head><body><h1>Mixin Test Page</h1><p>Testing button mixin:</p>");
|
||||
{
|
||||
const text = "Click Me";
|
||||
const @"type" = "primary";
|
||||
try o.appendSlice(a, "<button");
|
||||
try o.appendSlice(a, " class=\"");
|
||||
try o.appendSlice(a, "btn btn-");
|
||||
try o.appendSlice(a, strVal(@"type"));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, ">");
|
||||
try esc(&o, a, strVal(text));
|
||||
try o.appendSlice(a, "</button>");
|
||||
}
|
||||
{
|
||||
const text = "Cancel";
|
||||
const @"type" = "btn btn-secondary";
|
||||
try o.appendSlice(a, "<button");
|
||||
try o.appendSlice(a, " class=\"");
|
||||
try o.appendSlice(a, "btn btn-");
|
||||
try o.appendSlice(a, strVal(@"type"));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, ">");
|
||||
try esc(&o, a, strVal(text));
|
||||
try o.appendSlice(a, "</button>");
|
||||
}
|
||||
try o.appendSlice(a, "<p>Testing link mixin:</p>");
|
||||
{
|
||||
const href = "/home";
|
||||
const text = "Go Home";
|
||||
try o.appendSlice(a, "<a class=\"btn btn-link\"");
|
||||
try o.appendSlice(a, " href=\"");
|
||||
try o.appendSlice(a, strVal(href));
|
||||
try o.appendSlice(a, "\"");
|
||||
try o.appendSlice(a, ">");
|
||||
try esc(&o, a, strVal(text));
|
||||
try o.appendSlice(a, "</a>");
|
||||
}
|
||||
try o.appendSlice(a, "</body></html>");
|
||||
_ = d;
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn page_b(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<html><head><title>My Site - ");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div class=\"sidebar\"><p>nothing</p></div><div class=\"primary\"><p>nothing</p></div><div id=\"footer\"><p>some footer content</p></div></body></html>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn layout_2(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "<html><head><script src=\"/vendor/jquery.js\"></script><script src=\"/vendor/caustic.js\"></script></head><body></body></html>";
|
||||
}
|
||||
|
||||
pub fn layout(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<html><head><title>My Site - ");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div id=\"footer\"><p>some footer content</p></div></body></html>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn page_append(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
_ = .{ a, d };
|
||||
return "<html><head><script src=\"/vendor/jquery.js\"></script><script src=\"/vendor/caustic.js\"></script><script src=\"/vendor/three.js\"></script><script src=\"/game.js\"></script></head><body><p>cheks manually the head section<br />hello there</p></body></html>";
|
||||
}
|
||||
|
||||
pub fn users(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>Users</title></head><body><h1>User List</h1><ul class=\"user-list\">");
|
||||
for (@field(d, "users")) |user| {
|
||||
try o.appendSlice(a, "<li class=\"user\"><strong>");
|
||||
try esc(&o, a, strVal(user.name));
|
||||
try o.appendSlice(a, "</strong><span class=\"email\">");
|
||||
try esc(&o, a, strVal(user.email));
|
||||
try o.appendSlice(a, "</span></li>");
|
||||
}
|
||||
try o.appendSlice(a, "</ul></body></html>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn page_appen_optional_blk(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<html><head><title>My Site - ");
|
||||
try esc(&o, a, strVal(@field(d, "title")));
|
||||
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div id=\"footer\"><p>some footer content</p></div></body></html>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub fn pet(a: Allocator, d: anytype) Allocator.Error![]u8 {
|
||||
var o: ArrayList = .empty;
|
||||
try o.appendSlice(a, "<p>");
|
||||
try esc(&o, a, strVal(@field(d, "petName")));
|
||||
try o.appendSlice(a, "</p>");
|
||||
return o.items;
|
||||
}
|
||||
|
||||
pub const template_names = [_][]const u8{
|
||||
"index",
|
||||
"sub_layout",
|
||||
"_404",
|
||||
"mixins_alert",
|
||||
"mixins_buttons",
|
||||
"mixins_cards",
|
||||
"mixins_alert_error",
|
||||
"mixins_input_text",
|
||||
"home",
|
||||
"page_a",
|
||||
"mixin_test",
|
||||
"page_b",
|
||||
"layout_2",
|
||||
"layout",
|
||||
"page_append",
|
||||
"users",
|
||||
"page_appen_optional_blk",
|
||||
"pet",
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title #{title}
|
||||
link(rel="stylesheet" href="/style.css")
|
||||
body
|
||||
header
|
||||
h1 #{title}
|
||||
if authenticated
|
||||
span.user Welcome back!
|
||||
main
|
||||
p This page is rendered using a compiled template.
|
||||
p Compiled templates are 3x faster than Pug.js!
|
||||
footer
|
||||
p © 2024 Pugz Demo
|
||||
@@ -1,15 +0,0 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title hello
|
||||
body
|
||||
p some thing
|
||||
| ballah
|
||||
| ballah
|
||||
+btn("click me ", "secondary")
|
||||
br
|
||||
a(href='//google.com' target="_blank") Google 1
|
||||
br
|
||||
a(class='button' href='//google.com' target="_blank") Google 2
|
||||
br
|
||||
a(class='button', href='//google.com' target="_blank") Google 3
|
||||
@@ -1,7 +0,0 @@
|
||||
html
|
||||
head
|
||||
block head
|
||||
script(src='/vendor/jquery.js')
|
||||
script(src='/vendor/caustic.js')
|
||||
body
|
||||
block content
|
||||
@@ -1,10 +0,0 @@
|
||||
html
|
||||
head
|
||||
title My Site - #{title}
|
||||
block scripts
|
||||
script(src='/jquery.js')
|
||||
body
|
||||
block content
|
||||
block foot
|
||||
#footer
|
||||
p some footer content
|
||||
28
examples/demo/views/layouts/base.pug
Normal file
28
examples/demo/views/layouts/base.pug
Normal file
@@ -0,0 +1,28 @@
|
||||
doctype html
|
||||
html(lang="en")
|
||||
head
|
||||
meta(charset="UTF-8")
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1.0")
|
||||
link(rel="stylesheet" href="/css/style.css")
|
||||
block title
|
||||
title Pugz Store
|
||||
body
|
||||
header.header
|
||||
.container
|
||||
.header-content
|
||||
a.logo(href="/") Pugz Store
|
||||
nav.nav
|
||||
a.nav-link(href="/") Home
|
||||
a.nav-link(href="/products") Products
|
||||
a.nav-link(href="/about") About
|
||||
.header-actions
|
||||
a.cart-link(href="/cart")
|
||||
| Cart (#{cartCount})
|
||||
|
||||
main
|
||||
block content
|
||||
|
||||
footer.footer
|
||||
.container
|
||||
.footer-content
|
||||
p Built with Pugz - A Pug template engine for Zig
|
||||
@@ -1,15 +0,0 @@
|
||||
include mixins/buttons.pug
|
||||
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title Mixin Test
|
||||
body
|
||||
h1 Mixin Test Page
|
||||
|
||||
p Testing button mixin:
|
||||
+btn("Click Me")
|
||||
+btn("Cancel", "secondary")
|
||||
|
||||
p Testing link mixin:
|
||||
+btn-link("/home", "Go Home")
|
||||
@@ -1,5 +0,0 @@
|
||||
mixin alert(message)
|
||||
div.alert(role="alert" class!=attributes.class)
|
||||
svg(xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24")
|
||||
path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z")
|
||||
span= message
|
||||
@@ -1,2 +0,0 @@
|
||||
mixin alert_error(message)
|
||||
+alert(message)(class="alert-error")
|
||||
12
examples/demo/views/mixins/alerts.pug
Normal file
12
examples/demo/views/mixins/alerts.pug
Normal file
@@ -0,0 +1,12 @@
|
||||
//- Alert/notification mixins
|
||||
|
||||
mixin alert(message, type)
|
||||
- var alertClass = type ? "alert alert-" + type : "alert alert-info"
|
||||
.alert(class=alertClass)
|
||||
p= message
|
||||
|
||||
mixin alert-dismissible(message, type)
|
||||
- var alertClass = type ? "alert alert-" + type : "alert alert-info"
|
||||
.alert.alert-dismissible(class=alertClass)
|
||||
p= message
|
||||
button.alert-close(type="button" aria-label="Close") x
|
||||
@@ -1,5 +1,15 @@
|
||||
mixin btn(text, type="primary")
|
||||
button(class="btn btn-" + type)= text
|
||||
//- Button mixins with various styles
|
||||
|
||||
mixin btn-link(href, text)
|
||||
a.btn.btn-link(href=href)= text
|
||||
mixin btn(text, type)
|
||||
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
|
||||
button(class=btnClass)= text
|
||||
|
||||
mixin btn-link(href, text, type)
|
||||
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
|
||||
a(href=href class=btnClass)= text
|
||||
|
||||
mixin btn-icon(icon, text, type)
|
||||
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
|
||||
button(class=btnClass)
|
||||
span.icon= icon
|
||||
span= text
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
mixin card(title)
|
||||
.card
|
||||
.card-header
|
||||
h3= title
|
||||
.card-body
|
||||
block
|
||||
|
||||
mixin card-simple(title, body)
|
||||
.card
|
||||
h3= title
|
||||
p= body
|
||||
17
examples/demo/views/mixins/cart-item.pug
Normal file
17
examples/demo/views/mixins/cart-item.pug
Normal file
@@ -0,0 +1,17 @@
|
||||
//- Cart item display
|
||||
|
||||
mixin cart-item(item)
|
||||
.cart-item
|
||||
.cart-item-image
|
||||
img(src=item.image alt=item.name)
|
||||
.cart-item-details
|
||||
h4.cart-item-name #{item.name}
|
||||
p.cart-item-variant #{item.variant}
|
||||
span.cart-item-price $#{item.price}
|
||||
.cart-item-quantity
|
||||
button.qty-btn.qty-minus -
|
||||
input.qty-input(type="number" value=item.quantity min="1")
|
||||
button.qty-btn.qty-plus +
|
||||
.cart-item-total
|
||||
span $#{item.total}
|
||||
button.cart-item-remove(aria-label="Remove item") x
|
||||
25
examples/demo/views/mixins/forms.pug
Normal file
25
examples/demo/views/mixins/forms.pug
Normal file
@@ -0,0 +1,25 @@
|
||||
//- Form input mixins
|
||||
|
||||
mixin input(name, label, type, placeholder)
|
||||
.form-group
|
||||
label(for=name)= label
|
||||
input.form-control(type=type id=name name=name placeholder=placeholder)
|
||||
|
||||
mixin input-required(name, label, type, placeholder)
|
||||
.form-group
|
||||
label(for=name)
|
||||
= label
|
||||
span.required *
|
||||
input.form-control(type=type id=name name=name placeholder=placeholder required)
|
||||
|
||||
mixin select(name, label, options)
|
||||
.form-group
|
||||
label(for=name)= label
|
||||
select.form-control(id=name name=name)
|
||||
each opt in options
|
||||
option(value=opt.value)= opt.label
|
||||
|
||||
mixin textarea(name, label, placeholder, rows)
|
||||
.form-group
|
||||
label(for=name)= label
|
||||
textarea.form-control(id=name name=name placeholder=placeholder rows=rows)
|
||||
@@ -1,4 +0,0 @@
|
||||
mixin input_text(name, label, placeholder)
|
||||
fieldset.fieldset
|
||||
legend.fieldset-legend= label
|
||||
input(type="text" name=name class="input" placeholder=placeholder)
|
||||
38
examples/demo/views/mixins/product-card.pug
Normal file
38
examples/demo/views/mixins/product-card.pug
Normal file
@@ -0,0 +1,38 @@
|
||||
//- Product card mixin - displays a product in grid/list view
|
||||
//- Parameters:
|
||||
//- product: { id, name, price, image, rating, category }
|
||||
|
||||
mixin product-card(product)
|
||||
article.product-card
|
||||
a.product-image(href="/products/" + product.id)
|
||||
img(src=product.image alt=product.name)
|
||||
if product.sale
|
||||
span.badge.badge-sale Sale
|
||||
.product-info
|
||||
span.product-category #{product.category}
|
||||
h3.product-name
|
||||
a(href="/products/" + product.id) #{product.name}
|
||||
.product-rating
|
||||
+rating(product.rating)
|
||||
.product-footer
|
||||
span.product-price $#{product.price}
|
||||
button.btn.btn-primary.btn-sm(data-product=product.id) Add to Cart
|
||||
|
||||
//- Featured product card with larger display
|
||||
mixin product-featured(product)
|
||||
article.product-card.product-featured
|
||||
.product-image-large
|
||||
img(src=product.image alt=product.name)
|
||||
if product.sale
|
||||
span.badge.badge-sale Sale
|
||||
.product-details
|
||||
span.product-category #{product.category}
|
||||
h2.product-name #{product.name}
|
||||
p.product-description #{product.description}
|
||||
.product-rating
|
||||
+rating(product.rating)
|
||||
span.review-count (#{product.reviewCount} reviews)
|
||||
.product-price-large $#{product.price}
|
||||
.product-actions
|
||||
button.btn.btn-primary.btn-lg Add to Cart
|
||||
button.btn.btn-outline Wishlist
|
||||
13
examples/demo/views/mixins/rating.pug
Normal file
13
examples/demo/views/mixins/rating.pug
Normal file
@@ -0,0 +1,13 @@
|
||||
//- Star rating display
|
||||
//- Parameters:
|
||||
//- stars: number of stars (1-5)
|
||||
|
||||
mixin rating(stars)
|
||||
.stars
|
||||
- var i = 1
|
||||
while i <= 5
|
||||
if i <= stars
|
||||
span.star.star-filled
|
||||
else
|
||||
span.star.star-empty
|
||||
- i = i + 1
|
||||
@@ -1,36 +0,0 @@
|
||||
extends layout.pug
|
||||
|
||||
block scripts
|
||||
script(src='/jquery.js')
|
||||
script(src='/pets.js')
|
||||
|
||||
block content
|
||||
h1= title
|
||||
p Welcome to the pets page!
|
||||
ul
|
||||
li Cat
|
||||
li Dog
|
||||
ul
|
||||
each val in items
|
||||
li= val
|
||||
input(data-json=`
|
||||
{
|
||||
"very-long": "piece of ",
|
||||
"data": true
|
||||
}
|
||||
`)
|
||||
|
||||
br
|
||||
div(class='div-class', (click)='play()') one
|
||||
div(class='div-class' '(click)'='play()') two
|
||||
a(style={color: 'red', background: 'green'}) sdfsdfs
|
||||
a.button btn
|
||||
br
|
||||
|
||||
form(method="post")
|
||||
+input_text("firstName", "First Name", "first name")
|
||||
br
|
||||
+input_text("lastName", "Last Name", "last name")
|
||||
submit sumit
|
||||
if error
|
||||
+alert_error(error)
|
||||
@@ -1,5 +0,0 @@
|
||||
extends layout
|
||||
|
||||
append head
|
||||
script(src='/vendor/three.js')
|
||||
script(src='/game.js')
|
||||
@@ -1,11 +0,0 @@
|
||||
extends layout-2.pug
|
||||
|
||||
block append head
|
||||
script(src='/vendor/three.js')
|
||||
script(src='/game.js')
|
||||
|
||||
block content
|
||||
p
|
||||
| cheks manually the head section
|
||||
br
|
||||
| hello there
|
||||
@@ -1,9 +0,0 @@
|
||||
extends sub-layout.pug
|
||||
|
||||
block content
|
||||
.sidebar
|
||||
block sidebar
|
||||
p nothing
|
||||
.primary
|
||||
block primary
|
||||
p nothing
|
||||
15
examples/demo/views/pages/404.pug
Normal file
15
examples/demo/views/pages/404.pug
Normal file
@@ -0,0 +1,15 @@
|
||||
extends layouts/base.pug
|
||||
|
||||
block title
|
||||
title #{title} | Pugz Store
|
||||
|
||||
block content
|
||||
section.error-page
|
||||
.container
|
||||
.error-content
|
||||
h1.error-code 404
|
||||
h2 Page Not Found
|
||||
p The page you are looking for does not exist or has been moved.
|
||||
.error-actions
|
||||
a.btn.btn-primary(href="/") Go Home
|
||||
a.btn.btn-outline(href="/products") View Products
|
||||
51
examples/demo/views/pages/about.pug
Normal file
51
examples/demo/views/pages/about.pug
Normal file
@@ -0,0 +1,51 @@
|
||||
extends layouts/base.pug
|
||||
|
||||
block title
|
||||
title #{title} | Pugz Store
|
||||
|
||||
block content
|
||||
section.page-header
|
||||
.container
|
||||
h1 About Pugz
|
||||
p A Pug template engine written in Zig
|
||||
|
||||
section.section
|
||||
.container
|
||||
.about-grid
|
||||
.about-main
|
||||
h2 What is Pugz?
|
||||
p Pugz is a high-performance Pug template engine implemented in Zig. It provides both runtime interpretation and build-time compilation for maximum flexibility.
|
||||
|
||||
h3 Key Features
|
||||
ul.feature-list
|
||||
li Template inheritance with extends and blocks
|
||||
li Partial includes for modular templates
|
||||
li Mixins for reusable components
|
||||
li Conditionals (if/else/unless)
|
||||
li Iteration with each loops
|
||||
li Variable interpolation
|
||||
li Pretty-printed output
|
||||
li LRU caching with TTL
|
||||
|
||||
h3 Performance
|
||||
p Compiled templates run approximately 3x faster than Pug.js, with zero runtime parsing overhead.
|
||||
|
||||
.about-sidebar
|
||||
.info-card
|
||||
h3 This Demo Shows
|
||||
ul
|
||||
li Template inheritance (extends)
|
||||
li Named blocks
|
||||
li Conditional rendering
|
||||
li Variable interpolation
|
||||
li Simple iteration
|
||||
|
||||
.info-card
|
||||
h3 Links
|
||||
ul
|
||||
li
|
||||
a(href="https://github.com/ankitpatial/pugz") GitHub Repository
|
||||
li
|
||||
a(href="/products") View Products
|
||||
li
|
||||
a(href="/") Back to Home
|
||||
47
examples/demo/views/pages/cart.pug
Normal file
47
examples/demo/views/pages/cart.pug
Normal file
@@ -0,0 +1,47 @@
|
||||
extends layouts/base.pug
|
||||
|
||||
block title
|
||||
title #{title} | Pugz Store
|
||||
|
||||
block content
|
||||
section.page-header
|
||||
.container
|
||||
h1 Shopping Cart
|
||||
p Review your items before checkout
|
||||
|
||||
section.section
|
||||
.container
|
||||
.cart-layout
|
||||
.cart-main
|
||||
.cart-items
|
||||
each item in cartItems
|
||||
.cart-item
|
||||
.cart-item-info
|
||||
h3 #{name}
|
||||
p.text-muted #{variant}
|
||||
span.cart-item-price $#{price}
|
||||
.cart-item-qty
|
||||
button.qty-btn -
|
||||
input.qty-input(type="text" value=quantity)
|
||||
button.qty-btn +
|
||||
.cart-item-total $#{total}
|
||||
button.cart-item-remove x
|
||||
|
||||
.cart-actions
|
||||
a.btn.btn-outline(href="/products") Continue Shopping
|
||||
|
||||
.cart-summary
|
||||
h3 Order Summary
|
||||
.summary-row
|
||||
span Subtotal
|
||||
span $#{subtotal}
|
||||
.summary-row
|
||||
span Shipping
|
||||
span.text-success Free
|
||||
.summary-row
|
||||
span Tax
|
||||
span $#{tax}
|
||||
.summary-row.summary-total
|
||||
span Total
|
||||
span $#{total}
|
||||
a.btn.btn-primary.btn-block(href="/checkout") Proceed to Checkout
|
||||
144
examples/demo/views/pages/checkout.pug
Normal file
144
examples/demo/views/pages/checkout.pug
Normal file
@@ -0,0 +1,144 @@
|
||||
extends layouts/base.pug
|
||||
include mixins/forms.pug
|
||||
include mixins/alerts.pug
|
||||
include mixins/buttons.pug
|
||||
|
||||
block content
|
||||
h1 Checkout
|
||||
|
||||
if errors
|
||||
+alert("Please correct the errors below", "error")
|
||||
|
||||
.checkout-layout
|
||||
form.checkout-form(action="/checkout" method="POST")
|
||||
//- Shipping Information
|
||||
section.checkout-section
|
||||
h2 Shipping Information
|
||||
|
||||
.form-row
|
||||
+input-required("firstName", "First Name", "text", "John")
|
||||
+input-required("lastName", "Last Name", "text", "Doe")
|
||||
|
||||
+input-required("email", "Email Address", "email", "john@example.com")
|
||||
+input-required("phone", "Phone Number", "tel", "+1 (555) 123-4567")
|
||||
|
||||
+input-required("address", "Street Address", "text", "123 Main St")
|
||||
+input("address2", "Apartment, suite, etc.", "text", "Apt 4B")
|
||||
|
||||
.form-row
|
||||
+input-required("city", "City", "text", "New York")
|
||||
.form-group
|
||||
label(for="state")
|
||||
| State
|
||||
span.required *
|
||||
select.form-control#state(name="state" required)
|
||||
option(value="") Select State
|
||||
each state in states
|
||||
option(value=state.code)= state.name
|
||||
+input-required("zip", "ZIP Code", "text", "10001")
|
||||
|
||||
.form-group
|
||||
label(for="country")
|
||||
| Country
|
||||
span.required *
|
||||
select.form-control#country(name="country" required)
|
||||
option(value="US" selected) United States
|
||||
option(value="CA") Canada
|
||||
|
||||
//- Shipping Method
|
||||
section.checkout-section
|
||||
h2 Shipping Method
|
||||
|
||||
.shipping-options
|
||||
each method in shippingMethods
|
||||
label.shipping-option
|
||||
input(type="radio" name="shipping" value=method.id checked=method.id == "standard")
|
||||
.shipping-info
|
||||
span.shipping-name #{method.name}
|
||||
span.shipping-time #{method.time}
|
||||
span.shipping-price
|
||||
if method.price > 0
|
||||
| $#{method.price}
|
||||
else
|
||||
| Free
|
||||
|
||||
//- Payment Information
|
||||
section.checkout-section
|
||||
h2 Payment Information
|
||||
|
||||
.payment-methods-select
|
||||
label.payment-method
|
||||
input(type="radio" name="paymentMethod" value="card" checked)
|
||||
span Credit/Debit Card
|
||||
label.payment-method
|
||||
input(type="radio" name="paymentMethod" value="paypal")
|
||||
span PayPal
|
||||
|
||||
.card-details(id="card-details")
|
||||
+input-required("cardNumber", "Card Number", "text", "1234 5678 9012 3456")
|
||||
|
||||
.form-row
|
||||
+input-required("expiry", "Expiration Date", "text", "MM/YY")
|
||||
+input-required("cvv", "CVV", "text", "123")
|
||||
|
||||
+input-required("cardName", "Name on Card", "text", "John Doe")
|
||||
|
||||
.form-group
|
||||
label.checkbox-label
|
||||
input(type="checkbox" name="saveCard")
|
||||
span Save card for future purchases
|
||||
|
||||
//- Billing Address
|
||||
section.checkout-section
|
||||
.form-group
|
||||
label.checkbox-label
|
||||
input(type="checkbox" name="sameAsShipping" checked)
|
||||
span Billing address same as shipping
|
||||
|
||||
.billing-address(id="billing-address" style="display: none")
|
||||
+input-required("billingAddress", "Street Address", "text", "")
|
||||
.form-row
|
||||
+input-required("billingCity", "City", "text", "")
|
||||
+input-required("billingState", "State", "text", "")
|
||||
+input-required("billingZip", "ZIP Code", "text", "")
|
||||
|
||||
button.btn.btn-primary.btn-lg(type="submit") Place Order
|
||||
|
||||
//- Order Summary Sidebar
|
||||
aside.order-summary
|
||||
h3 Order Summary
|
||||
|
||||
.summary-items
|
||||
each item in cart.items
|
||||
.summary-item
|
||||
img(src=item.image alt=item.name)
|
||||
.item-info
|
||||
span.item-name #{item.name}
|
||||
span.item-qty x#{item.quantity}
|
||||
span.item-price $#{item.total}
|
||||
|
||||
.summary-details
|
||||
.summary-row
|
||||
span Subtotal
|
||||
span $#{cart.subtotal}
|
||||
|
||||
if cart.discount
|
||||
.summary-row.discount
|
||||
span Discount
|
||||
span -$#{cart.discount}
|
||||
|
||||
.summary-row
|
||||
span Shipping
|
||||
span#shipping-cost $#{selectedShipping.price}
|
||||
|
||||
.summary-row
|
||||
span Tax
|
||||
span $#{cart.tax}
|
||||
|
||||
.summary-row.total
|
||||
span Total
|
||||
span $#{cart.total}
|
||||
|
||||
.secure-checkout
|
||||
span Secure Checkout
|
||||
p Your information is protected with 256-bit SSL encryption
|
||||
56
examples/demo/views/pages/home.pug
Normal file
56
examples/demo/views/pages/home.pug
Normal file
@@ -0,0 +1,56 @@
|
||||
extends layouts/base.pug
|
||||
|
||||
block title
|
||||
title #{title} | Pugz Store
|
||||
|
||||
block content
|
||||
section.hero
|
||||
.container
|
||||
h1 Welcome to Pugz Store
|
||||
p Discover amazing products powered by Zig
|
||||
.hero-actions
|
||||
a.btn.btn-primary(href="/products") Shop Now
|
||||
a.btn.btn-outline(href="/about") Learn More
|
||||
|
||||
section.section
|
||||
.container
|
||||
h2 Template Features
|
||||
.feature-grid
|
||||
.feature-card
|
||||
h3 Conditionals
|
||||
if authenticated
|
||||
p.text-success You are logged in!
|
||||
else
|
||||
p.text-muted Please log in to continue.
|
||||
|
||||
.feature-card
|
||||
h3 Variables
|
||||
p Title: #{title}
|
||||
p Cart Items: #{cartCount}
|
||||
|
||||
.feature-card
|
||||
h3 Iteration
|
||||
ul
|
||||
each item in items
|
||||
li= item
|
||||
|
||||
.feature-card
|
||||
h3 Clean Syntax
|
||||
p Pug templates compile to HTML with minimal overhead.
|
||||
|
||||
section.section.section-alt
|
||||
.container
|
||||
h2 Shop by Category
|
||||
.category-grid
|
||||
a.category-card(href="/products?cat=electronics")
|
||||
.category-icon E
|
||||
h3 Electronics
|
||||
span 24 products
|
||||
a.category-card(href="/products?cat=accessories")
|
||||
.category-icon A
|
||||
h3 Accessories
|
||||
span 18 products
|
||||
a.category-card(href="/products?cat=home")
|
||||
.category-icon H
|
||||
h3 Home Office
|
||||
span 12 products
|
||||
65
examples/demo/views/pages/product-detail.pug
Normal file
65
examples/demo/views/pages/product-detail.pug
Normal file
@@ -0,0 +1,65 @@
|
||||
extends layouts/base.pug
|
||||
|
||||
block title
|
||||
title #{productName} | Pugz Store
|
||||
|
||||
block content
|
||||
section.page-header
|
||||
.container
|
||||
.breadcrumb
|
||||
a(href="/") Home
|
||||
span /
|
||||
a(href="/products") Products
|
||||
span /
|
||||
span #{productName}
|
||||
|
||||
section.section
|
||||
.container
|
||||
.product-detail
|
||||
.product-detail-image
|
||||
.product-image-placeholder
|
||||
.product-detail-info
|
||||
span.product-category #{category}
|
||||
h1 #{productName}
|
||||
.product-price-large $#{price}
|
||||
p.product-description #{description}
|
||||
|
||||
.product-actions
|
||||
.quantity-selector
|
||||
label Quantity:
|
||||
button.qty-btn -
|
||||
input.qty-input(type="text" value="1")
|
||||
button.qty-btn +
|
||||
a.btn.btn-primary.btn-lg(href="/cart") Add to Cart
|
||||
|
||||
.product-meta
|
||||
p SKU: #{sku}
|
||||
p Category: #{category}
|
||||
|
||||
section.section.section-alt
|
||||
.container
|
||||
h2 You May Also Like
|
||||
.product-grid
|
||||
.product-card
|
||||
.product-image
|
||||
.product-info
|
||||
span.product-category Electronics
|
||||
h3.product-name Smart Watch Pro
|
||||
.product-price $199.99
|
||||
a.btn.btn-sm(href="/products/2") View Details
|
||||
|
||||
.product-card
|
||||
.product-image
|
||||
.product-info
|
||||
span.product-category Accessories
|
||||
h3.product-name Laptop Stand
|
||||
.product-price $49.99
|
||||
a.btn.btn-sm(href="/products/3") View Details
|
||||
|
||||
.product-card
|
||||
.product-image
|
||||
.product-info
|
||||
span.product-category Accessories
|
||||
h3.product-name USB-C Hub
|
||||
.product-price $39.99
|
||||
a.btn.btn-sm(href="/products/4") View Details
|
||||
79
examples/demo/views/pages/products.pug
Normal file
79
examples/demo/views/pages/products.pug
Normal file
@@ -0,0 +1,79 @@
|
||||
extends layouts/base.pug
|
||||
|
||||
block title
|
||||
title #{title} | Pugz Store
|
||||
|
||||
block content
|
||||
section.page-header
|
||||
.container
|
||||
h1 All Products
|
||||
p Browse our selection of quality products
|
||||
|
||||
section.section
|
||||
.container
|
||||
.products-toolbar
|
||||
span.results-count #{productCount} products
|
||||
.sort-options
|
||||
label Sort by:
|
||||
select
|
||||
option(value="featured") Featured
|
||||
option(value="price-low") Price: Low to High
|
||||
option(value="price-high") Price: High to Low
|
||||
|
||||
.product-grid
|
||||
.product-card
|
||||
.product-image
|
||||
.product-badge Sale
|
||||
.product-info
|
||||
span.product-category Electronics
|
||||
h3.product-name Wireless Headphones
|
||||
.product-price $79.99
|
||||
a.btn.btn-sm(href="/products/1") View Details
|
||||
|
||||
.product-card
|
||||
.product-image
|
||||
.product-info
|
||||
span.product-category Electronics
|
||||
h3.product-name Smart Watch Pro
|
||||
.product-price $199.99
|
||||
a.btn.btn-sm(href="/products/2") View Details
|
||||
|
||||
.product-card
|
||||
.product-image
|
||||
.product-info
|
||||
span.product-category Accessories
|
||||
h3.product-name Laptop Stand
|
||||
.product-price $49.99
|
||||
a.btn.btn-sm(href="/products/3") View Details
|
||||
|
||||
.product-card
|
||||
.product-image
|
||||
.product-badge Sale
|
||||
.product-info
|
||||
span.product-category Accessories
|
||||
h3.product-name USB-C Hub
|
||||
.product-price $39.99
|
||||
a.btn.btn-sm(href="/products/4") View Details
|
||||
|
||||
.product-card
|
||||
.product-image
|
||||
.product-info
|
||||
span.product-category Electronics
|
||||
h3.product-name Mechanical Keyboard
|
||||
.product-price $129.99
|
||||
a.btn.btn-sm(href="/products/5") View Details
|
||||
|
||||
.product-card
|
||||
.product-image
|
||||
.product-info
|
||||
span.product-category Home Office
|
||||
h3.product-name Desk Lamp
|
||||
.product-price $34.99
|
||||
a.btn.btn-sm(href="/products/6") View Details
|
||||
|
||||
.pagination
|
||||
a.page-link(href="#") Prev
|
||||
a.page-link.active(href="#") 1
|
||||
a.page-link(href="#") 2
|
||||
a.page-link(href="#") 3
|
||||
a.page-link(href="#") Next
|
||||
4
examples/demo/views/partials/footer.pug
Normal file
4
examples/demo/views/partials/footer.pug
Normal file
@@ -0,0 +1,4 @@
|
||||
footer.footer
|
||||
.container
|
||||
.footer-content
|
||||
p Built with Pugz - A Pug template engine for Zig
|
||||
3
examples/demo/views/partials/head.pug
Normal file
3
examples/demo/views/partials/head.pug
Normal file
@@ -0,0 +1,3 @@
|
||||
meta(charset="UTF-8")
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1.0")
|
||||
link(rel="stylesheet" href="/css/style.css")
|
||||
11
examples/demo/views/partials/header.pug
Normal file
11
examples/demo/views/partials/header.pug
Normal file
@@ -0,0 +1,11 @@
|
||||
header.header
|
||||
.container
|
||||
.header-content
|
||||
a.logo(href="/") Pugz Store
|
||||
nav.nav
|
||||
a.nav-link(href="/") Home
|
||||
a.nav-link(href="/products") Products
|
||||
a.nav-link(href="/about") About
|
||||
.header-actions
|
||||
a.cart-link(href="/cart")
|
||||
| Cart (#{cartCount})
|
||||
@@ -1 +0,0 @@
|
||||
p= petName
|
||||
@@ -1,9 +0,0 @@
|
||||
extends layout.pug
|
||||
|
||||
block content
|
||||
.sidebar
|
||||
block sidebar
|
||||
p nothing
|
||||
.primary
|
||||
block primary
|
||||
p nothing
|
||||
@@ -1,11 +0,0 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title Users
|
||||
body
|
||||
h1 User List
|
||||
ul.user-list
|
||||
each user in users
|
||||
li.user
|
||||
strong= user.name
|
||||
span.email= user.email
|
||||
Reference in New Issue
Block a user