use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::cell::Cell;
use crate::ast::{Block, Inline};
use super::escape::{attr_escape_gfm, encode_href};
pub struct FootnoteContext {
order: Vec<String>,
numbers: BTreeMap<String, usize>,
defs: BTreeMap<String, Vec<Block>>,
ref_totals: BTreeMap<String, usize>,
referenced: BTreeSet<String>,
display_labels: BTreeMap<String, String>,
seen_during_render: BTreeMap<String, Cell<usize>>,
inline_emitted: Cell<usize>,
}
impl FootnoteContext {
pub fn has_any(&self) -> bool {
!self.referenced.is_empty()
}
pub fn number(&self, id: &str) -> usize {
self.numbers.get(id).copied().unwrap_or(0)
}
pub fn is_defined(&self, id: &str) -> bool {
self.numbers.contains_key(id)
}
fn display_id<'a>(&'a self, id: &'a str) -> &'a str {
self.display_labels
.get(id)
.map(String::as_str)
.unwrap_or(id)
}
}
pub fn build(blocks: &[Block]) -> FootnoteContext {
let mut display_labels: BTreeMap<String, String> = BTreeMap::new();
let mut refs = RefBuilder {
order: Vec::new(),
numbers: BTreeMap::new(),
ref_totals: BTreeMap::new(),
referenced: BTreeSet::new(),
defs: BTreeMap::new(),
inline_counter: 0,
};
collect_defs(blocks, &mut refs.defs, &mut display_labels);
refs.walk_blocks(blocks);
let seen_during_render = refs
.order
.iter()
.map(|id| (id.clone(), Cell::new(0)))
.collect();
FootnoteContext {
order: refs.order,
numbers: refs.numbers,
defs: refs.defs,
ref_totals: refs.ref_totals,
referenced: refs.referenced,
display_labels,
seen_during_render,
inline_emitted: Cell::new(0),
}
}
fn collect_defs(
blocks: &[Block],
defs: &mut BTreeMap<String, Vec<Block>>,
display_labels: &mut BTreeMap<String, String>,
) {
for block in blocks {
match block {
Block::FootnoteDefinition(fd) => {
defs.entry(fd.identifier.clone())
.or_insert_with(|| fd.children.clone());
display_labels
.entry(fd.identifier.clone())
.or_insert_with(|| fd.label.clone());
}
Block::BlockQuote(bq) => collect_defs(&bq.children, defs, display_labels),
Block::Alert(a) => collect_defs(&a.children, defs, display_labels),
Block::List(list) => {
for item in &list.children {
collect_defs(&item.children, defs, display_labels);
}
}
Block::DescriptionList(dl) => {
for item in &dl.children {
for details in &item.details {
collect_defs(&details.children, defs, display_labels);
}
}
}
Block::ContainerDirective(dir) => collect_defs(&dir.children, defs, display_labels),
_ => {}
}
}
}
struct RefBuilder {
order: Vec<String>,
numbers: BTreeMap<String, usize>,
defs: BTreeMap<String, Vec<Block>>,
ref_totals: BTreeMap<String, usize>,
referenced: BTreeSet<String>,
inline_counter: usize,
}
impl RefBuilder {
fn walk_blocks(&mut self, blocks: &[Block]) {
for block in blocks {
match block {
Block::Paragraph(p) => self.walk_inlines(&p.children),
Block::Heading(h) => self.walk_inlines(&h.children),
Block::BlockQuote(bq) => self.walk_blocks(&bq.children),
Block::Alert(a) => self.walk_blocks(&a.children),
Block::List(list) => {
for item in &list.children {
self.walk_blocks(&item.children);
}
}
Block::DescriptionList(dl) => {
for item in &dl.children {
self.walk_inlines(&item.term);
for details in &item.details {
self.walk_blocks(&details.children);
}
}
}
Block::Table(table) => {
for row in &table.rows {
for cell in &row.cells {
self.walk_inlines(&cell.children);
}
}
}
Block::FootnoteDefinition(fd) => self.walk_blocks(&fd.children),
Block::ContainerDirective(dir) => {
self.walk_inlines(&dir.label);
self.walk_blocks(&dir.children);
}
Block::LeafDirective(dir) => self.walk_inlines(&dir.label),
_ => {}
}
}
}
fn walk_inlines(&mut self, inlines: &[Inline]) {
for inline in inlines {
match inline {
Inline::FootnoteReference(fr) => {
if self.defs.contains_key(&fr.identifier) {
self.register_ref(&fr.identifier);
}
}
Inline::InlineFootnote(node) => {
self.inline_counter += 1;
let id = format!("__inline_{}", self.inline_counter);
self.defs
.insert(id.clone(), block_wrap_inline(&node.children));
self.register_ref(&id);
self.walk_inlines(&node.children);
}
Inline::Emphasis(n) => self.walk_inlines(&n.children),
Inline::Strong(n) => self.walk_inlines(&n.children),
Inline::Underline(n) => self.walk_inlines(&n.children),
Inline::Delete(n) => self.walk_inlines(&n.children),
Inline::Insert(n) => self.walk_inlines(&n.children),
Inline::Mark(n) => self.walk_inlines(&n.children),
Inline::Subscript(n) => self.walk_inlines(&n.children),
Inline::Superscript(n) => self.walk_inlines(&n.children),
Inline::Spoiler(n) => self.walk_inlines(&n.children),
Inline::Link(n) => self.walk_inlines(&n.children),
Inline::LinkReference(n) => self.walk_inlines(&n.children),
Inline::TextDirective(d) => self.walk_inlines(&d.label),
_ => {}
}
}
}
fn register_ref(&mut self, id: &str) {
if !self.numbers.contains_key(id) {
self.order.push(String::from(id));
self.numbers.insert(String::from(id), self.order.len());
}
*self.ref_totals.entry(String::from(id)).or_insert(0) += 1;
self.referenced.insert(String::from(id));
}
}
fn block_wrap_inline(children: &[Inline]) -> Vec<Block> {
use crate::ast::{NodeMeta, Paragraph};
alloc::vec![Block::Paragraph(Paragraph {
meta: NodeMeta::default(),
children: children.to_vec(),
})]
}
pub fn reference_marker(ctx: &FootnoteContext, id: &str) -> (usize, String) {
let number = ctx.number(id);
let count = ctx
.seen_during_render
.get(id)
.map(|c| {
let v = c.get() + 1;
c.set(v);
v
})
.unwrap_or(1);
let enc = footnote_id_encode(ctx.display_id(id));
let fnref = if count == 1 {
format!("fnref-{}", enc)
} else {
format!("fnref-{}-{}", enc, count)
};
(number, fnref)
}
pub fn reference_fn_target(ctx: &FootnoteContext, id: &str) -> String {
footnote_id_encode(ctx.display_id(id))
}
pub fn next_inline_id(ctx: &FootnoteContext) -> String {
let n = ctx.inline_emitted.get() + 1;
ctx.inline_emitted.set(n);
format!("__inline_{}", n)
}
pub fn footnote_id_encode(id: &str) -> String {
attr_escape_gfm(&encode_href(id))
}
pub fn emit_footnote_section<F>(ctx: &FootnoteContext, render_blocks: F) -> String
where
F: Fn(&[Block]) -> String,
{
if !ctx.has_any() {
return String::new();
}
let mut out = String::from("<section class=\"footnotes\" data-footnotes>\n<ol>\n");
for id in &ctx.order {
if !ctx.referenced.contains(id) {
continue;
}
let number = ctx.number(id);
let enc = footnote_id_encode(ctx.display_id(id));
out.push_str(&format!("<li id=\"fn-{}\">\n", enc));
let empty: Vec<Block> = Vec::new();
let body = ctx.defs.get(id).unwrap_or(&empty);
let rendered = render_blocks(body);
let total = ctx.ref_totals.get(id).copied().unwrap_or(1);
let backrefs = build_backrefs(&enc, number, total);
out.push_str(&place_backrefs(&rendered, &backrefs));
out.push('\n');
out.push_str("</li>\n");
}
out.push_str("</ol>\n</section>");
out
}
fn build_backrefs(enc: &str, number: usize, total: usize) -> String {
let mut anchors: Vec<String> = Vec::new();
let total = total.max(1);
for k in 1..=total {
if k == 1 {
anchors.push(format!(
"<a href=\"#fnref-{enc}\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"{number}\" aria-label=\"Back to reference {number}\">\u{21a9}</a>",
));
} else {
anchors.push(format!(
"<a href=\"#fnref-{enc}-{k}\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"{number}-{k}\" aria-label=\"Back to reference {number}-{k}\">\u{21a9}<sup class=\"footnote-ref\">{k}</sup></a>",
));
}
}
anchors.join(" ")
}
fn place_backrefs(rendered: &str, backrefs: &str) -> String {
if backrefs.is_empty() {
return String::from(rendered);
}
if rendered.is_empty() {
return String::from(backrefs);
}
if rendered.ends_with("</p>") {
let head = &rendered[..rendered.len() - "</p>".len()];
return format!("{head} {backrefs}</p>");
}
format!("{rendered}\n{backrefs}")
}