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#[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 #[inline(always)]
60 pub fn label(&self) -> &text::Value {
61 &self.label
62 }
63
64 #[inline(always)]
66 pub fn index(&self) -> usize {
67 self.index
68 }
69
70 #[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#[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 #[inline(always)]
120 fn label(&self) -> &text::Value {
121 &self.label
122 }
123
124 #[inline(always)]
126 fn index(&self) -> usize {
127 self.index
128 }
129
130 #[inline(always)]
132 fn references(&self) -> &[usize] {
133 &self.references
134 }
135
136 #[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
167struct 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 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; 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 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 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#[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#[derive(Debug, Clone)]
457pub struct FootnoteHtmlRendererOptions {
458 pub link_class: String,
462
463 pub backlink_class: String,
467
468 pub backlink_html: String,
471
472 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: "↩︎".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 ctx.get(self.footnote_render).is_some() {
579 return Ok(WalkStatus::Continue);
580 }
581
582 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 " <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
731pub 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
751pub 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