use dioxus_core::{
Attribute, DynamicNode, Element, IntoDynNode, Template, TemplateAttribute, TemplateNode, VNode,
};
use serde::Serialize;
use crate::Html;
static DIV: Template = Template {
roots: &[TemplateNode::Element {
tag: "div",
namespace: None,
attrs: &[TemplateAttribute::Dynamic { id: 0 }],
children: &[],
}],
node_paths: &[],
attr_paths: &[&[0]],
};
static STATE_SCRIPT: Template = Template {
roots: &[TemplateNode::Element {
tag: "script",
namespace: None,
attrs: &[
TemplateAttribute::Static {
name: "type",
value: "application/json",
namespace: None,
},
TemplateAttribute::Dynamic { id: 0 },
],
children: &[],
}],
node_paths: &[],
attr_paths: &[&[0]],
};
const INNER_HTML: &str = "dangerous_inner_html";
fn element(template: Template, attrs: Vec<Attribute>) -> Element {
Ok(VNode::new(
None,
template,
Box::new([]),
Box::new([attrs.into_boxed_slice()]),
))
}
impl<P: Serialize, F: Fn(&mut String, &P)> Html<'_, P, F> {
pub fn to_element(&self) -> Element {
let mut markup = String::new();
(self.render)(&mut markup, self.props.get());
let mut attrs = Vec::with_capacity(4);
if let Some(name) = self.island {
attrs.push(Attribute::new("data-island", name, None, false));
let json = crate::json::to_string(self.props.get());
attrs.push(Attribute::new("data-props", json, None, false));
if let Some(hydrate) = &self.hydrate {
let value = hydrate.attribute().into_owned();
attrs.push(Attribute::new("data-hydrate", value, None, false));
}
}
attrs.push(Attribute::new(INNER_HTML, markup, None, false));
element(DIV, attrs)
}
}
impl<P: Serialize, F: Fn(&mut String, &P)> IntoDynNode for Html<'_, P, F> {
fn into_dyn_node(self) -> DynamicNode {
self.to_element().into_dyn_node()
}
}
pub fn state_script(id: &str, state: &impl Serialize) -> Element {
let json = crate::json::to_string(state);
let mut content = String::with_capacity(json.len());
crate::state::json_escaped_into(&mut content, &json);
element(
STATE_SCRIPT,
vec![
Attribute::new("id", id, None, false),
Attribute::new(INNER_HTML, content, None, false),
],
)
}
#[cfg(test)]
mod tests {
use super::*;
use dioxus_ssr::Renderer;
#[derive(Serialize)]
struct Label<'a> {
label: &'a str,
}
fn label(out: &mut String, props: &Label<'_>) {
out.push_str("<b>");
crate::escape_into(out, props.label);
out.push_str("</b><!--[--><i>x</i><!--]-->");
}
static PAGE: Template = Template {
roots: &[TemplateNode::Element {
tag: "main",
namespace: None,
attrs: &[],
children: &[
TemplateNode::Element {
tag: "h1",
namespace: None,
attrs: &[],
children: &[TemplateNode::Text { text: "Page" }],
},
TemplateNode::Dynamic { id: 0 },
],
}],
node_paths: &[&[0, 1]],
attr_paths: &[],
};
fn page(child: impl IntoDynNode) -> Element {
Ok(VNode::new(
None,
PAGE,
Box::new([child.into_dyn_node()]),
Box::new([]),
))
}
fn rendered(element: impl Fn() -> Element) -> [String; 2] {
let mut pre_render = Renderer::new();
pre_render.pre_render = true;
[
Renderer::new().render_element(element()),
pre_render.render_element(element()),
]
}
#[test]
fn an_island_in_a_page_holds_exactly_the_components_markup() {
let props = Label {
label: "\"Tom\" & 'Jerry' <3",
};
let mut markup = String::new();
label(&mut markup, &props);
let [plain, hydratable] = rendered(|| page(Html::island("Label", &props, label)));
let json = r#"{"label":"\"Tom\" & 'Jerry' <3"}"#;
let attrs = format!(r#"data-island="Label" data-props="{json}""#);
assert_eq!(
plain,
format!("<main><h1>Page</h1><div {attrs}>{markup}</div></main>")
);
assert_eq!(
hydratable,
format!(
r#"<main data-node-hydration="0"><h1>Page</h1><div {attrs} data-node-hydration="1">{markup}</div></main>"#
)
);
}
#[test]
fn an_island_carries_when_it_hydrates_after_its_props() {
let props = Label { label: "x" };
let when = crate::Hydrate::media(r#"(min-width: 1px)" onclick="x"#);
let [plain, _] =
rendered(|| page(Html::island("Label", &props, label).hydrate(when.clone())));
assert!(
plain.starts_with(
r#"<main><h1>Page</h1><div data-island="Label" data-props="{"label":"x"}" data-hydrate="media:(min-width: 1px)" onclick="x"><b>x</b>"#
),
"{plain}"
);
let [plain, _] = rendered(|| page(Html::markup(&props, label).hydrate(when.clone())));
assert!(!plain.contains("data-hydrate"), "{plain}");
}
#[test]
fn markup_is_written_into_a_div_or_the_pages_own_element() {
let props = Label { label: "<x>" };
let [plain, _] = rendered(|| page(Html::markup(&props, label)));
assert_eq!(
plain,
"<main><h1>Page</h1><div><b><x></b><!--[--><i>x</i><!--]--></div></main>"
);
}
#[test]
fn the_state_script_is_state_script_into() {
let state = serde_json::json!({ "a": "<&>\u{2028}\u{2029}", "b": [1, 2] });
let mut want = String::new();
crate::state_script_into(&mut want, "__pinia", &state);
let [plain, hydratable] = rendered(|| state_script("__pinia", &state));
assert_eq!(plain, want);
assert_eq!(
hydratable,
want.replacen(
r#"id="__pinia""#,
r#"id="__pinia" data-node-hydration="0""#,
1
)
);
}
}