Skip to content

Search with Elasticlunr #472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@

* text=auto eol=lf
*.rs rust
*.woff -text
*.ttf -text
*.otf -text
*.png -text
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ toml = "0.4"
open = "1.1"
regex = "0.2.1"
tempdir = "0.3.4"
elasticlunr-rs = "0.2.1"

# Watch feature
notify = { version = "4.0", optional = true }
Expand Down
13 changes: 12 additions & 1 deletion book-example/book.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David"

[output.html]
mathjax-support = true
mathjax-support = true

[output.html.search]
enable = true
limit-results = 20
use-boolean-and = true
boost-title = 2
boost-hierarchy = 2
boost-paragraph = 1
expand = true
split-until-heading = 2
39 changes: 36 additions & 3 deletions book-example/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ description = "The example book covers examples."
[output.html]
destination = "my-example-book"
additional-css = ["custom.css"]

[output.html.search]
enable = true
limit-results = 15
```

## Supported configuration options
Expand Down Expand Up @@ -51,8 +55,6 @@ renderer need to be specified under the TOML table `[output.html]`.

The following configuration options are available:

pub playpen: Playpen,

- **theme:** mdBook comes with a default theme and all the resource files
needed for it. But if this option is set, mdBook will selectively overwrite
the theme files with the ones found in the specified folder.
Expand All @@ -68,13 +70,33 @@ The following configuration options are available:
removing the current behaviour, you can specify a set of javascript files
that will be loaded alongside the default one.
- **playpen:** A subtable for configuring various playpen settings.
- **search:** A subtable for configuring the browser based search functionality.

**book.toml**
Available configuration options for the `[output.html.search]` table:

- **enable:** Enable or disable the search function. Disabling can improve compilation time by a factor of two. Defaults to `true`.
- **limit-results:** The maximum number of search results. Defaults to `30`.
- **teaser-word-count:** The number of words used for a search result teaser. Defaults to `30`.
- **use-boolean-and:** Define the logical link between multiple search words. If true, all search words must appear in each result. Defaults to `true`.
- **boost-title:** Boost factor for the search result score if a search word appears in the header. Defaults to `2`.
- **boost-hierarchy:** Boost factor for the search result score if a search word appears in the hierarchy. The hierarchy contains all titles of the parent documents and all parent headings. Defaults to `1`.
- **boost-paragraph:** Boost factor for the search result score if a search word appears in the text. Defaults to `1`.
- **expand:** True if the searchword `micro` should match `microwave`. Defaults to `true`.
- **split-until-heading:** Documents are split into smaller parts, seperated by headings. This defines, until which level of heading documents should be split. Defaults to `3`. (`### This is a level 3 heading`)

Available configuration options for the `[output.html.playpen]` table:

- **editor:** Source folder for the editors javascript files. Defaults to `""`.
- **editable:** Allow editing the source code. Defaults to `false`.

