feat(core): add namespaced component support

Fixes: https://github.com/nurulhudaapon/ziex/issues/55
This commit is contained in:
Nurul Huda (Apon) 2026-03-03 20:07:40 +06:00
parent f4a18e27e8
commit 1d9dcaba44
No known key found for this signature in database
GPG Key ID: 5D3F1DE2855A2F79
9 changed files with 1843 additions and 1766 deletions

View File

@ -88,8 +88,8 @@ module.exports = grammar(zig, {
'</>',
),
// HTML tag name (e.g., div, main, button, CustomComponent)
zx_tag_name: _$ => /[a-zA-Z_][a-zA-Z0-9_]*/,
// HTML tag name (e.g., div, main, button, CustomComponent, icons.GitHub)
zx_tag_name: _$ => /[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*/,
// HTML/ZX attributes
zx_attribute: $ => choice(

View File

@ -6689,7 +6689,7 @@
},
"zx_tag_name": {
"type": "PATTERN",
"value": "[a-zA-Z_][a-zA-Z0-9_]*"
"value": "[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*"
},
"zx_attribute": {
"type": "CHOICE",

File diff suppressed because it is too large Load Diff

View File

@ -691,9 +691,19 @@ pub fn transpileFragment(self: *Ast, node: ts.Node, ctx: *TranspileContext, is_r
}
pub fn isCustomComponent(tag: []const u8) bool {
// Namespaced components (e.g., components.Button, icons.GitHub) are always custom
if (std.mem.indexOfScalar(u8, tag, '.') != null) return true;
return tag.len > 0 and std.ascii.isUpper(tag[0]);
}
/// Extract the component display name from a tag (part after the last dot, or the full tag)
fn componentDisplayName(tag: []const u8) []const u8 {
if (std.mem.lastIndexOfScalar(u8, tag, '.')) |dot_pos| {
return tag[dot_pos + 1 ..];
}
return tag;
}
/// Check if element is a <pre> tag (preserve whitespace but still process children)
fn isPreElement(tag: []const u8) bool {
return std.mem.eql(u8, tag, "pre");
@ -883,7 +893,7 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
// Write _zx.client(.{ .name = "Name", .path = "path", .id = "id" }, .{ props })
try ctx.writeM("_zx.client", node.startByte(), self);
try ctx.write("(.{ .name = \"");
try ctx.write(tag);
try ctx.write(componentDisplayName(tag));
try ctx.write("\", .path = \"");
try ctx.write(full_path);
try ctx.write("\", .id = \"");
@ -951,9 +961,9 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
try ctx.write(tag);
try ctx.write(", ");
try ctx.write(".{ .name = \"");
try ctx.write(tag);
try ctx.write(componentDisplayName(tag));
try ctx.write("\", .client = .{ .name = \"");
try ctx.write(tag);
try ctx.write(componentDisplayName(tag));
// try ctx.write("\", .path = \"");
// try ctx.write(full_path);
try ctx.write("\", .id = \"");
@ -1017,7 +1027,7 @@ fn writeCustomComponent(self: *Ast, node: ts.Node, tag: []const u8, attributes:
// Write options parameter first (name + builtin attributes)
try ctx.write(".{ .name = \"");
try ctx.write(tag);
try ctx.write(componentDisplayName(tag));
try ctx.write("\"");
try writeComponentBuiltinOptions(self, builtin_attrs.items, ctx, true);
try ctx.write(" }, ");

View File

@ -199,6 +199,11 @@ test "expression_multiline_string" {
test "component_basic" {
try test_fmt("component/basic");
}
test "component_namespace" {
try test_fmt("component/namespace");
}
test "component_multiple" {
try test_fmt("component/multiple");
}

1
test/data/component/namespace.html generated Normal file
View File

@ -0,0 +1 @@
<main><button>Custom Button</button></main>

View File

@ -0,0 +1,34 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
var _zx = @import("zx").allocInit(allocator);
return _zx.ele(
.main,
.{
.allocator = allocator,
.children = &.{
_zx.cmp(
components.Button,
.{ .name = "Button" },
.{ .title = "Custom Button" },
),
},
},
);
}
const components = struct {
const ButtonProps = struct { title: []const u8 };
pub fn Button(allocator: zx.Allocator, props: ButtonProps) zx.Component {
var _zx = @import("zx").allocInit(allocator);
return _zx.ele(
.button,
.{
.allocator = allocator,
.children = &.{
_zx.expr(props.title),
},
},
);
}
};
const zx = @import("zx");

View File

@ -0,0 +1,16 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
return (
<main @allocator={allocator}>
<components.Button title="Custom Button" />
</main>
);
}
const components = struct {
const ButtonProps = struct { title: []const u8 };
pub fn Button(allocator: zx.Allocator, props: ButtonProps) zx.Component {
return (<button @allocator={allocator}>{props.title}</button>);
}
};
const zx = @import("zx");

View File

@ -310,6 +310,10 @@ test "component_basic" {
try test_transpile("component/basic");
try test_render("component/basic", @import("./../data/component/basic.zig").Page);
}
test "component_namespace" {
try test_transpile("component/namespace");
try test_render("component/namespace", @import("./../data/component/namespace.zig").Page);
}
test "component_multiple" {
try test_transpile("component/multiple");
try test_render("component/multiple", @import("./../data/component/multiple.zig").Page);
@ -540,6 +544,7 @@ fn getPageFn(comptime path: []const u8) ?fn (std.mem.Allocator) zx.Component {
.{ "expression/optional", @import("./../data/expression/optional.zig") },
.{ "expression/template", @import("./../data/expression/template.zig") },
.{ "component/basic", @import("./../data/component/basic.zig") },
.{ "component/namespace", @import("./../data/component/namespace.zig") },
.{ "component/multiple", @import("./../data/component/multiple.zig") },
.{ "component/nested", @import("./../data/component/nested.zig") },
.{ "component/children_only", @import("./../data/component/children_only.zig") },