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(
136 &mut self,
137 name: &str,
138 _raw: bool,
139 writer: &mut dyn ResponseWriter,
140 ) -> Result<()> {
141 if !self.is_active() {
142 return Ok(());
143 }
144 let index = self.next_index();
145 self.binding_stack.push(index);
146 self.build_binding_comment(V2_BINDING_START_PREFIX, index, name);
147 writer.write(&self.buffer)
148 }
149
150 fn on_binding_end(
151 &mut self,
152 name: &str,
153 _raw: bool,
154 writer: &mut dyn ResponseWriter,
155 ) -> Result<()> {
156 if !self.is_active() {
157 return Ok(());
158 }
159 let index = self.binding_stack.pop().unwrap_or(0);
160 self.build_binding_comment(V2_BINDING_END_PREFIX, index, name);
161 writer.write(&self.buffer)
162 }
163
164 fn on_repeat_item_start(
165 &mut self,
166 index: usize,
167 writer: &mut dyn ResponseWriter,
168 ) -> Result<()> {
169 if !self.is_active() {
170 return Ok(());
171 }
172 self.build_repeat_comment(V2_REPEAT_START_PREFIX, index);
173 writer.write(&self.buffer)
174 }
175
176 fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()> {
177 if !self.is_active() {
178 return Ok(());
179 }
180 self.build_repeat_comment(V2_REPEAT_END_PREFIX, index);
181 writer.write(&self.buffer)
182 }
183
184 fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()> {
185 if !self.is_active() {
186 return Ok(());
187 }
188 let (decoded, reset_child_index) = FastElementData::decode_v2(data).map_err(|error| {
189 HandlerError::PluginData(format!(
190 "FAST v2 hydration plugin received invalid element data: {error}"
191 ))
192 })?;
193 if decoded.binding_count > 0 {
194 let binding_index = self.next_index_n(decoded.binding_count);
195 self.build_attribute_marker(binding_index, decoded.binding_count);
196 writer.write(&self.buffer)?;
197 }
198 if reset_child_index {
199 if let Some(counter) = self.scopes.last_mut() {
200 *counter = 0;
201 }
202 }
203 Ok(())
204 }
205
206 fn write_route_component_state(
209 &self,
210 state: &Value,
211 writer: &mut dyn ResponseWriter,
212 ) -> Result<()> {
213 super::fast::write_route_component_state(state, writer)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 #![allow(clippy::disallowed_methods)]
220
221 use super::*;
222
223 struct TestWriter {
224 output: String,
225 }
226
227 impl TestWriter {
228 fn new() -> Self {
229 Self {
230 output: String::new(),
231 }
232 }
233 }
234
235 impl ResponseWriter for TestWriter {
236 fn write(&mut self, content: &str) -> Result<()> {
237 self.output.push_str(content);
238 Ok(())
239 }
240 fn end(&mut self) -> Result<()> {
241 Ok(())
242 }
243 }
244
245 #[test]
246 fn test_fast_v2_binding_marker_format() {
247 let mut plugin = FastV2HydrationPlugin::new();
248 plugin.push_scope();
249 let mut writer = TestWriter::new();
250 plugin
251 .on_binding_start("userName", false, &mut writer)
252 .unwrap();
253 plugin
254 .on_binding_end("userName", false, &mut writer)
255 .unwrap();
256 assert_eq!(
257 writer.output,
258 "<!--fe-b$$start$$0$$userName$$fe-b--><!--fe-b$$end$$0$$userName$$fe-b-->"
259 );
260 }
261
262 #[test]
263 fn test_fast_v2_binding_sequence_uses_indexes() {
264 let mut plugin = FastV2HydrationPlugin::new();
265 plugin.push_scope();
266 let mut writer = TestWriter::new();
267 plugin.on_binding_start("a", false, &mut writer).unwrap();
268 plugin.on_binding_end("a", false, &mut writer).unwrap();
269 writer.output.clear();
270 plugin.on_binding_start("b", false, &mut writer).unwrap();
271 assert_eq!(writer.output, "<!--fe-b$$start$$1$$b$$fe-b-->");
272 }
273
274 #[test]
275 fn test_fast_v2_repeat_marker_format() {
276 let mut plugin = FastV2HydrationPlugin::new();
277 plugin.push_scope();
278 let mut writer = TestWriter::new();
279 plugin.on_repeat_item_start(2, &mut writer).unwrap();
280 plugin.on_repeat_item_end(2, &mut writer).unwrap();
281 assert_eq!(
282 writer.output,
283 "<!--fe-repeat$$start$$2$$fe-repeat--><!--fe-repeat$$end$$2$$fe-repeat-->"
284 );
285 }
286
287 #[test]
288 fn test_fast_v2_attribute_marker_formats() {
289 let mut single = FastV2HydrationPlugin::new();
290 single.push_scope();
291 let mut writer = TestWriter::new();
292 let one = FastElementData { binding_count: 1 }.encode_v2(false);
293 single.on_element_data(&one, &mut writer).unwrap();
294 assert_eq!(writer.output, " data-fe-b-0");
295
296 let mut multi = FastV2HydrationPlugin::new();
297 multi.push_scope();
298 writer.output.clear();
299 let three = FastElementData { binding_count: 3 }.encode_v2(false);
300 multi.on_element_data(&three, &mut writer).unwrap();
301 assert_eq!(writer.output, " data-fe-c-0-3");
302 }
303
304 #[test]
305 fn test_fast_v2_attribute_count_advances_binding_index() {
306 let mut plugin = FastV2HydrationPlugin::new();
307 plugin.push_scope();
308 let mut writer = TestWriter::new();
309 let three = FastElementData { binding_count: 3 }.encode_v2(false);
310 plugin.on_element_data(&three, &mut writer).unwrap();
311
312 writer.output.clear();
313 plugin.on_binding_start("next", false, &mut writer).unwrap();
314 assert_eq!(writer.output, "<!--fe-b$$start$$3$$next$$fe-b-->");
315 }
316
317 #[test]
318 fn test_fast_v2_root_host_bindings_reset_child_index() {
319 let mut plugin = FastV2HydrationPlugin::new();
320 plugin.push_scope();
321 let mut writer = TestWriter::new();
322 let root = FastElementData { binding_count: 2 }.encode_v2(true);
323 plugin.on_element_data(&root, &mut writer).unwrap();
324 let child = FastElementData { binding_count: 1 }.encode_v2(false);
325 plugin.on_element_data(&child, &mut writer).unwrap();
326 assert_eq!(writer.output, " data-fe-c-0-2 data-fe-b-0");
327 }
328
329 #[test]
330 fn test_fast_v2_rejects_four_byte_element_data() {
331 let mut plugin = FastV2HydrationPlugin::new();
332 plugin.push_scope();
333 let mut writer = TestWriter::new();
334 let result = plugin.on_element_data(&1u32.to_le_bytes(), &mut writer);
335 assert!(
336 matches!(result, Err(crate::HandlerError::PluginData(ref msg)) if msg.contains("5 bytes")),
337 "four-byte FAST 2 data should be rejected: {result:?}"
338 );
339 assert_eq!(writer.output, "");
340 }
341
342 #[test]
343 fn test_fast_v2_parser_and_handler_keep_child_index_local() {
344 use crate::{RenderOptions, WebUIHandler};
345 use webui_parser::{
346 plugin::fast_v2::FastV2ParserPlugin, ComponentRegistration, HtmlParser,
347 };
348 use webui_protocol::WebUIProtocol;
349
350 let mut parser = HtmlParser::with_plugin(Box::new(FastV2ParserPlugin::new()));
351 parser
352 .component_registry_mut()
353 .register_component(ComponentRegistration::new(
354 "binding-card",
355 r#"<f-template name="binding-card" shadowrootmode="open"><template @click="{click($e)}" @keydown="{keydown($e)}"><slot f-ref="{slot}"></slot></template></f-template>"#,
356 None,
357 true,
358 ))
359 .unwrap();
360 parser
361 .parse("index.html", "<binding-card></binding-card>")
362 .unwrap();
363 let mut protocol = WebUIProtocol::new(parser.into_fragment_records());
364 protocol
365 .components
366 .entry("binding-card".to_string())
367 .or_default()
368 .uses_shadow_dom = true;
369 protocol.populate_style_closures(&["index.html"]);
370 let handler = WebUIHandler::with_plugin(|| Box::new(FastV2HydrationPlugin::new()));
371 let mut writer = TestWriter::new();
372 handler
373 .handle(
374 &protocol,
375 &serde_json::json!({}),
376 &RenderOptions::new("index.html", "/"),
377 &mut writer,
378 )
379 .unwrap();
380
381 assert!(
382 writer.output.contains(concat!(
383 r#"<template shadowrootmode="open" data-fe-c-0-2>"#,
384 r#"<slot data-fe-b-0></slot></template>"#
385 )),
386 "FAST 2 child markers should exclude consumed host bindings: {}",
387 writer.output
388 );
389 }
390
391 #[test]
392 fn test_fast_v2_root_scope_disabled() {
393 let mut plugin = FastV2HydrationPlugin::new();
394 let mut writer = TestWriter::new();
395 plugin.on_binding_start("x", false, &mut writer).unwrap();
396 plugin.on_binding_end("x", false, &mut writer).unwrap();
397 plugin.on_repeat_item_start(0, &mut writer).unwrap();
398 plugin.on_repeat_item_end(0, &mut writer).unwrap();
399 let data = FastElementData { binding_count: 3 }.encode_v2(false);
400 plugin.on_element_data(&data, &mut writer).unwrap();
401 assert_eq!(writer.output, "");
402 }
403}