use core::fmt;
#[cfg(feature = "http")]
use http::{HeaderMap, StatusCode};
use topcoat_core::context::Cx;
use crate::{HtmlContext, HtmlWriter, View, buffer::ViewBuffer};
pub trait DynViewPart: 'static + fmt::Debug + Send + Sync {
#[track_caller]
fn render(&self, cx: &Cx, w: &mut HtmlWriter<'_, '_>);
#[inline]
fn size_hint(&self) -> usize {
0
}
}
macro_rules! impl_push_primitive {
($method:ident, $ty:ty, $size_hint:expr) => {
#[doc = concat!("Appends a `", stringify!($ty), "` rendered as text.")]
#[inline]
pub fn $method(&mut self, value: $ty) -> &mut Self {
self.size_hint += $size_hint;
self.buffer.$method(value);
self
}
};
}
pub struct PartsWriter<'a> {
buffer: &'a mut ViewBuffer,
context: HtmlContext,
size_hint: usize,
}
impl<'a> PartsWriter<'a> {
#[inline]
fn new(buffer: &'a mut ViewBuffer, context: HtmlContext) -> Self {
Self {
buffer,
context,
size_hint: 0,
}
}
pub(crate) fn block(buffer: &mut ViewBuffer, f: impl FnOnce(&mut PartsWriter<'_>)) -> View {
let entry = buffer.next_ptr();
let mut parts = PartsWriter::new(buffer, HtmlContext::Text);
f(&mut parts);
let size_hint = parts.size_hint();
buffer.push_ret();
View::from_scope(buffer.id(), entry, size_hint)
}
#[inline]
pub(crate) fn size_hint(&self) -> usize {
self.size_hint
}
#[inline]
pub(crate) fn in_context<R>(
&mut self,
context: HtmlContext,
f: impl FnOnce(&mut Self) -> R,
) -> R {
let previous = std::mem::replace(&mut self.context, context);
let result = f(self);
self.context = previous;
result
}
fn str_size_hint(value: &str, context: HtmlContext) -> usize {
match context {
HtmlContext::Unescaped => value.len(),
_ => value.len() + value.len() / 8,
}
}
#[inline]
pub fn push_str(&mut self, value: &str) -> &mut Self {
self.size_hint += Self::str_size_hint(value, self.context);
self.buffer.push_str(value, self.context);
self
}
#[inline]
pub fn push_static_str(&mut self, value: &'static str) -> &mut Self {
self.size_hint += Self::str_size_hint(value, self.context);
self.buffer.push_static_str(value, self.context);
self
}
#[inline]
pub fn push_promoted_str(&mut self, value: &'static &'static str) -> &mut Self {
self.size_hint += Self::str_size_hint(value, self.context);
self.buffer.push_promoted_str(value, self.context);
self
}
#[inline]
pub fn push_string(&mut self, value: String) -> &mut Self {
self.size_hint += Self::str_size_hint(&value, self.context);
self.buffer.push_string(value, self.context);
self
}
#[inline]
pub fn push_str_unescaped(&mut self, value: &str) -> &mut Self {
self.size_hint += value.len();
self.buffer.push_str(value, HtmlContext::Unescaped);
self
}
#[inline]
pub fn push_static_str_unescaped(&mut self, value: &'static str) -> &mut Self {
self.size_hint += value.len();
self.buffer.push_static_str(value, HtmlContext::Unescaped);
self
}
#[inline]
pub fn push_promoted_str_unescaped(&mut self, value: &'static &'static str) -> &mut Self {
self.size_hint += value.len();
self.buffer.push_promoted_str(value, HtmlContext::Unescaped);
self
}
#[inline]
pub fn push_string_unescaped(&mut self, value: String) -> &mut Self {
self.size_hint += value.len();
self.buffer.push_string(value, HtmlContext::Unescaped);
self
}
#[inline]
pub fn push_comment(&mut self, build: impl FnOnce(&mut PartsWriter<'_>)) -> &mut Self {
assert!(
self.context == HtmlContext::Text,
"tried to push comment in html context {:?}",
self.context,
);
self.push_promoted_str_unescaped(&"<!-- ");
self.in_context(HtmlContext::Comment, build);
self.push_promoted_str_unescaped(&" -->");
self
}
#[inline]
pub fn push_char(&mut self, value: char) -> &mut Self {
self.size_hint += 3;
self.buffer.push_char(value, self.context);
self
}
impl_push_primitive!(push_bool, bool, 5);
impl_push_primitive!(push_i8, i8, 3);
impl_push_primitive!(push_i16, i16, 4);
impl_push_primitive!(push_i32, i32, 6);
impl_push_primitive!(push_i64, i64, 11);
impl_push_primitive!(push_i128, i128, 21);
impl_push_primitive!(push_isize, isize, 11);
impl_push_primitive!(push_u8, u8, 2);
impl_push_primitive!(push_u16, u16, 3);
impl_push_primitive!(push_u32, u32, 6);
impl_push_primitive!(push_u64, u64, 11);
impl_push_primitive!(push_u128, u128, 20);
impl_push_primitive!(push_usize, usize, 11);
impl_push_primitive!(push_f32, f32, 9);
impl_push_primitive!(push_f64, f64, 13);
#[inline]
pub fn push_dyn(&mut self, part: Box<dyn DynViewPart>) -> &mut Self {
self.size_hint += part.size_hint();
self.buffer.push_dyn(part, self.context);
self
}
#[inline]
pub(crate) fn push_view(&mut self, view: View) -> &mut Self {
self.size_hint += view.size_hint();
self.buffer.push_view(view);
self
}
#[cfg(feature = "http")]
#[inline]
pub fn push_status_code(&mut self, status_code: StatusCode) -> &mut Self {
self.buffer.push_status_code(status_code);
self
}
#[cfg(feature = "http")]
#[inline]
pub fn push_headers(&mut self, headers: HeaderMap) -> &mut Self {
self.buffer.push_headers(headers);
self
}
}
#[cfg(test)]
mod tests {
use std::{
future::Future,
pin::pin,
task::{Context, Poll, Waker},
};
use super::*;
use crate::{
buffer::ViewBufferScope,
internal::{build_sync, write_block},
};
fn block_on<F: Future>(fut: F) -> F::Output {
let mut fut = pin!(fut);
let mut task = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(output) = fut.as_mut().poll(&mut task) {
return output;
}
}
}
fn in_scope<R>(f: impl AsyncFnOnce(&Cx) -> R) -> R {
block_on(ViewBufferScope::scope(async { f(&Cx::default()).await })).0
}
fn render_with(context: HtmlContext, f: impl FnOnce(&mut PartsWriter<'_>)) -> String {
in_scope(async |cx| {
build_sync(|| write_block(|parts| parts.in_context(context, f))).render(cx)
})
}
#[test]
fn push_str_seals_the_writer_context() {
let out = render_with(HtmlContext::Text, |w| {
w.push_str("<b> & \"q\"");
});
assert_eq!(out, "<b> & \"q\"");
let out = render_with(HtmlContext::AttributeValue, |w| {
w.push_str("<b> & \"q\"");
});
assert_eq!(out, "<b> & "q"");
}
#[test]
fn push_str_unescaped_bypasses_the_context() {
let out = render_with(HtmlContext::Text, |w| {
w.push_str_unescaped("<b>raw</b>");
});
assert_eq!(out, "<b>raw</b>");
}
#[test]
fn push_promoted_str_seals_the_writer_context() {
let out = render_with(HtmlContext::Text, |w| {
w.push_promoted_str(&"<b> & \"q\"");
});
assert_eq!(out, "<b> & \"q\"");
let out = render_with(HtmlContext::AttributeValue, |w| {
w.push_promoted_str(&"<b> & \"q\"");
});
assert_eq!(out, "<b> & "q"");
}
#[test]
fn push_promoted_str_unescaped_bypasses_the_context() {
let out = render_with(HtmlContext::Text, |w| {
w.push_promoted_str_unescaped(&"<b>raw</b>");
});
assert_eq!(out, "<b>raw</b>");
}
#[test]
fn push_promoted_str_skips_empty_strings() {
let out = render_with(HtmlContext::Text, |w| {
w.push_promoted_str(&"a").push_promoted_str(&"");
w.push_promoted_str_unescaped(&"").push_promoted_str(&"b");
});
assert_eq!(out, "ab");
}
#[test]
fn push_char_seals_the_writer_context() {
let out = render_with(HtmlContext::Text, |w| {
w.push_char('<');
});
assert_eq!(out, "<");
}
#[test]
#[should_panic(expected = "invalid attribute key")]
fn ident_context_panics_on_forbidden_characters_at_render() {
render_with(HtmlContext::AttributeKey, |w| {
w.push_str("on click");
});
}
#[test]
fn push_primitives_render_as_text() {
let out = render_with(HtmlContext::Text, |w| {
w.push_i32(-42).push_str_unescaped(" ");
w.push_bool(true).push_str_unescaped(" ");
w.push_f64(1.5).push_str_unescaped(" ");
w.push_i128(-1 << 100).push_str_unescaped(" ");
w.push_u128(1 << 100);
});
assert_eq!(
out,
"-42 true 1.5 -1267650600228229401496703205376 1267650600228229401496703205376"
);
}
}