#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InlineStyle { Bold, Italic, } #[derive(Debug, Clone)] pub enum ListStyle { Numbered(usize), Bullet(char), } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct InlineStyles { pub bold: bool, pub italic: bool, } pub trait Typesetter { fn push_list(&mut self, style: ListStyle); fn pop_list(&mut self); fn push_list_item(&mut self); fn pop_list_item(&mut self); fn push_paragraph(&mut self); fn pop_paragraph(&mut self); fn push_code_block(&mut self); fn pop_code_block(&mut self); fn push_blockquote(&mut self); fn pop_blockquote(&mut self); fn push_style(&mut self, style: InlineStyle); fn pop_style(&mut self); fn append_text(&mut self, space: bool, text: &str); fn finish_line(&mut self); } #[derive(Debug, Default, Clone)] struct Token { text: String, styles: InlineStyles, } #[derive(Debug, Default, Clone)] struct Word { tokens: Vec, } #[derive(Debug, Default, Clone)] struct LineBuilder { prefix: String, blockquote: usize, words: Vec, word: Word, indent: usize, full: bool, } impl Token { fn width(&self) -> usize { self.text.chars().count() } } impl Word { fn width(&self) -> usize { self.tokens.iter().map(Token::width).sum() } fn is_empty(&self) -> bool { self.tokens.is_empty() } fn push(&mut self, ch: char, styles: InlineStyles) { if let Some(last) = self.tokens.last_mut() { if last.styles == styles { last.text.push(ch); return; } } let mut token = Token { text: String::new(), styles, }; token.text.push(ch); self.tokens.push(token); } } impl LineBuilder { fn width(&self, blockquote_width: usize) -> usize { self.prefix.chars().count() + self.indent * 2 + self.words.iter().map(Word::width).sum::() + self.words.len() + self.word.width() + self.blockquote * blockquote_width + 2 } fn is_empty(&self) -> bool { self.words.is_empty() } fn finish_word(&mut self) { let word = std::mem::take(&mut self.word); if !word.is_empty() { self.words.push(word); } } fn push(&mut self, ch: char, styles: InlineStyles) { if ch == ' ' { // Word break self.finish_word(); return; } self.word.push(ch, styles); } } pub struct TerminalPrinter { style: InlineStyles, cursor: usize, margin_left: usize, prefix_style: Option, verbatim_style: Option, blockquote_prefix: &'static str, verbatim_prefix: &'static str, } impl Default for TerminalPrinter { fn default() -> Self { Self { style: InlineStyles::default(), margin_left: 0, cursor: 0, prefix_style: Some(InlineStyles { bold: true, italic: false, }), verbatim_style: Some(InlineStyles { bold: true, italic: true, }), blockquote_prefix: "║ ", verbatim_prefix: "│ ", } } } impl TerminalPrinter { fn reset_style(&mut self) { print!("\x1B[0m"); self.style = InlineStyles::default(); } fn set_style(&mut self, styles: InlineStyles) { // Update styles if styles.bold != self.style.bold { if styles.bold { print!("\x1B[1m"); } else { print!("\x1B[22m"); } } if styles.italic != self.style.italic { if styles.italic { print!("\x1B[3m"); } else { print!("\x1B[23m"); } } self.style = styles; } fn print_whitespace(&mut self, count: usize, styles: InlineStyles) { self.set_style(styles); for _ in 0..count { print!(" "); } self.cursor += count; } fn print_token(&mut self, token: &Token) { self.set_style(token.styles); print!("{}", token.text); self.cursor += token.text.chars().count(); } fn print_word(&mut self, word: &Word) { word.tokens.iter().for_each(|token| self.print_token(token)); } fn print_prefix(&mut self, prefix: &str) { if let Some(prefix_style) = self.prefix_style { self.set_style(prefix_style); print!("{prefix}"); self.reset_style(); } else { print!("{prefix}"); } self.cursor += prefix.chars().count(); } fn print_blockquote(&mut self, prefix: &str, depth: usize) { for _ in 0..depth { print!("{prefix}"); self.cursor += prefix.chars().count(); } } fn print_line(&mut self, line: &LineBuilder, page_width: usize) { self.cursor = 0; self.print_blockquote(self.blockquote_prefix, line.blockquote); self.print_whitespace(self.margin_left + line.indent * 2, Default::default()); self.print_prefix(&line.prefix); let page_width = page_width - self.cursor; if line.full { let line_width = line.words.iter().map(Word::width).sum::(); assert!( line_width < page_width, "Line width: {line_width}, Page width: {page_width}" ); let need_spaces = page_width - line_width; let word_breaks = line.words.len() - 1; let even_spaces = (need_spaces / word_breaks).max(1); let odd_spaces = need_spaces % word_breaks; for (i, word) in line.words.iter().enumerate() { if i != 0 { let mut spaces = even_spaces; if i - 1 < odd_spaces { spaces += 1; } self.print_whitespace(spaces, word.tokens[0].styles); } self.print_word(word); } self.reset_style(); } else { for (i, word) in line.words.iter().enumerate() { if i != 0 { self.print_whitespace(1, word.tokens[0].styles); } self.print_word(word); } } println!(); } fn begin_verbatim(&mut self) { print!("{}", self.verbatim_prefix); if let Some(style) = self.verbatim_style { self.set_style(style); } } fn end_verbatim(&mut self) { if self.verbatim_style.is_some() { self.reset_style(); } } fn print_verbatim(&mut self, ch: char) { print!("{ch}"); } } pub struct TerminalTypesetter { style_stack: Vec, list_stack: Vec, line: LineBuilder, indent: usize, page_width: usize, printer: TerminalPrinter, last_empty: bool, verbatim: bool, verbatim_empty: bool, blockquote: usize, } impl Default for TerminalTypesetter { fn default() -> Self { Self { style_stack: vec![], list_stack: vec![], line: LineBuilder::default(), indent: 0, page_width: 80, printer: TerminalPrinter::default(), last_empty: true, verbatim: false, verbatim_empty: true, blockquote: 0, } } } impl TerminalTypesetter { pub fn set_page_width(&mut self, width: usize) { self.page_width = width.max(40); } fn styles(&self) -> InlineStyles { let mut styles = InlineStyles::default(); for style in self.style_stack.iter() { match style { InlineStyle::Bold => styles.bold = true, InlineStyle::Italic => styles.italic = true, } } styles } fn append_char(&mut self, ch: char) { if self.verbatim { if self.verbatim_empty { self.printer.begin_verbatim(); for _ in 0..self.indent { print!(" "); } } if ch == '\n' { self.printer.end_verbatim(); self.verbatim_empty = true; } else { self.verbatim_empty = false; } self.printer.print_verbatim(ch); return; } if self .line .width(self.printer.blockquote_prefix.chars().count()) + 1 >= self.page_width { // Finish the line, next word goes to the next line self.line.full = true; // Ugliness reduction by refusing to take trailing short words let popped = self.line.words.pop_if(|word| word.width() < 3); let mut word = std::mem::take(&mut self.line.word); word.push(ch, self.styles()); self.finish_line(); self.line.words.extend(popped); self.line.word = word; return; } self.line.push(ch, self.styles()); } fn increment_list_index(&mut self) { if let Some(last) = self.list_stack.last_mut() { match last { ListStyle::Numbered(index) => *index += 1, _ => (), } } } fn set_indent(&mut self) { let indent = self.indent; self.line.indent = indent; self.line.blockquote = self.blockquote; } fn hard_break(&mut self, set_indent: bool) { let empty = self.last_empty; self.finish_line(); if !empty && self.blockquote == 0 { println!(); self.last_empty = true; } if set_indent { self.set_indent(); } } } impl Typesetter for TerminalTypesetter { fn push_style(&mut self, style: InlineStyle) { self.style_stack.push(style); } fn pop_style(&mut self) { self.style_stack.pop(); } fn append_text(&mut self, space: bool, text: &str) { let space = space && !self.verbatim; if space { self.append_char(' '); } for ch in text.chars() { self.append_char(ch); } } fn finish_line(&mut self) { self.line.finish_word(); if !self.line.is_empty() { let line = std::mem::take(&mut self.line); self.last_empty = false; self.printer.print_line(&line, self.page_width); self.set_indent(); } else { self.last_empty = true; } } fn push_list(&mut self, style: ListStyle) { self.hard_break(false); self.list_stack.push(style); } fn pop_list(&mut self) { self.list_stack.pop(); self.hard_break(false); } fn push_list_item(&mut self) { self.hard_break(false); self.indent += 1; if let Some(last) = self.list_stack.last() { match last { &ListStyle::Bullet(p) => { self.line.prefix.push(p); } &ListStyle::Numbered(n) => { self.line.prefix.push_str(&format!("{n}.")); } } self.line.prefix.push(' '); } } fn pop_list_item(&mut self) { self.increment_list_index(); self.indent -= 1; self.hard_break(true); } fn push_paragraph(&mut self) { self.hard_break(false); } fn pop_paragraph(&mut self) { self.hard_break(true); } fn push_code_block(&mut self) { self.hard_break(false); self.verbatim_empty = true; self.verbatim = true; } fn pop_code_block(&mut self) { println!(); self.verbatim = false; } fn push_blockquote(&mut self) { self.blockquote += 1; self.hard_break(true); } fn pop_blockquote(&mut self) { self.blockquote -= 1; if self.blockquote == 0 { println!(); } self.line.prefix.clear(); self.hard_break(true); } }