Skip to content

Commit 18f57f5

Browse files
authored
Merge pull request #2533 from ehuss/search-chapter-settings
Add output.html.search.chapter
2 parents dff5ac6 + 09a3728 commit 18f57f5

File tree

4 files changed

+169
-10
lines changed

4 files changed

+169
-10
lines changed

guide/src/format/configuration/renderers.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,20 @@ copy-js = true # include Javascript code for search
281281
- **copy-js:** Copy JavaScript files for the search implementation to the output
282282
directory. Defaults to `true`.
283283

284+
#### `[output.html.search.chapter]`
285+
286+
The [`output.html.search.chapter`] table provides the ability to modify search settings per chapter or directory. Each key is the path to the chapter source file or directory, and the value is a table of settings to apply to that path. This will merge recursively, with more specific paths taking precedence.
287+
288+
```toml
289+
[output.html.search.chapter]
290+
# Disables search indexing for all chapters in the `appendix` directory.
291+
"appendix" = { enable = false }
292+
# Enables search indexing for just this one appendix chapter.
293+
"appendix/glossary.md" = { enable = true }
294+
```
295+
296+
- **enable:** Enables or disables search indexing for the given chapters. Defaults to `true`. This does not override the overall `output.html.search.enable` setting; that must be `true` for any search functionality to be enabled. Be cautious when disabling indexing for chapters because that can potentially lead to user confusion when they search for terms and expect them to be found. This should only be used in exceptional circumstances where keeping the chapter in the index will cause issues with the quality of the search results.
297+
284298
### `[output.html.redirect]`
285299

286300
The `[output.html.redirect]` table provides a way to add redirects.

src/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,11 @@ pub struct Search {
735735
/// Copy JavaScript files for the search functionality to the output directory?
736736
/// Default: `true`.
737737
pub copy_js: bool,
738+
/// Specifies search settings for the given path.
739+
///
740+
/// The path can be for a specific chapter, or a directory. This will
741+
/// merge recursively, with more specific paths taking precedence.
742+
pub chapter: HashMap<String, SearchChapterSettings>,
738743
}
739744

740745
impl Default for Search {
@@ -751,10 +756,19 @@ impl Default for Search {
751756
expand: true,
752757
heading_split_level: 3,
753758
copy_js: true,
759+
chapter: HashMap::new(),
754760
}
755761
}
756762
}
757763

764+
/// Search options for chapters (or paths).
765+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
766+
#[serde(default, rename_all = "kebab-case")]
767+
pub struct SearchChapterSettings {
768+
/// Whether or not indexing is enabled, default `true`.
769+
pub enable: Option<bool>,
770+
}
771+
758772
/// Allows you to "update" any arbitrary field in a struct by round-tripping via
759773
/// a `toml::Value`.
760774
///

src/renderer/html_handlebars/search.rs

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use std::borrow::Cow;
22
use std::collections::{HashMap, HashSet};
3-
use std::path::Path;
3+
use std::path::{Path, PathBuf};
44

55
use elasticlunr::{Index, IndexBuilder};
66
use once_cell::sync::Lazy;
77
use pulldown_cmark::*;
88

9-
use crate::book::{Book, BookItem};
10-
use crate::config::Search;
9+
use crate::book::{Book, BookItem, Chapter};
10+
use crate::config::{Search, SearchChapterSettings};
1111
use crate::errors::*;
1212
use crate::theme::searcher;
1313
use crate::utils;
@@ -35,8 +35,20 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
3535

3636
let mut doc_urls = Vec::with_capacity(book.sections.len());
3737

38+
let chapter_configs = sort_search_config(&search_config.chapter);
39+
validate_chapter_config(&chapter_configs, book)?;
40+
3841
for item in book.iter() {
39-
render_item(&mut index, search_config, &mut doc_urls, item)?;
42+
let chapter = match item {
43+
BookItem::Chapter(ch) if !ch.is_draft_chapter() => ch,
44+
_ => continue,
45+
};
46+
let chapter_settings =
47+
get_chapter_settings(&chapter_configs, chapter.source_path.as_ref().unwrap());
48+
if !chapter_settings.enable.unwrap_or(true) {
49+
continue;
50+
}
51+
render_item(&mut index, search_config, &mut doc_urls, chapter)?;
4052
}
4153

