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}