use core::sync::atomic::{AtomicBool, Ordering};
use mf2_catalog::Dir;
use mf2_runtime::{BidiStrategy, FormatContext, Host, Registry, TimeZone};
use crate::{LanguageMatching, Tr};
use std::sync::OnceLock;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum TextUse {
Displayed,
Plain,
}
const PLAIN_ATTRIBUTES: [&str; 16] = [
"value",
"href",
"src",
"srcset",
"action",
"formaction",
"poster",
"cite",
"download",
"id",
"name",
"for",
"form",
"list",
"class",
"type",
];
pub(crate) fn attribute_use(key: &str) -> TextUse {
let data = key
.get(..5)
.is_some_and(|p| p.eq_ignore_ascii_case("data-"));
if data || PLAIN_ATTRIBUTES.iter().any(|a| a.eq_ignore_ascii_case(key)) {
TextUse::Plain
} else {
TextUse::Displayed
}
}
#[derive(Clone, Copy)]
#[non_exhaustive]
pub struct Setup {
pub registry: &'static Registry,
pub host: &'static dyn Host,
pub manifest_hash: u64,
pub source_locale: &'static str,
pub locales: &'static [(&'static str, Dir)],
pub time_zone: TimeZone,
matching: Option<&'static LanguageMatching>,
names: Option<fn(usize) -> Option<Tr>>,
}
impl Setup {
#[must_use]
pub const fn new(
registry: &'static Registry,
host: &'static dyn Host,
manifest_hash: u64,
source_locale: &'static str,
locales: &'static [(&'static str, Dir)],
) -> Setup {
Setup {
registry,
host,
manifest_hash,
source_locale,
locales,
time_zone: TimeZone::UTC,
matching: None,
names: None,
}
}
#[must_use]
pub const fn with_language_matching(mut self, matching: &'static LanguageMatching) -> Setup {
self.matching = Some(matching);
self
}
#[must_use]
pub const fn with_names(mut self, names: fn(usize) -> Option<Tr>) -> Setup {
self.names = Some(names);
self
}
#[must_use]
pub const fn with_time_zone(mut self, zone: TimeZone) -> Setup {
self.time_zone = zone;
self
}
}
impl core::fmt::Debug for Setup {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Setup")
.field("manifest_hash", &self.manifest_hash)
.field("source_locale", &self.source_locale)
.field("locales", &self.locales)
.field("time_zone", &self.time_zone)
.finish_non_exhaustive()
}
}
struct Runtime {
setup: Setup,
}
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
static RE_INSTALLED: AtomicBool = AtomicBool::new(false);
pub fn install(setup: Setup) {
if RUNTIME.set(Runtime { setup }).is_err() {
RE_INSTALLED.store(true, Ordering::Relaxed);
}
}
#[must_use]
pub fn installed() -> bool {
RUNTIME.get().is_some()
}
#[must_use]
pub fn installed_twice() -> bool {
RE_INSTALLED.load(Ordering::Relaxed)
}
#[must_use]
pub fn setup() -> Option<Setup> {
RUNTIME.get().map(|r| r.setup)
}
#[must_use]
pub fn manifest_hash() -> u64 {
RUNTIME.get().map_or(0, |r| r.setup.manifest_hash)
}
#[must_use]
pub fn source_locale() -> &'static str {
RUNTIME.get().map_or("", |r| r.setup.source_locale)
}
#[must_use]
pub fn locales() -> &'static [(&'static str, Dir)] {
RUNTIME.get().map_or(&[], |r| r.setup.locales)
}
pub(crate) fn locale_name(index: usize) -> Option<Tr> {
RUNTIME
.get()
.and_then(|r| r.setup.names)
.and_then(|names| names(index))
}
#[must_use]
pub fn dir_of(tag: &str) -> Option<Dir> {
locales().iter().find(|(t, _)| *t == tag).map(|(_, d)| *d)
}
#[must_use]
pub fn lookup_locale(
candidate: &str,
locales: &[(&'static str, Dir)],
) -> Option<(&'static str, Dir)> {
best_locale([candidate], locales)
}
#[doc(hidden)]
#[must_use]
pub fn best_locale<'a>(
candidates: impl IntoIterator<Item = &'a str>,
locales: &[(&'static str, Dir)],
) -> Option<(&'static str, Dir)> {
let at = matching().best_match(candidates, locales)?;
locales.get(at).copied()
}
fn matching() -> &'static LanguageMatching {
#[cfg(feature = "host-std")]
{
LanguageMatching::cldr()
}
#[cfg(not(feature = "host-std"))]
{
RUNTIME
.get()
.and_then(|r| r.setup.matching)
.unwrap_or(&LanguageMatching::EMPTY)
}
}
pub(crate) fn context_for(use_: TextUse, bidi: Option<BidiStrategy>) -> Option<FormatContext> {
let runtime = RUNTIME.get()?;
let mut cx = FormatContext::new(runtime.setup.host);
cx.bidi = match use_ {
TextUse::Plain => BidiStrategy::None,
TextUse::Displayed => bidi.unwrap_or(BidiStrategy::Default),
};
#[cfg(any(feature = "ssr", feature = "fn-datetime"))]
{
cx.time_zone = crate::leptos::zone::current().unwrap_or(runtime.setup.time_zone);
}
#[cfg(not(any(feature = "ssr", feature = "fn-datetime")))]
{
cx.time_zone = runtime.setup.time_zone;
}
Some(cx)
}
pub(crate) fn registry() -> Option<&'static Registry> {
RUNTIME.get().map(|r| r.setup.registry)
}