use std::fmt::Display;
use std::future::Future;
use std::pin::Pin;
pub mod attr;
pub mod props;
pub mod renderers;
pub mod trusted;
pub use attr::{
Attr, AttrSpread, ClassItem, ClassList, DataItem, DataSet, DataValue, is_attr_name_safe,
};
pub use renderers::{HtmlRenderer, Whitespace};
pub use trusted::{Content, Sink, ToTrusted, Trusted, Value, splice};
pub use damask_macros::Component;
#[doc(hidden)]
pub use damask_macros::__tag;
#[macro_export]
macro_rules! tag {
($($tt:tt)*) => {
$crate::__tag!(($crate) $($tt)*)
};
}
pub trait Renderer: Send {
fn write_raw(&mut self, s: &str);
fn write_text(&mut self, s: &str) {
self.write_raw(s);
}
fn write_escaped(&mut self, value: &dyn Display);
fn write_display_raw(&mut self, value: &dyn Display) {
self.write_raw(&value.to_string());
}
fn push_indent(&mut self, levels: usize) {
let _ = levels;
}
fn pop_indent(&mut self, levels: usize) {
let _ = levels;
}
fn close_line(&mut self, depth: usize) {
let _ = depth;
}
fn set_verbatim(&mut self, on: bool) {
let _ = on;
}
fn finish(self: Box<Self>) -> String;
}
pub const DEFAULT_SLOT: &str = "";
pub struct Slot<'a> {
name: &'a str,
content: Fill<'a>,
}
impl<'a> Slot<'a> {
pub const fn new(name: &'a str, content: &'a (dyn Render + Sync)) -> Self {
Slot {
name,
content: Fill::Ready(content),
}
}
pub const fn new_async(name: &'a str, content: &'a (dyn AsyncRender + Sync)) -> Self {
Slot {
name,
content: Fill::Awaiting(content),
}
}
}
#[derive(Clone, Copy)]
pub enum Fill<'a> {
Ready(&'a (dyn Render + Sync)),
Awaiting(&'a (dyn AsyncRender + Sync)),
}
impl<'a> Fill<'a> {
pub fn render_async<'r>(self, r: &'r mut dyn Renderer) -> RenderFuture<'r>
where
'a: 'r,
{
match self {
Fill::Ready(content) => {
content.render_into(r);
Box::pin(async {})
}
Fill::Awaiting(content) => content.render_into_async(r),
}
}
}
impl Render for Fill<'_> {
fn render_into(&self, r: &mut dyn Renderer) {
match self {
Fill::Ready(content) => content.render_into(r),
Fill::Awaiting(_) => panic!(
"this slot was filled with markup that `.await`s, and it is being rendered \
synchronously. Render it with `<slot/>` from a template that awaits, or with \
`Slots::render_async`."
),
}
}
}
#[derive(Clone, Copy, Default)]
pub struct Slots<'a> {
entries: &'a [Slot<'a>],
}
impl<'a> Slots<'a> {
pub const EMPTY: Slots<'static> = Slots { entries: &[] };
pub const fn new(entries: &'a [Slot<'a>]) -> Self {
Slots { entries }
}
pub fn get(&self, name: &str) -> Option<Fill<'a>> {
self.entries
.iter()
.find(|s| s.name == name)
.map(|s| s.content)
}
pub fn has(&self, name: &str) -> bool {
self.get(name).is_some()
}
pub fn get_default(&self) -> Option<Fill<'a>> {
self.get(DEFAULT_SLOT)
}
pub fn has_default(&self) -> bool {
self.has(DEFAULT_SLOT)
}
pub fn render(
&self,
name: &str,
r: &mut dyn Renderer,
indent: usize,
fallback: impl FnOnce(&mut dyn Renderer),
) {
match self.get(name) {
Some(content) => {
r.push_indent(indent);
content.render_into(r);
r.pop_indent(indent);
}
None => fallback(r),
}
}
pub fn render_async<'r>(
&self,
name: &str,
r: &'r mut dyn Renderer,
indent: usize,
fallback: impl FnOnce(&'r mut dyn Renderer) -> RenderFuture<'r>,
) -> RenderFuture<'r>
where
'a: 'r,
{
match self.get(name) {
Some(content) => Box::pin(async move {
r.push_indent(indent);
content.render_async(r).await;
r.pop_indent(indent);
}),
None => fallback(r),
}
}
}
pub trait Render {
fn render_into(&self, r: &mut dyn Renderer);
fn render_slots(&self, r: &mut dyn Renderer, _slots: Slots<'_>) {
self.render_into(r);
}
}
impl<T: Render + ?Sized> Render for Box<T> {
fn render_into(&self, r: &mut dyn Renderer) {
(**self).render_into(r);
}
fn render_slots(&self, r: &mut dyn Renderer, slots: Slots<'_>) {
(**self).render_slots(r, slots);
}
}
impl<T: Render + ?Sized> Render for &T {
fn render_into(&self, r: &mut dyn Renderer) {
(**self).render_into(r);
}
fn render_slots(&self, r: &mut dyn Renderer, slots: Slots<'_>) {
(**self).render_slots(r, slots);
}
}
impl<T: Render> Render for Option<T> {
fn render_into(&self, r: &mut dyn Renderer) {
if let Some(content) = self {
content.render_into(r);
}
}
fn render_slots(&self, r: &mut dyn Renderer, slots: Slots<'_>) {
if let Some(content) = self {
content.render_slots(r, slots);
}
}
}
pub type RenderFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
pub trait AsyncRender: Sync {
fn render_into_async<'life0, 'life1, 'async_trait>(
&'life0 self,
r: &'life1 mut dyn Renderer,
) -> RenderFuture<'async_trait>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait;
fn render_slots_async<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
r: &'life1 mut dyn Renderer,
_slots: Slots<'life2>,
) -> RenderFuture<'async_trait>
where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait,
{
self.render_into_async(r)
}
}
impl<T: Render + Sync + ?Sized> AsyncRender for T {
fn render_into_async<'life0, 'life1, 'async_trait>(
&'life0 self,
r: &'life1 mut dyn Renderer,
) -> RenderFuture<'async_trait>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
{
Render::render_into(self, r);
Box::pin(async {})
}
fn render_slots_async<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
r: &'life1 mut dyn Renderer,
slots: Slots<'life2>,
) -> RenderFuture<'async_trait>
where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait,
{
Render::render_slots(self, r, slots);
Box::pin(async {})
}
}
pub struct Fragment<F>(pub F);
impl<F: Fn(&mut dyn Renderer)> Render for Fragment<F> {
fn render_into(&self, r: &mut dyn Renderer) {
(self.0)(r);
}
}
pub fn fragment<F: Fn(&mut dyn Renderer)>(f: F) -> Fragment<F> {
Fragment(f)
}
pub struct AsyncFragment<F>(pub F);
impl<F> AsyncRender for AsyncFragment<F>
where
F: for<'r> Fn(&'r mut dyn Renderer) -> RenderFuture<'r> + Sync,
{
fn render_into_async<'life0, 'life1, 'async_trait>(
&'life0 self,
r: &'life1 mut dyn Renderer,
) -> RenderFuture<'async_trait>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
{
(self.0)(r)
}
}
pub fn fragment_async<F>(f: F) -> AsyncFragment<F>
where
F: for<'r> Fn(&'r mut dyn Renderer) -> RenderFuture<'r>,
{
AsyncFragment(f)
}
#[inline]
pub fn as_display<'a, T: Display + 'a>(value: &'a T) -> &'a dyn Display {
value
}
pub trait Component: Render {
fn default_renderer(&self) -> Box<dyn Renderer>;
fn render(&self) -> String {
self.render_with(Slots::EMPTY)
}
fn render_with(&self, slots: Slots<'_>) -> String {
let mut r = self.default_renderer();
self.render_slots(r.as_mut(), slots);
r.finish()
}
}
pub trait AsyncComponent: AsyncRender {
fn default_renderer(&self) -> Box<dyn Renderer>;
fn render_async<'life0, 'async_trait>(
&'life0 self,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
Self: 'async_trait,
{
self.render_with_async(Slots::EMPTY)
}
fn render_with_async<'life0, 'life1, 'async_trait>(
&'life0 self,
slots: Slots<'life1>,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
{
Box::pin(async move {
let mut r = self.default_renderer();
self.render_slots_async(r.as_mut(), slots).await;
r.finish()
})
}
}
impl<T: Component + Sync + ?Sized> AsyncComponent for T {
fn default_renderer(&self) -> Box<dyn Renderer> {
Component::default_renderer(self)
}
}
pub mod prelude {
pub use crate::attr::{Attr, AttrSpread, ClassItem, ClassList, DataItem, DataSet, DataValue};
pub use crate::renderers::{HtmlRenderer, StringRenderer, Whitespace};
pub use crate::{
AsyncComponent, AsyncRender, Component, DEFAULT_SLOT, Render, RenderFuture, Renderer, Slot,
Slots, fragment, fragment_async,
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::renderers::{HtmlRenderer, StringRenderer, escape_html};
struct Greeting {
name: String,
}
impl Render for Greeting {
fn render_into(&self, r: &mut dyn Renderer) {
r.write_raw("Hello ");
r.write_escaped(&self.name);
r.write_raw("!");
}
}
impl Component for Greeting {
fn default_renderer(&self) -> Box<dyn Renderer> {
Box::new(HtmlRenderer::new())
}
}
#[test]
fn render_uses_default_renderer_and_escapes() {
let g = Greeting {
name: "<Ada>".into(),
};
assert_eq!(g.render(), "Hello <Ada>!");
}
#[test]
fn render_into_accepts_any_renderer() {
let g = Greeting { name: "Bob".into() };
let mut custom: Box<dyn Renderer> = Box::new(StringRenderer::with_escape(escape_html));
g.render_into(custom.as_mut());
assert_eq!(custom.finish(), "Hello Bob!");
}
#[test]
fn components_are_object_safe() {
let items: Vec<Box<dyn Component>> = vec![
Box::new(Greeting { name: "a".into() }),
Box::new(Greeting { name: "b".into() }),
];
let out: String = items.iter().map(|c| c.render()).collect();
assert_eq!(out, "Hello a!Hello b!");
}
fn rendered(content: impl Render) -> String {
let mut r: Box<dyn Renderer> = Box::new(HtmlRenderer::new());
content.render_into(r.as_mut());
r.finish()
}
#[test]
fn a_slot_lookup_is_renderable_either_way() {
let g = Greeting { name: "Ada".into() };
let entries = [Slot::new("body", &g)];
let slots = Slots::new(&entries);
assert_eq!(rendered(slots.get("body")), "Hello Ada!");
assert_eq!(rendered(slots.get("absent")), "");
}
#[test]
fn slots_report_what_the_caller_filled() {
let g = Greeting { name: "Ada".into() };
let entries = [Slot::new(DEFAULT_SLOT, &g)];
let slots = Slots::new(&entries);
assert!(slots.has_default() && slots.has(DEFAULT_SLOT));
assert!(!slots.has("body"));
assert_eq!(rendered(slots.get_default()), "Hello Ada!");
assert!(!Slots::EMPTY.has_default());
}
fn block_on<F: Future>(f: F) -> F::Output {
let mut f = std::pin::pin!(f);
let waker = std::task::Waker::noop();
let mut cx = std::task::Context::from_waker(waker);
loop {
if let std::task::Poll::Ready(v) = f.as_mut().poll(&mut cx) {
return v;
}
}
}
struct AsyncGreeting {
name: String,
}
impl AsyncRender for AsyncGreeting {
fn render_into_async<'life0, 'life1, 'async_trait>(
&'life0 self,
r: &'life1 mut dyn Renderer,
) -> RenderFuture<'async_trait>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
{
Box::pin(async move {
let name = std::future::ready(self.name.clone()).await;
r.write_raw("Hello ");
r.write_escaped(&name);
r.write_raw("!");
})
}
}
impl AsyncComponent for AsyncGreeting {
fn default_renderer(&self) -> Box<dyn Renderer> {
Box::new(HtmlRenderer::new())
}
}
#[test]
fn a_genuinely_async_component_renders_through_render_async() {
let g = AsyncGreeting {
name: "<Ada>".into(),
};
assert_eq!(block_on(g.render_async()), "Hello <Ada>!");
}
#[test]
fn a_sync_component_renders_through_the_async_path_for_free() {
let g = Greeting { name: "Bob".into() };
let mut r: Box<dyn Renderer> = Box::new(HtmlRenderer::new());
block_on(AsyncRender::render_into_async(&g, r.as_mut()));
assert_eq!(r.finish(), "Hello Bob!");
assert_eq!(block_on(g.render_async()), "Hello Bob!");
}
#[test]
fn slots_render_async_fills_and_falls_back() {
let g = Greeting { name: "Ada".into() };
let entries = [Slot::new("body", &g)];
let slots = Slots::new(&entries);
let filled = block_on(async {
let mut r: Box<dyn Renderer> = Box::new(HtmlRenderer::new());
slots
.render_async("body", r.as_mut(), 0, |r| {
Box::pin(async { r.write_raw("fallback") })
})
.await;
r.finish()
});
assert_eq!(filled, "Hello Ada!");
let fallback = block_on(async {
let mut r: Box<dyn Renderer> = Box::new(HtmlRenderer::new());
slots
.render_async("absent", r.as_mut(), 0, |r| {
Box::pin(async { r.write_raw("fallback") })
})
.await;
r.finish()
});
assert_eq!(fallback, "fallback");
}
#[test]
fn a_slot_may_be_filled_with_content_that_awaits() {
let slow = AsyncGreeting { name: "Ada".into() };
let entries = [Slot::new_async(DEFAULT_SLOT, &slow)];
let slots = Slots::new(&entries);
let out = block_on(async {
let mut r: Box<dyn Renderer> = Box::new(HtmlRenderer::new());
slots
.render_async(DEFAULT_SLOT, r.as_mut(), 0, |r| {
Box::pin(async { r.write_raw("fallback") })
})
.await;
r.finish()
});
assert_eq!(out, "Hello Ada!");
}
#[test]
fn an_awaiting_fill_counts_as_filled() {
let slow = AsyncGreeting { name: "Ada".into() };
let entries = [Slot::new_async(DEFAULT_SLOT, &slow)];
let slots = Slots::new(&entries);
assert!(slots.has_default());
assert!(slots.get_default().is_some());
}
#[test]
#[should_panic(expected = "Slots::render_async")]
fn rendering_an_awaiting_fill_synchronously_says_so() {
let slow = AsyncGreeting { name: "Ada".into() };
let entries = [Slot::new_async(DEFAULT_SLOT, &slow)];
let slots = Slots::new(&entries);
let _ = rendered(slots.get_default());
}
}