inkling/line/parse/
kind.rs

1//! Parse all kinds of lines as marked up `ParsedLineKind` objects.
2
3use crate::{
4    consts::DIVERT_MARKER,
5    error::{parse::line::LineError, utils::MetaData},
6    line::{
7        parse::{parse_choice, parse_gather, parse_internal_line},
8        InternalChoice, InternalLine,
9    },
10};
11
12#[derive(Clone, Debug, PartialEq)]
13/// Representation of a parsed line of content.
14///
15/// To construct the nested tree structure of branching choices and gather points
16/// we need information about which level every choice and gather line is at.
17///
18/// This structure marks the actual data of choices and gathers with their level.
19pub enum ParsedLineKind {
20    Choice {
21        /// Nested level of choice.
22        level: u32,
23        /// Parsed data of choice.
24        choice_data: InternalChoice,
25    },
26    Gather {
27        /// Nested level of gather.
28        level: u32,
29        /// Parsed line of gather point.
30        line: InternalLine,
31    },
32    /// Regular line of content.
33    Line(InternalLine),
34}
35
36#[cfg(test)]
37impl ParsedLineKind {
38    /// Construct a `ParsedLineKind::Choice` object with given level and choice data.
39    pub fn choice(level: u32, choice_data: InternalChoice) -> Self {
40        ParsedLineKind::Choice { level, choice_data }
41    }
42
43    /// Construct a `ParsedLineKind::Gather` object with given level and line.
44    pub fn gather(level: u32, line: InternalLine) -> Self {
45        ParsedLineKind::Gather { level, line }
46    }
47
48    /// Construct a `ParsedLineKind::Line` object with given line.
49    pub fn line(line: InternalLine) -> Self {
50        ParsedLineKind::Line(line)
51    }
52}
53
54/// Parse a line into a `ParsedLineKind` object.
55pub fn parse_line(content: &str, meta_data: &MetaData) -> Result<ParsedLineKind, LineError> {
56    if let Some(choice) = parse_choice(content, meta_data).transpose() {
57        choice
58    } else if let Some(gather) = parse_gather(content, meta_data).transpose() {
59        gather
60    } else {
61        parse_internal_line(content, meta_data).map(|line| ParsedLineKind::Line(line))
62    }
63    .map_err(|kind| LineError {
64        line: content.to_string(),
65        kind,
66        meta_data: meta_data.clone(),
67    })
68}
69
70/// Count leading markers and return the number and a string without them.
71pub fn parse_markers_and_text(line: &str, marker: char) -> Option<(u32, &str)> {
72    if line.trim_start().starts_with(marker) {
73        let (markers, line_text) = split_markers_from_text(line, marker);
74        let num = markers.matches(|c| c == marker).count() as u32;
75
76        Some((num, line_text))
77    } else {
78        None
79    }
80}
81
82/// Split leading markers from a string and return both parts.
83fn split_markers_from_text(line: &str, marker: char) -> (&str, &str) {
84    let split_at = line.find(|c: char| !(c == marker || c.is_whitespace()));
85
86    match split_at {
87        Some(i) => line.split_at(i),
88        None => (line, ""),
89    }
90}
91
92/// Split a string at the divert marker and return both parts.
93pub fn split_at_divert_marker(content: &str) -> (&str, &str) {
94    if let Some(i) = content.find(DIVERT_MARKER) {
95        content.split_at(i)
96    } else {
97        (content, "")
98    }
99}
100
101#[cfg(test)]
102pub mod tests {
103    use super::*;
104
105    #[test]
106    fn simple_line_parses_to_line() {
107        let line = parse_line("Hello, World!", &().into()).unwrap();
108        let comparison = parse_internal_line("Hello, World!", &().into()).unwrap();
109
110        assert_eq!(line, ParsedLineKind::Line(comparison));
111    }
112
113    #[test]
114    fn line_with_choice_markers_parses_to_choice() {
115        let line = parse_line("* Hello, World!", &().into()).unwrap();
116
117        match line {
118            ParsedLineKind::Choice { .. } => (),
119            other => panic!("expected `ParsedLineKind::Choice` but got {:?}", other),
120        }
121    }
122
123    #[test]
124    fn line_with_gather_markers_parses_to_gather() {
125        let line = parse_line("- Hello, World!", &().into()).unwrap();
126
127        match line {
128            ParsedLineKind::Gather { .. } => (),
129            other => panic!("expected `ParsedLineKind::Gather` but got {:?}", other),
130        }
131    }
132
133    #[test]
134    fn choices_are_parsed_before_gathers() {
135        let line = parse_line("* - Hello, World!", &().into()).unwrap();
136
137        match line {
138            ParsedLineKind::Choice { .. } => (),
139            other => panic!("expected `ParsedLineKind::Choice` but got {:?}", other),
140        }
141    }
142}