diff --git a/build.zig.zon b/build.zig.zon index f47672d..af505d1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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 = .{}, diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 98d6b91..817bcaa 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -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 diff --git a/src/codegen.zig b/src/codegen.zig index 194f277..a784968 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -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 { diff --git a/src/template.zig b/src/template.zig index b1fb370..2011a74 100644 --- a/src/template.zig +++ b/src/template.zig @@ -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")) { diff --git a/src/tests/general_test.zig b/src/tests/general_test.zig index 96507a6..6347a02 100644 --- a/src/tests/general_test.zig +++ b/src/tests/general_test.zig @@ -202,6 +202,18 @@ test "Implicit div with ID" { try expectOutput("#content", .{}, "
"); } +test "Multiple classes merged into single attribute" { + try expectOutput("div.foo.bar.baz", .{}, ""); +} + +test "Multiple classes with text" { + try expectOutput(".a.b hello", .{}, "