use anyhow::{Result, anyhow};
use hayagriva::{
Library,
archive::ArchivedStyle,
citationberg::{IndependentStyle, Style},
io::{from_biblatex_str, from_yaml_str},
};
use itertools::Itertools;
use mdbook_core::{config::Config as MdbookConfig, utils::fs};
use std::{path::PathBuf, str::FromStr};
use tracing::warn;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum BibliographyLocation {
Global,
Chapter,
Footnotes,
}
impl FromStr for BibliographyLocation {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"global" => Ok(BibliographyLocation::Global),
"chapter" => Ok(BibliographyLocation::Chapter),
"footnotes" => Ok(BibliographyLocation::Footnotes),
_ => Err(anyhow!(
"Unknown bibliography location: {}. Supported locations are: global, chapter, footnotes",
s
)),
}
}
}
#[derive(Debug)]
pub(crate) struct Config {
pub title: String,
pub bib: Option<Library>,
pub style: IndependentStyle,
pub errors_on_missing_keys: bool,
pub biblio_location: BibliographyLocation,
pub display_all: bool,
pub citation_preview: bool,
}
impl TryFrom<&MdbookConfig> for Config {
type Error = anyhow::Error;
fn try_from(mdbook_conf: &MdbookConfig) -> Result<Self, Self::Error> {
let title = mdbook_conf
.get::<String>("preprocessor.bibtex.title")?
.unwrap_or_else(|| "Bibliography".to_string());
let bib = mdbook_conf
.get::<String>("preprocessor.bibtex.bibliography")?
.map(PathBuf::from)
.map(|bib_file| match bib_file.extension() {
Some(ext) if ext == "bib" => {
let content = fs::read_to_string(bib_file)?;
from_biblatex_str(&content).map_err(|e| {
anyhow!("Failed to parse BibTeX file:\n{}", e.into_iter().join("\n"))
})
}
Some(ext) if ext == "yaml" => {
let content = fs::read_to_string(bib_file)?;
Ok(from_yaml_str(&content)?)
}
_ => Err(anyhow!(
"Unsupported bibliography file format. Supported formats are .bib and .yaml"
)),
})
.transpose()?;
let style = style_from_str(
&mdbook_conf
.get::<String>("preprocessor.bibtex.style")?
.unwrap_or_else(|| "ieee".to_string()),
)?;
let errors_on_missing_keys = mdbook_conf
.get::<bool>("preprocessor.bibtex.errors_on_missing_keys")?
.unwrap_or(true);
let biblio_location = mdbook_conf
.get::<String>("preprocessor.bibtex.biblio_location")?
.map(|s| s.parse())
.transpose()?
.unwrap_or(BibliographyLocation::Global);
let display_all = mdbook_conf
.get::<bool>("preprocessor.bibtex.display_all")?
.unwrap_or(true);
let mut citation_preview = mdbook_conf
.get::<bool>("preprocessor.bibtex.citation_preview")?
.unwrap_or(true);
if biblio_location == BibliographyLocation::Footnotes && citation_preview {
warn!(
"Citation preview is not compatible with footnote bibliography location and will be disabled."
);
citation_preview = false;
}
Ok(Config {
title,
bib,
style,
errors_on_missing_keys,
biblio_location,
display_all,
citation_preview,
})
}
}
fn style_from_str(style: &str) -> Result<IndependentStyle> {
if style.ends_with(".xml") {
Ok(IndependentStyle::from_xml(style)?)
} else {
ArchivedStyle::by_name(style.to_lowercase().as_str())
.ok_or_else(|| {
anyhow!(
"Unknown citation style: {}. Supported styles are: {}",
style,
ArchivedStyle::all()
.iter()
.filter(|s| match s.get() {
Style::Independent(i) => i.bibliography.is_some(),
Style::Dependent(_) => false,
})
.flat_map(|s| s.names())
.join(", ")
)
})
.and_then(|style| match style.get() {
Style::Independent(style) if style.bibliography.is_some() => Ok(style),
Style::Independent(_) => Err(anyhow!(
"{} does not have a bibliography style and cannot be used. See https://github.com/typst/typst/issues/2707#issuecomment-1824488390 for details.",
style.display_name()
)),
Style::Dependent(_) => Err(anyhow!(
"{} is a dependent style but only independent styles are supported",
style.display_name()
)),
})
}
}