]> git.proxmox.com Git - rustc.git/blame - vendor/mdbook/src/book/summary.rs
New upstream version 1.68.2+dfsg1
[rustc.git] / vendor / mdbook / src / book / summary.rs
CommitLineData
dc9dc135 1use crate::errors::*;
f25598a0 2use log::{debug, trace, warn};
9fa01778 3use memchr::{self, Memchr};
a2a8927a 4use pulldown_cmark::{self, Event, HeadingLevel, Tag};
064997fb 5use serde::{Deserialize, Serialize};
2c00a5a8
XL
6use std::fmt::{self, Display, Formatter};
7use std::iter::FromIterator;
8use std::ops::{Deref, DerefMut};
9use std::path::{Path, PathBuf};
2c00a5a8
XL
10
11/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
12/// used when loading a book from disk.
13///
14/// # Summary Format
15///
16/// **Title:** It's common practice to begin with a title, generally
17/// "# Summary". It's not mandatory and the parser (currently) ignores it, so
18/// you can too if you feel like it.
19///
20/// **Prefix Chapter:** Before the main numbered chapters you can add a couple
21/// of elements that will not be numbered. This is useful for forewords,
22/// introductions, etc. There are however some constraints. You can not nest
23/// prefix chapters, they should all be on the root level. And you can not add
24/// prefix chapters once you have added numbered chapters.
25///
26/// ```markdown
27/// [Title of prefix element](relative/path/to/markdown.md)
28/// ```
29///
f035d41b
XL
30/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered
31/// chapters can be broken into as many parts as desired.
32///
2c00a5a8
XL
33/// **Numbered Chapter:** Numbered chapters are the main content of the book,
34/// they
35/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
36/// sub-chapters, etc.)
37///
38/// ```markdown
f035d41b
XL
39/// # Title of Part
40///
2c00a5a8
XL
41/// - [Title of the Chapter](relative/path/to/markdown.md)
42/// ```
43///
44/// You can either use - or * to indicate a numbered chapter, the parser doesn't
45/// care but you'll probably want to stay consistent.
46///
47/// **Suffix Chapter:** After the numbered chapters you can add a couple of
48/// non-numbered chapters. They are the same as prefix chapters but come after
49/// the numbered chapters instead of before.
50///
51/// All other elements are unsupported and will be ignored at best or result in
52/// an error.
53pub fn parse_summary(summary: &str) -> Result<Summary> {
54 let parser = SummaryParser::new(summary);
55 parser.parse()
56}
57
58/// The parsed `SUMMARY.md`, specifying how the book should be laid out.
59#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
60pub struct Summary {
61 /// An optional title for the `SUMMARY.md`, currently just ignored.
62 pub title: Option<String>,
63 /// Chapters before the main text (e.g. an introduction).
64 pub prefix_chapters: Vec<SummaryItem>,
f035d41b 65 /// The main numbered chapters of the book, broken into one or more possibly named parts.
2c00a5a8
XL
66 pub numbered_chapters: Vec<SummaryItem>,
67 /// Items which come after the main document (e.g. a conclusion).
68 pub suffix_chapters: Vec<SummaryItem>,
69}
70
71/// A struct representing an entry in the `SUMMARY.md`, possibly with nested
72/// entries.
73///
74/// This is roughly the equivalent of `[Some section](./path/to/file.md)`.
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub struct Link {
77 /// The name of the chapter.
78 pub name: String,
79 /// The location of the chapter's source file, taking the book's `src`
80 /// directory as the root.
f035d41b 81 pub location: Option<PathBuf>,
2c00a5a8
XL
82 /// The section number, if this chapter is in the numbered section.
83 pub number: Option<SectionNumber>,
84 /// Any nested items this chapter may contain.
85 pub nested_items: Vec<SummaryItem>,
86}
87
88impl Link {
89 /// Create a new link with no nested items.
90 pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
91 Link {
92 name: name.into(),
f035d41b 93 location: Some(location.as_ref().to_path_buf()),
2c00a5a8
XL
94 number: None,
95 nested_items: Vec::new(),
96 }
97 }
98}
99
100impl Default for Link {
101 fn default() -> Self {
102 Link {
103 name: String::new(),
f035d41b 104 location: Some(PathBuf::new()),
2c00a5a8
XL
105 number: None,
106 nested_items: Vec::new(),
107 }
108 }
109}
110
111/// An item in `SUMMARY.md` which could be either a separator or a `Link`.
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub enum SummaryItem {
114 /// A link to a chapter.
115 Link(Link),
116 /// A separator (`---`).
117 Separator,
f035d41b
XL
118 /// A part title.
119 PartTitle(String),
2c00a5a8
XL
120}
121
122impl SummaryItem {
123 fn maybe_link_mut(&mut self) -> Option<&mut Link> {
124 match *self {
125 SummaryItem::Link(ref mut l) => Some(l),
126 _ => None,
127 }
128 }
129}
130
131impl From<Link> for SummaryItem {
132 fn from(other: Link) -> SummaryItem {
133 SummaryItem::Link(other)
134 }
135}
136
137/// A recursive descent (-ish) parser for a `SUMMARY.md`.
138///
139///
140/// # Grammar
141///
142/// The `SUMMARY.md` file has a grammar which looks something like this:
143///
144/// ```text
145/// summary ::= title prefix_chapters numbered_chapters
f035d41b 146/// suffix_chapters
2c00a5a8
XL
147/// title ::= "# " TEXT
148/// | EPSILON
149/// prefix_chapters ::= item*
150/// suffix_chapters ::= item*
f035d41b
XL
151/// numbered_chapters ::= part+
152/// part ::= title dotted_item+
2c00a5a8
XL
153/// dotted_item ::= INDENT* DOT_POINT item
154/// item ::= link
155/// | separator
156/// separator ::= "---"
157/// link ::= "[" TEXT "]" "(" TEXT ")"
158/// DOT_POINT ::= "-"
159/// | "*"
160/// ```
161///
162/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly)
163/// > match the following regex: "[^<>\n[]]+".
164struct SummaryParser<'a> {
165 src: &'a str,
a2a8927a 166 stream: pulldown_cmark::OffsetIter<'a, 'a>,
e74abb32 167 offset: usize,
f035d41b
XL
168
169 /// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
170 /// here until somebody calls `next_event` again.
171 back: Option<Event<'a>>,
2c00a5a8
XL
172}
173
174/// Reads `Events` from the provided stream until the corresponding
175/// `Event::End` is encountered which matches the `$delimiter` pattern.
176///
177/// This is the equivalent of doing
94222f64 178/// `$stream.take_while(|e| e != $delimiter).collect()` but it allows you to
2c00a5a8
XL
179/// use pattern matching and you won't get errors because `take_while()`
180/// moves `$stream` out of self.
181macro_rules! collect_events {
9fa01778 182 ($stream:expr,start $delimiter:pat) => {
2c00a5a8
XL
183 collect_events!($stream, Event::Start($delimiter))
184 };
9fa01778 185 ($stream:expr,end $delimiter:pat) => {
2c00a5a8
XL
186 collect_events!($stream, Event::End($delimiter))
187 };
9fa01778
XL
188 ($stream:expr, $delimiter:pat) => {{
189 let mut events = Vec::new();
190
191 loop {
e74abb32 192 let event = $stream.next().map(|(ev, _range)| ev);
9fa01778
XL
193 trace!("Next event: {:?}", event);
194
195 match event {
196 Some($delimiter) => break,
197 Some(other) => events.push(other),
198 None => {
199 debug!(
200 "Reached end of stream without finding the closing pattern, {}",
201 stringify!($delimiter)
202 );
203 break;
2c00a5a8
XL
204 }
205 }
2c00a5a8 206 }
9fa01778
XL
207
208 events
209 }};
2c00a5a8
XL
210}
211
212impl<'a> SummaryParser<'a> {
dc9dc135 213 fn new(text: &str) -> SummaryParser<'_> {
e74abb32 214 let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
2c00a5a8
XL
215
216 SummaryParser {
217 src: text,
218 stream: pulldown_parser,
e74abb32 219 offset: 0,
f035d41b 220 back: None,
2c00a5a8
XL
221 }
222 }
223
224 /// Get the current line and column to give the user more useful error
225 /// messages.
226 fn current_location(&self) -> (usize, usize) {
e74abb32 227 let previous_text = self.src[..self.offset].as_bytes();
2c00a5a8
XL
228 let line = Memchr::new(b'\n', previous_text).count() + 1;
229 let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
e74abb32 230 let col = self.src[start_of_line..self.offset].chars().count();
2c00a5a8
XL
231
232 (line, col)
233 }
234
235 /// Parse the text the `SummaryParser` was created with.
236 fn parse(mut self) -> Result<Summary> {
237 let title = self.parse_title();
238
9fa01778
XL
239 let prefix_chapters = self
240 .parse_affix(true)
f035d41b 241 .with_context(|| "There was an error parsing the prefix chapters")?;
9fa01778 242 let numbered_chapters = self
f035d41b
XL
243 .parse_parts()
244 .with_context(|| "There was an error parsing the numbered chapters")?;
9fa01778
XL
245 let suffix_chapters = self
246 .parse_affix(false)
f035d41b 247 .with_context(|| "There was an error parsing the suffix chapters")?;
2c00a5a8
XL
248
249 Ok(Summary {
250 title,
251 prefix_chapters,
252 numbered_chapters,
253 suffix_chapters,
254 })
255 }
256
f035d41b 257 /// Parse the affix chapters.
2c00a5a8
XL
258 fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
259 let mut items = Vec::new();
260 debug!(
261 "Parsing {} items",
262 if is_prefix { "prefix" } else { "suffix" }
263 );
264
265 loop {
266 match self.next_event() {
f035d41b 267 Some(ev @ Event::Start(Tag::List(..)))
a2a8927a 268 | Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
2c00a5a8
XL
269 if is_prefix {
270 // we've finished prefix chapters and are at the start
271 // of the numbered section.
f035d41b 272 self.back(ev);
2c00a5a8
XL
273 break;
274 } else {
275 bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
276 }
277 }
dc9dc135 278 Some(Event::Start(Tag::Link(_type, href, _title))) => {
f035d41b 279 let link = self.parse_link(href.to_string());
2c00a5a8
XL
280 items.push(SummaryItem::Link(link));
281 }
e74abb32 282 Some(Event::Rule) => items.push(SummaryItem::Separator),
2c00a5a8
XL
283 Some(_) => {}
284 None => break,
285 }
286 }
287
288 Ok(items)
289 }
290
f035d41b
XL
291 fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
292 let mut parts = vec![];
293
294 // We want the section numbers to be continues through all parts.
295 let mut root_number = SectionNumber::default();
296 let mut root_items = 0;
297
298 loop {
299 // Possibly match a title or the end of the "numbered chapters part".
300 let title = match self.next_event() {
301 Some(ev @ Event::Start(Tag::Paragraph)) => {
302 // we're starting the suffix chapters
303 self.back(ev);
304 break;
305 }
306
a2a8927a 307 Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
f035d41b
XL
308 debug!("Found a h1 in the SUMMARY");
309
a2a8927a 310 let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
f035d41b
XL
311 Some(stringify_events(tags))
312 }
313
314 Some(ev) => {
315 self.back(ev);
316 None
317 }
318
319 None => break, // EOF, bail...
320 };
321
322 // Parse the rest of the part.
323 let numbered_chapters = self
324 .parse_numbered(&mut root_items, &mut root_number)
325 .with_context(|| "There was an error parsing the numbered chapters")?;
326
327 if let Some(title) = title {
328 parts.push(SummaryItem::PartTitle(title));
329 }
330 parts.extend(numbered_chapters);
331 }
332
333 Ok(parts)
334 }
335
336 /// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
337 fn parse_link(&mut self, href: String) -> Link {
3dfed10e 338 let href = href.replace("%20", " ");
2c00a5a8
XL
339 let link_content = collect_events!(self.stream, end Tag::Link(..));
340 let name = stringify_events(link_content);
341
f035d41b
XL
342 let path = if href.is_empty() {
343 None
2c00a5a8 344 } else {
f035d41b
XL
345 Some(PathBuf::from(href))
346 };
347
348 Link {
349 name,
350 location: path,
351 number: None,
352 nested_items: Vec::new(),
2c00a5a8
XL
353 }
354 }
355
f035d41b
XL
356 /// Parse the numbered chapters.
357 fn parse_numbered(
358 &mut self,
359 root_items: &mut u32,
360 root_number: &mut SectionNumber,
361 ) -> Result<Vec<SummaryItem>> {
2c00a5a8 362 let mut items = Vec::new();
2c00a5a8 363
f035d41b
XL
364 // For the first iteration, we want to just skip any opening paragraph tags, as that just
365 // marks the start of the list. But after that, another opening paragraph indicates that we
366 // have started a new part or the suffix chapters.
367 let mut first = true;
2c00a5a8
XL
368
369 loop {
2c00a5a8 370 match self.next_event() {
f035d41b
XL
371 Some(ev @ Event::Start(Tag::Paragraph)) => {
372 if !first {
373 // we're starting the suffix chapters
374 self.back(ev);
375 break;
376 }
377 }
378 // The expectation is that pulldown cmark will terminate a paragraph before a new
379 // heading, so we can always count on this to return without skipping headings.
a2a8927a 380 Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
f035d41b
XL
381 // we're starting a new part
382 self.back(ev);
2c00a5a8
XL
383 break;
384 }
f035d41b
XL
385 Some(ev @ Event::Start(Tag::List(..))) => {
386 self.back(ev);
a2a8927a 387 let mut bunch_of_items = self.parse_nested_numbered(root_number)?;
f035d41b
XL
388
389 // if we've resumed after something like a rule the root sections
390 // will be numbered from 1. We need to manually go back and update
391 // them
392 update_section_numbers(&mut bunch_of_items, 0, *root_items);
393 *root_items += bunch_of_items.len() as u32;
394 items.extend(bunch_of_items);
395 }
2c00a5a8 396 Some(Event::Start(other_tag)) => {
2c00a5a8
XL
397 trace!("Skipping contents of {:?}", other_tag);
398
399 // Skip over the contents of this tag
400 while let Some(event) = self.next_event() {
83c7162d
XL
401 if event == Event::End(other_tag.clone()) {
402 break;
2c00a5a8
XL
403 }
404 }
2c00a5a8 405 }
e74abb32
XL
406 Some(Event::Rule) => {
407 items.push(SummaryItem::Separator);
2c00a5a8 408 }
f035d41b
XL
409
410 // something else... ignore
411 Some(_) => {}
412
413 // EOF, bail...
2c00a5a8 414 None => {
2c00a5a8
XL
415 break;
416 }
417 }
f035d41b
XL
418
419 // From now on, we cannot accept any new paragraph opening tags.
420 first = false;
2c00a5a8
XL
421 }
422
423 Ok(items)
424 }
425
f035d41b
XL
426 /// Push an event back to the tail of the stream.
427 fn back(&mut self, ev: Event<'a>) {
428 assert!(self.back.is_none());
429 trace!("Back: {:?}", ev);
430 self.back = Some(ev);
431 }
432
2c00a5a8 433 fn next_event(&mut self) -> Option<Event<'a>> {
f035d41b
XL
434 let next = self.back.take().or_else(|| {
435 self.stream.next().map(|(ev, range)| {
436 self.offset = range.start;
437 ev
438 })
e74abb32 439 });
f035d41b 440
2c00a5a8
XL
441 trace!("Next event: {:?}", next);
442
443 next
444 }
445
446 fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result<Vec<SummaryItem>> {
447 debug!("Parsing numbered chapters at level {}", parent);
448 let mut items = Vec::new();
449
450 loop {
451 match self.next_event() {
452 Some(Event::Start(Tag::Item)) => {
453 let item = self.parse_nested_item(parent, items.len())?;
454 items.push(item);
455 }
456 Some(Event::Start(Tag::List(..))) => {
f25598a0 457 // Skip this tag after comment because it is not nested.
f9f354fc
XL
458 if items.is_empty() {
459 continue;
460 }
2c00a5a8
XL
461 // recurse to parse the nested list
462 let (_, last_item) = get_last_link(&mut items)?;
463 let last_item_number = last_item
464 .number
465 .as_ref()
466 .expect("All numbered chapters have numbers");
467
468 let sub_items = self.parse_nested_numbered(last_item_number)?;
469
470 last_item.nested_items = sub_items;
471 }
472 Some(Event::End(Tag::List(..))) => break,
473 Some(_) => {}
474 None => break,
475 }
476 }
477
478 Ok(items)
479 }
480
481 fn parse_nested_item(
482 &mut self,
483 parent: &SectionNumber,
484 num_existing_items: usize,
485 ) -> Result<SummaryItem> {
486 loop {
487 match self.next_event() {
488 Some(Event::Start(Tag::Paragraph)) => continue,
dc9dc135 489 Some(Event::Start(Tag::Link(_type, href, _title))) => {
f035d41b 490 let mut link = self.parse_link(href.to_string());
2c00a5a8
XL
491
492 let mut number = parent.clone();
493 number.0.push(num_existing_items as u32 + 1);
494 trace!(
495 "Found chapter: {} {} ({})",
496 number,
497 link.name,
f035d41b
XL
498 link.location
499 .as_ref()
500 .map(|p| p.to_str().unwrap_or(""))
501 .unwrap_or("[draft]")
2c00a5a8
XL
502 );
503
504 link.number = Some(number);
505
506 return Ok(SummaryItem::Link(link));
507 }
508 other => {
509 warn!("Expected a start of a link, actually got {:?}", other);
510 bail!(self.parse_error(
511 "The link items for nested chapters must only contain a hyperlink"
512 ));
513 }
514 }
515 }
516 }
517
518 fn parse_error<D: Display>(&self, msg: D) -> Error {
519 let (line, col) = self.current_location();
f035d41b
XL
520 anyhow::anyhow!(
521 "failed to parse SUMMARY.md line {}, column {}: {}",
522 line,
523 col,
524 msg
525 )
2c00a5a8
XL
526 }
527
528 /// Try to parse the title line.
529 fn parse_title(&mut self) -> Option<String> {
94222f64
XL
530 loop {
531 match self.next_event() {
a2a8927a 532 Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
94222f64 533 debug!("Found a h1 in the SUMMARY");
2c00a5a8 534
a2a8927a 535 let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
94222f64
XL
536 return Some(stringify_events(tags));
537 }
538 // Skip a HTML element such as a comment line.
539 Some(Event::Html(_)) => {}
540 // Otherwise, no title.
04454e1e
FG
541 Some(ev) => {
542 self.back(ev);
543 return None;
544 }
94222f64 545 _ => return None,
f035d41b 546 }
2c00a5a8
XL
547 }
548 }
549}
550
551fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) {
552 for section in sections {
553 if let SummaryItem::Link(ref mut link) = *section {
554 if let Some(ref mut number) = link.number {
555 number.0[level] += by;
556 }
557
558 update_section_numbers(&mut link.nested_items, level, by);
559 }
560 }
561}
562
563/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its
564/// index.
565fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
566 links
567 .iter_mut()
568 .enumerate()
569 .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
570 .rev()
571 .next()
f035d41b
XL
572 .ok_or_else(||
573 anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
574 )
2c00a5a8
XL
575}
576
577/// Removes the styling from a list of Markdown events and returns just the
578/// plain text.
dc9dc135 579fn stringify_events(events: Vec<Event<'_>>) -> String {
2c00a5a8
XL
580 events
581 .into_iter()
582 .filter_map(|t| match t {
dc9dc135 583 Event::Text(text) | Event::Code(text) => Some(text.into_string()),
3dfed10e 584 Event::SoftBreak => Some(String::from(" ")),
2c00a5a8 585 _ => None,
dc9dc135
XL
586 })
587 .collect()
2c00a5a8
XL
588}
589
2c00a5a8
XL
590/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
591/// a pretty `Display` impl.
592#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
593pub struct SectionNumber(pub Vec<u32>);
594
595impl Display for SectionNumber {
dc9dc135 596 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2c00a5a8
XL
597 if self.0.is_empty() {
598 write!(f, "0")
599 } else {
600 for item in &self.0 {
601 write!(f, "{}.", item)?;
602 }
603 Ok(())
604 }
605 }
606}
607
608impl Deref for SectionNumber {
609 type Target = Vec<u32>;
610 fn deref(&self) -> &Self::Target {
611 &self.0
612 }
613}
614
615impl DerefMut for SectionNumber {
616 fn deref_mut(&mut self) -> &mut Self::Target {
617 &mut self.0
618 }
619}
620
621impl FromIterator<u32> for SectionNumber {
622 fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
623 SectionNumber(it.into_iter().collect())
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn section_number_has_correct_dotted_representation() {
633 let inputs = vec![
634 (vec![0], "0."),
635 (vec![1, 3], "1.3."),
636 (vec![1, 2, 3], "1.2.3."),
637 ];
638
639 for (input, should_be) in inputs {
640 let section_number = SectionNumber(input).to_string();
641 assert_eq!(section_number, should_be);
642 }
643 }
644
645 #[test]
646 fn parse_initial_title() {
647 let src = "# Summary";
648 let should_be = String::from("Summary");
649
650 let mut parser = SummaryParser::new(src);
651 let got = parser.parse_title().unwrap();
652
653 assert_eq!(got, should_be);
654 }
655
04454e1e
FG
656 #[test]
657 fn no_initial_title() {
658 let src = "[Link]()";
659 let mut parser = SummaryParser::new(src);
660
661 assert!(parser.parse_title().is_none());
662 assert!(matches!(
663 parser.next_event(),
664 Some(Event::Start(Tag::Paragraph))
665 ));
666 }
667
2c00a5a8
XL
668 #[test]
669 fn parse_title_with_styling() {
670 let src = "# My **Awesome** Summary";
671 let should_be = String::from("My Awesome Summary");
672
673 let mut parser = SummaryParser::new(src);
674 let got = parser.parse_title().unwrap();
675
676 assert_eq!(got, should_be);
677 }
678
679 #[test]
680 fn convert_markdown_events_to_a_string() {
681 let src = "Hello *World*, `this` is some text [and a link](./path/to/link)";
682 let should_be = "Hello World, this is some text and a link";
683
684 let events = pulldown_cmark::Parser::new(src).collect();
685 let got = stringify_events(events);
686
687 assert_eq!(got, should_be);
688 }
689
690 #[test]
691 fn parse_some_prefix_items() {
692 let src = "[First](./first.md)\n[Second](./second.md)\n";
693 let mut parser = SummaryParser::new(src);
694
695 let should_be = vec![
696 SummaryItem::Link(Link {
697 name: String::from("First"),
f035d41b 698 location: Some(PathBuf::from("./first.md")),
2c00a5a8
XL
699 ..Default::default()
700 }),
701 SummaryItem::Link(Link {
702 name: String::from("Second"),
f035d41b 703 location: Some(PathBuf::from("./second.md")),
2c00a5a8
XL
704 ..Default::default()
705 }),
706 ];
707
2c00a5a8
XL
708 let got = parser.parse_affix(true).unwrap();
709
710 assert_eq!(got, should_be);
711 }
712
713 #[test]
714 fn parse_prefix_items_with_a_separator() {
715 let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
716 let mut parser = SummaryParser::new(src);
717
2c00a5a8
XL
718 let got = parser.parse_affix(true).unwrap();
719
720 assert_eq!(got.len(), 3);
721 assert_eq!(got[1], SummaryItem::Separator);
722 }
723
724 #[test]
725 fn suffix_items_cannot_be_followed_by_a_list() {
726 let src = "[First](./first.md)\n- [Second](./second.md)\n";
727 let mut parser = SummaryParser::new(src);
728
2c00a5a8
XL
729 let got = parser.parse_affix(false);
730
731 assert!(got.is_err());
732 }
733
734 #[test]
735 fn parse_a_link() {
736 let src = "[First](./first.md)";
737 let should_be = Link {
738 name: String::from("First"),
f035d41b 739 location: Some(PathBuf::from("./first.md")),
2c00a5a8
XL
740 ..Default::default()
741 };
742
743 let mut parser = SummaryParser::new(src);
f035d41b 744 let _ = parser.stream.next(); // Discard opening paragraph
2c00a5a8
XL
745
746 let href = match parser.stream.next() {
e74abb32 747 Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
2c00a5a8
XL
748 other => panic!("Unreachable, {:?}", other),
749 };
750
f035d41b 751 let got = parser.parse_link(href);
2c00a5a8
XL
752 assert_eq!(got, should_be);
753 }
754
755 #[test]
756 fn parse_a_numbered_chapter() {
757 let src = "- [First](./first.md)\n";
758 let link = Link {
759 name: String::from("First"),
f035d41b 760 location: Some(PathBuf::from("./first.md")),
2c00a5a8
XL
761 number: Some(SectionNumber(vec![1])),
762 ..Default::default()
763 };
764 let should_be = vec![SummaryItem::Link(link)];
765
766 let mut parser = SummaryParser::new(src);
f035d41b
XL
767 let got = parser
768 .parse_numbered(&mut 0, &mut SectionNumber::default())
769 .unwrap();
2c00a5a8
XL
770
771 assert_eq!(got, should_be);
772 }
773
774 #[test]
775 fn parse_nested_numbered_chapters() {
776 let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)";
777
778 let should_be = vec![
779 SummaryItem::Link(Link {
780 name: String::from("First"),
f035d41b 781 location: Some(PathBuf::from("./first.md")),
2c00a5a8 782 number: Some(SectionNumber(vec![1])),
9fa01778
XL
783 nested_items: vec![SummaryItem::Link(Link {
784 name: String::from("Nested"),
f035d41b 785 location: Some(PathBuf::from("./nested.md")),
9fa01778
XL
786 number: Some(SectionNumber(vec![1, 1])),
787 nested_items: Vec::new(),
788 })],
2c00a5a8
XL
789 }),
790 SummaryItem::Link(Link {
791 name: String::from("Second"),
f035d41b 792 location: Some(PathBuf::from("./second.md")),
2c00a5a8 793 number: Some(SectionNumber(vec![2])),
f9f354fc
XL
794 nested_items: Vec::new(),
795 }),
796 ];
797
798 let mut parser = SummaryParser::new(src);
f035d41b
XL
799 let got = parser
800 .parse_numbered(&mut 0, &mut SectionNumber::default())
801 .unwrap();
f9f354fc
XL
802
803 assert_eq!(got, should_be);
804 }
805
806 #[test]
807 fn parse_numbered_chapters_separated_by_comment() {
808 let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)";
809
810 let should_be = vec![
811 SummaryItem::Link(Link {
812 name: String::from("First"),
f035d41b 813 location: Some(PathBuf::from("./first.md")),
f9f354fc
XL
814 number: Some(SectionNumber(vec![1])),
815 nested_items: Vec::new(),
816 }),
817 SummaryItem::Link(Link {
818 name: String::from("Second"),
f035d41b 819 location: Some(PathBuf::from("./second.md")),
f9f354fc 820 number: Some(SectionNumber(vec![2])),
2c00a5a8
XL
821 nested_items: Vec::new(),
822 }),
823 ];
824
825 let mut parser = SummaryParser::new(src);
f035d41b
XL
826 let got = parser
827 .parse_numbered(&mut 0, &mut SectionNumber::default())
828 .unwrap();
829
830 assert_eq!(got, should_be);
831 }
2c00a5a8 832
f035d41b
XL
833 #[test]
834 fn parse_titled_parts() {
835 let src = "- [First](./first.md)\n- [Second](./second.md)\n\
836 # Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
837
838 let should_be = vec![
839 SummaryItem::Link(Link {
840 name: String::from("First"),
841 location: Some(PathBuf::from("./first.md")),
842 number: Some(SectionNumber(vec![1])),
843 nested_items: Vec::new(),
844 }),
845 SummaryItem::Link(Link {
846 name: String::from("Second"),
847 location: Some(PathBuf::from("./second.md")),
848 number: Some(SectionNumber(vec![2])),
849 nested_items: Vec::new(),
850 }),
851 SummaryItem::PartTitle(String::from("Title 2")),
852 SummaryItem::Link(Link {
853 name: String::from("Third"),
854 location: Some(PathBuf::from("./third.md")),
855 number: Some(SectionNumber(vec![3])),
856 nested_items: vec![SummaryItem::Link(Link {
857 name: String::from("Fourth"),
858 location: Some(PathBuf::from("./fourth.md")),
859 number: Some(SectionNumber(vec![3, 1])),
860 nested_items: Vec::new(),
861 })],
862 }),
863 ];
864
865 let mut parser = SummaryParser::new(src);
866 let got = parser.parse_parts().unwrap();
2c00a5a8
XL
867
868 assert_eq!(got, should_be);
869 }
870
871 /// This test ensures the book will continue to pass because it breaks the
872 /// `SUMMARY.md` up using level 2 headers ([example]).
873 ///
874 /// [example]: https://github.com/rust-lang/book/blob/2c942dc094f4ddcdc7aba7564f80782801197c99/second-edition/src/SUMMARY.md#basic-rust-literacy
875 #[test]
876 fn can_have_a_subheader_between_nested_items() {
877 let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n";
878 let should_be = vec![
879 SummaryItem::Link(Link {
880 name: String::from("First"),
f035d41b 881 location: Some(PathBuf::from("./first.md")),
2c00a5a8
XL
882 number: Some(SectionNumber(vec![1])),
883 nested_items: Vec::new(),
884 }),
885 SummaryItem::Link(Link {
886 name: String::from("Second"),
f035d41b 887 location: Some(PathBuf::from("./second.md")),
2c00a5a8
XL
888 number: Some(SectionNumber(vec![2])),
889 nested_items: Vec::new(),
890 }),
891 ];
892
893 let mut parser = SummaryParser::new(src);
f035d41b
XL
894 let got = parser
895 .parse_numbered(&mut 0, &mut SectionNumber::default())
896 .unwrap();
2c00a5a8
XL
897
898 assert_eq!(got, should_be);
899 }
900
901 #[test]
f035d41b 902 fn an_empty_link_location_is_a_draft_chapter() {
2c00a5a8
XL
903 let src = "- [Empty]()\n";
904 let mut parser = SummaryParser::new(src);
2c00a5a8 905
f035d41b
XL
906 let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
907 let should_be = vec![SummaryItem::Link(Link {
908 name: String::from("Empty"),
909 location: None,
910 number: Some(SectionNumber(vec![1])),
911 nested_items: Vec::new(),
912 })];
913
914 assert!(got.is_ok());
915 assert_eq!(got.unwrap(), should_be);
2c00a5a8 916 }
9fa01778 917
e74abb32 918 /// Regression test for https://github.com/rust-lang/mdBook/issues/779
9fa01778
XL
919 /// Ensure section numbers are correctly incremented after a horizontal separator.
920 #[test]
921 fn keep_numbering_after_separator() {
922 let src =
923 "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
924 let should_be = vec![
925 SummaryItem::Link(Link {
926 name: String::from("First"),
f035d41b 927 location: Some(PathBuf::from("./first.md")),
9fa01778
XL
928 number: Some(SectionNumber(vec![1])),
929 nested_items: Vec::new(),
930 }),
931 SummaryItem::Separator,
932 SummaryItem::Link(Link {
933 name: String::from("Second"),
f035d41b 934 location: Some(PathBuf::from("./second.md")),
9fa01778
XL
935 number: Some(SectionNumber(vec![2])),
936 nested_items: Vec::new(),
937 }),
938 SummaryItem::Separator,
939 SummaryItem::Link(Link {
940 name: String::from("Third"),
f035d41b 941 location: Some(PathBuf::from("./third.md")),
9fa01778
XL
942 number: Some(SectionNumber(vec![3])),
943 nested_items: Vec::new(),
944 }),
945 ];
946
947 let mut parser = SummaryParser::new(src);
f035d41b
XL
948 let got = parser
949 .parse_numbered(&mut 0, &mut SectionNumber::default())
950 .unwrap();
9fa01778
XL
951
952 assert_eq!(got, should_be);
953 }
3dfed10e
XL
954
955 /// Regression test for https://github.com/rust-lang/mdBook/issues/1218
956 /// Ensure chapter names spread across multiple lines have spaces between all the words.
957 #[test]
958 fn add_space_for_multi_line_chapter_names() {
959 let src = "- [Chapter\ntitle](./chapter.md)";
960 let should_be = vec![SummaryItem::Link(Link {
961 name: String::from("Chapter title"),
962 location: Some(PathBuf::from("./chapter.md")),
963 number: Some(SectionNumber(vec![1])),
964 nested_items: Vec::new(),
965 })];
966
967 let mut parser = SummaryParser::new(src);
968 let got = parser
969 .parse_numbered(&mut 0, &mut SectionNumber::default())
970 .unwrap();
971
972 assert_eq!(got, should_be);
973 }
974
975 #[test]
976 fn allow_space_in_link_destination() {
977 let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)";
978 let should_be = vec![
979 SummaryItem::Link(Link {
980 name: String::from("test1"),
981 location: Some(PathBuf::from("./test link1.md")),
982 number: Some(SectionNumber(vec![1])),
983 nested_items: Vec::new(),
984 }),
985 SummaryItem::Link(Link {
986 name: String::from("test2"),
987 location: Some(PathBuf::from("./test link2.md")),
988 number: Some(SectionNumber(vec![2])),
989 nested_items: Vec::new(),
990 }),
991 ];
992 let mut parser = SummaryParser::new(src);
993 let got = parser
994 .parse_numbered(&mut 0, &mut SectionNumber::default())
995 .unwrap();
996
997 assert_eq!(got, should_be);
998 }
94222f64
XL
999
1000 #[test]
1001 fn skip_html_comments() {
1002 let src = r#"<!--
1003# Title - En
1004-->
1005# Title - Local
1006
1007<!--
1008[Prefix 00-01 - En](ch00-01.md)
1009[Prefix 00-02 - En](ch00-02.md)
1010-->
1011[Prefix 00-01 - Local](ch00-01.md)
1012[Prefix 00-02 - Local](ch00-02.md)
1013
1014<!--
1015## Section Title - En
1016-->
1017## Section Title - Localized
1018
1019<!--
1020- [Ch 01-00 - En](ch01-00.md)
1021 - [Ch 01-01 - En](ch01-01.md)
1022 - [Ch 01-02 - En](ch01-02.md)
1023-->
1024- [Ch 01-00 - Local](ch01-00.md)
1025 - [Ch 01-01 - Local](ch01-01.md)
1026 - [Ch 01-02 - Local](ch01-02.md)
1027
1028<!--
1029- [Ch 02-00 - En](ch02-00.md)
1030-->
1031- [Ch 02-00 - Local](ch02-00.md)
1032
1033<!--
1034[Appendix A - En](appendix-01.md)
1035[Appendix B - En](appendix-02.md)
1036-->`
1037[Appendix A - Local](appendix-01.md)
1038[Appendix B - Local](appendix-02.md)
1039"#;
1040
1041 let mut parser = SummaryParser::new(src);
1042
1043 // ---- Title ----
1044 let title = parser.parse_title();
1045 assert_eq!(title, Some(String::from("Title - Local")));
1046
1047 // ---- Prefix Chapters ----
1048
1049 let new_affix_item = |name, location| {
1050 SummaryItem::Link(Link {
1051 name: String::from(name),
1052 location: Some(PathBuf::from(location)),
1053 ..Default::default()
1054 })
1055 };
1056
1057 let should_be = vec![
1058 new_affix_item("Prefix 00-01 - Local", "ch00-01.md"),
1059 new_affix_item("Prefix 00-02 - Local", "ch00-02.md"),
1060 ];
1061
1062 let got = parser.parse_affix(true).unwrap();
1063 assert_eq!(got, should_be);
1064
1065 // ---- Numbered Chapters ----
1066
1067 let new_numbered_item = |name, location, numbers: &[u32], nested_items| {
1068 SummaryItem::Link(Link {
1069 name: String::from(name),
1070 location: Some(PathBuf::from(location)),
1071 number: Some(SectionNumber(numbers.to_vec())),
1072 nested_items,
1073 })
1074 };
1075
1076 let ch01_nested = vec![
1077 new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]),
1078 new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]),
1079 ];
1080
1081 let should_be = vec![
1082 new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested),
1083 new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]),
1084 ];
1085 let got = parser.parse_parts().unwrap();
1086 assert_eq!(got, should_be);
1087
1088 // ---- Suffix Chapters ----
1089
1090 let should_be = vec![
1091 new_affix_item("Appendix A - Local", "appendix-01.md"),
1092 new_affix_item("Appendix B - Local", "appendix-02.md"),
1093 ];
1094
1095 let got = parser.parse_affix(false).unwrap();
1096 assert_eq!(got, should_be);
1097 }
2c00a5a8 1098}