wayver's git archive


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

sable-markdown/src/parser/inline/emphasis.rs@main

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,
6    combinator::{map, map_opt, not, peek, recognize, value, verify},
7    multi::many1,
8    sequence::{delimited, preceded},
9};
10
11use crate::ast::Inline;
12
13pub(super) fn emphasis() -> impl FnMut(&str) -> IResult<&str, Inline> {
14    move |input: &str| {
15        alt((
16            map(
17                alt((
18                    delimited(
19                        open_tag("***"),
20                        emphasis_content(close_tag("***")),
21                        close_tag("***"),
22                    ),
23                    delimited(
24                        open_tag("___"),
25                        emphasis_content(close_tag("___")),
26                        close_tag("___"),
27                    ),
28                )),
29                |inner| Inline::Strong(vec![Inline::Emphasis(inner)]),
30            ),
31            map(
32                alt((
33                    delimited(
34                        open_tag("**"),
35                        emphasis_content(close_tag("**")),
36                        close_tag("**"),
37                    ),
38                    delimited(
39                        open_tag("__"),
40                        emphasis_content(close_tag("__")),
41                        close_tag("__"),
42                    ),
43                )),
44                Inline::Strong,
45            ),
46            map(
47                alt((
48                    delimited(
49                        open_tag("*"),
50                        emphasis_content(close_tag("*")),
51                        close_tag("*"),
52                    ),
53                    delimited(
54                        open_tag("_"),
55                        emphasis_content(close_tag("_")),
56                        close_tag("_"),
57                    ),
58                )),
59                Inline::Emphasis,
60            ),
61        ))
62        .parse(input)
63    }
64}
65
66fn emphasis_content<'a, P>(mut close_tag: P) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<Inline>>
67where
68    P: Parser<&'a str, Output = (), Error = nom::error::Error<&'a str>>,
69{
70    move |input: &str| {
71        let not_end = |i: &'a str| close_tag.parse(i);
72        map_opt(
73            recognize(many1(preceded(
74                peek(not(not_end)),
75                alt((value((), tag("\\*")), value((), anychar))),
76            ))),
77            |content: &str| {
78                crate::parser::inline::inline_many1()
79                    .parse(content)
80                    .map(|(_, content)| content)
81                    .ok()
82            },
83        )
84        .parse(input)
85    }
86}
87
88fn open_tag(tag_value: &'static str) -> impl FnMut(&str) -> IResult<&str, ()> {
89    move |input: &str| {
90        value(
91            (),
92            verify(tag(tag_value), |v: &str| {
93                can_open(v.chars().next().unwrap(), input.chars().nth(v.len()))
94            }),
95        )
96        .parse(input)
97    }
98}
99
100fn can_open(marker: char, next: Option<char>) -> bool {
101    let left_flanking = next.is_some_and(|c| !c.is_whitespace())
102        && (next.is_some_and(|c| !is_punctuation(c)) || (next.is_some_and(is_punctuation)));
103    if !left_flanking {
104        return false;
105    }
106    if marker == '_' {
107        let right_flanking = next.is_none_or(|c| c.is_whitespace() || is_punctuation(c));
108        return !right_flanking;
109    }
110    true
111}
112
113fn close_tag(tag_value: &'static str) -> impl FnMut(&str) -> IResult<&str, ()> {
114    move |input: &str| {
115        value(
116            (),
117            verify(tag(tag_value), |v: &str| {
118                can_close(v.chars().next().unwrap(), input.chars().nth(v.len()))
119            }),
120        )
121        .parse(input)
122    }
123}
124
125fn can_close(marker: char, next: Option<char>) -> bool {
126    let right_flanking = next.is_none_or(|c| c.is_whitespace() || is_punctuation(c));
127    if !right_flanking {
128        return false;
129    }
130
131    if marker == '_' {
132        let left_flanking = next.is_some_and(|c| !c.is_whitespace())
133            && (next.is_some_and(|c| !is_punctuation(c)))
134            || (next.is_some_and(is_punctuation));
135        return !left_flanking || next.is_some_and(is_punctuation);
136    }
137    true
138}
139
140fn is_punctuation(c: char) -> bool {
141    use unicode_categories::UnicodeCategories;
142    c.is_ascii_punctuation() || c.is_punctuation()
143}
144