#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub struct Fmt {
pub bold: bool,
pub italic: bool,
}
impl Fmt {
pub const NONE: Fmt = Fmt { bold: false, italic: false };
fn is_none(self) -> bool {
self == Fmt::NONE
}
fn marker_len(self) -> usize {
usize::from(self.bold) * 2 + usize::from(self.italic)
}
}
#[derive(Default)]
pub struct Out {
text: String,
open: Fmt,
open_pos: usize,
prefix: Option<String>,
}
impl Out {
pub fn set_line_prefix(&mut self, prefix: String) {
self.prefix = Some(prefix);
}
pub fn has_line_prefix(&self) -> bool {
self.prefix.is_some()
}
pub fn clear_line_prefix(&mut self) {
self.prefix = None;
}
pub fn push_text(&mut self, s: &str, want: Fmt) {
if s.is_empty() {
return;
}
self.flush_prefix();
if want == self.open {
self.text.push_str(s);
return;
}
let trimmed = s.trim_start_matches(char::is_whitespace);
if trimmed.is_empty() {
self.text.push_str(s);
return;
}
let lead = &s[..s.len() - trimmed.len()];
self.close_span();
self.text.push_str(lead);
self.open_span(want);
self.text.push_str(trimmed);
}
pub fn push_char(&mut self, c: char, want: Fmt) {
let mut buf = [0u8; 4];
self.push_text(c.encode_utf8(&mut buf), want);
}
pub fn push_structural(&mut self, s: &str) {
self.close_span();
self.text.push_str(s);
}
pub fn break_line(&mut self) {
self.close_span();
self.prefix = None;
self.text.push('\n');
}
pub fn ends_with_newline(&self) -> bool {
self.text.is_empty() || self.text.ends_with('\n')
}
pub fn finish(mut self) -> String {
self.close_span();
normalize(&self.text)
}
fn flush_prefix(&mut self) {
let Some(prefix) = self.prefix.take() else {
return;
};
self.close_span();
if !self.ends_with_newline() {
self.text.push('\n');
}
self.text.push_str(&prefix);
}
fn open_span(&mut self, want: Fmt) {
if want.bold {
self.text.push_str("**");
}
if want.italic {
self.text.push('*');
}
self.open = want;
self.open_pos = self.text.len();
}
fn close_span(&mut self) {
if self.open.is_none() {
return;
}
let body = &self.text[self.open_pos..];
if body.trim().is_empty() {
let ws = body.to_string();
self.text.truncate(self.open_pos - self.open.marker_len());
self.text.push_str(&ws);
} else {
let keep = self.open_pos + body.trim_end_matches(char::is_whitespace).len();
let tail = self.text[keep..].to_string();
self.text.truncate(keep);
if self.open.italic {
self.text.push('*');
}
if self.open.bold {
self.text.push_str("**");
}
self.text.push_str(&tail);
}
self.open = Fmt::NONE;
self.open_pos = self.text.len();
}
}
fn normalize(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut blank_run = 0;
for line in s.split('\n') {
let t = line.trim_end();
if t.trim().is_empty() {
blank_run += 1;
if blank_run <= 2 {
out.push('\n');
}
} else {
blank_run = 0;
out.push_str(t.trim_start_matches(' '));
out.push('\n');
}
}
out.trim().to_string()
}