use line_index::LineIndex; use rowan::{ast::AstNode, NodeOrToken}; use syntax::bibtex::{self, HasName, HasType, HasValue}; pub struct Options { pub insert_spaces: bool, pub tab_size: usize, pub line_length: usize, } impl Options { fn indent(&self) -> String { if self.insert_spaces { " ".repeat(self.tab_size) } else { String::from("\t") } } } impl Default for Options { fn default() -> Self { Self { insert_spaces: true, tab_size: 4, line_length: 80, } } } pub fn format(root: &bibtex::SyntaxNode, line_index: &LineIndex, options: &Options) -> String { let indent = options.indent(); let mut output = String::new(); for elem in root.children_with_tokens() { match elem { NodeOrToken::Token(token) => { output.push_str(token.text()); } NodeOrToken::Node(node) => { let mut fmt = Formatter { indent: &indent, output: &mut output, options, align: Vec::new(), line_index, }; fmt.visit_node(node); } } } output } struct Formatter<'a> { output: &'a mut String, indent: &'a str, options: &'a Options, align: Vec, line_index: &'a LineIndex, } impl<'a> Formatter<'a> { fn visit_token_lowercase(&mut self, token: &bibtex::SyntaxToken) { self.output.push_str(&token.text().to_lowercase()); } fn should_insert_space( &self, previous: &bibtex::SyntaxToken, current: &bibtex::SyntaxToken, ) -> bool { let previous = self.line_index.line_col(previous.text_range().end()); let current = self.line_index.line_col(current.text_range().start()); previous.line != current.line || previous.col < current.col } fn base_align(&self) -> usize { self.output[self.output.rfind('\n').unwrap_or(0)..] .chars() .count() } fn visit_node(&mut self, parent: bibtex::SyntaxNode) { match parent.kind() { bibtex::PREAMBLE => { let preamble = bibtex::Preamble::cast(parent).unwrap(); self.visit_token_lowercase(&preamble.type_token().unwrap()); self.output.push('{'); if preamble.syntax().children().next().is_some() { self.align.push(self.base_align()); for node in preamble.syntax().children() { self.visit_node(node); } self.output.push('}'); } } bibtex::STRING => { let string = bibtex::StringDef::cast(parent).unwrap(); self.visit_token_lowercase(&string.type_token().unwrap()); self.output.push('{'); if let Some(name) = string.name_token() { self.output.push_str(name.text()); self.output.push_str(" = "); if let Some(value) = string.value() { self.align.push(self.base_align()); self.visit_node(value.syntax().clone()); self.output.push('}'); } } } bibtex::ENTRY => { let entry = bibtex::Entry::cast(parent).unwrap(); self.visit_token_lowercase(&entry.type_token().unwrap()); self.output.push('{'); if let Some(key) = entry.name_token() { self.output.push_str(&key.to_string()); self.output.push(','); self.output.push('\n'); for field in entry.fields() { self.visit_node(field.syntax().clone()); } self.output.push('}'); } } bibtex::FIELD => { let field = bibtex::Field::cast(parent).unwrap(); self.output.push_str(self.indent); let name = field.name_token().unwrap(); self.output.push_str(name.text()); self.output.push_str(" = "); if let Some(value) = field.value() { let count = name.text().chars().count(); self.align.push(self.options.tab_size + count + 3); self.visit_node(value.syntax().clone()); self.output.push(','); self.output.push('\n'); } } kind if bibtex::Value::can_cast(kind) => { let tokens: Vec<_> = parent .descendants_with_tokens() .filter_map(|element| element.into_token()) .filter(|token| token.kind() != bibtex::WHITESPACE) .collect(); self.output.push_str(tokens[0].text()); let align = self.align.pop().unwrap_or_default(); let mut length = align + tokens[0].text().chars().count(); for i in 1..tokens.len() { let previous = &tokens[i - 1]; let current = &tokens[i]; let current_length = current.text().chars().count(); let insert_space = self.should_insert_space(previous, current); let space_length = if insert_space { 1 } else { 0 }; if length + current_length + space_length > self.options.line_length { self.output.push('\n'); self.output.push_str(self.indent.as_ref()); for _ in 0..=align - self.options.tab_size { self.output.push(' '); } length = align; } else if insert_space { self.output.push(' '); length += 1; } self.output.push_str(current.text()); length += current_length; } } bibtex::ROOT | bibtex::JUNK => { for element in parent.children_with_tokens() { match element { NodeOrToken::Token(token) => { self.output.push_str(token.text()); } NodeOrToken::Node(node) => { self.visit_node(node); } } } } _ => unreachable!(), } } } #[cfg(test)] mod tests;