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.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#[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#[derive(Debug, Clone)]
456pub struct FootnoteHtmlRendererOptions {
457 pub link_class: String,
461
462 pub backlink_class: String,
466
467 pub backlink_html: String,
470
471 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: "↩︎".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 ctx.get(self.footnote_render).is_some() {
578 return Ok(WalkStatus::Continue);
579 }
580
581 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 " <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
730pub 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
750pub 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