summaryrefslogtreecommitdiff
path: root/src/table.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/table.rs')
-rw-r--r--src/table.rs396
1 files changed, 396 insertions, 0 deletions
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<Group>,
+}
+
+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<Self, Self::Error>
+ {
+ let groups = elements
+ .get_all_tagged_elements_with_name("tgroup")
+ .into_iter()
+ .map(Group::from_tagged_element)
+ .collect::<Result<Vec<_>, _>>()?;
+
+ Ok(Self { groups })
+ }
+}
+
+/// Table.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Table
+{
+ title: String,
+ groups: Vec<Group>,
+}
+
+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<Self, Self::Error>
+ {
+ 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::<Result<Vec<_>, _>>()?;
+
+ 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<String>,
+ column_specs: Vec<ColumnSpec>,
+ head: Vec<Row>,
+ body: Vec<Row>,
+}
+
+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<Self, GroupError>
+ {
+ let cols = String::from_utf8(
+ element
+ .attributes()
+ .iter()
+ .find(|attr| attr.key == "cols")
+ .ok_or(GroupError::MissingCols)?
+ .value
+ .clone(),
+ )
+ .map_err(|_| GroupError::ColsNotUTF8)?
+ .parse::<u32>()
+ .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::<Result<Vec<_>, _>>()?;
+
+ 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::<Result<Vec<_>, _>>()?;
+
+ 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::<Result<Vec<_>, _>>()
+ },
+ )?;
+
+ 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<u32>,
+ name: Option<String>,
+}
+
+impl ColumnSpec
+{
+ /// Returns the width.
+ #[must_use]
+ pub fn width(&self) -> &str
+ {
+ &self.width
+ }
+
+ /// Returns the number.
+ #[must_use]
+ pub fn num(&self) -> &Option<u32>
+ {
+ &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<Self, ColumnSpecError>
+ {
+ 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::<u32>()
+ .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<Paragraph>,
+}
+
+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<Self, Self::Error>
+ {
+ let entries = elements
+ .get_all_tagged_elements_with_name("entry")
+ .into_iter()
+ .map(|entry_element| Paragraph::from_elements(entry_element.child_elements()))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ Ok(Self { entries })
+ }
+}
+
+/// [`Row`] error.
+#[derive(Debug, thiserror::Error)]
+pub enum RowError
+{
+ /// Invalid paragraph.
+ #[error("Invalid paragraph")]
+ InvalidParagraph(#[from] ParagraphError),
+}