| // Copyright 2014 The Rust Project Developers. See the COPYRIGHT |
| // file at the top-level directory of this distribution and at |
| // http://rust-lang.org/COPYRIGHT. |
| // |
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
| // option. This file may not be copied, modified, or distributed |
| // except according to those terms. |
| |
| //! Basic data structures for representing a book. |
| |
| use std::io::prelude::*; |
| use std::io::BufReader; |
| use std::iter; |
| use std::path::{Path, PathBuf}; |
| |
| pub struct BookItem { |
| pub title: String, |
| pub path: PathBuf, |
| pub path_to_root: PathBuf, |
| pub children: Vec<BookItem>, |
| } |
| |
| pub struct Book { |
| pub chapters: Vec<BookItem>, |
| } |
| |
| /// A depth-first iterator over a book. |
| pub struct BookItems<'a> { |
| cur_items: &'a [BookItem], |
| cur_idx: usize, |
| stack: Vec<(&'a [BookItem], usize)>, |
| } |
| |
| impl<'a> Iterator for BookItems<'a> { |
| type Item = (String, &'a BookItem); |
| |
| fn next(&mut self) -> Option<(String, &'a BookItem)> { |
| loop { |
| if self.cur_idx >= self.cur_items.len() { |
| match self.stack.pop() { |
| None => return None, |
| Some((parent_items, parent_idx)) => { |
| self.cur_items = parent_items; |
| self.cur_idx = parent_idx + 1; |
| } |
| } |
| } else { |
| let cur = self.cur_items.get(self.cur_idx).unwrap(); |
| |
| let mut section = "".to_string(); |
| for &(_, idx) in &self.stack { |
| section.push_str(&(idx + 1).to_string()[..]); |
| section.push('.'); |
| } |
| section.push_str(&(self.cur_idx + 1).to_string()[..]); |
| section.push('.'); |
| |
| self.stack.push((self.cur_items, self.cur_idx)); |
| self.cur_items = &cur.children[..]; |
| self.cur_idx = 0; |
| return Some((section, cur)) |
| } |
| } |
| } |
| } |
| |
| impl Book { |
| pub fn iter(&self) -> BookItems { |
| BookItems { |
| cur_items: &self.chapters[..], |
| cur_idx: 0, |
| stack: Vec::new(), |
| } |
| } |
| } |
| |
| /// Construct a book by parsing a summary (markdown table of contents). |
| pub fn parse_summary(input: &mut Read, src: &Path) -> Result<Book, Vec<String>> { |
| fn collapse(stack: &mut Vec<BookItem>, |
| top_items: &mut Vec<BookItem>, |
| to_level: usize) { |
| loop { |
| if stack.len() < to_level { return } |
| if stack.len() == 1 { |
| top_items.push(stack.pop().unwrap()); |
| return; |
| } |
| |
| let tip = stack.pop().unwrap(); |
| let last = stack.len() - 1; |
| stack[last].children.push(tip); |
| } |
| } |
| |
| let mut top_items = vec!(); |
| let mut stack = vec!(); |
| let mut errors = vec!(); |
| |
| // always include the introduction |
| top_items.push(BookItem { |
| title: "Introduction".to_string(), |
| path: PathBuf::from("README.md"), |
| path_to_root: PathBuf::from(""), |
| children: vec!(), |
| }); |
| |
| for line_result in BufReader::new(input).lines() { |
| let line = match line_result { |
| Ok(line) => line, |
| Err(err) => { |
| errors.push(err.to_string()); |
| return Err(errors); |
| } |
| }; |
| |
| let star_idx = match line.find("*") { Some(i) => i, None => continue }; |
| |
| let start_bracket = star_idx + line[star_idx..].find("[").unwrap(); |
| let end_bracket = start_bracket + line[start_bracket..].find("](").unwrap(); |
| let start_paren = end_bracket + 1; |
| let end_paren = start_paren + line[start_paren..].find(")").unwrap(); |
| |
| let given_path = &line[start_paren + 1 .. end_paren]; |
| let title = line[start_bracket + 1..end_bracket].to_string(); |
| let indent = &line[..star_idx]; |
| |
| let path_from_root = match src.join(given_path).strip_prefix(src) { |
| Ok(p) => p.to_path_buf(), |
| Err(..) => { |
| errors.push(format!("paths in SUMMARY.md must be relative, \ |
| but path '{}' for section '{}' is not.", |
| given_path, title)); |
| PathBuf::new() |
| } |
| }; |
| let path_to_root = PathBuf::from(&iter::repeat("../") |
| .take(path_from_root.components().count() - 1) |
| .collect::<String>()); |
| let item = BookItem { |
| title: title, |
| path: path_from_root, |
| path_to_root: path_to_root, |
| children: vec!(), |
| }; |
| let level = indent.chars().map(|c| -> usize { |
| match c { |
| ' ' => 1, |
| '\t' => 4, |
| _ => unreachable!() |
| } |
| }).sum::<usize>() / 4 + 1; |
| |
| if level > stack.len() + 1 { |
| errors.push(format!("section '{}' is indented too deeply; \ |
| found {}, expected {} or less", |
| item.title, level, stack.len() + 1)); |
| } else if level <= stack.len() { |
| collapse(&mut stack, &mut top_items, level); |
| } |
| stack.push(item) |
| } |
| |
| if errors.is_empty() { |
| collapse(&mut stack, &mut top_items, 1); |
| Ok(Book { chapters: top_items }) |
| } else { |
| Err(errors) |
| } |
| } |