2017-11-16 15:51:12 +08:00
mod dummy_book ;
2019-05-26 01:50:41 +07:00
use crate ::dummy_book ::{ assert_contains_strings , assert_doesnt_contain_strings , DummyBook } ;
2017-11-16 15:51:12 +08:00
2020-05-20 14:32:00 -07:00
use anyhow ::Context ;
2025-02-17 08:20:16 -08:00
use mdbook ::book ::Chapter ;
2018-07-23 12:45:01 -05:00
use mdbook ::config ::Config ;
use mdbook ::errors ::* ;
2019-06-19 22:49:18 -04:00
use mdbook ::utils ::fs ::write_file ;
2025-02-17 08:20:16 -08:00
use mdbook ::{ BookItem , MDBook } ;
2023-01-15 11:42:46 -08:00
use pretty_assertions ::assert_eq ;
2018-07-23 12:45:01 -05:00
use select ::document ::Document ;
2024-07-16 12:23:26 -07:00
use select ::predicate ::{ Attr , Class , Name , Predicate } ;
2020-05-27 02:35:15 +08:00
use std ::collections ::HashMap ;
2018-07-23 12:45:01 -05:00
use std ::ffi ::OsStr ;
2017-12-10 23:13:46 +11:00
use std ::fs ;
2022-06-22 23:40:36 +02:00
use std ::io ::Write ;
2020-05-30 04:15:24 +08:00
use std ::path ::{ Component , Path , PathBuf } ;
2022-03-26 15:34:07 +09:00
use std ::str ::FromStr ;
2018-03-27 01:47:37 +02:00
use tempfile ::Builder as TempFileBuilder ;
2018-07-23 12:45:01 -05:00
use walkdir ::{ DirEntry , WalkDir } ;
2017-07-10 18:17:19 +08:00
2019-05-07 01:20:58 +07:00
const BOOK_ROOT : & str = concat! ( env! ( " CARGO_MANIFEST_DIR " ) , " /tests/dummy_book " ) ;
const TOC_TOP_LEVEL : & [ & str ] = & [
2017-11-18 21:22:30 +08:00
" 1. First Chapter " ,
" 2. Second Chapter " ,
" Conclusion " ,
2018-07-25 12:45:20 -05:00
" Dummy Book " ,
2017-11-18 21:22:30 +08:00
" Introduction " ,
] ;
2019-05-07 01:20:58 +07:00
const TOC_SECOND_LEVEL : & [ & str ] = & [
2019-05-05 21:57:43 +07:00
" 1.1. Nested Chapter " ,
" 1.2. Includes " ,
" 1.3. Recursive " ,
2019-06-12 08:02:03 -07:00
" 1.4. Markdown " ,
2019-07-12 09:53:11 -07:00
" 1.5. Unicode " ,
2021-08-31 12:41:49 -07:00
" 1.6. No Headers " ,
2022-02-18 15:27:24 +00:00
" 1.7. Duplicate Headers " ,
2023-05-28 11:33:58 -07:00
" 1.8. Heading Attributes " ,
2019-06-12 08:02:03 -07:00
" 2.1. Nested Chapter " ,
2019-05-05 21:57:43 +07:00
] ;
2017-11-16 15:51:12 +08:00
2017-07-10 18:17:19 +08:00
#[ test ]
fn by_default_mdbook_generates_rendered_content_in_the_book_directory ( ) {
2017-11-16 15:51:12 +08:00
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
2018-01-07 22:10:48 +08:00
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
2017-07-10 18:17:19 +08:00
assert! ( ! temp . path ( ) . join ( " book " ) . exists ( ) ) ;
md . build ( ) . unwrap ( ) ;
assert! ( temp . path ( ) . join ( " book " ) . exists ( ) ) ;
2018-01-22 06:44:28 +08:00
let index_file = md . build_dir_for ( " html " ) . join ( " index.html " ) ;
assert! ( index_file . exists ( ) ) ;
2017-07-10 18:17:19 +08:00
}
#[ test ]
fn check_correct_cross_links_in_nested_dir ( ) {
2017-11-16 15:51:12 +08:00
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
2018-01-07 22:10:48 +08:00
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
2017-07-10 18:17:19 +08:00
md . build ( ) . unwrap ( ) ;
let first = temp . path ( ) . join ( " book " ) . join ( " first " ) ;
2017-09-01 16:54:57 -07:00
2017-11-18 21:22:30 +08:00
assert_contains_strings (
first . join ( " index.html " ) ,
2023-05-28 11:32:31 -07:00
& [ r ## "<h2 id="some-section"><a class="header" href="#some-section">"## ] ,
2017-11-18 21:22:30 +08:00
) ;
assert_contains_strings (
first . join ( " nested.html " ) ,
2023-05-28 11:32:31 -07:00
& [ r ## "<h2 id="some-section"><a class="header" href="#some-section">"## ] ,
2017-11-18 21:22:30 +08:00
) ;
2017-07-10 18:17:19 +08:00
}
#[ test ]
fn chapter_content_appears_in_rendered_document ( ) {
2017-11-18 21:22:30 +08:00
let content = vec! [
2018-07-25 12:45:20 -05:00
( " index.html " , " This file is just here to cause the " ) ,
( " intro.html " , " Here's some interesting text " ) ,
2017-11-18 21:22:30 +08:00
( " second.html " , " Second Chapter " ) ,
( " first/nested.html " , " testable code " ) ,
( " first/index.html " , " more text " ) ,
( " conclusion.html " , " Conclusion " ) ,
] ;
2017-07-10 18:17:19 +08:00
2017-11-16 15:51:12 +08:00
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
2018-01-07 22:10:48 +08:00
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
2017-07-10 18:17:19 +08:00
md . build ( ) . unwrap ( ) ;
let destination = temp . path ( ) . join ( " book " ) ;
for ( filename , text ) in content {
let path = destination . join ( filename ) ;
2017-09-01 16:40:39 -07:00
assert_contains_strings ( path , & [ text ] ) ;
2017-07-10 18:17:19 +08:00
}
2017-09-01 16:40:39 -07:00
}
2017-11-16 15:51:12 +08:00
/// Apply a series of predicates to some root predicate, where each
/// successive predicate is the descendant of the last one. Similar to how you
/// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list.
macro_rules ! descendants {
( $root :expr , $( $child :expr ) , * ) = > {
$root
$(
. descendant ( $child )
) *
} ;
}
/// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered
/// and placed in the `book` directory with their extensions set to `*.html`.
#[ test ]
fn chapter_files_were_rendered_to_html ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let src = Path ::new ( BOOK_ROOT ) . join ( " src " ) ;
2017-11-18 21:22:30 +08:00
let chapter_files = WalkDir ::new ( & src )
. into_iter ( )
. filter_entry ( | entry | entry_ends_with ( entry , " .md " ) )
2019-05-07 01:20:58 +07:00
. filter_map ( std ::result ::Result ::ok )
2017-11-18 21:22:30 +08:00
. map ( | entry | entry . path ( ) . to_path_buf ( ) )
. filter ( | path | path . file_name ( ) . and_then ( OsStr ::to_str ) ! = Some ( " SUMMARY.md " ) ) ;
2017-11-16 15:51:12 +08:00
for chapter in chapter_files {
2018-08-02 20:22:49 -05:00
let rendered_location = temp
. path ( )
2017-11-18 21:22:30 +08:00
. join ( chapter . strip_prefix ( & src ) . unwrap ( ) )
. with_extension ( " html " ) ;
assert! (
rendered_location . exists ( ) ,
" {} doesn't exits " ,
rendered_location . display ( )
) ;
2017-11-16 15:51:12 +08:00
}
}
fn entry_ends_with ( entry : & DirEntry , ending : & str ) -> bool {
entry . file_name ( ) . to_string_lossy ( ) . ends_with ( ending )
}
2024-07-15 18:38:50 -07:00
/// Read the TOC (`book/toc.js`) nested HTML and expose it as a DOM which we
2017-11-16 15:51:12 +08:00
/// can search with the `select` crate
2024-07-16 12:23:26 -07:00
fn toc_js_html ( ) -> Result < Document > {
2017-11-18 21:22:30 +08:00
let temp = DummyBook ::new ( )
. build ( )
2020-05-20 14:32:00 -07:00
. with_context ( | | " Couldn't create the dummy book " ) ? ;
2017-11-18 21:22:30 +08:00
MDBook ::load ( temp . path ( ) ) ?
. build ( )
2020-05-20 14:32:00 -07:00
. with_context ( | | " Book building failed " ) ? ;
2017-11-16 15:51:12 +08:00
2024-07-15 18:38:50 -07:00
let toc_path = temp . path ( ) . join ( " book " ) . join ( " toc.js " ) ;
let html = fs ::read_to_string ( toc_path ) . with_context ( | | " Unable to read index.html " ) ? ;
for line in html . lines ( ) {
2024-11-06 14:56:49 -07:00
if let Some ( left ) = line . strip_prefix ( " this.innerHTML = ' " ) {
2024-07-15 18:38:50 -07:00
if let Some ( html ) = left . strip_suffix ( " '; " ) {
return Ok ( Document ::from ( html ) ) ;
}
}
}
panic! ( " cannot find toc in file " )
2017-11-16 15:51:12 +08:00
}
2024-07-16 12:23:26 -07:00
/// Read the TOC fallback (`book/toc.html`) HTML and expose it as a DOM which we
/// can search with the `select` crate
fn toc_fallback_html ( ) -> Result < Document > {
let temp = DummyBook ::new ( )
. build ( )
. with_context ( | | " Couldn't create the dummy book " ) ? ;
MDBook ::load ( temp . path ( ) ) ?
. build ( )
. with_context ( | | " Book building failed " ) ? ;
let toc_path = temp . path ( ) . join ( " book " ) . join ( " toc.html " ) ;
let html = fs ::read_to_string ( toc_path ) . with_context ( | | " Unable to read index.html " ) ? ;
Ok ( Document ::from ( html . as_str ( ) ) )
}
2017-11-16 15:51:12 +08:00
#[ test ]
fn check_second_toc_level ( ) {
2024-07-16 12:23:26 -07:00
let doc = toc_js_html ( ) . unwrap ( ) ;
2017-11-16 15:51:12 +08:00
let mut should_be = Vec ::from ( TOC_SECOND_LEVEL ) ;
2021-08-24 08:45:06 +01:00
should_be . sort_unstable ( ) ;
2017-11-16 15:51:12 +08:00
2019-10-19 15:56:08 +08:00
let pred = descendants! (
Class ( " chapter " ) ,
Name ( " li " ) ,
Name ( " li " ) ,
Name ( " a " ) . and ( Class ( " toggle " ) . not ( ) )
) ;
2017-11-16 15:51:12 +08:00
2018-08-02 20:22:49 -05:00
let mut children_of_children : Vec < _ > = doc
. find ( pred )
2017-11-18 21:22:30 +08:00
. map ( | elem | elem . text ( ) . trim ( ) . to_string ( ) )
. collect ( ) ;
2017-11-16 15:51:12 +08:00
children_of_children . sort ( ) ;
assert_eq! ( children_of_children , should_be ) ;
}
#[ test ]
fn check_first_toc_level ( ) {
2024-07-16 12:23:26 -07:00
let doc = toc_js_html ( ) . unwrap ( ) ;
2017-11-16 15:51:12 +08:00
let mut should_be = Vec ::from ( TOC_TOP_LEVEL ) ;
should_be . extend ( TOC_SECOND_LEVEL ) ;
2021-08-24 08:45:06 +01:00
should_be . sort_unstable ( ) ;
2017-11-16 15:51:12 +08:00
2019-10-19 15:56:08 +08:00
let pred = descendants! (
Class ( " chapter " ) ,
Name ( " li " ) ,
Name ( " a " ) . and ( Class ( " toggle " ) . not ( ) )
) ;
2017-11-16 15:51:12 +08:00
2018-08-02 20:22:49 -05:00
let mut children : Vec < _ > = doc
. find ( pred )
2017-11-18 21:22:30 +08:00
. map ( | elem | elem . text ( ) . trim ( ) . to_string ( ) )
. collect ( ) ;
2017-11-16 15:51:12 +08:00
children . sort ( ) ;
assert_eq! ( children , should_be ) ;
}
#[ test ]
fn check_spacers ( ) {
2024-07-16 12:23:26 -07:00
let doc = toc_js_html ( ) . unwrap ( ) ;
2019-10-25 08:33:21 -07:00
let should_be = 2 ;
2017-11-16 15:51:12 +08:00
2018-08-02 20:22:49 -05:00
let num_spacers = doc
. find ( Class ( " chapter " ) . descendant ( Name ( " li " ) . and ( Class ( " spacer " ) ) ) )
2017-11-18 21:22:30 +08:00
. count ( ) ;
2017-11-16 15:51:12 +08:00
assert_eq! ( num_spacers , should_be ) ;
}
2017-12-02 22:53:05 -08:00
2024-07-16 12:23:26 -07:00
// don't use target="_parent" in JS
#[ test ]
fn check_link_target_js ( ) {
let doc = toc_js_html ( ) . unwrap ( ) ;
let num_parent_links = doc
. find (
Class ( " chapter " )
. descendant ( Name ( " li " ) )
. descendant ( Name ( " a " ) . and ( Attr ( " target " , " _parent " ) ) ) ,
)
. count ( ) ;
assert_eq! ( num_parent_links , 0 ) ;
}
// don't use target="_parent" in IFRAME
#[ test ]
fn check_link_target_fallback ( ) {
let doc = toc_fallback_html ( ) . unwrap ( ) ;
let num_parent_links = doc
. find (
Class ( " chapter " )
. descendant ( Name ( " li " ) )
. descendant ( Name ( " a " ) . and ( Attr ( " target " , " _parent " ) ) ) ,
)
. count ( ) ;
assert_eq! (
num_parent_links ,
TOC_TOP_LEVEL . len ( ) + TOC_SECOND_LEVEL . len ( )
) ;
}
2017-12-04 15:38:57 +08:00
#[ test ]
fn example_book_can_build ( ) {
2017-12-10 23:13:46 +11:00
let example_book_dir = dummy_book ::new_copy_of_example_book ( ) . unwrap ( ) ;
2017-12-04 15:38:57 +08:00
2018-01-07 22:10:48 +08:00
let md = MDBook ::load ( example_book_dir . path ( ) ) . unwrap ( ) ;
2017-12-04 15:38:57 +08:00
2018-01-07 22:10:48 +08:00
md . build ( ) . unwrap ( ) ;
2017-12-10 23:13:46 +11:00
}
2018-01-06 17:02:23 +01:00
2022-06-22 22:55:52 +02:00
#[ test ]
fn first_chapter_is_copied_as_index_even_if_not_first_elem ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let mut cfg = Config ::default ( ) ;
cfg . set ( " book.src " , " index_html_test " )
. expect ( " Couldn't set config.book.src to \" index_html_test \" " ) ;
let md = MDBook ::load_with_config ( temp . path ( ) , cfg ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
2022-06-22 23:40:36 +02:00
let root = temp . path ( ) . join ( " book " ) ;
let chapter = fs ::read_to_string ( root . join ( " chapter_1.html " ) ) . expect ( " read chapter 1 " ) ;
let index = fs ::read_to_string ( root . join ( " index.html " ) ) . expect ( " read index " ) ;
pretty_assertions ::assert_eq! ( chapter , index ) ;
2022-06-22 22:55:52 +02:00
}
2018-05-14 14:52:29 -05:00
#[ test ]
fn theme_dir_overrides_work_correctly ( ) {
let book_dir = dummy_book ::new_copy_of_example_book ( ) . unwrap ( ) ;
let book_dir = book_dir . path ( ) ;
let theme_dir = book_dir . join ( " theme " ) ;
2019-05-30 23:12:33 +07:00
let mut index = mdbook ::theme ::INDEX . to_vec ( ) ;
2018-05-14 14:52:29 -05:00
index . extend_from_slice ( b " \n <!-- This is a modified index.hbs! --> " ) ;
write_file ( & theme_dir , " index.hbs " , & index ) . unwrap ( ) ;
let md = MDBook ::load ( book_dir ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let built_index = book_dir . join ( " book " ) . join ( " index.html " ) ;
dummy_book ::assert_contains_strings ( built_index , & [ " This is a modified index.hbs! " ] ) ;
}
2019-05-08 00:32:43 +02:00
#[ test ]
fn no_index_for_print_html ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let print_html = temp . path ( ) . join ( " book/print.html " ) ;
assert_contains_strings ( print_html , & [ r ## "noindex"## ] ) ;
let index_html = temp . path ( ) . join ( " book/index.html " ) ;
assert_doesnt_contain_strings ( index_html , & [ r ## "noindex"## ] ) ;
}
2020-05-27 02:35:15 +08:00
#[ test ]
fn redirects_are_emitted_correctly ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let mut md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
// override the "outputs.html.redirect" table
let redirects : HashMap < PathBuf , String > = vec! [
2020-05-30 04:11:11 +08:00
( PathBuf ::from ( " /overview.html " ) , String ::from ( " index.html " ) ) ,
2020-05-27 02:35:15 +08:00
(
2020-05-30 04:11:11 +08:00
PathBuf ::from ( " /nexted/page.md " ) ,
2020-05-27 02:35:15 +08:00
String ::from ( " https://rust-lang.org/ " ) ,
) ,
]
. into_iter ( )
. collect ( ) ;
md . config . set ( " output.html.redirect " , & redirects ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
for ( original , redirect ) in & redirects {
2020-05-30 04:15:24 +08:00
let mut redirect_file = md . build_dir_for ( " html " ) ;
// append everything except the bits that make it absolute
// (e.g. "/" or "C:\")
2021-08-24 08:45:06 +01:00
redirect_file . extend ( remove_absolute_components ( original ) ) ;
2020-05-27 02:35:15 +08:00
let contents = fs ::read_to_string ( & redirect_file ) . unwrap ( ) ;
assert! ( contents . contains ( redirect ) ) ;
}
}
2021-05-27 21:07:35 -07:00
#[ test ]
fn edit_url_has_default_src_dir_edit_url ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let book_toml = r #"
[ book ]
title = " implicit "
[ output . html ]
edit - url - template = " https://github.com/rust-lang/mdBook/edit/master/guide/{path} "
" #;
2021-08-24 08:45:06 +01:00
write_file ( temp . path ( ) , " book.toml " , book_toml . as_bytes ( ) ) . unwrap ( ) ;
2021-05-27 21:07:35 -07:00
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let index_html = temp . path ( ) . join ( " book " ) . join ( " index.html " ) ;
assert_contains_strings (
index_html ,
2021-08-24 08:48:24 +01:00
& [
2021-05-27 21:07:35 -07:00
r # "href="https://github.com/rust-lang/mdBook/edit/master/guide/src/README.md" title="Suggest an edit""# ,
] ,
) ;
}
#[ test ]
fn edit_url_has_configured_src_dir_edit_url ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let book_toml = r #"
[ book ]
title = " implicit "
src = " src2 "
[ output . html ]
edit - url - template = " https://github.com/rust-lang/mdBook/edit/master/guide/{path} "
" #;
2021-08-24 08:45:06 +01:00
write_file ( temp . path ( ) , " book.toml " , book_toml . as_bytes ( ) ) . unwrap ( ) ;
2021-05-27 21:07:35 -07:00
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let index_html = temp . path ( ) . join ( " book " ) . join ( " index.html " ) ;
assert_contains_strings (
index_html ,
2021-08-24 08:48:24 +01:00
& [
2021-05-27 21:07:35 -07:00
r # "href="https://github.com/rust-lang/mdBook/edit/master/guide/src2/README.md" title="Suggest an edit""# ,
] ,
) ;
}
2020-05-30 04:15:24 +08:00
fn remove_absolute_components ( path : & Path ) -> impl Iterator < Item = Component > + '_ {
2023-05-13 09:55:51 -07:00
path . components ( )
. skip_while ( | c | matches! ( c , Component ::Prefix ( _ ) | Component ::RootDir ) )
2020-05-30 04:15:24 +08:00
}
2022-04-14 20:35:39 -07:00
/// Checks formatting of summary names with inline elements.
#[ test ]
fn summary_with_markdown_formatting ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let mut cfg = Config ::default ( ) ;
cfg . set ( " book.src " , " summary-formatting " ) . unwrap ( ) ;
let md = MDBook ::load_with_config ( temp . path ( ) , cfg ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
2024-07-15 18:38:50 -07:00
let rendered_path = temp . path ( ) . join ( " book/toc.js " ) ;
2022-04-14 20:35:39 -07:00
assert_contains_strings (
rendered_path ,
& [
2024-07-15 18:38:50 -07:00
r # "<a href="formatted-summary.html"><strong aria-hidden="true">1.</strong> Italic code *escape* `escape2`</a>"# ,
2022-04-14 20:35:39 -07:00
r # "<a href="soft.html"><strong aria-hidden="true">2.</strong> Soft line break</a>"# ,
r # "<a href="escaped-tag.html"><strong aria-hidden="true">3.</strong> <escaped tag></a>"# ,
] ,
) ;
let generated_md = temp . path ( ) . join ( " summary-formatting/formatted-summary.md " ) ;
assert_eq! (
fs ::read_to_string ( generated_md ) . unwrap ( ) ,
" # Italic code *escape* `escape2` \n "
) ;
let generated_md = temp . path ( ) . join ( " summary-formatting/soft.md " ) ;
assert_eq! (
fs ::read_to_string ( generated_md ) . unwrap ( ) ,
" # Soft line break \n "
) ;
let generated_md = temp . path ( ) . join ( " summary-formatting/escaped-tag.md " ) ;
assert_eq! (
fs ::read_to_string ( generated_md ) . unwrap ( ) ,
" # <escaped tag> \n "
) ;
}
2022-04-28 13:13:58 +08:00
/// Ensure building fails if `[output.html].theme` points to a non-existent directory
#[ test ]
fn failure_on_missing_theme_directory ( ) {
// 1. Using default theme should work
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let book_toml = r #"
[ book ]
title = " implicit "
src = " src "
" #;
write_file ( temp . path ( ) , " book.toml " , book_toml . as_bytes ( ) ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
let got = md . build ( ) ;
assert! ( got . is_ok ( ) ) ;
// 2. Pointing to a normal directory should work
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let created = fs ::create_dir ( temp . path ( ) . join ( " theme-directory " ) ) ;
assert! ( created . is_ok ( ) ) ;
let book_toml = r #"
[ book ]
title = " implicit "
src = " src "
[ output . html ]
theme = " ./theme-directory "
" #;
write_file ( temp . path ( ) , " book.toml " , book_toml . as_bytes ( ) ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
let got = md . build ( ) ;
assert! ( got . is_ok ( ) ) ;
// 3. Pointing to a non-existent directory should fail
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let book_toml = r #"
[ book ]
title = " implicit "
src = " src "
[ output . html ]
theme = " ./non-existent-directory "
" #;
write_file ( temp . path ( ) , " book.toml " , book_toml . as_bytes ( ) ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
let got = md . build ( ) ;
assert! ( got . is_err ( ) ) ;
}
2018-03-07 07:02:06 -06:00
#[ cfg(feature = " search " ) ]
mod search {
2019-05-26 01:50:41 +07:00
use crate ::dummy_book ::DummyBook ;
2025-01-27 19:45:50 -08:00
use mdbook ::utils ::fs ::write_file ;
2018-03-07 07:02:06 -06:00
use mdbook ::MDBook ;
2019-06-19 22:49:18 -04:00
use std ::fs ::{ self , File } ;
2018-07-23 12:45:01 -05:00
use std ::path ::Path ;
2018-03-07 07:02:06 -06:00
fn read_book_index ( root : & Path ) -> serde_json ::Value {
let index = root . join ( " book/searchindex.js " ) ;
2019-06-19 22:49:18 -04:00
let index = fs ::read_to_string ( index ) . unwrap ( ) ;
2025-04-02 21:03:01 +02:00
let index = index . trim_start_matches ( " window.search = JSON.parse(' " ) ;
let index = index . trim_end_matches ( " '); " ) ;
// We need unescape the string as it's supposed to be an escaped JS string.
serde_json ::from_str ( & index . replace ( " \\ ' " , " ' " ) . replace ( " \\ \\ " , " \\ " ) ) . unwrap ( )
2018-03-07 07:02:06 -06:00
}
#[ test ]
fn book_creates_reasonable_search_index ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let index = read_book_index ( temp . path ( ) ) ;
2018-06-13 15:15:58 -05:00
let doc_urls = index [ " doc_urls " ] . as_array ( ) . unwrap ( ) ;
2024-02-05 14:11:27 -08:00
eprintln! ( " doc_urls= {doc_urls:#?} " , ) ;
2018-07-23 12:45:01 -05:00
let get_doc_ref =
| url : & str | -> String { doc_urls . iter ( ) . position ( | s | s = = url ) . unwrap ( ) . to_string ( ) } ;
2018-06-13 15:15:58 -05:00
let first_chapter = get_doc_ref ( " first/index.html#first-chapter " ) ;
let introduction = get_doc_ref ( " intro.html#introduction " ) ;
let some_section = get_doc_ref ( " first/index.html#some-section " ) ;
let summary = get_doc_ref ( " first/includes.html#summary " ) ;
2021-08-31 12:41:49 -07:00
let no_headers = get_doc_ref ( " first/no-headers.html " ) ;
2022-02-18 15:27:24 +00:00
let duplicate_headers_1 = get_doc_ref ( " first/duplicate-headers.html#header-text-1 " ) ;
2018-06-13 15:15:58 -05:00
let conclusion = get_doc_ref ( " conclusion.html#conclusion " ) ;
2023-05-28 11:55:56 -07:00
let heading_attrs = get_doc_ref ( " first/heading-attributes.html#both " ) ;
2018-06-13 15:15:58 -05:00
2018-03-07 07:02:06 -06:00
let bodyidx = & index [ " index " ] [ " index " ] [ " body " ] [ " root " ] ;
let textidx = & bodyidx [ " t " ] [ " e " ] [ " x " ] [ " t " ] ;
2022-02-18 15:27:24 +00:00
assert_eq! ( textidx [ " df " ] , 5 ) ;
2018-06-13 15:15:58 -05:00
assert_eq! ( textidx [ " docs " ] [ & first_chapter ] [ " tf " ] , 1.0 ) ;
assert_eq! ( textidx [ " docs " ] [ & introduction ] [ " tf " ] , 1.0 ) ;
2018-03-07 07:02:06 -06:00
let docs = & index [ " index " ] [ " documentStore " ] [ " docs " ] ;
2018-06-13 15:15:58 -05:00
assert_eq! ( docs [ & first_chapter ] [ " body " ] , " more text. " ) ;
assert_eq! ( docs [ & some_section ] [ " body " ] , " " ) ;
2018-03-07 07:02:06 -06:00
assert_eq! (
2018-06-13 15:15:58 -05:00
docs [ & summary ] [ " body " ] ,
2023-05-28 11:33:58 -07:00
" Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Heading Attributes Second Chapter Nested Chapter Conclusion "
2018-03-07 07:02:06 -06:00
) ;
2020-11-26 08:57:43 +11:00
assert_eq! (
docs [ & summary ] [ " breadcrumbs " ] ,
" First Chapter » Includes » Summary "
) ;
2024-02-05 14:11:27 -08:00
// See note about InlineHtml in search.rs. Ideally the `alert()` part
// should not be in the index, but we don't have a way to scrub inline
// html.
assert_eq! ( docs [ & conclusion ] [ " body " ] , " I put <HTML> in here! Sneaky inline event alert( \" inline \" );. But regular inline is indexed. " ) ;
2021-08-31 12:41:49 -07:00
assert_eq! (
docs [ & no_headers ] [ " breadcrumbs " ] ,
" First Chapter » No Headers "
) ;
2022-02-18 15:27:24 +00:00
assert_eq! (
docs [ & duplicate_headers_1 ] [ " breadcrumbs " ] ,
" First Chapter » Duplicate Headers » Header Text "
) ;
2021-08-31 12:41:49 -07:00
assert_eq! (
docs [ & no_headers ] [ " body " ] ,
2022-05-22 13:57:09 +01:00
" Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex. "
2021-08-31 12:41:49 -07:00
) ;
2023-05-28 11:55:56 -07:00
assert_eq! (
docs [ & heading_attrs ] [ " breadcrumbs " ] ,
" First Chapter » Heading Attributes » Heading with id and classes "
) ;
2018-03-07 07:02:06 -06:00
}
2025-01-27 19:45:50 -08:00
#[ test ]
fn can_disable_individual_chapters ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let book_toml = r #"
[ book ]
title = " Search Test "
[ output . html . search . chapter ]
" second " = { enable = false }
" first/unicode.md " = { enable = false }
" #;
write_file ( temp . path ( ) , " book.toml " , book_toml . as_bytes ( ) ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let index = read_book_index ( temp . path ( ) ) ;
let doc_urls = index [ " doc_urls " ] . as_array ( ) . unwrap ( ) ;
let contains = | path | {
doc_urls
. iter ( )
. any ( | p | p . as_str ( ) . unwrap ( ) . starts_with ( path ) )
} ;
assert! ( contains ( " second.html " ) ) ;
assert! ( ! contains ( " second/ " ) ) ;
assert! ( ! contains ( " first/unicode.html " ) ) ;
assert! ( contains ( " first/markdown.html " ) ) ;
}
#[ test ]
fn chapter_settings_validation_error ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let book_toml = r #"
[ book ]
title = " Search Test "
[ output . html . search . chapter ]
" does-not-exist " = { enable = false }
" #;
write_file ( temp . path ( ) , " book.toml " , book_toml . as_bytes ( ) ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
let err = md . build ( ) . unwrap_err ( ) ;
assert! ( format! ( " {err:?} " ) . contains (
" [output.html.search.chapter] key `does-not-exist` does not match any chapter paths "
) ) ;
}
2018-03-07 07:02:06 -06:00
// Setting this to `true` may cause issues with `cargo watch`,
// since it may not finish writing the fixture before the tests
// are run again.
2018-06-13 15:15:58 -05:00
const GENERATE_FIXTURE : bool = false ;
2018-03-07 07:02:06 -06:00
fn get_fixture ( ) -> serde_json ::Value {
if GENERATE_FIXTURE {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let src = read_book_index ( temp . path ( ) ) ;
let dest = Path ::new ( env! ( " CARGO_MANIFEST_DIR " ) ) . join ( " tests/searchindex_fixture.json " ) ;
2023-05-13 09:44:11 -07:00
let dest = File ::create ( dest ) . unwrap ( ) ;
2018-03-07 07:02:06 -06:00
serde_json ::to_writer_pretty ( dest , & src ) . unwrap ( ) ;
src
} else {
let json = include_str! ( " searchindex_fixture.json " ) ;
serde_json ::from_str ( json ) . expect ( " Unable to deserialize the fixture " )
}
}
// So you've broken the test. If you changed dummy_book, it's probably
// safe to regenerate the fixture. If you haven't then make sure that the
// search index still works. Run `cargo run -- serve tests/dummy_book`
// and try some searches. Are you getting results? Do the teasers look OK?
// Are there new errors in the JS console?
//
// If you're pretty sure you haven't broken anything, change `GENERATE_FIXTURE`
// above to `true`, and run `cargo test` to generate a new fixture. Then
2018-06-13 15:15:58 -05:00
// **change it back to `false`**. Include the changed `searchindex_fixture.json` in your commit.
2018-03-07 07:02:06 -06:00
#[ test ]
fn search_index_hasnt_changed_accidentally ( ) {
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
md . build ( ) . unwrap ( ) ;
let book_index = read_book_index ( temp . path ( ) ) ;
let fixture_index = get_fixture ( ) ;
// Uncomment this if you're okay with pretty-printing 32KB of JSON
//assert_eq!(fixture_index, book_index);
if book_index ! = fixture_index {
panic! ( " The search index has changed from the fixture " ) ;
}
}
}
2023-01-15 11:42:46 -08:00
#[ test ]
fn custom_fonts ( ) {
// Tests to ensure custom fonts are copied as expected.
let builtin_fonts = [
" OPEN-SANS-LICENSE.txt " ,
" SOURCE-CODE-PRO-LICENSE.txt " ,
" fonts.css " ,
" open-sans-v17-all-charsets-300.woff2 " ,
" open-sans-v17-all-charsets-300italic.woff2 " ,
" open-sans-v17-all-charsets-600.woff2 " ,
" open-sans-v17-all-charsets-600italic.woff2 " ,
" open-sans-v17-all-charsets-700.woff2 " ,
" open-sans-v17-all-charsets-700italic.woff2 " ,
" open-sans-v17-all-charsets-800.woff2 " ,
" open-sans-v17-all-charsets-800italic.woff2 " ,
" open-sans-v17-all-charsets-italic.woff2 " ,
" open-sans-v17-all-charsets-regular.woff2 " ,
" source-code-pro-v11-all-charsets-500.woff2 " ,
] ;
let actual_files = | path : & Path | -> Vec < String > {
let mut actual : Vec < _ > = path
. read_dir ( )
. unwrap ( )
. map ( | entry | entry . unwrap ( ) . file_name ( ) . into_string ( ) . unwrap ( ) )
. collect ( ) ;
actual . sort ( ) ;
actual
} ;
let has_fonts_css = | path : & Path | -> bool {
let contents = fs ::read_to_string ( path . join ( " book/index.html " ) ) . unwrap ( ) ;
contents . contains ( " fonts/fonts.css " )
} ;
// No theme:
let temp = TempFileBuilder ::new ( ) . prefix ( " mdbook " ) . tempdir ( ) . unwrap ( ) ;
let p = temp . path ( ) ;
MDBook ::init ( p ) . build ( ) . unwrap ( ) ;
MDBook ::load ( p ) . unwrap ( ) . build ( ) . unwrap ( ) ;
assert_eq! ( actual_files ( & p . join ( " book/fonts " ) ) , & builtin_fonts ) ;
assert! ( has_fonts_css ( p ) ) ;
// Full theme.
let temp = TempFileBuilder ::new ( ) . prefix ( " mdbook " ) . tempdir ( ) . unwrap ( ) ;
let p = temp . path ( ) ;
MDBook ::init ( p ) . copy_theme ( true ) . build ( ) . unwrap ( ) ;
assert_eq! ( actual_files ( & p . join ( " theme/fonts " ) ) , & builtin_fonts ) ;
MDBook ::load ( p ) . unwrap ( ) . build ( ) . unwrap ( ) ;
assert_eq! ( actual_files ( & p . join ( " book/fonts " ) ) , & builtin_fonts ) ;
assert! ( has_fonts_css ( p ) ) ;
2023-05-13 08:59:28 -07:00
// Mixed with copy-fonts=true
// Should ignore the copy-fonts setting since the user has provided their own fonts.css.
2023-01-15 11:42:46 -08:00
let temp = TempFileBuilder ::new ( ) . prefix ( " mdbook " ) . tempdir ( ) . unwrap ( ) ;
let p = temp . path ( ) ;
MDBook ::init ( p ) . build ( ) . unwrap ( ) ;
write_file ( & p . join ( " theme/fonts " ) , " fonts.css " , b " /*custom*/ " ) . unwrap ( ) ;
write_file ( & p . join ( " theme/fonts " ) , " myfont.woff " , b " " ) . unwrap ( ) ;
MDBook ::load ( p ) . unwrap ( ) . build ( ) . unwrap ( ) ;
assert! ( has_fonts_css ( p ) ) ;
2023-05-13 08:59:28 -07:00
assert_eq! (
actual_files ( & p . join ( " book/fonts " ) ) ,
[ " fonts.css " , " myfont.woff " ]
) ;
2023-01-15 11:42:46 -08:00
// copy-fonts=false, no theme
// This should generate a deprecation warning.
let temp = TempFileBuilder ::new ( ) . prefix ( " mdbook " ) . tempdir ( ) . unwrap ( ) ;
let p = temp . path ( ) ;
MDBook ::init ( p ) . build ( ) . unwrap ( ) ;
let config = Config ::from_str ( " output.html.copy-fonts = false " ) . unwrap ( ) ;
MDBook ::load_with_config ( p , config )
. unwrap ( )
. build ( )
. unwrap ( ) ;
assert! ( ! has_fonts_css ( p ) ) ;
assert! ( ! p . join ( " book/fonts " ) . exists ( ) ) ;
// copy-fonts=false with empty fonts.css
let temp = TempFileBuilder ::new ( ) . prefix ( " mdbook " ) . tempdir ( ) . unwrap ( ) ;
let p = temp . path ( ) ;
MDBook ::init ( p ) . build ( ) . unwrap ( ) ;
write_file ( & p . join ( " theme/fonts " ) , " fonts.css " , b " " ) . unwrap ( ) ;
let config = Config ::from_str ( " output.html.copy-fonts = false " ) . unwrap ( ) ;
MDBook ::load_with_config ( p , config )
. unwrap ( )
. build ( )
. unwrap ( ) ;
assert! ( ! has_fonts_css ( p ) ) ;
assert! ( ! p . join ( " book/fonts " ) . exists ( ) ) ;
// copy-fonts=false with fonts theme
let temp = TempFileBuilder ::new ( ) . prefix ( " mdbook " ) . tempdir ( ) . unwrap ( ) ;
let p = temp . path ( ) ;
MDBook ::init ( p ) . build ( ) . unwrap ( ) ;
write_file ( & p . join ( " theme/fonts " ) , " fonts.css " , b " /*custom*/ " ) . unwrap ( ) ;
write_file ( & p . join ( " theme/fonts " ) , " myfont.woff " , b " " ) . unwrap ( ) ;
let config = Config ::from_str ( " output.html.copy-fonts = false " ) . unwrap ( ) ;
MDBook ::load_with_config ( p , config )
. unwrap ( )
. build ( )
. unwrap ( ) ;
assert! ( has_fonts_css ( p ) ) ;
assert_eq! (
actual_files ( & p . join ( " book/fonts " ) ) ,
& [ " fonts.css " , " myfont.woff " ]
) ;
}
2023-05-28 11:33:58 -07:00
2025-02-17 08:20:16 -08:00
#[ test ]
fn with_no_source_path ( ) {
// Test for a regression where search would fail if source_path is None.
let temp = DummyBook ::new ( ) . build ( ) . unwrap ( ) ;
let mut md = MDBook ::load ( temp . path ( ) ) . unwrap ( ) ;
let chapter = Chapter {
name : " Sample chapter " . to_string ( ) ,
content : " " . to_string ( ) ,
number : None ,
sub_items : Vec ::new ( ) ,
path : Some ( PathBuf ::from ( " sample.html " ) ) ,
source_path : None ,
parent_names : Vec ::new ( ) ,
} ;
md . book . sections . push ( BookItem ::Chapter ( chapter ) ) ;
md . build ( ) . unwrap ( ) ;
}