// K3 of the annotation writers: footnotes and endnotes on freshly
// written documents. Notes are supplied to `write_docx_with_annotations`
// as plain-content bodies and referenced from body runs via
// `note_reference(kind, index)` (0-based, exactly once per note); the
// writer emits word/footnotes.xml / word/endnotes.xml as MAIN-part
// relationships with the separator/continuationSeparator plumbing notes
// Word expects, the in-note footnoteRef/endnoteRef mark run at the head
// of each note's first paragraph, and positive w:id allocation (index+1)
// clear of the -1/0 plumbing ids.
///|
/// One footnote or endnote body: plain-content, paragraph-only, and no
/// note references of its own (notes do not nest). Construct with
/// `note_spec`.
pub struct NoteSpec {
priv body : Array[DocumentElement]
}
///|
/// Validates and builds a `NoteSpec`. The body rules match comment
/// bodies (non-empty, paragraph-only, plain content) plus the no-nesting
/// rule; they are re-checked at write time because the arrays stay
/// caller-mutable. Which KIND the note is (footnote or endnote) is
/// decided by which array it is passed in, so one spec type serves both.
pub fn note_spec(body : Array[DocumentElement]) -> NoteSpec raise DocxError {
check_annotation_body(body, what="note")
{ body, }
}
///|
/// Every supplied note must be referenced from the body EXACTLY once;
/// the walk also rejects references to notes that were never supplied
/// (by kind and range) before anything is serialized, so the error
/// carries the note index rather than surfacing mid-write.
fn check_note_references(
body : Array[DocumentElement],
footnote_count : Int,
endnote_count : Int,
) -> Unit raise DocxError {
let footnote_uses = Array::make(footnote_count, 0)
let endnote_uses = Array::make(endnote_count, 0)
fn walk(element : DocumentElement) -> Unit raise DocxError {
match element {
NoteReference(note_type~, note_id~) => {
let uses = match note_type {
"footnote" => footnote_uses
"endnote" => endnote_uses
other =>
raise Unsupported(
message="unknown note type '\{other}' (footnote or endnote)",
)
}
match parse_note_index(note_id) {
Some(index) if index < uses.length() => uses[index] += 1
_ =>
raise Unsupported(
message="the \{note_type} reference '\{note_id}' does not name a supplied note (this write has \{uses.length()} \{note_type}(s); references are 0-based indexes)",
)
}
}
Document(children~, ..)
| Paragraph(children~, ..)
| Run(children~, ..)
| Hyperlink(children~, ..)
| Table(children~, ..)
| TableRow(children~, ..)
| TableCell(children~, ..) =>
for child in children {
walk(child)
}
_ => ()
}
}
for element in body {
walk(element)
}
for index, uses in footnote_uses {
if uses == 0 {
raise Unsupported(
message="footnotes[\{index}] is never referenced from the body (each note needs exactly one reference run)",
)
}
if uses > 1 {
raise Unsupported(
message="footnotes[\{index}] is referenced \{uses} times (each note needs exactly one reference run)",
)
}
}
for index, uses in endnote_uses {
if uses == 0 {
raise Unsupported(
message="endnotes[\{index}] is never referenced from the body (each note needs exactly one reference run)",
)
}
if uses > 1 {
raise Unsupported(
message="endnotes[\{index}] is referenced \{uses} times (each note needs exactly one reference run)",
)
}
}
}
///|
/// Serializes word/footnotes.xml or word/endnotes.xml: the two plumbing
/// notes (separator id -1, continuationSeparator id 0 — the ids the
/// reader's scanner suppresses), then one content note per spec with
/// w:id = index+1 and the in-note mark run (w:footnoteRef/w:endnoteRef)
/// leading its first paragraph.
fn notes_part_xml(
kind : String,
notes : Array[NoteSpec],
ctx : WriteContext,
) -> String raise DocxError {
let element_name = if kind == "footnote" { "w:footnote" } else { "w:endnote" }
let mark_name = if kind == "footnote" {
"w:footnoteRef"
} else {
"w:endnoteRef"
}
let root_name = if kind == "footnote" { "w:footnotes" } else { "w:endnotes" }
fn plumbing(note_type : String, id : String, mark : String) -> XmlNode {
XmlElement(
@xml.xml_element(
element_name,
attributes={ "w:type": note_type, "w:id": id },
children=[
XmlElement(
@xml.xml_element("w:p", children=[
XmlElement(
@xml.xml_element("w:r", children=[
XmlElement(@xml.xml_element(mark)),
]),
),
]),
),
],
),
)
}
let children : Array[XmlNode] = [
plumbing("separator", "-1", "w:separator"),
plumbing("continuationSeparator", "0", "w:continuationSeparator"),
]
for index, note in notes {
let mark_run : Array[XmlNode] = [
XmlElement(
@xml.xml_element("w:r", children=[
XmlElement(@xml.xml_element(mark_name)),
]),
),
]
let note_children : Array[XmlNode] = []
for block_index, block in note.body {
if block_index == 0 {
guard block is Paragraph(children=paragraph_children, properties~) else {
// Unreachable: note bodies were validated paragraph-only.
raise Unsupported(message="note bodies are paragraph-only")
}
note_children.push(
XmlElement(
write_paragraph(
paragraph_children,
properties,
ctx,
leading=mark_run,
),
),
)
} else {
note_children.push(XmlElement(write_block(block, ctx)))
}
}
children.push(
XmlElement(
@xml.xml_element(
element_name,
attributes={ "w:id": (index + 1).to_string() },
children=note_children,
),
),
)
}
@xml.write_xml_string(@xml.xml_element(root_name, children~), namespaces={
"w": WORDPROCESSINGML_NAMESPACE,
})
}