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, title: String, } impl Detail { fn new(filename: &str) -> Result { 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 = 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 { self.flatten(&"start") } fn flatten(&self, rule: &str) -> Option { 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 += "
"; output += display; output += ""; output += &expand; output += "
"; } None => { output += detail_to; } } } } Some(output) } fn check_valid(&self, rule: &str, already_visited: &mut Vec) -> Option { 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#" "# .to_string() } fn title() -> String { r#" "# .to_string() } fn footer() -> String { r#" "# .to_string() }