Skip to main content

bibtex_parser/
writer.rs

1//! BibTeX writer for serializing libraries
2
3use crate::{Block, Entry, Library, ParsedBlock, ParsedDocument, ParsedEntry, Result, Value};
4use std::borrow::Cow;
5use std::io::{self, Write};
6
7/// Configuration for writing BibTeX
8#[derive(Debug, Clone)]
9pub struct WriterConfig {
10    /// Indentation string (default: "  ")
11    pub indent: String,
12    /// Whether to align field values (default: false)
13    pub align_values: bool,
14    /// Maximum line length for wrapping (default: 80)
15    pub max_line_length: usize,
16    /// Whether to sort entries by key (default: false)
17    pub sort_entries: bool,
18    /// Whether to sort fields within entries (default: false)
19    pub sort_fields: bool,
20    /// Raw-backed document writing behavior.
21    pub raw_write_mode: RawWriteMode,
22    /// Trailing comma behavior for structured entry writing.
23    pub trailing_comma: TrailingComma,
24    /// Separator written between document blocks.
25    pub entry_separator: String,
26}
27
28/// Raw-backed document writing behavior.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum RawWriteMode {
31    /// Reuse retained raw text where possible.
32    Preserve,
33    /// Ignore retained raw text and write normalized structured data.
34    Normalize,
35}
36
37/// Trailing comma behavior for structured entry writing.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum TrailingComma {
40    /// Omit a trailing comma after the final field.
41    Omit,
42    /// Add a trailing comma after the final field.
43    Always,
44}
45
46impl Default for WriterConfig {
47    fn default() -> Self {
48        Self {
49            indent: "  ".to_string(),
50            align_values: false,
51            max_line_length: 80,
52            sort_entries: false,
53            sort_fields: false,
54            raw_write_mode: RawWriteMode::Preserve,
55            trailing_comma: TrailingComma::Omit,
56            entry_separator: "\n".to_string(),
57        }
58    }
59}
60
61/// BibTeX writer
62#[derive(Debug)]
63pub struct Writer<W: Write> {
64    writer: W,
65    config: WriterConfig,
66}
67
68impl<W: Write> Writer<W> {
69    /// Create a new writer with default configuration
70    pub fn new(writer: W) -> Self {
71        Self {
72            writer,
73            config: WriterConfig::default(),
74        }
75    }
76
77    /// Create a new writer with custom configuration
78    pub const fn with_config(writer: W, config: WriterConfig) -> Self {
79        Self { writer, config }
80    }
81
82    /// Access the writer configuration mutably
83    #[must_use]
84    pub fn config_mut(&mut self) -> &mut WriterConfig {
85        &mut self.config
86    }
87
88    /// Consume the writer and return the underlying writer
89    #[must_use]
90    pub fn into_inner(self) -> W {
91        self.writer
92    }
93
94    /// Write a complete library.
95    pub fn write_library(&mut self, library: &Library) -> io::Result<()> {
96        if self.config.sort_entries {
97            return self.write_library_sorted(library);
98        }
99
100        for (index, block) in library.blocks().into_iter().enumerate() {
101            if index > 0 {
102                writeln!(self.writer)?;
103            }
104            match block {
105                Block::Entry(entry, _) => self.write_entry(entry)?,
106                Block::String(definition) => {
107                    self.write_string(&definition.name, &definition.value)?;
108                }
109                Block::Preamble(preamble) => self.write_preamble(&preamble.value)?,
110                Block::Comment(comment) => self.write_comment(comment.text())?,
111                Block::Failed(failed) => self.writer.write_all(failed.raw.as_bytes())?,
112            }
113        }
114
115        Ok(())
116    }
117
118    /// Write a parsed document, reusing retained raw blocks when configured.
119    pub fn write_document(&mut self, document: &ParsedDocument) -> io::Result<()> {
120        for (index, block) in document.blocks().iter().copied().enumerate() {
121            if index > 0 {
122                self.writer
123                    .write_all(self.config.entry_separator.as_bytes())?;
124            }
125
126            match block {
127                ParsedBlock::Entry(entry_index) => {
128                    self.write_parsed_entry(&document.entries()[entry_index])?;
129                }
130                ParsedBlock::String(string_index) => {
131                    let string = &document.strings()[string_index];
132                    if self.config.raw_write_mode == RawWriteMode::Preserve {
133                        if let Some(raw) = &string.raw {
134                            self.writer.write_all(raw.as_bytes())?;
135                            continue;
136                        }
137                    }
138                    self.write_string(&string.name, &string.value.value)?;
139                }
140                ParsedBlock::Preamble(preamble_index) => {
141                    let preamble = &document.preambles()[preamble_index];
142                    if self.config.raw_write_mode == RawWriteMode::Preserve {
143                        if let Some(raw) = &preamble.raw {
144                            self.writer.write_all(raw.as_bytes())?;
145                            continue;
146                        }
147                    }
148                    self.write_preamble(&preamble.value.value)?;
149                }
150                ParsedBlock::Comment(comment_index) => {
151                    let comment = &document.comments()[comment_index];
152                    if self.config.raw_write_mode == RawWriteMode::Preserve {
153                        if let Some(raw) = &comment.raw {
154                            self.writer.write_all(raw.as_bytes())?;
155                            continue;
156                        }
157                    }
158                    self.write_comment(&comment.text)?;
159                }
160                ParsedBlock::Failed(failed_index) => {
161                    self.writer
162                        .write_all(document.failed_blocks()[failed_index].raw.as_bytes())?;
163                }
164            }
165        }
166
167        Ok(())
168    }
169
170    /// Write selected parsed-document entries in source order.
171    ///
172    /// Non-entry blocks are skipped. Duplicate keys in `keys` do not duplicate
173    /// output entries.
174    pub fn write_selected_entries(
175        &mut self,
176        document: &ParsedDocument,
177        keys: &[&str],
178    ) -> io::Result<()> {
179        let mut written = 0usize;
180        for block in document.blocks().iter().copied() {
181            let ParsedBlock::Entry(entry_index) = block else {
182                continue;
183            };
184            let entry = &document.entries()[entry_index];
185            if !keys.iter().any(|key| *key == entry.key()) {
186                continue;
187            }
188            if written > 0 {
189                self.writer
190                    .write_all(self.config.entry_separator.as_bytes())?;
191            }
192            self.write_parsed_entry(entry)?;
193            written += 1;
194        }
195
196        Ok(())
197    }
198
199    fn write_library_sorted(&mut self, library: &Library) -> io::Result<()> {
200        // Write preambles
201        for preamble in library.preambles() {
202            self.write_preamble(&preamble.value)?;
203            writeln!(self.writer)?;
204        }
205
206        // Write strings
207        let mut strings: Vec<_> = library.strings().iter().collect();
208        if self.config.sort_entries {
209            strings.sort_by(|a, b| a.name.cmp(&b.name));
210        }
211
212        for definition in strings {
213            self.write_string(&definition.name, &definition.value)?;
214            writeln!(self.writer)?;
215        }
216
217        // Write entries
218        let mut entries = library.entries().iter().collect::<Vec<_>>();
219        if self.config.sort_entries {
220            entries.sort_by(|a, b| a.key.cmp(&b.key));
221        }
222
223        for (i, entry) in entries.iter().enumerate() {
224            if i > 0 {
225                writeln!(self.writer)?;
226            }
227            self.write_entry(entry)?;
228        }
229
230        Ok(())
231    }
232
233    /// Write a single entry
234    pub fn write_entry(&mut self, entry: &Entry) -> io::Result<()> {
235        writeln!(self.writer, "@{}{{{},", entry.ty, entry.key)?;
236
237        let mut fields = entry.fields().to_vec();
238        if self.config.sort_fields {
239            fields.sort_by(|a, b| a.name.cmp(&b.name));
240        }
241
242        // Calculate alignment if needed
243        let max_name_len = if self.config.align_values {
244            fields.iter().map(|f| f.name.len()).max().unwrap_or(0)
245        } else {
246            0
247        };
248
249        for (i, field) in fields.iter().enumerate() {
250            write!(self.writer, "{}", self.config.indent)?;
251            write!(self.writer, "{}", field.name)?;
252
253            if self.config.align_values {
254                let padding = max_name_len - field.name.len();
255                write!(self.writer, "{}", " ".repeat(padding))?;
256            }
257
258            write!(self.writer, " = ")?;
259            self.write_value(&field.value)?;
260
261            if i < fields.len() - 1 || self.config.trailing_comma == TrailingComma::Always {
262                writeln!(self.writer, ",")?;
263            } else {
264                writeln!(self.writer)?;
265            }
266        }
267
268        writeln!(self.writer, "}}")?;
269        Ok(())
270    }
271
272    fn write_parsed_entry(&mut self, entry: &ParsedEntry) -> io::Result<()> {
273        if self.config.raw_write_mode == RawWriteMode::Preserve {
274            if let Some(raw) = patched_entry_raw(entry) {
275                self.writer.write_all(raw.as_bytes())?;
276                return Ok(());
277            }
278        }
279
280        self.write_entry(&entry.clone().into_entry())
281    }
282
283    /// Write a string definition
284    fn write_string(&mut self, name: &str, value: &Value) -> io::Result<()> {
285        write!(self.writer, "@string{{{name} = ")?;
286        self.write_value(value)?;
287        writeln!(self.writer, "}}")?;
288        Ok(())
289    }
290
291    /// Write a preamble
292    fn write_preamble(&mut self, value: &Value) -> io::Result<()> {
293        write!(self.writer, "@preamble{{")?;
294        self.write_value(value)?;
295        writeln!(self.writer, "}}")?;
296        Ok(())
297    }
298
299    /// Write a comment.
300    fn write_comment(&mut self, text: &str) -> io::Result<()> {
301        let trimmed = text.trim_start();
302        if trimmed.starts_with('%') || trimmed.starts_with('@') {
303            self.writer.write_all(text.as_bytes())?;
304            if !text.ends_with('\n') {
305                writeln!(self.writer)?;
306            }
307        } else {
308            writeln!(self.writer, "@comment{{{text}}}")?;
309        }
310        Ok(())
311    }
312
313    /// Write a value
314    fn write_value(&mut self, value: &Value) -> io::Result<()> {
315        match value {
316            Value::Literal(s) => {
317                // Quote if contains special characters
318                if needs_quoting(s) {
319                    write!(self.writer, "\"{}\"", escape_quotes(s))?;
320                } else {
321                    write!(self.writer, "{{{s}}}")?;
322                }
323            }
324            Value::Number(n) => write!(self.writer, "{n}")?,
325            Value::Variable(name) => write!(self.writer, "{name}")?,
326            Value::Concat(parts) => {
327                for (i, part) in parts.iter().enumerate() {
328                    if i > 0 {
329                        write!(self.writer, " # ")?;
330                    }
331                    self.write_value(part)?;
332                }
333            }
334        }
335        Ok(())
336    }
337}
338
339/// Check if a string needs quoting
340#[must_use]
341fn needs_quoting(s: &str) -> bool {
342    s.contains(['{', '}', ',', '='])
343}
344
345/// Escape quotes in a string
346#[must_use]
347fn escape_quotes(s: &str) -> String {
348    s.replace('"', "\\\"")
349}
350
351fn patched_entry_raw<'entry>(entry: &'entry ParsedEntry<'_>) -> Option<Cow<'entry, str>> {
352    let raw = entry.raw.as_deref()?;
353    let source = entry.source?;
354    let mut replacements = Vec::new();
355
356    push_token_replacement(
357        &mut replacements,
358        raw,
359        source.byte_start,
360        entry.entry_type_source,
361        &entry.ty.to_string(),
362        |raw_type| crate::EntryType::parse(raw_type) == entry.ty,
363    )?;
364    push_token_replacement(
365        &mut replacements,
366        raw,
367        source.byte_start,
368        entry.key_source,
369        &entry.key,
370        |raw_key| raw_key == entry.key,
371    )?;
372
373    for field in &entry.fields {
374        push_token_replacement(
375            &mut replacements,
376            raw,
377            source.byte_start,
378            field.name_source,
379            &field.name,
380            |raw_name| raw_name == field.name,
381        )?;
382
383        if field.value.raw.is_none() {
384            let value_source = field.value_source?;
385            let start = value_source.byte_start.checked_sub(source.byte_start)?;
386            let end = value_source.byte_end.checked_sub(source.byte_start)?;
387            replacements.push((start, end, field.value.value.to_bibtex_source()));
388        }
389    }
390
391    if replacements.is_empty() {
392        return Some(Cow::Borrowed(raw));
393    }
394
395    replacements.sort_by_key(|(start, _, _)| *start);
396    let mut output = String::with_capacity(raw.len());
397    let mut cursor = 0;
398    for (start, end, replacement) in replacements {
399        if start < cursor || end > raw.len() {
400            return None;
401        }
402        output.push_str(&raw[cursor..start]);
403        output.push_str(&replacement);
404        cursor = end;
405    }
406    output.push_str(&raw[cursor..]);
407    Some(Cow::Owned(output))
408}
409
410fn push_token_replacement(
411    replacements: &mut Vec<(usize, usize, String)>,
412    raw: &str,
413    base: usize,
414    span: Option<crate::SourceSpan>,
415    replacement: &str,
416    unchanged: impl FnOnce(&str) -> bool,
417) -> Option<()> {
418    let span = span?;
419    let start = span.byte_start.checked_sub(base)?;
420    let end = span.byte_end.checked_sub(base)?;
421    let original = raw.get(start..end)?;
422    if !unchanged(original) {
423        replacements.push((start, end, replacement.to_string()));
424    }
425    Some(())
426}
427
428/// Convenience function to write a library to a string.
429#[must_use = "Check the result to detect serialization errors"]
430pub fn to_string(library: &Library) -> Result<String> {
431    let mut buf = Vec::new();
432    let mut writer = Writer::new(&mut buf);
433    writer.write_library(library)?;
434    Ok(String::from_utf8(buf).expect("valid UTF-8"))
435}
436
437/// Convenience function to write a parsed document to a string.
438#[must_use = "Check the result to detect serialization errors"]
439pub fn document_to_string(document: &ParsedDocument) -> Result<String> {
440    let mut buf = Vec::new();
441    let mut writer = Writer::new(&mut buf);
442    writer.write_document(document)?;
443    Ok(String::from_utf8(buf).expect("valid UTF-8"))
444}
445
446/// Convenience function to write selected parsed-document entries to a string.
447#[must_use = "Check the result to detect serialization errors"]
448pub fn selected_entries_to_string(document: &ParsedDocument, keys: &[&str]) -> Result<String> {
449    let mut buf = Vec::new();
450    let mut writer = Writer::new(&mut buf);
451    writer.write_selected_entries(document, keys)?;
452    Ok(String::from_utf8(buf).expect("valid UTF-8"))
453}
454
455/// Convenience function to write a library to a file.
456#[must_use = "Check the result to detect IO or serialization errors"]
457pub fn to_file(library: &Library, path: impl AsRef<std::path::Path>) -> Result<()> {
458    let file = std::fs::File::create(path)?;
459    let mut writer = Writer::new(file);
460    writer.write_library(library)?;
461    Ok(())
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::model::{EntryType, Field};
468    use std::borrow::Cow;
469
470    #[test]
471    fn test_write_entry() {
472        let entry = Entry {
473            ty: EntryType::Article,
474            key: Cow::Borrowed("test2023"),
475            fields: vec![
476                Field::new("author", Value::Literal(Cow::Borrowed("John Doe"))),
477                Field::new("title", Value::Literal(Cow::Borrowed("Test Article"))),
478                Field::new("year", Value::Number(2023)),
479            ],
480        };
481
482        let mut buf = Vec::new();
483        let mut writer = Writer::new(&mut buf);
484        writer.write_entry(&entry).unwrap();
485
486        let result = String::from_utf8(buf).unwrap();
487        assert!(result.contains("@article{test2023,"));
488        assert!(result.contains("author = {John Doe}"));
489        assert!(result.contains("title = {Test Article}"));
490        assert!(result.contains("year = 2023"));
491    }
492}