From 03f1daec22e3e5e2ae8d117d503ee9648c5b3f91 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Sun, 13 Aug 2023 03:03:49 +0000 Subject: CTAN sync 202308130303 --- support/texlab/crates/bibtex-utils/Cargo.toml | 23 ++ support/texlab/crates/bibtex-utils/src/field.rs | 4 + .../texlab/crates/bibtex-utils/src/field/author.rs | 80 ++++++ .../texlab/crates/bibtex-utils/src/field/date.rs | 85 ++++++ .../texlab/crates/bibtex-utils/src/field/number.rs | 60 ++++ .../texlab/crates/bibtex-utils/src/field/text.rs | 306 +++++++++++++++++++++ support/texlab/crates/bibtex-utils/src/lib.rs | 1 + 7 files changed, 559 insertions(+) create mode 100644 support/texlab/crates/bibtex-utils/Cargo.toml create mode 100644 support/texlab/crates/bibtex-utils/src/field.rs create mode 100644 support/texlab/crates/bibtex-utils/src/field/author.rs create mode 100644 support/texlab/crates/bibtex-utils/src/field/date.rs create mode 100644 support/texlab/crates/bibtex-utils/src/field/number.rs create mode 100644 support/texlab/crates/bibtex-utils/src/field/text.rs create mode 100644 support/texlab/crates/bibtex-utils/src/lib.rs (limited to 'support/texlab/crates/bibtex-utils') 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 { + 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, +} + +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + let mut builder = TextFieldDataBuilder::default(); + builder.visit_value(value)?; + Some(builder.data) + } +} + +#[derive(Default)] +struct TextFieldDataBuilder { + data: TextFieldData, + string_stack: FxHashSet, +} + +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; -- cgit v1.2.3