11
11
12
12
use pulldown_cmark:: { CodeBlockKind , CowStr , Event , Options , Parser , Tag , TagEnd , html} ;
13
13
use regex:: Regex ;
14
- use std:: borrow:: Cow ;
15
14
use std:: collections:: HashMap ;
16
15
use std:: fmt:: Write ;
17
16
use std:: path:: Path ;
@@ -23,35 +22,55 @@ pub use pulldown_cmark;
23
22
#[ cfg( test) ]
24
23
mod tests;
25
24
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
+ }
29
56
}
30
57
31
58
/// 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 > {
33
60
let mut opts = Options :: empty ( ) ;
34
61
opts. insert ( Options :: ENABLE_TABLES ) ;
35
62
opts. insert ( Options :: ENABLE_FOOTNOTES ) ;
36
63
opts. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
37
64
opts. insert ( Options :: ENABLE_TASKLISTS ) ;
38
65
opts. insert ( Options :: ENABLE_HEADING_ATTRIBUTES ) ;
39
- if smart_punctuation {
66
+ if options . smart_punctuation {
40
67
opts. insert ( Options :: ENABLE_SMART_PUNCTUATION ) ;
41
68
}
42
69
Parser :: new_ext ( text, opts)
43
70
}
44
71
45
72
/// 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 {
55
74
let mut body = String :: with_capacity ( text. len ( ) * 3 / 2 ) ;
56
75
57
76
// Based on
@@ -84,9 +103,9 @@ pub fn render_markdown_with_path(
84
103
// to figure out a way to do this just with pure CSS.
85
104
let mut prev_was_footnote = false ;
86
105
87
- let events = new_cmark_parser ( text, smart_punctuation )
106
+ let events = new_cmark_parser ( text, & options . markdown_options )
88
107
. map ( clean_codeblock_headers)
89
- . map ( |event| adjust_links ( event, path ) )
108
+ . map ( |event| adjust_links ( event, options ) )
90
109
. flat_map ( |event| {
91
110
let ( a, b) = wrap_tables ( event) ;
92
111
a. into_iter ( ) . chain ( b)
@@ -98,7 +117,10 @@ pub fn render_markdown_with_path(
98
117
Event :: Start ( Tag :: FootnoteDefinition ( name) ) => {
99
118
prev_was_footnote = false ;
100
119
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
+ ) ;
102
124
}
103
125
in_footnote_name = special_escape ( & name) ;
104
126
None
@@ -111,7 +133,7 @@ pub fn render_markdown_with_path(
111
133
log:: warn!(
112
134
"footnote `{name}` in {} defined multiple times - \
113
135
not updating to new definition",
114
- path. map_or_else ( || Cow :: from ( "<unknown>" ) , |p| p . to_string_lossy ( ) )
136
+ options . path. display ( )
115
137
) ;
116
138
} else {
117
139
footnote_defs. insert ( name, def_events) ;
@@ -162,7 +184,7 @@ pub fn render_markdown_with_path(
162
184
if !footnote_defs. is_empty ( ) {
163
185
add_footnote_defs (
164
186
& mut body,
165
- path ,
187
+ options ,
166
188
footnote_defs. into_iter ( ) . collect ( ) ,
167
189
& footnote_numbers,
168
190
) ;
@@ -174,7 +196,7 @@ pub fn render_markdown_with_path(
174
196
/// Adds all footnote definitions into `body`.
175
197
fn add_footnote_defs (
176
198
body : & mut String ,
177
- path : Option < & Path > ,
199
+ options : & HtmlRenderOptions < ' _ > ,
178
200
mut defs : Vec < ( String , Vec < Event < ' _ > > ) > ,
179
201
numbers : & HashMap < String , ( usize , u32 ) > ,
180
202
) {
@@ -183,7 +205,7 @@ fn add_footnote_defs(
183
205
if !numbers. contains_key ( name) {
184
206
log:: warn!(
185
207
"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 ( )
187
209
) ;
188
210
false
189
211
} else {
@@ -270,17 +292,17 @@ fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
270
292
/// page go to the original location. Normal page rendering sets `path` to
271
293
/// None. Ideally, print page links would link to anchors on the print page,
272
294
/// 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 > {
274
296
static SCHEME_LINK : LazyLock < Regex > =
275
297
LazyLock :: new ( || Regex :: new ( r"^[a-z][a-z0-9+.-]*:" ) . unwrap ( ) ) ;
276
298
static MD_LINK : LazyLock < Regex > =
277
299
LazyLock :: new ( || Regex :: new ( r"(?P<link>.*)\.md(?P<anchor>#.*)?" ) . unwrap ( ) ) ;
278
300
279
- fn fix < ' a > ( dest : CowStr < ' a > , path : Option < & Path > ) -> CowStr < ' a > {
301
+ fn fix < ' a > ( dest : CowStr < ' a > , options : & HtmlRenderOptions < ' _ > ) -> CowStr < ' a > {
280
302
if dest. starts_with ( '#' ) {
281
303
// 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 ( ) ;
284
306
if base. ends_with ( ".md" ) {
285
307
base. replace_range ( base. len ( ) - 3 .., ".html" ) ;
286
308
}
@@ -293,8 +315,9 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
293
315
if !SCHEME_LINK . is_match ( & dest) {
294
316
// This is a relative link, adjust it as necessary.
295
317
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
298
321
. parent ( )
299
322
. expect ( "path can't be empty" )
300
323
. to_str ( )
@@ -318,7 +341,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
318
341
dest
319
342
}
320
343
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 > {
322
345
// This is a terrible hack, but should be reasonably reliable. Nobody
323
346
// should ever parse a tag with a regex. However, there isn't anything
324
347
// 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> {
332
355
333
356
HTML_LINK
334
357
. replace_all ( & html, |caps : & regex:: Captures < ' _ > | {
335
- let fixed = fix ( caps[ 2 ] . into ( ) , path ) ;
358
+ let fixed = fix ( caps[ 2 ] . into ( ) , options ) ;
336
359
format ! ( "{}{}\" " , & caps[ 1 ] , fixed)
337
360
} )
338
361
. into_owned ( )
@@ -347,7 +370,7 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
347
370
id,
348
371
} ) => Event :: Start ( Tag :: Link {
349
372
link_type,
350
- dest_url : fix ( dest_url, path ) ,
373
+ dest_url : fix ( dest_url, options ) ,
351
374
title,
352
375
id,
353
376
} ) ,
@@ -358,12 +381,12 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
358
381
id,
359
382
} ) => Event :: Start ( Tag :: Image {
360
383
link_type,
361
- dest_url : fix ( dest_url, path ) ,
384
+ dest_url : fix ( dest_url, options ) ,
362
385
title,
363
386
id,
364
387
} ) ,
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 ) ) ,
367
390
_ => event,
368
391
}
369
392
}
0 commit comments