mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-21 02:59:36 -06:00
doc: add editor setup section
This commit is contained in:
parent
87a146eff4
commit
076112aea5
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.zig-cache
|
||||
zig-out
|
||||
.zx
|
||||
tmp
|
||||
@ -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 = .{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 = .{
|
||||
|
||||
@ -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">
|
||||
<h1>ZX</h1>
|
||||
<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">
|
||||
<h2>Introduction</h2>
|
||||
<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
153
site/pages/doc/util.zig
Normal 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
4
site/public/zx-icon.svg
Normal 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 |
81
src/main.zig
81
src/main.zig
@ -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,34 +254,56 @@ 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 (is_zx) {
|
||||
// Remove .zx and add .zig
|
||||
output_rel_path = try std.mem.concat(allocator, u8, &.{
|
||||
relative_path[0 .. relative_path.len - (".zx").len], // Remove .zx
|
||||
".zig",
|
||||
});
|
||||
if (should_transpile) {
|
||||
if (is_zx) {
|
||||
// Remove .zx and add .zig
|
||||
output_rel_path = try std.mem.concat(allocator, u8, &.{
|
||||
relative_path[0 .. relative_path.len - (".zx").len], // Remove .zx
|
||||
".zig",
|
||||
});
|
||||
} else {
|
||||
// For .zig files, keep the same name
|
||||
output_rel_path = try allocator.dupe(u8, relative_path);
|
||||
}
|
||||
} else {
|
||||
// For .zig files, keep the same name
|
||||
// For copying, keep the same name
|
||||
output_rel_path = try allocator.dupe(u8, relative_path);
|
||||
}
|
||||
defer allocator.free(output_rel_path);
|
||||
@ -283,10 +311,27 @@ fn transpileDirectory(
|
||||
const output_path = try std.fs.path.join(allocator, &.{ output_dir, output_rel_path });
|
||||
defer allocator.free(output_path);
|
||||
|
||||
transpileFile(allocator, input_path, output_path) catch |err| {
|
||||
std.debug.print("Error transpiling {s}: {}\n", .{ input_path, err });
|
||||
continue;
|
||||
};
|
||||
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
|
||||
|
||||
14
src/root.zig
14
src/root.zig
@ -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| {
|
||||
// HTML escape attribute values to prevent XSS
|
||||
// Escape quotes, ampersands, and other HTML special characters
|
||||
try writer.writeAll("=\"");
|
||||
try escapeAttributeValueToWriter(writer, value);
|
||||
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 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,
|
||||
|
||||
@ -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,10 +512,45 @@ 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 }
|
||||
|
||||
try elem.attributes.append(allocator, .{ .name = attr_name, .value = .{ .dynamic = expr } });
|
||||
// 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 } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, ",");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user