diff --git a/assets/files/works_too.bib b/assets/files/works_too.bib new file mode 100644 index 000000000..8ccbe5d1b --- /dev/null +++ b/assets/files/works_too.bib @@ -0,0 +1,10 @@ +@article{keshav2007read, + title = {How to read a paper}, + author = {Keshav, Srinivasan}, + journal = {ACM SIGCOMM Computer Communication Review}, + volume = {37}, + number = {3}, + pages = {83--84}, + year = {2007}, + publisher = {ACM New York, NY, USA} +} diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs index e25877b15..808d8026d 100644 --- a/library/src/meta/bibliography.rs +++ b/library/src/meta/bibliography.rs @@ -48,13 +48,17 @@ pub struct BibliographyElem { /// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file. #[required] #[parse( - let Spanned { v: path, span } = - args.expect::>("path to bibliography file")?; - let path: EcoString = vm.locate(&path).at(span)?.to_string_lossy().into(); - let _ = load(vm.world(), &path).at(span)?; - path + let Spanned { v: mut paths, span } = + args.expect::>("path to bibliography file")?; + for path in &mut paths.0 { + // resolve paths + *path = vm.locate(&path).at(span)?.to_string_lossy().into(); + } + // check that parsing works + let _ = load(vm.world(), &paths).at(span)?; + paths )] - pub path: EcoString, + pub path: BibPaths, /// The title of the bibliography. /// @@ -70,6 +74,22 @@ pub struct BibliographyElem { pub style: BibliographyStyle, } + +/// A list of bib file paths. +#[derive(Debug, Default, Clone, Hash)] +pub struct BibPaths(Vec); + +cast_from_value! { + BibPaths, + v: EcoString => Self(vec![v]), + v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), +} + +cast_to_value! { + v: BibPaths => v.0.into() +} + + impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked) -> StrResult { @@ -534,22 +554,53 @@ fn create( /// Load bibliography entries from a path. #[comemo::memoize] -fn load(world: Tracked, path: &str) -> StrResult> { - let path = Path::new(path); - let buffer = world.file(path)?; - let src = std::str::from_utf8(&buffer).map_err(|_| "file is not valid utf-8")?; +fn load( + world: Tracked, + paths: &BibPaths, +) -> StrResult> { + let mut result = EcoVec::new(); + + // We might have multiple bib/yaml files + for path in &paths.0 { + let buffer = world.file(Path::new(path.as_str()))?; + let src = std::str::from_utf8(&buffer).map_err(|_| "file is not valid utf-8")?; + let entries = parse_bib(path, src)?; + result.extend(entries); + } + + // Biblatex only checks for duplicate keys within files + // -> We have to do this between files again + let mut keys = result.iter().map(|r| r.key()).collect::>(); + keys.sort_unstable(); + // Waiting for `slice_partition_dedup` #54279 + let mut duplicates = Vec::new(); + for pair in keys.windows(2) { + if pair[0] == pair[1] { + duplicates.push(pair[0]); + } + } + + if !duplicates.is_empty() { + Err(eco_format!("duplicate bibliography keys: {}", duplicates.join(", "))) + } else { + Ok(result) + } +} + +/// Parse a bibliography file (bib/yml) +fn parse_bib(path_str: &str, src: &str) -> StrResult> { + let path = Path::new(path_str); let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default(); - let entries = match ext.to_lowercase().as_str() { - "yml" => hayagriva::io::from_yaml_str(src).map_err(format_hayagriva_error)?, + match ext.to_lowercase().as_str() { + "yml" => hayagriva::io::from_yaml_str(src).map_err(format_hayagriva_error), "bib" => hayagriva::io::from_biblatex_str(src).map_err(|err| { err.into_iter() .next() - .map(|error| format_biblatex_error(src, error)) - .unwrap_or_else(|| "failed to parse biblatex file".into()) - })?, - _ => return Err("unknown bibliography format".into()), - }; - Ok(entries.into_iter().collect()) + .map(|error| format_biblatex_error(path_str, src, error)) + .unwrap_or_else(|| eco_format!("failed to parse {path_str}")) + }), + _ => Err("unknown bibliography format".into()), + } } /// Format a Hayagriva loading error. @@ -558,13 +609,13 @@ fn format_hayagriva_error(error: YamlBibliographyError) -> EcoString { } /// Format a BibLaTeX loading error. -fn format_biblatex_error(src: &str, error: BibLaTeXError) -> EcoString { +fn format_biblatex_error(path: &str, src: &str, error: BibLaTeXError) -> EcoString { let (span, msg) = match error { BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()), BibLaTeXError::Type(error) => (error.span, error.kind.to_string()), }; let line = src.get(..span.start).unwrap_or_default().lines().count(); - eco_format!("failed to parse biblatex file: {msg} in line {line}") + eco_format!("parsing failed at {path}:{line}: {msg}") } /// Hayagriva only supports strings, but we have a content supplement. To deal diff --git a/tests/ref/meta/bibliography.png b/tests/ref/meta/bibliography.png index 15b99ec00..c5a408af2 100644 Binary files a/tests/ref/meta/bibliography.png and b/tests/ref/meta/bibliography.png differ diff --git a/tests/typ/meta/bibliography.typ b/tests/typ/meta/bibliography.typ index f1dbe3c8d..82cd3a582 100644 --- a/tests/typ/meta/bibliography.typ +++ b/tests/typ/meta/bibliography.typ @@ -1,7 +1,7 @@ // Test citations and bibliographies. --- -// Error: 15-25 failed to parse biblatex file: wrong number of digits in line 5 +// Error: 15-25 parsing failed at ../assets/files/bad.bib:5: wrong number of digits #bibliography("/bad.bib") --- @@ -27,3 +27,13 @@ As described by @netwok], the net-work is a creature of its own. This is close to piratery! @arrgh And quark! @quark + +--- +// Error: 15-43 duplicate bibliography keys: arrgh, distress, glacier-melt, issue201, mcintosh_anxiety, netwok, psychology25, quark, restful, sharing, tolkien54 +#bibliography(("/works.bib", "/works.bib")) + +--- +#set page(width: 200pt) += Multiple Bibs +Now we have multiple bibliographies containing #cite("glacier-melt", "keshav2007read") +#bibliography(("/works.bib", "/works_too.bib"))