zenoengine 0.1.0

Batteries-included Rust web engine — Blade templating, ZenoLang scripting, OpenAPI docs, and standard library in one facade
Documentation

⚡ 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!