use lex_extension::schema::{BodyKind, BodyPresence, BodyShape, Capabilities, HookSet, Schema};
use lex_extension::wire::{Position, Range, WireInline, WireNode, WireRow, WireTableCell};
use std::collections::BTreeMap;
pub const LEX_TABULAR_TABLE: &str = "lex.tabular.table";
pub fn parse_pipe_table_to_wire(content: &str) -> WireNode {
let lines: Vec<&str> = content
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect();
let default_range = Range {
start: Position(0, 0),
end: Position(0, 0),
};
if lines.is_empty() {
return WireNode::Table {
range: default_range,
origin: None,
caption: String::new(),
header_rows: 0,
column_aligns: Vec::new(),
rows: Vec::new(),
footnotes: Vec::new(),
};
}
let mut rows: Vec<WireRow> = Vec::new();
let mut column_aligns: Vec<String> = Vec::new();
let header_cells = parse_pipe_row(lines[0]);
rows.push(WireRow {
cells: header_cells
.into_iter()
.map(|c| WireTableCell {
inlines: vec![WireInline::Text { text: c }],
colspan: 1,
rowspan: 1,
})
.collect(),
});
let header_rows: u32 = 1;
let mut body_start_idx = 1;
if lines.len() > 1 {
let separator = lines[1];
if separator.contains(['-', '|']) {
for part in parse_pipe_row(separator) {
let trimmed = part.trim();
column_aligns.push(match (trimmed.starts_with(':'), trimmed.ends_with(':')) {
(true, true) => "center".to_string(),
(false, true) => "right".to_string(),
(true, false) => "left".to_string(),
(false, false) => String::new(),
});
}
body_start_idx = 2;
}
}
for line in lines.iter().skip(body_start_idx) {
let cells = parse_pipe_row(line);
rows.push(WireRow {
cells: cells
.into_iter()
.map(|c| WireTableCell {
inlines: vec![WireInline::Text { text: c }],
colspan: 1,
rowspan: 1,
})
.collect(),
});
}
let widest = rows.iter().map(|r| r.cells.len()).max().unwrap_or(0);
while column_aligns.len() < widest {
column_aligns.push(String::new());
}
WireNode::Table {
range: default_range,
origin: None,
caption: String::new(),
header_rows,
column_aligns,
rows,
footnotes: Vec::new(),
}
}
fn parse_pipe_row(line: &str) -> Vec<String> {
let line = line.trim();
let line = line.strip_prefix('|').unwrap_or(line);
let line = line.strip_suffix('|').unwrap_or(line);
line.split('|').map(|s| s.trim().to_string()).collect()
}
pub fn lex_tabular_table_schema() -> Schema {
Schema {
schema_version: 1,
label: LEX_TABULAR_TABLE.into(),
description: Some(
"Pipe-table verbatim. The verbatim body uses markdown-style pipe-table syntax \
(`| col1 | col2 |\\n|------|------|\\n| ... |`). The schema's `on_ir_build` \
hook parses it into a typed `WireNode::Table` consumed by the IR builder \
(#615 unified registry surface)."
.into(),
),
params: BTreeMap::new(),
attaches_to: vec!["verbatim".into()],
body: BodyShape {
kind: BodyKind::Text,
presence: BodyPresence::Required,
description: Some(
"Pipe-table source: header row, alignment row, then one row per body line.".into(),
),
},
verbatim_label: true,
capabilities: Capabilities::default(),
hooks: HookSet {
ir_build: true,
..HookSet::default()
},
handler: None,
diagnostics: Vec::new(),
}
}
pub fn all_schemas() -> Vec<Schema> {
vec![lex_tabular_table_schema()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tabular_table_is_a_verbatim_label() {
let schema = lex_tabular_table_schema();
assert_eq!(schema.label, LEX_TABULAR_TABLE);
assert!(
schema.verbatim_label,
"tabular.table must be a verbatim label"
);
assert_eq!(schema.attaches_to, vec!["verbatim".to_string()]);
assert_eq!(schema.body.kind, BodyKind::Text);
assert_eq!(schema.body.presence, BodyPresence::Required);
}
#[test]
fn tabular_schema_declares_ir_build_hook() {
let schema = lex_tabular_table_schema();
assert!(schema.hooks.ir_build);
assert!(!schema.hooks.resolve);
assert!(!schema.hooks.validate);
assert!(schema.hooks.render.is_empty());
}
#[test]
fn tabular_schema_round_trips_through_json() {
let schema = lex_tabular_table_schema();
let json = serde_json::to_string(&schema).expect("serialize");
let back: Schema = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, schema);
}
}