diff options
Diffstat (limited to 'support/texlab/src/citeproc')
-rw-r--r-- | support/texlab/src/citeproc/bibutils.rs | 188 | ||||
-rw-r--r-- | support/texlab/src/citeproc/mod.rs | 192 | ||||
-rw-r--r-- | support/texlab/src/citeproc/name/mod.rs | 41 | ||||
-rw-r--r-- | support/texlab/src/citeproc/name/parser.lalrpop | 4 | ||||
-rw-r--r-- | support/texlab/src/citeproc/ris.rs | 6 |
5 files changed, 382 insertions, 49 deletions
diff --git a/support/texlab/src/citeproc/bibutils.rs b/support/texlab/src/citeproc/bibutils.rs new file mode 100644 index 0000000000..03a5c2996a --- /dev/null +++ b/support/texlab/src/citeproc/bibutils.rs @@ -0,0 +1,188 @@ +use bibutils_sys::{ + bibl, bibl_free, bibl_freeparams, bibl_init, bibl_initparams, bibl_read, bibl_write, fclose, + fopen, param, BIBL_ADSABSOUT, BIBL_BIBLATEXIN, BIBL_BIBTEXIN, BIBL_BIBTEXOUT, BIBL_COPACIN, + BIBL_EBIIN, BIBL_ENDNOTEIN, BIBL_ENDNOTEOUT, BIBL_ENDNOTEXMLIN, BIBL_ISIOUT, BIBL_MEDLINEIN, + BIBL_MODSIN, BIBL_MODSOUT, BIBL_NBIBIN, BIBL_NBIBOUT, BIBL_OK, BIBL_RISIN, BIBL_RISOUT, + BIBL_WORD2007OUT, BIBL_WORDIN, FILE, +}; +use std::{ffi::CString, fs, mem::MaybeUninit, path::Path}; +use tempfile::tempdir; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum InputFormat { + Bibtex, + Biblatex, + Copac, + Ebi, + Endnote, + EndnoteXml, + Medline, + Mods, + Nbib, + Ris, + Word, +} + +impl InputFormat { + fn read_mode(self) -> u32 { + match self { + Self::Bibtex => BIBL_BIBTEXIN, + Self::Biblatex => BIBL_BIBLATEXIN, + Self::Copac => BIBL_COPACIN, + Self::Ebi => BIBL_EBIIN, + Self::Endnote => BIBL_ENDNOTEIN, + Self::EndnoteXml => BIBL_ENDNOTEXMLIN, + Self::Medline => BIBL_MEDLINEIN, + Self::Mods => BIBL_MODSIN, + Self::Nbib => BIBL_NBIBIN, + Self::Ris => BIBL_RISIN, + Self::Word => BIBL_WORDIN, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum OutputFormat { + Adsabs, + Bibtex, + Endnote, + Isi, + Mods, + Nbib, + Ris, + Word2007, +} + +impl OutputFormat { + fn write_mode(self) -> u32 { + match self { + Self::Adsabs => BIBL_ADSABSOUT, + Self::Bibtex => BIBL_BIBTEXOUT, + Self::Endnote => BIBL_ENDNOTEOUT, + Self::Isi => BIBL_ISIOUT, + Self::Mods => BIBL_MODSOUT, + Self::Nbib => BIBL_NBIBOUT, + Self::Ris => BIBL_RISOUT, + Self::Word2007 => BIBL_WORD2007OUT, + } + } +} + +struct Context { + inner: MaybeUninit<bibl>, +} + +impl Context { + fn new() -> Self { + let mut inner = MaybeUninit::zeroed(); + unsafe { + bibl_init(inner.as_mut_ptr()); + } + Self { inner } + } +} + +impl Drop for Context { + fn drop(&mut self) { + unsafe { + bibl_free(self.inner.as_mut_ptr()); + } + } +} + +unsafe impl Send for Context {} + +struct Params { + inner: MaybeUninit<param>, +} + +impl Params { + fn new(from: InputFormat, to: OutputFormat) -> Self { + let program = CString::new("texlab").unwrap(); + let mut inner = MaybeUninit::zeroed(); + unsafe { + bibl_initparams( + inner.as_mut_ptr(), + from.read_mode() as i32, + to.write_mode() as i32, + program.as_ptr() as *mut i8, + ); + } + Self { inner } + } +} + +impl Drop for Params { + fn drop(&mut self) { + unsafe { + bibl_freeparams(self.inner.as_mut_ptr()); + } + } +} + +unsafe impl Send for Params {} + +struct File { + path: CString, + handle: *mut FILE, +} + +impl File { + fn new<M: Into<Vec<u8>>>(path: &Path, mode: M) -> Self { + let path = CString::new(path.to_str().unwrap()).unwrap(); + let mode = CString::new(mode).unwrap(); + let handle = unsafe { fopen(path.as_ptr(), mode.as_ptr()) }; + Self { path, handle } + } +} + +impl Drop for File { + fn drop(&mut self) { + unsafe { + fclose(self.handle); + } + } +} + +unsafe impl Send for File {} + +pub fn convert(input: &str, from: InputFormat, to: OutputFormat) -> Option<String> { + let mut context = Context::new(); + let mut params = Params::new(from, to); + let dir = tempdir().expect("failed to create a temporary directory"); + + let input_path = dir.path().join("input"); + fs::write(&input_path, input).ok()?; + let input_file = File::new(&input_path, "r"); + unsafe { + let status = bibl_read( + context.inner.as_mut_ptr(), + input_file.handle, + input_file.path.as_ptr() as *mut i8, + params.inner.as_mut_ptr(), + ); + + if status != BIBL_OK as i32 { + return None; + } + } + + let output_path = dir.path().join("output"); + let output_file = File::new(&output_path, "w"); + unsafe { + let status = bibl_write( + context.inner.as_mut_ptr(), + output_file.handle, + params.inner.as_mut_ptr(), + ); + + if status != BIBL_OK as i32 { + return None; + } + } + + // Remove BOM + let data = fs::read(&output_path).ok()?; + let text = String::from_utf8_lossy(&data[3..]).into_owned(); + Some(text) +} diff --git a/support/texlab/src/citeproc/mod.rs b/support/texlab/src/citeproc/mod.rs index 9827c190f3..6def2ed8a4 100644 --- a/support/texlab/src/citeproc/mod.rs +++ b/support/texlab/src/citeproc/mod.rs @@ -1,52 +1,88 @@ +mod bibutils; mod name; mod ris; -use self::ris::*; -use crate::formatting::bibtex::{format_entry, format_string, BibtexFormattingParams}; -use crate::syntax::*; -use bibutils::{InputFormat, OutputFormat}; +use self::{ + bibutils::{InputFormat, OutputFormat}, + ris::{RisLibrary, RisReference}, +}; +use crate::{ + protocol::{BibtexFormattingOptions, MarkupContent, MarkupKind}, + syntax::bibtex, +}; use citeproc::prelude::*; use citeproc_db::PredefinedLocales; -use lsp_types::{MarkupContent, MarkupKind}; +use once_cell::sync::Lazy; +use regex::Regex; use std::sync::Arc; static APA_STYLE: &str = include_str!("apa.csl"); -pub fn render_citation(tree: &BibtexSyntaxTree, key: &str) -> Option<MarkupContent> { - let reference: Reference = convert_to_ris(tree, key)?.into(); +static DOI_URL_PATTERN: &str = r#"https://doi.org/\[.*\]\(.*\)"#; - let html = generate_bibliography(reference)?; - let markdown = html2md::parse_html(&html).trim().to_owned(); +static DOI_URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(DOI_URL_PATTERN).unwrap()); + +pub fn render_citation(tree: &bibtex::Tree, key: &str) -> Option<MarkupContent> { + let ris_reference = convert_to_ris(tree, key)?; + let doi_url = get_doi_url_markdown(&ris_reference); + let csl_reference: Reference = ris_reference.into(); + let html = generate_bibliography(csl_reference)?; + + let mut markdown = html2md::parse_html(&html).trim().to_owned(); if markdown == "" { return None; } + if let Some(doi_url) = doi_url { + markdown = DOI_URL_REGEX + .replace(&markdown, doi_url.as_str()) + .into_owned(); + } + + markdown = markdown + .replace("..", ".") + .replace("\\\'", "'") + .replace("\\-", "-") + .replace("\\\\textsubscript", "") + .replace("\\\\textsuperscript", ""); let content = MarkupContent { kind: MarkupKind::Markdown, - value: markdown.replace("..", "."), + value: markdown, }; Some(content) } -fn convert_to_ris(tree: &BibtexSyntaxTree, key: &str) -> Option<RisReference> { - let bib_params = BibtexFormattingParams::default(); - let mut bib_code = String::new(); +fn convert_to_ris(tree: &bibtex::Tree, key: &str) -> Option<RisReference> { + let options = BibtexFormattingOptions { + line_length: None, + formatter: None, + }; + let params = bibtex::FormattingParams { + insert_spaces: true, + tab_size: 4, + options: &options, + }; - for string in tree.strings() { - bib_code.push_str(&format_string(string, &bib_params)); - bib_code.push('\n'); - } + let mut bib_code = String::new(); + tree.children(tree.root) + .filter(|node| tree.as_string(*node).is_some()) + .map(|node| bibtex::format(tree, node, params)) + .for_each(|string| { + bib_code.push_str(&string); + bib_code.push('\n'); + }); - let entry = tree.find_entry(key)?; - if let Some(crossref) = tree.resolve_crossref(entry) { - bib_code.push_str(&format_entry(crossref, &bib_params)); + let entry = tree.entry_by_key(key)?; + if let Some(crossref) = tree.crossref(entry) { + bib_code.push_str(&bibtex::format(tree, crossref, params)); bib_code.push('\n'); } - bib_code.push_str(&format_entry(entry, &bib_params)); + bib_code.push_str(&bibtex::format(tree, entry, params)); bib_code.push('\n'); + bib_code = bib_code.replace("\\hypen", "-"); - let ris_code = bibutils::convert(bib_code, InputFormat::Biblatex, OutputFormat::Ris)?; + let ris_code = bibutils::convert(&bib_code, InputFormat::Biblatex, OutputFormat::Ris)?; let ris_lib = RisLibrary::parse(ris_code.lines()); ris_lib .references @@ -54,16 +90,124 @@ fn convert_to_ris(tree: &BibtexSyntaxTree, key: &str) -> Option<RisReference> { .find(|reference| reference.id.as_ref().map(AsRef::as_ref) == Some(key)) } +fn get_doi_url_markdown(ris_reference: &RisReference) -> Option<String> { + ris_reference + .doi + .as_ref() + .map(|doi| format!("[doi:{}](https://doi.org/{})", doi, doi)) +} + fn generate_bibliography(reference: Reference) -> Option<String> { let locales = Arc::new(PredefinedLocales::bundled_en_us()); let mut processor = Processor::new(APA_STYLE, locales, false, SupportedFormat::Html).unwrap(); let cite = Cite::basic(&reference.id); - let cluster = Cluster2::Note { + let cluster = Cluster { id: 1, - note: IntraNote::Single(1), cites: vec![cite], }; processor.insert_reference(reference); processor.init_clusters(vec![cluster]); + processor + .set_cluster_order(&[ClusterPosition { + id: 1, + note: Some(1), + }]) + .unwrap(); processor.get_bibliography().pop() } + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + + #[test] + fn simple() { + let tree = bibtex::open(indoc!( + r#" + @article{foo, + author = {Foo Bar}, + title = {Baz Qux}, + year = {2020} + } + "# + )); + + let actual_md = render_citation(&tree, "foo").unwrap(); + + let expected_md = MarkupContent { + kind: MarkupKind::Markdown, + value: "Bar, F. (2020). *Baz Qux*.".into(), + }; + + assert_eq!(actual_md, expected_md); + } + + #[test] + fn crossref() { + let tree = bibtex::open(indoc!( + r#" + https://tex.stackexchange.com/questions/401138/what-is-the-bibtex-crossref-field-used-for + + @inproceedings{duck2015, + author = {Duck, D.}, + title = {Duck tales}, + crossref = {ICRC2015}, + } + + @inproceedings{mouse2015, + author = {Mouse, M.}, + title = {Mouse stories}, + crossref = {ICRC2015}, + } + + @proceedings{ICRC2015, + title = "{Proceedings of the 34\textsuperscript{th} International Cosmic Ray Conference}", + year = "2015", + month = aug, + } + "# + )); + + let actual_md = render_citation(&tree, "mouse2015").unwrap(); + + let expected_md = MarkupContent { + kind: MarkupKind::Markdown, + value: "Mouse, M. (2015). Mouse stories. In *Proceedings of the 34th International Cosmic Ray Conference*.".into(), + }; + + assert_eq!(actual_md, expected_md); + } + + #[test] + fn string() { + let tree = bibtex::open(indoc!( + r#" + @string{author = "Foo Bar"} + @article{foo, + author = author, + title = {Baz Qux}, + year = {2020} + } + "# + )); + + let actual_md = render_citation(&tree, "foo").unwrap(); + + let expected_md = MarkupContent { + kind: MarkupKind::Markdown, + value: "Bar, F. (2020). *Baz Qux*.".into(), + }; + + assert_eq!(actual_md, expected_md); + } + + #[test] + fn unknown_key() { + let tree = bibtex::open(""); + + let actual_md = render_citation(&tree, "foo"); + + assert_eq!(actual_md, None); + } +} diff --git a/support/texlab/src/citeproc/name/mod.rs b/support/texlab/src/citeproc/name/mod.rs index 7668d0fff8..ef97d30130 100644 --- a/support/texlab/src/citeproc/name/mod.rs +++ b/support/texlab/src/citeproc/name/mod.rs @@ -1,7 +1,10 @@ // Ported from: https://github.com/michel-kraemer/citeproc-java/blob/master/citeproc-java/grammars/InternalName.g4 // Michel Kraemer // Apache License 2.0 -mod parser; +mod parser { + #![allow(warnings)] + include!(concat!(env!("OUT_DIR"), "/citeproc/name/parser.rs")); +} use self::parser::NamesParser; use citeproc_io::Name; @@ -21,7 +24,7 @@ mod tests { use citeproc_io::PersonName; #[test] - fn test_family_only() { + fn family_only() { let name = Name::Person(PersonName { family: Some("Thompson".into()), given: None, @@ -33,7 +36,7 @@ mod tests { } #[test] - fn test_simple() { + fn simple() { let name = Name::Person(PersonName { family: Some("Thompson".into()), given: Some("Ken".into()), @@ -45,7 +48,7 @@ mod tests { } #[test] - fn test_middle_name() { + fn middle_name() { let name = Name::Person(PersonName { family: Some("Ritchie".into()), given: Some("Dennis M.".into()), @@ -57,7 +60,7 @@ mod tests { } #[test] - fn test_initials() { + fn initials() { let name = Name::Person(PersonName { family: Some("Johnson".into()), given: Some("S. C.".into()), @@ -69,7 +72,7 @@ mod tests { } #[test] - fn test_non_dropping_particle() { + fn non_dropping_particle() { let name = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -81,7 +84,7 @@ mod tests { } #[test] - fn test_non_dropping_particle_family_only() { + fn non_dropping_particle_family_only() { let name = Name::Person(PersonName { family: Some("Gerwen".into()), given: None, @@ -93,7 +96,7 @@ mod tests { } #[test] - fn test_comma() { + fn comma() { let name = Name::Person(PersonName { family: Some("Thompson".into()), given: Some("Ken".into()), @@ -105,7 +108,7 @@ mod tests { } #[test] - fn test_comma_junior() { + fn comma_junior() { let name = Name::Person(PersonName { family: Some("Friedman".into()), given: Some("George".into()), @@ -117,7 +120,7 @@ mod tests { } #[test] - fn test_comma_no_junior() { + fn comma_no_junior() { let name = Name::Person(PersonName { family: Some("Familya Familyb".into()), given: Some("Given".into()), @@ -129,7 +132,7 @@ mod tests { } #[test] - fn test_comma_initials() { + fn comma_initials() { let name = Name::Person(PersonName { family: Some("Ritchie".into()), given: Some("Dennis M.".into()), @@ -141,7 +144,7 @@ mod tests { } #[test] - fn test_comma_non_dropping_particle() { + fn comma_non_dropping_particle() { let name = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -153,7 +156,7 @@ mod tests { } #[test] - fn test_comma_non_dropping_particles() { + fn comma_non_dropping_particles() { let name = Name::Person(PersonName { family: Some("Voort".into()), given: Some("Vincent".into()), @@ -165,7 +168,7 @@ mod tests { } #[test] - fn test_and() { + fn and() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -187,7 +190,7 @@ mod tests { } #[test] - fn test_and_comma1() { + fn and_comma1() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -209,7 +212,7 @@ mod tests { } #[test] - fn test_and_comma2() { + fn and_comma2() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -231,7 +234,7 @@ mod tests { } #[test] - fn test_and_comma_mix() { + fn and_comma_mix() { let name1 = Name::Person(PersonName { family: Some("Gerwen".into()), given: Some("Michael".into()), @@ -253,7 +256,7 @@ mod tests { } #[test] - fn test_junior() { + fn junior() { let name = Name::Person(PersonName { family: Some("Friedman".into()), given: Some("George".into()), @@ -265,7 +268,7 @@ mod tests { } #[test] - fn test_non_parseable() { + fn non_parseable() { let literal = "Jerry Peek and Tim O'Reilly and Mike Loukides and other authors of the Nutshell handbooks"; let name = Name::Literal { literal: literal.into(), diff --git a/support/texlab/src/citeproc/name/parser.lalrpop b/support/texlab/src/citeproc/name/parser.lalrpop index 556e3bd361..e7bce8a302 100644 --- a/support/texlab/src/citeproc/name/parser.lalrpop +++ b/support/texlab/src/citeproc/name/parser.lalrpop @@ -144,9 +144,7 @@ Word: &'input str = { LWord => (<>), }; + UWord: &'input str = r"[A-Z\u00C0-\uFFFF(?][A-Z\u00C0-\uFFFF(?a-z\-)&/.]+" => (<>); LWord: &'input str = r"[a-z\-)&/.][A-Z\u00C0-\uFFFF(?a-z\-)&/.]+" => (<>); - - - diff --git a/support/texlab/src/citeproc/ris.rs b/support/texlab/src/citeproc/ris.rs index 4a0379a698..1cb5754aec 100644 --- a/support/texlab/src/citeproc/ris.rs +++ b/support/texlab/src/citeproc/ris.rs @@ -226,8 +226,8 @@ impl RisLibrary { continue; } - let key: String = (&chars[..2]).into_iter().collect(); - let value: String = (&chars[6..]).into_iter().collect(); + let key: String = (&chars[..2]).iter().collect(); + let value: String = (&chars[6..]).iter().collect(); match key.to_uppercase().as_str() { "TY" => reference.ty = RisType::parse(&value), "A2" => reference.editors.push(value), @@ -347,7 +347,7 @@ impl Into<Reference> for RisReference { if let Some(place) = self.place { ordinary.insert(Variable::EventPlace, place.clone()); - ordinary.insert(Variable::PublisherPlace, place.clone()); + ordinary.insert(Variable::PublisherPlace, place); } if let Some(abstrct) = self.abstrct { |