Skip to content

nextcore/zeno-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚡ zeno-rs

Your Laravel Blade Templates. Now Running in Rust.

You already know @if, @foreach, @extends, {{ $var }}, and <x-component>.
You don't need to learn a new template language. You need a faster runtime.

Why Switch? · vs Tera · Quickstart · Blade Reference · Components · Hot Reload


🤔 Why Leave PHP?

You love Laravel. The DX is excellent, the ecosystem is mature, and Blade is genuinely good.
But at some point, every Laravel project hits the same wall:

Problem PHP/Laravel zeno-rs (Rust)
Memory per request ~20–50 MB (FPM workers) ~2–5 MB (single binary)
Cold start Opcache warm-up required Instant — binary is pre-compiled
Concurrency Process-per-request (FPM) or Swoole Native async with Tokio / Axum
Deployment PHP runtime + Composer + env Single static binary, zero deps
Template syntax Laravel Blade Identical Blade syntax

The catch with every other Rust web framework: you have to throw away your templates.
Tera, Handlebars, MiniJinja — none of them speak Blade.

zeno-rs does. Your .blade.zl files work as-is.


🆚 zeno-blade vs Tera — Why Blade Wins

Tera is the most popular Rust template engine. It's solid, well-documented, and widely used.
But if you're a Laravel developer — or if you care about developer experience — it falls short in ways that matter every day.

The Hot Reload Problem (This Is the Big One)

Here's what your workflow looks like when you change a template:

With Tera:

// Option A: Restart the server every time.
// Option B: Call full_reload() — which re-reads and re-parses EVERY template.
tera.full_reload()?; // ← nukes the entire cache, re-parses all files

Tera has no per-file invalidation. Change one file → invalidate everything → re-parse everything.
On a project with 50+ templates, this adds latency to every dev refresh.

With zeno-blade:

Edit one template → Save → Refresh browser

✅ Only that one file is re-parsed (mtime check = 1 syscall)
✅ Every other template stays in RAM untouched
✅ Zero manual reload call needed
✅ Zero restart needed

zeno-blade uses mtime-based per-file cache invalidation:
check the file's last-modified timestamp on every request, re-parse only when it changes.
It's the best of both worlds — RAM speed when nothing changed, instant pickup when you saved.

Full Feature Comparison

Feature zeno-blade Tera Notes
🔥 Hot reload — auto, per-file Tera: call full_reload() to nuke entire cache
🎨 Laravel Blade syntax Tera uses Jinja2 / Django-like syntax
🧩 HTML components () Tera has no component system
📐 Layout inheritance (@extends) Both support @extends / @section / @yield
🔁 Loop with empty fallback (@forelse) No forelse equivalent in Tera
🎯 Conditional CSS classes (@class) Laravel-style @class directive
🔐 Form helpers (@csrf, @method) Tera has no form helpers
🧠 Embedded scripting (ZenoLang) Full scripting runtime built-in
🔌 Custom handler / slot system Register Rust functions callable from templates
📄 Built-in OpenAPI / Swagger UI Bundled in the zeno-rs workspace
🛡️ Zero unsafe code in core Both are memory-safe
📦 Maturity / ecosystem 🆕 Tera has a larger community — honest trade-off

Syntax: What You Already Know vs What You'd Have to Learn

<!-- Tera — Jinja2-style, new syntax to learn -->
{% for post in posts %}
  {% if post.featured %}
    <article>{{ post.title | upper }}</article>
  {% endif %}
{% else %}
  <p>No posts.</p>
{% endfor %}
{{-- zeno-blade — Laravel Blade, you already know this --}}
@forelse($posts as $post)
  @if($post_featured)
    <article>{{ $post }}</article>
  @endif
@empty
  <p>No posts.</p>
@endforelse

If you've written a single Laravel view, you already know how to write zeno-blade templates.

Note

Tera is an excellent library and the right choice if you're not coming from a Blade background.
If you are — zeno-blade gives you Laravel's template DX at Rust's performance level.


🔥 What Exactly Is This?

zeno-rs is a Rust workspace (monorepo) containing:

zeno-rs/
├── crates/
│   ├── zenocore/        # 🔩 Core engine: lexer, parser, executor, scope — zero dependencies
│   ├── zeno-blade/      # 🎨 THE Blade engine — transpiles .blade.zl → AST → HTML
│   ├── zeno-std/        # 🧰 Standard library: math, date, string, money
│   ├── zeno-apidoc/     # 📄 OpenAPI 3.0 spec + Swagger UI
│   └── zenoengine/      # 📦 Batteries-included facade (start here)
└── examples/
    └── web_server/      # 🚀 Full Axum web server, ready to run

