asciidoc_parser/document/
author_line.rs

1use std::slice::Iter;
2
3use crate::{HasSpan, Parser, Span, document::Author};
4
5/// The author line is directly after the document title line in the document
6/// header. When the content on this line is structured correctly, the processor
7/// assigns the content to the built-in author and email attributes.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct AuthorLine<'src> {
10    authors: Vec<Author>,
11    source: Span<'src>,
12}
13
14impl<'src> AuthorLine<'src> {
15    pub(crate) fn parse(source: Span<'src>, parser: &mut Parser) -> Self {
16        let authors: Vec<Author> = source
17            .data()
18            .split("; ")
19            .filter_map(|raw_author| Author::parse(raw_author, parser))
20            .collect();
21
22        for (index, author) in authors.iter().enumerate() {
23            set_nth_attribute(parser, "author", index, author.name());
24            set_nth_attribute(parser, "authorinitials", index, author.initials());
25            set_nth_attribute(parser, "firstname", index, author.firstname());
26
27            if let Some(middlename) = author.middlename() {
28                set_nth_attribute(parser, "middlename", index, middlename);
29            }
30
31            if let Some(lastname) = author.lastname() {
32                set_nth_attribute(parser, "lastname", index, lastname);
33            }
34
35            if let Some(email) = author.email() {
36                set_nth_attribute(parser, "email", index, email);
37            }
38        }
39
40        Self { authors, source }
41    }
42
43    /// Return an iterator over the authors in this author line.
44    pub fn authors(&'src self) -> Iter<'src, Author> {
45        self.authors.iter()
46    }
47}
48
49fn set_nth_attribute<V: AsRef<str>>(parser: &mut Parser, name: &str, index: usize, value: V) {
50    let name = if index == 0 {
51        name.to_string()
52    } else {
53        format!("{name}_{count}", count = index + 1)
54    };
55
56    parser.set_attribute_by_value_from_header(name, value);
57}
58
59impl<'src> HasSpan<'src> for AuthorLine<'src> {
60    fn span(&self) -> Span<'src> {
61        self.source
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use pretty_assertions_sorted::assert_eq;
68
69    use crate::{Parser, parser::ModificationContext, tests::prelude::*};
70
71    #[test]
72    fn empty_line() {
73        let mut parser = Parser::default();
74
75        let al = crate::document::AuthorLine::parse(crate::Span::new(""), &mut parser);
76
77        assert_eq!(
78            &al,
79            AuthorLine {
80                authors: &[],
81                source: Span {
82                    data: "",
83                    line: 1,
84                    col: 1,
85                    offset: 0,
86                },
87            }
88        );
89    }
90
91    #[test]
92    fn attr_sub_with_html_encoding_fallback() {
93        // Test case for code coverage: input contains attributes but after expansion
94        // doesn't match AUTHOR regex and contains angle brackets that need HTML
95        // encoding.
96        let mut parser = Parser::default().with_intrinsic_attribute(
97            "weird-content",
98            "Complex <weird> & stuff",
99            ModificationContext::Anywhere,
100        );
101
102        let al = crate::document::AuthorLine::parse(
103            crate::Span::new("Some {weird-content} pattern"),
104            &mut parser,
105        );
106
107        assert_eq!(
108            al,
109            AuthorLine {
110                authors: &[Author {
111                    name: "Some Complex &lt;weird&gt; &amp; stuff pattern",
112                    firstname: "Some Complex &lt;weird&gt; &amp; stuff pattern",
113                    middlename: None,
114                    lastname: None,
115                    email: None,
116                },],
117                source: Span {
118                    data: "Some {weird-content} pattern",
119                    line: 1,
120                    col: 1,
121                    offset: 0,
122                },
123            }
124        );
125    }
126
127    #[test]
128    fn empty_author() {
129        let mut parser = Parser::default();
130
131        let al = crate::document::AuthorLine::parse(
132            crate::Span::new("Author One; ; Author Three"),
133            &mut parser,
134        );
135
136        assert_eq!(
137            al,
138            AuthorLine {
139                authors: &[
140                    Author {
141                        name: "Author One",
142                        firstname: "Author",
143                        middlename: None,
144                        lastname: Some("One",),
145                        email: None,
146                    },
147                    Author {
148                        name: "Author Three",
149                        firstname: "Author",
150                        middlename: None,
151                        lastname: Some("Three",),
152                        email: None,
153                    },
154                ],
155                source: Span {
156                    data: "Author One; ; Author Three",
157                    line: 1,
158                    col: 1,
159                    offset: 0,
160                },
161            }
162        );
163    }
164
165    #[test]
166    fn one_simple_author() {
167        let mut parser = Parser::default();
168
169        let al = crate::document::AuthorLine::parse(
170            crate::Span::new("Kismet R. Lee <kismet@asciidoctor.org>"),
171            &mut parser,
172        );
173
174        assert_eq!(
175            &al,
176            AuthorLine {
177                authors: &[Author {
178                    name: "Kismet R. Lee",
179                    firstname: "Kismet",
180                    middlename: Some("R.",),
181                    lastname: Some("Lee",),
182                    email: Some("kismet@asciidoctor.org",),
183                },],
184                source: Span {
185                    data: "Kismet R. Lee <kismet@asciidoctor.org>",
186                    line: 1,
187                    col: 1,
188                    offset: 0,
189                },
190            }
191        );
192    }
193
194    #[test]
195    fn author_without_middle_name() {
196        let mut parser = Parser::default();
197
198        let al = crate::document::AuthorLine::parse(
199            crate::Span::new("Doc Writer <doc@example.com>"),
200            &mut parser,
201        );
202
203        assert_eq!(
204            al,
205            AuthorLine {
206                authors: &[Author {
207                    name: "Doc Writer",
208                    firstname: "Doc",
209                    middlename: None,
210                    lastname: Some("Writer",),
211                    email: Some("doc@example.com",),
212                },],
213                source: Span {
214                    data: "Doc Writer <doc@example.com>",
215                    line: 1,
216                    col: 1,
217                    offset: 0,
218                },
219            }
220        );
221    }
222
223    #[test]
224    fn too_many_names() {
225        let mut parser = Parser::default();
226
227        let al = crate::document::AuthorLine::parse(
228            crate::Span::new("Four Names Not Supported <doc@example.com>"),
229            &mut parser,
230        );
231
232        assert_eq!(
233            al,
234            AuthorLine {
235                authors: &[Author {
236                    name: "Four Names Not Supported &lt;doc@example.com&gt;",
237                    firstname: "Four Names Not Supported &lt;doc@example.com&gt;",
238                    middlename: None,
239                    lastname: None,
240                    email: None,
241                },],
242                source: Span {
243                    data: "Four Names Not Supported <doc@example.com>",
244                    line: 1,
245                    col: 1,
246                    offset: 0,
247                },
248            }
249        );
250    }
251
252    #[test]
253    fn one_name() {
254        let mut parser = Parser::default();
255
256        let al = crate::document::AuthorLine::parse(
257            crate::Span::new("John <john@example.com>"),
258            &mut parser,
259        );
260
261        assert_eq!(
262            al,
263            AuthorLine {
264                authors: &[Author {
265                    name: "John",
266                    firstname: "John",
267                    middlename: None,
268                    lastname: None,
269                    email: Some("john@example.com",),
270                },],
271                source: Span {
272                    data: "John <john@example.com>",
273                    line: 1,
274                    col: 1,
275                    offset: 0,
276                },
277            }
278        );
279    }
280
281    #[test]
282    fn underscore_join() {
283        let mut parser = Parser::default();
284
285        let al =
286            crate::document::AuthorLine::parse(crate::Span::new("Mary_Sue Brontë"), &mut parser);
287
288        assert_eq!(
289            al,
290            AuthorLine {
291                authors: &[Author {
292                    name: "Mary Sue Brontë", // Underscore replaced with space
293                    firstname: "Mary Sue",   // Underscore replaced with space
294                    middlename: None,
295                    lastname: Some("Brontë",),
296                    email: None,
297                },],
298                source: Span {
299                    data: "Mary_Sue Brontë",
300                    line: 1,
301                    col: 1,
302                    offset: 0,
303                },
304            }
305        );
306    }
307
308    #[test]
309    fn greek() {
310        let mut parser = Parser::default();
311
312        let al = crate::document::AuthorLine::parse(
313            crate::Span::new("Αλέξανδρος Παπαδόπουλος"),
314            &mut parser,
315        );
316
317        assert_eq!(
318            al,
319            AuthorLine {
320                authors: &[Author {
321                    name: "Αλέξανδρος Παπαδόπουλος",
322                    firstname: "Αλέξανδρος",
323                    middlename: None,
324                    lastname: Some("Παπαδόπουλος",),
325                    email: None,
326                },],
327                source: Span {
328                    data: "Αλέξανδρος Παπαδόπουλος",
329                    line: 1,
330                    col: 1,
331                    offset: 0,
332                },
333            }
334        );
335    }
336
337    #[test]
338    fn japanese() {
339        let mut parser = Parser::default();
340
341        let al = crate::document::AuthorLine::parse(crate::Span::new("山田太郎"), &mut parser);
342
343        assert_eq!(
344            al,
345            AuthorLine {
346                authors: &[Author {
347                    name: "山田太郎",
348                    firstname: "山田太郎",
349                    middlename: None,
350                    lastname: None,
351                    email: None,
352                },],
353                source: Span {
354                    data: "山田太郎",
355                    line: 1,
356                    col: 1,
357                    offset: 0,
358                },
359            }
360        );
361    }
362
363    #[test]
364    fn arabic() {
365        let mut parser = Parser::default();
366
367        let al = crate::document::AuthorLine::parse(crate::Span::new("عبد_الله"), &mut parser);
368
369        assert_eq!(
370            al,
371            AuthorLine {
372                authors: &[Author {
373                    name: "عبد الله",      // Underscore replaced with space
374                    firstname: "عبد الله", // Underscore replaced with space
375                    middlename: None,
376                    lastname: None,
377                    email: None,
378                },],
379                source: Span {
380                    data: "عبد_الله",
381                    line: 1,
382                    col: 1,
383                    offset: 0,
384                },
385            }
386        );
387    }
388
389    #[test]
390    fn underscore_replacement_in_all_name_parts() {
391        let mut parser = Parser::default();
392
393        let al = crate::document::AuthorLine::parse(
394            crate::Span::new("John_Paul Mary_Jane Smith_Jones <email@example.com>"),
395            &mut parser,
396        );
397
398        assert_eq!(
399            al,
400            AuthorLine {
401                authors: &[Author {
402                    name: "John Paul Mary Jane Smith Jones", // Underscore replaced with space
403                    firstname: "John Paul",                  // Underscore replaced with space
404                    middlename: Some("Mary Jane"),           // Underscore replaced with space
405                    lastname: Some("Smith Jones"),           // Underscore replaced with space
406                    email: Some("email@example.com"),
407                },],
408                source: Span {
409                    data: "John_Paul Mary_Jane Smith_Jones <email@example.com>",
410                    line: 1,
411                    col: 1,
412                    offset: 0,
413                },
414            }
415        );
416    }
417
418    #[test]
419    fn multiple_underscores_in_name_parts() {
420        let mut parser = Parser::default();
421
422        let al =
423            crate::document::AuthorLine::parse(crate::Span::new("A_B_C D_E_F G_H_I"), &mut parser);
424
425        assert_eq!(
426            al,
427            AuthorLine {
428                authors: &[Author {
429                    name: "A B C D E F G H I", // Multiple underscores replaced with spaces
430                    firstname: "A B C",        // Multiple underscores replaced with spaces
431                    middlename: Some("D E F"), // Multiple underscores replaced with spaces
432                    lastname: Some("G H I"),   // Multiple underscores replaced with spaces
433                    email: None,
434                },],
435                source: Span {
436                    data: "A_B_C D_E_F G_H_I",
437                    line: 1,
438                    col: 1,
439                    offset: 0,
440                },
441            }
442        );
443    }
444
445    #[test]
446    fn underscore_replacement_with_attribute_substitution() {
447        let mut parser = Parser::default()
448            .with_intrinsic_attribute("first-part", "John_Paul", ModificationContext::Anywhere)
449            .with_intrinsic_attribute("last-part", "Smith_Jones", ModificationContext::Anywhere);
450
451        let al = crate::document::AuthorLine::parse(
452            crate::Span::new("{first-part} {last-part} <email@example.com>"),
453            &mut parser,
454        );
455
456        // Note: This test documents the current behavior where attribute substitution
457        // happens after parsing, which results in HTML encoding of the angle brackets.
458        // The underscore replacement should still work on the attribute-substituted
459        // values.
460        assert_eq!(
461            al,
462            AuthorLine {
463                authors: &[Author {
464                    name: "John Paul Smith Jones &lt;email@example.com&gt;", /* Underscore
465                                                                              * replaced
466                                                                              * with space */
467                    firstname: "John Paul Smith Jones &lt;email@example.com&gt;", /* Underscore
468                                                                                   * replaced with
469                                                                                   * space */
470                    middlename: None,
471                    lastname: None,
472                    email: None,
473                },],
474                source: Span {
475                    data: "{first-part} {last-part} <email@example.com>",
476                    line: 1,
477                    col: 1,
478                    offset: 0,
479                },
480            }
481        );
482    }
483
484    #[test]
485    fn attr_sub_email() {
486        let mut parser = Parser::default()
487            .with_intrinsic_attribute(
488                "jane-email",
489                "jane@example.com",
490                ModificationContext::Anywhere,
491            )
492            .with_intrinsic_attribute(
493                "john-email",
494                "john@example.com",
495                ModificationContext::Anywhere,
496            );
497
498        let al = crate::document::AuthorLine::parse(
499            crate::Span::new("Jane Smith <{jane-email}>; John Doe <{john-email}>"),
500            &mut parser,
501        );
502
503        assert_eq!(
504            al,
505            AuthorLine {
506                authors: &[
507                    Author {
508                        name: "Jane Smith",
509                        firstname: "Jane",
510                        middlename: None,
511                        lastname: Some("Smith",),
512                        email: Some("jane@example.com",),
513                    },
514                    Author {
515                        name: "John Doe",
516                        firstname: "John",
517                        middlename: None,
518                        lastname: Some("Doe",),
519                        email: Some("john@example.com",),
520                    },
521                ],
522                source: Span {
523                    data: "Jane Smith <{jane-email}>; John Doe <{john-email}>",
524                    line: 1,
525                    col: 1,
526                    offset: 0,
527                },
528            }
529        );
530    }
531
532    #[test]
533    fn attr_sub_applied_after_parsing() {
534        // This is to demonstrate compatibility with Ruby asciidoctor behavior. In that
535        // implementation, the attribute substitution is applied *after* parsing for
536        // individual authors, which results in the unexpected treatment that the entire
537        // list is one author with mangled results.
538        let mut parser = Parser::default().with_intrinsic_attribute(
539            "author-list",
540            "Jane Smith <jane@example.com>; John Doe <john@example.com>",
541            ModificationContext::Anywhere,
542        );
543
544        let al = crate::document::AuthorLine::parse(crate::Span::new("{author-list}"), &mut parser);
545
546        assert_eq!(
547            al,
548            AuthorLine {
549                authors: &[Author {
550                    name: "Jane Smith <jane@example.com>; John Doe <john@example.com>",
551                    firstname: "Jane Smith <jane@example.com>; John Doe <john@example.com>",
552                    middlename: None,
553                    lastname: None,
554                    email: None,
555                },],
556                source: Span {
557                    data: "{author-list}",
558                    line: 1,
559                    col: 1,
560                    offset: 0,
561                },
562            }
563        );
564    }
565
566    #[test]
567    fn attr_sub_for_individual_author() {
568        let mut parser = Parser::default().with_intrinsic_attribute(
569            "full-author",
570            "John Doe <john@example.com>",
571            ModificationContext::Anywhere,
572        );
573
574        let al = crate::document::AuthorLine::parse(crate::Span::new("{full-author}"), &mut parser);
575
576        assert_eq!(
577            al,
578            AuthorLine {
579                authors: &[Author {
580                    name: "John Doe <john@example.com>",
581                    firstname: "John Doe <john@example.com>",
582                    middlename: None,
583                    lastname: None,
584                    email: None,
585                },],
586                source: Span {
587                    data: "{full-author}",
588                    line: 1,
589                    col: 1,
590                    offset: 0,
591                },
592            }
593        );
594    }
595
596    #[test]
597    fn err_individual_name_components_as_attributes() {
598        // This approach doesn't work in Ruby AsciiDoctor either.
599        let mut parser = Parser::default()
600            .with_intrinsic_attribute("first-name", "Jane", ModificationContext::Anywhere)
601            .with_intrinsic_attribute("last-name", "Smith", ModificationContext::Anywhere)
602            .with_intrinsic_attribute(
603                "author-email",
604                "jane@example.com",
605                ModificationContext::Anywhere,
606            );
607
608        let al = crate::document::AuthorLine::parse(
609            crate::Span::new("{first-name} {last-name} <{author-email}>"),
610            &mut parser,
611        );
612
613        assert_eq!(
614            al,
615            AuthorLine {
616                authors: &[Author {
617                    name: "Jane Smith &lt;jane@example.com&gt;",
618                    firstname: "Jane Smith &lt;jane@example.com&gt;",
619                    middlename: None,
620                    lastname: None,
621                    email: None,
622                },],
623                source: Span {
624                    data: "{first-name} {last-name} <{author-email}>",
625                    line: 1,
626                    col: 1,
627                    offset: 0,
628                },
629            }
630        );
631    }
632
633    #[test]
634    fn sets_author_attributes_single_author_with_all_parts() {
635        let mut parser = Parser::default();
636        let _doc = parser.parse("= Document Title\nKismet R. Lee <kismet@asciidoctor.org>");
637
638        // Primary author attributes
639        assert_eq!(
640            parser.attribute_value("author"),
641            InterpretedValue::Value("Kismet R. Lee")
642        );
643        assert_eq!(
644            parser.attribute_value("firstname"),
645            InterpretedValue::Value("Kismet")
646        );
647        assert_eq!(
648            parser.attribute_value("middlename"),
649            InterpretedValue::Value("R.")
650        );
651        assert_eq!(
652            parser.attribute_value("lastname"),
653            InterpretedValue::Value("Lee")
654        );
655        assert_eq!(
656            parser.attribute_value("authorinitials"),
657            InterpretedValue::Value("KRL")
658        );
659        assert_eq!(
660            parser.attribute_value("email"),
661            InterpretedValue::Value("kismet@asciidoctor.org")
662        );
663    }
664
665    #[test]
666    fn sets_author_attributes_single_author_without_middle_name() {
667        let mut parser = Parser::default();
668        let _doc = parser.parse("= Document Title\nDoc Writer <doc@example.com>");
669
670        assert_eq!(
671            parser.attribute_value("author"),
672            InterpretedValue::Value("Doc Writer")
673        );
674        assert_eq!(
675            parser.attribute_value("firstname"),
676            InterpretedValue::Value("Doc")
677        );
678        assert_eq!(
679            parser.attribute_value("middlename"),
680            InterpretedValue::Unset
681        );
682        assert_eq!(
683            parser.attribute_value("lastname"),
684            InterpretedValue::Value("Writer")
685        );
686        assert_eq!(
687            parser.attribute_value("authorinitials"),
688            InterpretedValue::Value("DW")
689        );
690        assert_eq!(
691            parser.attribute_value("email"),
692            InterpretedValue::Value("doc@example.com")
693        );
694    }
695
696    #[test]
697    fn sets_author_attributes_single_author_first_name_only() {
698        let mut parser = Parser::default();
699        let _doc = parser.parse("= Document Title\nJohn <john@example.com>");
700
701        assert_eq!(
702            parser.attribute_value("author"),
703            InterpretedValue::Value("John")
704        );
705        assert_eq!(
706            parser.attribute_value("firstname"),
707            InterpretedValue::Value("John")
708        );
709        assert_eq!(
710            parser.attribute_value("middlename"),
711            InterpretedValue::Unset
712        );
713        assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
714        assert_eq!(
715            parser.attribute_value("authorinitials"),
716            InterpretedValue::Value("J")
717        );
718        assert_eq!(
719            parser.attribute_value("email"),
720            InterpretedValue::Value("john@example.com")
721        );
722    }
723
724    #[test]
725    fn sets_author_attributes_single_author_without_email() {
726        let mut parser = Parser::default();
727        let _doc = parser.parse("= Document Title\nMary Sue Brontë");
728
729        assert_eq!(
730            parser.attribute_value("author"),
731            InterpretedValue::Value("Mary Sue Brontë")
732        );
733        assert_eq!(
734            parser.attribute_value("firstname"),
735            InterpretedValue::Value("Mary")
736        );
737        assert_eq!(
738            parser.attribute_value("middlename"),
739            InterpretedValue::Value("Sue")
740        );
741        assert_eq!(
742            parser.attribute_value("lastname"),
743            InterpretedValue::Value("Brontë")
744        );
745        assert_eq!(
746            parser.attribute_value("authorinitials"),
747            InterpretedValue::Value("MSB")
748        );
749        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
750    }
751
752    #[test]
753    fn sets_author_attributes_multiple_authors() {
754        let mut parser = Parser::default();
755        let _doc = parser
756            .parse("= Document Title\nJane Smith <jane@example.com>; John Doe <john@example.com>");
757
758        // First author (primary)
759        assert_eq!(
760            parser.attribute_value("author"),
761            InterpretedValue::Value("Jane Smith")
762        );
763        assert_eq!(
764            parser.attribute_value("firstname"),
765            InterpretedValue::Value("Jane")
766        );
767        assert_eq!(
768            parser.attribute_value("lastname"),
769            InterpretedValue::Value("Smith")
770        );
771        assert_eq!(
772            parser.attribute_value("authorinitials"),
773            InterpretedValue::Value("JS")
774        );
775        assert_eq!(
776            parser.attribute_value("email"),
777            InterpretedValue::Value("jane@example.com")
778        );
779
780        // Second author
781        assert_eq!(
782            parser.attribute_value("author_2"),
783            InterpretedValue::Value("John Doe")
784        );
785        assert_eq!(
786            parser.attribute_value("firstname_2"),
787            InterpretedValue::Value("John")
788        );
789        assert_eq!(
790            parser.attribute_value("lastname_2"),
791            InterpretedValue::Value("Doe")
792        );
793        assert_eq!(
794            parser.attribute_value("authorinitials_2"),
795            InterpretedValue::Value("JD")
796        );
797        assert_eq!(
798            parser.attribute_value("email_2"),
799            InterpretedValue::Value("john@example.com")
800        );
801
802        // Verify middlename attributes are unset for both authors
803        assert_eq!(
804            parser.attribute_value("middlename"),
805            InterpretedValue::Unset
806        );
807        assert_eq!(
808            parser.attribute_value("middlename_2"),
809            InterpretedValue::Unset
810        );
811    }
812
813    #[test]
814    fn sets_author_attributes_unicode_names() {
815        let mut parser = Parser::default();
816        let _doc = parser.parse("= Document Title\nΑλέξανδρος Μ. Παπαδόπουλος");
817
818        assert_eq!(
819            parser.attribute_value("author"),
820            InterpretedValue::Value("Αλέξανδρος Μ. Παπαδόπουλος")
821        );
822        assert_eq!(
823            parser.attribute_value("firstname"),
824            InterpretedValue::Value("Αλέξανδρος")
825        );
826        assert_eq!(
827            parser.attribute_value("middlename"),
828            InterpretedValue::Value("Μ.")
829        );
830        assert_eq!(
831            parser.attribute_value("lastname"),
832            InterpretedValue::Value("Παπαδόπουλος")
833        );
834        assert_eq!(
835            parser.attribute_value("authorinitials"),
836            InterpretedValue::Value("ΑΜΠ")
837        );
838    }
839
840    #[test]
841    fn semicolon_in_character_reference_not_treated_as_separator() {
842        let mut parser = Parser::default();
843
844        let al = crate::document::AuthorLine::parse(
845            crate::Span::new("AsciiDoc&#174;{empty} WG; Another Author"),
846            &mut parser,
847        );
848
849        assert_eq!(
850            al,
851            AuthorLine {
852                authors: &[
853                    Author {
854                        name: "AsciiDoc&#174; WG",
855                        firstname: "AsciiDoc&#174;",
856                        middlename: None,
857                        lastname: Some("WG"),
858                        email: None,
859                    },
860                    Author {
861                        name: "Another Author",
862                        firstname: "Another",
863                        middlename: None,
864                        lastname: Some("Author"),
865                        email: None,
866                    },
867                ],
868                source: Span {
869                    data: "AsciiDoc&#174;{empty} WG; Another Author",
870                    line: 1,
871                    col: 1,
872                    offset: 0,
873                },
874            }
875        );
876    }
877
878    #[test]
879    fn comprehensive_author_attribute_test() {
880        // This test verifies that all author attribute types work correctly for
881        // multiple authors, including edge cases like missing middle names and emails.
882
883        let mut parser = Parser::default();
884        let doc = parser.parse("= Document Title\nFirst Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy");
885
886        assert_eq!(
887            parser.attribute_value("author"),
888            InterpretedValue::Value("First Second Last")
889        );
890
891        assert_eq!(
892            parser.attribute_value("firstname"),
893            InterpretedValue::Value("First")
894        );
895
896        assert_eq!(
897            parser.attribute_value("middlename"),
898            InterpretedValue::Value("Second")
899        );
900
901        assert_eq!(
902            parser.attribute_value("lastname"),
903            InterpretedValue::Value("Last")
904        );
905
906        assert_eq!(
907            parser.attribute_value("authorinitials"),
908            InterpretedValue::Value("FSL")
909        );
910
911        assert_eq!(
912            parser.attribute_value("email"),
913            InterpretedValue::Value("first@example.com")
914        );
915
916        assert_eq!(
917            parser.attribute_value("author_2"),
918            InterpretedValue::Value("Only First")
919        );
920
921        assert_eq!(
922            parser.attribute_value("firstname_2"),
923            InterpretedValue::Value("Only")
924        );
925
926        assert_eq!(
927            parser.attribute_value("middlename_2"),
928            InterpretedValue::Unset
929        );
930
931        assert_eq!(
932            parser.attribute_value("lastname_2"),
933            InterpretedValue::Value("First")
934        );
935
936        assert_eq!(
937            parser.attribute_value("authorinitials_2"),
938            InterpretedValue::Value("OF")
939        );
940
941        assert_eq!(parser.attribute_value("email_2"), InterpretedValue::Unset);
942
943        assert_eq!(
944            parser.attribute_value("author_3"),
945            InterpretedValue::Value("A B C")
946        );
947
948        assert_eq!(
949            parser.attribute_value("firstname_3"),
950            InterpretedValue::Value("A")
951        );
952
953        assert_eq!(
954            parser.attribute_value("middlename_3"),
955            InterpretedValue::Value("B")
956        );
957
958        assert_eq!(
959            parser.attribute_value("lastname_3"),
960            InterpretedValue::Value("C")
961        );
962
963        assert_eq!(
964            parser.attribute_value("authorinitials_3"),
965            InterpretedValue::Value("ABC")
966        );
967
968        assert_eq!(
969            parser.attribute_value("email_3"),
970            InterpretedValue::Value("abc@example.com")
971        );
972
973        assert_eq!(
974            parser.attribute_value("author_4"),
975            InterpretedValue::Value("No Email Guy")
976        );
977
978        assert_eq!(
979            parser.attribute_value("firstname_4"),
980            InterpretedValue::Value("No")
981        );
982
983        assert_eq!(
984            parser.attribute_value("middlename_4"),
985            InterpretedValue::Value("Email")
986        );
987
988        assert_eq!(
989            parser.attribute_value("lastname_4"),
990            InterpretedValue::Value("Guy")
991        );
992
993        assert_eq!(
994            parser.attribute_value("authorinitials_4"),
995            InterpretedValue::Value("NEG")
996        );
997
998        assert_eq!(parser.attribute_value("email_4"), InterpretedValue::Unset);
999
1000        assert_eq!(
1001            doc,
1002            Document {
1003                header: Header {
1004                    title_source: Some(Span {
1005                        data: "Document Title",
1006                        line: 1,
1007                        col: 3,
1008                        offset: 2,
1009                    },),
1010                    title: Some("Document Title",),
1011                    attributes: &[],
1012                    author_line: Some(AuthorLine {
1013                        authors: &[
1014                            Author {
1015                                name: "First Second Last",
1016                                firstname: "First",
1017                                middlename: Some("Second",),
1018                                lastname: Some("Last",),
1019                                email: Some("first@example.com",),
1020                            },
1021                            Author {
1022                                name: "Only First",
1023                                firstname: "Only",
1024                                middlename: None,
1025                                lastname: Some("First",),
1026                                email: None,
1027                            },
1028                            Author {
1029                                name: "A B C",
1030                                firstname: "A",
1031                                middlename: Some("B",),
1032                                lastname: Some("C",),
1033                                email: Some("abc@example.com",),
1034                            },
1035                            Author {
1036                                name: "No Email Guy",
1037                                firstname: "No",
1038                                middlename: Some("Email",),
1039                                lastname: Some("Guy",),
1040                                email: None,
1041                            },
1042                        ],
1043                        source: Span {
1044                            data: "First Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy",
1045                            line: 2,
1046                            col: 1,
1047                            offset: 17,
1048                        },
1049                    },),
1050                    revision_line: None,
1051                    comments: &[],
1052                    source: Span {
1053                        data: "= Document Title\nFirst Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy",
1054                        line: 1,
1055                        col: 1,
1056                        offset: 0,
1057                    },
1058                },
1059                blocks: &[],
1060                source: Span {
1061                    data: "= Document Title\nFirst Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy",
1062                    line: 1,
1063                    col: 1,
1064                    offset: 0,
1065                },
1066                warnings: &[],
1067                source_map: SourceMap(&[]),
1068                catalog: Catalog::default(),
1069            }
1070        );
1071    }
1072}