2026-01-17 18:32:29 +05:30
|
|
|
const std = @import("std");
|
2026-01-24 23:53:19 +05:30
|
|
|
const mem = std.mem;
|
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
|
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
// Pug Runtime - HTML generation utilities
|
|
|
|
|
// ============================================================================
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Escape HTML special characters in a string.
|
|
|
|
|
/// Characters escaped: " & < >
|
|
|
|
|
pub fn escape(allocator: Allocator, html: []const u8) ![]const u8 {
|
|
|
|
|
// Quick check if escaping is needed
|
|
|
|
|
var needs_escape = false;
|
|
|
|
|
for (html) |c| {
|
|
|
|
|
if (c == '"' or c == '&' or c == '<' or c == '>') {
|
|
|
|
|
needs_escape = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
if (!needs_escape) {
|
|
|
|
|
return try allocator.dupe(u8, html);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
var result: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer result.deinit(allocator);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
for (html) |c| {
|
|
|
|
|
switch (c) {
|
|
|
|
|
'"' => try result.appendSlice(allocator, """),
|
|
|
|
|
'&' => try result.appendSlice(allocator, "&"),
|
|
|
|
|
'<' => try result.appendSlice(allocator, "<"),
|
|
|
|
|
'>' => try result.appendSlice(allocator, ">"),
|
|
|
|
|
else => try result.append(allocator, c),
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
|
|
|
|
|
return try result.toOwnedSlice(allocator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Style value types
|
|
|
|
|
pub const StyleValue = union(enum) {
|
|
|
|
|
string: []const u8,
|
|
|
|
|
object: []const StyleProperty,
|
|
|
|
|
none,
|
2026-01-17 18:32:29 +05:30
|
|
|
};
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub const StyleProperty = struct {
|
|
|
|
|
name: []const u8,
|
|
|
|
|
value: []const u8,
|
2026-01-17 18:32:29 +05:30
|
|
|
};
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Convert a style value to a CSS string.
|
|
|
|
|
/// If val is an object, formats as "key:value;key:value;"
|
|
|
|
|
/// If val is a string, returns it as-is.
|
|
|
|
|
pub fn style(allocator: Allocator, val: StyleValue) ![]const u8 {
|
|
|
|
|
switch (val) {
|
|
|
|
|
.none => return try allocator.dupe(u8, ""),
|
|
|
|
|
.string => |s| {
|
|
|
|
|
if (s.len == 0) return try allocator.dupe(u8, "");
|
|
|
|
|
return try allocator.dupe(u8, s);
|
|
|
|
|
},
|
|
|
|
|
.object => |props| {
|
|
|
|
|
var result: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer result.deinit(allocator);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
for (props) |prop| {
|
|
|
|
|
try result.appendSlice(allocator, prop.name);
|
|
|
|
|
try result.append(allocator, ':');
|
|
|
|
|
try result.appendSlice(allocator, prop.value);
|
|
|
|
|
try result.append(allocator, ';');
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
return try result.toOwnedSlice(allocator);
|
|
|
|
|
},
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Attribute value types
|
|
|
|
|
pub const AttrValue = union(enum) {
|
|
|
|
|
string: []const u8,
|
|
|
|
|
boolean: bool,
|
|
|
|
|
number: i64,
|
|
|
|
|
none, // null/undefined equivalent
|
|
|
|
|
};
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Render a single HTML attribute.
|
|
|
|
|
/// Returns empty string for false/null values.
|
|
|
|
|
/// For true values, returns terse form " key" or full form " key="key"".
|
|
|
|
|
pub fn attr(allocator: Allocator, key: []const u8, val: AttrValue, escaped: bool, terse: bool) ![]const u8 {
|
2026-01-25 15:23:57 +05:30
|
|
|
var result: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer result.deinit(allocator);
|
|
|
|
|
try appendAttr(allocator, &result, key, val, escaped, terse);
|
|
|
|
|
if (result.items.len == 0) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
return try result.toOwnedSlice(allocator);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Append attribute directly to output buffer - avoids intermediate allocations
|
|
|
|
|
/// This is the preferred method for rendering attributes in hot paths
|
|
|
|
|
pub fn appendAttr(allocator: Allocator, output: *ArrayListUnmanaged(u8), key: []const u8, val: AttrValue, escaped: bool, terse: bool) !void {
|
2026-01-24 23:53:19 +05:30
|
|
|
switch (val) {
|
2026-01-25 15:23:57 +05:30
|
|
|
.none => return,
|
2026-01-24 23:53:19 +05:30
|
|
|
.boolean => |b| {
|
2026-01-25 15:23:57 +05:30
|
|
|
if (!b) return;
|
2026-01-24 23:53:19 +05:30
|
|
|
// true value
|
2026-01-25 15:23:57 +05:30
|
|
|
try output.append(allocator, ' ');
|
|
|
|
|
try output.appendSlice(allocator, key);
|
|
|
|
|
if (!terse) {
|
|
|
|
|
try output.appendSlice(allocator, "=\"");
|
|
|
|
|
try output.appendSlice(allocator, key);
|
|
|
|
|
try output.append(allocator, '"');
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
.number => |n| {
|
2026-01-25 15:23:57 +05:30
|
|
|
try output.append(allocator, ' ');
|
|
|
|
|
try output.appendSlice(allocator, key);
|
|
|
|
|
try output.appendSlice(allocator, "=\"");
|
2026-01-24 23:53:19 +05:30
|
|
|
|
2026-01-25 15:23:57 +05:30
|
|
|
// Format number directly to buffer
|
2026-01-24 23:53:19 +05:30
|
|
|
var buf: [32]u8 = undefined;
|
2026-01-25 15:23:57 +05:30
|
|
|
const num_str = std.fmt.bufPrint(&buf, "{d}", .{n}) catch return;
|
|
|
|
|
try output.appendSlice(allocator, num_str);
|
2026-01-24 23:53:19 +05:30
|
|
|
|
2026-01-25 15:23:57 +05:30
|
|
|
try output.append(allocator, '"');
|
2026-01-24 23:53:19 +05:30
|
|
|
},
|
|
|
|
|
.string => |s| {
|
2026-01-25 15:23:57 +05:30
|
|
|
// Skip empty class or style
|
2026-01-24 23:53:19 +05:30
|
|
|
if (s.len == 0 and (mem.eql(u8, key, "class") or mem.eql(u8, key, "style"))) {
|
2026-01-25 15:23:57 +05:30
|
|
|
return;
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-25 15:23:57 +05:30
|
|
|
try output.append(allocator, ' ');
|
|
|
|
|
try output.appendSlice(allocator, key);
|
|
|
|
|
try output.appendSlice(allocator, "=\"");
|
2026-01-24 23:53:19 +05:30
|
|
|
|
|
|
|
|
if (escaped) {
|
2026-01-25 15:23:57 +05:30
|
|
|
try appendEscaped(allocator, output, s);
|
2026-01-24 23:53:19 +05:30
|
|
|
} else {
|
2026-01-25 15:23:57 +05:30
|
|
|
try output.appendSlice(allocator, s);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-25 15:23:57 +05:30
|
|
|
try output.append(allocator, '"');
|
2026-01-24 23:53:19 +05:30
|
|
|
},
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Class value types for the classes function
|
|
|
|
|
pub const ClassValue = union(enum) {
|
|
|
|
|
string: []const u8,
|
|
|
|
|
array: []const ClassValue,
|
|
|
|
|
object: []const ClassCondition,
|
|
|
|
|
none,
|
2026-01-17 18:32:29 +05:30
|
|
|
};
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub const ClassCondition = struct {
|
2026-01-17 18:32:29 +05:30
|
|
|
name: []const u8,
|
2026-01-24 23:53:19 +05:30
|
|
|
condition: bool,
|
2026-01-17 18:32:29 +05:30
|
|
|
};
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Process class values into a space-delimited string.
|
|
|
|
|
/// Arrays are flattened, objects include keys with truthy values.
|
|
|
|
|
/// Optimized to minimize allocations by writing directly to result buffer.
|
|
|
|
|
pub fn classes(allocator: Allocator, val: ClassValue, escaping: ?[]const bool) ![]const u8 {
|
|
|
|
|
var result: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer result.deinit(allocator);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
try classesInternal(allocator, val, escaping, &result, 0);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
if (result.items.len == 0) {
|
|
|
|
|
result.deinit(allocator);
|
|
|
|
|
return try allocator.dupe(u8, "");
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
return try result.toOwnedSlice(allocator);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Internal recursive helper that writes directly to result buffer (avoids intermediate allocations)
|
|
|
|
|
fn classesInternal(
|
|
|
|
|
allocator: Allocator,
|
|
|
|
|
val: ClassValue,
|
|
|
|
|
escaping: ?[]const bool,
|
|
|
|
|
result: *ArrayListUnmanaged(u8),
|
|
|
|
|
depth: usize,
|
|
|
|
|
) !void {
|
|
|
|
|
switch (val) {
|
|
|
|
|
.none => {},
|
|
|
|
|
.string => |s| {
|
|
|
|
|
if (s.len == 0) return;
|
|
|
|
|
// Add space separator if not first item
|
|
|
|
|
if (result.items.len > 0) try result.append(allocator, ' ');
|
|
|
|
|
try result.appendSlice(allocator, s);
|
|
|
|
|
},
|
|
|
|
|
.object => |conditions| {
|
|
|
|
|
for (conditions) |cond| {
|
|
|
|
|
if (cond.condition and cond.name.len > 0) {
|
|
|
|
|
if (result.items.len > 0) try result.append(allocator, ' ');
|
|
|
|
|
try result.appendSlice(allocator, cond.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
.array => |items| {
|
|
|
|
|
for (items, 0..) |item, i| {
|
|
|
|
|
// Check if this item needs escaping (only at top level)
|
|
|
|
|
const should_escape = if (depth == 0) blk: {
|
|
|
|
|
break :blk if (escaping) |esc| (i < esc.len and esc[i]) else false;
|
|
|
|
|
} else false;
|
|
|
|
|
|
|
|
|
|
if (should_escape) {
|
|
|
|
|
// Need to escape: collect item first, then escape and append
|
|
|
|
|
const start_len = result.items.len;
|
|
|
|
|
const had_content = start_len > 0;
|
|
|
|
|
|
|
|
|
|
// Temporarily collect the class string
|
|
|
|
|
var temp: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
defer temp.deinit(allocator);
|
|
|
|
|
try classesInternal(allocator, item, null, &temp, depth + 1);
|
|
|
|
|
|
|
|
|
|
if (temp.items.len > 0) {
|
|
|
|
|
if (had_content) try result.append(allocator, ' ');
|
|
|
|
|
// Escape directly into result
|
|
|
|
|
try appendEscaped(allocator, result, temp.items);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No escaping: write directly to result
|
|
|
|
|
try classesInternal(allocator, item, null, result, depth + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Append escaped HTML directly to result buffer (avoids intermediate allocation)
|
|
|
|
|
/// Public for use by codegen and other modules
|
|
|
|
|
pub fn appendEscaped(allocator: Allocator, result: *ArrayListUnmanaged(u8), html: []const u8) !void {
|
|
|
|
|
for (html) |c| {
|
|
|
|
|
if (escapeChar(c)) |escaped| {
|
|
|
|
|
try result.appendSlice(allocator, escaped);
|
|
|
|
|
} else {
|
|
|
|
|
try result.append(allocator, c);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Escape a single character, returning the escape sequence or null if no escaping needed
|
|
|
|
|
pub fn escapeChar(c: u8) ?[]const u8 {
|
|
|
|
|
return switch (c) {
|
|
|
|
|
'"' => """,
|
|
|
|
|
'&' => "&",
|
|
|
|
|
'<' => "<",
|
|
|
|
|
'>' => ">",
|
|
|
|
|
else => null,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Attribute entry for attrs function
|
|
|
|
|
pub const AttrEntry = struct {
|
|
|
|
|
key: []const u8,
|
|
|
|
|
value: AttrValue,
|
|
|
|
|
is_class: bool = false,
|
|
|
|
|
is_style: bool = false,
|
|
|
|
|
class_value: ?ClassValue = null,
|
|
|
|
|
style_value: ?StyleValue = null,
|
|
|
|
|
};
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Render multiple attributes.
|
|
|
|
|
/// Class attributes are processed specially and placed first.
|
|
|
|
|
pub fn attrs(allocator: Allocator, entries: []const AttrEntry, terse: bool) ![]const u8 {
|
|
|
|
|
var result: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer result.deinit(allocator);
|
|
|
|
|
|
|
|
|
|
// First pass: find and render class attribute
|
|
|
|
|
for (entries) |entry| {
|
|
|
|
|
if (entry.is_class) {
|
|
|
|
|
if (entry.class_value) |cv| {
|
|
|
|
|
const class_str = try classes(allocator, cv, null);
|
|
|
|
|
defer allocator.free(class_str);
|
|
|
|
|
|
|
|
|
|
if (class_str.len > 0) {
|
|
|
|
|
const attr_str = try attr(allocator, "class", .{ .string = class_str }, false, terse);
|
|
|
|
|
defer allocator.free(attr_str);
|
|
|
|
|
try result.appendSlice(allocator, attr_str);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
break;
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Second pass: render other attributes
|
|
|
|
|
for (entries) |entry| {
|
|
|
|
|
if (entry.is_class) continue;
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
if (entry.is_style) {
|
|
|
|
|
if (entry.style_value) |sv| {
|
|
|
|
|
const style_str = try style(allocator, sv);
|
|
|
|
|
defer allocator.free(style_str);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
if (style_str.len > 0) {
|
|
|
|
|
const attr_str = try attr(allocator, "style", .{ .string = style_str }, false, terse);
|
|
|
|
|
defer allocator.free(attr_str);
|
|
|
|
|
try result.appendSlice(allocator, attr_str);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const attr_str = try attr(allocator, entry.key, entry.value, false, terse);
|
|
|
|
|
defer allocator.free(attr_str);
|
|
|
|
|
try result.appendSlice(allocator, attr_str);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
return try result.toOwnedSlice(allocator);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Merge entry for combining attribute objects
|
|
|
|
|
pub const MergeEntry = struct {
|
|
|
|
|
key: []const u8,
|
|
|
|
|
value: MergeValue,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pub const MergeValue = union(enum) {
|
|
|
|
|
string: []const u8,
|
|
|
|
|
class_array: []const []const u8,
|
|
|
|
|
style_object: []const StyleProperty,
|
|
|
|
|
none,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// Merge result for a single key
|
|
|
|
|
pub const MergedValue = struct {
|
|
|
|
|
key: []const u8,
|
|
|
|
|
value: MergeValue,
|
|
|
|
|
allocator: Allocator,
|
|
|
|
|
owned_strings: ArrayListUnmanaged([]const u8),
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub fn deinit(self: *MergedValue) void {
|
|
|
|
|
for (self.owned_strings.items) |s| {
|
|
|
|
|
self.allocator.free(s);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
self.owned_strings.deinit(self.allocator);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Ensure style string ends with semicolon
|
|
|
|
|
fn ensureTrailingSemicolon(allocator: Allocator, s: []const u8) ![]const u8 {
|
|
|
|
|
if (s.len == 0) return try allocator.dupe(u8, "");
|
|
|
|
|
if (s[s.len - 1] == ';') return try allocator.dupe(u8, s);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
var result: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer result.deinit(allocator);
|
|
|
|
|
try result.appendSlice(allocator, s);
|
|
|
|
|
try result.append(allocator, ';');
|
|
|
|
|
return try result.toOwnedSlice(allocator);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Convert style value to string with trailing semicolon
|
|
|
|
|
fn styleToString(allocator: Allocator, val: StyleValue) ![]const u8 {
|
|
|
|
|
const s = try style(allocator, val);
|
|
|
|
|
defer allocator.free(s);
|
|
|
|
|
return try ensureTrailingSemicolon(allocator, s);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
// Merge function
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/// Merged attributes result with O(1) lookups for class/style
|
|
|
|
|
pub const MergedAttrs = struct {
|
|
|
|
|
allocator: Allocator,
|
|
|
|
|
entries: ArrayListUnmanaged(MergedAttrEntry),
|
|
|
|
|
owned_strings: ArrayListUnmanaged([]const u8),
|
|
|
|
|
owned_class_arrays: ArrayListUnmanaged([][]const u8),
|
|
|
|
|
// O(1) index tracking for special keys
|
|
|
|
|
class_idx: ?usize = null,
|
|
|
|
|
style_idx: ?usize = null,
|
|
|
|
|
|
|
|
|
|
pub fn init(allocator: Allocator) MergedAttrs {
|
|
|
|
|
return .{
|
|
|
|
|
.allocator = allocator,
|
|
|
|
|
.entries = .{},
|
|
|
|
|
.owned_strings = .{},
|
|
|
|
|
.owned_class_arrays = .{},
|
|
|
|
|
.class_idx = null,
|
|
|
|
|
.style_idx = null,
|
|
|
|
|
};
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub fn deinit(self: *MergedAttrs) void {
|
|
|
|
|
for (self.owned_strings.items) |s| {
|
|
|
|
|
self.allocator.free(s);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
self.owned_strings.deinit(self.allocator);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
for (self.owned_class_arrays.items) |arr| {
|
|
|
|
|
self.allocator.free(arr);
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
self.owned_class_arrays.deinit(self.allocator);
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
self.entries.deinit(self.allocator);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub fn get(self: *const MergedAttrs, key: []const u8) ?MergedAttrValue {
|
|
|
|
|
// O(1) lookup for class and style
|
|
|
|
|
if (mem.eql(u8, key, "class")) {
|
|
|
|
|
if (self.class_idx) |idx| {
|
|
|
|
|
return self.entries.items[idx].value;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
if (mem.eql(u8, key, "style")) {
|
|
|
|
|
if (self.style_idx) |idx| {
|
|
|
|
|
return self.entries.items[idx].value;
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
return null;
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
// Linear search for other keys
|
|
|
|
|
for (self.entries.items) |entry| {
|
|
|
|
|
if (mem.eql(u8, entry.key, key)) {
|
|
|
|
|
return entry.value;
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
|
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
return null;
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Find index of a key (O(1) for class/style, O(n) for others)
|
|
|
|
|
fn findKey(self: *const MergedAttrs, key: []const u8) ?usize {
|
|
|
|
|
if (mem.eql(u8, key, "class")) return self.class_idx;
|
|
|
|
|
if (mem.eql(u8, key, "style")) return self.style_idx;
|
|
|
|
|
for (self.entries.items, 0..) |entry, i| {
|
|
|
|
|
if (mem.eql(u8, entry.key, key)) return i;
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub const MergedAttrEntry = struct {
|
|
|
|
|
key: []const u8,
|
|
|
|
|
value: MergedAttrValue,
|
|
|
|
|
};
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub const MergedAttrValue = union(enum) {
|
|
|
|
|
string: []const u8,
|
|
|
|
|
class_array: [][]const u8,
|
|
|
|
|
none,
|
|
|
|
|
};
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Merge two attribute objects.
|
|
|
|
|
/// class attributes are combined into arrays.
|
|
|
|
|
/// style attributes are concatenated with semicolons.
|
|
|
|
|
/// Optimized with O(1) lookups for class/style and branch prediction hints.
|
|
|
|
|
pub fn merge(allocator: Allocator, a: []const MergedAttrEntry, b: []const MergedAttrEntry) !MergedAttrs {
|
|
|
|
|
var result = MergedAttrs.init(allocator);
|
|
|
|
|
errdefer result.deinit();
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Pre-allocate capacity to avoid reallocations (cache-friendly)
|
|
|
|
|
const total_entries = a.len + b.len;
|
|
|
|
|
if (total_entries > 0) {
|
|
|
|
|
try result.entries.ensureTotalCapacity(allocator, total_entries);
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Process first object
|
|
|
|
|
for (a) |entry| {
|
|
|
|
|
try mergeEntry(&result, entry);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Process second object
|
|
|
|
|
for (b) |entry| {
|
|
|
|
|
try mergeEntry(&result, entry);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
return result;
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Fast key classification for branch prediction
|
|
|
|
|
const KeyType = enum { class, style, other };
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
inline fn classifyKey(key: []const u8) KeyType {
|
|
|
|
|
// Most common case: short keys that aren't class/style
|
|
|
|
|
// Use length check first (branch-friendly, avoids string compare)
|
|
|
|
|
if (key.len == 5) {
|
|
|
|
|
if (key[0] == 'c' and mem.eql(u8, key, "class")) return .class;
|
|
|
|
|
if (key[0] == 's' and mem.eql(u8, key, "style")) return .style;
|
|
|
|
|
}
|
|
|
|
|
return .other;
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
fn mergeEntry(result: *MergedAttrs, entry: MergedAttrEntry) !void {
|
|
|
|
|
const allocator = result.allocator;
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Branch prediction: classify key type once
|
|
|
|
|
const key_type = classifyKey(entry.key);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
switch (key_type) {
|
|
|
|
|
.class => {
|
|
|
|
|
// O(1) lookup using cached index
|
|
|
|
|
if (result.class_idx) |idx| {
|
|
|
|
|
@branchHint(.likely);
|
|
|
|
|
try mergeClassValue(result, idx, entry.value);
|
|
|
|
|
} else {
|
|
|
|
|
@branchHint(.unlikely);
|
|
|
|
|
try addNewClassEntry(result, entry.value);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
},
|
|
|
|
|
.style => {
|
|
|
|
|
// O(1) lookup using cached index
|
|
|
|
|
if (result.style_idx) |idx| {
|
|
|
|
|
@branchHint(.likely);
|
|
|
|
|
try mergeStyleValue(result, idx, entry.value);
|
2026-01-17 18:32:29 +05:30
|
|
|
} else {
|
2026-01-24 23:53:19 +05:30
|
|
|
@branchHint(.unlikely);
|
|
|
|
|
try addNewStyleEntry(result, entry.value);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
},
|
|
|
|
|
.other => {
|
|
|
|
|
// Regular attribute - linear search but rare in typical usage
|
|
|
|
|
const found_idx = result.findKey(entry.key);
|
|
|
|
|
if (found_idx) |idx| {
|
|
|
|
|
result.entries.items[idx].value = entry.value;
|
|
|
|
|
} else {
|
|
|
|
|
try result.entries.append(allocator, entry);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Merge a class value with existing class at index
|
|
|
|
|
fn mergeClassValue(result: *MergedAttrs, idx: usize, value: MergedAttrValue) !void {
|
|
|
|
|
const allocator = result.allocator;
|
|
|
|
|
const existing = result.entries.items[idx].value;
|
|
|
|
|
|
|
|
|
|
switch (value) {
|
|
|
|
|
.string => |s| {
|
|
|
|
|
switch (existing) {
|
|
|
|
|
.class_array => |arr| {
|
|
|
|
|
const new_arr = try allocator.alloc([]const u8, arr.len + 1);
|
|
|
|
|
@memcpy(new_arr[0..arr.len], arr);
|
|
|
|
|
new_arr[arr.len] = s;
|
|
|
|
|
try result.owned_class_arrays.append(allocator, new_arr);
|
|
|
|
|
result.entries.items[idx].value = .{ .class_array = new_arr };
|
2026-01-17 18:32:29 +05:30
|
|
|
},
|
2026-01-24 23:53:19 +05:30
|
|
|
.string => |existing_s| {
|
|
|
|
|
const new_arr = try allocator.alloc([]const u8, 2);
|
|
|
|
|
new_arr[0] = existing_s;
|
|
|
|
|
new_arr[1] = s;
|
|
|
|
|
try result.owned_class_arrays.append(allocator, new_arr);
|
|
|
|
|
result.entries.items[idx].value = .{ .class_array = new_arr };
|
|
|
|
|
},
|
|
|
|
|
.none => {
|
|
|
|
|
const new_arr = try allocator.alloc([]const u8, 1);
|
|
|
|
|
new_arr[0] = s;
|
|
|
|
|
try result.owned_class_arrays.append(allocator, new_arr);
|
|
|
|
|
result.entries.items[idx].value = .{ .class_array = new_arr };
|
2026-01-17 18:32:29 +05:30
|
|
|
},
|
|
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
},
|
|
|
|
|
.class_array => |arr| {
|
|
|
|
|
switch (existing) {
|
|
|
|
|
.class_array => |existing_arr| {
|
|
|
|
|
const new_arr = try allocator.alloc([]const u8, existing_arr.len + arr.len);
|
|
|
|
|
@memcpy(new_arr[0..existing_arr.len], existing_arr);
|
|
|
|
|
@memcpy(new_arr[existing_arr.len..], arr);
|
|
|
|
|
try result.owned_class_arrays.append(allocator, new_arr);
|
|
|
|
|
result.entries.items[idx].value = .{ .class_array = new_arr };
|
|
|
|
|
},
|
|
|
|
|
.string => |existing_s| {
|
|
|
|
|
const new_arr = try allocator.alloc([]const u8, 1 + arr.len);
|
|
|
|
|
new_arr[0] = existing_s;
|
|
|
|
|
@memcpy(new_arr[1..], arr);
|
|
|
|
|
try result.owned_class_arrays.append(allocator, new_arr);
|
|
|
|
|
result.entries.items[idx].value = .{ .class_array = new_arr };
|
|
|
|
|
},
|
|
|
|
|
.none => {
|
|
|
|
|
result.entries.items[idx].value = .{ .class_array = arr };
|
|
|
|
|
},
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
},
|
|
|
|
|
.none => {
|
|
|
|
|
// null class, convert existing to array if string
|
|
|
|
|
switch (existing) {
|
|
|
|
|
.string => |existing_s| {
|
|
|
|
|
const new_arr = try allocator.alloc([]const u8, 1);
|
|
|
|
|
new_arr[0] = existing_s;
|
|
|
|
|
try result.owned_class_arrays.append(allocator, new_arr);
|
|
|
|
|
result.entries.items[idx].value = .{ .class_array = new_arr };
|
|
|
|
|
},
|
|
|
|
|
else => {},
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Add a new class entry (first occurrence)
|
|
|
|
|
fn addNewClassEntry(result: *MergedAttrs, value: MergedAttrValue) !void {
|
|
|
|
|
const allocator = result.allocator;
|
|
|
|
|
switch (value) {
|
|
|
|
|
.string => |s| {
|
|
|
|
|
const new_arr = try allocator.alloc([]const u8, 1);
|
|
|
|
|
new_arr[0] = s;
|
|
|
|
|
try result.owned_class_arrays.append(allocator, new_arr);
|
|
|
|
|
result.class_idx = result.entries.items.len;
|
|
|
|
|
try result.entries.append(allocator, .{ .key = "class", .value = .{ .class_array = new_arr } });
|
|
|
|
|
},
|
|
|
|
|
.class_array => |arr| {
|
|
|
|
|
result.class_idx = result.entries.items.len;
|
|
|
|
|
try result.entries.append(allocator, .{ .key = "class", .value = .{ .class_array = arr } });
|
|
|
|
|
},
|
|
|
|
|
.none => {},
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Merge a style value with existing style at index
|
|
|
|
|
fn mergeStyleValue(result: *MergedAttrs, idx: usize, value: MergedAttrValue) !void {
|
|
|
|
|
const allocator = result.allocator;
|
|
|
|
|
const existing = result.entries.items[idx].value;
|
|
|
|
|
|
|
|
|
|
switch (value) {
|
|
|
|
|
.string => |s| {
|
|
|
|
|
switch (existing) {
|
|
|
|
|
.string => |existing_s| {
|
|
|
|
|
// Concatenate styles with semicolons
|
|
|
|
|
const s1 = try ensureTrailingSemicolon(allocator, existing_s);
|
|
|
|
|
defer allocator.free(s1);
|
|
|
|
|
const s2 = try ensureTrailingSemicolon(allocator, s);
|
|
|
|
|
defer allocator.free(s2);
|
|
|
|
|
|
|
|
|
|
var combined: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer combined.deinit(allocator);
|
|
|
|
|
try combined.appendSlice(allocator, s1);
|
|
|
|
|
try combined.appendSlice(allocator, s2);
|
|
|
|
|
const combined_str = try combined.toOwnedSlice(allocator);
|
|
|
|
|
try result.owned_strings.append(allocator, combined_str);
|
|
|
|
|
result.entries.items[idx].value = .{ .string = combined_str };
|
|
|
|
|
},
|
|
|
|
|
.none => {
|
|
|
|
|
const s_with_semi = try ensureTrailingSemicolon(allocator, s);
|
|
|
|
|
try result.owned_strings.append(allocator, s_with_semi);
|
|
|
|
|
result.entries.items[idx].value = .{ .string = s_with_semi };
|
|
|
|
|
},
|
|
|
|
|
else => {},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
.none => {
|
|
|
|
|
// null style, ensure existing has trailing semicolon
|
|
|
|
|
switch (existing) {
|
|
|
|
|
.string => |existing_s| {
|
|
|
|
|
const s_with_semi = try ensureTrailingSemicolon(allocator, existing_s);
|
|
|
|
|
try result.owned_strings.append(allocator, s_with_semi);
|
|
|
|
|
result.entries.items[idx].value = .{ .string = s_with_semi };
|
|
|
|
|
},
|
|
|
|
|
else => {},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
else => {},
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Add a new style entry (first occurrence)
|
|
|
|
|
fn addNewStyleEntry(result: *MergedAttrs, value: MergedAttrValue) !void {
|
|
|
|
|
const allocator = result.allocator;
|
|
|
|
|
switch (value) {
|
|
|
|
|
.string => |s| {
|
|
|
|
|
const s_with_semi = try ensureTrailingSemicolon(allocator, s);
|
|
|
|
|
try result.owned_strings.append(allocator, s_with_semi);
|
|
|
|
|
result.style_idx = result.entries.items.len;
|
|
|
|
|
try result.entries.append(allocator, .{ .key = "style", .value = .{ .string = s_with_semi } });
|
|
|
|
|
},
|
|
|
|
|
.none => {},
|
|
|
|
|
else => {},
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
// Rethrow function for error handling
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
pub const PugError = struct {
|
|
|
|
|
message: []const u8,
|
|
|
|
|
filename: ?[]const u8,
|
|
|
|
|
line: usize,
|
|
|
|
|
src: ?[]const u8,
|
|
|
|
|
formatted_message: ?[]const u8,
|
|
|
|
|
allocator: Allocator,
|
|
|
|
|
|
|
|
|
|
pub fn init(allocator: Allocator, err_message: []const u8, filename: ?[]const u8, line: usize, src: ?[]const u8) !PugError {
|
|
|
|
|
var pug_err = PugError{
|
|
|
|
|
.message = err_message,
|
|
|
|
|
.filename = filename,
|
|
|
|
|
.line = line,
|
|
|
|
|
.src = src,
|
|
|
|
|
.formatted_message = null,
|
|
|
|
|
.allocator = allocator,
|
|
|
|
|
};
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Format the error message with context
|
|
|
|
|
if (src) |s| {
|
|
|
|
|
pug_err.formatted_message = try formatErrorMessage(allocator, err_message, filename, line, s);
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
return pug_err;
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub fn deinit(self: *PugError) void {
|
|
|
|
|
if (self.formatted_message) |msg| {
|
|
|
|
|
self.allocator.free(msg);
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
pub fn getMessage(self: *const PugError) []const u8 {
|
|
|
|
|
if (self.formatted_message) |msg| {
|
|
|
|
|
return msg;
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
return self.message;
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
};
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
fn formatErrorMessage(allocator: Allocator, err_message: []const u8, filename: ?[]const u8, line: usize, src: []const u8) ![]const u8 {
|
|
|
|
|
var result: ArrayListUnmanaged(u8) = .{};
|
|
|
|
|
errdefer result.deinit(allocator);
|
|
|
|
|
|
|
|
|
|
// Add filename and line
|
|
|
|
|
if (filename) |f| {
|
|
|
|
|
try result.appendSlice(allocator, f);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
2026-01-24 23:53:19 +05:30
|
|
|
try result.append(allocator, ':');
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Format line number
|
|
|
|
|
var line_buf: [32]u8 = undefined;
|
|
|
|
|
const line_str = std.fmt.bufPrint(&line_buf, "{d}", .{line}) catch return error.FormatError;
|
|
|
|
|
try result.appendSlice(allocator, line_str);
|
|
|
|
|
try result.append(allocator, '\n');
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Split source into lines and show context
|
|
|
|
|
var lines_iter = mem.splitSequence(u8, src, "\n");
|
|
|
|
|
var line_num: usize = 1;
|
|
|
|
|
while (lines_iter.next()) |src_line| {
|
|
|
|
|
// Show lines around the error (context window)
|
|
|
|
|
const start_line = if (line > 3) line - 3 else 1;
|
|
|
|
|
const end_line = line + 3;
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
if (line_num >= start_line and line_num <= end_line) {
|
|
|
|
|
// Line number prefix
|
|
|
|
|
var num_buf: [32]u8 = undefined;
|
|
|
|
|
const num_str = std.fmt.bufPrint(&num_buf, "{d: >4}| ", .{line_num}) catch return error.FormatError;
|
|
|
|
|
try result.appendSlice(allocator, num_str);
|
|
|
|
|
try result.appendSlice(allocator, src_line);
|
|
|
|
|
try result.append(allocator, '\n');
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
line_num += 1;
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
if (line_num > end_line) break;
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// Add the original error message
|
|
|
|
|
try result.appendSlice(allocator, err_message);
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
return try result.toOwnedSlice(allocator);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
/// Rethrow an error with file context.
|
|
|
|
|
/// Creates a PugError with formatted message including source line context.
|
|
|
|
|
pub fn rethrow(allocator: Allocator, err_message: []const u8, filename: ?[]const u8, line: usize, src: ?[]const u8) !PugError {
|
|
|
|
|
return try PugError.init(allocator, err_message, filename, line, src);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
// Tests
|
|
|
|
|
// ============================================================================
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "escape - no escaping needed" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try escape(allocator, "foo");
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "escape - less than" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try escape(allocator, "foo<bar");
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo<bar", result);
|
|
|
|
|
}
|
2026-01-17 19:35:49 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "escape - ampersand and less than" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try escape(allocator, "foo&<bar");
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo&<bar", result);
|
|
|
|
|
}
|
2026-01-17 19:35:49 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "escape - all special chars" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try escape(allocator, "foo&<>\"bar\"");
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo&<>"bar"", result);
|
|
|
|
|
}
|
2026-01-17 19:35:49 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "style - empty string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try style(allocator, .{ .string = "" });
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 19:35:49 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "style - none" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try style(allocator, .none);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "style - string passthrough" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try style(allocator, .{ .string = "foo: bar" });
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo: bar", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "style - object" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const props = [_]StyleProperty{
|
|
|
|
|
.{ .name = "foo", .value = "bar" },
|
|
|
|
|
};
|
|
|
|
|
const result = try style(allocator, .{ .object = &props });
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo:bar;", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "style - object multiple" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const props = [_]StyleProperty{
|
|
|
|
|
.{ .name = "foo", .value = "bar" },
|
|
|
|
|
.{ .name = "baz", .value = "bash" },
|
|
|
|
|
};
|
|
|
|
|
const result = try style(allocator, .{ .object = &props });
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo:bar;baz:bash;", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean true terse" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = true }, true, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key", result);
|
|
|
|
|
}
|
2026-01-17 19:35:49 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean true not terse" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = true }, true, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"key\"", result);
|
|
|
|
|
}
|
2026-01-17 19:35:49 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = false }, true, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - none" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .none, true, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - number" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .number = 500 }, true, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"500\"", result);
|
|
|
|
|
}
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo" }, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo\"", result);
|
|
|
|
|
}
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - string escaped" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo>bar" }, true, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo>bar\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - empty class" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "class", .{ .string = "" }, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - empty style" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "style", .{ .string = "" }, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "classes - string array" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const items = [_]ClassValue{
|
|
|
|
|
.{ .string = "foo" },
|
|
|
|
|
.{ .string = "bar" },
|
|
|
|
|
};
|
|
|
|
|
const result = try classes(allocator, .{ .array = &items }, null);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo bar", result);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "classes - nested array" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const inner1 = [_]ClassValue{
|
|
|
|
|
.{ .string = "foo" },
|
|
|
|
|
.{ .string = "bar" },
|
|
|
|
|
};
|
|
|
|
|
const inner2 = [_]ClassValue{
|
|
|
|
|
.{ .string = "baz" },
|
|
|
|
|
.{ .string = "bash" },
|
|
|
|
|
};
|
|
|
|
|
const items = [_]ClassValue{
|
|
|
|
|
.{ .array = &inner1 },
|
|
|
|
|
.{ .array = &inner2 },
|
|
|
|
|
};
|
|
|
|
|
const result = try classes(allocator, .{ .array = &items }, null);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo bar baz bash", result);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "classes - object" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const conditions = [_]ClassCondition{
|
|
|
|
|
.{ .name = "baz", .condition = true },
|
|
|
|
|
.{ .name = "bash", .condition = false },
|
|
|
|
|
};
|
|
|
|
|
const result = try classes(allocator, .{ .object = &conditions }, null);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("baz", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "classes - mixed array and object" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const inner = [_]ClassValue{
|
|
|
|
|
.{ .string = "foo" },
|
|
|
|
|
.{ .string = "bar" },
|
|
|
|
|
};
|
|
|
|
|
const conditions = [_]ClassCondition{
|
|
|
|
|
.{ .name = "baz", .condition = true },
|
|
|
|
|
.{ .name = "bash", .condition = false },
|
|
|
|
|
};
|
|
|
|
|
const items = [_]ClassValue{
|
|
|
|
|
.{ .array = &inner },
|
|
|
|
|
.{ .object = &conditions },
|
|
|
|
|
};
|
|
|
|
|
const result = try classes(allocator, .{ .array = &items }, null);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo bar baz", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "classes - with escaping" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const inner = [_]ClassValue{
|
|
|
|
|
.{ .string = "fo<o" },
|
|
|
|
|
.{ .string = "bar" },
|
|
|
|
|
};
|
|
|
|
|
const conditions = [_]ClassCondition{
|
|
|
|
|
.{ .name = "ba>z", .condition = true },
|
|
|
|
|
.{ .name = "bash", .condition = false },
|
|
|
|
|
};
|
|
|
|
|
const items = [_]ClassValue{
|
|
|
|
|
.{ .array = &inner },
|
|
|
|
|
.{ .object = &conditions },
|
|
|
|
|
};
|
|
|
|
|
const escaping = [_]bool{ true, false };
|
|
|
|
|
const result = try classes(allocator, .{ .array = &items }, &escaping);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("fo<o bar ba>z", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - simple" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "foo", .value = .{ .string = "bar" } },
|
|
|
|
|
};
|
|
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" foo=\"bar\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - multiple" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "foo", .value = .{ .string = "bar" } },
|
|
|
|
|
.{ .key = "hoo", .value = .{ .string = "boo" } },
|
|
|
|
|
};
|
|
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" foo=\"bar\" hoo=\"boo\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - with class" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_items = [_]ClassValue{
|
|
|
|
|
.{ .string = "foo" },
|
|
|
|
|
.{ .object = &[_]ClassCondition{.{ .name = "bar", .condition = true }} },
|
|
|
|
|
};
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .none, .is_class = true, .class_value = .{ .array = &class_items } },
|
|
|
|
|
.{ .key = "foo", .value = .{ .string = "bar" } },
|
|
|
|
|
};
|
|
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" class=\"foo bar\" foo=\"bar\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - with style object" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const style_props = [_]StyleProperty{
|
|
|
|
|
.{ .name = "foo", .value = "bar" },
|
|
|
|
|
};
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .none, .is_style = true, .style_value = .{ .object = &style_props } },
|
|
|
|
|
};
|
|
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" style=\"foo:bar;\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
// Additional tests from index.test.js
|
|
|
|
|
// ============================================================================
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// attr tests - boolean combinations
|
|
|
|
|
test "attr - boolean true escaped=false terse=true" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = true }, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean true escaped=true terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = true }, true, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"key\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean true escaped=false terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = true }, false, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"key\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean false escaped=false terse=true" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = false }, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean false escaped=true terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = false }, true, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - boolean false escaped=false terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .boolean = false }, false, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - none escaped=false terse=true" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .none, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - none escaped=true terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .none, true, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - none escaped=false terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .none, false, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// attr number combinations
|
|
|
|
|
test "attr - number escaped=false terse=true" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .number = 500 }, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"500\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - number escaped=true terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .number = 500 }, true, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"500\"", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - number escaped=false terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .number = 500 }, false, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"500\"", result);
|
|
|
|
|
}
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// attr string combinations
|
|
|
|
|
test "attr - string escaped=true terse=true" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo" }, true, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo\"", result);
|
|
|
|
|
}
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - string escaped=true terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo" }, true, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo\"", result);
|
|
|
|
|
}
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - string escaped=false terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo" }, false, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo\"", result);
|
|
|
|
|
}
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - string with > escaped=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo>bar" }, false, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo>bar\"", result);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - string with > escaped=true terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo>bar" }, true, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo>bar\"", result);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attr - string with > escaped=false terse=false" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try attr(allocator, "key", .{ .string = "foo>bar" }, false, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" key=\"foo>bar\"", result);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// attrs tests
|
|
|
|
|
test "attrs - empty string value" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "foo", .value = .{ .string = "" } },
|
2026-01-22 11:10:47 +05:30
|
|
|
};
|
2026-01-24 23:53:19 +05:30
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" foo=\"\"", result);
|
|
|
|
|
}
|
2026-01-22 11:10:47 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - empty class" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .string = "" } },
|
2026-01-22 11:10:47 +05:30
|
|
|
};
|
2026-01-24 23:53:19 +05:30
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("", result);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - style string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "foo: bar;" } },
|
2026-01-24 14:31:24 +05:30
|
|
|
};
|
2026-01-24 23:53:19 +05:30
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" style=\"foo: bar;\"", result);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - class first then foo" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_items = [_]ClassValue{
|
|
|
|
|
.{ .string = "foo" },
|
|
|
|
|
.{ .object = &[_]ClassCondition{.{ .name = "bar", .condition = true }} },
|
2026-01-24 14:31:24 +05:30
|
|
|
};
|
2026-01-24 23:53:19 +05:30
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .none, .is_class = true, .class_value = .{ .array = &class_items } },
|
|
|
|
|
.{ .key = "foo", .value = .{ .string = "bar" } },
|
|
|
|
|
};
|
|
|
|
|
const result = try attrs(allocator, &entries, true);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings(" class=\"foo bar\" foo=\"bar\"", result);
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "attrs - foo then class reordered" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_items = [_]ClassValue{
|
|
|
|
|
.{ .string = "foo" },
|
|
|
|
|
.{ .object = &[_]ClassCondition{.{ .name = "bar", .condition = true }} },
|
|
|
|
|
};
|
|
|
|
|
const entries = [_]AttrEntry{
|
|
|
|
|
.{ .key = "foo", .value = .{ .string = "bar" } },
|
|
|
|
|
.{ .key = "class", .value = .none, .is_class = true, .class_value = .{ .array = &class_items } },
|
|
|
|
|
};
|
|
|
|
|
const result = try attrs(allocator, &entries, false);
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
// Class should come first even if listed second
|
|
|
|
|
try std.testing.expectEqualStrings(" class=\"foo bar\" foo=\"bar\"", result);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// style tests
|
|
|
|
|
test "style - string with trailing semicolon" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try style(allocator, .{ .string = "foo: bar;" });
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo: bar;", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// escape tests - additional
|
|
|
|
|
test "escape - ampersand less than greater than" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try escape(allocator, "foo&<>bar");
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo&<>bar", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "escape - ampersand less than greater than quote" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const result = try escape(allocator, "foo&<>\"bar");
|
|
|
|
|
defer allocator.free(result);
|
|
|
|
|
try std.testing.expectEqualStrings("foo&<>"bar", result);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
// Merge tests from index.test.js
|
|
|
|
|
// ============================================================================
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - simple merge" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "foo", .value = .{ .string = "bar" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "baz", .value = .{ .string = "bash" } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
try std.testing.expectEqual(@as(usize, 2), result.entries.items.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", result.get("foo").?.string);
|
|
|
|
|
try std.testing.expectEqualStrings("bash", result.get("baz").?.string);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - class string + class string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .string = "bar" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .string = "bash" } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const class_val = result.get("class").?;
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 2), class_val.class_array.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", class_val.class_array[0]);
|
|
|
|
|
try std.testing.expectEqualStrings("bash", class_val.class_array[1]);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - class array + class string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_arr = [_][]const u8{"bar"};
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .string = "bash" } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const class_val = result.get("class").?;
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 2), class_val.class_array.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", class_val.class_array[0]);
|
|
|
|
|
try std.testing.expectEqualStrings("bash", class_val.class_array[1]);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - class string + class array" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_arr = [_][]const u8{"bash"};
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .string = "bar" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const class_val = result.get("class").?;
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 2), class_val.class_array.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", class_val.class_array[0]);
|
|
|
|
|
try std.testing.expectEqualStrings("bash", class_val.class_array[1]);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - class string + class null" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .string = "bar" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .none },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const class_val = result.get("class").?;
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", class_val.class_array[0]);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - class null + class array" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_arr = [_][]const u8{"bar"};
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .none },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const class_val = result.get("class").?;
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", class_val.class_array[0]);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - empty + class array" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_arr = [_][]const u8{"bar"};
|
|
|
|
|
const a = [_]MergedAttrEntry{};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const class_val = result.get("class").?;
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", class_val.class_array[0]);
|
|
|
|
|
}
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - class array + empty" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const class_arr = [_][]const u8{"bar"};
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "class", .value = .{ .class_array = @constCast(&class_arr) } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-24 14:31:24 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const class_val = result.get("class").?;
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), class_val.class_array.len);
|
|
|
|
|
try std.testing.expectEqualStrings("bar", class_val.class_array[0]);
|
2026-01-24 14:31:24 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - style string + style string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "foo:bar" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "baz:bash" } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const style_val = result.get("style").?;
|
|
|
|
|
try std.testing.expectEqualStrings("foo:bar;baz:bash;", style_val.string);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - style with semicolon + style string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "foo:bar;" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "baz:bash" } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const style_val = result.get("style").?;
|
|
|
|
|
try std.testing.expectEqualStrings("foo:bar;baz:bash;", style_val.string);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - style string + style null" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "foo:bar" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .none },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const style_val = result.get("style").?;
|
|
|
|
|
try std.testing.expectEqualStrings("foo:bar;", style_val.string);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - style with semicolon + style null" {
|
2026-01-17 18:32:29 +05:30
|
|
|
const allocator = std.testing.allocator;
|
2026-01-24 23:53:19 +05:30
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "foo:bar;" } },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .none },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const style_val = result.get("style").?;
|
|
|
|
|
try std.testing.expectEqualStrings("foo:bar;", style_val.string);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - style null + style string" {
|
2026-01-17 18:32:29 +05:30
|
|
|
const allocator = std.testing.allocator;
|
2026-01-24 23:53:19 +05:30
|
|
|
const a = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .none },
|
|
|
|
|
};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "baz:bash" } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const style_val = result.get("style").?;
|
|
|
|
|
try std.testing.expectEqualStrings("baz:bash;", style_val.string);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "merge - empty + style string" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
const a = [_]MergedAttrEntry{};
|
|
|
|
|
const b = [_]MergedAttrEntry{
|
|
|
|
|
.{ .key = "style", .value = .{ .string = "baz:bash" } },
|
|
|
|
|
};
|
|
|
|
|
var result = try merge(allocator, &a, &b);
|
|
|
|
|
defer result.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
const style_val = result.get("style").?;
|
|
|
|
|
try std.testing.expectEqualStrings("baz:bash;", style_val.string);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
// Rethrow tests
|
|
|
|
|
// ============================================================================
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "rethrow - basic error without src" {
|
2026-01-17 18:32:29 +05:30
|
|
|
const allocator = std.testing.allocator;
|
2026-01-24 23:53:19 +05:30
|
|
|
var pug_err = try rethrow(allocator, "test error", "foo.pug", 3, null);
|
|
|
|
|
defer pug_err.deinit();
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
try std.testing.expectEqualStrings("test error", pug_err.getMessage());
|
|
|
|
|
try std.testing.expectEqualStrings("foo.pug", pug_err.filename.?);
|
|
|
|
|
try std.testing.expectEqual(@as(usize, 3), pug_err.line);
|
|
|
|
|
}
|
2026-01-17 18:32:29 +05:30
|
|
|
|
2026-01-24 23:53:19 +05:30
|
|
|
test "rethrow - error with src shows context" {
|
|
|
|
|
const allocator = std.testing.allocator;
|
|
|
|
|
var pug_err = try rethrow(allocator, "test error", "foo.pug", 1, "hello world");
|
|
|
|
|
defer pug_err.deinit();
|
|
|
|
|
|
|
|
|
|
const msg = pug_err.getMessage();
|
|
|
|
|
// Should contain filename:line, source line, and error message
|
|
|
|
|
try std.testing.expect(mem.indexOf(u8, msg, "foo.pug:1") != null);
|
|
|
|
|
try std.testing.expect(mem.indexOf(u8, msg, "hello world") != null);
|
|
|
|
|
try std.testing.expect(mem.indexOf(u8, msg, "test error") != null);
|
2026-01-17 18:32:29 +05:30
|
|
|
}
|