1use 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
15pub 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
32fn 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
68fn is_choice_fallback(selection_text: &InternalLine) -> bool {
72 selection_text
73 .chunk
74 .items
75 .iter()
76 .all(|item| item == &Content::Empty)
77}
78
79pub 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
104fn 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
112fn 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 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}