490 lines
12 KiB
Rust
490 lines
12 KiB
Rust
#[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);
|
|
}
|
|
}
|