Skip to content

Commit f401275

Browse files
committed
Introduce options struct for markdown rendering
This adds `MarkdownOptions` for creating the pulldown-cmark parser, and `HtmlRenderOptions` for converting markdown to HTML. These types should help make it easier to extend the rendering options while remaining semver compatible. It should also help with just general ergonomics of using these functions.
1 parent 0722d81 commit f401275

File tree

6 files changed

+115
-60
lines changed

6 files changed

+115
-60
lines changed

crates/mdbook-html/src/html_handlebars/hbs_renderer.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use mdbook_core::book::{Book, BookItem, Chapter};
88
use mdbook_core::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
99
use mdbook_core::utils;
1010
use mdbook_core::utils::fs::get_404_output_file;
11-
use mdbook_markdown::{render_markdown, render_markdown_with_path};
11+
use mdbook_markdown::render_markdown;
1212
use mdbook_renderer::{RenderContext, Renderer};
1313
use regex::{Captures, Regex};
1414
use serde_json::json;
@@ -56,10 +56,10 @@ impl HtmlHandlebars {
5656
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
5757
}
5858

59-
let content = render_markdown(&ch.content, ctx.html_config.smart_punctuation);
60-
61-
let fixed_content =
62-
render_markdown_with_path(&ch.content, ctx.html_config.smart_punctuation, Some(path));
59+
let mut options = crate::html_render_options_from_config(path, &ctx.html_config);
60+
let content = render_markdown(&ch.content, &options);
61+
options.for_print = true;
62+
let fixed_content = render_markdown(&ch.content, &options);
6363
if prev_ch.is_some() && ctx.html_config.print.page_break {
6464
// Add page break between chapters
6565
// 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 {
193193
.to_string()
194194
}
195195
};
196-
let html_content_404 = render_markdown(&content_404, html_config.smart_punctuation);
196+
let options = crate::html_render_options_from_config(Path::new("404.md"), html_config);
197+
let html_content_404 = render_markdown(&content_404, &options);
197198

198199
let mut data_404 = data.clone();
199200
let base_url = if let Some(site_url) = &html_config.site_url {

crates/mdbook-html/src/html_handlebars/search.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use log::{debug, warn};
66
use mdbook_core::book::{Book, BookItem, Chapter};
77
use mdbook_core::config::{Search, SearchChapterSettings};
88
use mdbook_core::utils;
9+
use mdbook_markdown::HtmlRenderOptions;
910
use mdbook_markdown::new_cmark_parser;
1011
use pulldown_cmark::*;
1112
use serde::Serialize;
@@ -133,7 +134,8 @@ fn render_item(
133134
.with_context(|| "Could not convert HTML path to str")?;
134135
let anchor_base = utils::fs::normalize_path(filepath);
135136

136-
let mut p = new_cmark_parser(&chapter.content, false).peekable();
137+
let options = HtmlRenderOptions::new(&chapter_path);
138+
let mut p = new_cmark_parser(&chapter.content, &options.markdown_options).peekable();
137139

138140
let mut in_heading = false;
139141
let max_section_depth = u32::from(search_config.heading_split_level);

crates/mdbook-html/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,16 @@ mod html_handlebars;
44
pub mod theme;
55

66
pub use html_handlebars::HtmlHandlebars;
7+
use mdbook_core::config::HtmlConfig;
8+
use mdbook_markdown::HtmlRenderOptions;
9+
use std::path::Path;
10+
11+
/// Creates an [`HtmlRenderOptions`] from the given config.
12+
pub fn html_render_options_from_config<'a>(
13+
path: &'a Path,
14+
config: &HtmlConfig,
15+
) -> HtmlRenderOptions<'a> {
16+
let mut options = HtmlRenderOptions::new(path);
17+
options.markdown_options.smart_punctuation = config.smart_punctuation;
18+
options
19+
}

crates/mdbook-markdown/src/lib.rs

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
1212
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd, html};
1313
use regex::Regex;
14-
use std::borrow::Cow;
1514
use std::collections::HashMap;
1615
use std::fmt::Write;
1716
use std::path::Path;
@@ -23,35 +22,55 @@ pub use pulldown_cmark;
2322
#[cfg(test)]
2423
mod tests;
2524

