use super::HandlerPlugin;
use crate::{HandlerError, ResponseWriter, Result};
use serde_json::Value;
use std::fmt::Write;
use webui_protocol::FastElementData;
const V2_BINDING_START_PREFIX: &str = "<!--fe-b$$start$$";
const V2_BINDING_END_PREFIX: &str = "<!--fe-b$$end$$";
const V2_BINDING_SUFFIX: &str = "$$fe-b-->";
const V2_SEPARATOR: &str = "$$";
const V2_REPEAT_START_PREFIX: &str = "<!--fe-repeat$$start$$";
const V2_REPEAT_END_PREFIX: &str = "<!--fe-repeat$$end$$";
const V2_REPEAT_SUFFIX: &str = "$$fe-repeat-->";
const V2_ATTR_SINGLE_PREFIX: &str = " data-fe-b-";
const V2_ATTR_MULTI_PREFIX: &str = " data-fe-c-";
pub struct FastV2HydrationPlugin {
scopes: Vec<usize>,
binding_stack: Vec<usize>,
buffer: String,
}
impl FastV2HydrationPlugin {
#[must_use]
pub fn new() -> Self {
Self {
scopes: vec![0],
binding_stack: Vec::with_capacity(8),
buffer: String::with_capacity(64),
}
}
fn is_active(&self) -> bool {
self.scopes.len() > 1
}
fn next_index(&mut self) -> usize {
if let Some(counter) = self.scopes.last_mut() {
let index = *counter;
*counter += 1;
index
} else {
0
}
}
fn next_index_n(&mut self, count: u32) -> usize {
if let Some(counter) = self.scopes.last_mut() {
let index = *counter;
*counter += count as usize;
index
} else {
0
}
}
fn build_binding_comment(&mut self, prefix: &str, index: usize, name: &str) {
self.buffer.clear();
self.buffer.push_str(prefix);
let _ = write!(self.buffer, "{}", index);
self.buffer.push_str(V2_SEPARATOR);
self.buffer.push_str(name);
self.buffer.push_str(V2_BINDING_SUFFIX);
}
fn build_repeat_comment(&mut self, prefix: &str, index: usize) {
self.buffer.clear();
self.buffer.push_str(prefix);
let _ = write!(self.buffer, "{}", index);
self.buffer.push_str(V2_REPEAT_SUFFIX);
}
fn build_attribute_marker(&mut self, binding_index: usize, count: u32) {
self.buffer.clear();
if count == 1 {
self.buffer.push_str(V2_ATTR_SINGLE_PREFIX);
let _ = write!(self.buffer, "{}", binding_index);
} else {
self.buffer.push_str(V2_ATTR_MULTI_PREFIX);
let _ = write!(self.buffer, "{}-{}", binding_index, count);
}
}
}
impl Default for FastV2HydrationPlugin {
fn default() -> Self {
Self::new()
}
}
impl HandlerPlugin for FastV2HydrationPlugin {
fn push_scope(&mut self) {
self.scopes.push(0);
}
fn pop_scope(&mut self) {
self.scopes.pop();
}
fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
if !self.is_active() {
return Ok(());
}
let index = self.next_index();
self.binding_stack.push(index);
self.build_binding_comment(V2_BINDING_START_PREFIX, index, name);
writer.write(&self.buffer)
}
fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()> {
if !self.is_active() {
return Ok(());
}
let index = self.binding_stack.pop().unwrap_or(0);
self.build_binding_comment(V2_BINDING_END_PREFIX, index, name);
writer.write(&self.buffer)
}
fn on_repeat_item_start(
&mut self,
index: usize,
writer: &mut dyn ResponseWriter,
) -> Result<()> {
if !self.is_active() {
return Ok(());
}
self.build_repeat_comment(V2_REPEAT_START_PREFIX, index);
writer.write(&self.buffer)
}
fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()> {
if !self.is_active() {
return Ok(());
}
self.build_repeat_comment(V2_REPEAT_END_PREFIX, index);
writer.write(&self.buffer)
}
fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()> {
if !self.is_active() {
return Ok(());
}
let decoded = FastElementData::decode(data).map_err(|error| {
HandlerError::PluginData(format!(
"FAST v2 hydration plugin expected 4 bytes of element data: {error}"
))
})?;
if decoded.binding_count > 0 {
let binding_index = self.next_index_n(decoded.binding_count);
self.build_attribute_marker(binding_index, decoded.binding_count);
writer.write(&self.buffer)?;
}
Ok(())
}
fn write_route_component_state(
&self,
state: &Value,
writer: &mut dyn ResponseWriter,
) -> Result<()> {
write_fast_route_component_state(state, writer)
}
}
fn write_fast_route_component_state(state: &Value, writer: &mut dyn ResponseWriter) -> Result<()> {
let map = match state.as_object() {
Some(m) => m,
None => return Ok(()),
};
for (key, value) in map {
let val_str = match value {
Value::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
Value::Number(n) => std::borrow::Cow::Owned(n.to_string()),
Value::Bool(true) => std::borrow::Cow::Borrowed("true"),
Value::Bool(false) => std::borrow::Cow::Borrowed("false"),
_ => continue,
};
let attr_name = webui_protocol::attrs::camel_to_kebab(key);
writer.write(" ")?;
writer.write(&attr_name)?;
writer.write("=\"")?;
crate::route_renderer::write_escaped_state_attr(writer, val_str.as_ref())?;
writer.write("\"")?;
}
let has_complex = map.values().any(|v| v.is_array() || v.is_object());
if has_complex {
let json_str = state.to_string();
writer.write(" data-state=\"")?;
crate::route_renderer::write_escaped_state_attr(writer, &json_str)?;
writer.write("\"")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::disallowed_methods)]
use super::*;
struct TestWriter {
output: String,
}
impl TestWriter {
fn new() -> Self {
Self {
output: String::new(),
}
}
}
impl ResponseWriter for TestWriter {
fn write(&mut self, content: &str) -> Result<()> {
self.output.push_str(content);
Ok(())
}
fn end(&mut self) -> Result<()> {
Ok(())
}
}
#[test]
fn test_fast_v2_binding_marker_format() {
let mut plugin = FastV2HydrationPlugin::new();
plugin.push_scope();
let mut writer = TestWriter::new();
plugin.on_binding_start("userName", &mut writer).unwrap();
plugin.on_binding_end("userName", &mut writer).unwrap();
assert_eq!(
writer.output,
"<!--fe-b$$start$$0$$userName$$fe-b--><!--fe-b$$end$$0$$userName$$fe-b-->"
);
}
#[test]
fn test_fast_v2_binding_sequence_uses_indexes() {
let mut plugin = FastV2HydrationPlugin::new();
plugin.push_scope();
let mut writer = TestWriter::new();
plugin.on_binding_start("a", &mut writer).unwrap();
plugin.on_binding_end("a", &mut writer).unwrap();
writer.output.clear();
plugin.on_binding_start("b", &mut writer).unwrap();
assert_eq!(writer.output, "<!--fe-b$$start$$1$$b$$fe-b-->");
}
#[test]
fn test_fast_v2_repeat_marker_format() {
let mut plugin = FastV2HydrationPlugin::new();
plugin.push_scope();
let mut writer = TestWriter::new();
plugin.on_repeat_item_start(2, &mut writer).unwrap();
plugin.on_repeat_item_end(2, &mut writer).unwrap();
assert_eq!(
writer.output,
"<!--fe-repeat$$start$$2$$fe-repeat--><!--fe-repeat$$end$$2$$fe-repeat-->"
);
}
#[test]
fn test_fast_v2_attribute_marker_formats() {
let mut single = FastV2HydrationPlugin::new();
single.push_scope();
let mut writer = TestWriter::new();
let one = 1u32.to_le_bytes();
single.on_element_data(&one, &mut writer).unwrap();
assert_eq!(writer.output, " data-fe-b-0");
let mut multi = FastV2HydrationPlugin::new();
multi.push_scope();
writer.output.clear();
let three = 3u32.to_le_bytes();
multi.on_element_data(&three, &mut writer).unwrap();
assert_eq!(writer.output, " data-fe-c-0-3");
}
#[test]
fn test_fast_v2_attribute_count_advances_binding_index() {
let mut plugin = FastV2HydrationPlugin::new();
plugin.push_scope();
let mut writer = TestWriter::new();
let three = 3u32.to_le_bytes();
plugin.on_element_data(&three, &mut writer).unwrap();
writer.output.clear();
plugin.on_binding_start("next", &mut writer).unwrap();
assert_eq!(writer.output, "<!--fe-b$$start$$3$$next$$fe-b-->");
}
#[test]
fn test_fast_v2_root_scope_disabled() {
let mut plugin = FastV2HydrationPlugin::new();
let mut writer = TestWriter::new();
plugin.on_binding_start("x", &mut writer).unwrap();
plugin.on_binding_end("x", &mut writer).unwrap();
plugin.on_repeat_item_start(0, &mut writer).unwrap();
plugin.on_repeat_item_end(0, &mut writer).unwrap();
let data = 3u32.to_le_bytes();
plugin.on_element_data(&data, &mut writer).unwrap();
assert_eq!(writer.output, "");
}
#[test]
fn test_fast_v2_write_route_component_state_emits_data_state() {
let plugin = FastV2HydrationPlugin::new();
let mut writer = TestWriter::new();
let state = serde_json::json!({
"title": "Hello",
"items": [{"name": "A&B"}]
});
plugin
.write_route_component_state(&state, &mut writer)
.unwrap();
assert!(
writer.output.contains("data-state="),
"FAST v2 handler plugin should emit data-state: {}",
writer.output
);
assert!(
writer.output.contains(r#"title="Hello""#),
"FAST v2 handler plugin should still emit scalar attrs: {}",
writer.output
);
}
}