wayver's git archive


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

sable-markdown/src/parser/blocks/heading.rs@337ba67f65eaa17b44e371af7c0f0c761d6aa914

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    character::complete::{char, space0, space1},
5    combinator::{opt, value},
6    multi::{many_m_n, many1},
7    sequence::{preceded, terminated},
8};
9
10use crate::{
11    ast::{Block, Heading, HeadingKind, SetextHeading},
12    parser::util::{line_terminated, not_eof_or_eol1},
13};
14
15/// Parse headings in format:
16///      ### Header text
17pub(super) fn heading_v1<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Heading> {
18    move |input: &'a str| {
19        let (input, (prefix, _, content)) = (
20            many_m_n(1, 6, char('#')),
21            space1,
22            line_terminated(not_eof_or_eol1),
23        )
24            .parse(input)?;
25
26        let (_, content) = crate::parser::inline::inline_many0().parse(content)?;
27
28        let heading = Heading {
29            kind: HeadingKind::Atx(prefix.len() as u8),
30            content,
31        };
32
33        Ok((input, heading))
34    }
35}
36
37/// Parse headings in format:
38///      Heading text
39///      ====
40pub(super) fn heading_v2_or_paragraph<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Block> {
41    move |input: &'a str| {
42        let (input, (content, level)) = (
43            crate::parser::blocks::paragraph::paragraph(true),
44            opt(heading_v2_level()),
45        )
46            .parse(input)?;
47
48        if let Some(level) = level {
49            let heading = Heading {
50                kind: HeadingKind::Setext(level),
51                content,
52            };
53            return Ok((input, Block::Heading(heading)));
54        }
55
56        Ok((input, Block::Paragraph(content)))
57    }
58}
59
60pub(super) fn heading_v2_level<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, SetextHeading> {
61    move |input: &'a str| {
62        let setext_parser = alt((
63            value(SetextHeading::Level1, many1(char('='))),
64            value(SetextHeading::Level2, many1(char('-'))),
65        ));
66
67        let r = line_terminated(preceded(
68            many_m_n(0, 3, char(' ')),
69            terminated(setext_parser, space0),
70        ))
71        .parse(input)?;
72
73        Ok(r)
74    }
75}
76