Repository: typst/hayagriva (rust)

Issue:
Bibliography incorrectly adds a space between author initials
### Description

Just as an example I used the style "vancouver-superscript". It works with most styles though. 
 
<img width="414" alt="Bildschirmfoto 2024-02-17 um 22 18 02" src="https://github.com/typst/typst/assets/32485045/cd269380-dd55-4630-92c6-2b457be85983">

According to the official style, there shouldn't be a space between the "R" and the "G", so it should be "Astley RG" and not "Astley R G".

### Reproduction URL

_No response_

### Operating system

Web app

### Typst version

- [X] I am using the latest version of Typst

Pull request:
Fix handling of trailing whitespaces in initials' delimiters
Fixes #137

Some styles, such as `vancouver`, require there to be nothing between initials, so `Astley, Rick Gavin` would be formatted as `Astley, RG` instead of `Astley, R. G.` (more usual) or `Astley, R G` for example. However, even when setting `initializer-with=""` (empty delimiter), Hayagriva was still forcing whitespaces between initials.

It was thought that #150 would have fixed it by only adding whitespaces if a delimiter was indeed specified, but it actually didn't change anything as **there is always a delimiter** - however, it is sometimes **an empty string**. While adding an explicit check for empty strings would have fixed this specific problem, it turns out Hayagriva's behavior of forcing a whitespace to always be added after the delimiter, replacing any of its trailing whitespaces if there are any (otherwise, one is added anyways), is not consistent with other CSL processors. Indeed, several citeproc tests demonstrated that the expected way to add whitespaces between initials is through `initializer-with=". "` instead of `initializer-with="."`. So, I removed Hayagriva's automatic whitespace behavior, **except** when we're replacing hyphens (e.g. for `initializer-with=". "`, `Aa-Bb` should become `A.-B.` and not `A. -B.`).

It is, however, unfortunate that this seems to be somewhat inconsistent with a specific example given by the relevant section of the CSL 1.0.2 spec:

```md
`initialize-with`
    When set, given names are converted to initials. The attribute value is added after each initial (“.” results in “J. J. Doe”).
```

...but most official styles I've seen appear to assume that whitespaces are preserved, using `". "` instead of `"."` in that case. So this seems to be the way to go.

I've also added some tests. Two more citeproc tests are now passing as well.

I'll be keeping this as a draft as I still need to test the case with `initialize="false", initialize-with="."`, but in principle the code seems to preserve whitespace here as well, however whitespaces from the name itself are also kept, which is why `initialize-with="."` is used instead of `initialize-with=". "` by official styles in this case.

