|
|
@ -1,5 +1,4 @@ |
|
|
|
#![allow(dead_code)]
|
|
|
|
|
|
|
|
use markdown::to_html;
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use std::io::{Error as IoError, Write};
|
|
|
|
use std::{env::args, fs};
|
|
|
@ -16,29 +15,56 @@ fn main() { |
|
|
|
let mut file =
|
|
|
|
std::fs::File::create(out.clone()).unwrap_or_else(|_| panic!("couldn't create {}", out));
|
|
|
|
|
|
|
|
file.write_all(detail.into_html().unwrap().as_bytes())
|
|
|
|
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>,
|
|
|
|
node_graph: Vec<Vec<bool>>,
|
|
|
|
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 contents.lines() {
|
|
|
|
for line in lines {
|
|
|
|
let line = line.trim();
|
|
|
|
|
|
|
|
// if line begins with ~detail
|
|
|
|
if let Some(passage_name) = line.strip_prefix("~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();
|
|
|
@ -57,6 +83,7 @@ impl Detail { |
|
|
|
} else {
|
|
|
|
// case: regular line
|
|
|
|
cur_lines += line;
|
|
|
|
cur_lines += "\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
@ -74,24 +101,15 @@ impl Detail { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut node_graph = Vec::new();
|
|
|
|
node_graph.resize_with(passages.len(), || {
|
|
|
|
let mut row = Vec::new();
|
|
|
|
row.resize(passages.len(), false);
|
|
|
|
row
|
|
|
|
});
|
|
|
|
|
|
|
|
let mut detail = Self {
|
|
|
|
let detail = Self {
|
|
|
|
passages,
|
|
|
|
node_graph,
|
|
|
|
title: title.to_string(),
|
|
|
|
};
|
|
|
|
|
|
|
|
detail.build_graph();
|
|
|
|
|
|
|
|
if detail.check_valid() {
|
|
|
|
Ok(detail)
|
|
|
|
} else {
|
|
|
|
Err(DetailError::InfiniteLoop)
|
|
|
|
match detail.check_valid(&"start", &mut Vec::new()) {
|
|
|
|
Some(true) => Ok(detail),
|
|
|
|
Some(false) => Err(DetailError::InfiniteLoop),
|
|
|
|
None => Err(DetailError::FlattenError),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
@ -105,7 +123,7 @@ impl Detail { |
|
|
|
for (sep, piece) in rule.split('#').enumerate() {
|
|
|
|
// search keys
|
|
|
|
if sep % 2 == 0 {
|
|
|
|
output += piece;
|
|
|
|
output += &to_html(&piece);
|
|
|
|
} else {
|
|
|
|
let (display, detail_to) = piece.split_once('~').unwrap_or((piece, piece));
|
|
|
|
let expand = self.flatten(&detail_to)?;
|
|
|
@ -119,27 +137,28 @@ impl Detail { |
|
|
|
Some(output)
|
|
|
|
}
|
|
|
|
|
|
|
|
// don't speak to me about how nested this method is.
|
|
|
|
fn build_graph(&mut self) {
|
|
|
|
for (i, content) in self.passages.values().enumerate() {
|
|
|
|
for (sep, piece) in content.split('#').enumerate() {
|
|
|
|
// search keys
|
|
|
|
if sep % 2 == 1 {
|
|
|
|
if let Some((_, detail_to)) = piece.split_once('~') {
|
|
|
|
if let Some(j) = self.passages.keys().position(|name| name == &detail_to) {
|
|
|
|
self.node_graph[i][j] = true;
|
|
|
|
}
|
|
|
|
} else if let Some(j) = self.passages.keys().position(|name| name == &piece) {
|
|
|
|
self.node_graph[i][j] = true;
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO actually write this
|
|
|
|
fn check_valid(&self) -> bool {
|
|
|
|
true
|
|
|
|
Some(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
@ -147,4 +166,36 @@ impl Detail { |
|
|
|
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()
|
|
|
|
}
|