1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/// A parsed `.blade.xr` template, before layout resolution.
///
/// Expression text (`expr`, `cond`, `iter`) is kept as raw strings here -
/// this crate is pure text parsing with no `syn`/`proc-macro2` dependency.
/// `larust-macros` parses those strings into real `syn::Expr`s at codegen
/// time, which is also what makes `{{ user.name }}` a genuine, type-checked
/// Rust expression rather than a custom mini-language.
/// One `name = expr` line inside a `@globals ... @endglobals` block -
/// see `Node::Globals`. `persist` marks a `persist name = expr` line
/// (`persist` is a line prefix, not a separate directive), meaning `expr`
/// is only ever the *fallback* used when the browser hasn't set the
/// corresponding preference cookie yet - see `Node::PersistGlobal`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobalEntry {
pub name: String,
pub expr: String,
pub persist: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Node {
Text(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.
Code(String),
Interpolate {
expr: String,
/// `{{ }}` (HTML-escaped) vs `{!! !!}` (raw).
escape: bool,
},
/// `@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()`).
Js(String),
If {
cond: String,
then_branch: Vec<Node>,
else_branch: Vec<Node>,
},
Foreach {
/// Any 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).
binding: String,
iter: String,
body: Vec<Node>,
},
Extends(String),
Section {
name: String,
body: Vec<Node>,
},
Yield(String),
/// `@push('name') ... @endpush` - unlike `@section` (single body,
/// last-write-wins), multiple `@push`es to the same name *accumulate*;
/// see `larust_view::resolve` for how they're collected across an
/// `@extends` chain and spliced into the matching `@stack`.
Push {
name: String,
body: Vec<Node>,
},
/// `@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.
Stack(String),
/// `@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).
Csrf,
/// `@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`.
Global {
name: String,
fallback: Option<String>,
},
/// 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.
PersistGlobal {
cookie_name: String,
fallback_expr: String,
},
/// `@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.
Globals(Vec<GlobalEntry>),
/// `@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).
Wire {
name: String,
props: Vec<(String, String)>,
},
/// `@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.
LarustScripts,
/// `@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.
LoadOnce(Vec<Node>),
/// `@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.
Resource {
name: String,
props: Vec<(String, String)>,
slot: Vec<Node>,
},
/// `@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.
Live {
channel: String,
body: Vec<Node>,
},
/// `@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).
Can {
permission: String,
then_branch: Vec<Node>,
else_branch: Vec<Node>,
},
/// `@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`.
Role {
role: String,
then_branch: Vec<Node>,
else_branch: Vec<Node>,
},
/// `@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)`.
Vitex(Vec<String>),
/// `@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:
///
/// ```blade
/// @spa
/// @yield('content')
/// @endspa
/// ```
///
/// Because 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.
Spa(Vec<Node>),
}