1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
use std::{fs, io, path::Path};
use log::debug;
use serde::Deserialize;
use crate::{kebab, manifest::DocumentManifest, walk::Walker, Author, Date};
const DEFAULT_LOCALE: &str = "en_US";
/// Enumerates the types of documents that can be generated.
///
/// The type dictates the template that will be used to generate the document.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum DocumentType {
#[default]
Article,
Report,
Book,
}
impl AsRef<str> for DocumentType {
fn as_ref(&self) -> &str {
match self {
DocumentType::Article => "article",
DocumentType::Report => "report",
DocumentType::Book => "book",
}
}
}
/// In-memory representation of a document.
///
/// # Examples
///
/// ```
/// use djoc::{Document, DocumentType};
///
/// let mut document = Document::default();
/// document
/// .title("My Document")
/// .document_type(DocumentType::Report)
/// .author("John Doe".into())
/// .author("Jane Doe".into())
/// .text("This is the first paragraph.");
///
/// assert_eq!(document.title, "My Document");
/// assert_eq!(document.document_type, DocumentType::Report);
/// assert_eq!(document.authors.len(), 2);
/// ```
pub struct Document {
pub title: String,
pub authors: Vec<Author>,
pub date: Date,
pub locale: String,
pub document_type: DocumentType,
pub(crate) texts: Vec<String>,
}
impl Default for Document {
fn default() -> Self {
Self {
title: "Document".into(),
locale: DEFAULT_LOCALE.into(),
texts: Default::default(),
authors: Default::default(),
date: Default::default(),
document_type: Default::default(),
}
}
}
impl Document {
/// Sets the document title.
pub fn title(&mut self, title: impl Into<String>) -> &mut Self {
self.title = title.into();
self
}
/// Adds a text to the document.
pub fn text(&mut self, text: impl Into<String>) -> &mut Self {
self.texts.push(text.into());
self
}
/// Adds multiple texts to the document.
pub fn texts<S: Into<String>>(&mut self, texts: impl IntoIterator<Item = S>) -> &mut Self {
texts.into_iter().for_each(|text| {
self.text(text);
});
self
}
/// Adds an author to the document.
pub fn author(&mut self, author: Author) -> &mut Self {
self.authors.push(author);
self
}
/// Adds multiple authors to the document.
pub fn authors(&mut self, authors: impl IntoIterator<Item = Author>) -> &mut Self {
self.authors.extend(authors);
self
}
/// Sets the document type.
pub fn document_type(&mut self, document_type: DocumentType) -> &mut Self {
self.document_type = document_type;
self
}
/// Sets the date of the document.
pub fn date(&mut self, date: Date) -> &mut Self {
self.date = date;
self
}
/// Sets the locale for the document.
///
/// All locales present in the [`pure-rust-locales`] crate are supported. In
/// general, most [BCP 47] language tags are supported.
///
/// [`pure-rust-locales`]: https://docs.rs/pure-rust-locales
/// [BCP 47]: https://tools.ietf.org/html/bcp47
pub fn locale(&mut self, locale: impl Into<String>) -> &mut Self {
self.locale = locale.into();
self
}
/// Creates a new document from a path. If the path points to a Djot file,
/// the document will be loaded from the file. If the path points to a
/// directory, the directory will be recursively walked and all Djot files
/// will be loaded.
///
/// # Errors
///
/// This function will return an error if the path does not exist or if any
/// of the files cannot be read.
pub fn from_path(path: impl AsRef<Path>) -> io::Result<Self> {
let path = fs::canonicalize(path)?;
let mut texts = Vec::new();
extend_texts(&path, &mut texts)?;
Ok(Self {
texts,
..Default::default()
})
}
/// Produces a filename for naming the output file(s).
pub fn filename(&self) -> String {
kebab(&self.title)
}
}
impl<S: Into<String>> From<S> for Document {
fn from(content: S) -> Self {
Self {
texts: vec![content.into()],
..Default::default()
}
}
}
impl TryFrom<DocumentManifest> for Document {
type Error = io::Error;
fn try_from(def: DocumentManifest) -> Result<Self, Self::Error> {
let mut texts = Vec::new();
for path in def.texts {
if path.is_dir() {
extend_texts(&path, &mut texts)?;
} else {
texts.push(fs::read_to_string(path)?);
}
}
Ok(Self {
texts,
date: def.date.map(|d| d.into()).unwrap_or_default(),
title: def.title.to_owned(),
authors: def.authors.clone().into_iter().map(Into::into).collect(),
locale: def.locale.clone().unwrap_or(DEFAULT_LOCALE.into()),
document_type: def.document_type,
})
}
}
fn extend_texts(path: impl AsRef<Path>, texts: &mut Vec<String>) -> io::Result<()> {
for path in Walker::new(path)?.filter_extensions(&["dj"]) {
debug!("Loading chapter from {path:?}...");
texts.push(fs::read_to_string(path)?);
}
Ok(())
}