Skip to main content

asciidoc_parser/document/
author_line.rs

1use std::sync::LazyLock;
2
3use regex::Regex;
4
5use crate::{HasSpan, Parser, Span, document::Author, internal::opaque_iter::opaque_slice_iter};
6
7opaque_slice_iter! {
8    /// An iterator over the [`Author`]s in an [`AuthorLine`], returned by
9    /// [`AuthorLine::authors`].
10    pub struct Authors<'a> yielding Author;
11}
12
13/// The author line is directly after the document title line in the document
14/// header. When the content on this line is structured correctly, the processor
15/// assigns the content to the built-in author and email attributes.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct AuthorLine<'src> {
18    authors: Vec<Author>,
19    source: Span<'src>,
20}
21
22impl<'src> AuthorLine<'src> {
23    pub(crate) fn parse(source: Span<'src>, parser: &mut Parser) -> Self {
24        let authors: Vec<Author> = split_authors(source.data())
25            .into_iter()
26            .filter_map(|raw_author| Author::parse(raw_author, parser, false))
27            .collect();
28
29        for (index, author) in authors.iter().enumerate() {
30            set_nth_attribute(parser, "author", index, author.name());
31            set_nth_attribute(parser, "authorinitials", index, author.initials());
32            set_nth_attribute(parser, "firstname", index, author.firstname());
33
34            if let Some(middlename) = author.middlename() {
35                set_nth_attribute(parser, "middlename", index, middlename);
36            }
37
38            if let Some(lastname) = author.lastname() {
39                set_nth_attribute(parser, "lastname", index, lastname);
40            }
41
42            if let Some(email) = author.email() {
43                set_nth_attribute(parser, "email", index, email);
44            }
45        }
46
47        Self { authors, source }
48    }
49
50    /// Return an iterator over the authors in this author line.
51    pub fn authors(&'src self) -> Authors<'src> {
52        Authors::new(&self.authors)
53    }
54}
55
56/// Matches a numeric HTML character reference – decimal (`&#174;`) or
57/// hexadecimal (`&#xAE;`). The terminating semicolon of such a reference must
58/// never be treated as an author separator.
59///
60/// Only numeric references are recognized here. A named reference such as
61/// `&reg;` cannot be distinguished structurally from arbitrary `&word;` text
62/// (the crate does not carry a table of valid entity names), so treating every
63/// `&word;` as a reference would suppress genuine separators – e.g. the
64/// semicolon in `Alice &Development; Bob`. Numeric references are unambiguous,
65/// and they are what the implicit author line needs to guard (see issue #757).
66static NUMERIC_CHARACTER_REFERENCE: LazyLock<Regex> = LazyLock::new(|| {
67    #[allow(clippy::unwrap_used)]
68    Regex::new(r"&#(?:[0-9]+|[xX][0-9a-fA-F]+);").unwrap()
69});
70
71/// Split the implicit author line into raw author entries.
72///
73/// Following Asciidoctor, a semicolon separates authors only when it is
74/// immediately followed by a space or the end of the line. A semicolon that is
75/// followed by any other character (as in `Joe Doe;Smith Johnson`) is part of a
76/// single author's name. Blank entries – produced by a trailing separator or an
77/// empty middle entry – are left in place; [`Author::parse`] trims each entry
78/// and discards the empty ones.
79///
80/// Semicolons that terminate a numeric HTML character reference (such as
81/// `&#174;`) are never treated as separators, even when followed by a space, so
82/// a name like `AsciiDoc&#174; WG` is not split apart.
83fn split_authors(data: &str) -> Vec<&str> {
84    // Byte offsets of the semicolons that terminate a numeric character
85    // reference; these are excluded from consideration as separators.
86    let char_ref_terminators: Vec<usize> = NUMERIC_CHARACTER_REFERENCE
87        .find_iter(data)
88        .map(|m| m.end() - 1)
89        .collect();
90
91    let bytes = data.as_bytes();
92    let mut authors: Vec<&str> = Vec::new();
93    let mut start = 0;
94
95    for (index, c) in data.char_indices() {
96        if c != ';' || char_ref_terminators.contains(&index) {
97            continue;
98        }
99
100        // A semicolon is a separator only when followed by a space or the end of
101        // the line.
102        let is_separator = match bytes.get(index + 1) {
103            Some(next) => *next == b' ',
104            None => true,
105        };
106
107        if is_separator {
108            authors.push(&data[start..index]);
109            start = index + 1;
110        }
111    }
112
113    authors.push(&data[start..]);
114    authors
115}
116
117fn set_nth_attribute<V: AsRef<str>>(parser: &mut Parser, name: &str, index: usize, value: V) {
118    let name = if index == 0 {
119        name.to_string()
120    } else {
121        format!("{name}_{count}", count = index + 1)
122    };
123
124    parser.set_attribute_by_value_from_header(name, value);
125}
126
127impl<'src> HasSpan<'src> for AuthorLine<'src> {
128    fn span(&self) -> Span<'src> {
129        self.source
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::{parser::ModificationContext, tests::prelude::*};
136
137    #[test]
138    fn empty_line() {
139        let mut parser = Parser::default();
140
141        let al = crate::document::AuthorLine::parse(crate::Span::new(""), &mut parser);
142
143        assert_eq!(
144            &al,
145            AuthorLine {
146                authors: &[],
147                source: Span {
148                    data: "",
149                    line: 1,
150                    col: 1,
151                    offset: 0,
152                },
153            }
154        );
155    }
156
157    #[test]
158    fn attr_sub_with_html_encoding_fallback() {
159        // Test case for code coverage: input contains attributes but after expansion
160        // doesn't match AUTHOR regex and contains angle brackets that need HTML
161        // encoding.
162        let mut parser = Parser::default().with_intrinsic_attribute(
163            "weird-content",
164            "Complex <weird> & stuff",
165            ModificationContext::Anywhere,
166        );
167
168        let al = crate::document::AuthorLine::parse(
169            crate::Span::new("Some {weird-content} pattern"),
170            &mut parser,
171        );
172
173        assert_eq!(
174            al,
175            AuthorLine {
176                authors: &[Author {
177                    name: "Some Complex &lt;weird&gt; &amp; stuff pattern",
178                    firstname: "Some Complex &lt;weird&gt; &amp; stuff pattern",
179                    middlename: None,
180                    lastname: None,
181                    email: None,
182                },],
183                source: Span {
184                    data: "Some {weird-content} pattern",
185                    line: 1,
186                    col: 1,
187                    offset: 0,
188                },
189            }
190        );
191    }
192
193    #[test]
194    fn empty_author() {
195        let mut parser = Parser::default();
196
197        let al = crate::document::AuthorLine::parse(
198            crate::Span::new("Author One; ; Author Three"),
199            &mut parser,
200        );
201
202        assert_eq!(
203            al,
204            AuthorLine {
205                authors: &[
206                    Author {
207                        name: "Author One",
208                        firstname: "Author",
209                        middlename: None,
210                        lastname: Some("One",),
211                        email: None,
212                    },
213                    Author {
214                        name: "Author Three",
215                        firstname: "Author",
216                        middlename: None,
217                        lastname: Some("Three",),
218                        email: None,
219                    },
220                ],
221                source: Span {
222                    data: "Author One; ; Author Three",
223                    line: 1,
224                    col: 1,
225                    offset: 0,
226                },
227            }
228        );
229    }
230
231    #[test]
232    fn one_simple_author() {
233        let mut parser = Parser::default();
234
235        let al = crate::document::AuthorLine::parse(
236            crate::Span::new("Kismet R. Lee <kismet@asciidoctor.org>"),
237            &mut parser,
238        );
239
240        assert_eq!(
241            &al,
242            AuthorLine {
243                authors: &[Author {
244                    name: "Kismet R. Lee",
245                    firstname: "Kismet",
246                    middlename: Some("R.",),
247                    lastname: Some("Lee",),
248                    email: Some("kismet@asciidoctor.org",),
249                },],
250                source: Span {
251                    data: "Kismet R. Lee <kismet@asciidoctor.org>",
252                    line: 1,
253                    col: 1,
254                    offset: 0,
255                },
256            }
257        );
258    }
259
260    #[test]
261    fn author_without_middle_name() {
262        let mut parser = Parser::default();
263
264        let al = crate::document::AuthorLine::parse(
265            crate::Span::new("Doc Writer <doc@example.com>"),
266            &mut parser,
267        );
268
269        assert_eq!(
270            al,
271            AuthorLine {
272                authors: &[Author {
273                    name: "Doc Writer",
274                    firstname: "Doc",
275                    middlename: None,
276                    lastname: Some("Writer",),
277                    email: Some("doc@example.com",),
278                },],
279                source: Span {
280                    data: "Doc Writer <doc@example.com>",
281                    line: 1,
282                    col: 1,
283                    offset: 0,
284                },
285            }
286        );
287    }
288
289    #[test]
290    fn too_many_names() {
291        let mut parser = Parser::default();
292
293        let al = crate::document::AuthorLine::parse(
294            crate::Span::new("Four Names Not Supported <doc@example.com>"),
295            &mut parser,
296        );
297
298        assert_eq!(
299            al,
300            AuthorLine {
301                authors: &[Author {
302                    name: "Four Names Not Supported <doc@example.com>",
303                    firstname: "Four Names Not Supported <doc@example.com>",
304                    middlename: None,
305                    lastname: None,
306                    email: None,
307                },],
308                source: Span {
309                    data: "Four Names Not Supported <doc@example.com>",
310                    line: 1,
311                    col: 1,
312                    offset: 0,
313                },
314            }
315        );
316    }
317
318    #[test]
319    fn fallback_condenses_whitespace_and_keeps_underscores_literal() {
320        // A line that does not match the author pattern (here because of the comma)
321        // is stored verbatim as the author with runs of spaces condensed. Unlike the
322        // matching path, underscores are left literal rather than replaced with
323        // spaces (matching Asciidoctor's `tr_s ' ', ' '` fallback).
324        let mut parser = Parser::default();
325
326        let al =
327            crate::document::AuthorLine::parse(crate::Span::new("Jane_    Q, Doe"), &mut parser);
328
329        assert_eq!(
330            al,
331            AuthorLine {
332                authors: &[Author {
333                    name: "Jane_ Q, Doe",
334                    firstname: "Jane_ Q, Doe",
335                    middlename: None,
336                    lastname: None,
337                    email: None,
338                },],
339                source: Span {
340                    data: "Jane_    Q, Doe",
341                    line: 1,
342                    col: 1,
343                    offset: 0,
344                },
345            }
346        );
347    }
348
349    #[test]
350    fn one_name() {
351        let mut parser = Parser::default();
352
353        let al = crate::document::AuthorLine::parse(
354            crate::Span::new("John <john@example.com>"),
355            &mut parser,
356        );
357
358        assert_eq!(
359            al,
360            AuthorLine {
361                authors: &[Author {
362                    name: "John",
363                    firstname: "John",
364                    middlename: None,
365                    lastname: None,
366                    email: Some("john@example.com",),
367                },],
368                source: Span {
369                    data: "John <john@example.com>",
370                    line: 1,
371                    col: 1,
372                    offset: 0,
373                },
374            }
375        );
376    }
377
378    #[test]
379    fn underscore_join() {
380        let mut parser = Parser::default();
381
382        let al =
383            crate::document::AuthorLine::parse(crate::Span::new("Mary_Sue Brontë"), &mut parser);
384
385        assert_eq!(
386            al,
387            AuthorLine {
388                authors: &[Author {
389                    name: "Mary Sue Brontë", // Underscore replaced with space
390                    firstname: "Mary Sue",   // Underscore replaced with space
391                    middlename: None,
392                    lastname: Some("Brontë",),
393                    email: None,
394                },],
395                source: Span {
396                    data: "Mary_Sue Brontë",
397                    line: 1,
398                    col: 1,
399                    offset: 0,
400                },
401            }
402        );
403    }
404
405    #[test]
406    fn greek() {
407        let mut parser = Parser::default();
408
409        let al = crate::document::AuthorLine::parse(
410            crate::Span::new("Αλέξανδρος Παπαδόπουλος"),
411            &mut parser,
412        );
413
414        assert_eq!(
415            al,
416            AuthorLine {
417                authors: &[Author {
418                    name: "Αλέξανδρος Παπαδόπουλος",
419                    firstname: "Αλέξανδρος",
420                    middlename: None,
421                    lastname: Some("Παπαδόπουλος",),
422                    email: None,
423                },],
424                source: Span {
425                    data: "Αλέξανδρος Παπαδόπουλος",
426                    line: 1,
427                    col: 1,
428                    offset: 0,
429                },
430            }
431        );
432    }
433
434    #[test]
435    fn japanese() {
436        let mut parser = Parser::default();
437
438        let al = crate::document::AuthorLine::parse(crate::Span::new("山田太郎"), &mut parser);
439
440        assert_eq!(
441            al,
442            AuthorLine {
443                authors: &[Author {
444                    name: "山田太郎",
445                    firstname: "山田太郎",
446                    middlename: None,
447                    lastname: None,
448                    email: None,
449                },],
450                source: Span {
451                    data: "山田太郎",
452                    line: 1,
453                    col: 1,
454                    offset: 0,
455                },
456            }
457        );
458    }
459
460    #[test]
461    fn arabic() {
462        let mut parser = Parser::default();
463
464        let al = crate::document::AuthorLine::parse(crate::Span::new("عبد_الله"), &mut parser);
465
466        assert_eq!(
467            al,
468            AuthorLine {
469                authors: &[Author {
470                    name: "عبد الله",      // Underscore replaced with space
471                    firstname: "عبد الله", // Underscore replaced with space
472                    middlename: None,
473                    lastname: None,
474                    email: None,
475                },],
476                source: Span {
477                    data: "عبد_الله",
478                    line: 1,
479                    col: 1,
480                    offset: 0,
481                },
482            }
483        );
484    }
485
486    #[test]
487    fn underscore_replacement_in_all_name_parts() {
488        let mut parser = Parser::default();
489
490        let al = crate::document::AuthorLine::parse(
491            crate::Span::new("John_Paul Mary_Jane Smith_Jones <email@example.com>"),
492            &mut parser,
493        );
494
495        assert_eq!(
496            al,
497            AuthorLine {
498                authors: &[Author {
499                    name: "John Paul Mary Jane Smith Jones", // Underscore replaced with space
500                    firstname: "John Paul",                  // Underscore replaced with space
501                    middlename: Some("Mary Jane"),           // Underscore replaced with space
502                    lastname: Some("Smith Jones"),           // Underscore replaced with space
503                    email: Some("email@example.com"),
504                },],
505                source: Span {
506                    data: "John_Paul Mary_Jane Smith_Jones <email@example.com>",
507                    line: 1,
508                    col: 1,
509                    offset: 0,
510                },
511            }
512        );
513    }
514
515    #[test]
516    fn multiple_underscores_in_name_parts() {
517        let mut parser = Parser::default();
518
519        let al =
520            crate::document::AuthorLine::parse(crate::Span::new("A_B_C D_E_F G_H_I"), &mut parser);
521
522        assert_eq!(
523            al,
524            AuthorLine {
525                authors: &[Author {
526                    name: "A B C D E F G H I", // Multiple underscores replaced with spaces
527                    firstname: "A B C",        // Multiple underscores replaced with spaces
528                    middlename: Some("D E F"), // Multiple underscores replaced with spaces
529                    lastname: Some("G H I"),   // Multiple underscores replaced with spaces
530                    email: None,
531                },],
532                source: Span {
533                    data: "A_B_C D_E_F G_H_I",
534                    line: 1,
535                    col: 1,
536                    offset: 0,
537                },
538            }
539        );
540    }
541
542    #[test]
543    fn underscore_replacement_with_attribute_substitution() {
544        let mut parser = Parser::default()
545            .with_intrinsic_attribute("first-part", "John_Paul", ModificationContext::Anywhere)
546            .with_intrinsic_attribute("last-part", "Smith_Jones", ModificationContext::Anywhere);
547
548        let al = crate::document::AuthorLine::parse(
549            crate::Span::new("{first-part} {last-part} <email@example.com>"),
550            &mut parser,
551        );
552
553        // Note: This test documents the current behavior where attribute substitution
554        // happens after parsing, which results in HTML encoding of the angle brackets.
555        // The underscore replacement should still work on the attribute-substituted
556        // values.
557        assert_eq!(
558            al,
559            AuthorLine {
560                authors: &[Author {
561                    name: "John Paul Smith Jones &lt;email@example.com&gt;", /* Underscore
562                                                                              * replaced
563                                                                              * with space */
564                    firstname: "John Paul Smith Jones &lt;email@example.com&gt;", /* Underscore
565                                                                                   * replaced with
566                                                                                   * space */
567                    middlename: None,
568                    lastname: None,
569                    email: None,
570                },],
571                source: Span {
572                    data: "{first-part} {last-part} <email@example.com>",
573                    line: 1,
574                    col: 1,
575                    offset: 0,
576                },
577            }
578        );
579    }
580
581    #[test]
582    fn attr_sub_email() {
583        let mut parser = Parser::default()
584            .with_intrinsic_attribute(
585                "jane-email",
586                "jane@example.com",
587                ModificationContext::Anywhere,
588            )
589            .with_intrinsic_attribute(
590                "john-email",
591                "john@example.com",
592                ModificationContext::Anywhere,
593            );
594
595        let al = crate::document::AuthorLine::parse(
596            crate::Span::new("Jane Smith <{jane-email}>; John Doe <{john-email}>"),
597            &mut parser,
598        );
599
600        assert_eq!(
601            al,
602            AuthorLine {
603                authors: &[
604                    Author {
605                        name: "Jane Smith",
606                        firstname: "Jane",
607                        middlename: None,
608                        lastname: Some("Smith",),
609                        email: Some("jane@example.com",),
610                    },
611                    Author {
612                        name: "John Doe",
613                        firstname: "John",
614                        middlename: None,
615                        lastname: Some("Doe",),
616                        email: Some("john@example.com",),
617                    },
618                ],
619                source: Span {
620                    data: "Jane Smith <{jane-email}>; John Doe <{john-email}>",
621                    line: 1,
622                    col: 1,
623                    offset: 0,
624                },
625            }
626        );
627    }
628
629    #[test]
630    fn attr_sub_applied_after_parsing() {
631        // This is to demonstrate compatibility with Ruby asciidoctor behavior. In that
632        // implementation, the attribute substitution is applied *after* parsing for
633        // individual authors, which results in the unexpected treatment that the entire
634        // list is one author with mangled results.
635        let mut parser = Parser::default().with_intrinsic_attribute(
636            "author-list",
637            "Jane Smith <jane@example.com>; John Doe <john@example.com>",
638            ModificationContext::Anywhere,
639        );
640
641        let al = crate::document::AuthorLine::parse(crate::Span::new("{author-list}"), &mut parser);
642
643        assert_eq!(
644            al,
645            AuthorLine {
646                authors: &[Author {
647                    name: "Jane Smith <jane@example.com>; John Doe <john@example.com>",
648                    firstname: "Jane Smith <jane@example.com>; John Doe <john@example.com>",
649                    middlename: None,
650                    lastname: None,
651                    email: None,
652                },],
653                source: Span {
654                    data: "{author-list}",
655                    line: 1,
656                    col: 1,
657                    offset: 0,
658                },
659            }
660        );
661    }
662
663    #[test]
664    fn attr_sub_for_individual_author() {
665        let mut parser = Parser::default().with_intrinsic_attribute(
666            "full-author",
667            "John Doe <john@example.com>",
668            ModificationContext::Anywhere,
669        );
670
671        let al = crate::document::AuthorLine::parse(crate::Span::new("{full-author}"), &mut parser);
672
673        assert_eq!(
674            al,
675            AuthorLine {
676                authors: &[Author {
677                    name: "John Doe <john@example.com>",
678                    firstname: "John Doe <john@example.com>",
679                    middlename: None,
680                    lastname: None,
681                    email: None,
682                },],
683                source: Span {
684                    data: "{full-author}",
685                    line: 1,
686                    col: 1,
687                    offset: 0,
688                },
689            }
690        );
691    }
692
693    #[test]
694    fn err_individual_name_components_as_attributes() {
695        // This approach doesn't work in Ruby AsciiDoctor either.
696        let mut parser = Parser::default()
697            .with_intrinsic_attribute("first-name", "Jane", ModificationContext::Anywhere)
698            .with_intrinsic_attribute("last-name", "Smith", ModificationContext::Anywhere)
699            .with_intrinsic_attribute(
700                "author-email",
701                "jane@example.com",
702                ModificationContext::Anywhere,
703            );
704
705        let al = crate::document::AuthorLine::parse(
706            crate::Span::new("{first-name} {last-name} <{author-email}>"),
707            &mut parser,
708        );
709
710        assert_eq!(
711            al,
712            AuthorLine {
713                authors: &[Author {
714                    name: "Jane Smith &lt;jane@example.com&gt;",
715                    firstname: "Jane Smith &lt;jane@example.com&gt;",
716                    middlename: None,
717                    lastname: None,
718                    email: None,
719                },],
720                source: Span {
721                    data: "{first-name} {last-name} <{author-email}>",
722                    line: 1,
723                    col: 1,
724                    offset: 0,
725                },
726            }
727        );
728    }
729
730    #[test]
731    fn sets_author_attributes_single_author_with_all_parts() {
732        let mut parser = Parser::default();
733        let _doc = parser.parse("= Document Title\nKismet R. Lee <kismet@asciidoctor.org>");
734
735        // Primary author attributes
736        assert_eq!(
737            parser.attribute_value("author"),
738            InterpretedValue::Value("Kismet R. Lee")
739        );
740        assert_eq!(
741            parser.attribute_value("firstname"),
742            InterpretedValue::Value("Kismet")
743        );
744        assert_eq!(
745            parser.attribute_value("middlename"),
746            InterpretedValue::Value("R.")
747        );
748        assert_eq!(
749            parser.attribute_value("lastname"),
750            InterpretedValue::Value("Lee")
751        );
752        assert_eq!(
753            parser.attribute_value("authorinitials"),
754            InterpretedValue::Value("KRL")
755        );
756        assert_eq!(
757            parser.attribute_value("email"),
758            InterpretedValue::Value("kismet@asciidoctor.org")
759        );
760    }
761
762    #[test]
763    fn sets_author_attributes_single_author_without_middle_name() {
764        let mut parser = Parser::default();
765        let _doc = parser.parse("= Document Title\nDoc Writer <doc@example.com>");
766
767        assert_eq!(
768            parser.attribute_value("author"),
769            InterpretedValue::Value("Doc Writer")
770        );
771        assert_eq!(
772            parser.attribute_value("firstname"),
773            InterpretedValue::Value("Doc")
774        );
775        assert_eq!(
776            parser.attribute_value("middlename"),
777            InterpretedValue::Unset
778        );
779        assert_eq!(
780            parser.attribute_value("lastname"),
781            InterpretedValue::Value("Writer")
782        );
783        assert_eq!(
784            parser.attribute_value("authorinitials"),
785            InterpretedValue::Value("DW")
786        );
787        assert_eq!(
788            parser.attribute_value("email"),
789            InterpretedValue::Value("doc@example.com")
790        );
791    }
792
793    #[test]
794    fn sets_author_attributes_single_author_first_name_only() {
795        let mut parser = Parser::default();
796        let _doc = parser.parse("= Document Title\nJohn <john@example.com>");
797
798        assert_eq!(
799            parser.attribute_value("author"),
800            InterpretedValue::Value("John")
801        );
802        assert_eq!(
803            parser.attribute_value("firstname"),
804            InterpretedValue::Value("John")
805        );
806        assert_eq!(
807            parser.attribute_value("middlename"),
808            InterpretedValue::Unset
809        );
810        assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
811        assert_eq!(
812            parser.attribute_value("authorinitials"),
813            InterpretedValue::Value("J")
814        );
815        assert_eq!(
816            parser.attribute_value("email"),
817            InterpretedValue::Value("john@example.com")
818        );
819    }
820
821    #[test]
822    fn sets_author_attributes_single_author_without_email() {
823        let mut parser = Parser::default();
824        let _doc = parser.parse("= Document Title\nMary Sue Brontë");
825
826        assert_eq!(
827            parser.attribute_value("author"),
828            InterpretedValue::Value("Mary Sue Brontë")
829        );
830        assert_eq!(
831            parser.attribute_value("firstname"),
832            InterpretedValue::Value("Mary")
833        );
834        assert_eq!(
835            parser.attribute_value("middlename"),
836            InterpretedValue::Value("Sue")
837        );
838        assert_eq!(
839            parser.attribute_value("lastname"),
840            InterpretedValue::Value("Brontë")
841        );
842        assert_eq!(
843            parser.attribute_value("authorinitials"),
844            InterpretedValue::Value("MSB")
845        );
846        assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
847    }
848
849    #[test]
850    fn sets_author_attributes_multiple_authors() {
851        let mut parser = Parser::default();
852        let _doc = parser
853            .parse("= Document Title\nJane Smith <jane@example.com>; John Doe <john@example.com>");
854
855        // First author (primary)
856        assert_eq!(
857            parser.attribute_value("author"),
858            InterpretedValue::Value("Jane Smith")
859        );
860        assert_eq!(
861            parser.attribute_value("firstname"),
862            InterpretedValue::Value("Jane")
863        );
864        assert_eq!(
865            parser.attribute_value("lastname"),
866            InterpretedValue::Value("Smith")
867        );
868        assert_eq!(
869            parser.attribute_value("authorinitials"),
870            InterpretedValue::Value("JS")
871        );
872        assert_eq!(
873            parser.attribute_value("email"),
874            InterpretedValue::Value("jane@example.com")
875        );
876
877        // Second author
878        assert_eq!(
879            parser.attribute_value("author_2"),
880            InterpretedValue::Value("John Doe")
881        );
882        assert_eq!(
883            parser.attribute_value("firstname_2"),
884            InterpretedValue::Value("John")
885        );
886        assert_eq!(
887            parser.attribute_value("lastname_2"),
888            InterpretedValue::Value("Doe")
889        );
890        assert_eq!(
891            parser.attribute_value("authorinitials_2"),
892            InterpretedValue::Value("JD")
893        );
894        assert_eq!(
895            parser.attribute_value("email_2"),
896            InterpretedValue::Value("john@example.com")
897        );
898
899        // Verify middlename attributes are unset for both authors.
900        assert_eq!(
901            parser.attribute_value("middlename"),
902            InterpretedValue::Unset
903        );
904        assert_eq!(
905            parser.attribute_value("middlename_2"),
906            InterpretedValue::Unset
907        );
908    }
909
910    #[test]
911    fn sets_author_attributes_unicode_names() {
912        let mut parser = Parser::default();
913        let _doc = parser.parse("= Document Title\nΑλέξανδρος Μ. Παπαδόπουλος");
914
915        assert_eq!(
916            parser.attribute_value("author"),
917            InterpretedValue::Value("Αλέξανδρος Μ. Παπαδόπουλος")
918        );
919        assert_eq!(
920            parser.attribute_value("firstname"),
921            InterpretedValue::Value("Αλέξανδρος")
922        );
923        assert_eq!(
924            parser.attribute_value("middlename"),
925            InterpretedValue::Value("Μ.")
926        );
927        assert_eq!(
928            parser.attribute_value("lastname"),
929            InterpretedValue::Value("Παπαδόπουλος")
930        );
931        assert_eq!(
932            parser.attribute_value("authorinitials"),
933            InterpretedValue::Value("ΑΜΠ")
934        );
935    }
936
937    #[test]
938    fn semicolon_in_character_reference_not_treated_as_separator() {
939        let mut parser = Parser::default();
940
941        let al = crate::document::AuthorLine::parse(
942            crate::Span::new("AsciiDoc&#174;{empty} WG; Another Author"),
943            &mut parser,
944        );
945
946        assert_eq!(
947            al,
948            AuthorLine {
949                authors: &[
950                    Author {
951                        name: "AsciiDoc&#174; WG",
952                        firstname: "AsciiDoc&#174;",
953                        middlename: None,
954                        lastname: Some("WG"),
955                        email: None,
956                    },
957                    Author {
958                        name: "Another Author",
959                        firstname: "Another",
960                        middlename: None,
961                        lastname: Some("Author"),
962                        email: None,
963                    },
964                ],
965                source: Span {
966                    data: "AsciiDoc&#174;{empty} WG; Another Author",
967                    line: 1,
968                    col: 1,
969                    offset: 0,
970                },
971            }
972        );
973    }
974
975    #[test]
976    fn skips_blank_and_trailing_author_entries() {
977        // https://github.com/asciidoc-rs/asciidoc-parser/issues/757: the author
978        // line is split on a semicolon followed by a space or the end of the
979        // line, so the blank middle entry and the trailing bare `;` are both
980        // dropped instead of leaving the trailing `;` attached to the last
981        // author.
982        let mut parser = Parser::default();
983
984        let al = crate::document::AuthorLine::parse(
985            crate::Span::new("Doc Writer; ; John Smith <john.smith@asciidoc.org>;"),
986            &mut parser,
987        );
988
989        assert_eq!(
990            al,
991            AuthorLine {
992                authors: &[
993                    Author {
994                        name: "Doc Writer",
995                        firstname: "Doc",
996                        middlename: None,
997                        lastname: Some("Writer"),
998                        email: None,
999                    },
1000                    Author {
1001                        name: "John Smith",
1002                        firstname: "John",
1003                        middlename: None,
1004                        lastname: Some("Smith"),
1005                        email: Some("john.smith@asciidoc.org"),
1006                    },
1007                ],
1008                source: Span {
1009                    data: "Doc Writer; ; John Smith <john.smith@asciidoc.org>;",
1010                    line: 1,
1011                    col: 1,
1012                    offset: 0,
1013                },
1014            }
1015        );
1016    }
1017
1018    #[test]
1019    fn semicolon_not_followed_by_space_is_single_author() {
1020        // A semicolon that is not followed by a space (or end of line) does not
1021        // separate authors.
1022        let mut parser = Parser::default();
1023
1024        let al = crate::document::AuthorLine::parse(
1025            crate::Span::new("Joe Doe;Smith Johnson"),
1026            &mut parser,
1027        );
1028
1029        assert_eq!(al.authors().len(), 1);
1030    }
1031
1032    #[test]
1033    fn character_reference_followed_by_space_not_treated_as_separator() {
1034        // The terminating `;` of a character reference must not split the author
1035        // even when it is followed by a space.
1036        let mut parser = Parser::default();
1037
1038        let al = crate::document::AuthorLine::parse(
1039            crate::Span::new("AsciiDoc&#174; WG; Another Author"),
1040            &mut parser,
1041        );
1042
1043        assert_eq!(
1044            al,
1045            AuthorLine {
1046                authors: &[
1047                    Author {
1048                        name: "AsciiDoc&#174; WG",
1049                        firstname: "AsciiDoc&#174;",
1050                        middlename: None,
1051                        lastname: Some("WG"),
1052                        email: None,
1053                    },
1054                    Author {
1055                        name: "Another Author",
1056                        firstname: "Another",
1057                        middlename: None,
1058                        lastname: Some("Author"),
1059                        email: None,
1060                    },
1061                ],
1062                source: Span {
1063                    data: "AsciiDoc&#174; WG; Another Author",
1064                    line: 1,
1065                    col: 1,
1066                    offset: 0,
1067                },
1068            }
1069        );
1070    }
1071
1072    #[test]
1073    fn invalid_named_entity_does_not_suppress_separator() {
1074        // A literal `&word;` sequence is not a numeric character reference, so
1075        // its semicolon still separates authors when followed by a space. Only
1076        // numeric references (`&#nnn;`) guard against splitting.
1077        let mut parser = Parser::default();
1078
1079        let al = crate::document::AuthorLine::parse(
1080            crate::Span::new("Alice &Development; Bob"),
1081            &mut parser,
1082        );
1083
1084        assert_eq!(
1085            al,
1086            AuthorLine {
1087                authors: &[
1088                    Author {
1089                        name: "Alice &Development",
1090                        firstname: "Alice",
1091                        middlename: None,
1092                        lastname: Some("&Development"),
1093                        email: None,
1094                    },
1095                    Author {
1096                        name: "Bob",
1097                        firstname: "Bob",
1098                        middlename: None,
1099                        lastname: None,
1100                        email: None,
1101                    },
1102                ],
1103                source: Span {
1104                    data: "Alice &Development; Bob",
1105                    line: 1,
1106                    col: 1,
1107                    offset: 0,
1108                },
1109            }
1110        );
1111    }
1112
1113    #[test]
1114    fn comprehensive_author_attribute_test() {
1115        // This test verifies that all author attribute types work correctly for
1116        // multiple authors, including edge cases like missing middle names and emails.
1117
1118        let mut parser = Parser::default();
1119        let doc = parser.parse("= Document Title\nFirst Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy");
1120
1121        assert_eq!(
1122            parser.attribute_value("author"),
1123            InterpretedValue::Value("First Second Last")
1124        );
1125
1126        assert_eq!(
1127            parser.attribute_value("firstname"),
1128            InterpretedValue::Value("First")
1129        );
1130
1131        assert_eq!(
1132            parser.attribute_value("middlename"),
1133            InterpretedValue::Value("Second")
1134        );
1135
1136        assert_eq!(
1137            parser.attribute_value("lastname"),
1138            InterpretedValue::Value("Last")
1139        );
1140
1141        assert_eq!(
1142            parser.attribute_value("authorinitials"),
1143            InterpretedValue::Value("FSL")
1144        );
1145
1146        assert_eq!(
1147            parser.attribute_value("email"),
1148            InterpretedValue::Value("first@example.com")
1149        );
1150
1151        assert_eq!(
1152            parser.attribute_value("author_2"),
1153            InterpretedValue::Value("Only First")
1154        );
1155
1156        assert_eq!(
1157            parser.attribute_value("firstname_2"),
1158            InterpretedValue::Value("Only")
1159        );
1160
1161        assert_eq!(
1162            parser.attribute_value("middlename_2"),
1163            InterpretedValue::Unset
1164        );
1165
1166        assert_eq!(
1167            parser.attribute_value("lastname_2"),
1168            InterpretedValue::Value("First")
1169        );
1170
1171        assert_eq!(
1172            parser.attribute_value("authorinitials_2"),
1173            InterpretedValue::Value("OF")
1174        );
1175
1176        assert_eq!(parser.attribute_value("email_2"), InterpretedValue::Unset);
1177
1178        assert_eq!(
1179            parser.attribute_value("author_3"),
1180            InterpretedValue::Value("A B C")
1181        );
1182
1183        assert_eq!(
1184            parser.attribute_value("firstname_3"),
1185            InterpretedValue::Value("A")
1186        );
1187
1188        assert_eq!(
1189            parser.attribute_value("middlename_3"),
1190            InterpretedValue::Value("B")
1191        );
1192
1193        assert_eq!(
1194            parser.attribute_value("lastname_3"),
1195            InterpretedValue::Value("C")
1196        );
1197
1198        assert_eq!(
1199            parser.attribute_value("authorinitials_3"),
1200            InterpretedValue::Value("ABC")
1201        );
1202
1203        assert_eq!(
1204            parser.attribute_value("email_3"),
1205            InterpretedValue::Value("abc@example.com")
1206        );
1207
1208        assert_eq!(
1209            parser.attribute_value("author_4"),
1210            InterpretedValue::Value("No Email Guy")
1211        );
1212
1213        assert_eq!(
1214            parser.attribute_value("firstname_4"),
1215            InterpretedValue::Value("No")
1216        );
1217
1218        assert_eq!(
1219            parser.attribute_value("middlename_4"),
1220            InterpretedValue::Value("Email")
1221        );
1222
1223        assert_eq!(
1224            parser.attribute_value("lastname_4"),
1225            InterpretedValue::Value("Guy")
1226        );
1227
1228        assert_eq!(
1229            parser.attribute_value("authorinitials_4"),
1230            InterpretedValue::Value("NEG")
1231        );
1232
1233        assert_eq!(parser.attribute_value("email_4"), InterpretedValue::Unset);
1234
1235        assert_eq!(
1236            doc,
1237            Document {
1238                header: Header {
1239                    title_source: Some(Span {
1240                        data: "Document Title",
1241                        line: 1,
1242                        col: 3,
1243                        offset: 2,
1244                    },),
1245                    title: Some("Document Title",),
1246                    attributes: &[],
1247                    author_line: Some(AuthorLine {
1248                        authors: &[
1249                            Author {
1250                                name: "First Second Last",
1251                                firstname: "First",
1252                                middlename: Some("Second",),
1253                                lastname: Some("Last",),
1254                                email: Some("first@example.com",),
1255                            },
1256                            Author {
1257                                name: "Only First",
1258                                firstname: "Only",
1259                                middlename: None,
1260                                lastname: Some("First",),
1261                                email: None,
1262                            },
1263                            Author {
1264                                name: "A B C",
1265                                firstname: "A",
1266                                middlename: Some("B",),
1267                                lastname: Some("C",),
1268                                email: Some("abc@example.com",),
1269                            },
1270                            Author {
1271                                name: "No Email Guy",
1272                                firstname: "No",
1273                                middlename: Some("Email",),
1274                                lastname: Some("Guy",),
1275                                email: None,
1276                            },
1277                        ],
1278                        source: Span {
1279                            data: "First Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy",
1280                            line: 2,
1281                            col: 1,
1282                            offset: 17,
1283                        },
1284                    },),
1285                    revision_line: None,
1286                    comments: &[],
1287                    source: Span {
1288                        data: "= Document Title\nFirst Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy",
1289                        line: 1,
1290                        col: 1,
1291                        offset: 0,
1292                    },
1293                },
1294                blocks: &[],
1295                source: Span {
1296                    data: "= Document Title\nFirst Second Last <first@example.com>; Only First; A B C <abc@example.com>; No Email Guy",
1297                    line: 1,
1298                    col: 1,
1299                    offset: 0,
1300                },
1301                warnings: &[],
1302                source_map: SourceMap(&[]),
1303                catalog: Catalog::default(),
1304            }
1305        );
1306    }
1307}