Skip to main content

rd_ast/view/
tabular.rs

1use super::*;
2
3/// Alignment of one column in a `\\tabular` column specification.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum RdColumnAlign {
7    Left,
8    Center,
9    Right,
10}
11
12/// A borrowed, structurally valid `\\tabular` view.
13///
14/// The colspec path is `path().with_child(0)`. Cell paths are anchored to the
15/// body Group: a non-empty cell uses the body child index of its first node,
16/// while an empty cell uses the index of the separator that closed it. A row
17/// uses the path of its first cell; an entirely empty row uses the separator
18/// that opened its row region. Colspec characters accept only `l`, `c`, and
19/// `r`; whitespace and every other character are reported and skipped.
20#[derive(Debug, Clone, PartialEq)]
21pub struct RdTabular<'a> {
22    path: RdPath,
23    columns: Vec<RdColumnAlign>,
24    rows: Vec<RdTableRow<'a>>,
25    diagnostics: Vec<RdShapeError>,
26}
27
28impl<'a> RdTabular<'a> {
29    pub fn path(&self) -> &RdPath {
30        &self.path
31    }
32    pub fn columns(&self) -> &[RdColumnAlign] {
33        &self.columns
34    }
35    pub fn rows(&self) -> &[RdTableRow<'a>] {
36        &self.rows
37    }
38    pub fn diagnostics(&self) -> &[RdShapeError] {
39        &self.diagnostics
40    }
41}
42
43/// One row of a borrowed `\\tabular` view.
44#[derive(Debug, Clone, PartialEq)]
45pub struct RdTableRow<'a> {
46    path: RdPath,
47    cells: Vec<RdTableCell<'a>>,
48}
49
50impl<'a> RdTableRow<'a> {
51    pub fn path(&self) -> &RdPath {
52        &self.path
53    }
54    pub fn cells(&self) -> &[RdTableCell<'a>] {
55        &self.cells
56    }
57}
58
59/// One cell of a borrowed `\\tabular` view.
60#[derive(Debug, Clone, PartialEq)]
61pub struct RdTableCell<'a> {
62    path: RdPath,
63    nodes: &'a [RdNode],
64}
65
66impl<'a> RdTableCell<'a> {
67    pub fn path(&self) -> &RdPath {
68        &self.path
69    }
70    pub fn nodes(&self) -> &'a [RdNode] {
71        self.nodes
72    }
73}
74
75impl RdTagged {
76    /// Strictly inspects a `\\tabular` container. Container-shape failures
77    /// are returned atomically; malformed separators, column-specification
78    /// characters, and row widths are retained as diagnostics on the view.
79    /// A terminal `\\tab` does not create a trailing empty cell, matching
80    /// R's `Rd2HTML` rendering.
81    pub fn inspect_tabular<'a>(
82        &'a self,
83        base_path: &RdPath,
84    ) -> Result<RdTabular<'a>, RdShapeError> {
85        if self.tag() != &RdTag::Tabular {
86            return Err(shape(
87                base_path.clone(),
88                Some(self.tag().clone()),
89                RdShapeErrorKind::UnexpectedNode {
90                    expected: RdExpectedNode::Tabular,
91                    actual: RdNodeKind::Tagged,
92                },
93            ));
94        }
95        if self.option().is_some() {
96            return Err(shape(
97                base_path.clone(),
98                Some(RdTag::Tabular),
99                RdShapeErrorKind::UnexpectedOption,
100            ));
101        }
102        if self.children().len() != 2 {
103            return Err(shape(
104                base_path.clone(),
105                Some(RdTag::Tabular),
106                RdShapeErrorKind::WrongArity {
107                    expected: RdArity::Exactly(2),
108                    actual: self.children().len(),
109                },
110            ));
111        }
112        let [colspec_node, body_node] = self.children() else {
113            unreachable!()
114        };
115        let colspec_path = base_path.with_child(0);
116        let colspec_group = colspec_node.as_group().ok_or_else(|| {
117            shape(
118                colspec_path.clone(),
119                Some(RdTag::Tabular),
120                RdShapeErrorKind::UnexpectedNode {
121                    expected: RdExpectedNode::Group,
122                    actual: RdNodeKind::of(colspec_node),
123                },
124            )
125        })?;
126        let body_path = base_path.with_child(1);
127        let body_group = body_node.as_group().ok_or_else(|| {
128            shape(
129                body_path.clone(),
130                Some(RdTag::Tabular),
131                RdShapeErrorKind::UnexpectedNode {
132                    expected: RdExpectedNode::Group,
133                    actual: RdNodeKind::of(body_node),
134                },
135            )
136        })?;
137
138        let colspec_children = colspec_group.children();
139        if colspec_children.len() != 1 {
140            return Err(shape(
141                colspec_path,
142                Some(RdTag::Tabular),
143                RdShapeErrorKind::WrongArity {
144                    expected: RdArity::Exactly(1),
145                    actual: colspec_children.len(),
146                },
147            ));
148        }
149        let RdNode::Text(spec) = &colspec_children[0] else {
150            return Err(shape(
151                base_path.with_child(0).with_child(0),
152                Some(RdTag::Tabular),
153                RdShapeErrorKind::UnexpectedContent {
154                    actual: RdNodeKind::of(&colspec_children[0]),
155                },
156            ));
157        };
158
159        let mut columns = Vec::new();
160        let mut diagnostics = Vec::new();
161        let spec_path = base_path.with_child(0).with_child(0);
162        for (index, character) in spec.chars().enumerate() {
163            let alignment = match character {
164                'l' => Some(RdColumnAlign::Left),
165                'c' => Some(RdColumnAlign::Center),
166                'r' => Some(RdColumnAlign::Right),
167                _ => {
168                    diagnostics.push(shape(
169                        spec_path.clone().with_character(index),
170                        Some(RdTag::Tabular),
171                        RdShapeErrorKind::InvalidValue {
172                            construct: RdConstruct::ColumnSpec,
173                            value: character.to_string(),
174                        },
175                    ));
176                    None
177                }
178            };
179            if let Some(alignment) = alignment {
180                columns.push(alignment);
181            }
182        }
183
184        let body = body_group.children();
185        let mut rows = Vec::new();
186        let mut current_cells = Vec::new();
187        let mut cell_start = 0;
188        let mut row_anchor = body_path.clone();
189        let mut row_has_content = false;
190
191        for (index, node) in body.iter().enumerate() {
192            let separator = node.as_tagged().and_then(|tagged| {
193                if tagged.tag() == &RdTag::Tab {
194                    Some(RdTag::Tab)
195                } else if tagged.tag() == &RdTag::Cr {
196                    Some(RdTag::Cr)
197                } else {
198                    None
199                }
200            });
201            let Some(separator) = separator else {
202                if !row_has_content && current_cells.is_empty() {
203                    row_anchor = body_path.with_child(index);
204                }
205                row_has_content = true;
206                continue;
207            };
208            let tagged = node.as_tagged().unwrap();
209            let separator_path = body_path.with_child(index);
210            if !row_has_content && current_cells.is_empty() {
211                row_anchor = separator_path.clone();
212            }
213            if tagged.option().is_some() {
214                diagnostics.push(shape(
215                    separator_path.clone(),
216                    Some(separator.clone()),
217                    RdShapeErrorKind::UnexpectedOption,
218                ));
219            }
220            if !tagged.children().is_empty() {
221                diagnostics.push(shape(
222                    separator_path.clone(),
223                    Some(separator.clone()),
224                    RdShapeErrorKind::WrongArity {
225                        expected: RdArity::Exactly(0),
226                        actual: tagged.children().len(),
227                    },
228                ));
229            }
230            let cell_path = if cell_start < index {
231                body_path.with_child(cell_start)
232            } else {
233                separator_path.clone()
234            };
235            current_cells.push(RdTableCell {
236                path: cell_path,
237                nodes: &body[cell_start..index],
238            });
239            if separator == RdTag::Cr {
240                finish_table_row(
241                    &mut rows,
242                    &mut current_cells,
243                    &mut diagnostics,
244                    row_anchor.clone(),
245                    columns.len(),
246                );
247                cell_start = index + 1;
248                row_anchor = body_path.with_child(index);
249                row_has_content = false;
250            } else {
251                cell_start = index + 1;
252            }
253        }
254        if cell_start < body.len() {
255            let cell_path = body_path.with_child(cell_start);
256            current_cells.push(RdTableCell {
257                path: cell_path.clone(),
258                nodes: &body[cell_start..],
259            });
260            if !row_has_content {
261                row_anchor = cell_path.clone();
262            }
263            finish_table_row(
264                &mut rows,
265                &mut current_cells,
266                &mut diagnostics,
267                row_anchor,
268                columns.len(),
269            );
270        } else if !current_cells.is_empty() {
271            finish_table_row(
272                &mut rows,
273                &mut current_cells,
274                &mut diagnostics,
275                row_anchor,
276                columns.len(),
277            );
278        }
279
280        Ok(RdTabular {
281            path: base_path.clone(),
282            columns,
283            rows,
284            diagnostics,
285        })
286    }
287}
288fn finish_table_row<'a>(
289    rows: &mut Vec<RdTableRow<'a>>,
290    cells: &mut Vec<RdTableCell<'a>>,
291    diagnostics: &mut Vec<RdShapeError>,
292    path: RdPath,
293    expected_columns: usize,
294) {
295    if cells.len() != expected_columns {
296        diagnostics.push(shape(
297            path.clone(),
298            Some(RdTag::Cr),
299            RdShapeErrorKind::CountMismatch {
300                construct: RdConstruct::TableRow,
301                expected: expected_columns,
302                actual: cells.len(),
303            },
304        ));
305    }
306    rows.push(RdTableRow {
307        path,
308        cells: std::mem::take(cells),
309    });
310}