Skip to main content

typst_library/foundations/
repr.rs

1//! Debug representation of values.
2
3use ecow::{EcoString, eco_format};
4use typst_utils::round_with_precision;
5
6use crate::foundations::{Str, Value, func};
7
8/// The Unicode minus sign.
9pub const MINUS_SIGN: &str = "\u{2212}";
10
11/// Returns the string representation of a value.
12///
13/// When inserted into content, most values are displayed as this representation
14/// in monospace with syntax-highlighting. The exceptions are `{none}`,
15/// integers, floats, strings, content, and functions.
16///
17/// = Example <example>
18/// ```example
19/// #none vs #repr(none) \
20/// #"hello" vs #repr("hello") \
21/// #(1, 2) vs #repr((1, 2)) \
22/// #[*Hi*] vs #repr([*Hi*])
23/// ```
24///
25/// = #short-or-long[Debugging Only][For debugging purposes only] <debugging-only>
26/// This function is for debugging purposes. Its output should not be considered
27/// stable and may change at any time.
28///
29/// To be specific, having the same `repr` does not guarantee that values are
30/// equivalent, and `repr` is not a strict inverse of @eval. In the following
31/// example, for readability, the @length is rounded to two significant digits
32/// and the parameter list and body of the @function:unnamed[unnamed `function`]
33/// are omitted.
34///
35/// ```example
36/// #assert(2pt / 3 < 0.67pt)
37/// #repr(2pt / 3)
38///
39/// #repr(x => x + 1)
40/// ```
41#[func(title = "Representation")]
42pub fn repr(
43    /// The value whose string representation to produce.
44    value: Value,
45) -> Str {
46    value.repr().into()
47}
48
49/// A trait that defines the `repr` of a Typst value.
50pub trait Repr {
51    /// Return the debug representation of the value.
52    fn repr(&self) -> EcoString;
53}
54
55/// Format an integer in a base.
56pub fn format_int_with_base(mut n: i64, base: i64) -> EcoString {
57    if n == 0 {
58        return "0".into();
59    }
60
61    // The largest output is `to_base(i64::MIN, 2)`, which is 64 bytes long,
62    // plus the length of the minus sign.
63    const SIZE: usize = 64 + MINUS_SIGN.len();
64    let mut digits = [b'\0'; SIZE];
65    let mut i = SIZE;
66
67    // It's tempting to take the absolute value, but this will fail for i64::MIN.
68    // Instead, we turn n negative, as -i64::MAX is perfectly representable.
69    let negative = n < 0;
70    if n > 0 {
71        n = -n;
72    }
73
74    while n != 0 {
75        let digit = char::from_digit(-(n % base) as u32, base as u32);
76        i -= 1;
77        digits[i] = digit.unwrap_or('?') as u8;
78        n /= base;
79    }
80
81    if negative {
82        let prev = i;
83        i -= MINUS_SIGN.len();
84        digits[i..prev].copy_from_slice(MINUS_SIGN.as_bytes());
85    }
86
87    std::str::from_utf8(&digits[i..]).unwrap_or_default().into()
88}
89
90/// Converts a float to a string representation with a specific precision and a
91/// unit, all with a single allocation.
92///
93/// The returned string is always valid Typst code. As such, it might not be a
94/// float literal. For example, it may return `"float.inf"`.
95pub fn format_float(
96    mut value: f64,
97    precision: Option<u8>,
98    force_separator: bool,
99    unit: &str,
100) -> EcoString {
101    if let Some(p) = precision {
102        value = round_with_precision(value, p as i16);
103    }
104    // Debug for f64 always prints a decimal separator, while Display only does
105    // when necessary.
106    let unit_multiplication = if unit.is_empty() { "" } else { " * 1" };
107    if value.is_nan() {
108        eco_format!("float.nan{unit_multiplication}{unit}")
109    } else if value.is_infinite() {
110        let sign = if value < 0.0 { "-" } else { "" };
111        eco_format!("{sign}float.inf{unit_multiplication}{unit}")
112    } else if force_separator {
113        eco_format!("{value:?}{unit}")
114    } else {
115        eco_format!("{value}{unit}")
116    }
117}
118
119/// Converts a float to a string representation with a precision of three
120/// decimal places. This is intended to be used as part of a larger structure
121/// containing multiple float components, such as colors.
122pub fn format_float_component(value: f64) -> EcoString {
123    format_float(value, Some(3), false, "")
124}
125
126/// Converts a float to a string representation with a precision of two decimal
127/// places, followed by a unit.
128pub fn format_float_with_unit(value: f64, unit: &str) -> EcoString {
129    format_float(value, Some(2), false, unit)
130}
131
132/// Converts a float to a string that can be used to display the float as text.
133pub fn display_float(value: f64) -> EcoString {
134    if value.is_nan() {
135        "NaN".into()
136    } else if value.is_infinite() {
137        let sign = if value < 0.0 { MINUS_SIGN } else { "" };
138        eco_format!("{sign}∞")
139    } else if value < 0.0 {
140        eco_format!("{}{}", MINUS_SIGN, value.abs())
141    } else {
142        eco_format!("{}", value.abs())
143    }
144}
145
146/// Formats pieces separated with commas and a final "and" or "or".
147pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String {
148    let mut buf = String::new();
149    for (i, part) in pieces.iter().enumerate() {
150        match i {
151            0 => {}
152            1 if pieces.len() == 2 => {
153                buf.push(' ');
154                buf.push_str(last);
155                buf.push(' ');
156            }
157            i if i + 1 == pieces.len() => {
158                buf.push_str(", ");
159                buf.push_str(last);
160                buf.push(' ');
161            }
162            _ => buf.push_str(", "),
163        }
164        buf.push_str(part.as_ref());
165    }
166    buf
167}
168
169/// Formats a comma-separated list.
170///
171/// Tries to format horizontally, but falls back to vertical formatting if the
172/// pieces are too long.
173pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String {
174    const MAX_WIDTH: usize = 50;
175
176    let mut buf = String::new();
177    let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>()
178        + 2 * pieces.len().saturating_sub(1);
179
180    if len <= MAX_WIDTH {
181        for (i, piece) in pieces.iter().enumerate() {
182            if i > 0 {
183                buf.push_str(", ");
184            }
185            buf.push_str(piece.as_ref());
186        }
187        if trailing_comma {
188            buf.push(',');
189        }
190    } else {
191        for piece in pieces {
192            buf.push_str(piece.as_ref().trim());
193            buf.push_str(",\n");
194        }
195    }
196
197    buf
198}
199
200/// Formats an array-like construct.
201///
202/// Tries to format horizontally, but falls back to vertical formatting if the
203/// pieces are too long.
204pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String {
205    let list = pretty_comma_list(parts, trailing_comma);
206    let mut buf = String::new();
207    buf.push('(');
208    if list.contains('\n') {
209        buf.push('\n');
210        for (i, line) in list.lines().enumerate() {
211            if i > 0 {
212                buf.push('\n');
213            }
214            buf.push_str("  ");
215            buf.push_str(line);
216        }
217        buf.push('\n');
218    } else {
219        buf.push_str(&list);
220    }
221    buf.push(')');
222    buf
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_to_base() {
231        assert_eq!(&format_int_with_base(0, 10), "0");
232        assert_eq!(&format_int_with_base(0, 16), "0");
233        assert_eq!(&format_int_with_base(0, 36), "0");
234        assert_eq!(
235            &format_int_with_base(i64::MAX, 2),
236            "111111111111111111111111111111111111111111111111111111111111111"
237        );
238        assert_eq!(
239            &format_int_with_base(i64::MIN, 2),
240            "\u{2212}1000000000000000000000000000000000000000000000000000000000000000"
241        );
242        assert_eq!(&format_int_with_base(i64::MAX, 10), "9223372036854775807");
243        assert_eq!(&format_int_with_base(i64::MIN, 10), "\u{2212}9223372036854775808");
244        assert_eq!(&format_int_with_base(i64::MAX, 16), "7fffffffffffffff");
245        assert_eq!(&format_int_with_base(i64::MIN, 16), "\u{2212}8000000000000000");
246        assert_eq!(&format_int_with_base(i64::MAX, 36), "1y2p0ij32e8e7");
247        assert_eq!(&format_int_with_base(i64::MIN, 36), "\u{2212}1y2p0ij32e8e8");
248    }
249}