1 | use crate::errors::*; |
2 | use log::{debug, trace, warn}; |
3 | use memchr::{self, Memchr}; |
4 | use pulldown_cmark::{self, Event, HeadingLevel, Tag}; |
5 | use serde::{Deserialize, Serialize}; |
6 | use std::fmt::{self, Display, Formatter}; |
7 | use std::iter::FromIterator; |
8 | use std::ops::{Deref, DerefMut}; |
9 | use std::path::{Path, PathBuf}; |
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 | /// |
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 | /// |
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 |
39 | /// # Title of Part |
40 | /// |
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. |
53 | pub fn parse_summary(summary: &str) -> Result<Summary> { |
54 | let parser: SummaryParser<'_> = SummaryParser::new(text: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)] |
60 | pub 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>, |
65 | /// The main numbered chapters of the book, broken into one or more possibly named parts. |
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)] |
76 | pub 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. |
81 | pub location: Option<PathBuf>, |
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 | |
88 | impl 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(), |
93 | location: Some(location.as_ref().to_path_buf()), |
94 | number: None, |
95 | nested_items: Vec::new(), |
96 | } |
97 | } |
98 | } |
99 | |
100 | impl Default for Link { |
101 | fn default() -> Self { |
102 | Link { |
103 | name: String::new(), |
104 | location: Some(PathBuf::new()), |
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)] |
113 | pub enum SummaryItem { |
114 | /// A link to a chapter. |
115 | Link(Link), |
116 | /// A separator (`---`). |
117 | Separator, |
118 | /// A part title. |
119 | PartTitle(String), |
120 | } |
121 | |
122 | impl SummaryItem { |
123 | fn maybe_link_mut(&mut self) -> Option<&mut Link> { |
124 | match *self { |
125 | SummaryItem::Link(ref mut l: &mut Link) => Some(l), |
126 | _ => None, |
127 | } |
128 | } |
129 | } |
130 | |
131 | impl 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 |
146 | /// suffix_chapters |
147 | /// title ::= "# " TEXT |
148 | /// | EPSILON |
149 | /// prefix_chapters ::= item* |
150 | /// suffix_chapters ::= item* |
151 | /// numbered_chapters ::= part+ |
152 | /// part ::= title dotted_item+ |
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[]]+". |
164 | struct SummaryParser<'a> { |
165 | src: &'a str, |
166 | stream: pulldown_cmark::OffsetIter<'a, 'a>, |
167 | offset: usize, |
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>>, |
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 |
178 | /// `$stream.take_while(|e| e != $delimiter).collect()` but it allows you to |
179 | /// use pattern matching and you won't get errors because `take_while()` |
180 | /// moves `$stream` out of self. |
181 | macro_rules! collect_events { |
182 | ($stream:expr,start $delimiter:pat) => { |
183 | collect_events!($stream, Event::Start($delimiter)) |
184 | }; |
185 | ($stream:expr,end $delimiter:pat) => { |
186 | collect_events!($stream, Event::End($delimiter)) |
187 | }; |
188 | ($stream:expr, $delimiter:pat) => {{ |
189 | let mut events = Vec::new(); |
190 | |
191 | loop { |
192 | let event = $stream.next().map(|(ev, _range)| ev); |
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; |
204 | } |
205 | } |
206 | } |
207 | |
208 | events |
209 | }}; |
210 | } |
211 | |
212 | impl<'a> SummaryParser<'a> { |
213 | fn new(text: &str) -> SummaryParser<'_> { |
214 | let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter(); |
215 | |
216 | SummaryParser { |
217 | src: text, |
218 | stream: pulldown_parser, |
219 | offset: 0, |
220 | back: None, |
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) { |
227 | let previous_text = self.src[..self.offset].as_bytes(); |
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); |
230 | let col = self.src[start_of_line..self.offset].chars().count(); |
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 | |
239 | let prefix_chapters = self |
240 | .parse_affix(true) |
241 | .with_context(|| "There was an error parsing the prefix chapters" )?; |
242 | let numbered_chapters = self |
243 | .parse_parts() |
244 | .with_context(|| "There was an error parsing the numbered chapters" )?; |
245 | let suffix_chapters = self |
246 | .parse_affix(false) |
247 | .with_context(|| "There was an error parsing the suffix chapters" )?; |
248 | |
249 | Ok(Summary { |
250 | title, |
251 | prefix_chapters, |
252 | numbered_chapters, |
253 | suffix_chapters, |
254 | }) |
255 | } |
256 | |
257 | /// Parse the affix chapters. |
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() { |
267 | Some(ev @ Event::Start(Tag::List(..))) |
268 | | Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { |
269 | if is_prefix { |
270 | // we've finished prefix chapters and are at the start |
271 | // of the numbered section. |
272 | self.back(ev); |
273 | break; |
274 | } else { |
275 | bail!(self.parse_error("Suffix chapters cannot be followed by a list" )); |
276 | } |
277 | } |
278 | Some(Event::Start(Tag::Link(_type, href, _title))) => { |
279 | let link = self.parse_link(href.to_string()); |
280 | items.push(SummaryItem::Link(link)); |
281 | } |
282 | Some(Event::Rule) => items.push(SummaryItem::Separator), |
283 | Some(_) => {} |
284 | None => break, |
285 | } |
286 | } |
287 | |
288 | Ok(items) |
289 | } |
290 | |
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 | |
307 | Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { |
308 | debug!("Found a h1 in the SUMMARY" ); |
309 | |
310 | let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); |
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 { |
338 | let href = href.replace("%20" , " " ); |
339 | let link_content = collect_events!(self.stream, end Tag::Link(..)); |
340 | let name = stringify_events(link_content); |
341 | |
342 | let path = if href.is_empty() { |
343 | None |
344 | } else { |
345 | Some(PathBuf::from(href)) |
346 | }; |
347 | |
348 | Link { |
349 | name, |
350 | location: path, |
351 | number: None, |
352 | nested_items: Vec::new(), |
353 | } |
354 | } |
355 | |
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>> { |
362 | let mut items = Vec::new(); |
363 | |
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; |
368 | |
369 | loop { |
370 | match self.next_event() { |
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. |
380 | Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { |
381 | // we're starting a new part |
382 | self.back(ev); |
383 | break; |
384 | } |
385 | Some(ev @ Event::Start(Tag::List(..))) => { |
386 | self.back(ev); |
387 | let mut bunch_of_items = self.parse_nested_numbered(root_number)?; |
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 | } |
396 | Some(Event::Start(other_tag)) => { |
397 | trace!("Skipping contents of {:?}" , other_tag); |
398 | |
399 | // Skip over the contents of this tag |
400 | while let Some(event) = self.next_event() { |
401 | if event == Event::End(other_tag.clone()) { |
402 | break; |
403 | } |
404 | } |
405 | } |
406 | Some(Event::Rule) => { |
407 | items.push(SummaryItem::Separator); |
408 | } |
409 | |
410 | // something else... ignore |
411 | Some(_) => {} |
412 | |
413 | // EOF, bail... |
414 | None => { |
415 | break; |
416 | } |
417 | } |
418 | |
419 | // From now on, we cannot accept any new paragraph opening tags. |
420 | first = false; |
421 | } |
422 | |
423 | Ok(items) |
424 | } |
425 | |
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 | |
433 | fn next_event(&mut self) -> Option<Event<'a>> { |
434 | let next = self.back.take().or_else(|| { |
435 | self.stream.next().map(|(ev, range)| { |
436 | self.offset = range.start; |
437 | ev |
438 | }) |
439 | }); |
440 | |
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(..))) => { |
457 | // Skip this tag after comment because it is not nested. |
458 | if items.is_empty() { |
459 | continue; |
460 | } |
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, |
489 | Some(Event::Start(Tag::Link(_type, href, _title))) => { |
490 | let mut link = self.parse_link(href.to_string()); |
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, |
498 | link.location |
499 | .as_ref() |
500 | .map(|p| p.to_str().unwrap_or("" )) |
501 | .unwrap_or("[draft]" ) |
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(); |
520 | anyhow::anyhow!( |
521 | "failed to parse SUMMARY.md line {}, column {}: {}" , |
522 | line, |
523 | col, |
524 | msg |
525 | ) |
526 | } |
527 | |
528 | /// Try to parse the title line. |
529 | fn parse_title(&mut self) -> Option<String> { |
530 | loop { |
531 | match self.next_event() { |
532 | Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { |
533 | debug!("Found a h1 in the SUMMARY" ); |
534 | |
535 | let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); |
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. |
541 | Some(ev) => { |
542 | self.back(ev); |
543 | return None; |
544 | } |
545 | _ => return None, |
546 | } |
547 | } |
548 | } |
549 | } |
550 | |
551 | fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) { |
552 | for section: &mut SummaryItem in sections { |
553 | if let SummaryItem::Link(ref mut link: &mut Link) = *section { |
554 | if let Some(ref mut number: &mut SectionNumber) = 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. |
565 | fn 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() |
572 | .ok_or_else(|| |
573 | anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links" ) |
574 | ) |
575 | } |
576 | |
577 | /// Removes the styling from a list of Markdown events and returns just the |
578 | /// plain text. |
579 | fn stringify_events(events: Vec<Event<'_>>) -> String { |
580 | eventsimpl Iterator |
581 | .into_iter() |
582 | .filter_map(|t: Event<'_>| match t { |
583 | Event::Text(text: CowStr<'_>) | Event::Code(text: CowStr<'_>) => Some(text.into_string()), |
584 | Event::SoftBreak => Some(String::from(" " )), |
585 | _ => None, |
586 | }) |
587 | .collect() |
588 | } |
589 | |
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)] |
593 | pub struct SectionNumber(pub Vec<u32>); |
594 | |
595 | impl Display for SectionNumber { |
596 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
597 | if self.0.is_empty() { |
598 | write!(f, "0" ) |
599 | } else { |
600 | for item: &u32 in &self.0 { |
601 | write!(f, " {}." , item)?; |
602 | } |
603 | Ok(()) |
604 | } |
605 | } |
606 | } |
607 | |
608 | impl Deref for SectionNumber { |
609 | type Target = Vec<u32>; |
610 | fn deref(&self) -> &Self::Target { |
611 | &self.0 |
612 | } |
613 | } |
614 | |
615 | impl DerefMut for SectionNumber { |
616 | fn deref_mut(&mut self) -> &mut Self::Target { |
617 | &mut self.0 |
618 | } |
619 | } |
620 | |
621 | impl 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)] |
628 | mod 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 | |
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 | |
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" ), |
698 | location: Some(PathBuf::from("./first.md" )), |
699 | ..Default::default() |
700 | }), |
701 | SummaryItem::Link(Link { |
702 | name: String::from("Second" ), |
703 | location: Some(PathBuf::from("./second.md" )), |
704 | ..Default::default() |
705 | }), |
706 | ]; |
707 | |
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 | |
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 | |
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" ), |
739 | location: Some(PathBuf::from("./first.md" )), |
740 | ..Default::default() |
741 | }; |
742 | |
743 | let mut parser = SummaryParser::new(src); |
744 | let _ = parser.stream.next(); // Discard opening paragraph |
745 | |
746 | let href = match parser.stream.next() { |
747 | Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(), |
748 | other => panic!("Unreachable, {:?}" , other), |
749 | }; |
750 | |
751 | let got = parser.parse_link(href); |
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" ), |
760 | location: Some(PathBuf::from("./first.md" )), |
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); |
767 | let got = parser |
768 | .parse_numbered(&mut 0, &mut SectionNumber::default()) |
769 | .unwrap(); |
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" ), |
781 | location: Some(PathBuf::from("./first.md" )), |
782 | number: Some(SectionNumber(vec![1])), |
783 | nested_items: vec![SummaryItem::Link(Link { |
784 | name: String::from("Nested" ), |
785 | location: Some(PathBuf::from("./nested.md" )), |
786 | number: Some(SectionNumber(vec![1, 1])), |
787 | nested_items: Vec::new(), |
788 | })], |
789 | }), |
790 | SummaryItem::Link(Link { |
791 | name: String::from("Second" ), |
792 | location: Some(PathBuf::from("./second.md" )), |
793 | number: Some(SectionNumber(vec![2])), |
794 | nested_items: Vec::new(), |
795 | }), |
796 | ]; |
797 | |
798 | let mut parser = SummaryParser::new(src); |
799 | let got = parser |
800 | .parse_numbered(&mut 0, &mut SectionNumber::default()) |
801 | .unwrap(); |
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" ), |
813 | location: Some(PathBuf::from("./first.md" )), |
814 | number: Some(SectionNumber(vec![1])), |
815 | nested_items: Vec::new(), |
816 | }), |
817 | SummaryItem::Link(Link { |
818 | name: String::from("Second" ), |
819 | location: Some(PathBuf::from("./second.md" )), |
820 | number: Some(SectionNumber(vec![2])), |
821 | nested_items: Vec::new(), |
822 | }), |
823 | ]; |
824 | |
825 | let mut parser = SummaryParser::new(src); |
826 | let got = parser |
827 | .parse_numbered(&mut 0, &mut SectionNumber::default()) |
828 | .unwrap(); |
829 | |
830 | assert_eq!(got, should_be); |
831 | } |
832 | |
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(); |
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" ), |
881 | location: Some(PathBuf::from("./first.md" )), |
882 | number: Some(SectionNumber(vec![1])), |
883 | nested_items: Vec::new(), |
884 | }), |
885 | SummaryItem::Link(Link { |
886 | name: String::from("Second" ), |
887 | location: Some(PathBuf::from("./second.md" )), |
888 | number: Some(SectionNumber(vec![2])), |
889 | nested_items: Vec::new(), |
890 | }), |
891 | ]; |
892 | |
893 | let mut parser = SummaryParser::new(src); |
894 | let got = parser |
895 | .parse_numbered(&mut 0, &mut SectionNumber::default()) |
896 | .unwrap(); |
897 | |
898 | assert_eq!(got, should_be); |
899 | } |
900 | |
901 | #[test ] |
902 | fn an_empty_link_location_is_a_draft_chapter() { |
903 | let src = "- [Empty]() \n" ; |
904 | let mut parser = SummaryParser::new(src); |
905 | |
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); |
916 | } |
917 | |
918 | /// Regression test for https://github.com/rust-lang/mdBook/issues/779 |
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" ), |
927 | location: Some(PathBuf::from("./first.md" )), |
928 | number: Some(SectionNumber(vec![1])), |
929 | nested_items: Vec::new(), |
930 | }), |
931 | SummaryItem::Separator, |
932 | SummaryItem::Link(Link { |
933 | name: String::from("Second" ), |
934 | location: Some(PathBuf::from("./second.md" )), |
935 | number: Some(SectionNumber(vec![2])), |
936 | nested_items: Vec::new(), |
937 | }), |
938 | SummaryItem::Separator, |
939 | SummaryItem::Link(Link { |
940 | name: String::from("Third" ), |
941 | location: Some(PathBuf::from("./third.md" )), |
942 | number: Some(SectionNumber(vec![3])), |
943 | nested_items: Vec::new(), |
944 | }), |
945 | ]; |
946 | |
947 | let mut parser = SummaryParser::new(src); |
948 | let got = parser |
949 | .parse_numbered(&mut 0, &mut SectionNumber::default()) |
950 | .unwrap(); |
951 | |
952 | assert_eq!(got, should_be); |
953 | } |
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 | } |
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 | } |
1098 | } |
1099 | |