diff --git a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
index 73b2a141de..e703752a7e 100644
--- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
+++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs
@@ -8,7 +8,7 @@ use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
use mdbook_core::utils;
use mdbook_core::utils::fs::get_404_output_file;
-use mdbook_markdown::{render_markdown, render_markdown_with_path};
+use mdbook_markdown::render_markdown;
use mdbook_renderer::{RenderContext, Renderer};
use regex::{Captures, Regex};
use serde_json::json;
@@ -56,10 +56,10 @@ impl HtmlHandlebars {
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
}
- let content = render_markdown(&ch.content, ctx.html_config.smart_punctuation);
-
- let fixed_content =
- render_markdown_with_path(&ch.content, ctx.html_config.smart_punctuation, Some(path));
+ let mut options = crate::html_render_options_from_config(path, &ctx.html_config);
+ let content = render_markdown(&ch.content, &options);
+ options.for_print = true;
+ let fixed_content = render_markdown(&ch.content, &options);
if prev_ch.is_some() && ctx.html_config.print.page_break {
// Add page break between chapters
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
@@ -193,7 +193,8 @@ impl HtmlHandlebars {
.to_string()
}
};
- let html_content_404 = render_markdown(&content_404, html_config.smart_punctuation);
+ let options = crate::html_render_options_from_config(Path::new("404.md"), html_config);
+ let html_content_404 = render_markdown(&content_404, &options);
let mut data_404 = data.clone();
let base_url = if let Some(site_url) = &html_config.site_url {
diff --git a/crates/mdbook-html/src/html_handlebars/search.rs b/crates/mdbook-html/src/html_handlebars/search.rs
index 25f19d26ce..1a235208f6 100644
--- a/crates/mdbook-html/src/html_handlebars/search.rs
+++ b/crates/mdbook-html/src/html_handlebars/search.rs
@@ -6,6 +6,7 @@ use log::{debug, warn};
use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::{Search, SearchChapterSettings};
use mdbook_core::utils;
+use mdbook_markdown::HtmlRenderOptions;
use mdbook_markdown::new_cmark_parser;
use pulldown_cmark::*;
use serde::Serialize;
@@ -133,7 +134,8 @@ fn render_item(
.with_context(|| "Could not convert HTML path to str")?;
let anchor_base = utils::fs::normalize_path(filepath);
- let mut p = new_cmark_parser(&chapter.content, false).peekable();
+ let options = HtmlRenderOptions::new(&chapter_path);
+ let mut p = new_cmark_parser(&chapter.content, &options.markdown_options).peekable();
let mut in_heading = false;
let max_section_depth = u32::from(search_config.heading_split_level);
diff --git a/crates/mdbook-html/src/lib.rs b/crates/mdbook-html/src/lib.rs
index 62b392a5a0..d3025e426d 100644
--- a/crates/mdbook-html/src/lib.rs
+++ b/crates/mdbook-html/src/lib.rs
@@ -4,3 +4,16 @@ mod html_handlebars;
pub mod theme;
pub use html_handlebars::HtmlHandlebars;
+use mdbook_core::config::HtmlConfig;
+use mdbook_markdown::HtmlRenderOptions;
+use std::path::Path;
+
+/// Creates an [`HtmlRenderOptions`] from the given config.
+pub fn html_render_options_from_config<'a>(
+ path: &'a Path,
+ config: &HtmlConfig,
+) -> HtmlRenderOptions<'a> {
+ let mut options = HtmlRenderOptions::new(path);
+ options.markdown_options.smart_punctuation = config.smart_punctuation;
+ options
+}
diff --git a/crates/mdbook-markdown/src/lib.rs b/crates/mdbook-markdown/src/lib.rs
index b45d06b0a4..1925d9fa2e 100644
--- a/crates/mdbook-markdown/src/lib.rs
+++ b/crates/mdbook-markdown/src/lib.rs
@@ -11,7 +11,6 @@
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd, html};
use regex::Regex;
-use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Write;
use std::path::Path;
@@ -23,35 +22,55 @@ pub use pulldown_cmark;
#[cfg(test)]
mod tests;
-/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
-pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
- render_markdown_with_path(text, smart_punctuation, None)
+/// Options for parsing markdown.
+#[derive(Default)]
+#[non_exhaustive]
+pub struct MarkdownOptions {
+ /// Enables smart punctuation.
+ ///
+ /// Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and
+ /// `---` to em-dash.
+ pub smart_punctuation: bool,
+}
+
+/// Options for converting markdown to HTML.
+#[non_exhaustive]
+pub struct HtmlRenderOptions<'a> {
+ /// Options for parsing markdown.
+ pub markdown_options: MarkdownOptions,
+ /// The chapter's location, relative to the `SUMMARY.md` file.
+ pub path: &'a Path,
+ /// If true, render for the print page.
+ pub for_print: bool,
+}
+
+impl<'a> HtmlRenderOptions<'a> {
+ /// Creates a new [`HtmlRenderOptions`].
+ pub fn new(path: &'a Path) -> HtmlRenderOptions<'a> {
+ HtmlRenderOptions {
+ markdown_options: MarkdownOptions::default(),
+ path,
+ for_print: false,
+ }
+ }
}
/// Creates a new pulldown-cmark parser of the given text.
-pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
+pub fn new_cmark_parser<'text>(text: &'text str, options: &MarkdownOptions) -> Parser<'text> {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_FOOTNOTES);
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TASKLISTS);
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
- if smart_punctuation {
+ if options.smart_punctuation {
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
}
Parser::new_ext(text, opts)
}
/// Renders markdown to HTML.
-///
-/// `path` should only be set if this is being generated for the consolidated
-/// print page. It should point to the page being rendered relative to the
-/// root of the book.
-pub fn render_markdown_with_path(
- text: &str,
- smart_punctuation: bool,
- path: Option<&Path>,
-) -> String {
+pub fn render_markdown(text: &str, options: &HtmlRenderOptions<'_>) -> String {
let mut body = String::with_capacity(text.len() * 3 / 2);
// Based on
@@ -84,9 +103,9 @@ pub fn render_markdown_with_path(
// to figure out a way to do this just with pure CSS.
let mut prev_was_footnote = false;
- let events = new_cmark_parser(text, smart_punctuation)
+ let events = new_cmark_parser(text, &options.markdown_options)
.map(clean_codeblock_headers)
- .map(|event| adjust_links(event, path))
+ .map(|event| adjust_links(event, options))
.flat_map(|event| {
let (a, b) = wrap_tables(event);
a.into_iter().chain(b)
@@ -98,7 +117,10 @@ pub fn render_markdown_with_path(
Event::Start(Tag::FootnoteDefinition(name)) => {
prev_was_footnote = false;
if !in_footnote.is_empty() {
- log::warn!("internal bug: nested footnote not expected in {path:?}");
+ log::warn!(
+ "internal bug: nested footnote not expected in {:?}",
+ options.path
+ );
}
in_footnote_name = special_escape(&name);
None
@@ -111,7 +133,7 @@ pub fn render_markdown_with_path(
log::warn!(
"footnote `{name}` in {} defined multiple times - \
not updating to new definition",
- path.map_or_else(|| Cow::from(""), |p| p.to_string_lossy())
+ options.path.display()
);
} else {
footnote_defs.insert(name, def_events);
@@ -162,7 +184,7 @@ pub fn render_markdown_with_path(
if !footnote_defs.is_empty() {
add_footnote_defs(
&mut body,
- path,
+ options,
footnote_defs.into_iter().collect(),
&footnote_numbers,
);
@@ -174,7 +196,7 @@ pub fn render_markdown_with_path(
/// Adds all footnote definitions into `body`.
fn add_footnote_defs(
body: &mut String,
- path: Option<&Path>,
+ options: &HtmlRenderOptions<'_>,
mut defs: Vec<(String, Vec>)>,
numbers: &HashMap,
) {
@@ -183,7 +205,7 @@ fn add_footnote_defs(
if !numbers.contains_key(name) {
log::warn!(
"footnote `{name}` in `{}` is defined but not referenced",
- path.map_or_else(|| Cow::from(""), |p| p.to_string_lossy())
+ options.path.display()
);
false
} else {
@@ -270,17 +292,17 @@ fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
/// page go to the original location. Normal page rendering sets `path` to
/// None. Ideally, print page links would link to anchors on the print page,
/// but that is very difficult.
-fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
+fn adjust_links<'a>(event: Event<'a>, options: &HtmlRenderOptions<'_>) -> Event<'a> {
static SCHEME_LINK: LazyLock =
LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
static MD_LINK: LazyLock =
LazyLock::new(|| Regex::new(r"(?P .*)\.md(?P#.*)?").unwrap());
- fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
+ fn fix<'a>(dest: CowStr<'a>, options: &HtmlRenderOptions<'_>) -> CowStr<'a> {
if dest.starts_with('#') {
// Fragment-only link.
- if let Some(path) = path {
- let mut base = path.display().to_string();
+ if options.for_print {
+ let mut base = options.path.display().to_string();
if base.ends_with(".md") {
base.replace_range(base.len() - 3.., ".html");
}
@@ -293,8 +315,9 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
if !SCHEME_LINK.is_match(&dest) {
// This is a relative link, adjust it as necessary.
let mut fixed_link = String::new();
- if let Some(path) = path {
- let base = path
+ if options.for_print {
+ let base = options
+ .path
.parent()
.expect("path can't be empty")
.to_str()
@@ -318,7 +341,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
dest
}
- fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
+ fn fix_html<'a>(html: CowStr<'a>, options: &HtmlRenderOptions<'_>) -> CowStr<'a> {
// This is a terrible hack, but should be reasonably reliable. Nobody
// should ever parse a tag with a regex. However, there isn't anything
// in Rust that I know of that is suitable for handling partial html
@@ -332,7 +355,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
HTML_LINK
.replace_all(&html, |caps: ®ex::Captures<'_>| {
- let fixed = fix(caps[2].into(), path);
+ let fixed = fix(caps[2].into(), options);
format!("{}{}\"", &caps[1], fixed)
})
.into_owned()
@@ -347,7 +370,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
id,
}) => Event::Start(Tag::Link {
link_type,
- dest_url: fix(dest_url, path),
+ dest_url: fix(dest_url, options),
title,
id,
}),
@@ -358,12 +381,12 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
id,
}) => Event::Start(Tag::Image {
link_type,
- dest_url: fix(dest_url, path),
+ dest_url: fix(dest_url, options),
title,
id,
}),
- Event::Html(html) => Event::Html(fix_html(html, path)),
- Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
+ Event::Html(html) => Event::Html(fix_html(html, options)),
+ Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, options)),
_ => event,
}
}
diff --git a/crates/mdbook-markdown/src/tests.rs b/crates/mdbook-markdown/src/tests.rs
index 7f2aea2798..64f3000a1c 100644
--- a/crates/mdbook-markdown/src/tests.rs
+++ b/crates/mdbook-markdown/src/tests.rs
@@ -16,26 +16,28 @@ fn escaped_special() {
#[test]
fn preserves_external_links() {
+ let options = HtmlRenderOptions::new(&Path::new(""));
assert_eq!(
- render_markdown("[example](https://www.rust-lang.org/)", false),
+ render_markdown("[example](https://www.rust-lang.org/)", &options),
"example
\n"
);
}
#[test]
fn it_can_adjust_markdown_links() {
+ let options = HtmlRenderOptions::new(&Path::new(""));
assert_eq!(
- render_markdown("[example](example.md)", false),
+ render_markdown("[example](example.md)", &options),
"example
\n"
);
assert_eq!(
- render_markdown("[example_anchor](example.md#anchor)", false),
+ render_markdown("[example_anchor](example.md#anchor)", &options),
"example_anchor
\n"
);
// this anchor contains 'md' inside of it
assert_eq!(
- render_markdown("[phantom data](foo.html#phantomdata)", false),
+ render_markdown("[phantom data](foo.html#phantomdata)", &options),
"phantom data
\n"
);
}
@@ -53,12 +55,14 @@ fn it_can_wrap_tables() {
"#.trim();
- assert_eq!(render_markdown(src, false), out);
+ let options = HtmlRenderOptions::new(&Path::new(""));
+ assert_eq!(render_markdown(src, &options), out);
}
#[test]
fn it_can_keep_quotes_straight() {
- assert_eq!(render_markdown("'one'", false), "'one'
\n");
+ let options = HtmlRenderOptions::new(&Path::new(""));
+ assert_eq!(render_markdown("'one'", &options), "'one'
\n");
}
#[test]
@@ -74,7 +78,9 @@ fn it_can_make_quotes_curly_except_when_they_are_in_code() {
'three'
‘four’
"#;
- assert_eq!(render_markdown(input, true), expected);
+ let mut options = HtmlRenderOptions::new(&Path::new(""));
+ options.markdown_options.smart_punctuation = true;
+ assert_eq!(render_markdown(input, &options), expected);
}
#[test]
@@ -96,8 +102,10 @@ more text with spaces
more text with spaces
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ let mut options = HtmlRenderOptions::new(&Path::new(""));
+ assert_eq!(render_markdown(input, &options), expected);
+ options.markdown_options.smart_punctuation = true;
+ assert_eq!(render_markdown(input, &options), expected);
}
#[test]
@@ -109,8 +117,10 @@ fn rust_code_block_properties_are_passed_as_space_delimited_class() {
let expected = r#"
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ let mut options = HtmlRenderOptions::new(&Path::new(""));
+ assert_eq!(render_markdown(input, &options), expected);
+ options.markdown_options.smart_punctuation = true;
+ assert_eq!(render_markdown(input, &options), expected);
}
#[test]
@@ -122,8 +132,10 @@ fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_clas
let expected = r#"
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ let mut options = HtmlRenderOptions::new(&Path::new(""));
+ assert_eq!(render_markdown(input, &options), expected);
+ options.markdown_options.smart_punctuation = true;
+ assert_eq!(render_markdown(input, &options), expected);
}
#[test]
@@ -135,13 +147,17 @@ fn rust_code_block_without_properties_has_proper_html_class() {
let expected = r#"
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ let mut options = HtmlRenderOptions::new(&Path::new(""));
+ assert_eq!(render_markdown(input, &options), expected);
+ options.markdown_options.smart_punctuation = true;
+ assert_eq!(render_markdown(input, &options), expected);
let input = r#"
```rust
```
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ let mut options = HtmlRenderOptions::new(&Path::new(""));
+ assert_eq!(render_markdown(input, &options), expected);
+ options.markdown_options.smart_punctuation = true;
+ assert_eq!(render_markdown(input, &options), expected);
}
diff --git a/tests/testsuite/markdown.rs b/tests/testsuite/markdown.rs
index e8366a4801..9e73f2950f 100644
--- a/tests/testsuite/markdown.rs
+++ b/tests/testsuite/markdown.rs
@@ -22,8 +22,8 @@ fn footnotes() {
cmd.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend
-[TIMESTAMP] [WARN] (mdbook_markdown): footnote `multiple-definitions` in defined multiple times - not updating to new definition
-[TIMESTAMP] [WARN] (mdbook_markdown): footnote `unused` in `` is defined but not referenced
+[TIMESTAMP] [WARN] (mdbook_markdown): footnote `multiple-definitions` in footnotes.md defined multiple times - not updating to new definition
+[TIMESTAMP] [WARN] (mdbook_markdown): footnote `unused` in `footnotes.md` is defined but not referenced
[TIMESTAMP] [WARN] (mdbook_markdown): footnote `multiple-definitions` in footnotes.md defined multiple times - not updating to new definition
[TIMESTAMP] [WARN] (mdbook_markdown): footnote `unused` in `footnotes.md` is defined but not referenced
[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book`