zeno-blade is the star of the show — a full Blade engine living inside zeno-rs.
It is the Rust sibling of nextcore/zeno-go, the original Go implementation.
Templates are 100% portable between Go and Rust backends.


⚡ 2-Minute Migration

Step 1 — Add to Cargo.toml

[dependencies]
zenoengine = "0.1"   # batteries-included facade
zeno-blade  = "0.1"  # or just the Blade engine, if you don't need the full stack

All crates are published on crates.io. No git URLs needed.

Step 2 — Point it at your existing views directory

use std::sync::Mutex;
use zenoengine::{new_engine, executor::Context, scope::{Scope, Value}};
use zeno_blade::{register_blade_slots, slots::HtmlBuffer};
use zenocore::parser::parse_string;

let mut engine = new_engine();
register_blade_slots(&mut engine);

let mut ctx = Context::new();
ctx.set("httpWriter", HtmlBuffer(Mutex::new(String::new())));

let scope = Scope::new(None);
scope.set("_view_root", Value::String("resources/views".to_string())); // 👈 same path
scope.set("user",  Value::String("Andi".to_string()));
scope.set("title", Value::String("Dashboard".to_string()));

let node = parse_string("view.blade: 'dashboard'", "main.zl").unwrap();
engine.execute(&mut ctx, &node, &scope).unwrap();

let html = ctx.get::<HtmlBuffer>("httpWriter").unwrap();
println!("{}", html.0.lock().unwrap()); // ← your rendered HTML

Step 3 — Your existing Blade templates work unchanged

{{-- resources/views/dashboard.blade.zl — no changes needed --}}
@extends('layouts.app')

@section('content')
  <h1>Welcome, {{ $user }}!</h1>

  @if($role == 'admin')
    <span class="badge badge-danger">Admin</span>
  @endif

  @forelse($posts as $post)
    <article><h2>{{ $post }}</h2></article>
  @empty
    <p>No posts yet.</p>
  @endforelse
@endsection

That's it. No rewrite. No new syntax. Just a faster runtime.


🎨 Blade Directives

zeno-blade transpiles .blade.zl templates to ZenoLang AST nodes, then executes them against the zenocore engine. The result is standard HTML — same as what Laravel would produce.

Full directive support, identical to Laravel Blade:

@extends('layouts.app')

@section('content')

<h1>Welcome, {{ $user }}!</h1>

{{-- Comments never appear in output --}}

@if($role == 'admin')
    <span class="badge">Admin</span>
@elseif($role == 'moderator')
    <span class="badge">Mod</span>
@else
    <span class="badge">User</span>
@endif

@forelse($posts as $post)
    <article>
        <h2>{{ $post }}</h2>
    </article>
@empty
    <p>No posts yet. Start writing!</p>
@endforelse

<form method="POST" action="/update">
    @csrf
    @method('PUT')
    <button type="submit">Save</button>
</form>

@endsection

@push('scripts')
    <script src="/app.js"></script>
@endpush

Directive Reference

Directive Laravel Blade zeno-blade
{{ $var }} — escaped echo
{!! $raw !!} — raw echo
@if / @elseif / @else / @endif
@foreach / @endforeach
@forelse / @empty / @endforelse
@extends('layout')
@section / @endsection
@yield('name')
@include('partial')
@push('stack') / @stack('stack')
@class(['cls' => $cond])
@method('PUT')
@csrf
{{-- comment --}}

🧩 HTML Components

Identical to Laravel Blade components — <x-component> with named slots and dynamic props.

Define onceresources/views/components/alert.blade.zl:

<div @class(['alert', 'alert-danger' => $is_danger, 'alert-success' => $is_success])>
    <strong>{{ $header }}</strong>
    <p>{{ $slot }}</p>
</div>

Use anywhere — same syntax as Laravel:

<x-alert :is_danger="true">
    <x-slot name="header">Access Denied</x-slot>
    You don't have permission to view this page.
</x-alert>

Output:

<div class="alert alert-danger">
    <strong>Access Denied</strong>
    <p>You don't have permission to view this page.</p>
</div>

Props are automatically isolated — each component gets its own scope. No variable pollution.


⚙️ Template Loading & Hot Reload

Important

Hot reload is the #1 reason to choose zeno-blade over Tera.
See the full comparison for details.

Most Rust template engines force a painful choice: either restart the server, or reload everything from scratch. zeno-blade does neither.

zeno-blade uses a smart mtime-based per-file cache:

  1. Template loads → parsed to AST, stored in RAM. ⚡
  2. Next request → check file's modified time (one lightweight syscall, no file read).
  3. File unchanged → serve AST straight from RAM. Zero disk I/O.
  4. File changed → re-read, re-parse, update cache automatically.
