summaryrefslogtreecommitdiff
path: root/support/texlab/src/citeproc
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/citeproc')
-rw-r--r--support/texlab/src/citeproc/bibutils.rs188
-rw-r--r--support/texlab/src/citeproc/mod.rs192
-rw-r--r--support/texlab/src/citeproc/name/mod.rs41
-rw-r--r--support/texlab/src/citeproc/name/parser.lalrpop4
-rw-r--r--support/texlab/src/citeproc/ris.rs6
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 {