pub enum Node {
Show 24 variants
Text(String),
Code(String),
Interpolate {
expr: String,
escape: bool,
},
Js(String),
If {
cond: String,
then_branch: Vec<Node>,
else_branch: Vec<Node>,
},
Foreach {
binding: String,
iter: String,
body: Vec<Node>,
},
Extends(String),
Section {
name: String,
body: Vec<Node>,
},
Yield(String),
Push {
name: String,
body: Vec<Node>,
},
Stack(String),
Csrf,
Global {
name: String,
fallback: Option<String>,
},
PersistGlobal {
cookie_name: String,
fallback_expr: String,
},
Globals(Vec<GlobalEntry>),
Wire {
name: String,
props: Vec<(String, String)>,
},
LarustScripts,
LoadOnce(Vec<Node>),
Resource {
name: String,
props: Vec<(String, String)>,
slot: Vec<Node>,
},
Live {
channel: String,
body: Vec<Node>,
},
Can {
permission: String,
then_branch: Vec<Node>,
else_branch: Vec<Node>,
},
Role {
role: String,
then_branch: Vec<Node>,
else_branch: Vec<Node>,
},
Vitex(Vec<String>),
Spa(Vec<Node>),
}Variants§
Text(String)
Code(String)
@code ... @endcode - trusted, inline Rust statements executed in
the generated view function. It renders no output itself; use it for
small local derivations consumed by later interpolations. This is a
Rust escape hatch, never a PHP compatibility layer.
Interpolate
Js(String)
@js($expr) - serializes an arbitrary Serialize Rust value into
JS-safe (not HTML-escaped) inline output, for dropping server data
into an inline <script> block. A separate variant from
Interpolate rather than a third state on its escape: bool: JS-safe
JSON escaping is a fundamentally different transform than
HTML-escaping, not a variation on it. See larust_view::runtime::js
for the actual encoding (mirrors Laravel’s Illuminate\Support\ Js::from()).
If
Foreach
Fields
binding: StringAny valid Rust pattern - a single identifier (post), or a
tuple pattern ((key, item)) for keyed iteration, e.g.
@foreach((key, item) in items.iter().enumerate()). Stored raw,
same convention as iter/cond/expr elsewhere in this enum -
larust-macros parses it as a real syn::Pat at codegen time
(M4; a single bare identifier was the only M3 shape).
Extends(String)
Section
Yield(String)
Push
@push('name') ... @endpush - unlike @section (single body,
last-write-wins), multiple @pushes to the same name accumulate;
see larust_view::resolve for how they’re collected across an
@extends chain and spliced into the matching @stack.
Stack(String)
@stack('name') - renders every @push body pushed to that name,
concatenated in the order they were collected. Laravel’s own
pairing: @stack sits in the layout (usually <head> or right
before </body>), @push calls live in whichever child templates
have page-specific <script>/<meta>/etc. to contribute.
Csrf
@csrf - a hidden CSRF-token input field. Expands to a fixed
<input> reading a csrf_token variable that must be present in
the view’s context (a compile-time-checked convention, same as any
other undeclared-variable-becomes-an-error interpolation).
Global
@global(name) or @global(name, fallback) - a page-overridable
named placeholder, usually in a layout
(<title>@global(title, "Larust")</title>). Single value,
last-write-wins per name across the whole @extends chain - see
Globals and larust_view::resolve for how a page sets it and why
resolution is a whole-chain collect-then-substitute pass (same shape
as Push/Stack), not a per-level one (unlike Section/Yield).
fallback, like Globals’ own expr values, is a raw Rust
expression string, used only when no @globals block anywhere in
the chain sets name - if absent, an unset global renders empty,
same convention as an unset @stack.
PersistGlobal
Substituted in by resolve::substitute_globals in place of an
ordinary Global/Interpolate, for a persist-flagged
@globals entry - see docs/MACROS.md‘s persist section for
the full design. Unlike every other Global substitution, this
one’s actual value isn’t knowable at compile time (it’s whatever
cookie the current request carries, not a fixed expression), so
it needs its own codegen arm emitting real runtime code - the
same reason Wire gets its own arm instead of collapsing to
Interpolate. cookie_name is the bare preference name (e.g.
"theme", not the larust_pref_theme cookie name itself - that
prefixing is larust_http::preferences’ own concern); fallback_expr
is the @globals entry’s own raw expression, used when the
browser never set that cookie.
Globals(Vec<GlobalEntry>)
@globals ... @endglobals - one or more name = expr assignment
lines on a page; each overrides the matching @global(name)
placeholder anywhere in whichever layout(s) this page’s @extends
chain reaches. expr is stored raw (same convention as
Interpolate::expr) and is substituted into a real Interpolate
node at resolve time, so it’s a genuine, type-checked Rust
expression - any type implementing ToString, not just string
literals. A persist-prefixed entry (persist theme = "dark")
substitutes to Node::PersistGlobal instead - see its own doc
comment for why that needs a different substitution target.
Wire
@wire('name') or @wire('name', { prop: expr, ... }) - a mount
point for a server-state-backed reactive component (see
larust-live; the crate keeps its original name, only the
user-facing directive/trait/route surface renamed from @live to
@wire, freeing @live/@endlive for a future genuinely
server-pushed live-update feature - see docs/ARCHITECTURE.md).
props are raw Rust expression strings, same convention as
Globals’ (name, expr) pairs, evaluated and JSON-encoded at
codegen time in larust-macros. Unlike Section/Push/Global,
this node needs no resolve.rs pass at all - it renders positionally
wherever it appears (including inside If/Foreach, deliberately
unrestricted, since mounting is an ordinary runtime statement, not a
compile-time collection pass).
LarustScripts
@larustscripts - Livewire’s @livewireScripts equivalent: a
layout-placed marker (conventionally right before </body>) that
expands to the <script src="/__larust_wire/runtime.js" defer>
tag only if this page actually mounts a @wire(...) component
somewhere in its resolved tree (itself or, via @extends, the
layout it’s rendered through) - otherwise it expands to nothing.
Written once in a shared layout so no individual page needs its own
<script> tag for wire:model/wire:click/wire:submit to work;
see larust_macros::view::expand’s contains_wire check for how
“only if” is decided at compile time, not a runtime branch.
LoadOnce(Vec<Node>)
@loadonce ... @endloadonce - sugar for wrapping body in a
<div wire:ignore>...</div>. Content inside still renders on
every server response (including every @wire(...) component
re-render fragment) - the “once” is enforced client-side, by
wire:ignore telling the DOM patcher to skip that subtree entirely
after its first mount, not by the server omitting it. That
distinction matters: if the server instead omitted this block from
later fragments, the client’s positional child diffing (see
larust-live/assets/wire-runtime.js) would misalign every sibling
after it - this node exists specifically so callers get the
“contained, load-once” ergonomics without hand-writing wire:ignore
and without that correctness trap. Safe to use even outside a
@wire(...) template: wire:ignore is simply inert markup on a
page with no client runtime patching it.
Resource
@resource('name', { prop: expr, ... }) ... @endresource - a
static, non-reactive, slot-capable template inclusion. Laravel’s
real split: @wire(...) is the Livewire-equivalent reactive
component (session state, AJAX round-trip); @resource(...) is the
Blade-component equivalent (@component/@endcomponent) - props
plus a slot, resolved once at render time, no session storage, no
client JS, no round-trip at all.
Always a block (@endresource required, even with nothing between
the tags) - deliberately unlike @wire(...)’s self-closing form, to
avoid the parser needing lookahead to decide whether a body follows;
matches Blade’s own @component/@endcomponent always-paired shape.
props become real let bindings in the included template’s own
scope at codegen time (see larust-macros), not a serialized
HashMap the way @wire(...)’s props are - there’s no session/JSON
boundary to cross here, so this can just lean on Rust’s own type
system directly, no serde_json round-trip needed at all. slot
holds @resource(...)’s captured body exactly as written in the
caller’s own template (its expressions still resolve against the
caller’s own scope, not the included template’s) - codegen renders
it first, into a plain String, and binds that as a slot variable
the included template can place anywhere via the existing
{!! slot !!} raw-interpolation mechanism. No new “slot placeholder”
AST concept needed on the receiving side at all - it’s just another
variable.
Live
@live(channel_expr) ... @endlive - genuine server-pushed
real-time updates (see larust-live::push’s own module doc for the
full design), the third and last of this framework’s three
template-inclusion directives: @wire(...) is client-initiated
reactive (a user’s own action triggers an AJAX round-trip);
@resource(...) is static compile-time inclusion (props + slot, no
server round-trip at all); @live(...) is server-initiated push (a
server-side event updates every currently-connected viewer, with
no interaction required in any of their tabs - the live-chat/
live-notification case neither of the other two can express).
channel is a raw Rust expression (any ToString-yielding value),
not a quoted string literal like @wire/@resource’s own name
argument - parsed via parse_paren_expr, the same balanced-paren
scanner @if/@foreach use. Unlike those two, there’s no
compile-time file/registry lookup keyed on this value (a channel is
just a runtime string handed to the broadcast/subscribe mechanism),
so it can safely be dynamic - @live(format!("post.{}.comments", post.id)) scopes a channel per-resource, which a fixed literal
couldn’t express.
body renders inline, once, at page-load time, in the caller’s
own scope - same “no separate buffer, no runtime dispatch” shape
@loadonce’s body has, not @wire’s stateful mount/render/call
machinery. There’s no component struct here at all: the app is
responsible for constructing new HTML (shaped the same way this
node’s own codegen wraps body) and calling
larust_support::push::broadcast whenever the state this channel
represents actually changes - this node only handles the initial
render and the <div data-live-channel="..."> wrapper the client
runtime’s WebSocket patches into.
Can
@can(expr) ... @endcan, optionally with @else ... @endcan - the
larust_support::permission (backed by larust-permissions)
template-level check that crate’s own doc comment names as “the one
real gap this version leaves.” Renders then_branch if the
request’s user currently has the permission expr names,
else_branch (or nothing, with no @else) otherwise.
expr is a raw Rust expression - same convention as Node::Live’s
own channel - not a quoted string literal like @wire/@resource’s
name. This matters beyond consistency: it’s what carries
larust_permissions’s own “names are compile-checked, assignment is
not” hybrid design (see that crate’s module doc comment) all the way
into templates. @can('edit-posts') would silently compile a typo’d
permission name into an always-false string lookup; @can(Permission ::EditPosts) makes the same typo (EditPost, missing the s) a
rustc error at the template’s own call site, the same guarantee
every other Rust call in this codebase already has.
Requires a user: &U binding (U: Authenticatable) in the view!
context, checked eagerly by larust-macros::view::expand - the same
“requires a binding, checked eagerly, before codegen ever reaches
this arm” shape Node::Wire’s own session requirement already
establishes. Also like Node::Wire, this needs .await - the
permission check is a real DB round trip, not a compile-time or
pure-runtime-data lookup - so the view! call site must be an
async fn returning a Result; a DB error while checking
propagates as a real error (?), never silently rendered as “no
permission” (matching this codebase’s “never silently swallow
errors” convention - see larust_permissions::has_permission_to’s
own Result-returning signature, which this codegen doesn’t
downgrade to a bool-with-swallowed-error).
Role
@role(expr) ... @endrole, optionally with @else ... @endrole -
Node::Can’s exact shape and reasoning, checking
larust_support::permission::has_role instead of
has_permission_to. expr is a raw Rust expression yielding a
RoleName implementor (e.g. Role::Moderator), same “compile-check
the name, not the assignment” reasoning as Node::Can’s own expr.
Vitex(Vec<String>)
@vitex(['resources/css/app.css', 'resources/js/app.js']) -
Larust’s own answer to Laravel’s @vite(...) directive: real
integration with the app’s existing Vite/Tailwind toolchain (see
larust_support::vitex’s own module doc for the full dev/
production dual-mode design), not a from-scratch bundler. Each
entry is a plain string - the exact key Vite’s own manifest/dev
server use - never a general expression, matching @vite’s own
real syntax so a converted template reads the same way the
original Laravel source did. No scope dependency at all (no
session/csrf_token, unlike @wire/@csrf) - safe to use
anywhere, including directly in WireComponent::render(&self).
Spa(Vec<Node>)
@spa ... @endspa - marks a layout’s single swappable SPA-navigation
region. Renders body wrapped in a fixed sentinel
<div id="__larust_spa_root">...</div>; the client runtime
(larust-spa’s spa-runtime.js) replaces this div’s innerHTML
wholesale on every intercepted same-origin link click or form
submit, updates <title>, and pushes the new URL via the History
API - see docs/MACROS.md’s @spa section for the full design.
Deliberately not @csrf’s bare, position-only shape, despite
the directive’s own simple “just enable this” spirit: unlike
@csrf, this needs to wrap a region (typically
@yield('content')), and resolve.rs’s substitute_yields only
knows how to replace a bare Node::Yield inline - there’s no “wrap
this yield’s output” concept, and inventing one would be a bigger,
more magic change than the feature warrants (it would also have to
guess which yield is “the” content region, when the yield name is
just a per-app convention, not hardcoded). So this follows the same
open-body-close shape @loadonce/@resource/@live/@can/@role
already use instead, letting the layout author place @spa/
@endspa explicitly around whatever they want swapped:
@spa
@yield('content')
@endspaBecause it has a body, resolve.rs’s substitute_yields (once
Node::Spa is threaded through it, same as every other body-bearing
node) reaches into that body and replaces the nested
Node::Yield("content") exactly as it already does for
Node::If/Node::Section bodies - this is what actually makes the
design work, and is the concrete reason a bare tag doesn’t.
No scope dependency at all (no session/csrf_token/user,
unlike @wire/@csrf/@can/@role) - it emits only static markup
and (via @larustscripts) a static <script> tag, matching
Node::Vitex’s own “safe to use anywhere” precedent. A resolved
tree with more than one @spa block is a compile-time error (the
sentinel id can’t be duplicated) - checked eagerly by
larust-macros::view::expand’s count_spa scan.