490 lines
12 KiB
Rust
Raw Normal View History

2025-03-05 11:30:04 +02:00
#[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<Token>,
}
#[derive(Debug, Default, Clone)]
struct LineBuilder {
prefix: String,
blockquote: usize,
words: Vec<Word>,
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::<usize>()
+ 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<InlineStyles>,
verbatim_style: Option<InlineStyles>,
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::<usize>();
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<InlineStyle>,
list_stack: Vec<ListStyle>,
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);
}
}