Skip to main content

setzer_import/
file.rs

1//! Reading a manuscript file: the one place that turns a path into an
2//! `Imported`, for the CLI and the app façade alike. The importers
3//! themselves take bytes or text and never touch the filesystem.
4
5use std::fs;
6use std::io::{self, Read};
7use std::path::Path;
8
9use crate::{ImportError, Imported};
10
11/// Why a manuscript file could not be read.
12#[derive(Debug, thiserror::Error)]
13pub enum ReadError {
14    /// Not a `.md`, `.markdown` or `.docx` file.
15    #[error("not a Markdown or Word file")]
16    Unsupported,
17    #[error("{0}")]
18    Io(#[from] io::Error),
19    #[error("not UTF-8 text")]
20    NotUtf8,
21    #[error("{0}")]
22    Import(#[from] ImportError),
23}
24
25/// The book, its images and the import warnings, by file extension
26/// (`.md`, `.markdown`, `.docx`). A Markdown manuscript's images are read
27/// relative to its folder.
28pub fn read_manuscript(path: &Path) -> Result<Imported, ReadError> {
29    let ext = path
30        .extension()
31        .and_then(|e| e.to_str())
32        .map(str::to_ascii_lowercase);
33    match ext.as_deref() {
34        Some("md" | "markdown") => {
35            let bytes = fs::read(path)?;
36            let src = std::str::from_utf8(&bytes).map_err(|_| ReadError::NotUtf8)?;
37            // Windows editors start UTF-8 files with a byte order mark.
38            let src = src.strip_prefix('\u{feff}').unwrap_or(src);
39            let dir = path.parent().unwrap_or(Path::new(""));
40            Ok(crate::markdown::import(src, |p| read_image(dir, p))?)
41        }
42        Some("docx") => Ok(crate::docx::import(&fs::read(path)?)?),
43        _ => Err(ReadError::Unsupported),
44    }
45}
46
47/// An image a Markdown manuscript refers to, relative to its folder. Reads
48/// at most one byte over the import limit, so the importer can tell a file
49/// that is too large without all of it in memory.
50pub fn read_image(dir: &Path, path: &str) -> Option<Vec<u8>> {
51    let file = dir.join(path);
52    if !file.is_file() {
53        return None;
54    }
55    let mut bytes = Vec::new();
56    fs::File::open(file)
57        .ok()?
58        .take(crate::MAX_IMAGE_BYTES + 1)
59        .read_to_end(&mut bytes)
60        .ok()?;
61    Some(bytes)
62}