1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 content::{Content, SubstitutionGroup},
6 document::{Attribute, Author, AuthorLine, RevisionLine},
7 internal::debug::DebugSliceReference,
8 span::MatchedItem,
9 warnings::{MatchAndWarnings, Warning, WarningType},
10};
11
12#[derive(Clone, Eq, PartialEq)]
16pub struct Header<'src> {
17 title_source: Option<Span<'src>>,
18 title: Option<String>,
19 attributes: Vec<Attribute<'src>>,
20 author_line: Option<AuthorLine<'src>>,
21 revision_line: Option<RevisionLine<'src>>,
22 comments: Vec<Span<'src>>,
23 source: Span<'src>,
24}
25
26impl<'src> Header<'src> {
27 pub(crate) fn parse(
28 mut source: Span<'src>,
29 parser: &mut Parser,
30 ) -> MatchAndWarnings<'src, MatchedItem<'src, Self>> {
31 let original_source = source.discard_empty_lines();
32
33 let mut title_source: Option<Span<'src>> = None;
34 let mut title: Option<String> = None;
35 let mut attributes: Vec<Attribute> = vec![];
36 let mut author_line: Option<AuthorLine<'src>> = None;
37 let mut revision_line: Option<RevisionLine<'src>> = None;
38 let mut comments: Vec<Span<'src>> = vec![];
39 let mut warnings: Vec<Warning<'src>> = vec![];
40
41 while !source.is_empty() {
43 let line_mi = source.take_normalized_line();
44 let line = line_mi.item;
45
46 if line.is_empty() {
48 if title.is_some() {
49 break;
50 }
51 source = line_mi.after;
52 } else if line.starts_with("//") && !line.starts_with("///") {
53 comments.push(line);
54 source = line_mi.after;
55 } else if line.starts_with(':')
56 && let Some(attr) = Attribute::parse(source, parser)
57 {
58 if attr.item.name().data().eq_ignore_ascii_case("author")
61 && let Some(raw_value) = attr.item.raw_value()
62 && let Some(author) = Author::parse(raw_value.data(), parser)
63 {
64 parser.set_attribute_by_value_from_header("firstname", author.firstname());
66 if let Some(middlename) = author.middlename() {
67 parser.set_attribute_by_value_from_header("middlename", middlename);
68 }
69 if let Some(lastname) = author.lastname() {
70 parser.set_attribute_by_value_from_header("lastname", lastname);
71 }
72 parser.set_attribute_by_value_from_header("authorinitials", author.initials());
73 if let Some(email) = author.email() {
74 parser.set_attribute_by_value_from_header("email", email);
75 }
76 }
77
78 parser.set_attribute_from_header(&attr.item, &mut warnings);
79 attributes.push(attr.item);
80 source = attr.after;
81 } else if title.is_none() && line.starts_with("= ") {
82 let title_span = line.discard(2).discard_whitespace();
83 let title_str = apply_header_subs(title_span.data(), parser);
84
85 parser.set_attribute_by_value_from_header("doctitle", &title_str);
86
87 title = Some(title_str);
88 title_source = Some(title_span);
89 source = line_mi.after;
90 } else if title.is_some() && author_line.is_none() {
91 author_line = Some(AuthorLine::parse(line, parser));
92 source = line_mi.after;
93 } else if title.is_some() && author_line.is_some() && revision_line.is_none() {
94 revision_line = Some(RevisionLine::parse(line, parser));
95 source = line_mi.after;
96 } else {
97 if title.is_some() {
98 warnings.push(Warning {
99 source: line,
100 warning: WarningType::DocumentHeaderNotTerminated,
101 });
102 }
103 break;
104 }
105 }
106
107 let after = source.discard_empty_lines();
108 let source = original_source.trim_remainder(source);
109
110 MatchAndWarnings {
111 item: MatchedItem {
112 item: Self {
113 title_source,
114 title,
115 attributes,
116 author_line,
117 revision_line,
118 comments,
119 source: source.trim_trailing_whitespace(),
120 },
121 after,
122 },
123 warnings,
124 }
125 }
126
127 pub fn title_source(&'src self) -> Option<Span<'src>> {
129 self.title_source
130 }
131
132 pub fn title(&self) -> Option<&str> {
135 self.title.as_deref()
136 }
137
138 pub fn attributes(&'src self) -> Iter<'src, Attribute<'src>> {
140 self.attributes.iter()
141 }
142
143 pub fn author_line(&self) -> Option<&AuthorLine<'src>> {
145 self.author_line.as_ref()
146 }
147
148 pub fn revision_line(&self) -> Option<&RevisionLine<'src>> {
150 self.revision_line.as_ref()
151 }
152
153 pub fn comments(&'src self) -> Iter<'src, Span<'src>> {
155 self.comments.iter()
156 }
157}
158
159impl<'src> HasSpan<'src> for Header<'src> {
160 fn span(&self) -> Span<'src> {
161 self.source
162 }
163}
164
165fn apply_header_subs(source: &str, parser: &Parser) -> String {
166 let span = Span::new(source);
167
168 let mut content = Content::from(span);
169 SubstitutionGroup::Header.apply(&mut content, parser, None);
170
171 content.rendered().to_string()
172}
173
174impl std::fmt::Debug for Header<'_> {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("Header")
177 .field("title_source", &self.title_source)
178 .field("title", &self.title)
179 .field("attributes", &DebugSliceReference(&self.attributes))
180 .field("author_line", &self.author_line)
181 .field("revision_line", &self.revision_line)
182 .field("comments", &DebugSliceReference(&self.comments))
183 .field("source", &self.source)
184 .finish()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 #![allow(clippy::unwrap_used)]
191
192 use pretty_assertions_sorted::assert_eq;
193
194 use crate::{Parser, tests::prelude::*};
195
196 #[test]
197 fn impl_clone() {
198 let mut parser = Parser::default();
200
201 let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
202 .unwrap_if_no_warnings();
203 let h2 = h1.clone();
204
205 assert_eq!(h1, h2);
206 }
207
208 #[test]
209 fn only_title() {
210 let mut parser = Parser::default();
211 let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
212 .unwrap_if_no_warnings();
213
214 assert_eq!(
215 mi.item,
216 Header {
217 title_source: Some(Span {
218 data: "Just the Title",
219 line: 1,
220 col: 3,
221 offset: 2,
222 }),
223 title: Some("Just the Title"),
224 attributes: &[],
225 author_line: None,
226 revision_line: None,
227 comments: &[],
228 source: Span {
229 data: "= Just the Title",
230 line: 1,
231 col: 1,
232 offset: 0,
233 }
234 }
235 );
236
237 assert_eq!(
238 mi.after,
239 Span {
240 data: "",
241 line: 1,
242 col: 17,
243 offset: 16
244 }
245 );
246 }
247
248 #[test]
249 fn trims_leading_spaces_in_title() {
250 let mut parser = Parser::default();
253 let mi =
254 crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
255 .unwrap_if_no_warnings();
256
257 assert_eq!(
258 mi.item,
259 Header {
260 title_source: Some(Span {
261 data: "Just the Title",
262 line: 1,
263 col: 6,
264 offset: 5,
265 }),
266 title: Some("Just the Title"),
267 attributes: &[],
268 author_line: None,
269 revision_line: None,
270 comments: &[],
271 source: Span {
272 data: "= Just the Title",
273 line: 1,
274 col: 1,
275 offset: 0,
276 }
277 }
278 );
279
280 assert_eq!(
281 mi.after,
282 Span {
283 data: "",
284 line: 1,
285 col: 20,
286 offset: 19
287 }
288 );
289 }
290
291 #[test]
292 fn trims_trailing_spaces_in_title() {
293 let mut parser = Parser::default();
294 let mi =
295 crate::document::Header::parse(crate::Span::new("= Just the Title "), &mut parser)
296 .unwrap_if_no_warnings();
297
298 assert_eq!(
299 mi.item,
300 Header {
301 title_source: Some(Span {
302 data: "Just the Title",
303 line: 1,
304 col: 3,
305 offset: 2,
306 }),
307 title: Some("Just the Title"),
308 attributes: &[],
309 author_line: None,
310 revision_line: None,
311 comments: &[],
312 source: Span {
313 data: "= Just the Title",
314 line: 1,
315 col: 1,
316 offset: 0,
317 }
318 }
319 );
320
321 assert_eq!(
322 mi.after,
323 Span {
324 data: "",
325 line: 1,
326 col: 20,
327 offset: 19
328 }
329 );
330 }
331
332 #[test]
333 fn title_and_attribute() {
334 let mut parser = Parser::default();
335
336 let mi = crate::document::Header::parse(
337 crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
338 &mut parser,
339 )
340 .unwrap_if_no_warnings();
341
342 assert_eq!(
343 mi.item,
344 Header {
345 title_source: Some(Span {
346 data: "Just the Title",
347 line: 1,
348 col: 3,
349 offset: 2,
350 }),
351 title: Some("Just the Title"),
352 attributes: &[Attribute {
353 name: Span {
354 data: "foo",
355 line: 2,
356 col: 2,
357 offset: 18,
358 },
359 value_source: Some(Span {
360 data: "bar",
361 line: 2,
362 col: 7,
363 offset: 23,
364 }),
365 value: InterpretedValue::Value("bar"),
366 source: Span {
367 data: ":foo: bar",
368 line: 2,
369 col: 1,
370 offset: 17,
371 }
372 }],
373 author_line: None,
374 revision_line: None,
375 comments: &[],
376 source: Span {
377 data: "= Just the Title\n:foo: bar",
378 line: 1,
379 col: 1,
380 offset: 0,
381 }
382 }
383 );
384
385 assert_eq!(
386 mi.after,
387 Span {
388 data: "blah",
389 line: 4,
390 col: 1,
391 offset: 28
392 }
393 );
394 }
395
396 #[test]
397 fn title_applies_header_substitutions() {
398 let mut parser = Parser::default();
399
400 let mi = crate::document::Header::parse(
401 crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
402 &mut parser,
403 )
404 .unwrap_if_no_warnings();
405
406 assert_eq!(
407 mi.item,
408 Header {
409 title_source: Some(Span {
410 data: "The Title & Some{sp}Nonsense",
411 line: 1,
412 col: 3,
413 offset: 2,
414 }),
415 title: Some("The Title & Some Nonsense"),
416 attributes: &[Attribute {
417 name: Span {
418 data: "foo",
419 line: 2,
420 col: 2,
421 offset: 32,
422 },
423 value_source: Some(Span {
424 data: "bar",
425 line: 2,
426 col: 7,
427 offset: 37,
428 }),
429 value: InterpretedValue::Value("bar"),
430 source: Span {
431 data: ":foo: bar",
432 line: 2,
433 col: 1,
434 offset: 31,
435 }
436 }],
437 author_line: None,
438 revision_line: None,
439 comments: &[],
440 source: Span {
441 data: "= The Title & Some{sp}Nonsense\n:foo: bar",
442 line: 1,
443 col: 1,
444 offset: 0,
445 }
446 }
447 );
448
449 assert_eq!(
450 mi.after,
451 Span {
452 data: "blah",
453 line: 4,
454 col: 1,
455 offset: 42
456 }
457 );
458 }
459
460 #[test]
461 fn attribute_without_title() {
462 let mut parser = Parser::default();
463 let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
464 .unwrap_if_no_warnings();
465
466 assert_eq!(
467 mi.item,
468 Header {
469 title_source: None,
470 title: None,
471 attributes: &[Attribute {
472 name: Span {
473 data: "foo",
474 line: 1,
475 col: 2,
476 offset: 1,
477 },
478 value_source: Some(Span {
479 data: "bar",
480 line: 1,
481 col: 7,
482 offset: 6,
483 }),
484 value: InterpretedValue::Value("bar"),
485 source: Span {
486 data: ":foo: bar",
487 line: 1,
488 col: 1,
489 offset: 0,
490 }
491 }],
492 author_line: None,
493 revision_line: None,
494 comments: &[],
495 source: Span {
496 data: ":foo: bar",
497 line: 1,
498 col: 1,
499 offset: 0,
500 }
501 }
502 );
503
504 assert_eq!(
505 mi.after,
506 Span {
507 data: "blah",
508 line: 3,
509 col: 1,
510 offset: 11
511 }
512 );
513 }
514
515 #[test]
516 fn sets_doctitle_attribute() {
517 let mut parser = Parser::default();
518 let _doc = parser.parse("= Document Title Goes Here");
519
520 assert_eq!(
521 parser.attribute_value("doctitle"),
522 InterpretedValue::Value("Document Title Goes Here")
523 );
524 }
525
526 #[test]
527 fn sets_author_attributes_from_author_attribute() {
528 let mut parser = Parser::default();
529 let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
530
531 assert_eq!(
533 parser.attribute_value("firstname"),
534 InterpretedValue::Value("John")
535 );
536 assert_eq!(
537 parser.attribute_value("middlename"),
538 InterpretedValue::Value("Q.")
539 );
540 assert_eq!(
541 parser.attribute_value("lastname"),
542 InterpretedValue::Value("Smith")
543 );
544 assert_eq!(
545 parser.attribute_value("authorinitials"),
546 InterpretedValue::Value("JQS")
547 );
548 assert_eq!(
549 parser.attribute_value("email"),
550 InterpretedValue::Value("john@example.com")
551 );
552
553 assert_eq!(
555 parser.attribute_value("author"),
556 InterpretedValue::Value("John Q. Smith <john@example.com>")
557 );
558 }
559
560 #[test]
561 fn sets_author_attributes_from_author_attribute_two_names() {
562 let mut parser = Parser::default();
563 let _doc = parser.parse(":author: Jane Doe");
564
565 assert_eq!(
567 parser.attribute_value("firstname"),
568 InterpretedValue::Value("Jane")
569 );
570 assert_eq!(
571 parser.attribute_value("middlename"),
572 InterpretedValue::Unset
573 );
574 assert_eq!(
575 parser.attribute_value("lastname"),
576 InterpretedValue::Value("Doe")
577 );
578 assert_eq!(
579 parser.attribute_value("authorinitials"),
580 InterpretedValue::Value("JD")
581 );
582 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
583 }
584
585 #[test]
586 fn sets_author_attributes_from_author_attribute_single_name() {
587 let mut parser = Parser::default();
588 let _doc = parser.parse(":author: Cher");
589
590 assert_eq!(
592 parser.attribute_value("firstname"),
593 InterpretedValue::Value("Cher")
594 );
595 assert_eq!(
596 parser.attribute_value("middlename"),
597 InterpretedValue::Unset
598 );
599 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
600 assert_eq!(
601 parser.attribute_value("authorinitials"),
602 InterpretedValue::Value("C")
603 );
604 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
605 }
606
607 #[test]
608 fn sets_author_attributes_from_empty_string() {
609 let mut parser = Parser::default();
610 let _doc = parser.parse(":author:");
611
612 assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
614 assert_eq!(
615 parser.attribute_value("middlename"),
616 InterpretedValue::Unset
617 );
618 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
619 assert_eq!(
620 parser.attribute_value("authorinitials"),
621 InterpretedValue::Unset
622 );
623 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
624
625 assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
626 }
627
628 #[test]
629 fn impl_debug() {
630 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
631 let header = doc.header();
632
633 assert_eq!(
634 format!("{header:#?}"),
635 r#"Header {
636 title_source: Some(
637 Span {
638 data: "Example Title",
639 line: 1,
640 col: 3,
641 offset: 2,
642 },
643 ),
644 title: Some(
645 "Example Title",
646 ),
647 attributes: &[],
648 author_line: None,
649 revision_line: None,
650 comments: &[],
651 source: Span {
652 data: "= Example Title",
653 line: 1,
654 col: 1,
655 offset: 0,
656 },
657}"#
658 );
659 }
660}