wayver's git archive


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

sable-markdown/src/parser/blocks/callout.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::{char, none_of},
6    combinator::{opt, recognize, value, verify},
7    multi::{many_m_n, many1},
8    sequence::preceded,
9};
10
11use crate::{
12    ast::Callout,
13    parser::util::{line_terminated, not_eof_or_eol0, not_eof_or_eol1},
14};
15
16pub(super) fn callout<'a>() -> impl FnMut(&'a str) -> IResult<&'a str, Callout> {
17    move |input: &'a str| {
18        let prefix = || preceded(many_m_n(0, 3, char(' ')), char('>'));
19
20        let (input, (level, kind, title)) =
21            preceded(prefix(), line_terminated(callout_header)).parse(input)?;
22
23        let (input, lines) =
24            many1(preceded(prefix(), line_terminated(not_eof_or_eol0))).parse(input)?;
25        let inner = lines.join("\n");
26
27        let (_, inner) = many1(crate::parser::blocks::block())
28            .parse(&inner)
29            .map_err(|err| err.map_input(|_| input))?;
30
31        let callout = Callout {
32            level,
33            title,
34            foldable: kind.is_some(),
35            open: kind.unwrap_or(true),
36            blocks: inner.into_iter().flatten().collect(),
37        };
38
39        Ok((input, callout))
40    }
41}
42
43fn callout_header(input: &str) -> IResult<&str, (String, Option<bool>, Option<String>)> {
44    let (input, _) = many_m_n(0, 3, char(' ')).parse(input)?;
45    let (input, _) = tag("[!").parse(input)?;
46    let (input, level) = recognize(many1(verify(none_of("]"), |c| *c != ']'))).parse(input)?;
47    let (input, _) = tag("]").parse(input)?;
48    let (input, kind) = opt(alt((value(true, tag("+")), value(false, tag("-"))))).parse(input)?;
49    let (input, title) = opt(callout_title).parse(input)?;
50
51    Ok((input, (level.to_owned(), kind, title)))
52}
53
54fn callout_title(input: &str) -> IResult<&str, String> {
55    let (input, _) = many_m_n(0, 3, char(' ')).parse(input)?;
56    let (input, label) = not_eof_or_eol1.parse(input)?;
57
58    Ok((input, label.to_owned()))
59}
60