Skip to content

Commit 4dcba19

Browse files
committed
Search: Configuration via book.toml
1 parent 850df09 commit 4dcba19

File tree

6 files changed

+163
-32
lines changed

6 files changed

+163
-32
lines changed

book-example/book.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,14 @@ description = "Create book from markdown files. Like Gitbook but implemented in
33
author = "Mathieu David"
44

55
[output.html]
6-
mathjax-support = true
6+
mathjax-support = true
7+
8+
[output.html.search]
9+
enable = true
10+
limit-results = 20
11+
use-boolean-and = true
12+
boost-title = 2
13+
boost-hierarchy = 2
14+
boost-paragraph = 1
15+
expand = true
16+
split-until-heading = 2

src/config.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ impl Config {
9090
get_and_insert!(table, "source" => cfg.book.src);
9191
get_and_insert!(table, "description" => cfg.book.description);
9292

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

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

240+
/// Configuration of the search functionality of the HTML renderer.
241+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242+
#[serde(default, rename_all = "kebab-case")]
243+
pub struct Search {
244+
/// Enable in browser searching. Default: true.
245+
pub enable: bool,
246+
/// Maximum number of visible results. Default: 30.
247+
pub limit_results: u32,
248+
/// The number of words used for a search result teaser. Default: 30,
249+
pub teaser_word_count: u32,
250+
/// Define the logical link between multiple search words.
251+
/// If true, all search words must appear in each result. Default: true.
252+
pub use_boolean_and: bool,
253+
/// Boost factor for the search result score if a search word appears in the header.
254+
/// Default: 2.
255+
pub boost_title: u8,
256+
/// Boost factor for the search result score if a search word appears in the hierarchy.
257+
/// The hierarchy contains all titles of the parent documents and all parent headings.
258+
/// Default: 1.
259+
pub boost_hierarchy: u8,
260+
/// Boost factor for the search result score if a search word appears in the text.
261+
/// Default: 1.
262+
pub boost_paragraph: u8,
263+
/// True if the searchword `micro` should match `microwave`. Default: true.
264+
pub expand : bool,
265+
/// Documents are split into smaller parts, seperated by headings. This defines, until which
266+
/// level of heading documents should be split. Default: 3. (`### This is a level 3 heading`)
267+
pub split_until_heading: u8,
268+
}
269+
270+
impl Default for Search {
271+
fn default() -> Search {
272+
// Please update the documentation of `Search` when changing values!
273+
Search {
274+
enable: true,
275+
limit_results: 30,
276+
teaser_word_count: 30,
277+
use_boolean_and: false,
278+
boost_title: 2,
279+
boost_hierarchy: 1,
280+
boost_paragraph: 1,
281+
expand: true,
282+
split_until_heading: 3,
283+
}
284+
}
285+
}
286+
239287

240288
#[cfg(test)]
241289
mod tests {

src/renderer/html_handlebars/hbs_renderer.rs

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use preprocess;
33
use renderer::Renderer;
44
use book::MDBook;
55
use book::bookitem::{BookItem, Chapter};
6-
use config::{Config, Playpen, HtmlConfig};
6+
use config::{Config, Playpen, HtmlConfig, Search};
77
use {utils, theme};
88
use theme::{Theme, playpen_editor};
99
use errors::*;
@@ -60,7 +60,9 @@ impl HtmlHandlebars {
6060
.eq_ignore_ascii_case(&ch.name) {
6161
parents_names.push(ch.name.clone());
6262
}
63-
utils::render_markdown_into_searchindex(search_documents,
63+
utils::render_markdown_into_searchindex(
64+
&ctx.html_config.search,
65+
search_documents,
6466
&content,
6567
filepath,
6668
parents_names,
@@ -311,7 +313,7 @@ impl Renderer for HtmlHandlebars {
311313
}
312314

313315
// Search index
314-
make_searchindex(book, search_documents)?;
316+
make_searchindex(book, search_documents, &html_config.search)?;
315317

316318
// Print version
317319
self.configure_print_version(&mut data, &print_content);
@@ -650,28 +652,92 @@ pub fn normalize_id(content: &str) -> String {
650652
}
651653

652654
/// Uses elasticlunr to create a search index and exports that into `searchindex.json`.
653-
fn make_searchindex(book: &MDBook, search_documents : Vec<utils::SearchDocument>) -> Result<()> {
654-
let mut index = elasticlunr::index::Index::new("id",
655-
&["title".into(), "body".into(), "breadcrumbs".into()]);
655+
fn make_searchindex(book: &MDBook,
656+
search_documents : Vec<utils::SearchDocument>,
657+
searchconfig : &Search) -> Result<()> {
656658

657-
for sd in search_documents {
658-
let anchor = if let Some(s) = sd.anchor.1 {
659-
format!("{}#{}", sd.anchor.0, &s)
660-
} else {
661-
sd.anchor.0
662-
};
663659

664-
let mut map = HashMap::new();
665-
map.insert("id".into(), anchor.clone());
666-
map.insert("title".into(), sd.title);
667-
map.insert("body".into(), sd.body);
668-
map.insert("breadcrumbs".into(), sd.hierarchy.join(" » "));
669-
index.add_doc(&anchor, map);
660+
#[derive(Serialize)]
661+
struct SearchOptionsField {
662+
boost: u8,
663+
}
664+
665+
#[derive(Serialize)]
666+
struct SearchOptionsFields {
667+
title: SearchOptionsField,
668+
body: SearchOptionsField,
669+
breadcrumbs: SearchOptionsField,
670+
}
671+
672+
/// The searchoptions for elasticlunr.js
673+
#[derive(Serialize)]
674+
struct SearchOptions {
675+
bool: String,
676+
expand: bool,
677+
limit_results: u32,
678+
teaser_word_count: u32,
679+
fields: SearchOptionsFields,
670680
}
671681

682+
#[derive(Serialize)]
683+
struct SearchindexJson {
684+
enable: bool,
685+
#[serde(skip_serializing_if = "Option::is_none")]
686+
searchoptions: Option<SearchOptions>,
687+
#[serde(skip_serializing_if = "Option::is_none")]
688+
index: Option<elasticlunr::index::Index>,
689+
690+
}
691+
692+
let searchoptions = SearchOptions {
693+
bool : if searchconfig.use_boolean_and { "AND".into() } else { "OR".into() },
694+
expand : searchconfig.expand,
695+
limit_results : searchconfig.limit_results,
696+
teaser_word_count : searchconfig.teaser_word_count,
697+
fields : SearchOptionsFields {
698+
title : SearchOptionsField { boost : searchconfig.boost_title },
699+
body : SearchOptionsField { boost : searchconfig.boost_paragraph },
700+
breadcrumbs : SearchOptionsField { boost : searchconfig.boost_hierarchy },
701+
}
702+
};
703+
704+
let json_contents = if searchconfig.enable {
705+
706+
let mut index = elasticlunr::index::Index::new("id",
707+
&["title".into(), "body".into(), "breadcrumbs".into()]);
708+
709+
for sd in search_documents {
710+
let anchor = if let Some(s) = sd.anchor.1 {
711+
format!("{}#{}", sd.anchor.0, &s)
712+
} else {
713+
sd.anchor.0
714+
};
715+
716+
let mut map = HashMap::new();
717+
map.insert("id".into(), anchor.clone());
718+
map.insert("title".into(), sd.title);
719+
map.insert("body".into(), sd.body);
720+
map.insert("breadcrumbs".into(), sd.hierarchy.join(" » "));
721+
index.add_doc(&anchor, map);
722+
}
723+
724+
SearchindexJson {
725+
enable : searchconfig.enable,
726+
searchoptions : Some(searchoptions),
727+
index : Some(index),
728+
}
729+
} else {
730+
SearchindexJson {
731+
enable : false,
732+
searchoptions : None,
733+
index : None,
734+
}
735+
};
736+
737+
672738
book.write_file(
673739
Path::new("searchindex").with_extension("json"),
674-
&serde_json::to_string(&index).unwrap().as_bytes(),
740+
&serde_json::to_string(&json_contents).unwrap().as_bytes(),
675741
)?;
676742
info!("[*] Creating \"searchindex.json\" ✓");
677743

src/theme/book.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ $( document ).ready(function() {
1717
searchoptions : {
1818
bool: "AND",
1919
expand: true,
20+
teaser_word_count : 30,
21+
limit_results : 30,
2022
fields: {
2123
title: {boost: 1},
2224
body: {boost: 1},
@@ -25,8 +27,6 @@ $( document ).ready(function() {
2527
},
2628
mark_exclude : [], // ['.hljs']
2729
current_searchterm : "",
28-
teaser_words : 30,
29-
resultcount_limit : 30,
3030
SEARCH_PARAM : 'search',
3131
MARK_PARAM : 'highlight',
3232

@@ -220,7 +220,7 @@ $( document ).ready(function() {
220220
}
221221

222222
var window_weight = [];
223-
var window_size = Math.min(weighted.length, this.teaser_words);
223+
var window_size = Math.min(weighted.length, this.searchoptions.teaser_word_count);
224224

225225
var cur_sum = 0;
226226
for (var wordindex = 0; wordindex < window_size; wordindex++) {
@@ -280,8 +280,7 @@ $( document ).ready(function() {
280280

281281
// Do the actual search
282282
var results = this.searchindex.search(searchterm, this.searchoptions);
283-
var resultcount = (results.length > this.resultcount_limit)
284-
? this.resultcount_limit : results.length;
283+
var resultcount = Math.min(results.length, this.searchoptions.limit_results);
285284

286285
// Display search metrics
287286
this.searchresults_header.text(this.formatSearchMetric(resultcount, searchterm));
@@ -327,7 +326,14 @@ $( document ).ready(function() {
327326
//this.create_test_searchindex();
328327

329328
$.getJSON("searchindex.json", function(json) {
330-
//this_.searchindex = elasticlunr.Index.load(json);
329+
330+
if (json.enable == false) {
331+
this_.searchicon.hide();
332+
return;
333+
}
334+
335+
this_.searchoptions = json.searchoptions;
336+
//this_.searchindex = elasticlunr.Index.load(json.index);
331337

332338
// TODO: Workaround: reindex everything
333339
var searchindex = elasticlunr(function () {
@@ -337,7 +343,8 @@ $( document ).ready(function() {
337343
this.setRef('id');
338344
});
339345
window.mjs = json;
340-
var docs = json.documentStore.docs;
346+
window.search = this_;
347+
var docs = json.index.documentStore.docs;
341348
for (var key in docs) {
342349
searchindex.addDoc(docs[key]);
343350
}

src/theme/index.hbs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@
6464
}
6565
</script>
6666

67-
68-
6967
<!-- Fetch store.js from local - TODO add CDN when 2.x.x is available on cdnjs -->
7068
<script src="store.js"></script>
7169

src/utils/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::ascii::AsciiExt;
66
use std::borrow::Cow;
77
use std::fmt::Write;
88
use regex::Regex;
9+
use config::Search;
910

1011
/// A heading together with the successive content until the next heading will
1112
/// make up one `SearchDocument`. It represents some independently searchable part of the book.
@@ -65,6 +66,7 @@ impl SearchDocument {
6566
/// The field `anchor` in the `SearchDocument` struct becomes
6667
/// `(anchor_base, Some(heading_to_anchor("The Section Heading")))`
6768
pub fn render_markdown_into_searchindex<F>(
69+
searchconfig: &Search,
6870
search_documents: &mut Vec<SearchDocument>,
6971
text: &str,
7072
anchor_base: &str,
@@ -79,7 +81,7 @@ pub fn render_markdown_into_searchindex<F>(
7981

8082
let mut current = SearchDocument::new(&anchor_base, &hierarchy);
8183
let mut in_header = false;
82-
let max_paragraph_level = 3;
84+
let max_paragraph_level = searchconfig.split_until_heading as i32;
8385
let mut header_hierarchy = vec!["".to_owned(); max_paragraph_level as usize];
8486

8587
for event in p {

0 commit comments

Comments
 (0)