diff --git a/DESIGN.md b/DESIGN.md
index 3f7bf038c..823b7049e 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -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
diff --git a/crates/webui-cli/src/commands/serve.rs b/crates/webui-cli/src/commands/serve.rs
index a9fa5385a..0ea5b19f1 100644
--- a/crates/webui-cli/src/commands/serve.rs
+++ b/crates/webui-cli/src/commands/serve.rs
@@ -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)]
@@ -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
/// `` and across sub-path deployments.
const HMR_ENDPOINT: &str = "/__webui/livereload";
@@ -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 {
diff --git a/crates/webui-ffi/src/lib.rs b/crates/webui-ffi/src/lib.rs
index 1a12ede67..412631f44 100644
--- a/crates/webui-ffi/src/lib.rs
+++ b/crates/webui-ffi/src/lib.rs
@@ -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.
@@ -81,29 +81,7 @@ struct ProtocolContext {
protocol: Arc,
}
-/// 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
@@ -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)
diff --git a/crates/webui-handler/benches/bootstrap_state_bench.rs b/crates/webui-handler/benches/bootstrap_state_bench.rs
index bfe66f0c4..498ab2b72 100644
--- a/crates/webui-handler/benches/bootstrap_state_bench.rs
+++ b/crates/webui-handler/benches/bootstrap_state_bench.rs
@@ -89,6 +89,8 @@ impl ResponseWriter for BenchWriter {
Ok(())
}
+ webui_handler::string_response_writer_methods!(output);
+
fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
diff --git a/crates/webui-handler/benches/handler_bench.rs b/crates/webui-handler/benches/handler_bench.rs
index 4392913c0..2fca20ad1 100644
--- a/crates/webui-handler/benches/handler_bench.rs
+++ b/crates/webui-handler/benches/handler_bench.rs
@@ -37,6 +37,8 @@ impl ResponseWriter for BenchWriter {
Ok(())
}
+ webui_handler::string_response_writer_methods!(output);
+
fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
diff --git a/crates/webui-handler/benches/streaming_hydration_bench.rs b/crates/webui-handler/benches/streaming_hydration_bench.rs
index 9588d9ff4..077cc8e8b 100644
--- a/crates/webui-handler/benches/streaming_hydration_bench.rs
+++ b/crates/webui-handler/benches/streaming_hydration_bench.rs
@@ -59,6 +59,8 @@ impl ResponseWriter for BenchWriter {
Ok(())
}
+ webui_handler::string_response_writer_methods!(output);
+
fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
diff --git a/crates/webui-handler/src/lib.rs b/crates/webui-handler/src/lib.rs
index ffbfd208b..e7a50e490 100644
--- a/crates/webui-handler/src/lib.rs
+++ b/crates/webui-handler/src/lib.rs
@@ -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
@@ -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<()>;
@@ -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)]
@@ -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
diff --git a/crates/webui-handler/src/response_writer.rs b/crates/webui-handler/src/response_writer.rs
new file mode 100644
index 000000000..f60232ecb
--- /dev/null
+++ b/crates/webui-handler/src/response_writer.rs
@@ -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, 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, 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,
+ }
+
+ 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(())
+ }
+ }
+ };
+}
diff --git a/crates/webui-handler/src/streaming/mod.rs b/crates/webui-handler/src/streaming/mod.rs
index d66d1511d..b95515896 100644
--- a/crates/webui-handler/src/streaming/mod.rs
+++ b/crates/webui-handler/src/streaming/mod.rs
@@ -146,6 +146,26 @@ impl ResponseWriter for StreamingSink<'_, W> {
self.transport.write(content)
}
+ fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
+ if let Some(opening) = self.component_opening.as_mut() {
+ crate::response_writer::append_attribute_to_string(&mut opening.bytes, name, value);
+ return Ok(());
+ }
+ self.written = self
+ .written
+ .wrapping_add(name.len().wrapping_add(value.len()).wrapping_add(4));
+ self.transport.write_attribute(name, value)
+ }
+
+ fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
+ if let Some(opening) = self.component_opening.as_mut() {
+ crate::response_writer::append_boolean_attribute_to_string(&mut opening.bytes, name);
+ return Ok(());
+ }
+ self.written = self.written.wrapping_add(name.len().wrapping_add(1));
+ self.transport.write_boolean_attribute(name)
+ }
+
fn end(&mut self) -> Result<()> {
self.transport.end()
}
diff --git a/crates/webui-handler/src/streaming/owned.rs b/crates/webui-handler/src/streaming/owned.rs
index ac1c76eab..ceada76aa 100644
--- a/crates/webui-handler/src/streaming/owned.rs
+++ b/crates/webui-handler/src/streaming/owned.rs
@@ -38,6 +38,16 @@ impl ResponseWriter for BufferSink {
Ok(())
}
+ fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
+ crate::append_attribute_to_bytes(&mut self.bytes, name, value);
+ Ok(())
+ }
+
+ fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
+ crate::append_boolean_attribute_to_bytes(&mut self.bytes, name);
+ Ok(())
+ }
+
fn end(&mut self) -> Result<()> {
Ok(())
}
diff --git a/crates/webui-node/src/lib.rs b/crates/webui-node/src/lib.rs
index acf039152..a30c93bef 100644
--- a/crates/webui-node/src/lib.rs
+++ b/crates/webui-node/src/lib.rs
@@ -557,28 +557,7 @@ fn create_handler(plugin: Option) -> napi::Result {
})
}
-struct BufferedWriter {
- output: String,
-}
-
-impl BufferedWriter {
- fn new() -> Self {
- Self {
- output: String::with_capacity(4096),
- }
- }
-}
-
-impl ResponseWriter for BufferedWriter {
- fn write(&mut self, content: &str) -> webui_handler::Result<()> {
- self.output.push_str(content);
- Ok(())
- }
-
- fn end(&mut self) -> webui_handler::Result<()> {
- Ok(())
- }
-}
+webui_handler::define_string_response_writer!(BufferedWriter, output);
fn render_to_string(
handler: &WebUIHandler,
@@ -586,7 +565,7 @@ fn render_to_string(
state: &Value,
options: &RenderOptions<'_>,
) -> napi::Result {
- let mut writer = BufferedWriter::new();
+ let mut writer = BufferedWriter::with_capacity(4096);
handler
.render(protocol, state, options, &mut writer)
.map_err(|e| NapiError::from_reason(format!("Render error: {e}")))?;
@@ -644,6 +623,28 @@ where
Ok(())
}
+ fn write_attribute(&mut self, name: &str, value: &str) -> webui_handler::Result<()> {
+ if self.error.is_some() {
+ return Err(callback_writer_error());
+ }
+ webui_handler::append_attribute_to_string(&mut self.buffer, name, value);
+ if self.buffer.len() >= STREAM_CHUNK_SIZE {
+ self.flush()?;
+ }
+ Ok(())
+ }
+
+ fn write_boolean_attribute(&mut self, name: &str) -> webui_handler::Result<()> {
+ if self.error.is_some() {
+ return Err(callback_writer_error());
+ }
+ webui_handler::append_boolean_attribute_to_string(&mut self.buffer, name);
+ if self.buffer.len() >= STREAM_CHUNK_SIZE {
+ self.flush()?;
+ }
+ Ok(())
+ }
+
fn end(&mut self) -> webui_handler::Result<()> {
self.flush()
}
diff --git a/crates/webui-press/src/build.rs b/crates/webui-press/src/build.rs
index 0027c5d76..90c6551cb 100644
--- a/crates/webui-press/src/build.rs
+++ b/crates/webui-press/src/build.rs
@@ -14,7 +14,7 @@ use console::style;
use rayon::prelude::*;
use serde_json::{Map, Value};
use webui::BuildOptions;
-use webui_handler::{Protocol, RenderOptions, ResponseWriter, WebUIHandler};
+use webui_handler::{Protocol, RenderOptions, WebUIHandler};
use webui_tokens::TokenFile;
use crate::bundler::{
@@ -29,6 +29,8 @@ use crate::markdown::Highlighter;
use crate::state::{load_render_states, merge_page_state};
use crate::types::{BuildStats, DocsConfig};
+webui_handler::define_string_response_writer!(StringWriter, buf);
+
/// Persistent state held by the dev server across rebuilds. The dev
/// server always performs a full rebuild on every watcher tick — the
/// previous incremental machinery proved too complex for the marginal
@@ -217,30 +219,6 @@ fn json_obj(entries: [(&str, Value); N]) -> Value {
Value::Object(map)
}
-/// A writer that collects rendered HTML into a String buffer.
-struct StringWriter {
- buf: String,
-}
-
-impl StringWriter {
- fn with_capacity(cap: usize) -> Self {
- Self {
- buf: String::with_capacity(cap),
- }
- }
-}
-
-impl ResponseWriter for StringWriter {
- fn write(&mut self, content: &str) -> webui_handler::Result<()> {
- self.buf.push_str(content);
- Ok(())
- }
-
- fn end(&mut self) -> webui_handler::Result<()> {
- Ok(())
- }
-}
-
/// Build a documentation site from the given configuration.
///
/// `config_dir` is the directory containing `config.json`. It is used to
diff --git a/crates/webui-python/src/lib.rs b/crates/webui-python/src/lib.rs
index 28c681bb8..483e179c0 100644
--- a/crates/webui-python/src/lib.rs
+++ b/crates/webui-python/src/lib.rs
@@ -15,7 +15,7 @@ use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
use webui_handler::plugin::webui::WebUIHydrationPlugin;
use webui_handler::{
BoundaryDescriptor, BoundaryInstanceId, BoundaryKey, BoundaryMode, HandlerError, Protocol,
- RenderOptions, ResponseWriter, SessionOptions, StreamStep as HandlerStreamStep,
+ RenderOptions, SessionOptions, StreamStep as HandlerStreamStep,
StreamingSession as HandlerStreamingSession, WebUIHandler,
};
@@ -44,6 +44,8 @@ type PyRenderOptions = (
type PyPartialOptions = (String, String, String);
type PyTemplateOptions = (Vec, String);
+webui_handler::define_bytes_response_writer!(BytesWriter, bytes);
+
enum JsonInput {
Text(PyBackedStr),
Bytes(PyBackedBytes),
@@ -182,29 +184,6 @@ impl BindingError {
}
}
-struct BytesWriter {
- bytes: Vec,
-}
-
-impl BytesWriter {
- fn new() -> Self {
- Self {
- bytes: Vec::with_capacity(4096),
- }
- }
-}
-
-impl ResponseWriter for BytesWriter {
- fn write(&mut self, content: &str) -> Result<(), HandlerError> {
- self.bytes.extend_from_slice(content.as_bytes());
- Ok(())
- }
-
- fn end(&mut self) -> Result<(), HandlerError> {
- Ok(())
- }
-}
-
#[pyclass(name = "_Renderer", frozen, module = "microsoft_webui._native")]
struct NativeRenderer {
protocol: Arc,
@@ -338,7 +317,7 @@ impl NativeRenderer {
) -> Result, BindingError> {
let state = serde_json::from_slice::(input.as_bytes())
.map_err(|error| BindingError::state(format!("failed to parse state JSON: {error}")))?;
- let mut writer = BytesWriter::new();
+ let mut writer = BytesWriter::with_capacity(4096);
self.handler
.render(&self.protocol, &state, &options.borrowed(), &mut writer)
.map_err(render_binding_error)?;
diff --git a/crates/webui-wasm/src/handler.rs b/crates/webui-wasm/src/handler.rs
index 56e09f7e8..b91c17e70 100644
--- a/crates/webui-wasm/src/handler.rs
+++ b/crates/webui-wasm/src/handler.rs
@@ -21,29 +21,7 @@ use webui_protocol::WebUIProtocol;
const STREAM_CHUNK_SIZE: usize = 16 * 1024;
-/// A string buffer for collecting rendered output.
-struct StringWriter {
- content: String,
-}
-
-impl StringWriter {
- fn with_capacity(cap: usize) -> Self {
- Self {
- content: String::with_capacity(cap),
- }
- }
-}
-
-impl ResponseWriter for StringWriter {
- 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!(StringWriter, content);
/// A writer that batches rendered fragments before crossing into JavaScript.
struct CallbackWriter<'a> {
@@ -81,6 +59,22 @@ impl ResponseWriter for CallbackWriter<'_> {
Ok(())
}
+ fn write_attribute(&mut self, name: &str, value: &str) -> webui_handler::Result<()> {
+ webui_handler::append_attribute_to_string(&mut self.buffer, name, value);
+ if self.buffer.len() >= STREAM_CHUNK_SIZE {
+ self.flush()?;
+ }
+ Ok(())
+ }
+
+ fn write_boolean_attribute(&mut self, name: &str) -> webui_handler::Result<()> {
+ webui_handler::append_boolean_attribute_to_string(&mut self.buffer, name);
+ if self.buffer.len() >= STREAM_CHUNK_SIZE {
+ self.flush()?;
+ }
+ Ok(())
+ }
+
fn end(&mut self) -> webui_handler::Result<()> {
self.flush()
}
diff --git a/crates/webui/benches/contact_book_bench.rs b/crates/webui/benches/contact_book_bench.rs
index 8b8813a86..ff7be52fb 100644
--- a/crates/webui/benches/contact_book_bench.rs
+++ b/crates/webui/benches/contact_book_bench.rs
@@ -6,7 +6,7 @@
//! benchmark time, then measures protocol parsing and handler rendering at
//! different data scales (10 / 100 / 1,000 contacts).
//!
-//! Run with: `cargo bench -p webui --bench contact_book_bench`
+//! Run with: `cargo bench -p microsoft-webui --bench contact_book_bench`
use criterion::{criterion_group, BenchmarkId, Criterion, Throughput};
use serde_json::{json, Value};
@@ -96,6 +96,8 @@ impl ResponseWriter for BenchWriter {
Ok(())
}
+ webui_handler::string_response_writer_methods!(output);
+
fn end(&mut self) -> webui_handler::Result<()> {
Ok(())
}
diff --git a/crates/webui/src/server.rs b/crates/webui/src/server.rs
index 13b7d4c3b..ab8f2a4d3 100644
--- a/crates/webui/src/server.rs
+++ b/crates/webui/src/server.rs
@@ -27,10 +27,12 @@
//! }
//! ```
-use crate::{Protocol, ResponseWriter, WebUIHandler};
+use crate::{Protocol, WebUIHandler};
use webui_handler::route_handler;
use webui_handler::RenderOptions;
+webui_handler::define_string_response_writer!(MemWriter, buf);
+
/// A server request to be handled by [`serve_request`].
pub struct ServeRequest<'a> {
/// The URL path (e.g., `"/email/thread-5"`, `"/folder/sent"`).
@@ -108,29 +110,6 @@ pub fn serve_request(
}
}
-struct MemWriter {
- buf: String,
-}
-
-impl MemWriter {
- fn with_capacity(cap: usize) -> Self {
- Self {
- buf: String::with_capacity(cap),
- }
- }
-}
-
-impl ResponseWriter for MemWriter {
- fn write(&mut self, content: &str) -> webui_handler::Result<()> {
- self.buf.push_str(content);
- Ok(())
- }
-
- fn end(&mut self) -> webui_handler::Result<()> {
- Ok(())
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/webui/src/streaming.rs b/crates/webui/src/streaming.rs
index 0af8e2076..4bca74015 100644
--- a/crates/webui/src/streaming.rs
+++ b/crates/webui/src/streaming.rs
@@ -618,6 +618,28 @@ impl ResponseWriter for StreamingWriter {
Ok(())
}
+ fn write_attribute(&mut self, name: &str, value: &str) -> Result<()> {
+ if let Some(cause) = self.terminated {
+ return Err(cause.into());
+ }
+ webui_handler::append_attribute_to_bytes(&mut self.buf, name, value);
+ if self.buf.len() >= self.chunk_target {
+ self.flush_buf()?;
+ }
+ Ok(())
+ }
+
+ fn write_boolean_attribute(&mut self, name: &str) -> Result<()> {
+ if let Some(cause) = self.terminated {
+ return Err(cause.into());
+ }
+ webui_handler::append_boolean_attribute_to_bytes(&mut self.buf, name);
+ if self.buf.len() >= self.chunk_target {
+ self.flush_buf()?;
+ }
+ Ok(())
+ }
+
fn end(&mut self) -> Result<()> {
// Surface the final-flush error so the caller can distinguish
// "fully delivered" from "client gave up at the very last
diff --git a/examples/demo/server/src/shell.rs b/examples/demo/server/src/shell.rs
index f422b067f..694b1f859 100644
--- a/examples/demo/server/src/shell.rs
+++ b/examples/demo/server/src/shell.rs
@@ -15,10 +15,12 @@ use std::sync::Arc;
use webui::{build, BuildOptions, Plugin, Protocol};
use webui_handler::plugin::webui::WebUIHydrationPlugin;
-use webui_handler::{RenderOptions, ResponseWriter, WebUIHandler};
+use webui_handler::{RenderOptions, WebUIHandler};
use crate::registry::AppEntry;
+webui_handler::define_string_response_writer!(StringWriter, buf);
+
/// Shared state for the shell renderer: the compiled protocol and the
/// directory containing client-side assets (`dist/`).
pub(crate) struct ShellState {
@@ -97,21 +99,6 @@ fn build_state(apps: &[AppEntry], current_index: usize) -> serde_json::Value {
})
}
-struct StringWriter {
- buf: String,
-}
-
-impl ResponseWriter for StringWriter {
- fn write(&mut self, content: &str) -> webui_handler::Result<()> {
- self.buf.push_str(content);
- Ok(())
- }
-
- fn end(&mut self) -> webui_handler::Result<()> {
- Ok(())
- }
-}
-
/// Serves the shell page at `/`. Picks the initial current app via the
/// `?app=` query parameter when present.
pub(crate) async fn shell_page(
@@ -136,9 +123,7 @@ pub(crate) async fn shell_page(
let state = build_state(&apps, current_index);
- let mut writer = StringWriter {
- buf: String::with_capacity(8 * 1024),
- };
+ let mut writer = StringWriter::with_capacity(8 * 1024);
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
let opts = RenderOptions::new("index.html", "/");