diff --git a/src/book/book.rs b/src/book/book.rs index da2a0a3c..c3573d8c 100644 --- a/src/book/book.rs +++ b/src/book/book.rs @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf}; use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; use crate::config::BuildConfig; use crate::errors::*; +use crate::utils::bracket_escape; /// Load a book into memory from its `src/` directory. pub fn load_book>(src_dir: P, cfg: &BuildConfig) -> Result { @@ -53,7 +54,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> { let mut f = File::create(&filename).with_context(|| { format!("Unable to create missing file: {}", filename.display()) })?; - writeln!(f, "# {}", link.name)?; + writeln!(f, "# {}", bracket_escape(&link.name))?; } } diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs index a2ea501d..5869dd36 100644 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ b/src/renderer/html_handlebars/helpers/toc.rs @@ -1,11 +1,10 @@ use std::collections::BTreeMap; -use std::io; use std::path::Path; use crate::utils; +use crate::utils::bracket_escape; use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; -use pulldown_cmark::{html, Event, Parser}; // Handlebars helper to construct TOC #[derive(Clone, Copy)] @@ -103,7 +102,7 @@ impl HelperDef for RenderToc { // Part title if let Some(title) = item.get("part") { out.write("
  • ")?; - write_escaped(out, title)?; + out.write(&bracket_escape(title))?; out.write("
  • ")?; continue; } @@ -148,20 +147,7 @@ impl HelperDef for RenderToc { } if let Some(name) = item.get("name") { - // Render only inline code blocks - - // filter all events that are not inline code blocks - let parser = Parser::new(name).filter(|event| match *event { - Event::Code(_) | Event::Html(_) | Event::Text(_) => true, - _ => false, - }); - - // render markdown to html - let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2); - html::push_html(&mut markdown_parsed_name, parser); - - // write to the handlebars template - write_escaped(out, &markdown_parsed_name)?; + out.write(&bracket_escape(name))? } if path_exists { @@ -205,18 +191,3 @@ fn write_li_open_tag( li.push_str("\">"); out.write(&li) } - -fn write_escaped(out: &mut dyn Output, mut title: &str) -> io::Result<()> { - let needs_escape: &[char] = &['<', '>']; - while let Some(next) = title.find(needs_escape) { - out.write(&title[..next])?; - match title.as_bytes()[next] { - b'<' => out.write("<")?, - b'>' => out.write(">")?, - _ => unreachable!(), - } - title = &title[next + 1..]; - } - out.write(title)?; - Ok(()) -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index cf213264..2000d661 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -233,8 +233,26 @@ pub fn log_backtrace(e: &Error) { } } +pub(crate) fn bracket_escape(mut s: &str) -> String { + let mut escaped = String::with_capacity(s.len()); + let needs_escape: &[char] = &['<', '>']; + while let Some(next) = s.find(needs_escape) { + escaped.push_str(&s[..next]); + match s.as_bytes()[next] { + b'<' => escaped.push_str("<"), + b'>' => escaped.push_str(">"), + _ => unreachable!(), + } + s = &s[next + 1..]; + } + escaped.push_str(s); + escaped +} + #[cfg(test)] mod tests { + use super::bracket_escape; + mod render_markdown { use super::super::render_markdown; @@ -431,4 +449,14 @@ more text with spaces assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-2"); } } + + #[test] + fn escaped_brackets() { + assert_eq!(bracket_escape(""), ""); + assert_eq!(bracket_escape("<"), "<"); + assert_eq!(bracket_escape(">"), ">"); + assert_eq!(bracket_escape("<>"), "<>"); + assert_eq!(bracket_escape(""), "<test>"); + assert_eq!(bracket_escape("ab"), "a<test>b"); + } } diff --git a/tests/dummy_book/summary-formatting/SUMMARY.md b/tests/dummy_book/summary-formatting/SUMMARY.md new file mode 100644 index 00000000..336218d8 --- /dev/null +++ b/tests/dummy_book/summary-formatting/SUMMARY.md @@ -0,0 +1,6 @@ +# Summary formatting tests + +- [*Italic* `code` \*escape\* \`escape2\`](formatted-summary.md) +- [Soft +line break](soft.md) +- [\](escaped-tag.md) diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index ad02dabe..79c120bb 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -621,6 +621,42 @@ fn remove_absolute_components(path: &Path) -> impl Iterator + }) } +/// 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(); + + let rendered_path = temp.path().join("book/formatted-summary.html"); + assert_contains_strings( + rendered_path, + &[ + r#" Italic code *escape* `escape2`"#, + r#" Soft line break"#, + r#" <escaped tag>"#, + ], + ); + + 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" + ); +} + #[cfg(feature = "search")] mod search { use crate::dummy_book::DummyBook;