use alloc::string::String;
use alloc::vec::Vec;
use js_sys::Uint8Array;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Document, Element, Response};
use mf2_catalog::Dir;
use crate::error::LoadError;
use crate::leptos::links::{CATALOG_LINK_LOCALE_ATTR, CATALOG_ROUTE, PRELOAD_ATTR};
#[cfg(feature = "csr")]
use crate::leptos::links::{CSR_INDEX_ATTR, CSR_INDEX_URL, LOCALE_STORAGE_KEY};
#[cfg(not(feature = "csr"))]
use crate::leptos::links::{LOCALE_COOKIE, LOCALE_QUERY, QUERY_ATTR};
#[cfg(any(feature = "hydrate", not(feature = "static-locale")))]
use crate::leptos::registry;
use crate::leptos::{catalog, state};
#[allow(unused_imports)]
use crate::line::{leptos, reactive_graph, tachys};
fn document() -> Option<Document> {
web_sys::window()?.document()
}
fn html(document: &Document) -> Option<Element> {
document.document_element()
}
#[must_use]
pub fn document_locale() -> Option<String> {
let lang = html(&document()?)?.get_attribute("lang")?;
(!lang.is_empty()).then_some(lang)
}
#[must_use]
pub fn catalog_url(tag: &str) -> Option<String> {
#[cfg(feature = "csr")]
if let Some(url) = indexed_url(tag) {
return Some(url);
}
let document = document()?;
if document_locale().as_deref() == Some(tag)
&& let Ok(Some(link)) =
document.query_selector(&["link[", PRELOAD_ATTR, "][href]"].concat())
&& let Some(href) = link.get_attribute("href")
{
return Some(href);
}
let selector = ["link[", CATALOG_LINK_LOCALE_ATTR, "=\"", tag, "\"][href]"].concat();
if let Ok(Some(link)) = document.query_selector(&selector)
&& let Some(href) = link.get_attribute("href")
{
return Some(href);
}
if cfg!(feature = "csr") {
return None;
}
Some([CATALOG_ROUTE, tag].concat())
}
async fn fetch_response(url: &str) -> Result<Response, LoadError> {
let window = web_sys::window().ok_or(LoadError::Fetch)?;
let response: Response = JsFuture::from(window.fetch_with_str(url))
.await
.map_err(|_| LoadError::Fetch)?
.dyn_into()
.map_err(|_| LoadError::Fetch)?;
if response.ok() {
Ok(response)
} else {
Err(LoadError::Fetch)
}
}
async fn fetch(url: &str) -> Result<Vec<u8>, LoadError> {
let response = fetch_response(url).await?;
let buffer = JsFuture::from(response.array_buffer().map_err(|_| LoadError::Fetch)?)
.await
.map_err(|_| LoadError::Fetch)?;
Ok(Uint8Array::new(&buffer).to_vec())
}
pub async fn preload_locale(tag: &str) -> Result<(), LoadError> {
let url = catalog_url(tag).ok_or(LoadError::UnknownLocale)?;
catalog::read(fetch(&url).await?).map(|_| ())
}
#[cfg(not(feature = "static-locale"))]
pub async fn set_locale(tag: &str) -> Result<(), LoadError> {
if state::dir_of(tag).is_none() {
return Err(LoadError::UnknownLocale);
}
match switch_live(tag).await {
Ok(()) => {}
Err(LoadError::ManifestMismatch) => {
web_sys::console::error_1(&JsValue::from_str(
"mf2: the catalog is from another deploy; reloading into the new locale.",
));
let window = web_sys::window().ok_or(LoadError::Fetch)?;
return reload_into(&window, tag);
}
Err(error) => return Err(error),
}
#[cfg(feature = "csr")]
remember_locale(tag);
#[cfg(not(feature = "csr"))]
if let Some(window) = web_sys::window() {
let _ = write_locale_cookie(&window, tag);
drop_locale_query(&window);
}
Ok(())
}
#[cfg(feature = "static-locale")]
#[allow(clippy::unused_async)]
pub async fn set_locale(tag: &str) -> Result<(), LoadError> {
if state::dir_of(tag).is_none() {
return Err(LoadError::UnknownLocale);
}
if document_locale().as_deref() == Some(tag) {
return Ok(());
}
let window = web_sys::window().ok_or(LoadError::Fetch)?;
reload_into(&window, tag)
}
#[cfg(feature = "csr")]
fn reload_into(window: &web_sys::Window, tag: &str) -> Result<(), LoadError> {
remember_locale(tag);
window.location().reload().map_err(|_| LoadError::Fetch)
}
#[cfg(not(feature = "csr"))]
fn reload_into(window: &web_sys::Window, tag: &str) -> Result<(), LoadError> {
write_locale_cookie(window, tag)?;
let location = window.location();
match location
.search()
.ok()
.and_then(|search| without_param(&search, &page_query()))
{
Some(search) => location.set_search(&search),
None => location.reload(),
}
.map_err(|_| LoadError::Fetch)
}
#[cfg(not(feature = "csr"))]
fn write_locale_cookie(window: &web_sys::Window, tag: &str) -> Result<(), LoadError> {
let document = window.document().ok_or(LoadError::Fetch)?;
let secure = window.location().protocol().is_ok_and(|p| p == "https:");
let cookie = [
LOCALE_COOKIE,
"=",
tag,
"; path=/; max-age=31536000; samesite=lax",
if secure { "; secure" } else { "" },
]
.concat();
js_sys::Reflect::set(
&document,
&JsValue::from_str("cookie"),
&JsValue::from_str(&cookie),
)
.map_err(|_| LoadError::Fetch)?;
Ok(())
}
#[cfg(all(feature = "hydrate", feature = "fn-datetime"))]
pub(crate) fn write_zone_cookie(window: &web_sys::Window, zone: &str) {
let Some(document) = window.document() else {
return;
};
let secure = window.location().protocol().is_ok_and(|p| p == "https:");
let cookie = [
crate::leptos::links::TIME_ZONE_COOKIE,
"=",
zone,
"; path=/; max-age=31536000; samesite=lax",
if secure { "; secure" } else { "" },
]
.concat();
let _ = js_sys::Reflect::set(
&document,
&JsValue::from_str("cookie"),
&JsValue::from_str(&cookie),
);
}
#[cfg(all(not(feature = "static-locale"), not(feature = "csr")))]
fn drop_locale_query(window: &web_sys::Window) {
let location = window.location();
let Some(search) = location
.search()
.ok()
.and_then(|search| without_param(&search, &page_query()))
else {
return;
};
let (Ok(path), Ok(hash)) = (location.pathname(), location.hash()) else {
return;
};
let url = [path.as_str(), search.as_str(), hash.as_str()].concat();
if let Ok(history) = window.history() {
let _ = history.replace_state_with_url(&JsValue::NULL, "", Some(&url));
}
}
#[cfg(not(feature = "csr"))]
fn page_query() -> String {
document()
.and_then(|document| {
document
.query_selector(&["link[", PRELOAD_ATTR, "][", QUERY_ATTR, "]"].concat())
.ok()
.flatten()
})
.and_then(|link| link.get_attribute(QUERY_ATTR))
.unwrap_or_else(|| String::from(LOCALE_QUERY))
}
#[cfg(not(feature = "csr"))]
fn without_param(search: &str, name: &str) -> Option<String> {
let query = search.strip_prefix('?').unwrap_or(search);
let named = |pair: &&str| pair.split('=').next() == Some(name);
if !query.split('&').any(|pair| named(&pair)) {
return None;
}
let mut out = String::new();
for pair in query
.split('&')
.filter(|pair| !pair.is_empty() && !named(pair))
{
out.push(if out.is_empty() { '?' } else { '&' });
out.push_str(pair);
}
Some(out)
}
#[cfg(not(feature = "static-locale"))]
async fn switch_live(tag: &str) -> Result<(), LoadError> {
if catalog::active().is_some_and(|c| c.locale() == tag) {
return Ok(());
}
let url = catalog_url(tag).ok_or(LoadError::UnknownLocale)?;
let catalog = catalog::read(fetch(&url).await?)?;
install_active(catalog);
Ok(())
}
#[cfg(not(feature = "static-locale"))]
fn install_active(catalog: alloc::sync::Arc<mf2_catalog::Catalog>) {
let tag = alloc::string::ToString::to_string(catalog.locale());
let dir = catalog.dir();
catalog::set_active(catalog);
registry::relocalize_all();
reactive_graph::traits::Notify::notify(&catalog::changed());
set_document_lang(&tag, dir);
}
pub fn set_document_lang(tag: &str, dir: Dir) {
let Some(document) = document() else { return };
let Some(html) = html(&document) else { return };
let _ = html.set_attribute("lang", tag);
let _ = html.set_attribute("dir", if dir == Dir::Rtl { "rtl" } else { "ltr" });
}
pub async fn load_page_catalog() -> Result<(), LoadError> {
let tag = document_locale().ok_or(LoadError::UnknownLocale)?;
let url = catalog_url(&tag).ok_or(LoadError::UnknownLocale)?;
let catalog = catalog::read(fetch(&url).await?)?;
catalog::set_active(catalog);
Ok(())
}
#[cfg(feature = "hydrate")]
const LEFT_AS: &str = "; the page stays as served, not interactive.";
#[cfg(feature = "csr")]
const LEFT_AS: &str = "; the application is not started.";
fn report_boot_failure(error: &LoadError) {
let what = match error {
LoadError::ManifestMismatch => {
"mf2: this page's catalog is from another deploy; reloading."
}
LoadError::NotInstalled => "mf2: install() was not called before the boot.",
LoadError::UnknownLocale if cfg!(feature = "csr") => {
"mf2: the catalog index does not list this locale"
}
LoadError::UnknownLocale => "mf2: the page does not say which locale it is in",
LoadError::Fetch if cfg!(feature = "csr") => {
"mf2: the catalog index or the catalog could not be fetched"
}
LoadError::Fetch => "mf2: the catalog could not be fetched",
LoadError::Malformed(_) => "mf2: the catalog is malformed",
};
let line = match error {
LoadError::ManifestMismatch | LoadError::NotInstalled => String::from(what),
_ => [what, LEFT_AS].concat(),
};
web_sys::console::error_1(&JsValue::from_str(&line));
if matches!(error, LoadError::ManifestMismatch)
&& let Some(window) = web_sys::window()
{
let _ = window.location().reload();
}
}
#[cfg(feature = "hydrate")]
pub fn hydrate_body<F, N>(app: F)
where
F: FnOnce() -> N + 'static,
N: leptos::IntoView,
{
boot_then(app, false);
}
#[cfg(feature = "hydrate")]
pub fn hydrate_lazy<F, N>(app: F)
where
F: FnOnce() -> N + 'static,
N: leptos::IntoView,
{
boot_then(app, true);
}
#[cfg(feature = "hydrate")]
fn boot_then<F, N>(app: F, lazy: bool)
where
F: FnOnce() -> N + 'static,
N: leptos::IntoView,
{
let _ = any_spawner::Executor::init_wasm_bindgen();
leptos::task::spawn_local(async move {
if let Err(error) = load_page_catalog().await {
report_boot_failure(&error);
return;
}
#[cfg(feature = "fn-datetime")]
{
crate::leptos::zone::before_hydration(false);
if lazy {
leptos::mount::hydrate_from_async(tachys::dom::body(), app)
.await
.forget();
} else {
leptos::mount::hydrate_body(app);
}
crate::leptos::zone::after_hydration();
}
#[cfg(not(feature = "fn-datetime"))]
if lazy {
leptos::mount::hydrate_lazy(app);
} else {
leptos::mount::hydrate_body(app);
}
});
}
#[cfg(feature = "hydrate")]
pub fn hydrate_islands() {
leptos::mount::hydrate_islands();
#[cfg(feature = "fn-datetime")]
crate::leptos::zone::before_hydration(true);
let boot = wasm_bindgen_futures::future_to_promise(async {
match load_page_catalog().await {
Ok(()) => {
registry::relocalize_all();
reactive_graph::traits::Notify::notify(&catalog::changed());
Ok(JsValue::UNDEFINED)
}
Err(error) => {
report_boot_failure(&error);
Err(JsValue::UNDEFINED)
}
}
});
ISLANDS_BOOT.with(|slot| slot.replace(Some(boot)));
}
#[cfg(feature = "hydrate")]
std::thread_local! {
static ISLANDS_BOOT: core::cell::RefCell<Option<js_sys::Promise>> =
const { core::cell::RefCell::new(None) };
}
#[cfg(feature = "hydrate")]
pub async fn wait_for_catalog() {
let Some(boot) = ISLANDS_BOOT.with(|slot| slot.borrow().clone()) else {
return;
};
if JsFuture::from(boot).await.is_err() {
core::future::pending::<()>().await;
}
}
#[cfg(feature = "csr")]
std::thread_local! {
static CSR_INDEX: core::cell::RefCell<Vec<(&'static str, String)>> =
const { core::cell::RefCell::new(Vec::new()) };
}
#[cfg(feature = "csr")]
fn indexed_url(tag: &str) -> Option<String> {
CSR_INDEX.with(|index| {
index
.borrow()
.iter()
.find(|(t, _)| *t == tag)
.map(|(_, url)| url.clone())
})
}
#[cfg(feature = "csr")]
fn storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok()?
}
#[cfg(feature = "csr")]
fn remember_locale(tag: &str) {
if let Some(storage) = storage() {
let _ = storage.set_item(LOCALE_STORAGE_KEY, tag);
}
}
#[cfg(feature = "csr")]
#[must_use]
pub fn client_locale() -> &'static str {
let locales = state::locales();
if let Some((tag, _)) = storage()
.and_then(|s| s.get_item(LOCALE_STORAGE_KEY).ok().flatten())
.as_deref()
.and_then(|remembered| state::lookup_locale(remembered, locales))
{
return tag;
}
if let Some(window) = web_sys::window() {
let navigator = window.navigator();
let mut languages: Vec<String> = navigator
.languages()
.iter()
.filter_map(|l| l.as_string())
.collect();
languages.extend(navigator.language());
if let Some((tag, _)) = state::best_locale(languages.iter().map(String::as_str), locales) {
return tag;
}
}
state::source_locale()
}
#[cfg(feature = "csr")]
async fn load_index() -> Result<(), LoadError> {
let href = document()
.and_then(|d| {
d.query_selector(&["link[", CSR_INDEX_ATTR, "][href]"].concat())
.ok()
.flatten()
})
.and_then(|link| link.get_attribute("href"))
.unwrap_or_else(|| String::from(CSR_INDEX_URL));
let response = fetch_response(&href).await?;
let json = JsFuture::from(response.json().map_err(|_| LoadError::Fetch)?)
.await
.map_err(|_| LoadError::Fetch)?;
let base = href.rfind('/').and_then(|at| href.get(..=at)).unwrap_or("");
let mut index = Vec::new();
for (tag, _) in state::locales() {
if let Some(file) = js_sys::Reflect::get(&json, &JsValue::from_str(tag))
.ok()
.and_then(|value| value.as_string())
{
index.push((*tag, [base, file.as_str()].concat()));
}
}
CSR_INDEX.with(|slot| *slot.borrow_mut() = index);
Ok(())
}
#[cfg(feature = "csr")]
pub async fn load_client_catalog() -> Result<(), LoadError> {
load_index().await?;
let tag = client_locale();
let url = indexed_url(tag).ok_or(LoadError::UnknownLocale)?;
let catalog = catalog::read(fetch(&url).await?)?;
let dir = catalog.dir();
catalog::set_active(catalog);
set_document_lang(tag, dir);
#[cfg(feature = "fn-datetime")]
crate::leptos::zone::mount_in_reader_zone();
Ok(())
}
#[cfg(feature = "csr")]
pub fn mount_to_body<F, N>(app: F)
where
F: FnOnce() -> N + 'static,
N: leptos::IntoView,
{
wasm_bindgen_futures::spawn_local(async move {
if let Err(error) = load_client_catalog().await {
report_boot_failure(&error);
return;
}
leptos::mount::mount_to_body(app);
});
}