use std::{
borrow::Borrow,
fs,
path::{Path, PathBuf},
result,
};
use log::info;
use crate::{
asset::Asset,
error::Result,
search,
toc::Toc,
utils::{self, slugify_path},
};
pub(crate) enum DoctreeItem {
Page(Page),
Bale(Bale),
}
#[derive(Debug)]
pub(crate) struct Page {
slug: String,
title: String,
tree: Toc,
}
impl search::SearchableDocument for Page {
fn title(&self) -> &str {
&self.title
}
fn slug(&self) -> &str {
&self.slug
}
fn search_index(&self) -> Option<&search::TermFrequenciesIndex> {
Some(&self.content().search_index())
}
}
impl Page {
pub fn open<P: AsRef<Path>>(path: P) -> result::Result<Self, std::io::Error> {
let markdown = fs::read_to_string(&path)?;
Ok(Self::from_parts(path, markdown))
}
fn from_parts<P: AsRef<Path>, M: Borrow<str>>(path: P, markdown: M) -> Self {
let slug = utils::slugify_path(&path);
let tree = Toc::new(markdown.borrow());
let title = tree.primary_heading().cloned().unwrap_or_else(|| {
path.as_ref()
.file_stem()
.unwrap()
.to_string_lossy()
.into_owned()
});
Page { slug, title, tree }
}
pub fn title(&self) -> &str {
&self.title
}
pub fn slug(&self) -> &str {
&self.slug
}
pub fn content(&self) -> &Toc {
&self.tree
}
}
#[derive(Debug)]
pub(crate) struct Bale {
frontispiece: Frontispiece,
pages: Vec<PathBuf>,
assets: Vec<PathBuf>,
nested: Vec<PathBuf>,
}
impl Bale {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut index = None;
let mut footer = None;
let mut pages = Vec::new();
let mut assets = Vec::new();
let mut nested = Vec::new();
for entry in fs::read_dir(&path)? {
let entry = entry?;
let path = entry.path().clone();
if path.is_file() {
match utils::normalised_path_ext(&path).as_deref() {
Some("md" | "markdown" | "mdown") => {
match utils::normalised_stem(&path).as_deref() {
Some("index" | "readme") => index = Some(path),
Some("footer") => footer = Some(fs::read_to_string(path)?),
_ => pages.push(path),
}
}
_ => assets.push(path),
}
} else {
nested.push(path);
}
}
let index = match index {
Some(path) => Some(Page::open(path)?),
None => None,
};
Ok(Bale {
frontispiece: Frontispiece::new(path, index, footer),
pages,
assets,
nested,
})
}
pub fn break_open(self) -> Result<(Frontispiece, Vec<Asset>, Vec<DoctreeItem>)> {
info!(
"Breaking open bale {} ({})",
self.frontispiece.title,
self.frontispiece.slug(),
);
let mut assets: Vec<_> = self.assets.into_iter().map(Asset::path).collect();
let mut items = Vec::with_capacity(self.pages.len() + self.nested.len());
for page in self.pages {
items.push((
utils::normalised_stem(&page),
DoctreeItem::Page(Page::open(page)?),
));
}
for nested in self.nested {
let bale = Bale::new(&nested)?;
if bale.frontispiece.index.is_none() && bale.pages.is_empty() {
info!(
"Inner item {:?} does not appear to be able. Adding as an asset",
&nested
);
assets.push(Asset::path(nested));
} else {
items.push((utils::normalised_stem(&nested), DoctreeItem::Bale(bale)));
}
}
items.sort_by_cached_key(|(k, _)| k.clone());
Ok((
self.frontispiece,
assets,
items.into_iter().map(|(_, i)| i).collect(),
))
}
pub(crate) fn frontispiece(&self) -> &Frontispiece {
&self.frontispiece
}
}
#[derive(Debug)]
pub(crate) struct Frontispiece {
title: String,
slug: String,
index: Option<Page>,
footer: Option<String>,
}
impl Frontispiece {
fn new<P: AsRef<Path>>(path: P, index: Option<Page>, footer: Option<String>) -> Frontispiece {
let title = match &index {
Some(page) => page.title.clone(),
None => utils::prettify_dir(&path).expect("Could not create a title"),
};
let footer = footer.map(|text| {
let mut output = String::new();
pulldown_cmark::html::push_html(&mut output, pulldown_cmark::Parser::new(&text));
output
});
Frontispiece {
title,
slug: slugify_path(path),
index,
footer,
}
}
pub fn slug(&self) -> &str {
&self.slug
}
pub fn title(&self) -> &str {
&self.title
}
pub fn index_page(&self) -> Option<&Page> {
self.index.as_ref()
}
pub fn footer(&self) -> Option<&str> {
self.footer.as_deref()
}
}
pub(crate) fn open<P: AsRef<Path>>(path: P) -> Result<Bale> {
Bale::new(path)
}
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
#[test]
fn page_has_search_terms() {
let path = PathBuf::from("foo/bar.md");
let page = Page::from_parts(&path, "Some sample text in some text");
let index = page.content().search_index().as_raw();
assert_ne!(0, index.len());
let some_fq = index.get("some").cloned().unwrap_or_default();
let sample_fq = index.get("sample").cloned().unwrap_or_default();
let text_fq = index.get("text").cloned().unwrap_or_default();
assert_eq!(some_fq, text_fq);
assert!(some_fq > sample_fq);
}
#[test]
fn index_of_example_markdown() {
let path = PathBuf::from("foo/bar.md");
let page = Page::from_parts(
&path,
r###"
# Down the Rabbit Hole
Either the well was very deep, or she fell very slowly, for she had
plenty of time as she went down to look about her, and to wonder what
was going to happen next. First, she tried to look down and make out
what she was coming to, but it was too dark to see anything; then she
looked at the sides of the well and noticed that they were filled with
cupboards and book-shelves: here and there she saw maps and pictures
hung upon pegs. She took down a jar from one of the shelves as she
passed; it was labelled "ORANGE MARMALADE," but to her disappointment it
was empty; she did not like to drop the jar for fear of killing
somebody underneath, so managed to put it into one of the cupboards as
she fell past it.
"Well!" thought Alice to herself. "After such a fall as this, I shall
think nothing of tumbling down stairs! How brave they'll all think me at
home! Why, I wouldn't say anything about it, even if I fell off the top
of the house!" (Which was very likely true.)
### Going Down?
Down, down, down. Would the fall _never_ come to an end? "I wonder how
many miles I've fallen by this time?" she said aloud. "I must be getting
somewhere near the centre of the earth. Let me see: that would be four
thousand miles down. I think--" (for, you see, Alice had learnt several
things of this sort in her lessons in the schoolroom, and though this
was not a _very_ good opportunity for showing off her knowledge, as
there was no one to listen to her, still it was good practice to say it
over) "--yes, that's about the right distance--but then I wonder what
Latitude or Longitude I've got to?" (Alice had no idea what Latitude
was, or Longitude either, but thought they were nice grand words to
say.)
"###,
);
assert_eq!("Down the Rabbit Hole", page.title);
let index = page.content().search_index().as_raw();
assert_ne!(0, index.len());
let rabbit_fq = index.get("rabbit").cloned().unwrap_or_default();
assert!(rabbit_fq > 0.0);
let well_fq = index.get("well").cloned().unwrap_or_default();
assert!(well_fq > rabbit_fq);
assert_eq!(
index.get("distance").cloned().unwrap_or_default(),
rabbit_fq
);
assert!(index.get("down").cloned().unwrap_or_default() > well_fq);
assert_ne!(None, index.get("orange"));
assert_eq!(None, index.get("MARMALADE"));
assert_eq!(None, index.get(""));
assert_eq!(None, index.get("!"));
assert_eq!(None, index.get("-"));
assert_eq!(None, index.get(" "));
assert_eq!(None, index.get("\t"));
assert_eq!(None, index.get("("));
}
}