summaryrefslogtreecommitdiff
path: root/support/texlab/crates/bibtex-utils
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2023-08-13 03:03:49 +0000
committerNorbert Preining <norbert@preining.info>2023-08-13 03:03:49 +0000
commit03f1daec22e3e5e2ae8d117d503ee9648c5b3f91 (patch)
tree1d0fde49b1a45b2263d5688a2e82cec643aea65b /support/texlab/crates/bibtex-utils
parent1671b301fbe6cef52ccfa6085824e9677703e023 (diff)
CTAN sync 202308130303
Diffstat (limited to 'support/texlab/crates/bibtex-utils')
-rw-r--r--support/texlab/crates/bibtex-utils/Cargo.toml23
-rw-r--r--support/texlab/crates/bibtex-utils/src/field.rs4
-rw-r--r--support/texlab/crates/bibtex-utils/src/field/author.rs80
-rw-r--r--support/texlab/crates/bibtex-utils/src/field/date.rs85
-rw-r--r--support/texlab/crates/bibtex-utils/src/field/number.rs60
-rw-r--r--support/texlab/crates/bibtex-utils/src/field/text.rs306
-rw-r--r--support/texlab/crates/bibtex-utils/src/lib.rs1
7 files changed, 559 insertions, 0 deletions
diff --git a/support/texlab/crates/bibtex-utils/Cargo.toml b/support/texlab/crates/bibtex-utils/Cargo.toml
new file mode 100644
index 0000000000..7dd75ff738
--- /dev/null
+++ b/support/texlab/crates/bibtex-utils/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "bibtex-utils"
+version = "0.0.0"
+license.workspace = true
+authors.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+chrono = { version = "0.4.26", default-features = false, features = ["std"] }
+human_name = "2.0.2"
+itertools = "0.11.0"
+rowan = "0.15.11"
+rustc-hash = "1.1.0"
+syntax = { path = "../syntax" }
+unicode-normalization = "0.1.22"
+
+[lib]
+doctest = false
+
+[dev-dependencies]
+expect-test = "1.4.1"
+parser = { path = "../parser" }
diff --git a/support/texlab/crates/bibtex-utils/src/field.rs b/support/texlab/crates/bibtex-utils/src/field.rs
new file mode 100644
index 0000000000..d9f3adacab
--- /dev/null
+++ b/support/texlab/crates/bibtex-utils/src/field.rs
@@ -0,0 +1,4 @@
+pub mod author;
+pub mod date;
+pub mod number;
+pub mod text;
diff --git a/support/texlab/crates/bibtex-utils/src/field/author.rs b/support/texlab/crates/bibtex-utils/src/field/author.rs
new file mode 100644
index 0000000000..d8576cbaa4
--- /dev/null
+++ b/support/texlab/crates/bibtex-utils/src/field/author.rs
@@ -0,0 +1,80 @@
+use std::{borrow::Cow, fmt};
+
+use human_name::Name;
+use itertools::Itertools;
+use syntax::bibtex::Value;
+
+use super::text::TextFieldData;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
+pub enum AuthorField {
+ Afterword,
+ Annotator,
+ Author,
+ Commentator,
+ Editor,
+ EditorA,
+ EditorB,
+ EditorC,
+ Foreword,
+ Introduction,
+ Translator,
+}
+
+impl AuthorField {
+ pub fn parse(input: &str) -> Option<Self> {
+ Some(match input.to_ascii_lowercase().as_str() {
+ "afterword" => Self::Afterword,
+ "annotator" => Self::Annotator,
+ "author" => Self::Author,
+ "commentator" => Self::Commentator,
+ "editor" => Self::Editor,
+ "editora" => Self::EditorA,
+ "editorb" => Self::EditorB,
+ "editorc" => Self::EditorC,
+ "foreword" => Self::Foreword,
+ "introduction" => Self::Introduction,
+ "translator" => Self::Translator,
+ _ => return None,
+ })
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct AuthorFieldData {
+ pub authors: Vec<Name>,
+}
+
+impl fmt::Display for AuthorFieldData {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let names = self.authors.iter().map(Name::display_initial_surname);
+
+ for part in Itertools::intersperse(names, Cow::Borrowed(", ")) {
+ write!(f, "{}", part)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl AuthorFieldData {
+ pub fn parse(value: &Value) -> Option<Self> {
+ let TextFieldData { text } = TextFieldData::parse(value)?;
+ let mut authors = Vec::new();
+ let mut words = Vec::new();
+ for word in text.split_whitespace() {
+ if word.eq_ignore_ascii_case("and") {
+ authors.push(Name::parse(&words.join(" "))?);
+ words.clear();
+ } else {
+ words.push(word);
+ }
+ }
+
+ if !words.is_empty() {
+ authors.push(Name::parse(&words.join(" "))?);
+ }
+
+ Some(Self { authors })
+ }
+}
diff --git a/support/texlab/crates/bibtex-utils/src/field/date.rs b/support/texlab/crates/bibtex-utils/src/field/date.rs
new file mode 100644
index 0000000000..8e2c0b7d72
--- /dev/null
+++ b/support/texlab/crates/bibtex-utils/src/field/date.rs
@@ -0,0 +1,85 @@
+use std::{fmt, ops::Add, str::FromStr};
+
+use chrono::{Datelike, Month, NaiveDate};
+use syntax::bibtex::Value;
+
+use super::text::TextFieldData;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
+pub enum DateField {
+ Date,
+ EventDate,
+ Month,
+ UrlDate,
+ Year,
+}
+
+impl DateField {
+ pub fn parse(input: &str) -> Option<Self> {
+ Some(match input.to_ascii_lowercase().as_str() {
+ "date" => Self::Date,
+ "eventdate" => Self::EventDate,
+ "month" => Self::Month,
+ "urldate" => Self::UrlDate,
+ "year" => Self::Year,
+ _ => return None,
+ })
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub enum DateFieldData {
+ Date(NaiveDate),
+ Year(i32),
+ Month(Month),
+ Other(String),
+}
+
+impl Add for DateFieldData {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ match (self, rhs) {
+ (date @ Self::Year(_), Self::Year(_))
+ | (date @ Self::Month(_), Self::Month(_))
+ | (date @ Self::Date(_), Self::Date(_))
+ | (Self::Other(_), date)
+ | (date, Self::Other(_)) => date,
+ (Self::Year(year), Self::Month(month)) | (Self::Month(month), Self::Year(year)) => {
+ let new_date = NaiveDate::from_ymd_opt(year, month.number_from_month(), 1).unwrap();
+ Self::Date(new_date)
+ }
+ (Self::Year(year), Self::Date(date)) | (Self::Date(date), Self::Year(year)) => {
+ let new_date = date.with_year(year).unwrap_or(date);
+ Self::Date(new_date)
+ }
+ (Self::Date(date), Self::Month(month)) | (Self::Month(month), Self::Date(date)) => {
+ let new_date = date.with_month(month.number_from_month()).unwrap_or(date);
+ Self::Date(new_date)
+ }
+ }
+ }
+}
+
+impl fmt::Display for DateFieldData {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Year(year) => write!(f, "{}", year),
+ Self::Date(date) => write!(f, "{}", date.format("%b. %Y")),
+ Self::Month(month) => write!(f, "{}", month.name()),
+ Self::Other(text) => write!(f, "{}", text),
+ }
+ }
+}
+
+impl DateFieldData {
+ pub fn parse(value: &Value) -> Option<Self> {
+ let TextFieldData { text } = TextFieldData::parse(value)?;
+ NaiveDate::from_str(&text)
+ .ok()
+ .map(Self::Date)
+ .or_else(|| text.parse().ok().map(Self::Year))
+ .or_else(|| text.parse().ok().map(Self::Month))
+ .or(Some(Self::Other(text)))
+ }
+}
diff --git a/support/texlab/crates/bibtex-utils/src/field/number.rs b/support/texlab/crates/bibtex-utils/src/field/number.rs
new file mode 100644
index 0000000000..227f3d5b75
--- /dev/null
+++ b/support/texlab/crates/bibtex-utils/src/field/number.rs
@@ -0,0 +1,60 @@
+use std::fmt;
+
+use syntax::bibtex::Value;
+
+use super::text::TextFieldData;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
+pub enum NumberField {
+ Edition,
+ Number,
+ Pages,
+ PageTotal,
+ Part,
+ Volume,
+ Volumes,
+}
+
+impl NumberField {
+ pub fn parse(input: &str) -> Option<Self> {
+ Some(match input.to_ascii_lowercase().as_str() {
+ "edition" => Self::Edition,
+ "number" => Self::Number,
+ "pages" => Self::Pages,
+ "pagetotal" => Self::PageTotal,
+ "part" => Self::Part,
+ "volume" => Self::Volume,
+ "volumes" => Self::Volumes,
+ _ => return None,
+ })
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub enum NumberFieldData {
+ Scalar(u32),
+ Range(u32, u32),
+ Other(String),
+}
+
+impl fmt::Display for NumberFieldData {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Scalar(value) => write!(f, "{}", value),
+ Self::Range(start, end) => write!(f, "{}-{}", start, end),
+ Self::Other(value) => write!(f, "{}", value.replace("--", "-")),
+ }
+ }
+}
+
+impl NumberFieldData {
+ pub fn parse(value: &Value) -> Option<Self> {
+ let TextFieldData { text } = TextFieldData::parse(value)?;
+ text.split_once("--")
+ .or_else(|| text.split_once('-'))
+ .and_then(|(a, b)| Some((a.parse().ok()?, b.parse().ok()?)))
+ .map(|(a, b)| Self::Range(a, b))
+ .or_else(|| text.parse().ok().map(Self::Scalar))
+ .or(Some(Self::Other(text)))
+ }
+}
diff --git a/support/texlab/crates/bibtex-utils/src/field/text.rs b/support/texlab/crates/bibtex-utils/src/field/text.rs
new file mode 100644
index 0000000000..9689264779
--- /dev/null
+++ b/support/texlab/crates/bibtex-utils/src/field/text.rs
@@ -0,0 +1,306 @@
+use rowan::{ast::AstNode, NodeOrToken};
+use rustc_hash::FxHashSet;
+
+use syntax::bibtex::{
+ Accent, Command, CurlyGroup, HasAccentName, HasCommandName, HasName, HasValue, HasWord, Join,
+ Literal, QuoteGroup, Root, SyntaxKind::*, SyntaxToken, Value,
+};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
+pub enum TextField {
+ Abstract,
+ Addendum,
+ BookSubtitle,
+ BookTitle,
+ BookTitleAddon,
+ Chapter,
+ Doi,
+ EditorType,
+ EditorTypeA,
+ EditorTypeB,
+ EditorTypeC,
+ Eid,
+ Eprint,
+ EprintClass,
+ EprintType,
+ EventTitle,
+ EventTitleAddon,
+ Holder,
+ HowPublished,
+ Isbn,
+ Issn,
+ Issue,
+ IssueSubtitle,
+ IssueTitle,
+ IssueTitleAddon,
+ Journal,
+ JournalSubtitle,
+ JournalTitle,
+ JournalTitleAddon,
+ Language,
+ Location,
+ MainTitle,
+ MainSubtitle,
+ MainTitleAddon,
+ Note,
+ OrigLanguage,
+ Publisher,
+ Pubstate,
+ Series,
+ Subtitle,
+ Title,
+ TitleAddon,
+ Type,
+ Unknown,
+ Url,
+ Venue,
+ Version,
+}
+
+impl TextField {
+ pub fn parse(input: &str) -> Option<Self> {
+ Some(match input.to_ascii_lowercase().as_str() {
+ "abstract" => Self::Abstract,
+ "addendum" => Self::Addendum,
+ "booksubtitle" => Self::BookSubtitle,
+ "booktitle" => Self::BookTitle,
+ "booktitleaddon" => Self::BookTitleAddon,
+ "chapter" => Self::Chapter,
+ "doi" => Self::Doi,
+ "editortype" => Self::EditorType,
+ "editortypea" => Self::EditorTypeA,
+ "editortypeb" => Self::EditorTypeB,
+ "editortypec" => Self::EditorTypeC,
+ "eid" => Self::Eid,
+ "eprint" => Self::Eprint,
+ "eprintclass" => Self::EprintClass,
+ "eprinttype" => Self::EprintType,
+ "eventtitle" => Self::EventTitle,
+ "eventtitleaddon" => Self::EventTitleAddon,
+ "holder" => Self::Holder,
+ "howpublished" => Self::HowPublished,
+ "isbn" => Self::Isbn,
+ "issn" => Self::Issn,
+ "issue" => Self::Issue,
+ "issuesubtitle" => Self::IssueSubtitle,
+ "issuetitle" => Self::IssueTitle,
+ "issuetitleaddon" => Self::IssueTitleAddon,
+ "journal" => Self::Journal,
+ "journalsubtitle" => Self::JournalSubtitle,
+ "journaltitle" => Self::JournalTitle,
+ "journaltitleaddon" => Self::JournalTitleAddon,
+ "language" => Self::Language,
+ "location" => Self::Location,
+ "maintitle" => Self::MainTitle,
+ "mainsubtitle" => Self::MainSubtitle,
+ "maintitleaddon" => Self::MainTitleAddon,
+ "note" => Self::Note,
+ "origlanguage" => Self::OrigLanguage,
+ "publisher" => Self::Publisher,
+ "pubstate" => Self::Pubstate,
+ "series" => Self::Series,
+ "subtitle" => Self::Subtitle,
+ "title" => Self::Title,
+ "titleaddon" => Self::TitleAddon,
+ "type" => Self::Type,
+ "unknown" => Self::Unknown,
+ "url" => Self::Url,
+ "venue" => Self::Venue,
+ "version" => Self::Version,
+ _ => return None,
+ })
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Default)]
+pub struct TextFieldData {
+ pub text: String,
+}
+
+impl TextFieldData {
+ pub fn parse(value: &Value) -> Option<Self> {
+ let mut builder = TextFieldDataBuilder::default();
+ builder.visit_value(value)?;
+ Some(builder.data)
+ }
+}
+
+#[derive(Default)]
+struct TextFieldDataBuilder {
+ data: TextFieldData,
+ string_stack: FxHashSet<String>,
+}
+
+impl TextFieldDataBuilder {
+ fn visit_value(&mut self, value: &Value) -> Option<()> {
+ match value {
+ Value::Literal(lit) => {
+ self.visit_literal(lit);
+ }
+ Value::CurlyGroup(group) => {
+ self.visit_curly_group(group)?;
+ }
+ Value::QuoteGroup(group) => {
+ self.visit_quote_group(group)?;
+ }
+ Value::Join(join) => {
+ self.visit_join(join)?;
+ }
+ Value::Accent(accent) => {
+ let _ = self.visit_accent(accent);
+ }
+ Value::Command(command) => {
+ let _ = self.visit_command(command);
+ }
+ };
+
+ Some(())
+ }
+
+ fn visit_literal(&mut self, lit: &Literal) {
+ if lit
+ .name_token()
+ .and_then(|name| self.visit_string_reference(&name))
+ .is_none()
+ {
+ lit.syntax()
+ .text()
+ .for_each_chunk(|text| self.data.text.push_str(text));
+ }
+ }
+
+ fn visit_string_reference(&mut self, name: &SyntaxToken) -> Option<()> {
+ let root = Root::cast(name.parent_ancestors().last()?)?;
+ let name = name.text();
+
+ let value = root
+ .strings()
+ .filter(|string| {
+ string
+ .name_token()
+ .map_or(false, |token| token.text() == name)
+ })
+ .find_map(|string| string.value())?;
+
+ if !self.string_stack.insert(name.to_string()) {
+ return None;
+ }
+
+ let _ = self.visit_value(&value);
+ self.string_stack.remove(name);
+ Some(())
+ }
+
+ fn visit_curly_group(&mut self, group: &CurlyGroup) -> Option<()> {
+ for child in group.syntax().children_with_tokens() {
+ match child {
+ NodeOrToken::Node(node) => {
+ self.visit_value(&Value::cast(node)?)?;
+ }
+ NodeOrToken::Token(token) => {
+ match token.kind() {
+ L_CURLY | R_CURLY => (),
+ WHITESPACE | NBSP => self.data.text.push(' '),
+ _ => self.data.text.push_str(token.text()),
+ };
+ }
+ };
+ }
+
+ Some(())
+ }
+
+ fn visit_quote_group(&mut self, group: &QuoteGroup) -> Option<()> {
+ for child in group.syntax().children_with_tokens() {
+ match child {
+ NodeOrToken::Node(node) => {
+ self.visit_value(&Value::cast(node)?)?;
+ }
+ NodeOrToken::Token(token) => {
+ match token.kind() {
+ QUOTE => (),
+ WHITESPACE | NBSP => self.data.text.push(' '),
+ _ => self.data.text.push_str(token.text()),
+ };
+ }
+ };
+ }
+
+ Some(())
+ }
+
+ fn visit_join(&mut self, join: &Join) -> Option<()> {
+ if let Some(left) = join.left_value() {
+ self.visit_value(&left)?;
+ }
+
+ if let Some(right) = join.right_value() {
+ self.visit_value(&right)?;
+ }
+
+ Some(())
+ }
+
+ fn visit_accent(&mut self, accent: &Accent) -> Option<()> {
+ let name = accent.accent_name_token()?;
+ let word = accent.word_token()?;
+
+ let mut chars = word.text().chars();
+ let a = chars.next()?;
+
+ if chars.next().is_some() {
+ self.data.text.push_str(word.text());
+ } else {
+ let b = match name.text() {
+ r#"\`"# => '\u{0300}',
+ r#"\'"# => '\u{0301}',
+ r#"\^"# => '\u{0302}',
+ r#"\""# => '\u{0308}',
+ r#"\H"# => '\u{030B}',
+ r#"\~"# => '\u{0303}',
+
+ r#"\c"# => '\u{0327}',
+ r#"\k"# => '\u{0328}',
+ r#"\="# => '\u{0304}',
+ r#"\b"# => '\u{0331}',
+ r#"\."# => '\u{0307}',
+ r#"\d"# => '\u{0323}',
+ r#"\r"# => '\u{030A}',
+ r#"\u"# => '\u{0306}',
+ r#"\v"# => '\u{030C}',
+ _ => '\u{0000}',
+ };
+
+ match unicode_normalization::char::compose(a, b) {
+ Some(c) => self.data.text.push(c),
+ None => self.data.text.push_str(word.text()),
+ };
+ }
+
+ Some(())
+ }
+
+ fn visit_command(&mut self, command: &Command) -> Option<()> {
+ let name = command.command_name_token()?;
+ let replacement = match name.text() {
+ r#"\l"# => "\u{0142}",
+ r#"\o"# => "\u{00F8}",
+ r#"\i"# => "\u{0131}",
+ r#"\&"# => "&",
+ r#"\$"# => "$",
+ r#"\{"# => "{",
+ r#"\}"# => "}",
+ r#"\%"# => "%",
+ r#"\#"# => "#",
+ r#"\_"# => "_",
+ r#"\ "# | r#"\,"# | r#"\;"# => " ",
+ r#"\hyphen"# => "-",
+ r#"\TeX"# => "TeX",
+ r#"\LaTeX"# => "LaTeX",
+ text => text,
+ };
+
+ self.data.text.push_str(replacement);
+ Some(())
+ }
+}
diff --git a/support/texlab/crates/bibtex-utils/src/lib.rs b/support/texlab/crates/bibtex-utils/src/lib.rs
new file mode 100644
index 0000000000..5b8f2df9f4
--- /dev/null
+++ b/support/texlab/crates/bibtex-utils/src/lib.rs
@@ -0,0 +1 @@
+pub mod field;