26-
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
27-
pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
28-
render_markdown_with_path(text, smart_punctuation, None)
25+
/// Options for parsing markdown.
26+
#[derive(Default)]
27+
#[non_exhaustive]
28+
pub struct MarkdownOptions {
29+
/// Enables smart punctuation.
30+
///
31+
/// Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and
32+
/// `---` to em-dash.
33+
pub smart_punctuation: bool,
34+
}
35+
36+
/// Options for converting markdown to HTML.
37+
#[non_exhaustive]
38+
pub struct HtmlRenderOptions<'a> {
39+
/// Options for parsing markdown.
40+
pub markdown_options: MarkdownOptions,
41+
/// The chapter's location, relative to the `SUMMARY.md` file.
42+
pub path: &'a Path,
43+
/// If true, render for the print page.
44+
pub for_print: bool,
45+
}
46+
47+
impl<'a> HtmlRenderOptions<'a> {
48+
/// Creates a new [`HtmlRenderOptions`].
49+
pub fn new(path: &'a Path) -> HtmlRenderOptions<'a> {
50+
HtmlRenderOptions {
51+
markdown_options: MarkdownOptions::default(),
52+
path,
53+
for_print: false,
54+
}
55+
}
2956
}
3057

3158
/// Creates a new pulldown-cmark parser of the given text.
32-
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
59+
pub fn new_cmark_parser<'text>(text: &'text str, options: &MarkdownOptions) -> Parser<'text> {
3360
let mut opts = Options::empty();
3461
opts.insert(Options::ENABLE_TABLES);
3562
opts.insert(Options::ENABLE_FOOTNOTES);
3663
opts.insert(Options::ENABLE_STRIKETHROUGH);
3764
opts.insert(Options::ENABLE_TASKLISTS);
3865
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
39-
if smart_punctuation {
66+
if options.smart_punctuation {
4067
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
4168
}
4269
Parser::new_ext(text, opts)
4370
}
4471

