-use std::{borrow::Cow, fs::File, io::Write, sync::Arc};
-
-use csv::Writer;
+use std::{borrow::Cow, fmt::Display, fs::File, io::Write, sync::Arc};
use crate::output::pivot::Coord2;
struct CsvDriver {
file: File,
+ options: CsvOptions,
/// Number of items written so far.
n_items: usize,
}
+#[derive(Copy, Clone, Debug)]
+struct CsvOptions {
+ quote: u8,
+ delimiter: u8,
+}
+
+impl Default for CsvOptions {
+ fn default() -> Self {
+ Self {
+ quote: b'"',
+ delimiter: b',',
+ }
+ }
+}
+
+impl CsvOptions {
+ fn byte_needs_quoting(&self, b: u8) -> bool {
+ b == b'\r' || b == b'\n' || b == self.quote || b == self.delimiter
+ }
+
+ fn string_needs_quoting(&self, s: &str) -> bool {
+ s.bytes().find(|&b| self.byte_needs_quoting(b)).is_some()
+ }
+}
+
+struct CsvField<'a> {
+ text: &'a str,
+ options: CsvOptions,
+}
+
+impl<'a> CsvField<'a> {
+ fn new(text: &'a str, options: CsvOptions) -> Self {
+ Self { text, options }
+ }
+}
+
+impl<'a> Display for CsvField<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if self.options.string_needs_quoting(self.text) {
+ let quote = self.options.quote as char;
+ write!(f, "{quote}")?;
+ for c in self.text.chars() {
+ if c == quote {
+ write!(f, "{c}")?;
+ }
+ write!(f, "{c}")?;
+ }
+ write!(f, "{quote}")
+ } else {
+ write!(f, "{}", self.text)
+ }
+ }
+}
+
impl CsvDriver {
pub fn new(file: File) -> Self {
- Self { file, n_items: 0 }
+ Self {
+ file,
+ options: CsvOptions::default(),
+ n_items: 0,
+ }
}
fn start_item(&mut self) {
let output = pt.output(layer, true);
self.start_item();
- let mut writer = Writer::from_writer(&mut self.file);
- output_table(&mut writer, pt, output.title.as_ref(), Some("Table"))?;
- output_table(&mut writer, pt, output.layers.as_ref(), Some("Layer"))?;
- output_table(&mut writer, pt, Some(&output.body), None)?;
- output_table(&mut writer, pt, output.caption.as_ref(), Some("Caption"))?;
- output_table(&mut writer, pt, output.footnotes.as_ref(), Some("Footnote"))?;
+ self.output_table(pt, output.title.as_ref(), Some("Table"))?;
+ self.output_table(pt, output.layers.as_ref(), Some("Layer"))?;
+ self.output_table(pt, Some(&output.body), None)?;
+ self.output_table(pt, output.caption.as_ref(), Some("Caption"))?;
+ self.output_table(pt, output.footnotes.as_ref(), Some("Footnote"))?;
Ok(())
}
-}
-fn output_table<W>(
- writer: &mut Writer<W>,
- pivot_table: &PivotTable,
- table: Option<&Table>,
- leader: Option<&str>,
-) -> Result<(), csv::Error>
-where
- W: Write,
-{
- let Some(table) = table else {
- return Ok(());
- };
-
- for y in 0..table.n.y() {
- for x in 0..table.n.x() {
- let coord = Coord2::new(x, y);
- let content = table.get(coord);
- match &content.inner().value {
- Some(value) if content.is_top_left(coord) => {
- let display = value.display(Some(pivot_table));
- let s = match leader {
- Some(leader) if x == 0 && y == 0 => format!("{leader}: {display}"),
- _ => display.to_string(),
- };
- writer.write_field(&s)?
+ fn output_table(
+ &mut self,
+ pivot_table: &PivotTable,
+ table: Option<&Table>,
+ leader: Option<&str>,
+ ) -> Result<(), csv::Error> {
+ let Some(table) = table else {
+ return Ok(());
+ };
+
+ for y in 0..table.n.y() {
+ for x in 0..table.n.x() {
+ if x > 0 {
+ write!(&mut self.file, "{}", self.options.delimiter as char).unwrap();
+ }
+
+ let coord = Coord2::new(x, y);
+ let content = table.get(coord);
+ if content.is_top_left(coord) {
+ if let Some(value) = &content.inner().value {
+ let display = value.display(Some(pivot_table));
+ let s = match leader {
+ Some(leader) if x == 0 && y == 0 => format!("{leader}: {display}"),
+ _ => display.to_string(),
+ };
+ write!(&mut self.file, "{}", CsvField::new(&s, self.options)).unwrap();
+ }
}
- _ => writer.write_field("")?,
}
+ writeln!(&mut self.file).unwrap();
}
- writer.write_record(None::<&[u8]>)?;
- }
- Ok(())
+ Ok(())
+ }
}
impl Driver for CsvDriver {
Details::Chart | Details::Image | Details::Group(_) => (),
Details::Message(diagnostic) => {
self.start_item();
- Writer::from_writer(&mut self.file)
- .write_record([diagnostic.to_string()])
- .unwrap();
+ let text = diagnostic.to_string();
+ writeln!(&self.file, "{}", CsvField::new(&text, self.options)).unwrap();
}
Details::Table(pivot_table) => {
for layer in pivot_table.layers(true) {
TextType::Syntax | TextType::PageTitle => (),
TextType::Title | TextType::Log => {
self.start_item();
- let mut writer = Writer::from_writer(&mut self.file);
for line in text.content.display(None).to_string().lines() {
- writer.write_record([line]).unwrap();
+ writeln!(&self.file, "{}", CsvField::new(line, self.options)).unwrap();
}
}
},
}
fn flush(&mut self) {
- self.file.flush();
+ let _ = self.file.flush();
}
}