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
413                    .merge_or_append_text_segment(arena, (seg.start(), seg.start() + 1).into());
414            }
415            return Some(node);
416        }
417
418        None
419    }
420}
421
422impl From<FootnoteReferenceParser> for AnyInlineParser {
423    fn from(p: FootnoteReferenceParser) -> Self {
424        AnyInlineParser::Extension(Box::new(p))
425    }
426}
427
428// }}}
429
430// Renderer {{{
431
432/// An enum representing the prefix of footnote IDs.
433#[derive(Debug, Clone)]
434pub enum FootnoteIdPrefix {
435    None,
436    Value(String),
437    Function(fn(&Arena, NodeRef, &renderer::Context) -> String),
438}
439
440impl FootnoteIdPrefix {
441    pub fn get_id(
442        &self,
443        arena: &Arena,
444        node_ref: NodeRef,
445        ctx: &renderer::Context,
446    ) -> Cow<'static, str> {
447        match self {
448            FootnoteIdPrefix::None => Cow::Borrowed(""),
449            FootnoteIdPrefix::Value(prefix) => Cow::Owned(prefix.clone()),
450            FootnoteIdPrefix::Function(f) => Cow::Owned(f(arena, node_ref, ctx)),
451        }
452    }
453}
454
455/// Options for the footnote HTML renderer.
456#[derive(Debug, Clone)]
457pub struct FootnoteHtmlRendererOptions {
458    /// The class name for the footnote reference link.
459    ///
460    /// This defaults to "footnote-ref".
461    pub link_class: String,
462
463    /// The class name for the footnote backlink.
464    ///
465    /// This defaults to "footnote-backref".
466    pub backlink_class: String,
467
468    /// The HTML content for the footnote backlink.
469    /// This defaults to "&#x21a9;&#xfe0e;" (the leftwards arrow with hook character).
470    pub backlink_html: String,
471
472    /// The prefix for footnote IDs.
473    pub id_prefix: FootnoteIdPrefix,
474}
475
476impl Default for FootnoteHtmlRendererOptions {
477    fn default() -> Self {
478        Self {
479            link_class: "footnote-ref".to_string(),
480            backlink_class: "footnote-backref".to_string(),
481            backlink_html: "&#x21a9;&#xfe0e;".to_string(),
482            id_prefix: FootnoteIdPrefix::None,
483        }
484    }
485}
486
487impl RendererOptions for FootnoteHtmlRendererOptions {}
488
489struct FootnoteReferenceHtmlRenderer<W: TextWrite> {
490    _phantom: core::marker::PhantomData<W>,
491    options: FootnoteHtmlRendererOptions,
492    writer: html::Writer,
493}
494
495impl<W: TextWrite> FootnoteReferenceHtmlRenderer<W> {
496    fn new(
497        _reg: Rc<RefCell<ContextKeyRegistry>>,
498        html_opts: html::Options,
499        options: FootnoteHtmlRendererOptions,
500    ) -> Self {
501        Self {
502            _phantom: core::marker::PhantomData,
503            options,
504            writer: html::Writer::with_options(html_opts),
505        }
506    }
507}
508
509impl<W: TextWrite> RenderNode<W> for FootnoteReferenceHtmlRenderer<W> {
510    fn render_node<'a>(
511        &self,
512        w: &mut W,
513        _source: &'a str,
514        arena: &'a Arena,
515        node_ref: NodeRef,
516        entering: bool,
517        ctx: &mut renderer::Context,
518    ) -> Result<WalkStatus> {
519        let data = as_extension_data!(arena, node_ref, FootnoteReference);
520        if entering {
521            let prefix = self.options.id_prefix.get_id(arena, node_ref, ctx);
522            self.writer.write_html(
523                w,
524                &format!(
525                    "<sup id=\"{}fnref:{}\"><a href=\"#{}fn:{}\" class=\"{}\" role=\"doc-noteref\">{}</a></sup>",
526                    prefix,
527                    data.ref_index(),
528                    prefix,
529                    data.index(),
530                    self.options.link_class,
531                    data.index()
532                ),
533            )?;
534        }
535        Ok(WalkStatus::SkipChildren)
536    }
537}
538
539impl<'cb, W> NodeRenderer<'cb, W> for FootnoteReferenceHtmlRenderer<W>
540where
541    W: TextWrite + 'cb,
542{
543    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
544        nrr.register_node_renderer_fn(TypeId::of::<FootnoteReference>(), BoxRenderNode::new(self));
545    }
546}
547
548struct FootnoteDefinitionHtmlRenderer<W: TextWrite> {
549    _phantom: core::marker::PhantomData<W>,
550    footnote_list: ContextKey<ObjectValue>,
551    footnote_render: ContextKey<BoolValue>,
552}
553
554impl<W: TextWrite> FootnoteDefinitionHtmlRenderer<W> {
555    pub fn new(reg: Rc<RefCell<ContextKeyRegistry>>) -> Self {
556        let footnote_list = reg.borrow_mut().get_or_create::<ObjectValue>(FOOTNOTE_LIST);
557        let footnote_render = reg.borrow_mut().get_or_create::<BoolValue>(FOOTNOTE_RENDER);
558        Self {
559            _phantom: core::marker::PhantomData,
560            footnote_list,
561            footnote_render,
562        }
563    }
564}
565
566impl<W: TextWrite> RenderNode<W> for FootnoteDefinitionHtmlRenderer<W> {
567    fn render_node<'a>(
568        &self,
569        _w: &mut W,
570        _source: &'a str,
571        _arena: &'a Arena,
572        node_ref: NodeRef,
573        entering: bool,
574        ctx: &mut renderer::Context,
575    ) -> Result<WalkStatus> {
576        // If the footnote render flag is set, it means we are currently rendering footnotes, so we
577        // continue rendering the footnote definition as normal.
578        if ctx.get(self.footnote_render).is_some() {
579            return Ok(WalkStatus::Continue);
580        }
581
582        // If we are entering the footnote definition node, we add it to the footnote list in the
583        // context.
584        // This is necessary because we need to render the footnote definitions at the end of the
585        // document, and we need to know which footnote definitions to render.
586        if entering {
587            let mut list_opt = ctx.get_mut(self.footnote_list);
588            if list_opt.is_none() {
589                let lst = FootnoteDefinitions::new();
590                ctx.insert(self.footnote_list, Box::new(lst));
591                list_opt = ctx.get_mut(self.footnote_list);
592            }
593            let list = list_opt
594                .unwrap()
595                .downcast_mut::<FootnoteDefinitions>()
596                .expect("Failed to downcast footnote list");
597            list.definitions.push(node_ref);
598        }
599        Ok(WalkStatus::SkipChildren)
600    }
601}
602
603impl<'cb, W> NodeRenderer<'cb, W> for FootnoteDefinitionHtmlRenderer<W>
604where
605    W: TextWrite + 'cb,
606{
607    fn register_node_renderer_fn(self, nrr: &mut impl NodeRendererRegistry<'cb, W>) {
608        nrr.register_node_renderer_fn(TypeId::of::<FootnoteDefinition>(), BoxRenderNode::new(self));
609    }
610}
611
612struct FootnotePostRenderHook<W: TextWrite> {
613    _phantom: core::marker::PhantomData<W>,
614    writer: html::Writer,
615    footnote_list: ContextKey<ObjectValue>,
616    footnote_render: ContextKey<BoolValue>,
617    html_opts: html::Options,
618    options: FootnoteHtmlRendererOptions,
619}
620
621impl<W: TextWrite> FootnotePostRenderHook<W> {
622    pub fn new(
623        reg: Rc<RefCell<ContextKeyRegistry>>,
624        html_opts: html::Options,
625        options: FootnoteHtmlRendererOptions,
626    ) -> Self {
627        let footnote_list = reg.borrow_mut().get_or_create::<ObjectValue>(FOOTNOTE_LIST);
628        let footnote_render = reg.borrow_mut().get_or_create::<BoolValue>(FOOTNOTE_RENDER);
629        Self {
630            _phantom: core::marker::PhantomData,
631            writer: html::Writer::with_options(html_opts.clone()),
632            options,
633            footnote_list,
634            footnote_render,
635            html_opts,
636        }
637    }
638}
639
640impl<W: TextWrite> PostRender<W> for FootnotePostRenderHook<W> {
641    fn post_render(
642        &self,
643        w: &mut W,
644        source: &str,
645        arena: &Arena,
646        _node_ref: NodeRef,
647        render: &dyn Render<W>,
648        ctx: &mut renderer::Context,
649    ) -> Result<()> {
650        if let Some(list_any) = ctx.remove(self.footnote_list) {
651            let mut list = list_any
652                .downcast::<FootnoteDefinitions>()
653                .expect("Failed to downcast footnote list");
654            if list.definitions.is_empty()
655                || list.definitions.iter().all(|r| {
656                    as_extension_data!(arena[*r], FootnoteDefinition)
657                        .references()
658                        .is_empty()
659                })
660            {
661                return Ok(());
662            }
663
664            ctx.insert(self.footnote_render, true);
665            list.definitions.sort_by(|a, b| {
666                let a_data = as_extension_data!(arena[*a], FootnoteDefinition);
667                let b_data = as_extension_data!(arena[*b], FootnoteDefinition);
668                let ref_a = a_data.references().first().unwrap_or(&usize::MAX);
669                let ref_b = b_data.references().first().unwrap_or(&usize::MAX);
670                ref_a.cmp(ref_b)
671            });
672            self.writer
673                .write_html(w, r#"<div class="footnotes" role="doc-endnotes">"#)?;
674            self.writer.write_newline(w)?;
675            if self.html_opts.xhtml {
676                self.writer.write_html(w, "<hr />\n")?;
677            } else {
678                self.writer.write_html(w, "<hr>\n")?;
679            }
680            self.writer.write_html(w, "<ol>\n")?;
681            let prefix = self.options.id_prefix.get_id(arena, _node_ref, ctx);
682
683            for def_ref in &list.definitions {
684                let def_data = as_extension_data!(arena, *def_ref, FootnoteDefinition);
685                self.writer.write_html(
686                    w,
687                    &format!("<li id=\"{}fn:{}\">\n", prefix, def_data.index()),
688                )?;
689                let mut last_is_paragraph = false;
690                for c in arena[*def_ref].children(arena) {
691                    if c == arena[*def_ref].last_child().unwrap()
692                        && matches_kind!(arena[c], Paragraph)
693                    {
694                        last_is_paragraph = true;
695                        break;
696                    }
697                    render.render(w, source, arena, c, ctx)?;
698                }
699                if last_is_paragraph {
700                    let last_child = arena[*def_ref].last_child().unwrap();
701                    self.writer.write_safe_str(w, "<p>")?;
702                    for c in arena[last_child].children(arena) {
703                        render.render(w, source, arena, c, ctx)?;
704                    }
705                }
706                for ref_index in def_data.references() {
707                    self.writer.write_html(
708                            w,
709                            &format!(
710                                "&#160;<a href=\"#{}fnref:{}\" class=\"{}\" role=\"doc-backlink\">{}</a>",
711                                prefix,
712                                ref_index,
713                                self.options.backlink_class,
714                                self.options.backlink_html
715                            ),
716                        )?;
717                }
718                if last_is_paragraph {
719                    self.writer.write_safe_str(w, "</p>\n")?;
720                }
721                self.writer.write_html(w, "</li>\n")?;
722            }
723            self.writer.write_html(w, "</ol>\n")?;
724            self.writer.write_html(w, "</div>\n")?;
725            ctx.remove(self.footnote_render);
726        }
727        Ok(())
728    }
729}
730
731// }}} Renderer
732
733// Extension {{{
734
735/// Returns a parser extension that parses footnotes.
736pub fn footnote_parser_extension() -> impl ParserExtension {
737    ParserExtensionFn::new(|p: &mut Parser| {
738        p.add_inline_parser(
739            FootnoteReferenceParser::new,
740            NoParserOptions,
741            PRIORITY_LINK - 100,
742        );
743        p.add_block_parser(
744            FootnoteDefinitionParser::new,
745            NoParserOptions,
746            PRIORITY_LIST + 100,
747        );
748    })
749}
750
751/// Returns a renderer extension that renders footnotes in HTML.
752pub fn footnote_html_renderer_extension<'cb, W>(
753    options: impl Into<FootnoteHtmlRendererOptions>,
754) -> impl RendererExtension<'cb, W>
755where
756    W: TextWrite + 'cb,
757{
758    RendererExtensionFn::new(move |r: &mut Renderer<'cb, W>| {
759        let options = options.into();
760        r.add_post_render_hook(FootnotePostRenderHook::new, options.clone(), 500);
761        r.add_node_renderer(FootnoteDefinitionHtmlRenderer::new, options.clone());
762        r.add_node_renderer(FootnoteReferenceHtmlRenderer::new, options);
763    })
764}
765
766// }}}