use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::{Path, PathBuf};
use unhwp::model::{Block, Metadata, Section, StyleRegistry};
use unhwp::render::{render_frontmatter, MarkdownRenderer, RenderOptions};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Markdown,
Text,
Json,
}
impl OutputFormat {
pub fn from_str(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"md" | "markdown" => Some(Self::Markdown),
"txt" | "text" => Some(Self::Text),
"json" => Some(Self::Json),
_ => None,
}
}
}
pub struct MultiFormatWriter {
txt: Option<BufWriter<File>>,
txt_path: Option<PathBuf>,
json: Option<BufWriter<File>>,
json_path: Option<PathBuf>,
json_first_section: bool,
md: Option<BufWriter<File>>,
md_path: Option<PathBuf>,
md_renderer: Option<MarkdownRenderer>,
word_count: usize,
}
impl MultiFormatWriter {
pub fn new(
out_dir: &Path,
formats: &[OutputFormat],
render_opts: RenderOptions,
styles: &StyleRegistry,
) -> io::Result<Self> {
let want_md = formats.contains(&OutputFormat::Markdown);
let want_txt = formats.contains(&OutputFormat::Text);
let want_json = formats.contains(&OutputFormat::Json);
let _ = styles;
let (md, md_path, md_renderer) = if want_md {
let p = out_dir.join("extract.md");
let f = File::create(&p)?;
let renderer = MarkdownRenderer::new(render_opts.clone());
(Some(BufWriter::new(f)), Some(p), Some(renderer))
} else {
(None, None, None)
};
let (txt, txt_path) = if want_txt {
let p = out_dir.join("extract.txt");
let f = File::create(&p)?;
(Some(BufWriter::new(f)), Some(p))
} else {
(None, None)
};
let (json, json_path) = if want_json {
let p = out_dir.join("content.json");
let f = File::create(&p)?;
(Some(BufWriter::new(f)), Some(p))
} else {
(None, None)
};
Ok(Self {
txt,
txt_path,
json,
json_path,
json_first_section: true,
md,
md_path,
md_renderer,
word_count: 0,
})
}
pub fn write_document_start(
&mut self,
metadata: &Metadata,
styles: &StyleRegistry,
) -> io::Result<()> {
if let Some(ref mut json) = self.json {
let meta_json = serde_json::to_string(metadata)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let styles_json = serde_json::to_string(styles)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
write!(
json,
"{{\"metadata\":{},\"styles\":{},\"sections\":[",
meta_json, styles_json
)?;
}
if let (Some(ref mut md), Some(ref renderer)) = (&mut self.md, &self.md_renderer) {
if renderer.options().include_frontmatter {
let frontmatter = render_frontmatter(metadata);
if !frontmatter.is_empty() {
md.write_all(frontmatter.as_bytes())?;
}
}
}
Ok(())
}
pub fn write_section(&mut self, section: &Section, styles: &StyleRegistry) -> io::Result<()> {
if let Some(ref mut txt) = self.txt {
for block in §ion.content {
match block {
Block::Paragraph(p) => {
let line = p.plain_text();
self.word_count += line.split_whitespace().count();
writeln!(txt, "{}", line)?;
}
Block::Table(t) => {
for row in &t.rows {
for cell in &row.cells {
let text = cell.plain_text();
if !text.is_empty() {
self.word_count += text.split_whitespace().count();
writeln!(txt, "{}", text)?;
}
}
}
}
}
}
} else {
for block in §ion.content {
match block {
Block::Paragraph(p) => {
self.word_count += p.plain_text().split_whitespace().count();
}
Block::Table(t) => {
for row in &t.rows {
for cell in &row.cells {
let text = cell.plain_text();
if !text.is_empty() {
self.word_count += text.split_whitespace().count();
}
}
}
}
}
}
}
if let Some(ref mut json) = self.json {
if !self.json_first_section {
write!(json, ",")?;
}
let section_json = serde_json::to_string(section)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
write!(json, "{}", section_json)?;
self.json_first_section = false;
}
if let (Some(ref mut md), Some(ref renderer)) = (&mut self.md, &self.md_renderer) {
let rendered =
MarkdownRenderer::render_section_standalone(section, styles, renderer.options())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
md.write_all(rendered.as_bytes())?;
}
Ok(())
}
pub fn finish(mut self) -> io::Result<WriteSummary> {
let mut summary = WriteSummary::default();
if let (Some(mut md), Some(md_path)) = (self.md.take(), self.md_path.take()) {
md.flush()?;
summary.md_path = Some(md_path);
}
if let (Some(mut txt), Some(txt_path)) = (self.txt.take(), self.txt_path.take()) {
txt.flush()?;
summary.txt_path = Some(txt_path);
}
if let (Some(mut json), Some(json_path)) = (self.json.take(), self.json_path.take()) {
write!(json, "]}}")?;
json.flush()?;
summary.json_path = Some(json_path);
}
summary.word_count = self.word_count;
Ok(summary)
}
}
#[derive(Default)]
pub struct WriteSummary {
pub md_path: Option<PathBuf>,
pub txt_path: Option<PathBuf>,
pub json_path: Option<PathBuf>,
pub word_count: usize,
}