fix: escape quotes in backtick strings and merge duplicate class attributes

- HTML-escape double quotes as " in backtick template literals for valid attribute values
- Merge shorthand classes (.alert) with class attribute values instead of emitting duplicates
- Handle string concatenation expressions in class attributes (e.g., class="btn btn-" + type)
This commit is contained in:
2026-01-23 11:50:18 +05:30
parent 3de712452c
commit b079bbffff
4 changed files with 672 additions and 8 deletions

2
.gitignore vendored
View File

@@ -6,7 +6,7 @@ zig-cache/
node_modules node_modules
# compiled template file # compiled template file
generated.zig #generated.zig
# IDE # IDE
.vscode/ .vscode/

View File

@@ -0,0 +1,284 @@
//! 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['<'] = "&lt;";
t['>'] = "&gt;";
t['"'] = "&quot;";
t['\''] = "&#x27;";
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 (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>&copy; 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=\"{ &quot;very-long&quot;: &quot;piece of &quot;, &quot;data&quot;: 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 (truthy(@field(d, "error"))) {
{
const message = @field(d, "error");
{
try o.appendSlice(a, "<div");
try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "alert ");
try o.appendSlice(a, strVal(@field(d, "attributes").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 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",
"page_b",
"layout_2",
"layout",
"page_append",
"users",
"page_appen_optional_blk",
"pet",
};

View File

@@ -0,0 +1,279 @@
//! 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['&'] = "&amp;";
t['<'] = "&lt;";
t['>'] = "&gt;";
t['"'] = "&quot;";
t['\''] = "&#x27;";
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 simple_2(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<div><h1 class=\"header\">");
try esc(&o, a, strVal(@field(d, "header")));
try o.appendSlice(a, "</h1><h2 class=\"header2\">");
try esc(&o, a, strVal(@field(d, "header2")));
try o.appendSlice(a, "</h2><h3 class=\"header3\">");
try esc(&o, a, strVal(@field(d, "header3")));
try o.appendSlice(a, "</h3><h4 class=\"header4\">");
try esc(&o, a, strVal(@field(d, "header4")));
try o.appendSlice(a, "</h4><h5 class=\"header5\">");
try esc(&o, a, strVal(@field(d, "header5")));
try o.appendSlice(a, "</h5><h6 class=\"header6\">");
try esc(&o, a, strVal(@field(d, "header6")));
try o.appendSlice(a, "</h6><ul class=\"list\">");
for (@field(d, "list")) |item| {
try o.appendSlice(a, "<li class=\"item\">");
try esc(&o, a, strVal(item));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul></div>");
return o.items;
}
pub fn simple_1(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<div class=\"simple-1\" style=\"background-color: blue; border: 1px solid black\"><div class=\"colors\"><span class=\"hello\">Hello ");
try esc(&o, a, strVal(@field(d, "name")));
try o.appendSlice(a, "!<strong>You have ");
try esc(&o, a, strVal(@field(d, "messageCount")));
try o.appendSlice(a, " messages!</strong></span>");
if (truthy(@field(d, "colors"))) {
try o.appendSlice(a, "<ul>");
for (@field(d, "colors")) |color| {
try o.appendSlice(a, "<li class=\"color\">");
try esc(&o, a, strVal(color));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul>");
} else {
try o.appendSlice(a, "<div>No colors!</div>");
}
try o.appendSlice(a, "</div>");
if (truthy(@field(d, "primary"))) {
try o.appendSlice(a, "<button class=\"primary\" type=\"button\">Click me!</button>");
} else {
try o.appendSlice(a, "<button class=\"secondary\" type=\"button\">Click me!</button>");
}
try o.appendSlice(a, "</div>");
return o.items;
}
pub fn simple_0(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<h1>Hello, ");
try esc(&o, a, strVal(@field(d, "name")));
try o.appendSlice(a, "</h1>");
return o.items;
}
pub fn projects_escaped(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></head><body><p>");
try esc(&o, a, strVal(@field(d, "text")));
try o.appendSlice(a, "</p>");
if (@field(d, "projects").len > 0) {
for (@field(d, "projects")) |project| {
try o.appendSlice(a, "<a");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(project.url));
try o.appendSlice(a, "\"");
try o.appendSlice(a, "></a><project class=\"name\"></project><p>");
try esc(&o, a, strVal(project.description));
try o.appendSlice(a, "</p>");
}
} else {
try o.appendSlice(a, "<p>No projects</p>");
}
try o.appendSlice(a, "</body></html>");
return o.items;
}
pub fn friends(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><title>Friends</title></head><body><div class=\"friends\">");
for (@field(d, "friends")) |friend| {
try o.appendSlice(a, "<div class=\"friend\"><ul><li>Name: ");
try esc(&o, a, strVal(friend.name));
try o.appendSlice(a, "</li><li>Balance: ");
try esc(&o, a, strVal(friend.balance));
try o.appendSlice(a, "</li><li>Age: ");
try esc(&o, a, strVal(friend.age));
try o.appendSlice(a, "</li><li>Address: ");
try esc(&o, a, strVal(friend.address));
try o.appendSlice(a, "</li><li>Image:<img");
try o.appendSlice(a, " src=\"");
try o.appendSlice(a, strVal(friend.picture));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " /></li><li>Company: ");
try esc(&o, a, strVal(friend.company));
try o.appendSlice(a, "</li><li>Email:<a");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(friend.emailHref));
try o.appendSlice(a, "\"");
try o.appendSlice(a, "></a><friend class=\"email\"></friend></li><li>About: ");
try esc(&o, a, strVal(friend.about));
try o.appendSlice(a, "</li>");
if (truthy(friend.tags)) {
try o.appendSlice(a, "<li>Tags:<ul>");
for (if (@typeInfo(@TypeOf(friend.tags)) == .optional) (friend.tags orelse &.{}) else friend.tags) |tag| {
try o.appendSlice(a, "<li>");
try esc(&o, a, strVal(tag));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul></li>");
}
if (truthy(friend.friends)) {
try o.appendSlice(a, "<li>Friends:<ul>");
for (if (@typeInfo(@TypeOf(friend.friends)) == .optional) (friend.friends orelse &.{}) else friend.friends) |subFriend| {
try o.appendSlice(a, "<li>");
try esc(&o, a, strVal(subFriend.name));
try o.appendSlice(a, " (");
try esc(&o, a, strVal(subFriend.id));
try o.appendSlice(a, ")</li>");
}
try o.appendSlice(a, "</ul></li>");
}
try o.appendSlice(a, "</ul></div>");
}
try o.appendSlice(a, "</div></body></html>");
return o.items;
}
pub fn search_results(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<div class=\"search-results view-gallery\">");
for (@field(d, "searchRecords")) |searchRecord| {
try o.appendSlice(a, "<div class=\"search-item\"><div class=\"search-item-container drop-shadow\"><div class=\"img-container\"><img");
try o.appendSlice(a, " src=\"");
try o.appendSlice(a, strVal(searchRecord.imgUrl));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " /></div><h4 class=\"title\"><a");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(searchRecord.viewItemUrl));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(searchRecord.title));
try o.appendSlice(a, "</a></h4>");
try esc(&o, a, strVal(searchRecord.description));
if (truthy(searchRecord.featured)) {
try o.appendSlice(a, "<div>Featured!</div>");
}
if (truthy(searchRecord.sizes)) {
try o.appendSlice(a, "<div>Sizes available:<ul>");
for (if (@typeInfo(@TypeOf(searchRecord.sizes)) == .optional) (searchRecord.sizes orelse &.{}) else searchRecord.sizes) |size| {
try o.appendSlice(a, "<li>");
try esc(&o, a, strVal(size));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul></div>");
}
try o.appendSlice(a, "</div></div>");
}
try o.appendSlice(a, "</div>");
return o.items;
}
pub fn if_expression(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
for (@field(d, "accounts")) |account| {
try o.appendSlice(a, "<div>");
if (std.mem.eql(u8, strVal(account.status), "closed")) {
try o.appendSlice(a, "<div>Your account has been closed!</div>");
}
if (std.mem.eql(u8, strVal(account.status), "suspended")) {
try o.appendSlice(a, "<div>Your account has been temporarily suspended</div>");
}
if (std.mem.eql(u8, strVal(account.status), "open")) {
try o.appendSlice(a, "<div>Bank balance:");
if (truthy(account.negative)) {
try o.appendSlice(a, "<span class=\"negative\">");
try esc(&o, a, strVal(account.balanceFormatted));
try o.appendSlice(a, "</span>");
} else {
try o.appendSlice(a, "<span class=\"positive\">");
try esc(&o, a, strVal(account.balanceFormatted));
try o.appendSlice(a, "</span>");
}
try o.appendSlice(a, "</div>");
}
try o.appendSlice(a, "</div>");
}
return o.items;
}
pub const template_names = [_][]const u8{
"simple_2",
"simple_1",
"simple_0",
"projects_escaped",
"friends",
"search_results",
"if_expression",
};

