Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_text/src/string.rs

11.0 KiB, 5 runs

created by r1870400018:1111, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use oxedyne_fe2o3_core::prelude::*;
2
3use std::fmt;
4
5
6#[derive(Clone, Debug, PartialEq)]
7pub enum Quote {
8 Single,
9 Double,
10 None,
11}
12
13impl Default for Quote {
14 fn default() -> Self {
15 Self::None
16 }
17}
18
19#[derive(Clone, Debug, Default)]
20pub struct Indenter {
21 level: usize,
22}
23
24impl Indenter {
25 const INDENT_LIMIT: usize = 50;
26
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 pub fn level(&self) -> usize {
32 self.level
33 }
34
35 pub fn inc(&mut self) {
36 if self.level < Self::INDENT_LIMIT {
37 self.level += 1;
38 }
39 }
40
41 pub fn dec(&mut self) {
42 if self.level > 0 {
43 self.level -= 1;
44 }
45 }
46
47 pub fn plus(&self, n: usize) -> usize {
48 let new_level = self.level + n;
49 if new_level <= Self::INDENT_LIMIT {
50 new_level
51 } else {
52 Self::INDENT_LIMIT
53 }
54 }
55
56 pub fn minus(&self, n: usize) -> usize {
57 if self.level >= n {
58 self.level - n
59 } else {
60 0
61 }
62 }
63}
64
65new_type!(Stringer, String, Clone, Debug, Eq, Ord, PartialEq, PartialOrd);
66
67impl fmt::Display for Stringer {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "{}", self.0)
70 }
71}
72
73impl Stringer {
74
75 pub fn new<S: Into<String>>(s: S) -> Self {
76 Self(s.into())
77 }
78
79 pub fn inner(self) -> String { self.0 }
80
81 /// Takes a one-line string representation containing maps and lists and creates a multi-line
82 /// representation with indentations using the given tab string. Assumes the incoming strings has
83 /// no no newlines, and matching brackets. This can be used with the `Debug` representation of a
84 /// Rust struct. A "no look ahead" policy is used for simplicity and speed.
85 pub fn to_lines<'a>(&self, tab: &'a str) -> Vec<String> {
86 let s = &self.0;
87 let mut lines = Vec::new();
88 let mut indenter = Indenter::new();
89 let mut line = String::new();
90 let mut prev_char: Option<char> = None;
91 let mut printed_char: bool;
92 let mut quote_protection: Quote = Quote::None;
93 let mut backslash_active: bool = false;
94 for c in s.chars() {
95 match c {
96 '"' => {
97 if quote_protection != Quote::Single {
98 if quote_protection == Quote::Double {
99 quote_protection = Quote::None;
100 } else {
101 quote_protection = Quote::Double;
102 }
103 }
104 },
105 '\'' => {
106 if quote_protection != Quote::Double {
107 if quote_protection == Quote::Single {
108 quote_protection = Quote::None;
109 } else {
110 quote_protection = Quote::Single;
111 }
112 }
113 },
114 _ => (),
115 }
116 if quote_protection != Quote::None {
117 if c == '\\' {
118 backslash_active = true;
119 line.push(c);
120 lines.push(line);
121 line = String::new();
122 line.push_str(&tab.repeat(indenter.plus(1)));
123 continue;
124 }
125 match c {
126 ' ' | '\t' | '\n' | '\\' if backslash_active => continue,
127 _ => backslash_active = false,
128 }
129 let opening_quote = (quote_protection == Quote::Single && c == '\'')
130 || (quote_protection == Quote::Double && c == '"' );
131 if !opening_quote {
132 // If we are in the midst of a quoted string, print the character and continue.
133 // Otherwise, we want the opening quote to respond like other characters to important
134 // boundaries like ','.
135 line.push(c);
136 continue;
137 }
138 }
139 if let Some(p) = prev_char {
140 // Ignore single space after these characters.
141 if c == ' ' && (p == ',' || p == '[' || p == '{') {
142 continue;
143 }
144 }
145 printed_char = false;
146
147 // Indentation.
148 match prev_char {
149 Some('[') if c == ']' => {
150 line.push(c);
151 printed_char = true;
152 },
153 Some('{') if c == '}' => {
154 line.push(c);
155 printed_char = true;
156 },
157 Some(',') | Some('[') | Some('{') => {
158 if quote_protection == Quote::None
159 || (quote_protection == Quote::Single && c == '\'')
160 || (quote_protection == Quote::Double && c == '"' )
161 {
162 lines.push(line);
163 line = String::new();
164 if prev_char != Some(',') {
165 // If the previous character is an opening bracket, we increment the
166 // indent.
167 indenter.inc();
168 } else if c == ']' || c == '}' {
169 // The previous character is a ',' and the current character is a closing
170 // bracket, so decrement the indent.
171 indenter.dec();
172 }
173 line.push_str(&tab.repeat(indenter.level()));
174 line.push(c);
175 printed_char = true;
176 }
177 },
178 _ => match c {
179 ']' | '}' if quote_protection == Quote::None => {
180 lines.push(line);
181 line = String::new();
182 indenter.dec();
183 line.push_str(&tab.repeat(indenter.level()));
184 line.push(c);
185 printed_char = true;
186 },
187 _ => (),
188 },
189 }
190 if !printed_char {
191 if line.len() == 0 {
192 line.push_str(&tab.repeat(indenter.level()));
193 }
194 line.push(c);
195 }
196 prev_char = Some(c);
197 }
198 if line.len() > 0 {
199 lines.push(line);
200 }
201 lines
202 }
203
204 pub fn wrap_lines(&self, width: usize, prefix: Option<&str>) -> Vec<String> {
205
206 let lines = self.0.lines().map(String::from);
207 let mut wrapped_lines = Vec::new();
208
209 let prefix_width = prefix.map(|p| p.chars().count()).unwrap_or(0);
210
211 for line in lines {
212 if line.chars().count() <= width {
213 wrapped_lines.push(line);
214 } else {
215 let mut current_line = String::new();
216 let mut current_width = 0;
217 let mut first_word = true;
218
219 for word in line.split_whitespace() {
220 let word_width = word.chars().count();
221
222 if current_width + word_width + 1 > width {
223 if !current_line.is_empty() {
224 wrapped_lines.push(current_line.trim_end().to_string());
225 current_line = prefix.map(|p| p.to_string()).unwrap_or_default();
226 current_width = prefix_width;
227 first_word = true;
228 }
229 }
230
231 if current_width + word_width <= width {
232 if !first_word || current_width > prefix_width {
233 current_line.push(' ');
234 current_width += 1;
235 }
236 current_line.push_str(word);
237 current_width += word_width;
238 first_word = false;
239 } else {
240 let line_with_prefix = prefix
241 .map(|p| fmt!("{}{}", p, word))
242 .unwrap_or(word.to_string());
243 wrapped_lines.push(line_with_prefix);
244 current_line = prefix.map(|p| p.to_string()).unwrap_or_default();
245 current_width = prefix_width;
246 first_word = true;
247 }
248 }
249
250 if !current_line.is_empty() {
251 wrapped_lines.push(current_line.trim_end().to_string());
252 }
253 }
254 }
255
256 wrapped_lines
257 }
258
259 pub fn trim_newline(&mut self) {
260 let s = &mut self.0;
261 if s.ends_with('\n') {
262 s.pop();
263 if s.ends_with('\r') {
264 s.pop();
265 }
266 }
267 }
268
269 /// Insert the given string at regular character intervals, from left to right.
270 ///
271 /// ```ignore
272 /// let s = Stringer::new(format!("{}", 10_000_000));
273 /// assert_eq!(&s.insert_every("_", 3), "10_000_000");
274 ///
275 /// ```
276 pub fn insert_every(&self, sep: &str, every: usize) -> Self {
277 if every == 0 {
278 return Self(self.0.clone());
279 }
280
281 let mut s = String::new();
282 for (index, c) in self.chars().enumerate() {
283 if index > 0 && index % every == 0 {
284 s.push_str(sep);
285 }
286 s.push(c);
287 }
288
289 Self(s.chars().collect())
290 }
291
292 pub fn pad_ends(self) -> Self {
293 Self::new(fmt!(" {} ", self))
294 }
295
296 /// One line of text a person typed, fit to be stored as a label.
297 ///
298 /// Control characters are dropped rather than escaped, because a newline in
299 /// the middle of a name is a paste gone wrong and not a choice; the ends are
300 /// trimmed; and the result is bounded in **characters** rather than bytes, so
301 /// that a limit means the same thing in every script. Nothing is answered
302 /// where nothing readable is left, since a label nobody can tell from an
303 /// empty one is worse than a refusal the caller can explain.
304 pub fn tidy_label(&self, max: usize) -> Option<String> {
305 let mut out = String::with_capacity(self.0.len());
306 for c in self.0.trim().chars() {
307 if c.is_control() {
308 continue;
309 }
310 if out.chars().count() >= max {
311 break;
312 }
313 out.push(c);
314 }
315 let out = out.trim().to_string();
316 if out.is_empty() {
317 None
318 } else {
319 Some(out)
320 }
321 }
322
323 pub fn fit_into(
324 &mut self,
325 new_len: usize,
326 truncation_str: &str,
327 ) {
328 let ts_len = truncation_str.len();
329 if new_len > ts_len {
330 let len = self.chars().count();
331 let mut result = self.0.clone();
332 if new_len < len {
333 result.truncate(new_len - ts_len);
334 result.push_str(truncation_str);
335 }
336 self.0 = result;
337 } else {
338 self.0 = String::new();
339 }
340 }
341}
342