fix: merge multiple class attributes into single attribute

- codegen.zig: collect class values and output as single merged attribute
- template.zig: respect quoted flag to prevent data lookup for static class values
- Added tests for multiple class merging scenarios
This commit is contained in:
2026-01-29 22:04:59 +05:30
parent aa77a31809
commit 416ddf5b33
5 changed files with 62 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
.{
.name = .pugz,
.version = "0.3.3",
.version = "0.3.4",
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.dependencies = .{},

View File

@@ -12,6 +12,10 @@ Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It compiles Pug t
- When the user specifies a new rule, update this CLAUDE.md file to include it.
- Code comments are required but must be meaningful, not bloated. Focus on explaining "why" not "what". Avoid obvious comments like "// increment counter" - instead explain complex logic, non-obvious decisions, or tricky edge cases.
- **All documentation files (.md) must be saved to the `docs/` directory.** Do not create .md files in the root directory or examples directories - always place them in `docs/`.
- **Publish command**: When user says "publish", do the following:
1. Bump the fix version (patch version in build.zig.zon)
2. Git commit with appropriate message
3. Git push to remote `origin` and remote `github`
## Build Commands

View File

@@ -354,19 +354,31 @@ pub const Compiler = struct {
}
fn visitAttributes(self: *Compiler, tag: *Node) CompilerError!void {
// Collect class values to merge them into a single attribute
var class_values = std.ArrayListUnmanaged([]const u8){};
defer class_values.deinit(self.allocator);
// First pass: collect class values and output non-class attributes
for (tag.attrs.items) |attr| {
if (attr.val) |val| {
// Skip empty class/style attributes
if (mem.eql(u8, attr.name, "class") or mem.eql(u8, attr.name, "style")) {
// Skip if value is empty, null, or undefined
if (val.len == 0 or
// Check if value should be skipped (empty, null, undefined)
const should_skip = val.len == 0 or
mem.eql(u8, val, "''") or
mem.eql(u8, val, "\"\"") or
mem.eql(u8, val, "null") or
mem.eql(u8, val, "undefined"))
{
mem.eql(u8, val, "undefined");
if (mem.eql(u8, attr.name, "class")) {
// Collect class values to merge later
if (!should_skip) {
try class_values.append(self.allocator, val);
}
continue;
}
// Skip empty style attributes
if (mem.eql(u8, attr.name, "style") and should_skip) {
continue;
}
// Check for boolean attributes in terse mode
@@ -398,6 +410,19 @@ pub const Compiler = struct {
try self.write(attr.name);
}
}
// Output merged class attribute if any classes were collected
if (class_values.items.len > 0) {
try self.writeChar(' ');
try self.write("class=\"");
for (class_values.items, 0..) |class_val, i| {
if (i > 0) {
try self.writeChar(' ');
}
try self.write(class_val);
}
try self.writeChar('"');
}
}
fn visitText(self: *Compiler, text: *Node) CompilerError!void {

View File

@@ -288,7 +288,11 @@ fn renderTag(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), tag: *No
substituteArgValue(attr.val, bindings)
else
attr.val;
const attr_val = try evaluateAttrValue(allocator, final_val, data);
// Static/quoted values (e.g., from .class shorthand) should not be looked up in data
const attr_val = if (attr.quoted)
runtime.AttrValue{ .string = final_val orelse "" }
else
try evaluateAttrValue(allocator, final_val, data);
// Collect class attributes for merging
if (std.mem.eql(u8, attr.name, "class")) {
@@ -685,7 +689,11 @@ fn renderTagWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8),
// Render attributes directly to output buffer (avoids intermediate allocations)
for (tag.attrs.items) |attr| {
const attr_val = try evaluateAttrValue(allocator, attr.val, data);
// Static/quoted values (e.g., from .class shorthand) should not be looked up in data
const attr_val = if (attr.quoted)
runtime.AttrValue{ .string = attr.val orelse "" }
else
try evaluateAttrValue(allocator, attr.val, data);
// Collect class attributes for merging
if (std.mem.eql(u8, attr.name, "class")) {

View File

@@ -202,6 +202,18 @@ test "Implicit div with ID" {
try expectOutput("#content", .{}, "<div id=\"content\"></div>");
}
test "Multiple classes merged into single attribute" {
try expectOutput("div.foo.bar.baz", .{}, "<div class=\"foo bar baz\"></div>");
}
test "Multiple classes with text" {
try expectOutput(".a.b hello", .{}, "<div class=\"a b\">hello</div>");
}
test "Class attribute merged with shorthand classes" {
try expectOutput("div(class=\"foo\").bar.baz", .{}, "<div class=\"foo bar baz\"></div>");
}
// ─────────────────────────────────────────────────────────────────────────────
// Test Case 10: &attributes spread operator
// TODO: &attributes spread with JS object literal not yet implemented