typst_library/foundations/
repr.rs

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