View File

@@ -702,6 +702,7 @@ const Compiler = struct {
/// Appends string content with normalized whitespace (for backtick template literals). /// Appends string content with normalized whitespace (for backtick template literals).
/// Collapses newlines and multiple spaces into single spaces, trims leading/trailing whitespace. /// Collapses newlines and multiple spaces into single spaces, trims leading/trailing whitespace.
/// Also HTML-escapes double quotes to &quot; for valid HTML attribute values.
fn appendNormalizedWhitespace(self: *Compiler, s: []const u8) !void { fn appendNormalizedWhitespace(self: *Compiler, s: []const u8) !void {
var in_whitespace = true; // Start true to skip leading whitespace var in_whitespace = true; // Start true to skip leading whitespace
for (s) |c| { for (s) |c| {
@@ -713,7 +714,8 @@ const Compiler = struct {
} else { } else {
const escaped: []const u8 = switch (c) { const escaped: []const u8 = switch (c) {
'\\' => "\\\\", '\\' => "\\\\",
'"' => "\\\"", // Escape double quotes as HTML entity for valid attribute values
'"' => "&quot;",
else => &[_]u8{c}, else => &[_]u8{c},
}; };
try self.buf.appendSlice(self.allocator, escaped); try self.buf.appendSlice(self.allocator, escaped);
@@ -776,16 +778,25 @@ const Compiler = struct {
try self.appendStatic("\""); try self.appendStatic("\"");
} }
if (e.classes.len > 0) { // Check if there's a class attribute that needs to be merged with shorthand classes
try self.appendStatic(" class=\""); var class_attr_value: ?[]const u8 = null;
for (e.classes, 0..) |cls, i| { var class_attr_escaped: bool = true;
if (i > 0) try self.appendStatic(" "); for (e.attributes) |attr| {
try self.appendStatic(cls); if (std.mem.eql(u8, attr.name, "class")) {
class_attr_value = attr.value;
class_attr_escaped = attr.escaped;
break;
} }
try self.appendStatic("\"");
} }
// Emit merged class attribute (shorthand classes + class attribute value)
if (e.classes.len > 0 or class_attr_value != null) {
try self.emitMergedClassAttribute(e.classes, class_attr_value, class_attr_escaped);
}
// Emit other attributes (skip class since we handled it above)
for (e.attributes) |attr| { for (e.attributes) |attr| {
if (std.mem.eql(u8, attr.name, "class")) continue; // Already handled
if (attr.value) |v| { if (attr.value) |v| {
try self.emitAttribute(attr.name, v, attr.escaped); try self.emitAttribute(attr.name, v, attr.escaped);
} else { } else {
@@ -822,6 +833,96 @@ const Compiler = struct {
try self.appendStatic(">"); try self.appendStatic(">");
} }
/// Emits a merged class attribute combining shorthand classes and class attribute value
fn emitMergedClassAttribute(self: *Compiler, shorthand_classes: []const []const u8, attr_value: ?[]const u8, escaped: bool) !void {
_ = escaped;
if (attr_value) |value| {
// Check for string concatenation first: "literal" + variable
if (findConcatOperator(value)) |concat_pos| {
// Has concatenation - need runtime handling
try self.flush();
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \" class=\\\"\");\n");
// Add shorthand classes first
if (shorthand_classes.len > 0) {
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \"");
for (shorthand_classes, 0..) |cls, i| {
if (i > 0) try self.writer.writeAll(" ");
try self.writer.writeAll(cls);
}
try self.writer.writeAll(" \");\n"); // trailing space before concat value
}
// Emit the concatenation expression
try self.emitConcatExpr(value, concat_pos);
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n");
return;
}
// Check if attribute value is static (string literal) or dynamic
const is_static = value.len >= 2 and (value[0] == '"' or value[0] == '\'' or value[0] == '`');
const is_array = value.len >= 2 and value[0] == '[' and value[value.len - 1] == ']';
if (is_static or is_array) {
// Static value - can merge at compile time
try self.appendStatic(" class=\"");
// First add shorthand classes
for (shorthand_classes, 0..) |cls, i| {
if (i > 0) try self.appendStatic(" ");
try self.appendStatic(cls);
}
// Then add attribute value
if (shorthand_classes.len > 0) try self.appendStatic(" ");
if (is_array) {
try self.appendStatic(parseArrayToSpaceSeparated(value));
} else if (value[0] == '`') {
try self.appendNormalizedWhitespace(value[1 .. value.len - 1]);
} else {
try self.appendStatic(value[1 .. value.len - 1]);
}
try self.appendStatic("\"");
} else {
// Dynamic value - need runtime concatenation
try self.flush();
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \" class=\\\"\");\n");
// Add shorthand classes first
if (shorthand_classes.len > 0) {
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \"");
for (shorthand_classes, 0..) |cls, i| {
if (i > 0) try self.writer.writeAll(" ");
try self.writer.writeAll(cls);
}
try self.writer.writeAll(" \");\n"); // trailing space before dynamic value
}
// Add dynamic value
var accessor_buf: [512]u8 = undefined;
const accessor = self.buildAccessor(value, &accessor_buf);
try self.writeIndent();
try self.writer.print("try o.appendSlice(a, strVal({s}));\n", .{accessor});
try self.writeIndent();
try self.writer.writeAll("try o.appendSlice(a, \"\\\"\");\n");
}
} else {
// No attribute value, just shorthand classes
try self.appendStatic(" class=\"");
for (shorthand_classes, 0..) |cls, i| {
if (i > 0) try self.appendStatic(" ");
try self.appendStatic(cls);
}
try self.appendStatic("\"");
}
}
fn emitText(self: *Compiler, segs: []const ast.TextSegment) anyerror!void { fn emitText(self: *Compiler, segs: []const ast.TextSegment) anyerror!void {
for (segs) |seg| { for (segs) |seg| {
switch (seg) { switch (seg) {