use std::fmt::Display;
pub mod attr;
pub mod props;
pub mod renderers;
pub use attr::{Attr, AttrSpread, ClassItem, ClassList};
pub use renderers::{HtmlRenderer, Whitespace};
pub use damask_macros::Component;
pub trait Renderer {
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: &'a dyn Render,
}
impl<'a> Slot<'a> {
pub const fn new(name: &'a str, content: &'a dyn Render) -> Self {
Slot { name, content }
}
}
#[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<&'a dyn Render> {
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<&'a dyn Render> {
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 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 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)
}
#[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 mod prelude {
pub use crate::attr::{Attr, AttrSpread, ClassItem, ClassList};
pub use crate::renderers::{HtmlRenderer, StringRenderer, Whitespace};
pub use crate::{Component, DEFAULT_SLOT, Render, Renderer, Slot, Slots, fragment};
}
#[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());
}
}