Skip to main content

lex_core/lex/ast/elements/
table.rs

1//! Table element
2//!
3//!     Tables are a native element for structured, tabular data. They share the outer
4//!     structure of verbatim blocks (subject line, indented content, closing annotation)
5//!     but with inline-parsed pipe-delimited content instead of raw text.
6//!
7//! Structure
8//!
9//!     - subject: The table caption (inline-parsed)
10//!     - header_rows: Header rows (default: first row)
11//!     - body_rows: Body/data rows
12//!     - footnotes: Optional scoped footnote list
13//!     - annotations: Attached annotations (including :: table :: config)
14//!     - mode: Inflow or Fullwidth (inherited from verbatim wall logic)
15//!
16//! Syntax
17//!
18//!     <subject-line>
19//!         | cell | cell | cell |
20//!         | cell | cell | cell |
21//!
22//! Cell Merging
23//!
24//!     Merge markers (`>>` for colspan, `^^` for rowspan) are resolved during AST
25//!     assembly. The content cell gets its colspan/rowspan incremented, and absorbed
26//!     cells are removed. The final AST contains only content cells with span counts.
27//!
28//! Multi-line Mode
29//!
30//!     When blank lines separate pipe groups, consecutive pipe lines within a group
31//!     form a single row. Auto-detected; no flags needed.
32//!
33//! Learn More:
34//!
35//!     - Table element spec: specs/elements/table.lex
36//!     - Table proposal: specs/proposals/table.lex
37
38use super::super::range::Range;
39use super::super::text_content::TextContent;
40use super::super::traits::{AstNode, Container, Visitor, VisualStructure};
41use super::annotation::Annotation;
42use super::container::GeneralContainer;
43use super::content_item::ContentItem;
44use super::list::List;
45use super::typed_content::ContentElement;
46use super::verbatim::VerbatimBlockMode;
47use std::fmt;
48
49/// Alignment hint for a table cell.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum TableCellAlignment {
52    /// Left-aligned (default)
53    Left,
54    /// Center-aligned
55    Center,
56    /// Right-aligned
57    Right,
58    /// No explicit alignment
59    None,
60}
61
62/// A single cell in a table row.
63#[derive(Debug, Clone, PartialEq)]
64pub struct TableCell {
65    /// The cell's inline content (trimmed text, inline-parsed)
66    pub content: TextContent,
67    /// Block-level children (lists, definitions, etc.) when cell has block content
68    pub children: GeneralContainer,
69    /// Number of columns this cell spans (1 = no merge)
70    pub colspan: usize,
71    /// Number of rows this cell spans (1 = no merge)
72    pub rowspan: usize,
73    /// Column alignment for this cell
74    pub align: TableCellAlignment,
75    /// Whether this cell is in a header row
76    pub header: bool,
77    /// Byte range location
78    pub location: Range,
79}
80
81impl TableCell {
82    pub fn new(content: TextContent) -> Self {
83        Self {
84            content,
85            children: GeneralContainer::empty(),
86            colspan: 1,
87            rowspan: 1,
88            align: TableCellAlignment::None,
89            header: false,
90            location: Range::default(),
91        }
92    }
93
94    pub fn with_children(mut self, children: Vec<ContentElement>) -> Self {
95        self.children = GeneralContainer::from_typed(children);
96        self
97    }
98
99    /// Whether this cell has block-level content (lists, definitions, etc.)
100    pub fn has_block_content(&self) -> bool {
101        !self.children.is_empty()
102    }
103
104    pub fn with_span(mut self, colspan: usize, rowspan: usize) -> Self {
105        self.colspan = colspan;
106        self.rowspan = rowspan;
107        self
108    }
109
110    pub fn with_align(mut self, align: TableCellAlignment) -> Self {
111        self.align = align;
112        self
113    }
114
115    pub fn with_header(mut self, header: bool) -> Self {
116        self.header = header;
117        self
118    }
119
120    pub fn at(mut self, location: Range) -> Self {
121        self.location = location;
122        self
123    }
124
125    /// The text content of this cell
126    pub fn text(&self) -> &str {
127        self.content.as_string()
128    }
129
130    /// Whether this cell is empty (whitespace-only or no content)
131    pub fn is_empty(&self) -> bool {
132        self.content.as_string().trim().is_empty()
133    }
134}
135
136/// A row in a table.
137#[derive(Debug, Clone, PartialEq)]
138pub struct TableRow {
139    /// The cells in this row
140    pub cells: Vec<TableCell>,
141    /// Byte range location
142    pub location: Range,
143}
144
145impl TableRow {
146    pub fn new(cells: Vec<TableCell>) -> Self {
147        Self {
148            cells,
149            location: Range::default(),
150        }
151    }
152
153    pub fn at(mut self, location: Range) -> Self {
154        self.location = location;
155        self
156    }
157
158    /// Number of cells in this row
159    pub fn cell_count(&self) -> usize {
160        self.cells.len()
161    }
162}
163
164/// A table element with structured, pipe-delimited content.
165#[derive(Debug, Clone, PartialEq)]
166pub struct Table {
167    /// Caption/subject line (inline-parsed)
168    pub subject: TextContent,
169    /// Header rows (typically first row; controlled by header=N parameter)
170    pub header_rows: Vec<TableRow>,
171    /// Body/data rows
172    pub body_rows: Vec<TableRow>,
173    /// Optional scoped footnote definitions
174    pub footnotes: Option<Box<List>>,
175    /// Annotations attached to this table (including :: table :: config annotation)
176    pub annotations: Vec<Annotation>,
177    /// Location spanning the entire table element
178    pub location: Range,
179    /// Rendering mode (Inflow or Fullwidth, same as verbatim blocks)
180    pub mode: VerbatimBlockMode,
181}
182
183impl Table {
184    pub fn new(
185        subject: TextContent,
186        header_rows: Vec<TableRow>,
187        body_rows: Vec<TableRow>,
188        mode: VerbatimBlockMode,
189    ) -> Self {
190        Self {
191            subject,
192            header_rows,
193            body_rows,
194            footnotes: None,
195            annotations: Vec::new(),
196            location: Range::default(),
197            mode,
198        }
199    }
200
201    pub fn with_footnotes(mut self, footnotes: List) -> Self {
202        self.footnotes = Some(Box::new(footnotes));
203        self
204    }
205
206    pub fn at(mut self, location: Range) -> Self {
207        self.location = location;
208        self
209    }
210
211    /// All rows (header + body) in document order
212    pub fn all_rows(&self) -> impl Iterator<Item = &TableRow> {
213        self.header_rows.iter().chain(self.body_rows.iter())
214    }
215
216    /// Total number of rows (header + body)
217    pub fn row_count(&self) -> usize {
218        self.header_rows.len() + self.body_rows.len()
219    }
220
221    /// Maximum column count across all rows
222    pub fn column_count(&self) -> usize {
223        self.all_rows()
224            .map(|row| row.cells.len())
225            .max()
226            .unwrap_or(0)
227    }
228
229    /// Iterate over all block-level children across all cells in all rows
230    pub fn cell_children_iter(&self) -> impl Iterator<Item = &ContentItem> {
231        self.all_rows()
232            .flat_map(|row| row.cells.iter())
233            .flat_map(|cell| cell.children.iter())
234    }
235
236    /// Annotations attached to this table.
237    pub fn annotations(&self) -> &[Annotation] {
238        &self.annotations
239    }
240
241    /// Mutable access to table annotations.
242    pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
243        &mut self.annotations
244    }
245}
246
247impl AstNode for Table {
248    fn node_type(&self) -> &'static str {
249        "Table"
250    }
251
252    fn display_label(&self) -> String {
253        let subject_text = self.subject.as_string();
254        if subject_text.chars().count() > 50 {
255            format!("{}…", subject_text.chars().take(50).collect::<String>())
256        } else {
257            subject_text.to_string()
258        }
259    }
260
261    fn range(&self) -> &Range {
262        &self.location
263    }
264
265    fn accept(&self, visitor: &mut dyn Visitor) {
266        visitor.visit_table(self);
267        // Descend into cell children (block-level content inside cells)
268        for row in self.all_rows() {
269            for cell in &row.cells {
270                for child in cell.children.iter() {
271                    child.accept(visitor);
272                }
273            }
274        }
275        // Visit annotations
276        for annotation in &self.annotations {
277            annotation.accept(visitor);
278        }
279        // Visit footnotes
280        if let Some(footnotes) = &self.footnotes {
281            footnotes.accept(visitor);
282        }
283        visitor.leave_table(self);
284    }
285}
286
287impl VisualStructure for Table {
288    fn is_source_line_node(&self) -> bool {
289        true
290    }
291
292    fn has_visual_header(&self) -> bool {
293        true
294    }
295}
296
297impl Container for Table {
298    fn label(&self) -> &str {
299        self.subject.as_string()
300    }
301
302    fn children(&self) -> &[ContentItem] {
303        // Tables don't use the generic ContentItem children pattern;
304        // their structure is rows/cells. Return empty slice.
305        &[]
306    }
307
308    fn children_mut(&mut self) -> &mut Vec<ContentItem> {
309        // Tables don't use generic children. This is a design tension with
310        // the Container trait but is consistent with how they work.
311        // For now, panic - callers should use the typed row/cell API.
312        panic!("Tables use structured rows/cells, not generic children")
313    }
314}
315
316impl fmt::Display for Table {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        write!(
319            f,
320            "Table('{}', {} header + {} body rows, {} cols)",
321            self.subject.as_string(),
322            self.header_rows.len(),
323            self.body_rows.len(),
324            self.column_count()
325        )
326    }
327}