wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-markdown/src/parser/blocks/table.rs@2b84405277e54ab809e328cf0237374d4b4dbd0c

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1use nom::{
2    IResult, Parser,
3    branch::alt,
4    bytes::complete::tag,
5    character::complete::{anychar, char, space0},
6    combinator::{map, not, opt, recognize, value},
7    multi::{many_m_n, many0, many1, separated_list1},
8    sequence::{delimited, preceded, terminated},
9};
10
11use crate::ast::{Alignment, Inline, Table, TableRow};
12
13use super::{eof_or_eol, line_terminated};
14
15pub(super) fn table<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Table> {
16    move |input: &'a str| {
17        let (input, header) = parse_table_row().parse(input)?;
18        let col_count = header.len();
19
20        let (input, alignments) = parse_alignment_row.parse(input)?;
21        if alignments.len() != col_count {
22            return Err(nom::Err::Error(nom::error::Error::new(
23                input,
24                nom::error::ErrorKind::Verify,
25            )));
26        }
27
28        let (input, rows) = parse_table_data_rows(col_count).parse(input)?;
29
30        Ok((
31            input,
32            Table {
33                rows: std::iter::once(header).chain(rows).collect(),
34                alignments,
35            },
36        ))
37    }
38}
39
40fn parse_table_data_rows<'a>(
41    col_count: usize,
42) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<TableRow>> {
43    move |input: &'a str| {
44        many0(map(parse_table_row(), move |mut row| {
45            match row.len().cmp(&col_count) {
46                std::cmp::Ordering::Less => {
47                    row.extend(
48                        (0..(col_count - row.len())).map(|_| vec![Inline::Text(String::new())]),
49                    );
50                }
51                std::cmp::Ordering::Greater => {
52                    row.truncate(col_count);
53                }
54                std::cmp::Ordering::Equal => {}
55            }
56            row
57        }))
58        .parse(input)
59    }
60}
61
62fn parse_alignment_row(input: &str) -> IResult<&str, Vec<Alignment>> {
63    fn parse_cell_alignment(cell: &str) -> Alignment {
64        let trimmed = cell.trim();
65        let starts_with_colon = trimmed.starts_with(':');
66        let ends_with_colon = trimmed.ends_with(':');
67
68        match (starts_with_colon, ends_with_colon) {
69            (true, true) => Alignment::Center,
70            (true, false) => Alignment::Left,
71            (false, true) => Alignment::Right,
72            (false, false) => Alignment::None,
73        }
74    }
75
76    let alignment_parser = delimited(
77        space0,
78        alt((
79            recognize(delimited(char(':'), many1(char('-')), char(':'))),
80            recognize(preceded(char(':'), many1(char('-')))),
81            recognize(terminated(many1(char('-')), char(':'))),
82            recognize(many1(char('-'))),
83        )),
84        space0,
85    );
86
87    line_terminated(preceded(
88        many_m_n(0, 3, char(' ')),
89        delimited(
90            char('|'),
91            separated_list1(char('|'), map(alignment_parser, parse_cell_alignment)),
92            opt(char('|')),
93        ),
94    ))
95    .parse(input)
96}
97
98fn parse_table_row<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, TableRow> {
99    move |input: &'a str| {
100        line_terminated(preceded(
101            many_m_n(0, 3, char(' ')),
102            delimited(
103                char('|'),
104                separated_list1(char('|'), cell_content()),
105                char('|'),
106            ),
107        ))
108        .parse(input)
109    }
110}
111
112fn cell_content<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Vec<Inline>> {
113    move |input: &'a str| {
114        let (input, chars) = many1(preceded(
115            not(alt((value((), eof_or_eol), value((), char('|'))))),
116            alt((value('|', tag("\\|")), anychar)),
117        ))
118        .parse(input)?;
119
120        let content = chars.iter().collect::<String>();
121        let trimmed_content = content.trim();
122        let (_, content) = crate::parser::inline::inline_many0()
123            .parse(trimmed_content)
124            .map_err(|err| err.map_input(|_| input))?;
125
126        Ok((input, content))
127    }
128}
129