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