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 crate::tests::prelude::*;
193
194 #[test]
195 fn impl_clone() {
196 let mut parser = Parser::default();
198
199 let h1 = crate::document::Header::parse(crate::Span::new("= Title"), &mut parser)
200 .unwrap_if_no_warnings();
201 let h2 = h1.clone();
202
203 assert_eq!(h1, h2);
204 }
205
206 #[test]
207 fn only_title() {
208 let mut parser = Parser::default();
209 let mi = crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
210 .unwrap_if_no_warnings();
211
212 assert_eq!(
213 mi.item,
214 Header {
215 title_source: Some(Span {
216 data: "Just the Title",
217 line: 1,
218 col: 3,
219 offset: 2,
220 }),
221 title: Some("Just the Title"),
222 attributes: &[],
223 author_line: None,
224 revision_line: None,
225 comments: &[],
226 source: Span {
227 data: "= Just the Title",
228 line: 1,
229 col: 1,
230 offset: 0,
231 }
232 }
233 );
234
235 assert_eq!(
236 mi.after,
237 Span {
238 data: "",
239 line: 1,
240 col: 17,
241 offset: 16
242 }
243 );
244 }
245
246 #[test]
247 fn trims_leading_spaces_in_title() {
248 let mut parser = Parser::default();
251 let mi =
252 crate::document::Header::parse(crate::Span::new("= Just the Title"), &mut parser)
253 .unwrap_if_no_warnings();
254
255 assert_eq!(
256 mi.item,
257 Header {
258 title_source: Some(Span {
259 data: "Just the Title",
260 line: 1,
261 col: 6,
262 offset: 5,
263 }),
264 title: Some("Just the Title"),
265 attributes: &[],
266 author_line: None,
267 revision_line: None,
268 comments: &[],
269 source: Span {
270 data: "= Just the Title",
271 line: 1,
272 col: 1,
273 offset: 0,
274 }
275 }
276 );
277
278 assert_eq!(
279 mi.after,
280 Span {
281 data: "",
282 line: 1,
283 col: 20,
284 offset: 19
285 }
286 );
287 }
288
289 #[test]
290 fn trims_trailing_spaces_in_title() {
291 let mut parser = Parser::default();
292 let mi =
293 crate::document::Header::parse(crate::Span::new("= Just the Title "), &mut parser)
294 .unwrap_if_no_warnings();
295
296 assert_eq!(
297 mi.item,
298 Header {
299 title_source: Some(Span {
300 data: "Just the Title",
301 line: 1,
302 col: 3,
303 offset: 2,
304 }),
305 title: Some("Just the Title"),
306 attributes: &[],
307 author_line: None,
308 revision_line: None,
309 comments: &[],
310 source: Span {
311 data: "= Just the Title",
312 line: 1,
313 col: 1,
314 offset: 0,
315 }
316 }
317 );
318
319 assert_eq!(
320 mi.after,
321 Span {
322 data: "",
323 line: 1,
324 col: 20,
325 offset: 19
326 }
327 );
328 }
329
330 #[test]
331 fn title_and_attribute() {
332 let mut parser = Parser::default();
333
334 let mi = crate::document::Header::parse(
335 crate::Span::new("= Just the Title\n:foo: bar\n\nblah"),
336 &mut parser,
337 )
338 .unwrap_if_no_warnings();
339
340 assert_eq!(
341 mi.item,
342 Header {
343 title_source: Some(Span {
344 data: "Just the Title",
345 line: 1,
346 col: 3,
347 offset: 2,
348 }),
349 title: Some("Just the Title"),
350 attributes: &[Attribute {
351 name: Span {
352 data: "foo",
353 line: 2,
354 col: 2,
355 offset: 18,
356 },
357 value_source: Some(Span {
358 data: "bar",
359 line: 2,
360 col: 7,
361 offset: 23,
362 }),
363 value: InterpretedValue::Value("bar"),
364 source: Span {
365 data: ":foo: bar",
366 line: 2,
367 col: 1,
368 offset: 17,
369 }
370 }],
371 author_line: None,
372 revision_line: None,
373 comments: &[],
374 source: Span {
375 data: "= Just the Title\n:foo: bar",
376 line: 1,
377 col: 1,
378 offset: 0,
379 }
380 }
381 );
382
383 assert_eq!(
384 mi.after,
385 Span {
386 data: "blah",
387 line: 4,
388 col: 1,
389 offset: 28
390 }
391 );
392 }
393
394 #[test]
395 fn title_applies_header_substitutions() {
396 let mut parser = Parser::default();
397
398 let mi = crate::document::Header::parse(
399 crate::Span::new("= The Title & Some{sp}Nonsense\n:foo: bar\n\nblah"),
400 &mut parser,
401 )
402 .unwrap_if_no_warnings();
403
404 assert_eq!(
405 mi.item,
406 Header {
407 title_source: Some(Span {
408 data: "The Title & Some{sp}Nonsense",
409 line: 1,
410 col: 3,
411 offset: 2,
412 }),
413 title: Some("The Title & Some Nonsense"),
414 attributes: &[Attribute {
415 name: Span {
416 data: "foo",
417 line: 2,
418 col: 2,
419 offset: 32,
420 },
421 value_source: Some(Span {
422 data: "bar",
423 line: 2,
424 col: 7,
425 offset: 37,
426 }),
427 value: InterpretedValue::Value("bar"),
428 source: Span {
429 data: ":foo: bar",
430 line: 2,
431 col: 1,
432 offset: 31,
433 }
434 }],
435 author_line: None,
436 revision_line: None,
437 comments: &[],
438 source: Span {
439 data: "= The Title & Some{sp}Nonsense\n:foo: bar",
440 line: 1,
441 col: 1,
442 offset: 0,
443 }
444 }
445 );
446
447 assert_eq!(
448 mi.after,
449 Span {
450 data: "blah",
451 line: 4,
452 col: 1,
453 offset: 42
454 }
455 );
456 }
457
458 #[test]
459 fn attribute_without_title() {
460 let mut parser = Parser::default();
461 let mi = crate::document::Header::parse(crate::Span::new(":foo: bar\n\nblah"), &mut parser)
462 .unwrap_if_no_warnings();
463
464 assert_eq!(
465 mi.item,
466 Header {
467 title_source: None,
468 title: None,
469 attributes: &[Attribute {
470 name: Span {
471 data: "foo",
472 line: 1,
473 col: 2,
474 offset: 1,
475 },
476 value_source: Some(Span {
477 data: "bar",
478 line: 1,
479 col: 7,
480 offset: 6,
481 }),
482 value: InterpretedValue::Value("bar"),
483 source: Span {
484 data: ":foo: bar",
485 line: 1,
486 col: 1,
487 offset: 0,
488 }
489 }],
490 author_line: None,
491 revision_line: None,
492 comments: &[],
493 source: Span {
494 data: ":foo: bar",
495 line: 1,
496 col: 1,
497 offset: 0,
498 }
499 }
500 );
501
502 assert_eq!(
503 mi.after,
504 Span {
505 data: "blah",
506 line: 3,
507 col: 1,
508 offset: 11
509 }
510 );
511 }
512
513 #[test]
514 fn sets_doctitle_attribute() {
515 let mut parser = Parser::default();
516 let _doc = parser.parse("= Document Title Goes Here");
517
518 assert_eq!(
519 parser.attribute_value("doctitle"),
520 InterpretedValue::Value("Document Title Goes Here")
521 );
522 }
523
524 #[test]
525 fn sets_author_attributes_from_author_attribute() {
526 let mut parser = Parser::default();
527 let _doc = parser.parse(":author: John Q. Smith <john@example.com>");
528
529 assert_eq!(
531 parser.attribute_value("firstname"),
532 InterpretedValue::Value("John")
533 );
534 assert_eq!(
535 parser.attribute_value("middlename"),
536 InterpretedValue::Value("Q.")
537 );
538 assert_eq!(
539 parser.attribute_value("lastname"),
540 InterpretedValue::Value("Smith")
541 );
542 assert_eq!(
543 parser.attribute_value("authorinitials"),
544 InterpretedValue::Value("JQS")
545 );
546 assert_eq!(
547 parser.attribute_value("email"),
548 InterpretedValue::Value("john@example.com")
549 );
550
551 assert_eq!(
553 parser.attribute_value("author"),
554 InterpretedValue::Value("John Q. Smith <john@example.com>")
555 );
556 }
557
558 #[test]
559 fn sets_author_attributes_from_author_attribute_two_names() {
560 let mut parser = Parser::default();
561 let _doc = parser.parse(":author: Jane Doe");
562
563 assert_eq!(
565 parser.attribute_value("firstname"),
566 InterpretedValue::Value("Jane")
567 );
568 assert_eq!(
569 parser.attribute_value("middlename"),
570 InterpretedValue::Unset
571 );
572 assert_eq!(
573 parser.attribute_value("lastname"),
574 InterpretedValue::Value("Doe")
575 );
576 assert_eq!(
577 parser.attribute_value("authorinitials"),
578 InterpretedValue::Value("JD")
579 );
580 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
581 }
582
583 #[test]
584 fn sets_author_attributes_from_author_attribute_single_name() {
585 let mut parser = Parser::default();
586 let _doc = parser.parse(":author: Cher");
587
588 assert_eq!(
590 parser.attribute_value("firstname"),
591 InterpretedValue::Value("Cher")
592 );
593 assert_eq!(
594 parser.attribute_value("middlename"),
595 InterpretedValue::Unset
596 );
597 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
598 assert_eq!(
599 parser.attribute_value("authorinitials"),
600 InterpretedValue::Value("C")
601 );
602 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
603 }
604
605 #[test]
606 fn sets_author_attributes_from_empty_string() {
607 let mut parser = Parser::default();
608 let _doc = parser.parse(":author:");
609
610 assert_eq!(parser.attribute_value("firstname"), InterpretedValue::Unset);
612 assert_eq!(
613 parser.attribute_value("middlename"),
614 InterpretedValue::Unset
615 );
616 assert_eq!(parser.attribute_value("lastname"), InterpretedValue::Unset);
617 assert_eq!(
618 parser.attribute_value("authorinitials"),
619 InterpretedValue::Unset
620 );
621 assert_eq!(parser.attribute_value("email"), InterpretedValue::Unset);
622
623 assert_eq!(parser.attribute_value("author"), InterpretedValue::Set);
624 }
625
626 #[test]
627 fn impl_debug() {
628 let doc = Parser::default().parse("= Example Title\n\nabc\n\ndef");
629 let header = doc.header();
630
631 assert_eq!(
632 format!("{header:#?}"),
633 r#"Header {
634 title_source: Some(
635 Span {
636 data: "Example Title",
637 line: 1,
638 col: 3,
639 offset: 2,
640 },
641 ),
642 title: Some(
643 "Example Title",
644 ),
645 attributes: &[],
646 author_line: None,
647 revision_line: None,
648 comments: &[],
649 source: Span {
650 data: "= Example Title",
651 line: 1,
652 col: 1,
653 offset: 0,
654 },
655}"#
656 );
657 }
658}