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.
212 lines
6.3 KiB
212 lines
6.3 KiB
use comrak::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 {
|
|
let mut options = comrak::ComrakOptions::default();
|
|
options.extension.strikethrough = true;
|
|
options.parse.smart = true;
|
|
options.render.unsafe_ = true;
|
|
output += &markdown_to_html(&piece, &options);
|
|
} else {
|
|
let (display, detail_to) = piece.rsplit_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()
|
|
}
|