Skip to main content

ftml/render/html/element/
date.rs

1/*
2 * render/html/element/date.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2026 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21use super::prelude::*;
22use crate::tree::DateItem;
23
24pub fn render_date(
25    ctx: &mut HtmlContext,
26    date: DateItem,
27    date_format: Option<&str>,
28    hover: bool,
29) {
30    let formatted_datetime = date.format_or_default(date_format, ctx.language());
31
32    match ctx.layout() {
33        Layout::Wikidot => {
34            render_date_wikidot(ctx, date, date_format, hover, &formatted_datetime)
35        }
36        Layout::Wikijump => {
37            render_date_wikijump(ctx, date, date_format, hover, &formatted_datetime)
38        }
39    }
40}
41
42fn render_date_wikidot(
43    ctx: &mut HtmlContext,
44    date: DateItem,
45    date_format: Option<&str>,
46    hover: bool,
47    formatted_datetime: &str,
48) {
49    let timestamp = date.timestamp();
50    let mut class = format!("odate time_{timestamp}");
51    push_date_format_class(&mut class, date_format, hover);
52    let style = if hover {
53        "cursor: help; display: inline;"
54    } else {
55        "display: inline;"
56    };
57
58    ctx.html()
59        .span()
60        .attr(attr!(
61            "class" => &class,
62            "style" => style,
63        ))
64        .contents(formatted_datetime);
65}
66
67fn render_date_wikijump(
68    ctx: &mut HtmlContext,
69    date: DateItem,
70    date_format: Option<&str>,
71    hover: bool,
72    formatted_datetime: &str,
73) {
74    let timestamp = str!(date.timestamp());
75    let delta = str!(date.time_since());
76    let mut class = str!("wj-date");
77
78    if hover {
79        class.push_str(" wj-date-hover");
80    }
81
82    push_date_format_class(&mut class, date_format, false);
83
84    ctx.html()
85        .span()
86        .attr(attr!(
87            "class" => &class,
88            "data-timestamp" => &timestamp,
89            "data-delta" => &delta,
90        ))
91        .contents(formatted_datetime);
92}
93
94fn push_date_format_class(
95    class: &mut String,
96    date_format: Option<&str>,
97    append_ago_hover: bool,
98) {
99    if let Some(date_format) = date_format {
100        class.push_str(" format_");
101        class.push_str(&encode_date_format(date_format));
102
103        if append_ago_hover {
104            class.push_str("%7Cagohover");
105        }
106    } else if append_ago_hover {
107        class.push_str(" format_");
108        class.push_str("%7Cagohover");
109    }
110}
111
112fn encode_date_format(date_format: &str) -> String {
113    let mut encoded = String::new();
114    for byte in date_format.bytes() {
115        match byte {
116            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
117                encoded.push(char::from(byte));
118            }
119            _ => {
120                encoded.push('%');
121                str_write!(&mut encoded, "{byte:02X}");
122            }
123        }
124    }
125
126    encoded
127}
128
129#[test]
130fn date_format_encoding() {
131    assert_eq!(encode_date_format("%d. %m. %Y"), "%25d.%20%25m.%20%25Y");
132}
133
134#[test]
135fn wikidot_date_class_includes_format() {
136    let mut class = str!("odate time_1216153821");
137    push_date_format_class(&mut class, Some("%d. %m. %Y"), false);
138
139    assert_eq!(class, "odate time_1216153821 format_%25d.%20%25m.%20%25Y");
140}
141
142#[test]
143fn wikidot_date_class_includes_ago_hover() {
144    let mut class = str!("odate time_1216153821");
145    push_date_format_class(&mut class, Some("%d. %m. %Y"), true);
146
147    assert_eq!(
148        class,
149        "odate time_1216153821 format_%25d.%20%25m.%20%25Y%7Cagohover"
150    );
151}
152
153#[test]
154fn wikidot_date_class_allows_ago_hover_without_format() {
155    let mut class = str!("odate time_1216153821");
156    push_date_format_class(&mut class, None, true);
157
158    assert_eq!(class, "odate time_1216153821 format_%7Cagohover");
159}
160
161#[test]
162fn wikijump_date_class_includes_format() {
163    let mut class = str!("wj-date");
164    push_date_format_class(&mut class, Some("%d. %m. %Y"), false);
165
166    assert_eq!(class, "wj-date format_%25d.%20%25m.%20%25Y");
167}