mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-20 02:29:36 -06:00
feat: improve the style definition (#94)
This commit is contained in:
parent
b01645f5d9
commit
9c91733ebd
15
build.zig
15
build.zig
@ -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/) --- //
|
||||
|
||||
@ -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(','),
|
||||
)),
|
||||
'}',
|
||||
),
|
||||
|
||||
|
||||
@ -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
54
src/zx.zig
54
src/zx.zig
@ -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)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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| {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
1
test/data/style/basic.html
generated
1
test/data/style/basic.html
generated
@ -1 +0,0 @@
|
||||
<div style="background-color: #ff0000; display: flex; "> Hello </div>
|
||||
@ -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");
|
||||
@ -5,7 +5,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
||||
};
|
||||
return (
|
||||
<div @allocator={allocator} style={style}>
|
||||
Hello
|
||||
Hello
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1
test/data/style/component.html
generated
1
test/data/style/component.html
generated
@ -1 +0,0 @@
|
||||
<div class="card" style="color: #0000ff; "> Hello Component </div>
|
||||
@ -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");
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
1
test/data/style/inline.html
generated
1
test/data/style/inline.html
generated
@ -1 +0,0 @@
|
||||
<div style="display: flex; row-gap: 10px; "> Inline Style </div>
|
||||
@ -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");
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
\\ }
|
||||
\\};
|
||||
|
||||
@ -27,6 +27,6 @@
|
||||
"repo": "git@github.com:w3c/webref.git",
|
||||
"https": "https://github.com/w3c/webref",
|
||||
"branch": "curated",
|
||||
"commit": "e2748d3c96fea68f2ac16990a29d3331b067ab5f"
|
||||
"commit": "b1bed72dca526e68a6f0aac228f5c2550f5e93ee"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user