raw
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