twee_v3/parser/
passage.rs

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}