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    /// Annotations attached to this table.
230    pub fn annotations(&self) -> &[Annotation] {
231        &self.annotations
232    }
233
234    /// Mutable access to table annotations.
235    pub fn annotations_mut(&mut self) -> &mut Vec<Annotation> {
236        &mut self.annotations
237    }
238}
239
240impl AstNode for Table {
241    fn node_type(&self) -> &'static str {
242        "Table"
243    }
244
245    fn display_label(&self) -> String {
246        let subject_text = self.subject.as_string();
247        if subject_text.chars().count() > 50 {
248            format!("{}…", subject_text.chars().take(50).collect::<String>())
249        } else {
250            subject_text.to_string()
251        }
252    }
253
254    fn range(&self) -> &Range {
255        &self.location
256    }
257
258    fn accept(&self, visitor: &mut dyn Visitor) {
259        visitor.visit_table(self);
260        visitor.leave_table(self);
261    }
262}
263
264impl VisualStructure for Table {
265    fn is_source_line_node(&self) -> bool {
266        true
267    }
268
269    fn has_visual_header(&self) -> bool {
270        true
271    }
272}
273
274impl Container for Table {
275    fn label(&self) -> &str {
276        self.subject.as_string()
277    }
278
279    fn children(&self) -> &[ContentItem] {
280        // Tables don't use the generic ContentItem children pattern;
281        // their structure is rows/cells. Return empty slice.
282        &[]
283    }
284
285    fn children_mut(&mut self) -> &mut Vec<ContentItem> {
286        // Tables don't use generic children. This is a design tension with
287        // the Container trait but is consistent with how they work.
288        // For now, panic - callers should use the typed row/cell API.
289        panic!("Tables use structured rows/cells, not generic children")
290    }
291}
292
293impl fmt::Display for Table {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        write!(
296            f,
297            "Table('{}', {} header + {} body rows, {} cols)",
298            self.subject.as_string(),
299            self.header_rows.len(),
300            self.body_rows.len(),
301            self.column_count()
302        )
303    }
304}