From a15a878b87a4891ec856178c151faeaaa1799ad3 Mon Sep 17 00:00:00 2001 From: HampusM Date: Sat, 4 Mar 2023 13:18:33 +0100 Subject: feat: add table support --- src/table.rs | 396 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 src/table.rs (limited to 'src/table.rs') diff --git a/src/table.rs b/src/table.rs new file mode 100644 index 0000000..b549b95 --- /dev/null +++ b/src/table.rs @@ -0,0 +1,396 @@ +//! 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), +} -- cgit v1.2.3-18-g5258