vimwiki_core/lang/parsers/vimwiki/blocks/
code.rs

1use crate::lang::{
2    elements::{CodeBlock, Located},
3    parsers::{
4        utils::{
5            any_line, beginning_of_line, capture, context, cow_str,
6            end_of_line_or_input, locate, take_line_until, take_line_until1,
7        },
8        IResult, Span,
9    },
10};
11use nom::{
12    bytes::complete::tag,
13    character::complete::{char, space0, space1},
14    combinator::{map_parser, not, opt, verify},
15    multi::{many0, separated_list0},
16    sequence::{delimited, preceded, separated_pair},
17};
18use std::{borrow::Cow, collections::HashMap};
19
20type MaybeLang<'a> = Option<Cow<'a, str>>;
21type Metadata<'a> = HashMap<Cow<'a, str>, Cow<'a, str>>;
22
23#[inline]
24pub fn code_block(input: Span) -> IResult<Located<CodeBlock>> {
25    fn inner(input: Span) -> IResult<CodeBlock> {
26        let (input, (maybe_lang, metadata)) = code_block_start(input)?;
27        let (input, lines) = many0(preceded(
28            not(code_block_end),
29            map_parser(any_line, cow_str),
30        ))(input)?;
31        let (input, _) = code_block_end(input)?;
32
33        Ok((input, CodeBlock::new(maybe_lang, metadata, lines)))
34    }
35
36    context("Preformatted Text", locate(capture(inner)))(input)
37}
38
39#[inline]
40fn code_block_start<'a>(
41    input: Span<'a>,
42) -> IResult<(MaybeLang<'a>, Metadata<'a>)> {
43    // First, verify we have the start of a block and consume it
44    let (input, _) = beginning_of_line(input)?;
45    let (input, _) = space0(input)?;
46    let (input, _) = tag("{{{")(input)?;
47
48    // Second, look for optional language and consume it
49    //
50    // e.g. {{{c++ -> Some("c++")
51    let (input, maybe_lang) = opt(map_parser(
52        verify(take_line_until1(" "), |s: &Span| {
53            !s.as_remaining().contains(&b'=')
54        }),
55        cow_str,
56    ))(input)?;
57
58    // Third, remove any extra spaces before metadata
59    let (input, _) = space0(input)?;
60
61    // Fourth, look for optional metadata and consume it
62    //
63    // e.g. {{{key1="value 1" key2="value 2"
64    let (input, pairs) = separated_list0(
65        space1,
66        separated_pair(
67            map_parser(take_line_until1("="), cow_str),
68            char('='),
69            delimited(
70                char('"'),
71                map_parser(take_line_until("\""), cow_str),
72                char('"'),
73            ),
74        ),
75    )(input)?;
76
77    // Fifth, consume end of line
78    let (input, _) = space0(input)?;
79    let (input, _) = end_of_line_or_input(input)?;
80
81    Ok((input, (maybe_lang, pairs.into_iter().collect())))
82}
83
84#[inline]
85fn code_block_end(input: Span) -> IResult<()> {
86    let (input, _) = beginning_of_line(input)?;
87    let (input, _) = space0(input)?;
88    let (input, _) = tag("}}}")(input)?;
89    let (input, _) = space0(input)?;
90    let (input, _) = end_of_line_or_input(input)?;
91
92    Ok((input, ()))
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use indoc::indoc;
99
100    #[test]
101    fn code_block_should_fail_if_does_not_have_starting_line() {
102        let input = Span::from(indoc! {r"
103            some code
104            }}}
105        "});
106        assert!(code_block(input).is_err());
107    }
108
109    #[test]
110    fn code_block_should_fail_if_starting_block_not_on_own_line() {
111        let input = Span::from(indoc! {r"
112            {{{some code
113            }}}
114        "});
115        assert!(code_block(input).is_err());
116    }
117
118    #[test]
119    fn code_block_should_fail_if_does_not_have_ending_line() {
120        let input = Span::from(indoc! {r"
121            {{{
122            some code
123        "});
124        assert!(code_block(input).is_err());
125    }
126
127    #[test]
128    fn code_block_should_fail_if_ending_block_not_on_own_line() {
129        let input = Span::from(indoc! {r"
130            {{{
131            some code}}}
132        "});
133        assert!(code_block(input).is_err());
134    }
135
136    #[test]
137    fn code_block_should_support_having_no_lines() {
138        let input = Span::from(indoc! {r"
139            {{{
140            }}}
141        "});
142        let (input, p) = code_block(input).unwrap();
143        assert!(input.is_empty(), "Did not consume code block");
144        assert!(p.language.is_none(), "Has unexpected language");
145        assert!(p.lines.is_empty(), "Has unexpected lines");
146        assert!(p.metadata.is_empty(), "Has unexpected metadata");
147    }
148
149    #[test]
150    fn code_block_should_support_lang_shorthand() {
151        let input = Span::from(indoc! {r"
152            {{{c++
153            some code
154            }}}
155        "});
156        let (input, p) = code_block(input).unwrap();
157        assert!(input.is_empty(), "Did not consume code block");
158        assert_eq!(p.language.as_deref(), Some("c++"));
159        assert_eq!(
160            p.lines.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
161            vec!["some code"]
162        );
163        assert!(p.metadata.is_empty(), "Has unexpected metadata");
164    }
165
166    #[test]
167    fn code_block_should_support_lang_shorthand_with_metadata() {
168        let input = Span::from(indoc! {r#"
169            {{{c++ key="value"
170            some code
171            }}}
172        "#});
173        let (input, p) = code_block(input).unwrap();
174        assert!(input.is_empty(), "Did not consume code block");
175        assert_eq!(p.language.as_deref(), Some("c++"));
176        assert_eq!(
177            p.lines.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
178            vec!["some code"]
179        );
180        assert_eq!(p.metadata.get("key"), Some(&Cow::from("value")));
181    }
182
183    #[test]
184    fn code_block_should_parse_all_lines_between() {
185        let input = Span::from(indoc! {r"
186            {{{
187            Tyger! Tyger! burning bright
188             In the forests of the night,
189              What immortal hand or eye
190               Could frame thy fearful symmetry?
191            In what distant deeps or skies
192             Burnt the fire of thine eyes?
193              On what wings dare he aspire?
194               What the hand dare sieze the fire?
195            }}}
196        "});
197        let (input, p) = code_block(input).unwrap();
198        assert!(input.is_empty(), "Did not consume code block");
199        assert_eq!(
200            p.lines.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
201            vec![
202                "Tyger! Tyger! burning bright",
203                " In the forests of the night,",
204                "  What immortal hand or eye",
205                "   Could frame thy fearful symmetry?",
206                "In what distant deeps or skies",
207                " Burnt the fire of thine eyes?",
208                "  On what wings dare he aspire?",
209                "   What the hand dare sieze the fire?",
210            ]
211        );
212        assert!(p.language.is_none(), "Has unexpected language");
213        assert!(p.metadata.is_empty(), "Has unexpected metadata");
214    }
215
216    #[test]
217    fn code_block_should_support_single_metadata() {
218        let input = Span::from(indoc! {r#"
219            {{{class="brush: python"
220            def hello(world):
221                for x in range(10):
222                    print("Hello {0} number {1}".format(world, x))
223            }}}
224        "#});
225        let (input, p) = code_block(input).unwrap();
226        assert!(input.is_empty(), "Did not consume code block");
227        assert_eq!(
228            p.lines.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
229            vec![
230                r#"def hello(world):"#,
231                r#"    for x in range(10):"#,
232                r#"        print("Hello {0} number {1}".format(world, x))"#,
233            ]
234        );
235        assert_eq!(p.metadata.get("class"), Some(&Cow::from("brush: python")));
236    }
237
238    #[test]
239    fn code_block_should_support_multiple_metadata() {
240        let input = Span::from(indoc! {r#"
241            {{{class="brush: python" style="position: relative"
242            def hello(world):
243                for x in range(10):
244                    print("Hello {0} number {1}".format(world, x))
245            }}}
246        "#});
247        let (input, p) = code_block(input).unwrap();
248        assert!(input.is_empty(), "Did not consume code block");
249        assert_eq!(
250            p.lines.iter().map(AsRef::as_ref).collect::<Vec<&str>>(),
251            vec![
252                r#"def hello(world):"#,
253                r#"    for x in range(10):"#,
254                r#"        print("Hello {0} number {1}".format(world, x))"#,
255            ]
256        );
257        assert!(p.language.is_none(), "Has unexpected language");
258        assert_eq!(p.metadata.get("class"), Some(&Cow::from("brush: python")));
259        assert_eq!(
260            p.metadata.get("style"),
261            Some(&Cow::from("position: relative"))
262        );
263    }
264}