use core::{
fmt::{self, Debug, Formatter, Write},
marker::PhantomData,
ptr,
};
use std::{borrow::Cow, rc::Rc, sync::Arc};
use crate::context::{AttributeValue, Context, DatastarSource, Element, ScriptSource};
#[derive(Clone, Copy, Default, Eq, Hash)]
pub struct Raw<T: AsRef<str>, C: Context = Element> {
inner: T,
context: PhantomData<C>,
}
impl<T: AsRef<str>, C: Context> Raw<T, C> {
#[inline]
pub const fn dangerously_create(value: T) -> Self {
Self {
inner: value,
context: PhantomData,
}
}
#[inline]
pub const fn into_inner(self) -> T {
unsafe { const_precise_live_drops_hack!(self.inner) }
}
#[inline]
pub const fn as_inner(&self) -> &T {
&self.inner
}
#[inline]
pub fn as_str(&self) -> &str {
self.inner.as_ref()
}
}
impl<T: AsRef<str>> Raw<T> {
#[inline]
#[must_use]
pub const fn rendered(self) -> Rendered<T> {
let value = unsafe { const_precise_live_drops_hack!(self.inner) };
Rendered(value)
}
}
impl<T: AsRef<str> + PartialEq<U>, C: Context, U: AsRef<str>> PartialEq<Raw<U, C>> for Raw<T, C> {
#[inline]
fn eq(&self, other: &Raw<U, C>) -> bool {
self.inner == other.inner
}
}
impl<T: AsRef<str>, C: Context> Debug for Raw<T, C> {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("Raw").field(&self.inner.as_ref()).finish()
}
}
pub type RawAttribute<T> = Raw<T, AttributeValue>;
pub type RawDatastarSource<T> = Raw<T, DatastarSource>;
pub type RawScript<T> = Raw<T, ScriptSource>;
#[derive(Debug, Clone, Copy, Default, Eq, Hash)]
pub struct Rendered<T>(T);
impl<T> Rendered<T> {
#[inline]
pub const fn into_inner(self) -> T {
unsafe { const_precise_live_drops_hack!(self.0) }
}
#[inline]
pub const fn as_inner(&self) -> &T {
&self.0
}
}
impl<T: PartialEq<U>, U> PartialEq<Rendered<U>> for Rendered<T> {
#[inline]
fn eq(&self, other: &Rendered<U>) -> bool {
self.0 == other.0
}
}
macro_rules! const_precise_live_drops_hack {
($self:ident. $field:tt) => {{
let this = core::mem::ManuallyDrop::new($self);
(&raw const (*(&raw const this).cast::<Self>()).$field).read()
}};
}
pub(crate) use const_precise_live_drops_hack;
#[derive(Clone, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct Buffer<C: Context = Element> {
inner: String,
context: PhantomData<C>,
}
pub type AttributeBuffer = Buffer<AttributeValue>;
#[expect(
clippy::missing_const_for_fn,
reason = "`Buffer` does not make sense in `const` contexts"
)]
impl<C: Context> Buffer<C> {
#[inline]
fn cast_context<T: Context>(&mut self) -> &mut Buffer<T> {
unsafe { &mut *ptr::from_mut(self).cast::<Buffer<T>>() }
}
#[inline]
#[must_use]
pub fn new() -> Self {
Self::dangerously_from_string(String::new())
}
#[inline]
#[must_use]
pub fn dangerously_from_string(string: String) -> Self {
Self {
inner: string,
context: PhantomData,
}
}
#[inline]
#[must_use]
pub fn dangerously_from_string_mut(string: &mut String) -> &mut Self {
unsafe { &mut *ptr::from_mut(string).cast::<Self>() }
}
#[inline]
pub fn as_attribute_buffer(&mut self) -> &mut AttributeBuffer {
self.cast_context()
}
#[inline]
pub fn as_datastar_buffer(&mut self) -> &mut Buffer<DatastarSource> {
self.cast_context()
}
#[inline]
pub fn as_script_buffer(&mut self) -> &mut Buffer<ScriptSource> {
self.cast_context()
}
#[inline]
#[must_use]
pub fn rendered(self) -> Rendered<String> {
Rendered(self.inner)
}
}
#[expect(
clippy::missing_const_for_fn,
reason = "`Buffer` does not make sense in `const` contexts"
)]
impl<C: Context> Buffer<C> {
#[inline]
pub fn dangerously_get_string(&mut self) -> &mut String {
&mut self.inner
}
}
impl Debug for Buffer {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_tuple("Buffer").field(&self.inner).finish()
}
}
pub trait Render<C: Context = Element> {
fn render_to(&self, buffer: &mut Buffer<C>);
#[inline]
fn render(&self) -> Rendered<String>
where
Self: Render<C>,
{
let mut buffer = Buffer::<C>::new();
self.render_to(&mut buffer);
buffer.rendered()
}
}
pub trait RenderExt: Render {
#[inline]
fn memoize(&self) -> Raw<String> {
Raw::dangerously_create(self.render().into_inner())
}
}
impl<T: Render> RenderExt for T {}
#[derive(Clone, Copy)]
#[must_use = "`Lazy` does nothing unless `.render()` or `.render_to()` is called"]
pub struct Lazy<F: Fn(&mut Buffer<C>), C: Context = Element> {
f: F,
context: PhantomData<C>,
}
pub type LazyAttribute<F> = Lazy<F, AttributeValue>;
pub type LazyScript<F> = Lazy<F, ScriptSource>;
impl<F: Fn(&mut Buffer<C>), C: Context> Lazy<F, C> {
#[inline]
pub const fn dangerously_create(f: F) -> Self {
Self {
f,
context: PhantomData,
}
}
#[inline]
pub const fn into_inner(self) -> F {
unsafe { const_precise_live_drops_hack!(self.f) }
}
#[inline]
pub const fn as_inner(&self) -> &F {
&self.f
}
}
impl<F: Fn(&mut Buffer<C>), C: Context> Render<C> for Lazy<F, C> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
(self.f)(buffer);
}
}
impl<F: Fn(&mut Buffer<C>), C: Context> Debug for Lazy<F, C> {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_tuple("Lazy").finish_non_exhaustive()
}
}
impl<T: AsRef<str>, C: Context> Render<C> for Raw<T, C> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
buffer.dangerously_get_string().push_str(self.as_str());
}
#[inline]
fn render(&self) -> Rendered<String> {
Rendered(self.as_str().into())
}
}
#[inline]
fn push_html_double_quoted_attribute_char(dst: &mut String, ch: char) {
match ch {
'&' => dst.push_str("&"),
'<' => dst.push_str("<"),
'>' => dst.push_str(">"),
'"' => dst.push_str("""),
ch => dst.push(ch),
}
}
#[inline]
pub(crate) fn push_datastar_source_to_html_attribute(dst: &mut String, source: &str) {
for ch in source.chars() {
push_html_double_quoted_attribute_char(dst, ch);
}
}
#[inline]
pub(crate) fn push_json_source_to_html_attribute(dst: &mut String, source: &str) {
for ch in source.chars() {
match ch {
'\u{2028}' => dst.push_str("\\u2028"),
'\u{2029}' => dst.push_str("\\u2029"),
ch => push_html_double_quoted_attribute_char(dst, ch),
}
}
}
#[inline]
pub(crate) fn push_js_single_quoted_string_to_html_attribute(dst: &mut String, value: &str) {
dst.push('\'');
for ch in value.chars() {
match ch {
'\\' => dst.push_str("\\\\"),
'\'' => dst.push_str("\\'"),
'\n' => dst.push_str("\\n"),
'\r' => dst.push_str("\\r"),
'\t' => dst.push_str("\\t"),
'\u{2028}' => dst.push_str("\\u2028"),
'\u{2029}' => dst.push_str("\\u2029"),
ch if ch.is_control() => {
_ = write!(dst, "\\u{:04x}", ch as u32);
}
ch => push_html_double_quoted_attribute_char(dst, ch),
}
}
dst.push('\'');
}
#[inline]
pub(crate) fn push_js_single_quoted_string_to_script(dst: &mut String, value: &str) {
dst.push('\'');
for ch in value.chars() {
match ch {
'\\' => dst.push_str("\\\\"),
'\'' => dst.push_str("\\'"),
'\n' => dst.push_str("\\n"),
'\r' => dst.push_str("\\r"),
'\t' => dst.push_str("\\t"),
'<' => dst.push_str("\\x3C"),
'\u{2028}' => dst.push_str("\\u2028"),
'\u{2029}' => dst.push_str("\\u2029"),
ch if ch.is_control() => {
_ = write!(dst, "\\u{:04x}", ch as u32);
}
ch => dst.push(ch),
}
}
dst.push('\'');
}
impl Render for fmt::Arguments<'_> {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
struct ElementEscaper<'a>(&'a mut String);
impl Write for ElementEscaper<'_> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
html_escape::encode_text_to_string(s, self.0);
Ok(())
}
}
_ = ElementEscaper(buffer.dangerously_get_string()).write_fmt(*self);
}
}
impl Render<AttributeValue> for fmt::Arguments<'_> {
#[inline]
fn render_to(&self, buffer: &mut AttributeBuffer) {
struct AttributeEscaper<'a>(&'a mut String);
impl Write for AttributeEscaper<'_> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
html_escape::encode_double_quoted_attribute_to_string(s, self.0);
Ok(())
}
}
_ = AttributeEscaper(buffer.dangerously_get_string()).write_fmt(*self);
}
}
impl Render for char {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
let s = buffer.dangerously_get_string();
match *self {
'&' => s.push_str("&"),
'<' => s.push_str("<"),
'>' => s.push_str(">"),
c => s.push(c),
}
}
#[inline]
fn render(&self) -> Rendered<String> {
Rendered(match *self {
'&' => "&".into(),
'<' => "<".into(),
'>' => ">".into(),
c => c.into(),
})
}
}
impl Render<AttributeValue> for char {
#[inline]
fn render_to(&self, buffer: &mut AttributeBuffer) {
let s = buffer.dangerously_get_string();
match *self {
'&' => s.push_str("&"),
'<' => s.push_str("<"),
'>' => s.push_str(">"),
'"' => s.push_str("""),
c => s.push(c),
}
}
}
impl Render<DatastarSource> for char {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
let mut encoded = [0; 4];
push_js_single_quoted_string_to_html_attribute(
buffer.dangerously_get_string(),
self.encode_utf8(&mut encoded),
);
}
#[inline]
fn render(&self) -> Rendered<String> {
let mut s = String::with_capacity(8);
let mut encoded = [0; 4];
push_js_single_quoted_string_to_html_attribute(&mut s, self.encode_utf8(&mut encoded));
Rendered(s)
}
}
impl Render<ScriptSource> for char {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
let mut encoded = [0; 4];
push_js_single_quoted_string_to_script(
buffer.dangerously_get_string(),
self.encode_utf8(&mut encoded),
);
}
#[inline]
fn render(&self) -> Rendered<String> {
let mut s = String::with_capacity(8);
let mut encoded = [0; 4];
push_js_single_quoted_string_to_script(&mut s, self.encode_utf8(&mut encoded));
Rendered(s)
}
}
impl Render for str {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
html_escape::encode_text_to_string(self, buffer.dangerously_get_string());
}
#[inline]
fn render(&self) -> Rendered<String> {
Rendered(html_escape::encode_text(self).into_owned())
}
}
impl Render<AttributeValue> for str {
#[inline]
fn render_to(&self, buffer: &mut AttributeBuffer) {
html_escape::encode_double_quoted_attribute_to_string(
self,
buffer.dangerously_get_string(),
);
}
}
impl Render<DatastarSource> for str {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
push_js_single_quoted_string_to_html_attribute(buffer.dangerously_get_string(), self);
}
#[inline]
fn render(&self) -> Rendered<String> {
let mut s = String::with_capacity(self.len() + 2);
push_js_single_quoted_string_to_html_attribute(&mut s, self);
Rendered(s)
}
}
impl Render<ScriptSource> for str {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
push_js_single_quoted_string_to_script(buffer.dangerously_get_string(), self);
}
#[inline]
fn render(&self) -> Rendered<String> {
let mut s = String::with_capacity(self.len() + 2);
push_js_single_quoted_string_to_script(&mut s, self);
Rendered(s)
}
}
impl Render for String {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
self.as_str().render_to(buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
Render::<Element>::render(self.as_str())
}
}
impl Render<AttributeValue> for String {
#[inline]
fn render_to(&self, buffer: &mut AttributeBuffer) {
self.as_str().render_to(buffer);
}
}
impl Render<DatastarSource> for String {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
self.as_str().render_to(buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
Render::<DatastarSource>::render(self.as_str())
}
}
impl Render<ScriptSource> for String {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
self.as_str().render_to(buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
Render::<ScriptSource>::render(self.as_str())
}
}
impl<C: Context> Render<C> for bool {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
buffer
.dangerously_get_string()
.push_str(if *self { "true" } else { "false" });
}
#[inline]
fn render(&self) -> Rendered<String> {
Rendered(if *self { "true" } else { "false" }.into())
}
}
macro_rules! render_via_itoa {
($($Ty:ty)*) => {
$(
impl<C: Context> Render<C> for $Ty {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
buffer.dangerously_get_string().push_str(itoa::Buffer::new().format(*self));
}
#[inline]
fn render(&self) -> Rendered<String> {
Rendered(itoa::Buffer::new().format(*self).into())
}
}
)*
};
}
render_via_itoa! {
i8 i16 i32
u8 u16 u32
}
macro_rules! render_big_int_via_itoa {
($($Ty:ty)*) => {
$(
impl Render<Element> for $Ty {
#[inline]
fn render_to(&self, buffer: &mut Buffer<Element>) {
buffer.dangerously_get_string().push_str(itoa::Buffer::new().format(*self));
}
#[inline]
fn render(&self) -> Rendered<String> {
Rendered(itoa::Buffer::new().format(*self).into())
}
}
impl Render<AttributeValue> for $Ty {
#[inline]
fn render_to(&self, buffer: &mut Buffer<AttributeValue>) {
buffer.dangerously_get_string().push_str(itoa::Buffer::new().format(*self));
}
}
)*
};
}
render_big_int_via_itoa! {
i64 i128 isize
u64 u128 usize
}
macro_rules! render_via_zmij {
($($Ty:ty)*) => {
$(
impl<C: Context> Render<C> for $Ty {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
buffer.dangerously_get_string().push_str(zmij::Buffer::new().format(*self));
}
#[inline]
fn render(&self) -> Rendered<String> {
Rendered(zmij::Buffer::new().format(*self).into())
}
}
)*
};
}
render_via_zmij! {
f32 f64
}
macro_rules! render_via_deref {
($($Ty:ty)*) => {
$(
impl<T: Render + ?Sized> Render for $Ty {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
T::render_to(&**self, buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
T::render(&**self)
}
}
impl<T: Render<AttributeValue> + ?Sized> Render<AttributeValue> for $Ty {
#[inline]
fn render_to(&self, buffer: &mut AttributeBuffer) {
T::render_to(&**self, buffer);
}
}
impl<T: Render<DatastarSource> + ?Sized> Render<DatastarSource> for $Ty {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
T::render_to(&**self, buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
T::render(&**self)
}
}
impl<T: Render<ScriptSource> + ?Sized> Render<ScriptSource> for $Ty {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
T::render_to(&**self, buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
T::render(&**self)
}
}
)*
};
}
render_via_deref! {
&T
&mut T
Box<T>
Rc<T>
Arc<T>
}
impl<'a, B: 'a + Render + ToOwned + ?Sized> Render for Cow<'a, B> {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
B::render_to(&**self, buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
B::render(&**self)
}
}
impl<'a, B: 'a + Render<AttributeValue> + ToOwned + ?Sized> Render<AttributeValue> for Cow<'a, B> {
#[inline]
fn render_to(&self, buffer: &mut AttributeBuffer) {
B::render_to(&**self, buffer);
}
}
impl<'a, B: 'a + Render<DatastarSource> + ToOwned + ?Sized> Render<DatastarSource> for Cow<'a, B> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
B::render_to(&**self, buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
B::render(&**self)
}
}
impl<'a, B: 'a + Render<ScriptSource> + ToOwned + ?Sized> Render<ScriptSource> for Cow<'a, B> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
B::render_to(&**self, buffer);
}
#[inline]
fn render(&self) -> Rendered<String> {
B::render(&**self)
}
}
impl<T: Render> Render for [T] {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
for item in self {
item.render_to(buffer);
}
}
}
impl<T: Render<DatastarSource>> Render<DatastarSource> for [T] {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
buffer.dangerously_get_string().push('[');
for (index, item) in self.iter().enumerate() {
if index != 0 {
buffer.dangerously_get_string().push(',');
}
item.render_to(buffer);
}
buffer.dangerously_get_string().push(']');
}
}
impl<T: Render<ScriptSource>> Render<ScriptSource> for [T] {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
buffer.dangerously_get_string().push('[');
for (index, item) in self.iter().enumerate() {
if index != 0 {
buffer.dangerously_get_string().push(',');
}
item.render_to(buffer);
}
buffer.dangerously_get_string().push(']');
}
}
impl<T: Render, const N: usize> Render for [T; N] {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
self.as_slice().render_to(buffer);
}
}
impl<T: Render<DatastarSource>, const N: usize> Render<DatastarSource> for [T; N] {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
self.as_slice().render_to(buffer);
}
}
impl<T: Render<ScriptSource>, const N: usize> Render<ScriptSource> for [T; N] {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
self.as_slice().render_to(buffer);
}
}
impl<T: Render> Render for Vec<T> {
#[inline]
fn render_to(&self, buffer: &mut Buffer) {
self.as_slice().render_to(buffer);
}
}
impl<T: Render<DatastarSource>> Render<DatastarSource> for Vec<T> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<DatastarSource>) {
self.as_slice().render_to(buffer);
}
}
impl<T: Render<ScriptSource>> Render<ScriptSource> for Vec<T> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<ScriptSource>) {
self.as_slice().render_to(buffer);
}
}
impl<T: Render<C>, C: Context> Render<C> for Option<T> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
if let Some(value) = self {
value.render_to(buffer);
}
}
}
impl<T: Render<C>, E: Render<C>, C: Context> Render<C> for Result<T, E> {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
match self {
Ok(value) => value.render_to(buffer),
Err(err) => err.render_to(buffer),
}
}
}
macro_rules! impl_tuple {
() => {
impl<C: Context> Render<C> for () {
#[inline]
fn render_to(&self, _: &mut Buffer<C>) {}
}
};
(($i:tt $T:ident)) => {
#[cfg_attr(docsrs, doc(fake_variadic))]
#[cfg_attr(docsrs, doc = "This trait is implemented for tuples up to twelve items long.")]
impl<$T: Render<C>, C: Context> Render<C> for ($T,) {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
self.$i.render_to(buffer);
}
}
};
(($i0:tt $T0:ident) $(($i:tt $T:ident))+) => {
#[cfg_attr(docsrs, doc(hidden))]
impl<$T0: Render<C>, $($T: Render<C>),*, C: Context> Render<C> for ($T0, $($T,)*) {
#[inline]
fn render_to(&self, buffer: &mut Buffer<C>) {
self.$i0.render_to(buffer);
$(self.$i.render_to(buffer);)*
}
}
}
}
impl_tuple!();
impl_tuple!((0 T));
impl_tuple!((0 T0) (1 T1));
impl_tuple!((0 T0) (1 T1) (2 T2));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8) (9 T9));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8) (9 T9) (10 T10));
impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8) (9 T9) (10 T10) (11 T11));