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:
@@ -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 = .{},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
mem.eql(u8, val, "''") or
|
||||
mem.eql(u8, val, "\"\"") or
|
||||
mem.eql(u8, val, "null") or
|
||||
mem.eql(u8, val, "undefined"))
|
||||
{
|
||||
continue;
|
||||
// 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");
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user