//! Tables. use crate::description::{Paragraph, ParagraphError}; use crate::xml::element::{Elements, FromElements, Tagged}; /// Informal table. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Informal { groups: Vec, } impl Informal { /// Returns the table groups. #[must_use] pub fn groups(&self) -> &[Group] { &self.groups } } impl FromElements for Informal { type Error = Error; fn from_elements(elements: &Elements) -> Result { let groups = elements .get_all_tagged_elements_with_name("tgroup") .into_iter() .map(Group::from_tagged_element) .collect::, _>>()?; Ok(Self { groups }) } } /// Table. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Table { title: String, groups: Vec, } impl Table { /// Returns the table title. #[must_use] pub fn title(&self) -> &str { &self.title } /// Returns the table groups. #[must_use] pub fn groups(&self) -> &[Group] { &self.groups } } impl FromElements for Table { type Error = Error; fn from_elements(elements: &Elements) -> Result { let title = elements .get_first_tagged_with_name("title") .ok_or(Self::Error::MissingTitle)? .child_elements() .get_first_text_element() .cloned() .unwrap_or_default(); let groups = elements .get_all_tagged_elements_with_name("tgroup") .into_iter() .map(Group::from_tagged_element) .collect::, _>>()?; Ok(Self { title, groups }) } } /// Table error. #[derive(Debug, thiserror::Error)] pub enum Error { /// Invalid group. #[error("Invalid group")] InvalidGroup(#[from] GroupError), /// Missing title. #[error("Missing title")] MissingTitle, } /// Table group #[derive(Debug, Clone, PartialEq, Eq)] pub struct Group { cols: u32, align: Option, column_specs: Vec, head: Vec, body: Vec, } impl Group { /// Returns the column count. #[must_use] pub fn cols(&self) -> u32 { self.cols } /// Returns the column alignment. #[must_use] pub fn align(&self) -> Option<&str> { self.align.as_deref() } /// Returns the column specifications. #[must_use] pub fn column_specs(&self) -> &[ColumnSpec] { &self.column_specs } /// Returns the head. #[must_use] pub fn head(&self) -> &[Row] { &self.head } /// Returns the body. #[must_use] pub fn body(&self) -> &[Row] { &self.body } } impl Group { fn from_tagged_element(element: &Tagged) -> Result { let cols = String::from_utf8( element .attributes() .iter() .find(|attr| attr.key == "cols") .ok_or(GroupError::MissingCols)? .value .clone(), ) .map_err(|_| GroupError::ColsNotUTF8)? .parse::() .map_err(|_| GroupError::ColsNotNumber)?; let align = match element.attributes().iter().find(|attr| attr.key == "align") { Some(attr) => Some( String::from_utf8(attr.value.clone()) .map_err(|_| GroupError::AlignNotUTF8)?, ), None => None, }; let column_specs = element .child_elements() .get_all_tagged_elements_with_name("colspec") .into_iter() .map(|column_spec_element| { ColumnSpec::from_tagged_element(column_spec_element) }) .collect::, _>>()?; let mut body = element .child_elements() .get_first_tagged_with_name("tbody") .ok_or(GroupError::MissingBody)? .child_elements() .get_all_tagged_elements_with_name("row") .into_iter() .map(|row_element| Row::from_elements(row_element.child_elements())) .collect::, _>>()?; let head = element .child_elements() .get_first_tagged_with_name("thead") .map_or_else( || Ok(vec![body.remove(0)]), |head_element| { head_element .child_elements() .get_all_tagged_elements_with_name("row") .into_iter() .map(|row_element| { Row::from_elements(row_element.child_elements()) }) .collect::, _>>() }, )?; Ok(Self { cols, align, column_specs, head, body, }) } } /// [`Group`] error. #[derive(Debug, thiserror::Error)] pub enum GroupError { /// Missing cols element attribute. #[error("Missing cols")] MissingCols, /// Invalid column specification. #[error("Invalid column specification")] InvalidColumnSpec(#[from] ColumnSpecError), /// Missing body element. #[error("Missing body")] MissingBody, /// Invalid row. #[error("Invalid row")] InvalidRow(#[from] RowError), /// Cols is not valid UTF-8. #[error("Cols is not valid UTF-8")] ColsNotUTF8, /// Cols is not a number #[error("Cols is not a number")] ColsNotNumber, /// Align is not valid UTF-8. #[error("Align is not valid UTF-8")] AlignNotUTF8, } /// Column specification. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ColumnSpec { width: String, num: Option, name: Option, } impl ColumnSpec { /// Returns the width. #[must_use] pub fn width(&self) -> &str { &self.width } /// Returns the number. #[must_use] pub fn num(&self) -> &Option { &self.num } /// Returns the name. #[must_use] pub fn name(&self) -> Option<&str> { self.name.as_deref() } } impl ColumnSpec { fn from_tagged_element(element: &Tagged) -> Result { let width = String::from_utf8( element .attributes() .iter() .find(|attr| attr.key == "colwidth") .ok_or(ColumnSpecError::MissingWidth)? .value .clone(), ) .map_err(|_| ColumnSpecError::WidthNotUTF8)?; let num_result = element .attributes() .iter() .find(|attr| attr.key == "colnum") .map(|attr| String::from_utf8(attr.value.clone())); let num = match num_result { Some(num) => Some( num.map_err(|_| ColumnSpecError::NumNotUTF8)? .parse::() .map_err(|_| ColumnSpecError::NumNotNumber)?, ), None => None, }; let name_result = element .attributes() .iter() .find(|attr| attr.key == "colname") .map(|attr| String::from_utf8(attr.value.clone())); let name = match name_result { Some(name) => Some(name.map_err(|_| ColumnSpecError::NameNotUTF8)?), None => None, }; Ok(Self { width, num, name }) } } /// [`ColumnSpec`] error. #[derive(Debug, thiserror::Error)] pub enum ColumnSpecError { /// Missing width element. #[error("Missing width")] MissingWidth, /// Width is not valid UTF-8. #[error("Width is not valid UTF-8")] WidthNotUTF8, /// Num is not valid UTF-8. #[error("Num is not valid UTF-8")] NumNotUTF8, /// Name is not valid UTF-8. #[error("Name is not valid UTF-8")] NameNotUTF8, /// Num is not a number. #[error("Num is not a number")] NumNotNumber, } /// Table row. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Row { entries: Vec, } impl Row { /// Returns the entries. #[must_use] pub fn entries(&self) -> &[Paragraph] { &self.entries } } impl FromElements for Row { type Error = RowError; fn from_elements(elements: &Elements) -> Result { let entries = elements .get_all_tagged_elements_with_name("entry") .into_iter() .map(|entry_element| Paragraph::from_elements(entry_element.child_elements())) .collect::, _>>()?; Ok(Self { entries }) } } /// [`Row`] error. #[derive(Debug, thiserror::Error)] pub enum RowError { /// Invalid paragraph. #[error("Invalid paragraph")] InvalidParagraph(#[from] ParagraphError), }