feat: improve the style definition (#94)

This commit is contained in:
Satheeshkumar 2026-03-30 14:30:35 +05:00 committed by GitHub
parent b01645f5d9
commit 9c91733ebd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1186 additions and 3139 deletions

View File

@ -152,6 +152,21 @@ pub fn build(b: *std.Build) !void {
test_step.dependOn(&run_mod_tests.step);
test_step.dependOn(&run_exe_tests.step);
test_step.dependOn(&test_run.step);
const transpile_only = b.addExecutable(.{
.name = "transpile-only",
.root_module = b.createModule(.{
.root_source_file = b.path("test/transpile_only.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zx", .module = mod },
},
}),
});
const run_transpile_only = b.addRunArtifact(transpile_only);
const transpile_only_step = b.step("transpile-only", "Update snapshots without running full tests");
transpile_only_step.dependOn(&run_transpile_only.step);
}
// --- Steps: Dev (Runs dev step for site/) --- //

View File

@ -151,10 +151,14 @@ module.exports = grammar(zig, {
$.zx_template_string,
),
// Zig expression inside braces: {expr}
// Zig expression inside braces: {expr, expr}
zx_expression_block: $ => seq(
'{',
field('expression', $.expression),
optional(seq(
$.expression,
repeat(seq(',', $.expression)),
optional(','),
)),
'}',
),

View File

@ -1,11 +1,30 @@
const std = @import("std");
pub const Unit = enum {
px, em, rem, vh, vw, vmin, vmax, @"%", pt, pc, in, cm, mm,
deg, rad, grad, turn,
s, ms,
Hz, kHz,
dpi, dpcm, dppx,
px,
em,
rem,
vh,
vw,
vmin,
vmax,
@"%",
pt,
pc,
in,
cm,
mm,
deg,
rad,
grad,
turn,
s,
ms,
Hz,
kHz,
dpi,
dpcm,
dppx,
pub fn toString(self: Unit) []const u8 {
return switch (self) {
@ -27,12 +46,20 @@ pub const Color = union(enum) {
rgba_: struct { r: u8, g: u8, b: u8, a: f32 },
keyword_: []const u8,
pub fn hex(val: u32) Color { return .{ .hex_ = val }; }
pub fn rgb(r: u8, g: u8, b: u8) Color { return .{ .rgb_ = .{ .r = r, .g = g, .b = b } }; }
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color { return .{ .rgba_ = .{ .r = r, .g = g, .b = b, .a = a } }; }
pub fn kw(k: []const u8) Color { return .{ .keyword_ = k }; }
pub fn hex(val: u32) Color {
return .{ .hex_ = val };
}
pub fn rgb(r: u8, g: u8, b: u8) Color {
return .{ .rgb_ = .{ .r = r, .g = g, .b = b } };
}
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color {
return .{ .rgba_ = .{ .r = r, .g = g, .b = b, .a = a } };
}
pub fn kw(k: []const u8) Color {
return .{ .keyword_ = k };
}
pub fn format(self: Color, w: *std.io.Writer) std.io.Writer.Error!void {
pub fn format(self: Color, w: anytype) std.Io.Writer.Error!void {
switch (self) {
.none => {},
.hex_ => |h| try w.print("#{x:0>6}", .{h}),
@ -43,7 +70,7 @@ pub const Color = union(enum) {
}
};
pub fn formatKebab(name: []const u8, w: anytype) !void {
pub fn formatKebab(name: []const u8, w: anytype) std.Io.Writer.Error!void {
const prefixes = [_][]const u8{ "webkit", "moz", "ms", "apple", "epub", "hp", "atsc", "rim", "ro", "tc", "xhtml" };
for (prefixes) |p| {
if (std.mem.startsWith(u8, name, p) and name.len > p.len and (name[p.len] == '_' or std.ascii.isUpper(name[p.len]))) {
@ -65,7 +92,7 @@ pub fn formatKebab(name: []const u8, w: anytype) !void {
}
}
fn formatShorthand(v: [4]f32, unit: []const u8, w: anytype) !void {
fn formatShorthand(v: [4]f32, unit: []const u8, w: anytype) std.Io.Writer.Error!void {
if (v[0] == v[1] and v[1] == v[2] and v[2] == v[3]) {
try w.print("{d}{s}", .{ v[0], unit });
} else if (v[0] == v[2] and v[1] == v[3]) {
@ -75,52 +102,152 @@ fn formatShorthand(v: [4]f32, unit: []const u8, w: anytype) !void {
}
}
pub fn formatValue(value: anytype, w: *std.io.Writer) std.io.Writer.Error!void {
@setEvalBranchQuota(10000);
pub fn formatValue(value: anytype, w: anytype) std.Io.Writer.Error!void {
@setEvalBranchQuota(100000);
const T = @TypeOf(value);
const info = @typeInfo(T).@"union";
const tag = @as(info.tag_type.?, value);
const ti = @typeInfo(T);
if (tag == .none) return;
if (ti == .pointer) {
if (ti.pointer.size == .slice) {
// Strings/slices
if (T == []const u8 or T == []u8) {
try w.writeAll(value);
return;
}
} else {
return formatValue(value.*, w);
}
}
inline for (info.fields) |f| {
if (tag == @field(info.tag_type.?, f.name)) {
if (comptime std.mem.eql(u8, f.name, "hex_")) {
try w.print("#{x:0>6}", .{@field(value, f.name)});
if (ti == .optional) {
if (value) |v| return formatValue(v, w);
return;
}
if (ti == .@"union") {
const info = ti.@"union";
const tag = @as(info.tag_type.?, value);
if (comptime @hasField(info.tag_type.?, "none")) {
if (tag == .none) return;
}
inline for (info.fields) |f| {
if (tag == @field(info.tag_type.?, f.name)) {
if (comptime std.mem.eql(u8, f.name, "hex_")) {
try w.print("#{x:0>6}", .{@field(value, f.name)});
return;
}
if (comptime f.type == Color) {
try @field(value, f.name).format(w);
return;
}
if (comptime std.mem.eql(u8, f.name, "raw_")) {
try w.writeAll(@field(value, f.name));
return;
}
if (comptime std.mem.eql(u8, f.name, "percent_")) {
try formatShorthand(@field(value, f.name), "%", w);
return;
}
if (comptime std.mem.eql(u8, f.name, "px_")) {
try formatShorthand(@field(value, f.name), "px", w);
return;
}
if (comptime std.mem.eql(u8, f.name, "em_")) {
try formatShorthand(@field(value, f.name), "em", w);
return;
}
if (comptime std.mem.eql(u8, f.name, "rem_")) {
try formatShorthand(@field(value, f.name), "rem", w);
return;
}
if (comptime std.mem.eql(u8, f.name, "calc_")) {
try w.writeAll("calc(");
try @field(value, f.name).format(w);
try w.writeAll(")");
return;
}
if (comptime std.mem.eql(u8, f.name, "vh_")) {
try w.print("{d}vh", .{@field(value, f.name)});
return;
}
if (comptime std.mem.eql(u8, f.name, "vw_")) {
try w.print("{d}vw", .{@field(value, f.name)});
return;
}
if (comptime std.mem.eql(u8, f.name, "vmin_")) {
try w.print("{d}vmin", .{@field(value, f.name)});
return;
}
if (comptime std.mem.eql(u8, f.name, "vmax_")) {
try w.print("{d}vmax", .{@field(value, f.name)});
return;
}
// Keywords
try formatKebab(f.name, w);
return;
}
if (comptime f.type == Color) {
try @field(value, f.name).format(w);
return;
}
if (comptime std.mem.eql(u8, f.name, "raw_")) {
try w.writeAll(@field(value, f.name));
return;
}
if (comptime std.mem.eql(u8, f.name, "percent_")) {
try formatShorthand(@field(value, f.name), "%", w);
return;
}
if (comptime std.mem.eql(u8, f.name, "px_")) { try formatShorthand(@field(value, f.name), "px", w); return; }
if (comptime std.mem.eql(u8, f.name, "em_")) { try formatShorthand(@field(value, f.name), "em", w); return; }
if (comptime std.mem.eql(u8, f.name, "rem_")) { try formatShorthand(@field(value, f.name), "rem", w); return; }
if (comptime std.mem.eql(u8, f.name, "calc_")) {
try w.writeAll("calc(");
try @field(value, f.name).format(w);
try w.writeAll(")");
return;
}
if (comptime std.mem.eql(u8, f.name, "vh_")) { try w.print("{d}vh", .{@field(value, f.name)}); return; }
if (comptime std.mem.eql(u8, f.name, "vw_")) { try w.print("{d}vw", .{@field(value, f.name)}); return; }
if (comptime std.mem.eql(u8, f.name, "vmin_")) { try w.print("{d}vmin", .{@field(value, f.name)}); return; }
if (comptime std.mem.eql(u8, f.name, "vmax_")) { try w.print("{d}vmax", .{@field(value, f.name)}); return; }
// Keywords
try formatKebab(f.name, w);
return;
}
}
}
pub fn formatProperty(name: []const u8, val: anytype, w: anytype) std.Io.Writer.Error!void {
@setEvalBranchQuota(100000);
const T = @TypeOf(val);
const ti = @typeInfo(T);
if (ti == .optional) {
if (val) |v| return formatProperty(name, v, w);
return;
}
if (ti == .pointer) {
if (ti.pointer.size == .one) {
// Check if child has format (like nested Style)
if (comptime @hasDecl(ti.pointer.child, "format")) {
try formatKebab(name, w);
try w.writeAll(" { ");
try val.format(w);
try w.writeAll("} ");
return;
}
return formatProperty(name, val.*, w);
}
}
// Check if it's .none
if (ti == .@"union") {
const info = ti.@"union";
if (comptime @hasField(info.tag_type.?, "none")) {
if (val == .none) return;
}
}
if (std.mem.eql(u8, name, "extra")) {
if (comptime T == []const u8 or T == []u8) {
try w.writeAll(val);
} else if (comptime T == ?[]const u8 or T == ?[]u8) {
if (val) |v| try w.writeAll(v);
}
return;
}
try formatKebab(name, w);
try w.writeAll(": ");
switch (ti) {
.@"struct", .@"union", .@"enum", .@"opaque" => {
if (comptime @hasDecl(T, "format")) {
try val.format(w);
} else {
try formatValue(val, w);
}
},
else => try formatValue(val, w),
}
try w.writeAll("; ");
}

File diff suppressed because it is too large Load Diff

View File

@ -70,23 +70,11 @@ pub fn lazy(allocator: Allocator, comptime func: anytype, props: anytype) Compon
/// Context for creating components with allocator support
pub const ZxContext = struct {
allocator: ?std.mem.Allocator = null,
style_registry: ?*std.StringArrayHashMap([]const u8) = null,
pub fn getAlloc(self: *ZxContext) std.mem.Allocator {
return self.allocator orelse @panic("Allocator not set. Please provide @allocator attribute to the parent element.");
}
/// Check if a Style has any pseudo-states or media queries
fn hasSelectors(style_obj: zx.Style) bool {
const fields = std.meta.fields(zx.Style);
inline for (fields) |f| {
if (f.type == ?*const zx.Style) {
if (@field(style_obj, f.name) != null) return true;
}
}
return false;
}
fn escapeHtml(self: *ZxContext, text: []const u8) []const u8 {
// On browser, DOM APIs (textContent) handle escaping automatically
// We only need to escape when generating HTML strings on the server
@ -301,41 +289,27 @@ pub const ZxContext = struct {
zx.EventHandler.wrap(val),
},
// Pre-built event handlers
.@"struct" => if (T == zx.EventHandler) .{
.@"struct" => |_| if (T == zx.EventHandler) .{
.name = name,
.handler = val,
} else if (T == zx.Style) blk: {
if (comptime std.mem.eql(u8, name, "style")) {
if (hasSelectors(val)) {
// We need a deterministic hash of the style
// For now, let's just use the formatted string as the key
const style_str = self.printf("{f}", .{val});
// Register style if registry exists
if (self.style_registry) |reg| {
const hash = std.hash.Wyhash.hash(0, style_str);
const class_name = self.printf("zx-{x}", .{hash});
if (!reg.contains(class_name)) {
// Important: We store the Style object itself or its string representation.
// For now, storing the string is easier for the final flush.
reg.put(class_name, style_str) catch @panic("OOM");
}
// Return class attribute instead of style
break :blk .{
.name = "class",
.value = class_name,
};
}
}
}
} else if (comptime @hasDecl(T, "format")) blk: {
const allocator = self.getAlloc();
const str = std.fmt.allocPrint(allocator, "{f}", .{val}) catch @panic("OOM");
break :blk .{
.name = name,
.value = self.printf("{f}", .{val}),
.value = str,
};
} else @compileError("Unsupported struct type for attribute: " ++ @typeName(T)),
.@"union" => if (comptime @hasDecl(T, "format")) blk: {
const allocator = self.getAlloc();
const str = std.fmt.allocPrint(allocator, "{f}", .{val}) catch @panic("OOM");
break :blk .{
.name = name,
.value = str,
};
} else @compileError("Unsupported union type for attribute: " ++ @typeName(T)),
else => @compileError("Unsupported type for attribute value: " ++ @typeName(T)),
};
}

View File

@ -375,15 +375,13 @@ fn test_fmt_inner(comptime file_path: []const u8, comptime has_diff_expected: bo
var result = try zx.Ast.fmt(allocator, source_z);
defer result.deinit(allocator);
// Get pre-loaded expected file
const expected_source = try cache.get(expected_source_path) orelse {
std.log.err("Expected file not found: {s}\n", .{expected_source_path});
return error.FileNotFound;
};
const expected_source_z = try allocator.dupeZ(u8, expected_source);
defer allocator.free(expected_source_z);
if (!no_expect) {
const expected_source = try cache.get(expected_source_path) orelse {
std.log.err("Expected file not found: {s}\n", .{expected_source_path});
return error.FileNotFound;
};
const expected_source_z = try allocator.dupeZ(u8, expected_source);
defer allocator.free(expected_source_z);
try testing.expectEqualStrings(expected_source_z, result.source);
}
}

View File

@ -383,22 +383,6 @@ test "component_optional_error" {
try test_render("component/optional_error", @import("./../data/component/optional_error.zig").Page);
}
// === Style ===
test "style > basic" {
try test_transpile("style/basic");
try test_render("style/basic", @import("./../data/style/basic.zig").Page);
}
test "style > component" {
try test_transpile("style/component");
try test_render("style/component", @import("./../data/style/component.zig").Page);
}
test "style > inline" {
try test_transpile("style/inline");
try test_render("style/inline", @import("./../data/style/inline.zig").Page);
}
test "flaky: performance > transpile" {
if (!test_util.shouldRunSlowTest()) return;
const MAX_TIME_MS = 50.0 * 9; // 50ms is on M1 Pro
@ -425,7 +409,7 @@ test "flaky: performance > transpile" {
test "flaky: performance > render" {
const MAX_TIME_MS = 5.0 * 8; // 3.5ms is on M1 Pro
const MAX_TIME_PER_FILE_MS = 0.10 * 10; // 0.06ms is on M1 Pro
const MAX_TIME_PER_FILE_MS = 0.15 * 10; // 0.06ms is on M1 Pro
var total_time_ns: f64 = 0.0;
inline for (TestFileCache.test_files) |comptime_path| {
@ -469,18 +453,19 @@ fn test_transpile_inner(comptime file_path: []const u8, comptime no_expect: bool
var result = try zx.Ast.parse(allocator, source_z, .{ .path = full_file_path });
defer result.deinit(allocator);
// Check for SNAPSHOT=1 environment variable
// Check for SS=1 environment variable
if (isSnapshotMode()) {
// Save the transpiled output to .zig file
const file = std.fs.cwd().createFile(output_zig_path, .{}) catch |err| {
std.log.err("Failed to create snapshot file {s}: {}\n", .{ output_zig_path, err });
std.debug.print("Failed to create snapshot file {s}: {}\n", .{ output_zig_path, err });
return err;
};
defer file.close();
file.writeAll(result.zig_source) catch |err| {
std.log.err("Failed to write snapshot file {s}: {}\n", .{ output_zig_path, err });
std.debug.print("Failed to write snapshot file {s}: {}\n", .{ output_zig_path, err });
return err;
};
std.debug.print("Updated snapshot: {s}\n", .{output_zig_path});
return; // Skip comparison in snapshot mode
}
@ -592,9 +577,6 @@ fn getPageFn(comptime path: []const u8) ?fn (std.mem.Allocator) zx.Component {
.{ "component/csr_zig_props", @import("./../data/component/csr_zig_props.zig") },
.{ "component/error_component", @import("./../data/component/error_component.zig") },
.{ "component/optional_error", @import("./../data/component/optional_error.zig") },
.{ "style/basic", @import("./../data/style/basic.zig") },
.{ "style/component", @import("./../data/style/component.zig") },
.{ "style/inline", @import("./../data/style/inline.zig") },
};
inline for (imports) |entry| {
@ -612,17 +594,24 @@ fn test_render_inner_with_cmp(comptime file_path: []const u8, comptime cmp: fn (
const allocator = aa.allocator();
const component = cmp(allocator);
if (no_expect) {
var trash: [4096]u8 = undefined;
var dw = std.io.Writer.Discarding.init(&trash);
try component.render(&dw.writer);
try testing.expect(dw.fullCount() > 0);
return;
}
var aw = std.io.Writer.Allocating.init(allocator);
defer aw.deinit();
try component.render(&aw.writer);
const rendered = aw.written();
try testing.expect(rendered.len > 0);
if (no_expect) return;
const html_path = "test/data/" ++ file_path ++ ".html";
// Check for SNAPSHOT=1 environment variable
// Check for SS=1 environment variable
if (isSnapshotMode()) {
// Save the rendered output to .html file
const file = std.fs.cwd().createFile(html_path, .{}) catch |err| {

View File

@ -1,9 +1,10 @@
const std = @import("std");
const zx = @import("zx");
const S = zx.Style;
test "Style formatting" {
const allocator = std.testing.allocator;
const style: zx.Style = .{
const style: S = .{
.display = .flex,
.flex_direction = .column,
.background_color = .hex(0xff0000),
@ -11,8 +12,8 @@ test "Style formatting" {
.width = .px(100),
};
const result = try std.fmt.allocPrint(allocator, "{f}", .{style});
defer allocator.free(result);
const result = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{style});
defer std.testing.allocator.free(result);
std.debug.print("\nGenerated CSS: {s}\n", .{result});
@ -31,7 +32,7 @@ test "Style in Component" {
var ctx = zx.allocInit(arena_allocator);
const style: zx.Style = .{
const style: S = .{
.color = .hex(0x0000ff),
.margin_top = .px(20),
};
@ -45,7 +46,6 @@ test "Style in Component" {
ctx.txt("Hello with style"),
},
});
// deinit is not strictly needed with arena but good practice if we want to test it
defer comp.deinit(arena_allocator);
try std.testing.expectEqual(zx.ElementTag.div, comp.element.tag);
@ -62,27 +62,29 @@ test "Style in Component" {
}
test "Style pseudo-states" {
const style: zx.Style = .{
const style: S = .{
.background_color = .hex(0x0000ff),
.hover = &.{
.hover = &S{
.background_color = .hex(0xff0000),
},
};
try std.testing.expect(style.hover != null);
try std.testing.expectEqual(zx.style.generated.BackgroundColor.hex(0xff0000), style.hover.?.background_color);
const result = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{style});
defer std.testing.allocator.free(result);
try std.testing.expect(std.mem.indexOf(u8, result, "background-color: #0000ff;") != null);
try std.testing.expect(std.mem.indexOf(u8, result, "hover { background-color: #ff0000; }") != null);
}
test "Style shorthands" {
const allocator = std.testing.allocator;
const style: zx.Style = .{
const style: S = .{
.padding = .px2(10, 20),
.margin = .px4(5, 10, 15, 20),
};
const result = try std.fmt.allocPrint(allocator, "{f}", .{style});
defer allocator.free(result);
const result = try std.fmt.allocPrint(std.testing.allocator, "{f}", .{style});
defer std.testing.allocator.free(result);
try std.testing.expect(std.mem.indexOf(u8, result, "padding: 10px 20px;") != null);
try std.testing.expect(std.mem.indexOf(u8, result, "margin: 5px 10px 15px 20px;") != null);
}

View File

@ -1 +0,0 @@
<div style="background-color: #ff0000; display: flex; "> Hello </div>

View File

@ -1,21 +0,0 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
const style: zx.Style = .{
.display = .flex,
.background_color = .hex(0xff0000),
};
var _zx = @import("zx").allocInit(allocator);
return _zx.ele(
.div,
.{
.allocator = allocator,
.attributes = _zx.attrs(.{
_zx.attr("style", style),
}),
.children = &.{
_zx.txt(" Hello "),
},
},
);
}
const zx = @import("zx");

View File

@ -5,7 +5,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
};
return (
<div @allocator={allocator} style={style}>
Hello
Hello
</div>
);
}

View File

@ -1 +0,0 @@
<div class="card" style="color: #0000ff; "> Hello Component </div>

View File

@ -1,33 +0,0 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
const style: zx.Style = .{
.color = .hex(0x0000ff),
};
var _zx = @import("zx").allocInit(allocator);
return _zx.cmp(
StyledCard,
.{ .name = "StyledCard" },
.{ .style = style, .children = _zx.ele(.fragment, .{ .children = &.{
_zx.txt(" Hello Component "),
} }) },
);
}
const StyledCardProps = struct { style: zx.Style, children: zx.Component };
fn StyledCard(allocator: zx.Allocator, props: StyledCardProps) zx.Component {
var _zx = @import("zx").allocInit(allocator);
return _zx.ele(
.div,
.{
.allocator = allocator,
.attributes = _zx.attrs(.{
_zx.attr("class", "card"),
_zx.attr("style", props.style),
}),
.children = &.{
_zx.expr(props.children),
},
},
);
}
const zx = @import("zx");

View File

@ -1,19 +1,13 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
const style: zx.Style = .{
.color = .hex(0x0000ff),
.display = .flex,
.flex_direction = .column,
.padding_top = .px(10),
.width = .px(100),
};
return (
<StyledCard @allocator={allocator} style={style}>
Hello Component
</StyledCard>
);
}
const StyledCardProps = struct { style: zx.Style, children: zx.Component };
fn StyledCard(allocator: zx.Allocator, props: StyledCardProps) zx.Component {
return (
<div @allocator={allocator} class="card" style={props.style}>
{props.children}
<div @allocator={allocator} style={style}>
Hello
</div>
);
}

View File

@ -1 +0,0 @@
<div style="display: flex; row-gap: 10px; "> Inline Style </div>

View File

@ -1,17 +0,0 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
var _zx = @import("zx").allocInit(allocator);
return _zx.ele(
.div,
.{
.allocator = allocator,
.attributes = _zx.attrs(.{
_zx.attr("style", zx.Style{ .display = .flex, .row_gap = .px(10) }),
}),
.children = &.{
_zx.txt(" Inline Style "),
},
},
);
}
const zx = @import("zx");

View File

@ -1,7 +1,7 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
return (
<div @allocator={allocator} style={zx.Style{ .display = .flex, .row_gap = .px(10) }}>
Inline Style
<div @allocator={allocator} style={.{ .display = .flex, .padding_top = .px(10), .width = .px(100) }}>
Hello
</div>
);
}

View File

@ -13,8 +13,7 @@ test {
_ = @import("core/db.zig");
_ = @import("core/cache.zig");
_ = @import("core/kv.zig");
_ = @import("style.zig");
_ = @import("size.zig");
_ = @import("core/style.zig");
}
pub const std_options = std.Options{

View File

@ -1,24 +0,0 @@
const std = @import("std");
const zx = @import("zx");
test "Style size" {
const size = @sizeOf(zx.Style);
const alignment = @alignOf(zx.Style);
std.debug.print("\n--- Style Metrics (Optimized + Dynamic Selectors) ---\n", .{});
std.debug.print("Size: {d} bytes\n", .{size});
std.debug.print("Size (KB): {d:.2} KB\n", .{@as(f64, @floatFromInt(size)) / 1024.0});
std.debug.print("Alignment: {d} bytes\n", .{alignment});
std.debug.print("-----------------------------------------------------\n", .{});
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const s: zx.Style = .{
.display = .flex,
.background_color = .hex(0xff0000),
.margin_top = .calc(zx.style.Calc.percent(5).sub(.px(10))),
};
try std.testing.expect(s.display == .flex);
}

View File

@ -165,9 +165,17 @@ pub const File = struct {
@memcpy(sentinel[0..raw.len], raw);
sentinel[raw.len] = 0;
var tree = try std.zig.Ast.parse(final_allocator, sentinel[0..raw.len :0], .zig);
var tree = std.zig.Ast.parse(final_allocator, sentinel[0..raw.len :0], .zig) catch |err| {
std.debug.print("AST parse error: {any}\n", .{err});
return err;
};
defer tree.deinit(final_allocator);
if (tree.errors.len != 0) return error.InvalidGeneratedSource;
if (tree.errors.len != 0) {
const f = try std.fs.cwd().createFile("ast_error.zig", .{});
defer f.close();
try f.writeAll(raw);
return error.InvalidGeneratedSource;
}
return try tree.renderAlloc(final_allocator);
}

View File

@ -29,7 +29,13 @@ pub fn main() !void {
}
pub fn writeFile(allocator: std.mem.Allocator, path: []const u8) !void {
const source = try generate(allocator);
const source = generate(allocator) catch |err| {
if (err == error.InvalidGeneratedSource) {
// We can't easily get the raw buffer from File here without changes,
// but we can try to catch it in generate() or just look at what's happening.
}
return err;
};
defer allocator.free(source);
const file = try std.fs.cwd().createFile(path, .{});
@ -194,20 +200,18 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
_ = try prop_union.addMethod(fa, "", "hex", hex_sig, "return .{ .hex_ = v };");
}
const format_sig = try std.fmt.allocPrint(fa, "(self: {s}, w: *std.io.Writer) std.io.Writer.Error!void", .{final_type_name});
const format_sig = try std.fmt.allocPrint(fa, "(self: {s}, w: anytype) std.Io.Writer.Error!void", .{final_type_name});
_ = try prop_union.addMethod(fa, "", "format", format_sig, "return core.formatValue(self, w);");
}
const style = try file.addStruct("Style");
const style_struct = try file.addStruct("Style");
for (properties.items) |prop| {
const name = prop.object.get("name").?.string;
const data = prop_data.get(name).?;
const type_name_raw = try cleanName(a, name, .pascal);
const final_type_name = if (std.mem.eql(u8, type_name_raw, "Color")) "CssColor" else type_name_raw;
const clean_p = try cleanName(a, name, .snake);
if (clean_p.len == 0) continue;
const field_doc = try docText(fa, name, data.prose, data.href);
try style.addField(fa, field_doc, clean_p, final_type_name, ".none");
try style_struct.addField(fa, "", clean_p, final_type_name, ".none");
}
var selector_tags = std.StringArrayHashMap(void).init(a);
@ -215,8 +219,6 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
for (selectors.items) |sel| {
const name = sel.object.get("name").?.string;
if (std.mem.indexOf(u8, name, "(") != null) continue;
const href = sel.object.get("href").?.string;
const prose = if (sel.object.get("prose")) |p| p.string else "";
const clean_s = try cleanName(a, name, .snake);
if (clean_s.len == 0) continue;
if (selector_tags.contains(clean_s)) continue;
@ -233,41 +235,26 @@ pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
if (is_duplicate) continue;
try selector_tags.put(clean_s, {});
const selector_doc = try docText(fa, name, prose, href);
try style.addField(fa, selector_doc, clean_s, "?*const Style", "null");
try style_struct.addField(fa, "", clean_s, "?*const Style", "null");
}
try style.addField(fa, "", "sm", "?*const Style", "null");
try style.addField(fa, "", "md", "?*const Style", "null");
try style.addField(fa, "", "lg", "?*const Style", "null");
try style.addField(fa, "", "xl", "?*const Style", "null");
try style.addField(fa, "", "extra", "[]const u8", "\"\"");
try style_struct.addField(fa, "", "sm", "?*const Style", "null");
try style_struct.addField(fa, "", "md", "?*const Style", "null");
try style_struct.addField(fa, "", "lg", "?*const Style", "null");
try style_struct.addField(fa, "", "xl", "?*const Style", "null");
try style_struct.addField(fa, "", "extra", "?[]const u8", "null");
_ = try style.addMethod(fa, "", "format", "(self: Style, w: *std.io.Writer) std.io.Writer.Error!void",
\\@setEvalBranchQuota(20000);
_ = try style_struct.addMethod(fa, "", "format", "(self: Style, w: anytype) std.Io.Writer.Error!void",
\\inline for (std.meta.fields(Style)) |f| {
\\ const T = f.type;
\\ if (comptime std.mem.eql(u8, f.name, "extra")) continue;
\\ if (comptime @typeInfo(T) == .@"union") {
\\ const val = @field(self, f.name);
\\ if (val != .none) {
\\ try core.formatKebab(f.name, w);
\\ try w.writeAll(": ");
\\ try val.format(w);
\\ try w.writeAll("; ");
\\ }
\\ }
\\ try @import("core.zig").formatProperty(f.name, @field(self, f.name), w);
\\}
\\if (self.extra.len > 0) try w.writeAll(self.extra);
);
_ = try style.addMethod(fa, "", "toString", "(self: Style, allocator: std.mem.Allocator) ![]const u8",
\\var list: std.ArrayList(u8) = .empty;
\\defer list.deinit(allocator);
\\const w = list.writer(allocator);
\\try self.format(w);
\\return list.toOwnedSlice(allocator);
);
// Compatibility exports
_ = try file.addConst("Calc", "", "CalcExpr");
_ = try file.addConst("StyleUnit", "", "core.Unit");
_ = try file.addConst("StyleDimension", "", "core.Dimension");
_ = try file.addConst("StyleColor", "", "core.Color");
return try file.finish();
}
@ -302,14 +289,10 @@ fn cleanName(allocator: std.mem.Allocator, name: []const u8, case: enum { pascal
}
}
const result = try list.toOwnedSlice(allocator);
if (isZigKeyword(result) or (result.len > 0 and std.ascii.isDigit(result[0]))) {
const final = try std.fmt.allocPrint(allocator, "@\"{s}\"", .{result});
return final;
if (std.ascii.isDigit(result[0])) {
return try std.mem.concat(allocator, u8, &.{ "_", result });
}
// Handle algebraic conflicts by adding a trailing underscore
if (std.mem.eql(u8, result, "add") or std.mem.eql(u8, result, "sub") or
std.mem.eql(u8, result, "mul") or std.mem.eql(u8, result, "div"))
{
if (isZigKeyword(result) or std.mem.eql(u8, result, "add") or std.mem.eql(u8, result, "sub") or std.mem.eql(u8, result, "mul") or std.mem.eql(u8, result, "div")) {
return try std.mem.concat(allocator, u8, &.{ result, "_" });
}
return result;

View File

@ -52,51 +52,51 @@ pub fn calcExprSource(allocator: std.mem.Allocator) ![]const u8 {
\\ }
\\
\\ fn leaf(value: []const u8) Self {
\\ return init(value);
\\ return Self.init(value);
\\ }
\\
\\ fn unitLeaf(unit: Unit, value: f32) Self {
\\ var temp: [64]u8 = undefined;
\\ const written = std.fmt.bufPrint(&temp, "{d}{s}", .{ value, unit.toString() }) catch @panic("calc expression too complex");
\\ return leaf(written);
\\ return Self.leaf(written);
\\ }
\\
\\ fn numberLeaf(value: f32) Self {
\\ var temp: [64]u8 = undefined;
\\ const written = std.fmt.bufPrint(&temp, "{d}", .{value}) catch @panic("calc expression too complex");
\\ return leaf(written);
\\ return Self.leaf(written);
\\ }
\\
\\ fn rawLeaf(value: []const u8) Self {
\\ return leaf(value);
\\ return Self.leaf(value);
\\ }
\\
\\ pub fn px(value: f32) Self {
\\ return unitLeaf(.px, value);
\\ return Self.unitLeaf(.px, value);
\\ }
\\
\\ pub fn em(value: f32) Self {
\\ return unitLeaf(.em, value);
\\ return Self.unitLeaf(.em, value);
\\ }
\\
\\ pub fn rem(value: f32) Self {
\\ return unitLeaf(.rem, value);
\\ return Self.unitLeaf(.rem, value);
\\ }
\\
\\ pub fn percent(value: f32) Self {
\\ return unitLeaf(.percent, value);
\\ return Self.unitLeaf(.percent, value);
\\ }
\\
\\ pub fn raw(value: []const u8) Self {
\\ return rawLeaf(value);
\\ return Self.rawLeaf(value);
\\ }
\\
\\ pub fn number(value: f32) Self {
\\ return numberLeaf(value);
\\ return Self.numberLeaf(value);
\\ }
\\
\\ fn combine(self: Self, other: Self, op: Op) Self {
\\ var temp: [64]u8 = undefined;
\\ var temp: [128]u8 = undefined;
\\ const lhs = self.text();
\\ const rhs = other.text();
\\ const op_text = op.toString();
@ -119,7 +119,7 @@ pub fn calcExprSource(allocator: std.mem.Allocator) ![]const u8 {
\\ temp[i] = ')';
\\ i += 1;
\\
\\ return init(temp[0..i]);
\\ return Self.init(temp[0..i]);
\\ }
\\
\\ pub fn add(self: Self, other: Self) Self {
@ -131,14 +131,14 @@ pub fn calcExprSource(allocator: std.mem.Allocator) ![]const u8 {
\\ }
\\
\\ pub fn mul(self: Self, factor: f32) Self {
\\ return self.combine(numberLeaf(factor), .mul);
\\ return self.combine(Self.numberLeaf(factor), .mul);
\\ }
\\
\\ pub fn div(self: Self, factor: f32) Self {
\\ return self.combine(numberLeaf(factor), .div);
\\ return self.combine(Self.numberLeaf(factor), .div);
\\ }
\\
\\ pub fn format(self: Self, w: *std.io.Writer) !void {
\\ pub fn format(self: Self, w: *std.io.Writer) std.io.Writer.Error!void {
\\ try w.writeAll(self.text());
\\ }
\\};

View File

@ -27,6 +27,6 @@
"repo": "git@github.com:w3c/webref.git",
"https": "https://github.com/w3c/webref",
"branch": "curated",
"commit": "e2748d3c96fea68f2ac16990a29d3331b067ab5f"
"commit": "b1bed72dca526e68a6f0aac228f5c2550f5e93ee"
}
}