use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::Path;
use mf2_catalog::Dir;
use crate::build::{Emit, LocaleInfo};
use crate::features::Features;
#[derive(Debug)]
pub struct Module<'a> {
pub facade: &'a str,
pub manifest_path: &'a Path,
pub manifest_hash: u64,
pub source_locale: &'a str,
pub locales: &'a [LocaleInfo],
pub functions: &'a [String],
pub custom: &'a BTreeMap<String, String>,
pub features: &'a Features,
pub unannotated: bool,
pub messages: usize,
pub emit: Emit,
pub manifest_bytes: Option<&'a [u8]>,
}
pub fn write(module: &Module<'_>) -> String {
let mut s = String::with_capacity(2048);
header(&mut s, module);
identity(&mut s, module);
locales(&mut s, module);
if module.emit == Emit::Both {
catalogs(&mut s, module, true);
}
registry(&mut s, module);
host(&mut s, module);
tr(&mut s, module);
s
}
pub fn write_catalogs(module: &Module<'_>) -> String {
let mut s = String::with_capacity(512);
header(&mut s, module);
let _ = write!(
s,
"
/// The facade the embedded catalogs are read through.
#[doc(hidden)]
pub use {facade} as __mf2;
",
facade = module.facade
);
catalogs(&mut s, module, false);
s
}
fn header(s: &mut String, m: &Module<'_>) {
let _ = writeln!(
s,
"// @generated by mf2-build from locales/ — do not edit.\n\
// {} messages, {} locale{}, {} function{}; features: {}.",
m.messages,
m.locales.len(),
plural(m.locales.len()),
m.functions.len(),
plural(m.functions.len()),
if m.features.names().next().is_none() {
"(none)".to_owned()
} else {
m.features.names().collect::<Vec<_>>().join(", ")
}
);
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
fn hex_u64(value: u64) -> String {
let hex = format!("{value:016x}");
let mut out = String::from("0x");
for (i, chunk) in hex.as_bytes().chunks(4).enumerate() {
if i > 0 {
out.push('_');
}
out.push_str(core::str::from_utf8(chunk).unwrap_or("0000"));
}
out
}
fn identity(s: &mut String, m: &Module<'_>) {
let _ = write!(
s,
"
/// The facade every `tr!` expansion goes through.
#[doc(hidden)]
pub use {facade} as __mf2;
/// `manifest_hash` (`plans/02-catalog-format.md` §3): the ids, slots, markup
/// names and functions the wasm and every catalog agree on. A catalog whose
/// header carries another one is rejected and refetched (F6).
pub const MANIFEST_HASH: u64 = {hash};
/// The locale the manifest was built from.
pub const SOURCE_LOCALE: &str = {source:?};
",
facade = m.facade,
hash = hex_u64(m.manifest_hash),
source = m.source_locale
);
}
fn locales(s: &mut String, m: &Module<'_>) {
let _ = write!(
s,
"
/// Every locale this corpus was built for, with its base direction. No
/// catalog name and no hash: the client is told which URL to fetch.
pub static LOCALES: &[(&str, __mf2::Dir)] = &[
"
);
for locale in m.locales {
let _ = writeln!(
s,
" ({:?}, __mf2::Dir::{}),",
locale.tag,
match locale.dir {
Dir::Rtl => "Rtl",
_ => "Ltr",
}
);
}
let _ = write!(
s,
"];
/// Whether `tag` is one of them.
pub fn has_locale(tag: &str) -> bool {{
LOCALES.iter().any(|(t, _)| *t == tag)
}}
"
);
}
fn catalogs(s: &mut String, m: &Module<'_>, gated: bool) {
let gate = if gated {
"#[cfg(feature = \"ssr\")]\n"
} else {
""
};
let _ = write!(
s,
"
/// The catalogs, embedded for the **server** only: `(tag, file name,
/// bytes)`. The client fetches its one locale instead, so its wasm holds no
/// message text, no id and no catalog name (B6).
{gate}pub static CATALOGS: &[(&str, &str, &[u8])] = &[
"
);
for locale in m.locales {
let _ = writeln!(
s,
" ({tag:?}, {file:?}, include_bytes!(concat!(env!(\"OUT_DIR\"), \"/\", {file:?}))),",
tag = locale.tag,
file = locale.file_name
);
}
let _ = write!(
s,
"];
/// The embedded catalog of `tag`, for a server that serves it from memory.
{gate}pub fn catalog(tag: &str) -> Option<&'static [u8]> {{
CATALOGS.iter().find(|(t, _, _)| *t == tag).map(|(_, _, b)| *b)
}}
/// The file name `tag`'s catalog is published under, content-hashed and
/// served immutable.
{gate}pub fn catalog_name(tag: &str) -> Option<&'static str> {{
CATALOGS.iter().find(|(t, _, _)| *t == tag).map(|(_, n, _)| *n)
}}
"
);
}
fn builtin_path(name: &str, features: &Features) -> Option<&'static str> {
Some(match (name, features.fn_number()) {
("string", _) => "__mf2::functions::STRING",
("number", false) => "__mf2::functions::NUMBER",
("integer", false) => "__mf2::functions::INTEGER",
("offset", false) => "__mf2::functions::OFFSET",
("number", true) => "__mf2::fn_number::NUMBER",
("integer", true) => "__mf2::fn_number::INTEGER",
("offset", true) => "__mf2::fn_number::OFFSET",
("percent", _) => "__mf2::fn_number::PERCENT",
("currency", _) => "__mf2::fn_number::CURRENCY",
("unit", _) => "__mf2::fn_number::UNIT",
("datetime", _) => "__mf2::fn_datetime::DATETIME",
("date", _) => "__mf2::fn_datetime::DATE",
("time", _) => "__mf2::fn_datetime::TIME",
_ => return None,
})
}
fn registry(s: &mut String, m: &Module<'_>) {
let mut entries: Vec<(String, String)> = Vec::new();
for name in m.functions {
let path = match builtin_path(name, m.features) {
Some(path) => path.to_owned(),
None => match m.custom.get(name) {
Some(path) => path.clone(),
None => continue,
},
};
entries.push((name.clone(), path));
}
let _ = write!(
s,
"
/// The handlers this corpus uses — and no others, so an unused function is
/// not linked (B13).
static FUNCTIONS: [(&str, &dyn __mf2::Function); {n}] = [
",
n = entries.len()
);
for (name, path) in &entries {
let _ = writeln!(s, " ({name:?}, &{path}),");
}
let _ = write!(
s,
"];\n\nstatic REGISTRY: __mf2::Registry = __mf2::Registry::new(&FUNCTIONS)"
);
if m.unannotated && m.features.fn_number() {
let _ = write!(s, "\n .with_numbers(&__mf2::fn_number::NUMBERS)");
}
if m.unannotated && m.features.fn_datetime() {
let _ = write!(s, "\n .with_dates(&__mf2::fn_datetime::DATES)");
}
let _ = write!(
s,
";
/// The registry every formatter in this application uses.
pub fn registry() -> &'static __mf2::Registry {{
®ISTRY
}}
"
);
}
fn host(s: &mut String, m: &Module<'_>) {
let uses_dates = m.features.fn_datetime();
let _ = write!(
s,
"
/// The host this build formats through: the server's, or the browser's — and
/// of the browser's, the one this corpus actually needs, so that the glue of
/// a feature that is on but unused is never linked (B1′).
pub mod host {{
#[cfg(feature = \"ssr\")]
pub use super::__mf2::host_std::HOST;
"
);
if uses_dates {
let _ = write!(
s,
" #[cfg(all(not(feature = \"ssr\"), feature = \"datetime-intl\"))]
pub use super::__mf2::host_web::INTL_HOST as HOST;
#[cfg(all(not(feature = \"ssr\"), not(feature = \"datetime-intl\"), feature = \"datetime-icu\"))]
pub use super::__mf2::host_web::ZONES_HOST as HOST;
#[cfg(all(
not(feature = \"ssr\"),
not(feature = \"datetime-intl\"),
not(feature = \"datetime-icu\")
))]
pub use super::__mf2::host_web::HOST;
"
);
} else {
let _ = write!(
s,
" // This corpus formats no dates, so the date and zone hosts are
// never named here.
#[cfg(not(feature = \"ssr\"))]
pub use super::__mf2::host_web::HOST;
"
);
}
let _ = writeln!(s, "}}");
}
fn tr(s: &mut String, m: &Module<'_>) {
let (source, how) = match m.manifest_bytes {
None => (
format!("{:?}", m.manifest_path.display().to_string()),
"The manifest's absolute path and its hash are baked in, so any crate that\n\
/// depends on this one can call it",
),
Some(bytes) => (
format!("bytes {}", byte_string(bytes)),
"The manifest's bytes and its hash are baked in (inline mode), so an\n\
/// expansion needs no file and survives a target directory that moved",
),
};
let _ = write!(
s,
"
/// `tr!(\"id\", name = value, …)` — the id and the argument set checked
/// against the manifest at compile time.
///
/// {how}, and a manifest whose hash differs is
/// reported as stale rather than used (D8).
#[macro_export]
macro_rules! tr {{
($($t:tt)*) => {{
$crate::__mf2::__tr_impl!({source} 0x{hash:016x}u64 ; $crate ; $($t)*)
}};
}}
/// `msg_id!(\"id\")` — the id checked against the manifest, and nothing
/// else: the `MsgId` a caller needs to format a message whose arguments are
/// not known until run time (`mf2::TrDyn`).
#[macro_export]
macro_rules! msg_id {{
($($t:tt)*) => {{
$crate::__mf2::__msg_id_impl!({source} 0x{hash:016x}u64 ; $crate ; $($t)*)
}};
}}
",
hash = m.manifest_hash
);
}
fn byte_string(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2 + 3);
out.push_str("b\"");
for &b in bytes {
match b {
b'\\' => out.push_str("\\\\"),
b'"' => out.push_str("\\\""),
0x20..=0x7e => out.push(b as char),
_ => {
let _ = write!(out, "\\x{b:02x}");
}
}
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::{Module, write};
use crate::build::{Emit, LocaleInfo};
use crate::features::Features;
use mf2_catalog::Dir;
use std::collections::BTreeMap;
use std::path::Path;
fn locales() -> Vec<LocaleInfo> {
vec![
LocaleInfo {
tag: "en".to_owned(),
dir: Dir::Ltr,
hash: "0123456789abcdef".to_owned(),
file_name: "en.0123456789abcdef.mf2b".to_owned(),
},
LocaleInfo {
tag: "ar".to_owned(),
dir: Dir::Rtl,
hash: "fedcba9876543210".to_owned(),
file_name: "ar.fedcba9876543210.mf2b".to_owned(),
},
]
}
fn module<'a>(
functions: &'a [String],
features: &'a Features,
custom: &'a BTreeMap<String, String>,
locales: &'a [LocaleInfo],
unannotated: bool,
) -> Module<'a> {
Module {
facade: "::mf2",
manifest_path: Path::new("/out/manifest.mf2m"),
manifest_hash: 0x43e0_dc12_eeb0_5ef1,
source_locale: "en",
locales,
functions,
custom,
features,
unannotated,
messages: 3,
emit: Emit::Both,
manifest_bytes: None,
}
}
#[test]
fn the_client_sees_no_catalog_name_or_hash() {
let locales = locales();
let features = Features::default();
let custom = BTreeMap::new();
let code = write(&module(&[], &features, &custom, &locales, false));
for (i, line) in code.lines().enumerate() {
if line.contains(".mf2b") {
let before = code.lines().take(i).collect::<Vec<_>>().join("\n");
assert!(
before.rfind("#[cfg(feature = \"ssr\")]").is_some(),
"line {i} names a catalog outside an ssr block: {line}"
);
}
}
assert!(
code.contains("pub const MANIFEST_HASH: u64 = 0x43e0_dc12_eeb0_5ef1;"),
"{code}"
);
assert!(code.contains("pub static LOCALES: &[(&str, __mf2::Dir)]"));
assert!(code.contains("(\"ar\", __mf2::Dir::Rtl),"));
}
#[test]
fn the_registry_is_closed_over_what_the_corpus_uses() {
let locales = locales();
let custom = BTreeMap::from([("app:emoji".to_owned(), "my_app::EMOJI".to_owned())]);
let functions = ["integer".to_owned(), "app:emoji".to_owned()];
let core = Features::default();
let code = write(&module(&functions, &core, &custom, &locales, false));
assert!(
code.contains("(\"integer\", &__mf2::functions::INTEGER),"),
"{code}"
);
assert!(code.contains("(\"app:emoji\", &my_app::EMOJI),"), "{code}");
assert!(
!code.contains("STRING"),
"an unused handler is linked:\n{code}"
);
assert!(!code.contains("with_numbers"), "{code}");
let localized = Features::parse("fn-number");
let code = write(&module(&functions, &localized, &custom, &locales, false));
assert!(
code.contains("(\"integer\", &__mf2::fn_number::INTEGER),"),
"{code}"
);
assert!(!code.contains("with_numbers"), "{code}");
}
#[test]
fn the_unannotated_hooks_need_a_placeholder_that_can_reach_them() {
let locales = locales();
let custom = BTreeMap::new();
let both = Features::parse("fn-number,fn-datetime");
let code = write(&module(&[], &both, &custom, &locales, true));
assert!(
code.contains(".with_numbers(&__mf2::fn_number::NUMBERS)"),
"{code}"
);
assert!(
code.contains(".with_dates(&__mf2::fn_datetime::DATES)"),
"{code}"
);
}
#[test]
fn a_corpus_without_dates_never_names_a_date_host() {
let locales = locales();
let custom = BTreeMap::new();
let none = Features::default();
let code = write(&module(&[], &none, &custom, &locales, false));
assert!(!code.contains("INTL_HOST"), "{code}");
assert!(!code.contains("ZONES_HOST"), "{code}");
assert!(
code.contains("pub use super::__mf2::host_web::HOST;"),
"{code}"
);
let dates = Features::parse("fn-datetime,datetime-intl");
let code = write(&module(&[], &dates, &custom, &locales, false));
assert!(code.contains("INTL_HOST"), "{code}");
}
#[test]
fn inline_mode_bakes_the_manifest_itself() {
let locales = locales();
let custom = BTreeMap::new();
let features = Features::default();
let mut m = module(&[], &features, &custom, &locales, false);
let bytes = b"MF2M\x00\x01\xf1^\xb0\xee\x12\xdc\xe0C\"\\";
m.manifest_bytes = Some(bytes);
let code = write(&m);
assert!(
code.contains(
"$crate::__mf2::__tr_impl!(bytes b\"MF2M\\x00\\x01\\xf1^\\xb0\\xee\\x12\\xdc\\xe0C\\\"\\\\\" 0x43e0dc12eeb05ef1u64"
),
"{code}"
);
assert!(!code.contains("/out/manifest.mf2m"), "{code}");
}
#[test]
fn the_tr_wrapper_bakes_in_the_path_and_the_hash() {
let locales = locales();
let custom = BTreeMap::new();
let features = Features::default();
let code = write(&module(&[], &features, &custom, &locales, false));
assert!(
code.contains("$crate::__mf2::__tr_impl!(\"/out/manifest.mf2m\" 0x43e0dc12eeb05ef1u64 ; $crate ; $($t)*)"),
"{code}"
);
assert!(
code.contains("$crate::__mf2::__msg_id_impl!(\"/out/manifest.mf2m\" 0x43e0dc12eeb05ef1u64 ; $crate ; $($t)*)"),
"{code}"
);
}
}