use std::borrow::Cow;
use std::collections::HashMap;
use easydoc_core::{DocError, Result};
use quick_xml::Reader as XmlReader;
use quick_xml::events::Event;
#[derive(Debug, Clone, Default)]
pub struct Numbering {
pub num_to_abstract: HashMap<u32, u32>,
pub abstract_nums: HashMap<u32, AbstractNum>,
}
#[derive(Debug, Clone, Default)]
pub struct AbstractNum {
pub levels: HashMap<u8, Level>,
}
#[derive(Debug, Clone, Default)]
pub struct Level {
pub ordered: bool,
pub start: Option<u32>,
pub ilvl: u8,
}
impl Numbering {
pub fn parse(xml: &str) -> Result<Self> {
let mut numbering = Numbering::default();
let mut reader = XmlReader::from_reader(xml.as_bytes());
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut current_abstract_id: Option<u32> = None;
let mut current_ilvl: Option<u8> = None;
let mut current_num_fmt: Option<String> = None;
let mut current_start: Option<u32> = None;
let mut current_num_id: Option<u32> = None;
let mut in_num: bool = false;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Eof) => break,
Ok(Event::Start(ref start)) => {
let name = start.name();
let local = name.as_ref();
match local {
b"w:abstractNum" => {
current_abstract_id = extract_u32_attr(start, b"w:abstractNumId");
}
b"w:lvl" => {
current_ilvl = extract_u8_attr(start, b"w:ilvl");
current_num_fmt = None;
current_start = None;
}
b"w:numFmt" => {
current_num_fmt = extract_val_attr(start);
}
b"w:start" => {
current_start =
extract_val_attr(start).and_then(|v| v.parse::<u32>().ok());
}
b"w:num" => {
current_num_id = extract_u32_attr(start, b"w:numId");
in_num = true;
}
b"w:abstractNumId" if in_num => {
if let (Some(abstract_id), Some(num_id)) =
(extract_val_attr_u32(start), current_num_id)
{
numbering.num_to_abstract.insert(num_id, abstract_id);
}
}
_ => {}
}
}
Ok(Event::Empty(ref empty)) => {
let name = empty.name();
let local = name.as_ref();
match local {
b"w:numFmt" => {
current_num_fmt = extract_val_attr(empty);
}
b"w:start" => {
current_start =
extract_val_attr(empty).and_then(|v| v.parse::<u32>().ok());
}
b"w:abstractNumId" if in_num => {
if let (Some(abstract_id), Some(num_id)) =
(extract_val_attr_u32(empty), current_num_id)
{
numbering.num_to_abstract.insert(num_id, abstract_id);
}
}
_ => {}
}
}
Ok(Event::End(ref end)) => {
let name = end.name();
let local = name.as_ref();
match local {
b"w:lvl" => {
if let (Some(abstract_id), Some(ilvl)) =
(current_abstract_id, current_ilvl)
{
let ordered =
!matches!(current_num_fmt.as_deref(), Some("bullet") | None);
let level = Level {
ordered,
start: current_start,
ilvl,
};
numbering
.abstract_nums
.entry(abstract_id)
.or_default()
.levels
.insert(ilvl, level);
}
current_ilvl = None;
current_num_fmt = None;
current_start = None;
}
b"w:num" => {
current_num_id = None;
in_num = false;
}
_ => {}
}
}
Err(e) => {
return Err(DocError::Format(format!(
"XML parse error in numbering: {e}"
)));
}
_ => {}
}
buf.clear();
}
Ok(numbering)
}
#[must_use]
pub fn lookup(&self, num_id: u32, ilvl: u8) -> Option<&Level> {
let abstract_id = self.num_to_abstract.get(&num_id)?;
let abstract_num = self.abstract_nums.get(abstract_id)?;
abstract_num.levels.get(&ilvl)
}
}
fn extract_u32_attr(tag: &quick_xml::events::BytesStart, attr_name: &[u8]) -> Option<u32> {
for attr in tag.attributes().flatten() {
if attr.key.as_ref() == attr_name {
let val = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()?;
return val.parse::<u32>().ok();
}
}
None
}
fn extract_u8_attr(tag: &quick_xml::events::BytesStart, attr_name: &[u8]) -> Option<u8> {
for attr in tag.attributes().flatten() {
if attr.key.as_ref() == attr_name {
let val = attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()?;
return val.parse::<u8>().ok();
}
}
None
}
fn extract_val_attr(tag: &quick_xml::events::BytesStart) -> Option<String> {
for attr in tag.attributes().flatten() {
if attr.key.as_ref() == b"w:val" {
return attr
.normalized_value(quick_xml::XmlVersion::Implicit1_0)
.ok()
.map(Cow::into_owned);
}
}
None
}
fn extract_val_attr_u32(tag: &quick_xml::events::BytesStart) -> Option<u32> {
extract_val_attr(tag).and_then(|v| v.parse::<u32>().ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_numbering_bullet_lists_unordered() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0">
<w:numFmt w:val="bullet"/>
<w:lvlText w:val="•"/>
</w:lvl>
<w:lvl w:ilvl="1">
<w:numFmt w:val="bullet"/>
<w:lvlText w:val="–"/>
</w:lvl>
</w:abstractNum>
<w:num w:numId="1">
<w:abstractNumId w:val="0"/>
</w:num>
</w:numbering>"#;
let numbering = Numbering::parse(xml).unwrap();
let level0 = numbering.lookup(1, 0).unwrap();
assert!(!level0.ordered, "bullet should be unordered");
let level1 = numbering.lookup(1, 1).unwrap();
assert!(!level1.ordered, "bullet should be unordered");
}
#[test]
fn parse_numbering_decimal_lists_ordered() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="1">
<w:lvl w:ilvl="0">
<w:start w:val="1"/>
<w:numFmt w:val="decimal"/>
<w:lvlText w:val="%1."/>
</w:lvl>
</w:abstractNum>
<w:num w:numId="2">
<w:abstractNumId w:val="1"/>
</w:num>
</w:numbering>"#;
let numbering = Numbering::parse(xml).unwrap();
let level = numbering.lookup(2, 0).unwrap();
assert!(level.ordered, "decimal should be ordered");
assert_eq!(level.start, Some(1));
}
#[test]
fn parse_numbering_decimal_with_start_value() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0">
<w:start w:val="5"/>
<w:numFmt w:val="decimal"/>
<w:lvlText w:val="%1."/>
</w:lvl>
</w:abstractNum>
<w:num w:numId="1">
<w:abstractNumId w:val="0"/>
</w:num>
</w:numbering>"#;
let numbering = Numbering::parse(xml).unwrap();
let level = numbering.lookup(1, 0).unwrap();
assert!(level.ordered);
assert_eq!(level.start, Some(5), "start should be 5");
}
#[test]
fn lookup_returns_correct_format_for_num_id() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0">
<w:numFmt w:val="bullet"/>
</w:lvl>
</w:abstractNum>
<w:abstractNum w:abstractNumId="1">
<w:lvl w:ilvl="0">
<w:start w:val="1"/>
<w:numFmt w:val="decimal"/>
</w:lvl>
</w:abstractNum>
<w:num w:numId="1">
<w:abstractNumId w:val="0"/>
</w:num>
<w:num w:numId="2">
<w:abstractNumId w:val="1"/>
</w:num>
</w:numbering>"#;
let numbering = Numbering::parse(xml).unwrap();
assert!(!numbering.lookup(1, 0).unwrap().ordered);
assert!(numbering.lookup(2, 0).unwrap().ordered);
assert!(numbering.lookup(99, 0).is_none());
}
#[test]
fn parse_numbering_roman_is_ordered() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0">
<w:start w:val="1"/>
<w:numFmt w:val="upperRoman"/>
<w:lvlText w:val="%1."/>
</w:lvl>
<w:lvl w:ilvl="1">
<w:start w:val="1"/>
<w:numFmt w:val="lowerLetter"/>
<w:lvlText w:val="%2)"/>
</w:lvl>
</w:abstractNum>
<w:num w:numId="1">
<w:abstractNumId w:val="0"/>
</w:num>
</w:numbering>"#;
let numbering = Numbering::parse(xml).unwrap();
assert!(numbering.lookup(1, 0).unwrap().ordered);
assert!(numbering.lookup(1, 1).unwrap().ordered);
}
#[test]
fn parse_empty_numbering() {
let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
</w:numbering>"#;
let numbering = Numbering::parse(xml).unwrap();
assert!(numbering.num_to_abstract.is_empty());
assert!(numbering.abstract_nums.is_empty());
assert!(numbering.lookup(1, 0).is_none());
}
}