larust-view 0.10.0

A Laravel-shaped web framework for Rust.
Documentation
/// 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>),
    /// `<Image src="..." width="..." height="..." alt="..." quality="..." />` -
    /// Larust's own answer to Next.js's `<Image />` component; see
    /// `larust-image`'s own doc comment for the full design. Core
    /// framework surface, not a stand-in for an optional third-party
    /// Laravel package - the same tier as [`Node::Wire`]/[`Node::Spa`]/
    /// [`Node::Vitex`], never behind a Cargo feature flag.
    ///
    /// `props` is the exact same `(name, codegen-ready Rust expression)`
    /// shape [`Node::Wire`]'s own props already use, straight from the
    /// same shared attribute parser (`larust-view::parser::parse_tag_attrs`).
    /// This node carries no special-cased fields of its own (no `src`/
    /// `width`/`height` struct members), specifically so the parser needs
    /// no `<Image>`-specific attribute grammar at all. Validating which
    /// names are required/recognized, and coercing a plain numeric literal
    /// attribute (`width="400"`, which the shared parser represents as the
    /// Rust string `"400"`) back into a real integer expression, both
    /// happen in `larust-macros`' own codegen for this node: a type-
    /// coercion concern, not a lexical-grammar one.
    Image {
        props: Vec<(String, String)>,
    },
}