Skip to main content

autumn_plugin_vanilla/behaviors/
local_time.rs

1use autumn_web::Markup;
2use autumn_web::html;
3use autumn_web::reexports::chrono::{DateTime, SecondsFormat, TimeZone, Utc};
4
5use super::{Behavior, marker};
6use crate::markup::{Attributes, common, element};
7
8/// Shows a `<time>` in the browser locale and time zone.
9///
10/// The server writes UTC text. The browser replaces it with local text. If
11/// the element has no `title`, the runtime copies the server text to
12/// `title`. The locale is the nearest `lang` attribute, or the browser
13/// locale. The runtime sets the text one time, when it binds. The text does
14/// not update.
15///
16/// To format many `<time datetime>` elements, put
17/// `data-vanilla=(Behavior::LocalTime)` on a container.
18///
19/// The type uses chrono through `autumn_web::reexports::chrono`.
20///
21/// ```rust
22/// use autumn_plugin_vanilla::{LocalTime, TimeFormat};
23/// use autumn_web::reexports::chrono::{TimeZone as _, Utc};
24///
25/// let at = Utc.with_ymd_and_hms(2026, 10, 5, 12, 0, 0).unwrap();
26/// let html = LocalTime::new(&at).format(TimeFormat::Date).render().into_string();
27/// assert!(html.starts_with(r#"<time datetime="2026-10-05T12:00:00Z""#), "{html}");
28/// ```
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[must_use]
31pub struct LocalTime {
32    at: DateTime<Utc>,
33    format: TimeFormat,
34    fallback: Option<String>,
35    extra: Attributes,
36}
37
38/// How [`LocalTime`] formats the time.
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
40#[non_exhaustive]
41pub enum TimeFormat {
42    /// Date and time, for example `Oct 5, 2026, 8:00 AM`.
43    #[default]
44    DateTime,
45    /// Date only, for example `Oct 5, 2026`.
46    Date,
47    /// Time only, for example `8:00 AM`.
48    Time,
49    /// Relative to now, for example `2 hours ago`.
50    Relative,
51}
52
53impl TimeFormat {
54    /// The value of `data-vanilla-local-time`.
55    #[must_use]
56    pub const fn name(self) -> &'static str {
57        match self {
58            Self::DateTime => "datetime",
59            Self::Date => "date",
60            Self::Time => "time",
61            Self::Relative => "relative",
62        }
63    }
64
65    /// The `strftime` pattern of the server text.
66    const fn pattern(self) -> &'static str {
67        match self {
68            Self::Date => "%Y-%m-%d",
69            Self::Time => "%H:%M UTC",
70            Self::DateTime | Self::Relative => "%Y-%m-%d %H:%M UTC",
71        }
72    }
73}
74
75impl LocalTime {
76    /// Shows `at`. It converts any time zone to UTC for `datetime`.
77    pub fn new<Tz: TimeZone>(at: &DateTime<Tz>) -> Self {
78        Self {
79            at: at.with_timezone(&Utc),
80            format: TimeFormat::DateTime,
81            fallback: None,
82            extra: Vec::new(),
83        }
84    }
85
86    /// Sets the format.
87    pub const fn format(mut self, format: TimeFormat) -> Self {
88        self.format = format;
89        self
90    }
91
92    /// Sets the text to show without JavaScript. The default is UTC text
93    /// that matches the format, for example `2026-10-05 12:00 UTC`.
94    pub fn fallback(mut self, text: impl Into<String>) -> Self {
95        self.fallback = Some(text.into());
96        self
97    }
98
99    /// The attributes, in render order.
100    #[must_use]
101    pub fn attributes(&self) -> Attributes {
102        let mut attributes = vec![
103            (
104                "datetime",
105                self.at.to_rfc3339_opts(SecondsFormat::Secs, true),
106            ),
107            marker(Behavior::LocalTime),
108        ];
109        if self.format != TimeFormat::DateTime {
110            attributes.push(("data-vanilla-local-time", self.format.name().to_owned()));
111        }
112        attributes.extend(self.extra.iter().cloned());
113        attributes
114    }
115
116    /// Renders the `<time>` element.
117    #[must_use]
118    pub fn render(&self) -> Markup {
119        let text = self
120            .fallback
121            .clone()
122            .unwrap_or_else(|| self.at.format(self.format.pattern()).to_string());
123        element("time", &self.attributes(), html! { (text) })
124    }
125}
126
127common!(LocalTime);
128
129impl maud::Render for LocalTime {
130    fn render(&self) -> Markup {
131        Self::render(self)
132    }
133}