Skip to main content

webui_handler/plugin/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4//! Handler plugin trait and built-in plugin implementations.
5//!
6//! Handler plugins write framework-specific hydration markers while shared
7//! completion work, such as component template emission, stays in handler core.
8
9pub mod fast;
10pub mod fast_v2;
11pub mod fast_v3;
12pub mod webui;
13
14use crate::{ResponseWriter, Result};
15use std::collections::HashSet;
16use webui_protocol::WebUIProtocol;
17
18/// A handler plugin that can inject additional content during rendering.
19///
20/// Plugins receive callbacks at key points in the rendering lifecycle:
21/// - **Scope management**: `push_scope` / `pop_scope` for component and loop boundaries
22/// - **Binding lifecycle**: `on_binding_start` / `on_binding_end` around signals
23/// - **For-loop lifecycle**: `on_for_start` / `on_for_end` around for-loop blocks
24/// - **If-condition lifecycle**: `on_if_start` / `on_if_end` around if-condition blocks
25/// - **Repeat items**: `on_repeat_item_start` / `on_repeat_item_end` per for-loop item
26/// - **Element data**: `on_element_data` for parser-produced hydration metadata
27/// - **Route state**: `write_route_component_state` for framework-specific opening-tag attributes
28///
29/// WebUI does not interpret what plugins write — it just calls the hooks.
30/// Each framework defines its own marker format.
31pub trait HandlerPlugin {
32    /// Enter a new scope (component or for-loop item boundary).
33    /// Typically resets per-scope counters.
34    fn push_scope(&mut self);
35
36    /// Exit the current scope, restoring the parent scope state.
37    fn pop_scope(&mut self);
38
39    /// Called before rendering a signal binding.
40    fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
41
42    /// Called after rendering a signal binding.
43    fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
44
45    /// Called before rendering a for-loop block.
46    /// Defaults to [`on_binding_start`](HandlerPlugin::on_binding_start).
47    fn on_for_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
48        self.on_binding_start(name, writer)
49    }
50
51    /// Called after rendering a for-loop block.
52    /// Defaults to [`on_binding_end`](HandlerPlugin::on_binding_end).
53    fn on_for_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
54        self.on_binding_end(name, writer)
55    }
56
57    /// Called before rendering an if-condition block.
58    /// Defaults to [`on_binding_start`](HandlerPlugin::on_binding_start).
59    fn on_if_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
60        self.on_binding_start(name, writer)
61    }
62
63    /// Called after rendering an if-condition block.
64    /// Defaults to [`on_binding_end`](HandlerPlugin::on_binding_end).
65    fn on_if_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
66        self.on_binding_end(name, writer)
67    }
68
69    /// Called before rendering a repeat item in a for loop.
70    fn on_repeat_item_start(&mut self, index: usize, writer: &mut dyn ResponseWriter)
71        -> Result<()>;
72
73    /// Called after rendering a repeat item.
74    fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;
75
76    /// Called when parser-produced element metadata is encountered.
77    fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()>;
78
79    /// Called when emitting a matched route component's opening tag.
80    /// Plugins can write framework-specific attributes before the closing `>`.
81    /// The default is a no-op.
82    fn write_route_component_state(
83        &self,
84        _state: &serde_json::Value,
85        _writer: &mut dyn ResponseWriter,
86    ) -> Result<()> {
87        Ok(())
88    }
89
90    /// Emit component templates collected during SSR.  The default emits
91    /// each template as-is (suitable for FAST `<f-template>` tags).  The
92    /// WebUI plugin overrides this to wrap all raw JS IIFE templates in a
93    /// single `<script>` tag.
94    fn emit_templates(
95        &self,
96        protocol: &WebUIProtocol,
97        components: &HashSet<String>,
98        _nonce: Option<&str>,
99        writer: &mut dyn ResponseWriter,
100    ) -> Result<()> {
101        emit_component_templates(protocol, components, writer)
102    }
103
104    /// Return raw JS template source strings for the given components.
105    ///
106    /// Plugins whose templates are executable JS (e.g. WebUI IIFEs) override
107    /// this so `lib.rs` can embed them in the consolidated `window.__webui`
108    /// script block — eliminating a separate `<script>` tag.  Returns `None`
109    /// when templates are non-JS (e.g. HTML `<f-template>` tags).
110    fn collect_template_js(
111        &self,
112        _protocol: &WebUIProtocol,
113        _components: &HashSet<String>,
114    ) -> Option<Vec<String>> {
115        None
116    }
117}
118
119/// Default template emission: write each non-empty template verbatim.
120/// Used by FAST parser plugins for `<f-template>` tags.
121pub(crate) fn emit_component_templates(
122    protocol: &WebUIProtocol,
123    components: &HashSet<String>,
124    writer: &mut dyn ResponseWriter,
125) -> Result<()> {
126    for name in components {
127        if let Some(template) = protocol
128            .components
129            .get(name)
130            .map(|component| component.template.as_str())
131            .filter(|template| !template.is_empty())
132        {
133            writer.write(template)?;
134        }
135    }
136    Ok(())
137}