1use crate::{Block, Entry, Library, ParsedBlock, ParsedDocument, ParsedEntry, Result, Value};
4use std::borrow::Cow;
5use std::io::{self, Write};
6
7#[derive(Debug, Clone)]
9pub struct WriterConfig {
10 pub indent: String,
12 pub align_values: bool,
14 pub max_line_length: usize,
16 pub sort_entries: bool,
18 pub sort_fields: bool,
20 pub raw_write_mode: RawWriteMode,
22 pub trailing_comma: TrailingComma,
24 pub entry_separator: String,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum RawWriteMode {
31 Preserve,
33 Normalize,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum TrailingComma {
40 Omit,
42 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#[derive(Debug)]
63pub struct Writer<W: Write> {
64 writer: W,
65 config: WriterConfig,
66}
67
68impl<W: Write> Writer<W> {
69 pub fn new(writer: W) -> Self {
71 Self {
72 writer,
73 config: WriterConfig::default(),
74 }
75 }
76
77 pub const fn with_config(writer: W, config: WriterConfig) -> Self {
79 Self { writer, config }
80 }
81
82 #[must_use]
84 pub fn config_mut(&mut self) -> &mut WriterConfig {
85 &mut self.config
86 }
87
88 #[must_use]
90 pub fn into_inner(self) -> W {
91 self.writer
92 }
93
94 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 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 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 for preamble in library.preambles() {
202 self.write_preamble(&preamble.value)?;
203 writeln!(self.writer)?;
204 }
205
206 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 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 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 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 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 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 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 fn write_value(&mut self, value: &Value) -> io::Result<()> {
315 match value {
316 Value::Literal(s) => {
317 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#[must_use]
341fn needs_quoting(s: &str) -> bool {
342 s.contains(['{', '}', ',', '='])
343}
344
345#[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#[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#[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#[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#[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}