use crate::renderer::{str_truthy, BOOL_ATTRS};
use dioxus_core::{TemplateAttribute, TemplateNode, VNode};
use std::{fmt::Write, ops::AddAssign};
#[derive(Debug)]
pub(crate) struct StringCache {
pub segments: Vec<Segment>,
}
#[derive(Default)]
pub struct StringChain {
add_text_to_last_segment: bool,
segments: Vec<Segment>,
}
impl StringChain {
fn if_hydration_enabled<O>(
&mut self,
during_prerender: impl FnOnce(&mut StringChain) -> O,
) -> O {
let jump_index = self.segments.len();
*self += Segment::HydrationOnlySection(0);
let out = during_prerender(self);
let after_hydration_only_section = self.segments.len();
self.add_text_to_last_segment = false;
self.segments[jump_index] = Segment::HydrationOnlySection(after_hydration_only_section);
out
}
pub fn push(&mut self, segment: Segment) {
self.add_text_to_last_segment = matches!(segment, Segment::PreRendered(_));
self.segments.push(segment);
}
}
impl AddAssign<Segment> for StringChain {
fn add_assign(&mut self, rhs: Segment) {
self.push(rhs)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum EscapeText {
Escape,
NoEscape,
ParentEscape,
}
impl EscapeText {
pub fn should_escape(&self, parent_escaped: bool) -> bool {
match self {
EscapeText::Escape => true,
EscapeText::NoEscape => false,
EscapeText::ParentEscape => parent_escaped,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum Segment {
Attr(usize),
Node {
index: usize,
escape_text: EscapeText,
},
PreRendered(String),
PreRenderedMaybeEscaped {
value: String,
renderer_if_escaped: bool,
},
HydrationOnlySection(usize),
StyleMarker {
inside_style_tag: bool,
},
InnerHtmlMarker,
AttributeNodeMarker,
RootNodeMarker,
}
impl std::fmt::Write for StringChain {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
if self.add_text_to_last_segment {
match self.segments.last_mut() {
Some(Segment::PreRendered(s2)) => s2.push_str(s),
_ => unreachable!(),
}
} else {
self.segments.push(Segment::PreRendered(s.to_string()))
}
self.add_text_to_last_segment = true;
Ok(())
}
}
impl StringCache {
pub fn from_template(template: &VNode) -> Result<Self, std::fmt::Error> {
let mut chain = StringChain::default();
let mut cur_path = vec![];
for (root_idx, root) in template.template.roots.iter().enumerate() {
from_template_recursive(
root,
&mut cur_path,
root_idx,
true,
EscapeText::ParentEscape,
&mut chain,
)?;
}
Ok(Self {
segments: chain.segments,
})
}
}
fn from_template_recursive(
root: &TemplateNode,
cur_path: &mut Vec<usize>,
root_idx: usize,
is_root: bool,
escape_text: EscapeText,
chain: &mut StringChain,
) -> Result<(), std::fmt::Error> {
match root {
TemplateNode::Element {
tag,
attrs,
children,
..
} => {
cur_path.push(root_idx);
write!(chain, "<{tag}")?;
let mut styles = Vec::new();
let mut inner_html = None;
let mut has_dyn_attrs = false;
for attr in *attrs {
match attr {
TemplateAttribute::Static {
name,
value,
namespace,
} => {
if *name == "dangerous_inner_html" {
inner_html = Some(value);
} else if let Some("style") = namespace {
styles.push((name, value));
} else if BOOL_ATTRS.contains(name) {
if str_truthy(value) {
write!(
chain,
" {name}=\"{}\"",
askama_escape::escape(value, askama_escape::Html)
)?;
}
} else {
write!(
chain,
" {name}=\"{}\"",
askama_escape::escape(value, askama_escape::Html)
)?;
}
}
TemplateAttribute::Dynamic { id: index } => {
let index = *index;
*chain += Segment::Attr(index);
has_dyn_attrs = true
}
}
}
if !styles.is_empty() {
write!(chain, " style=\"")?;
for (name, value) in styles {
write!(
chain,
"{name}:{};",
askama_escape::escape(value, askama_escape::Html)
)?;
}
*chain += Segment::StyleMarker {
inside_style_tag: true,
};
write!(chain, "\"")?;
} else if has_dyn_attrs {
*chain += Segment::StyleMarker {
inside_style_tag: false,
};
}
if has_dyn_attrs || is_root {
chain.if_hydration_enabled(|chain| {
write!(chain, " data-node-hydration=\"")?;
if has_dyn_attrs {
*chain += Segment::AttributeNodeMarker;
} else if is_root {
*chain += Segment::RootNodeMarker;
}
write!(chain, "\"")?;
std::fmt::Result::Ok(())
})?;
}
if children.is_empty() && tag_is_self_closing(tag) {
write!(chain, "/>")?;
} else {
write!(chain, ">")?;
if let Some(inner_html) = inner_html {
chain.write_str(inner_html)?;
} else if has_dyn_attrs {
*chain += Segment::InnerHtmlMarker;
}
let escape_text = match *tag {
"style" | "script" => EscapeText::NoEscape,
_ => EscapeText::Escape,
};
for child in *children {
from_template_recursive(child, cur_path, root_idx, false, escape_text, chain)?;
}
write!(chain, "</{tag}>")?;
}
cur_path.pop();
}
TemplateNode::Text { text } => {
if is_root {
chain.if_hydration_enabled(|chain| {
write!(chain, "<!--node-id")?;
*chain += Segment::RootNodeMarker;
write!(chain, "-->")?;
std::fmt::Result::Ok(())
})?;
}
match escape_text {
EscapeText::Escape => {
write!(
chain,
"{}",
askama_escape::escape(text, askama_escape::Html)
)?;
}
EscapeText::NoEscape => {
write!(chain, "{}", text)?;
}
EscapeText::ParentEscape => {
*chain += Segment::PreRenderedMaybeEscaped {
value: text.to_string(),
renderer_if_escaped: false,
};
*chain += Segment::PreRenderedMaybeEscaped {
value: askama_escape::escape(text, askama_escape::Html).to_string(),
renderer_if_escaped: true,
};
}
}
if is_root {
chain.if_hydration_enabled(|chain| write!(chain, "<!--#-->"))?;
}
}
TemplateNode::Dynamic { id: idx } => {
*chain += Segment::Node {
index: *idx,
escape_text,
}
}
}
Ok(())
}
fn tag_is_self_closing(tag: &str) -> bool {
matches!(
tag,
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
}