1use std::collections::HashMap;
4
5use core::iter::once;
6use core::mem;
7
8include!("shared.rs");
9include!(concat!(env!("OUT_DIR"), "/tables.rs"));
10
11pub fn apply_newlines(
13 input: &str,
14 max_width: usize,
15 font: &HashMap<char, usize>,
16) -> Result<String, NoLegalLinebreakOpportunity> {
17 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 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 if *c == '\0' {
36 break;
37 }
38
39 if *c == '\n' {
41 current_width = 0;
42 continue;
43 }
44
45 current_width += font.get(c).unwrap_or(&0);
47
48 if break_op.is_some() && cursor != 0 {
51 break_point = Some(cursor);
52 }
53
54 if current_width > max_width {
57 if let Some(break_point) = break_point {
58 use std::fmt::Write;
59
60 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 chars = postfix.to_vec();
68 applied_line_break = true;
69 break;
70 } else {
71 return Err(NoLegalLinebreakOpportunity);
72 }
73 }
74 }
75
76 if !applied_line_break {
79 break;
80 }
81 }
82
83 let s: String = chars.into_iter().map(|n| n.0).collect();
85 output.push_str(&s); 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#[derive(Copy, Clone, PartialEq, Eq, Debug)]
105enum BreakOpportunity {
106 Mandatory,
108 Allowed,
110}
111
112fn 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 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}