use pretty::BoxDoc;
use crate::code_block::{CodeBlock, validate_no_unresolved_indent_markers};
use crate::code_node::CodeNode;
use crate::error::SigilStitchError;
use crate::import::ImportGroup;
use crate::lang::RendererLang;
pub struct CodeRenderer<'a> {
lang: &'a dyn RendererLang,
imports: &'a ImportGroup,
width: usize,
output: String,
indent_level: usize,
current_column: usize,
at_line_start: bool,
}
impl<'a> CodeRenderer<'a> {
pub fn new(lang: &'a dyn RendererLang, imports: &'a ImportGroup, width: usize) -> Self {
Self {
lang,
imports,
width,
output: String::new(),
indent_level: 0,
current_column: 0,
at_line_start: true,
}
}
pub fn render(&mut self, block: &CodeBlock) -> Result<String, SigilStitchError> {
let mut nodes = block.nodes.clone();
self.lang.rewrite_nodes(&mut nodes);
validate_no_unresolved_indent_markers(&nodes)?;
self.render_nodes(&nodes)?;
Ok(std::mem::take(&mut self.output))
}
fn render_nodes(&mut self, nodes: &[CodeNode]) -> Result<(), SigilStitchError> {
if contains_soft_break(nodes) {
self.render_nodes_pretty(nodes)
} else {
self.render_nodes_direct(nodes)
}
}
fn resolve_type_doc(&self, tn: &crate::type_name::TypeName) -> BoxDoc<'static, ()> {
let lang = self.lang;
let resolve = |module: &str, name: &str| -> String {
let resolved = self
.imports
.resolved_name(module, name)
.unwrap_or(name)
.to_string();
lang.qualify_import_name(module, &resolved)
};
tn.to_doc_with_lang(&resolve, self.lang)
}
fn resolve_block_open<'b>(lang: &'b dyn RendererLang, cond: &str) -> &'b str {
lang.block_open_for(cond)
.unwrap_or(lang.block_syntax().block_open)
}
fn resolve_block_close<'b>(lang: &'b dyn RendererLang, cond: &str) -> &'b str {
lang.block_close_for(cond)
.unwrap_or(lang.block_syntax().block_close)
}
fn resolve_comment(lang: &dyn RendererLang, text: &str) -> String {
let prefix = lang.line_comment_prefix();
let suffix = lang.line_comment_suffix();
text.split('\n')
.map(|line| {
if line.is_empty() {
format!("{prefix}{suffix}")
} else {
format!("{prefix} {line}{suffix}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn render_nodes_direct(&mut self, nodes: &[CodeNode]) -> Result<(), SigilStitchError> {
for node in nodes {
match node {
CodeNode::Literal(text) => {
self.emit_possibly_multiline(text);
}
CodeNode::TypeRef(tn) => {
self.ensure_indent();
let remaining_width = self.width.saturating_sub(self.current_column);
let doc = self.resolve_type_doc(tn);
let mut buf = Vec::new();
doc.render(remaining_width, &mut buf).map_err(|e| {
SigilStitchError::Render {
context: "CodeRenderer::render_nodes_direct TypeRef".to_string(),
message: e.to_string(),
}
})?;
let rendered =
String::from_utf8(buf).map_err(|e| SigilStitchError::Render {
context: "CodeRenderer::render_nodes_direct TypeRef UTF-8".to_string(),
message: e.to_string(),
})?;
let lines: Vec<&str> = rendered.split('\n').collect();
for (i, line) in lines.iter().enumerate() {
if i > 0 {
self.emit_newline();
self.ensure_indent();
let padding = " ".repeat(self.current_column);
self.emit(&padding);
}
self.emit(line);
}
}
CodeNode::NameRef(name) => {
self.ensure_indent();
let escaped = self.lang.escape_reserved(name);
self.emit(&escaped);
}
CodeNode::StringLit(s) => {
self.ensure_indent();
let rendered = self.lang.render_string_literal(s);
self.emit(&rendered);
}
CodeNode::VerbatimStr(s) => {
self.ensure_indent();
let rendered = self.lang.render_verbatim_string(s);
self.emit(&rendered);
}
CodeNode::InlineLiteral(s) => {
self.emit_possibly_multiline(s);
}
CodeNode::Nested(block) => {
self.render_nodes(&block.nodes)?;
}
CodeNode::Comment(text) => {
self.ensure_indent();
let comment = Self::resolve_comment(self.lang, text);
self.emit(&comment);
}
CodeNode::Attribute(text) => {
self.ensure_indent();
let attr = self.lang.render_attribute(text);
self.emit(&attr);
}
CodeNode::SoftBreak => {
self.emit(" ");
}
CodeNode::Indent => {
self.indent_level += 1;
}
CodeNode::Dedent => {
self.indent_level = self.indent_level.saturating_sub(1);
}
CodeNode::StatementBegin => {
self.ensure_indent();
}
CodeNode::StatementEnd => {
if self.lang.block_syntax().uses_semicolons {
self.emit(";");
}
}
CodeNode::Newline => {
self.emit_newline();
}
CodeNode::BlockOpen(cond) => {
let open = Self::resolve_block_open(self.lang, cond);
if !open.is_empty() {
self.emit(open);
}
}
CodeNode::BlockClose(condition) => {
let close = Self::resolve_block_close(self.lang, condition);
if !close.is_empty() {
self.ensure_indent();
self.emit(close);
}
}
CodeNode::BranchClose(cond) => {
if self.lang.block_syntax().close_on_transition {
let close = Self::resolve_block_close(self.lang, cond);
if !close.is_empty() {
self.ensure_indent();
self.emit(close);
self.emit(" ");
}
}
}
CodeNode::Sequence(children) => {
self.render_nodes(children)?;
}
}
}
Ok(())
}
fn render_nodes_pretty(&mut self, nodes: &[CodeNode]) -> Result<(), SigilStitchError> {
let doc = self.nodes_to_doc(nodes);
let remaining_width = self.width.saturating_sub(self.current_column);
let mut buf = Vec::new();
doc.render(remaining_width, &mut buf)
.map_err(|e| SigilStitchError::Render {
context: "CodeRenderer::render_nodes_pretty".to_string(),
message: e.to_string(),
})?;
let rendered = String::from_utf8(buf).map_err(|e| SigilStitchError::Render {
context: "CodeRenderer::render_nodes_pretty UTF-8".to_string(),
message: e.to_string(),
})?;
let lines: Vec<&str> = rendered.split('\n').collect();
for (i, line) in lines.iter().enumerate() {
if i > 0 {
self.emit_newline();
self.ensure_indent();
}
self.emit(line);
}
Ok(())
}
fn nodes_to_doc(&self, nodes: &[CodeNode]) -> BoxDoc<'static, ()> {
let mut doc = BoxDoc::nil();
for node in nodes {
let node_doc = match node {
CodeNode::Literal(text) => BoxDoc::text(text.clone()),
CodeNode::TypeRef(tn) => self.resolve_type_doc(tn),
CodeNode::NameRef(name) => BoxDoc::text(self.lang.escape_reserved(name)),
CodeNode::StringLit(s) => BoxDoc::text(self.lang.render_string_literal(s)),
CodeNode::VerbatimStr(s) => BoxDoc::text(self.lang.render_verbatim_string(s)),
CodeNode::InlineLiteral(s) => BoxDoc::text(s.clone()),
CodeNode::Nested(block) => self.nodes_to_doc(&block.nodes),
CodeNode::Comment(text) => BoxDoc::text(Self::resolve_comment(self.lang, text)),
CodeNode::Attribute(text) => BoxDoc::text(self.lang.render_attribute(text)),
CodeNode::SoftBreak => BoxDoc::softline(),
CodeNode::Indent | CodeNode::Dedent => BoxDoc::nil(),
CodeNode::StatementBegin => BoxDoc::nil(),
CodeNode::StatementEnd => {
if self.lang.block_syntax().uses_semicolons {
BoxDoc::text(";")
} else {
BoxDoc::nil()
}
}
CodeNode::Newline => BoxDoc::hardline(),
CodeNode::BlockOpen(cond) => {
let open = Self::resolve_block_open(self.lang, cond);
if open.is_empty() {
BoxDoc::nil()
} else {
BoxDoc::text(open.to_string())
}
}
CodeNode::BlockClose(condition) => {
let close = Self::resolve_block_close(self.lang, condition);
if close.is_empty() {
BoxDoc::nil()
} else {
BoxDoc::text(close.to_string())
}
}
CodeNode::BranchClose(cond) => {
if !self.lang.block_syntax().close_on_transition {
BoxDoc::nil()
} else {
let close = Self::resolve_block_close(self.lang, cond);
if close.is_empty() {
BoxDoc::nil()
} else {
BoxDoc::text(format!("{close} "))
}
}
}
CodeNode::Sequence(children) => self.nodes_to_doc(children),
};
doc = doc.append(node_doc);
}
doc.group()
}
fn emit_possibly_multiline(&mut self, text: &str) {
if !text.contains('\n') {
self.ensure_indent();
self.emit(text);
return;
}
for (i, line) in text.split('\n').enumerate() {
if i > 0 {
self.emit_newline();
}
if !line.is_empty() {
self.ensure_indent();
self.emit(line);
}
}
}
fn ensure_indent(&mut self) {
if self.at_line_start {
let indent_str = self.lang.block_syntax().indent_unit;
for _ in 0..self.indent_level {
self.output.push_str(indent_str);
self.current_column += indent_str.len();
}
self.at_line_start = false;
}
}
fn emit(&mut self, text: &str) {
self.output.push_str(text);
if let Some(last_nl) = text.rfind('\n') {
self.current_column = text.len() - last_nl - 1;
} else {
self.current_column += text.len();
}
}
fn emit_newline(&mut self) {
self.output.push('\n');
self.current_column = 0;
self.at_line_start = true;
}
}
fn contains_soft_break(nodes: &[CodeNode]) -> bool {
nodes.iter().any(|n| match n {
CodeNode::SoftBreak => true,
CodeNode::Sequence(children) => contains_soft_break(children),
_ => false,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code_block::CodeBlock;
use crate::import::ImportGroup;
use crate::lang::typescript::TypeScript;
use crate::type_name::TypeName;
fn render_block(block: &CodeBlock, width: usize) -> String {
let ts = TypeScript::new();
let imports = ImportGroup::new();
let mut renderer = CodeRenderer::new(&ts, &imports, width);
renderer.render(block).unwrap()
}
#[test]
fn test_simple_statement() {
let mut b = CodeBlock::builder();
b.add_statement("const x = 42", ());
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert_eq!(output.trim(), "const x = 42;");
}
#[test]
fn test_control_flow() {
let mut b = CodeBlock::builder();
b.begin_control_flow("if (x > 0)", ());
b.add_statement("return x", ());
b.end_control_flow();
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert!(output.contains("if (x > 0) {"));
assert!(output.contains(" return x;"));
assert!(output.contains("}"));
}
#[test]
fn test_if_else() {
let mut b = CodeBlock::builder();
b.begin_control_flow("if (x > 0)", ());
b.add_statement("return x", ());
b.next_control_flow("else", ());
b.add_statement("return 0", ());
b.end_control_flow();
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert!(output.contains("if (x > 0) {"));
assert!(output.contains("} else {"));
assert!(output.contains(" return 0;"));
}
#[test]
fn test_type_rendering() {
let user = TypeName::importable("./models", "User");
let imports = ImportGroup {
entries: vec![crate::import::ImportEntry {
module: "./models".to_string(),
name: "User".to_string(),
alias: None,
is_type_only: true,
is_side_effect: false,
is_wildcard: false,
}],
};
let mut b = CodeBlock::builder();
b.add_statement("const u: %T = getUser()", (user,));
let block = b.build().unwrap();
let ts = TypeScript::new();
let mut renderer = CodeRenderer::new(&ts, &imports, 80);
let output = renderer.render(&block).unwrap();
assert_eq!(output.trim(), "const u: User = getUser();");
}
#[test]
fn test_string_literal() {
let mut b = CodeBlock::builder();
b.add_statement(
"const x = %S",
(crate::code_block::StringLitArg("hello".to_string()),),
);
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert_eq!(output.trim(), "const x = 'hello';");
}
#[test]
fn test_nested_indent() {
let mut b = CodeBlock::builder();
b.begin_control_flow("if (a)", ());
b.begin_control_flow("if (b)", ());
b.add_statement("return c", ());
b.end_control_flow();
b.end_control_flow();
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert!(output.contains(" return c;"));
}
#[test]
fn test_comment() {
let mut b = CodeBlock::builder();
b.add_comment("This is a comment");
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert!(output.contains("// This is a comment"));
}
#[test]
fn test_multiline_literal_via_percent_l_reindents_each_line() {
let mut b = CodeBlock::builder();
b.begin_control_flow("interface User", ());
b.add("%L", "/**\n * The user's name.\n */".to_string());
b.add_line();
b.add_statement("name: string", ());
b.end_control_flow();
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert!(
output.contains(" /**"),
"first line of doc should be indented, got:\n{output}"
);
assert!(
output.contains(" * The user's name."),
"middle line of doc should be indented (indent + ' * ...'), got:\n{output}"
);
assert!(
output.contains(" */"),
"closing line of doc should be indented, got:\n{output}"
);
assert!(
!output.contains("\n * The user's name."),
"middle line must not be flush-left, got:\n{output}"
);
assert!(
!output.contains("\n */"),
"closing line must not be flush-left, got:\n{output}"
);
}
#[test]
fn test_multiline_literal_direct_reindents_each_line() {
let mut b = CodeBlock::builder();
b.begin_control_flow("function f()", ());
b.add("line1\nline2\nline3", ());
b.add_line();
b.end_control_flow();
let block = b.build().unwrap();
let output = render_block(&block, 80);
assert!(
output.contains(" line1"),
"first literal line should be indented, got:\n{output}"
);
assert!(
output.contains(" line2"),
"second literal line should be indented, got:\n{output}"
);
assert!(
output.contains(" line3"),
"third literal line should be indented, got:\n{output}"
);
assert!(
!output.contains("\nline2"),
"line2 must not be flush-left, got:\n{output}"
);
}
#[test]
fn test_block_open_for_override() {
use crate::lang::haskell::Haskell;
let hs = Haskell::new();
let imports = ImportGroup::new();
let mut b = CodeBlock::builder();
b.begin_control_flow("class Functor f", ());
b.add_statement("fmap :: a -> b", ());
b.end_control_flow();
let block = b.build().unwrap();
let mut renderer = CodeRenderer::new(&hs, &imports, 80);
let output = renderer.render(&block).unwrap();
assert!(
output.contains("class Functor f where"),
"Haskell backend should emit 'where' via block_open_for, got:\n{output}"
);
}
}