fix: merge multiple class attributes in parser (single fix location)
- parser.zig: collect and merge all class values (shorthand and parenthesized) into single attribute - Filter out empty, null, undefined class values during parsing - Reverted redundant merging logic from codegen.zig, template.zig, zig_codegen.zig - Added documentation about shared AST consumers relationship
This commit is contained in:
@@ -354,31 +354,19 @@ 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| {
|
||||
// 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);
|
||||
// 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;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty style attributes
|
||||
if (mem.eql(u8, attr.name, "style") and should_skip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for boolean attributes in terse mode
|
||||
@@ -410,19 +398,6 @@ 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 {
|
||||
|
||||
103
src/parser.zig
103
src/parser.zig
@@ -1486,6 +1486,12 @@ pub const Parser = struct {
|
||||
var attribute_names = std.ArrayListUnmanaged([]const u8){};
|
||||
defer attribute_names.deinit(self.allocator);
|
||||
|
||||
// Collect class values to merge into single attribute
|
||||
var class_values = std.ArrayListUnmanaged([]const u8){};
|
||||
defer class_values.deinit(self.allocator);
|
||||
var class_line: usize = 0;
|
||||
var class_column: usize = 0;
|
||||
|
||||
// (attrs | class | id)*
|
||||
outer: while (true) {
|
||||
switch (self.peek().type) {
|
||||
@@ -1500,21 +1506,30 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
try attribute_names.append(self.allocator, "id");
|
||||
// ID: add directly to attrs
|
||||
const val_str = tok.val.getString() orelse "";
|
||||
const final_val = try self.allocator.dupe(u8, val_str);
|
||||
try tag.attrs.append(self.allocator, .{
|
||||
.name = "id",
|
||||
.val = final_val,
|
||||
.line = tok.loc.start.line,
|
||||
.column = tok.loc.start.column,
|
||||
.filename = self.filename,
|
||||
.must_escape = false,
|
||||
.val_owned = true,
|
||||
.quoted = true,
|
||||
});
|
||||
} else {
|
||||
// Class: collect for merging later
|
||||
const val_str = tok.val.getString() orelse "";
|
||||
if (val_str.len > 0) {
|
||||
try class_values.append(self.allocator, val_str);
|
||||
if (class_line == 0) {
|
||||
class_line = tok.loc.start.line;
|
||||
class_column = tok.loc.start.column;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Class/id values from shorthand are always static strings
|
||||
const val_str = tok.val.getString() orelse "";
|
||||
const final_val = try self.allocator.dupe(u8, val_str);
|
||||
|
||||
try tag.attrs.append(self.allocator, .{
|
||||
.name = if (tok.type == .id) "id" else "class",
|
||||
.val = final_val,
|
||||
.line = tok.loc.start.line,
|
||||
.column = tok.loc.start.column,
|
||||
.filename = self.filename,
|
||||
.must_escape = false,
|
||||
.val_owned = true, // We allocated this string
|
||||
.quoted = true, // Shorthand class/id are always static
|
||||
});
|
||||
},
|
||||
.start_attributes => {
|
||||
if (seen_attrs) {
|
||||
@@ -1523,7 +1538,32 @@ pub const Parser = struct {
|
||||
seen_attrs = true;
|
||||
var new_attrs = try self.attrs(&attribute_names);
|
||||
for (new_attrs.items) |attr| {
|
||||
try tag.attrs.append(self.allocator, attr);
|
||||
// Collect class attributes for merging
|
||||
if (mem.eql(u8, attr.name, "class")) {
|
||||
if (attr.val) |val| {
|
||||
// Skip empty, null, undefined values
|
||||
const should_skip = val.len == 0 or
|
||||
mem.eql(u8, val, "null") or
|
||||
mem.eql(u8, val, "undefined") or
|
||||
mem.eql(u8, val, "''") or
|
||||
mem.eql(u8, val, "\"\"");
|
||||
if (!should_skip) {
|
||||
try class_values.append(self.allocator, val);
|
||||
if (class_line == 0) {
|
||||
class_line = attr.line;
|
||||
class_column = attr.column;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Free owned value since we're collecting it
|
||||
if (attr.val_owned) {
|
||||
if (attr.val) |val| {
|
||||
self.allocator.free(val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try tag.attrs.append(self.allocator, attr);
|
||||
}
|
||||
}
|
||||
new_attrs.deinit(self.allocator);
|
||||
},
|
||||
@@ -1540,6 +1580,39 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Create single merged class attribute if any classes were collected
|
||||
if (class_values.items.len > 0) {
|
||||
// Calculate total length needed
|
||||
var total_len: usize = 0;
|
||||
for (class_values.items, 0..) |val, i| {
|
||||
total_len += val.len;
|
||||
if (i > 0) total_len += 1; // space separator
|
||||
}
|
||||
|
||||
// Build merged class string
|
||||
const merged = try self.allocator.alloc(u8, total_len);
|
||||
var pos: usize = 0;
|
||||
for (class_values.items, 0..) |val, i| {
|
||||
if (i > 0) {
|
||||
merged[pos] = ' ';
|
||||
pos += 1;
|
||||
}
|
||||
@memcpy(merged[pos .. pos + val.len], val);
|
||||
pos += val.len;
|
||||
}
|
||||
|
||||
try tag.attrs.append(self.allocator, .{
|
||||
.name = "class",
|
||||
.val = merged,
|
||||
.line = class_line,
|
||||
.column = class_column,
|
||||
.filename = self.filename,
|
||||
.must_escape = false,
|
||||
.val_owned = true,
|
||||
.quoted = true, // Merged classes are static
|
||||
});
|
||||
}
|
||||
|
||||
// Check for textOnly (.)
|
||||
if (self.peek().type == .dot) {
|
||||
tag.text_only = true;
|
||||
|
||||
@@ -277,10 +277,6 @@ fn renderTag(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), tag: *No
|
||||
try output.appendSlice(allocator, "<");
|
||||
try output.appendSlice(allocator, name);
|
||||
|
||||
// Collect class values separately to merge them into one attribute
|
||||
var class_parts = std.ArrayListUnmanaged([]const u8){};
|
||||
defer class_parts.deinit(allocator);
|
||||
|
||||
// Render attributes directly to output buffer (avoids intermediate allocations)
|
||||
for (tag.attrs.items) |attr| {
|
||||
// Substitute mixin arguments in attribute value if we're inside a mixin
|
||||
@@ -294,25 +290,7 @@ fn renderTag(allocator: Allocator, output: *std.ArrayListUnmanaged(u8), tag: *No
|
||||
else
|
||||
try evaluateAttrValue(allocator, final_val, data);
|
||||
|
||||
// Collect class attributes for merging
|
||||
if (std.mem.eql(u8, attr.name, "class")) {
|
||||
switch (attr_val) {
|
||||
.string => |s| if (s.len > 0) try class_parts.append(allocator, s),
|
||||
else => {},
|
||||
}
|
||||
} else {
|
||||
try runtime.appendAttr(allocator, output, attr.name, attr_val, true, ctx.terse);
|
||||
}
|
||||
}
|
||||
|
||||
// Output merged class attribute
|
||||
if (class_parts.items.len > 0) {
|
||||
try output.appendSlice(allocator, " class=\"");
|
||||
for (class_parts.items, 0..) |part, i| {
|
||||
if (i > 0) try output.append(allocator, ' ');
|
||||
try output.appendSlice(allocator, part);
|
||||
}
|
||||
try output.append(allocator, '"');
|
||||
try runtime.appendAttr(allocator, output, attr.name, attr_val, true, ctx.terse);
|
||||
}
|
||||
|
||||
// Self-closing logic differs by mode:
|
||||
@@ -683,10 +661,6 @@ fn renderTagWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8),
|
||||
try output.appendSlice(allocator, "<");
|
||||
try output.appendSlice(allocator, name);
|
||||
|
||||
// Collect class values separately to merge them into one attribute
|
||||
var class_parts = std.ArrayListUnmanaged([]const u8){};
|
||||
defer class_parts.deinit(allocator);
|
||||
|
||||
// Render attributes directly to output buffer (avoids intermediate allocations)
|
||||
for (tag.attrs.items) |attr| {
|
||||
// Static/quoted values (e.g., from .class shorthand) should not be looked up in data
|
||||
@@ -695,25 +669,7 @@ fn renderTagWithItem(allocator: Allocator, output: *std.ArrayListUnmanaged(u8),
|
||||
else
|
||||
try evaluateAttrValue(allocator, attr.val, data);
|
||||
|
||||
// Collect class attributes for merging
|
||||
if (std.mem.eql(u8, attr.name, "class")) {
|
||||
switch (attr_val) {
|
||||
.string => |s| if (s.len > 0) try class_parts.append(allocator, s),
|
||||
else => {},
|
||||
}
|
||||
} else {
|
||||
try runtime.appendAttr(allocator, output, attr.name, attr_val, true, ctx.terse);
|
||||
}
|
||||
}
|
||||
|
||||
// Output merged class attribute
|
||||
if (class_parts.items.len > 0) {
|
||||
try output.appendSlice(allocator, " class=\"");
|
||||
for (class_parts.items, 0..) |part, i| {
|
||||
if (i > 0) try output.append(allocator, ' ');
|
||||
try output.appendSlice(allocator, part);
|
||||
}
|
||||
try output.append(allocator, '"');
|
||||
try runtime.appendAttr(allocator, output, attr.name, attr_val, true, ctx.terse);
|
||||
}
|
||||
|
||||
const is_void = isSelfClosing(name);
|
||||
|
||||
@@ -224,69 +224,28 @@ pub const Codegen = struct {
|
||||
|
||||
if (!has_dynamic_attrs) {
|
||||
// All static attributes - include in buffer
|
||||
// Collect class values to merge into single attribute
|
||||
var class_values = std.ArrayListUnmanaged([]const u8){};
|
||||
defer class_values.deinit(self.allocator);
|
||||
|
||||
for (tag.attrs.items) |attr| {
|
||||
if (std.mem.eql(u8, attr.name, "class")) {
|
||||
// Collect class values for merging
|
||||
if (attr.val) |val| {
|
||||
if (val.len > 0) {
|
||||
try class_values.append(self.allocator, val);
|
||||
}
|
||||
}
|
||||
try self.addStatic(" ");
|
||||
try self.addStatic(attr.name);
|
||||
if (attr.val) |val| {
|
||||
try self.addStatic("=\"");
|
||||
try self.addStatic(val);
|
||||
try self.addStatic("\"");
|
||||
} else {
|
||||
try self.addStatic(" ");
|
||||
try self.addStatic(attr.name);
|
||||
if (attr.val) |val| {
|
||||
// Boolean attribute
|
||||
if (!self.terse) {
|
||||
try self.addStatic("=\"");
|
||||
try self.addStatic(val);
|
||||
try self.addStatic(attr.name);
|
||||
try self.addStatic("\"");
|
||||
} else {
|
||||
// Boolean attribute
|
||||
if (!self.terse) {
|
||||
try self.addStatic("=\"");
|
||||
try self.addStatic(attr.name);
|
||||
try self.addStatic("\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Output merged class attribute
|
||||
if (class_values.items.len > 0) {
|
||||
try self.addStatic(" class=\"");
|
||||
for (class_values.items, 0..) |class_val, i| {
|
||||
if (i > 0) {
|
||||
try self.addStatic(" ");
|
||||
}
|
||||
try self.addStatic(class_val);
|
||||
}
|
||||
try self.addStatic("\"");
|
||||
}
|
||||
|
||||
try self.addStatic(">");
|
||||
} else {
|
||||
// Flush static content before dynamic attributes (this closes any open string)
|
||||
try self.flushStaticBuffer();
|
||||
|
||||
// Collect static class values for merging
|
||||
var static_class_values = std.ArrayListUnmanaged([]const u8){};
|
||||
defer static_class_values.deinit(self.allocator);
|
||||
|
||||
// First pass: output non-class attributes
|
||||
for (tag.attrs.items) |attr| {
|
||||
// Skip class attributes - handle them separately
|
||||
if (std.mem.eql(u8, attr.name, "class")) {
|
||||
if (attr.val) |val| {
|
||||
if (val.len > 0) {
|
||||
try static_class_values.append(self.allocator, val);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attr.val) |val| {
|
||||
// Quoted values are always static, unquoted can be field references
|
||||
if (!attr.quoted and self.isDataFieldReference(val)) {
|
||||
@@ -344,19 +303,6 @@ pub const Codegen = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Output merged class attribute
|
||||
if (static_class_values.items.len > 0) {
|
||||
try self.writeIndent();
|
||||
try self.write("try buf.appendSlice(allocator, \" class=\\\"");
|
||||
for (static_class_values.items, 0..) |class_val, i| {
|
||||
if (i > 0) {
|
||||
try self.write(" ");
|
||||
}
|
||||
try self.writeEscaped(class_val);
|
||||
}
|
||||
try self.writeLine("\\\"\");");
|
||||
}
|
||||
|
||||
try self.writeIndent();
|
||||
try self.writeLine("try buf.appendSlice(allocator, \">\");");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user