// Resolving footnotes once a whole document (or the whole remaining stream)
// has been read: a reference points at a definition by its label, a definition
// is shown in the order its first reference appears, and the referenced
// definitions are written after the rest of the document. This is the phase the
// block and inline parsers defer to, because a definition may be read after the
// reference that uses it.
///|
/// A definition as it is collected: the label as written, the body, and the
/// numbering that the pass assigns to it.
priv struct FootnoteDef {
label : String
mut blocks : Array[Block]
number : Int32
refs : Int32
}
///|
/// Settle the footnotes of a document. Resolves every reference to the number
/// of the definition it points at, removes the definitions from the body, and
/// appends the referenced ones, in the order their first references appear, at
/// the end. Returns the finished blocks; a document without footnotes is
/// handed back unchanged.
fn settle_footnotes(blocks : Array[Block], flavor : Flavor) -> Array[Block] {
if !flavor.footnotes {
return blocks
}
let defs = (Map([]) : Map[String, FootnoteDef])
collect_footnote_defs(blocks, defs)
// A reference without a definition is settled into the text it was written
// as, so the pass runs even when there is nothing to collect.
let order : Array[String] = []
let body = resolve_footnote_blocks(blocks, defs, order)
for key in order {
let def = defs[key]
body.push(
Footnote({
label: def.label,
blocks: def.blocks,
number: def.number,
backrefs: def.refs,
}),
)
}
body
}
///|
/// Collect every definition, at any depth, by the label it is looked up with.
fn collect_footnote_defs(
blocks : Array[Block],
defs : Map[String, FootnoteDef],
) -> Unit {
for block in blocks {
match block {
Footnote(f) => {
let key = normalize_label(f.label[:])
if key.length() > 0 && !defs.contains(key) {
defs[key] = { label: f.label, blocks: f.blocks, number: 0, refs: 0, }
}
collect_footnote_defs(f.blocks, defs)
}
Quote(children) => collect_footnote_defs(children, defs)
List(list) =>
for item in list.items {
collect_footnote_defs(item.blocks, defs)
}
_ => ()
}
}
}
///|
/// Rebuild the blocks, resolving the references in them and removing the
/// definitions, which are kept for re-emission at the end. The numbering is
/// assigned as the references are met, in document order.
fn resolve_footnote_blocks(
blocks : Array[Block],
defs : Map[String, FootnoteDef],
order : Array[String],
) -> Array[Block] {
let out : Array[Block] = []
for block in blocks {
match block {
Footnote(f) => {
let key = normalize_label(f.label[:])
if defs.contains(key) {
defs[key].blocks = resolve_footnote_blocks(f.blocks, defs, order)
}
}
Quote(children) =>
out.push(Quote(resolve_footnote_blocks(children, defs, order)))
List(list) => {
let items : Array[Item] = []
for item in list.items {
items.push({
blocks: resolve_footnote_blocks(item.blocks, defs, order),
task: item.task,
})
}
out.push(List({ kind: list.kind, tight: list.tight, items, }))
}
Paragraph(inlines) =>
out.push(Paragraph(resolve_footnote_inlines(inlines, defs, order)))
Heading(heading) =>
out.push(
Heading({
level: heading.level,
content: resolve_footnote_inlines(heading.content, defs, order),
}),
)
Table(table) => {
let head : Array[Array[Inline]] = []
for cell in table.head {
head.push(resolve_footnote_inlines(cell, defs, order))
}
let body : Array[Array[Array[Inline]]] = []
for row in table.body {
let cells : Array[Array[Inline]] = []
for cell in row {
cells.push(resolve_footnote_inlines(cell, defs, order))
}
body.push(cells)
}
out.push(Table({ align: table.align, head, body, }))
}
Code(_) | Html(_) | Break => out.push(block)
}
}
out
}
///|
fn resolve_footnote_inlines(
inlines : Array[Inline],
defs : Map[String, FootnoteDef],
order : Array[String],
) -> Array[Inline] {
let out : Array[Inline] = []
for inline in inlines {
out.push(resolve_footnote_inline(inline, defs, order))
}
out
}
///|
fn resolve_footnote_inline(
inline : Inline,
defs : Map[String, FootnoteDef],
order : Array[String],
) -> Inline {
match inline {
FootnoteRef(fnref) => {
let key = normalize_label(fnref.label[:])
if key.length() > 0 && defs.contains(key) {
let def = defs[key]
let number = if def.number == 0 {
let new_number = order.length() + 1
order.push(key)
new_number
} else {
def.number
}
let refs = def.refs + 1
defs[key] = { label: def.label, blocks: def.blocks, number, refs, }
FootnoteRef({ label: def.label, number, ref_ix: refs, })
} else {
// A reference without a definition keeps the text it was written as.
Text("[^\{fnref.label}]")
}
}
Emphasis(children) =>
Emphasis(resolve_footnote_inlines(children, defs, order))
Strong(children) => Strong(resolve_footnote_inlines(children, defs, order))
Strike(children) => Strike(resolve_footnote_inlines(children, defs, order))
Link(link) =>
Link({
dest: link.dest,
title: link.title,
content: resolve_footnote_inlines(link.content, defs, order),
})
Image(link) =>
Image({
dest: link.dest,
title: link.title,
content: resolve_footnote_inlines(link.content, defs, order),
})
_ => inline
}
}