1use nom::{
2 branch::alt,
3 bytes::complete::tag,
4 character::complete::{anychar, char, line_ending, multispace0, none_of, space0},
5 combinator::{map, opt, recognize, value},
6 multi::{many1_count, separated_list0, separated_list1},
7 sequence::{delimited, pair, preceded},
8 IResult,
9};
10
11use crate::{
12 parser::metadata::parse_metadata,
13 utils::{split_escaped, until_link1},
14 ContentNode, Passage, Tag,
15};
16
17fn parse_escaped_char(input: &str) -> IResult<&str, char> {
18 preceded(char('\\'), anychar)(input)
19}
20
21fn parse_tag(input: &str) -> IResult<&str, Tag<&str>> {
22 let parse_tag = recognize(many1_count(alt((parse_escaped_char, none_of(" ]")))));
23 map(parse_tag, Tag::new)(input)
24}
25
26pub fn parse_tags(input: &str) -> IResult<&str, Vec<Tag<&str>>> {
27 let each_tags = separated_list0(tag(" "), parse_tag);
28
29 let mut parse_tags = delimited(tag("["), each_tags, tag("]"));
30 parse_tags(input)
31}
32
33fn parse_title(input: &str) -> IResult<&str, &str> {
34 let parse_word = recognize(many1_count(alt((parse_escaped_char, none_of(" \n\r[{")))));
35
36 let title_block = recognize(separated_list1(tag(" "), value((), parse_word)));
37
38 preceded(tag(":: "), title_block)(input)
39}
40
41fn find_content_block(input: &str) -> IResult<&str, &str> {
42 match input.find("\r\n::") {
43 Some(index) => Ok((&input[index..], &input[..index])),
44 None => match input.find("\n::") {
45 Some(index) => Ok((&input[index..], &input[..index])),
46 None => Ok(("", input)),
47 },
48 }
49}
50
51fn parse_text_node(input: &str) -> IResult<&str, ContentNode<&str>> {
52 let (input, text) = until_link1(input)?;
53 Ok((input, ContentNode::text_node(text)))
54}
55
56fn parse_link_node<'a>(input: &'a str) -> IResult<&str, ContentNode<&str>> {
57 let parse_link_content = recognize(many1_count(alt((parse_escaped_char, none_of("\n\r]")))));
58
59 let (input, link_content) = delimited(tag("[["), parse_link_content, tag("]]"))(input)?;
60
61 let piped = |link_content| split_escaped(link_content, "|");
62 let to_right = |link_content| split_escaped(link_content, "->");
63 let to_left =
64 |link_content| split_escaped(link_content, "<-").map(|(target, text)| (text, target));
65 let simple = |link_content: &'a str| -> (&str, &str) { (link_content, link_content) };
66
67 let (text, target) = piped(link_content)
68 .or_else(|| to_right(link_content))
69 .or_else(|| to_left(link_content))
70 .unwrap_or_else(|| simple(link_content));
71
72 Ok((input, ContentNode::link_node(text, target)))
73}
74
75fn parse_node(input: &str) -> IResult<&str, ContentNode<&str>> {
76 alt((parse_text_node, parse_link_node))(input)
77}
78
79pub fn parse_passage(input: &str) -> IResult<&str, Passage<&str>> {
80 let (input, title) = parse_title(input)?;
81 let (input, _) = space0(input)?;
82 let (input, tags) = opt(parse_tags)(input)?;
83 let (input, _) = space0(input)?;
84 let (input, metadata) = opt(parse_metadata)(input)?;
85 let (input, _) = recognize(pair(space0, line_ending))(input)?;
86 let (input, content) = find_content_block(input)?;
87 let (input, _) = multispace0(input)?;
88
89 let mut nodes = vec![];
90 let mut content = content.trim_end_matches(&['\r', '\n']);
91 while !content.is_empty() {
92 let (c, node) = parse_node(content)?;
93 nodes.push(node);
94 content = c;
95 }
96
97 Ok((
98 input,
99 Passage::new(title, tags.unwrap_or_default(), metadata, nodes),
100 ))
101}
102
103#[cfg(test)]
104mod tests {
105 use nom::{
106 error::{Error, ErrorKind, ParseError},
107 Err,
108 };
109
110 use crate::{
111 parser::passage::{find_content_block, parse_passage, parse_tags, parse_title},
112 Metadata, Passage, Tag,
113 };
114
115 use super::{parse_link_node, parse_text_node, ContentNode};
116
117 #[test]
118 fn test_tags() {
119 let input = "[hello tag]";
120
121 assert_eq!(
122 parse_tags(input),
123 Ok(("", vec![Tag::new("hello"), Tag::new("tag")]))
124 );
125 }
126
127 #[test]
128 fn test_tags_escaped() {
129 let input = r"[hello\] tag]";
130
131 assert_eq!(
132 parse_tags(input),
133 Ok(("", vec![Tag::new(r"hello\]"), Tag::new("tag")]))
134 );
135 }
136
137 #[test]
138 fn test_tags_escaped_and_dash() {
139 let input = r"[hello\[-\]tag how-are you]";
140
141 assert_eq!(
142 parse_tags(input),
143 Ok((
144 "",
145 vec![
146 Tag::new(r"hello\[-\]tag"),
147 Tag::new("how-are"),
148 Tag::new("you")
149 ]
150 ))
151 );
152 }
153
154 #[test]
155 fn test_title() {
156 let story = ":: Hello, this is a title";
157
158 assert_eq!(parse_title(story), Ok(("", "Hello, this is a title")));
159 }
160
161 #[test]
162 fn test_title_with_tags() {
163 let story = ":: Hello, this is a title [tag1 tag2]";
164
165 assert_eq!(
166 parse_title(story),
167 Ok((" [tag1 tag2]", "Hello, this is a title"))
168 );
169 }
170
171 #[test]
172 fn test_title_with_metadata() {
173 let input = r#":: Hello, this is a title {"position":"600,400","size":"100,200"}"#;
174
175 assert_eq!(
176 parse_title(input),
177 Ok((
178 r#" {"position":"600,400","size":"100,200"}"#,
179 "Hello, this is a title"
180 ))
181 );
182 }
183
184 #[test]
185 fn test_title_start_with_whitespace() {
186 let input = r":: \ Second [tag]";
187 assert_eq!(parse_title(input), Ok((r#" [tag]"#, r"\ Second")));
188 }
189
190 #[test]
191 fn test_passage() {
192 let input = ":: Hello, this is a title [tag1 tag2]\n";
193
194 let expected = Passage::new(
195 "Hello, this is a title",
196 vec![Tag::new("tag1"), Tag::new("tag2")],
197 None,
198 vec![],
199 );
200
201 assert_eq!(parse_passage(input), Ok(("", expected)));
202 }
203
204 #[test]
205 fn test_passage_tag_and_metadata() {
206 let input =
207 ":: Hello, this is a title [tag1 tag2] {\"position\":\"900,600\",\"size\":\"200,200\"}\n";
208
209 let expected = Passage::new(
210 "Hello, this is a title",
211 vec![Tag::new("tag1"), Tag::new("tag2")],
212 Some(Metadata::new(r#"{"position":"900,600","size":"200,200"}"#)),
213 vec![],
214 );
215
216 assert_eq!(parse_passage(input), Ok(("", expected)));
217 }
218
219 #[test]
220 fn test_passage_til_next() {
221 let input = ":: Some title\nHello\n\n:: Other title";
222
223 let result = parse_passage(input);
224
225 println!("{result:?}");
226 }
227
228 #[test]
229 fn test_find_content_block() {
230 let input = "Hello\n\n:: Other title";
231
232 let result = find_content_block(input);
233
234 assert_eq!(result, Ok(("\n:: Other title", "Hello\n")));
235 }
236
237 #[test]
238 fn test_parse_text_node() {
239 let input = "Hello\nThis is text[[link]]";
240
241 assert_eq!(
242 parse_text_node(input),
243 Ok(("[[link]]", ContentNode::text_node("Hello\nThis is text")))
244 );
245 }
246
247 #[test]
248 fn test_parse_text_node_escaped_bracket() {
249 let input = "Hello\nThis is text\\[[link]]";
250
251 assert_eq!(
252 parse_text_node(input),
253 Ok(("", ContentNode::text_node(input)))
254 );
255 }
256
257 #[test]
258 fn test_parse_text_node_is_link_node() {
259 let input = "[[link]]";
260
261 assert_eq!(
262 parse_text_node(input),
263 Err(Err::Error(Error::from_error_kind(
264 input,
265 ErrorKind::TakeUntil,
266 )))
267 );
268 }
269
270 #[test]
271 fn test_parse_link_node_simple() {
272 let input = "[[link]]";
273
274 assert_eq!(
275 parse_link_node(input),
276 Ok(("", ContentNode::link_node("link", "link")))
277 )
278 }
279
280 #[test]
281 fn test_parse_link_node_pipe() {
282 let input = "[[first|First]]";
283
284 assert_eq!(
285 parse_link_node(input),
286 Ok(("", ContentNode::link_node("first", "First")))
287 )
288 }
289
290 #[test]
291 fn test_parse_link_node_to_right() {
292 let input = "[[some text->First page]]";
293
294 assert_eq!(
295 parse_link_node(input),
296 Ok(("", ContentNode::link_node("some text", "First page")))
297 )
298 }
299
300 #[test]
301 fn test_parse_link_node_to_left() {
302 let input = "[[A page<-going somewhere?]]";
303
304 assert_eq!(
305 parse_link_node(input),
306 Ok(("", ContentNode::link_node("going somewhere?", "A page")))
307 )
308 }
309
310 #[test]
311 fn test_find_content_block_weird_char() {
312 let input = "C'est ça\n:: Okay";
313
314 assert_eq!(find_content_block(input), Ok(("\n:: Okay", "C'est ça")));
315 }
316
317 #[test]
318 fn test_find_content_block_weird_char_end_text() {
319 let input = "C'est ça";
320
321 assert_eq!(find_content_block(input), Ok(("", "C'est ça")));
322 }
323
324 #[test]
325 fn test_parse_passage_weird_char() {
326 let input = r#":: Aller au bar {"position":"800,300","size":"100,100"}
327Bon, il est toujours possible de rencontrer des inconnus !
328Et ça tombe bien, tu connais un PMU juste en bas de chez toi !
329La majeur parti de ses clients sont des poivreaux assez sympathiques et ça tombe bien, tu en est également un."#;
330
331 assert!(parse_passage(input).is_ok());
332 }
333}