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,
|
.name = .pugz,
|
||||||
.version = "0.3.3",
|
.version = "0.3.4",
|
||||||
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
.fingerprint = 0x822db0790e17621d, // Changing this has security and trust implications.
|
||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{},
|
.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.
|
- 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.
|
- 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/`.
|
- **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
|
## Build Commands
|
||||||
|
|
||||||
|
|||||||
@@ -354,19 +354,31 @@ pub const Compiler = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn visitAttributes(self: *Compiler, tag: *Node) CompilerError!void {
|
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| {
|
for (tag.attrs.items) |attr| {
|
||||||
if (attr.val) |val| {
|
if (attr.val) |val| {
|
||||||
// Skip empty class/style attributes
|
// Check if value should be skipped (empty, null, undefined)
|
||||||
if (mem.eql(u8, attr.name, "class") or mem.eql(u8, attr.name, "style")) {
|
const should_skip = val.len == 0 or
|
||||||
// Skip if value is empty, null, or undefined
|
mem.eql(u8, val, "''") or
|
||||||
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, "\"\"") or
|
mem.eql(u8, val, "undefined");
|
||||||
mem.eql(u8, val, "null") or
|
|
||||||
mem.eql(u8, val, "undefined"))
|
if (mem.eql(u8, attr.name, "class")) {
|
||||||
{
|
// Collect class values to merge later
|
||||||
continue;
|
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
|
// Check for boolean attributes in terse mode
|
||||||
@@ -398,6 +410,19 @@ pub const Compiler = struct {
|
|||||||
try self.write(attr.name);
|
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 {
|
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)
|
substituteArgValue(attr.val, bindings)
|
||||||
else
|
else
|
||||||
attr.val;
|
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
|
// Collect class attributes for merging
|
||||||
if (std.mem.eql(u8, attr.name, "class")) {
|
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)
|
// Render attributes directly to output buffer (avoids intermediate allocations)
|
||||||
for (tag.attrs.items) |attr| {
|
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
|
// Collect class attributes for merging
|
||||||
if (std.mem.eql(u8, attr.name, "class")) {
|
if (std.mem.eql(u8, attr.name, "class")) {
|
||||||
|
|||||||
@@ -202,6 +202,18 @@ test "Implicit div with ID" {
|
|||||||
try expectOutput("#content", .{}, "<div id=\"content\"></div>");
|
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
|
// Test Case 10: &attributes spread operator
|
||||||
// TODO: &attributes spread with JS object literal not yet implemented
|
// TODO: &attributes spread with JS object literal not yet implemented
|
||||||
|
|||||||
Reference in New Issue
Block a user