use std::collections::BTreeMap;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use topos_bible::data::books::BookId;
use crate::{
error::Result,
package::Package,
references::Cited,
summary::{EntryHash, HashRule, VerseHash},
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TextFacts {
pub books: IndexMap<String, Vec<u8>>,
pub contents: BTreeMap<String, u64>,
pub verses: Vec<VerseHash>,
pub titles: BTreeMap<String, String>,
pub untitled: Vec<String>,
pub cited: Vec<Cited>,
pub references: u64,
pub mappings: u64,
pub entries: Vec<EntryHash>,
pub skipped: Vec<Skipped>,
}
pub trait TextReader: Send + Sync {
fn counts(&self, package: &Package) -> Result<Vec<(BookId, Vec<u8>)>>;
fn facts(&self, package: &Package, rule: HashRule) -> Result<TextFacts>;
fn timing_problems(&self, package: &Package, text: Option<&Package>) -> Result<Vec<String>>;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoReader;
impl TextReader for NoReader {
fn counts(&self, _: &Package) -> Result<Vec<(BookId, Vec<u8>)>> {
Ok(vec![])
}
fn facts(&self, _: &Package, _: HashRule) -> Result<TextFacts> {
Ok(TextFacts::default())
}
fn timing_problems(&self, _: &Package, _: Option<&Package>) -> Result<Vec<String>> {
Ok(vec![])
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SkipReason {
Peripheral,
Unknown,
NoBook,
LetteredChapter,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Skipped {
pub path: String,
pub book: String,
pub reason: SkipReason,
}
impl Skipped {
pub fn is_warning(&self) -> bool {
self.reason != SkipReason::Peripheral
}
}
impl std::fmt::Display for Skipped {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (path, book) = (&self.path, &self.book);
match self.reason {
SkipReason::Peripheral => {
write!(f, "{path}: {book} isn't a book of the Bible, passed over")
}
SkipReason::Unknown => {
write!(
f,
"{path}: {book} is a book logoxide doesn't know, passed over"
)
}
SkipReason::NoBook => write!(f, "{path}: names no book, passed over"),
SkipReason::LetteredChapter => write!(
f,
"{path}: {book} has lettered chapters (A to F), which logoxide doesn't read, passed over"
),
}
}
}
#[cfg(feature = "read")]
pub(crate) use with_text::verse_hashes;
#[cfg(feature = "read")]
pub use with_text::{DefaultReader, StorageBooks, StorageFiles, versification_map};
#[cfg(feature = "read")]
mod with_text {
use std::sync::Arc;
use logoxide_text::{Files, TextError, hash, source::TextFormat};
use topos_bible::data::books::BookId;
use super::{SkipReason, Skipped, TextFacts, TextReader};
use crate::{
books::Book,
error::{LogoxideError, Result},
manifest::{Format, PackageKind},
package::Package,
storage::Storage,
summary::{EntryHash, HashRule, VerseHash},
};
pub struct StorageFiles(pub Arc<dyn Storage>);
impl Files for StorageFiles {
fn read(&self, path: &str) -> logoxide_text::Result<Vec<u8>> {
self.0.read(path).map_err(files_error)
}
fn list(&self, dir: &str) -> logoxide_text::Result<Vec<String>> {
self.0.list(dir).map_err(files_error)
}
fn walk(&self, dir: &str) -> logoxide_text::Result<Vec<String>> {
self.0.walk(dir).map_err(files_error)
}
fn exists(&self, path: &str) -> bool {
self.0.exists(path)
}
fn read_string(&self, path: &str) -> logoxide_text::Result<String> {
self.0.read_string(path).map_err(files_error)
}
fn with_text(&self, path: &str, f: &mut dyn FnMut(&str)) -> logoxide_text::Result<()> {
self.0.with_text(path, f).map_err(files_error)
}
}
pub struct StorageBooks(pub Arc<dyn Storage>);
impl logoxide_text::cache::BookStore for StorageBooks {
fn read(&self, path: &str) -> Option<Vec<u8>> {
self.0.read(path).ok()
}
fn write(&self, path: &str, bytes: &[u8]) -> logoxide_text::Result<()> {
self.0.write(path, bytes).map_err(files_error)
}
}
fn files_error(e: LogoxideError) -> TextError {
match e {
LogoxideError::NotFound(what) => TextError::NotFound(what),
other => TextError::Files(other.to_string()),
}
}
impl From<TextError> for LogoxideError {
fn from(e: TextError) -> Self {
match e {
TextError::Book { book, message } => LogoxideError::Book { book, message },
TextError::NotFound(what) => LogoxideError::NotFound(what),
TextError::Files(what) => LogoxideError::io_message("", what),
TextError::Versification(what) => LogoxideError::Versification(what),
TextError::Timings(what) => LogoxideError::Media(what),
TextError::Archive(what) => LogoxideError::Import(what),
TextError::Entries(what) => LogoxideError::Book {
book: String::from("entries"),
message: what,
},
}
}
}
impl From<logoxide_text::Skipped> for Skipped {
fn from(s: logoxide_text::Skipped) -> Self {
Skipped {
path: s.path,
book: s.book,
reason: match s.reason {
logoxide_text::SkipReason::Peripheral => SkipReason::Peripheral,
logoxide_text::SkipReason::Unknown => SkipReason::Unknown,
logoxide_text::SkipReason::NoBook => SkipReason::NoBook,
logoxide_text::SkipReason::LetteredChapter => SkipReason::LetteredChapter,
logoxide_text::SkipReason::NotText | logoxide_text::SkipReason::Duplicate => {
SkipReason::NoBook
}
},
}
}
}
impl Format {
pub fn text_format(self) -> Option<TextFormat> {
Some(match self {
Format::Usx => TextFormat::Usx,
Format::Usfm => TextFormat::Usfm,
Format::Usj => TextFormat::Usj,
Format::Osis => TextFormat::Osis,
Format::Thml => TextFormat::Thml,
Format::Gbf => TextFormat::Gbf,
Format::Dictionary => TextFormat::Dictionary,
Format::Xref => TextFormat::Xref,
_ => return None,
})
}
}
fn text_rule(rule: HashRule) -> Result<hash::HashRule> {
hash::HashRule::from_number(rule.number()).ok_or_else(|| {
LogoxideError::Newer(format!(
"verse-hash rule {}: logoxide-text doesn't know it",
rule.number()
))
})
}
pub(crate) fn entry_hashes(
source: &dyn logoxide_text::Source,
rule: HashRule,
) -> Result<Vec<EntryHash>> {
let rule = text_rule(rule)?;
Ok((hash::entry_hashes(source, rule)?.into_iter())
.map(|r| EntryHash {
key: r.key,
hash: r.hash,
})
.collect())
}
pub(crate) fn verse_hashes(
book: &logoxide_text::Book,
rule: HashRule,
) -> Result<Vec<VerseHash>> {
let rule = text_rule(rule)?;
Ok((hash::book_hashes(book, rule).into_iter())
.filter_map(|r| {
Some(VerseHash {
book: Book::from_book_id(r.book)?,
chapter: r.chapter,
verse: r.verse,
last: r.last,
part: r.part,
hash: r.hash,
})
})
.collect())
}
pub fn versification_map(
package: &Package,
) -> Result<logoxide_text::versification::VersificationMap> {
let info = package.info();
let (Some(to), PackageKind::VersificationMap) = (&info.maps_to, info.kind) else {
return Err(LogoxideError::Versification(format!(
"{} isn't a versification map (kind = \"versification-map\", with maps-to)",
info.id
)));
};
let storage = package.storage();
let dirs: Vec<&str> = (package.manifest().formats(package.enabled()).into_iter())
.filter(|(name, _)| Format::from_name(name) == Some(Format::Vrs))
.map(|(_, entry)| entry.path.trim_end_matches('/'))
.collect();
let mut files = vec![];
for path in crate::package::declared_files(storage.as_ref())? {
let inside = (dirs.iter()).any(|d| path == *d || path.starts_with(&format!("{d}/")));
if inside && path.to_ascii_lowercase().ends_with(".vrs") {
let text = storage.read_string(&path)?;
files.push((path, text));
}
}
if files.is_empty() {
return Err(LogoxideError::Versification(format!(
"{}: no .vrs file in its formats",
info.id
)));
}
let files = files.iter().map(|(p, t)| (p.as_str(), t.as_str()));
Ok(logoxide_text::versification::VersificationMap::parse(
&info.versification,
to,
files,
)?)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct DefaultReader;
impl TextReader for DefaultReader {
fn counts(&self, package: &Package) -> Result<Vec<(BookId, Vec<u8>)>> {
let source = package.source();
(source.books().into_iter())
.map(|id| Ok((id, source.chapter_verses(id)?)))
.collect()
}
fn facts(&self, package: &Package, rule: HashRule) -> Result<TextFacts> {
let source = package.source();
let mut facts = TextFacts::default();
for id in source.books() {
let book = source.book(id)?;
let code = crate::bible_data()
.books()
.get_usfm(id)
.cloned()
.unwrap_or_else(|| id.0.to_string());
facts.books.insert(code, book.chapter_verses());
hash::count_elements(&book.nodes, &mut facts.contents);
facts.verses.extend(verse_hashes(&book, rule)?);
}
for titled in crate::titles::titles(package)? {
match titled.title {
Some(title) => {
facts.titles.insert(titled.path, title);
}
None => facts.untitled.push(titled.path),
}
}
let found = crate::references::find(package)?;
facts.references = found.len() as u64;
facts.cited = crate::references::cited(&found);
if package.info().kind == PackageKind::VersificationMap {
facts.mappings = versification_map(package)?.len() as u64;
}
facts.entries = entry_hashes(source, rule)?;
facts.skipped = (source.skipped().iter()).cloned().map(Into::into).collect();
Ok(facts)
}
fn timing_problems(
&self,
package: &Package,
text: Option<&Package>,
) -> Result<Vec<String>> {
crate::media::check_package_timings_with(package, text)
}
}
}