smol_layout/
lib.rs

1//! This is a basic newline layout library. It is designed for very limited uses, and users are encouraged to find a better layouting solution.
2
3use std::collections::HashMap;
4
5use core::iter::once;
6use core::mem;
7
8include!("shared.rs");
9include!(concat!(env!("OUT_DIR"), "/tables.rs"));
10
11/// Returns newlines where this text needs it.
12pub fn apply_newlines(
13    input: &str,
14    max_width: usize,
15    font: &HashMap<char, usize>,
16) -> Result<String, NoLegalLinebreakOpportunity> {
17    // Set up our output string and retrieve our linebreak information
18    let mut output = String::new();
19    let mut breakers = linebreaks(input);
20
21    let mut chars: Vec<(char, Option<BreakOpportunity>)> = input
22        .char_indices()
23        .map(|(_, c)| (c, breakers.next().expect("linebreak issue in `inner`").1))
24        .collect();
25
26    // Iterate over our input until
27    // we have successfully processed the whole thing.
28    loop {
29        let mut current_width = 0;
30        let mut break_point: Option<usize> = None;
31        let mut applied_line_break = false;
32
33        for (cursor, (c, break_op)) in chars.iter().enumerate() {
34            // Break on null terminator -- we probably shouldn't find any of these...
35            if *c == '\0' {
36                break;
37            }
38
39            // Reset on newlines
40            if *c == '\n' {
41                current_width = 0;
42                continue;
43            }
44
45            // Add the width of this character
46            current_width += font.get(c).unwrap_or(&0);
47
48            // We weren't over the limit, so we can continue -- but if this is a safe
49            // break point, let's remember that
50            if break_op.is_some() && cursor != 0 {
51                break_point = Some(cursor);
52            }
53
54            // Are we over the max width now? If so, create a linebreak at our last
55            // safe break point
56            if current_width > max_width {
57                if let Some(break_point) = break_point {
58                    use std::fmt::Write;
59
60                    // Create the split
61                    let (prefix, postfix) = chars.split_at(break_point);
62                    let prefix: String = prefix.iter().map(|&(c, _)| c).collect();
63                    writeln!(output, "{}", prefix).unwrap();
64
65                    // We will now modify chars so that if we need to run again, we will only be
66                    // iterating on the unprocessed characters.
67                    chars = postfix.to_vec();
68                    applied_line_break = true;
69                    break;
70                } else {
71                    return Err(NoLegalLinebreakOpportunity);
72                }
73            }
74        }
75
76        // Once we're here, we check if we made it to the end of our characters.
77        // If we did, we're done!
78        if !applied_line_break {
79            break;
80        }
81    }
82
83    // push in the final characters into the str
84    let s: String = chars.into_iter().map(|n| n.0).collect();
85    output.push_str(&s); // pushing the last bit in!
86    Ok(output)
87}
88
89#[derive(Debug, PartialEq, Eq, Clone, Copy)]
90pub struct NoLegalLinebreakOpportunity;
91
92fn break_property(codepoint: u32) -> BreakClass {
93    let codepoint = codepoint as usize;
94    match PAGE_INDICES.get(codepoint >> 8) {
95        Some(&page_idx) if page_idx & UNIFORM_PAGE != 0 => unsafe {
96            mem::transmute::<u8, BreakClass>((page_idx & !UNIFORM_PAGE) as u8)
97        },
98        Some(&page_idx) => BREAK_PROP_DATA[page_idx][codepoint & 0xFF],
99        None => BreakClass::Unknown,
100    }
101}
102
103/// Break opportunity type.
104#[derive(Copy, Clone, PartialEq, Eq, Debug)]
105enum BreakOpportunity {
106    /// A line must break at this spot.
107    Mandatory,
108    /// A line is allowed to end at this spot.
109    Allowed,
110}
111
112/// Returns an iterator over line break opportunities in the specified string.
113fn linebreaks(s: &str) -> impl Iterator<Item = (usize, Option<BreakOpportunity>)> + Clone + '_ {
114    use BreakOpportunity::{Allowed, Mandatory};
115
116    s.char_indices()
117        .map(|(i, c)| (i, break_property(c as u32) as u8))
118        .chain(once((s.len(), eot)))
119        .scan((sot, false), |state, (i, cls)| {
120            // ZWJ is handled outside the table to reduce its size
121            let val = PAIR_TABLE[state.0 as usize][cls as usize];
122            let is_mandatory = val & MANDATORY_BREAK_BIT != 0;
123            let is_break = val & ALLOWED_BREAK_BIT != 0 && (!state.1 || is_mandatory);
124            *state = (
125                val & !(ALLOWED_BREAK_BIT | MANDATORY_BREAK_BIT),
126                cls == BreakClass::ZeroWidthJoiner as u8,
127            );
128
129            Some((i, is_break, is_mandatory))
130        })
131        .map(|(i, is_break, is_mandatory)| {
132            if is_break {
133                (i, Some(if is_mandatory { Mandatory } else { Allowed }))
134            } else {
135                (i, None)
136            }
137        })
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn make_font() -> HashMap<char, usize> {
145        (0..=255)
146            .filter_map(char::from_u32)
147            .map(|c| (c, 1))
148            .collect()
149    }
150
151    #[test]
152    fn basic() {
153        assert_eq!(
154            apply_newlines("This is a simple newline string.", 35, &make_font()).unwrap(),
155            "This is a simple newline string."
156        );
157
158        assert_eq!(
159            apply_newlines(
160                "This is a simple newline string. But then it gets a little longer.",
161                35,
162                &make_font()
163            )
164            .unwrap(),
165            "This is a simple newline string. \nBut then it gets a little longer."
166        );
167
168        assert_eq!(
169            apply_newlines("Supercalifragalisticexpialidocious", 30, &make_font()).unwrap_err(),
170            NoLegalLinebreakOpportunity
171        );
172
173        assert_eq!(apply_newlines("≤", 30, &make_font()).unwrap(), "≤");
174    }
175}