use std::fmt::{Result, Write};
use crate::styled_string::{
Document, DocumentNode, HeadingLevel, ListItem, ShowWhen, Span, TruncationLevel,
};
struct PlainRenderer<'w, W: Write> {
output: &'w mut W,
indent: String,
}
pub fn render(document: &Document, output: &mut impl Write) -> Result {
let mut renderer = PlainRenderer::new(output);
renderer.render_block_sequence(&document.nodes)
}
impl<'w, W: Write> PlainRenderer<'w, W> {
fn new(output: &'w mut W) -> Self {
Self {
output,
indent: String::new(),
}
}
fn write_indent(&mut self) -> Result {
write!(self.output, "{}", self.indent)
}
fn render_block_sequence(&mut self, nodes: &[DocumentNode]) -> Result {
for (idx, node) in nodes.iter().enumerate() {
if idx > 0 {
writeln!(self.output)?; }
self.render_node(node)?;
}
Ok(())
}
fn render_nodes(&mut self, nodes: &[DocumentNode]) -> Result {
for node in nodes {
self.render_node(node)?;
}
Ok(())
}
fn render_node(&mut self, node: &DocumentNode) -> Result {
match node {
DocumentNode::Paragraph { spans } => {
self.write_indent()?;
self.render_spans(spans)?;
writeln!(self.output)?; Ok(())
}
DocumentNode::Heading { level, spans } => {
self.write_indent()?;
self.render_spans(spans)?;
writeln!(self.output)?;
self.write_indent()?;
match level {
HeadingLevel::Title => {
for _ in 0..80 {
write!(self.output, "=")?;
}
writeln!(self.output)?;
}
HeadingLevel::Section => {
for _ in 0..80 {
write!(self.output, "-")?;
}
writeln!(self.output)?;
}
}
Ok(())
}
DocumentNode::Section { title, nodes } => {
if let Some(title_spans) = title {
self.write_indent()?;
self.render_spans(title_spans)?;
writeln!(self.output)?;
writeln!(self.output)?; }
self.render_block_sequence(nodes)
}
DocumentNode::List { items } => {
for (idx, item) in items.iter().enumerate() {
if idx > 0 {
writeln!(self.output)?; }
self.render_list_item(item)?;
}
Ok(())
}
DocumentNode::CodeBlock { code, .. } => {
self.write_indent()?;
writeln!(self.output, "```")?;
for line in code.lines() {
self.write_indent()?;
writeln!(self.output, "{line}")?;
}
if !code.ends_with('\n') && !code.is_empty() {
writeln!(self.output)?;
}
self.write_indent()?;
writeln!(self.output, "```")?;
Ok(())
}
DocumentNode::GeneratedCode { spans } => {
self.write_indent()?;
self.render_spans(spans)?;
writeln!(self.output)?; Ok(())
}
DocumentNode::HorizontalRule => {
self.write_indent()?;
for _ in 0..80 {
write!(self.output, "─")?;
}
writeln!(self.output)?;
Ok(())
}
DocumentNode::BlockQuote { nodes } => {
for (idx, node) in nodes.iter().enumerate() {
if idx > 0 {
writeln!(self.output)?; }
self.write_indent()?;
write!(self.output, "> ")?;
let saved_indent = self.indent.clone();
self.indent.push_str(" ");
self.render_node(node)?;
self.indent = saved_indent;
}
Ok(())
}
DocumentNode::Table { header, rows } => {
let row_count = rows.len();
let col_count = header
.as_ref()
.map_or_else(|| rows.first().map_or(0, |r| r.len()), |h| h.len());
self.write_indent()?;
writeln!(
self.output,
"[Table: {} columns × {} rows]",
col_count, row_count
)?;
Ok(())
}
DocumentNode::TruncatedBlock { nodes, level } => {
match level {
TruncationLevel::SingleLine => {
if let Some(first_node) = nodes.first() {
match first_node {
DocumentNode::Paragraph { spans } => {
self.write_indent()?;
self.render_spans(spans)?;
}
DocumentNode::Heading { spans, .. } => {
self.write_indent()?;
self.render_spans(spans)?;
}
_ => {
self.render_node(first_node)?;
}
}
if nodes.len() > 1 {
write!(self.output, " [...]")?;
}
}
writeln!(self.output)?; }
TruncationLevel::Brief => {
if let Some(first_node) = nodes.first() {
self.render_node(first_node)?;
if nodes.len() > 1 {
self.write_indent()?;
write!(self.output, "[+{} more]", nodes.len() - 1)?;
writeln!(self.output)?;
}
}
}
TruncationLevel::Full => {
self.render_block_sequence(nodes)?;
}
}
Ok(())
}
DocumentNode::Conditional { show_when, nodes } => {
let should_show = match show_when {
ShowWhen::Always => true,
ShowWhen::Interactive => false,
ShowWhen::NonInteractive => true,
};
if should_show {
for (idx, node) in nodes.iter().enumerate() {
if idx > 0 {
writeln!(self.output)?; }
self.render_node(node)?;
}
}
Ok(())
}
}
}
fn render_spans(&mut self, spans: &[Span]) -> Result {
for span in spans {
self.render_span(span)?;
}
Ok(())
}
fn render_span(&mut self, Span { text, .. }: &Span) -> Result {
for (idx, line) in text.split('\n').enumerate() {
if idx > 0 {
writeln!(self.output)?;
self.write_indent()?;
}
write!(self.output, "{line}")?;
}
Ok(())
}
fn render_list_item(&mut self, item: &ListItem) -> Result {
self.write_indent()?;
let bullet = crate::renderer::bullet_for_indent(self.indent.len() as u16);
write!(self.output, " {} ", bullet)?;
let saved_indent = self.indent.clone();
if let Some(first) = item.content.first() {
self.render_node(first)?;
}
self.indent.push_str(" ");
for node in item.content.iter().skip(1) {
self.render_node(node)?;
}
self.indent = saved_indent;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_heading() {
let doc = Document::with_nodes(vec![DocumentNode::heading(
HeadingLevel::Title,
vec![Span::plain("Item: "), Span::type_name("Vec")],
)]);
let mut output = String::new();
render(&doc, &mut output).unwrap();
assert!(output.contains("Item: Vec"));
assert!(output.contains("===="));
}
#[test]
fn test_render_list() {
let doc = Document::with_nodes(vec![DocumentNode::list(vec![
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain("First")])]),
ListItem::new(vec![DocumentNode::paragraph(vec![Span::plain("Second")])]),
])]);
let mut output = String::new();
render(&doc, &mut output).unwrap();
dbg!(&output);
assert!(output.contains(" ◦ First"));
assert!(output.contains(" ◦ Second"));
}
}