Skip to content

Commit e123879

Browse files
committed
Move Book to mdbook-core
This moves the Book definition to mdbook-core, along with related types it needs.
1 parent 7bcdfe6 commit e123879

File tree

8 files changed

+389
-381
lines changed

8 files changed

+389
-381
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/mdbook-core/src/book.rs

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
//! A tree structure representing a book.
2+
3+
use serde::{Deserialize, Serialize};
4+
use std::collections::VecDeque;
5+
use std::fmt::{self, Display, Formatter};
6+
use std::ops::{Deref, DerefMut};
7+
use std::path::PathBuf;
8+
9+
/// A tree structure representing a book.
10+
///
11+
/// For the moment a book is just a collection of [`BookItems`] which are
12+
/// accessible by either iterating (immutably) over the book with [`iter()`], or
13+
/// recursively applying a closure to each section to mutate the chapters, using
14+
/// [`for_each_mut()`].
15+
///
16+
/// [`iter()`]: #method.iter
17+
/// [`for_each_mut()`]: #method.for_each_mut
18+
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
19+
pub struct Book {
20+
/// The sections in this book.
21+
pub sections: Vec<BookItem>,
22+
__non_exhaustive: (),
23+
}
24+
25+
impl Book {
26+
/// Create an empty book.
27+
pub fn new() -> Self {
28+
Default::default()
29+
}
30+
31+
/// Creates a new book with the given items.
32+
pub fn new_with_items(items: Vec<BookItem>) -> Book {
33+
Book {
34+
sections: items,
35+
__non_exhaustive: (),
36+
}
37+
}
38+
39+
/// Get a depth-first iterator over the items in the book.
40+
pub fn iter(&self) -> BookItems<'_> {
41+
BookItems {
42+
items: self.sections.iter().collect(),
43+
}
44+
}
45+
46+
/// Recursively apply a closure to each item in the book, allowing you to
47+
/// mutate them.
48+
///
49+
/// # Note
50+
///
51+
/// Unlike the `iter()` method, this requires a closure instead of returning
52+
/// an iterator. This is because using iterators can possibly allow you
53+
/// to have iterator invalidation errors.
54+
pub fn for_each_mut<F>(&mut self, mut func: F)
55+
where
56+
F: FnMut(&mut BookItem),
57+
{
58+
for_each_mut(&mut func, &mut self.sections);
59+
}
60+
61+
/// Append a `BookItem` to the `Book`.
62+
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
63+
self.sections.push(item.into());
64+
self
65+
}
66+
}
67+
68+
fn for_each_mut<'a, F, I>(func: &mut F, items: I)
69+
where
70+
F: FnMut(&mut BookItem),
71+
I: IntoIterator<Item = &'a mut BookItem>,
72+
{
73+
for item in items {
74+
if let BookItem::Chapter(ch) = item {
75+
for_each_mut(func, &mut ch.sub_items);
76+
}
77+
78+
func(item);
79+
}
80+
}
81+
82+
/// Enum representing any type of item which can be added to a book.
83+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84+
pub enum BookItem {
85+
/// A nested chapter.
86+
Chapter(Chapter),
87+
/// A section separator.
88+
Separator,
89+
/// A part title.
90+
PartTitle(String),
91+
}
92+
93+
impl From<Chapter> for BookItem {
94+
fn from(other: Chapter) -> BookItem {
95+
BookItem::Chapter(other)
96+
}
97+
}
98+
99+
/// The representation of a "chapter", usually mapping to a single file on
100+
/// disk however it may contain multiple sub-chapters.
101+
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
102+
pub struct Chapter {
103+
/// The chapter's name.
104+
pub name: String,
105+
/// The chapter's contents.
106+
pub content: String,
107+
/// The chapter's section number, if it has one.
108+
pub number: Option<SectionNumber>,
109+
/// Nested items.
110+
pub sub_items: Vec<BookItem>,
111+
/// The chapter's location, relative to the `SUMMARY.md` file.
112+
///
113+
/// **Note**: After the index preprocessor runs, any README files will be
114+
/// modified to be `index.md`. If you need access to the actual filename
115+
/// on disk, use [`Chapter::source_path`] instead.
116+
///
117+
/// This is `None` for a draft chapter.
118+
pub path: Option<PathBuf>,
119+
/// The chapter's source file, relative to the `SUMMARY.md` file.
120+
///
121+
/// **Note**: Beware that README files will internally be treated as
122+
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
123+
/// exists if you need access to the true file path.
124+
///
125+
/// This is `None` for a draft chapter, or a synthetically generated
126+
/// chapter that has no file on disk.
127+
pub source_path: Option<PathBuf>,
128+
/// An ordered list of the names of each chapter above this one in the hierarchy.
129+
pub parent_names: Vec<String>,
130+
}
131+
132+
impl Chapter {
133+
/// Create a new chapter with the provided content.
134+
pub fn new<P: Into<PathBuf>>(
135+
name: &str,
136+
content: String,
137+
p: P,
138+
parent_names: Vec<String>,
139+
) -> Chapter {
140+
let path: PathBuf = p.into();
141+
Chapter {
142+
name: name.to_string(),
143+
content,
144+
path: Some(path.clone()),
145+
source_path: Some(path),
146+
parent_names,
147+
..Default::default()
148+
}
149+
}
150+
151+
/// Create a new draft chapter that is not attached to a source markdown file (and thus
152+
/// has no content).
153+
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
154+
Chapter {
155+
name: name.to_string(),
156+
content: String::new(),
157+
path: None,
158+
source_path: None,
159+
parent_names,
160+
..Default::default()
161+
}
162+
}
163+
164+
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
165+
pub fn is_draft_chapter(&self) -> bool {
166+
self.path.is_none()
167+
}
168+
}
169+
170+
impl Display for Chapter {
171+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
172+
if let Some(ref section_number) = self.number {
173+
write!(f, "{section_number} ")?;
174+
}
175+
176+
write!(f, "{}", self.name)
177+
}
178+
}
179+
180+
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
181+
/// a pretty `Display` impl.
182+
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
183+
pub struct SectionNumber(pub Vec<u32>);
184+
185+
impl Display for SectionNumber {
186+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
187+
if self.0.is_empty() {
188+
write!(f, "0")
189+
} else {
190+
for item in &self.0 {
191+
write!(f, "{item}.")?;
192+
}
193+
Ok(())
194+
}
195+
}
196+
}
197+
198+
impl Deref for SectionNumber {
199+
type Target = Vec<u32>;
200+
fn deref(&self) -> &Self::Target {
201+
&self.0
202+
}
203+
}
204+
205+
impl DerefMut for SectionNumber {
206+
fn deref_mut(&mut self) -> &mut Self::Target {
207+
&mut self.0
208+
}
209+
}
210+
211+
impl FromIterator<u32> for SectionNumber {
212+
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
213+
SectionNumber(it.into_iter().collect())
214+
}
215+
}
216+
217+
/// A depth-first iterator over the items in a book.
218+
///
219+
/// # Note
220+
///
221+
/// This struct shouldn't be created directly, instead prefer the
222+
/// [`Book::iter()`] method.
223+
pub struct BookItems<'a> {
224+
items: VecDeque<&'a BookItem>,
225+
}
226+
227+
impl<'a> Iterator for BookItems<'a> {
228+
type Item = &'a BookItem;
229+
230+
fn next(&mut self) -> Option<Self::Item> {
231+
let item = self.items.pop_front();
232+
233+
if let Some(BookItem::Chapter(ch)) = item {
234+
// if we wanted a breadth-first iterator we'd `extend()` here
235+
for sub_item in ch.sub_items.iter().rev() {
236+
self.items.push_front(sub_item);
237+
}
238+
}
239+
240+
item
241+
}
242+
}

