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
| 1 | use oxedyne_fe2o3_core::prelude::*; |
| 2 | |
| 3 | use std::fmt; |
| 4 | |
| 5 | |
| 6 | #[derive(Clone, Debug, PartialEq)] |
| 7 | pub enum Quote { |
| 8 | Single, |
| 9 | Double, |
| 10 | None, |
| 11 | } |
| 12 | |
| 13 | impl Default for Quote { |
| 14 | fn default() -> Self { |
| 15 | Self::None |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | #[derive(Clone, Debug, Default)] |
| 20 | pub struct Indenter { |
| 21 | level: usize, |
| 22 | } |
| 23 | |
| 24 | impl 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 | |
| 65 | new_type!(Stringer, String, Clone, Debug, Eq, Ord, PartialEq, PartialOrd); |
| 66 | |
| 67 | impl fmt::Display for Stringer { |
| 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 69 | write!(f, "{}", self.0) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | impl 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 |