use alloc::string::{String, ToString};
#[derive(Debug, Clone)]
pub struct Writer {
out: String,
depth: usize,
unit: &'static str,
line_started: bool,
}
impl Default for Writer {
fn default() -> Self {
Self::new(" ")
}
}
impl Writer {
#[must_use]
pub fn new(unit: &'static str) -> Self {
Self {
out: String::new(),
depth: 0,
unit,
line_started: false,
}
}
#[must_use]
pub fn spaces4() -> Self {
Self::new(" ")
}
pub fn indent(&mut self) {
self.depth = self.depth.saturating_add(1);
}
pub fn dedent(&mut self) {
self.depth = self.depth.saturating_sub(1);
}
#[must_use]
pub const fn depth(&self) -> usize {
self.depth
}
pub fn push(&mut self, text: &str) {
if !self.line_started && !text.is_empty() {
for _ in 0..self.depth {
self.out.push_str(self.unit);
}
self.line_started = true;
}
self.out.push_str(text);
}
pub fn line(&mut self, text: &str) {
self.push(text);
self.newline();
}
pub fn newline(&mut self) {
self.out.push('\n');
self.line_started = false;
}
pub fn finish_line(&mut self) {
if self.line_started {
self.newline();
}
}
pub fn blank_line(&mut self) {
self.finish_line();
if self.out.is_empty() || self.out.ends_with("\n\n") {
return;
}
self.out.push('\n');
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.out.is_empty()
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.out
}
#[must_use]
pub fn finish(mut self) -> String {
self.finish_line();
self.out
}
}
pub fn verbatim_lines(writer: &mut Writer, text: &str) {
for line in text.lines() {
writer.line(line);
}
}
pub fn comment_lines(writer: &mut Writer, marker: &str, lines: &[String]) {
for line in lines {
if line.is_empty() {
writer.line(marker.trim_end());
} else {
let mut text = marker.to_string();
text.push_str(line);
writer.line(&text);
}
}
}
#[must_use]
pub fn escape_string(text: &str) -> String {
let mut out = String::new();
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\0' => out.push_str("\\0"),
other => out.push(other),
}
}
out
}
#[must_use]
pub fn escape_char(ch: char) -> String {
match ch {
'\'' => "\\'".to_string(),
'\\' => "\\\\".to_string(),
'\n' => "\\n".to_string(),
'\r' => "\\r".to_string(),
'\t' => "\\t".to_string(),
'\0' => "\\0".to_string(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn indentation_applies_once_per_line_not_per_write() {
let mut w = Writer::spaces4();
w.indent();
w.push("a");
w.push("b");
w.newline();
assert_eq!(w.finish(), " ab\n");
}
#[test]
fn a_blank_line_carries_no_trailing_whitespace() {
let mut w = Writer::spaces4();
w.indent();
w.line("a");
w.newline();
w.line("b");
assert_eq!(w.finish(), " a\n\n b\n");
}
#[test]
fn a_blank_line_does_not_double_up() {
let mut w = Writer::spaces4();
w.line("a");
w.blank_line();
w.blank_line();
w.line("b");
assert_eq!(w.finish(), "a\n\nb\n");
}
#[test]
fn a_blank_line_at_the_start_writes_nothing() {
let mut w = Writer::spaces4();
w.blank_line();
w.line("a");
assert_eq!(w.finish(), "a\n");
}
#[test]
fn dedent_below_zero_stays_at_zero() {
let mut w = Writer::spaces4();
w.dedent();
w.dedent();
assert_eq!(w.depth(), 0);
w.line("a");
assert_eq!(w.finish(), "a\n");
}
#[test]
fn string_escapes_cover_the_ones_a_generated_literal_meets() {
assert_eq!(escape_string("a\"b"), "a\\\"b");
assert_eq!(escape_string("a\\b"), "a\\\\b");
assert_eq!(escape_string("a\nb"), "a\\nb");
assert_eq!(escape_string("plain"), "plain");
}
#[test]
fn a_comment_block_marks_every_line() {
let mut w = Writer::spaces4();
comment_lines(
&mut w,
"/// ",
&["first".to_string(), String::new(), "third".to_string()],
);
assert_eq!(w.finish(), "/// first\n///\n/// third\n");
}
}