diff --git a/Cargo.lock b/Cargo.lock index ada579fc3..dd944696f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4994,6 +4994,7 @@ dependencies = [ name = "wordcraft-layout" version = "0.4.0" dependencies = [ + "js-sys", "log", "serde", "serde_json", @@ -5026,6 +5027,7 @@ dependencies = [ "hayro-interpret", "hayro-syntax", "image", + "js-sys", "krilla", "kurbo", "log", @@ -5065,6 +5067,7 @@ dependencies = [ "egui", "egui_kittest", "image", + "js-sys", "log", "serde", "serde_json", diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index e99d75a8e..0be2dd516 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -449,20 +449,8 @@ pub fn format_date(fmt: &str) -> String { const MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; const DAYS: [&str; 7] = ["Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday"]; - let days_since = { - // Days since epoch for weekday. - let secs = { - #[cfg(not(target_arch = "wasm32"))] - { - std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) - } - #[cfg(target_arch = "wasm32")] - { - 0u64 - } - }; - (secs / 86_400) as usize - }; + // Days since the epoch, for the weekday. + let days_since = (super::now_unix() / 86_400) as usize; let month = MONTHS.get(m.saturating_sub(1)).copied().unwrap_or("January"); let day = DAYS.get(days_since % 7).copied().unwrap_or("Monday"); let mut out = String::new(); diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index d457db913..bd1962a55 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -254,28 +254,32 @@ pub fn split_para(s: &mut Session, at: &Pos) -> Result { } /// ISO-8601 timestamp (UTC, seconds) for `secs` since the Unix epoch. -fn iso_from_unix_secs(secs: u64) -> String { +pub fn iso_from_unix_secs(secs: u64) -> String { let (y, m, d) = civil_from_days((secs / 86_400) as i64); let t = secs % 86_400; format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", t / 3600, t / 60 % 60, t % 60) } -/// ISO-8601 timestamp (UTC, seconds). -pub fn now_iso() -> String { +/// Seconds since the Unix epoch (UTC): the system clock, or the browser's on the web. +pub fn now_unix() -> u64 { #[cfg(not(target_arch = "wasm32"))] { - let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); - iso_from_unix_secs(secs) + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) } // `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock. #[cfg(target_arch = "wasm32")] { let ms = js_sys::Date::now(); // Clamp a NaN or pre-epoch clock to 0 rather than fail. - iso_from_unix_secs(if ms.is_finite() && ms > 0.0 { (ms / 1000.0) as u64 } else { 0 }) + if ms.is_finite() && ms > 0.0 { (ms / 1000.0) as u64 } else { 0 } } } +/// ISO-8601 timestamp (UTC, seconds). +pub fn now_iso() -> String { + iso_from_unix_secs(now_unix()) +} + /// Days since 1970-01-01 → (year, month, day) (Howard Hinnant's algorithm). pub fn civil_from_days(z: i64) -> (i64, u32, u32) { let z = z + 719_468; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index d19627da2..88b46f9bd 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1638,3 +1638,17 @@ fn styles_apply_rejected_on_protected_document() { assert_eq!(para_style(&s, 0), None); assert_eq!(s.undo_labels(), undo); } + +/// Comments, revisions and dates read the real clock (#188: the web stamped 2026-01-01). +#[test] +fn timestamps_come_from_the_clock() { + let before = cmd::now_unix(); + assert!(before > 1_760_000_000, "{before}"); + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "Dated"})); + run(&mut s, "select.text", json!({"text": "Dated"})); + run(&mut s, "review.newComment", json!({"text": "When?"})); + let date = run(&mut s, "review.comments", json!({}))[0]["date"].as_str().unwrap_or_default().to_string(); + // Fixed-width ISO strings sort by time. + assert!(date >= cmd::iso_from_unix_secs(before) && date <= cmd::iso_from_unix_secs(cmd::now_unix()), "{date}"); +} diff --git a/crates/layout/Cargo.toml b/crates/layout/Cargo.toml index ac2401885..a97175fe9 100644 --- a/crates/layout/Cargo.toml +++ b/crates/layout/Cargo.toml @@ -19,3 +19,7 @@ unicode-segmentation = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +# The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32). +js-sys = "0.3" diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 5e3984de1..944f06c1a 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -1617,16 +1617,18 @@ pub fn is_floating(o: &InlineObject) -> bool { } } -/// Milliseconds since an arbitrary epoch (monotonic on native; 0 on wasm). +/// Milliseconds since an arbitrary epoch (monotonic on native; the browser's clock on the web). pub fn now_ms() -> f64 { #[cfg(not(target_arch = "wasm32"))] { static T0: std::sync::OnceLock = std::sync::OnceLock::new(); T0.get_or_init(std::time::Instant::now).elapsed().as_secs_f64() * 1000.0 } + // `Instant::now()` panics on wasm32-unknown-unknown, so ask the browser's clock. #[cfg(target_arch = "wasm32")] { - 0.0 + let ms = js_sys::Date::now(); + if ms.is_finite() { ms } else { 0.0 } } } diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index b64c6c526..2126f28cf 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -19,6 +19,10 @@ image = { workspace = true } thiserror = { workspace = true } log = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +# The browser clock (`SystemTime::now()` panics on wasm32). +js-sys = "0.3" + [dev-dependencies] hayro-syntax = "0.7.2" hayro-interpret = { version = "0.7.0", default-features = false, features = ["embed-fonts"] } diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 23ea5c3e8..b348bfd7f 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -259,15 +259,20 @@ fn parse_iso(s: &str) -> Option { Some((y, mo as u8, d as u8, h as u8, mi as u8, sec as u8)) } -#[cfg(not(target_arch = "wasm32"))] +/// The system clock, or the browser's on the web. fn now_unix() -> Option { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs() as i64) -} - -#[cfg(target_arch = "wasm32")] -fn now_unix() -> Option { - None + #[cfg(not(target_arch = "wasm32"))] + { + use std::time::{SystemTime, UNIX_EPOCH}; + let secs = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); + i64::try_from(secs).ok() + } + // `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock. + #[cfg(target_arch = "wasm32")] + { + let ms = js_sys::Date::now(); + (ms.is_finite() && ms > 0.0).then(|| (ms / 1000.0) as i64) + } } /// Unix seconds (UTC) → civil time (proleptic Gregorian). diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index d06751531..b46af1ef2 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -23,6 +23,10 @@ log = { workspace = true } # The system's preferred interface languages (#8). Native: OS APIs; wasm: none (English). sys-locale = "0.3" +[target.'cfg(target_arch = "wasm32")'.dependencies] +# The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32). +js-sys = "0.3" + [dev-dependencies] egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } image = { workspace = true } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d9a44fbff..449f5c3be 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -890,15 +890,18 @@ fn discards_document(id: &str, params: &Value) -> bool { } } +/// Wall-clock milliseconds since the Unix epoch: the system clock, or the browser's on the web. pub fn now_ms() -> f64 { #[cfg(not(target_arch = "wasm32"))] { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs_f64() * 1000.0).unwrap_or(0.0) } + // `SystemTime::now()` panics on wasm32-unknown-unknown, so ask the browser's clock. #[cfg(target_arch = "wasm32")] { - 0.0 + let ms = js_sys::Date::now(); + if ms.is_finite() && ms > 0.0 { ms } else { 0.0 } } }