Skip to main content

larust_view/
ast.rs

1/// A parsed `.blade.xr` template, before layout resolution.
2///
3/// Expression text (`expr`, `cond`, `iter`) is kept as raw strings here -
4/// this crate is pure text parsing with no `syn`/`proc-macro2` dependency.
5/// `larust-macros` parses those strings into real `syn::Expr`s at codegen
6/// time, which is also what makes `{{ user.name }}` a genuine, type-checked
7/// Rust expression rather than a custom mini-language.
8/// One `name = expr` line inside a `@globals ... @endglobals` block -
9/// see `Node::Globals`. `persist` marks a `persist name = expr` line
10/// (`persist` is a line prefix, not a separate directive), meaning `expr`
11/// is only ever the *fallback* used when the browser hasn't set the
12/// corresponding preference cookie yet - see `Node::PersistGlobal`.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct GlobalEntry {
15    pub name: String,
16    pub expr: String,
17    pub persist: bool,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Node {
22    Text(String),
23    /// `@code ... @endcode` - trusted, inline Rust statements executed in
24    /// the generated view function. It renders no output itself; use it for
25    /// small local derivations consumed by later interpolations. This is a
26    /// Rust escape hatch, never a PHP compatibility layer.
27    Code(String),
28    Interpolate {
29        expr: String,
30        /// `{{ }}` (HTML-escaped) vs `{!! !!}` (raw).
31        escape: bool,
32    },
33    /// `@js($expr)` - serializes an arbitrary `Serialize` Rust value into
34    /// JS-safe (not HTML-escaped) inline output, for dropping server data
35    /// into an inline `<script>` block. A separate variant from
36    /// `Interpolate` rather than a third state on its `escape: bool`: JS-safe
37    /// JSON escaping is a fundamentally different transform than
38    /// HTML-escaping, not a variation on it. See `larust_view::runtime::js`
39    /// for the actual encoding (mirrors Laravel's `Illuminate\Support\
40    /// Js::from()`).
41    Js(String),
42    If {
43        cond: String,
44        then_branch: Vec<Node>,
45        else_branch: Vec<Node>,
46    },
47    Foreach {
48        /// Any valid Rust pattern - a single identifier (`post`), or a
49        /// tuple pattern (`(key, item)`) for keyed iteration, e.g.
50        /// `@foreach((key, item) in items.iter().enumerate())`. Stored raw,
51        /// same convention as `iter`/`cond`/`expr` elsewhere in this enum -
52        /// `larust-macros` parses it as a real `syn::Pat` at codegen time
53        /// (M4; a single bare identifier was the only M3 shape).
54        binding: String,
55        iter: String,
56        body: Vec<Node>,
57    },
58    Extends(String),
59    Section {
60        name: String,
61        body: Vec<Node>,
62    },
63    Yield(String),
64    /// `@push('name') ... @endpush` - unlike `@section` (single body,
65    /// last-write-wins), multiple `@push`es to the same name *accumulate*;
66    /// see `larust_view::resolve` for how they're collected across an
67    /// `@extends` chain and spliced into the matching `@stack`.
68    Push {
69        name: String,
70        body: Vec<Node>,
71    },
72    /// `@stack('name')` - renders every `@push` body pushed to that name,
73    /// concatenated in the order they were collected. Laravel's own
74    /// pairing: `@stack` sits in the layout (usually `<head>` or right
75    /// before `</body>`), `@push` calls live in whichever child templates
76    /// have page-specific `<script>`/`<meta>`/etc. to contribute.
77    Stack(String),
78    /// `@csrf` - a hidden CSRF-token input field. Expands to a fixed
79    /// `<input>` reading a `csrf_token` variable that must be present in
80    /// the view's context (a compile-time-checked convention, same as any
81    /// other undeclared-variable-becomes-an-error interpolation).
82    Csrf,
83    /// `@global(name)` or `@global(name, fallback)` - a page-overridable
84    /// named placeholder, usually in a layout
85    /// (`<title>@global(title, "Larust")</title>`). Single value,
86    /// last-write-wins per name across the whole `@extends` chain - see
87    /// `Globals` and `larust_view::resolve` for how a page sets it and why
88    /// resolution is a whole-chain collect-then-substitute pass (same shape
89    /// as `Push`/`Stack`), not a per-level one (unlike `Section`/`Yield`).
90    /// `fallback`, like `Globals`' own `expr` values, is a raw Rust
91    /// expression string, used only when no `@globals` block anywhere in
92    /// the chain sets `name` - if absent, an unset global renders empty,
93    /// same convention as an unset `@stack`.
94    Global {
95        name: String,
96        fallback: Option<String>,
97    },
98    /// Substituted in by `resolve::substitute_globals` in place of an
99    /// ordinary `Global`/`Interpolate`, for a `persist`-flagged
100    /// `@globals` entry - see `docs/MACROS.md`'s `persist` section for
101    /// the full design. Unlike every other `Global` substitution, this
102    /// one's actual value isn't knowable at compile time (it's whatever
103    /// cookie the *current request* carries, not a fixed expression), so
104    /// it needs its own codegen arm emitting real runtime code - the
105    /// same reason `Wire` gets its own arm instead of collapsing to
106    /// `Interpolate`. `cookie_name` is the bare preference name (e.g.
107    /// `"theme"`, not the `larust_pref_theme` cookie name itself - that
108    /// prefixing is `larust_http::preferences`' own concern); `fallback_expr`
109    /// is the `@globals` entry's own raw expression, used when the
110    /// browser never set that cookie.
111    PersistGlobal {
112        cookie_name: String,
113        fallback_expr: String,
114    },
115    /// `@globals ... @endglobals` - one or more `name = expr` assignment
116    /// lines on a page; each overrides the matching `@global(name)`
117    /// placeholder anywhere in whichever layout(s) this page's `@extends`
118    /// chain reaches. `expr` is stored raw (same convention as
119    /// `Interpolate::expr`) and is substituted into a real `Interpolate`
120    /// node at resolve time, so it's a genuine, type-checked Rust
121    /// expression - any type implementing `ToString`, not just string
122    /// literals. A `persist`-prefixed entry (`persist theme = "dark"`)
123    /// substitutes to `Node::PersistGlobal` instead - see its own doc
124    /// comment for why that needs a different substitution target.
125    Globals(Vec<GlobalEntry>),
126    /// `@wire('name')` or `@wire('name', { prop: expr, ... })` - a mount
127    /// point for a server-state-backed reactive component (see
128    /// `larust-live`; the crate keeps its original name, only the
129    /// user-facing directive/trait/route surface renamed from `@live` to
130    /// `@wire`, freeing `@live`/`@endlive` for a future genuinely
131    /// server-pushed live-update feature - see `docs/ARCHITECTURE.md`).
132    /// `props` are raw Rust expression strings, same convention as
133    /// `Globals`' `(name, expr)` pairs, evaluated and JSON-encoded at
134    /// codegen time in `larust-macros`. Unlike `Section`/`Push`/`Global`,
135    /// this node needs no `resolve.rs` pass at all - it renders positionally
136    /// wherever it appears (including inside `If`/`Foreach`, deliberately
137    /// unrestricted, since mounting is an ordinary runtime statement, not a
138    /// compile-time collection pass).
139    Wire {
140        name: String,
141        props: Vec<(String, String)>,
142    },
143    /// `@larustscripts` - Livewire's `@livewireScripts` equivalent: a
144    /// layout-placed marker (conventionally right before `</body>`) that
145    /// expands to the `<script src="/__larust_wire/runtime.js" defer>`
146    /// tag *only if* this page actually mounts a `@wire(...)` component
147    /// somewhere in its resolved tree (itself or, via `@extends`, the
148    /// layout it's rendered through) - otherwise it expands to nothing.
149    /// Written once in a shared layout so no individual page needs its own
150    /// `<script>` tag for `wire:model`/`wire:click`/`wire:submit` to work;
151    /// see `larust_macros::view::expand`'s `contains_wire` check for how
152    /// "only if" is decided at compile time, not a runtime branch.
153    LarustScripts,
154    /// `@loadonce ... @endloadonce` - sugar for wrapping `body` in a
155    /// `<div wire:ignore>...</div>`. Content inside still renders on
156    /// *every* server response (including every `@wire(...)` component
157    /// re-render fragment) - the "once" is enforced client-side, by
158    /// `wire:ignore` telling the DOM patcher to skip that subtree entirely
159    /// after its first mount, not by the server omitting it. That
160    /// distinction matters: if the server instead omitted this block from
161    /// later fragments, the client's *positional* child diffing (see
162    /// `larust-live/assets/wire-runtime.js`) would misalign every sibling
163    /// after it - this node exists specifically so callers get the
164    /// "contained, load-once" ergonomics without hand-writing `wire:ignore`
165    /// and without that correctness trap. Safe to use even outside a
166    /// `@wire(...)` template: `wire:ignore` is simply inert markup on a
167    /// page with no client runtime patching it.
168    LoadOnce(Vec<Node>),
169    /// `@resource('name', { prop: expr, ... }) ... @endresource` - a
170    /// static, non-reactive, slot-capable template inclusion. Laravel's
171    /// real split: `@wire(...)` is the Livewire-equivalent reactive
172    /// component (session state, AJAX round-trip); `@resource(...)` is the
173    /// Blade-component equivalent (`@component`/`@endcomponent`) - props
174    /// plus a slot, resolved once at render time, no session storage, no
175    /// client JS, no round-trip at all.
176    ///
177    /// Always a block (`@endresource` required, even with nothing between
178    /// the tags) - deliberately unlike `@wire(...)`'s self-closing form, to
179    /// avoid the parser needing lookahead to decide whether a body follows;
180    /// matches Blade's own `@component`/`@endcomponent` always-paired shape.
181    ///
182    /// `props` become real `let` bindings in the included template's own
183    /// scope at codegen time (see `larust-macros`), not a serialized
184    /// HashMap the way `@wire(...)`'s props are - there's no session/JSON
185    /// boundary to cross here, so this can just lean on Rust's own type
186    /// system directly, no `serde_json` round-trip needed at all. `slot`
187    /// holds `@resource(...)`'s captured body exactly as written in the
188    /// *caller's* own template (its expressions still resolve against the
189    /// caller's own scope, not the included template's) - codegen renders
190    /// it first, into a plain `String`, and binds that as a `slot` variable
191    /// the included template can place anywhere via the *existing*
192    /// `{!! slot !!}` raw-interpolation mechanism. No new "slot placeholder"
193    /// AST concept needed on the receiving side at all - it's just another
194    /// variable.
195    Resource {
196        name: String,
197        props: Vec<(String, String)>,
198        slot: Vec<Node>,
199    },
200    /// `@live(channel_expr) ... @endlive` - genuine server-*pushed*
201    /// real-time updates (see `larust-live::push`'s own module doc for the
202    /// full design), the third and last of this framework's three
203    /// template-inclusion directives: `@wire(...)` is client-initiated
204    /// reactive (a user's own action triggers an AJAX round-trip);
205    /// `@resource(...)` is static compile-time inclusion (props + slot, no
206    /// server round-trip at all); `@live(...)` is server-initiated push (a
207    /// server-side event updates *every* currently-connected viewer, with
208    /// no interaction required in any of their tabs - the live-chat/
209    /// live-notification case neither of the other two can express).
210    ///
211    /// `channel` is a raw Rust expression (any `ToString`-yielding value),
212    /// not a quoted string literal like `@wire`/`@resource`'s own `name`
213    /// argument - parsed via `parse_paren_expr`, the same balanced-paren
214    /// scanner `@if`/`@foreach` use. Unlike those two, there's no
215    /// compile-time file/registry lookup keyed on this value (a channel is
216    /// just a runtime string handed to the broadcast/subscribe mechanism),
217    /// so it can safely be dynamic - `@live(format!("post.{}.comments",
218    /// post.id))` scopes a channel per-resource, which a fixed literal
219    /// couldn't express.
220    ///
221    /// `body` renders inline, once, at page-load time, in the *caller's*
222    /// own scope - same "no separate buffer, no runtime dispatch" shape
223    /// `@loadonce`'s body has, not `@wire`'s stateful mount/render/call
224    /// machinery. There's no component struct here at all: the app is
225    /// responsible for constructing new HTML (shaped the same way this
226    /// node's own codegen wraps `body`) and calling
227    /// `larust_support::push::broadcast` whenever the state this channel
228    /// represents actually changes - this node only handles the initial
229    /// render and the `<div data-live-channel="...">` wrapper the client
230    /// runtime's WebSocket patches into.
231    Live {
232        channel: String,
233        body: Vec<Node>,
234    },
235    /// `@can(expr) ... @endcan`, optionally with `@else ... @endcan` - the
236    /// `larust_support::permission` (backed by `larust-permissions`)
237    /// template-level check that crate's own doc comment names as "the one
238    /// real gap this version leaves." Renders `then_branch` if the
239    /// request's `user` currently has the permission `expr` names,
240    /// `else_branch` (or nothing, with no `@else`) otherwise.
241    ///
242    /// `expr` is a raw Rust expression - same convention as [`Node::Live`]'s
243    /// own `channel` - not a quoted string literal like `@wire`/`@resource`'s
244    /// `name`. This matters beyond consistency: it's what carries
245    /// `larust_permissions`'s own "names are compile-checked, assignment is
246    /// not" hybrid design (see that crate's module doc comment) all the way
247    /// into templates. `@can('edit-posts')` would silently compile a typo'd
248    /// permission name into an always-false string lookup; `@can(Permission
249    /// ::EditPosts)` makes the same typo (`EditPost`, missing the `s`) a
250    /// `rustc` error at the template's own call site, the same guarantee
251    /// every other Rust call in this codebase already has.
252    ///
253    /// Requires a `user: &U` binding (`U: Authenticatable`) in the `view!`
254    /// context, checked eagerly by `larust-macros::view::expand` - the same
255    /// "requires a binding, checked eagerly, before codegen ever reaches
256    /// this arm" shape [`Node::Wire`]'s own `session` requirement already
257    /// establishes. Also like `Node::Wire`, this needs `.await` - the
258    /// permission check is a real DB round trip, not a compile-time or
259    /// pure-runtime-data lookup - so the `view!` call site must be an
260    /// `async fn` returning a `Result`; a DB error while checking
261    /// propagates as a real error (`?`), never silently rendered as "no
262    /// permission" (matching this codebase's "never silently swallow
263    /// errors" convention - see `larust_permissions::has_permission_to`'s
264    /// own `Result`-returning signature, which this codegen doesn't
265    /// downgrade to a bool-with-swallowed-error).
266    Can {
267        permission: String,
268        then_branch: Vec<Node>,
269        else_branch: Vec<Node>,
270    },
271    /// `@role(expr) ... @endrole`, optionally with `@else ... @endrole` -
272    /// [`Node::Can`]'s exact shape and reasoning, checking
273    /// `larust_support::permission::has_role` instead of
274    /// `has_permission_to`. `expr` is a raw Rust expression yielding a
275    /// `RoleName` implementor (e.g. `Role::Moderator`), same "compile-check
276    /// the name, not the assignment" reasoning as `Node::Can`'s own `expr`.
277    Role {
278        role: String,
279        then_branch: Vec<Node>,
280        else_branch: Vec<Node>,
281    },
282    /// `@vitex(['resources/css/app.css', 'resources/js/app.js'])` -
283    /// Larust's own answer to Laravel's `@vite(...)` directive: real
284    /// integration with the app's existing Vite/Tailwind toolchain (see
285    /// `larust_support::vitex`'s own module doc for the full dev/
286    /// production dual-mode design), not a from-scratch bundler. Each
287    /// entry is a plain string - the exact key Vite's own manifest/dev
288    /// server use - never a general expression, matching `@vite`'s own
289    /// real syntax so a converted template reads the same way the
290    /// original Laravel source did. No scope dependency at all (no
291    /// `session`/`csrf_token`, unlike `@wire`/`@csrf`) - safe to use
292    /// anywhere, including directly in `WireComponent::render(&self)`.
293    Vitex(Vec<String>),
294    /// `@spa ... @endspa` - marks a layout's single swappable SPA-navigation
295    /// region. Renders `body` wrapped in a fixed sentinel
296    /// `<div id="__larust_spa_root">...</div>`; the client runtime
297    /// (`larust-spa`'s `spa-runtime.js`) replaces this div's `innerHTML`
298    /// wholesale on every intercepted same-origin link click or form
299    /// submit, updates `<title>`, and pushes the new URL via the History
300    /// API - see `docs/MACROS.md`'s `@spa` section for the full design.
301    ///
302    /// Deliberately **not** `@csrf`'s bare, position-only shape, despite
303    /// the directive's own simple "just enable this" spirit: unlike
304    /// `@csrf`, this needs to *wrap* a region (typically
305    /// `@yield('content')`), and `resolve.rs`'s `substitute_yields` only
306    /// knows how to replace a bare `Node::Yield` inline - there's no "wrap
307    /// this yield's output" concept, and inventing one would be a bigger,
308    /// more magic change than the feature warrants (it would also have to
309    /// guess which yield is "the" content region, when the yield name is
310    /// just a per-app convention, not hardcoded). So this follows the same
311    /// open-body-close shape `@loadonce`/`@resource`/`@live`/`@can`/`@role`
312    /// already use instead, letting the layout author place `@spa`/
313    /// `@endspa` explicitly around whatever they want swapped:
314    ///
315    /// ```blade
316    /// @spa
317    /// @yield('content')
318    /// @endspa
319    /// ```
320    ///
321    /// Because it has a body, `resolve.rs`'s `substitute_yields` (once
322    /// `Node::Spa` is threaded through it, same as every other body-bearing
323    /// node) reaches into that body and replaces the nested
324    /// `Node::Yield("content")` exactly as it already does for
325    /// `Node::If`/`Node::Section` bodies - this is what actually makes the
326    /// design work, and is the concrete reason a bare tag doesn't.
327    ///
328    /// No scope dependency at all (no `session`/`csrf_token`/`user`,
329    /// unlike `@wire`/`@csrf`/`@can`/`@role`) - it emits only static markup
330    /// and (via `@larustscripts`) a static `<script>` tag, matching
331    /// `Node::Vitex`'s own "safe to use anywhere" precedent. A resolved
332    /// tree with more than one `@spa` block is a compile-time error (the
333    /// sentinel id can't be duplicated) - checked eagerly by
334    /// `larust-macros::view::expand`'s `count_spa` scan.
335    Spa(Vec<Node>),
336}