typst_batch/codegen/
literal.rs1use typst::foundations::Value;
12use typst::layout::{Abs, Angle, Em, Length, Ratio};
13use typst::visualize::Color;
14
15pub fn parse_typst_literal(s: &str) -> Option<Value> {
28 let s = s.trim();
29
30 match s {
32 "auto" => return Some(Value::Auto),
33 "none" => return Some(Value::None),
34 "true" => return Some(Value::Bool(true)),
35 "false" => return Some(Value::Bool(false)),
36 _ => {}
37 }
38
39 if let Some(length) = parse_length(s) {
41 return Some(Value::Length(length));
42 }
43
44 if let Some(angle) = parse_angle(s) {
45 return Some(Value::Angle(angle));
46 }
47
48 if let Some(ratio) = parse_ratio(s) {
49 return Some(Value::Ratio(ratio));
50 }
51
52 if let Some(color) = parse_color(s) {
53 return Some(Value::Color(color));
54 }
55
56 None
57}
58
59pub fn parse_length(s: &str) -> Option<Length> {
71 let s = s.trim();
72
73 for (suffix, factor) in [
80 ("pt", 1.0),
81 ("mm", 2.834_645_669_291_339),
82 ("cm", 28.346_456_692_913_39),
83 ("in", 72.0),
84 ] {
85 if let Some(num_str) = s.strip_suffix(suffix)
86 && let Ok(n) = num_str.trim().parse::<f64>() {
87 return Some(Abs::pt(n * factor).into());
88 }
89 }
90
91 if let Some(num_str) = s.strip_suffix("em")
93 && let Ok(n) = num_str.trim().parse::<f64>() {
94 return Some(Em::new(n).into());
95 }
96
97 None
98}
99
100pub fn parse_angle(s: &str) -> Option<Angle> {
110 let s = s.trim();
111
112 if let Some(num_str) = s.strip_suffix("deg")
113 && let Ok(n) = num_str.trim().parse::<f64>() {
114 return Some(Angle::deg(n));
115 }
116
117 if let Some(num_str) = s.strip_suffix("rad")
118 && let Ok(n) = num_str.trim().parse::<f64>() {
119 return Some(Angle::rad(n));
120 }
121
122 if let Some(num_str) = s.strip_suffix("turn")
123 && let Ok(n) = num_str.trim().parse::<f64>() {
124 return Some(Angle::deg(n * 360.0));
126 }
127
128 None
129}
130
131pub fn parse_ratio(s: &str) -> Option<Ratio> {
141 let s = s.trim();
142
143 if let Some(num_str) = s.strip_suffix('%')
144 && let Ok(n) = num_str.trim().parse::<f64>() {
145 return Some(Ratio::new(n / 100.0));
146 }
147
148 None
149}
150
151pub fn parse_color(s: &str) -> Option<Color> {
164 let s = s.trim();
165
166 if !s.starts_with('#') {
167 return None;
168 }
169
170 let hex = &s[1..];
171
172 match hex.len() {
173 3 => {
175 let chars: Vec<char> = hex.chars().collect();
176 let r = parse_hex_digit(chars[0])? * 17;
177 let g = parse_hex_digit(chars[1])? * 17;
178 let b = parse_hex_digit(chars[2])? * 17;
179 Some(Color::from_u8(r, g, b, 255))
180 }
181 6 => {
183 let r = parse_hex_byte(&hex[0..2])?;
184 let g = parse_hex_byte(&hex[2..4])?;
185 let b = parse_hex_byte(&hex[4..6])?;
186 Some(Color::from_u8(r, g, b, 255))
187 }
188 8 => {
190 let r = parse_hex_byte(&hex[0..2])?;
191 let g = parse_hex_byte(&hex[2..4])?;
192 let b = parse_hex_byte(&hex[4..6])?;
193 let a = parse_hex_byte(&hex[6..8])?;
194 Some(Color::from_u8(r, g, b, a))
195 }
196 _ => None,
197 }
198}
199
200fn parse_hex_digit(c: char) -> Option<u8> {
201 match c {
202 '0'..='9' => Some(c as u8 - b'0'),
203 'a'..='f' => Some(c as u8 - b'a' + 10),
204 'A'..='F' => Some(c as u8 - b'A' + 10),
205 _ => None,
206 }
207}
208
209fn parse_hex_byte(s: &str) -> Option<u8> {
210 u8::from_str_radix(s, 16).ok()
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_parse_length_pt() {
219 let length = parse_length("12pt").unwrap();
220 assert_eq!(length, Abs::pt(12.0).into());
221 }
222
223 #[test]
224 fn test_parse_length_em() {
225 let length = parse_length("1.5em").unwrap();
226 assert_eq!(length, Em::new(1.5).into());
227 }
228
229 #[test]
230 fn test_parse_length_mm() {
231 let length = parse_length("10mm").unwrap();
232 let Length { abs, em: _ } = length;
235 assert!((abs.to_pt() - 28.346_456_692_913_39).abs() < 0.001);
236 }
237
238 #[test]
239 fn test_parse_angle_deg() {
240 let angle = parse_angle("90deg").unwrap();
241 assert_eq!(angle, Angle::deg(90.0));
242 }
243
244 #[test]
245 fn test_parse_angle_rad() {
246 let angle = parse_angle("3.14159rad").unwrap();
247 assert!((angle.to_rad() - 3.14159).abs() < 0.0001);
248 }
249
250 #[test]
251 fn test_parse_ratio() {
252 let ratio = parse_ratio("50%").unwrap();
253 assert_eq!(ratio, Ratio::new(0.5));
254 }
255
256 #[test]
257 fn test_parse_color_short() {
258 let color = parse_color("#f00").unwrap();
259 assert_eq!(color, Color::from_u8(255, 0, 0, 255));
260 }
261
262 #[test]
263 fn test_parse_color_long() {
264 let color = parse_color("#ff0000").unwrap();
265 assert_eq!(color, Color::from_u8(255, 0, 0, 255));
266 }
267
268 #[test]
269 fn test_parse_color_with_alpha() {
270 let color = parse_color("#ff000080").unwrap();
271 assert_eq!(color, Color::from_u8(255, 0, 0, 128));
272 }
273
274 #[test]
275 fn test_parse_special_values() {
276 assert_eq!(parse_typst_literal("auto"), Some(Value::Auto));
277 assert_eq!(parse_typst_literal("none"), Some(Value::None));
278 assert_eq!(parse_typst_literal("true"), Some(Value::Bool(true)));
279 assert_eq!(parse_typst_literal("false"), Some(Value::Bool(false)));
280 }
281
282 #[test]
283 fn test_plain_string() {
284 assert!(parse_typst_literal("hello").is_none());
286 assert!(parse_typst_literal("12").is_none()); assert!(parse_typst_literal("just some text").is_none());
288 }
289}