This shows all available options in the **book.toml**:
```toml
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
build-dir = "build"

[output.html]
theme = "my-theme"
Expand All @@ -83,6 +105,17 @@ google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]

[output.html.search]
enable = true
limit-results = 30
teaser-word-count = 30
use-boolean-and = true
boost-title = 2
boost-hierarchy = 1
boost-paragraph = 1
expand = true
split-until-heading = 3

[output.html.playpen]
editor = "./path/to/editor"
editable = false
Expand Down
1 change: 1 addition & 0 deletions book-example/src/misc/contributors.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
- [funnkill](https://github.com/funkill)
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
- [Phaiax](https://github.com/Phaiax)
115 changes: 85 additions & 30 deletions src/book/bookitem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,44 @@ use serde::{Serialize, Serializer};
use serde::ser::SerializeStruct;
use std::path::PathBuf;


/// A BookItem corresponds to one entry of the table of contents file SUMMARY.md.
/// A line in that file can either be a numbered chapter with a section number like 2.1.3 or a
/// suffix or postfix chapter without such a section number.
/// The `String` field in the `Chapter` variant contains the section number as `2.1.3`.
/// The `Chapter` type contains the child elements (which can only be other `BookItem::Chapters`).
/// `BookItem::Affix` and `BookItem::Spacer` are only allowed within the root level.
#[derive(Debug, Clone)]
pub enum BookItem {
Chapter(String, Chapter), // String = section
Affix(Chapter),
Spacer,
}

/// A chapter is a `.md` file that is referenced by some line in the `SUMMARY.md` table of
/// contents. It also has references to its sub chapters via `sub_items`. These items can
/// only be of the variant `BookItem::Chapter`.
#[derive(Debug, Clone)]
pub struct Chapter {
pub name: String,
pub path: PathBuf,
pub sub_items: Vec<BookItem>,
}

/// A flattening, depth-first iterator over Bookitems and it's children.
/// It can be obtained by calling `MDBook::iter()`.
#[derive(Debug, Clone)]
pub struct BookItems<'a> {
pub items: &'a [BookItem],
pub current_index: usize,
pub stack: Vec<(&'a [BookItem], usize)>,
/// The remaining items in the iterator in the current, deepest level of the iterator
items: &'a [BookItem],
/// The higher levels of the hierarchy. The parents of the current level are still
/// in the list and accessible as `[stack[0][0], stack[1][0], stack[2][0], ...]`.
stack: Vec<&'a [BookItem]>,
}

/// Iterator for the parent `BookItem`s of a `BookItem`.
pub struct BookItemParents<'a> {
stack: &'a [ &'a [BookItem] ]
}

impl Chapter {
pub fn new(name: String, path: PathBuf) -> Self {
Expand All @@ -48,39 +64,78 @@ impl Serialize for Chapter {
}
}



// Shamelessly copied from Rustbook
// (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs)
impl<'a> Iterator for BookItems<'a> {
type Item = &'a BookItem;

fn next(&mut self) -> Option<&'a BookItem> {
loop {
if self.current_index >= self.items.len() {
match self.stack.pop() {
None => return None,
Some((parent_items, parent_idx)) => {
self.items = parent_items;
self.current_index = parent_idx + 1;
}
}
} else {
let cur = &self.items[self.current_index];

match *cur {
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
self.stack.push((self.items, self.current_index));
if let Some((first, rest)) = self.items.split_first() {
// Return the first element in `items` and optionally dive into afterwards.
match first {
&BookItem::Spacer => {
self.items = rest;
},
&BookItem::Chapter(_, ref ch) |
&BookItem::Affix(ref ch) => {
if ch.sub_items.is_empty() {
self.items = rest;
} else {
// Don't remove `first` for now. (Because of Parent Iterator)
self.stack.push(self.items);
self.items = &ch.sub_items[..];
self.current_index = 0;
}
BookItem::Spacer => {
self.current_index += 1;
}
}

return Some(cur);
},
};
Some(first)
} else {
// Current level is drained => pop from `stack` or return `None`
if let Some(stacked_items) = self.stack.pop() {
// The first item of the popped slice is the bookitem we previously dived into.
self.items = &stacked_items[1..];
self.next()
} else {
None
}
}
}
}

impl<'a> BookItems<'a> {
pub fn new(items : &'a[BookItem]) -> BookItems<'a> {
BookItems {
items : items,
stack : vec![],
}
}

/// Returns an iterator to iterate the parents of the last yielded `BookItem`.
/// Starts with the root item.
pub fn current_parents(&'a self) -> BookItemParents<'a> {
BookItemParents { stack : &self.stack }
}

/// Collects the names of the parent `BookItem`s of the last yielded `Bookitem` into a list.
pub fn collect_current_parents_names(&self) -> Vec<String> {
self.current_parents().filter_map(|i| match i {
&BookItem::Chapter(_, ref ch) | &BookItem::Affix(ref ch) => Some(ch.name.clone()),
_ => None,
}).collect()
}

/// Get the level of the last yielded `BookItem`. Root level = 0
pub fn current_depth(&'a self) -> usize {
self.stack.len()
}
}

impl<'a> Iterator for BookItemParents<'a> {
type Item = &'a BookItem;

fn next(&mut self) -> Option<&'a BookItem> {
if let Some((first, rest)) = self.stack.split_first() {
self.stack = rest;
Some (&first[0])
} else {
None
}
}
}
6 changes: 1 addition & 5 deletions src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,7 @@ impl MDBook {
/// ```

