use crate::types::*;
use crate::{CmlError, Result};
use quick_xml::events::{BytesStart, Event};
use quick_xml::Reader;
use std::io::BufRead;
pub struct CmlParser;
impl CmlParser {
pub fn parse_str(xml: &str) -> Result<CmlDocument> {
let reader = Reader::from_str(xml);
Self::parse(reader)
}
pub fn parse<R: BufRead>(mut reader: Reader<R>) -> Result<CmlDocument> {
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"cml" => {
return Self::parse_cml(&mut reader, e);
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"No <cml> root element found".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
}
fn parse_cml<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<CmlDocument> {
let mut version = None;
let mut encoding = None;
let mut profile = None;
let mut id = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"version" => version = Some(value),
b"encoding" => encoding = Some(value),
b"profile" => profile = Some(value),
b"id" => id = Some(value),
_ => {}
}
}
let version = version
.ok_or_else(|| CmlError::MissingAttribute("version required on <cml>".to_string()))?;
let encoding = encoding
.ok_or_else(|| CmlError::MissingAttribute("encoding required on <cml>".to_string()))?;
let profile = profile
.ok_or_else(|| CmlError::MissingAttribute("profile required on <cml>".to_string()))?;
let mut header = None;
let mut body = None;
let mut footer = None;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => match e.name().as_ref() {
b"header" => {
header = Some(Self::parse_header(reader)?);
}
b"body" => {
body = Some(Self::parse_body(reader)?);
}
b"footer" => {
footer = Some(Self::parse_footer(reader)?);
}
_ => {}
},
Ok(Event::End(e)) if e.name().as_ref() == b"cml" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <cml>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
let header =
header.ok_or_else(|| CmlError::InvalidStructure("<header> required".to_string()))?;
let body = body.ok_or_else(|| CmlError::InvalidStructure("<body> required".to_string()))?;
let footer =
footer.ok_or_else(|| CmlError::InvalidStructure("<footer> required".to_string()))?;
Ok(CmlDocument {
version,
encoding,
profile,
id,
header,
body,
footer,
})
}
fn parse_header<R: BufRead>(reader: &mut Reader<R>) -> Result<Header> {
let mut title = None;
let mut authors = Vec::new();
let mut dates = Vec::new();
let mut identifiers = Vec::new();
let mut version = None;
let mut description = None;
let mut provenance = None;
let mut source = None;
let mut meta = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => match e.name().as_ref() {
b"title" => {
title = Some(Self::read_text(reader, "title")?);
}
b"author" => {
authors.push(Self::parse_author(reader, e)?);
}
b"identifier" => {
identifiers.push(Self::parse_identifier(reader, e)?);
}
b"version" => {
version = Some(Self::read_text(reader, "version")?);
}
b"description" => {
description = Some(Self::read_text(reader, "description")?);
}
b"provenance" => {
provenance = Some(Self::read_text(reader, "provenance")?);
}
b"source" => {
source = Some(Self::read_text(reader, "source")?);
}
_ => {}
},
Ok(Event::Empty(e)) => match e.name().as_ref() {
b"date" => {
dates.push(Self::parse_date_entry(e)?);
}
b"meta" => {
meta.push(Self::parse_meta_entry(e)?);
}
_ => {}
},
Ok(Event::End(e)) if e.name().as_ref() == b"header" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <header>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
let title = title
.ok_or_else(|| CmlError::InvalidStructure("<title> required in header".to_string()))?;
Ok(Header {
title,
authors,
dates,
identifiers,
version,
description,
provenance,
source,
meta,
})
}
fn parse_author<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Author> {
let mut role = None;
let mut reference = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"role" => role = Some(value),
b"reference" => reference = Some(value),
_ => {}
}
}
let name = Self::read_text(reader, "author")?;
Ok(Author {
name,
role,
reference,
})
}
fn parse_date_entry(start: BytesStart) -> Result<DateEntry> {
let mut date_type = None;
let mut when = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"type" => date_type = Some(value),
b"when" => when = Some(value),
_ => {}
}
}
let date_type = date_type
.ok_or_else(|| CmlError::MissingAttribute("type required on <date>".to_string()))?;
let when =
when.ok_or_else(|| CmlError::MissingAttribute("when required on <date>".to_string()))?;
Ok(DateEntry { date_type, when })
}
fn parse_identifier<R: BufRead>(
reader: &mut Reader<R>,
start: BytesStart,
) -> Result<Identifier> {
let mut scheme = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
if key == b"scheme" {
scheme = Some(value);
}
}
let scheme = scheme.ok_or_else(|| {
CmlError::MissingAttribute("scheme required on <identifier>".to_string())
})?;
let value = Self::read_text(reader, "identifier")?;
Ok(Identifier { scheme, value })
}
fn parse_meta_entry(start: BytesStart) -> Result<MetaEntry> {
let mut name = None;
let mut value = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let attr_value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"name" => name = Some(attr_value),
b"value" => value = Some(attr_value),
_ => {}
}
}
let name =
name.ok_or_else(|| CmlError::MissingAttribute("name required on <meta>".to_string()))?;
let value = value
.ok_or_else(|| CmlError::MissingAttribute("value required on <meta>".to_string()))?;
Ok(MetaEntry { name, value })
}
fn parse_body<R: BufRead>(reader: &mut Reader<R>) -> Result<Body> {
let blocks = Self::parse_blocks_until(reader, b"body")?;
if blocks.is_empty() {
return Err(CmlError::InvalidStructure(
"<body> must contain at least one block element".to_string(),
));
}
Ok(Body { blocks })
}
fn parse_blocks_until<R: BufRead>(
reader: &mut Reader<R>,
end_tag: &[u8],
) -> Result<Vec<BlockElement>> {
let mut blocks = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
if let Some(block) = Self::parse_block_element(reader, e)? {
blocks.push(block);
}
}
Ok(Event::Empty(e)) => {
if e.name().as_ref() == b"break" {
blocks.push(BlockElement::Break(Self::parse_break_empty(e)?));
}
}
Ok(Event::End(e)) if e.name().as_ref() == end_tag => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(format!(
"Unexpected EOF waiting for end tag: {}",
String::from_utf8_lossy(end_tag)
)))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(blocks)
}
fn parse_block_element<R: BufRead>(
reader: &mut Reader<R>,
start: BytesStart,
) -> Result<Option<BlockElement>> {
let element = match start.name().as_ref() {
b"section" => BlockElement::Section(Self::parse_section(reader, start)?),
b"paragraph" => BlockElement::Paragraph(Self::parse_paragraph(reader, start)?),
b"heading" => BlockElement::Heading(Self::parse_heading(reader, start)?),
b"aside" => BlockElement::Aside(Self::parse_aside(reader, start)?),
b"quote" => BlockElement::Quote(Self::parse_quote(reader, start)?),
b"list" => BlockElement::List(Self::parse_list(reader, start)?),
b"table" => BlockElement::Table(Self::parse_table(reader, start)?),
b"code" => BlockElement::Code(Self::parse_code(reader, start)?),
b"break" => BlockElement::Break(Self::parse_break(reader, start)?),
b"figure" => BlockElement::Figure(Self::parse_figure(reader, start)?),
_ => return Ok(None),
};
Ok(Some(element))
}
fn parse_section<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Section> {
let mut id = None;
let mut section_type = None;
let mut reference = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => section_type = Some(value),
b"ref" => reference = Some(value),
_ => {}
}
}
let content = Self::parse_blocks_until(reader, b"section")?;
Ok(Section {
id,
section_type,
reference,
content,
})
}
fn parse_paragraph<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Paragraph> {
let mut id = None;
let mut paragraph_type = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => paragraph_type = Some(value),
_ => {}
}
}
let content = Self::parse_inline_content(reader, b"paragraph")?;
Ok(Paragraph {
id,
paragraph_type,
content,
})
}
fn parse_heading<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Heading> {
let mut id = None;
let mut heading_type = None;
let mut size = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => heading_type = Some(value),
b"size" => {
size = Some(value.parse().map_err(|_| {
CmlError::InvalidAttribute("size must be a number".to_string())
})?)
}
_ => {}
}
}
let size = size
.ok_or_else(|| CmlError::MissingAttribute("size required on <heading>".to_string()))?;
let content = Self::parse_inline_content(reader, b"heading")?;
Ok(Heading {
id,
heading_type,
size,
content,
})
}
fn parse_aside<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Aside> {
let mut id = None;
let mut aside_type = None;
let mut side = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => aside_type = Some(value),
b"side" => {
side = Some(match value.as_str() {
"left" => Side::Left,
"right" => Side::Right,
_ => {
return Err(CmlError::InvalidAttribute(
"side must be 'left' or 'right'".to_string(),
))
}
})
}
_ => {}
}
}
let side =
side.ok_or_else(|| CmlError::MissingAttribute("side required on <aside>".to_string()))?;
let content = Self::parse_blocks_until(reader, b"aside")?;
Ok(Aside {
id,
aside_type,
side,
content,
})
}
fn parse_quote<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Quote> {
let mut id = None;
let mut reference = None;
let mut source = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"ref" => reference = Some(value),
b"source" => source = Some(value),
_ => {}
}
}
let content = Self::parse_blocks_until(reader, b"quote")?;
Ok(Quote {
id,
reference,
source,
content,
})
}
fn parse_list<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<List> {
let mut id = None;
let mut list_type = None;
let mut style = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => {
list_type = Some(match value.as_str() {
"ordered" => ListType::Ordered,
"unordered" => ListType::Unordered,
_ => {
return Err(CmlError::InvalidAttribute(
"list type must be 'ordered' or 'unordered'".to_string(),
))
}
})
}
b"style" => {
style = Some(match value.as_str() {
"numeric" => ListStyle::Numeric,
"roman" => ListStyle::Roman,
"alpha" => ListStyle::Alpha,
"symbolic" => ListStyle::Symbolic,
_ => {
return Err(CmlError::InvalidAttribute(
"invalid list style".to_string(),
))
}
})
}
_ => {}
}
}
let mut items = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"item" => {
items.push(Self::parse_list_item(reader, e)?);
}
Ok(Event::End(e)) if e.name().as_ref() == b"list" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <list>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
if items.is_empty() {
return Err(CmlError::InvalidStructure(
"<list> must contain at least one <item>".to_string(),
));
}
Ok(List {
id,
list_type,
style,
items,
})
}
fn parse_list_item<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<ListItem> {
let mut id = None;
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"id" {
id = Some(String::from_utf8_lossy(&attr.value).to_string());
}
}
let content = Self::parse_list_item_content(reader)?;
Ok(ListItem { id, content })
}
fn parse_list_item_content<R: BufRead>(reader: &mut Reader<R>) -> Result<ListItemContent> {
let mut blocks = Vec::new();
let mut inlines = Vec::new();
let mut has_blocks = false;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
if Self::is_block_element(name.as_ref()) {
has_blocks = true;
if let Some(block) = Self::parse_block_element(reader, e)? {
blocks.push(block);
}
} else if let Some(inline) = Self::parse_inline_element(reader, e)? {
inlines.push(inline);
}
}
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap().to_string().trim().to_string();
if !text.is_empty() {
inlines.push(InlineElement::Text(text));
}
}
Ok(Event::End(e)) if e.name().as_ref() == b"item" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <item>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
if has_blocks {
Ok(ListItemContent::Block(blocks))
} else {
Ok(ListItemContent::Inline(inlines))
}
}
fn is_block_element(name: &[u8]) -> bool {
matches!(
name,
b"section"
| b"paragraph"
| b"heading"
| b"aside"
| b"quote"
| b"list"
| b"table"
| b"code"
| b"break"
| b"figure"
)
}
fn parse_table<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Table> {
let mut id = None;
let mut table_type = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => table_type = Some(value),
_ => {}
}
}
let mut header = None;
let mut body = None;
let mut footer = None;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => match e.name().as_ref() {
b"header" => {
header = Some(Self::parse_table_header(reader)?);
}
b"body" => {
body = Some(Self::parse_table_body(reader)?);
}
b"footer" => {
footer = Some(Self::parse_table_footer(reader)?);
}
_ => {}
},
Ok(Event::End(e)) if e.name().as_ref() == b"table" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <table>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
let body =
body.ok_or_else(|| CmlError::InvalidStructure("<body> required in table".to_string()))?;
Ok(Table {
id,
table_type,
header,
body,
footer,
})
}
fn parse_table_header<R: BufRead>(reader: &mut Reader<R>) -> Result<TableHeader> {
let mut rows = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"row" => {
rows.push(Self::parse_table_row(reader, true)?);
}
Ok(Event::End(e)) if e.name().as_ref() == b"header" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in table <header>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(TableHeader { rows })
}
fn parse_table_body<R: BufRead>(reader: &mut Reader<R>) -> Result<TableBody> {
let mut rows = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"row" => {
rows.push(Self::parse_table_row(reader, false)?);
}
Ok(Event::End(e)) if e.name().as_ref() == b"body" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in table <body>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(TableBody { rows })
}
fn parse_table_footer<R: BufRead>(reader: &mut Reader<R>) -> Result<TableFooter> {
let mut caption = None;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"caption" => {
let content = Self::parse_inline_content(reader, b"caption")?;
caption = Some(Caption { content });
}
Ok(Event::End(e)) if e.name().as_ref() == b"footer" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in table <footer>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
let caption = caption.ok_or_else(|| {
CmlError::InvalidStructure("<caption> required in table footer".to_string())
})?;
Ok(TableFooter { caption })
}
fn parse_table_row<R: BufRead>(reader: &mut Reader<R>, is_header: bool) -> Result<TableRow> {
let mut columns = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"column" => {
columns.push(Self::parse_table_column(reader, e, is_header)?);
}
Ok(Event::End(e)) if e.name().as_ref() == b"row" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in table <row>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(TableRow { columns })
}
fn parse_table_column<R: BufRead>(
reader: &mut Reader<R>,
start: BytesStart,
is_header: bool,
) -> Result<TableColumn> {
let mut sort = None;
if is_header {
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"sort" {
let value = String::from_utf8_lossy(&attr.value).to_string();
sort = Some(match value.as_str() {
"asc" => SortOrder::Asc,
"desc" => SortOrder::Desc,
_ => {
return Err(CmlError::InvalidAttribute(
"sort must be 'asc' or 'desc'".to_string(),
))
}
});
}
}
}
let cell = Self::parse_table_cell(reader)?;
Ok(TableColumn { sort, cell })
}
fn parse_table_cell<R: BufRead>(reader: &mut Reader<R>) -> Result<TableCell> {
let mut buf = Vec::new();
let mut colspan = None;
let mut rowspan = None;
let mut content = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"cell" => {
for attr in e.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"colspan" => {
colspan = Some(value.parse().map_err(|_| {
CmlError::InvalidAttribute(
"colspan must be a number".to_string(),
)
})?)
}
b"rowspan" => {
rowspan = Some(value.parse().map_err(|_| {
CmlError::InvalidAttribute(
"rowspan must be a number".to_string(),
)
})?)
}
_ => {}
}
}
content = Self::parse_inline_content(reader, b"cell")?;
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Expected <cell> in column".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(TableCell {
colspan,
rowspan,
content,
})
}
fn parse_code<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Code> {
let mut id = None;
let mut lang = None;
let mut copyable = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"lang" => lang = Some(value),
b"copyable" => {
copyable = Some(match value.as_str() {
"true" => true,
"false" => false,
_ => {
return Err(CmlError::InvalidAttribute(
"copyable must be 'true' or 'false'".to_string(),
))
}
})
}
_ => {}
}
}
let content = Self::read_text(reader, "code")?;
Ok(Code {
id,
lang,
copyable,
content,
})
}
fn parse_break<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Break> {
let mut break_type = None;
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"type" {
break_type = Some(String::from_utf8_lossy(&attr.value).to_string());
}
}
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::End(e)) if e.name().as_ref() == b"break" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <break>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(Break { break_type })
}
fn parse_break_empty(start: BytesStart) -> Result<Break> {
let mut break_type = None;
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"type" {
break_type = Some(String::from_utf8_lossy(&attr.value).to_string());
}
}
Ok(Break { break_type })
}
fn parse_figure<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Figure> {
let mut id = None;
let mut figure_type = None;
let mut reference = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => figure_type = Some(value),
b"ref" => reference = Some(value),
_ => {}
}
}
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::End(e)) if e.name().as_ref() == b"figure" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <figure>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(Figure {
id,
figure_type,
reference,
})
}
fn parse_inline_content<R: BufRead>(
reader: &mut Reader<R>,
end_tag: &[u8],
) -> Result<Vec<InlineElement>> {
let mut elements = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
if let Some(inline) = Self::parse_inline_element(reader, e)? {
elements.push(inline);
}
}
Ok(Event::Empty(e)) => {
if e.name().as_ref() == b"end" {
elements.push(InlineElement::End(Self::parse_end_empty(e)?));
}
}
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap().to_string();
if !text.trim().is_empty() {
elements.push(InlineElement::Text(text));
}
}
Ok(Event::End(e)) if e.name().as_ref() == end_tag => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(format!(
"Unexpected EOF waiting for end tag: {}",
String::from_utf8_lossy(end_tag)
)))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(elements)
}
fn parse_inline_element<R: BufRead>(
reader: &mut Reader<R>,
start: BytesStart,
) -> Result<Option<InlineElement>> {
let element = match start.name().as_ref() {
b"em" => InlineElement::Em(Self::parse_em(reader, start)?),
b"bo" => InlineElement::Bo(Self::parse_bo(reader)?),
b"un" => InlineElement::Un(Self::parse_un(reader)?),
b"st" => InlineElement::St(Self::parse_st(reader)?),
b"snip" => InlineElement::Snip(Self::parse_snip(reader, start)?),
b"key" => InlineElement::Key(Self::parse_key(reader)?),
b"rf" => InlineElement::Rf(Self::parse_rf(reader, start)?),
b"tg" => InlineElement::Tg(Self::parse_tg(reader, start)?),
b"lk" => InlineElement::Lk(Self::parse_lk(reader, start)?),
b"curr" => InlineElement::Curr(Self::parse_curr(reader, start)?),
b"end" => InlineElement::End(Self::parse_end(reader, start)?),
_ => return Ok(None),
};
Ok(Some(element))
}
fn parse_em<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Em> {
let mut em_type = None;
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"type" {
let value = String::from_utf8_lossy(&attr.value).to_string();
em_type = Some(match value.as_str() {
"stress" => EmphasisType::Stress,
"contrast" => EmphasisType::Contrast,
_ => {
return Err(CmlError::InvalidAttribute(
"em type must be 'stress' or 'contrast'".to_string(),
))
}
});
}
}
let content = Self::parse_inline_content(reader, b"em")?;
Ok(Em { em_type, content })
}
fn parse_bo<R: BufRead>(reader: &mut Reader<R>) -> Result<Bo> {
let content = Self::parse_inline_content(reader, b"bo")?;
Ok(Bo { content })
}
fn parse_un<R: BufRead>(reader: &mut Reader<R>) -> Result<Un> {
let content = Self::parse_inline_content(reader, b"un")?;
Ok(Un { content })
}
fn parse_st<R: BufRead>(reader: &mut Reader<R>) -> Result<St> {
let content = Self::parse_inline_content(reader, b"st")?;
Ok(St { content })
}
fn parse_snip<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Snip> {
let mut char = None;
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"char" {
char = Some(String::from_utf8_lossy(&attr.value).to_string());
}
}
let content = Self::read_text(reader, "snip")?;
Ok(Snip { char, content })
}
fn parse_key<R: BufRead>(reader: &mut Reader<R>) -> Result<Key> {
let content = Self::read_text(reader, "key")?;
Ok(Key { content })
}
fn parse_rf<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Rf> {
let mut reference = None;
let mut role = None;
let mut title = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"ref" => reference = Some(value),
b"role" => role = Some(value),
b"title" => title = Some(value),
_ => {}
}
}
let reference = reference
.ok_or_else(|| CmlError::MissingAttribute("ref required on <rf>".to_string()))?;
let content = Self::read_text(reader, "rf")?;
Ok(Rf {
reference,
role,
title,
content,
})
}
fn parse_tg<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Tg> {
let mut reference = None;
let mut role = None;
let mut title = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"ref" => reference = Some(value),
b"role" => role = Some(value),
b"title" => title = Some(value),
_ => {}
}
}
let reference = reference
.ok_or_else(|| CmlError::MissingAttribute("ref required on <tg>".to_string()))?;
let content = Self::read_text(reader, "tg")?;
Ok(Tg {
reference,
role,
title,
content,
})
}
fn parse_lk<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Lk> {
let mut reference = None;
let mut role = None;
let mut title = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"ref" => reference = Some(value),
b"role" => role = Some(value),
b"title" => title = Some(value),
_ => {}
}
}
let reference = reference
.ok_or_else(|| CmlError::MissingAttribute("ref required on <lk>".to_string()))?;
let content = Self::read_text(reader, "lk")?;
Ok(Lk {
reference,
role,
title,
content,
})
}
fn parse_curr<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Curr> {
let mut currency_type = None;
let mut format = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"type" => currency_type = Some(value),
b"format" => {
format = Some(match value.as_str() {
"symbol" => CurrencyFormat::Symbol,
"code" => CurrencyFormat::Code,
"name" => CurrencyFormat::Name,
_ => {
return Err(CmlError::InvalidAttribute(
"currency format must be 'symbol', 'code', or 'name'".to_string(),
))
}
})
}
_ => {}
}
}
let currency_type = currency_type
.ok_or_else(|| CmlError::MissingAttribute("type required on <curr>".to_string()))?;
let value = Self::read_text(reader, "curr")?;
Ok(Curr {
currency_type,
format,
value,
})
}
fn parse_end<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<End> {
let mut kind = None;
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"kind" {
let value = String::from_utf8_lossy(&attr.value).to_string();
kind = Some(match value.as_str() {
"line" => EndKind::Line,
"verse" => EndKind::Verse,
"item" => EndKind::Item,
_ => {
return Err(CmlError::InvalidAttribute(
"end kind must be 'line', 'verse', or 'item'".to_string(),
))
}
});
}
}
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::End(e)) if e.name().as_ref() == b"end" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <end>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(End { kind })
}
fn parse_end_empty(start: BytesStart) -> Result<End> {
let mut kind = None;
for attr in start.attributes() {
let attr = attr?;
if attr.key.as_ref() == b"kind" {
let value = String::from_utf8_lossy(&attr.value).to_string();
kind = Some(match value.as_str() {
"line" => EndKind::Line,
"verse" => EndKind::Verse,
"item" => EndKind::Item,
_ => {
return Err(CmlError::InvalidAttribute(
"end kind must be 'line', 'verse', or 'item'".to_string(),
))
}
});
}
}
Ok(End { kind })
}
fn parse_footer<R: BufRead>(reader: &mut Reader<R>) -> Result<Footer> {
let mut signatures = None;
let mut citations = None;
let mut annotations = None;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => match e.name().as_ref() {
b"signatures" => {
signatures = Some(Self::parse_signatures(reader)?);
}
b"citations" => {
citations = Some(Self::parse_citations(reader)?);
}
b"annotations" => {
annotations = Some(Self::parse_annotations(reader)?);
}
_ => {}
},
Ok(Event::End(e)) if e.name().as_ref() == b"footer" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <footer>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(Footer {
signatures,
citations,
annotations,
})
}
fn parse_signatures<R: BufRead>(reader: &mut Reader<R>) -> Result<Signatures> {
let mut signatures = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"signature" => {
signatures.push(Self::parse_signature(reader, e)?);
}
Ok(Event::End(e)) if e.name().as_ref() == b"signatures" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <signatures>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(Signatures { signatures })
}
fn parse_signature<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Signature> {
let mut when = None;
let mut role = None;
let mut reference = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"when" => when = Some(value),
b"role" => role = Some(value),
b"ref" => reference = Some(value),
_ => {}
}
}
let when = when.ok_or_else(|| {
CmlError::MissingAttribute("when required on <signature>".to_string())
})?;
let content = Self::read_text(reader, "signature")?;
Ok(Signature {
when,
role,
reference,
content,
})
}
fn parse_citations<R: BufRead>(reader: &mut Reader<R>) -> Result<Citations> {
let mut citations = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"citation" => {
citations.push(Self::parse_citation(reader, e)?);
}
Ok(Event::End(e)) if e.name().as_ref() == b"citations" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <citations>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(Citations { citations })
}
fn parse_citation<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Citation> {
let mut reference = None;
let mut citation_type = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"ref" => reference = Some(value),
b"type" => citation_type = Some(value),
_ => {}
}
}
let reference = reference
.ok_or_else(|| CmlError::MissingAttribute("ref required on <citation>".to_string()))?;
let content = Self::parse_inline_content(reader, b"citation")?;
Ok(Citation {
reference,
citation_type,
content,
})
}
fn parse_annotations<R: BufRead>(reader: &mut Reader<R>) -> Result<Annotations> {
let mut notes = Vec::new();
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) if e.name().as_ref() == b"note" => {
notes.push(Self::parse_note(reader, e)?);
}
Ok(Event::End(e)) if e.name().as_ref() == b"annotations" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <annotations>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(Annotations { notes })
}
fn parse_note<R: BufRead>(reader: &mut Reader<R>, start: BytesStart) -> Result<Note> {
let mut id = None;
let mut note_type = None;
let mut reference = None;
for attr in start.attributes() {
let attr = attr?;
let key = attr.key.as_ref();
let value = String::from_utf8_lossy(&attr.value).to_string();
match key {
b"id" => id = Some(value),
b"type" => note_type = Some(value),
b"ref" => reference = Some(value),
_ => {}
}
}
let content = Self::parse_note_content(reader)?;
Ok(Note {
id,
note_type,
reference,
content,
})
}
fn parse_note_content<R: BufRead>(reader: &mut Reader<R>) -> Result<NoteContent> {
let mut blocks = Vec::new();
let mut inlines = Vec::new();
let mut has_blocks = false;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let name = e.name();
if Self::is_block_element(name.as_ref()) {
has_blocks = true;
if let Some(block) = Self::parse_block_element(reader, e)? {
blocks.push(block);
}
} else if let Some(inline) = Self::parse_inline_element(reader, e)? {
inlines.push(inline);
}
}
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap().to_string().trim().to_string();
if !text.is_empty() {
inlines.push(InlineElement::Text(text));
}
}
Ok(Event::End(e)) if e.name().as_ref() == b"note" => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(
"Unexpected EOF in <note>".to_string(),
))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
if has_blocks {
Ok(NoteContent::Block(blocks))
} else {
Ok(NoteContent::Inline(inlines))
}
}
fn read_text<R: BufRead>(reader: &mut Reader<R>, tag_name: &str) -> Result<String> {
let mut buf = Vec::new();
let mut text = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Text(e)) => {
text.push_str(&e.unescape().unwrap());
}
Ok(Event::End(e)) if e.name().as_ref() == tag_name.as_bytes() => {
break;
}
Ok(Event::Eof) => {
return Err(CmlError::InvalidStructure(format!(
"Unexpected EOF in <{}>",
tag_name
)))
}
Ok(_) => {}
Err(e) => return Err(e.into()),
}
buf.clear();
}
Ok(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_document() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<cml profile="core" version="0.2" encoding="utf-8">
<header>
<title>Test Document</title>
</header>
<body>
<paragraph>Hello, world!</paragraph>
</body>
<footer>
</footer>
</cml>"#;
let doc = CmlParser::parse_str(xml).unwrap();
assert_eq!(doc.version, "0.2");
assert_eq!(doc.profile, "core");
assert_eq!(doc.header.title, "Test Document");
assert_eq!(doc.body.blocks.len(), 1);
}
#[test]
fn test_parse_header_with_metadata() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<cml profile="core" version="0.2" encoding="utf-8">
<header>
<title>Test</title>
<author role="editor">John Doe</author>
<date type="created" when="2025-12-22"/>
<identifier scheme="doi">10.1234/test</identifier>
<meta name="status" value="draft"/>
</header>
<body>
<paragraph>Content</paragraph>
</body>
<footer>
</footer>
</cml>"#;
let doc = CmlParser::parse_str(xml).unwrap();
assert_eq!(doc.header.authors.len(), 1);
assert_eq!(doc.header.authors[0].name, "John Doe");
assert_eq!(doc.header.dates.len(), 1);
assert_eq!(doc.header.identifiers.len(), 1);
assert_eq!(doc.header.meta.len(), 1);
}
#[test]
fn test_parse_inline_elements() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<cml profile="core" version="0.2" encoding="utf-8">
<header>
<title>Test</title>
</header>
<body>
<paragraph>This is <em>emphasized</em> and <bo>bold</bo> text.</paragraph>
</body>
<footer>
</footer>
</cml>"#;
let doc = CmlParser::parse_str(xml).unwrap();
if let BlockElement::Paragraph(para) = &doc.body.blocks[0] {
assert_eq!(para.content.len(), 5); } else {
panic!("Expected paragraph");
}
}
#[test]
fn test_parse_list() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<cml profile="core" version="0.2" encoding="utf-8">
<header>
<title>Test</title>
</header>
<body>
<list type="ordered" style="numeric">
<item>First</item>
<item>Second</item>
</list>
</body>
<footer>
</footer>
</cml>"#;
let doc = CmlParser::parse_str(xml).unwrap();
if let BlockElement::List(list) = &doc.body.blocks[0] {
assert_eq!(list.items.len(), 2);
assert!(matches!(list.list_type, Some(ListType::Ordered)));
} else {
panic!("Expected list");
}
}
#[test]
fn test_parse_footer_with_signature() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<cml profile="core" version="0.2" encoding="utf-8">
<header>
<title>Test</title>
</header>
<body>
<paragraph>Content</paragraph>
</body>
<footer>
<signatures>
<signature when="2025-12-22T10:30:00Z" role="author">Jane Doe</signature>
</signatures>
</footer>
</cml>"#;
let doc = CmlParser::parse_str(xml).unwrap();
assert!(doc.footer.signatures.is_some());
let sigs = doc.footer.signatures.unwrap();
assert_eq!(sigs.signatures.len(), 1);
assert_eq!(sigs.signatures[0].content, "Jane Doe");
}
}