1use super::HandlerPlugin;
18use crate::{HandlerError, ResponseWriter, Result};
19use serde_json::Value;
20use std::fmt::Write;
21use webui_protocol::FastElementData;
22
23const V2_BINDING_START_PREFIX: &str = "<!--fe-b$$start$$";
25const V2_BINDING_END_PREFIX: &str = "<!--fe-b$$end$$";
26const V2_BINDING_SUFFIX: &str = "$$fe-b-->";
27const V2_SEPARATOR: &str = "$$";
28const V2_REPEAT_START_PREFIX: &str = "<!--fe-repeat$$start$$";
29const V2_REPEAT_END_PREFIX: &str = "<!--fe-repeat$$end$$";
30const V2_REPEAT_SUFFIX: &str = "$$fe-repeat-->";
31const V2_ATTR_SINGLE_PREFIX: &str = " data-fe-b-";
32const V2_ATTR_MULTI_PREFIX: &str = " data-fe-c-";
33
34pub struct FastV2HydrationPlugin {
40 scopes: Vec<usize>,
43 binding_stack: Vec<usize>,
45 buffer: String,
47}
48
49impl FastV2HydrationPlugin {
50 #[must_use]
53 pub fn new() -> Self {
54 Self {
55 scopes: vec![0],
57 binding_stack: Vec::with_capacity(8),
58 buffer: String::with_capacity(64),
59 }
60 }
61
62 fn is_active(&self) -> bool {
64 self.scopes.len() > 1
65 }
66
67 fn next_index(&mut self) -> usize {
69 if let Some(counter) = self.scopes.last_mut() {
70 let index = *counter;
71 *counter += 1;
72 index
73 } else {
74 0
75 }
76 }
77
78 fn next_index_n(&mut self, count: u32) -> usize {
80 if let Some(counter) = self.scopes.last_mut() {
81 let index = *counter;
82 *counter += count as usize;
83 index
84 } else {
85 0
86 }
87 }
88
89 fn build_binding_comment(&mut self, prefix: &str, index: usize, name: &str) {
91 self.buffer.clear();
92 self.buffer.push_str(prefix);
93 let _ = write!(self.buffer, "{}", index);
94 self.buffer.push_str(V2_SEPARATOR);
95 self.buffer.push_str(name);
96 self.buffer.push_str(V2_BINDING_SUFFIX);
97 }
98
99 fn build_repeat_comment(&mut self, prefix: &str, index: usize) {
101 self.buffer.clear();
102 self.buffer.push_str(prefix);
103 let _ = write!(self.buffer, "{}", index);
104 self.buffer.push_str(V2_REPEAT_SUFFIX);
105 }
106
107 fn build_attribute_marker(&mut self, binding_index: usize, count: u32) {
109 self.buffer.clear();
110 if count == 1 {
111 self.buffer.push_str(V2_ATTR_SINGLE_PREFIX);
112 let _ = write!(self.buffer, "{}", binding_index);
113 } else {
114 self.buffer.push_str(V2_ATTR_MULTI_PREFIX);
115 let _ = write!(self.buffer, "{}-{}", binding_index, count);
116 }
117 }
118}
119
120impl Default for FastV2HydrationPlugin {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl HandlerPlugin for FastV2HydrationPlugin {
127 fn push_scope(&mut self) {
128 self.scopes.push(0);
129 }
130
131 fn pop_scope(&mut self) {
132 self.scopes.pop();
133 }
134
135 fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
136 if !self.is_active() {
137 return Ok(());
138 }
139 let index = self.next_index();
140 self.binding_stack.push(index);
141 self.build_binding_comment(V2_BINDING_START_PREFIX, index, name);
142 writer.write(&self.buffer)
143 }
144
145 fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
146 if !self.is_active() {
147 return Ok(());
148 }
149 let index = self.binding_stack.pop().unwrap_or(0);
150 self.build_binding_comment(V2_BINDING_END_PREFIX, index, name);
151 writer.write(&self.buffer)
152 }
153
154 fn on_repeat_item_start(
155 &mut self,
156 index: usize,
157 writer: &mut dyn ResponseWriter,
158 ) -> Result<()> {
159 if !self.is_active() {
160 return Ok(());
161 }
162 self.build_repeat_comment(V2_REPEAT_START_PREFIX, index);
163 writer.write(&self.buffer)
164 }
165
166 fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()> {
167 if !self.is_active() {
168 return Ok(());
169 }
170 self.build_repeat_comment(V2_REPEAT_END_PREFIX, index);
171 writer.write(&self.buffer)
172 }
173
174 fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()> {
175 if !self.is_active() {
176 return Ok(());
177 }
178 let decoded = FastElementData::decode(data).map_err(|error| {
179 HandlerError::PluginData(format!(
180 "FAST v2 hydration plugin expected 4 bytes of element data: {error}"
181 ))
182 })?;
183 if decoded.binding_count > 0 {
184 let binding_index = self.next_index_n(decoded.binding_count);
185 self.build_attribute_marker(binding_index, decoded.binding_count);
186 writer.write(&self.buffer)?;
187 }
188 Ok(())
189 }
190
191 fn write_route_component_state(
194 &self,
195 state: &Value,
196 writer: &mut dyn ResponseWriter,
197 ) -> Result<()> {
198 write_fast_route_component_state(state, writer)
199 }
200}
201
202fn write_fast_route_component_state(state: &Value, writer: &mut dyn ResponseWriter) -> Result<()> {
203 let map = match state.as_object() {
204 Some(m) => m,
205 None => return Ok(()),
206 };
207
208 for (key, value) in map {
210 let val_str = match value {
211 Value::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
212 Value::Number(n) => std::borrow::Cow::Owned(n.to_string()),
213 Value::Bool(true) => std::borrow::Cow::Borrowed("true"),
214 Value::Bool(false) => std::borrow::Cow::Borrowed("false"),
215 _ => continue,
216 };
217 let attr_name = webui_protocol::attrs::camel_to_kebab(key);
218 writer.write(" ")?;
219 writer.write(&attr_name)?;
220 writer.write("=\"")?;
221 crate::route_renderer::write_escaped_state_attr(writer, val_str.as_ref())?;
222 writer.write("\"")?;
223 }
224
225 let has_complex = map.values().any(|v| v.is_array() || v.is_object());
227 if has_complex {
228 let json_str = state.to_string();
229 writer.write(" data-state=\"")?;
230 crate::route_renderer::write_escaped_state_attr(writer, &json_str)?;
231 writer.write("\"")?;
232 }
233
234 Ok(())
235}
236
237#[cfg(test)]
238mod tests {
239 #![allow(clippy::disallowed_methods)]
240
241 use super::*;
242
243 struct TestWriter {
244 output: String,
245 }
246
247 impl TestWriter {
248 fn new() -> Self {
249 Self {
250 output: String::new(),
251 }
252 }
253 }
254
255 impl ResponseWriter for TestWriter {
256 fn write(&mut self, content: &str) -> Result<()> {
257 self.output.push_str(content);
258 Ok(())
259 }
260 fn end(&mut self) -> Result<()> {
261 Ok(())
262 }
263 }
264
265 #[test]
266 fn test_fast_v2_binding_marker_format() {
267 let mut plugin = FastV2HydrationPlugin::new();
268 plugin.push_scope();
269 let mut writer = TestWriter::new();
270 plugin.on_binding_start("userName", &mut writer).unwrap();
271 plugin.on_binding_end("userName", &mut writer).unwrap();
272 assert_eq!(
273 writer.output,
274 "<!--fe-b$$start$$0$$userName$$fe-b--><!--fe-b$$end$$0$$userName$$fe-b-->"
275 );
276 }
277
278 #[test]
279 fn test_fast_v2_binding_sequence_uses_indexes() {
280 let mut plugin = FastV2HydrationPlugin::new();
281 plugin.push_scope();
282 let mut writer = TestWriter::new();
283 plugin.on_binding_start("a", &mut writer).unwrap();
284 plugin.on_binding_end("a", &mut writer).unwrap();
285 writer.output.clear();
286 plugin.on_binding_start("b", &mut writer).unwrap();
287 assert_eq!(writer.output, "<!--fe-b$$start$$1$$b$$fe-b-->");
288 }
289
290 #[test]
291 fn test_fast_v2_repeat_marker_format() {
292 let mut plugin = FastV2HydrationPlugin::new();
293 plugin.push_scope();
294 let mut writer = TestWriter::new();
295 plugin.on_repeat_item_start(2, &mut writer).unwrap();
296 plugin.on_repeat_item_end(2, &mut writer).unwrap();
297 assert_eq!(
298 writer.output,
299 "<!--fe-repeat$$start$$2$$fe-repeat--><!--fe-repeat$$end$$2$$fe-repeat-->"
300 );
301 }
302
303 #[test]
304 fn test_fast_v2_attribute_marker_formats() {
305 let mut single = FastV2HydrationPlugin::new();
306 single.push_scope();
307 let mut writer = TestWriter::new();
308 let one = 1u32.to_le_bytes();
309 single.on_element_data(&one, &mut writer).unwrap();
310 assert_eq!(writer.output, " data-fe-b-0");
311
312 let mut multi = FastV2HydrationPlugin::new();
313 multi.push_scope();
314 writer.output.clear();
315 let three = 3u32.to_le_bytes();
316 multi.on_element_data(&three, &mut writer).unwrap();
317 assert_eq!(writer.output, " data-fe-c-0-3");
318 }
319
320 #[test]
321 fn test_fast_v2_attribute_count_advances_binding_index() {
322 let mut plugin = FastV2HydrationPlugin::new();
323 plugin.push_scope();
324 let mut writer = TestWriter::new();
325 let three = 3u32.to_le_bytes();
326 plugin.on_element_data(&three, &mut writer).unwrap();
327
328 writer.output.clear();
329 plugin.on_binding_start("next", &mut writer).unwrap();
330 assert_eq!(writer.output, "<!--fe-b$$start$$3$$next$$fe-b-->");
331 }
332
333 #[test]
334 fn test_fast_v2_root_scope_disabled() {
335 let mut plugin = FastV2HydrationPlugin::new();
336 let mut writer = TestWriter::new();
337 plugin.on_binding_start("x", &mut writer).unwrap();
338 plugin.on_binding_end("x", &mut writer).unwrap();
339 plugin.on_repeat_item_start(0, &mut writer).unwrap();
340 plugin.on_repeat_item_end(0, &mut writer).unwrap();
341 let data = 3u32.to_le_bytes();
342 plugin.on_element_data(&data, &mut writer).unwrap();
343 assert_eq!(writer.output, "");
344 }
345
346 #[test]
347 fn test_fast_v2_write_route_component_state_emits_data_state() {
348 let plugin = FastV2HydrationPlugin::new();
349 let mut writer = TestWriter::new();
350 let state = serde_json::json!({
351 "title": "Hello",
352 "items": [{"name": "A&B"}]
353 });
354
355 plugin
356 .write_route_component_state(&state, &mut writer)
357 .unwrap();
358
359 assert!(
360 writer.output.contains("data-state="),
361 "FAST v2 handler plugin should emit data-state: {}",
362 writer.output
363 );
364 assert!(
365 writer.output.contains(r#"title="Hello""#),
366 "FAST v2 handler plugin should still emit scalar attrs: {}",
367 writer.output
368 );
369 }
370}