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}