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}