use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use quick_xml::{Reader, Writer};
use crate::error::Result;
use crate::namespace::{W_NS, matches_local_name};
use crate::numbering::word_prefixes_at;
use crate::properties::is_word_element;
use crate::text::CT_P;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NoteType {
#[default]
Normal,
Separator,
ContinuationSeparator,
ContinuationNotice,
}
impl NoteType {
fn from_str(s: &str) -> Self {
match s {
"separator" => NoteType::Separator,
"continuationSeparator" => NoteType::ContinuationSeparator,
"continuationNotice" => NoteType::ContinuationNotice,
_ => NoteType::Normal,
}
}
fn to_str(self) -> Option<&'static str> {
match self {
NoteType::Normal => None,
NoteType::Separator => Some("separator"),
NoteType::ContinuationSeparator => Some("continuationSeparator"),
NoteType::ContinuationNotice => Some("continuationNotice"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_Footnote {
pub id: i32,
pub note_type: NoteType,
pub paragraphs: Vec<CT_P>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CT_Footnotes {
pub footnotes: Vec<CT_Footnote>,
}
#[allow(non_snake_case)]
impl CT_Footnotes {
pub fn new() -> Self {
CT_Footnotes {
footnotes: Vec::new(),
}
}
pub fn get_by_id(&self, id: i32) -> Option<&CT_Footnote> {
self.footnotes
.iter()
.find(|f| f.id == id && f.note_type == NoteType::Normal)
}
pub fn has_continuation_separator(&self) -> bool {
self.footnotes
.iter()
.any(|f| f.note_type == NoteType::ContinuationSeparator)
}
pub fn from_xml(xml: &[u8]) -> Result<Self> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut footnotes = Vec::new();
let mut buf = Vec::new();
let mut word_prefixes = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name();
let prefixes = word_prefixes_at(e, &word_prefixes)?;
if is_word_element(name.as_ref(), b"footnote", &prefixes)
|| is_word_element(name.as_ref(), b"endnote", &prefixes)
{
let mut id: i32 = 0;
let mut note_type = NoteType::Normal;
for attr in e.attributes().flatten() {
if matches_local_name(attr.key.as_ref(), b"id") {
id = std::str::from_utf8(&attr.value)
.unwrap_or("0")
.parse()
.unwrap_or(0);
} else if matches_local_name(attr.key.as_ref(), b"type") {
note_type = NoteType::from_str(
std::str::from_utf8(&attr.value).unwrap_or(""),
);
}
}
if note_type == NoteType::Normal && id <= 0 {
note_type = NoteType::Separator;
}
let paragraphs = parse_footnote_content(&mut reader, &prefixes)?;
footnotes.push(CT_Footnote {
id,
note_type,
paragraphs,
});
} else if is_word_element(name.as_ref(), b"footnotes", &prefixes)
|| is_word_element(name.as_ref(), b"endnotes", &prefixes)
{
word_prefixes = prefixes;
} else {
reader.read_to_end_into(name, &mut Vec::new())?;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(CT_Footnotes { footnotes })
}
pub fn to_xml_footnotes(&self) -> Result<Vec<u8>> {
self.to_xml_root("w:footnotes", "w:footnote")
}
pub fn to_xml_endnotes(&self) -> Result<Vec<u8>> {
self.to_xml_root("w:endnotes", "w:endnote")
}
fn to_xml_root(&self, root_tag: &str, item_tag: &str) -> Result<Vec<u8>> {
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
writer.write_event(Event::Decl(BytesDecl::new(
"1.0",
Some("UTF-8"),
Some("yes"),
)))?;
let mut start = BytesStart::new(root_tag);
start.push_attribute(("xmlns:w", W_NS));
start.push_attribute((
"xmlns:r",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships",
));
writer.write_event(Event::Start(start))?;
let mut buf = itoa::Buffer::new();
for footnote in &self.footnotes {
let mut fn_start = BytesStart::new(item_tag);
if let Some(type_str) = footnote.note_type.to_str() {
fn_start.push_attribute(("w:type", type_str));
}
fn_start.push_attribute(("w:id", buf.format(footnote.id)));
writer.write_event(Event::Start(fn_start))?;
for p in &footnote.paragraphs {
p.to_xml(&mut writer)?;
}
writer.write_event(Event::End(BytesEnd::new(item_tag)))?;
}
writer.write_event(Event::End(BytesEnd::new(root_tag)))?;
Ok(writer.into_inner())
}
}
impl Default for CT_Footnotes {
fn default() -> Self {
Self::new()
}
}
fn parse_footnote_content(
reader: &mut Reader<&[u8]>,
word_prefixes: &[String],
) -> Result<Vec<CT_P>> {
let mut paragraphs = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name();
let prefixes = word_prefixes_at(e, word_prefixes)?;
if is_word_element(name.as_ref(), b"p", &prefixes) {
paragraphs.push(CT_P::from_xml_with_prefixes(reader, &prefixes)?);
} else {
reader.read_to_end_into(name, &mut Vec::new())?;
}
}
Ok(Event::End(ref e)) => {
let name = e.name();
if matches_local_name(name.as_ref(), b"footnote")
|| matches_local_name(name.as_ref(), b"endnote")
{
break;
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(e.into()),
_ => {}
}
buf.clear();
}
Ok(paragraphs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_footnotes_xml() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnote w:id="0">
<w:p><w:r><w:t>separator</w:t></w:r></w:p>
</w:footnote>
<w:footnote w:id="1">
<w:p><w:r><w:t>First footnote text.</w:t></w:r></w:p>
</w:footnote>
<w:footnote w:id="2">
<w:p><w:r><w:t>Second footnote.</w:t></w:r></w:p>
<w:p><w:r><w:t>With two paragraphs.</w:t></w:r></w:p>
</w:footnote>
</w:footnotes>"#;
let footnotes = CT_Footnotes::from_xml(xml).unwrap();
assert_eq!(footnotes.footnotes.len(), 3);
assert_eq!(footnotes.footnotes[0].note_type, NoteType::Separator);
assert!(footnotes.get_by_id(0).is_none());
let first = footnotes.get_by_id(1).unwrap();
assert_eq!(first.paragraphs.len(), 1);
assert_eq!(first.paragraphs[0].text(), "First footnote text.");
assert_eq!(footnotes.get_by_id(2).unwrap().paragraphs.len(), 2);
}
#[test]
fn parse_endnotes_xml() {
let xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:endnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:endnote w:id="0">
<w:p><w:r><w:t>separator</w:t></w:r></w:p>
</w:endnote>
<w:endnote w:id="1">
<w:p><w:r><w:t>An endnote.</w:t></w:r></w:p>
</w:endnote>
</w:endnotes>"#;
let endnotes = CT_Footnotes::from_xml(xml).unwrap();
assert_eq!(endnotes.footnotes.len(), 2);
assert_eq!(endnotes.footnotes[0].note_type, NoteType::Separator);
assert_eq!(
endnotes.get_by_id(1).unwrap().paragraphs[0].text(),
"An endnote."
);
}
#[test]
fn aliased_footnote_paragraph_properties_keep_root_scope() {
let xml = format!(
r#"<q:footnotes xmlns:q="{W_NS}" xmlns:ext="urn:producer"><ext:footnote ext:id="9"><ext:p/></ext:footnote><q:footnote q:id="1"><ext:p><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:p><q:p><q:pPr><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr><q:r><q:t>Note</q:t></q:r></q:p></q:footnote></q:footnotes>"#
);
let footnotes = CT_Footnotes::from_xml(xml.as_bytes()).unwrap();
assert_eq!(footnotes.footnotes.len(), 1);
assert_eq!(footnotes.footnotes[0].paragraphs.len(), 1);
let paragraph = &footnotes.footnotes[0].paragraphs[0];
assert_eq!(paragraph.text(), "Note");
assert_eq!(
paragraph.properties.as_ref().unwrap().jc,
Some(crate::shared::ST_Jc::Center)
);
}
#[test]
fn default_namespace_footnote_properties_keep_root_scope() {
let xml = format!(
r#"<footnotes xmlns="{W_NS}" xmlns:w="{W_NS}" xmlns:ext="urn:producer"><ext:footnote ext:id="9"><ext:p/></ext:footnote><footnote w:id="1"><ext:p><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:p><p><pPr><ext:jc ext:val="right"/><jc w:val="center"/></pPr><r><t>Note</t></r></p></footnote></footnotes>"#
);
let footnotes = CT_Footnotes::from_xml(xml.as_bytes()).unwrap();
assert_eq!(footnotes.footnotes.len(), 1);
assert_eq!(footnotes.footnotes[0].paragraphs.len(), 1);
let paragraph = &footnotes.footnotes[0].paragraphs[0];
assert_eq!(paragraph.text(), "Note");
assert_eq!(
paragraph.properties.as_ref().unwrap().jc,
Some(crate::shared::ST_Jc::Center)
);
}
#[test]
fn get_footnote_by_id() {
let footnotes = CT_Footnotes {
footnotes: vec![
CT_Footnote {
id: 1,
note_type: NoteType::Normal,
paragraphs: vec![],
},
CT_Footnote {
id: 2,
note_type: NoteType::Normal,
paragraphs: vec![],
},
],
};
assert!(footnotes.get_by_id(1).is_some());
assert!(footnotes.get_by_id(2).is_some());
assert!(footnotes.get_by_id(3).is_none());
}
#[test]
fn round_trip_footnotes() {
let mut fn1_para = CT_P::new();
fn1_para.add_run("First footnote.");
let mut fn2_para = CT_P::new();
fn2_para.add_run("Second footnote.");
let footnotes = CT_Footnotes {
footnotes: vec![
CT_Footnote {
id: 1,
note_type: NoteType::Normal,
paragraphs: vec![fn1_para],
},
CT_Footnote {
id: 2,
note_type: NoteType::Normal,
paragraphs: vec![fn2_para],
},
],
};
let xml = footnotes.to_xml_footnotes().unwrap();
let parsed = CT_Footnotes::from_xml(&xml).unwrap();
assert_eq!(parsed.footnotes.len(), 2);
assert_eq!(parsed.footnotes[0].id, 1);
assert_eq!(parsed.footnotes[0].paragraphs[0].text(), "First footnote.");
assert_eq!(parsed.footnotes[1].id, 2);
assert_eq!(parsed.footnotes[1].paragraphs[0].text(), "Second footnote.");
}
const WITH_SEPARATORS: &str = r#"<?xml version="1.0"?>
<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnote w:type="separator" w:id="0"><w:p><w:r><w:separator/></w:r></w:p></w:footnote>
<w:footnote w:type="continuationSeparator" w:id="1"><w:p><w:r><w:continuationSeparator/></w:r></w:p></w:footnote>
<w:footnote w:id="2"><w:p><w:r><w:t>A real note.</w:t></w:r></w:p></w:footnote>
</w:footnotes>"#;
#[test]
fn a_separator_definition_survives_open_and_save() {
let parsed = CT_Footnotes::from_xml(WITH_SEPARATORS.as_bytes()).unwrap();
assert_eq!(parsed.footnotes.len(), 3, "separators must be retained");
let round_tripped = CT_Footnotes::from_xml(&parsed.to_xml_footnotes().unwrap()).unwrap();
assert_eq!(round_tripped.footnotes.len(), 3);
assert_eq!(round_tripped.footnotes[0].note_type, NoteType::Separator);
assert_eq!(round_tripped.footnotes[0].id, 0);
assert_eq!(
round_tripped.footnotes[1].note_type,
NoteType::ContinuationSeparator
);
assert_eq!(round_tripped.footnotes[1].id, 1);
assert_eq!(round_tripped.footnotes[2].note_type, NoteType::Normal);
assert_eq!(round_tripped, parsed, "a second trip must be a fixed point");
}
#[test]
fn get_by_id_does_not_return_a_separator() {
let parsed = CT_Footnotes::from_xml(WITH_SEPARATORS.as_bytes()).unwrap();
assert!(parsed.get_by_id(0).is_none(), "separator is not a note");
assert!(
parsed.get_by_id(1).is_none(),
"continuation separator is not a note"
);
assert_eq!(
parsed.get_by_id(2).unwrap().paragraphs[0].text(),
"A real note."
);
assert!(parsed.has_continuation_separator());
}
#[test]
fn note_types_are_read_through_a_foreign_prefix() {
let xml = r#"<?xml version="1.0"?>
<x:footnotes xmlns:x="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<x:footnote x:type="separator" x:id="0"><x:p/></x:footnote>
<x:footnote x:id="7"><x:p><x:r><x:t>Prefixed.</x:t></x:r></x:p></x:footnote>
</x:footnotes>"#;
let parsed = CT_Footnotes::from_xml(xml.as_bytes()).unwrap();
assert_eq!(parsed.footnotes.len(), 2);
assert_eq!(parsed.footnotes[0].note_type, NoteType::Separator);
assert!(parsed.get_by_id(0).is_none());
assert_eq!(
parsed.get_by_id(7).unwrap().paragraphs[0].text(),
"Prefixed."
);
}
#[test]
fn an_unknown_note_type_reads_as_a_normal_note() {
let xml = r#"<?xml version="1.0"?>
<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:footnote w:type="somethingNew" w:id="3"><w:p><w:r><w:t>Note.</w:t></w:r></w:p></w:footnote>
</w:footnotes>"#;
let parsed = CT_Footnotes::from_xml(xml.as_bytes()).unwrap();
assert_eq!(parsed.footnotes[0].note_type, NoteType::Normal);
assert!(parsed.get_by_id(3).is_some());
}
}