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