diff --git a/.github/workflows/archive.yml b/.github/workflows/archive.yml index f003f2a0..4ca26517 100644 --- a/.github/workflows/archive.yml +++ b/.github/workflows/archive.yml @@ -25,4 +25,4 @@ jobs: cd .. git clone --depth 1 https://github.com/citation-style-language/styles - name: Check if archives are up-to-date - run: cargo test --test archiver -- --ignored + run: cargo test --test test -- archiver --ignored diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acf18642..70b258e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: dtolnay/rust-toolchain@3056ad9b1fc916cb9a55f7f72ae4a848b7d50249 # 1.91.0 - run: cargo build - - run: cargo test --features csl-json + - run: cargo test --all-features checks: name: Check clippy, formatting, and documentation diff --git a/Cargo.lock b/Cargo.lock index 38e50597..ad10ccf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,9 +174,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -216,24 +216,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] -name = "hayagriva" +name = "hayagriva-archive" version = "0.10.1" dependencies = [ - "biblatex", "ciborium", "citationberg", + "serde", +] + +[[package]] +name = "hayagriva-cli" +version = "0.10.1" +dependencies = [ + "biblatex", + "citationberg", "clap", - "heck", + "hayagriva-archive", + "hayagriva-core", + "hayagriva-csl", + "hayagriva-format", + "strum 0.26.3", +] + +[[package]] +name = "hayagriva-core" +version = "0.10.1" +dependencies = [ + "citationberg", + "roman-numerals-rs", + "serde", + "thiserror", + "unicode-segmentation", + "unscanny", + "url", +] + +[[package]] +name = "hayagriva-csl" +version = "0.10.1" +dependencies = [ + "citationberg", + "hayagriva-core", + "hayagriva-format", "icu_collator", "icu_locale", "indexmap", + "unic-langid", +] + +[[package]] +name = "hayagriva-csl-json" +version = "0.10.1" +dependencies = [ + "citationberg", + "hayagriva-core", + "serde", +] + +[[package]] +name = "hayagriva-format" +version = "0.10.1" +dependencies = [ + "biblatex", + "citationberg", + "hayagriva-core", + "indexmap", "paste", - "regex", - "roman-numerals-rs", "serde", "serde_json", "serde_yaml", - "strum 0.26.3", "thiserror", "unic-langid", "unicode-segmentation", @@ -434,9 +485,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "once_cell_polyfill" @@ -503,9 +554,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -526,9 +577,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "roman-numerals-rs" @@ -698,6 +749,24 @@ dependencies = [ "syn", ] +[[package]] +name = "tests" +version = "0.10.1" +dependencies = [ + "ciborium", + "citationberg", + "hayagriva-archive", + "hayagriva-core", + "hayagriva-csl", + "hayagriva-csl-json", + "hayagriva-format", + "heck", + "regex", + "serde", + "serde_json", + "unscanny", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -805,9 +874,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unsafe-libyaml" @@ -881,9 +950,9 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -904,18 +973,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b05bd113..fb90e2eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,50 +1,33 @@ -[package] -name = "hayagriva" +[workspace] +members = ["crates/*", "tests"] +resolver = "2" + +[workspace.package] version = "0.10.1" -authors = ["Martin Haug "] +authors = [ + "Martin Haug ", + "Daniel Drodt ", +] edition = "2024" license = "MIT OR Apache-2.0" -description = "Work with references: Literature database management, storage, and citation formatting" repository = "https://github.com/typst/hayagriva" readme = "README.md" -categories = ["template-engine", "value-formatting", "command-line-utilities"] -keywords = ["bibliography", "citation", "reference", "bibtex", "literature"] - -[features] -default = ["biblatex", "archive"] -cli = ["clap", "strum"] -archive = ["ciborium"] -csl-json = ["citationberg/json"] +keywords = ["hayagriva", "bibliography", "citation", "reference", "literature"] -[dependencies] +[workspace.dependencies] +biblatex = { version = "0.12.0", features = ["unic-langid"] } +ciborium = { version = "0.2.1" } citationberg = "0.7.0" indexmap = { version = "2.0.2", features = ["serde"] } -roman-numerals-rs = "3.1.0" -paste = "1.0.14" +hayagriva-archive = { path = "crates/hayagriva-archive" } +hayagriva-cli = { path = "crates/hayagriva-cli" } +hayagriva-core = { path = "crates/hayagriva-core" } +hayagriva-csl = { path = "crates/hayagriva-csl" } +hayagriva-csl-json = { path = "crates/hayagriva-csl-json" } +hayagriva-format = { path = "crates/hayagriva-format" } serde = { version = "1", features = ["derive"] } -serde_yaml = "0.9.25" -thiserror = "2.0.16" unic-langid = { version = "0.9.6", features = ["serde"] } unicode-segmentation = "1.6.0" +thiserror = "2.0.16" unscanny = "0.1.0" url = { version = "2.4", features = ["serde"] } -biblatex = { version = "0.12.0", features = ["unic-langid"], optional = true } -ciborium = { version = "0.2.1", optional = true } -clap = { version = "4", optional = true, features = ["cargo", "derive"] } -strum = { version = "0.26", features = ["derive"], optional = true } -icu_collator = "2.2.0" -icu_locale = "2.2.0" - -[dev-dependencies] -heck = "0.5" -serde_json = "1" -regex = "1" - -[[bin]] -name = "hayagriva" -required-features = ["cli"] - -[[test]] -name = "citeproc" -path = "tests/citeproc.rs" -required-features = ["csl-json"] diff --git a/crates/hayagriva-archive/Cargo.toml b/crates/hayagriva-archive/Cargo.toml new file mode 100644 index 00000000..39ac6c87 --- /dev/null +++ b/crates/hayagriva-archive/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "hayagriva-archive" +description = "Archived CSL styles and locales for Hayagriva/Typst." +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +keywords = [ + "hayagriva", + "bibliography", + "citation", + "reference", + "literature", + "archive", + "citation style language", + "locales", +] + +[dependencies] +ciborium = { workspace = true } +citationberg = { workspace = true } +serde = { workspace = true } diff --git a/archive/locales/af-ZA.cbor b/crates/hayagriva-archive/archive/locales/af-ZA.cbor similarity index 100% rename from archive/locales/af-ZA.cbor rename to crates/hayagriva-archive/archive/locales/af-ZA.cbor diff --git a/archive/locales/ar.cbor b/crates/hayagriva-archive/archive/locales/ar.cbor similarity index 100% rename from archive/locales/ar.cbor rename to crates/hayagriva-archive/archive/locales/ar.cbor diff --git a/archive/locales/bal-PK.cbor b/crates/hayagriva-archive/archive/locales/bal-PK.cbor similarity index 100% rename from archive/locales/bal-PK.cbor rename to crates/hayagriva-archive/archive/locales/bal-PK.cbor diff --git a/archive/locales/bg-BG.cbor b/crates/hayagriva-archive/archive/locales/bg-BG.cbor similarity index 100% rename from archive/locales/bg-BG.cbor rename to crates/hayagriva-archive/archive/locales/bg-BG.cbor diff --git a/archive/locales/brh-PK.cbor b/crates/hayagriva-archive/archive/locales/brh-PK.cbor similarity index 100% rename from archive/locales/brh-PK.cbor rename to crates/hayagriva-archive/archive/locales/brh-PK.cbor diff --git a/archive/locales/ca-AD.cbor b/crates/hayagriva-archive/archive/locales/ca-AD.cbor similarity index 100% rename from archive/locales/ca-AD.cbor rename to crates/hayagriva-archive/archive/locales/ca-AD.cbor diff --git a/archive/locales/cs-CZ.cbor b/crates/hayagriva-archive/archive/locales/cs-CZ.cbor similarity index 100% rename from archive/locales/cs-CZ.cbor rename to crates/hayagriva-archive/archive/locales/cs-CZ.cbor diff --git a/archive/locales/cy-GB.cbor b/crates/hayagriva-archive/archive/locales/cy-GB.cbor similarity index 100% rename from archive/locales/cy-GB.cbor rename to crates/hayagriva-archive/archive/locales/cy-GB.cbor diff --git a/archive/locales/da-DK.cbor b/crates/hayagriva-archive/archive/locales/da-DK.cbor similarity index 100% rename from archive/locales/da-DK.cbor rename to crates/hayagriva-archive/archive/locales/da-DK.cbor diff --git a/archive/locales/de-AT.cbor b/crates/hayagriva-archive/archive/locales/de-AT.cbor similarity index 100% rename from archive/locales/de-AT.cbor rename to crates/hayagriva-archive/archive/locales/de-AT.cbor diff --git a/archive/locales/de-CH.cbor b/crates/hayagriva-archive/archive/locales/de-CH.cbor similarity index 100% rename from archive/locales/de-CH.cbor rename to crates/hayagriva-archive/archive/locales/de-CH.cbor diff --git a/archive/locales/de-DE.cbor b/crates/hayagriva-archive/archive/locales/de-DE.cbor similarity index 100% rename from archive/locales/de-DE.cbor rename to crates/hayagriva-archive/archive/locales/de-DE.cbor diff --git a/archive/locales/el-GR.cbor b/crates/hayagriva-archive/archive/locales/el-GR.cbor similarity index 100% rename from archive/locales/el-GR.cbor rename to crates/hayagriva-archive/archive/locales/el-GR.cbor diff --git a/archive/locales/en-GB.cbor b/crates/hayagriva-archive/archive/locales/en-GB.cbor similarity index 100% rename from archive/locales/en-GB.cbor rename to crates/hayagriva-archive/archive/locales/en-GB.cbor diff --git a/archive/locales/en-US.cbor b/crates/hayagriva-archive/archive/locales/en-US.cbor similarity index 100% rename from archive/locales/en-US.cbor rename to crates/hayagriva-archive/archive/locales/en-US.cbor diff --git a/archive/locales/es-CL.cbor b/crates/hayagriva-archive/archive/locales/es-CL.cbor similarity index 100% rename from archive/locales/es-CL.cbor rename to crates/hayagriva-archive/archive/locales/es-CL.cbor diff --git a/archive/locales/es-ES.cbor b/crates/hayagriva-archive/archive/locales/es-ES.cbor similarity index 100% rename from archive/locales/es-ES.cbor rename to crates/hayagriva-archive/archive/locales/es-ES.cbor diff --git a/archive/locales/es-MX.cbor b/crates/hayagriva-archive/archive/locales/es-MX.cbor similarity index 100% rename from archive/locales/es-MX.cbor rename to crates/hayagriva-archive/archive/locales/es-MX.cbor diff --git a/archive/locales/et-EE.cbor b/crates/hayagriva-archive/archive/locales/et-EE.cbor similarity index 100% rename from archive/locales/et-EE.cbor rename to crates/hayagriva-archive/archive/locales/et-EE.cbor diff --git a/archive/locales/eu.cbor b/crates/hayagriva-archive/archive/locales/eu.cbor similarity index 100% rename from archive/locales/eu.cbor rename to crates/hayagriva-archive/archive/locales/eu.cbor diff --git a/archive/locales/fa-IR.cbor b/crates/hayagriva-archive/archive/locales/fa-IR.cbor similarity index 100% rename from archive/locales/fa-IR.cbor rename to crates/hayagriva-archive/archive/locales/fa-IR.cbor diff --git a/archive/locales/fi-FI.cbor b/crates/hayagriva-archive/archive/locales/fi-FI.cbor similarity index 100% rename from archive/locales/fi-FI.cbor rename to crates/hayagriva-archive/archive/locales/fi-FI.cbor diff --git a/archive/locales/fr-CA.cbor b/crates/hayagriva-archive/archive/locales/fr-CA.cbor similarity index 100% rename from archive/locales/fr-CA.cbor rename to crates/hayagriva-archive/archive/locales/fr-CA.cbor diff --git a/archive/locales/fr-FR.cbor b/crates/hayagriva-archive/archive/locales/fr-FR.cbor similarity index 100% rename from archive/locales/fr-FR.cbor rename to crates/hayagriva-archive/archive/locales/fr-FR.cbor diff --git a/archive/locales/gl-ES.cbor b/crates/hayagriva-archive/archive/locales/gl-ES.cbor similarity index 100% rename from archive/locales/gl-ES.cbor rename to crates/hayagriva-archive/archive/locales/gl-ES.cbor diff --git a/archive/locales/he-IL.cbor b/crates/hayagriva-archive/archive/locales/he-IL.cbor similarity index 100% rename from archive/locales/he-IL.cbor rename to crates/hayagriva-archive/archive/locales/he-IL.cbor diff --git a/archive/locales/hi-IN.cbor b/crates/hayagriva-archive/archive/locales/hi-IN.cbor similarity index 100% rename from archive/locales/hi-IN.cbor rename to crates/hayagriva-archive/archive/locales/hi-IN.cbor diff --git a/archive/locales/hr-HR.cbor b/crates/hayagriva-archive/archive/locales/hr-HR.cbor similarity index 100% rename from archive/locales/hr-HR.cbor rename to crates/hayagriva-archive/archive/locales/hr-HR.cbor diff --git a/archive/locales/hu-HU.cbor b/crates/hayagriva-archive/archive/locales/hu-HU.cbor similarity index 100% rename from archive/locales/hu-HU.cbor rename to crates/hayagriva-archive/archive/locales/hu-HU.cbor diff --git a/archive/locales/hy-AM.cbor b/crates/hayagriva-archive/archive/locales/hy-AM.cbor similarity index 100% rename from archive/locales/hy-AM.cbor rename to crates/hayagriva-archive/archive/locales/hy-AM.cbor diff --git a/archive/locales/id-ID.cbor b/crates/hayagriva-archive/archive/locales/id-ID.cbor similarity index 100% rename from archive/locales/id-ID.cbor rename to crates/hayagriva-archive/archive/locales/id-ID.cbor diff --git a/archive/locales/is-IS.cbor b/crates/hayagriva-archive/archive/locales/is-IS.cbor similarity index 100% rename from archive/locales/is-IS.cbor rename to crates/hayagriva-archive/archive/locales/is-IS.cbor diff --git a/archive/locales/it-IT.cbor b/crates/hayagriva-archive/archive/locales/it-IT.cbor similarity index 100% rename from archive/locales/it-IT.cbor rename to crates/hayagriva-archive/archive/locales/it-IT.cbor diff --git a/archive/locales/ja-JP.cbor b/crates/hayagriva-archive/archive/locales/ja-JP.cbor similarity index 100% rename from archive/locales/ja-JP.cbor rename to crates/hayagriva-archive/archive/locales/ja-JP.cbor diff --git a/archive/locales/km-KH.cbor b/crates/hayagriva-archive/archive/locales/km-KH.cbor similarity index 100% rename from archive/locales/km-KH.cbor rename to crates/hayagriva-archive/archive/locales/km-KH.cbor diff --git a/archive/locales/ko-KR.cbor b/crates/hayagriva-archive/archive/locales/ko-KR.cbor similarity index 100% rename from archive/locales/ko-KR.cbor rename to crates/hayagriva-archive/archive/locales/ko-KR.cbor diff --git a/archive/locales/la.cbor b/crates/hayagriva-archive/archive/locales/la.cbor similarity index 100% rename from archive/locales/la.cbor rename to crates/hayagriva-archive/archive/locales/la.cbor diff --git a/archive/locales/lij-IT.cbor b/crates/hayagriva-archive/archive/locales/lij-IT.cbor similarity index 100% rename from archive/locales/lij-IT.cbor rename to crates/hayagriva-archive/archive/locales/lij-IT.cbor diff --git a/archive/locales/lt-LT.cbor b/crates/hayagriva-archive/archive/locales/lt-LT.cbor similarity index 100% rename from archive/locales/lt-LT.cbor rename to crates/hayagriva-archive/archive/locales/lt-LT.cbor diff --git a/archive/locales/lv-LV.cbor b/crates/hayagriva-archive/archive/locales/lv-LV.cbor similarity index 100% rename from archive/locales/lv-LV.cbor rename to crates/hayagriva-archive/archive/locales/lv-LV.cbor diff --git a/archive/locales/mn-MN.cbor b/crates/hayagriva-archive/archive/locales/mn-MN.cbor similarity index 100% rename from archive/locales/mn-MN.cbor rename to crates/hayagriva-archive/archive/locales/mn-MN.cbor diff --git a/archive/locales/ms-MY.cbor b/crates/hayagriva-archive/archive/locales/ms-MY.cbor similarity index 100% rename from archive/locales/ms-MY.cbor rename to crates/hayagriva-archive/archive/locales/ms-MY.cbor diff --git a/archive/locales/nb-NO.cbor b/crates/hayagriva-archive/archive/locales/nb-NO.cbor similarity index 100% rename from archive/locales/nb-NO.cbor rename to crates/hayagriva-archive/archive/locales/nb-NO.cbor diff --git a/archive/locales/nl-NL.cbor b/crates/hayagriva-archive/archive/locales/nl-NL.cbor similarity index 100% rename from archive/locales/nl-NL.cbor rename to crates/hayagriva-archive/archive/locales/nl-NL.cbor diff --git a/archive/locales/nn-NO.cbor b/crates/hayagriva-archive/archive/locales/nn-NO.cbor similarity index 100% rename from archive/locales/nn-NO.cbor rename to crates/hayagriva-archive/archive/locales/nn-NO.cbor diff --git a/archive/locales/pa-PK.cbor b/crates/hayagriva-archive/archive/locales/pa-PK.cbor similarity index 100% rename from archive/locales/pa-PK.cbor rename to crates/hayagriva-archive/archive/locales/pa-PK.cbor diff --git a/archive/locales/pl-PL.cbor b/crates/hayagriva-archive/archive/locales/pl-PL.cbor similarity index 100% rename from archive/locales/pl-PL.cbor rename to crates/hayagriva-archive/archive/locales/pl-PL.cbor diff --git a/archive/locales/pt-BR.cbor b/crates/hayagriva-archive/archive/locales/pt-BR.cbor similarity index 100% rename from archive/locales/pt-BR.cbor rename to crates/hayagriva-archive/archive/locales/pt-BR.cbor diff --git a/archive/locales/pt-PT.cbor b/crates/hayagriva-archive/archive/locales/pt-PT.cbor similarity index 100% rename from archive/locales/pt-PT.cbor rename to crates/hayagriva-archive/archive/locales/pt-PT.cbor diff --git a/archive/locales/ro-RO.cbor b/crates/hayagriva-archive/archive/locales/ro-RO.cbor similarity index 100% rename from archive/locales/ro-RO.cbor rename to crates/hayagriva-archive/archive/locales/ro-RO.cbor diff --git a/archive/locales/ru-RU.cbor b/crates/hayagriva-archive/archive/locales/ru-RU.cbor similarity index 100% rename from archive/locales/ru-RU.cbor rename to crates/hayagriva-archive/archive/locales/ru-RU.cbor diff --git a/archive/locales/sk-SK.cbor b/crates/hayagriva-archive/archive/locales/sk-SK.cbor similarity index 100% rename from archive/locales/sk-SK.cbor rename to crates/hayagriva-archive/archive/locales/sk-SK.cbor diff --git a/archive/locales/sl-SI.cbor b/crates/hayagriva-archive/archive/locales/sl-SI.cbor similarity index 100% rename from archive/locales/sl-SI.cbor rename to crates/hayagriva-archive/archive/locales/sl-SI.cbor diff --git a/archive/locales/sr-Cyrl-RS.cbor b/crates/hayagriva-archive/archive/locales/sr-Cyrl-RS.cbor similarity index 100% rename from archive/locales/sr-Cyrl-RS.cbor rename to crates/hayagriva-archive/archive/locales/sr-Cyrl-RS.cbor diff --git a/archive/locales/sr-Latn-RS.cbor b/crates/hayagriva-archive/archive/locales/sr-Latn-RS.cbor similarity index 100% rename from archive/locales/sr-Latn-RS.cbor rename to crates/hayagriva-archive/archive/locales/sr-Latn-RS.cbor diff --git a/archive/locales/sr-RS.cbor b/crates/hayagriva-archive/archive/locales/sr-RS.cbor similarity index 100% rename from archive/locales/sr-RS.cbor rename to crates/hayagriva-archive/archive/locales/sr-RS.cbor diff --git a/archive/locales/sv-SE.cbor b/crates/hayagriva-archive/archive/locales/sv-SE.cbor similarity index 100% rename from archive/locales/sv-SE.cbor rename to crates/hayagriva-archive/archive/locales/sv-SE.cbor diff --git a/archive/locales/th-TH.cbor b/crates/hayagriva-archive/archive/locales/th-TH.cbor similarity index 100% rename from archive/locales/th-TH.cbor rename to crates/hayagriva-archive/archive/locales/th-TH.cbor diff --git a/archive/locales/tl-PH.cbor b/crates/hayagriva-archive/archive/locales/tl-PH.cbor similarity index 100% rename from archive/locales/tl-PH.cbor rename to crates/hayagriva-archive/archive/locales/tl-PH.cbor diff --git a/archive/locales/tr-TR.cbor b/crates/hayagriva-archive/archive/locales/tr-TR.cbor similarity index 100% rename from archive/locales/tr-TR.cbor rename to crates/hayagriva-archive/archive/locales/tr-TR.cbor diff --git a/archive/locales/uk-UA.cbor b/crates/hayagriva-archive/archive/locales/uk-UA.cbor similarity index 100% rename from archive/locales/uk-UA.cbor rename to crates/hayagriva-archive/archive/locales/uk-UA.cbor diff --git a/archive/locales/vi-VN.cbor b/crates/hayagriva-archive/archive/locales/vi-VN.cbor similarity index 100% rename from archive/locales/vi-VN.cbor rename to crates/hayagriva-archive/archive/locales/vi-VN.cbor diff --git a/archive/locales/zh-CN.cbor b/crates/hayagriva-archive/archive/locales/zh-CN.cbor similarity index 100% rename from archive/locales/zh-CN.cbor rename to crates/hayagriva-archive/archive/locales/zh-CN.cbor diff --git a/archive/locales/zh-TW.cbor b/crates/hayagriva-archive/archive/locales/zh-TW.cbor similarity index 100% rename from archive/locales/zh-TW.cbor rename to crates/hayagriva-archive/archive/locales/zh-TW.cbor diff --git a/archive/styles/alphanumeric.cbor b/crates/hayagriva-archive/archive/styles/alphanumeric.cbor similarity index 100% rename from archive/styles/alphanumeric.cbor rename to crates/hayagriva-archive/archive/styles/alphanumeric.cbor diff --git a/archive/styles/american-anthropological-association.cbor b/crates/hayagriva-archive/archive/styles/american-anthropological-association.cbor similarity index 100% rename from archive/styles/american-anthropological-association.cbor rename to crates/hayagriva-archive/archive/styles/american-anthropological-association.cbor diff --git a/archive/styles/american-chemical-society.cbor b/crates/hayagriva-archive/archive/styles/american-chemical-society.cbor similarity index 100% rename from archive/styles/american-chemical-society.cbor rename to crates/hayagriva-archive/archive/styles/american-chemical-society.cbor diff --git a/archive/styles/american-geophysical-union.cbor b/crates/hayagriva-archive/archive/styles/american-geophysical-union.cbor similarity index 100% rename from archive/styles/american-geophysical-union.cbor rename to crates/hayagriva-archive/archive/styles/american-geophysical-union.cbor diff --git a/archive/styles/american-institute-of-aeronautics-and-astronautics.cbor b/crates/hayagriva-archive/archive/styles/american-institute-of-aeronautics-and-astronautics.cbor similarity index 100% rename from archive/styles/american-institute-of-aeronautics-and-astronautics.cbor rename to crates/hayagriva-archive/archive/styles/american-institute-of-aeronautics-and-astronautics.cbor diff --git a/archive/styles/american-institute-of-physics.cbor b/crates/hayagriva-archive/archive/styles/american-institute-of-physics.cbor similarity index 100% rename from archive/styles/american-institute-of-physics.cbor rename to crates/hayagriva-archive/archive/styles/american-institute-of-physics.cbor diff --git a/archive/styles/american-mathematical-society-label.cbor b/crates/hayagriva-archive/archive/styles/american-mathematical-society-label.cbor similarity index 100% rename from archive/styles/american-mathematical-society-label.cbor rename to crates/hayagriva-archive/archive/styles/american-mathematical-society-label.cbor diff --git a/archive/styles/american-mathematical-society-numeric.cbor b/crates/hayagriva-archive/archive/styles/american-mathematical-society-numeric.cbor similarity index 100% rename from archive/styles/american-mathematical-society-numeric.cbor rename to crates/hayagriva-archive/archive/styles/american-mathematical-society-numeric.cbor diff --git a/archive/styles/american-medical-association.cbor b/crates/hayagriva-archive/archive/styles/american-medical-association.cbor similarity index 100% rename from archive/styles/american-medical-association.cbor rename to crates/hayagriva-archive/archive/styles/american-medical-association.cbor diff --git a/archive/styles/american-meteorological-society.cbor b/crates/hayagriva-archive/archive/styles/american-meteorological-society.cbor similarity index 100% rename from archive/styles/american-meteorological-society.cbor rename to crates/hayagriva-archive/archive/styles/american-meteorological-society.cbor diff --git a/archive/styles/american-physics-society.cbor b/crates/hayagriva-archive/archive/styles/american-physics-society.cbor similarity index 100% rename from archive/styles/american-physics-society.cbor rename to crates/hayagriva-archive/archive/styles/american-physics-society.cbor diff --git a/archive/styles/american-physiological-society.cbor b/crates/hayagriva-archive/archive/styles/american-physiological-society.cbor similarity index 100% rename from archive/styles/american-physiological-society.cbor rename to crates/hayagriva-archive/archive/styles/american-physiological-society.cbor diff --git a/archive/styles/american-political-science-association.cbor b/crates/hayagriva-archive/archive/styles/american-political-science-association.cbor similarity index 100% rename from archive/styles/american-political-science-association.cbor rename to crates/hayagriva-archive/archive/styles/american-political-science-association.cbor diff --git a/archive/styles/american-society-for-microbiology.cbor b/crates/hayagriva-archive/archive/styles/american-society-for-microbiology.cbor similarity index 100% rename from archive/styles/american-society-for-microbiology.cbor rename to crates/hayagriva-archive/archive/styles/american-society-for-microbiology.cbor diff --git a/archive/styles/american-society-of-civil-engineers.cbor b/crates/hayagriva-archive/archive/styles/american-society-of-civil-engineers.cbor similarity index 100% rename from archive/styles/american-society-of-civil-engineers.cbor rename to crates/hayagriva-archive/archive/styles/american-society-of-civil-engineers.cbor diff --git a/archive/styles/american-society-of-mechanical-engineers.cbor b/crates/hayagriva-archive/archive/styles/american-society-of-mechanical-engineers.cbor similarity index 100% rename from archive/styles/american-society-of-mechanical-engineers.cbor rename to crates/hayagriva-archive/archive/styles/american-society-of-mechanical-engineers.cbor diff --git a/archive/styles/american-sociological-association.cbor b/crates/hayagriva-archive/archive/styles/american-sociological-association.cbor similarity index 100% rename from archive/styles/american-sociological-association.cbor rename to crates/hayagriva-archive/archive/styles/american-sociological-association.cbor diff --git a/archive/styles/angewandte-chemie.cbor b/crates/hayagriva-archive/archive/styles/angewandte-chemie.cbor similarity index 100% rename from archive/styles/angewandte-chemie.cbor rename to crates/hayagriva-archive/archive/styles/angewandte-chemie.cbor diff --git a/archive/styles/annual-reviews-author-date.cbor b/crates/hayagriva-archive/archive/styles/annual-reviews-author-date.cbor similarity index 100% rename from archive/styles/annual-reviews-author-date.cbor rename to crates/hayagriva-archive/archive/styles/annual-reviews-author-date.cbor diff --git a/archive/styles/annual-reviews.cbor b/crates/hayagriva-archive/archive/styles/annual-reviews.cbor similarity index 100% rename from archive/styles/annual-reviews.cbor rename to crates/hayagriva-archive/archive/styles/annual-reviews.cbor diff --git a/archive/styles/apa.cbor b/crates/hayagriva-archive/archive/styles/apa.cbor similarity index 100% rename from archive/styles/apa.cbor rename to crates/hayagriva-archive/archive/styles/apa.cbor diff --git a/archive/styles/associacao-brasileira-de-normas-tecnicas.cbor b/crates/hayagriva-archive/archive/styles/associacao-brasileira-de-normas-tecnicas.cbor similarity index 100% rename from archive/styles/associacao-brasileira-de-normas-tecnicas.cbor rename to crates/hayagriva-archive/archive/styles/associacao-brasileira-de-normas-tecnicas.cbor diff --git a/archive/styles/association-for-computing-machinery.cbor b/crates/hayagriva-archive/archive/styles/association-for-computing-machinery.cbor similarity index 100% rename from archive/styles/association-for-computing-machinery.cbor rename to crates/hayagriva-archive/archive/styles/association-for-computing-machinery.cbor diff --git a/archive/styles/biomed-central.cbor b/crates/hayagriva-archive/archive/styles/biomed-central.cbor similarity index 100% rename from archive/styles/biomed-central.cbor rename to crates/hayagriva-archive/archive/styles/biomed-central.cbor diff --git a/archive/styles/bmj.cbor b/crates/hayagriva-archive/archive/styles/bmj.cbor similarity index 100% rename from archive/styles/bmj.cbor rename to crates/hayagriva-archive/archive/styles/bmj.cbor diff --git a/archive/styles/bristol-university-press.cbor b/crates/hayagriva-archive/archive/styles/bristol-university-press.cbor similarity index 100% rename from archive/styles/bristol-university-press.cbor rename to crates/hayagriva-archive/archive/styles/bristol-university-press.cbor diff --git a/archive/styles/cell.cbor b/crates/hayagriva-archive/archive/styles/cell.cbor similarity index 100% rename from archive/styles/cell.cbor rename to crates/hayagriva-archive/archive/styles/cell.cbor diff --git a/archive/styles/chicago-author-date-17th-edition.cbor b/crates/hayagriva-archive/archive/styles/chicago-author-date-17th-edition.cbor similarity index 100% rename from archive/styles/chicago-author-date-17th-edition.cbor rename to crates/hayagriva-archive/archive/styles/chicago-author-date-17th-edition.cbor diff --git a/archive/styles/chicago-author-date.cbor b/crates/hayagriva-archive/archive/styles/chicago-author-date.cbor similarity index 100% rename from archive/styles/chicago-author-date.cbor rename to crates/hayagriva-archive/archive/styles/chicago-author-date.cbor diff --git a/archive/styles/chicago-fullnote-bibliography.cbor b/crates/hayagriva-archive/archive/styles/chicago-fullnote-bibliography.cbor similarity index 100% rename from archive/styles/chicago-fullnote-bibliography.cbor rename to crates/hayagriva-archive/archive/styles/chicago-fullnote-bibliography.cbor diff --git a/archive/styles/chicago-note-bibliography.cbor b/crates/hayagriva-archive/archive/styles/chicago-note-bibliography.cbor similarity index 100% rename from archive/styles/chicago-note-bibliography.cbor rename to crates/hayagriva-archive/archive/styles/chicago-note-bibliography.cbor diff --git a/archive/styles/chicago-notes-bibliography-subsequent-author-title-17th-edition.cbor b/crates/hayagriva-archive/archive/styles/chicago-notes-bibliography-subsequent-author-title-17th-edition.cbor similarity index 100% rename from archive/styles/chicago-notes-bibliography-subsequent-author-title-17th-edition.cbor rename to crates/hayagriva-archive/archive/styles/chicago-notes-bibliography-subsequent-author-title-17th-edition.cbor diff --git a/archive/styles/chicago-notes-bibliography.cbor b/crates/hayagriva-archive/archive/styles/chicago-notes-bibliography.cbor similarity index 100% rename from archive/styles/chicago-notes-bibliography.cbor rename to crates/hayagriva-archive/archive/styles/chicago-notes-bibliography.cbor diff --git a/archive/styles/chicago-shortened-notes-bibliography.cbor b/crates/hayagriva-archive/archive/styles/chicago-shortened-notes-bibliography.cbor similarity index 100% rename from archive/styles/chicago-shortened-notes-bibliography.cbor rename to crates/hayagriva-archive/archive/styles/chicago-shortened-notes-bibliography.cbor diff --git a/archive/styles/china-national-standard-gb-t-7714-2015-author-date.cbor b/crates/hayagriva-archive/archive/styles/china-national-standard-gb-t-7714-2015-author-date.cbor similarity index 100% rename from archive/styles/china-national-standard-gb-t-7714-2015-author-date.cbor rename to crates/hayagriva-archive/archive/styles/china-national-standard-gb-t-7714-2015-author-date.cbor diff --git a/archive/styles/china-national-standard-gb-t-7714-2015-note.cbor b/crates/hayagriva-archive/archive/styles/china-national-standard-gb-t-7714-2015-note.cbor similarity index 100% rename from archive/styles/china-national-standard-gb-t-7714-2015-note.cbor rename to crates/hayagriva-archive/archive/styles/china-national-standard-gb-t-7714-2015-note.cbor diff --git a/archive/styles/china-national-standard-gb-t-7714-2015-numeric.cbor b/crates/hayagriva-archive/archive/styles/china-national-standard-gb-t-7714-2015-numeric.cbor similarity index 100% rename from archive/styles/china-national-standard-gb-t-7714-2015-numeric.cbor rename to crates/hayagriva-archive/archive/styles/china-national-standard-gb-t-7714-2015-numeric.cbor diff --git a/archive/styles/chinese-gb7714-2005-numeric.cbor b/crates/hayagriva-archive/archive/styles/chinese-gb7714-2005-numeric.cbor similarity index 100% rename from archive/styles/chinese-gb7714-2005-numeric.cbor rename to crates/hayagriva-archive/archive/styles/chinese-gb7714-2005-numeric.cbor diff --git a/archive/styles/copernicus-publications.cbor b/crates/hayagriva-archive/archive/styles/copernicus-publications.cbor similarity index 100% rename from archive/styles/copernicus-publications.cbor rename to crates/hayagriva-archive/archive/styles/copernicus-publications.cbor diff --git a/archive/styles/council-of-science-editors-author-date.cbor b/crates/hayagriva-archive/archive/styles/council-of-science-editors-author-date.cbor similarity index 100% rename from archive/styles/council-of-science-editors-author-date.cbor rename to crates/hayagriva-archive/archive/styles/council-of-science-editors-author-date.cbor diff --git a/archive/styles/council-of-science-editors-brackets.cbor b/crates/hayagriva-archive/archive/styles/council-of-science-editors-brackets.cbor similarity index 100% rename from archive/styles/council-of-science-editors-brackets.cbor rename to crates/hayagriva-archive/archive/styles/council-of-science-editors-brackets.cbor diff --git a/archive/styles/cse-citation-sequence-brackets-8th-edition.cbor b/crates/hayagriva-archive/archive/styles/cse-citation-sequence-brackets-8th-edition.cbor similarity index 100% rename from archive/styles/cse-citation-sequence-brackets-8th-edition.cbor rename to crates/hayagriva-archive/archive/styles/cse-citation-sequence-brackets-8th-edition.cbor diff --git a/archive/styles/cse-name-year.cbor b/crates/hayagriva-archive/archive/styles/cse-name-year.cbor similarity index 100% rename from archive/styles/cse-name-year.cbor rename to crates/hayagriva-archive/archive/styles/cse-name-year.cbor diff --git a/archive/styles/current-opinion.cbor b/crates/hayagriva-archive/archive/styles/current-opinion.cbor similarity index 100% rename from archive/styles/current-opinion.cbor rename to crates/hayagriva-archive/archive/styles/current-opinion.cbor diff --git a/archive/styles/deutsche-gesellschaft-fur-psychologie.cbor b/crates/hayagriva-archive/archive/styles/deutsche-gesellschaft-fur-psychologie.cbor similarity index 100% rename from archive/styles/deutsche-gesellschaft-fur-psychologie.cbor rename to crates/hayagriva-archive/archive/styles/deutsche-gesellschaft-fur-psychologie.cbor diff --git a/archive/styles/deutsche-sprache.cbor b/crates/hayagriva-archive/archive/styles/deutsche-sprache.cbor similarity index 100% rename from archive/styles/deutsche-sprache.cbor rename to crates/hayagriva-archive/archive/styles/deutsche-sprache.cbor diff --git a/archive/styles/elsevier-harvard.cbor b/crates/hayagriva-archive/archive/styles/elsevier-harvard.cbor similarity index 100% rename from archive/styles/elsevier-harvard.cbor rename to crates/hayagriva-archive/archive/styles/elsevier-harvard.cbor diff --git a/archive/styles/elsevier-vancouver.cbor b/crates/hayagriva-archive/archive/styles/elsevier-vancouver.cbor similarity index 100% rename from archive/styles/elsevier-vancouver.cbor rename to crates/hayagriva-archive/archive/styles/elsevier-vancouver.cbor diff --git a/archive/styles/elsevier-with-titles.cbor b/crates/hayagriva-archive/archive/styles/elsevier-with-titles.cbor similarity index 100% rename from archive/styles/elsevier-with-titles.cbor rename to crates/hayagriva-archive/archive/styles/elsevier-with-titles.cbor diff --git a/archive/styles/frontiers.cbor b/crates/hayagriva-archive/archive/styles/frontiers.cbor similarity index 100% rename from archive/styles/frontiers.cbor rename to crates/hayagriva-archive/archive/styles/frontiers.cbor diff --git a/archive/styles/future-medicine.cbor b/crates/hayagriva-archive/archive/styles/future-medicine.cbor similarity index 100% rename from archive/styles/future-medicine.cbor rename to crates/hayagriva-archive/archive/styles/future-medicine.cbor diff --git a/archive/styles/future-science-group.cbor b/crates/hayagriva-archive/archive/styles/future-science-group.cbor similarity index 100% rename from archive/styles/future-science-group.cbor rename to crates/hayagriva-archive/archive/styles/future-science-group.cbor diff --git a/archive/styles/gost-r-7-0-5-2008-numeric.cbor b/crates/hayagriva-archive/archive/styles/gost-r-7-0-5-2008-numeric.cbor similarity index 100% rename from archive/styles/gost-r-7-0-5-2008-numeric.cbor rename to crates/hayagriva-archive/archive/styles/gost-r-7-0-5-2008-numeric.cbor diff --git a/archive/styles/harvard-cite-them-right.cbor b/crates/hayagriva-archive/archive/styles/harvard-cite-them-right.cbor similarity index 100% rename from archive/styles/harvard-cite-them-right.cbor rename to crates/hayagriva-archive/archive/styles/harvard-cite-them-right.cbor diff --git a/archive/styles/ieee.cbor b/crates/hayagriva-archive/archive/styles/ieee.cbor similarity index 100% rename from archive/styles/ieee.cbor rename to crates/hayagriva-archive/archive/styles/ieee.cbor diff --git a/archive/styles/institute-of-physics-numeric.cbor b/crates/hayagriva-archive/archive/styles/institute-of-physics-numeric.cbor similarity index 100% rename from archive/styles/institute-of-physics-numeric.cbor rename to crates/hayagriva-archive/archive/styles/institute-of-physics-numeric.cbor diff --git a/archive/styles/iso690-author-date-en.cbor b/crates/hayagriva-archive/archive/styles/iso690-author-date-en.cbor similarity index 100% rename from archive/styles/iso690-author-date-en.cbor rename to crates/hayagriva-archive/archive/styles/iso690-author-date-en.cbor diff --git a/archive/styles/iso690-numeric-en.cbor b/crates/hayagriva-archive/archive/styles/iso690-numeric-en.cbor similarity index 100% rename from archive/styles/iso690-numeric-en.cbor rename to crates/hayagriva-archive/archive/styles/iso690-numeric-en.cbor diff --git a/archive/styles/karger-journals.cbor b/crates/hayagriva-archive/archive/styles/karger-journals.cbor similarity index 100% rename from archive/styles/karger-journals.cbor rename to crates/hayagriva-archive/archive/styles/karger-journals.cbor diff --git a/archive/styles/mary-ann-liebert-vancouver.cbor b/crates/hayagriva-archive/archive/styles/mary-ann-liebert-vancouver.cbor similarity index 100% rename from archive/styles/mary-ann-liebert-vancouver.cbor rename to crates/hayagriva-archive/archive/styles/mary-ann-liebert-vancouver.cbor diff --git a/archive/styles/mhra-notes.cbor b/crates/hayagriva-archive/archive/styles/mhra-notes.cbor similarity index 100% rename from archive/styles/mhra-notes.cbor rename to crates/hayagriva-archive/archive/styles/mhra-notes.cbor diff --git a/archive/styles/modern-humanities-research-association-notes.cbor b/crates/hayagriva-archive/archive/styles/modern-humanities-research-association-notes.cbor similarity index 100% rename from archive/styles/modern-humanities-research-association-notes.cbor rename to crates/hayagriva-archive/archive/styles/modern-humanities-research-association-notes.cbor diff --git a/archive/styles/modern-humanities-research-association.cbor b/crates/hayagriva-archive/archive/styles/modern-humanities-research-association.cbor similarity index 100% rename from archive/styles/modern-humanities-research-association.cbor rename to crates/hayagriva-archive/archive/styles/modern-humanities-research-association.cbor diff --git a/archive/styles/modern-language-association-8th-edition.cbor b/crates/hayagriva-archive/archive/styles/modern-language-association-8th-edition.cbor similarity index 100% rename from archive/styles/modern-language-association-8th-edition.cbor rename to crates/hayagriva-archive/archive/styles/modern-language-association-8th-edition.cbor diff --git a/archive/styles/modern-language-association.cbor b/crates/hayagriva-archive/archive/styles/modern-language-association.cbor similarity index 100% rename from archive/styles/modern-language-association.cbor rename to crates/hayagriva-archive/archive/styles/modern-language-association.cbor diff --git a/archive/styles/multidisciplinary-digital-publishing-institute.cbor b/crates/hayagriva-archive/archive/styles/multidisciplinary-digital-publishing-institute.cbor similarity index 100% rename from archive/styles/multidisciplinary-digital-publishing-institute.cbor rename to crates/hayagriva-archive/archive/styles/multidisciplinary-digital-publishing-institute.cbor diff --git a/archive/styles/nature.cbor b/crates/hayagriva-archive/archive/styles/nature.cbor similarity index 100% rename from archive/styles/nature.cbor rename to crates/hayagriva-archive/archive/styles/nature.cbor diff --git a/archive/styles/nlm-citation-sequence-superscript.cbor b/crates/hayagriva-archive/archive/styles/nlm-citation-sequence-superscript.cbor similarity index 100% rename from archive/styles/nlm-citation-sequence-superscript.cbor rename to crates/hayagriva-archive/archive/styles/nlm-citation-sequence-superscript.cbor diff --git a/archive/styles/nlm-citation-sequence.cbor b/crates/hayagriva-archive/archive/styles/nlm-citation-sequence.cbor similarity index 100% rename from archive/styles/nlm-citation-sequence.cbor rename to crates/hayagriva-archive/archive/styles/nlm-citation-sequence.cbor diff --git a/archive/styles/pensoft-journals.cbor b/crates/hayagriva-archive/archive/styles/pensoft-journals.cbor similarity index 100% rename from archive/styles/pensoft-journals.cbor rename to crates/hayagriva-archive/archive/styles/pensoft-journals.cbor diff --git a/archive/styles/plos.cbor b/crates/hayagriva-archive/archive/styles/plos.cbor similarity index 100% rename from archive/styles/plos.cbor rename to crates/hayagriva-archive/archive/styles/plos.cbor diff --git a/archive/styles/royal-society-of-chemistry.cbor b/crates/hayagriva-archive/archive/styles/royal-society-of-chemistry.cbor similarity index 100% rename from archive/styles/royal-society-of-chemistry.cbor rename to crates/hayagriva-archive/archive/styles/royal-society-of-chemistry.cbor diff --git a/archive/styles/sage-vancouver.cbor b/crates/hayagriva-archive/archive/styles/sage-vancouver.cbor similarity index 100% rename from archive/styles/sage-vancouver.cbor rename to crates/hayagriva-archive/archive/styles/sage-vancouver.cbor diff --git a/archive/styles/sist02.cbor b/crates/hayagriva-archive/archive/styles/sist02.cbor similarity index 100% rename from archive/styles/sist02.cbor rename to crates/hayagriva-archive/archive/styles/sist02.cbor diff --git a/archive/styles/spie-journals.cbor b/crates/hayagriva-archive/archive/styles/spie-journals.cbor similarity index 100% rename from archive/styles/spie-journals.cbor rename to crates/hayagriva-archive/archive/styles/spie-journals.cbor diff --git a/archive/styles/springer-basic-author-date.cbor b/crates/hayagriva-archive/archive/styles/springer-basic-author-date.cbor similarity index 100% rename from archive/styles/springer-basic-author-date.cbor rename to crates/hayagriva-archive/archive/styles/springer-basic-author-date.cbor diff --git a/archive/styles/springer-basic-brackets.cbor b/crates/hayagriva-archive/archive/styles/springer-basic-brackets.cbor similarity index 100% rename from archive/styles/springer-basic-brackets.cbor rename to crates/hayagriva-archive/archive/styles/springer-basic-brackets.cbor diff --git a/archive/styles/springer-fachzeitschriften-medizin-psychologie.cbor b/crates/hayagriva-archive/archive/styles/springer-fachzeitschriften-medizin-psychologie.cbor similarity index 100% rename from archive/styles/springer-fachzeitschriften-medizin-psychologie.cbor rename to crates/hayagriva-archive/archive/styles/springer-fachzeitschriften-medizin-psychologie.cbor diff --git a/archive/styles/springer-humanities-author-date.cbor b/crates/hayagriva-archive/archive/styles/springer-humanities-author-date.cbor similarity index 100% rename from archive/styles/springer-humanities-author-date.cbor rename to crates/hayagriva-archive/archive/styles/springer-humanities-author-date.cbor diff --git a/archive/styles/springer-lecture-notes-in-computer-science.cbor b/crates/hayagriva-archive/archive/styles/springer-lecture-notes-in-computer-science.cbor similarity index 100% rename from archive/styles/springer-lecture-notes-in-computer-science.cbor rename to crates/hayagriva-archive/archive/styles/springer-lecture-notes-in-computer-science.cbor diff --git a/archive/styles/springer-mathphys-brackets.cbor b/crates/hayagriva-archive/archive/styles/springer-mathphys-brackets.cbor similarity index 100% rename from archive/styles/springer-mathphys-brackets.cbor rename to crates/hayagriva-archive/archive/styles/springer-mathphys-brackets.cbor diff --git a/archive/styles/springer-socpsych-author-date.cbor b/crates/hayagriva-archive/archive/styles/springer-socpsych-author-date.cbor similarity index 100% rename from archive/styles/springer-socpsych-author-date.cbor rename to crates/hayagriva-archive/archive/styles/springer-socpsych-author-date.cbor diff --git a/archive/styles/springer-vancouver-brackets.cbor b/crates/hayagriva-archive/archive/styles/springer-vancouver-brackets.cbor similarity index 100% rename from archive/styles/springer-vancouver-brackets.cbor rename to crates/hayagriva-archive/archive/styles/springer-vancouver-brackets.cbor diff --git a/archive/styles/taylor-and-francis-chicago-author-date.cbor b/crates/hayagriva-archive/archive/styles/taylor-and-francis-chicago-author-date.cbor similarity index 100% rename from archive/styles/taylor-and-francis-chicago-author-date.cbor rename to crates/hayagriva-archive/archive/styles/taylor-and-francis-chicago-author-date.cbor diff --git a/archive/styles/taylor-and-francis-national-library-of-medicine.cbor b/crates/hayagriva-archive/archive/styles/taylor-and-francis-national-library-of-medicine.cbor similarity index 100% rename from archive/styles/taylor-and-francis-national-library-of-medicine.cbor rename to crates/hayagriva-archive/archive/styles/taylor-and-francis-national-library-of-medicine.cbor diff --git a/archive/styles/the-institution-of-engineering-and-technology.cbor b/crates/hayagriva-archive/archive/styles/the-institution-of-engineering-and-technology.cbor similarity index 100% rename from archive/styles/the-institution-of-engineering-and-technology.cbor rename to crates/hayagriva-archive/archive/styles/the-institution-of-engineering-and-technology.cbor diff --git a/archive/styles/the-lancet.cbor b/crates/hayagriva-archive/archive/styles/the-lancet.cbor similarity index 100% rename from archive/styles/the-lancet.cbor rename to crates/hayagriva-archive/archive/styles/the-lancet.cbor diff --git a/archive/styles/thieme-german.cbor b/crates/hayagriva-archive/archive/styles/thieme-german.cbor similarity index 100% rename from archive/styles/thieme-german.cbor rename to crates/hayagriva-archive/archive/styles/thieme-german.cbor diff --git a/archive/styles/trends-journals.cbor b/crates/hayagriva-archive/archive/styles/trends-journals.cbor similarity index 100% rename from archive/styles/trends-journals.cbor rename to crates/hayagriva-archive/archive/styles/trends-journals.cbor diff --git a/archive/styles/turabian-author-date.cbor b/crates/hayagriva-archive/archive/styles/turabian-author-date.cbor similarity index 100% rename from archive/styles/turabian-author-date.cbor rename to crates/hayagriva-archive/archive/styles/turabian-author-date.cbor diff --git a/archive/styles/turabian-fullnote-bibliography-8th-edition.cbor b/crates/hayagriva-archive/archive/styles/turabian-fullnote-bibliography-8th-edition.cbor similarity index 100% rename from archive/styles/turabian-fullnote-bibliography-8th-edition.cbor rename to crates/hayagriva-archive/archive/styles/turabian-fullnote-bibliography-8th-edition.cbor diff --git a/archive/styles/vancouver-superscript.cbor b/crates/hayagriva-archive/archive/styles/vancouver-superscript.cbor similarity index 100% rename from archive/styles/vancouver-superscript.cbor rename to crates/hayagriva-archive/archive/styles/vancouver-superscript.cbor diff --git a/archive/styles/vancouver.cbor b/crates/hayagriva-archive/archive/styles/vancouver.cbor similarity index 100% rename from archive/styles/vancouver.cbor rename to crates/hayagriva-archive/archive/styles/vancouver.cbor diff --git a/src/csl/archive.rs b/crates/hayagriva-archive/src/lib.rs similarity index 84% rename from src/csl/archive.rs rename to crates/hayagriva-archive/src/lib.rs index e6c56319..6d5ab445 100644 --- a/src/csl/archive.rs +++ b/crates/hayagriva-archive/src/lib.rs @@ -1,5 +1,5 @@ -//! Optional archive of included CSL styles. -// This file is generated by tests/archive.rs +//! Archive CSL styles. Included in Typst. +// This file is generated by tests/src/archiver.rs // Do not edit by hand! use citationberg::{Locale, Style}; @@ -623,88 +623,88 @@ impl ArchivedStyle { /// Get the CBOR representation of this style. fn bytes(self) -> &'static [u8] { match self { - Self::Alphanumeric => include_bytes!("../../archive/styles/alphanumeric.cbor"), - Self::AmericanAnthropologicalAssociation => include_bytes!("../../archive/styles/american-anthropological-association.cbor"), - Self::AmericanChemicalSociety => include_bytes!("../../archive/styles/american-chemical-society.cbor"), - Self::AmericanGeophysicalUnion => include_bytes!("../../archive/styles/american-geophysical-union.cbor"), - Self::AmericanInstituteOfAeronauticsAndAstronautics => include_bytes!("../../archive/styles/american-institute-of-aeronautics-and-astronautics.cbor"), - Self::AmericanInstituteOfPhysics => include_bytes!("../../archive/styles/american-institute-of-physics.cbor"), - Self::AmericanMathematicalSocietyLabel => include_bytes!("../../archive/styles/american-mathematical-society-label.cbor"), - Self::AmericanMathematicalSocietyNumeric => include_bytes!("../../archive/styles/american-mathematical-society-numeric.cbor"), - Self::AmericanMedicalAssociation => include_bytes!("../../archive/styles/american-medical-association.cbor"), - Self::AmericanMeteorologicalSociety => include_bytes!("../../archive/styles/american-meteorological-society.cbor"), - Self::AmericanPhysicsSociety => include_bytes!("../../archive/styles/american-physics-society.cbor"), - Self::AmericanPhysiologicalSociety => include_bytes!("../../archive/styles/american-physiological-society.cbor"), - Self::AmericanPoliticalScienceAssociation => include_bytes!("../../archive/styles/american-political-science-association.cbor"), - Self::AmericanPsychologicalAssociation => include_bytes!("../../archive/styles/apa.cbor"), - Self::AmericanSocietyForMicrobiology => include_bytes!("../../archive/styles/american-society-for-microbiology.cbor"), - Self::AmericanSocietyOfCivilEngineers => include_bytes!("../../archive/styles/american-society-of-civil-engineers.cbor"), - Self::AmericanSocietyOfMechanicalEngineers => include_bytes!("../../archive/styles/american-society-of-mechanical-engineers.cbor"), - Self::AmericanSociologicalAssociation => include_bytes!("../../archive/styles/american-sociological-association.cbor"), - Self::AngewandteChemie => include_bytes!("../../archive/styles/angewandte-chemie.cbor"), - Self::AnnualReviews => include_bytes!("../../archive/styles/annual-reviews.cbor"), - Self::AnnualReviewsAuthorDate => include_bytes!("../../archive/styles/annual-reviews-author-date.cbor"), - Self::AssociacaoBrasileiraDeNormasTecnicas => include_bytes!("../../archive/styles/associacao-brasileira-de-normas-tecnicas.cbor"), - Self::AssociationForComputingMachinery => include_bytes!("../../archive/styles/association-for-computing-machinery.cbor"), - Self::BiomedCentral => include_bytes!("../../archive/styles/biomed-central.cbor"), - Self::BristolUniversityPress => include_bytes!("../../archive/styles/bristol-university-press.cbor"), - Self::BritishMedicalJournal => include_bytes!("../../archive/styles/bmj.cbor"), - Self::Cell => include_bytes!("../../archive/styles/cell.cbor"), - Self::ChicagoAuthorDate => include_bytes!("../../archive/styles/chicago-author-date.cbor"), - Self::ChicagoNotes => include_bytes!("../../archive/styles/chicago-notes-bibliography.cbor"), - Self::ChicagoShortenedNotes => include_bytes!("../../archive/styles/chicago-shortened-notes-bibliography.cbor"), - Self::Copernicus => include_bytes!("../../archive/styles/copernicus-publications.cbor"), - Self::CseCitationSequenceBrackets8thEdition => include_bytes!("../../archive/styles/cse-citation-sequence-brackets-8th-edition.cbor"), - Self::CseNameYear => include_bytes!("../../archive/styles/cse-name-year.cbor"), - Self::CurrentOpinion => include_bytes!("../../archive/styles/current-opinion.cbor"), - Self::DeutscheGesellschaftFürPsychologie => include_bytes!("../../archive/styles/deutsche-gesellschaft-fur-psychologie.cbor"), - Self::DeutscheSprache => include_bytes!("../../archive/styles/deutsche-sprache.cbor"), - Self::ElsevierHarvard => include_bytes!("../../archive/styles/elsevier-harvard.cbor"), - Self::ElsevierVancouver => include_bytes!("../../archive/styles/elsevier-vancouver.cbor"), - Self::ElsevierWithTitles => include_bytes!("../../archive/styles/elsevier-with-titles.cbor"), - Self::Frontiers => include_bytes!("../../archive/styles/frontiers.cbor"), - Self::FutureMedicine => include_bytes!("../../archive/styles/future-medicine.cbor"), - Self::FutureScience => include_bytes!("../../archive/styles/future-science-group.cbor"), - Self::Gb77142005Numeric => include_bytes!("../../archive/styles/chinese-gb7714-2005-numeric.cbor"), - Self::Gb77142015AuthorDate => include_bytes!("../../archive/styles/china-national-standard-gb-t-7714-2015-author-date.cbor"), - Self::Gb77142015Note => include_bytes!("../../archive/styles/china-national-standard-gb-t-7714-2015-note.cbor"), - Self::Gb77142015Numeric => include_bytes!("../../archive/styles/china-national-standard-gb-t-7714-2015-numeric.cbor"), - Self::GostR7052008Numeric => include_bytes!("../../archive/styles/gost-r-7-0-5-2008-numeric.cbor"), - Self::HarvardCiteThemRight => include_bytes!("../../archive/styles/harvard-cite-them-right.cbor"), - Self::InstituteOfElectricalAndElectronicsEngineers => include_bytes!("../../archive/styles/ieee.cbor"), - Self::InstituteOfPhysicsNumeric => include_bytes!("../../archive/styles/institute-of-physics-numeric.cbor"), - Self::Iso690AuthorDate => include_bytes!("../../archive/styles/iso690-author-date-en.cbor"), - Self::Iso690Numeric => include_bytes!("../../archive/styles/iso690-numeric-en.cbor"), - Self::Karger => include_bytes!("../../archive/styles/karger-journals.cbor"), - Self::MaryAnnLiebertVancouver => include_bytes!("../../archive/styles/mary-ann-liebert-vancouver.cbor"), - Self::ModernHumanitiesResearchAssociationNotes => include_bytes!("../../archive/styles/mhra-notes.cbor"), - Self::ModernLanguageAssociation => include_bytes!("../../archive/styles/modern-language-association.cbor"), - Self::MultidisciplinaryDigitalPublishingInstitute => include_bytes!("../../archive/styles/multidisciplinary-digital-publishing-institute.cbor"), - Self::Nature => include_bytes!("../../archive/styles/nature.cbor"), - Self::NlmCitationSequence => include_bytes!("../../archive/styles/nlm-citation-sequence.cbor"), - Self::NlmCitationSequenceSuperscript => include_bytes!("../../archive/styles/nlm-citation-sequence-superscript.cbor"), - Self::Pensoft => include_bytes!("../../archive/styles/pensoft-journals.cbor"), - Self::PublicLibraryOfScience => include_bytes!("../../archive/styles/plos.cbor"), - Self::RoyalSocietyOfChemistry => include_bytes!("../../archive/styles/royal-society-of-chemistry.cbor"), - Self::SageVancouver => include_bytes!("../../archive/styles/sage-vancouver.cbor"), - Self::Sist02 => include_bytes!("../../archive/styles/sist02.cbor"), - Self::Spie => include_bytes!("../../archive/styles/spie-journals.cbor"), - Self::SpringerBasic => include_bytes!("../../archive/styles/springer-basic-brackets.cbor"), - Self::SpringerBasicAuthorDate => include_bytes!("../../archive/styles/springer-basic-author-date.cbor"), - Self::SpringerFachzeitschriftenMedizinPsychologie => include_bytes!("../../archive/styles/springer-fachzeitschriften-medizin-psychologie.cbor"), - Self::SpringerHumanitiesAuthorDate => include_bytes!("../../archive/styles/springer-humanities-author-date.cbor"), - Self::SpringerLectureNotesInComputerScience => include_bytes!("../../archive/styles/springer-lecture-notes-in-computer-science.cbor"), - Self::SpringerMathphys => include_bytes!("../../archive/styles/springer-mathphys-brackets.cbor"), - Self::SpringerSocpsychAuthorDate => include_bytes!("../../archive/styles/springer-socpsych-author-date.cbor"), - Self::SpringerVancouver => include_bytes!("../../archive/styles/springer-vancouver-brackets.cbor"), - Self::TaylorAndFrancisChicagoAuthorDate => include_bytes!("../../archive/styles/taylor-and-francis-chicago-author-date.cbor"), - Self::TaylorAndFrancisNationalLibraryOfMedicine => include_bytes!("../../archive/styles/taylor-and-francis-national-library-of-medicine.cbor"), - Self::TheInstitutionOfEngineeringAndTechnology => include_bytes!("../../archive/styles/the-institution-of-engineering-and-technology.cbor"), - Self::TheLancet => include_bytes!("../../archive/styles/the-lancet.cbor"), - Self::Thieme => include_bytes!("../../archive/styles/thieme-german.cbor"), - Self::Trends => include_bytes!("../../archive/styles/trends-journals.cbor"), - Self::TurabianAuthorDate => include_bytes!("../../archive/styles/chicago-author-date-17th-edition.cbor"), - Self::TurabianFullnote8 => include_bytes!("../../archive/styles/chicago-notes-bibliography-subsequent-author-title-17th-edition.cbor"), + Self::Alphanumeric => include_bytes!("../archive/styles/alphanumeric.cbor"), + Self::AmericanAnthropologicalAssociation => include_bytes!("../archive/styles/american-anthropological-association.cbor"), + Self::AmericanChemicalSociety => include_bytes!("../archive/styles/american-chemical-society.cbor"), + Self::AmericanGeophysicalUnion => include_bytes!("../archive/styles/american-geophysical-union.cbor"), + Self::AmericanInstituteOfAeronauticsAndAstronautics => include_bytes!("../archive/styles/american-institute-of-aeronautics-and-astronautics.cbor"), + Self::AmericanInstituteOfPhysics => include_bytes!("../archive/styles/american-institute-of-physics.cbor"), + Self::AmericanMathematicalSocietyLabel => include_bytes!("../archive/styles/american-mathematical-society-label.cbor"), + Self::AmericanMathematicalSocietyNumeric => include_bytes!("../archive/styles/american-mathematical-society-numeric.cbor"), + Self::AmericanMedicalAssociation => include_bytes!("../archive/styles/american-medical-association.cbor"), + Self::AmericanMeteorologicalSociety => include_bytes!("../archive/styles/american-meteorological-society.cbor"), + Self::AmericanPhysicsSociety => include_bytes!("../archive/styles/american-physics-society.cbor"), + Self::AmericanPhysiologicalSociety => include_bytes!("../archive/styles/american-physiological-society.cbor"), + Self::AmericanPoliticalScienceAssociation => include_bytes!("../archive/styles/american-political-science-association.cbor"), + Self::AmericanPsychologicalAssociation => include_bytes!("../archive/styles/apa.cbor"), + Self::AmericanSocietyForMicrobiology => include_bytes!("../archive/styles/american-society-for-microbiology.cbor"), + Self::AmericanSocietyOfCivilEngineers => include_bytes!("../archive/styles/american-society-of-civil-engineers.cbor"), + Self::AmericanSocietyOfMechanicalEngineers => include_bytes!("../archive/styles/american-society-of-mechanical-engineers.cbor"), + Self::AmericanSociologicalAssociation => include_bytes!("../archive/styles/american-sociological-association.cbor"), + Self::AngewandteChemie => include_bytes!("../archive/styles/angewandte-chemie.cbor"), + Self::AnnualReviews => include_bytes!("../archive/styles/annual-reviews.cbor"), + Self::AnnualReviewsAuthorDate => include_bytes!("../archive/styles/annual-reviews-author-date.cbor"), + Self::AssociacaoBrasileiraDeNormasTecnicas => include_bytes!("../archive/styles/associacao-brasileira-de-normas-tecnicas.cbor"), + Self::AssociationForComputingMachinery => include_bytes!("../archive/styles/association-for-computing-machinery.cbor"), + Self::BiomedCentral => include_bytes!("../archive/styles/biomed-central.cbor"), + Self::BristolUniversityPress => include_bytes!("../archive/styles/bristol-university-press.cbor"), + Self::BritishMedicalJournal => include_bytes!("../archive/styles/bmj.cbor"), + Self::Cell => include_bytes!("../archive/styles/cell.cbor"), + Self::ChicagoAuthorDate => include_bytes!("../archive/styles/chicago-author-date.cbor"), + Self::ChicagoNotes => include_bytes!("../archive/styles/chicago-notes-bibliography.cbor"), + Self::ChicagoShortenedNotes => include_bytes!("../archive/styles/chicago-shortened-notes-bibliography.cbor"), + Self::Copernicus => include_bytes!("../archive/styles/copernicus-publications.cbor"), + Self::CseCitationSequenceBrackets8thEdition => include_bytes!("../archive/styles/cse-citation-sequence-brackets-8th-edition.cbor"), + Self::CseNameYear => include_bytes!("../archive/styles/cse-name-year.cbor"), + Self::CurrentOpinion => include_bytes!("../archive/styles/current-opinion.cbor"), + Self::DeutscheGesellschaftFürPsychologie => include_bytes!("../archive/styles/deutsche-gesellschaft-fur-psychologie.cbor"), + Self::DeutscheSprache => include_bytes!("../archive/styles/deutsche-sprache.cbor"), + Self::ElsevierHarvard => include_bytes!("../archive/styles/elsevier-harvard.cbor"), + Self::ElsevierVancouver => include_bytes!("../archive/styles/elsevier-vancouver.cbor"), + Self::ElsevierWithTitles => include_bytes!("../archive/styles/elsevier-with-titles.cbor"), + Self::Frontiers => include_bytes!("../archive/styles/frontiers.cbor"), + Self::FutureMedicine => include_bytes!("../archive/styles/future-medicine.cbor"), + Self::FutureScience => include_bytes!("../archive/styles/future-science-group.cbor"), + Self::Gb77142005Numeric => include_bytes!("../archive/styles/chinese-gb7714-2005-numeric.cbor"), + Self::Gb77142015AuthorDate => include_bytes!("../archive/styles/china-national-standard-gb-t-7714-2015-author-date.cbor"), + Self::Gb77142015Note => include_bytes!("../archive/styles/china-national-standard-gb-t-7714-2015-note.cbor"), + Self::Gb77142015Numeric => include_bytes!("../archive/styles/china-national-standard-gb-t-7714-2015-numeric.cbor"), + Self::GostR7052008Numeric => include_bytes!("../archive/styles/gost-r-7-0-5-2008-numeric.cbor"), + Self::HarvardCiteThemRight => include_bytes!("../archive/styles/harvard-cite-them-right.cbor"), + Self::InstituteOfElectricalAndElectronicsEngineers => include_bytes!("../archive/styles/ieee.cbor"), + Self::InstituteOfPhysicsNumeric => include_bytes!("../archive/styles/institute-of-physics-numeric.cbor"), + Self::Iso690AuthorDate => include_bytes!("../archive/styles/iso690-author-date-en.cbor"), + Self::Iso690Numeric => include_bytes!("../archive/styles/iso690-numeric-en.cbor"), + Self::Karger => include_bytes!("../archive/styles/karger-journals.cbor"), + Self::MaryAnnLiebertVancouver => include_bytes!("../archive/styles/mary-ann-liebert-vancouver.cbor"), + Self::ModernHumanitiesResearchAssociationNotes => include_bytes!("../archive/styles/mhra-notes.cbor"), + Self::ModernLanguageAssociation => include_bytes!("../archive/styles/modern-language-association.cbor"), + Self::MultidisciplinaryDigitalPublishingInstitute => include_bytes!("../archive/styles/multidisciplinary-digital-publishing-institute.cbor"), + Self::Nature => include_bytes!("../archive/styles/nature.cbor"), + Self::NlmCitationSequence => include_bytes!("../archive/styles/nlm-citation-sequence.cbor"), + Self::NlmCitationSequenceSuperscript => include_bytes!("../archive/styles/nlm-citation-sequence-superscript.cbor"), + Self::Pensoft => include_bytes!("../archive/styles/pensoft-journals.cbor"), + Self::PublicLibraryOfScience => include_bytes!("../archive/styles/plos.cbor"), + Self::RoyalSocietyOfChemistry => include_bytes!("../archive/styles/royal-society-of-chemistry.cbor"), + Self::SageVancouver => include_bytes!("../archive/styles/sage-vancouver.cbor"), + Self::Sist02 => include_bytes!("../archive/styles/sist02.cbor"), + Self::Spie => include_bytes!("../archive/styles/spie-journals.cbor"), + Self::SpringerBasic => include_bytes!("../archive/styles/springer-basic-brackets.cbor"), + Self::SpringerBasicAuthorDate => include_bytes!("../archive/styles/springer-basic-author-date.cbor"), + Self::SpringerFachzeitschriftenMedizinPsychologie => include_bytes!("../archive/styles/springer-fachzeitschriften-medizin-psychologie.cbor"), + Self::SpringerHumanitiesAuthorDate => include_bytes!("../archive/styles/springer-humanities-author-date.cbor"), + Self::SpringerLectureNotesInComputerScience => include_bytes!("../archive/styles/springer-lecture-notes-in-computer-science.cbor"), + Self::SpringerMathphys => include_bytes!("../archive/styles/springer-mathphys-brackets.cbor"), + Self::SpringerSocpsychAuthorDate => include_bytes!("../archive/styles/springer-socpsych-author-date.cbor"), + Self::SpringerVancouver => include_bytes!("../archive/styles/springer-vancouver-brackets.cbor"), + Self::TaylorAndFrancisChicagoAuthorDate => include_bytes!("../archive/styles/taylor-and-francis-chicago-author-date.cbor"), + Self::TaylorAndFrancisNationalLibraryOfMedicine => include_bytes!("../archive/styles/taylor-and-francis-national-library-of-medicine.cbor"), + Self::TheInstitutionOfEngineeringAndTechnology => include_bytes!("../archive/styles/the-institution-of-engineering-and-technology.cbor"), + Self::TheLancet => include_bytes!("../archive/styles/the-lancet.cbor"), + Self::Thieme => include_bytes!("../archive/styles/thieme-german.cbor"), + Self::Trends => include_bytes!("../archive/styles/trends-journals.cbor"), + Self::TurabianAuthorDate => include_bytes!("../archive/styles/chicago-author-date-17th-edition.cbor"), + Self::TurabianFullnote8 => include_bytes!("../archive/styles/chicago-notes-bibliography-subsequent-author-title-17th-edition.cbor"), } } @@ -1161,70 +1161,70 @@ fn from_cbor( } /// CBOR-encoded CSL locales. -pub const LOCALES: &[&[u8]] = &[ - include_bytes!("../../archive/locales/af-ZA.cbor"), - include_bytes!("../../archive/locales/ar.cbor"), - include_bytes!("../../archive/locales/bal-PK.cbor"), - include_bytes!("../../archive/locales/bg-BG.cbor"), - include_bytes!("../../archive/locales/brh-PK.cbor"), - include_bytes!("../../archive/locales/ca-AD.cbor"), - include_bytes!("../../archive/locales/cs-CZ.cbor"), - include_bytes!("../../archive/locales/cy-GB.cbor"), - include_bytes!("../../archive/locales/da-DK.cbor"), - include_bytes!("../../archive/locales/de-AT.cbor"), - include_bytes!("../../archive/locales/de-CH.cbor"), - include_bytes!("../../archive/locales/de-DE.cbor"), - include_bytes!("../../archive/locales/el-GR.cbor"), - include_bytes!("../../archive/locales/en-GB.cbor"), - include_bytes!("../../archive/locales/en-US.cbor"), - include_bytes!("../../archive/locales/es-CL.cbor"), - include_bytes!("../../archive/locales/es-ES.cbor"), - include_bytes!("../../archive/locales/es-MX.cbor"), - include_bytes!("../../archive/locales/et-EE.cbor"), - include_bytes!("../../archive/locales/eu.cbor"), - include_bytes!("../../archive/locales/fa-IR.cbor"), - include_bytes!("../../archive/locales/fi-FI.cbor"), - include_bytes!("../../archive/locales/fr-CA.cbor"), - include_bytes!("../../archive/locales/fr-FR.cbor"), - include_bytes!("../../archive/locales/gl-ES.cbor"), - include_bytes!("../../archive/locales/he-IL.cbor"), - include_bytes!("../../archive/locales/hi-IN.cbor"), - include_bytes!("../../archive/locales/hr-HR.cbor"), - include_bytes!("../../archive/locales/hu-HU.cbor"), - include_bytes!("../../archive/locales/hy-AM.cbor"), - include_bytes!("../../archive/locales/id-ID.cbor"), - include_bytes!("../../archive/locales/is-IS.cbor"), - include_bytes!("../../archive/locales/it-IT.cbor"), - include_bytes!("../../archive/locales/ja-JP.cbor"), - include_bytes!("../../archive/locales/km-KH.cbor"), - include_bytes!("../../archive/locales/ko-KR.cbor"), - include_bytes!("../../archive/locales/la.cbor"), - include_bytes!("../../archive/locales/lij-IT.cbor"), - include_bytes!("../../archive/locales/lt-LT.cbor"), - include_bytes!("../../archive/locales/lv-LV.cbor"), - include_bytes!("../../archive/locales/mn-MN.cbor"), - include_bytes!("../../archive/locales/ms-MY.cbor"), - include_bytes!("../../archive/locales/nb-NO.cbor"), - include_bytes!("../../archive/locales/nl-NL.cbor"), - include_bytes!("../../archive/locales/nn-NO.cbor"), - include_bytes!("../../archive/locales/pa-PK.cbor"), - include_bytes!("../../archive/locales/pl-PL.cbor"), - include_bytes!("../../archive/locales/pt-BR.cbor"), - include_bytes!("../../archive/locales/pt-PT.cbor"), - include_bytes!("../../archive/locales/ro-RO.cbor"), - include_bytes!("../../archive/locales/ru-RU.cbor"), - include_bytes!("../../archive/locales/sk-SK.cbor"), - include_bytes!("../../archive/locales/sl-SI.cbor"), - include_bytes!("../../archive/locales/sr-Cyrl-RS.cbor"), - include_bytes!("../../archive/locales/sr-Latn-RS.cbor"), - include_bytes!("../../archive/locales/sv-SE.cbor"), - include_bytes!("../../archive/locales/th-TH.cbor"), - include_bytes!("../../archive/locales/tl-PH.cbor"), - include_bytes!("../../archive/locales/tr-TR.cbor"), - include_bytes!("../../archive/locales/uk-UA.cbor"), - include_bytes!("../../archive/locales/vi-VN.cbor"), - include_bytes!("../../archive/locales/zh-CN.cbor"), - include_bytes!("../../archive/locales/zh-TW.cbor"), +const LOCALES: &[&[u8]] = &[ + include_bytes!("../archive/locales/af-ZA.cbor"), + include_bytes!("../archive/locales/ar.cbor"), + include_bytes!("../archive/locales/bal-PK.cbor"), + include_bytes!("../archive/locales/bg-BG.cbor"), + include_bytes!("../archive/locales/brh-PK.cbor"), + include_bytes!("../archive/locales/ca-AD.cbor"), + include_bytes!("../archive/locales/cs-CZ.cbor"), + include_bytes!("../archive/locales/cy-GB.cbor"), + include_bytes!("../archive/locales/da-DK.cbor"), + include_bytes!("../archive/locales/de-AT.cbor"), + include_bytes!("../archive/locales/de-CH.cbor"), + include_bytes!("../archive/locales/de-DE.cbor"), + include_bytes!("../archive/locales/el-GR.cbor"), + include_bytes!("../archive/locales/en-GB.cbor"), + include_bytes!("../archive/locales/en-US.cbor"), + include_bytes!("../archive/locales/es-CL.cbor"), + include_bytes!("../archive/locales/es-ES.cbor"), + include_bytes!("../archive/locales/es-MX.cbor"), + include_bytes!("../archive/locales/et-EE.cbor"), + include_bytes!("../archive/locales/eu.cbor"), + include_bytes!("../archive/locales/fa-IR.cbor"), + include_bytes!("../archive/locales/fi-FI.cbor"), + include_bytes!("../archive/locales/fr-CA.cbor"), + include_bytes!("../archive/locales/fr-FR.cbor"), + include_bytes!("../archive/locales/gl-ES.cbor"), + include_bytes!("../archive/locales/he-IL.cbor"), + include_bytes!("../archive/locales/hi-IN.cbor"), + include_bytes!("../archive/locales/hr-HR.cbor"), + include_bytes!("../archive/locales/hu-HU.cbor"), + include_bytes!("../archive/locales/hy-AM.cbor"), + include_bytes!("../archive/locales/id-ID.cbor"), + include_bytes!("../archive/locales/is-IS.cbor"), + include_bytes!("../archive/locales/it-IT.cbor"), + include_bytes!("../archive/locales/ja-JP.cbor"), + include_bytes!("../archive/locales/km-KH.cbor"), + include_bytes!("../archive/locales/ko-KR.cbor"), + include_bytes!("../archive/locales/la.cbor"), + include_bytes!("../archive/locales/lij-IT.cbor"), + include_bytes!("../archive/locales/lt-LT.cbor"), + include_bytes!("../archive/locales/lv-LV.cbor"), + include_bytes!("../archive/locales/mn-MN.cbor"), + include_bytes!("../archive/locales/ms-MY.cbor"), + include_bytes!("../archive/locales/nb-NO.cbor"), + include_bytes!("../archive/locales/nl-NL.cbor"), + include_bytes!("../archive/locales/nn-NO.cbor"), + include_bytes!("../archive/locales/pa-PK.cbor"), + include_bytes!("../archive/locales/pl-PL.cbor"), + include_bytes!("../archive/locales/pt-BR.cbor"), + include_bytes!("../archive/locales/pt-PT.cbor"), + include_bytes!("../archive/locales/ro-RO.cbor"), + include_bytes!("../archive/locales/ru-RU.cbor"), + include_bytes!("../archive/locales/sk-SK.cbor"), + include_bytes!("../archive/locales/sl-SI.cbor"), + include_bytes!("../archive/locales/sr-Cyrl-RS.cbor"), + include_bytes!("../archive/locales/sr-Latn-RS.cbor"), + include_bytes!("../archive/locales/sv-SE.cbor"), + include_bytes!("../archive/locales/th-TH.cbor"), + include_bytes!("../archive/locales/tl-PH.cbor"), + include_bytes!("../archive/locales/tr-TR.cbor"), + include_bytes!("../archive/locales/uk-UA.cbor"), + include_bytes!("../archive/locales/vi-VN.cbor"), + include_bytes!("../archive/locales/zh-CN.cbor"), + include_bytes!("../archive/locales/zh-TW.cbor"), ]; /// Get all CSL locales. diff --git a/crates/hayagriva-cli/Cargo.toml b/crates/hayagriva-cli/Cargo.toml new file mode 100644 index 00000000..f077f27e --- /dev/null +++ b/crates/hayagriva-cli/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "hayagriva-cli" +description = "The command line interface for Hayagriva." +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +keywords = [ + "hayagriva", + "bibliography", + "citation", + "reference", + "literature", + "cli", +] + +[dependencies] +citationberg = { workspace = true } +clap = { version = "4", features = ["cargo", "derive"] } +hayagriva-archive = { workspace = true } +hayagriva-core = { workspace = true } +hayagriva-csl = { workspace = true } +hayagriva-format = { workspace = true } +strum = { version = "0.26", features = ["derive"] } +biblatex = { workspace = true, optional = true } + +[features] +default = ["biblatex"] +biblatex = ["hayagriva-format/biblatex", "dep:biblatex"] + +[[bin]] +name = "hayagriva" +path = "src/main.rs" diff --git a/src/main.rs b/crates/hayagriva-cli/src/main.rs similarity index 50% rename from src/main.rs rename to crates/hayagriva-cli/src/main.rs index 10007de6..80d84f6b 100644 --- a/src/main.rs +++ b/crates/hayagriva-cli/src/main.rs @@ -1,3 +1,35 @@ +/*! +Hayagriva provides a YAML-backed format and data model for various +bibliography items as well as a CSL processor formatting both in-text citations and +reference lists based on these literature databases. + +The crate is intended to assist scholarly writing and reference management +and can be used both through a CLI and an API. + +Below, there is an example of how to parse a YAML database and get a Modern +Language Association-style citation. + +# Supported styles + +Hayagriva supports all styles provided in the +[official Citation Style Language repository](https://github.com/citation-style-language/styles), +currently over 2,600. You can provide your own style files or use the ones +bundled with this library in the [`hayagriva_archive`] crate. + +```ignore +use hayagriva_format::Entry; +let converted: Entry = your_biblatex_entry.into(); +``` + +If you do not need BibLaTeX compatibility, you can use Hayagriva without the +default features by writing this in your `Cargo.toml`: + +```toml +[dependencies] +hayagriva = { version = "0.9", default-features = false } +``` +*/ + use std::borrow::Cow; use std::fs::{self, read_to_string}; use std::io::ErrorKind as IoErrorKind; @@ -12,20 +44,26 @@ use clap::builder::PossibleValue; use clap::{Arg, ArgAction, Command, ValueEnum, crate_version}; use strum::VariantNames; -use hayagriva::archive::{ArchivedStyle, locales}; -use hayagriva::{ - BibliographyDriver, CitationItem, CitationRequest, LocatorPayload, SpecificLocator, - io, +use hayagriva_archive::{ArchivedStyle, locales}; +use hayagriva_csl::{ + BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, + LocatorPayload, SpecificLocator, }; -use hayagriva::{BibliographyRequest, Selector}; +#[cfg(feature = "biblatex")] +use hayagriva_format::io::from_biblatex_str; +use hayagriva_format::{Selector, io}; #[derive(Debug, Copy, Clone, PartialEq, VariantNames)] #[strum(serialize_all = "kebab_case")] +/// Which input format is expected. pub enum Format { #[cfg(feature = "biblatex")] + /// The bibtex format. Bibtex, #[cfg(feature = "biblatex")] + /// BibLaTeX format. Biblatex, + /// Hayagriva YAML. Yaml, } @@ -52,6 +90,9 @@ impl ValueEnum for Format { } #[derive(ValueEnum, Clone)] +/// CSL locator types. This enum is used to specify the type of locator given +/// via the `--locators` argument of the `cite` command. +/// See the `--locator-type` argument. enum LocatorTypes { Act, Appendix, @@ -325,7 +366,7 @@ verse volume"#) match format { Format::Yaml => io::from_yaml_str(&input).unwrap(), #[cfg(feature = "biblatex")] - Format::Biblatex | Format::Bibtex => io::from_biblatex_str(&input).unwrap(), + Format::Biblatex | Format::Bibtex => from_biblatex_str(&input).unwrap(), } }; @@ -460,13 +501,10 @@ verse volume"#) let (style, locales, locale) = retrieve_assets(style, csl, locale_path, locale_str); - let indicated_locator = if let Some(locator_type) = - sub_matches.get_one::("locator_type") - { - locator_type.into() - } else { - Locator::Custom - }; + let indicated_locator = sub_matches + .get_one::("locator_type") + .map(Into::into) + .unwrap_or(Locator::Custom); let assign_locator = |(i, e)| { let mut item = CitationItem::with_entry(e); @@ -524,8 +562,8 @@ verse volume"#) Some(("styles", _)) => { for key in ArchivedStyle::all() { let style = key.get(); - println!("- {}", &key.names()[0]); - println!(" Full name: {}", &style.info().title.value); + println!("- {}", key.names()[0]); + println!(" Full name: {}", style.info().title.value); println!(" Authors:"); for author in style.info().authors.iter() { print!(" - {}", author.name); @@ -536,7 +574,7 @@ verse volume"#) } } if let Some(desc) = &style.info().summary { - println!(" Summary: {}", &desc.value); + println!(" Summary: {}", desc.value); } } } @@ -585,3 +623,513 @@ fn retrieve_assets<'a>( (style, locales, locale) } + +#[cfg(test)] +mod tests { + use std::{borrow::Cow, fs, path::Path, sync::Arc}; + + use citationberg::{ + IndependentStyle, LocaleCode, LocaleFile, + taxonomy::{Locator, NumberVariable}, + }; + use hayagriva_core::{EntryLike, MaybeTyped, Numeric, NumericValue}; + use hayagriva_csl::{ + BibliographyDriver, BibliographyRequest, BufWriteFormat, CitationItem, + CitationRequest, CitePurpose, LocatorPayload, SpecificLocator, + TransparentLocator, + }; + use hayagriva_format::io::from_yaml_str; + + #[test] + fn test_csl() { + let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../"); + let en_locale = + fs::read_to_string(workspace.join("tests/data/locales-en-US.xml")).unwrap(); + let en_locale = LocaleFile::from_xml(&en_locale).unwrap(); + + let yaml = fs::read_to_string(workspace.join("tests/data/basic.yml")).unwrap(); + let bib = from_yaml_str(&yaml).unwrap(); + let en_locale = [en_locale.into()]; + + for style_thing in fs::read_dir(workspace.join("styles/")).unwrap().take(100) { + let thing = style_thing.unwrap(); + if thing.file_type().unwrap().is_dir() { + continue; + } + + let path = thing.path(); + let extension = path.extension(); + if let Some(extension) = extension { + if extension.to_str() != Some("csl") { + continue; + } + } else { + continue; + } + + println!("testing {path:?}"); + let style = fs::read_to_string(path).unwrap(); + let style = IndependentStyle::from_xml(&style).unwrap(); + let mut driver = BibliographyDriver::new(); + + for n in (0..bib.len()).step_by(2) { + let items = vec![ + CitationItem::with_entry(bib.nth(n).unwrap()), + CitationItem::with_entry(bib.nth(n + 1).unwrap()), + ]; + driver.citation(CitationRequest::new( + items, &style, None, &en_locale, None, + )); + } + + driver.finish(BibliographyRequest { + style: &style, + locale: None, + locale_files: &en_locale, + }); + + // for cite in finished.citations { + // println!("{}", cite.citation.to_string(BufWriteFormat::Plain)) + // } + + // if let Some(bib) = finished.bibliography { + // for (prefix, item) in bib.items { + // if let Some(prefix) = prefix { + // print!("{} ", prefix.to_string(BufWriteFormat::Plain)) + // } + // println!("{}", item.to_string(BufWriteFormat::Html)) + // } + // } + } + } + + #[test] + fn test_transparent_locator_ibid() { + let test_data = r#" + katalog: + type: Book + title: "Book" + author: "Surname, Name" + location: Warsaw + date: 1971 + katalog2: + type: Book + title: "Book2" + author: "Surname, Name" + location: Warsaw + date: 1971 + "#; + let test_style = r#" + "#; + + let library = from_yaml_str(test_data).unwrap(); + let style = citationberg::Style::from_xml(test_style).unwrap(); + let citationberg::Style::Independent(style) = style else { unreachable!() }; + + let mut driver = BibliographyDriver::new(); + let citations = [ + ("katalog", Some([2, 3]), "first."), + ("katalog", Some([2, 3]), "ibid."), + ("katalog", Some([2, 3]), "ibid."), + ("katalog", Some([2, 4]), "ibid-with-locator."), + ("katalog", None, "subsequent."), + ("katalog", Some([2, 3]), "ibid-with-locator."), + ("katalog", Some([2, 3]), "ibid."), + ("katalog2", Some([2, 3]), "first."), + ]; + + for (entry, supplement, _) in citations { + let entry = library.get(entry).unwrap(); + driver.citation(CitationRequest::new( + vec![CitationItem::with_locator( + entry, + supplement.map(|s| { + SpecificLocator( + Locator::Supplement, + LocatorPayload::Transparent(TransparentLocator(Arc::new(s))), + ) + }), + )], + &style, + None, + &[], + None, + )); + } + + let finished = driver.finish(BibliographyRequest { + style: &style, + locale: None, + locale_files: &[], + }); + + for (citation, (_, _, expected_value)) in finished.citations.iter().zip(citations) + { + let mut s = String::new(); + citation.citation.write_buf(&mut s, BufWriteFormat::Plain).unwrap(); + assert_eq!(s, expected_value); + } + } + + #[test] + fn test_chapter_field() { + let workspace = Path::new(env!("CARGO_MANIFEST_DIR")); + let yaml = + fs::read_to_string(workspace.join("../../tests/data/basic.yml")).unwrap(); + let bib = from_yaml_str(&yaml).unwrap(); + let book = bib.get("lamb").unwrap(); + assert_eq!(book.chapter().unwrap(), &MaybeTyped::Typed(Numeric::new(20))); + assert_eq!(book.volume().unwrap(), &MaybeTyped::Typed(Numeric::new(10))); + assert_eq!(book.keyed_serial_number("serial"), None); + assert_eq!(book.resolve_number_variable(NumberVariable::Number), None); + + let book = bib.get("snail").unwrap(); + assert_eq!( + book.chapter().unwrap(), + &MaybeTyped::Typed(Numeric { + value: NumericValue::Number(2), + prefix: None, + suffix: Some(Box::new("A".into())) + }) + ); + assert_eq!( + book.volume().unwrap(), + &MaybeTyped::Typed(Numeric { + value: NumericValue::Number(4), + prefix: None, + suffix: Some(Box::new("B".into())) + }) + ); + assert_eq!(book.keyed_serial_number("serial"), None); + assert_eq!(book.resolve_number_variable(NumberVariable::Number), None); + + let chapter = bib.get("lamb-chapter").unwrap(); + assert_eq!(chapter.chapter().unwrap(), &MaybeTyped::Typed(Numeric::new(3))); + assert_eq!(chapter.keyed_serial_number("serial"), None); + assert_eq!( + chapter.resolve_number_variable(NumberVariable::Number), + Some(MaybeTyped::Typed(Cow::Borrowed(&Numeric::new(3)))) + ); + assert_eq!(chapter.volume(), None); + assert_eq!( + chapter.parents().first().unwrap().volume().unwrap(), + &MaybeTyped::Typed(Numeric::new(10)) + ); + + let chapter = bib.get("snail-chapter").unwrap(); + assert_eq!(chapter.chapter(), None); + assert_eq!(chapter.keyed_serial_number("serial").unwrap(), "3"); + + assert_eq!(chapter.volume(), None); + assert_eq!( + chapter.parents().first().unwrap().volume().unwrap(), + &MaybeTyped::Typed(Numeric { + value: NumericValue::Number(4), + prefix: None, + suffix: Some(Box::new("B".into())) + }) + ); + } + + #[test] + fn test_alphanumeric_disambiguation() { + let bibtex = r#"@article{chenTransMorphTransformerUnsupervised2021, + title = {{{TransMorph}}: {{Transformer}} for Unsupervised Medical Image Registration}, + author = {Chen, Junyu and Frey, Eric C. and He, Yufan and Segars, William P. and Li, Ye and Du, Yong}, + date = {2021}, + } + + @article{chenViTVNetVisionTransformer2021, + title = {{{ViT-V-Net}}: {{Vision Transformer}} for {{Unsupervised Volumetric Medical Image Registration}}}, + author = {Chen, Junyu and He, Yufan and Frey, Eric C. and Li, Ye and Du, Yong}, + date = {2021}, + }"#; + + let library = crate::io::from_biblatex_str(bibtex).unwrap(); + let alphanumeric = hayagriva_archive::ArchivedStyle::Alphanumeric.get(); + let citationberg::Style::Independent(alphanumeric) = alphanumeric else { + unreachable!() + }; + + let mut driver = BibliographyDriver::new(); + for entry in library.iter() { + driver.citation(CitationRequest::new( + vec![CitationItem::with_entry(entry)], + &alphanumeric, + None, + &[], + None, + )); + } + + let finished = driver.finish(BibliographyRequest { + style: &alphanumeric, + locale: None, + locale_files: &[], + }); + + let mut c1 = String::new(); + let mut c2 = String::new(); + + finished.citations[0] + .citation + .write_buf(&mut c1, BufWriteFormat::Plain) + .unwrap(); + finished.citations[1] + .citation + .write_buf(&mut c2, BufWriteFormat::Plain) + .unwrap(); + + assert_eq!(c1, "[Che+21a]"); + assert_eq!(c2, "[Che+21b]"); + } + + #[test] + /// See https://github.com/typst/hayagriva/issues/243 + fn issue_243() { + let bibtex = r#"@book{downs57, + title = {An Economic Theory of Democracy}, + author = {Downs, Anthony}, + date = {1957}, + edition = {1}, + publisher = {Harper \& Row}, + location = {New York}, + langid = {english} + } + + @book{brady_collier10, + title = {Rethinking Social Inquiry. Diverse Tools, Shared Standards}, + editor = {Brady, Henry E. and Collier, David}, + date = {2010}, + edition = {2}, + publisher = {Rowman \& Littlefield Publishers}, + location = {Maryland} + }"#; + + let library = crate::io::from_biblatex_str(bibtex).unwrap(); + let apa = + hayagriva_archive::ArchivedStyle::AmericanPsychologicalAssociation.get(); + let citationberg::Style::Independent(apa) = apa else { unreachable!() }; + + let locales = hayagriva_archive::locales(); + + let mut driver = BibliographyDriver::new(); + for entry in library.iter() { + driver.citation(CitationRequest::new( + vec![CitationItem::new( + entry, + None, + Some(LocaleCode::en_us()), + false, + Some(CitePurpose::Prose), + )], + &apa, + Some(LocaleCode::en_us()), + &locales, + None, + )); + } + + let finished = driver.finish(BibliographyRequest { + style: &apa, + locale: Some(LocaleCode::en_us()), + locale_files: &locales, + }); + + let mut c1 = String::new(); + let mut c2 = String::new(); + + finished.citations[0] + .citation + .write_buf(&mut c1, BufWriteFormat::Plain) + .unwrap(); + finished.citations[1] + .citation + .write_buf(&mut c2, BufWriteFormat::Plain) + .unwrap(); + + assert_eq!(c1, "Downs (1957)"); + assert_eq!(c2, "Brady & Collier (2010)"); + } + + #[test] + /// See https://github.com/typst/hayagriva/issues/48 + fn issue_48() { + let bibtex = r#"@article{chenTransMorphTransformerUnsupervised2021, + title = {{{TransMorph}}: {{Transformer}} for Unsupervised Medical Image Registration}, + author = {Chen, Junyu and Frey, Eric C. and He, Yufan and Segars, William P. and Li, Ye and Du, Yong}, + date = {2021}, + } + + @article{chenViTVNetVisionTransformer2021, + title = {{{ViT-V-Net}}: {{Vision Transformer}} for {{Unsupervised Volumetric Medical Image Registration}}}, + author = {Chen, Junyu and He, Yufan and Frey, Eric C. and Li, Ye and Du, Yong}, + date = {2021}, + }"#; + + let library = crate::io::from_biblatex_str(bibtex).unwrap(); + let alphanumeric = hayagriva_archive::ArchivedStyle::Alphanumeric.get(); + let citationberg::Style::Independent(alphanumeric) = alphanumeric else { + unreachable!() + }; + + let mut driver = BibliographyDriver::new(); + + let locator = SpecificLocator(Locator::Custom, LocatorPayload::Str("12")); + for entry in library.iter() { + driver.citation(CitationRequest::new( + vec![CitationItem::with_locator(entry, Some(locator.clone()))], + &alphanumeric, + None, + &[], + None, + )); + } + + driver.citation(CitationRequest::new( + vec![CitationItem::with_locator(library.iter().next().unwrap(), None)], + &alphanumeric, + None, + &[], + None, + )); + + let finished = driver.finish(BibliographyRequest { + style: &alphanumeric, + locale: None, + locale_files: &[], + }); + + let mut c1 = String::new(); + let mut c2 = String::new(); + let mut c3 = String::new(); + + finished.citations[0] + .citation + .write_buf(&mut c1, BufWriteFormat::Plain) + .unwrap(); + finished.citations[1] + .citation + .write_buf(&mut c2, BufWriteFormat::Plain) + .unwrap(); + finished.citations[2] + .citation + .write_buf(&mut c3, BufWriteFormat::Plain) + .unwrap(); + + assert_eq!(c1, "[Che+21a, 12]"); + assert_eq!(c2, "[Che+21b, 12]"); + assert_eq!(c3, "[Che+21a]"); + } + + #[test] + fn ibid_handling_with_deutsche_sprache_csl() { + let bibtex = r#"@book{ITEM, + title = {A}, + type = {book}, + }"#; + + let library = crate::io::from_biblatex_str(bibtex).unwrap(); + let style = hayagriva_archive::ArchivedStyle::DeutscheSprache.get(); + let citationberg::Style::Independent(style) = style else { unreachable!() }; + + let mut driver = BibliographyDriver::new(); + let entry = library.iter().next().unwrap(); + + for locator in ["33", "33", "34"] { + driver.citation(CitationRequest::new( + vec![CitationItem::new( + entry, + Some(SpecificLocator(Locator::Page, LocatorPayload::Str(locator))), + None, + false, + Some(CitePurpose::Prose), + )], + &style, + None, + &[], + None, + )); + } + + let finished = driver.finish(BibliographyRequest { + style: &style, + locale: None, + locale_files: &[], + }); + + let actual = finished + .citations + .iter() + .map(|c| { + let mut s = String::new(); + c.citation.write_buf(&mut s, BufWriteFormat::Plain).unwrap(); + s + }) + .collect::>(); + + assert_eq!(actual, ["(33)", "()", "(34)"]); + } + + #[test] + #[cfg(feature = "biblatex")] + fn test_issue_227() { + let yaml = r#" +AAAnonymous_AventureMortevielle_1987: + type: Book + page-range: 100"#; + + let library = crate::io::from_yaml_str(yaml).unwrap(); + let entry = library.get("AAAnonymous_AventureMortevielle_1987").unwrap(); + assert_eq!( + entry + .page_range() + .as_ref() + .unwrap() + .as_typed() + .unwrap() + .first() + .unwrap(), + &Numeric::new(100) + ); + } +} diff --git a/crates/hayagriva-core/Cargo.toml b/crates/hayagriva-core/Cargo.toml new file mode 100644 index 00000000..8bace8fc --- /dev/null +++ b/crates/hayagriva-core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "hayagriva-core" +description = "Core data types and utilities for Hayagriva." +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +keywords = ["hayagriva", "bibliography", "citation", "reference", "literature"] + +[dependencies] +serde = { workspace = true } +url = { workspace = true } +thiserror = { workspace = true } +citationberg = { workspace = true } +unscanny = { workspace = true } +roman-numerals-rs = "3.1.0" +unicode-segmentation = { workspace = true } diff --git a/src/lang/en.rs b/crates/hayagriva-core/src/lang/en.rs similarity index 100% rename from src/lang/en.rs rename to crates/hayagriva-core/src/lang/en.rs diff --git a/src/lang/mod.rs b/crates/hayagriva-core/src/lang/mod.rs similarity index 98% rename from src/lang/mod.rs rename to crates/hayagriva-core/src/lang/mod.rs index e0b39bbb..80b09434 100644 --- a/src/lang/mod.rs +++ b/crates/hayagriva-core/src/lang/mod.rs @@ -1,12 +1,14 @@ //! Language-dependant string transformations. -pub(crate) mod en; -pub(crate) mod name; - use std::{fmt::Write, mem}; +use citationberg::TextCase; + use crate::types::{FoldableKind, FoldableStringChunk}; +pub(crate) mod en; +pub(crate) mod name; + /// Rules for the title case transformation. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] @@ -135,6 +137,19 @@ impl From for Case { } } +impl From for Case { + fn from(case: TextCase) -> Self { + match case { + TextCase::Uppercase => Case::Uppercase, + TextCase::Lowercase => Case::Lowercase, + TextCase::TitleCase => Case::Title(TitleCase::default()), + TextCase::SentenceCase => Case::Sentence(SentenceCase::default()), + TextCase::CapitalizeFirst => Case::FirstUpper, + TextCase::CapitalizeAll => Case::AllUpper, + } + } +} + impl From for Case { fn from(props: SentenceCase) -> Self { Self::Sentence(props) @@ -653,7 +668,7 @@ impl CaseFolder { /// Yield the buffer as a mutable string. Must call [`Self::mark_changed`] /// if the length of the string is changed. - pub(crate) fn as_string_mut(&mut self) -> &mut String { + pub fn as_string_mut(&mut self) -> &mut String { &mut self.buf } diff --git a/src/lang/name.rs b/crates/hayagriva-core/src/lang/name.rs similarity index 100% rename from src/lang/name.rs rename to crates/hayagriva-core/src/lang/name.rs diff --git a/crates/hayagriva-core/src/lib.rs b/crates/hayagriva-core/src/lib.rs new file mode 100644 index 00000000..1190f3ca --- /dev/null +++ b/crates/hayagriva-core/src/lib.rs @@ -0,0 +1,49 @@ +//! Hayagriva's core. +//! +//! This crate contains data types, traits, and functions used for the +//! different formats and rendering crates of Hayagriva. + +use std::borrow::Cow; + +use citationberg::LongShortForm; +use citationberg::taxonomy; +use citationberg::taxonomy::{ + DateVariable, NameVariable, NumberVariable, PageVariable, StandardVariable, +}; + +pub use types::*; + +pub mod lang; +pub mod types; +#[doc(hidden)] +pub mod util; + +/// Defines an entry for CSL rendering. Implementing this trait enables +/// interaction between an input format like Hayagriva or CSL-JSON and the CSL +/// rendering implementation. +pub trait EntryLike { + fn resolve_number_variable( + &self, + variable: NumberVariable, + ) -> Option>>; + fn resolve_page_variable( + &self, + variable: PageVariable, + ) -> Option>; + fn resolve_standard_variable( + &self, + form: LongShortForm, + variable: StandardVariable, + ) -> Option>; + fn resolve_name_variable(&self, variable: NameVariable) -> Vec>; + fn resolve_date_variable(&self, variable: DateVariable) -> Option>; + /// `true` if the entry can be interpreted as the given `kind`. As most + /// formats do not have a perfect correspondance, this check is usually + /// "best-effort" and optimistic. + fn matches_entry_type(&self, kind: taxonomy::Kind) -> bool; + /// Returns `None` if the entry's language is unknown, `Some(true)` if it + /// is known to be English, and `Some(false)` otherwise. + fn is_english(&self) -> Option; + /// The entry's key in its containing library. + fn key(&self) -> Cow<'_, str>; +} diff --git a/src/types/mod.rs b/crates/hayagriva-core/src/types/mod.rs similarity index 72% rename from src/types/mod.rs rename to crates/hayagriva-core/src/types/mod.rs index a222a6b8..a37e9437 100644 --- a/src/types/mod.rs +++ b/crates/hayagriva-core/src/types/mod.rs @@ -1,16 +1,13 @@ //! Base types for the bibliography items and their content. use std::borrow::Cow; -use std::collections::BTreeMap; use std::convert::Infallible; use std::fmt::{self, Display}; use std::str::FromStr; use serde::de::value::StrDeserializer; -use serde::ser::SerializeMap; use serde::{Deserialize, Serialize}; use thiserror::Error; -use url::Url; pub use numeric::*; pub use page::*; @@ -25,9 +22,10 @@ mod strings; mod time; /// Use the [`Display`] implementation of a type for serialization. +#[macro_export] macro_rules! serialize_display { ($t:ty) => { - impl Serialize for $t { + impl serde::Serialize for $t { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -39,23 +37,25 @@ macro_rules! serialize_display { } /// Use the [`FromStr`] implementation of a type for deserialization. +#[macro_export] macro_rules! deserialize_from_str { ($t:ty) => { - impl<'de> Deserialize<'de> for $t { + impl<'de> serde::Deserialize<'de> for $t { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = <&'de str>::deserialize(deserializer)?; - FromStr::from_str(s).map_err(serde::de::Error::custom) + std::str::FromStr::from_str(s).map_err(serde::de::Error::custom) } } }; } +#[macro_export] macro_rules! custom_deserialize { ($type_name:ident where $expect:literal $($additional_visitors:item)+) => { - impl<'de> Deserialize<'de> for $type_name { + impl<'de> serde::Deserialize<'de> for $type_name { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -89,6 +89,7 @@ macro_rules! custom_deserialize { /// Use the [`FromStr`] implementation of a type for deserialization if it is a /// string. +#[macro_export] macro_rules! derive_or_from_str { ( $(#[$global:meta])* @@ -110,7 +111,7 @@ macro_rules! derive_or_from_str { } - crate::types::custom_deserialize!( + $crate::types::custom_deserialize!( $s where $expect fn visit_map(self, map: A) -> Result where A: serde::de::MapAccess<'de>, { @@ -132,10 +133,10 @@ macro_rules! derive_or_from_str { }; } -use custom_deserialize; -use derive_or_from_str; -use deserialize_from_str; -use serialize_display; +pub use custom_deserialize; +pub use derive_or_from_str; +pub use deserialize_from_str; +pub use serialize_display; /// Describes which kind of work a database entry refers to. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -254,7 +255,7 @@ pub enum EntryType { impl EntryType { /// Entry parents have implicit defaults. This function returns the default /// parent for this entry type. - pub(crate) fn default_parent(&self) -> Self { + pub fn default_parent(&self) -> Self { match self { Self::Article => Self::Periodical, Self::Chapter => Self::Book, @@ -287,8 +288,6 @@ impl FromStr for EntryType { pub enum DeserializationError { #[error("malformed date")] Date(#[from] time::DateError), - #[error("malformed duration")] - Duration(#[from] time::DurationError), #[error("malformed person")] Person(#[from] persons::PersonError), #[error("malformed numeric value")] @@ -358,7 +357,7 @@ impl MaybeTyped where T: FromStr, { - pub(crate) fn infallible_from_str(s: &str) -> Self { + pub fn infallible_from_str(s: &str) -> Self { match s.parse::() { Ok(t) => MaybeTyped::Typed(t), Err(_) => MaybeTyped::String(s.to_owned()), @@ -383,158 +382,6 @@ impl From for MaybeTyped { } } -derive_or_from_str! { - /// An URL, possibly with a last visited date. - #[derive(Clone, Debug, PartialEq, Eq, Hash)] - pub struct QualifiedUrl where "URL string or dictionary with keys \"url\" and \"date\"" { - /// The [Url]. - pub value: Url, - /// The last visited date. - #[serde(rename = "date")] - pub visit_date: Option, - } -} - -impl Serialize for QualifiedUrl { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - if let Some(date) = &self.visit_date { - let mut map = serializer.serialize_map(Some(2))?; - map.serialize_entry("value", &self.value)?; - map.serialize_entry("date", date)?; - map.end() - } else { - self.value.serialize(serializer) - } - } -} - -impl QualifiedUrl { - /// Create a new qualified URL. - pub fn new(value: Url, visit_date: Option) -> Self { - Self { value, visit_date } - } -} - -impl FromStr for QualifiedUrl { - type Err = url::ParseError; - - fn from_str(s: &str) -> Result { - Ok(Self { value: Url::parse(s)?, visit_date: None }) - } -} - -impl Display for QualifiedUrl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.value.fmt(f) - } -} - -derive_or_from_str! { - /// A publisher, possibly with a location. - #[derive(Clone, Debug, PartialEq, Eq, Hash)] - pub struct Publisher where "FormatString string or dictionary with \"name\" and \"location\"" { - /// Publisher of the item. - name: Option, - /// Physical location at which the item was published or created. - location: Option, - } -} - -impl Serialize for Publisher { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - if let Some(location) = &self.location { - let mut map = serializer.serialize_map(Some(2))?; - map.serialize_entry("name", &self.name)?; - map.serialize_entry("location", location)?; - map.end() - } else { - self.name.serialize(serializer) - } - } -} - -impl Publisher { - /// Create a new publisher. - pub fn new(name: Option, location: Option) -> Self { - Self { name, location } - } - - /// Publisher of the item. - pub fn name(&self) -> Option<&FormatString> { - self.name.as_ref() - } - - /// Physical location at which the item was published or created. - pub fn location(&self) -> Option<&FormatString> { - self.location.as_ref() - } -} - -impl FromStr for Publisher { - type Err = ChunkedStrParseError; - - /// Creates a new publisher with `s` as its name and no location. - fn from_str(s: &str) -> Result { - Ok(Publisher::new(Some(FormatString::from_str(s)?), None)) - } -} - -/// A set of serial numbers like DOIs, ISBNs, or ISSNs. -/// Keys should be lowercase. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] -#[serde(transparent)] -pub struct SerialNumber(pub BTreeMap); - -impl<'de> Deserialize<'de> for SerialNumber { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(untagged)] - enum Choice { - Map(BTreeMap), - Other(StringOrNumber), - } - - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrNumber { - String(String), - Number(i64), - UnsignedNumber(u64), - Float(f64), - } - - impl Display for StringOrNumber { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::String(s) => s.fmt(formatter), - Self::Number(n) => n.fmt(formatter), - Self::UnsignedNumber(n) => n.fmt(formatter), - Self::Float(f) => f.fmt(formatter), - } - } - } - - Choice::deserialize(deserializer).map(|choice| match choice { - Choice::Other(text) => SerialNumber(BTreeMap::from_iter(vec![( - "serial".to_owned(), - text.to_string(), - )])), - Choice::Map(map) => { - SerialNumber(map.into_iter().map(|(k, v)| (k, v.to_string())).collect()) - } - }) - } -} - #[cfg(test)] mod tests { use super::*; @@ -640,27 +487,4 @@ mod tests { ); } } - - #[test] - #[cfg(feature = "biblatex")] - fn test_issue_227() { - let yaml = r#" -AAAnonymous_AventureMortevielle_1987: - type: Book - page-range: 100"#; - - let library = crate::io::from_yaml_str(yaml).unwrap(); - let entry = library.get("AAAnonymous_AventureMortevielle_1987").unwrap(); - assert_eq!( - entry - .page_range - .as_ref() - .unwrap() - .as_typed() - .unwrap() - .first() - .unwrap(), - &Numeric::new(100) - ); - } } diff --git a/src/types/numeric.rs b/crates/hayagriva-core/src/types/numeric.rs similarity index 99% rename from src/types/numeric.rs rename to crates/hayagriva-core/src/types/numeric.rs index ac509323..6b44ae0a 100644 --- a/src/types/numeric.rs +++ b/crates/hayagriva-core/src/types/numeric.rs @@ -233,7 +233,7 @@ impl Numeric { impl MaybeTyped> { /// Order the values according to CSL rules. - pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { + pub fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { match (self, other) { (MaybeTyped::Typed(a), MaybeTyped::Typed(b)) => a.csl_cmp(b), _ => self.to_string().cmp(&other.to_string()), diff --git a/src/types/page.rs b/crates/hayagriva-core/src/types/page.rs similarity index 98% rename from src/types/page.rs rename to crates/hayagriva-core/src/types/page.rs index 4d205b45..bf550fd4 100644 --- a/src/types/page.rs +++ b/crates/hayagriva-core/src/types/page.rs @@ -1,14 +1,17 @@ -use std::{cmp::Ordering, fmt::Display, num::TryFromIntError, str::FromStr}; +use std::cmp::Ordering; +use std::fmt::Display; +use std::num::TryFromIntError; +use std::str::FromStr; -use crate::{MaybeTyped, Numeric, NumericError}; +use thiserror::Error; + +use crate::types::{MaybeTyped, Numeric, NumericError}; use super::{custom_deserialize, serialize_display}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; impl MaybeTyped { /// Order the values according to CSL rules. - pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { + pub fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { match (self, other) { (MaybeTyped::Typed(a), MaybeTyped::Typed(b)) => a.csl_cmp(b), _ => self.to_string().cmp(&other.to_string()), diff --git a/src/types/persons.rs b/crates/hayagriva-core/src/types/persons.rs similarity index 90% rename from src/types/persons.rs rename to crates/hayagriva-core/src/types/persons.rs index 2b7d3723..3e9f53a6 100644 --- a/src/types/persons.rs +++ b/crates/hayagriva-core/src/types/persons.rs @@ -3,88 +3,17 @@ use std::cmp::Ordering; use std::str::FromStr; use citationberg::LongShortForm; +use serde::Serialize; use serde::ser::SerializeMap; -use serde::{Deserialize, Serialize}; use thiserror::Error; use unicode_segmentation::UnicodeSegmentation; use crate::lang::en::ARTICLES; use crate::lang::is_cjk; use crate::lang::name::NAME_PARTICLES; -use crate::util::{deserialize_one_or_many, serialize_one_or_many}; use super::derive_or_from_str; -/// A list of persons with a common role. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct PersonsWithRoles { - /// The persons. - #[serde(serialize_with = "serialize_one_or_many")] - #[serde(deserialize_with = "deserialize_one_or_many")] - pub names: Vec, - /// The role the persons had in the creation of the cited item. - pub role: PersonRole, -} - -impl PersonsWithRoles { - /// Create a new list of persons with a common role. - pub fn new(names: Vec, role: PersonRole) -> Self { - Self { names, role } - } -} - -/// Specifies the role a group of persons had in the creation to the -/// cited item. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -#[non_exhaustive] -#[serde(rename_all = "kebab-case")] -pub enum PersonRole { - /// Translated the work from a foreign language to the cited edition. - Translator, - /// Authored an afterword. - Afterword, - /// Authored an foreword. - Foreword, - /// Authored an introduction. - Introduction, - /// Provided value-adding annotations. - Annotator, - /// Commented the work. - Commentator, - /// Holds a patent or similar. - Holder, - /// Compiled the works in an [Anthology](super::EntryType::Anthology). - Compiler, - /// Founded the publication. - Founder, - /// Collaborated on the cited item. - Collaborator, - /// Organized the creation of the cited item. - Organizer, - /// Performed in the cited item. - CastMember, - /// Composed all or parts of the cited item's musical / audible components. - Composer, - /// Produced the cited item. - Producer, - /// Lead Producer for the cited item. - ExecutiveProducer, - /// Did the writing for the cited item. - Writer, - /// Shot film/video for the cited item. - Cinematography, - /// Directed the cited item. - Director, - /// Illustrated the cited item. - Illustrator, - /// Provided narration or voice-over for the cited item. - Narrator, - - /// Various other roles described by the contained string. - #[serde(skip)] - Unknown(String), -} - derive_or_from_str! { /// Holds the name of a person. #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -502,7 +431,7 @@ impl Person { } /// Order according to the CSL specification. - pub(crate) fn csl_cmp( + pub fn csl_cmp( &self, other: &Self, form: LongShortForm, diff --git a/src/types/strings.rs b/crates/hayagriva-core/src/types/strings.rs similarity index 99% rename from src/types/strings.rs rename to crates/hayagriva-core/src/types/strings.rs index 4829b35c..57260a9f 100644 --- a/src/types/strings.rs +++ b/crates/hayagriva-core/src/types/strings.rs @@ -4,7 +4,9 @@ use std::fmt::Display; use std::str::FromStr; use citationberg::LongShortForm; -use serde::{Deserialize, Serialize, de::Visitor, ser::SerializeMap}; +use serde::de::Visitor; +use serde::ser::SerializeMap; +use serde::{Deserialize, Serialize}; use thiserror::Error; use unscanny::Scanner; diff --git a/src/types/time.rs b/crates/hayagriva-core/src/types/time.rs similarity index 57% rename from src/types/time.rs rename to crates/hayagriva-core/src/types/time.rs index e1a6dce2..380d9081 100644 --- a/src/types/time.rs +++ b/crates/hayagriva-core/src/types/time.rs @@ -1,16 +1,12 @@ -use std::{ - convert::TryInto, - fmt::{Debug, Display, Write}, - str::FromStr, -}; +use std::convert::TryInto; +use std::fmt::{Debug, Display, Write}; +use std::str::FromStr; use citationberg::taxonomy::Season; use serde::{Deserialize, Serialize, de}; use thiserror::Error; use unscanny::Scanner; -use super::{derive_or_from_str, deserialize_from_str, serialize_display}; - /// A date that can be as coarse as a year and as fine-grained as a day. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct Date { @@ -146,7 +142,7 @@ impl PartialOrd for Date { impl Date { /// Order two dates according to the CSL specification. - pub(crate) fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { + pub fn csl_cmp(&self, other: &Self) -> std::cmp::Ordering { let ord_fn = |a: Option, b: Option| match (a, b) { (Some(a), Some(b)) => a.cmp(&b), (Some(_), None) => std::cmp::Ordering::Greater, @@ -333,249 +329,6 @@ impl Serialize for Date { } } -/// A duration. -#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)] -pub struct Duration { - /// Days (24 hours). - pub days: u32, - /// Hours (60 minutes). - pub hours: u32, - /// Minutes (60 seconds). - pub minutes: u32, - /// Seconds (1000 milliseconds). - pub seconds: u8, - /// Milliseconds. - pub milliseconds: u16, -} - -serialize_display!(Duration); -deserialize_from_str!(Duration); - -impl Duration { - fn scan(s: &mut Scanner, require_end: bool) -> Result { - let mut days = 0; - let mut hours = 0; - let mut minutes = 0; - let mut seconds = 0; - let mut milliseconds = 0; - - let idx = s.cursor(); - let colons = - s.eat_while(|c: char| c != '-').chars().filter(|c| c == &':').count(); - s.jump(idx); - - if !(1..=3).contains(&colons) { - return Err(DurationError::Malformed); - } - - let start = 3 - colons; - - for i in start..5 { - match i { - 0 => days = parse_unsigned_int(s, 1..).ok_or(DurationError::Malformed)?, - 1 => { - hours = if start == i { - parse_unsigned_int(s, 2..) - } else { - parse_unsigned_int(s, 2..=2) - } - .ok_or(DurationError::Malformed)? - } - 2 => { - minutes = if start == i { - parse_unsigned_int(s, 2..) - } else { - parse_unsigned_int(s, 2..=2) - } - .ok_or(DurationError::Malformed)? - } - 3 => { - seconds = - parse_unsigned_int(s, 2..=2).ok_or(DurationError::Malformed)? - } - 4 => { - s.eat_whitespace(); - let num = s.eat_while(char::is_numeric); - if num.is_empty() { - return Err(DurationError::Malformed); - } - let str = format!("0.{num}"); - let ms: f64 = str.parse().map_err(|_| DurationError::Malformed)?; - milliseconds = (ms * 1000.0).round() as u16; - } - _ => unreachable!(), - } - - if s.done() { - if i < 3 { - return Err(DurationError::Malformed); - } else { - break; - } - } - - s.eat_whitespace(); - - if i < 3 && !s.eat_if(':') { - return Err(DurationError::Malformed); - } else if i == 3 { - if s.peek() == Some('-') { - break; - } - if !s.eat_if(",") { - return Err(DurationError::Malformed); - } - } - } - - s.eat_whitespace(); - if require_end && !s.done() { - return Err(DurationError::Malformed); - } - - for i in (0..=start).rev() { - match i { - 0 => {} - 1 => { - days = hours / 24; - hours %= 24; - } - 2 => { - hours += minutes / 60; - minutes %= 60; - } - _ => unreachable!(), - } - } - - if hours >= 24 || minutes >= 60 || seconds >= 60 || milliseconds >= 1000 { - return Err(DurationError::TooLarge); - } - - Ok(Duration { days, hours, minutes, seconds, milliseconds }) - } - - /// Get the duration in milliseconds. - fn milliseconds(self) -> u64 { - self.milliseconds as u64 - + self.seconds as u64 * 1000 - + self.minutes as u64 * 60 * 1000 - + self.hours as u64 * 60 * 60 * 1000 - + self.days as u64 * 24 * 60 * 60 * 1000 - } -} - -impl PartialOrd for Duration { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Duration { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.milliseconds().cmp(&other.milliseconds()) - } -} - -/// Errors that can occur when parsing a string to a duration -#[derive(Clone, Copy, Error, Debug, PartialEq, Eq, Hash)] -pub enum DurationError { - /// The string is malformed. - #[error("duration string malformed")] - Malformed, - /// The value is out of bounds when another, subsequent value is present (i.e. `01:61:48`). - #[error("out of bounds value when greater order value is specified")] - TooLarge, -} - -impl FromStr for Duration { - type Err = DurationError; - - fn from_str(s: &str) -> Result { - let mut s = Scanner::new(s); - Self::scan(&mut s, true) - } -} - -impl Display for Duration { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.days > 0 { - write!(f, "{}:", self.days)?; - } - - if self.hours > 0 { - write!(f, "{:02}:", self.hours)?; - } - - write!(f, "{:02}:{:02}", self.minutes, self.seconds)?; - - if self.milliseconds > 0 { - write!(f, ",{:03}", self.milliseconds)?; - } - - Ok(()) - } -} - -derive_or_from_str! { - /// An half-open interval of durations. - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] - pub struct DurationRange where "two durations separated by a hyphen or a map with a `from` and `to` field" { - /// The start of the interval. - pub start: Duration, - /// The end of the interval. - pub end: Duration, - } -} - -impl DurationRange { - /// Create a new duration range. - pub fn new(start: Duration, end: Option) -> Self { - Self { start, end: end.unwrap_or(start) } - } - - fn scan(s: &mut Scanner) -> Result { - let start = Duration::scan(s, false)?; - let hyphens = s.eat_while('-'); - - if hyphens.is_empty() { - s.eat_whitespace(); - - if s.done() { - return Ok(Self::new(start, None)); - } else { - return Err(DurationError::Malformed); - } - } - - let end = Duration::scan(s, true)?; - - Ok(Self { start, end }) - } -} - -impl Display for DurationRange { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.start)?; - if self.start != self.end { - write!(f, "-{}", self.end)?; - } - - Ok(()) - } -} - -impl FromStr for DurationRange { - type Err = DurationError; - - fn from_str(s: &str) -> Result { - let mut s = Scanner::new(s); - Self::scan(&mut s) - } -} - -serialize_display!(DurationRange); - fn parse_int(s: &mut Scanner, digits: R) -> Option where R: std::ops::RangeBounds, @@ -595,7 +348,7 @@ where Some(num) } -fn parse_unsigned_int(s: &mut Scanner, digits: R) -> Option +pub fn parse_unsigned_int(s: &mut Scanner, digits: R) -> Option where T: FromStr + Ord + Debug, ::Err: Debug, @@ -700,90 +453,3 @@ fn days_in_month(month: u8, year: i32) -> u8 { 30 + month % 2 } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_duration_parse() { - assert_eq!( - Duration::from_str("01:00").unwrap(), - Duration { - days: 0, - hours: 0, - minutes: 1, - seconds: 0, - milliseconds: 0, - } - ); - assert_eq!( - Duration::from_str("00:01:00").unwrap(), - Duration { - days: 0, - hours: 0, - minutes: 1, - seconds: 0, - milliseconds: 0, - } - ); - assert_eq!( - Duration::from_str("00 : 00 : 01 : 00").unwrap(), - Duration { - days: 0, - hours: 0, - minutes: 1, - seconds: 0, - milliseconds: 0, - } - ); - assert_eq!( - Duration::from_str("01:00,000").unwrap(), - Duration { - days: 0, - hours: 0, - minutes: 1, - seconds: 0, - milliseconds: 0, - } - ); - assert!(Duration::from_str("01:00,").is_err()); - assert!(Duration::from_str("010:00,").is_err()); - } - - #[test] - fn test_duration_range_parse() { - assert_eq!( - DurationRange::from_str("01:00").unwrap(), - DurationRange::new( - Duration { - days: 0, - hours: 0, - minutes: 1, - seconds: 0, - milliseconds: 0, - }, - None - ) - ); - assert_eq!( - DurationRange::from_str("01:00-02:00").unwrap(), - DurationRange::new( - Duration { - days: 0, - hours: 0, - minutes: 1, - seconds: 0, - milliseconds: 0, - }, - Some(Duration { - days: 0, - hours: 0, - minutes: 2, - seconds: 0, - milliseconds: 0, - }) - ) - ); - } -} diff --git a/src/util.rs b/crates/hayagriva-core/src/util.rs similarity index 97% rename from src/util.rs rename to crates/hayagriva-core/src/util.rs index 772fdfbf..1b3096dc 100644 --- a/src/util.rs +++ b/crates/hayagriva-core/src/util.rs @@ -1,6 +1,7 @@ //! Helpers for serializing and deserializing. -use serde::{Deserialize, Deserializer, Serialize, de::Visitor}; +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize}; /// Generic wrapper that allow one or more occurrences of specified type. /// @@ -82,7 +83,7 @@ where /// This is a wrapper for [`OneOrMany`] that assumes that the single /// representation isn't a sequence. This allows better error messages. #[derive(Clone, Debug, PartialEq, Eq)] -struct MapOneOrMany(OneOrMany); +pub struct MapOneOrMany(OneOrMany); impl From> for OneOrMany { fn from(from: MapOneOrMany) -> Self { diff --git a/crates/hayagriva-csl-json/Cargo.toml b/crates/hayagriva-csl-json/Cargo.toml new file mode 100644 index 00000000..a072ecdf --- /dev/null +++ b/crates/hayagriva-csl-json/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "hayagriva-csl-json" +description = "CSL-JSON handling for Hayagriva." +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +keywords = [ + "hayagriva", + "bibliography", + "citation", + "reference", + "literature", + "json", +] + +[dependencies] +citationberg = { workspace = true, features = ["json"] } +hayagriva-core = { workspace = true } +serde = { workspace = true, features = ["derive"] } diff --git a/crates/hayagriva-csl-json/src/lib.rs b/crates/hayagriva-csl-json/src/lib.rs new file mode 100644 index 00000000..eddfe5ca --- /dev/null +++ b/crates/hayagriva-csl-json/src/lib.rs @@ -0,0 +1,224 @@ +//! Hayagriva CSL-JSON handling. +//! +//! This crate contains a wrapper around [citationberg]'s [csl_json::Item] and +//! handles the resolution of CSL variables from [Item]. + +use std::{borrow::Cow, str::FromStr}; + +use citationberg::taxonomy::{ + DateVariable, Kind, NameVariable, NumberVariable, PageVariable, StandardVariable, +}; +use citationberg::{LongShortForm, json as csl_json}; +use serde::{Deserialize, Serialize}; + +use hayagriva_core::{ + ChunkedString, Date, EntryLike, MaybeTyped, Numeric, PageRanges, Person, StringChunk, +}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(transparent)] +/// A CSL-JSON item. It is a wrapper around a [csl_json::Item]. +pub struct Item(pub csl_json::Item); + +fn resolve_csl_json_standard_variable( + item: &Item, + variable: StandardVariable, +) -> Option> { + match item.0.0.get(&variable.to_string())? { + csl_json::Value::String(s) => { + Some(Cow::Owned(StringChunk::normal(s.clone()).into())) + } + csl_json::Value::Number(n) => { + Some(Cow::Owned(StringChunk::normal(n.to_string()).into())) + } + _ => None, + } +} + +impl EntryLike for Item { + fn resolve_standard_variable( + &self, + form: LongShortForm, + variable: StandardVariable, + ) -> Option> { + match variable { + StandardVariable::Title => match form { + LongShortForm::Short => { + // Per citeproc tests, a 'title-short' without 'title' is + // valid and should be used when the short form is + // selected. + resolve_csl_json_standard_variable(self, StandardVariable::TitleShort) + .or_else(|| { + resolve_csl_json_standard_variable( + self, + StandardVariable::Title, + ) + }) + } + LongShortForm::Long => { + resolve_csl_json_standard_variable(self, StandardVariable::Title) + } + }, + _ => resolve_csl_json_standard_variable(self, variable), + } + } + + fn resolve_date_variable(&self, variable: DateVariable) -> Option> { + match self.0.0.get(&variable.to_string())? { + csl_json::Value::Date(d) => { + let Ok(d) = csl_json::FixedDateRange::try_from(d.clone()) else { + return None; + }; + if d.end.is_some() { + panic!("ranges are not supported") + } + let d = d.start; + Some(Cow::Owned(Date { + year: d.year as i32, + month: d.month, + day: d.day, + approximate: d.circa, + season: d.season, + })) + } + _ => None, + } + } + + fn resolve_name_variable(&self, variable: NameVariable) -> Vec> { + match self.0.0.get(&variable.to_string()) { + Some(csl_json::Value::Names(names)) => names + .iter() + .filter_map(|name| { + Some(Cow::Owned(match name { + csl_json::NameValue::Literal(l) => Person { + name: l.literal.clone(), + prefix: None, + suffix: None, + given_name: None, + alias: None, + comma_suffix: false, + }, + csl_json::NameValue::Item(csl_json::NameItem { + family, + given, + non_dropping_particle: None, + dropping_particle: None, + suffix, + comma_suffix, + }) => { + let mut parts = vec![family.as_str()]; + if let Some(given) = given { + parts.push(given.as_str()); + } + let mut p = Person::from_strings(parts).ok()?; + if let Some(suffix) = suffix { + p.suffix = Some(suffix.as_str().to_owned()); + } + if comma_suffix.unwrap_or_default() { + p.comma_suffix = true; + } + + p + } + csl_json::NameValue::Item(csl_json::NameItem { + family, + given, + non_dropping_particle, + dropping_particle, + suffix, + comma_suffix, + }) => Person { + name: if let Some(non_drop) = non_dropping_particle { + format!("{non_drop} {family}") + } else { + family.clone() + }, + prefix: dropping_particle.clone(), + suffix: suffix.clone(), + given_name: given.clone(), + alias: None, + comma_suffix: comma_suffix.unwrap_or_default(), + }, + })) + }) + .collect(), + _ => vec![], + } + } + + fn resolve_page_variable( + &self, + variable: PageVariable, + ) -> Option> { + match variable { + PageVariable::Page => match self.0.0.get("page")? { + &csl_json::Value::Number(n) => { + // Page ranges use i32 internally, so we check whether the + // number is in range. + Some(match i32::try_from(n) { + Ok(n) => MaybeTyped::Typed(PageRanges::from(n)), + // If the number is not in range, we degrade to a + // string, which disables some CSL features. + Err(_) => MaybeTyped::String(n.to_string()), + }) + } + csl_json::Value::String(s) => { + let res = MaybeTyped::::infallible_from_str(s); + Some(match res { + MaybeTyped::String(s) => MaybeTyped::String(s), + MaybeTyped::Typed(r) => MaybeTyped::Typed(r), + }) + } + _ => None, + }, + } + } + + fn resolve_number_variable( + &self, + variable: NumberVariable, + ) -> Option>> { + if matches!(variable, NumberVariable::PageFirst) + && let Some(MaybeTyped::Typed(n)) = + self.resolve_page_variable(PageVariable::Page) + { + return n.first().map(|r| MaybeTyped::Typed(Cow::Owned(r.clone()))); + } + match self.0.0.get(&variable.to_string())? { + csl_json::Value::Number(n) => { + Some(MaybeTyped::Typed(Cow::Owned(Numeric::from(*n as u32)))) + } + csl_json::Value::String(s) => { + let res = MaybeTyped::::infallible_from_str(s); + Some(match res { + MaybeTyped::String(s) => MaybeTyped::String(s), + MaybeTyped::Typed(n) => MaybeTyped::Typed(Cow::Owned(n)), + }) + } + _ => None, + } + } + + fn matches_entry_type(&self, kind: Kind) -> bool { + let Some(actual) = self.0.0.get("type") else { + return false; + }; + let Some(string) = actual.to_str() else { + return false; + }; + Kind::from_str(&string) == Ok(kind) + } + + fn is_english(&self) -> Option { + self.0 + .0 + .get("language") + .and_then(|l| l.to_str()) + .map(|l| l.starts_with("en")) + } + + fn key(&self) -> Cow<'_, str> { + self.0.id().unwrap_or_default() + } +} diff --git a/crates/hayagriva-csl/Cargo.toml b/crates/hayagriva-csl/Cargo.toml new file mode 100644 index 00000000..ae88439d --- /dev/null +++ b/crates/hayagriva-csl/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "hayagriva-csl" +description = "Citation Style Language procesor and renderer for Hayagriva." +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +keywords = [ + "hayagriva", + "bibliography", + "citation", + "reference", + "literature", + "rendering", +] + +[dependencies] +citationberg = { workspace = true } +indexmap = { workspace = true } +icu_collator = "2.2.0" +icu_locale = "2.2.0" +hayagriva-core = { workspace = true } +unic-langid = { workspace = true } + +[dev-dependencies] +hayagriva-format = { workspace = true } diff --git a/src/csl/elem.rs b/crates/hayagriva-csl/src/elem.rs similarity index 99% rename from src/csl/elem.rs rename to crates/hayagriva-csl/src/elem.rs index 84670eb8..459840cf 100644 --- a/src/csl/elem.rs +++ b/crates/hayagriva-csl/src/elem.rs @@ -7,7 +7,7 @@ use citationberg::{ Display, FontStyle, FontVariant, FontWeight, TextDecoration, VerticalAlign, }; -use crate::types::Person; +use hayagriva_core::Person; /// A container for elements with useful methods. #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/src/csl/mod.rs b/crates/hayagriva-csl/src/lib.rs similarity index 87% rename from src/csl/mod.rs rename to crates/hayagriva-csl/src/lib.rs index 6221859e..952566c7 100644 --- a/src/csl/mod.rs +++ b/crates/hayagriva-csl/src/lib.rs @@ -1,3 +1,62 @@ +//! Hayagriva CSL rendering +//! +//! # Usage +//! +//! We show a simple example using the Hayagriva format but the usage is similar for CSL-JSON. +//! +//! ```rust +//! use hayagriva_format::io::from_yaml_str; +//! +//! let yaml = r#" +//! crazy-rich: +//! type: Book +//! title: Crazy Rich Asians +//! author: Kwan, Kevin +//! date: 2014 +//! publisher: Anchor Books +//! location: New York, NY, US +//! "#; +//! +//! // Parse a bibliography +//! let bib = from_yaml_str(yaml).unwrap(); +//! assert_eq!(bib.get("crazy-rich").unwrap().date().unwrap().year, 2014); +//! +//! // Format the reference +//! use std::fs; +//! use hayagriva_csl::{ +//! BibliographyDriver, BibliographyRequest, BufWriteFormat, +//! CitationItem, CitationRequest, +//! }; +//! use citationberg::{LocaleFile, IndependentStyle}; +//! +//! let en_locale = fs::read_to_string("../../tests/data/locales-en-US.xml").unwrap(); +//! let locales = [LocaleFile::from_xml(&en_locale).unwrap().into()]; +//! +//! let style = fs::read_to_string("../../tests/data/art-history.csl").unwrap(); +//! let style = IndependentStyle::from_xml(&style).unwrap(); +//! +//! let mut driver = BibliographyDriver::new(); +//! +//! for entry in bib.iter() { +//! let items = vec![CitationItem::with_entry(entry)]; +//! driver.citation(CitationRequest::from_items(items, &style, &locales)); +//! } +//! +//! let result = driver.finish(BibliographyRequest { +//! style: &style, +//! locale: None, +//! locale_files: &locales, +//! }); +//! +//! for cite in result.citations { +//! println!("{}", cite.citation) +//! } +//! ``` +//! +//! To format entries, you need to wrap them in a [`CitationRequest`]. Each of these +//! can reference multiple entries in their respective [`CitationItem`]s. +//! Use these with a [`BibliographyDriver`] to obtain formatted citations and bibliographies. + use std::any::Any; use std::borrow::Cow; use std::cell::RefCell; @@ -14,31 +73,29 @@ use citationberg::taxonomy::{ PageVariable, StandardVariable, Term, Variable, }; use citationberg::{ - Affixes, BaseLanguage, Citation, CitationFormat, Collapse, CslMacro, + Affixes, BaseLanguage, Citation, CitationFormat, Collapse, CslMacro, DateForm, DisambiguationRule, Display, GrammarGender, IndependentStyle, InheritableNameOptions, - Layout, LayoutRenderingElement, Locale, LocaleCode, Names, SecondFieldAlign, - StyleCategory, StyleClass, SubsequentAuthorSubstituteRule, TermForm, ToAffixes, - ToFormatting, taxonomy as csl_taxonomy, + Layout, LayoutRenderingElement, Locale, LocaleCode, LongShortForm, Names, + OrdinalLookup, SecondFieldAlign, StyleCategory, StyleClass, + SubsequentAuthorSubstituteRule, TermForm, TextCase, ToAffixes, ToFormatting, + taxonomy as csl_taxonomy, }; -use citationberg::{DateForm, LongShortForm, OrdinalLookup, TextCase}; use indexmap::IndexSet; -use crate::csl::elem::{NonEmptyStack, simplify_children}; -use crate::csl::rendering::RenderCsl; -use crate::csl::rendering::names::{DisambiguatedNameForm, NameDisambiguationProperties}; -use crate::csl::taxonomy::NumberOrPageVariableResult; -use crate::lang::CaseFolder; -use crate::types::{ChunkKind, ChunkedString, Date, MaybeTyped, Person}; +use hayagriva_core::lang::CaseFolder; +use hayagriva_core::{ChunkKind, ChunkedString, Date, EntryLike, MaybeTyped, Person}; + +use crate::elem::{NonEmptyStack, simplify_children}; +use crate::rendering::RenderCsl; +use crate::rendering::names::{DisambiguatedNameForm, NameDisambiguationProperties}; +use crate::taxonomy::NumberOrPageVariableResult; use self::elem::last_text_mut_child; pub use self::elem::{ BufWriteFormat, Elem, ElemChild, ElemChildren, ElemMeta, Formatted, Formatting, }; -use self::taxonomy::{EntryLike, NumberVariableResult, PageVariableResult}; +use self::taxonomy::{NumberVariableResult, PageVariableResult}; -#[cfg(feature = "archive")] -pub mod archive; -mod citation_label; mod elem; mod rendering; mod sort; @@ -678,7 +735,7 @@ fn date_replacement( })]) } -pub fn write_year( +pub(crate) fn write_year( year: i32, short: bool, w: &mut W, @@ -1543,7 +1600,7 @@ pub struct RenderedCitation { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Sorting { +enum Sorting { Variable, Macro, } @@ -2012,7 +2069,7 @@ impl<'a> BibliographyRequest<'a> { } } -/// A reference to an [`crate::Entry`] within a [`CitationRequest`]. +/// A reference to an [`EntryLike`] within a [`CitationRequest`]. #[derive(Debug, Clone, PartialEq)] pub struct CitationItem<'a, T: EntryLike> { /// The entry to format. @@ -2647,7 +2704,7 @@ impl IdentifierUsage { } #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] -pub enum DisambiguateState { +enum DisambiguateState { #[default] None, NameDisambiguation(NameDisambiguationProperties), @@ -2689,7 +2746,7 @@ impl DisambiguateState { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum IbidState { +enum IbidState { /// The previous cite referenced another entry, or this cite has no locator /// when the previous one did. Different, @@ -2745,7 +2802,7 @@ pub enum LocatorPayload<'a> { /// Wraps an object that is used to determine whether two locators are the same. #[derive(Clone)] -pub struct TransparentLocator(Arc); +pub struct TransparentLocator(pub Arc); impl TransparentLocator { /// Creates a new [`TransparentLocator`] which wraps the given payload. @@ -2766,7 +2823,11 @@ impl PartialEq for TransparentLocator { } } -trait TransparentLocatorPayload: Any { +/// Content of the "transparent" locator. It has to privide a way to check +/// for equality with other transparent locators for the "ibid" usage. +pub trait TransparentLocatorPayload: Any { + /// Returns `true` iff a locator with this payload is considered equal to + /// the locator with payload `other`. fn dyn_eq(&self, other: &dyn TransparentLocatorPayload) -> bool; } @@ -3420,137 +3481,7 @@ fn get_last_text(child: &mut ElemChild) -> Option<&mut String> { #[cfg(test)] mod tests { - use std::{fs, path::Path}; - - use citationberg::LocaleFile; - use super::*; - use crate::{ - io::from_yaml_str, - types::{Numeric, NumericValue}, - }; - - #[test] - fn test_csl() { - let workspace = Path::new(env!("CARGO_MANIFEST_DIR")); - let en_locale = - fs::read_to_string(workspace.join("tests/data/locales-en-US.xml")).unwrap(); - let en_locale = LocaleFile::from_xml(&en_locale).unwrap(); - - let yaml = fs::read_to_string(workspace.join("tests/data/basic.yml")).unwrap(); - let bib = from_yaml_str(&yaml).unwrap(); - let en_locale = [en_locale.into()]; - - for style_thing in fs::read_dir(workspace.join("styles/")).unwrap().take(100) { - let thing = style_thing.unwrap(); - if thing.file_type().unwrap().is_dir() { - continue; - } - - let path = thing.path(); - let extension = path.extension(); - if let Some(extension) = extension { - if extension.to_str() != Some("csl") { - continue; - } - } else { - continue; - } - - println!("testing {path:?}"); - let style = fs::read_to_string(path).unwrap(); - let style = IndependentStyle::from_xml(&style).unwrap(); - let mut driver = BibliographyDriver::new(); - - for n in (0..bib.len()).step_by(2) { - let items = vec![ - CitationItem::with_entry(bib.nth(n).unwrap()), - CitationItem::with_entry(bib.nth(n + 1).unwrap()), - ]; - driver.citation(CitationRequest::new( - items, &style, None, &en_locale, None, - )); - } - - driver.finish(BibliographyRequest { - style: &style, - locale: None, - locale_files: &en_locale, - }); - - // for cite in finished.citations { - // println!("{}", cite.citation.to_string(BufWriteFormat::Plain)) - // } - - // if let Some(bib) = finished.bibliography { - // for (prefix, item) in bib.items { - // if let Some(prefix) = prefix { - // print!("{} ", prefix.to_string(BufWriteFormat::Plain)) - // } - // println!("{}", item.to_string(BufWriteFormat::Html)) - // } - // } - } - } - - #[test] - fn test_chapter_field() { - let workspace = Path::new(env!("CARGO_MANIFEST_DIR")); - let yaml = fs::read_to_string(workspace.join("tests/data/basic.yml")).unwrap(); - let bib = from_yaml_str(&yaml).unwrap(); - let book = bib.get("lamb").unwrap(); - assert_eq!(book.chapter().unwrap(), &MaybeTyped::Typed(Numeric::new(20))); - assert_eq!(book.volume().unwrap(), &MaybeTyped::Typed(Numeric::new(10))); - assert_eq!(book.keyed_serial_number("serial"), None); - assert_eq!(book.resolve_number_variable(NumberVariable::Number), None); - - let book = bib.get("snail").unwrap(); - assert_eq!( - book.chapter().unwrap(), - &MaybeTyped::Typed(Numeric { - value: NumericValue::Number(2), - prefix: None, - suffix: Some(Box::new("A".into())) - }) - ); - assert_eq!( - book.volume().unwrap(), - &MaybeTyped::Typed(Numeric { - value: NumericValue::Number(4), - prefix: None, - suffix: Some(Box::new("B".into())) - }) - ); - assert_eq!(book.keyed_serial_number("serial"), None); - assert_eq!(book.resolve_number_variable(NumberVariable::Number), None); - - let chapter = bib.get("lamb-chapter").unwrap(); - assert_eq!(chapter.chapter().unwrap(), &MaybeTyped::Typed(Numeric::new(3))); - assert_eq!(chapter.keyed_serial_number("serial"), None); - assert_eq!( - chapter.resolve_number_variable(NumberVariable::Number), - Some(MaybeTyped::Typed(Cow::Borrowed(&Numeric::new(3)))) - ); - assert_eq!(chapter.volume(), None); - assert_eq!( - chapter.parents().first().unwrap().volume().unwrap(), - &MaybeTyped::Typed(Numeric::new(10)) - ); - - let chapter = bib.get("snail-chapter").unwrap(); - assert_eq!(chapter.chapter(), None); - assert_eq!(chapter.keyed_serial_number("serial").unwrap(), "3"); - - assert_eq!(chapter.volume(), None); - assert_eq!( - chapter.parents().first().unwrap().volume().unwrap(), - &MaybeTyped::Typed(Numeric { - value: NumericValue::Number(4), - prefix: None, - suffix: Some(Box::new("B".into())) - }) - ); - } #[test] fn low_year_test() { @@ -3569,353 +3500,4 @@ mod tests { assert_eq!(yield_year(-1, true), "2BC"); assert_eq!(yield_year(1, true), "1AD"); } - - #[test] - fn test_transparent_locator_ibid() { - let test_data = r#" - katalog: - type: Book - title: "Book" - author: "Surname, Name" - location: Warsaw - date: 1971 - katalog2: - type: Book - title: "Book2" - author: "Surname, Name" - location: Warsaw - date: 1971 - "#; - let test_style = r#" - "#; - - let library = from_yaml_str(test_data).unwrap(); - let style = citationberg::Style::from_xml(test_style).unwrap(); - let citationberg::Style::Independent(style) = style else { unreachable!() }; - - let mut driver = BibliographyDriver::new(); - let citations = [ - ("katalog", Some([2, 3]), "first."), - ("katalog", Some([2, 3]), "ibid."), - ("katalog", Some([2, 3]), "ibid."), - ("katalog", Some([2, 4]), "ibid-with-locator."), - ("katalog", None, "subsequent."), - ("katalog", Some([2, 3]), "ibid-with-locator."), - ("katalog", Some([2, 3]), "ibid."), - ("katalog2", Some([2, 3]), "first."), - ]; - - for (entry, supplement, _) in citations { - let entry = library.get(entry).unwrap(); - driver.citation(CitationRequest::new( - vec![CitationItem::with_locator( - entry, - supplement.map(|s| { - SpecificLocator( - Locator::Supplement, - LocatorPayload::Transparent(TransparentLocator(Arc::new(s))), - ) - }), - )], - &style, - None, - &[], - None, - )); - } - - let finished = driver.finish(BibliographyRequest { - style: &style, - locale: None, - locale_files: &[], - }); - - for (citation, (_, _, expected_value)) in finished.citations.iter().zip(citations) - { - let mut s = String::new(); - citation.citation.write_buf(&mut s, BufWriteFormat::Plain).unwrap(); - assert_eq!(s, expected_value); - } - } - - #[test] - #[cfg(feature = "archive")] - fn test_alphanumeric_disambiguation() { - let bibtex = r#"@article{chenTransMorphTransformerUnsupervised2021, - title = {{{TransMorph}}: {{Transformer}} for Unsupervised Medical Image Registration}, - author = {Chen, Junyu and Frey, Eric C. and He, Yufan and Segars, William P. and Li, Ye and Du, Yong}, - date = {2021}, - } - - @article{chenViTVNetVisionTransformer2021, - title = {{{ViT-V-Net}}: {{Vision Transformer}} for {{Unsupervised Volumetric Medical Image Registration}}}, - author = {Chen, Junyu and He, Yufan and Frey, Eric C. and Li, Ye and Du, Yong}, - date = {2021}, - }"#; - - let library = crate::io::from_biblatex_str(bibtex).unwrap(); - let alphanumeric = archive::ArchivedStyle::Alphanumeric.get(); - let citationberg::Style::Independent(alphanumeric) = alphanumeric else { - unreachable!() - }; - - let mut driver = BibliographyDriver::new(); - for entry in library.iter() { - driver.citation(CitationRequest::new( - vec![CitationItem::with_entry(entry)], - &alphanumeric, - None, - &[], - None, - )); - } - - let finished = driver.finish(BibliographyRequest { - style: &alphanumeric, - locale: None, - locale_files: &[], - }); - - let mut c1 = String::new(); - let mut c2 = String::new(); - - finished.citations[0] - .citation - .write_buf(&mut c1, BufWriteFormat::Plain) - .unwrap(); - finished.citations[1] - .citation - .write_buf(&mut c2, BufWriteFormat::Plain) - .unwrap(); - - assert_eq!(c1, "[Che+21a]"); - assert_eq!(c2, "[Che+21b]"); - } - - #[test] - #[cfg(feature = "archive")] - /// See https://github.com/typst/hayagriva/issues/243 - fn issue_243() { - let bibtex = r#"@book{downs57, - title = {An Economic Theory of Democracy}, - author = {Downs, Anthony}, - date = {1957}, - edition = {1}, - publisher = {Harper \& Row}, - location = {New York}, - langid = {english} - } - - @book{brady_collier10, - title = {Rethinking Social Inquiry. Diverse Tools, Shared Standards}, - editor = {Brady, Henry E. and Collier, David}, - date = {2010}, - edition = {2}, - publisher = {Rowman \& Littlefield Publishers}, - location = {Maryland} - }"#; - - let library = crate::io::from_biblatex_str(bibtex).unwrap(); - let apa = archive::ArchivedStyle::AmericanPsychologicalAssociation.get(); - let citationberg::Style::Independent(apa) = apa else { unreachable!() }; - - let locales = archive::locales(); - - let mut driver = BibliographyDriver::new(); - for entry in library.iter() { - driver.citation(CitationRequest::new( - vec![CitationItem::new( - entry, - None, - Some(LocaleCode::en_us()), - false, - Some(CitePurpose::Prose), - )], - &apa, - Some(LocaleCode::en_us()), - &locales, - None, - )); - } - - let finished = driver.finish(BibliographyRequest { - style: &apa, - locale: Some(LocaleCode::en_us()), - locale_files: &locales, - }); - - let mut c1 = String::new(); - let mut c2 = String::new(); - - finished.citations[0] - .citation - .write_buf(&mut c1, BufWriteFormat::Plain) - .unwrap(); - finished.citations[1] - .citation - .write_buf(&mut c2, BufWriteFormat::Plain) - .unwrap(); - - assert_eq!(c1, "Downs (1957)"); - assert_eq!(c2, "Brady & Collier (2010)"); - } - #[test] - #[cfg(feature = "archive")] - /// See https://github.com/typst/hayagriva/issues/48 - fn issue_48() { - let bibtex = r#"@article{chenTransMorphTransformerUnsupervised2021, - title = {{{TransMorph}}: {{Transformer}} for Unsupervised Medical Image Registration}, - author = {Chen, Junyu and Frey, Eric C. and He, Yufan and Segars, William P. and Li, Ye and Du, Yong}, - date = {2021}, - } - - @article{chenViTVNetVisionTransformer2021, - title = {{{ViT-V-Net}}: {{Vision Transformer}} for {{Unsupervised Volumetric Medical Image Registration}}}, - author = {Chen, Junyu and He, Yufan and Frey, Eric C. and Li, Ye and Du, Yong}, - date = {2021}, - }"#; - - let library = crate::io::from_biblatex_str(bibtex).unwrap(); - let alphanumeric = archive::ArchivedStyle::Alphanumeric.get(); - let citationberg::Style::Independent(alphanumeric) = alphanumeric else { - unreachable!() - }; - - let mut driver = BibliographyDriver::new(); - - let locator = SpecificLocator(Locator::Custom, LocatorPayload::Str("12")); - for entry in library.iter() { - driver.citation(CitationRequest::new( - vec![CitationItem::with_locator(entry, Some(locator.clone()))], - &alphanumeric, - None, - &[], - None, - )); - } - - driver.citation(CitationRequest::new( - vec![CitationItem::with_locator(library.iter().next().unwrap(), None)], - &alphanumeric, - None, - &[], - None, - )); - - let finished = driver.finish(BibliographyRequest { - style: &alphanumeric, - locale: None, - locale_files: &[], - }); - - let mut c1 = String::new(); - let mut c2 = String::new(); - let mut c3 = String::new(); - - finished.citations[0] - .citation - .write_buf(&mut c1, BufWriteFormat::Plain) - .unwrap(); - finished.citations[1] - .citation - .write_buf(&mut c2, BufWriteFormat::Plain) - .unwrap(); - finished.citations[2] - .citation - .write_buf(&mut c3, BufWriteFormat::Plain) - .unwrap(); - - assert_eq!(c1, "[Che+21a, 12]"); - assert_eq!(c2, "[Che+21b, 12]"); - assert_eq!(c3, "[Che+21a]"); - } - - #[test] - #[cfg(feature = "archive")] - fn ibid_handling_with_deutsche_sprache_csl() { - let bibtex = r#"@book{ITEM, - title = {A}, - type = {book}, - }"#; - - let library = crate::io::from_biblatex_str(bibtex).unwrap(); - let style = archive::ArchivedStyle::DeutscheSprache.get(); - let citationberg::Style::Independent(style) = style else { unreachable!() }; - - let mut driver = BibliographyDriver::new(); - let entry = library.iter().next().unwrap(); - - for locator in ["33", "33", "34"] { - driver.citation(CitationRequest::new( - vec![CitationItem::new( - entry, - Some(SpecificLocator(Locator::Page, LocatorPayload::Str(locator))), - None, - false, - Some(CitePurpose::Prose), - )], - &style, - None, - &[], - None, - )); - } - - let finished = driver.finish(BibliographyRequest { - style: &style, - locale: None, - locale_files: &[], - }); - - let actual = finished - .citations - .iter() - .map(|c| { - let mut s = String::new(); - c.citation.write_buf(&mut s, BufWriteFormat::Plain).unwrap(); - s - }) - .collect::>(); - - assert_eq!(actual, ["(33)", "()", "(34)"]); - } } diff --git a/src/csl/rendering/mod.rs b/crates/hayagriva-csl/src/rendering/mod.rs similarity index 97% rename from src/csl/rendering/mod.rs rename to crates/hayagriva-csl/src/rendering/mod.rs index a2ed9f06..084c82fc 100644 --- a/src/csl/rendering/mod.rs +++ b/crates/hayagriva-csl/src/rendering/mod.rs @@ -9,18 +9,18 @@ use citationberg::taxonomy::{ use citationberg::{ ChooseBranch, CslMacro, DateDayForm, DateMonthForm, DatePartName, DateParts, DateStrongAnyForm, GrammarGender, LabelPluralize, LayoutRenderingElement, - LongShortForm, NumberForm, PageRangeFormat, TestPosition, TextCase, ToAffixes, - ToFormatting, + LongShortForm, NumberForm, PageRangeFormat, TermForm, TestPosition, TextTarget, + ToAffixes, ToFormatting, }; -use citationberg::{TermForm, TextTarget}; -use crate::PageRanges; -use crate::csl::Sorting; -use crate::csl::taxonomy::{NumberVariableResult, PageVariableResult}; -use crate::lang::{Case, SentenceCase, TitleCase}; -use crate::types::{ChunkedString, Date, MaybeTyped, Numeric}; +use hayagriva_core::{ + ChunkedString, Date, EntryLike, MaybeTyped, Numeric, PageRanges, PageRangesPart, +}; + +use crate::Sorting; +use crate::taxonomy::{NumberVariableResult, PageVariableResult}; -use super::taxonomy::{EntryLike, NumberOrPageVariableResult}; +use super::taxonomy::NumberOrPageVariableResult; use super::{Context, ElemMeta, IbidState, SpecialForm, UsageInfo, write_year}; pub mod names; @@ -460,12 +460,16 @@ fn render_page_range(range: &PageRanges, ctx: &mut Context) { .ranges .iter() .try_for_each(|r| match r { - crate::PageRangesPart::Ampersand => ctx.write_str(" & "), - crate::PageRangesPart::Comma => ctx.write_str(", "), - crate::PageRangesPart::EscapedRange(start, end) => PageRangeFormat::Expanded - .format(ctx, &start.to_string(), &end.to_string(), delim), - crate::PageRangesPart::SinglePage(page) => ctx.write_str(&page.to_string()), - crate::PageRangesPart::Range(start, end) => { + PageRangesPart::Ampersand => ctx.write_str(" & "), + PageRangesPart::Comma => ctx.write_str(", "), + PageRangesPart::EscapedRange(start, end) => PageRangeFormat::Expanded.format( + ctx, + &start.to_string(), + &end.to_string(), + delim, + ), + PageRangesPart::SinglePage(page) => ctx.write_str(&page.to_string()), + PageRangesPart::Range(start, end) => { format.format(ctx, &start.to_string(), &end.to_string(), delim) } }) @@ -1449,16 +1453,3 @@ impl RenderCsl for citationberg::RenderingElement { } } } - -impl From for Case { - fn from(case: TextCase) -> Self { - match case { - TextCase::Uppercase => Case::Uppercase, - TextCase::Lowercase => Case::Lowercase, - TextCase::TitleCase => Case::Title(TitleCase::default()), - TextCase::SentenceCase => Case::Sentence(SentenceCase::default()), - TextCase::CapitalizeFirst => Case::FirstUpper, - TextCase::CapitalizeAll => Case::AllUpper, - } - } -} diff --git a/src/csl/rendering/names.rs b/crates/hayagriva-csl/src/rendering/names.rs similarity index 99% rename from src/csl/rendering/names.rs rename to crates/hayagriva-csl/src/rendering/names.rs index cb1bc510..60c83709 100644 --- a/src/csl/rendering/names.rs +++ b/crates/hayagriva-csl/src/rendering/names.rs @@ -10,9 +10,9 @@ use citationberg::{ ToFormatting, }; -use crate::csl::taxonomy::EntryLike; -use crate::csl::{Context, DisambiguateState, ElemMeta, SpecialForm, UsageInfo}; -use crate::types::Person; +use hayagriva_core::{EntryLike, Person}; + +use crate::{Context, DisambiguateState, ElemMeta, SpecialForm, UsageInfo}; use super::{RenderCsl, render_label_with_var}; diff --git a/src/csl/sort.rs b/crates/hayagriva-csl/src/sort.rs similarity index 98% rename from src/csl/sort.rs rename to crates/hayagriva-csl/src/sort.rs index 7a251ef4..c20d71e9 100644 --- a/src/csl/sort.rs +++ b/crates/hayagriva-csl/src/sort.rs @@ -9,10 +9,11 @@ use icu_collator::Collator; use icu_collator::options::CollatorOptions; use icu_locale::Locale as IcuLocale; -use crate::csl::BufWriteFormat; -use crate::csl::rendering::RenderCsl; +use hayagriva_core::EntryLike; + +use crate::BufWriteFormat; +use crate::rendering::RenderCsl; -use super::taxonomy::EntryLike; use super::{CitationItem, InstanceContext, StyleContext}; trait CollationOrd: Ord { diff --git a/crates/hayagriva-csl/src/taxonomy.rs b/crates/hayagriva-csl/src/taxonomy.rs new file mode 100644 index 00000000..555b360b --- /dev/null +++ b/crates/hayagriva-csl/src/taxonomy.rs @@ -0,0 +1,126 @@ +use std::borrow::Cow; +use std::cmp; +use std::str::FromStr; + +use citationberg::LongShortForm; +use citationberg::taxonomy::{NumberVariable, PageVariable, StandardVariable}; + +use hayagriva_core::types::{ChunkedString, MaybeTyped, Numeric, StringChunk}; +use hayagriva_core::{EntryLike, PageRanges}; + +use super::{DisambiguateState, InstanceContext, LocatorPayload}; + +impl<'a, T: EntryLike> InstanceContext<'a, T> { + pub(super) fn resolve_number_variable( + &self, + variable: NumberVariable, + ) -> Option> { + match variable { + NumberVariable::CitationNumber => { + Some(NumberVariableResult::Regular(MaybeTyped::Typed(Cow::Owned( + Numeric::from(self.cite_props.speculative.citation_number as u32 + 1), + )))) + } + NumberVariable::FirstReferenceNoteNumber => { + self.cite_props.certain.first_note_number.map(|n| { + NumberVariableResult::Regular(MaybeTyped::Typed(Cow::Owned( + Numeric::from(n as u32), + ))) + }) + } + NumberVariable::Locator => { + match &self.cite_props.speculative.locator.as_ref()?.1 { + &LocatorPayload::Str(l) => Some(NumberVariableResult::from_regular( + Numeric::from_str(l) + .map(|n| MaybeTyped::Typed(Cow::Owned(n))) + .unwrap_or_else(|_| MaybeTyped::String(l.to_owned())), + )), + LocatorPayload::Transparent(_) => { + Some(NumberVariableResult::Transparent( + self.cite_props.certain.initial_idx, + )) + } + } + } + _ => self + .entry + .resolve_number_variable(variable) + .map(NumberVariableResult::from_regular), + } + } + + pub(super) fn resolve_page_variable( + &self, + variable: PageVariable, + ) -> Option { + self.entry.resolve_page_variable(variable) + } + + // Number variables are standard variables. + pub(super) fn resolve_standard_variable( + &self, + form: LongShortForm, + variable: StandardVariable, + ) -> Option> { + match variable { + StandardVariable::YearSuffix => { + if let DisambiguateState::YearSuffix(s) = + self.cite_props.speculative.disambiguation + { + Some(Cow::Owned(StringChunk::normal(letter(s)).into())) + } else { + None + } + } + _ => self.entry.resolve_standard_variable(form, variable), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum NumberVariableResult<'a> { + Regular(MaybeTyped>), + Transparent(usize), +} + +pub(super) type PageVariableResult = MaybeTyped; + +pub(super) enum NumberOrPageVariableResult<'a> { + Number(NumberVariableResult<'a>), + Page(PageVariableResult), +} + +impl<'a> NumberVariableResult<'a> { + pub(super) fn from_regular(regular: MaybeTyped>) -> Self { + Self::Regular(regular) + } + + pub(super) fn csl_cmp(&self, other: &Self) -> cmp::Ordering { + match (self, other) { + (Self::Regular(a), Self::Regular(b)) => a.csl_cmp(b), + (Self::Regular(_), Self::Transparent(_)) => cmp::Ordering::Less, + (Self::Transparent(_), Self::Regular(_)) => cmp::Ordering::Greater, + (Self::Transparent(a), Self::Transparent(b)) => a.cmp(b), + } + } +} + +// A function that takes a usize value and returns a String +fn letter(val: u8) -> String { + let mut result = String::with_capacity(1); + let mut current = val; + + loop { + let remainder = current % 26; + let c = (remainder + b'a') as char; + result.insert(0, c); + + if current < 26 { + break; + } + + current = (current - 26) / 26; + } + + result +} diff --git a/crates/hayagriva-format/Cargo.toml b/crates/hayagriva-format/Cargo.toml new file mode 100644 index 00000000..9796e771 --- /dev/null +++ b/crates/hayagriva-format/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "hayagriva-format" +description = "The bibliography format Hayagriva, used in Typst." +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +keywords = ["hayagriva", "bibliography", "citation", "reference", "literature"] + +[dependencies] +biblatex = { workspace = true, optional = true } +citationberg = { workspace = true } +indexmap = { workspace = true } +paste = "1.0.14" +serde = { workspace = true } +unic-langid = { workspace = true } +serde_yaml = "0.9.25" +unicode-segmentation = { workspace = true } +hayagriva-core = { workspace = true } +thiserror = { workspace = true } +unscanny = { workspace = true } +url = { workspace = true } + +[dev-dependencies] +serde_json = "1" diff --git a/src/csl/citation_label.rs b/crates/hayagriva-format/src/citation_label.rs similarity index 95% rename from src/csl/citation_label.rs rename to crates/hayagriva-format/src/citation_label.rs index 0b0fc3e5..0227f9b3 100644 --- a/src/csl/citation_label.rs +++ b/crates/hayagriva-format/src/citation_label.rs @@ -3,10 +3,10 @@ use std::fmt::Write; use unicode_segmentation::UnicodeSegmentation; -use crate::{ - Entry, - types::{EntryType, Person, PersonRole}, -}; +use hayagriva_core::types::{EntryType, Person}; + +use crate::Entry; +use crate::types::PersonRole; /// Citation labels in the form of numbers. /// @@ -26,7 +26,7 @@ impl Default for Alphanumerical { } impl Alphanumerical { - /// Create a new instance of this [`CitationStyle`]. + /// Create a new instance of this citation style. pub fn new() -> Self { Self { letters: 3 } } @@ -79,6 +79,7 @@ impl Alphanumerical { }) } + /// Renders a citation of `entry` pub fn citation(self, entry: &Entry) -> String { let full_entry = entry.get_full(); diff --git a/src/interop.rs b/crates/hayagriva-format/src/interop.rs similarity index 86% rename from src/interop.rs rename to crates/hayagriva-format/src/interop.rs index ee719baa..c36aeac5 100644 --- a/src/interop.rs +++ b/crates/hayagriva-format/src/interop.rs @@ -1,4 +1,7 @@ //! Provides conversion methods for BibLaTeX. +//! +//! Should you need more manual control of the conversion from Bib(la)TeX to +//! Hayagriva, use the conversion functions in this module. use std::convert::TryFrom; @@ -7,11 +10,13 @@ use tex::{ Chunk, ChunksExt, DateValue, EditorType, PermissiveType, RetrievalError, Spanned, TypeError, }; - use url::Url; +use hayagriva_core::types::*; + +use crate::types::*; + use super::Entry; -use super::types::*; macro_rules! tex_kinds { ($self:expr, $mv_attr:expr, [$({$kind:pat, $new_kind:expr, $top_level:expr, $expand_mv:expr}),* $(,)*] $(,)*) => { @@ -39,78 +44,70 @@ macro_rules! tex_kinds { }; } -impl From<&tex::Person> for Person { - fn from(person: &tex::Person) -> Self { - fn optional(part: &str) -> Option { - if !part.is_empty() { Some(part.to_string()) } else { None } - } +fn person(person: &tex::Person) -> Person { + fn optional(part: &str) -> Option { + if !part.is_empty() { Some(part.to_string()) } else { None } + } - Self { - name: person.name.clone(), - given_name: optional(&person.given_name), - prefix: optional(&person.prefix), - suffix: optional(&person.suffix), - comma_suffix: false, - alias: None, - } + Person { + name: person.name.clone(), + given_name: optional(&person.given_name), + prefix: optional(&person.prefix), + suffix: optional(&person.suffix), + comma_suffix: false, + alias: None, } } -impl From for Date { - fn from(date: tex::Date) -> Self { - let approximate = date.uncertain || date.approximate; - - match date.value { - DateValue::At(x) | DateValue::After(x) | DateValue::Before(x) => Date { - year: x.year, - month: x.month, - day: x.day, - approximate, - season: None, - }, - DateValue::Between(_, x) => Self { - year: x.year, - month: x.month, - day: x.day, - approximate, - season: None, - }, - } +fn date(date: tex::Date) -> Date { + let approximate = date.uncertain || date.approximate; + + match date.value { + DateValue::At(x) | DateValue::After(x) | DateValue::Before(x) => Date { + year: x.year, + month: x.month, + day: x.day, + approximate, + season: None, + }, + DateValue::Between(_, x) => Date { + year: x.year, + month: x.month, + day: x.day, + approximate, + season: None, + }, } } -impl From<&[Spanned]> for ChunkedString { - fn from(chunks: &[Spanned]) -> Self { - let mut res = Self::new(); - for chunk in chunks { - match &chunk.v { - Chunk::Normal(s) => res.push_str(s, ChunkKind::Normal), - Chunk::Verbatim(s) => res.push_str(s, ChunkKind::Verbatim), - Chunk::Math(s) => res.push_str(s, ChunkKind::Math), - } +fn chunks_to_chunked_str(chunks: &[Spanned]) -> ChunkedString { + let mut res = ChunkedString::new(); + for chunk in chunks { + match &chunk.v { + Chunk::Normal(s) => res.push_str(s, ChunkKind::Normal), + Chunk::Verbatim(s) => res.push_str(s, ChunkKind::Verbatim), + Chunk::Math(s) => res.push_str(s, ChunkKind::Math), } - res } + res } -impl From<&[Spanned]> for FormatString { - fn from(chunks: &[Spanned]) -> Self { - Self { value: chunks.into(), short: None } - } +fn chunks_to_fmt_str(chunks: &[Spanned]) -> FormatString { + FormatString { value: chunks_to_chunked_str(chunks), short: None } } -impl From<&[Spanned]> for MaybeTyped { - fn from(chunks: &[Spanned]) -> Self { - let verb = chunks.format_verbatim(); - MaybeTyped::infallible_from_str(&verb) - } +fn chunks_to_mby_numeric(chunks: &[Spanned]) -> MaybeTyped { + let verb = chunks.format_verbatim(); + MaybeTyped::infallible_from_str(&verb) } -impl From<&PermissiveType> for MaybeTyped { - fn from(edition_or_volume: &PermissiveType) -> Self { - match edition_or_volume { - PermissiveType::Typed(i) => Self::Typed(Numeric::new(*i as i32)), - PermissiveType::Chunks(c) => Self::infallible_from_str(&c.format_verbatim()), +fn permissive_to_mby_numeric( + edition_or_volume: &PermissiveType, +) -> MaybeTyped { + match edition_or_volume { + PermissiveType::Typed(i) => MaybeTyped::Typed(Numeric::new(*i as i32)), + PermissiveType::Chunks(c) => { + MaybeTyped::infallible_from_str(&c.format_verbatim()) } } } @@ -216,7 +213,7 @@ impl TryFrom<&tex::Entry> for Entry { { tex::EntryType::Unknown(_), EntryType::Misc, None, false }, ]); - if let Ok(a) = entry.author().map(|a| a.iter().map(Into::into).collect()) { + if let Ok(a) = entry.author().map(|a| a.iter().map(person).collect()) { item.set_authors(a); } @@ -225,9 +222,9 @@ impl TryFrom<&tex::Entry> for Entry { for (editors, role) in entry.editors()? { let ptype = ed_role(role, &entry.entry_type); match ptype { - None => eds.extend(editors.iter().map(Into::into)), + None => eds.extend(editors.iter().map(person)), Some(role) => collaborators.push(PersonsWithRoles::new( - editors.iter().map(Into::into).collect(), + editors.iter().map(person).collect(), role, )), } @@ -240,33 +237,32 @@ impl TryFrom<&tex::Entry> for Entry { item.set_affiliated(collaborators); } - if let Some(a) = - map_res(entry.holder())?.map(|a| a.iter().map(Into::into).collect()) + if let Some(a) = map_res(entry.holder())?.map(|a| a.iter().map(person).collect()) { item.add_affiliated_persons((a, PersonRole::Holder)); } if let Some(parent) = book(&mut item, parent) && let Some(a) = - map_res(entry.book_author())?.map(|a| a.iter().map(Into::into).collect()) + map_res(entry.book_author())?.map(|a| a.iter().map(person).collect()) { parent.set_authors(a); } if let Some(a) = - map_res(entry.annotator())?.map(|a| a.iter().map(Into::into).collect()) + map_res(entry.annotator())?.map(|a| a.iter().map(person).collect()) { item.add_affiliated_persons((a, PersonRole::Annotator)); } if let Some(a) = - map_res(entry.commentator())?.map(|a| a.iter().map(Into::into).collect()) + map_res(entry.commentator())?.map(|a| a.iter().map(person).collect()) { item.add_affiliated_persons((a, PersonRole::Commentator)); } if let Some(a) = - map_res(entry.translator())?.map(|a| a.iter().map(Into::into).collect()) + map_res(entry.translator())?.map(|a| a.iter().map(person).collect()) { item.add_affiliated_persons((a, PersonRole::Translator)); } @@ -291,25 +287,27 @@ impl TryFrom<&tex::Entry> for Entry { } if let Some(a) = - map_res(entry.afterword())?.map(|a| a.iter().map(Into::into).collect()) + map_res(entry.afterword())?.map(|a| a.iter().map(person).collect()) { item.add_affiliated_persons((a, PersonRole::Afterword)); } if let Some(a) = - map_res(entry.foreword())?.map(|a| a.iter().map(Into::into).collect()) + map_res(entry.foreword())?.map(|a| a.iter().map(person).collect()) { item.add_affiliated_persons((a, PersonRole::Foreword)); } if let Some(a) = - map_res(entry.introduction())?.map(|a| a.iter().map(Into::into).collect()) + map_res(entry.introduction())?.map(|a| a.iter().map(person).collect()) { item.add_affiliated_persons((a, PersonRole::Introduction)); } - if let Some(title) = map_res(entry.title())?.map(Into::into) { - if let Some(short_title) = map_res(entry.short_title())?.map(Into::into) { + if let Some(title) = map_res(entry.title())?.map(chunks_to_chunked_str) { + if let Some(short_title) = + map_res(entry.short_title())?.map(chunks_to_chunked_str) + { item.set_title(FormatString { value: title, short: Some(Box::new(short_title)), @@ -322,17 +320,21 @@ impl TryFrom<&tex::Entry> for Entry { // NOTE: Ignoring subtitle and titleaddon for now if let Some(parent) = mv(&mut item, parent, mv_parent) - && let Some(title) = map_res(entry.main_title())?.map(Into::into) + && let Some(title) = map_res(entry.main_title())?.map(chunks_to_fmt_str) { parent.set_title(title); } if let Some(parent) = book(&mut item, parent) { if entry.entry_type == tex::EntryType::Article { - if let Some(title) = map_res(entry.journal_title())?.map(Into::into) { + if let Some(title) = + map_res(entry.journal_title())?.map(chunks_to_fmt_str) + { parent.set_title(title); } - } else if let Some(title) = map_res(entry.book_title())?.map(Into::into) { + } else if let Some(title) = + map_res(entry.book_title())?.map(chunks_to_fmt_str) + { parent.set_title(title); } } @@ -353,14 +355,14 @@ impl TryFrom<&tex::Entry> for Entry { PermissiveType::Typed(d) => Some(d), PermissiveType::Chunks(_) => None, }) - .map(|d| d.into()) + .map(date) { conference.set_date(event_date); } - if let Some(title) = map_res(entry.eventtitle())?.map(Into::into) { + if let Some(title) = map_res(entry.eventtitle())?.map(chunks_to_fmt_str) { conference.set_title(title); } - if let Some(venue) = map_res(entry.venue())?.map(|d| d.into()) { + if let Some(venue) = map_res(entry.venue())?.map(chunks_to_fmt_str) { conference.set_location(venue); } @@ -372,12 +374,14 @@ impl TryFrom<&tex::Entry> for Entry { PermissiveType::Typed(d) => Some(d), PermissiveType::Chunks(_) => None, }) - .map(|d| d.into()) + .map(date) { item.set_date(date); } - if let Some(edition) = map_res(entry.edition())?.map(|d| (&d).into()) { + if let Some(edition) = + map_res(entry.edition())?.map(|d| permissive_to_mby_numeric(&d)) + { if let Some(parent) = book(&mut item, parent) { parent.set_edition(edition); } else { @@ -389,14 +393,14 @@ impl TryFrom<&tex::Entry> for Entry { entry.entry_type, tex::EntryType::Article | tex::EntryType::Proceedings ) { - if let Some(issue) = map_res(entry.issue())?.map(|d| d.into()) { + if let Some(issue) = map_res(entry.issue())?.map(chunks_to_mby_numeric) { if let Some(parent) = book(&mut item, parent) { parent.set_issue(issue); } else { item.set_issue(issue); } } - if let Some(ititle) = map_res(entry.issue_title())?.map(Into::into) { + if let Some(ititle) = map_res(entry.issue_title())?.map(chunks_to_fmt_str) { if let Some(parent) = book(&mut item, parent) { parent.set_title(ititle); } else { @@ -412,7 +416,7 @@ impl TryFrom<&tex::Entry> for Entry { // report, manual, and dataset, where it fits the use for patent. // Hayagriva uses "issue" for the journal/book sense of biblatex's number, // and "serial-number" for the record number / token sense. - if let Some(number) = map_res(entry.number())?.map(|d| d.into()) { + if let Some(number) = map_res(entry.number())?.map(chunks_to_mby_numeric) { if let Some(parent) = book(&mut item, parent) { parent.set_issue(number); } else { @@ -429,7 +433,7 @@ impl TryFrom<&tex::Entry> for Entry { } if let Some(volume) = map_res(entry.volume())? { - let val = (&volume).into(); + let val = permissive_to_mby_numeric(&volume); if let Some(parent) = book(&mut item, parent) { parent.set_volume(val); } else { @@ -488,21 +492,21 @@ impl TryFrom<&tex::Entry> for Entry { PermissiveType::Typed(d) => Some(d), PermissiveType::Chunks(_) => None, }) - .map(|d| d.into()); + .map(date); item.set_url(QualifiedUrl { value: url, visit_date: date }); } if let Some(publisher_name) = map_res(entry.publisher())?.map(|pubs| comma_list(&pubs)) { - let location = map_res(entry.location())?.map(|d| d.into()); + let location = map_res(entry.location())?.map(chunks_to_fmt_str); let publisher = Publisher::new(Some(publisher_name), location); if let Some(parent) = book(&mut item, parent) { parent.set_publisher(publisher); } else { item.set_publisher(publisher); } - } else if let Some(location) = map_res(entry.location())?.map(|d| d.into()) { + } else if let Some(location) = map_res(entry.location())?.map(chunks_to_fmt_str) { let publisher = Publisher::new(None, Some(location)); if let Some(parent) = book(&mut item, parent) { parent.set_publisher(publisher); @@ -519,7 +523,9 @@ impl TryFrom<&tex::Entry> for Entry { } else { item.set_organization(organization); } - } else if let Some(organization) = map_res(entry.institution())?.map(Into::into) { + } else if let Some(organization) = + map_res(entry.institution())?.map(chunks_to_fmt_str) + { if let Some(parent) = book(&mut item, parent) { parent.set_organization(organization); } else { @@ -527,7 +533,7 @@ impl TryFrom<&tex::Entry> for Entry { } } - if let Some(note) = map_res(entry.how_published())?.map(Into::into) { + if let Some(note) = map_res(entry.how_published())?.map(chunks_to_fmt_str) { if let Some(parent) = book(&mut item, parent) { parent.set_note(note); } else { @@ -568,20 +574,20 @@ impl TryFrom<&tex::Entry> for Entry { } } - if let Some(note) = map_res(entry.note())?.map(Into::into) { + if let Some(note) = map_res(entry.note())?.map(chunks_to_fmt_str) { item.set_note(note); } if let Some(note) = map_res(entry.annotation())? .or_else(|| entry.addendum().ok()) - .map(Into::into) + .map(chunks_to_fmt_str) && item.note.is_none() { item.set_note(note); } if let Some(abstract_) = map_res(entry.abstract_())? { - item.set_abstract_(abstract_.into()) + item.set_abstract_(chunks_to_fmt_str(abstract_)) } // BibLaTeX describes "type" as "The type of a manual, patent, report, or thesis. @@ -610,7 +616,7 @@ impl TryFrom<&tex::Entry> for Entry { } if let Some(series) = map_res(entry.series())? { - let title: FormatString = series.into(); + let title: FormatString = chunks_to_fmt_str(series); let mut new = Entry::new(&entry.key, item.entry_type); new.set_title(title); @@ -635,7 +641,7 @@ impl TryFrom<&tex::Entry> for Entry { // is better corresponded to by the `@InBook` BibLaTeX entry type: // "A part of a book which forms a self-contained unit with its // own title." - item.set_chapter(chapter.into()); + item.set_chapter(chunks_to_mby_numeric(chapter)); } Ok(item) @@ -649,7 +655,7 @@ fn comma_list(items: &[Vec>]) -> FormatString { value.push_str(", ", ChunkKind::Normal); } - let chunked = ChunkedString::from(entity.as_slice()); + let chunked = chunks_to_chunked_str(entity.as_slice()); value.extend(chunked); } @@ -660,7 +666,9 @@ fn comma_list(items: &[Vec>]) -> FormatString { mod tests { use unic_langid::LanguageIdentifier; - use crate::types::{EntryType, MaybeTyped, PersonRole}; + use hayagriva_core::types::{EntryType, MaybeTyped}; + + use crate::types::PersonRole; #[test] fn test_pmid_from_biblatex() { diff --git a/src/io.rs b/crates/hayagriva-format/src/io.rs similarity index 96% rename from src/io.rs rename to crates/hayagriva-format/src/io.rs index fbee6f4e..517e6ce6 100644 --- a/src/io.rs +++ b/crates/hayagriva-format/src/io.rs @@ -10,7 +10,7 @@ use crate::Library; /// Parse a bibliography from a YAML string. /// /// ``` -/// use hayagriva::io::from_yaml_str; +/// use hayagriva_format::io::from_yaml_str; /// /// let yaml = r#" /// crazy-rich: @@ -90,10 +90,10 @@ mod tests { #[test] fn roundtrip() { - let contents = fs::read_to_string("tests/data/basic.yml").unwrap(); + let contents = fs::read_to_string("../../tests/data/basic.yml").unwrap(); let entries = from_yaml_str(&contents).unwrap(); let yaml = to_yaml_str(&entries).unwrap(); - println!("{}", &yaml); + println!("{}", yaml); let reconstructed = from_yaml_str(&yaml).unwrap(); assert_eq!(entries.len(), reconstructed.len()); diff --git a/src/lib.rs b/crates/hayagriva-format/src/lib.rs similarity index 85% rename from src/lib.rs rename to crates/hayagriva-format/src/lib.rs index fa9fa0d2..e81d5a5d 100644 --- a/src/lib.rs +++ b/crates/hayagriva-format/src/lib.rs @@ -1,177 +1,96 @@ -/*! -Hayagriva provides a YAML-backed format and data model for various -bibliography items as well as a CSL processor formatting both in-text citations and -reference lists based on these literature databases. - -The crate is intended to assist scholarly writing and reference management -and can be used both through a CLI and an API. - -Below, there is an example of how to parse a YAML database and get a Modern -Language Association-style citation. - -# Supported styles - -Hayagriva supports all styles provided in the -[official Citation Style Language repository](https://github.com/citation-style-language/styles), -currently over 2,600. You can provide your own style files or use the ones -bundled with this library in the [`archive`] module. - -# Usage - -```rust -use hayagriva::io::from_yaml_str; - -let yaml = r#" -crazy-rich: - type: Book - title: Crazy Rich Asians - author: Kwan, Kevin - date: 2014 - publisher: Anchor Books - location: New York, NY, US -"#; - -// Parse a bibliography -let bib = from_yaml_str(yaml).unwrap(); -assert_eq!(bib.get("crazy-rich").unwrap().date().unwrap().year, 2014); - -// Format the reference -use std::fs; -use hayagriva::{ - BibliographyDriver, BibliographyRequest, BufWriteFormat, - CitationItem, CitationRequest, -}; -use hayagriva::citationberg::{LocaleFile, IndependentStyle}; - -let en_locale = fs::read_to_string("tests/data/locales-en-US.xml").unwrap(); -let locales = [LocaleFile::from_xml(&en_locale).unwrap().into()]; - -let style = fs::read_to_string("tests/data/art-history.csl").unwrap(); -let style = IndependentStyle::from_xml(&style).unwrap(); - -let mut driver = BibliographyDriver::new(); - -for entry in bib.iter() { - let items = vec![CitationItem::with_entry(entry)]; - driver.citation(CitationRequest::from_items(items, &style, &locales)); -} - -let result = driver.finish(BibliographyRequest { - style: &style, - locale: None, - locale_files: &locales, -}); - -for cite in result.citations { - println!("{}", cite.citation) -} -``` - -To format entries, you need to wrap them in a [`CitationRequest`]. Each of these -can reference multiple entries in their respective [`CitationItem`]s. -Use these with a [`BibliographyDriver`] to obtain formatted citations and bibliographies. - -If the default features are enabled, Hayagriva supports BibTeX and BibLaTeX -bibliographies. You can use [`io::from_biblatex_str`] to parse such -bibliographies. - -Should you need more manual control, the library's native `Entry` struct -also offers an implementation of the `From<&biblatex::Entry>`-Trait. You will -need to depend on the [biblatex](https://docs.rs/biblatex/latest/biblatex/) -crate to obtain its `Entry`. Therefore, you could also use your BibLaTeX -content like this: - -```ignore -use hayagriva::Entry; -let converted: Entry = your_biblatex_entry.into(); -``` - -If you do not need BibLaTeX compatibility, you can use Hayagriva without the -default features by writing this in your `Cargo.toml`: - -```toml -[dependencies] -hayagriva = { version = "0.9", default-features = false } -``` - -# Selectors - -Hayagriva uses a custom selector language that enables you to filter -bibliographies by type of media. For more information about selectors, refer -to the [selectors.md -file](https://github.com/typst/hayagriva/blob/main/docs/selectors.md). While -you can parse user-defined selectors using the function `Selector::parse`, -you may instead want to use the selector macro to avoid the run time cost of -parsing a selector when working with constant selectors. - -```rust -use hayagriva::select; -use hayagriva::io::from_yaml_str; - -let yaml = r#" -quantized-vortex: - type: Article - author: Gross, E. P. - title: Structure of a Quantized Vortex in Boson Systems - date: 1961-05 - page-range: 454-477 - serial-number: - doi: 10.1007/BF02731494 - parent: - issue: 3 - volume: 20 - title: Il Nuovo Cimento -"#; - -let entries = from_yaml_str(yaml).unwrap(); -let journal = select!((Article["date"]) > ("journal":Periodical)); -assert!(journal.matches(entries.nth(0).unwrap())); -``` - -There are two ways to check if a selector matches an entry. -You should use [`Selector::matches`] if you just want to know if an item -matches a selector and [`Selector::apply`] to continue to work with the data from -parents of a matching entry. Keep in mind that the latter function will -return `Some` even if no sub-entry was bound / if the hash map is empty. -*/ +//! The Hayagriva YAML-backed format. +//! +//! # Usage +//! +//! ```rust +//! use hayagriva_format::io::from_yaml_str; +//! +//! let yaml = r#" +//! crazy-rich: +//! type: Book +//! title: Crazy Rich Asians +//! author: Kwan, Kevin +//! date: 2014 +//! publisher: Anchor Books +//! location: New York, NY, US +//! "#; +//! +//! // Parse a bibliography +//! let bib = from_yaml_str(yaml).unwrap(); +//! assert_eq!(bib.get("crazy-rich").unwrap().date().unwrap().year, 2014); +//! ``` +//! +//! # Selectors +//! +//! Hayagriva uses a custom selector language that enables you to filter +//! bibliographies by type of media. For more information about selectors, refer +//! to the [selectors.md +//! file](https://github.com/typst/hayagriva/blob/main/docs/selectors.md). While +//! you can parse user-defined selectors using the function `Selector::parse`, +//! you may instead want to use the selector macro to avoid the run time cost of +//! parsing a selector when working with constant selectors. +//! +//! ```rust +//! use hayagriva_format::select; +//! use hayagriva_format::io::from_yaml_str; +//! +//! let yaml = r#" +//! quantized-vortex: +//! type: Article +//! author: Gross, E. P. +//! title: Structure of a Quantized Vortex in Boson Systems +//! date: 1961-05 +//! page-range: 454-477 +//! serial-number: +//! doi: 10.1007/BF02731494 +//! parent: +//! issue: 3 +//! volume: 20 +//! title: Il Nuovo Cimento +//! "#; +//! +//! let entries = from_yaml_str(yaml).unwrap(); +//! let journal = select!((Article["date"]) > ("journal":Periodical)); +//! assert!(journal.matches(entries.nth(0).unwrap())); +//! ``` +//! +//! There are two ways to check if a selector matches an entry. +//! You should use [`Selector::matches`] if you just want to know if an item +//! matches a selector and [`Selector::apply`] to continue to work with the data from +//! parents of a matching entry. Keep in mind that the latter function will +//! return `Some` even if no sub-entry was bound / if the hash map is empty. #![warn(missing_docs)] #![allow(clippy::comparison_chain)] -#[macro_use] -mod selectors; -#[cfg(feature = "biblatex")] -mod interop; - -mod csl; -pub mod io; -pub mod lang; -pub mod types; -mod util; - use std::collections::BTreeMap; -#[cfg(feature = "archive")] -pub use crate::csl::archive; pub use citationberg; -pub use csl::{ - BibliographyDriver, BibliographyItem, BibliographyRequest, Brackets, BufWriteFormat, - CitationItem, CitationRequest, CitePurpose, Elem, ElemChild, ElemChildren, ElemMeta, - Formatted, Formatting, LocatorPayload, Rendered, RenderedBibliography, - RenderedCitation, SpecificLocator, TransparentLocator, standalone_citation, -}; -pub use selectors::{Selector, SelectorError}; - use indexmap::IndexMap; use paste::paste; use serde::{Deserialize, Serialize, de::Visitor}; -use types::*; use unic_langid::LanguageIdentifier; -use util::{ + +use hayagriva_core::types::*; +use hayagriva_core::util::{ OneOrMany, deserialize_one_or_many_opt, serialize_one_or_many, serialize_one_or_many_opt, }; +pub use crate::types::*; + +pub use selectors::{Selector, SelectorError}; + +#[macro_use] +mod selectors; + +pub mod citation_label; +#[cfg(feature = "biblatex")] +mod interop; +pub mod io; +mod taxonomy; +pub mod types; + /// A collection of bibliographic entries. #[derive(Debug, Clone, Default, PartialEq, Serialize)] pub struct Library(IndexMap); @@ -943,7 +862,7 @@ mod tests { #[test] fn selectors() { - let contents = fs::read_to_string("tests/data/basic.yml").unwrap(); + let contents = fs::read_to_string("../../tests/data/basic.yml").unwrap(); let entries = from_yaml_str(&contents).unwrap(); select_all!("article > proceedings", entries, ["zygos"]); @@ -1011,7 +930,7 @@ mod tests { #[test] fn selector_bindings() { - let contents = fs::read_to_string("tests/data/basic.yml").unwrap(); + let contents = fs::read_to_string("../../tests/data/basic.yml").unwrap(); let entries = from_yaml_str(&contents).unwrap(); select!( diff --git a/src/selectors/mod.rs b/crates/hayagriva-format/src/selectors/mod.rs similarity index 97% rename from src/selectors/mod.rs rename to crates/hayagriva-format/src/selectors/mod.rs index c991bdfb..41270051 100644 --- a/src/selectors/mod.rs +++ b/crates/hayagriva-format/src/selectors/mod.rs @@ -21,8 +21,8 @@ /// This example illustrates some of the differences: /// /// ```rust -/// use hayagriva::select; -/// use hayagriva::Selector; +/// use hayagriva_format::select; +/// use hayagriva_format::Selector; /// /// // finds an article that is parented by a conference proceedings volume /// assert_eq!(Selector::parse("article > proceedings").unwrap(), select!(Article > Proceedings)); @@ -39,7 +39,7 @@ /// ``` /// /// [intro]: https://github.com/typst/hayagriva/blob/main/docs/selectors.md -/// [EType]: crate::types::EntryType +/// [EType]: hayagriva_core::types::EntryType #[macro_export] macro_rules! select { (($($tts:tt)*)) => { @@ -51,7 +51,7 @@ macro_rules! select { }; ($entry_type:ident) => { - $crate::Selector::Entry($crate::types::EntryType::$entry_type) + $crate::Selector::Entry(::hayagriva_core::types::EntryType::$entry_type) }; ($binding:literal:$expr:tt) => { @@ -95,7 +95,7 @@ use std::collections::HashMap; use thiserror::Error; use crate::Entry; -use crate::types::EntryType; +use hayagriva_core::EntryType; /// A selector used to filter bibliographies and match on entries. #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/src/selectors/parser.rs b/crates/hayagriva-format/src/selectors/parser.rs similarity index 99% rename from src/selectors/parser.rs rename to crates/hayagriva-format/src/selectors/parser.rs index 31cadf4e..1ab04a22 100644 --- a/src/selectors/parser.rs +++ b/crates/hayagriva-format/src/selectors/parser.rs @@ -1,7 +1,8 @@ use std::str::FromStr; +use hayagriva_core::types::EntryType; + use super::{Selector, SelectorError, SelectorResult}; -use crate::types::EntryType; /// Parse a selector. pub fn parse(src: &str) -> SelectorResult { diff --git a/src/csl/taxonomy.rs b/crates/hayagriva-format/src/taxonomy.rs similarity index 67% rename from src/csl/taxonomy.rs rename to crates/hayagriva-format/src/taxonomy.rs index a3d3ed3d..cd9abf17 100644 --- a/src/csl/taxonomy.rs +++ b/crates/hayagriva-format/src/taxonomy.rs @@ -1,139 +1,22 @@ -use std::borrow::Cow; -use std::cmp; -use std::str::FromStr; +//! Translation from Hayagriva to CSL variables -use crate::types::{ - ChunkedString, Date, EntryType, MaybeTyped, Numeric, Person, PersonRole, Publisher, - StringChunk, -}; -use crate::{Entry, PageRanges}; +use std::{borrow::Cow, str::FromStr}; + +use citationberg::LongShortForm; use citationberg::taxonomy::{ DateVariable, Kind, NameVariable, NumberVariable, PageVariable, StandardVariable, }; -use citationberg::{LongShortForm, taxonomy}; use unic_langid::LanguageIdentifier; -#[cfg(feature = "csl-json")] -use citationberg::json as csl_json; - -use super::citation_label::Alphanumerical; -use super::{DisambiguateState, InstanceContext, LocatorPayload}; - -pub trait EntryLike { - fn resolve_number_variable( - &self, - variable: NumberVariable, - ) -> Option>>; - fn resolve_page_variable( - &self, - variable: PageVariable, - ) -> Option>; - fn resolve_standard_variable( - &self, - form: LongShortForm, - variable: StandardVariable, - ) -> Option>; - fn resolve_name_variable(&self, variable: NameVariable) -> Vec>; - fn resolve_date_variable(&self, variable: DateVariable) -> Option>; - fn matches_entry_type(&self, kind: taxonomy::Kind) -> bool; - fn is_english(&self) -> Option; - fn key(&self) -> Cow<'_, str>; -} - -impl<'a, T: EntryLike> InstanceContext<'a, T> { - pub(super) fn resolve_number_variable( - &self, - variable: NumberVariable, - ) -> Option> { - match variable { - NumberVariable::CitationNumber => { - Some(NumberVariableResult::Regular(MaybeTyped::Typed(Cow::Owned( - Numeric::from(self.cite_props.speculative.citation_number as u32 + 1), - )))) - } - NumberVariable::FirstReferenceNoteNumber => { - self.cite_props.certain.first_note_number.map(|n| { - NumberVariableResult::Regular(MaybeTyped::Typed(Cow::Owned( - Numeric::from(n as u32), - ))) - }) - } - NumberVariable::Locator => { - match &self.cite_props.speculative.locator.as_ref()?.1 { - &LocatorPayload::Str(l) => Some(NumberVariableResult::from_regular( - Numeric::from_str(l) - .map(|n| MaybeTyped::Typed(Cow::Owned(n))) - .unwrap_or_else(|_| MaybeTyped::String(l.to_owned())), - )), - LocatorPayload::Transparent(_) => { - Some(NumberVariableResult::Transparent( - self.cite_props.certain.initial_idx, - )) - } - } - } - _ => self - .entry - .resolve_number_variable(variable) - .map(NumberVariableResult::from_regular), - } - } - - pub(super) fn resolve_page_variable( - &self, - variable: PageVariable, - ) -> Option { - self.entry.resolve_page_variable(variable) - } - - // Number variables are standard variables. - pub(super) fn resolve_standard_variable( - &self, - form: LongShortForm, - variable: StandardVariable, - ) -> Option> { - match variable { - StandardVariable::YearSuffix => { - if let DisambiguateState::YearSuffix(s) = - self.cite_props.speculative.disambiguation - { - Some(Cow::Owned(StringChunk::normal(letter(s)).into())) - } else { - None - } - } - _ => self.entry.resolve_standard_variable(form, variable), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) enum NumberVariableResult<'a> { - Regular(MaybeTyped>), - Transparent(usize), -} - -pub(super) type PageVariableResult = MaybeTyped; +use hayagriva_core::{ + ChunkedString, Date, EntryLike, EntryType, MaybeTyped, Numeric, PageRanges, Person, + StringChunk, +}; -pub(super) enum NumberOrPageVariableResult<'a> { - Number(NumberVariableResult<'a>), - Page(PageVariableResult), -} +use crate::citation_label::Alphanumerical; +use crate::types::*; -impl<'a> NumberVariableResult<'a> { - pub(super) fn from_regular(regular: MaybeTyped>) -> Self { - Self::Regular(regular) - } - - pub(super) fn csl_cmp(&self, other: &Self) -> cmp::Ordering { - match (self, other) { - (Self::Regular(a), Self::Regular(b)) => a.csl_cmp(b), - (Self::Regular(_), Self::Transparent(_)) => cmp::Ordering::Less, - (Self::Transparent(_), Self::Regular(_)) => cmp::Ordering::Greater, - (Self::Transparent(a), Self::Transparent(b)) => a.cmp(b), - } - } -} +use super::Entry; impl EntryLike for Entry { fn key(&self) -> Cow<'_, str> { @@ -427,10 +310,7 @@ impl EntryLike for Entry { }?)) } - fn resolve_name_variable( - &self, - variable: taxonomy::NameVariable, - ) -> Vec> { + fn resolve_name_variable(&self, variable: NameVariable) -> Vec> { match variable { NameVariable::Author => self.authors().map(|a| a.iter().collect()), NameVariable::Chair => self @@ -540,7 +420,7 @@ impl EntryLike for Entry { .collect() } - fn matches_entry_type(&self, kind: citationberg::taxonomy::Kind) -> bool { + fn matches_entry_type(&self, kind: Kind) -> bool { // Each match arm contains mutually exclusive entry kinds. match kind { Kind::Article @@ -720,210 +600,6 @@ impl EntryLike for Entry { } } -#[cfg(feature = "csl-json")] -fn resolve_csl_json_standard_variable( - item: &citationberg::json::Item, - variable: StandardVariable, -) -> Option> { - match item.0.get(&variable.to_string())? { - csl_json::Value::String(s) => { - Some(Cow::Owned(StringChunk::normal(s.clone()).into())) - } - csl_json::Value::Number(n) => { - Some(Cow::Owned(StringChunk::normal(n.to_string()).into())) - } - _ => None, - } -} - -#[cfg(feature = "csl-json")] -impl EntryLike for citationberg::json::Item { - fn resolve_standard_variable( - &self, - form: LongShortForm, - variable: StandardVariable, - ) -> Option> { - match variable { - StandardVariable::Title => match form { - LongShortForm::Short => { - // Per citeproc tests, a 'title-short' without 'title' is - // valid and should be used when the short form is - // selected. - resolve_csl_json_standard_variable(self, StandardVariable::TitleShort) - .or_else(|| { - resolve_csl_json_standard_variable( - self, - StandardVariable::Title, - ) - }) - } - LongShortForm::Long => { - resolve_csl_json_standard_variable(self, StandardVariable::Title) - } - }, - _ => resolve_csl_json_standard_variable(self, variable), - } - } - - fn resolve_date_variable(&self, variable: DateVariable) -> Option> { - match self.0.get(&variable.to_string())? { - csl_json::Value::Date(d) => { - let Ok(d) = csl_json::FixedDateRange::try_from(d.clone()) else { - return None; - }; - if d.end.is_some() { - panic!("ranges are not supported") - } - let d = d.start; - Some(Cow::Owned(Date { - year: d.year as i32, - month: d.month, - day: d.day, - approximate: d.circa, - season: d.season, - })) - } - _ => None, - } - } - - fn resolve_name_variable(&self, variable: NameVariable) -> Vec> { - match self.0.get(&variable.to_string()) { - Some(csl_json::Value::Names(names)) => names - .iter() - .filter_map(|name| { - Some(Cow::Owned(match name { - csl_json::NameValue::Literal(l) => Person { - name: l.literal.clone(), - prefix: None, - suffix: None, - given_name: None, - alias: None, - comma_suffix: false, - }, - csl_json::NameValue::Item(csl_json::NameItem { - family, - given, - non_dropping_particle: None, - dropping_particle: None, - suffix, - comma_suffix, - }) => { - let mut parts = vec![family.as_str()]; - if let Some(given) = given { - parts.push(given.as_str()); - } - let mut p = Person::from_strings(parts).ok()?; - if let Some(suffix) = suffix { - p.suffix = Some(suffix.as_str().to_owned()); - } - if comma_suffix.unwrap_or_default() { - p.comma_suffix = true; - } - - p - } - csl_json::NameValue::Item(csl_json::NameItem { - family, - given, - non_dropping_particle, - dropping_particle, - suffix, - comma_suffix, - }) => Person { - name: if let Some(non_drop) = non_dropping_particle { - format!("{non_drop} {family}") - } else { - family.clone() - }, - prefix: dropping_particle.clone(), - suffix: suffix.clone(), - given_name: given.clone(), - alias: None, - comma_suffix: comma_suffix.unwrap_or_default(), - }, - })) - }) - .collect(), - _ => vec![], - } - } - - fn resolve_page_variable( - &self, - variable: PageVariable, - ) -> Option> { - match variable { - PageVariable::Page => match self.0.get("page")? { - &csl_json::Value::Number(n) => { - // Page ranges use i32 internally, so we check whether the - // number is in range. - Some(match i32::try_from(n) { - Ok(n) => MaybeTyped::Typed(PageRanges::from(n)), - // If the number is not in range, we degrade to a - // string, which disables some CSL features. - Err(_) => MaybeTyped::String(n.to_string()), - }) - } - csl_json::Value::String(s) => { - let res = MaybeTyped::::infallible_from_str(s); - Some(match res { - MaybeTyped::String(s) => MaybeTyped::String(s), - MaybeTyped::Typed(r) => MaybeTyped::Typed(r), - }) - } - _ => None, - }, - } - } - - fn resolve_number_variable( - &self, - variable: NumberVariable, - ) -> Option>> { - if matches!(variable, NumberVariable::PageFirst) - && let Some(MaybeTyped::Typed(n)) = - self.resolve_page_variable(PageVariable::Page) - { - return n.first().map(|r| MaybeTyped::Typed(Cow::Owned(r.clone()))); - } - match self.0.get(&variable.to_string())? { - csl_json::Value::Number(n) => { - Some(MaybeTyped::Typed(Cow::Owned(Numeric::from(*n as u32)))) - } - csl_json::Value::String(s) => { - let res = MaybeTyped::::infallible_from_str(s); - Some(match res { - MaybeTyped::String(s) => MaybeTyped::String(s), - MaybeTyped::Typed(n) => MaybeTyped::Typed(Cow::Owned(n)), - }) - } - _ => None, - } - } - - fn matches_entry_type(&self, kind: taxonomy::Kind) -> bool { - let Some(actual) = self.0.get("type") else { - return false; - }; - let Some(string) = actual.to_str() else { - return false; - }; - Kind::from_str(&string) == Ok(kind) - } - - fn is_english(&self) -> Option { - self.0 - .get("language") - .and_then(|l| l.to_str()) - .map(|l| l.starts_with("en")) - } - - fn key(&self) -> Cow<'_, str> { - self.id().unwrap_or_default() - } -} - pub(super) fn csl_language(lang_id: &LanguageIdentifier) -> String { let mut buf = String::with_capacity(if lang_id.region.is_some() { 5 } else { 2 }); buf.push_str(lang_id.language.as_str()); @@ -934,27 +610,7 @@ pub(super) fn csl_language(lang_id: &LanguageIdentifier) -> String { buf } -// A function that takes a usize value and returns a String -fn letter(val: u8) -> String { - let mut result = String::with_capacity(1); - let mut current = val; - - loop { - let remainder = current % 26; - let c = (remainder + b'a') as char; - result.insert(0, c); - - if current < 26 { - break; - } - - current = (current - 26) / 26; - } - - result -} - -#[cfg(test)] +#[cfg(all(test, feature = "biblatex"))] mod tests { use super::*; use crate::io::from_biblatex_str; diff --git a/crates/hayagriva-format/src/types/mod.rs b/crates/hayagriva-format/src/types/mod.rs new file mode 100644 index 00000000..eebf8286 --- /dev/null +++ b/crates/hayagriva-format/src/types/mod.rs @@ -0,0 +1,172 @@ +//! Types for the Hayagriva format. + +use std::{ + collections::BTreeMap, + fmt::{self, Display}, + str::FromStr, +}; + +use serde::{Deserialize, Serialize, ser::SerializeMap}; +use url::Url; + +use hayagriva_core::{ + ChunkedStrParseError, Date, FormatString, types::derive_or_from_str, +}; + +pub use person::*; +pub use time::*; + +mod person; +mod time; + +derive_or_from_str! { + /// An URL, possibly with a last visited date. + #[derive(Clone, Debug, PartialEq, Eq, Hash)] + pub struct QualifiedUrl where "URL string or dictionary with keys \"url\" and \"date\"" { + /// The [Url]. + pub value: Url, + /// The last visited date. + #[serde(rename = "date")] + pub visit_date: Option, + } +} + +impl Serialize for QualifiedUrl { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if let Some(date) = &self.visit_date { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("value", &self.value)?; + map.serialize_entry("date", date)?; + map.end() + } else { + self.value.serialize(serializer) + } + } +} + +impl QualifiedUrl { + /// Create a new qualified URL. + pub fn new(value: Url, visit_date: Option) -> Self { + Self { value, visit_date } + } +} + +impl FromStr for QualifiedUrl { + type Err = url::ParseError; + + fn from_str(s: &str) -> Result { + Ok(Self { value: Url::parse(s)?, visit_date: None }) + } +} + +impl Display for QualifiedUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.value.fmt(f) + } +} + +derive_or_from_str! { + /// A publisher, possibly with a location. + #[derive(Clone, Debug, PartialEq, Eq, Hash)] + pub struct Publisher where "FormatString string or dictionary with \"name\" and \"location\"" { + /// Publisher of the item. + name: Option, + /// Physical location at which the item was published or created. + location: Option, + } +} + +impl Serialize for Publisher { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if let Some(location) = &self.location { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("name", &self.name)?; + map.serialize_entry("location", location)?; + map.end() + } else { + self.name.serialize(serializer) + } + } +} + +impl Publisher { + /// Create a new publisher. + pub fn new(name: Option, location: Option) -> Self { + Self { name, location } + } + + /// Publisher of the item. + pub fn name(&self) -> Option<&FormatString> { + self.name.as_ref() + } + + /// Physical location at which the item was published or created. + pub fn location(&self) -> Option<&FormatString> { + self.location.as_ref() + } +} + +impl FromStr for Publisher { + type Err = ChunkedStrParseError; + + /// Creates a new publisher with `s` as its name and no location. + fn from_str(s: &str) -> Result { + Ok(Publisher::new(Some(FormatString::from_str(s)?), None)) + } +} + +/// A set of serial numbers like DOIs, ISBNs, or ISSNs. +/// Keys should be lowercase. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] +#[serde(transparent)] +pub struct SerialNumber(pub BTreeMap); + +impl<'de> Deserialize<'de> for SerialNumber { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Choice { + Map(BTreeMap), + Other(StringOrNumber), + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrNumber { + String(String), + Number(i64), + UnsignedNumber(u64), + Float(f64), + } + + impl Display for StringOrNumber { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::String(s) => s.fmt(formatter), + Self::Number(n) => n.fmt(formatter), + Self::UnsignedNumber(n) => n.fmt(formatter), + Self::Float(f) => f.fmt(formatter), + } + } + } + + Choice::deserialize(deserializer).map(|choice| match choice { + Choice::Other(text) => SerialNumber(BTreeMap::from_iter(vec![( + "serial".to_owned(), + text.to_string(), + )])), + Choice::Map(map) => { + SerialNumber(map.into_iter().map(|(k, v)| (k, v.to_string())).collect()) + } + }) + } +} diff --git a/crates/hayagriva-format/src/types/person.rs b/crates/hayagriva-format/src/types/person.rs new file mode 100644 index 00000000..653dc11d --- /dev/null +++ b/crates/hayagriva-format/src/types/person.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; + +use hayagriva_core::Person; +use hayagriva_core::util::{deserialize_one_or_many, serialize_one_or_many}; + +/// A list of persons with a common role. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct PersonsWithRoles { + /// The persons. + #[serde(serialize_with = "serialize_one_or_many")] + #[serde(deserialize_with = "deserialize_one_or_many")] + pub names: Vec, + /// The role the persons had in the creation of the cited item. + pub role: PersonRole, +} + +impl PersonsWithRoles { + /// Create a new list of persons with a common role. + pub fn new(names: Vec, role: PersonRole) -> Self { + Self { names, role } + } +} + +/// Specifies the role a group of persons had in the creation to the +/// cited item. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[non_exhaustive] +#[serde(rename_all = "kebab-case")] +pub enum PersonRole { + /// Translated the work from a foreign language to the cited edition. + Translator, + /// Authored an afterword. + Afterword, + /// Authored an foreword. + Foreword, + /// Authored an introduction. + Introduction, + /// Provided value-adding annotations. + Annotator, + /// Commented the work. + Commentator, + /// Holds a patent or similar. + Holder, + /// Compiled the works in an [Anthology](hayagriva_core::EntryType::Anthology). + Compiler, + /// Founded the publication. + Founder, + /// Collaborated on the cited item. + Collaborator, + /// Organized the creation of the cited item. + Organizer, + /// Performed in the cited item. + CastMember, + /// Composed all or parts of the cited item's musical / audible components. + Composer, + /// Produced the cited item. + Producer, + /// Lead Producer for the cited item. + ExecutiveProducer, + /// Did the writing for the cited item. + Writer, + /// Shot film/video for the cited item. + Cinematography, + /// Directed the cited item. + Director, + /// Illustrated the cited item. + Illustrator, + /// Provided narration or voice-over for the cited item. + Narrator, + + /// Various other roles described by the contained string. + #[serde(skip)] + Unknown(String), +} diff --git a/crates/hayagriva-format/src/types/time.rs b/crates/hayagriva-format/src/types/time.rs new file mode 100644 index 00000000..776d9e09 --- /dev/null +++ b/crates/hayagriva-format/src/types/time.rs @@ -0,0 +1,338 @@ +use std::{fmt::Display, str::FromStr}; + +use thiserror::Error; +use unscanny::Scanner; + +use hayagriva_core::{ + derive_or_from_str, deserialize_from_str, parse_unsigned_int, serialize_display, +}; + +/// A duration. +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)] +pub struct Duration { + /// Days (24 hours). + pub days: u32, + /// Hours (60 minutes). + pub hours: u32, + /// Minutes (60 seconds). + pub minutes: u32, + /// Seconds (1000 milliseconds). + pub seconds: u8, + /// Milliseconds. + pub milliseconds: u16, +} + +serialize_display!(Duration); +deserialize_from_str!(Duration); + +impl Duration { + fn scan(s: &mut Scanner, require_end: bool) -> Result { + let mut days = 0; + let mut hours = 0; + let mut minutes = 0; + let mut seconds = 0; + let mut milliseconds = 0; + + let idx = s.cursor(); + let colons = + s.eat_while(|c: char| c != '-').chars().filter(|c| c == &':').count(); + s.jump(idx); + + if !(1..=3).contains(&colons) { + return Err(DurationError::Malformed); + } + + let start = 3 - colons; + + for i in start..5 { + match i { + 0 => days = parse_unsigned_int(s, 1..).ok_or(DurationError::Malformed)?, + 1 => { + hours = if start == i { + parse_unsigned_int(s, 2..) + } else { + parse_unsigned_int(s, 2..=2) + } + .ok_or(DurationError::Malformed)? + } + 2 => { + minutes = if start == i { + parse_unsigned_int(s, 2..) + } else { + parse_unsigned_int(s, 2..=2) + } + .ok_or(DurationError::Malformed)? + } + 3 => { + seconds = + parse_unsigned_int(s, 2..=2).ok_or(DurationError::Malformed)? + } + 4 => { + s.eat_whitespace(); + let num = s.eat_while(char::is_numeric); + if num.is_empty() { + return Err(DurationError::Malformed); + } + let str = format!("0.{num}"); + let ms: f64 = str.parse().map_err(|_| DurationError::Malformed)?; + milliseconds = (ms * 1000.0).round() as u16; + } + _ => unreachable!(), + } + + if s.done() { + if i < 3 { + return Err(DurationError::Malformed); + } else { + break; + } + } + + s.eat_whitespace(); + + if i < 3 && !s.eat_if(':') { + return Err(DurationError::Malformed); + } else if i == 3 { + if s.peek() == Some('-') { + break; + } + if !s.eat_if(",") { + return Err(DurationError::Malformed); + } + } + } + + s.eat_whitespace(); + if require_end && !s.done() { + return Err(DurationError::Malformed); + } + + for i in (0..=start).rev() { + match i { + 0 => {} + 1 => { + days = hours / 24; + hours %= 24; + } + 2 => { + hours += minutes / 60; + minutes %= 60; + } + _ => unreachable!(), + } + } + + if hours >= 24 || minutes >= 60 || seconds >= 60 || milliseconds >= 1000 { + return Err(DurationError::TooLarge); + } + + Ok(Duration { days, hours, minutes, seconds, milliseconds }) + } + + /// Get the duration in milliseconds. + fn milliseconds(self) -> u64 { + self.milliseconds as u64 + + self.seconds as u64 * 1000 + + self.minutes as u64 * 60 * 1000 + + self.hours as u64 * 60 * 60 * 1000 + + self.days as u64 * 24 * 60 * 60 * 1000 + } +} + +impl PartialOrd for Duration { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Duration { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.milliseconds().cmp(&other.milliseconds()) + } +} + +/// Errors that can occur when parsing a string to a duration +#[derive(Clone, Copy, Error, Debug, PartialEq, Eq, Hash)] +pub enum DurationError { + /// The string is malformed. + #[error("duration string malformed")] + Malformed, + /// The value is out of bounds when another, subsequent value is present (i.e. `01:61:48`). + #[error("out of bounds value when greater order value is specified")] + TooLarge, +} + +impl FromStr for Duration { + type Err = DurationError; + + fn from_str(s: &str) -> Result { + let mut s = Scanner::new(s); + Self::scan(&mut s, true) + } +} + +impl Display for Duration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.days > 0 { + write!(f, "{}:", self.days)?; + } + + if self.hours > 0 { + write!(f, "{:02}:", self.hours)?; + } + + write!(f, "{:02}:{:02}", self.minutes, self.seconds)?; + + if self.milliseconds > 0 { + write!(f, ",{:03}", self.milliseconds)?; + } + + Ok(()) + } +} + +derive_or_from_str! { + /// An half-open interval of durations. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct DurationRange where "two durations separated by a hyphen or a map with a `from` and `to` field" { + /// The start of the interval. + pub start: Duration, + /// The end of the interval. + pub end: Duration, + } +} + +impl DurationRange { + /// Create a new duration range. + pub fn new(start: Duration, end: Option) -> Self { + Self { start, end: end.unwrap_or(start) } + } + + fn scan(s: &mut Scanner) -> Result { + let start = Duration::scan(s, false)?; + let hyphens = s.eat_while('-'); + + if hyphens.is_empty() { + s.eat_whitespace(); + + if s.done() { + return Ok(Self::new(start, None)); + } else { + return Err(DurationError::Malformed); + } + } + + let end = Duration::scan(s, true)?; + + Ok(Self { start, end }) + } +} + +impl Display for DurationRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.start)?; + if self.start != self.end { + write!(f, "-{}", self.end)?; + } + + Ok(()) + } +} + +impl FromStr for DurationRange { + type Err = DurationError; + + fn from_str(s: &str) -> Result { + let mut s = Scanner::new(s); + Self::scan(&mut s) + } +} + +serialize_display!(DurationRange); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_duration_parse() { + assert_eq!( + Duration::from_str("01:00").unwrap(), + Duration { + days: 0, + hours: 0, + minutes: 1, + seconds: 0, + milliseconds: 0, + } + ); + assert_eq!( + Duration::from_str("00:01:00").unwrap(), + Duration { + days: 0, + hours: 0, + minutes: 1, + seconds: 0, + milliseconds: 0, + } + ); + assert_eq!( + Duration::from_str("00 : 00 : 01 : 00").unwrap(), + Duration { + days: 0, + hours: 0, + minutes: 1, + seconds: 0, + milliseconds: 0, + } + ); + assert_eq!( + Duration::from_str("01:00,000").unwrap(), + Duration { + days: 0, + hours: 0, + minutes: 1, + seconds: 0, + milliseconds: 0, + } + ); + assert!(Duration::from_str("01:00,").is_err()); + assert!(Duration::from_str("010:00,").is_err()); + } + + #[test] + fn test_duration_range_parse() { + assert_eq!( + DurationRange::from_str("01:00").unwrap(), + DurationRange::new( + Duration { + days: 0, + hours: 0, + minutes: 1, + seconds: 0, + milliseconds: 0, + }, + None + ) + ); + assert_eq!( + DurationRange::from_str("01:00-02:00").unwrap(), + DurationRange::new( + Duration { + days: 0, + hours: 0, + minutes: 1, + seconds: 0, + milliseconds: 0, + }, + Some(Duration { + days: 0, + hours: 0, + minutes: 2, + seconds: 0, + milliseconds: 0, + }) + ) + ); + } +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml new file mode 100644 index 00000000..1abdabf8 --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tests" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true +publish = false + +[dev-dependencies] +ciborium = { workspace = true } +citationberg = { workspace = true, features = ["json"] } +hayagriva-archive = { workspace = true } +hayagriva-core = { workspace = true } +hayagriva-csl = { workspace = true } +hayagriva-csl-json = { workspace = true } +hayagriva-format = { workspace = true, features = ["biblatex"] } +heck = "0.5" +serde = { workspace = true } +serde_json = "1" +regex = "1" +unscanny = { workspace = true } + +[[test]] +name = "test" +path = "src/tests.rs" diff --git a/tests/archiver.rs b/tests/src/archiver.rs similarity index 96% rename from tests/archiver.rs rename to tests/src/archiver.rs index cac1fb7c..16ca86c3 100644 --- a/tests/archiver.rs +++ b/tests/src/archiver.rs @@ -1,5 +1,5 @@ //! Generates the archive of locales and styles for the 'archive' feature, as -//! well as the source code of 'src/csl/archive.rs', used to retrieve those +//! well as the source code of 'hayagriva-archive/src/lib.rs', used to retrieve those //! assets. The archive is downloaded from upstream CSL repositories into a //! `target` subfolder, where it's ready to be copied into the `archive` //! folder. @@ -10,30 +10,29 @@ //! these tests (with `--test archiver`) to update the otherwise outdated //! files. -use citationberg::{IndependentStyle, LocaleCode, Style}; -use citationberg::{Locale, LocaleFile, XmlDeError}; -use serde::Serialize; use std::collections::{HashMap, HashSet}; +use std::fmt; use std::fmt::Write; use std::fs; use std::hash::RandomState; use std::io::{self, BufReader, Read}; +use std::iter::{self, FromIterator}; use std::path::{Path, PathBuf}; -use std::{fmt, iter}; -mod common; -use common::{ensure_repo, iter_files, iter_files_with_name}; +use citationberg::{IndependentStyle, LocaleCode, Style}; +use citationberg::{Locale, LocaleFile, XmlDeError}; +use serde::Serialize; -use crate::common::CACHE_PATH; +use super::{CACHE_PATH, ensure_repo, iter_files, iter_files_with_name}; const STYLES_REPO_NAME: &str = "styles"; const CSL_REPO: &str = "https://github.com/citation-style-language/styles"; const LOCALES_REPO: &str = "https://github.com/citation-style-language/locales"; const LOCALES_REPO_NAME: &str = "locales"; -const OWN_STYLES: &str = "styles"; -const ARCHIVE_STYLES_PATH: &str = "archive/styles/"; -const ARCHIVE_LOCALES_PATH: &str = "archive/locales/"; -const ARCHIVE_SRC_PATH: &str = "src/csl/archive.rs"; +const OWN_STYLES: &str = "../styles"; +const ARCHIVE_STYLES_PATH: &str = "../crates/hayagriva-archive/archive/styles/"; +const ARCHIVE_LOCALES_PATH: &str = "../crates/hayagriva-archive/archive/locales/"; +const ARCHIVE_SRC_PATH: &str = "../crates/hayagriva-archive/src/lib.rs"; const UPDATE_ARCHIVES_ENV_VAR: &str = "HAYAGRIVA_ARCHIVER_UPDATE"; @@ -44,7 +43,7 @@ const UPDATE_ARCHIVES_ENV_VAR: &str = "HAYAGRIVA_ARCHIVER_UPDATE"; /// below: /// /// ```sh -/// cargo test --test archiver -- --ignored +/// cargo test --test test -- archiver --ignored /// ``` #[test] #[ignore] @@ -252,8 +251,8 @@ fn write_styles_section( w: &mut String, items: &[(Vec, IndependentStyle, Vec, String)], ) -> fmt::Result { - writeln!(w, "//! Optional archive of included CSL styles.")?; - writeln!(w, "// This file is generated by tests/archive.rs")?; + writeln!(w, "//! Archive CSL styles. Included in Typst.")?; + writeln!(w, "// This file is generated by tests/src/archiver.rs")?; writeln!(w, "// Do not edit by hand!")?; writeln!(w)?; writeln!(w, "use citationberg::{{Locale, Style}};")?; @@ -326,7 +325,7 @@ fn write_styles_section( writeln!( w, - " Self::{variant} => include_bytes!(\"../../archive/styles/{stripped_id}.cbor\")," + " Self::{variant} => include_bytes!(\"../archive/styles/{stripped_id}.cbor\")," )?; } writeln!(w, " }}")?; @@ -386,11 +385,11 @@ fn write_locales_section(w: &mut String, items: &[(Vec, Locale)]) -> fmt::Re writeln!(w)?; writeln!(w, "/// CBOR-encoded CSL locales.")?; - writeln!(w, "pub const LOCALES: &[&[u8]] = &[")?; + writeln!(w, "const LOCALES: &[&[u8]] = &[")?; for (_, locale) in items { writeln!( w, - " include_bytes!(\"../../archive/locales/{}.cbor\"),", + " include_bytes!(\"../archive/locales/{}.cbor\"),", locale.lang.as_ref().unwrap() )?; } diff --git a/tests/citeproc.rs b/tests/src/citeproc.rs similarity index 93% rename from tests/citeproc.rs rename to tests/src/citeproc.rs index c827311f..f42e8c05 100644 --- a/tests/citeproc.rs +++ b/tests/src/citeproc.rs @@ -6,19 +6,20 @@ use std::str::FromStr; use std::sync::OnceLock; use std::{fmt, fs}; -mod common; +use citationberg::json as csl_json; use citationberg::taxonomy::Locator; use citationberg::{Locale, LocaleCode, Style, XmlDeError}; -use common::{CACHE_PATH, ensure_repo, iter_files_with_name}; +use unscanny::Scanner; -use citationberg::json as csl_json; -use hayagriva::archive::{ArchivedStyle, locales}; -use hayagriva::io::from_biblatex_str; -use hayagriva::{ +use hayagriva_archive::{ArchivedStyle, locales}; +use hayagriva_csl::{ BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, CitePurpose, - Entry, LocatorPayload, SpecificLocator, + LocatorPayload, SpecificLocator, }; -use unscanny::Scanner; +use hayagriva_format::Entry; +use hayagriva_format::io::from_biblatex_str; + +use super::{CACHE_PATH, ensure_repo, iter_files_with_name}; const TEST_REPO_NAME: &str = "test-suite"; const TEST_REPO: &str = "https://github.com/citation-style-language/test-suite"; @@ -223,7 +224,7 @@ struct TestCase { mode: TestMode, result: String, csl: Style, - input: Vec, + input: Vec, bib_entries: Option>>, bib_section: Option, citation_items: Option>>, @@ -362,7 +363,7 @@ fn test_parse_tests() { if fail { panic!( - "⚠️ Some test passed but were not found in the `passing` file.\nRebuild the file by running `cargo test --test citeproc --features csl-json -- write_passing --ignored`" + "⚠️ Some test passed but were not found in the `passing` file.\nRebuild the file by running `cargo test --test test -- citeproc::write_passing --ignored`" ) } @@ -452,7 +453,7 @@ fn test_single_file() { #[test] fn test_local_files() { let locales = locales(); - let test_path = PathBuf::from("tests/local"); + let test_path = PathBuf::from("local"); for path in iter_files_with_name(&test_path, "txt", |_| true) { let case = match build_case(&std::fs::read_to_string(&path).unwrap()) { @@ -495,7 +496,7 @@ where let contains_date_ranges = case .input .iter() - .flat_map(|i| i.0.values()) + .flat_map(|i| i.0.0.values()) .filter_map(|v| if let csl_json::Value::Date(d) = v { Some(d) } else { None }) .any(|d| { csl_json::FixedDateRange::try_from(d.clone()).is_ok_and(|d| d.end.is_some()) @@ -519,7 +520,7 @@ where eprintln!("Skipping test {}\t(cause: date ranges)", display()); } false - } else if case.input.iter().any(|i| i.has_html() || i.may_have_hack()) { + } else if case.input.iter().any(|i| i.0.has_html() || i.0.may_have_hack()) { if print { eprintln!("Skipping test {}\t(cause: HTML suspected in input)", display()); } @@ -538,7 +539,8 @@ where panic!("test {} has dependent style", display()); }; - let mut driver: BibliographyDriver<'_, csl_json::Item> = BibliographyDriver::new(); + let mut driver: BibliographyDriver<'_, hayagriva_csl_json::Item> = + BibliographyDriver::new(); let mut output = String::new(); if let Some(cites) = &case.citation_items { for cite in cites { @@ -548,7 +550,7 @@ where CitationItem::new( case.input .iter() - .find(|e| e.id().unwrap_or_default() == i.id.as_str()) + .find(|e| e.0.id().unwrap_or_default() == i.id.as_str()) .unwrap(), i.locator.as_deref().map(|lo| { SpecificLocator( @@ -562,7 +564,7 @@ where None, false, if i.suppress_author { - Some(hayagriva::CitePurpose::Author) + Some(hayagriva_csl::CitePurpose::Author) } else { None }, @@ -592,7 +594,7 @@ where for citation in rendered.citations { citation .citation - .write_buf(&mut output, hayagriva::BufWriteFormat::Plain) + .write_buf(&mut output, hayagriva_csl::BufWriteFormat::Plain) .unwrap(); output.push('\n'); } @@ -642,10 +644,10 @@ mod citeproc_bib { use citationberg::{ Display, FontStyle, FontVariant, FontWeight, TextDecoration, VerticalAlign, }; - use hayagriva::{BufWriteFormat, Elem, ElemChild, Formatting}; + use hayagriva_csl::{BufWriteFormat, Elem, ElemChild, Formatting}; pub(super) fn render( - bib: &hayagriva::RenderedBibliography, + bib: &hayagriva_csl::RenderedBibliography, output: &mut String, is_full: bool, ) -> Result<(), fmt::Error> { @@ -658,7 +660,7 @@ mod citeproc_bib { } fn render_item( - item: &hayagriva::BibliographyItem, + item: &hayagriva_csl::BibliographyItem, output: &mut String, is_full: bool, ) -> Result<(), fmt::Error> { @@ -713,7 +715,7 @@ mod citeproc_bib { /// this notation, especially since we do not expose csl-json input to /// hayagriva users anyway. fn render_formatted_text( - text: &hayagriva::Formatted, + text: &hayagriva_csl::Formatted, output: &mut String, ) -> Result<(), fmt::Error> { let formatting = text.formatting; @@ -831,8 +833,9 @@ fn purposes() { panic!("test has dependent style"); }; - let item: csl_json::Item = serde_json::from_str( - r#"{ + let item = hayagriva_csl_json::Item( + serde_json::from_str( + r#"{ "id": "ITEM-1", "title": "Book A", "author": [ @@ -850,8 +853,9 @@ fn purposes() { }, "type": "book" }"#, - ) - .unwrap(); + ) + .unwrap(), + ); for (purpose, res) in [ (CitePurpose::Author, "Doe"), @@ -859,7 +863,7 @@ fn purposes() { (CitePurpose::Year, "2000"), (CitePurpose::Full, "Doe, J. (2000). Book A."), ] { - let mut driver: BibliographyDriver<'_, csl_json::Item> = + let mut driver: BibliographyDriver<'_, hayagriva_csl_json::Item> = BibliographyDriver::new(); driver.citation(CitationRequest::new( vec![CitationItem::new(&item, None, None, false, Some(purpose))], @@ -873,7 +877,7 @@ fn purposes() { let mut buf = String::new(); rendered.citations[0] .citation - .write_buf(&mut buf, hayagriva::BufWriteFormat::Plain) + .write_buf(&mut buf, hayagriva_csl::BufWriteFormat::Plain) .unwrap(); assert_eq!(buf, res); } @@ -883,9 +887,7 @@ fn purposes() { /// The file should be located at `tests/citeproc-pass.txt`. /// It contains one test case name per line. fn load_passing_tests() -> Vec { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("citeproc-pass.txt"); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("citeproc-pass.txt"); std::fs::read_to_string(path) .unwrap() .lines() @@ -895,15 +897,13 @@ fn load_passing_tests() -> Vec { } /// Write the names of the test cases that should pass to the file -/// `tests/citeproc-pass.txt`. +/// `citeproc-pass.txt`. fn write_passing_test(tests: &[String]) { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("citeproc-pass.txt"); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("citeproc-pass.txt"); let comment = "# This file contains all test cases that should pass.\n\ # The test fails if a test case in this file fails or if a test case\n\ # that is not in this file passes.\n#\n\ - # This file is automatically generated by running `cargo test --test citeproc --features csl-json -- write_passing --ignored`.\n\n"; + # This file is automatically generated by running `cargo test --test test -- citeproc::write_passing --ignored`.\n\n"; let mut w = BufWriter::new(fs::File::create(path).unwrap()); w.write_all(comment.as_bytes()).unwrap(); @@ -921,16 +921,19 @@ fn case_folding() { panic!("test has dependent style"); }; - let item: csl_json::Item = serde_json::from_str( - r#"{ + let item: hayagriva_csl_json::Item = hayagriva_csl_json::Item( + serde_json::from_str( + r#"{ "id": "ITEM-1", "container-title": "my lowercase container title", "type": "paper-conference" }"#, - ) - .unwrap(); + ) + .unwrap(), + ); - let mut driver: BibliographyDriver<'_, csl_json::Item> = BibliographyDriver::new(); + let mut driver: BibliographyDriver<'_, hayagriva_csl_json::Item> = + BibliographyDriver::new(); driver.citation(CitationRequest::new( vec![CitationItem::new(&item, None, None, false, None)], &style, @@ -947,7 +950,7 @@ fn case_folding() { let mut buf = String::new(); rendered.bibliography.unwrap().items[0] .content - .write_buf(&mut buf, hayagriva::BufWriteFormat::Plain) + .write_buf(&mut buf, hayagriva_csl::BufWriteFormat::Plain) .unwrap(); assert_eq!(buf, "my lowercase container title."); } @@ -983,7 +986,7 @@ fn access_date() { let mut buf = String::new(); rendered.bibliography.unwrap().items[0] .content - .write_buf(&mut buf, hayagriva::BufWriteFormat::Plain) + .write_buf(&mut buf, hayagriva_csl::BufWriteFormat::Plain) .unwrap(); assert_eq!(buf, "(n.d.). Retrieved 2021, from https://example.com/"); } @@ -1024,7 +1027,7 @@ fn no_author() { let mut buf = String::new(); rendered.bibliography.unwrap().items[0] .content - .write_buf(&mut buf, hayagriva::BufWriteFormat::Plain) + .write_buf(&mut buf, hayagriva_csl::BufWriteFormat::Plain) .unwrap(); assert_eq!( @@ -1035,7 +1038,7 @@ fn no_author() { let mut buf = String::new(); rendered.citations[0] .citation - .write_buf(&mut buf, hayagriva::BufWriteFormat::Plain) + .write_buf(&mut buf, hayagriva_csl::BufWriteFormat::Plain) .unwrap(); assert_eq!(buf, "(Definition and Objectives of Systems Development, 2016)"); } diff --git a/tests/common/mod.rs b/tests/src/tests.rs similarity index 94% rename from tests/common/mod.rs rename to tests/src/tests.rs index 2e16f7e3..ee92c189 100644 --- a/tests/common/mod.rs +++ b/tests/src/tests.rs @@ -1,6 +1,11 @@ -use std::{fs, io, path::PathBuf, process::Command}; +mod archiver; +mod citeproc; -pub const CACHE_PATH: &str = "target/haya-cache"; +use std::path::PathBuf; +use std::process::Command; +use std::{fs, io}; + +pub const CACHE_PATH: &str = "../target/haya-cache"; #[allow(unused)] pub fn iter_files<'a>(