twee_parser/
twee3.rs

1use regex::RegexBuilder;
2
3use crate::*;
4
5#[derive(PartialEq, Eq)]
6enum PassageState {
7    Title,
8    Tags,
9    Between,
10}
11
12/// Parses Twee3 into a [Story].
13pub fn parse_twee3(source: &str) -> Result<(Story, Vec<Warning>), Error> {
14    let re = RegexBuilder::new("^::[^\n]*\n").multi_line(true).build().unwrap();
15    let mut warnings = vec![];
16    let mut passages: Vec<Passage> = Vec::new();
17    let mut start = 0;
18    let mut name = Vec::<char>::new();
19    let mut tags = Vec::<String>::new();
20    let mut meta: &str = "{}";
21    let mut title = String::new();
22    let mut story_meta = None;
23    fn handle_passage(warnings: &mut Vec<Warning>, title: &mut String, story_meta: &mut Option<Map<String, Value>>, passages: &mut Vec<Passage>, name: &str, content: &str, tags: &Vec<String>, meta: &str) {
24        if name.len() == 0 {
25            warnings.push(Warning::PassageNameMissing);
26        } else {
27            match name {
28                "StoryTitle" => {
29                    if title.len() != 0 {
30                        warnings.push(Warning::PassageDuplicated("StoryTitle".to_string()));
31                    }
32                    *title = content.trim().to_string();
33                },
34                "StoryData" => {
35                    if story_meta.is_some() {
36                        warnings.push(Warning::PassageDuplicated("StoryData".to_string()));
37                    }
38                    *story_meta = if let Ok(v) = serde_json::from_str(&content) {
39                        let v: Value = v;
40                        match v {
41                            Value::Object(o) => {
42                                Some(o)
43                            },
44                            _ => {
45                                warnings.push(Warning::StoryMetadataMalformed);
46                                Some(Map::new())
47                            }
48                        }
49                    } else {
50                        warnings.push(Warning::StoryMetadataMalformed);
51                        Some(Map::new())
52                    };
53                },
54                _ => {
55                    let mut dup = false;
56                    for p in &mut *passages {
57                        if p.name == name {
58                            warnings.push(Warning::PassageDuplicated(p.name.clone()));
59                            dup = true;
60                            break;
61                        }
62                    }
63                    if ! dup {
64                        let meta = if let Ok(v) = serde_json::from_str(meta) {
65                            let v: Value = v;
66                            match v {
67                                Value::Object(o) => {
68                                    o
69                                },
70                                _ => {
71                                    warnings.push(Warning::PassageMetadataMalformed(name.to_string()));
72                                    Map::new()
73                                }
74                            }
75                        } else {
76                            warnings.push(Warning::PassageMetadataMalformed(name.to_string()));
77                            Map::new()
78                        };
79                        passages.push(Passage { name: name.to_string(), tags: tags.clone(), meta, content: content.trim_end().to_string()});
80                    }
81                }
82            }
83        }
84    }
85    while let Some(a) = re.find_at(source, start) {
86        if start != 0 {
87            let name: String = name.iter().collect();
88            let name = name.trim().to_string();
89            let mut content = source[start..(a.start())].to_string();
90            if content.starts_with("\\::") {
91                content.remove(0);
92            }
93            handle_passage(&mut warnings, &mut title, &mut story_meta, &mut passages, &name, &content, &tags, meta);
94        }
95        start = a.start() + 2;
96        name.clear();
97        tags.clear();
98        meta = "{}";
99        let mut tag = Vec::<char>::new();
100        let mut state = PassageState::Title;
101        let mut escape = false;
102        for (i, c) in source[start..].char_indices() {
103            if ['\r', '\n'].contains(&c) {
104                break;
105            }
106            match state {
107                PassageState::Title => {
108                    if escape {
109                        escape = false;
110                        name.push(c);
111                        continue;
112                    }
113                    if c == '[' {
114                        state = PassageState::Tags;
115                        continue;
116                    }
117                    if c == '{' {
118                        let i = start + i;
119                        meta = &source[if let Some(newline) = source[i..].find("\n") {
120                            i..(i + newline)
121                        } else {
122                            i..source.len()
123                        }];
124                        break;
125                    }
126                    if c == '\\' {
127                        escape = true;
128                        continue;
129                    }
130                    name.push(c);
131                },
132                PassageState::Tags => {
133                    if escape {
134                        escape = false;
135                        tag.push(c);
136                        continue;
137                    }
138                    if c == '\\' {
139                        escape = true;
140                        continue;
141                    }
142                    if c == ']' {
143                        if ! tag.is_empty() {
144                            tags.push(tag.iter().collect());
145                        }
146                        state = PassageState::Between;
147                        continue;
148                    }
149                    if c.is_whitespace() && ! tag.is_empty() {
150                        tags.push(tag.iter().collect());
151                        tag = vec![];
152                    } else {
153                        tag.push(c);
154                    }
155                },
156                PassageState::Between => {
157                    if c == '{' {
158                        let i = start + i;
159                        meta = &source[if let Some(newline) = source[i..].find("\n") {
160                            i..(i + newline)
161                        } else {
162                            i..source.len()
163                        }];
164                        break;
165                    }
166                }
167            }
168        }
169        if state == PassageState::Tags {
170            warnings.push(Warning::PassageTagsMalformed(name.iter().collect()));
171        }
172        if ! tag.is_empty() {
173            tags.push(tag.iter().collect());
174        }
175        if meta.trim().len() == 0 {
176            meta = "{}";
177        }
178        start = a.end();
179    }
180    if ! name.is_empty() {
181        let name: String = name.iter().collect();
182        let name = name.trim().to_string();
183        let mut content = source[start..].to_string();
184        if content.starts_with("\\::") {
185            content.remove(0);
186        }
187        handle_passage(&mut warnings, &mut title, &mut story_meta, &mut passages, &name, &content, &tags, meta);
188    }
189    if title.is_empty() {
190        warnings.push(Warning::StoryTitleMissing);
191    }
192    return Ok((Story {
193        title,
194        passages,
195        meta: story_meta.unwrap_or(Map::new()),
196    }, warnings));
197}
198
199
200/// Serializes a [Story] into Twee3.
201pub fn serialize_twee3(story: &Story) -> String {
202    let escape = |t: &String| {
203        t.replace("\\", "\\\\")
204        .replace("[", "\\[")
205        .replace("]", "\\]")
206        .replace("{", "\\{")
207        .replace("}", "\\}")
208    };
209    
210    let mut res: Vec<char> = Vec::new();
211    res.extend(":: StoryTitle\n".chars());
212    res.extend(escape(&story.title).chars());
213    
214    res.extend("\n\n:: StoryData\n".chars());
215    res.extend(serde_json::to_string_pretty(&story.meta).unwrap().chars());
216    res.extend("\n\n".chars());
217    
218    for p in &story.passages {
219        res.extend("\n:: ".chars());
220        res.extend(escape(&p.name).chars());
221        if ! p.tags.is_empty() {
222            res.extend(" [".chars());
223            res.extend(p.tags.iter().map(escape).collect::<Vec<String>>().join(" ").chars());
224            res.push(']');
225        }
226        if ! p.meta.is_empty() {
227            res.extend(" {".chars());
228            res.extend(serde_json::to_string(&p.meta).unwrap().chars());
229            res.push('}');
230        }
231        res.push('\n');
232        if p.content.starts_with("::") {
233            res.push('\\');
234        }
235        res.extend(p.content.chars());
236        res.push('\n');
237    }
238    return res.into_iter().collect();
239}
240
241