use mf2_runtime::TimeZone;
#[cfg(all(feature = "hydrate", feature = "datetime"))]
use alloc::vec::Vec;
#[cfg(any(feature = "ssr", feature = "datetime"))]
std::thread_local! {
static ZONE: core::cell::Cell<Option<TimeZone>> = const { core::cell::Cell::new(None) };
}
#[cfg(any(feature = "ssr", feature = "datetime"))]
pub(crate) fn current() -> Option<TimeZone> {
ZONE.with(core::cell::Cell::get)
}
#[cfg(all(any(feature = "hydrate", feature = "csr"), feature = "datetime"))]
fn set(zone: Option<TimeZone>) {
ZONE.with(|z| z.set(zone));
}
#[cfg(any(feature = "ssr", all(feature = "hydrate", feature = "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 = "datetime")]
{
let zone = TimeZone::named(name)?;
let host = crate::leptos::state::setup()?.host;
host.zone_offset(name, 0).map(|_| zone)
}
#[cfg(not(feature = "datetime"))]
{
let _ = name;
None
}
}
#[cfg(any(feature = "ssr", all(feature = "hydrate", feature = "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 = "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 = "datetime"))]
pub(crate) fn mount_in_reader_zone() {
if let Some((zone, _)) = browser_zone() {
set(Some(zone));
}
}
#[cfg(all(feature = "hydrate", feature = "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;
use mf2_runtime::MsgId;
#[derive(Clone, Copy)]
enum Phase {
Idle,
Hydrating { reader: TimeZone, moved: bool },
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()) };
}
#[cfg(mf2_date_rewrite)]
std::thread_local! {
static REWRITE: Cell<bool> = const { Cell::new(false) };
}
#[cfg(mf2_date_rewrite)]
fn rewrite() -> bool {
REWRITE.with(Cell::get)
}
#[cfg(not(mf2_date_rewrite))]
const fn rewrite() -> bool {
false
}
fn formats_date(catalog: &mf2_catalog::Catalog, id: MsgId) -> bool {
let Some(registry) = crate::leptos::state::registry() else {
return true;
};
let is_date = |name: &str| registry.is_date_function(name);
catalog.names_function(is_date) && catalog.calls(id, is_date)
}
fn correct(target: &Target, desc: &Stored, catalog: &mf2_catalog::Catalog, page: TimeZone) {
if formats_date(catalog, desc.msg_id()) {
target.correct_zone(desc, catalog, page, rewrite());
}
}
fn preload_link() -> Option<web_sys::Element> {
web_sys::window().and_then(|w| w.document()).and_then(|d| {
d.query_selector(&["link[", crate::leptos::links::PRELOAD_ATTR, "]"].concat())
.ok()
.flatten()
})
}
#[cfg(mf2_date_rewrite)]
fn formatter_differs(link: Option<&web_sys::Element>) -> bool {
let Some(own) = crate::leptos::links::date_formatter() else {
return false;
};
let Some(page) = link.and_then(|l| l.get_attribute(crate::leptos::links::DATES_ATTR))
else {
return false;
};
let page = page.as_str();
page != own && !(page == "icu" && own == "intl")
}
fn phase() -> Phase {
PHASE.with(Cell::get)
}
fn page_zone(link: Option<&web_sys::Element>) -> (Option<TimeZone>, TimeZone) {
let stated = link
.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 link = preload_link();
let (stated, page) = page_zone(link.as_ref());
set(stated);
#[cfg(mf2_date_rewrite)]
let differs = formatter_differs(link.as_ref());
#[cfg(mf2_date_rewrite)]
REWRITE.with(|r| r.set(differs));
#[cfg(not(mf2_date_rewrite))]
let differs = rewrite();
let moved = browser_zone().filter(|(reader, _)| !same(&page, reader));
if moved.is_none() && !differs {
return;
}
if islands {
match moved {
Some((reader, name)) => {
set(Some(reader));
remember(&name);
}
None => set(Some(page)),
}
PHASE.with(|p| p.set(Phase::Corrected { page }));
} else {
let (reader, moved) = match moved {
Some((reader, _)) => (reader, true),
None => (page, false),
};
PHASE.with(|p| p.set(Phase::Hydrating { reader, moved }));
}
}
pub(crate) fn after_hydration() {
let Phase::Hydrating { reader, moved } = phase() else {
return;
};
let page = super::current().unwrap_or_else(|| page_zone(preload_link().as_ref()).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) => correct(target, desc, &catalog, page),
Queued::Rich(node) => {
if let Ok(mut node) = node.try_borrow_mut() {
relocalize(&mut *node, &catalog);
}
}
}
}
}
drop(queued);
reactive_graph::traits::Notify::notify(&catalog::changed());
if let Some(name) = zone_name(&reader).filter(|_| moved) {
remember(name);
}
}
fn relocalize(node: &mut dyn Relocalize, catalog: &mf2_catalog::Catalog) {
if !formats_date(catalog, node.msg_id()) {
return;
}
#[cfg(mf2_date_rewrite)]
if rewrite() {
node.rewrite(catalog);
return;
}
node.relocalize(catalog);
}
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() {
correct(target, 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()) {
relocalize(&mut *node, &catalog);
}
}
}
}
}
#[cfg(all(feature = "hydrate", feature = "datetime"))]
pub(crate) use correction::{
after_hydration, before_hydration, hydrated, hydrated_rich, hydrating_zone,
};