Skip to main content

inkling/line/parse/
choice.rs

1//! Parse choices as marked up `ParsedLineKind::Choice` objects.
2
3use crate::{
4    consts::{CHOICE_MARKER, STICKY_CHOICE_MARKER},
5    error::{parse::line::LineErrorKind, utils::MetaData},
6    line::{
7        parse::{
8            parse_choice_condition, parse_internal_line, parse_markers_and_text,
9            split_at_divert_marker,
10        },
11        Content, InternalChoice, InternalChoiceBuilder, InternalLine, ParsedLineKind,
12    },
13};
14
15/// Parse a `ParsedLineKind::Choice` from a line if the line represents a choice.
16pub fn parse_choice(
17    content: &str,
18    meta_data: &MetaData,
19) -> Result<Option<ParsedLineKind>, LineErrorKind> {
20    parse_choice_markers_and_text(content)?
21        .map(|(level, is_sticky, line)| {
22            parse_choice_data(line, meta_data)
23                .map(|mut choice_data| {
24                    choice_data.is_sticky = is_sticky;
25                    (level, choice_data)
26                })
27                .map(|(level, choice_data)| ParsedLineKind::Choice { level, choice_data })
28        })
29        .transpose()
30}
31
32/// Parse the content of an `InternalChoice` from a line.
33///
34/// The line should not contain the markers used to determine whether a line of content
35/// represents a choice. It should only contain the part of the line which represents
36/// the choice text.
37fn parse_choice_data(content: &str, meta_data: &MetaData) -> Result<InternalChoice, LineErrorKind> {
38    let mut buffer = content.to_string();
39    let choice_conditions = parse_choice_condition(&mut buffer)?;
40
41    let (selection_text_line, display_text_line) = parse_choice_line_variants(&buffer)?;
42
43    let (without_divert, _) = split_at_divert_marker(&selection_text_line);
44    let selection_text = parse_internal_line(without_divert, meta_data)?;
45
46    let is_fallback = is_choice_fallback(&selection_text);
47
48    let display_text = match parse_internal_line(&display_text_line, meta_data) {
49        Err(LineErrorKind::EmptyDivert) if is_fallback => {
50            let (without_divert, _) = split_at_divert_marker(&display_text_line);
51            parse_internal_line(without_divert, meta_data)
52        }
53        result => result,
54    }?;
55
56    let mut builder = InternalChoiceBuilder::from_line(display_text);
57
58    if let Some(ref condition) = choice_conditions {
59        builder.set_condition(condition);
60    }
61
62    builder.set_is_fallback(is_fallback);
63    builder.set_selection_text(selection_text);
64
65    Ok(builder.build())
66}
67
68/// Check whether a choice line is a fallback.
69///
70/// The condition for a fallback choice is that it has no displayed text for the user.
71fn is_choice_fallback(selection_text: &InternalLine) -> bool {
72    selection_text
73        .chunk
74        .items
75        .iter()
76        .all(|item| item == &Content::Empty)
77}
78
79/// Split choice markers from a line and determine whether it is sticky.
80///
81/// If markers are present, ensure that the line does not have both sticky and non-sticky markers.
82/// Return the number of markers along with whether the choice was sticky and the remaining line.
83pub fn parse_choice_markers_and_text(
84    content: &str,
85) -> Result<Option<(u32, bool, &str)>, LineErrorKind> {
86    let is_sticky = marker_exists_before_text(content, STICKY_CHOICE_MARKER);
87    let is_not_sticky = marker_exists_before_text(content, CHOICE_MARKER);
88
89    let marker = match (is_sticky, is_not_sticky) {
90        (false, false) => None,
91        (true, false) => Some(STICKY_CHOICE_MARKER),
92        (false, true) => Some(CHOICE_MARKER),
93        (true, true) => {
94            return Err(LineErrorKind::StickyAndNonSticky);
95        }
96    };
97
98    marker
99        .and_then(|c| parse_markers_and_text(content, c))
100        .map(|(level, line)| Ok((level, is_sticky, line)))
101        .transpose()
102}
103
104/// Check whether the input marker appears before the line text content.
105fn marker_exists_before_text(line: &str, marker: char) -> bool {
106    line.find(|c: char| !(c.is_whitespace() || c == CHOICE_MARKER || c == STICKY_CHOICE_MARKER))
107        .map(|i| line.get(..i).unwrap())
108        .unwrap_or(line)
109        .contains(marker)
110}
111
112/// Return `selection_text` and `display_text` strings from a line.
113///
114/// These are demarcated by `[]` brackets. Content before the bracket is both selection
115/// and display text. Content inside the bracket is only for the selection and content
116/// after the bracket only for display.
117fn parse_choice_line_variants(line: &str) -> Result<(String, String), LineErrorKind> {
118    match (line.find('['), line.find(']')) {
119        (Some(i), Some(j)) if i < j => {
120            // Ensure that we don't have more brackets
121            if line.rfind('[').unwrap() != i || line.rfind(']').unwrap() != j {
122                return Err(LineErrorKind::UnmatchedBrackets);
123            }
124
125            let head = line.get(..i).unwrap();
126            let inside = line.get(i + 1..j).unwrap();
127            let tail = line.get(j + 1..).unwrap();
128
129            let selection_text = format!("{}{}", head, inside);
130            let display_text = format!("{}{}", head, tail);
131
132            Ok((selection_text, display_text))
133        }
134        (None, None) => Ok((line.to_string(), line.to_string())),
135        _ => Err(LineErrorKind::UnmatchedBrackets),
136    }
137}
138
139#[cfg(test)]
140pub(crate) mod tests {
141    use super::*;
142
143    impl InternalChoice {
144        pub fn from_string(line: &str) -> Self {
145            parse_choice_data(line, &().into()).unwrap()
146        }
147    }
148
149    #[test]
150    fn parsing_line_with_no_choice_markers_returns_none() {
151        assert!(parse_choice_markers_and_text("Choice").unwrap().is_none());
152        assert!(parse_choice_markers_and_text("  Choice  ")
153            .unwrap()
154            .is_none());
155        assert!(parse_choice_markers_and_text("- Choice  ")
156            .unwrap()
157            .is_none());
158    }
159
160    #[test]
161    fn parsing_line_with_choice_markers_gets_number_of_markers() {
162        let (level, _, _) = parse_choice_markers_and_text("* Choice").unwrap().unwrap();
163        assert_eq!(level, 1);
164
165        let (level, _, _) = parse_choice_markers_and_text("** Choice").unwrap().unwrap();
166        assert_eq!(level, 2);
167
168        let (level, _, _) = parse_choice_markers_and_text("**** Choice")
169            .unwrap()
170            .unwrap();
171        assert_eq!(level, 4);
172    }
173
174    #[test]
175    fn number_of_markers_parsing_ignores_whitespace() {
176        let (level, _, _) = parse_choice_markers_and_text("  * * *   *     Choice")
177            .unwrap()
178            .unwrap();
179        assert_eq!(level, 4);
180    }
181
182    #[test]
183    fn sticky_choice_markers_gives_sticky_choices_and_vice_versa() {
184        let (_, is_sticky, _) = parse_choice_markers_and_text("* Choice").unwrap().unwrap();
185        assert!(!is_sticky);
186
187        let (_, is_sticky, _) = parse_choice_markers_and_text("+ Choice").unwrap().unwrap();
188        assert!(is_sticky);
189    }
190
191    #[test]
192    fn lines_cannot_have_both_sticky_and_non_sticky_markers_in_the_head() {
193        assert!(parse_choice_markers_and_text("*+ Choice").is_err());
194        assert!(parse_choice_markers_and_text("+* Choice").is_err());
195        assert!(parse_choice_markers_and_text(" +++*+ Choice").is_err());
196        assert!(parse_choice_markers_and_text("+ Choice *").is_ok());
197    }
198
199    #[test]
200    fn text_after_choice_markers_is_returned_when_parsing() {
201        let (_, _, line) = parse_choice_markers_and_text("* * Choice")
202            .unwrap()
203            .unwrap();
204        assert_eq!(line, "Choice");
205
206        let (_, _, line) = parse_choice_markers_and_text("+++ Choice")
207            .unwrap()
208            .unwrap();
209        assert_eq!(line, "Choice");
210    }
211
212    #[test]
213    fn simple_lines_parse_into_choices_with_same_display_and_selection_texts() {
214        let choice = parse_choice_data("Choice line", &().into()).unwrap();
215        let comparison = parse_internal_line("Choice line", &().into()).unwrap();
216
217        assert_eq!(*choice.selection_text.lock().unwrap(), comparison);
218        assert_eq!(choice.display_text, comparison);
219    }
220
221    #[test]
222    fn choices_can_be_parsed_with_alternatives_in_selection_text() {
223        let choice = parse_choice_data("Hi! {One|Two}", &().into()).unwrap();
224        assert_eq!(
225            *choice.selection_text.lock().unwrap(),
226            parse_internal_line("Hi! {One|Two}", &().into()).unwrap(),
227        );
228    }
229
230    #[test]
231    fn braces_with_backslash_are_not_conditions() {
232        let choice = parse_choice_data("\\{One|Two}", &().into()).unwrap();
233        assert_eq!(
234            *choice.selection_text.lock().unwrap(),
235            parse_internal_line("{One|Two}", &().into()).unwrap(),
236        );
237    }
238
239    #[test]
240    fn alternatives_can_be_within_brackets() {
241        let choice = parse_choice_data("[{One|Two}]", &().into()).unwrap();
242        assert_eq!(
243            *choice.selection_text.lock().unwrap(),
244            parse_internal_line("{One|Two}", &().into()).unwrap(),
245        );
246    }
247
248    #[test]
249    fn choice_with_variants_set_selection_and_display_text_separately() {
250        let choice = parse_choice_data("Selection[] plus display", &().into()).unwrap();
251
252        assert_eq!(
253            *choice.selection_text.lock().unwrap(),
254            parse_internal_line("Selection", &().into()).unwrap()
255        );
256        assert_eq!(
257            choice.display_text,
258            parse_internal_line("Selection plus display", &().into()).unwrap()
259        );
260
261        let choice = parse_choice_data("[Separate selection]And display", &().into()).unwrap();
262
263        assert_eq!(
264            *choice.selection_text.lock().unwrap(),
265            parse_internal_line("Separate selection", &().into()).unwrap()
266        );
267        assert_eq!(
268            choice.display_text,
269            parse_internal_line("And display", &().into()).unwrap()
270        );
271    }
272
273    #[test]
274    fn choice_with_no_selection_text_but_divert_is_fallback() {
275        assert!(
276            parse_choice_data("-> world", &().into())
277                .unwrap()
278                .is_fallback
279        );
280        assert!(
281            parse_choice_data(" -> world", &().into())
282                .unwrap()
283                .is_fallback
284        );
285    }
286
287    #[test]
288    fn choice_which_is_fallback_can_have_empty_divert() {
289        assert!(
290            parse_choice_data("->", &().into())
291                .expect("one")
292                .is_fallback
293        );
294        assert!(
295            parse_choice_data(" -> ", &().into())
296                .expect("two")
297                .is_fallback
298        );
299    }
300
301    #[test]
302    fn choices_without_displayed_text_can_have_regular_text() {
303        let choice = parse_choice_data("[]", &().into()).unwrap();
304
305        assert!(choice.is_fallback);
306
307        assert_eq!(
308            choice.display_text,
309            parse_internal_line("", &().into()).unwrap()
310        );
311
312        let choice = parse_choice_data("[] Some text", &().into()).unwrap();
313
314        assert!(choice.is_fallback);
315
316        assert_eq!(
317            choice.display_text,
318            parse_internal_line(" Some text", &().into()).unwrap()
319        );
320    }
321
322    #[test]
323    fn choices_can_be_parsed_with_conditions() {
324        let choice = parse_choice_data("{knot_name} Hello, World!", &().into()).unwrap();
325        assert!(choice.condition.is_some());
326    }
327
328    #[test]
329    fn parsing_choice_line_variants_return_same_line_if_no_brackets_are_present() {
330        let (displayed, line) = parse_choice_line_variants("Hello, World!").unwrap();
331        assert_eq!(displayed, line);
332    }
333
334    #[test]
335    fn parsing_choice_line_variants_break_the_displayed_line_when_encountering_square_brackets() {
336        let (displayed, line) = parse_choice_line_variants("Hello[], World!").unwrap();
337        assert_eq!(&displayed, "Hello");
338        assert_eq!(&line, "Hello, World!");
339    }
340
341    #[test]
342    fn parsing_choice_line_variants_include_content_inside_square_brackets_in_displayed() {
343        let (displayed, line) = parse_choice_line_variants("Hello[!], World!").unwrap();
344        assert_eq!(&displayed, "Hello!");
345        assert_eq!(&line, "Hello, World!");
346    }
347
348    #[test]
349    fn parsing_choice_line_variants_return_error_if_brackets_are_unmatched() {
350        assert!(parse_choice_line_variants("Hello[!, World!").is_err());
351        assert!(parse_choice_line_variants("Hello]!, World!").is_err());
352    }
353
354    #[test]
355    fn parsing_choice_line_variants_return_error_more_brackets_are_found() {
356        assert!(parse_choice_line_variants("Hello[!], [Worl] d!").is_err());
357        assert!(parse_choice_line_variants("Hello[!], [World!").is_err());
358        assert!(parse_choice_line_variants("Hello[!], ]World!").is_err());
359    }
360
361    #[test]
362    fn parsing_choice_line_variants_return_error_if_brackets_are_reversed() {
363        assert!(parse_choice_line_variants("Hello][, World!").is_err());
364    }
365}