1use std::collections::HashMap;
27use std::{fmt, fs};
28
29#[derive(Debug)]
30struct Layout {
31 layout: i32,
32}
33
34impl Layout {
35 fn new(full_layout: Option<i32>, old_layout: i32) -> Layout {
36 if let Some(full_layout) = full_layout {
37 Self {
38 layout: full_layout,
39 }
40 } else {
41 Self { layout: old_layout }
42 }
43 }
44 fn has_rule_1(&self) -> bool {
45 return self.layout & 1 != 0;
46 }
47 fn has_rule_2(&self) -> bool {
48 return self.layout & 2 != 0;
49 }
50 fn has_rule_3(&self) -> bool {
51 return self.layout & 4 != 0;
52 }
53 fn has_rule_4(&self) -> bool {
54 return self.layout & 8 != 0;
55 }
56 }
63
64#[derive(Debug)]
66pub struct FIGfont {
67 pub header_line: HeaderLine,
68 pub comments: String,
69 pub fonts: HashMap<u32, FIGcharacter>,
70}
71
72impl FIGfont {
73 fn read_font_file(filename: &str) -> Result<String, String> {
74 fs::read_to_string(filename).map_err(|e| format!("{e:?}"))
75 }
76
77 fn read_header_line(header_line: &str) -> Result<HeaderLine, String> {
78 HeaderLine::try_from(header_line)
79 }
80
81 fn read_comments(lines: &[&str], comment_count: i32) -> Result<String, String> {
82 let length = lines.len() as i32;
83 if length < comment_count + 1 {
84 Err("can't get comments from font".to_string())
85 } else {
86 let comment = lines[1..(1 + comment_count) as usize].join("\n");
87 Ok(comment)
88 }
89 }
90
91 fn extract_one_line(
92 lines: &[&str],
93 index: usize,
94 height: usize,
95 is_last_index: bool,
96 ) -> Result<String, String> {
97 let line = lines
98 .get(index)
99 .ok_or(format!("can't get line at specified index:{index}"))?;
100
101 let mut width = line.len() - 1;
102 if is_last_index && height != 1 {
103 width -= 1;
104 }
105
106 Ok(line[..width].to_string())
107 }
108
109 fn extract_one_font(
110 lines: &[&str],
111 code: u32,
112 start_index: usize,
113 height: usize,
114 ) -> Result<FIGcharacter, String> {
115 let mut characters = vec![];
116 for i in 0..height {
117 let index = start_index + i as usize;
118 let is_last_index = i == height - 1;
119 let one_line_character =
120 FIGfont::extract_one_line(lines, index, height, is_last_index)?;
121 characters.push(one_line_character);
122 }
123 let width = characters[0].len() as u32;
124 let height = height as u32;
125
126 Ok(FIGcharacter {
127 code,
128 characters,
129 width,
130 height,
131 })
132 }
133
134 fn read_required_font(
136 lines: &[&str],
137 headerline: &HeaderLine,
138 map: &mut HashMap<u32, FIGcharacter>,
139 ) -> Result<(), String> {
140 let offset = (1 + headerline.comment_lines) as usize;
141 let height = headerline.height as usize;
142 let size = lines.len();
143
144 for i in 0..=94 {
145 let code = (i + 32) as u32;
146 let start_index = offset + i * height;
147 if start_index >= size {
148 break;
149 }
150
151 let font = FIGfont::extract_one_font(lines, code, start_index, height)?;
152 map.insert(code, font);
153 }
154
155 let offset = offset + 95 * height;
156 let required_deutsch_characters_codes: [u32; 7] = [196, 214, 220, 228, 246, 252, 223];
157 for (i, code) in required_deutsch_characters_codes.iter().enumerate() {
158 let start_index = offset + i * height;
159 if start_index >= size {
160 break;
161 }
162
163 let font = FIGfont::extract_one_font(lines, *code, start_index, height)?;
164 map.insert(*code, font);
165 }
166
167 Ok(())
168 }
169
170 fn extract_codetag_font_code(lines: &[&str], index: usize) -> Result<u32, String> {
171 let line = lines
172 .get(index)
173 .ok_or_else(|| "get codetag line error".to_string())?;
174
175 let infos: Vec<&str> = line.trim().split(' ').collect();
176 if infos.is_empty() {
177 return Err("extract code for codetag font error".to_string());
178 }
179
180 let code = infos[0].trim();
181
182 let code = if let Some(s) = code.strip_prefix("0x") {
183 u32::from_str_radix(s, 16)
184 } else if let Some(s) = code.strip_prefix("0X") {
185 u32::from_str_radix(s, 16)
186 } else if let Some(s) = code.strip_prefix('0') {
187 u32::from_str_radix(s, 8)
188 } else {
189 code.parse()
190 };
191
192 code.map_err(|e| format!("{e:?}"))
193 }
194
195 fn read_codetag_font(
196 lines: &[&str],
197 headerline: &HeaderLine,
198 map: &mut HashMap<u32, FIGcharacter>,
199 ) -> Result<(), String> {
200 let offset = (1 + headerline.comment_lines + 102 * headerline.height) as usize;
201 let codetag_height = (headerline.height + 1) as usize;
202 let codetag_lines = lines.len() - offset;
203
204 if codetag_lines % codetag_height != 0 {
205 return Err("codetag font is illegal.".to_string());
206 }
207
208 let size = codetag_lines / codetag_height;
209
210 for i in 0..size {
211 let start_index = offset + i * codetag_height;
212 if start_index >= lines.len() {
213 break;
214 }
215
216 let code = FIGfont::extract_codetag_font_code(lines, start_index)?;
217 let font = FIGfont::extract_one_font(
218 lines,
219 code,
220 start_index + 1,
221 headerline.height as usize,
222 )?;
223 map.insert(code, font);
224 }
225
226 Ok(())
227 }
228
229 fn read_fonts(
230 lines: &[&str],
231 headerline: &HeaderLine,
232 ) -> Result<HashMap<u32, FIGcharacter>, String> {
233 let mut map = HashMap::new();
234 FIGfont::read_required_font(lines, headerline, &mut map)?;
235 FIGfont::read_codetag_font(lines, headerline, &mut map)?;
236 Ok(map)
237 }
238
239 pub fn from_content(contents: &str) -> Result<FIGfont, String> {
241 let lines: Vec<&str> = contents.lines().collect();
242
243 if lines.is_empty() {
244 return Err("can not generate FIGlet font from empty string".to_string());
245 }
246
247 let header_line = FIGfont::read_header_line(lines.first().unwrap())?;
248 let comments = FIGfont::read_comments(&lines, header_line.comment_lines)?;
249 let fonts = FIGfont::read_fonts(&lines, &header_line)?;
250
251 Ok(FIGfont {
252 header_line,
253 comments,
254 fonts,
255 })
256 }
257
258 pub fn from_file(fontname: &str) -> Result<FIGfont, String> {
260 let contents = FIGfont::read_font_file(fontname)?;
261 FIGfont::from_content(&contents)
262 }
263
264 pub fn standard() -> Result<FIGfont, String> {
268 let contents = std::include_str!("standard.flf");
269 FIGfont::from_content(contents)
270 }
271
272 pub fn convert(&self, message: &str) -> Option<FIGure> {
274 if message.is_empty() {
275 return None;
276 }
277
278 let mut characters: Vec<&FIGcharacter> = vec![];
279 for ch in message.chars() {
280 let code = ch as u32;
281 if let Some(character) = self.fonts.get(&code) {
282 characters.push(character);
283 }
284 }
285
286 if characters.is_empty() {
287 return None;
288 }
289
290 Some(FIGure {
291 characters,
292 height: self.header_line.height as u32,
293 layout: Layout::new(self.header_line.full_layout, self.header_line.old_layout),
294 hardblank: self.header_line.hardblank,
295 })
296 }
297}
298
299#[derive(Debug)]
303pub struct HeaderLine {
304 pub header_line: String,
305
306 pub signature: String,
308 pub hardblank: char,
309 pub height: i32,
310 pub baseline: i32,
311 pub max_length: i32,
312 pub old_layout: i32, pub comment_lines: i32,
314
315 pub print_direction: Option<i32>,
317 pub full_layout: Option<i32>, pub codetag_count: Option<i32>,
319}
320
321impl HeaderLine {
322 fn extract_signature_with_hardblank(
323 signature_with_hardblank: &str,
324 ) -> Result<(String, char), String> {
325 if signature_with_hardblank.len() < 6 {
326 Err("can't get signature with hardblank from first line of font".to_string())
327 } else {
328 let hardblank_index = signature_with_hardblank.len() - 1;
329 let signature = &signature_with_hardblank[..hardblank_index];
330 let hardblank = signature_with_hardblank[hardblank_index..]
331 .chars()
332 .next()
333 .unwrap();
334
335 Ok((String::from(signature), hardblank))
336 }
337 }
338
339 fn extract_required_info(infos: &[&str], index: usize, field: &str) -> Result<i32, String> {
340 let val = match infos.get(index) {
341 Some(val) => Ok(val),
342 None => Err(format!(
343 "can't get field:{field} index:{index} from {}",
344 infos.join(",")
345 )),
346 }?;
347
348 val.parse()
349 .map_err(|_| format!("can't parse required field:{field} of {val} to i32"))
350 }
351
352 fn extract_optional_info(infos: &[&str], index: usize, _field: &str) -> Option<i32> {
353 if let Some(val) = infos.get(index) {
354 val.parse().ok()
355 } else {
356 None
357 }
358 }
359}
360
361impl TryFrom<&str> for HeaderLine {
362 type Error = String;
363
364 fn try_from(header_line: &str) -> Result<Self, Self::Error> {
365 let infos: Vec<&str> = header_line.trim().split(' ').collect();
366
367 if infos.len() < 6 {
368 return Err("headerline is illegal".to_string());
369 }
370
371 let signature_with_hardblank =
372 HeaderLine::extract_signature_with_hardblank(infos.first().unwrap())?;
373
374 let height = HeaderLine::extract_required_info(&infos, 1, "height")?;
375 let baseline = HeaderLine::extract_required_info(&infos, 2, "baseline")?;
376 let max_length = HeaderLine::extract_required_info(&infos, 3, "max length")?;
377 let old_layout = HeaderLine::extract_required_info(&infos, 4, "old layout")?;
378 let comment_lines = HeaderLine::extract_required_info(&infos, 5, "comment lines")?;
379
380 let print_direction = HeaderLine::extract_optional_info(&infos, 6, "print direction");
381 let full_layout = HeaderLine::extract_optional_info(&infos, 7, "full layout");
382 let codetag_count = HeaderLine::extract_optional_info(&infos, 8, "codetag count");
383
384 Ok(HeaderLine {
385 header_line: String::from(header_line),
386 signature: signature_with_hardblank.0,
387 hardblank: signature_with_hardblank.1,
388 height,
389 baseline,
390 max_length,
391 old_layout,
392 comment_lines,
393 print_direction,
394 full_layout,
395 codetag_count,
396 })
397 }
398}
399
400#[derive(Debug)]
402pub struct FIGcharacter {
403 pub code: u32,
404 pub characters: Vec<String>,
405 pub width: u32,
406 pub height: u32,
407}
408
409impl fmt::Display for FIGcharacter {
410 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
411 write!(f, "{}", self.characters.join("\n"))
412 }
413}
414
415#[derive(Debug)]
417pub struct FIGure<'a> {
418 pub characters: Vec<&'a FIGcharacter>,
419 pub height: u32,
420 layout: Layout,
421 hardblank: char,
422}
423
424impl<'a> FIGure<'a> {
425 fn is_not_empty(&self) -> bool {
426 !self.characters.is_empty() && self.height > 0
427 }
428}
429
430fn first_non_space(a: impl Iterator<Item = char>, len: usize) -> (char, usize) {
431 for (idx, c) in a.enumerate() {
432 if !c.is_whitespace() {
433 return (c, idx);
434 }
435 }
436 (' ', len)
437}
438
439fn rule_2_char(c: char) -> bool {
440 return c == '|'
441 || c == '/'
442 || c == '\\'
443 || c == '['
444 || c == ']'
445 || c == '{'
446 || c == '}'
447 || c == '('
448 || c == ')'
449 || c == '<'
450 || c == '>';
451}
452
453fn rule_3_klass(c: char) -> u32 {
454 match c {
455 '|' => 1,
456 '/' => 2,
457 '\\' => 2,
458 '[' => 3,
459 ']' => 3,
460 '{' => 4,
461 '}' => 4,
462 '(' => 5,
463 ')' => 5,
464 '<' => 6,
465 '>' => 6,
466 _ => 0,
467 }
468}
469
470fn rule_4_chars(a: char, b: char) -> bool {
471 match a {
472 '(' => b == ')',
473 ')' => b == '(',
474 '[' => b == ']',
475 ']' => b == '[',
476 '{' => b == '}',
477 '}' => b == '{',
478 _ => false,
479 }
480}
481
482fn compute_kerning(a: &String, b: &String, layout: &Layout) -> (usize, usize) {
483 let (a_char, a_k) = first_non_space(a.chars().rev(), a.len());
484 let (b_char, b_k) = first_non_space(b.chars(), b.len());
485 if layout.has_rule_1() && a_char == b_char {
486 return (a_k + 1, b_k);
487 }
488 if layout.has_rule_2() && a_char == '_' && rule_2_char(b_char) {
489 return (a_k, b_k + 1);
490 }
491 if layout.has_rule_2() && b_char == '_' && rule_2_char(a_char) {
492 return (a_k, b_k + 1);
493 }
494 let a_3_klass = rule_3_klass(a_char);
495 let b_3_klass = rule_3_klass(b_char);
496 if layout.has_rule_3() && a_3_klass != b_3_klass && a_3_klass > 0 && b_3_klass > 0 {
497 return (a_k + 1, b_k);
498 }
499 if layout.has_rule_4() && rule_4_chars(a_char, b_char) {
500 return (a_k + 1, b_k);
501 }
502
503 return (a_k + 1, b_k - 1);
504}
505
506fn merge_string(a: &String, b: &String, left_kernel: usize, right_kernel: usize) -> String {
507 if a.len() <= left_kernel {
508 b.to_owned()
509 } else {
510 let (char_a, a_k) = first_non_space(a.chars().rev(), a.len());
511 let (char_b, b_k) = first_non_space(b.chars(), b.len());
512 let (middle_c, a_off, b_off) = if a_k < left_kernel
513 && rule_3_klass(char_a) >= rule_3_klass(char_b)
514 && !rule_4_chars(char_a, char_b)
515 {
516 (None, a_k, left_kernel + right_kernel - a_k)
517 } else if b_k < right_kernel
518 && rule_3_klass(char_a) <= rule_3_klass(char_b)
519 && !rule_4_chars(char_a, char_b)
520 {
521 (None, left_kernel + right_kernel - b_k, b_k)
522 } else if left_kernel == 0 {
523 (None, 0, right_kernel)
524 } else {
525 let a_c = a.chars().nth(a.len() - left_kernel).unwrap();
526 let b_c = b.chars().nth(right_kernel).unwrap();
527 if a_c == ' ' || rule_3_klass(a_c) < rule_3_klass(b_c) && rule_3_klass(a_c) > 0 {
528 (None, left_kernel, right_kernel)
529 } else if rule_4_chars(a_c, b_c) {
530 (Some("|"), left_kernel, right_kernel + 1)
531 } else {
532 (None, left_kernel - 1, right_kernel + 1)
533 }
534 };
535 match middle_c {
536 None => a[..a.len() - a_off].to_string() + &b[b_off..],
537 Some(middle_c) => a[..a.len() - a_off].to_string() + middle_c + &b[b_off..],
538 }
539 }
540}
541
542impl<'a> fmt::Display for FIGure<'a> {
543 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
544 if self.is_not_empty() {
545 let mut kerning: Vec<(usize, usize)> = vec![(0, 0)];
546 for i in 1..self.characters.len() {
547 kerning.push(
548 self.characters[i - 1]
549 .characters
550 .iter()
551 .zip(self.characters[i].characters.iter())
552 .map(|(lc, rc)| {
553 let k = compute_kerning(lc, rc, &self.layout);
554 k
555 })
556 .reduce(|acc, c| if acc.0 + acc.1 < c.0 + c.1 { acc } else { c })
557 .unwrap_or((0, 0)),
558 );
559 }
560 let mut rs: Vec<String> = vec![];
561 rs.resize(self.height as usize, String::new());
562 for (character, (left_kerning, right_kerning)) in
563 self.characters.iter().zip(kerning.iter())
564 {
565 rs = rs
566 .into_iter()
567 .zip(character.characters.iter())
568 .map(|(r, c)| merge_string(&r, &c, *left_kerning, *right_kerning))
569 .collect();
570 }
571
572 write!(f, "{}", rs.join("\n").replace(self.hardblank, " "))
573 } else {
574 write!(f, "")
575 }
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn test_new_headerline() {
585 let line = "flf2a$ 6 5 20 15 3 0 143 229";
586 let headerline = HeaderLine::try_from(line);
587 assert!(headerline.is_ok());
588 let headerline = headerline.unwrap();
589
590 assert_eq!(line, headerline.header_line);
591 assert_eq!("flf2a", headerline.signature);
592 assert_eq!('$', headerline.hardblank);
593 assert_eq!(6, headerline.height);
594 assert_eq!(5, headerline.baseline);
595 assert_eq!(20, headerline.max_length);
596 assert_eq!(15, headerline.old_layout);
597 assert_eq!(3, headerline.comment_lines);
598 assert_eq!(Some(0), headerline.print_direction);
599 assert_eq!(Some(143), headerline.full_layout);
600 assert_eq!(Some(229), headerline.codetag_count);
601 }
602
603 #[test]
604 fn test_new_figfont() {
605 let font = FIGfont::standard();
606 assert!(font.is_ok());
607 let font = font.unwrap();
608
609 let headerline = font.header_line;
610 assert_eq!("flf2a$ 6 5 16 15 11 0 24463", headerline.header_line);
611 assert_eq!("flf2a", headerline.signature);
612 assert_eq!('$', headerline.hardblank);
613 assert_eq!(6, headerline.height);
614 assert_eq!(5, headerline.baseline);
615 assert_eq!(16, headerline.max_length);
616 assert_eq!(15, headerline.old_layout);
617 assert_eq!(11, headerline.comment_lines);
618 assert_eq!(Some(0), headerline.print_direction);
619 assert_eq!(Some(24463), headerline.full_layout);
620 assert_eq!(None, headerline.codetag_count);
621
622 assert_eq!(
623 "Standard by Glenn Chappell & Ian Chai 3/93 -- based on Frank's .sig
624Includes ISO Latin-1
625figlet release 2.1 -- 12 Aug 1994
626Modified for figlet 2.2 by John Cowan <cowan@ccil.org>
627 to add Latin-{2,3,4,5} support (Unicode U+0100-017F).
628Permission is hereby given to modify this font, as long as the
629modifier's name is placed on a comment line.
630
631Modified by Paul Burton <solution@earthlink.net> 12/96 to include new parameter
632supported by FIGlet and FIGWin. May also be slightly modified for better use
633of new full-width/kern/smush alternatives, but default output is NOT changed.",
634 font.comments
635 );
636
637 let one_font = font.fonts.get(&('F' as u32));
638 assert!(one_font.is_some());
639
640 let one_font = one_font.unwrap();
641 assert_eq!(70, one_font.code);
642 assert_eq!(8, one_font.width);
643 assert_eq!(6, one_font.height);
644
645 assert_eq!(6, one_font.characters.len());
646 assert_eq!(" _____ ", one_font.characters.get(0).unwrap());
647 assert_eq!(" | ___|", one_font.characters.get(1).unwrap());
648 assert_eq!(" | |_ ", one_font.characters.get(2).unwrap());
649 assert_eq!(" | _| ", one_font.characters.get(3).unwrap());
650 assert_eq!(" |_| ", one_font.characters.get(4).unwrap());
651 assert_eq!(" ", one_font.characters.get(5).unwrap());
652 }
653
654 #[test]
655 fn test_convert() {
656 let standard_font = FIGfont::standard();
657 assert!(standard_font.is_ok());
658 let standard_font = standard_font.unwrap();
659
660 let figure = standard_font.convert("FIGlet");
661 assert!(figure.is_some());
662
663 let figure = figure.unwrap();
664 assert_eq!(6, figure.height);
665 assert_eq!(6, figure.characters.len());
666
667 let f = figure.characters.get(0).unwrap();
668 assert_eq!(figure.height, f.height);
669 assert_eq!(8, f.width);
670 assert_eq!(" _____ ", f.characters.get(0).unwrap());
671 assert_eq!(" | ___|", f.characters.get(1).unwrap());
672 assert_eq!(" | |_ ", f.characters.get(2).unwrap());
673 assert_eq!(" | _| ", f.characters.get(3).unwrap());
674 assert_eq!(" |_| ", f.characters.get(4).unwrap());
675 assert_eq!(" ", f.characters.get(5).unwrap());
676
677 let i = figure.characters.get(1).unwrap();
678 assert_eq!(figure.height, i.height);
679 assert_eq!(6, i.width);
680 assert_eq!(" ___ ", i.characters.get(0).unwrap());
681 assert_eq!(" |_ _|", i.characters.get(1).unwrap());
682 assert_eq!(" | | ", i.characters.get(2).unwrap());
683 assert_eq!(" | | ", i.characters.get(3).unwrap());
684 assert_eq!(" |___|", i.characters.get(4).unwrap());
685 assert_eq!(" ", i.characters.get(5).unwrap());
686
687 let g = figure.characters.get(2).unwrap();
688 assert_eq!(figure.height, g.height);
689 assert_eq!(8, g.width);
690 assert_eq!(r" ____ ", g.characters.get(0).unwrap());
691 assert_eq!(r" / ___|", g.characters.get(1).unwrap());
692 assert_eq!(r" | | _ ", g.characters.get(2).unwrap());
693 assert_eq!(r" | |_| |", g.characters.get(3).unwrap());
694 assert_eq!(r" \____|", g.characters.get(4).unwrap());
695 assert_eq!(r" ", g.characters.get(5).unwrap());
696
697 let l = figure.characters.get(3).unwrap();
698 assert_eq!(figure.height, l.height);
699 assert_eq!(4, l.width);
700 assert_eq!(" _ ", l.characters.get(0).unwrap());
701 assert_eq!(" | |", l.characters.get(1).unwrap());
702 assert_eq!(" | |", l.characters.get(2).unwrap());
703 assert_eq!(" | |", l.characters.get(3).unwrap());
704 assert_eq!(" |_|", l.characters.get(4).unwrap());
705 assert_eq!(" ", l.characters.get(5).unwrap());
706
707 let e = figure.characters.get(4).unwrap();
708 assert_eq!(figure.height, e.height);
709 assert_eq!(7, e.width);
710 assert_eq!(r" ", e.characters.get(0).unwrap());
711 assert_eq!(r" ___ ", e.characters.get(1).unwrap());
712 assert_eq!(r" / _ \", e.characters.get(2).unwrap());
713 assert_eq!(r" | __/", e.characters.get(3).unwrap());
714 assert_eq!(r" \___|", e.characters.get(4).unwrap());
715 assert_eq!(r" ", e.characters.get(5).unwrap());
716
717 let t = figure.characters.get(5).unwrap();
718 assert_eq!(figure.height, t.height);
719 assert_eq!(6, t.width);
720 assert_eq!(r" _ ", t.characters.get(0).unwrap());
721 assert_eq!(r" | |_ ", t.characters.get(1).unwrap());
722 assert_eq!(r" | __|", t.characters.get(2).unwrap());
723 assert_eq!(r" | |_ ", t.characters.get(3).unwrap());
724 assert_eq!(r" \__|", t.characters.get(4).unwrap());
725 assert_eq!(r" ", t.characters.get(5).unwrap());
726 }
727 #[test]
728 fn test_convert_with_kerning() {
729 let standard_font = FIGfont::standard();
730 assert!(standard_font.is_ok());
731 let standard_font = standard_font.unwrap();
732
733 let figure = standard_font.convert("FIGlet");
734 assert!(figure.is_some());
735 assert_eq!(
736 format!("{}", figure.unwrap()),
737 r" _____ ___ ____ _ _
738 | ___|_ _/ ___| | ___| |_
739 | |_ | | | _| |/ _ \ __|
740 | _| | | |_| | | __/ |_
741 |_| |___\____|_|\___|\__|
742 "
743 );
744 let figure = standard_font.convert("oVo Tr oTo");
745 assert!(figure.is_some());
746 assert_eq!(
747 format!("{}", figure.unwrap()),
748 r" __ __ _____ _____
749 __\ \ / /__ |_ _| __ __|_ _|__
750 / _ \ \ / / _ \ | || '__| / _ \| |/ _ \
751 | (_) \ V / (_) | | || | | (_) | | (_) |
752 \___/ \_/ \___/ |_||_| \___/|_|\___/
753 "
754 );
755 }
756}