logoxide_import/catalog.rs
1/*!
2Other publishers' lists as logoxide catalogs, each entry a [`CatalogSource::External`] saying
3what its import downloads:
4
5- **eBible.org's list** (`translations.csv`, [`logoxide::catalog::EBIBLE_CATALOG`]): about
6 1,500 translations it publishes as USFM ([`crate::ebible::catalog`])
7- **fetch.bible's manifest** (`manifest.json`, [`logoxide::catalog::FETCH_BIBLE_CATALOG`]):
8 about 1,400 translations it publishes as USX, a file per book ([`crate::fetch::catalog`])
9- **CCEL's Bibles** (its page of them, [`logoxide::catalog::CCEL_CATALOG`]): about 50 Bibles it
10 publishes as ThML, a file each. The page gives only titles: the language, rights, and version
11 are in each file's head, read when it's imported ([`crate::ccel::catalog`])
12- **CCEL's genre indexes** (`https://ccel.org/index/genre/gen-COM`, its 213 commentaries;
13 `gen-BIB`, its Bibles; any other genre's works as books): each work a `ccel:<author>/<work>`
14 import of its ThML ([`crate::ccel::genre_catalog`])
15- **Door43's catalog** (its catalog API, [`crate::door43::CATALOG`]): the Bibles in production,
16 each a release's archive ([`crate::door43::catalog`]; the API's JSON is as remembered, since
17 it didn't answer when this was written)
18- **SWORD repositories** (a repository's `mods.d.tar.gz`, every module's conf;
19 [`logoxide::catalog::SWORD_CATALOG`] is CrossWire's): the Bibles and commentaries whose markup
20 is OSIS, ThML, or GBF, and the dictionaries in any markup logoxide-markup reads, which aren't
21 locked, each a module zip ([`crate::sword::catalog`])
22
23[`parse`] reads any of them, or a `catalog.toml`, by where it came from.
24*/
25
26use logoxide::{
27 Result,
28 catalog::{Catalog, CatalogSource},
29};
30
31/// A catalog from a file's bytes and where it came from (a URL or a path): a SWORD repository's
32/// when `location` ends in `.tar.gz`, else its text ([`parse_text`])
33pub fn parse(location: &str, bytes: &[u8]) -> Result<Catalog> {
34 let path = location.split(['?', '#']).next().unwrap_or(location);
35 match path.to_ascii_lowercase().ends_with(".tar.gz") {
36 true => crate::sword::catalog(location, bytes),
37 false => parse_text(location, &String::from_utf8_lossy(bytes)),
38 }
39}
40
41/// A catalog from a file's text: eBible.org's CSV when `location` ends in `.csv`, fetch.bible's
42/// manifest when it ends in `.json`, CCEL's page of Bibles or a genre index of its when it's on
43/// ccel.org, Door43's catalog API, else a `catalog.toml` ([`Catalog::parse`])
44pub fn parse_text(location: &str, text: &str) -> Result<Catalog> {
45 let path = location
46 .split(['?', '#'])
47 .next()
48 .unwrap_or(location)
49 .to_ascii_lowercase();
50 if crate::door43::is_catalog(location) {
51 crate::door43::catalog(text)
52 } else if let Some(genre) = crate::ccel::genre_of(location) {
53 crate::ccel::genre_catalog(text, genre)
54 } else if path.contains("ccel.org/ccel/") {
55 crate::ccel::catalog(text)
56 } else if path.ends_with(".csv") {
57 crate::ebible::catalog(text)
58 } else if path.ends_with(".json") {
59 crate::fetch::catalog(text)
60 } else {
61 Catalog::parse(location, text)
62 }
63}
64
65/// Whether `location` is one of the publishers' lists this crate reads, by its form (as
66/// [`parse`] tells them apart), rather than a `catalog.toml`
67pub fn is_publishers(location: &str) -> bool {
68 let path = location
69 .split(['?', '#'])
70 .next()
71 .unwrap_or(location)
72 .to_ascii_lowercase();
73 path.contains("ccel.org/ccel/")
74 || path.contains("ccel.org/index/genre/")
75 || crate::door43::is_catalog(&path)
76 || path.ends_with(".csv")
77 || path.ends_with(".json")
78 || path.ends_with(".tar.gz")
79}
80
81/// The entries' sources as `lx search` and hosts show them: a short name for each publisher
82/// (`eBible.org`, `fetch.bible`, `CCEL`, `SWORD`, `Door43`), by an entry's import prefix
83pub fn publisher_name(source: &CatalogSource) -> Option<&'static str> {
84 Some(match source.prefix()? {
85 "ebible" => "eBible.org",
86 "fetch" => "fetch.bible",
87 "ccel" => "CCEL",
88 "sword" => "SWORD",
89 "door43" => "Door43",
90 _ => return None,
91 })
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn lists_are_told_apart_by_location() {
100 let toml = "[[package]]\nid = \"kjv\"\nname = \"KJV\"\nabbreviation = \"KJV\"\nlanguage = \"en\"\ngit = \"https://example.org/kjv\"\n";
101 let catalog = parse("https://example.org/catalog.toml", toml.as_bytes()).unwrap();
102 assert_eq!(catalog.entries[0].id, "kjv");
103 assert_eq!(publisher_name(&catalog.entries[0].source), None);
104 assert!(!is_publishers("https://example.org/catalog.toml"));
105 for location in logoxide::catalog::DEFAULT_CATALOGS {
106 assert!(is_publishers(location), "{location}");
107 }
108 // A query doesn't hide the form
109 assert!(is_publishers(
110 "https://ebible.org/Scriptures/translations.csv?x=1"
111 ));
112 assert!(parse("translations.csv", b"").is_err());
113 assert!(parse("https://ccel.org/ccel/bible/", b"<html></html>").is_err());
114 assert!(parse("mods.d.tar.gz", b"not gzip").is_err());
115 assert!(is_publishers("https://ccel.org/index/genre/gen-COM"));
116 assert!(parse("https://ccel.org/index/genre/gen-COM", b"<html></html>").is_err());
117 }
118}