twee_parser/
html.rs

1use crate::*;
2
3pub use xmltree::{Element, XMLNode, ParseError};
4
5pub use xmltree;
6
7fn search_storydata(e: &Element) -> Option<Element> {
8    if e.name == "tw-storydata" {
9        return Some(e.clone());
10    }
11    for c in &e.children {
12        if let Some(e) = c.as_element() {
13            if e.name == "tw-storydata" {
14                return Some(e.clone());
15            } else {
16                if let Some(e) = search_storydata(&e) {
17                    return Some(e);
18                }
19            }
20        }
21    }
22    return None;
23}
24
25/// Parses a Twine archive, a list of &lt;tw-storydata&gt; tags, into a list of [Story]s.
26pub fn parse_archive(source: &str) -> Result<Vec<(Story, Vec<Warning>)>, Error> {
27    let e = Element::parse_all(source.as_bytes()).map_err(|e| Error::HTMLParseError(e))?;
28    return e.into_iter().map(|e| e.as_element().ok_or(Error::HTMLStoryDataNotFound).and_then(|e| parse_element(e))).collect();
29}
30
31/// Parses a published Twine HTML file into a [Story], looking for a &lt;tw-storydata&gt; tag.
32pub fn parse_html(source: &str) -> Result<(Story, Vec<Warning>), Error> {
33    let e = Element::parse(source.as_bytes()).map_err(|e| Error::HTMLParseError(e))?;
34    let storydata = search_storydata(&e).ok_or(Error::HTMLStoryDataNotFound)?;
35    return parse_element(&storydata);
36}
37
38fn parse_element(storydata: &Element) -> Result<(Story, Vec<Warning>), Error> {
39    let mut warnings = vec![];
40    let mut passages: Vec<Passage> = vec![];
41    let mut tag_colors = Map::new();
42    let mut elements = storydata.children.iter().filter_map(|c| {
43        c.as_element()
44    }).collect::<Vec<&Element>>();
45    elements.sort_by(|a, b| {
46        let a = a.attributes.get("pid").and_then(|p| u32::from_str_radix(p, 10).ok()).unwrap_or(u32::MAX);
47        let b = b.attributes.get("pid").and_then(|p| u32::from_str_radix(p, 10).ok()).unwrap_or(u32::MAX);
48        a.cmp(&b)
49    });
50    for n in elements {
51        match n.name.as_str() {
52            "tw-passagedata" => {
53                let mut meta = Map::new();
54                for a in &n.attributes {
55                    meta.insert(a.0.clone(), Value::String(a.1.clone()));
56                }
57                meta.remove("pid");
58                if let Some(name) = meta.remove("name") {
59                    let tags = meta.remove("tags").and_then(|tags| {
60                        Some(tags.as_str().unwrap().split_whitespace().map(|s| s.to_string()).collect())
61                    }).unwrap_or(vec![]);
62                    let p = Passage {
63                        name: name.as_str().unwrap().to_string(),
64                        tags,
65                        meta,
66                        content: n.get_text().unwrap().clone().to_string(),
67                    };
68                    passages.push(p);
69                }
70            },
71            "style" => {
72                if let Some(p) = passages.iter_mut().find(|p| p.name == "StoryStylesheet") {
73                    if let Some(t) = n.get_text() {
74                        p.content += "\n";
75                        p.content += &t;
76                    }
77                } else {
78                    if let Some(t) = n.get_text() {
79                        let p = Passage {
80                            name: "StoryStylesheet".to_string(),
81                            tags: vec!["stylesheet".to_string()],
82                            meta: Map::new(),
83                            content: t.clone().to_string(),
84                        };
85                        passages.push(p);
86                    }
87                }
88            },
89            "script" => {
90                if let Some(p) = passages.iter_mut().find(|p| p.name == "StoryScript") {
91                    if let Some(t) = n.get_text() {
92                        p.content += "\n";
93                        p.content += &t;
94                    }
95                } else {
96                    if let Some(t) = n.get_text() {
97                        let p = Passage {
98                            name: "StoryScript".to_string(),
99                            tags: vec!["script".to_string()],
100                            meta: Map::new(),
101                            content: t.clone().to_string(),
102                        };
103                        passages.push(p);
104                    }
105                }
106            },
107            "tw-tag" => {
108                if let (Some(name), Some(value)) = (n.attributes.get("name"), n.attributes.get("color")) {
109                    tag_colors.insert(name.clone(), Value::String(value.clone()));
110                }
111            }
112            _ => {}
113        }
114    }
115    
116    
117    let mut meta = Map::new();
118    for a in &storydata.attributes {
119        meta.insert(a.0.clone(), Value::String(a.1.clone()));
120    }
121    let mut title = "".to_string();
122    meta.remove("hidden");
123    if let Some(t) = meta.remove("name") {
124        title = t.as_str().unwrap().to_string();
125    } else {
126        warnings.push(Warning::StoryTitleMissing);
127    }
128    if let Some(s) = meta.remove("startnode") {
129        if let Some(start) = s.as_str() {
130            let start = start.to_string();
131            if let Some(start) = storydata.children.iter().find(|c| c.as_element().and_then(|e| Some(e.attributes.get("pid") == Some(&start))).is_some()) {
132                if let Some(name) = start.as_element().and_then(|e| e.attributes.get("name")) {
133                    meta.insert("start".to_string(), Value::String(name.clone()));
134                }
135            }
136        }
137    }
138    meta.insert("tag-colors".to_string(), Value::Object(tag_colors));
139    
140    return Ok((Story {
141        title,
142        passages,
143        meta,
144    }, warnings));
145}
146
147/// Serializes a [Story] into a &lt;tw-storydata&gt; tag.
148pub fn serialize_html(story: &Story) -> Element {
149    let mut storydata = Element::new("tw-storydata");
150    storydata.attributes.insert("name".to_string(), story.title.clone());
151    
152    let stylesheet = "stylesheet".to_string();
153    let script = "script".to_string();
154    let mut pid = 1;
155    for p in &story.passages {
156        let mut e;
157        if p.tags.contains(&stylesheet) {
158            if let Some(e) = storydata.children.iter_mut().find(|e| e.as_element().and_then(|e| Some(e.name == "style")) == Some(true)) {
159                let e = e.as_mut_element().unwrap();
160                e.children.push(XMLNode::Text("\n".to_string()));
161                e.children.push(XMLNode::Text(p.content.clone()));
162                continue;
163            }
164            e = Element::new("style");
165            e.attributes.insert("role".to_string(), "stylesheet".to_string());
166            e.attributes.insert("id".to_string(), "twine-user-stylesheet".to_string());
167            e.attributes.insert("type".to_string(), "text/twine-css".to_string());
168            e.children.push(XMLNode::Text(p.content.clone()));
169        } else {
170            if p.tags.contains(&script) {
171                if let Some(e) = storydata.children.iter_mut().find(|e| e.as_element().and_then(|e| Some(e.name == "script")) == Some(true)) {
172                    let e = e.as_mut_element().unwrap();
173                    e.children.push(XMLNode::Text("\n".to_string()));
174                    e.children.push(XMLNode::Text(p.content.clone()));
175                    continue;
176                }
177                e = Element::new("script");
178                e.attributes.insert("role".to_string(), "script".to_string());
179                e.attributes.insert("id".to_string(), "twine-user-script".to_string());
180                e.attributes.insert("type".to_string(), "text/twine-javascript".to_string());
181                e.children.push(XMLNode::Text(p.content.clone()));
182            } else {
183                e = Element::new("tw-passagedata");
184                e.attributes.insert("pid".to_string(), pid.to_string());
185                pid += 1;
186                e.attributes.insert("name".to_string(), p.name.clone());
187                e.attributes.insert("tags".to_string(), p.tags.join(" "));
188                for m in &p.meta {
189                    if let Some(v) = m.1.as_str() {
190                        e.attributes.insert(m.0.clone(), v.to_string());
191                    }
192                }
193                let content = p.content.clone();
194                e.children.push(XMLNode::Text(content));
195            }
196        }
197        storydata.children.push(XMLNode::Element(e));
198    }
199    
200    
201    for m in &story.meta {
202        match m.0.as_str() {
203            "start" => {
204                if let Some(s) = m.1.as_str() {
205                    let s = s.to_string();
206                    if let Some(start) = storydata.children.iter().find(|c| c.as_element().and_then(|e| Some(e.attributes.get("name") == Some(&s))) == Some(true)) {
207                        storydata.attributes.insert("startnode".to_string(), start.as_element().unwrap().attributes.get("pid").unwrap().clone());
208                    }
209                }
210            },
211            "tag-colors" => {
212                if let Some(tags) = m.1.as_object() {
213                    for t in tags {
214                        if let Some(v) = t.1.as_str() {
215                            let mut e = Element::new("tw-tag");
216                            e.attributes.insert("name".to_string(), t.0.clone());
217                            e.attributes.insert("color".to_string(), v.to_string());
218                            storydata.children.insert(0, XMLNode::Element(e));
219                        }
220                    }
221                }
222            },
223            _ => {
224                if let Some(v) = &m.1.as_str() {
225                    storydata.attributes.insert(m.0.clone(), v.to_string());
226                }
227            }
228        }
229    }
230    return storydata;
231}
232