4254
let index = write_to_json(index, search_config, doc_urls)?;
@@ -100,13 +112,8 @@ fn render_item(
100112
index: &mut Index,
101113
search_config: &Search,
102114
doc_urls: &mut Vec<String>,
103-
item: &BookItem,
115+
chapter: &Chapter,
104116
) -> Result<()> {
105-
let chapter = match *item {
106-
BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
107-
_ => return Ok(()),
108-
};
109-
110117
let chapter_path = chapter
111118
.path
112119
.as_ref()
@@ -313,3 +320,81 @@ fn clean_html(html: &str) -> String {
313320
});
314321
AMMONIA.clean(html).to_string()
315322
}
323+
324+
fn validate_chapter_config(
325+
chapter_configs: &[(PathBuf, SearchChapterSettings)],
326+
book: &Book,
327+
) -> Result<()> {
328+
for (path, _) in chapter_configs {
329+
let found = book
330+
.iter()
331+
.filter_map(|item| match item {
332+
BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch),
333+
_ => None,
334+
})
335+
.any(|chapter| {
336+
let ch_path = chapter.source_path.as_ref().unwrap();
337+
ch_path.starts_with(path)
338+
});
339+
if !found {
340+
bail!(
341+
"[output.html.search.chapter] key `{}` does not match any chapter paths",
342+
path.display()
343+
);
344+
}
345+
}
346+
Ok(())
347+
}
348+
349+
fn sort_search_config(
350+
map: &HashMap<String, SearchChapterSettings>,
351+
) -> Vec<(PathBuf, SearchChapterSettings)> {
352+
let mut settings: Vec<_> = map
353+
.iter()
354+
.map(|(key, value)| (PathBuf::from(key), value.clone()))
355+
.collect();
356+
// Note: This is case-sensitive, and assumes the author uses the same case
357+
// as the actual filename.
358+
settings.sort_by(|a, b| a.0.cmp(&b.0));
359+
settings
360+
}
361+
362+
fn get_chapter_settings(
363+
chapter_configs: &[(PathBuf, SearchChapterSettings)],
364+
source_path: &Path,
365+
) -> SearchChapterSettings {
366+
let mut result = SearchChapterSettings::default();
367+
for (path, config) in chapter_configs {
368+
if source_path.starts_with(path) {
369+
result.enable = config.enable.or(result.enable);
370+
}
371+
}
372+
result
373+
}
374+
375+
#[test]
376+
fn chapter_settings_priority() {
377+
let cfg = r#"
378+
[output.html.search.chapter]
379+
"cli/watch.md" = { enable = true }
380+
"cli" = { enable = false }
381+
"cli/inner/foo.md" = { enable = false }
382+
"cli/inner" = { enable = true }
383+
"foo" = {} # Just to make sure empty table is allowed.
384+
"#;
385+
let cfg: crate::Config = toml::from_str(cfg).unwrap();
386+
let html = cfg.html_config().unwrap();
387+
let chapter_configs = sort_search_config(&html.search.unwrap().chapter);
388+
for (path, enable) in [
389+
("foo.md", None),
390+
("cli/watch.md", Some(true)),
391+
("cli/index.md", Some(false)),
392+
("cli/inner/index.md", Some(true)),
393+
("cli/inner/foo.md", Some(false)),
394+
] {
395+
assert_eq!(
396+
get_chapter_settings(&chapter_configs, Path::new(path)),
397+
SearchChapterSettings { enable }
398+
);
399+
}
400+
}

tests/rendered_output.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,7 @@ fn failure_on_missing_theme_directory() {
736736
#[cfg(feature = "search")]
737737
mod search {
738738
use crate::dummy_book::DummyBook;
739+
use mdbook::utils::fs::write_file;
739740
use mdbook::MDBook;
740741
use std::fs::{self, File};
741742
use std::path::Path;
@@ -810,6 +811,51 @@ mod search {
810811
);
811812
}
812813

814+
#[test]
815+
fn can_disable_individual_chapters() {
816+
let temp = DummyBook::new().build().unwrap();
817+
let book_toml = r#"
818+
[book]
819+
title = "Search Test"
820+
821+
[output.html.search.chapter]
822+
"second" = { enable = false }
823+
"first/unicode.md" = { enable = false }
824+
"#;
825+
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
826+
let md = MDBook::load(temp.path()).unwrap();
827+
md.build().unwrap();
828+
let index = read_book_index(temp.path());
829+
let doc_urls = index["doc_urls"].as_array().unwrap();
830+
let contains = |path| {
831+
doc_urls
832+
.iter()
833+
.any(|p| p.as_str().unwrap().starts_with(path))
834+
};
835+
assert!(contains("second.html"));
836+
assert!(!contains("second/"));
837+
assert!(!contains("first/unicode.html"));
838+
assert!(contains("first/markdown.html"));
839+
}
840+
841+
#[test]
842+
fn chapter_settings_validation_error() {
843+
let temp = DummyBook::new().build().unwrap();
844+
let book_toml = r#"
845+
[book]
846+
title = "Search Test"
847+
848+
[output.html.search.chapter]
849+
"does-not-exist" = { enable = false }
850+
"#;
851+
write_file(temp.path(), "book.toml", book_toml.as_bytes()).unwrap();
852+
let md = MDBook::load(temp.path()).unwrap();
853+
let err = md.build().unwrap_err();
854+
assert!(format!("{err:?}").contains(
855+
"[output.html.search.chapter] key `does-not-exist` does not match any chapter paths"
856+
));
857+
}
858+
813859
// Setting this to `true` may cause issues with `cargo watch`,
814860
// since it may not finish writing the fixture before the tests
815861
// are run again.

0 commit comments

Comments
 (0)