Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,15 @@ pub trait ResponseWriter {
}
```

The handler owns quoted and boolean attribute formatting in centralized string
and byte helpers. Normal buffered hosts use the hidden
`define_string_response_writer!` / `define_bytes_response_writer!` macros, or
the corresponding method macros for writers with additional fields. The macros
expand local, inlinable methods without duplicating formatter source in every
host. Generic writers retain byte-identical `write()`-based fallback behavior.
Only writers with distinct semantics - threshold flushing, stream forwarding,
or profiling counters - implement the hidden attribute methods directly.

### Streaming Response Writers (`webui::streaming`)

Hosts that support HTTP response streaming can render directly into a
Expand Down
31 changes: 5 additions & 26 deletions crates/webui-cli/src/commands/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ use crate::utils::output;
use metafile::temp_directory as metafile_temp_directory;
use metafile::write_atomic;

webui_handler::define_string_response_writer!(MemoryWriter, buf);

#[derive(Args)]
pub struct ServeArgs {
#[command(flatten)]
Expand Down Expand Up @@ -230,30 +232,6 @@ struct SharedState {
entry: String,
}

/// In-memory writer implementing `ResponseWriter` for the handler.
struct MemoryWriter {
buf: String,
}

impl MemoryWriter {
fn with_capacity(cap: usize) -> Self {
Self {
buf: String::with_capacity(cap),
}
}
}

impl ResponseWriter for MemoryWriter {
fn write(&mut self, content: &str) -> webui_handler::Result<()> {
self.buf.push_str(content);
Ok(())
}

fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
}

/// SSE endpoint path. Root-relative so the script works under any
/// `<base href>` and across sub-path deployments.
const HMR_ENDPOINT: &str = "/__webui/livereload";
Expand Down Expand Up @@ -564,9 +542,10 @@ fn build_and_render(
&mut writer,
)?;

let output = writer.buf;
let html = match livereload {
Some(lr) => lr.inject(&writer.buf),
None => writer.buf,
Some(lr) => lr.inject(&output),
None => output,
};

if let Some(path) = &config.metafile {
Expand Down
28 changes: 3 additions & 25 deletions crates/webui-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
use webui_handler::plugin::webui::WebUIHydrationPlugin;
use webui_handler::{
BoundaryDescriptor, BoundaryInstanceId, BoundaryKey, BoundaryMode, Protocol, RenderOptions,
ResponseWriter, SessionOptions, StreamStep, StreamingSession, WebUIHandler,
SessionOptions, StreamStep, StreamingSession, WebUIHandler,
};

/// Opaque C handle for a loaded WebUI protocol.
Expand Down Expand Up @@ -81,29 +81,7 @@ struct ProtocolContext {
protocol: Arc<Protocol>,
}

/// A simple string buffer for collecting rendered output.
struct StringResponseWriter {
content: String,
}

impl StringResponseWriter {
fn new() -> Self {
Self {
content: String::new(),
}
}
}

impl ResponseWriter for StringResponseWriter {
fn write(&mut self, content: &str) -> webui_handler::Result<()> {
self.content.push_str(content);
Ok(())
}

fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
}
webui_handler::define_string_response_writer!(StringResponseWriter, content);

// ---------------------------------------------------------------------------
// FFI: error reporting
Expand Down Expand Up @@ -414,7 +392,7 @@ unsafe fn render_decoded_protocol(
options = options.with_nonce(nonce);
}

let mut writer = StringResponseWriter::new();
let mut writer = StringResponseWriter::with_capacity(0);
match context
.handler
.render(protocol, &data, &options, &mut writer)
Expand Down
2 changes: 2 additions & 0 deletions crates/webui-handler/benches/bootstrap_state_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ impl ResponseWriter for BenchWriter {
Ok(())
}

webui_handler::string_response_writer_methods!(output);

fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
Expand Down
2 changes: 2 additions & 0 deletions crates/webui-handler/benches/handler_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ impl ResponseWriter for BenchWriter {
Ok(())
}

webui_handler::string_response_writer_methods!(output);

fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
Expand Down
2 changes: 2 additions & 0 deletions crates/webui-handler/benches/streaming_hydration_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ impl ResponseWriter for BenchWriter {
Ok(())
}

webui_handler::string_response_writer_methods!(output);

fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
Expand Down
59 changes: 54 additions & 5 deletions crates/webui-handler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
pub mod css_module;
pub(crate) mod html_encode;
pub mod plugin;
mod response_writer;
pub mod route_handler;
pub mod route_matcher;
pub(crate) mod route_renderer;
pub(crate) mod streaming;

#[doc(hidden)]
pub use response_writer::{
append_attribute_to_bytes, append_attribute_to_string, append_boolean_attribute_to_bytes,
append_boolean_attribute_to_string,
};
pub use route_handler::Protocol;

/// Minimal HTML escaper for the 6 XSS-critical characters
Expand Down Expand Up @@ -192,6 +198,23 @@ pub trait ResponseWriter {
/// Write content to the output
fn write(&mut self, content: &str) -> Result<()>;

/// Write one complete quoted HTML attribute.
#[doc(hidden)]
fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
self.write(" ")?;
self.write(name)?;
self.write("=\"")?;
self.write(value)?;
self.write("\"")
}

/// Write one complete boolean HTML attribute.
#[doc(hidden)]
fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
self.write(" ")?;
self.write(name)
}

/// Finalize the output
fn end(&mut self) -> Result<()>;

Expand Down Expand Up @@ -2939,11 +2962,7 @@ impl Default for WebUIHandler {

/// Write ` name="value"` to the writer without allocating a format string.
fn write_attr(writer: &mut dyn ResponseWriter, name: &str, value: &str) -> Result<()> {
writer.write(" ")?;
writer.write(name)?;
writer.write("=\"")?;
writer.write(value)?;
writer.write("\"")
writer.write_attribute(name, value)
}

#[cfg(test)]
Expand Down Expand Up @@ -3011,6 +3030,36 @@ mod tests {
}
}

#[test]
fn generated_string_writer_methods_preserve_exact_attribute_output() {
struct AttributeWriter {
output: String,
}

impl ResponseWriter for AttributeWriter {
fn write(&mut self, content: &str) -> Result<()> {
self.output.push_str(content);
Ok(())
}

crate::string_response_writer_methods!(output);

fn end(&mut self) -> Result<()> {
Ok(())
}
}

let mut writer = AttributeWriter {
output: String::new(),
};
write_attr(&mut writer, "data-id", "42")
.unwrap_or_else(|error| panic!("attribute write failed: {error}"));
writer
.write_boolean_attribute("disabled")
.unwrap_or_else(|error| panic!("boolean attribute write failed: {error}"));
assert_eq!(writer.output, " data-id=\"42\" disabled");
}

#[test]
fn test_handle_raw() {
// Create a simple protocol
Expand Down
148 changes: 148 additions & 0 deletions crates/webui-handler/src/response_writer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/// Append one quoted HTML attribute to an existing string allocation.
#[doc(hidden)]
#[inline]
pub fn append_attribute_to_string(output: &mut String, name: &str, value: &str) {
output.push(' ');
output.push_str(name);
output.push_str("=\"");
output.push_str(value);
output.push('"');
}

/// Append one boolean HTML attribute to an existing string allocation.
#[doc(hidden)]
#[inline]
pub fn append_boolean_attribute_to_string(output: &mut String, name: &str) {
output.push(' ');
output.push_str(name);
}

/// Append one quoted HTML attribute to an existing byte allocation.
#[doc(hidden)]
#[inline]
pub fn append_attribute_to_bytes(output: &mut Vec<u8>, name: &str, value: &str) {
output.push(b' ');
output.extend_from_slice(name.as_bytes());
output.extend_from_slice(b"=\"");
output.extend_from_slice(value.as_bytes());
output.push(b'"');
}

/// Append one boolean HTML attribute to an existing byte allocation.
#[doc(hidden)]
#[inline]
pub fn append_boolean_attribute_to_bytes(output: &mut Vec<u8>, name: &str) {
output.push(b' ');
output.extend_from_slice(name.as_bytes());
}

/// Generate optimized attribute methods for a string-backed `ResponseWriter`.
#[doc(hidden)]
#[macro_export]
macro_rules! string_response_writer_methods {
($field:ident) => {
fn write_attribute(&mut self, name: &str, value: &str) -> $crate::Result<()> {
self.$field.push(' ');
self.$field.push_str(name);
self.$field.push_str("=\"");
self.$field.push_str(value);
self.$field.push('"');
Ok(())
}

fn write_boolean_attribute(&mut self, name: &str) -> $crate::Result<()> {
self.$field.push(' ');
self.$field.push_str(name);
Ok(())
}
};
}

/// Generate optimized attribute methods for a byte-backed `ResponseWriter`.
#[doc(hidden)]
#[macro_export]
macro_rules! bytes_response_writer_methods {
($field:ident) => {
fn write_attribute(&mut self, name: &str, value: &str) -> $crate::Result<()> {
self.$field.push(b' ');
self.$field.extend_from_slice(name.as_bytes());
self.$field.extend_from_slice(b"=\"");
self.$field.extend_from_slice(value.as_bytes());
self.$field.push(b'"');
Ok(())
}

fn write_boolean_attribute(&mut self, name: &str) -> $crate::Result<()> {
self.$field.push(b' ');
self.$field.extend_from_slice(name.as_bytes());
Ok(())
}
};
}

/// Define a private string-backed writer with local, inlinable methods.
#[doc(hidden)]
#[macro_export]
macro_rules! define_string_response_writer {
($name:ident, $field:ident) => {
struct $name {
$field: String,
}

impl $name {
fn with_capacity(capacity: usize) -> Self {
Self {
$field: String::with_capacity(capacity),
}
}
}

impl $crate::ResponseWriter for $name {
fn write(&mut self, content: &str) -> $crate::Result<()> {
self.$field.push_str(content);
Ok(())
}

$crate::string_response_writer_methods!($field);

fn end(&mut self) -> $crate::Result<()> {
Ok(())
}
}
};
}

/// Define a private byte-backed writer with local, inlinable methods.
#[doc(hidden)]
#[macro_export]
macro_rules! define_bytes_response_writer {
($name:ident, $field:ident) => {
struct $name {
$field: Vec<u8>,
}

impl $name {
fn with_capacity(capacity: usize) -> Self {
Self {
$field: Vec::with_capacity(capacity),
}
}
}

impl $crate::ResponseWriter for $name {
fn write(&mut self, content: &str) -> $crate::Result<()> {
self.$field.extend_from_slice(content.as_bytes());
Ok(())
}

$crate::bytes_response_writer_methods!($field);

fn end(&mut self) -> $crate::Result<()> {
Ok(())
}
}
};
}
Loading
Loading