use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::Arc;
use gloo::utils::head;
use wasm_bindgen::UnwrapThrowExt;
use web_sys::Element;
use yew::prelude::*;
use yew::virtual_dom::AttrValue;
use super::state::{merge_helmet_states, HelmetState, HelmetTag};
use super::FormatTitle;
#[cfg(feature = "ssr")]
use super::StaticWriter;
use crate::states::artifact::use_artifacts;
#[cfg(debug_assertions)]
mod guard {
use super::*;
use std::rc::Rc;
use crate::root_state::BounceRootState;
use crate::states::slice::use_slice;
use crate::Slice;
enum HelmetBridgeGuardAction {
Increment,
Decrement,
}
#[derive(Default, PartialEq, Slice)]
struct HelmetBridgeGuard {
inner: usize,
}
impl Reducible for HelmetBridgeGuard {
type Action = HelmetBridgeGuardAction;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
match action {
Self::Action::Increment => {
debug_assert_eq!(
self.inner, 0,
"attempts to register more than 1 helmet bridge."
);
Self {
inner: self.inner + 1,
}
.into()
}
Self::Action::Decrement => Self {
inner: self.inner - 1,
}
.into(),
}
}
}
#[hook]
pub(super) fn use_helmet_guard() {
let guard = use_slice::<HelmetBridgeGuard>();
let root = use_context::<BounceRootState>().expect_throw("No bounce root found.");
use_effect_with(root, move |_| {
guard.dispatch(HelmetBridgeGuardAction::Increment);
move || {
guard.dispatch(HelmetBridgeGuardAction::Decrement);
}
});
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct HelmetBridgeProps {
#[prop_or_default]
pub default_title: Option<AttrValue>,
#[prop_or_default]
pub format_title: Option<FormatTitle>,
#[cfg(feature = "ssr")]
#[prop_or_default]
pub writer: Option<StaticWriter>,
}
impl fmt::Debug for HelmetBridgeProps {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HelmetBridgeProps")
.field("default_title", &self.default_title)
.field(
"format_title",
if self.format_title.is_some() {
&"Some(_)"
} else {
&"None"
},
)
.finish()
}
}
fn render_tags(
to_render: BTreeSet<Arc<HelmetTag>>,
mut last_rendered: Option<BTreeMap<Arc<HelmetTag>, Option<Element>>>,
) -> BTreeMap<Arc<HelmetTag>, Option<Element>> {
let mut rendered = BTreeMap::new();
let mut next_last_rendered = None;
for next_to_render in to_render.into_iter() {
'inner: loop {
next_last_rendered = next_last_rendered.or_else(|| {
last_rendered.as_mut().and_then(|last_rendered| {
last_rendered
.keys()
.next()
.cloned()
.and_then(|m| last_rendered.remove_entry(&*m))
})
});
match &mut next_last_rendered {
Some((ref key, ref mut value)) => match (**key).cmp(&next_to_render) {
Ordering::Greater => {
let el = next_to_render.apply();
rendered.insert(next_to_render, el);
break 'inner;
}
Ordering::Less => {
key.detach(value.take());
next_last_rendered = None;
}
Ordering::Equal => {
rendered.insert(next_to_render, value.take());
next_last_rendered = None;
break 'inner;
}
},
None => {
let el = next_to_render.apply();
rendered.insert(next_to_render, el);
break 'inner;
}
}
}
}
if let Some((key, value)) = next_last_rendered {
key.detach(value);
}
if let Some(last_rendered) = last_rendered {
for (key, value) in last_rendered.into_iter() {
key.detach(value);
}
}
rendered
}
#[function_component(HelmetBridge)]
pub fn helmet_bridge(props: &HelmetBridgeProps) -> Html {
#[cfg(debug_assertions)]
{
guard::use_helmet_guard();
}
let helmet_states = use_artifacts::<HelmetState>();
let rendered = use_mut_ref(|| -> Option<BTreeMap<Arc<HelmetTag>, Option<Element>>> { None });
#[cfg(feature = "ssr")]
{
use super::ssr::StaticWriterState;
use crate::use_atom_setter;
let writer = props.writer.clone();
let format_title = props.format_title.clone();
let default_title = props.default_title.clone();
let set_static_writer_state = use_atom_setter::<StaticWriterState>();
use_state(move || {
set_static_writer_state(StaticWriterState {
format_title,
default_title,
writer,
})
});
}
use_effect_with((), |_| {
let pre_rendered = head()
.query_selector_all("[data-bounce-helmet=pre-render]")
.expect_throw("failed to read pre rendered tags");
for i in 0..pre_rendered.length() {
if let Some(m) = pre_rendered.get(i) {
if let Some(parent) = m.parent_node() {
let _ = parent.remove_child(&m);
}
}
}
});
use_effect_with(
(
helmet_states,
props.format_title.clone(),
props.default_title.clone(),
),
move |(helmet_states, format_title, default_title)| {
let to_render =
merge_helmet_states(helmet_states, format_title.as_ref(), default_title.clone());
let mut rendered = rendered.borrow_mut();
*rendered = Some(render_tags(to_render, rendered.take()));
|| {}
},
);
Html::default()
}