diff options
Diffstat (limited to 'support/texlab/src/formatting/bibtex.rs')
-rw-r--r-- | support/texlab/src/formatting/bibtex.rs | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/support/texlab/src/formatting/bibtex.rs b/support/texlab/src/formatting/bibtex.rs new file mode 100644 index 0000000000..aba3299468 --- /dev/null +++ b/support/texlab/src/formatting/bibtex.rs @@ -0,0 +1,376 @@ +use crate::syntax::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BibtexFormattingOptions { + pub line_length: Option<i32>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BibtexFormattingParams { + pub tab_size: usize, + pub insert_spaces: bool, + pub options: BibtexFormattingOptions, +} + +impl BibtexFormattingParams { + pub fn line_length(&self) -> i32 { + let line_length = self.options.line_length.unwrap_or(120); + if line_length <= 0 { + std::i32::MAX + } else { + line_length + } + } +} + +impl Default for BibtexFormattingParams { + fn default() -> Self { + BibtexFormattingParams { + tab_size: 4, + insert_spaces: true, + options: BibtexFormattingOptions::default(), + } + } +} + +struct BibtexFormatter<'a> { + params: &'a BibtexFormattingParams, + indent: String, + output: String, +} + +impl<'a> BibtexFormatter<'a> { + fn new(params: &'a BibtexFormattingParams) -> Self { + let indent = if params.insert_spaces { + let mut buffer = String::new(); + for _ in 0..params.tab_size { + buffer.push(' '); + } + buffer + } else { + "\t".into() + }; + + Self { + params, + indent, + output: String::new(), + } + } + + fn format_comment(&mut self, comment: &BibtexComment) { + self.output.push_str(comment.token.text()); + } + + fn format_preamble(&mut self, preamble: &BibtexPreamble) { + self.format_token(&preamble.ty); + self.output.push('{'); + if let Some(ref content) = preamble.content { + self.format_content(content, self.output.chars().count()); + self.output.push('}'); + } + } + + fn format_string(&mut self, string: &BibtexString) { + self.format_token(&string.ty); + self.output.push('{'); + if let Some(ref name) = string.name { + self.output.push_str(name.text()); + self.output.push_str(" = "); + if let Some(ref value) = string.value { + self.format_content(value, self.output.chars().count()); + self.output.push('}'); + } + } + } + + fn format_entry(&mut self, entry: &BibtexEntry) { + self.format_token(&entry.ty); + self.output.push('{'); + if let Some(ref key) = entry.key { + self.output.push_str(key.text()); + self.output.push(','); + self.output.push('\n'); + for field in &entry.fields { + self.format_field(field); + } + self.output.push('}'); + } + } + + fn format_field(&mut self, field: &BibtexField) { + self.output.push_str(self.indent.as_ref()); + self.format_token(&field.name); + self.output.push_str(" = "); + let count = field.name.text().chars().count(); + let align = self.params.tab_size as usize + count + 3; + if let Some(ref content) = field.content { + self.format_content(content, align); + self.output.push(','); + self.output.push('\n'); + } + } + + fn format_content(&mut self, content: &BibtexContent, align: usize) { + let mut analyzer = BibtexContentAnalyzer::new(); + content.accept(&mut analyzer); + let tokens = analyzer.tokens; + self.output.push_str(tokens[0].text()); + + 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.params.line_length() as usize { + self.output.push('\n'); + self.output.push_str(self.indent.as_ref()); + for _ in 0..=align - self.params.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; + } + } + + fn format_token(&mut self, token: &BibtexToken) { + self.output.push_str(token.text().to_lowercase().as_ref()); + } + + fn should_insert_space(previous: &BibtexToken, current: &BibtexToken) -> bool { + previous.start().line != current.start().line + || previous.end().character < current.start().character + } +} + +struct BibtexContentAnalyzer<'a> { + pub tokens: Vec<&'a BibtexToken>, +} + +impl<'a> BibtexContentAnalyzer<'a> { + pub fn new() -> Self { + BibtexContentAnalyzer { tokens: Vec::new() } + } +} + +impl<'a> BibtexVisitor<'a> for BibtexContentAnalyzer<'a> { + fn visit_root(&mut self, _root: &'a BibtexRoot) {} + + fn visit_comment(&mut self, _comment: &'a BibtexComment) {} + + fn visit_preamble(&mut self, _preamble: &'a BibtexPreamble) {} + + fn visit_string(&mut self, _string: &'a BibtexString) {} + + fn visit_entry(&mut self, _entry: &'a BibtexEntry) {} + + fn visit_field(&mut self, _field: &'a BibtexField) {} + + fn visit_word(&mut self, word: &'a BibtexWord) { + self.tokens.push(&word.token); + } + + fn visit_command(&mut self, command: &'a BibtexCommand) { + self.tokens.push(&command.token); + } + + fn visit_quoted_content(&mut self, content: &'a BibtexQuotedContent) { + self.tokens.push(&content.left); + BibtexWalker::walk_quoted_content(self, content); + if let Some(ref right) = content.right { + self.tokens.push(right); + } + } + + fn visit_braced_content(&mut self, content: &'a BibtexBracedContent) { + self.tokens.push(&content.left); + BibtexWalker::walk_braced_content(self, content); + if let Some(ref right) = content.right { + self.tokens.push(right); + } + } + + fn visit_concat(&mut self, concat: &'a BibtexConcat) { + concat.left.accept(self); + self.tokens.push(&concat.operator); + if let Some(ref right) = concat.right { + right.accept(self); + } + } +} + +pub fn format_declaration( + declaration: &BibtexDeclaration, + params: &BibtexFormattingParams, +) -> String { + match declaration { + BibtexDeclaration::Comment(comment) => format_comment(comment, params), + BibtexDeclaration::Preamble(preamble) => format_preamble(preamble, params), + BibtexDeclaration::String(string) => format_string(string, params), + BibtexDeclaration::Entry(entry) => format_entry(entry, params), + } +} + +pub fn format_comment(comment: &BibtexComment, params: &BibtexFormattingParams) -> String { + let mut formatter = BibtexFormatter::new(params); + formatter.format_comment(&comment); + formatter.output +} + +pub fn format_preamble(preamble: &BibtexPreamble, params: &BibtexFormattingParams) -> String { + let mut formatter = BibtexFormatter::new(params); + formatter.format_preamble(&preamble); + formatter.output +} + +pub fn format_string(string: &BibtexString, params: &BibtexFormattingParams) -> String { + let mut formatter = BibtexFormatter::new(params); + formatter.format_string(&string); + formatter.output +} + +pub fn format_entry(entry: &BibtexEntry, params: &BibtexFormattingParams) -> String { + let mut formatter = BibtexFormatter::new(params); + formatter.format_entry(&entry); + formatter.output +} + +pub fn format_content(content: &BibtexContent, params: &BibtexFormattingParams) -> String { + let mut formatter = BibtexFormatter::new(params); + formatter.format_content(content, 0); + formatter.output +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + + fn verify(source: &str, expected: &str, line_length: i32) { + let tree = BibtexSyntaxTree::from(source); + let params = BibtexFormattingParams { + tab_size: 4, + insert_spaces: true, + options: BibtexFormattingOptions { + line_length: Some(line_length), + }, + }; + assert_eq!( + expected, + format_declaration(&tree.root.children[0], ¶ms) + ); + } + + #[test] + fn test_wrap_long_lines() { + let source = + "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}"; + let expected = indoc!( + " + @article{foo, + bar = {Lorem ipsum dolor + sit amet, + consectetur + adipiscing elit.}, + }" + ); + verify(source, expected, 30); + } + + #[test] + fn test_line_length_zero() { + let source = + "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}"; + let expected = indoc!( + " + @article{foo, + bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.}, + }" + ); + verify(source, expected, 0); + } + + #[test] + fn test_trailing_commas() { + let source = "@article{foo, bar = baz}"; + let expected = indoc!( + " + @article{foo, + bar = baz, + }" + ); + verify(source, expected, 30); + } + + #[test] + fn test_insert_braces() { + let source = "@article{foo, bar = baz,"; + let expected = indoc!( + " + @article{foo, + bar = baz, + }" + ); + verify(source, expected, 30); + } + + #[test] + fn test_commands() { + let source = "@article{foo, bar = \"\\baz\",}"; + let expected = indoc!( + "@article{foo, + bar = \"\\baz\", + }" + ); + verify(source, expected, 30); + } + + #[test] + fn test_concatenation() { + let source = "@article{foo, bar = \"baz\" # \"qux\"}"; + let expected = indoc!( + " + @article{foo, + bar = \"baz\" # \"qux\", + }" + ); + verify(source, expected, 30); + } + + #[test] + fn test_parentheses() { + let source = "@article(foo,)"; + let expected = indoc!( + " + @article{foo, + }" + ); + verify(source, expected, 30); + } + + #[test] + fn test_string() { + let source = "@string{foo=\"bar\"}"; + let expected = "@string{foo = \"bar\"}"; + verify(source, expected, 30); + } + + #[test] + fn test_preamble() { + let source = "@preamble{\n\"foo bar baz\"}"; + let expected = "@preamble{\"foo bar baz\"}"; + verify(source, expected, 30); + } +} |