pub fn iter(&self) -> BookItems {
BookItems {
items: &self.content[..],
current_index: 0,
stack: Vec::new(),
}
BookItems::new(&self.content[..])
}

/// `init()` creates some boilerplate files and directories
Expand Down
52 changes: 50 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ impl Config {
get_and_insert!(table, "source" => cfg.book.src);
get_and_insert!(table, "description" => cfg.book.description);

// This complicated chain of and_then's is so we can move
// "output.html.destination" to "book.build_dir" and parse it into a
// This complicated chain of and_then's is so we can move
// "output.html.destination" to "book.build_dir" and parse it into a
// PathBuf.
let destination: Option<PathBuf> = table.get_mut("output")
.and_then(|output| output.as_table_mut())
Expand Down Expand Up @@ -227,6 +227,7 @@ pub struct HtmlConfig {
pub additional_css: Vec<PathBuf>,
pub additional_js: Vec<PathBuf>,
pub playpen: Playpen,
pub search: Search,
}

/// Configuration for tweaking how the the HTML renderer handles the playpen.
Expand All @@ -236,6 +237,53 @@ pub struct Playpen {
pub editable: bool,
}

/// Configuration of the search functionality of the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Search {
/// Enable in browser searching. Default: true.
pub enable: bool,
/// Maximum number of visible results. Default: 30.
pub limit_results: u32,
/// The number of words used for a search result teaser. Default: 30,
pub teaser_word_count: u32,
/// Define the logical link between multiple search words.
/// If true, all search words must appear in each result. Default: true.
pub use_boolean_and: bool,
/// Boost factor for the search result score if a search word appears in the header.
/// Default: 2.
pub boost_title: u8,
/// Boost factor for the search result score if a search word appears in the hierarchy.
/// The hierarchy contains all titles of the parent documents and all parent headings.
/// Default: 1.
pub boost_hierarchy: u8,
/// Boost factor for the search result score if a search word appears in the text.
/// Default: 1.
pub boost_paragraph: u8,
/// True if the searchword `micro` should match `microwave`. Default: true.
pub expand : bool,
/// Documents are split into smaller parts, seperated by headings. This defines, until which
/// level of heading documents should be split. Default: 3. (`### This is a level 3 heading`)
pub split_until_heading: u8,
}

impl Default for Search {
fn default() -> Search {
// Please update the documentation of `Search` when changing values!
Search {
enable: true,
limit_results: 30,
teaser_word_count: 30,
use_boolean_and: false,
boost_title: 2,
boost_hierarchy: 1,
boost_paragraph: 1,
expand: true,
split_until_heading: 3,
}
}
}


#[cfg(test)]
mod tests {
Expand Down
7 changes: 4 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
//!
//! fn main() {
//! let mut md = MDBook::new("my-book");
//!
//!
//! // tweak the book configuration a bit
//! md.config.book.src = PathBuf::from("source");
//! md.config.book.build_dir = PathBuf::from("book");
//!
//!
//! // Render the book
//! md.build().unwrap();
//! md.build().unwrap();
//! }
//! ```
//!
Expand Down Expand Up @@ -88,6 +88,7 @@ extern crate serde_derive;
extern crate serde_json;
extern crate tempdir;
extern crate toml;
extern crate elasticlunr;

mod parse;
mod preprocess;
Expand Down
Loading