Edit template → Save → Refresh browser  ✅  (changes visible instantly)
No edits → Every subsequent request      ✅  (served from RAM, no disk touch)

No env vars. No restart. No cargo build. Works out of the box.

Tip

Recompiling Rust is only needed when you change Rust code (handlers, slots, business logic).
Template changes — layouts, components, partials — are always hot-reloaded automatically.

Preload mode (strict production)

If you want the server to fail at startup rather than at runtime when a template is missing:

use zeno_blade::transpiler::transpile_blade_native;

// In main() — warm up the entire cache before accepting requests
let views = ["dashboard", "layouts/app", "partials/header"];
for view in &views {
    let path = format!("resources/views/{}.blade.zl", view);
    let content = std::fs::read_to_string(&path)
        .unwrap_or_else(|_| panic!("Missing template at startup: {}", path));
    transpile_blade_native(&content, &path)
        .unwrap_or_else(|e| panic!("Template parse error at startup: {}", e));
}
// All templates pre-loaded. Server ready.

🧰 ZenoLang — The Logic Layer

Beyond Blade, zeno-rs includes ZenoLang — a readable, indented scripting language that powers the execution layer. You won't write it in templates directly (Blade directives handle that), but it's available for server-side scripts and custom logic:

# Variables & types
set: $name = "Andi"
set: $score = 95
set: $tags = ['rust', 'fast', 'safe']

# Conditionals
if: $score >= 90 {
  then: { set: $grade = "A" }
  elseif: $score >= 80 { set: $grade = "B" }
  else: { set: $grade = "C" }
}

# Loops
for: $tags {
  as: $tag
  do: { log: "$loop.iteration. $tag" }
}

# Functions
fn: add {
  params: [$a, $b]
  do: { return: $a + $b }
}

# Error handling
try {
  do: { http.get: 'https://api.example.com/data' }
  catch: { log: "Failed: $error" }
}

🔌 Custom Slots (Extend the Engine)

Register your own handlers in Rust and call them from any template or script — like Laravel's custom Blade directives, but with the full power of Rust:

use std::sync::Arc;
use zenocore::{Engine, SlotMeta, Value};

fn register_my_slots(engine: &mut Engine) {
    engine.register(
        "db.find",
        Arc::new(|engine, _ctx, node, scope| {
            let table = engine.resolve_shorthand_value(node, scope).to_string_coerce();
            // ... query your database (sqlx, diesel, etc.)
            scope.set("result", Value::String(format!("Queried {}", table)));
            Ok(())
        }),
        SlotMeta {
            description: "Query a database table".to_string(),
            ..Default::default()
        },
    );
}

Then call it from a template or script:

db.find: 'users'
log: $result   # → Queried users

🚀 Axum Example

A full Axum web server with Blade rendering, ZenoLang execution, and Swagger UI — clone and run:

git clone https://github.com/nextcore/zeno-rs
cd zeno-rs
cargo run -p web_server_example
🚀 ZenoEngine Axum server running at http://127.0.0.1:3000
📖 Swagger UI at http://127.0.0.1:3000/docs
Method Path Description
POST /execute Execute a ZenoLang script
GET /docs Swagger UI
GET /openapi.json OpenAPI 3.0 spec

📄 OpenAPI / Swagger (Bonus)

Auto-generate API docs from your routes with zero config — something you'd need a separate package for in Laravel:

use zenoengine::apidoc::{APIRegistry, RouteDoc};

let registry = APIRegistry::global();
registry.register("POST", "/users", RouteDoc {
    summary: "Create User".to_string(),
    tags: vec!["Users".to_string()],
    // ...
});

// GET /openapi.json → full OpenAPI 3.0 spec
// GET /docs         → interactive Swagger UI

🏗️ Build & Test

git clone https://github.com/nextcore/zeno-rs
cd zeno-rs

# Build all crates
cargo build

# Run all tests
cargo test --all

# Run only Blade engine tests
cargo test -p zeno-blade

Requirements: Rust 1.85+ (Edition 2024)


🔗 Ecosystem

Repository Language Description
nextcore/zeno-go Go Original ZenoEngine — Go implementation
nextcore/zeno-rs Rust This repository — Rust port

Templates written for zeno-go are 100% compatible with zeno-rs.
Same .blade.zl files. Same directives. Same component syntax. Different runtime.


📝 License

Apache 2.0 © NextCore


Keep your Blade templates. Ditch the PHP overhead. Ship in Rust.

zeno-rs is the workspace — zeno-blade is the Blade engine inside it.

⭐ If this saves you a rewrite, give it a star!

About

⚡ Embedded Blade templating engine for Rust with a full scripting runtime, HTML components, and zero-dependency core.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages