use mf2_runtime::TimeZone;
#[cfg(all(feature = "hydrate", feature = "fn-datetime"))]
use alloc::vec::Vec;
#[cfg(any(feature = "ssr", feature = "fn-datetime"))]
std::thread_local! {
static ZONE: core::cell::Cell<Option<TimeZone>> = const { core::cell::Cell::new(None) };
}
#[cfg(any(feature = "ssr", feature = "fn-datetime"))]
pub(crate) fn current() -> Option<TimeZone> {
ZONE.with(core::cell::Cell::get)
}
#[cfg(all(any(feature = "hydrate", feature = "csr"), feature = "fn-datetime"))]
fn set(zone: Option<TimeZone>) {
ZONE.with(|z| z.set(zone));
}
#[cfg(any(feature = "ssr", all(feature = "hydrate", feature = "fn-datetime")))]
pub(crate) fn scoped<R>(zone: Option<TimeZone>, body: impl FnOnce() -> R) -> R {
let Some(zone) = zone else {
return body();
};
let before = ZONE.with(|z| z.replace(Some(zone)));
let out = body();
ZONE.with(|z| z.set(before));
out
}
#[must_use]
pub fn reader_time_zone(name: &str) -> Option<TimeZone> {
#[cfg(feature = "fn-datetime")]
{
let zone = TimeZone::named(name)?;
let host = crate::leptos::state::setup()?.host;
host.zone_offset(name, 0).map(|_| zone)
}
#[cfg(not(feature = "fn-datetime"))]
{
let _ = name;
None
}
}
#[cfg(any(feature = "ssr", all(feature = "hydrate", feature = "fn-datetime")))]
pub(crate) fn zone_name(zone: &TimeZone) -> Option<&str> {
match zone.as_option() {
mf2_runtime::ZoneOption::Named(name) => Some(name),
_ => None,
}
}
#[cfg(all(any(feature = "hydrate", feature = "csr"), feature = "fn-datetime"))]
fn browser_zone() -> Option<(TimeZone, alloc::string::String)> {
let format = js_sys::Intl::DateTimeFormat::new(&js_sys::Array::new(), &js_sys::Object::new());
let name = js_sys::Reflect::get(
&format.resolved_options(),
&wasm_bindgen::JsValue::from_str("timeZone"),
)
.ok()?
.as_string()?;
reader_time_zone(&name).map(|zone| (zone, name))
}
#[cfg(all(feature = "csr", feature = "fn-datetime"))]
pub(crate) fn mount_in_reader_zone() {
if let Some((zone, _)) = browser_zone() {
set(Some(zone));
}
}
#[cfg(all(feature = "hydrate", feature = "fn-datetime"))]
mod correction {
use super::{TimeZone, Vec, browser_zone, set, zone_name};
use alloc::rc::Rc;
use core::cell::{Cell, RefCell};
use crate::leptos::catalog;
use crate::leptos::registry::{Relocalize, Target};
use crate::leptos::text::Stored;
use crate::line::reactive_graph;
#[derive(Clone, Copy)]
enum Phase {
Idle,
Hydrating { reader: TimeZone },
Corrected { page: TimeZone },
}
enum Queued {
Value(Target, Stored),
Rich(Rc<RefCell<dyn Relocalize>>),
}
std::thread_local! {
static PHASE: Cell<Phase> = const { Cell::new(Phase::Idle) };
static QUEUE: RefCell<Vec<Queued>> = const { RefCell::new(Vec::new()) };
}
fn phase() -> Phase {
PHASE.with(Cell::get)
}
fn page_zone() -> (Option<TimeZone>, TimeZone) {
let stated = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| {
d.query_selector(
&[
"link[",
crate::leptos::links::PRELOAD_ATTR,
"][",
crate::leptos::links::ZONE_ATTR,
"]",
]
.concat(),
)
.ok()
.flatten()
})
.and_then(|link| link.get_attribute(crate::leptos::links::ZONE_ATTR))
.and_then(|name| TimeZone::named(&name));
let setup = crate::leptos::state::setup().map_or(TimeZone::UTC, |s| s.time_zone);
(stated, stated.unwrap_or(setup))
}
fn same(a: &TimeZone, b: &TimeZone) -> bool {
if a == b {
return true;
}
let utc =
|z: &TimeZone| *z == TimeZone::UTC || matches!(zone_name(z), Some("UTC" | "Etc/UTC"));
utc(a) && utc(b)
}
pub(crate) fn before_hydration(islands: bool) {
let (stated, page) = page_zone();
set(stated);
let Some((reader, name)) = browser_zone() else {
return;
};
if same(&page, &reader) {
return;
}
if islands {
set(Some(reader));
PHASE.with(|p| p.set(Phase::Corrected { page }));
remember(&name);
} else {
PHASE.with(|p| p.set(Phase::Hydrating { reader }));
}
}
pub(crate) fn after_hydration() {
let Phase::Hydrating { reader } = phase() else {
return;
};
let page = super::current().unwrap_or_else(|| page_zone().1);
set(Some(reader));
PHASE.with(|p| p.set(Phase::Corrected { page }));
let queued = QUEUE.with(|q| core::mem::take(&mut *q.borrow_mut()));
if let Some(catalog) = catalog::active() {
for node in &queued {
match node {
Queued::Value(target, desc) => target.correct_zone(desc, &catalog, page),
Queued::Rich(node) => {
if let Ok(mut node) = node.try_borrow_mut() {
node.relocalize(&catalog);
}
}
}
}
}
drop(queued);
reactive_graph::traits::Notify::notify(&catalog::changed());
if let Some(name) = zone_name(&reader) {
remember(name);
}
}
fn remember(name: &str) {
if let Some(window) = web_sys::window() {
crate::leptos::boot::write_zone_cookie(&window, name);
}
}
pub(crate) fn hydrated(target: &Target, desc: &Stored) {
match phase() {
Phase::Idle => {}
Phase::Hydrating { .. } => {
QUEUE.with(|q| {
q.borrow_mut()
.push(Queued::Value(target.clone(), desc.clone()));
});
}
Phase::Corrected { page } => {
if let Some(catalog) = catalog::active() {
target.correct_zone(desc, &catalog, page);
}
}
}
}
pub(crate) fn hydrating_zone() -> Option<TimeZone> {
match phase() {
Phase::Corrected { page } => Some(page),
_ => None,
}
}
pub(crate) fn hydrated_rich(node: Rc<RefCell<dyn Relocalize>>) {
match phase() {
Phase::Idle => {}
Phase::Hydrating { .. } => {
QUEUE.with(|q| q.borrow_mut().push(Queued::Rich(node)));
}
Phase::Corrected { .. } => {
if let (Some(catalog), Ok(mut node)) = (catalog::active(), node.try_borrow_mut()) {
node.relocalize(&catalog);
}
}
}
}
}
#[cfg(all(feature = "hydrate", feature = "fn-datetime"))]
pub(crate) use correction::{
after_hydration, before_hydration, hydrated, hydrated_rich, hydrating_zone,
};