Skip to main content

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}