Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.2.3555.5 KB2026-10-160.2.2572.8 KB2026-10-150.2.1580.5 KB2026-10-150.2.0579.9 KB2026-09-150.1.1576.6 KB2026-09-12
Version
0.2.2
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-10-15
Size
572.8 KB
Downloads
28
Checksum
ce26f898ba26eb49c71ebc68597f62f268b83e41bb265b1b5a01688966054588
Dependencies
None

text.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! Small helpers for building strings


/// Replaces characters that have a special meaning in HTML and SVG with entity references
pub fn escape(source: &str) -> String {
    let mut out = String::with_capacity(source.len());
    for character in source.chars() {
        match character {
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '&' => out.push_str("&amp;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            _ => out.push(character),
        }
    }
    out
}


/// Writes a coordinate or a dimension. Trailing zeros are dropped
///
/// Smaller values keep more digits. A font that divides 1em into 2048 units has a scale
/// factor of 0.00048, so rounding every value to 3 decimal places would make it 0, and
/// nothing would be drawn
pub fn number(value: f64) -> String {
    let mut out = String::new();
    push_number(&mut out, value);

    out
}


/// Writes the same text as [`number`] directly at the end of the given string
///
/// After rounding, the value is a decimal that ends at the digits-th decimal place, so it is
/// converted to an integer and written out digit by digit.
/// This avoids both the conversion of a float to decimal text and a temporary String.
/// Thousands of coordinates are written for a single outline, so the difference here directly
/// affects the speed of the output
pub fn push_number(out: &mut String, value: f64) {
    let size = value.abs();
    let digits = if size >= 1.0 {
        3
    } else if size >= 0.1 {
        4
    } else if size >= 0.01 {
        5
    } else if size >= 0.001 {
        6
    } else {
        8
    };
    let step = 10f64.powi(digits);
    let scaled = (value * step).round();

    // Values too large for an integer, and values that are not numbers, fall back to
    // standard formatting
    if !scaled.is_finite() || scaled.abs() >= LARGEST {
        push_formatted(out, scaled / step, digits as usize);

        return;
    }

    let mut whole = scaled as i64;

    if whole < 0 {
        out.push('-');
        whole = -whole;
    }

    let unit = 10i64.pow(digits as u32);
    let mut fraction = whole % unit;
    push_integer(out, whole / unit);

    if fraction == 0 {
        return;
    }

    let mut width = digits as u32;

    while fraction % 10 == 0 {
        fraction /= 10;
        width -= 1;
    }

    out.push('.');
    push_fraction(out, fraction, width);
}


/// The magnitude at which converting to an integer overflows i64
const LARGEST: f64 = 9.0e18;


/// Writes a non-negative integer in decimal
fn push_integer(out: &mut String, value: i64) {
    if value >= 10 {
        push_integer(out, value / 10);
    }

    out.push((b'0' + (value % 10) as u8) as char);
}


/// Writes the fractional part as width digits. Leading zeros are kept too
fn push_fraction(out: &mut String, value: i64, width: u32) {
    let mut unit = 10i64.pow(width - 1);

    while unit > 0 {
        out.push((b'0' + (value / unit % 10) as u8) as char);
        unit /= 10;
    }
}


/// Writes with standard formatting and drops trailing zeros. Only values that cannot be
/// converted to an integer come here
fn push_formatted(out: &mut String, value: f64, digits: usize) {
    let positive_zero = if value == 0.0 { 0.0 } else { value };
    let mut text = format!("{positive_zero:.digits$}");

    if text.contains('.') {
        while text.ends_with('0') {
            text.pop();
        }

        if text.ends_with('.') {
            text.pop();
        }
    }

    out.push_str(&text);
}


/// Replaces a combining accent with a glyph that can stand alone. Needed only when the output
/// is written as characters
pub fn spacing_mark(character: char) -> char {
    match character {
        '\u{300}' => '\u{60}',
        '\u{301}' => '\u{b4}',
        '\u{302}' => '\u{2c6}',
        '\u{303}' => '\u{2dc}',
        '\u{304}' => '\u{af}',
        '\u{306}' => '\u{2d8}',
        '\u{307}' => '\u{2d9}',
        '\u{308}' => '\u{a8}',
        '\u{30c}' => '\u{2c7}',
        '\u{20d7}' => '\u{2192}',
        other => other,
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn escapes_markup() {
        assert_eq!(escape("a<b & c>\"d\""), "a&lt;b &amp; c&gt;&quot;d&quot;");
    }

    #[test]
    fn trims_trailing_zeros() {
        assert_eq!(number(1.5), "1.5");
        assert_eq!(number(2.0), "2");
        assert_eq!(number(-0.0), "0");
        assert_eq!(number(1.23456), "1.235");
    }

    #[test]
    fn small_values_keep_their_digits() {
        assert_eq!(number(1.0 / 2048.0), "0.00048828");
        assert_eq!(number(1.0 / 1000.0), "0.001");
        assert_eq!(number(0.12345), "0.1235");
    }

    #[test]
    fn negative_and_padded_places() {
        assert_eq!(number(-1.5), "-1.5");
        assert_eq!(number(-0.0004), "-0.0004");
        assert_eq!(number(1.005), "1.005");
        assert_eq!(number(0.10001), "0.1");
        assert_eq!(number(-0.00004), "-0.00004");
    }

    /// Values that cannot be converted to an integer use standard formatting. That fallback
    /// also drops trailing zeros
    #[test]
    fn values_too_large_for_an_integer() {
        assert_eq!(number(1.0e19), "10000000000000000000");
        assert_eq!(number(-1.0e19), "-10000000000000000000");
        assert_eq!(number(f64::INFINITY), "inf");
        assert_eq!(number(f64::NAN), "NaN");
    }

    /// Appending to the end does not remove what was written before
    #[test]
    fn writing_into_a_string_leaves_what_is_there() {
        let mut out = String::from("M0");
        push_number(&mut out, 1000.0);
        push_number(&mut out, -0.5);
        assert_eq!(out, "M01000-0.5");
    }
}