1
+ use std:: collections:: HashMap ;
1
2
use std:: fmt:: Write ;
2
3
4
+ use crate :: markdown:: cmark:: CowStr ;
3
5
use errors:: bail;
4
6
use libs:: gh_emoji:: Replacer as EmojiReplacer ;
5
7
use libs:: once_cell:: sync:: Lazy ;
@@ -239,6 +241,158 @@ fn get_heading_refs(events: &[Event]) -> Vec<HeadingRef> {
239
241
heading_refs
240
242
}
241
243
244
+ fn convert_footnotes_to_github_style ( old_events : & mut Vec < Event > ) {
245
+ let events = std:: mem:: take ( old_events) ;
246
+ // step 1: We need to extract footnotes from the event stream and tweak footnote references
247
+
248
+ // footnotes bodies are stored in a stack of vectors, because it is possible to have footnotes
249
+ // inside footnotes
250
+ let mut footnote_bodies_stack = Vec :: new ( ) ;
251
+ let mut footnotes = Vec :: new ( ) ;
252
+ // this will allow to create a multiple back references
253
+ let mut footnote_numbers = HashMap :: new ( ) ;
254
+ let filtered_events = events. into_iter ( ) . filter_map ( |event| {
255
+ match event {
256
+ // New footnote definition is pushed to the stack
257
+ Event :: Start ( Tag :: FootnoteDefinition ( _) ) => {
258
+ footnote_bodies_stack. push ( vec ! [ event] ) ;
259
+ None
260
+ }
261
+ // The topmost footnote definition is popped from the stack
262
+ Event :: End ( TagEnd :: FootnoteDefinition ) => {
263
+ // unwrap will never fail, because Tag::FootnoteDefinition always comes before
264
+ // TagEnd::FootnoteDefinition
265
+ let mut footnote_body = footnote_bodies_stack. pop ( ) . unwrap ( ) ;
266
+ footnote_body. push ( event) ;
267
+ footnotes. push ( footnote_body) ;
268
+ None
269
+ }
270
+ Event :: FootnoteReference ( name) => {
271
+ // n will be a unique index of the footnote
272
+ let n = footnote_numbers. len ( ) + 1 ;
273
+ // nr is a number of references to this footnote
274
+ let ( n, nr) = footnote_numbers. entry ( name. clone ( ) ) . or_insert ( ( n, 0usize ) ) ;
275
+ * nr += 1 ;
276
+ let reference = Event :: Html ( format ! ( r##"<sup class="footnote-reference" id="fr-{name}-{nr}"><a href="#fn-{name}">[{n}]</a></sup>"## ) . into ( ) ) ;
277
+
278
+ if footnote_bodies_stack. is_empty ( ) {
279
+ // we are in the main text, just output the reference
280
+ Some ( reference)
281
+ } else {
282
+ // we are inside other footnote, we have to push that reference into that
283
+ // footnote
284
+ footnote_bodies_stack. last_mut ( ) . unwrap ( ) . push ( reference) ;
285
+ None
286
+ }
287
+ }
288
+ _ if !footnote_bodies_stack. is_empty ( ) => {
289
+ footnote_bodies_stack. last_mut ( ) . unwrap ( ) . push ( event) ;
290
+ None
291
+ }
292
+ _ => Some ( event) ,
293
+ }
294
+ }
295
+ ) ;
296
+
297
+ old_events. extend ( filtered_events) ;
298
+
299
+ if footnotes. is_empty ( ) {
300
+ return ;
301
+ }
302
+
303
+ old_events. push ( Event :: Html ( "<hr><ol class=\" footnotes-list\" >\n " . into ( ) ) ) ;
304
+
305
+ // Step 2: retain only footnotes which was actually referenced
306
+ footnotes. retain ( |f| match f. first ( ) {
307
+ Some ( Event :: Start ( Tag :: FootnoteDefinition ( name) ) ) => {
308
+ footnote_numbers. get ( name) . unwrap_or ( & ( 0 , 0 ) ) . 1 != 0
309
+ }
310
+ _ => false ,
311
+ } ) ;
312
+
313
+ // Step 3: Sort footnotes in the order of their appearance
314
+ footnotes. sort_by_cached_key ( |f| match f. first ( ) {
315
+ Some ( Event :: Start ( Tag :: FootnoteDefinition ( name) ) ) => {
316
+ footnote_numbers. get ( name) . unwrap_or ( & ( 0 , 0 ) ) . 0
317
+ }
318
+ _ => unreachable ! ( ) ,
319
+ } ) ;
320
+
321
+ // Step 4: Add backreferences to footnotes
322
+ let footnotes = footnotes. into_iter ( ) . flat_map ( |fl| {
323
+ // To write backrefs, the name needs kept until the end of the footnote definition.
324
+ let mut name = CowStr :: from ( "" ) ;
325
+ // Backrefs are included in the final paragraph of the footnote, if it's normal text.
326
+ // For example, this DOM can be produced:
327
+ //
328
+ // Markdown:
329
+ //
330
+ // five [^feet].
331
+ //
332
+ // [^feet]:
333
+ // A foot is defined, in this case, as 0.3048 m.
334
+ //
335
+ // Historically, the foot has not been defined this way, corresponding to many
336
+ // subtly different units depending on the location.
337
+ //
338
+ // HTML:
339
+ //
340
+ // <p>five <sup class="footnote-reference" id="fr-feet-1"><a href="#fn-feet">[1]</a></sup>.</p>
341
+ //
342
+ // <ol class="footnotes-list">
343
+ // <li id="fn-feet">
344
+ // <p>A foot is defined, in this case, as 0.3048 m.</p>
345
+ // <p>Historically, the foot has not been defined this way, corresponding to many
346
+ // subtly different units depending on the location. <a href="#fr-feet-1">↩</a></p>
347
+ // </li>
348
+ // </ol>
349
+ //
350
+ // This is mostly a visual hack, so that footnotes use less vertical space.
351
+ //
352
+ // If there is no final paragraph, such as a tabular, list, or image footnote, it gets
353
+ // pushed after the last tag instead.
354
+ let mut has_written_backrefs = false ;
355
+ let fl_len = fl. len ( ) ;
356
+ let footnote_numbers = & footnote_numbers;
357
+ fl. into_iter ( ) . enumerate ( ) . map ( move |( i, f) | match f {
358
+ Event :: Start ( Tag :: FootnoteDefinition ( current_name) ) => {
359
+ name = current_name;
360
+ has_written_backrefs = false ;
361
+ Event :: Html ( format ! ( r##"<li id="fn-{name}">"## ) . into ( ) )
362
+ }
363
+ Event :: End ( TagEnd :: FootnoteDefinition ) | Event :: End ( TagEnd :: Paragraph )
364
+ if !has_written_backrefs && i >= fl_len - 2 =>
365
+ {
366
+ let usage_count = footnote_numbers. get ( & name) . unwrap ( ) . 1 ;
367
+ let mut end = String :: with_capacity (
368
+ name. len ( ) + ( r##" <a href="#fr--1">↩</a></li>"## . len ( ) * usage_count) ,
369
+ ) ;
370
+ for usage in 1 ..=usage_count {
371
+ if usage == 1 {
372
+ write ! ( & mut end, r##" <a href="#fr-{name}-{usage}">↩</a>"## ) . unwrap ( ) ;
373
+ } else {
374
+ write ! ( & mut end, r##" <a href="#fr-{name}-{usage}">↩{usage}</a>"## )
375
+ . unwrap ( ) ;
376
+ }
377
+ }
378
+ has_written_backrefs = true ;
379
+ if f == Event :: End ( TagEnd :: FootnoteDefinition ) {
380
+ end. push_str ( "</li>\n " ) ;
381
+ } else {
382
+ end. push_str ( "</p>\n " ) ;
383
+ }
384
+ Event :: Html ( end. into ( ) )
385
+ }
386
+ Event :: End ( TagEnd :: FootnoteDefinition ) => Event :: Html ( "</li>\n " . into ( ) ) ,
387
+ Event :: FootnoteReference ( _) => unreachable ! ( "converted to HTML earlier" ) ,
388
+ f => f,
389
+ } )
390
+ } ) ;
391
+
392
+ old_events. extend ( footnotes) ;
393
+ old_events. push ( Event :: Html ( "</ol>\n " . into ( ) ) ) ;
394
+ }
395
+
242
396
pub fn markdown_to_html (
243
397
content : & str ,
244
398
context : & RenderContext ,
@@ -623,6 +777,10 @@ pub fn markdown_to_html(
623
777
insert_many ( & mut events, anchors_to_insert) ;
624
778
}
625
779
780
+ if context. config . markdown . bottom_footnotes {
781
+ convert_footnotes_to_github_style ( & mut events) ;
782
+ }
783
+
626
784
cmark:: html:: push_html ( & mut html, events. into_iter ( ) ) ;
627
785
}
628
786
@@ -641,11 +799,11 @@ pub fn markdown_to_html(
641
799
642
800
#[ cfg( test) ]
643
801
mod tests {
802
+ use super :: * ;
644
803
use config:: Config ;
804
+ use insta:: assert_snapshot;
645
805
646
- use super :: * ;
647
806
#[ test]
648
-
649
807
fn insert_many_works ( ) {
650
808
let mut v = vec ! [ 1 , 2 , 3 , 4 , 5 ] ;
651
809
insert_many ( & mut v, vec ! [ ( 0 , 0 ) , ( 2 , -1 ) , ( 5 , 6 ) ] ) ;
@@ -714,4 +872,106 @@ mod tests {
714
872
assert_eq ! ( body, & bottom_rendered) ;
715
873
}
716
874
}
875
+
876
+ #[ test]
877
+ fn no_footnotes ( ) {
878
+ let mut opts = Options :: empty ( ) ;
879
+ opts. insert ( Options :: ENABLE_TABLES ) ;
880
+ opts. insert ( Options :: ENABLE_FOOTNOTES ) ;
881
+ opts. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
882
+ opts. insert ( Options :: ENABLE_TASKLISTS ) ;
883
+ opts. insert ( Options :: ENABLE_HEADING_ATTRIBUTES ) ;
884
+
885
+ let content = "Some text *without* footnotes.\n \n Only ~~fancy~~ formatting." ;
886
+ let mut events: Vec < _ > = Parser :: new_ext ( & content, opts) . collect ( ) ;
887
+ convert_footnotes_to_github_style ( & mut events) ;
888
+ let mut html = String :: new ( ) ;
889
+ cmark:: html:: push_html ( & mut html, events. into_iter ( ) ) ;
890
+ assert_snapshot ! ( html) ;
891
+ }
892
+
893
+ #[ test]
894
+ fn single_footnote ( ) {
895
+ let mut opts = Options :: empty ( ) ;
896
+ opts. insert ( Options :: ENABLE_TABLES ) ;
897
+ opts. insert ( Options :: ENABLE_FOOTNOTES ) ;
898
+ opts. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
899
+ opts. insert ( Options :: ENABLE_TASKLISTS ) ;
900
+ opts. insert ( Options :: ENABLE_HEADING_ATTRIBUTES ) ;
901
+
902
+ let content = "This text has a footnote[^1]\n [^1]:But it is meaningless." ;
903
+ let mut events: Vec < _ > = Parser :: new_ext ( & content, opts) . collect ( ) ;
904
+ convert_footnotes_to_github_style ( & mut events) ;
905
+ let mut html = String :: new ( ) ;
906
+ cmark:: html:: push_html ( & mut html, events. into_iter ( ) ) ;
907
+ assert_snapshot ! ( html) ;
908
+ }
909
+
910
+ #[ test]
911
+ fn reordered_footnotes ( ) {
912
+ let mut opts = Options :: empty ( ) ;
913
+ opts. insert ( Options :: ENABLE_TABLES ) ;
914
+ opts. insert ( Options :: ENABLE_FOOTNOTES ) ;
915
+ opts. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
916
+ opts. insert ( Options :: ENABLE_TASKLISTS ) ;
917
+ opts. insert ( Options :: ENABLE_HEADING_ATTRIBUTES ) ;
918
+
919
+ let content = "This text has two[^2] footnotes[^1]\n [^1]: not sorted.\n [^2]: But they are" ;
920
+ let mut events: Vec < _ > = Parser :: new_ext ( & content, opts) . collect ( ) ;
921
+ convert_footnotes_to_github_style ( & mut events) ;
922
+ let mut html = String :: new ( ) ;
923
+ cmark:: html:: push_html ( & mut html, events. into_iter ( ) ) ;
924
+ assert_snapshot ! ( html) ;
925
+ }
926
+
927
+ #[ test]
928
+ fn def_before_use ( ) {
929
+ let mut opts = Options :: empty ( ) ;
930
+ opts. insert ( Options :: ENABLE_TABLES ) ;
931
+ opts. insert ( Options :: ENABLE_FOOTNOTES ) ;
932
+ opts. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
933
+ opts. insert ( Options :: ENABLE_TASKLISTS ) ;
934
+ opts. insert ( Options :: ENABLE_HEADING_ATTRIBUTES ) ;
935
+
936
+ let content = "[^1]:It's before the reference.\n \n There is footnote definition?[^1]" ;
937
+ let mut events: Vec < _ > = Parser :: new_ext ( & content, opts) . collect ( ) ;
938
+ convert_footnotes_to_github_style ( & mut events) ;
939
+ let mut html = String :: new ( ) ;
940
+ cmark:: html:: push_html ( & mut html, events. into_iter ( ) ) ;
941
+ assert_snapshot ! ( html) ;
942
+ }
943
+
944
+ #[ test]
945
+ fn multiple_refs ( ) {
946
+ let mut opts = Options :: empty ( ) ;
947
+ opts. insert ( Options :: ENABLE_TABLES ) ;
948
+ opts. insert ( Options :: ENABLE_FOOTNOTES ) ;
949
+ opts. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
950
+ opts. insert ( Options :: ENABLE_TASKLISTS ) ;
951
+ opts. insert ( Options :: ENABLE_HEADING_ATTRIBUTES ) ;
952
+
953
+ let content = "This text has two[^1] identical footnotes[^1]\n [^1]: So one is present.\n [^2]: But another in not." ;
954
+ let mut events: Vec < _ > = Parser :: new_ext ( & content, opts) . collect ( ) ;
955
+ convert_footnotes_to_github_style ( & mut events) ;
956
+ let mut html = String :: new ( ) ;
957
+ cmark:: html:: push_html ( & mut html, events. into_iter ( ) ) ;
958
+ assert_snapshot ! ( html) ;
959
+ }
960
+
961
+ #[ test]
962
+ fn footnote_inside_footnote ( ) {
963
+ let mut opts = Options :: empty ( ) ;
964
+ opts. insert ( Options :: ENABLE_TABLES ) ;
965
+ opts. insert ( Options :: ENABLE_FOOTNOTES ) ;
966
+ opts. insert ( Options :: ENABLE_STRIKETHROUGH ) ;
967
+ opts. insert ( Options :: ENABLE_TASKLISTS ) ;
968
+ opts. insert ( Options :: ENABLE_HEADING_ATTRIBUTES ) ;
969
+
970
+ let content = "This text has a footnote[^1]\n [^1]: But the footnote has another footnote[^2].\n [^2]: That's it." ;
971
+ let mut events: Vec < _ > = Parser :: new_ext ( & content, opts) . collect ( ) ;
972
+ convert_footnotes_to_github_style ( & mut events) ;
973
+ let mut html = String :: new ( ) ;
974
+ cmark:: html:: push_html ( & mut html, events. into_iter ( ) ) ;
975
+ assert_snapshot ! ( html) ;
976
+ }
717
977
}
0 commit comments