vimwiki_core/lang/parsers/vimwiki/blocks/
code.rs1use 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 let (input, _) = beginning_of_line(input)?;
45 let (input, _) = space0(input)?;
46 let (input, _) = tag("{{{")(input)?;
47
48 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 let (input, _) = space0(input)?;
60
61 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 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}