Skip to main content

rushdown_footnote/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4extern crate alloc;
5
6use alloc::borrow::Cow;
7use alloc::boxed::Box;
8use alloc::format;
9use alloc::rc::Rc;
10use alloc::string::String;
11use alloc::string::ToString;
12use alloc::vec::Vec;
13
14use core::any::TypeId;
15use core::cell::RefCell;
16use core::fmt;
17use core::fmt::Write;
18
19use rushdown::{
20    as_extension_data, as_extension_data_mut,
21    ast::{pp_indent, Arena, KindData, NodeKind, NodeRef, NodeType, PrettyPrint, WalkStatus},
22    context::{BoolValue, ContextKey, ContextKeyRegistry, ObjectValue},
23    matches_kind,
24    parser::{
25        self, AnyBlockParser, AnyInlineParser, BlockParser, InlineParser, NoParserOptions, Parser,
26        ParserExtension, ParserExtensionFn, PRIORITY_LINK, PRIORITY_LIST,
27    },
28    renderer::{
29        self,
30        html::{self, Renderer, RendererExtension, RendererExtensionFn},
31        BoxRenderNode, NodeRenderer, NodeRendererRegistry, PostRender, Render, RenderNode,
32        RendererOptions, TextWrite,
33    },
34    text::{self, Reader},
35    util::{indent_position, is_blank},
36    Result,
37};
38
39// AST {{{
40
41/// A struct representing a footnote reference in the AST.
42#[derive(Debug)]
43pub struct FootnoteReference {
44    label: text::Value,
45    index: usize,
46    ref_index: usize,
47}
48
49impl FootnoteReference {
50    pub fn new(label: impl Into<text::Value>, index: usize, ref_index: usize) -> Self {
51        Self {
52            label: label.into(),
53            index,
54            ref_index,
55        }
56    }
57
58    /// Returns the label of the footnote reference.
59    #[inline(always)]
60    pub fn label(&self) -> &text::Value {
61        &self.label
62    }
63
64    /// Returns the index of the footnote definition.
65    #[inline(always)]
66    pub fn index(&self) -> usize {
67        self.index
68    }
69
70    /// Returns the reference index of the footnote reference.
71    #[inline(always)]
72    pub fn ref_index(&self) -> usize {
73        self.ref_index
74    }
75}
76
77impl NodeKind for FootnoteReference {
78    fn typ(&self) -> NodeType {
79        NodeType::Inline
80    }
81
82    fn kind_name(&self) -> &'static str {
83        "FootnoteReference"
84    }
85}
86
87impl PrettyPrint for FootnoteReference {
88    fn pretty_print(&self, w: &mut dyn Write, source: &str, level: usize) -> fmt::Result {
89        writeln!(w, "{}Label: {}", pp_indent(level), self.label().str(source))?;
90        writeln!(w, "{}Index: {}", pp_indent(level), self.index())?;
91        writeln!(w, "{}RefIndex: {}", pp_indent(level), self.ref_index())
92    }
93}
94
95impl From<FootnoteReference> for KindData {
96    fn from(e: FootnoteReference) -> Self {
97        KindData::Extension(Box::new(e))
98    }
99}
100
101/// A struct representing a footnote definition in the AST.
102#[derive(Debug)]
103pub struct FootnoteDefinition {
104    label: text::Value,
105    index: usize,
106    references: Vec<usize>,
107}
108
109impl FootnoteDefinition {
110    fn new(label: impl Into<text::Value>) -> Self {
111        Self {
112            label: label.into(),
113            index: 0,
114            references: Vec::new(),
115        }
116    }
117
118    /// Returns the label of the footnote definition.
119    #[inline(always)]
120    fn label(&self) -> &text::Value {
121        &self.label
122    }
123
124    /// Returns the index of the footnote definition.
125    #[inline(always)]
126    fn index(&self) -> usize {
127        self.index
128    }
129
130    /// Returns the reference indices of the footnote definition.
131    #[inline(always)]
132    fn references(&self) -> &[usize] {
133        &self.references
134    }
135
136    /// Adds a reference index to the footnote definition.
137    #[inline(always)]
138    fn add_reference(&mut self, ref_index: usize) {
139        self.references.push(ref_index);
140    }
141}
142
143impl NodeKind for FootnoteDefinition {
144    fn typ(&self) -> NodeType {
145        NodeType::ContainerBlock
146    }
147
148    fn kind_name(&self) -> &'static str {
149        "FootnoteDefinition"
150    }
151}
152
153impl PrettyPrint for FootnoteDefinition {
154    fn pretty_print(&self, w: &mut dyn Write, source: &str, level: usize) -> fmt::Result {
155        writeln!(w, "{}Label: {}", pp_indent(level), self.label.str(source))?;
156        writeln!(w, "{}Index: {}", pp_indent(level), self.index,)?;
157        writeln!(w, "{}References: {:?}", pp_indent(level), self.references())
158    }
159}
160
161impl From<FootnoteDefinition> for KindData {
162    fn from(e: FootnoteDefinition) -> Self {
163        KindData::Extension(Box::new(e))
164    }
165}
166
167// }}} AST
168
169// Parser {{{
170
171struct FootnoteDefinitions {
172    definitions: Vec<NodeRef>,
173    count: usize,
174}
175
176impl FootnoteDefinitions {
177    fn new() -> Self {
178        Self {
179            definitions: Vec::new(),
180            count: 0,
181        }
182    }
183}
184
185const FOOTNOTE_LIST: &str = "rushdown-footnote-l";
186const REFERENCE_LIST: &str = "rushdown-footnote-r";
187const FOOTNOTE_RENDER: &str = "rushdown-footnote-n";
188
189#[derive(Debug)]
190struct FootnoteDefinitionParser {
191    footnote_list: ContextKey<ObjectValue>,
192}
193
194impl FootnoteDefinitionParser {
195    /// Returns a new [`FootnoteDefinitionParser`].
196    pub fn new(reg: Rc<RefCell<ContextKeyRegistry>>) -> Self {
197        let footnote_list = reg.borrow_mut().get_or_create::<ObjectValue>(FOOTNOTE_LIST);
198        Self { footnote_list }
199    }
200}
201
202impl BlockParser for FootnoteDefinitionParser {
203    fn trigger(&self) -> &[u8] {
204        b"["
205    }
206
207    fn open(
208        &self,
209        arena: &mut Arena,
210        _parent_ref: NodeRef,
211        reader: &mut text::BasicReader,
212        ctx: &mut parser::Context,
213    ) -> Option<(NodeRef, parser::State)> {
214        let (line, seg) = reader.peek_line_bytes()?;
215        let mut pos = ctx.block_offset()?;
216        pos += 1; // skip the opening '['
217        if !line.get(pos)?.eq(&b'^') {
218            return None;
219        }
220        let open = pos + 1;
221        let mut cur = open;
222        let mut close = 0usize;
223        while cur < line.len() {
224            let c = line[cur];
225            if c == b'\\' && line.get(cur + 1)? == &b']' {
226                cur += 2;
227                continue;
228            }
229            if c == b']' {
230                close = cur;
231                break;
232            }
233            cur += 1;
234        }
235        if close == 0 {
236            return None;
237        }
238        if !line.get(close + 1)?.eq(&b':') {
239            return None;
240        }
241
242        let label = text::Segment::new(
243            seg.start() + open - seg.padding(),
244            seg.start() + close - seg.padding(),
245        );
246
247        if label.is_blank(reader.source()) {
248            return None;
249        }
250
251        let node = arena.new_node(FootnoteDefinition::new(label));
252        reader.advance(close + 2);
253
254        Some((node, parser::State::HAS_CHILDREN))
255    }
256
257    fn cont(
258        &self,
259        _arena: &mut Arena,
260        _node_ref: NodeRef,
261        reader: &mut text::BasicReader,
262        _ctx: &mut parser::Context,
263    ) -> Option<parser::State> {
264        let (line, _) = reader.peek_line_bytes()?;
265        if is_blank(&line) {
266            return Some(parser::State::HAS_CHILDREN);
267        }
268        let (childpos, padding) = indent_position(&line, reader.line_offset(), 4)?;
269        reader.advance_and_set_padding(childpos, padding);
270        Some(parser::State::HAS_CHILDREN)
271    }
272
273    fn close(
274        &self,
275        _arena: &mut Arena,
276        node_ref: NodeRef,
277        _reader: &mut text::BasicReader,
278        ctx: &mut parser::Context,
279    ) {
280        let mut list_opt = ctx.get_mut(self.footnote_list);
281        if list_opt.is_none() {
282            let lst = FootnoteDefinitions::new();
283            ctx.insert(self.footnote_list, Box::new(lst));
284            list_opt = ctx.get_mut(self.footnote_list);
285        }
286        let list = list_opt
287            .unwrap()
288            .downcast_mut::<FootnoteDefinitions>()
289            .expect("Failed to downcast footnote list");
290        list.definitions.push(node_ref);
291    }
292
293    fn can_interrupt_paragraph(&self) -> bool {
294        true
295    }
296}
297
298impl From<FootnoteDefinitionParser> for AnyBlockParser {
299    fn from(p: FootnoteDefinitionParser) -> Self {
300        AnyBlockParser::Extension(Box::new(p))
301    }
302}
303
304#[derive(Debug)]
305struct FootnoteReferenceParser {
306    footnote_list: ContextKey<ObjectValue>,
307    reference_list: ContextKey<ObjectValue>,
308}
309
310impl FootnoteReferenceParser {
311    /// Returns a new [`FootnoteReferenceParser`].
312    pub fn new(reg: Rc<RefCell<ContextKeyRegistry>>) -> Self {
313        let footnote_list = reg.borrow_mut().get_or_create::<ObjectValue>(FOOTNOTE_LIST);
314        let reference_list = reg
315            .borrow_mut()
316            .get_or_create::<ObjectValue>(REFERENCE_LIST);
317        Self {
318            footnote_list,
319            reference_list,
320        }
321    }
322}
323
324impl InlineParser for FootnoteReferenceParser {
325    fn trigger(&self) -> &[u8] {
326        // footnote syntax probably conflict with the image syntax.
327        // So we need trigger this parser with '!'.
328        b"!["
329    }
330
331    fn parse(
332        &self,
333        arena: &mut Arena,
334        parent_ref: NodeRef,
335        reader: &mut text::BlockReader,
336        ctx: &mut parser::Context,
337    ) -> Option<NodeRef> {
338        let (line, seg) = reader.peek_line_bytes()?;
339        let mut pos = 1;
340        if line.first()? == &b'!' {
341            pos += 1;
342        }
343        if line.get(pos)? != &b'^' {
344            return None;
345        }
346        let open = pos + 1;
347        let mut cur = open;
348        let mut close = 0usize;
349        while cur < line.len() {
350            let c = line[cur];
351            if c == b'\\' && line.get(cur + 1)? == &b']' {
352                cur += 2;
353                continue;
354            }
355            if c == b']' {
356                close = cur;
357                break;
358            }
359            cur += 1;
360        }
361        if close == 0 {
362            return None;
363        }
364        let label = text::Segment::new(seg.start() + open, seg.start() + close);
365
366        let ref_index = {
367            let list = if let Some(list) = ctx.get_mut(self.reference_list) {
368                list
369            } else {
370                ctx.insert(self.reference_list, Box::new(Vec::<NodeRef>::new()));
371                ctx.get_mut(self.reference_list).unwrap()
372            }
373            .downcast_mut::<Vec<NodeRef>>()
374            .expect("Failed to downcast reference list");
375            list.len() + 1
376        };
377
378        let list = ctx.get_mut(self.footnote_list).map(|v| {
379            v.downcast_mut::<FootnoteDefinitions>()
380                .expect("Failed to downcast footnote list")
381        });
382        if let Some(list) = list {
383            let mut index = 0;
384            for def_ref in &list.definitions {
385                let def_data = as_extension_data_mut!(arena, *def_ref, FootnoteDefinition);
386                if def_data.label().str(reader.source()) == label.str(reader.source()) {
387                    if def_data.index() < 1 {
388                        list.count += 1;
389                        def_data.index = list.count;
390                    }
391                    index = def_data.index();
392                    def_data.add_reference(ref_index);
393                    break;
394                }
395            }
396            if index == 0 {
397                return None;
398            }
399
400            let list = ctx
401                .get_mut(self.reference_list)
402                .unwrap()
403                .downcast_mut::<Vec<NodeRef>>()
404                .expect("Failed to downcast reference list");
405
406            let node = arena.new_node(FootnoteReference::new(label, index, ref_index));
407            list.push(node);
408
409            reader.advance(close + 1);
410
411            if line[0] == b'!' {
412                parent_ref.merge_or_append_text(arena, (seg.start(), seg.start() + 1).into());
413            }
414            return Some(node);
415        }
416
417        None
418    }
419}
420
421impl From<FootnoteReferenceParser> for AnyInlineParser {
422    fn from(p: FootnoteReferenceParser) -> Self {
423        AnyInlineParser::Extension(Box::new(p))
424    }
425}
426
427// }}}
428
429// Renderer {{{
430
431/// An enum representing the prefix of footnote IDs.
432#[derive(Debug, Clone)]
433pub enum FootnoteIdPrefix {
434    None,
435    Value(String),
436    Function(fn(&Arena, NodeRef, &renderer::Context) -> String),
437}
438
439impl FootnoteIdPrefix {
440    pub fn get_id(
441        &self,
442        arena: &Arena,
443        node_ref: NodeRef,
444        ctx: &renderer::Context,
445    ) -> Cow<'static, str> {
446        match self {
447            FootnoteIdPrefix::None => Cow::Borrowed(""),
448            FootnoteIdPrefix::Value(prefix) => Cow::Owned(prefix.clone()),
449            FootnoteIdPrefix::Function(f) => Cow::Owned(f(arena, node_ref, ctx)),
450        }
451    }
452}
453
454/// Options for the footnote HTML renderer.
455#[derive(Debug, Clone)]
456pub struct FootnoteHtmlRendererOptions {
457    /// The class name for the footnote reference link.
458    ///
459    /// This defaults to "footnote-ref".
460    pub link_class: String,
461
462    /// The class name for the footnote backlink.
463    ///
464    /// This defaults to "footnote-backref".
465    pub backlink_class: String,
466
467    /// The HTML content for the footnote backlink.
468    /// This defaults to "&#x21a9;&#xfe0e;" (the leftwards arrow with hook character).
469    pub backlink_html: String,
470
471    /// The prefix for footnote IDs.
472    pub id_prefix: FootnoteIdPrefix,
473}
474
475impl Default for FootnoteHtmlRendererOptions {
476    fn default() -> Self {
477        Self {
478            link_class: "footnote-ref".to_string(),
479            backlink_class: "footnote-backref".to_string(),
480            backlink_html: "&#x21a9;&#xfe0e;".to_string(),
481            id_prefix: FootnoteIdPrefix::None,
482        }
483    }
484}
485
486impl RendererOptions for FootnoteHtmlRendererOptions {}
487
488struct FootnoteReferenceHtmlRenderer<W: TextWrite> {
489    _phantom: core::marker::PhantomData<W>,
490    options: FootnoteHtmlRendererOptions,
491    writer: html::Writer,
492}
493
494impl<W: TextWrite> FootnoteReferenceHtmlRenderer<W> {
495    fn new(
496        _reg: Rc<RefCell<ContextKeyRegistry>>,
497        html_opts: html::Options,
498        options: FootnoteHtmlRendererOptions,
499    ) -> Self {
500        Self {
501            _phantom: core::marker::PhantomData,
502            options,
503            writer: html::Writer::with_options(html_opts),
504        }
505    }
506}
507
508impl<W: TextWrite> RenderNode<W> for FootnoteReferenceHtmlRenderer<W> {
509    fn render_node<'a>(
510        &self,
511        w: &mut W,
512        _source: &'a str,
513        arena: &'a Arena,
514        node_ref: NodeRef,
515        entering: bool,
516        ctx: &mut renderer::Context,
517    ) -> Result<WalkStatus> {
518        let data = as_extension_data!(arena, node_ref, FootnoteReference);
519        if entering {
520            let prefix = self.options.id_prefix.get_id(arena, node_ref, ctx);
521            self.writer.write_html(
522                w,
523                &format!(
524                    "<sup id=\"{}fnref:{}\"><a href=\"#{}fn:{}\" class=\"{}\" role=\"doc-noteref\">{}</a></sup>",
525                    prefix,
526                    data.ref_index(),
527                    prefix,
528                    data.index(),
529                    self.options.link_class,
530                    data.index()
531                ),
532            )?;
533        }
534        Ok(WalkStatus::SkipChildren)
535    }
536}
537
538impl<'cb, W> NodeRenderer<'cb, W> for FootnoteReferenceHtmlRenderer<W>
539where
540    W: TextWrite + 'cb,
541{
542    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
543        nrr.register_node_renderer_fn(TypeId::of::<FootnoteReference>(), BoxRenderNode::new(self));
544    }
545}
546
547struct FootnoteDefinitionHtmlRenderer<W: TextWrite> {
548    _phantom: core::marker::PhantomData<W>,
549    footnote_list: ContextKey<ObjectValue>,
550    footnote_render: ContextKey<BoolValue>,
551}
552
553impl<W: TextWrite> FootnoteDefinitionHtmlRenderer<W> {
554    pub fn new(reg: Rc<RefCell<ContextKeyRegistry>>) -> Self {
555        let footnote_list = reg.borrow_mut().get_or_create::<ObjectValue>(FOOTNOTE_LIST);
556        let footnote_render = reg.borrow_mut().get_or_create::<BoolValue>(FOOTNOTE_RENDER);
557        Self {
558            _phantom: core::marker::PhantomData,
559            footnote_list,
560            footnote_render,
561        }
562    }
563}
564
565impl<W: TextWrite> RenderNode<W> for FootnoteDefinitionHtmlRenderer<W> {
566    fn render_node<'a>(
567        &self,
568        _w: &mut W,
569        _source: &'a str,
570        _arena: &'a Arena,
571        node_ref: NodeRef,
572        entering: bool,
573        ctx: &mut renderer::Context,
574    ) -> Result<WalkStatus> {
575        // If the footnote render flag is set, it means we are currently rendering footnotes, so we
576        // continue rendering the footnote definition as normal.
577        if ctx.get(self.footnote_render).is_some() {
578            return Ok(WalkStatus::Continue);
579        }
580
581        // If we are entering the footnote definition node, we add it to the footnote list in the
582        // context.
583        // This is necessary because we need to render the footnote definitions at the end of the
584        // document, and we need to know which footnote definitions to render.
585        if entering {
586            let mut list_opt = ctx.get_mut(self.footnote_list);
587            if list_opt.is_none() {
588                let lst = FootnoteDefinitions::new();
589                ctx.insert(self.footnote_list, Box::new(lst));
590                list_opt = ctx.get_mut(self.footnote_list);
591            }
592            let list = list_opt
593                .unwrap()
594                .downcast_mut::<FootnoteDefinitions>()
595                .expect("Failed to downcast footnote list");
596            list.definitions.push(node_ref);
597        }
598        Ok(WalkStatus::SkipChildren)
599    }
600}
601
602impl<'cb, W> NodeRenderer<'cb, W> for FootnoteDefinitionHtmlRenderer<W>
603where
604    W: TextWrite + 'cb,
605{
606    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
607        nrr.register_node_renderer_fn(TypeId::of::<FootnoteDefinition>(), BoxRenderNode::new(self));
608    }
609}
610
611struct FootnotePostRenderHook<W: TextWrite> {
612    _phantom: core::marker::PhantomData<W>,
613    writer: html::Writer,
614    footnote_list: ContextKey<ObjectValue>,
615    footnote_render: ContextKey<BoolValue>,
616    html_opts: html::Options,
617    options: FootnoteHtmlRendererOptions,
618}
619
620impl<W: TextWrite> FootnotePostRenderHook<W> {
621    pub fn new(
622        reg: Rc<RefCell<ContextKeyRegistry>>,
623        html_opts: html::Options,
624        options: FootnoteHtmlRendererOptions,
625    ) -> Self {
626        let footnote_list = reg.borrow_mut().get_or_create::<ObjectValue>(FOOTNOTE_LIST);
627        let footnote_render = reg.borrow_mut().get_or_create::<BoolValue>(FOOTNOTE_RENDER);
628        Self {
629            _phantom: core::marker::PhantomData,
630            writer: html::Writer::with_options(html_opts.clone()),
631            options,
632            footnote_list,
633            footnote_render,
634            html_opts,
635        }
636    }
637}
638
639impl<W: TextWrite> PostRender<W> for FootnotePostRenderHook<W> {
640    fn post_render(
641        &self,
642        w: &mut W,
643        source: &str,
644        arena: &Arena,
645        _node_ref: NodeRef,
646        render: &dyn Render<W>,
647        ctx: &mut renderer::Context,
648    ) -> Result<()> {
649        if let Some(list_any) = ctx.remove(self.footnote_list) {
650            let mut list = list_any
651                .downcast::<FootnoteDefinitions>()
652                .expect("Failed to downcast footnote list");
653            if list.definitions.is_empty()
654                || list.definitions.iter().all(|r| {
655                    as_extension_data!(arena[*r], FootnoteDefinition)
656                        .references()
657                        .is_empty()
658                })
659            {
660                return Ok(());
661            }
662
663            ctx.insert(self.footnote_render, true);
664            list.definitions.sort_by(|a, b| {
665                let a_data = as_extension_data!(arena[*a], FootnoteDefinition);
666                let b_data = as_extension_data!(arena[*b], FootnoteDefinition);
667                let ref_a = a_data.references().first().unwrap_or(&usize::MAX);
668                let ref_b = b_data.references().first().unwrap_or(&usize::MAX);
669                ref_a.cmp(ref_b)
670            });
671            self.writer
672                .write_html(w, r#"<div class="footnotes" role="doc-endnotes">"#)?;
673            self.writer.write_newline(w)?;
674            if self.html_opts.xhtml {
675                self.writer.write_html(w, "<hr />\n")?;
676            } else {
677                self.writer.write_html(w, "<hr>\n")?;
678            }
679            self.writer.write_html(w, "<ol>\n")?;
680            let prefix = self.options.id_prefix.get_id(arena, _node_ref, ctx);
681
682            for def_ref in &list.definitions {
683                let def_data = as_extension_data!(arena, *def_ref, FootnoteDefinition);
684                self.writer.write_html(
685                    w,
686                    &format!("<li id=\"{}fn:{}\">\n", prefix, def_data.index()),
687                )?;
688                let mut last_is_paragraph = false;
689                for c in arena[*def_ref].children(arena) {
690                    if c == arena[*def_ref].last_child().unwrap()
691                        && matches_kind!(arena[c], Paragraph)
692                    {
693                        last_is_paragraph = true;
694                        break;
695                    }
696                    render.render(w, source, arena, c, ctx)?;
697                }
698                if last_is_paragraph {
699                    let last_child = arena[*def_ref].last_child().unwrap();
700                    self.writer.write_safe_str(w, "<p>")?;
701                    for c in arena[last_child].children(arena) {
702                        render.render(w, source, arena, c, ctx)?;
703                    }
704                }
705                for ref_index in def_data.references() {
706                    self.writer.write_html(
707                            w,
708                            &format!(
709                                "&#160;<a href=\"#{}fnref:{}\" class=\"{}\" role=\"doc-backlink\">{}</a>",
710                                prefix,
711                                ref_index,
712                                self.options.backlink_class,
713                                self.options.backlink_html
714                            ),
715                        )?;
716                }
717                if last_is_paragraph {
718                    self.writer.write_safe_str(w, "</p>\n")?;
719                }
720                self.writer.write_html(w, "</li>\n")?;
721            }
722            self.writer.write_html(w, "</ol>\n")?;
723            self.writer.write_html(w, "</div>\n")?;
724            ctx.remove(self.footnote_render);
725        }
726        Ok(())
727    }
728}
729
730// }}} Renderer
731
732// Extension {{{
733
734/// Returns a parser extension that parses footnotes.
735pub fn footnote_parser_extension() -> impl ParserExtension {
736    ParserExtensionFn::new(|p: &mut Parser| {
737        p.add_inline_parser(
738            FootnoteReferenceParser::new,
739            NoParserOptions,
740            PRIORITY_LINK - 100,
741        );
742        p.add_block_parser(
743            FootnoteDefinitionParser::new,
744            NoParserOptions,
745            PRIORITY_LIST + 100,
746        );
747    })
748}
749
750/// Returns a renderer extension that renders footnotes in HTML.
751pub fn footnote_html_renderer_extension<'cb, W>(
752    options: impl Into<FootnoteHtmlRendererOptions>,
753) -> impl RendererExtension<'cb, W>
754where
755    W: TextWrite + 'cb,
756{
757    RendererExtensionFn::new(move |r: &mut Renderer<'cb, W>| {
758        let options = options.into();
759        r.add_post_render_hook(FootnotePostRenderHook::new, options.clone(), 500);
760        r.add_node_renderer(FootnoteDefinitionHtmlRenderer::new, options.clone());
761        r.add_node_renderer(FootnoteReferenceHtmlRenderer::new, options);
762    })
763}
764
765// }}}