#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(all(doc, not(docsrs)), doc = include_str!("../../README.md"))]
#[cfg(all(hydrate, not(feature = "client")))]
compile_error!("`hydrate` cfg must be specified only when `client` feature is enabled");
#[cfg(all(hydrate, not(target_arch = "wasm32")))]
compile_error!("`hydrate` cfg must be specified only when compiling to Wasm");
extern crate self as uibeam;
#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
#[cfg(feature = "client")]
pub mod client;
#[cfg(feature = "__integration__")]
mod integration;
#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
#[cfg(feature = "client")]
pub use client::Signal;
pub use uibeam_html::escape;
pub use uibeam_macros::UI;
#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
#[cfg(feature = "client")]
pub use uibeam_macros::client;
use std::borrow::Cow;
#[doc(hidden)]
#[allow(non_camel_case_types)]
pub trait client_attribute<T> {
fn new(value: T) -> Self;
}
pub struct UI(
#[cfg(not(all(feature = "client", hydrate)))] Cow<'static, str>,
#[cfg(all(feature = "client", hydrate))] client::VNode,
);
pub trait Beam<Kind: bound::BeamKind = Server> {
fn render(self) -> UI;
}
#[doc(hidden)]
#[cfg(feature = "client")]
pub use bound::{Client, IslandBoundary, render_in_island};
#[doc(hidden)]
pub use bound::{Server, render_on_server};
#[doc(hidden)]
mod bound {
use crate::Beam;
pub trait BeamKind {}
pub struct Server;
#[cfg(feature = "client")]
pub struct Client;
impl BeamKind for Server {}
#[cfg(feature = "client")]
impl BeamKind for Client {}
#[cfg(feature = "client")]
pub trait IslandBoundary:
Beam<Client> + serde::Serialize + for<'de> serde::Deserialize<'de>
{
}
pub struct ServerOrIslandBoundary<K: BeamKind>(std::marker::PhantomData<K>);
#[cfg(feature = "client")]
pub struct Anywhere<K: BeamKind>(std::marker::PhantomData<K>);
impl<K: BeamKind> BeamKind for ServerOrIslandBoundary<K> {}
#[cfg(feature = "client")]
impl<K: BeamKind> BeamKind for Anywhere<K> {}
impl<T> Beam<ServerOrIslandBoundary<Server>> for T
where
T: Beam<Server>,
{
#[inline(always)]
fn render(self) -> super::UI {
Beam::<Server>::render(self)
}
}
#[cfg(feature = "client")]
impl<T> Beam<ServerOrIslandBoundary<Client>> for T
where
T: IslandBoundary,
{
#[inline(always)]
fn render(self) -> super::UI {
Beam::<Client>::render(self)
}
}
#[cfg(feature = "client")]
impl<T> Beam<Anywhere<Server>> for T
where
T: Beam<Server>,
{
#[inline(always)]
fn render(self) -> super::UI {
Beam::<Server>::render(self)
}
}
#[cfg(feature = "client")]
impl<T> Beam<Anywhere<Client>> for T
where
T: Beam<Client>,
{
#[inline(always)]
fn render(self) -> super::UI {
Beam::<Client>::render(self)
}
}
#[doc(hidden)]
pub fn render_on_server<K: BeamKind>(beam: impl Beam<ServerOrIslandBoundary<K>>) -> super::UI {
Beam::<ServerOrIslandBoundary<K>>::render(beam)
}
#[cfg(feature = "client")]
#[doc(hidden)]
pub fn render_in_island<K: BeamKind>(beam: impl Beam<Anywhere<K>>) -> super::UI {
Beam::<Anywhere<K>>::render(beam)
}
}
#[cfg(not(all(feature = "client", hydrate)))]
#[inline(always)]
pub fn shoot(ui: UI) -> Cow<'static, str> {
ui.0
}
impl FromIterator<UI> for UI {
#[cfg(not(all(feature = "client", hydrate)))]
#[inline]
fn from_iter<T: IntoIterator<Item = UI>>(iter: T) -> Self {
let mut result = String::new();
for item in iter {
result.push_str(&item.0);
}
UI(Cow::Owned(result))
}
#[cfg(all(feature = "client", hydrate))]
fn from_iter<T: IntoIterator<Item = UI>>(iter: T) -> Self {
UI(client::VNode::fragment(
iter.into_iter().map(|UI(vdom)| vdom).collect::<Vec<_>>(),
))
}
}
#[cfg(not(all(feature = "client", hydrate)))]
impl UI {
pub const EMPTY: UI = UI(Cow::Borrowed(""));
#[inline(always)]
pub fn concat<const N: usize>(uis: [UI; N]) -> Self {
match uis.len() {
0 => UI::EMPTY,
1 => unsafe {
std::ptr::read(
&*std::mem::ManuallyDrop::new(uis) as *const [UI] as *const [UI; 1]
as *const UI,
)
},
_ => {
let mut buf = String::with_capacity(uis.iter().map(|ui| ui.0.len()).sum());
for ui in uis {
buf.push_str(&ui.0);
}
UI(Cow::Owned(buf))
}
}
}
}
#[doc(hidden)]
pub enum Interpolator {
Attribute(AttributeValue),
Children(UI),
}
#[doc(hidden)]
pub enum AttributeValue {
Text(Cow<'static, str>),
Integer(i64),
Boolean(bool),
}
#[cfg(all(feature = "client", hydrate))]
impl From<AttributeValue> for wasm_bindgen::JsValue {
fn from(value: AttributeValue) -> wasm_bindgen::JsValue {
match value {
AttributeValue::Integer(int) => int.into(),
AttributeValue::Boolean(boo) => boo.into(),
AttributeValue::Text(text) => match uibeam_html::escape(&text) {
Cow::Owned(escaped) => escaped.into(),
Cow::Borrowed(_) => match text {
Cow::Owned(s) => s.into(),
Cow::Borrowed(s) => s.into(),
},
},
}
}
}
const _: () = {
impl From<bool> for AttributeValue {
#[inline(always)]
fn from(value: bool) -> Self {
AttributeValue::Boolean(value)
}
}
impl From<&'static str> for AttributeValue {
fn from(value: &'static str) -> Self {
AttributeValue::Text(value.into())
}
}
impl From<String> for AttributeValue {
#[inline(always)]
fn from(value: String) -> Self {
AttributeValue::Text(value.into())
}
}
impl From<Cow<'static, str>> for AttributeValue {
fn from(value: Cow<'static, str>) -> Self {
AttributeValue::Text(value)
}
}
impl From<i8> for AttributeValue {
fn from(it: i8) -> Self {
AttributeValue::Integer(it.into())
}
}
impl From<i16> for AttributeValue {
fn from(it: i16) -> Self {
AttributeValue::Integer(it.into())
}
}
impl From<i32> for AttributeValue {
#[inline(always)]
fn from(it: i32) -> Self {
AttributeValue::Integer(it.into())
}
}
impl From<i64> for AttributeValue {
fn from(it: i64) -> Self {
AttributeValue::Integer(it)
}
}
impl From<isize> for AttributeValue {
fn from(it: isize) -> Self {
AttributeValue::Integer(
it.try_into()
.unwrap_or_else(|_| panic!("{}", &too_large_error_message(it))),
)
}
}
impl From<u8> for AttributeValue {
fn from(it: u8) -> Self {
AttributeValue::Integer(it.into())
}
}
impl From<u16> for AttributeValue {
fn from(it: u16) -> Self {
AttributeValue::Integer(it.into())
}
}
impl From<u32> for AttributeValue {
#[inline(always)]
fn from(it: u32) -> Self {
AttributeValue::Integer(it.into())
}
}
impl From<u64> for AttributeValue {
fn from(it: u64) -> Self {
AttributeValue::Integer(
it.try_into()
.unwrap_or_else(|_| panic!("{}", &too_large_error_message(it))),
)
}
}
impl From<usize> for AttributeValue {
fn from(it: usize) -> Self {
AttributeValue::Integer(
it.try_into()
.unwrap_or_else(|_| panic!("{}", &too_large_error_message(it))),
)
}
}
#[cold]
#[inline(never)]
fn too_large_error_message(int: impl std::fmt::Display) -> String {
format!("can't use `{int}` as attribute value: too largem")
}
};
#[doc(hidden)]
pub trait IntoChildren<T, const ESCAPE: bool = true> {
fn into_children(self) -> UI;
}
const _: () = {
impl<const ESCAPE: bool> IntoChildren<UI, ESCAPE> for UI {
fn into_children(self) -> UI {
self
}
}
impl<const ESCAPE: bool, I> IntoChildren<(I,), ESCAPE> for I
where
I: IntoIterator<Item = UI>,
{
#[inline(always)]
fn into_children(self) -> UI {
UI::from_iter(self)
}
}
impl<const ESCAPE: bool, D: std::fmt::Display> IntoChildren<&dyn std::fmt::Display, ESCAPE> for D {
fn into_children(self) -> UI {
let text = self.to_string();
let text = if ESCAPE {
match escape(&text) {
Cow::Borrowed(_) => text,
Cow::Owned(escaped) => escaped,
}
} else {
text
};
#[cfg(not(all(feature = "client", hydrate)))]
return UI(Cow::Owned(text));
#[cfg(all(feature = "client", hydrate))]
return UI(client::VNode::text(text));
}
}
};
#[doc(hidden)]
impl UI {
#[cfg(all(feature = "client", hydrate))]
pub fn new_unchecked(vdom: client::VNode) -> Self {
Self(vdom)
}
#[cfg(all(feature = "client", hydrate))]
pub fn into_vdom(self) -> client::VNode {
self.0
}
#[cfg(not(all(feature = "client", hydrate)))]
pub unsafe fn new_unchecked<const N: usize>(
template_pieces: &'static [&'static str],
interpolators: [Interpolator; N],
) -> Self {
#[cfg(debug_assertions)]
{
let len = template_pieces.len();
assert!(
(len == 0 && N == 0) || len == N + 1,
"invalid template_pieces.len(): {len} where N = {N}: template_pieces must have 0 = N or exactly N + 1 pieces"
);
}
match template_pieces.len() {
0 => UI::EMPTY,
1 => UI(Cow::Borrowed(template_pieces[0])),
_ => {
let mut buf = String::with_capacity({
let mut size = 0;
for piece in template_pieces {
size += piece.len();
}
for expression in &interpolators {
size += match expression {
Interpolator::Children(children) => children.0.len(),
Interpolator::Attribute(value) => match value {
AttributeValue::Text(text) => {
1 + text.len() + 1
}
AttributeValue::Integer(_) => {
1 + 4 + 1
}
AttributeValue::Boolean(_) => {
0
}
},
}
}
size
});
for i in 0..N {
buf.push_str(template_pieces[i]);
match &interpolators[i] {
Interpolator::Children(children) => {
buf.push_str(&children.0);
}
Interpolator::Attribute(value) => {
#[cfg(debug_assertions)]
{
assert!(buf.ends_with('='));
}
match value {
AttributeValue::Text(text) => {
buf.push('"');
buf.push_str(&escape(text));
buf.push('"');
}
AttributeValue::Integer(int) => {
buf.push('"');
buf.push_str(&int.to_string());
buf.push('"');
}
AttributeValue::Boolean(boolean) => {
let Some('=') = buf.pop() else { unreachable!() };
if !*boolean {
let Some(sp) = buf.rfind(|c| {
matches!(c, ' ' | '\t' | '\n' | '\x0C' | '\r')
}) else {
unreachable!()
};
buf.truncate(sp);
}
}
}
}
}
}
buf.push_str(template_pieces[N]);
UI(Cow::Owned(buf))
}
}
}
}
#[cfg(not(feature = "client"))]
#[cfg(test)]
mod test {
use super::*;
fn __assert_impls__() {
fn is_children<X, C: IntoChildren<X>>(_: C) {}
fn dummy_ui() -> UI {
todo!()
}
is_children(dummy_ui());
is_children(Some(dummy_ui()));
is_children(None::<UI>);
is_children((1..=3).map(|_| dummy_ui()));
}
#[test]
fn test_ui_new_unchecked() {
assert_eq!((unsafe { UI::new_unchecked(&[], []) }).0, r##""##);
assert_eq!(
(unsafe { UI::new_unchecked(&[r##"<div>"##,], []) }).0,
r##"<div>"##
);
assert_eq!(
(unsafe {
UI::new_unchecked(
&[r##"<div class="##, r##"></div>"##],
[Interpolator::Attribute(AttributeValue::from("foo"))],
)
})
.0,
r##"<div class="foo"></div>"##
);
assert_eq!(
(unsafe {
UI::new_unchecked(
&[r##"<article class="##, r##">"##, r##"</article>"##],
[
Interpolator::Attribute(AttributeValue::from("main-article")),
Interpolator::Children(IntoChildren::<_, true>::into_children(
(1..=3_usize).map(|i| {
UI::new_unchecked(
&[r##"<p>i="##, r##"</p>"##],
[Interpolator::Children(
IntoChildren::<_, true>::into_children(i.to_string()),
)],
)
}),
)),
],
)
})
.0,
r##"<article class="main-article"><p>i=1</p><p>i=2</p><p>i=3</p></article>"##
);
}
#[test]
fn test_ui_interploate_expression() {
let ui = UI! {
{"an expression"}
};
assert_eq!(shoot(ui), r##"an expression"##);
let ui = UI! {
<p>"a text node"</p>
};
assert_eq!(shoot(ui), r##"<p>a text node</p>"##);
let ui = UI! {
<p>{"an expression"}</p>
};
assert_eq!(shoot(ui), r##"<p>an expression</p>"##);
let ui = UI! {
<div class="foo">
<p>"hello"</p>
</div>
};
let ui = UI! {
<div class="bar">
{ui}
</div>
};
assert_eq!(
shoot(ui),
r##"<div class="bar"><div class="foo"><p>hello</p></div></div>"##
);
struct Layout {
children: UI,
}
impl Beam for Layout {
fn render(self) -> UI {
UI! {
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
{self.children}
</body>
</html>
}
}
}
assert_eq!(
shoot(UI! { <Layout></Layout> }),
r##"<!DOCTYPE html><html><head><meta charset="UTF-8"/></head><body></body></html>"##
);
assert_eq!(
shoot(UI! { <Layout><h1>"Hello, Beam!"</h1></Layout> }),
r##"<!DOCTYPE html><html><head><meta charset="UTF-8"/></head><body><h1>Hello, Beam!</h1></body></html>"##
);
let content = UI! {
<h1>"Hello, Beam!"</h1>
};
assert_eq!(
shoot(UI! { <Layout>{content}"[test]"</Layout> }),
r##"<!DOCTYPE html><html><head><meta charset="UTF-8"/></head><body><h1>Hello, Beam!</h1>[test]</body></html>"##
);
}
}