1use krilla::metadata::{Metadata, TextDirection};
2use typst_library::foundations::{Datetime, Smart};
3use typst_library::layout::Dir;
4use typst_library::text::Locale;
5
6use crate::convert::GlobalContext;
7
8pub(crate) fn build_metadata(gc: &GlobalContext, doc_lang: Option<Locale>) -> Metadata {
9 let creator = format!("Typst {}", env!("CARGO_PKG_VERSION"));
10
11 let lang = doc_lang.unwrap_or(Locale::DEFAULT);
14
15 let dir = if lang.lang.dir() == Dir::RTL {
16 TextDirection::RightToLeft
17 } else {
18 TextDirection::LeftToRight
19 };
20
21 let mut metadata = Metadata::new()
22 .creator(creator)
23 .keywords(gc.document.info.keywords.iter().map(Into::into).collect())
24 .authors(gc.document.info.author.iter().map(Into::into).collect())
25 .language(lang.rfc_3066().to_string());
26
27 if let Some(title) = &gc.document.info.title {
28 metadata = metadata.title(title.to_string());
29 }
30
31 if let Some(description) = &gc.document.info.description {
32 metadata = metadata.description(description.to_string());
33 }
34
35 if let Some(ident) = gc.options.ident.custom() {
36 metadata = metadata.document_id(ident.to_string());
37 }
38
39 if let Some(date) = creation_date(gc) {
40 metadata = metadata.creation_date(date);
41 }
42
43 metadata = metadata.text_direction(dir);
44
45 metadata
46}
47
48pub fn creation_date(gc: &GlobalContext) -> Option<krilla::metadata::DateTime> {
53 let (datetime, tz) = match (gc.document.info.date, gc.options.timestamp) {
54 (Smart::Custom(Some(date)), _) => (date, None),
55 (Smart::Auto, Some(timestamp)) => (timestamp.datetime, Some(timestamp.timezone)),
56 _ => return None,
57 };
58
59 let year = datetime.year().filter(|&y| y >= 0)? as u16;
60
61 let mut kd = krilla::metadata::DateTime::new(year);
62
63 if let Some(month) = datetime.month() {
64 kd = kd.month(month);
65 }
66
67 if let Some(day) = datetime.day() {
68 kd = kd.day(day);
69 }
70
71 if let Some(h) = datetime.hour() {
72 kd = kd.hour(h);
73 }
74
75 if let Some(m) = datetime.minute() {
76 kd = kd.minute(m);
77 }
78
79 if let Some(s) = datetime.second() {
80 kd = kd.second(s);
81 }
82
83 match tz {
84 Some(Timezone::UTC) => kd = kd.utc_offset_hour(0).utc_offset_minute(0),
85 Some(Timezone::Local { hour_offset, minute_offset }) => {
86 kd = kd.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset)
87 }
88 None => {}
89 }
90
91 Some(kd)
92}
93
94#[derive(Debug, Copy, Clone)]
96pub struct Timestamp {
97 pub(crate) datetime: Datetime,
99 pub(crate) timezone: Timezone,
101}
102
103impl Timestamp {
104 pub fn new_utc(datetime: Datetime) -> Self {
106 Self { datetime, timezone: Timezone::UTC }
107 }
108
109 pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option<Self> {
111 let hour_offset = (whole_minute_offset / 60).try_into().ok()?;
112 let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?;
117 match (hour_offset, minute_offset) {
118 (-23..=23, 0..=59) => Some(Self {
121 datetime,
122 timezone: Timezone::Local { hour_offset, minute_offset },
123 }),
124 _ => None,
125 }
126 }
127}
128
129#[derive(Debug, Copy, Clone, Eq, PartialEq)]
131pub enum Timezone {
132 UTC,
134 Local { hour_offset: i8, minute_offset: u8 },
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_timestamp_new_local() {
145 let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap();
146 let test = |whole_minute_offset, expect_timezone| {
147 assert_eq!(
148 Timestamp::new_local(dummy_datetime, whole_minute_offset)
149 .unwrap()
150 .timezone,
151 expect_timezone
152 );
153 };
154
155 test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 });
157 test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 });
158 test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 });
159 test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 });
160 test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 });
161 test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 });
165 test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 });
166 test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 });
167 test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 });
168
169 assert!(Timestamp::new_local(dummy_datetime, 1440).is_none());
171 assert!(Timestamp::new_local(dummy_datetime, -1440).is_none());
172 assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none());
173 assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none());
174 }
175}