doc: add editor setup section

This commit is contained in:
Nurul Huda (Apon) 2025-11-07 03:18:46 +06:00
parent 87a146eff4
commit 076112aea5
No known key found for this signature in database
GPG Key ID: 5D3F1DE2855A2F79
10 changed files with 568 additions and 192 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.zig-cache
zig-out
.zx
tmp

View File

@ -1,6 +1,6 @@
.{
.name = .zx,
.version = "0.0.1-dev.23",
.version = "0.0.1-dev.24",
.fingerprint = 0xcad77a8d2a3389f2,
.minimum_zig_version = "0.15.2",
.dependencies = .{

View File

@ -65,6 +65,26 @@ body {
border-bottom: 2px solid var(--border-color);
}
.nav-header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.source-code-link {
font-size: 0.875rem;
color: var(--link-color);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.source-code-link:hover {
color: #0052a3;
text-decoration: underline;
}
.nav-header h1 {
font-size: 2rem;
font-weight: 700;
@ -84,6 +104,59 @@ body {
.nav-menu li {
border-bottom: 1px solid var(--border-color);
position: relative;
}
.nav-item-with-submenu {
position: relative;
}
.nav-parent {
display: flex;
justify-content: space-between;
align-items: center;
}
.submenu-arrow {
font-size: 0.7em;
transition: transform 0.2s;
margin-left: auto;
}
.nav-item-with-submenu:hover .submenu-arrow,
.nav-item-with-submenu:focus-within .submenu-arrow {
transform: rotate(180deg);
}
.nav-submenu {
list-style: none;
padding: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
background: rgba(0, 0, 0, 0.02);
}
.nav-item-with-submenu:hover .nav-submenu,
.nav-item-with-submenu:focus-within .nav-submenu {
max-height: 200px;
transition: max-height 0.3s ease-in;
}
.nav-submenu li {
border-bottom: none;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.nav-submenu a {
padding-left: 3rem;
font-size: 0.9em;
color: #666;
}
.nav-submenu a:hover {
background: rgba(0, 102, 204, 0.05);
border-left-color: var(--link-color);
}
.nav-menu a {
@ -100,6 +173,7 @@ body {
border-left-color: var(--link-color);
}
/* Main Content */
.content {
margin-left: var(--sidebar-width);
@ -115,14 +189,22 @@ body {
scroll-margin-top: 2rem;
}
.section h2 {
font-size: 2rem;
font-weight: 700;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border-color);
}
.section h2 {
font-size: 2rem;
font-weight: 700;
margin: 0;
flex: 1;
}
.section h3 {
font-size: 1.5rem;
font-weight: 600;
@ -141,6 +223,44 @@ body {
margin-bottom: 1rem;
}
/* Source Code Badge */
.source-code-badge {
display: inline-block;
margin-left: 1rem;
}
.source-code-badge-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #f6f8fa;
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
}
.source-code-badge-link:hover {
background: #eaeef2;
border-color: #d0d7de;
color: var(--link-color);
text-decoration: none;
}
.source-code-badge-link svg {
flex-shrink: 0;
width: 16px;
height: 16px;
}
.source-code-badge-link span {
white-space: nowrap;
}
.section ul {
margin-left: 2rem;
margin-bottom: 1rem;
@ -790,6 +910,16 @@ code[class*="language-"] {
font-size: 0.85em;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.source-code-badge {
margin-left: 0;
}
.section h2 {
font-size: 1.5rem;
}
@ -867,6 +997,16 @@ code[class*="language-"] {
font-size: 0.8em;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.source-code-badge {
margin-left: 0;
}
.section h2 {
font-size: 1.5rem;
}

View File

@ -10,7 +10,7 @@
},
.zx = .{
.url = "../",
.hash = "zx-0.0.1-dev.13-8okzKgkxAgD4YtDI6QCVIt-Rn9t8-SVR3w9xvA2dYoOk",
.hash = "zx-0.0.1-dev.23-8okzKlZXAgCczvT0Fb0y5HglAXl7PjbTf7KalHyuMuVb",
},
},
.paths = .{

View File

@ -1,5 +1,4 @@
pub fn Page(allocator: zx.Allocator) zx.Component {
// If example
var aw_if: std.io.Writer.Allocating = .init(allocator);
const zx_example_if = @embedFile("../../../pages/doc/example/if.zx");
@ -52,6 +51,10 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
const txt_expr = "{expression}";
const fmt_expr = "{[expression:format]}";
var aw_icon_svg_base64: std.io.Writer.Allocating = .init(allocator);
std.base64.standard.Encoder.encodeWriter(&aw_icon_svg_base64.writer, @embedFile("../../public/zx-icon.svg")) catch unreachable;
const icon_href = std.fmt.allocPrint(allocator, "data:image/svg+xml;base64,{s}", .{aw_icon_svg_base64.written()}) catch unreachable;
return (
<html>
<head>
@ -60,6 +63,7 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
<title>ZX Documentation - JSX-like Syntax for Zig</title>
<link rel="stylesheet" href="./assets/styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism.min.css" rel="stylesheet">
<link rel="icon" href={icon_href} type="image/svg+xml">
<style>
{[@embedFile("../../assets/doc.css"):s]}
</style>
@ -70,12 +74,20 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
<div class="container">
<nav class="sidebar">
<div class="nav-header">
<div class="nav-header-top">
<h1>ZX</h1>
</div>
<p class="nav-subtitle">HTML within Zig</p>
</div>
<ul class="nav-menu">
<li><a href="#introduction">Introduction</a></li>
<li><a href="#getting-started">Getting Started</a></li>
<li class="nav-item-with-submenu">
<a href="#getting-started" class="nav-parent">Getting Started <span class="submenu-arrow">▼</span></a>
<ul class="nav-submenu">
<li><a href="#getting-started">Quick Start</a></li>
<li><a href="#editor-setup">Editor Setup</a></li>
</ul>
</li>
<li><a href="#control-flow">Control Flow</a></li>
<li><a href="#expressions">Expressions</a></li>
<li><a href="#examples">Examples</a></li>
@ -84,10 +96,19 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
<main class="content">
<section id="introduction" class="section">
<div class="section-header">
<h2>Introduction</h2>
<div class="source-code-badge">
<a href="https://github.com/nurulhudaapon/zx" target="_blank" rel="noopener noreferrer" class="source-code-badge-link">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
<span>View Source Code</span>
</a>
</div>
</div>
<p>ZX is a <strong>prototype</strong> Zig library for building web applications with JSX-like syntax. Write declarative UI
components using familiar JSX patterns, transpiled to efficient Zig code.</p>
<p><strong>ZX is available on GitHub:</strong> <a href="https://github.com/nurulhudaapon/zx" target="_blank" rel="noopener noreferrer" style="font-size: 1.1em; font-weight: 600;">https://github.com/nurulhudaapon/zx</a></p>
<p>ZX combines the power and performance of Zig with the expressiveness of JSX, enabling you to build
fast, type-safe web applications. ZX uses <a href="https://github.com/karlseguin/http.zig" target="_blank" rel="noopener noreferrer">http.zig</a> to create high-performance HTTP servers, making it significantly faster than frameworks like Next.js. See benchmarks at <a href="https://zigweb.nuhu.dev" target="_blank" rel="noopener noreferrer">zigweb.nuhu.dev</a>.</p>
<p>Currently, ZX supports Server-Side Rendering (SSR) and Static Site Generation (SSG). Client-Side Rendering (CSR) may be implemented in the future by compiling Zig to WebAssembly and creating a bridge with JavaScript, once the prototype has proven the concept to be useful.</p>
@ -105,6 +126,11 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
<p>
A full, step-by-step getting started guide is currently a work in progress and will be available here soon.
</p>
<h3 id="editor-setup">Editor Setup</h3>
<p>
Enhance your development experience with the official ZX extension for VSCode and Cursor. The extension provides syntax highlighting, code completion, and other helpful features for working with <code>.zx</code> files. Install it from <a href="https://open-vsx.org/extension/nurulhudaapon/zx" target="_blank" rel="noopener noreferrer">Open VSX</a>.
</p>
</section>
<section id="control-flow" class="section">
@ -169,156 +195,6 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
);
}
// Helper function to find minimum indentation in non-empty lines
fn findMinIndent(lines: []const []const u8, first_non_empty: usize, last_non_empty: usize) usize {
var min_indent: usize = std.math.maxInt(usize);
for (lines[first_non_empty..last_non_empty+1]) |line| {
if (std.mem.trim(u8, line, " \t").len > 0) {
var indent: usize = 0;
for (line) |char| {
if (char == ' ' or char == '\t') {
indent += 1;
} else {
break;
}
}
if (indent < min_indent) {
min_indent = indent;
}
}
}
return if (min_indent == std.math.maxInt(usize)) 0 else min_indent;
}
// Helper function to remove common leading indentation
fn removeCommonIndentation(allocator: zx.Allocator, content: []const u8) []const u8 {
var lines = std.array_list.Managed([]const u8).init(allocator);
defer lines.deinit();
var it = std.mem.splitScalar(u8, content, '\n');
while (it.next()) |line| {
lines.append(line) catch unreachable;
}
if (lines.items.len == 0) {
return allocator.dupe(u8, "") catch unreachable;
}
// Find first and last non-empty lines
var first_non_empty: ?usize = null;
var last_non_empty: ?usize = null;
for (lines.items, 0..) |line, i| {
if (std.mem.trim(u8, line, " \t").len > 0) {
if (first_non_empty == null) {
first_non_empty = i;
}
last_non_empty = i;
}
}
if (first_non_empty == null) {
return allocator.dupe(u8, "") catch unreachable;
}
const first = first_non_empty.?;
const last = last_non_empty.?;
// Find minimum indentation
const min_indent = findMinIndent(lines.items, first, last);
// Build result with indentation removed
var result = std.array_list.Managed(u8).init(allocator);
defer result.deinit();
for (lines.items[first..last+1], 0..) |line, i| {
if (i > 0) {
result.append('\n') catch unreachable;
}
if (std.mem.trim(u8, line, " \t").len > 0) {
const start = @min(min_indent, line.len);
result.appendSlice(line[start..]) catch unreachable;
} else {
result.appendSlice(line) catch unreachable;
}
}
return allocator.dupe(u8, result.items) catch unreachable;
}
// Extract content inside return (...) for ZX code
fn extractZxReturnContent(allocator: zx.Allocator, content: []const u8) []const u8 {
const return_pattern = "return (";
if (std.mem.indexOf(u8, content, return_pattern)) |start_idx| {
var depth: usize = 1;
var i = start_idx + return_pattern.len;
while (i < content.len and depth > 0) {
if (content[i] == '(') {
depth += 1;
} else if (content[i] == ')') {
depth -= 1;
if (depth == 0) {
return allocator.dupe(u8, content[start_idx + return_pattern.len..i]) catch unreachable;
}
}
i += 1;
}
}
return allocator.dupe(u8, content) catch unreachable;
}
// Extract content after return statement for Zig code (until semicolon)
fn extractZigReturnContent(allocator: zx.Allocator, content: []const u8) []const u8 {
const return_pattern = "return ";
if (std.mem.indexOf(u8, content, return_pattern)) |start_idx| {
var depth: usize = 0;
var in_string = false;
var string_char: ?u8 = null;
var i = start_idx + return_pattern.len;
while (i < content.len) {
const char = content[i];
// Handle string literals
if (char == '"' or char == '\'') {
// Check if previous character is not a backslash (or if backslash is escaped)
var is_escaped = false;
if (i > start_idx + return_pattern.len) {
var backslash_count: usize = 0;
var j = i - 1;
while (j >= start_idx + return_pattern.len and content[j] == '\\') {
backslash_count += 1;
j -= 1;
}
is_escaped = (backslash_count % 2) == 1;
}
if (!is_escaped) {
if (!in_string) {
in_string = true;
string_char = char;
} else if (char == string_char) {
in_string = false;
string_char = null;
}
}
}
// Only process brackets/braces/parentheses outside of strings
if (!in_string) {
if (char == '(' or char == '{' or char == '[') {
depth += 1;
} else if (char == ')' or char == '}' or char == ']') {
depth -= 1;
} else if (char == ';' and depth == 0) {
return allocator.dupe(u8, content[start_idx + return_pattern.len..i]) catch unreachable;
}
}
i += 1;
}
}
return allocator.dupe(u8, content) catch unreachable;
}
const ExampleBlockProps = struct {
id: []const u8,
@ -329,13 +205,13 @@ const ExampleBlockProps = struct {
fn ExampleBlock(allocator: zx.Allocator, props: ExampleBlockProps) zx.Component {
// Extract and truncate ZX code
const zx_return_content = extractZxReturnContent(allocator, props.zx_code);
const truncated_zx_code = removeCommonIndentation(allocator, zx_return_content);
const zx_return_content = util.extractZxReturnContent(allocator, props.zx_code);
const truncated_zx_code = util.removeCommonIndentation(allocator, zx_return_content);
const escaped_full_zx = props.zx_code;
// Extract and truncate Zig code
const zig_return_content = extractZigReturnContent(allocator, props.zig_code);
const truncated_zig_code = removeCommonIndentation(allocator, zig_return_content);
const zig_return_content = util.extractZigReturnContent(allocator, props.zig_code);
const truncated_zig_code = util.removeCommonIndentation(allocator, zig_return_content);
const escaped_full_zig = props.zig_code;
const preview_id = std.fmt.allocPrint(allocator, "tab-{s}-preview", .{props.id}) catch unreachable;
@ -386,3 +262,4 @@ fn ExampleBlock(allocator: zx.Allocator, props: ExampleBlockProps) zx.Component
const zx = @import("zx");
const std = @import("std");
const util = @import("util.zig");

153
site/pages/doc/util.zig Normal file
View File

@ -0,0 +1,153 @@
/// Helper function to find minimum indentation in non-empty lines
pub fn findMinIndent(lines: []const []const u8, first_non_empty: usize, last_non_empty: usize) usize {
var min_indent: usize = std.math.maxInt(usize);
for (lines[first_non_empty .. last_non_empty + 1]) |line| {
if (std.mem.trim(u8, line, " \t").len > 0) {
var indent: usize = 0;
for (line) |char| {
if (char == ' ' or char == '\t') {
indent += 1;
} else {
break;
}
}
if (indent < min_indent) {
min_indent = indent;
}
}
}
return if (min_indent == std.math.maxInt(usize)) 0 else min_indent;
}
/// Helper function to remove common leading indentation
pub fn removeCommonIndentation(allocator: zx.Allocator, content: []const u8) []const u8 {
var lines = std.array_list.Managed([]const u8).init(allocator);
defer lines.deinit();
var it = std.mem.splitScalar(u8, content, '\n');
while (it.next()) |line| {
lines.append(line) catch unreachable;
}
if (lines.items.len == 0) {
return allocator.dupe(u8, "") catch unreachable;
}
// Find first and last non-empty lines
var first_non_empty: ?usize = null;
var last_non_empty: ?usize = null;
for (lines.items, 0..) |line, i| {
if (std.mem.trim(u8, line, " \t").len > 0) {
if (first_non_empty == null) {
first_non_empty = i;
}
last_non_empty = i;
}
}
if (first_non_empty == null) {
return allocator.dupe(u8, "") catch unreachable;
}
const first = first_non_empty.?;
const last = last_non_empty.?;
// Find minimum indentation
const min_indent = findMinIndent(lines.items, first, last);
// Build result with indentation removed
var result = std.array_list.Managed(u8).init(allocator);
defer result.deinit();
for (lines.items[first .. last + 1], 0..) |line, i| {
if (i > 0) {
result.append('\n') catch unreachable;
}
if (std.mem.trim(u8, line, " \t").len > 0) {
const start = @min(min_indent, line.len);
result.appendSlice(line[start..]) catch unreachable;
} else {
result.appendSlice(line) catch unreachable;
}
}
return allocator.dupe(u8, result.items) catch unreachable;
}
/// Extract content inside return (...) for ZX code
pub fn extractZxReturnContent(allocator: zx.Allocator, content: []const u8) []const u8 {
const return_pattern = "return (";
if (std.mem.indexOf(u8, content, return_pattern)) |start_idx| {
var depth: usize = 1;
var i = start_idx + return_pattern.len;
while (i < content.len and depth > 0) {
if (content[i] == '(') {
depth += 1;
} else if (content[i] == ')') {
depth -= 1;
if (depth == 0) {
return allocator.dupe(u8, content[start_idx + return_pattern.len .. i]) catch unreachable;
}
}
i += 1;
}
}
return allocator.dupe(u8, content) catch unreachable;
}
/// Extract content after return statement for Zig code (until semicolon)
pub fn extractZigReturnContent(allocator: zx.Allocator, content: []const u8) []const u8 {
const return_pattern = "return ";
if (std.mem.indexOf(u8, content, return_pattern)) |start_idx| {
var depth: usize = 0;
var in_string = false;
var string_char: ?u8 = null;
var i = start_idx + return_pattern.len;
while (i < content.len) {
const char = content[i];
// Handle string literals
if (char == '"' or char == '\'') {
// Check if previous character is not a backslash (or if backslash is escaped)
var is_escaped = false;
if (i > start_idx + return_pattern.len) {
var backslash_count: usize = 0;
var j = i - 1;
while (j >= start_idx + return_pattern.len and content[j] == '\\') {
backslash_count += 1;
j -= 1;
}
is_escaped = (backslash_count % 2) == 1;
}
if (!is_escaped) {
if (!in_string) {
in_string = true;
string_char = char;
} else if (char == string_char) {
in_string = false;
string_char = null;
}
}
}
// Only process brackets/braces/parentheses outside of strings
if (!in_string) {
if (char == '(' or char == '{' or char == '[') {
depth += 1;
} else if (char == ')' or char == '}' or char == ']') {
depth -= 1;
} else if (char == ';' and depth == 0) {
return allocator.dupe(u8, content[start_idx + return_pattern.len .. i]) catch unreachable;
}
}
i += 1;
}
}
return allocator.dupe(u8, content) catch unreachable;
}
const zx = @import("zx");
const std = @import("std");

4
site/public/zx-icon.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="16" y="16" width="480" height="480" rx="48" fill="black"/>
<path d="M84.3185 358V335.031L188.722 185.287H83.7219V154.364H234.461V177.332L130.157 327.077H235.057V358H84.3185ZM301.577 154.364L346.421 228.938H348.012L393.054 154.364H435.114L372.373 256.182L436.108 358H393.353L348.012 283.923H346.421L301.08 358H258.523L322.855 256.182L259.319 154.364H301.577Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -239,6 +239,12 @@ fn transpileDirectory(
var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
defer dir.close();
// Check if dir_path itself is or contains "pages"
const sep = std.fs.path.sep_str;
const dir_is_pages = std.mem.endsWith(u8, dir_path, sep ++ "pages") or
std.mem.eql(u8, getBasename(dir_path), "pages") or
std.mem.endsWith(u8, dir_path, "pages");
var walker = try dir.walk(allocator);
defer walker.deinit();
@ -248,26 +254,44 @@ fn transpileDirectory(
const is_zx = std.mem.endsWith(u8, entry.path, ".zx");
const is_zig = std.mem.endsWith(u8, entry.path, ".zig");
// Skip files that don't match our criteria
if (!is_zx and !(include_zig and is_zig)) continue;
// Check if this entry is within a "pages" directory
// entry.path is relative to dir_path, so check if it contains "pages" in its path
// or if dir_path itself is a pages directory
const is_in_pages_dir = dir_is_pages or
std.mem.startsWith(u8, entry.path, "pages" ++ sep) or
std.mem.indexOf(u8, entry.path, sep ++ "pages" ++ sep) != null;
// Build input path
const input_path = try std.fs.path.join(allocator, &.{ dir_path, entry.path });
defer allocator.free(input_path);
// For .zig files with --zig flag, check if they contain zx syntax
if (is_zig and include_zig) {
// Check if this file should be transpiled
var should_transpile = false;
var should_copy = false;
if (is_zx) {
should_transpile = true;
} else if (is_zig and include_zig) {
const has_zx = hasZXSyntax(allocator, input_path) catch false;
if (!has_zx) {
// Skip .zig files that don't contain zx syntax
continue;
if (has_zx) {
should_transpile = true;
} else if (is_in_pages_dir) {
// In pages directory, copy .zig files that don't have zx syntax
should_copy = true;
}
} else if (is_in_pages_dir) {
// In pages directory, copy all other files
should_copy = true;
}
// Skip files that don't match our criteria
if (!should_transpile and !should_copy) continue;
// Build output path
const relative_path = entry.path;
var output_rel_path: []const u8 = undefined;
if (should_transpile) {
if (is_zx) {
// Remove .zx and add .zig
output_rel_path = try std.mem.concat(allocator, u8, &.{
@ -278,15 +302,36 @@ fn transpileDirectory(
// For .zig files, keep the same name
output_rel_path = try allocator.dupe(u8, relative_path);
}
} else {
// For copying, keep the same name
output_rel_path = try allocator.dupe(u8, relative_path);
}
defer allocator.free(output_rel_path);
const output_path = try std.fs.path.join(allocator, &.{ output_dir, output_rel_path });
defer allocator.free(output_path);
if (should_transpile) {
transpileFile(allocator, input_path, output_path) catch |err| {
std.debug.print("Error transpiling {s}: {}\n", .{ input_path, err });
continue;
};
} else {
// Copy file as-is
// Create parent directory if needed
if (std.fs.path.dirname(output_path)) |parent| {
std.fs.cwd().makePath(parent) catch |err| switch (err) {
error.PathAlreadyExists => {},
else => {
std.debug.print("Error creating directory {s}: {}\n", .{ parent, err });
continue;
},
};
}
try std.fs.cwd().copyFile(input_path, std.fs.cwd(), output_path, .{});
std.debug.print("Copied: {s} -> {s}\n", .{ input_path, output_path });
}
}
// Copy asset directories after transpiling all files

View File

@ -4,7 +4,7 @@ const std = @import("std");
pub const Ast = @import("zx/Ast.zig");
pub const Allocator = std.mem.Allocator;
const ElementTag = enum { iframe, slot, svg, path, img, html, base, head, link, meta, script, style, title, address, article, body, h1, h6, footer, header, h2, h3, h4, h5, hgroup, nav, section, dd, dl, dt, div, figcaption, figure, hr, li, ol, ul, menu, main, p, pre, a, abbr, b, bdi, bdo, br, cite, code, data, time, dfn, em, i, kbd, mark, q, blockquote, rp, ruby, rt, rtc, rb, s, del, ins, samp, small, span, strong, sub, sup, u, @"var", wbr, area, map, audio, source, track, video, embed, object, param, canvas, noscript, caption, table, col, colgroup, tbody, tr, thead, tfoot, td, th, button, datalist, option, fieldset, label, form, input, keygen, legend, meter, optgroup, select, output, progress, textarea, details, dialog, menuitem, summary, content, element, shadow, template, acronym, applet, basefont, font, big, blink, center, command, dir, frame, frameset, isindex, listing, marquee, noembed, plaintext, spacer, strike, tt, xmp };
const ElementTag = enum { polyline, iframe, slot, svg, path, img, html, base, head, link, meta, script, style, title, address, article, body, h1, h6, footer, header, h2, h3, h4, h5, hgroup, nav, section, dd, dl, dt, div, figcaption, figure, hr, li, ol, ul, menu, main, p, pre, a, abbr, b, bdi, bdo, br, cite, code, data, time, dfn, em, i, kbd, mark, q, blockquote, rp, ruby, rt, rtc, rb, s, del, ins, samp, small, span, strong, sub, sup, u, @"var", wbr, area, map, audio, source, track, video, embed, object, param, canvas, noscript, caption, table, col, colgroup, tbody, tr, thead, tfoot, td, th, button, datalist, option, fieldset, label, form, input, keygen, legend, meter, optgroup, select, output, progress, textarea, details, dialog, menuitem, summary, content, element, shadow, template, acronym, applet, basefont, font, big, blink, center, command, dir, frame, frameset, isindex, listing, marquee, noembed, plaintext, spacer, strike, tt, xmp };
const SELF_CLOSING_ONLY: []const ElementTag = &.{ .br, .hr, .img, .input, .link, .source, .track, .wbr };
const NO_CHILDREN_ONLY: []const ElementTag = &.{ .meta, .link, .input };
@ -173,10 +173,15 @@ pub const Component = union(enum) {
for (attributes) |attribute| {
try writer.print(" {s}", .{attribute.name});
if (attribute.value) |value| {
try writer.writeAll("=\"");
if (attribute.format) |_| {
// Format field is present - value is already formatted, skip HTML escaping
try writer.writeAll(value);
} else {
// HTML escape attribute values to prevent XSS
// Escape quotes, ampersands, and other HTML special characters
try writer.writeAll("=\"");
try escapeAttributeValueToWriter(writer, value);
}
try writer.writeAll("\"");
}
}
@ -214,6 +219,7 @@ const Element = struct {
const Attribute = struct {
name: []const u8,
value: ?[]const u8 = null,
format: ?[]const u8 = null, // Format specifier for value (e.g., "{s}", "{d}")
};
tag: ElementTag,

View File

@ -123,6 +123,7 @@ const ZXElement = struct {
const AttributeValue = union(enum) {
static: []const u8, // "string value"
dynamic: []const u8, // .{expression}
format: struct { expr: []const u8, format: []const u8 }, // {[expr:fmt]}
};
const SwitchCase = struct {
@ -491,7 +492,7 @@ fn parseJsx(allocator: std.mem.Allocator, content: []const u8) error{ InvalidJsx
// Skip whitespace and =
while (i < content.len and (std.ascii.isWhitespace(content[i]) or content[i] == '=')) i += 1;
// Parse attribute value - either quoted string or dynamic expression
// Parse attribute value - either quoted string, dynamic expression, or format expression
if (i < content.len and content[i] == '"') {
// Static string value: "value"
i += 1; // skip opening quote
@ -502,7 +503,7 @@ fn parseJsx(allocator: std.mem.Allocator, content: []const u8) error{ InvalidJsx
try elem.attributes.append(allocator, .{ .name = attr_name, .value = .{ .static = attr_value } });
} else if (i + 1 < content.len and content[i] == '{') {
// Dynamic expression: .{expression}
// Check for format expression: {[expr:fmt]} or dynamic expression: {expr}
i += 1; // skip {
const expr_start = i;
var brace_depth: i32 = 1;
@ -511,12 +512,47 @@ fn parseJsx(allocator: std.mem.Allocator, content: []const u8) error{ InvalidJsx
if (content[i] == '}') brace_depth -= 1;
if (brace_depth > 0) i += 1;
}
const expr = content[expr_start..i];
var expr = content[expr_start..i];
i += 1; // skip closing }
// Trim whitespace first
while (expr.len > 0 and std.ascii.isWhitespace(expr[0])) expr = expr[1..];
while (expr.len > 0 and std.ascii.isWhitespace(expr[expr.len - 1])) expr = expr[0 .. expr.len - 1];
// Check for format expression: {[expr:fmt]} or {[expr]}
if (expr.len >= 2 and expr[0] == '[' and expr[expr.len - 1] == ']') {
// Remove the brackets
var inner = expr[1 .. expr.len - 1];
// Trim whitespace from inner content
while (inner.len > 0 and std.ascii.isWhitespace(inner[0])) inner = inner[1..];
while (inner.len > 0 and std.ascii.isWhitespace(inner[inner.len - 1])) inner = inner[0 .. inner.len - 1];
// Check for format specifier after colon
if (std.mem.indexOfScalar(u8, inner, ':')) |colon_pos| {
// Split at colon: expr:format
const expr_part = inner[0..colon_pos];
var format_part = inner[colon_pos + 1 ..];
// Trim whitespace from both parts
var trimmed_expr = expr_part;
while (trimmed_expr.len > 0 and std.ascii.isWhitespace(trimmed_expr[0])) trimmed_expr = trimmed_expr[1..];
while (trimmed_expr.len > 0 and std.ascii.isWhitespace(trimmed_expr[trimmed_expr.len - 1])) trimmed_expr = trimmed_expr[0 .. trimmed_expr.len - 1];
while (format_part.len > 0 and std.ascii.isWhitespace(format_part[0])) format_part = format_part[1..];
while (format_part.len > 0 and std.ascii.isWhitespace(format_part[format_part.len - 1])) format_part = format_part[0 .. format_part.len - 1];
try elem.attributes.append(allocator, .{ .name = attr_name, .value = .{ .format = .{ .expr = trimmed_expr, .format = format_part } } });
} else {
// No format specifier, default to "d" for decimal
try elem.attributes.append(allocator, .{ .name = attr_name, .value = .{ .format = .{ .expr = inner, .format = "d" } } });
}
} else {
// Regular dynamic expression: {expr}
try elem.attributes.append(allocator, .{ .name = attr_name, .value = .{ .dynamic = expr } });
}
}
}
// Check for self-closing tag (/>)
var is_self_closing = false;
@ -1145,6 +1181,31 @@ fn renderJsxAsTokensWithLoopContext(allocator: std.mem.Allocator, output: *Token
.dynamic => |expr| {
try output.addToken(.identifier, expr);
},
.format => |fmt| {
// Format expression: use std.fmt.allocPrint(allocator, "{format}", .{expr}) for attribute values
try output.addToken(.identifier, "std");
try output.addToken(.period, ".");
try output.addToken(.identifier, "fmt");
try output.addToken(.period, ".");
try output.addToken(.identifier, "allocPrint");
try output.addToken(.l_paren, "(");
try output.addToken(.identifier, "allocator");
try output.addToken(.comma, ",");
// Format string: "{format}"
const format_str = try std.fmt.allocPrint(allocator, "\"{{{s}}}\"", .{fmt.format});
defer allocator.free(format_str);
try output.addToken(.string_literal, format_str);
try output.addToken(.comma, ",");
// Expression wrapped in tuple: .{expr}
try output.addToken(.invalid, " ");
try output.addToken(.period, ".");
try output.addToken(.l_brace, "{");
try output.addToken(.identifier, fmt.expr);
try output.addToken(.r_brace, "}");
try output.addToken(.r_paren, ")");
},
}
if (i < elem.attributes.items.len - 1) {
try output.addToken(.comma, ",");
@ -1222,6 +1283,22 @@ fn renderJsxAsTokensWithLoopContext(allocator: std.mem.Allocator, output: *Token
// Dynamic expression - output as-is
try output.addToken(.identifier, expr);
},
.format => |fmt| {
// Format expression: pass expression directly and set format field
// .value = expr (expression as-is)
try output.addToken(.identifier, fmt.expr);
try output.addToken(.comma, ",");
try output.addToken(.invalid, "\n");
// .format = "{format}"
try addIndentTokens(output, indent + 3);
try output.addToken(.period, ".");
try output.addToken(.identifier, "format");
try output.addToken(.equal, "=");
const format_str = try std.fmt.allocPrint(allocator, "\"{{{s}}}\"", .{fmt.format});
defer allocator.free(format_str);
try output.addToken(.string_literal, format_str);
},
}
try output.addToken(.r_brace, "}");
@ -1619,6 +1696,22 @@ fn renderJsxAsTokensWithLoopContext(allocator: std.mem.Allocator, output: *Token
.dynamic => |expr| {
try output.addToken(.identifier, expr);
},
.format => |fmt| {
// Format expression: pass expression directly and set format field
// .value = expr (expression as-is)
try output.addToken(.identifier, fmt.expr);
try output.addToken(.comma, ",");
try output.addToken(.invalid, "\n");
// .format = "{format}"
try addIndentTokens(output, indent + 4);
try output.addToken(.period, ".");
try output.addToken(.identifier, "format");
try output.addToken(.equal, "=");
const format_str = try std.fmt.allocPrint(allocator, "\"{{{s}}}\"", .{fmt.format});
defer allocator.free(format_str);
try output.addToken(.string_literal, format_str);
},
}
if (i < child_elem.attributes.items.len - 1) {
try output.addToken(.comma, ",");
@ -1695,6 +1788,31 @@ fn renderNestedElementAsCall(allocator: std.mem.Allocator, output: *TokenBuilder
.dynamic => |expr| {
try output.addToken(.identifier, expr);
},
.format => |fmt| {
// Format expression: use std.fmt.allocPrint(allocator, "{format}", .{expr}) for attribute values
try output.addToken(.identifier, "std");
try output.addToken(.period, ".");
try output.addToken(.identifier, "fmt");
try output.addToken(.period, ".");
try output.addToken(.identifier, "allocPrint");
try output.addToken(.l_paren, "(");
try output.addToken(.identifier, "allocator");
try output.addToken(.comma, ",");
// Format string: "{format}"
const format_str = try std.fmt.allocPrint(allocator, "\"{{{s}}}\"", .{fmt.format});
defer allocator.free(format_str);
try output.addToken(.string_literal, format_str);
try output.addToken(.comma, ",");
// Expression wrapped in tuple: .{expr}
try output.addToken(.invalid, " ");
try output.addToken(.period, ".");
try output.addToken(.l_brace, "{");
try output.addToken(.identifier, fmt.expr);
try output.addToken(.r_brace, "}");
try output.addToken(.r_paren, ")");
},
}
if (i < elem.attributes.items.len - 1) {
try output.addToken(.comma, ",");
@ -1757,6 +1875,22 @@ fn renderNestedElementAsCall(allocator: std.mem.Allocator, output: *TokenBuilder
.dynamic => |expr| {
try output.addToken(.identifier, expr);
},
.format => |fmt| {
// Format expression: pass expression directly and set format field
// .value = expr (expression as-is)
try output.addToken(.identifier, fmt.expr);
try output.addToken(.comma, ",");
try output.addToken(.invalid, "\n");
// .format = "{format}"
try addIndentTokens(output, indent + 3);
try output.addToken(.period, ".");
try output.addToken(.identifier, "format");
try output.addToken(.equal, "=");
const format_str = try std.fmt.allocPrint(allocator, "\"{{{s}}}\"", .{fmt.format});
defer allocator.free(format_str);
try output.addToken(.string_literal, format_str);
},
}
try output.addToken(.r_brace, "}");
@ -2001,6 +2135,22 @@ fn renderElementAsStruct(allocator: std.mem.Allocator, output: *TokenBuilder, el
// Dynamic expression - output as-is
try output.addToken(.identifier, expr);
},
.format => |fmt| {
// Format expression: pass expression directly and set format field
// .value = expr (expression as-is)
try output.addToken(.identifier, fmt.expr);
try output.addToken(.comma, ",");
try output.addToken(.invalid, "\n");
// .format = "{format}"
try addIndentTokens(output, indent + 3);
try output.addToken(.period, ".");
try output.addToken(.identifier, "format");
try output.addToken(.equal, "=");
const format_str = try std.fmt.allocPrint(allocator, "\"{{{s}}}\"", .{fmt.format});
defer allocator.free(format_str);
try output.addToken(.string_literal, format_str);
},
}
try output.addToken(.r_brace, "}");
try output.addToken(.comma, ",");