4572
/// Renders markdown to HTML.
46-
///
47-
/// `path` should only be set if this is being generated for the consolidated
48-
/// print page. It should point to the page being rendered relative to the
49-
/// root of the book.
50-
pub fn render_markdown_with_path(
51-
text: &str,
52-
smart_punctuation: bool,
53-
path: Option<&Path>,
54-
) -> String {
73+
pub fn render_markdown(text: &str, options: &HtmlRenderOptions<'_>) -> String {
5574
let mut body = String::with_capacity(text.len() * 3 / 2);
5675

5776
// Based on
@@ -84,9 +103,9 @@ pub fn render_markdown_with_path(
84103
// to figure out a way to do this just with pure CSS.
85104
let mut prev_was_footnote = false;
86105

87-
let events = new_cmark_parser(text, smart_punctuation)
106+
let events = new_cmark_parser(text, &options.markdown_options)
88107
.map(clean_codeblock_headers)
89-
.map(|event| adjust_links(event, path))
108+
.map(|event| adjust_links(event, options))
90109
.flat_map(|event| {
91110
let (a, b) = wrap_tables(event);
92111
a.into_iter().chain(b)
@@ -98,7 +117,10 @@ pub fn render_markdown_with_path(
98117
Event::Start(Tag::FootnoteDefinition(name)) => {
99118
prev_was_footnote = false;
100119
if !in_footnote.is_empty() {
101-
log::warn!("internal bug: nested footnote not expected in {path:?}");
120+
log::warn!(
121+
"internal bug: nested footnote not expected in {:?}",
122+
options.path
123+
);
102124
}
103125
in_footnote_name = special_escape(&name);
104126
None
@@ -111,7 +133,7 @@ pub fn render_markdown_with_path(
111133
log::warn!(
112134
"footnote `{name}` in {} defined multiple times - \
113135
not updating to new definition",
114-
path.map_or_else(|| Cow::from("<unknown>"), |p| p.to_string_lossy())
136+
options.path.display()
115137
);
116138
} else {
117139
footnote_defs.insert(name, def_events);
@@ -162,7 +184,7 @@ pub fn render_markdown_with_path(
162184
if !footnote_defs.is_empty() {
163185
add_footnote_defs(
164186
&mut body,
165-
path,
187+
options,
166188
footnote_defs.into_iter().collect(),
167189
&footnote_numbers,
168190
);
@@ -174,7 +196,7 @@ pub fn render_markdown_with_path(
174196
/// Adds all footnote definitions into `body`.
175197
fn add_footnote_defs(
176198
body: &mut String,
177-
path: Option<&Path>,
199+
options: &HtmlRenderOptions<'_>,
178200
mut defs: Vec<(String, Vec<Event<'_>>)>,
179201
numbers: &HashMap<String, (usize, u32)>,
180202
) {
@@ -183,7 +205,7 @@ fn add_footnote_defs(
183205
if !numbers.contains_key(name) {
184206
log::warn!(
185207
"footnote `{name}` in `{}` is defined but not referenced",
186-
path.map_or_else(|| Cow::from("<unknown>"), |p| p.to_string_lossy())
208+
options.path.display()
187209
);
188210
false
189211
} else {
@@ -270,17 +292,17 @@ fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
270292
/// page go to the original location. Normal page rendering sets `path` to
271293
/// None. Ideally, print page links would link to anchors on the print page,
272294
/// but that is very difficult.
273-
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
295+
fn adjust_links<'a>(event: Event<'a>, options: &HtmlRenderOptions<'_>) -> Event<'a> {
274296
static SCHEME_LINK: LazyLock<Regex> =
275297
LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
276298
static MD_LINK: LazyLock<Regex> =
277299
LazyLock::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
278300

279-
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
301+
fn fix<'a>(dest: CowStr<'a>, options: &HtmlRenderOptions<'_>) -> CowStr<'a> {
280302
if dest.starts_with('#') {
281303
// Fragment-only link.
282-
if let Some(path) = path {
283-
let mut base = path.display().to_string();
304+
if options.for_print {
305+
let mut base = options.path.display().to_string();
284306
if base.ends_with(".md") {
285307
base.replace_range(base.len() - 3.., ".html");
286308
}
@@ -293,8 +315,9 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
293315
if !SCHEME_LINK.is_match(&dest) {
294316
// This is a relative link, adjust it as necessary.
295317
let mut fixed_link = String::new();
296-
if let Some(path) = path {
297-
let base = path
318+
if options.for_print {
319+
let base = options
320+
.path
298321
.parent()
299322
.expect("path can't be empty")
300323
.to_str()
@@ -318,7 +341,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
318341
dest
319342
}
320343

321-
fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
344+
fn fix_html<'a>(html: CowStr<'a>, options: &HtmlRenderOptions<'_>) -> CowStr<'a> {
322345
// This is a terrible hack, but should be reasonably reliable. Nobody
323346
// should ever parse a tag with a regex. However, there isn't anything
324347
// 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> {
332355

333356
HTML_LINK
334357
.replace_all(&html, |caps: &regex::Captures<'_>| {
335-
let fixed = fix(caps[2].into(), path);
358+
let fixed = fix(caps[2].into(), options);
336359
format!("{}{}\"", &caps[1], fixed)
337360
})
338361
.into_owned()
@@ -347,7 +370,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
347370
id,
348371
}) => Event::Start(Tag::Link {
349372
link_type,
350-
dest_url: fix(dest_url, path),
373+
dest_url: fix(dest_url, options),
351374
title,
352375
id,
353376
}),
@@ -358,12 +381,12 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
358381
id,
359382
}) => Event::Start(Tag::Image {
360383
link_type,
361-
dest_url: fix(dest_url, path),
384+
dest_url: fix(dest_url, options),
362385
title,
363386
id,
364387
}),
365-
Event::Html(html) => Event::Html(fix_html(html, path)),
366-
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
388+
Event::Html(html) => Event::Html(fix_html(html, options)),
389+
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, options)),
367390
_ => event,
368391
}
369392
}

0 commit comments

Comments
 (0)