Skip to main content

bibtex_parser/
writer.rs

1//! BibTeX writer for serializing libraries
2
3use crate::{Block, Entry, Library, Result, Value};
4use std::io::{self, Write};
5
6/// Configuration for writing BibTeX
7#[derive(Debug, Clone)]
8pub struct WriterConfig {
9    /// Indentation string (default: "  ")
10    pub indent: String,
11    /// Whether to align field values (default: false)
12    pub align_values: bool,
13    /// Maximum line length for wrapping (default: 80)
14    pub max_line_length: usize,
15    /// Whether to sort entries by key (default: false)
16    pub sort_entries: bool,
17    /// Whether to sort fields within entries (default: false)
18    pub sort_fields: bool,
19}
20
21impl Default for WriterConfig {
22    fn default() -> Self {
23        Self {
24            indent: "  ".to_string(),
25            align_values: false,
26            max_line_length: 80,
27            sort_entries: false,
28            sort_fields: false,
29        }
30    }
31}
32
33/// BibTeX writer
34#[derive(Debug)]
35pub struct Writer<W: Write> {
36    writer: W,
37    config: WriterConfig,
38}
39
40impl<W: Write> Writer<W> {
41    /// Create a new writer with default configuration
42    pub fn new(writer: W) -> Self {
43        Self {
44            writer,
45            config: WriterConfig::default(),
46        }
47    }
48
49    /// Create a new writer with custom configuration
50    pub const fn with_config(writer: W, config: WriterConfig) -> Self {
51        Self { writer, config }
52    }
53
54    /// Access the writer configuration mutably
55    #[must_use]
56    pub fn config_mut(&mut self) -> &mut WriterConfig {
57        &mut self.config
58    }
59
60    /// Consume the writer and return the underlying writer
61    #[must_use]
62    pub fn into_inner(self) -> W {
63        self.writer
64    }
65
66    /// Write a complete library.
67    pub fn write_library(&mut self, library: &Library) -> io::Result<()> {
68        if self.config.sort_entries {
69            return self.write_library_sorted(library);
70        }
71
72        for (index, block) in library.blocks().into_iter().enumerate() {
73            if index > 0 {
74                writeln!(self.writer)?;
75            }
76            match block {
77                Block::Entry(entry, _) => self.write_entry(entry)?,
78                Block::String(definition) => {
79                    self.write_string(&definition.name, &definition.value)?;
80                }
81                Block::Preamble(preamble) => self.write_preamble(&preamble.value)?,
82                Block::Comment(comment) => self.write_comment(comment.text())?,
83                Block::Failed(failed) => self.writer.write_all(failed.raw.as_bytes())?,
84            }
85        }
86
87        Ok(())
88    }
89
90    fn write_library_sorted(&mut self, library: &Library) -> io::Result<()> {
91        // Write preambles
92        for preamble in library.preambles() {
93            self.write_preamble(&preamble.value)?;
94            writeln!(self.writer)?;
95        }
96
97        // Write strings
98        let mut strings: Vec<_> = library.strings().iter().collect();
99        if self.config.sort_entries {
100            strings.sort_by(|a, b| a.name.cmp(&b.name));
101        }
102
103        for definition in strings {
104            self.write_string(&definition.name, &definition.value)?;
105            writeln!(self.writer)?;
106        }
107
108        // Write entries
109        let mut entries = library.entries().iter().collect::<Vec<_>>();
110        if self.config.sort_entries {
111            entries.sort_by(|a, b| a.key.cmp(&b.key));
112        }
113
114        for (i, entry) in entries.iter().enumerate() {
115            if i > 0 {
116                writeln!(self.writer)?;
117            }
118            self.write_entry(entry)?;
119        }
120
121        Ok(())
122    }
123
124    /// Write a single entry
125    pub fn write_entry(&mut self, entry: &Entry) -> io::Result<()> {
126        writeln!(self.writer, "@{}{{{},", entry.ty, entry.key)?;
127
128        let mut fields = entry.fields().to_vec();
129        if self.config.sort_fields {
130            fields.sort_by(|a, b| a.name.cmp(&b.name));
131        }
132
133        // Calculate alignment if needed
134        let max_name_len = if self.config.align_values {
135            fields.iter().map(|f| f.name.len()).max().unwrap_or(0)
136        } else {
137            0
138        };
139
140        for (i, field) in fields.iter().enumerate() {
141            write!(self.writer, "{}", self.config.indent)?;
142            write!(self.writer, "{}", field.name)?;
143
144            if self.config.align_values {
145                let padding = max_name_len - field.name.len();
146                write!(self.writer, "{}", " ".repeat(padding))?;
147            }
148
149            write!(self.writer, " = ")?;
150            self.write_value(&field.value)?;
151
152            if i < fields.len() - 1 {
153                writeln!(self.writer, ",")?;
154            } else {
155                writeln!(self.writer)?;
156            }
157        }
158
159        writeln!(self.writer, "}}")?;
160        Ok(())
161    }
162
163    /// Write a string definition
164    fn write_string(&mut self, name: &str, value: &Value) -> io::Result<()> {
165        write!(self.writer, "@string{{{name} = ")?;
166        self.write_value(value)?;
167        writeln!(self.writer, "}}")?;
168        Ok(())
169    }
170
171    /// Write a preamble
172    fn write_preamble(&mut self, value: &Value) -> io::Result<()> {
173        write!(self.writer, "@preamble{{")?;
174        self.write_value(value)?;
175        writeln!(self.writer, "}}")?;
176        Ok(())
177    }
178
179    /// Write a comment.
180    fn write_comment(&mut self, text: &str) -> io::Result<()> {
181        let trimmed = text.trim_start();
182        if trimmed.starts_with('%') || trimmed.starts_with('@') {
183            self.writer.write_all(text.as_bytes())?;
184            if !text.ends_with('\n') {
185                writeln!(self.writer)?;
186            }
187        } else {
188            writeln!(self.writer, "@comment{{{text}}}")?;
189        }
190        Ok(())
191    }
192
193    /// Write a value
194    fn write_value(&mut self, value: &Value) -> io::Result<()> {
195        match value {
196            Value::Literal(s) => {
197                // Quote if contains special characters
198                if needs_quoting(s) {
199                    write!(self.writer, "\"{}\"", escape_quotes(s))?;
200                } else {
201                    write!(self.writer, "{{{s}}}")?;
202                }
203            }
204            Value::Number(n) => write!(self.writer, "{n}")?,
205            Value::Variable(name) => write!(self.writer, "{name}")?,
206            Value::Concat(parts) => {
207                for (i, part) in parts.iter().enumerate() {
208                    if i > 0 {
209                        write!(self.writer, " # ")?;
210                    }
211                    self.write_value(part)?;
212                }
213            }
214        }
215        Ok(())
216    }
217}
218
219/// Check if a string needs quoting
220#[must_use]
221fn needs_quoting(s: &str) -> bool {
222    s.contains(['{', '}', ',', '='])
223}
224
225/// Escape quotes in a string
226#[must_use]
227fn escape_quotes(s: &str) -> String {
228    s.replace('"', "\\\"")
229}
230
231/// Convenience function to write a library to a string.
232#[must_use = "Check the result to detect serialization errors"]
233pub fn to_string(library: &Library) -> Result<String> {
234    let mut buf = Vec::new();
235    let mut writer = Writer::new(&mut buf);
236    writer.write_library(library)?;
237    Ok(String::from_utf8(buf).expect("valid UTF-8"))
238}
239
240/// Convenience function to write a library to a file.
241#[must_use = "Check the result to detect IO or serialization errors"]
242pub fn to_file(library: &Library, path: impl AsRef<std::path::Path>) -> Result<()> {
243    let file = std::fs::File::create(path)?;
244    let mut writer = Writer::new(file);
245    writer.write_library(library)?;
246    Ok(())
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::model::{EntryType, Field};
253    use std::borrow::Cow;
254
255    #[test]
256    fn test_write_entry() {
257        let entry = Entry {
258            ty: EntryType::Article,
259            key: Cow::Borrowed("test2023"),
260            fields: vec![
261                Field::new("author", Value::Literal(Cow::Borrowed("John Doe"))),
262                Field::new("title", Value::Literal(Cow::Borrowed("Test Article"))),
263                Field::new("year", Value::Number(2023)),
264            ],
265        };
266
267        let mut buf = Vec::new();
268        let mut writer = Writer::new(&mut buf);
269        writer.write_entry(&entry).unwrap();
270
271        let result = String::from_utf8(buf).unwrap();
272        assert!(result.contains("@article{test2023,"));
273        assert!(result.contains("author = {John Doe}"));
274        assert!(result.contains("title = {Test Article}"));
275        assert!(result.contains("year = 2023"));
276    }
277}