Skip to content

Commit 6be8e52

Browse files
authored
Merge pull request #2809 from ehuss/markdown-options
Introduce options struct for markdown rendering
2 parents 0722d81 + f401275 commit 6be8e52

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)