tiny gamemaking tool using the html details tag 💙 likely fragile, be nice to it
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

208 lines
6.1 KiB

use markdown::to_html;
use std::collections::BTreeMap;
use std::io::{Error as IoError, Write};
use std::{env::args, fs};
fn main() {
let filename = args().nth(1).expect("no file given");
let detail = Detail::new(&filename).unwrap();
let out = filename
.strip_suffix(".detail")
.unwrap_or(&filename)
.to_string()
+ ".html";
let mut file =
std::fs::File::create(out.clone()).unwrap_or_else(|_| panic!("couldn't create {}", out));
let header = header();
let title = title();
let footer = footer();
let content = header + &detail.title + &title + &detail.into_html().unwrap() + &footer;
file.write_all(content.as_bytes())
.unwrap_or_else(|_| panic!("unable to write to {}", out));
}
// ~~~ data structures ~~~
struct Detail {
passages: BTreeMap<String, String>,
title: String,
}
impl Detail {
fn new(filename: &str) -> Result<Self, DetailError> {
let contents = fs::read_to_string(filename).map_err(|err| DetailError::Io(err))?;
if contents.is_empty() {
return Err(DetailError::EmptyFile);
}
let mut passages = BTreeMap::new();
let mut cur_passage: Option<String> = None;
let mut cur_lines = String::new();
let mut has_title = true;
let title = contents
.lines()
.peekable()
.peek()
.unwrap()
.strip_prefix("~title")
.unwrap_or_else(|| {
has_title = false;
"details"
});
let mut lines = contents.lines();
if has_title {
lines.next();
}
for line in lines {
let line = line.trim();
// if line begins with ~detail
if let Some(passage_name) = line.strip_prefix("~detail") {
let passage_name = passage_name.trim();
// if this isn't the first passage, add previous passage to map
if let Some(prev_passage) = cur_passage {
let prev_passage = prev_passage.to_string();
if !cur_lines.is_empty() {
if passages.contains_key(&prev_passage) {
println!("WARNING: passage name \"{}\" duplicated", prev_passage);
passages.remove(&prev_passage);
} else {
// clear cur_lines and insert
passages.insert(prev_passage, std::mem::take(&mut cur_lines));
}
}
}
cur_passage = Some(passage_name.to_string());
} else {
// case: regular line
cur_lines += line;
cur_lines += "\n";
}
}
// add last passage
if let Some(prev_passage) = cur_passage {
let prev_passage = prev_passage.to_string();
if !cur_lines.is_empty() {
if passages.contains_key(&prev_passage) {
println!("WARNING: passage name \"{}\" duplicated", prev_passage);
passages.remove(&prev_passage);
} else {
// clear cur_lines and insert
passages.insert(prev_passage, std::mem::take(&mut cur_lines));
}
}
}
let detail = Self {
passages,
title: title.to_string(),
};
match detail.check_valid(&"start", &mut Vec::new()) {
Some(true) => Ok(detail),
Some(false) => Err(DetailError::InfiniteLoop),
None => Err(DetailError::FlattenError),
}
}
fn into_html(self) -> Option<String> {
self.flatten(&"start")
}
fn flatten(&self, rule: &str) -> Option<String> {
let rule = self.passages.get(rule)?;
let mut output = String::new();
for (sep, piece) in rule.split('#').enumerate() {
// search keys
if sep % 2 == 0 {
output += &to_html(&piece);
} else {
let (display, detail_to) = piece.split_once('~').unwrap_or((piece, piece));
let expand = self.flatten(&detail_to);
match expand {
Some(expand) => {
output += "<details><summary>";
output += display;
output += "</summary>";
output += &expand;
output += "</details>";
}
None => {
output += detail_to;
}
}
}
}
Some(output)
}
fn check_valid(&self, rule: &str, already_visited: &mut Vec<String>) -> Option<bool> {
already_visited.push(rule.to_string());
let rule = self.passages.get(rule)?;
let mut output = String::new();
for (sep, piece) in rule.split('#').enumerate() {
// search keys
if sep % 2 == 0 {
output += piece;
} else {
let (_, detail_to) = piece.split_once('~').unwrap_or((piece, piece));
if already_visited
.iter()
.find(|passage| *passage == detail_to)
.is_none()
{
return self.check_valid(&detail_to, already_visited);
} else {
return Some(false);
}
}
}
Some(true)
}
}
#[derive(Debug)]
enum DetailError {
Io(IoError),
InfiniteLoop,
FlattenError,
EmptyFile,
}
fn header() -> String {
r#"<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>"#
.to_string()
}
fn title() -> String {
r#"</title>
<style>
details {
margin-left: 1em;
}
</style>
</head>
<body>
"#
.to_string()
}
fn footer() -> String {
r#"</body>
</html>
"#
.to_string()
}