@@ -212,18 +212,156 @@ pub fn render_markdown_with_path(
212
212
smart_punctuation : bool ,
213
213
path : Option < & Path > ,
214
214
) -> String {
215
- let mut s = String :: with_capacity ( text. len ( ) * 3 / 2 ) ;
216
- let p = new_cmark_parser ( text, smart_punctuation) ;
217
- let events = p
215
+ let mut body = String :: with_capacity ( text. len ( ) * 3 / 2 ) ;
216
+
217
+ // Based on
218
+ // https://github.com/pulldown-cmark/pulldown-cmark/blob/master/pulldown-cmark/examples/footnote-rewrite.rs
219
+
220
+ // This handling of footnotes is a two-pass process. This is done to
221
+ // support linkbacks, little arrows that allow you to jump back to the
222
+ // footnote reference. The first pass collects the footnote definitions.
223
+ // The second pass modifies those definitions to include the linkbacks,
224
+ // and inserts the definitions back into the `events` list.
225
+
226
+ // This is a map of name -> (number, count)
227
+ // `name` is the name of the footnote.
228
+ // `number` is the footnote number displayed in the output.
229
+ // `count` is the number of references to this footnote (used for multiple
230
+ // linkbacks, and checking for unused footnotes).
231
+ let mut footnote_numbers = HashMap :: new ( ) ;
232
+ // This is a list of (name, Vec<Event>)
233
+ // `name` is the name of the footnote.
234
+ // The events list is the list of events needed to build the footnote definition.
235
+ let mut footnote_defs = Vec :: new ( ) ;
236
+
237
+ // The following are used when currently processing a footnote definition.
238
+ //
239
+ // This is the name of the footnote (escaped).
240
+ let mut in_footnote_name = String :: new ( ) ;
241
+ // This is the list of events to build the footnote definition.
242
+ let mut in_footnote = Vec :: new ( ) ;
243
+
244
+ let events = new_cmark_parser ( text, smart_punctuation)
218
245
. map ( clean_codeblock_headers)
219
246
. map ( |event| adjust_links ( event, path) )
220
247
. flat_map ( |event| {
221
248
let ( a, b) = wrap_tables ( event) ;
222
249
a. into_iter ( ) . chain ( b)
250
+ } )
251
+ // Footnote rewriting must go last to ensure inner definition contents
252
+ // are processed (since they get pulled out of the initial stream).
253
+ . filter_map ( |event| {
254
+ match event {
255
+ Event :: Start ( Tag :: FootnoteDefinition ( name) ) => {
256
+ if !in_footnote. is_empty ( ) {
257
+ log:: warn!( "internal bug: nested footnote not expected in {path:?}" ) ;
258
+ }
259
+ in_footnote_name = special_escape ( & name) ;
260
+ None
261
+ }
262
+ Event :: End ( TagEnd :: FootnoteDefinition ) => {
263
+ let def_events = std:: mem:: take ( & mut in_footnote) ;
264
+ let name = std:: mem:: take ( & mut in_footnote_name) ;
265
+ footnote_defs. push ( ( name, def_events) ) ;
266
+ None
267
+ }
268
+ Event :: FootnoteReference ( name) => {
269
+ let name = special_escape ( & name) ;
270
+ let len = footnote_numbers. len ( ) + 1 ;
271
+ let ( n, count) = footnote_numbers. entry ( name. clone ( ) ) . or_insert ( ( len, 0 ) ) ;
272
+ * count += 1 ;
273
+ let html = Event :: Html (
274
+ format ! (
275
+ "<sup class=\" footnote-reference\" id=\" fr-{name}-{count}\" >\
276
+ <a href=\" #footnote-{name}\" >{n}</a>\
277
+ </sup>"
278
+ )
279
+ . into ( ) ,
280
+ ) ;
281
+ if in_footnote_name. is_empty ( ) {
282
+ Some ( html)
283
+ } else {
284
+ // While inside a footnote, we need to accumulate.
285
+ in_footnote. push ( html) ;
286
+ None
287
+ }
288
+ }
289
+ // While inside a footnote, accumulate all events into a local.
290
+ _ if !in_footnote_name. is_empty ( ) => {
291
+ in_footnote. push ( event) ;
292
+ None
293
+ }
294
+ _ => Some ( event) ,
295
+ }
223
296
} ) ;
224
297
225
- html:: push_html ( & mut s, events) ;
226
- s
298
+ html:: push_html ( & mut body, events) ;
299
+
300
+ if !footnote_defs. is_empty ( ) {
301
+ add_footnote_defs ( & mut body, path, footnote_defs, & footnote_numbers) ;
302
+ }
303
+
304
+ body
305
+ }
306
+
307
+ /// Adds all footnote definitions into `body`.
308
+ fn add_footnote_defs (
309
+ body : & mut String ,
310
+ path : Option < & Path > ,
311
+ mut defs : Vec < ( String , Vec < Event < ' _ > > ) > ,
312
+ numbers : & HashMap < String , ( usize , u32 ) > ,
313
+ ) {
314
+ // Remove unused.
315
+ defs. retain ( |( name, _) | {
316
+ if !numbers. contains_key ( name) {
317
+ log:: warn!(
318
+ "footnote `{name}` in `{}` is defined but not referenced" ,
319
+ path. map_or_else( || Cow :: from( "<unknown>" ) , |p| p. to_string_lossy( ) )
320
+ ) ;
321
+ false
322
+ } else {
323
+ true
324
+ }
325
+ } ) ;
326
+
327
+ defs. sort_by_cached_key ( |( name, _) | numbers[ name] . 0 ) ;
328
+
329
+ body. push_str (
330
+ "<hr>\n \
331
+ <ol class=\" footnote-definition\" >",
332
+ ) ;
333
+
334
+ // Insert the backrefs to the definition, and put the definitions in the output.
335
+ for ( name, mut fn_events) in defs {
336
+ let count = numbers[ & name] . 1 ;
337
+ fn_events. insert (
338
+ 0 ,
339
+ Event :: Html ( format ! ( "<li id=\" footnote-{name}\" >" ) . into ( ) ) ,
340
+ ) ;
341
+ // Generate the linkbacks.
342
+ for usage in 1 ..=count {
343
+ let nth = if usage == 1 {
344
+ String :: new ( )
345
+ } else {
346
+ usage. to_string ( )
347
+ } ;
348
+ let backlink =
349
+ Event :: Html ( format ! ( " <a href=\" #fr-{name}-{usage}\" >↩{nth}</a>" ) . into ( ) ) ;
350
+ if matches ! ( fn_events. last( ) , Some ( Event :: End ( TagEnd :: Paragraph ) ) ) {
351
+ // Put the linkback at the end of the last paragraph instead
352
+ // of on a line by itself.
353
+ fn_events. insert ( fn_events. len ( ) - 1 , backlink) ;
354
+ } else {
355
+ // Not a clear place to put it in this circumstance, so put it
356
+ // at the end.
357
+ fn_events. push ( backlink) ;
358
+ }
359
+ }
360
+ fn_events. push ( Event :: Html ( "</li>\n " . into ( ) ) ) ;
361
+ html:: push_html ( body, fn_events. into_iter ( ) ) ;
362
+ }
363
+
364
+ body. push_str ( "</ol>" ) ;
227
365
}
228
366
229
367
/// Wraps tables in a `.table-wrapper` class to apply overflow-x rules to.
@@ -267,13 +405,14 @@ pub fn log_backtrace(e: &Error) {
267
405
268
406
pub ( crate ) fn special_escape ( mut s : & str ) -> String {
269
407
let mut escaped = String :: with_capacity ( s. len ( ) ) ;
270
- let needs_escape: & [ char ] = & [ '<' , '>' , '\'' , '\\' , '&' ] ;
408
+ let needs_escape: & [ char ] = & [ '<' , '>' , '\'' , '"' , ' \\', '&' ] ;
271
409
while let Some ( next) = s. find ( needs_escape) {
272
410
escaped. push_str ( & s[ ..next] ) ;
273
411
match s. as_bytes ( ) [ next] {
274
412
b'<' => escaped. push_str ( "<" ) ,
275
413
b'>' => escaped. push_str ( ">" ) ,
276
414
b'\'' => escaped. push_str ( "'" ) ,
415
+ b'"' => escaped. push_str ( """ ) ,
277
416
b'\\' => escaped. push_str ( "\" ) ,
278
417
b'&' => escaped. push_str ( "&" ) ,
279
418
_ => unreachable ! ( ) ,
0 commit comments