crates/mdbook-core/src/book/tests.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use super::*;
2+
3+
#[test]
4+
fn section_number_has_correct_dotted_representation() {
5+
let inputs = vec![
6+
(vec![0], "0."),
7+
(vec![1, 3], "1.3."),
8+
(vec![1, 2, 3], "1.2.3."),
9+
];
10+
11+
for (input, should_be) in inputs {
12+
let section_number = SectionNumber(input).to_string();
13+
assert_eq!(section_number, should_be);
14+
}
15+
}
16+
17+
#[test]
18+
fn book_iter_iterates_over_sequential_items() {
19+
let sections = vec![
20+
BookItem::Chapter(Chapter {
21+
name: String::from("Chapter 1"),
22+
content: String::from("# Chapter 1"),
23+
..Default::default()
24+
}),
25+
BookItem::Separator,
26+
];
27+
let book = Book::new_with_sections(sections);
28+
29+
let should_be: Vec<_> = book.sections.iter().collect();
30+
31+
let got: Vec<_> = book.iter().collect();
32+
33+
assert_eq!(got, should_be);
34+
}
35+
36+
#[test]
37+
fn for_each_mut_visits_all_items() {
38+
let sections = vec![
39+
BookItem::Chapter(Chapter {
40+
name: String::from("Chapter 1"),
41+
content: String::from("# Chapter 1"),
42+
number: None,
43+
path: Some(PathBuf::from("Chapter_1/index.md")),
44+
source_path: Some(PathBuf::from("Chapter_1/index.md")),
45+
parent_names: Vec::new(),
46+
sub_items: vec![
47+
BookItem::Chapter(Chapter::new(
48+
"Hello World",
49+
String::new(),
50+
"Chapter_1/hello.md",
51+
Vec::new(),
52+
)),
53+
BookItem::Separator,
54+
BookItem::Chapter(Chapter::new(
55+
"Goodbye World",
56+
String::new(),
57+
"Chapter_1/goodbye.md",
58+
Vec::new(),
59+
)),
60+
],
61+
}),
62+
BookItem::Separator,
63+
];
64+
let mut book = Book::new_with_sections(sections);
65+
66+
let num_items = book.iter().count();
67+
let mut visited = 0;
68+
69+
book.for_each_mut(|_| visited += 1);
70+
71+
assert_eq!(visited, num_items);
72+
}
73+
74+
#[test]
75+
fn iterate_over_nested_book_items() {
76+
let sections = vec![
77+
BookItem::Chapter(Chapter {
78+
name: String::from("Chapter 1"),
79+
content: String::from("# Chapter 1"),
80+
number: None,
81+
path: Some(PathBuf::from("Chapter_1/index.md")),
82+
source_path: Some(PathBuf::from("Chapter_1/index.md")),
83+
parent_names: Vec::new(),
84+
sub_items: vec![
85+
BookItem::Chapter(Chapter::new(
86+
"Hello World",
87+
String::new(),
88+
"Chapter_1/hello.md",
89+
Vec::new(),
90+
)),
91+
BookItem::Separator,
92+
BookItem::Chapter(Chapter::new(
93+
"Goodbye World",
94+
String::new(),
95+
"Chapter_1/goodbye.md",
96+
Vec::new(),
97+
)),
98+
],
99+
}),
100+
BookItem::Separator,
101+
];
102+
let book = Book::new_with_sections(sections);
103+
104+
let got: Vec<_> = book.iter().collect();
105+
106+
assert_eq!(got.len(), 5);
107+
108+
// checking the chapter names are in the order should be sufficient here...
109+
let chapter_names: Vec<String> = got
110+
.into_iter()
111+
.filter_map(|i| match *i {
112+
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
113+
_ => None,
114+
})
115+
.collect();
116+
let should_be: Vec<_> = vec![
117+
String::from("Chapter 1"),
118+
String::from("Hello World"),
119+
String::from("Goodbye World"),
120+
];
121+
122+
assert_eq!(chapter_names, should_be);
123+
}
124+

crates/mdbook-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/// compatibility checks.
77
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
88

9+
pub mod book;
910
pub mod config;
1011
pub mod utils;
1112

crates/mdbook-summary/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ rust-version.workspace = true
1010
[dependencies]
1111
anyhow.workspace = true
1212
log.workspace = true
13+
mdbook-core.workspace = true
1314
memchr.workspace = true
1415
pulldown-cmark.workspace = true
1516
serde.workspace = true

0 commit comments

Comments
 (0)