#[cfg(feature = "full-context")]
use crate::CodeMap;
#[cfg(feature = "full-context")]
use crate::ContextErrorList;
#[cfg(not(feature = "full-context"))]
use crate::ErrorList;
use crate::Output;
use crate::PassageContent;
use crate::StoryData;
use crate::StoryPassages;
use crate::TwinePassage;
use std::collections::HashMap;
use std::path::Path;
#[derive(Default)]
pub struct Story {
pub title: Option<String>,
pub data: Option<StoryData>,
pub passages: HashMap<String, TwinePassage>,
pub scripts: Vec<String>,
pub stylesheets: Vec<String>,
#[cfg(feature = "full-context")]
pub code_map: CodeMap,
}
#[cfg(not(feature = "full-context"))]
type ParseOutput = Output<Result<Story, ErrorList>>;
#[cfg(feature = "full-context")]
type ParseOutput = Output<Result<Story, ContextErrorList>>;
impl Story {
pub fn from_string(input: String) -> ParseOutput {
StoryPassages::from_string(input).into_result()
}
pub fn from_path<P: AsRef<Path>>(input: P) -> ParseOutput {
StoryPassages::from_path(input).into_result()
}
pub fn from_paths<P: AsRef<Path>>(input: &[P]) -> ParseOutput {
StoryPassages::from_paths(input).into_result()
}
pub fn get_start_passage_name(&self) -> Option<&str> {
self.data
.as_ref()
.and_then(|d| d.start.as_deref())
.or_else(|| {
if self.passages.contains_key("Start") {
Some("Start")
} else {
None
}
})
}
}
impl std::convert::From<StoryPassages> for Story {
fn from(mut s: StoryPassages) -> Story {
let title = match s.title {
Some(c) => match c.content {
PassageContent::StoryTitle(t) => Some(t.title),
_ => panic!("Expected title to be StoryTitle"),
},
None => None,
};
let data = match s.data {
Some(c) => match c.content {
PassageContent::StoryData(d) => d,
_ => panic!("Expected data to be StoryData"),
},
None => None,
};
let scripts = s
.scripts
.into_iter()
.map(|p| match p.content {
PassageContent::Script(script) => script.content,
_ => panic!("Expected script to be Script"),
})
.collect();
let stylesheets = s
.stylesheets
.into_iter()
.map(|p| match p.content {
PassageContent::Stylesheet(stylesheet) => stylesheet.content,
_ => panic!("Expected stylesheet to be Stylesheet"),
})
.collect();
let passages: HashMap<String, TwinePassage> =
s.passages.drain().map(|(k, v)| (k, v.into())).collect();
#[cfg(feature = "full-context")]
let code_map = s.code_map;
Story {
title,
data,
passages,
scripts,
stylesheets,
#[cfg(feature = "full-context")]
code_map,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Context;
use crate::Warning;
use crate::WarningKind;
use tempfile::tempdir;
#[test]
fn warning_offsets() {
let input = r#":: A passage
This
That
The Other
:: A\[nother passage
Foo
Bar
Baz
:: StoryTitle
Test Story
"#
.to_string();
use crate::FullContext;
use crate::Position;
let out = Story::from_string(input.clone());
assert_eq!(out.has_warnings(), true);
let (res, warnings) = out.take();
assert_eq!(res.is_ok(), true);
let context = FullContext::from(None, input);
assert_eq!(warnings[0], {
let warning = Warning::new(
WarningKind::EscapedOpenSquare,
Some(context.subcontext(Position::rel(7, 5)..=Position::rel(7, 6))),
);
warning
});
}
#[test]
fn file_input() -> Result<(), Box<dyn std::error::Error>> {
let input = r#":: A passage
This
That
The Other
:: Another passage
Foo
Bar
Baz
:: StoryTitle
Test Story
"#
.to_string();
use std::fs::File;
use std::io::Write;
let dir = tempdir()?;
let file_path = dir.path().join("test.twee");
let mut file = File::create(file_path.clone())?;
writeln!(file, "{}", input)?;
let out = Story::from_path(file_path);
assert_eq!(out.has_warnings(), true);
let (res, warnings) = out.take();
assert_eq!(res.is_ok(), true);
let story = res.ok().unwrap();
assert_eq!(story.title.is_some(), true);
let title = story.title.unwrap();
assert_eq!(title, "Test Story");
assert_eq!(
warnings[0],
Warning::new::<Context>(WarningKind::MissingStoryData, None)
);
Ok(())
}
#[test]
fn a_test() {
let input = r#":: A passage
This
That
The Other
:: Another passage
Foo
Bar
Baz
:: StoryTitle
Test Story
"#
.to_string();
let out = Story::from_string(input);
assert_eq!(out.has_warnings(), false);
let (res, _) = out.take();
assert_eq!(res.is_ok(), true);
let story = res.ok().unwrap();
assert_eq!(story.get_start_passage_name(), None);
assert_eq!(story.title.is_some(), true);
let title = story.title.unwrap();
assert_eq!(title, "Test Story");
}
#[test]
fn dir_input() -> Result<(), Box<dyn std::error::Error>> {
use std::fs::File;
let input_one = r#":: Start
At the start, link to [[A passage]]
:: A passage
This passage links to [[Another passage]]
:: StoryTitle
Test Story
:: Wa\{rning title one
blah blah
"#
.to_string();
let input_two = r#":: Another passage
Links back to [[Start]]
:: StoryData
{
"ifid": "ABC"
}
:: Warning titl\]e two
blah blah
"#
.to_string();
use std::io::Write;
let dir = tempdir()?;
let file_path_one = dir.path().join("test.twee");
let mut file_one = File::create(file_path_one.clone())?;
write!(file_one, "{}", input_one.clone())?;
let file_path_two = dir.path().join("test2.tw");
let mut file_two = File::create(file_path_two.clone())?;
write!(file_two, "{}", input_two.clone())?;
let out = Story::from_path(dir.path());
assert_eq!(out.has_warnings(), true);
let (res, warnings) = out.take();
assert_eq!(warnings.len(), 2);
assert_eq!(res.is_ok(), true);
let story = res.ok().unwrap();
assert_eq!(story.title, Some("Test Story".to_string()));
assert_eq!(story.get_start_passage_name(), Some("Start"));
use crate::FullContext;
use crate::Position;
let context = FullContext::from(Some("test.twee".to_string()), input_one);
assert!(warnings.contains(&{
let warning = Warning::new(
WarningKind::EscapedOpenCurly,
Some(context.subcontext(Position::rel(10, 6)..=Position::rel(10, 7))),
);
warning
}));
let context = FullContext::from(Some("test2.tw".to_string()), input_two);
assert!(warnings.contains(&{
let warning = Warning::new(
WarningKind::EscapedCloseSquare,
Some(context.subcontext(Position::rel(9, 16)..=Position::rel(9, 17))),
);
warning
}));
Ok(())
}
}