Skip to main content

inkling/story/
parse.rs

1//! Parsing of story content.
2//!
3//! While [individual lines][crate::line::parse] and the [nested tree node][crate::node::parse]
4//! structure of stitches are parsed elsewhere this module takes individual lines and groups
5//! them into knots and stitches. The content is then parsed using the aforementioned modules
6//! to create the final story structure.
7
8use crate::{
9    consts::{
10        CONST_MARKER, EXTERNAL_FUNCTION_MARKER, INCLUDE_MARKER, KNOT_MARKER, LINE_COMMENT_MARKER,
11        ROOT_KNOT_NAME, STITCH_MARKER, TAG_MARKER, TODO_COMMENT_MARKER, VARIABLE_MARKER,
12    },
13    error::{
14        parse::{
15            knot::{KnotError, KnotErrorKind, KnotNameError},
16            prelude::{PreludeError, PreludeErrorKind},
17            ParseError,
18        },
19        utils::MetaData,
20        ReadError,
21    },
22    knot::{parse_stitch_from_lines, read_knot_name, read_stitch_name, Knot, KnotSet, Stitch},
23    line::{parse_variable, Variable},
24    story::types::{VariableInfo, VariableSet},
25};
26
27use std::collections::HashMap;
28
29/// Read an Ink story from a string and return knots along with the metadata.
30pub fn read_story_content_from_string(
31    content: &str,
32) -> Result<(KnotSet, VariableSet, Vec<String>), ReadError> {
33    let all_lines = content
34        .lines()
35        .zip(0..)
36        .map(|(line, line_index)| (line, MetaData { line_index }))
37        .collect::<Vec<_>>();
38
39    let mut content_lines = remove_empty_and_comment_lines(all_lines);
40
41    let (root_knot, variables, tags, prelude_errors) =
42        split_off_and_parse_prelude(&mut content_lines)?;
43
44    let (mut knots, mut knot_errors) = parse_knots_from_lines(content_lines);
45
46    match root_knot {
47        Ok(knot) => {
48            knots.insert(ROOT_KNOT_NAME.to_string(), knot);
49        }
50        Err(knot_error) => knot_errors.insert(0, knot_error),
51    }
52
53    if knot_errors.is_empty() && prelude_errors.is_empty() {
54        Ok((knots, variables, tags))
55    } else {
56        Err(ParseError {
57            knot_errors,
58            prelude_errors,
59        }
60        .into())
61    }
62}
63
64/// Split off lines until the first named knot then parse its content and root knot.
65///
66/// After this function has been called, the given set of lines starts at the first named
67/// knot.
68///
69/// Will return an error only if there is no story content in the given list. Prelude errors
70/// are collected into a list which is returned in the `Ok` value. Any errors from parsing
71/// the root knot is returned in that item. This is all because we want to collect all
72/// encountered errors from parsing the story at once, not just the first.
73fn split_off_and_parse_prelude(
74    lines: &mut Vec<(&str, MetaData)>,
75) -> Result<
76    (
77        Result<Knot, KnotError>,
78        VariableSet,
79        Vec<String>,
80        Vec<PreludeError>,
81    ),
82    ReadError,
83> {
84    let prelude_and_root = split_off_prelude_lines(lines);
85    let (prelude_lines, root_lines) = split_prelude_into_metadata_and_text(&prelude_and_root);
86
87    let root_meta_data = root_lines
88        .first()
89        .or(lines.first())
90        .or(prelude_lines.last())
91        .map(|(_, meta_data)| meta_data.clone())
92        .ok_or(ReadError::Empty)?;
93
94    let tags = parse_global_tags(&prelude_lines);
95    let (variables, prelude_errors) = parse_global_variables(&prelude_lines);
96    let root_knot = parse_root_knot_from_lines(root_lines, root_meta_data);
97
98    Ok((root_knot, variables, tags, prelude_errors))
99}
100
101/// Parse all knots from a set of lines and return along with any encountered errors.
102fn parse_knots_from_lines(lines: Vec<(&str, MetaData)>) -> (KnotSet, Vec<KnotError>) {
103    let knot_line_sets = divide_lines_at_marker(lines, KNOT_MARKER);
104
105    let mut knots = HashMap::new();
106    let mut knot_errors = Vec::new();
107
108    for lines in knot_line_sets.into_iter().filter(|lines| !lines.is_empty()) {
109        match get_knot_from_lines(lines) {
110            Ok((knot_name, knot_data)) => {
111                if !knots.contains_key(&knot_name) {
112                    knots.insert(knot_name, knot_data);
113                } else {
114                    let prev_meta_data = knots.get(&knot_name).unwrap().meta_data.clone();
115
116                    knot_errors.push(KnotError {
117                        knot_meta_data: knot_data.meta_data.clone(),
118                        line_errors: vec![KnotErrorKind::DuplicateKnotName {
119                            name: knot_name,
120                            prev_meta_data,
121                        }],
122                    });
123                }
124            }
125            Err(error) => knot_errors.push(error),
126        }
127    }
128
129    (knots, knot_errors)
130}
131
132/// Parse the root knot from a set of lines.
133fn parse_root_knot_from_lines(
134    lines: Vec<(&str, MetaData)>,
135    meta_data: MetaData,
136) -> Result<Knot, KnotError> {
137    let (_, stitches, line_errors) = get_stitches_from_lines(lines, ROOT_KNOT_NAME);
138
139    if line_errors.is_empty() {
140        Ok(Knot {
141            default_stitch: ROOT_KNOT_NAME.to_string(),
142            stitches,
143            tags: Vec::new(),
144            meta_data,
145        })
146    } else {
147        Err(KnotError {
148            knot_meta_data: meta_data,
149            line_errors,
150        })
151    }
152}
153
154/// Parse a single `Knot` from a set of lines.
155///
156/// Creates `Stitch`es and their node tree of branching content. Returns the knot and its name.
157///
158/// Assumes that the set of lines is non-empty, which we assert before calling this function.
159fn get_knot_from_lines(lines: Vec<(&str, MetaData)>) -> Result<(String, Knot), KnotError> {
160    let (head, mut tail) = lines
161        .split_first()
162        .map(|(head, tail)| (head, tail.to_vec()))
163        .unwrap();
164
165    let (head_line, knot_meta_data) = head;
166
167    let mut line_errors = Vec::new();
168
169    let knot_name = match read_knot_name(head_line) {
170        Ok(name) => name,
171        Err(kind) => {
172            let (invalid_name, error) = get_invalid_name_error(head_line, kind, &knot_meta_data);
173
174            line_errors.push(error);
175
176            invalid_name
177        }
178    };
179
180    if tail.is_empty() {
181        line_errors.push(KnotErrorKind::EmptyKnot);
182    }
183
184    let tags = get_knot_tags(&mut tail);
185
186    let (default_stitch, stitches, stitch_errors) = get_stitches_from_lines(tail, &knot_name);
187    line_errors.extend(stitch_errors);
188
189    if default_stitch.is_some() && line_errors.is_empty() {
190        Ok((
191            knot_name,
192            Knot {
193                default_stitch: default_stitch.unwrap(),
194                stitches,
195                tags,
196                meta_data: knot_meta_data.clone(),
197            },
198        ))
199    } else {
200        Err(KnotError {
201            knot_meta_data: knot_meta_data.clone(),
202            line_errors,
203        })
204    }
205}
206
207/// Parse all stitches from a set of lines and return along with encountered errors.
208fn get_stitches_from_lines(
209    lines: Vec<(&str, MetaData)>,
210    knot_name: &str,
211) -> (Option<String>, HashMap<String, Stitch>, Vec<KnotErrorKind>) {
212    let knot_stitch_sets = divide_lines_at_marker(lines, STITCH_MARKER);
213
214    let mut default_stitch = None;
215    let mut stitches = HashMap::new();
216    let mut line_errors = Vec::new();
217
218    for (stitch_index, lines) in knot_stitch_sets
219        .into_iter()
220        .enumerate()
221        .filter(|(_, lines)| !lines.is_empty())
222    {
223        match get_stitch_from_lines(lines, stitch_index, knot_name) {
224            Ok((name, stitch)) => {
225                if default_stitch.is_none() {
226                    default_stitch.replace(name.clone());
227                }
228
229                if !stitches.contains_key(&name) {
230                    stitches.insert(name, stitch);
231                } else {
232                    let prev_meta_data = stitches.get(&name).unwrap().meta_data.clone();
233
234                    line_errors.push(KnotErrorKind::DuplicateStitchName {
235                        name: name,
236                        knot_name: knot_name.to_string(),
237                        meta_data: stitch.meta_data.clone(),
238                        prev_meta_data,
239                    });
240                }
241            }
242            Err(errors) => line_errors.extend(errors),
243        }
244    }
245
246    (default_stitch, stitches, line_errors)
247}
248
249/// Parse a single `Stitch` from a set of lines.
250///
251/// If a stitch name is found, return it too. This should be found for all stitches except
252/// possibly the first in a set, since we split the knot line content where the names are found.
253///
254/// This function assumes that at least one non-empty line exists in the set, from which
255/// the `MetaData` and stitch name (unless it's the root) will be read. This will always be
256/// the case, since we split the knot line content at stitch name markers and filter empty
257/// lines before calling this.
258fn get_stitch_from_lines(
259    mut lines: Vec<(&str, MetaData)>,
260    stitch_index: usize,
261    knot_name: &str,
262) -> Result<(String, Stitch), Vec<KnotErrorKind>> {
263    let mut line_errors = Vec::new();
264
265    let (first_line, meta_data) = lines[0].clone();
266
267    let stitch_name = match get_stitch_name(first_line, &meta_data) {
268        Ok(name) => {
269            if name.is_some() {
270                lines.remove(0);
271            }
272
273            get_stitch_identifier(name, stitch_index)
274        }
275        Err(kind) => {
276            line_errors.push(kind);
277            "$INVALID_NAME$".to_string()
278        }
279    };
280
281    match parse_stitch_from_lines(&lines, knot_name, &stitch_name, meta_data) {
282        Ok(stitch) => {
283            if line_errors.is_empty() {
284                Ok((stitch_name, stitch))
285            } else {
286                Err(line_errors)
287            }
288        }
289        Err(errors) => {
290            line_errors.extend(errors);
291            Err(line_errors)
292        }
293    }
294}
295
296/// Read stitch name from the first line in a set.
297///
298/// If the name was present, return it. If it was not present, return None. If there was
299/// another type of error reading the name, return that.
300fn get_stitch_name(
301    first_line: &str,
302    meta_data: &MetaData,
303) -> Result<Option<String>, KnotErrorKind> {
304    match read_stitch_name(first_line) {
305        Ok(name) => Ok(Some(name)),
306        Err(KnotNameError::Empty) => Ok(None),
307        Err(kind) => Err(KnotErrorKind::InvalidName {
308            line: first_line.to_string(),
309            kind,
310            meta_data: meta_data.clone(),
311        }),
312    }
313}
314
315/// Get an invalid knot name error and a default to use while checking remaining content.
316fn get_invalid_name_error(
317    line: &str,
318    kind: KnotNameError,
319    meta_data: &MetaData,
320) -> (String, KnotErrorKind) {
321    let invalid_name = "$INVALID_NAME$".to_string();
322
323    let error = KnotErrorKind::InvalidName {
324        line: line.to_string(),
325        kind,
326        meta_data: meta_data.clone(),
327    };
328
329    (invalid_name, error)
330}
331
332/// Get a verified name for a stitch.
333///
334/// Stitches are name spaced under their parent knot. If the given stitch has no read name
335/// but is the first content in the knot, it gets the [default name][crate::consts::ROOT_KNOT_NAME].
336fn get_stitch_identifier(name: Option<String>, stitch_index: usize) -> String {
337    match (stitch_index, name) {
338        (0, None) => ROOT_KNOT_NAME.to_string(),
339        (_, Some(name)) => format!("{}", name),
340        _ => unreachable!(
341            "No stitch name was present after dividing the set of lines into groups where \
342             the first line of each group is the stitch name: this is a contradiction which \
343             should not be possible."
344        ),
345    }
346}
347
348/// Parse knot tags from lines until the first line with content.
349///
350/// The lines which contain tags are split off of the input list.
351fn get_knot_tags(lines: &mut Vec<(&str, MetaData)>) -> Vec<String> {
352    if let Some(i) = lines
353        .iter()
354        .map(|(line, _)| line.trim_start())
355        .position(|line| !(line.is_empty() || line.starts_with('#')))
356    {
357        lines
358            .drain(..i)
359            .map(|(line, _)| line.trim())
360            .filter(|line| !line.is_empty())
361            .map(|line| line.trim_start_matches("#").trim_start().to_string())
362            .collect()
363    } else {
364        Vec::new()
365    }
366}
367
368/// Split a set of lines where they start with a marker.
369fn divide_lines_at_marker<'a>(
370    mut content: Vec<(&'a str, MetaData)>,
371    marker: &str,
372) -> Vec<Vec<(&'a str, MetaData)>> {
373    let mut buffer = Vec::new();
374
375    while let Some(i) = content
376        .iter()
377        .rposition(|(line, _)| line.trim_start().starts_with(marker))
378    {
379        buffer.push(content.split_off(i));
380    }
381
382    if !content.is_empty() {
383        buffer.push(content);
384    }
385
386    buffer.into_iter().rev().collect()
387}
388
389/// Filter empty and comment lines from a set.
390///
391/// Should at some point be removed since we ultimately want to return errors from parsing
392/// lines along with their original line numbers, which are thrown away by filtering some
393/// of them.
394fn remove_empty_and_comment_lines(content: Vec<(&str, MetaData)>) -> Vec<(&str, MetaData)> {
395    content
396        .into_iter()
397        .inspect(|(line, meta_data)| {
398            if line.starts_with(TODO_COMMENT_MARKER) {
399                eprintln!("{} (line {})", &line, meta_data.line_index + 1);
400            }
401        })
402        .filter(|(line, _)| {
403            !(line.starts_with(LINE_COMMENT_MARKER) || line.starts_with(TODO_COMMENT_MARKER))
404        })
405        .filter(|(line, _)| !line.trim().is_empty())
406        .map(|(line, meta_data)| {
407            if let Some(i) = line.find("//") {
408                (line.get(..i).unwrap(), meta_data)
409            } else {
410                (line, meta_data)
411            }
412        })
413        .collect()
414}
415
416/// Split given list of lines into a prelude and knot content.
417///
418/// The prelude contains metadata and the root knot, which the story will start from.
419fn split_off_prelude_lines<'a>(lines: &mut Vec<(&'a str, MetaData)>) -> Vec<(&'a str, MetaData)> {
420    let i = lines
421        .iter()
422        .position(|(line, _)| line.trim_start().starts_with(KNOT_MARKER))
423        .unwrap_or(lines.len());
424
425    lines.drain(..i).collect()
426}
427
428/// Split prelude content into metadata and root text content.
429fn split_prelude_into_metadata_and_text<'a>(
430    lines: &[(&'a str, MetaData)],
431) -> (Vec<(&'a str, MetaData)>, Vec<(&'a str, MetaData)>) {
432    // Add spaces after all keywords (except line comment) to search for whole words.
433    let metadata_keywords = &[
434        format!("{} ", CONST_MARKER),
435        format!("{} ", EXTERNAL_FUNCTION_MARKER),
436        format!("{} ", INCLUDE_MARKER),
437        format!("{} ", VARIABLE_MARKER),
438        format!("{} ", TODO_COMMENT_MARKER),
439        format!("{}", LINE_COMMENT_MARKER),
440    ];
441
442    const METADATA_CHARS: &[char] = &[TAG_MARKER];
443
444    if let Some(i) = lines
445        .iter()
446        .map(|(line, _)| line.trim_start())
447        .position(|line| {
448            metadata_keywords.iter().all(|key| !line.starts_with(key))
449                && METADATA_CHARS.iter().all(|&c| !line.starts_with(c))
450                && !line.is_empty()
451        })
452    {
453        let (metadata, text) = lines.split_at(i);
454        (metadata.to_vec(), text.to_vec())
455    } else {
456        (lines.to_vec(), Vec::new())
457    }
458}
459
460/// Parse global tags from a set of metadata lines in the prelude.
461fn parse_global_tags(lines: &[(&str, MetaData)]) -> Vec<String> {
462    lines
463        .iter()
464        .map(|(line, _)| line.trim())
465        .filter(|line| line.starts_with(TAG_MARKER))
466        .map(|line| line.get(1..).unwrap().trim().to_string())
467        .collect()
468}
469
470/// Parse global variables from a set of metadata lines in the prelude.
471fn parse_global_variables(lines: &[(&str, MetaData)]) -> (VariableSet, Vec<PreludeError>) {
472    let mut variables = HashMap::new();
473    let mut errors = Vec::new();
474
475    for (line, meta_data) in lines
476        .iter()
477        .map(|(line, meta_data)| (line.trim(), meta_data))
478        .filter(|(line, _)| line.starts_with(VARIABLE_MARKER))
479    {
480        if let Err(kind) = parse_variable_with_name(line).and_then(|(name, variable)| {
481            let variable_info = VariableInfo {
482                variable,
483                meta_data: meta_data.clone(),
484            };
485
486            match variables.insert(name.clone(), variable_info) {
487                Some(_) => Err(PreludeErrorKind::DuplicateVariable { name }),
488                None => Ok(()),
489            }
490        }) {
491            errors.push(PreludeError {
492                line: line.to_string(),
493                kind,
494                meta_data: meta_data.clone(),
495            });
496        }
497    }
498
499    (variables, errors)
500}
501
502/// Parse a single variable line into the variable name and initial value.
503///
504/// Variable lines are on the form `VAR variable_name = initial_value`.
505fn parse_variable_with_name(line: &str) -> Result<(String, Variable), PreludeErrorKind> {
506    line.find('=')
507        .ok_or_else(|| PreludeErrorKind::NoVariableAssignment)
508        .and_then(|i| {
509            let start = VARIABLE_MARKER.len();
510            let variable_name = line.get(start..i).unwrap().trim().to_string();
511
512            if variable_name.is_empty() {
513                Err(PreludeErrorKind::NoVariableName)
514            } else {
515                Ok((i, variable_name))
516            }
517        })
518        .and_then(|(i, variable_name)| {
519            parse_variable(line.get(i + 1..).unwrap().trim())
520                .map(|value| (variable_name, value))
521                .map_err(|err| err.into())
522        })
523}
524
525#[cfg(test)]
526pub mod tests {
527    use super::*;
528
529    use crate::knot::Address;
530
531    pub fn read_knots_from_string(content: &str) -> Result<KnotSet, Vec<KnotError>> {
532        let lines = content
533            .lines()
534            .enumerate()
535            .filter(|(_, line)| !line.trim().is_empty())
536            .map(|(i, line)| (line, MetaData::from(i)))
537            .collect();
538
539        let (knots, knot_errors) = parse_knots_from_lines(lines);
540
541        if knot_errors.is_empty() {
542            Ok(knots)
543        } else {
544            Err(knot_errors)
545        }
546    }
547
548    fn enumerate<'a>(lines: &[&'a str]) -> Vec<(&'a str, MetaData)> {
549        lines
550            .into_iter()
551            .map(|line| *line)
552            .enumerate()
553            .map(|(i, line)| (line, MetaData::from(i)))
554            .collect()
555    }
556
557    fn denumerate<'a, T>(lines: Vec<(&'a str, T)>) -> Vec<&'a str> {
558        lines.into_iter().map(|(line, _)| line).collect()
559    }
560
561    #[test]
562    fn split_lines_into_knots_and_preludes_drains_nothing_if_knots_begin_at_index_zero() {
563        let mut lines = enumerate(&["=== knot ==="]);
564
565        assert!(split_off_prelude_lines(&mut lines).is_empty());
566        assert_eq!(&denumerate(lines), &["=== knot ==="]);
567    }
568
569    #[test]
570    fn split_lines_into_knots_and_prelude_drains_all_items_if_knot_is_never_encountered() {
571        let mut lines = enumerate(&["No knot here, just prelude content"]);
572
573        split_off_prelude_lines(&mut lines);
574
575        assert!(lines.is_empty());
576    }
577
578    #[test]
579    fn split_lines_into_knots_and_prelude_drains_lines_up_until_first_knot() {
580        let mut lines = enumerate(&[
581            "Prelude content ",
582            "comes before ",
583            "the first named knot.",
584            "",
585            "=== here ===",
586            "Line one.",
587        ]);
588
589        let prelude = split_off_prelude_lines(&mut lines);
590
591        assert_eq!(
592            &denumerate(prelude),
593            &[
594                "Prelude content ",
595                "comes before ",
596                "the first named knot.",
597                ""
598            ]
599        );
600        assert_eq!(&denumerate(lines), &["=== here ===", "Line one."]);
601    }
602
603    #[test]
604    fn prelude_can_be_further_split_into_metadata_and_prelude_text() {
605        let lines = &[
606            "# All prelude content",
607            "",
608            "# comes before",
609            "The first regular string.",
610        ];
611
612        let (metadata, text) = split_prelude_into_metadata_and_text(&enumerate(lines));
613
614        assert_eq!(
615            &denumerate(metadata),
616            &["# All prelude content", "", "# comes before"]
617        );
618        assert_eq!(&denumerate(text), &["The first regular string."]);
619    }
620
621    #[test]
622    fn metadata_stops_when_it_does_not_start_with_variable_include_or_tag() {
623        let lines = &[
624            "# Tag",
625            "VAR variable",
626            "CONST constant variable",
627            "INCLUDE include",
628            "// line comment",
629            "TODO: comment",
630            "Regular line.",
631        ];
632
633        let (metadata, text) = split_prelude_into_metadata_and_text(&enumerate(lines));
634
635        assert_eq!(metadata.len(), 6);
636        assert_eq!(&denumerate(text), &["Regular line."]);
637    }
638
639    #[test]
640    fn parse_global_tags_from_metadata() {
641        let lines = &[
642            "# Tag",
643            "VAR variable",
644            "# Tag two ",
645            "// line comment",
646            "TODO: comment",
647        ];
648
649        assert_eq!(&parse_global_tags(&enumerate(lines)), &["Tag", "Tag two"]);
650    }
651
652    #[test]
653    fn parse_variables_from_metadata() {
654        let lines = &[
655            "# Tag",
656            "VAR float = 1.0",
657            "# Tag two ",
658            "VAR string = \"two words\"",
659        ];
660
661        let (variables, _) = parse_global_variables(&enumerate(lines));
662
663        assert_eq!(variables.len(), 2);
664        assert_eq!(
665            variables.get("float").unwrap().variable,
666            Variable::Float(1.0)
667        );
668        assert_eq!(
669            variables.get("string").unwrap().variable,
670            Variable::String("two words".to_string())
671        );
672    }
673
674    #[test]
675    fn two_variables_with_same_name_yields_error() {
676        let lines = &["VAR variable = 1.0", "VAR variable = \"two words\""];
677
678        let (_, errors) = parse_global_variables(&enumerate(lines));
679
680        assert_eq!(errors.len(), 1);
681    }
682
683    #[test]
684    fn global_variables_are_parsed_with_metadata() {
685        let lines = &["VAR float = 1.0", "VAR string = \"two words\""];
686
687        let (variables, _) = parse_global_variables(&enumerate(lines));
688
689        assert_eq!(variables.get("string").unwrap().meta_data, 1.into());
690    }
691
692    #[test]
693    fn parse_global_variables_returns_all_errors() {
694        let lines = &[
695            "VAR float = 1.0",
696            "VAR = 1.0",                  // no variable name
697            "VAR variable = ",            // no assignment
698            "VAR variable 10",            // no assignment operator
699            "VAR variable = 10chars",     // invalid characters in number
700            "VAR variable = \"two words", // unmatched quote marks
701            "VAR int = 10",
702        ];
703
704        let (variables, errors) = parse_global_variables(&enumerate(lines));
705
706        assert_eq!(variables.len(), 2);
707        assert_eq!(errors.len(), 5);
708    }
709
710    #[test]
711    fn regular_lines_can_start_with_variable_divert_or_text() {
712        let lines = &["# Tag", "Regular line."];
713
714        let (_, text) = split_prelude_into_metadata_and_text(&enumerate(lines));
715
716        assert_eq!(&denumerate(text), &["Regular line."]);
717
718        let lines_divert = &["# Tag", "-> divert"];
719
720        let (_, divert) = split_prelude_into_metadata_and_text(&enumerate(lines_divert));
721
722        assert_eq!(&denumerate(divert), &["-> divert"]);
723
724        let lines_variable = &["# Tag", "{variable}"];
725
726        let (_, variable) = split_prelude_into_metadata_and_text(&enumerate(lines_variable));
727
728        assert_eq!(&denumerate(variable), &["{variable}"]);
729    }
730
731    #[test]
732    fn read_knots_from_string_reads_several_present_knots() {
733        let content = "\
734== first ==
735First line.
736
737== second ==
738First line.
739
740== third ==
741First line.
742";
743
744        let knots = read_knots_from_string(content).unwrap();
745
746        assert_eq!(knots.len(), 3);
747
748        assert!(knots.contains_key("first"));
749        assert!(knots.contains_key("second"));
750        assert!(knots.contains_key("third"));
751    }
752
753    #[test]
754    fn read_knots_from_string_requires_named_knots() {
755        let content = "\
756First line.
757Second line.
758";
759
760        assert!(read_knots_from_string(content).is_err());
761    }
762
763    #[test]
764    fn divide_into_knots_splits_given_lines_at_knot_markers() {
765        let content = enumerate(&[
766            "== Knot one ",
767            "Line 1",
768            "Line 2",
769            "",
770            "=== Knot two ===",
771            "Line 3",
772            "",
773        ]);
774
775        let knot_lines = divide_lines_at_marker(content.clone(), KNOT_MARKER);
776
777        assert_eq!(knot_lines[0][..], content[0..4]);
778        assert_eq!(knot_lines[1][..], content[4..]);
779    }
780
781    #[test]
782    fn divide_into_knots_adds_content_from_nameless_knots_first() {
783        let content = enumerate(&["Line 1", "Line 2", "== Knot one ", "Line 3"]);
784
785        let knot_lines = divide_lines_at_marker(content.clone(), KNOT_MARKER);
786
787        assert_eq!(knot_lines[0][..], content[0..2]);
788        assert_eq!(knot_lines[1][..], content[2..]);
789    }
790
791    #[test]
792    fn divide_into_stitches_splits_lines_at_markers() {
793        let content = enumerate(&[
794            "Line 1",
795            "= Stitch one ",
796            "Line 2",
797            "Line 3",
798            "",
799            "= Stitch two",
800            "Line 4",
801            "",
802        ]);
803
804        let knot_lines = divide_lines_at_marker(content.clone(), STITCH_MARKER);
805
806        assert_eq!(knot_lines[0][..], content[0..1]);
807        assert_eq!(knot_lines[1][..], content[1..5]);
808        assert_eq!(knot_lines[2][..], content[5..]);
809    }
810
811    #[test]
812    fn empty_lines_and_comment_lines_are_removed_by_initial_processing() {
813        let content = vec![
814            "Good line",
815            "// Comment line is remove",
816            "",        // removed
817            "       ", // removed
818            "TODO: As is todo comments",
819            "TODO but not without a colon!",
820        ];
821
822        let lines = remove_empty_and_comment_lines(enumerate(&content));
823        assert_eq!(
824            &denumerate(lines),
825            &[content[0].clone(), content[5].clone()]
826        );
827    }
828
829    #[test]
830    fn initial_processing_splits_off_line_comments() {
831        let content = vec![
832            "Line before comment marker // Removed part",
833            "Line with no comment marker",
834        ];
835
836        let lines = remove_empty_and_comment_lines(enumerate(&content));
837        assert_eq!(lines[0].0, "Line before comment marker ");
838        assert_eq!(lines[1].0, "Line with no comment marker");
839    }
840
841    #[test]
842    fn parsing_knot_from_lines_gets_name() {
843        let content = enumerate(&["== Knot_name ==", "Line 1", "Line 2"]);
844
845        let (name, _) = get_knot_from_lines(content).unwrap();
846        assert_eq!(&name, "Knot_name");
847    }
848
849    #[test]
850    fn parsing_knot_from_lines_without_stitches_sets_content_in_default_named_stitch() {
851        let content = enumerate(&["== Knot_name ==", "Line 1", "Line 2"]);
852
853        let (_, knot) = get_knot_from_lines(content).unwrap();
854
855        assert_eq!(&knot.default_stitch, ROOT_KNOT_NAME);
856        assert_eq!(
857            knot.stitches.get(ROOT_KNOT_NAME).unwrap().root.items.len(),
858            2
859        );
860    }
861
862    #[test]
863    fn parsing_a_stitch_gets_name_if_present_else_default_root_name_if_index_is_zero() {
864        let (name, _) =
865            get_stitch_from_lines(enumerate(&["= stitch_name =", "Line 1"]), 0, "").unwrap();
866        assert_eq!(name, "stitch_name".to_string());
867
868        let (name, _) = get_stitch_from_lines(enumerate(&["Line 1"]), 0, "").unwrap();
869        assert_eq!(name, ROOT_KNOT_NAME);
870    }
871
872    #[test]
873    fn parsing_stitch_from_lines_sets_address_in_root_node() {
874        let (_, stitch) =
875            get_stitch_from_lines(enumerate(&["= cinema", "Line 1"]), 0, "tripoli").unwrap();
876
877        assert_eq!(
878            stitch.root.address,
879            Address::from_parts_unchecked("tripoli", Some("cinema"))
880        );
881
882        let (_, stitch) = get_stitch_from_lines(enumerate(&["Line 1"]), 0, "tripoli").unwrap();
883
884        assert_eq!(
885            stitch.root.address,
886            Address::from_parts_unchecked("tripoli", None)
887        );
888    }
889
890    #[test]
891    fn parsing_a_stitch_gets_all_content_regardless_of_whether_name_is_present() {
892        let (_, content) =
893            get_stitch_from_lines(enumerate(&["= stitch_name =", "Line 1"]), 0, "").unwrap();
894        assert_eq!(content.root.items.len(), 1);
895
896        let (_, content) = get_stitch_from_lines(enumerate(&["Line 1"]), 0, "").unwrap();
897        assert_eq!(content.root.items.len(), 1);
898    }
899
900    #[test]
901    fn parsing_a_knot_from_lines_sets_stitches_in_hash_map() {
902        let lines = enumerate(&[
903            "== knot_name",
904            "= stitch_one",
905            "Line one",
906            "= stitch_two",
907            "Line two",
908        ]);
909
910        let (_, knot) = get_knot_from_lines(lines).unwrap();
911
912        assert_eq!(knot.stitches.len(), 2);
913        assert!(knot.stitches.get("stitch_one").is_some());
914        assert!(knot.stitches.get("stitch_two").is_some());
915    }
916
917    #[test]
918    fn knot_with_root_content_gets_default_knot_as_first_stitch() {
919        let lines = enumerate(&[
920            "== knot_name",
921            "Line 1",
922            "= stitch_one",
923            "Line 2",
924            "= stitch_two",
925            "Line 3",
926        ]);
927
928        let (_, knot) = get_knot_from_lines(lines).unwrap();
929        assert_eq!(&knot.default_stitch, ROOT_KNOT_NAME);
930    }
931
932    #[test]
933    fn root_knot_parses_stitch_without_a_name() {
934        let lines = enumerate(&["Line 1", "Line 2"]);
935
936        let root = parse_root_knot_from_lines(lines.clone(), ().into()).unwrap();
937
938        let comparison =
939            parse_stitch_from_lines(&lines, ROOT_KNOT_NAME, ROOT_KNOT_NAME, ().into()).unwrap();
940
941        assert_eq!(
942            format!("{:?}", root.stitches.get(ROOT_KNOT_NAME).unwrap()),
943            format!("{:?}", comparison)
944        );
945    }
946
947    #[test]
948    fn root_knot_may_have_stitches() {
949        let lines = enumerate(&["Line 1", "= Stitch", "Line 2"]);
950
951        let root = parse_root_knot_from_lines(lines, ().into()).unwrap();
952
953        assert_eq!(root.stitches.len(), 2);
954    }
955
956    #[test]
957    fn knot_with_no_root_content_gets_default_knot_as_first_stitch() {
958        let lines = enumerate(&[
959            "== knot_name",
960            "= stitch_one",
961            "Line 1",
962            "= stitch_two",
963            "Line 2",
964        ]);
965
966        let (_, knot) = get_knot_from_lines(lines).unwrap();
967        assert_eq!(&knot.default_stitch, "stitch_one");
968    }
969
970    #[test]
971    fn knot_parses_tags_from_name_until_first_line_without_octothorpe() {
972        let lines = enumerate(&["== knot_name", "# Tag one", "# Tag two", "Line 1"]);
973
974        let (_, knot) = get_knot_from_lines(lines).unwrap();
975        assert_eq!(&knot.tags, &["Tag one".to_string(), "Tag two".to_string()]);
976    }
977
978    #[test]
979    fn knot_tags_ignore_empty_lines() {
980        let lines = enumerate(&["== knot_name", "", "# Tag one", "", "# Tag two", "Line 1"]);
981
982        let (_, knot) = get_knot_from_lines(lines).unwrap();
983        assert_eq!(&knot.tags, &["Tag one".to_string(), "Tag two".to_string()]);
984    }
985
986    #[test]
987    fn if_no_tags_are_set_the_tags_are_empty() {
988        let lines = enumerate(&["== knot_name", "Line 1"]);
989
990        let (_, knot) = get_knot_from_lines(lines).unwrap();
991        assert!(knot.tags.is_empty());
992    }
993
994    #[test]
995    fn tags_do_not_disturb_remaining_content() {
996        let lines_with_tags = enumerate(&["== knot_name", "# Tag one", "# Tag two", "", "Line 1"]);
997        let lines_without_tags = vec![
998            ("== knot_name", MetaData::from(0)),
999            ("Line 1", MetaData::from(4)),
1000        ];
1001
1002        let (_, knot_tags) = get_knot_from_lines(lines_with_tags).unwrap();
1003        let (_, knot_no_tags) = get_knot_from_lines(lines_without_tags).unwrap();
1004
1005        assert_eq!(
1006            format!("{:?}", knot_tags.stitches),
1007            format!("{:?}", knot_no_tags.stitches)
1008        );
1009    }
1010
1011    #[test]
1012    fn reading_story_data_gets_unordered_variables_in_prelude() {
1013        let content = "
1014# Random tag
1015VAR counter = 0
1016# Random tag
1017// Line comment
1018VAR hazardous = true
1019
1020-> introduction
1021";
1022
1023        let (_, variables, _) = read_story_content_from_string(content).unwrap();
1024
1025        assert_eq!(variables.len(), 2);
1026        assert!(variables.contains_key("counter"));
1027        assert!(variables.contains_key("hazardous"));
1028    }
1029
1030    #[test]
1031    fn variables_after_first_line_of_text_are_ignored() {
1032        let content = "
1033VAR counter = 0
1034
1035-> introduction
1036VAR hazardous = true
1037";
1038
1039        let (_, variables, _) = read_story_content_from_string(content).unwrap();
1040
1041        assert_eq!(variables.len(), 1);
1042        assert!(variables.contains_key("counter"));
1043    }
1044
1045    #[test]
1046    fn no_variables_give_empty_set() {
1047        let content = "
1048// Just a line comment!
1049-> introduction
1050";
1051
1052        let (_, variables, _) = read_story_content_from_string(content).unwrap();
1053
1054        assert_eq!(variables.len(), 0);
1055    }
1056
1057    #[test]
1058    fn reading_story_data_gets_all_global_tags_in_prelude() {
1059        let content = "
1060# title: test
1061VAR counter = 0
1062# rating: hazardous
1063// Line comment
1064VAR hazardous = true
1065
1066-> introduction
1067";
1068
1069        let (_, _, tags) = read_story_content_from_string(content).unwrap();
1070
1071        assert_eq!(
1072            &tags,
1073            &["title: test".to_string(), "rating: hazardous".to_string()]
1074        );
1075    }
1076
1077    #[test]
1078    fn reading_story_data_sets_knot_line_starting_line_indices_including_prelude_content() {
1079        let content = "\
1080# title: line_counting
1081VAR line_count = 0
1082
1083-> root
1084
1085== root
1086One line.
1087
1088== second
1089Second line.
1090";
1091
1092        let (knots, _, _) = read_story_content_from_string(content).unwrap();
1093
1094        assert_eq!(knots.get("root").unwrap().meta_data.line_index, 5);
1095        assert_eq!(knots.get("second").unwrap().meta_data.line_index, 8);
1096    }
1097
1098    #[test]
1099    fn all_prelude_and_knot_errors_are_caught_and_returned() {
1100        let content = "\
1101VAR = 0
1102
1103== knot.stitch
1104{2 +}
1105*+  Sticky or non-sticky?
1106
1107== empty_knot
1108
1109";
1110
1111        match read_story_content_from_string(content) {
1112            Err(ReadError::ParseError(error)) => {
1113                assert_eq!(error.prelude_errors.len(), 1);
1114                assert_eq!(error.knot_errors.len(), 2);
1115
1116                assert_eq!(error.knot_errors[0].line_errors.len(), 3);
1117                assert_eq!(error.knot_errors[1].line_errors.len(), 1);
1118            }
1119            other => panic!("expected `ReadError::ParseError` but got {:?}", other),
1120        }
1121    }
1122
1123    #[test]
1124    fn reading_story_content_works_if_content_only_has_text() {
1125        let content = "\
1126Line one.
1127";
1128
1129        assert!(read_story_content_from_string(content).is_ok());
1130    }
1131
1132    #[test]
1133    fn reading_story_content_works_if_content_starts_with_knot() {
1134        let content = "\
1135=== knot ===
1136Line one.
1137";
1138
1139        assert!(read_story_content_from_string(content).is_ok());
1140    }
1141
1142    #[test]
1143    fn reading_story_content_does_not_work_if_knot_has_no_content() {
1144        let content = "\
1145=== knot ===
1146";
1147
1148        assert!(read_story_content_from_string(content).is_err());
1149    }
1150
1151    #[test]
1152    fn reading_story_content_does_not_work_if_stitch_has_no_content() {
1153        let content = "\
1154=== knot ===
1155= stitch
1156";
1157
1158        assert!(read_story_content_from_string(content).is_err());
1159    }
1160
1161    #[test]
1162    fn reading_story_content_yields_error_if_duplicate_stitch_names_are_found_in_one_knot() {
1163        let content = "\
1164== knot
1165= stitch
1166Line one.
1167= stitch 
1168Line two.
1169";
1170
1171        match read_story_content_from_string(content) {
1172            Err(ReadError::ParseError(err)) => match &err.knot_errors[0].line_errors[0] {
1173                KnotErrorKind::DuplicateStitchName { .. } => (),
1174                other => panic!(
1175                    "expected `KnotErrorKind::DuplicateStitchName` but got {:?}",
1176                    other
1177                ),
1178            },
1179            other => panic!("expected `ReadError::ParseError` but got {:?}", other),
1180        }
1181    }
1182
1183    #[test]
1184    fn reading_story_content_yields_error_if_duplicate_knot_names_are_found() {
1185        let content = "\
1186== knot
1187Line one.
1188== knot 
1189Line two.
1190";
1191
1192        match read_story_content_from_string(content) {
1193            Err(ReadError::ParseError(err)) => match &err.knot_errors[0].line_errors[0] {
1194                KnotErrorKind::DuplicateKnotName { .. } => (),
1195                other => panic!(
1196                    "expected `KnotErrorKind::DuplicateKnotName` but got {:?}",
1197                    other
1198                ),
1199            },
1200            other => panic!("expected `ReadError::ParseError` but got {:?}", other),
1201        }
1202    }
1203}