Patch:
diff --git a/src/types/persons.rs b/src/types/persons.rs
index 4c05529..8734507 100644
--- a/src/types/persons.rs
+++ b/src/types/persons.rs
@@ -234,8 +234,14 @@ impl Person {
     /// Formats the given name into initials.
     ///
     /// For example, `"Judith Beatrice"` would yield `"J. B."` if the
-    /// `delimiter` argument is set to `Some(".")`, `"Klaus-Peter"` would become
-    /// `"K-P"` without a delimiter.
+    /// `delimiter` argument is set to `Some(". ")`, `"Klaus-Peter"` would
+    /// become `"K-P"` without a delimiter (or with an empty delimiter).
+    ///
+    /// Whitespace at the end of the delimiter is kept when parts are separated
+    /// by whitespace (as in the first example), but not when parts of a
+    /// compound given name are separated by hyphens (for example,
+    /// `"Klaus-Peter"` produces `"K.-P."` instead of `"K. -P."` for a
+    /// delimiter of `Some(". ")`).
     pub fn initials(
         &self,
         buf: &mut impl std::fmt::Write,
@@ -253,15 +259,21 @@ impl Person {
             if let Some(c) = gr.chars().next() {
                 if c.is_whitespace() || c == '-' {
                     if !collect {
+                        let hyphenate = with_hyphen && c == '-';
                         if let Some(delimiter) = delimiter {
-                            buf.write_str(delimiter.trim_end())?;
+                            // Use the given delimiter, including any spaces at
+                            // its end if there was a whitespace, but not if we
+                            // should add a hyphen in a compound given name.
+                            buf.write_str(if hyphenate {
+                                delimiter.trim_end()
+                            } else {
+                                delimiter
+                            })?;
                         }
 
                         collect = true;
-                        if with_hyphen && c == '-' {
-                            buf.write_char(c)?;
-                        } else if delimiter.is_some() {
-                            buf.write_char(' ')?;
+                        if hyphenate {
+                            buf.write_char('-')?;
                         }
                     }
                     continue;
@@ -277,7 +289,7 @@ impl Person {
 
         if non_empty && !collect {
             if let Some(delim) = delimiter {
-                buf.write_str(delim)?;
+                buf.write_str(delim.trim_end())?;
             }
         }
 
@@ -315,7 +327,7 @@ impl Person {
         Ok(())
     }
 
-    /// Get the name with the family name fist, the initials
+    /// Get the name with the family name first, the initials
     /// afterwards, separated by a comma.
     pub fn name_first(&self, initials: bool, prefix_given_name: bool) -> String {
         let mut res = if !prefix_given_name {
@@ -331,7 +343,7 @@ impl Person {
         if initials {
             if self.given_name.is_some() {
                 res += ", ";
-                self.initials(&mut res, Some("."), true).unwrap();
+                self.initials(&mut res, Some(". "), true).unwrap();
             }
         } else if let Some(given_name) = &self.given_name {
             res += ", ";
@@ -361,7 +373,7 @@ impl Person {
 
         if initials {
             if self.given_name.is_some() {
-                self.initials(&mut res, Some("."), true).unwrap();
+                self.initials(&mut res, Some(". "), true).unwrap();
                 res.push(' ');
             }
         } else if let Some(given_name) = &self.given_name {
@@ -536,17 +548,62 @@ mod tests {
         let mut s = String::new();
         let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
         p.initials(&mut s, Some("."), true).unwrap();
+        assert_eq!("C.D.", s);
+
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
+        p.initials(&mut s, Some(". "), true).unwrap();
         assert_eq!("C. D.", s);
 
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
+        p.initials(&mut s, Some(""), true).unwrap();
+        assert_eq!("CD", s);
+
         let mut s = String::new();
         let p = Person::from_strings(vec!["Dissmer", "Courtney Deliah"]).unwrap();
         p.initials(&mut s, None, true).unwrap();
         assert_eq!("CD", s);
 
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
+        p.initials(&mut s, Some(". "), true).unwrap();
+        assert_eq!("H.-J.", s);
+
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
+        p.initials(&mut s, Some("."), true).unwrap();
+        assert_eq!("H.-J.", s);
+
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
+        p.initials(&mut s, Some(""), true).unwrap();
+        assert_eq!("H-J", s);
+
         let mut s = String::new();
         let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
         p.initials(&mut s, None, true).unwrap();
         assert_eq!("H-J", s);
+
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
+        p.initials(&mut s, Some(". "), false).unwrap();
+        assert_eq!("H. J.", s);
+
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
+        p.initials(&mut s, Some("."), false).unwrap();
+        assert_eq!("H.J.", s);
+
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
+        p.initials(&mut s, Some(""), false).unwrap();
+        assert_eq!("HJ", s);
+
+        let mut s = String::new();
+        let p = Person::from_strings(vec!["Günther", "Hans-Joseph"]).unwrap();
+        p.initials(&mut s, None, false).unwrap();
+        assert_eq!("HJ", s);
     }
 
     #[test]
@@ -562,4 +619,16 @@ mod tests {
         p.first_name_with_delimiter(&mut s, Some(".")).unwrap();
         assert_eq!("James T.", s);
     }
+
+    #[test]
+    fn person_name_retrieval_order() {
+        let p =
+            Person::from_strings(vec!["van Dissmer", "Jr.", "Courtney Deliah"]).unwrap();
+        assert_eq!("van Dissmer, Courtney Deliah, Jr.", p.name_first(false, false));
+        assert_eq!("Dissmer, Courtney Deliah van, Jr.", p.name_first(false, true));
+        assert_eq!("van Dissmer, C. D., Jr.", p.name_first(true, false));
+        assert_eq!("Dissmer, C. D. van, Jr.", p.name_first(true, true));
+        assert_eq!("Courtney Deliah van Dissmer Jr.", p.given_first(false));
+        assert_eq!("C. D. van Dissmer Jr.", p.given_first(true));
+    }
 }


Test patch:
diff --git a/tests/citeproc-pass.txt b/tests/citeproc-pass.txt
index 6ae5158..3619e67 100644
--- a/tests/citeproc-pass.txt
+++ b/tests/citeproc-pass.txt
@@ -188,6 +188,7 @@ name_AsianGlyphs
 name_AuthorCountWithSameVarContentAndCombinedTermFail
 name_AuthorCountWithSameVarContentAndCombinedTermSucceed
 name_CelticClanName
+name_CeltsAndToffsNoHyphens
 name_CeltsAndToffsWithHyphens
 name_CiteGroupDelimiterWithYearCollapse
 name_CollapseRoleLabels
@@ -206,6 +207,7 @@ name_InstitutionDecoration
 name_LabelAfterPlural
 name_LabelAfterPluralDecorations
 name_LabelFormatBug
+name_MultipleLiteral
 name_NoNameNode
 name_NonDroppingParticleDefault
 name_OnlyFamilyname