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.

212 lines
6.3 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. use comrak::markdown_to_html;
  2. use std::collections::BTreeMap;
  3. use std::io::{Error as IoError, Write};
  4. use std::{env::args, fs};
  5. fn main() {
  6. let filename = args().nth(1).expect("no file given");
  7. let detail = Detail::new(&filename).unwrap();
  8. let out = filename
  9. .strip_suffix(".detail")
  10. .unwrap_or(&filename)
  11. .to_string()
  12. + ".html";
  13. let mut file =
  14. std::fs::File::create(out.clone()).unwrap_or_else(|_| panic!("couldn't create {}", out));
  15. let header = header();
  16. let title = title();
  17. let footer = footer();
  18. let content = header + &detail.title + &title + &detail.into_html().unwrap() + &footer;
  19. file.write_all(content.as_bytes())
  20. .unwrap_or_else(|_| panic!("unable to write to {}", out));
  21. }
  22. // ~~~ data structures ~~~
  23. struct Detail {
  24. passages: BTreeMap<String, String>,
  25. title: String,
  26. }
  27. impl Detail {
  28. fn new(filename: &str) -> Result<Self, DetailError> {
  29. let contents = fs::read_to_string(filename).map_err(|err| DetailError::Io(err))?;
  30. if contents.is_empty() {
  31. return Err(DetailError::EmptyFile);
  32. }
  33. let mut passages = BTreeMap::new();
  34. let mut cur_passage: Option<String> = None;
  35. let mut cur_lines = String::new();
  36. let mut has_title = true;
  37. let title = contents
  38. .lines()
  39. .peekable()
  40. .peek()
  41. .unwrap()
  42. .strip_prefix("~title")
  43. .unwrap_or_else(|| {
  44. has_title = false;
  45. "details"
  46. });
  47. let mut lines = contents.lines();
  48. if has_title {
  49. lines.next();
  50. }
  51. for line in lines {
  52. let line = line.trim();
  53. // if line begins with ~detail
  54. if let Some(passage_name) = line.strip_prefix("~detail") {
  55. let passage_name = passage_name.trim();
  56. // if this isn't the first passage, add previous passage to map
  57. if let Some(prev_passage) = cur_passage {
  58. let prev_passage = prev_passage.to_string();
  59. if !cur_lines.is_empty() {
  60. if passages.contains_key(&prev_passage) {
  61. println!("WARNING: passage name \"{}\" duplicated", prev_passage);
  62. passages.remove(&prev_passage);
  63. } else {
  64. // clear cur_lines and insert
  65. passages.insert(prev_passage, std::mem::take(&mut cur_lines));
  66. }
  67. }
  68. }
  69. cur_passage = Some(passage_name.to_string());
  70. } else {
  71. // case: regular line
  72. cur_lines += line;
  73. cur_lines += "\n";
  74. }
  75. }
  76. // add last passage
  77. if let Some(prev_passage) = cur_passage {
  78. let prev_passage = prev_passage.to_string();
  79. if !cur_lines.is_empty() {
  80. if passages.contains_key(&prev_passage) {
  81. println!("WARNING: passage name \"{}\" duplicated", prev_passage);
  82. passages.remove(&prev_passage);
  83. } else {
  84. // clear cur_lines and insert
  85. passages.insert(prev_passage, std::mem::take(&mut cur_lines));
  86. }
  87. }
  88. }
  89. let detail = Self {
  90. passages,
  91. title: title.to_string(),
  92. };
  93. match detail.check_valid(&"start", &mut Vec::new()) {
  94. Some(true) => Ok(detail),
  95. Some(false) => Err(DetailError::InfiniteLoop),
  96. None => Err(DetailError::FlattenError),
  97. }
  98. }
  99. fn into_html(self) -> Option<String> {
  100. self.flatten(&"start")
  101. }
  102. fn flatten(&self, rule: &str) -> Option<String> {
  103. let rule = self.passages.get(rule)?;
  104. let mut output = String::new();
  105. for (sep, piece) in rule.split('|').enumerate() {
  106. // search keys
  107. if sep % 2 == 0 {
  108. let mut options = comrak::ComrakOptions::default();
  109. options.extension.strikethrough = true;
  110. options.parse.smart = true;
  111. options.render.unsafe_ = true;
  112. output += &markdown_to_html(&piece, &options);
  113. } else {
  114. let (display, detail_to) = piece.rsplit_once('~').unwrap_or((piece, piece));
  115. let expand = self.flatten(&detail_to);
  116. match expand {
  117. Some(expand) => {
  118. output += "<details><summary>";
  119. output += display;
  120. output += "</summary>";
  121. output += &expand;
  122. output += "</details>";
  123. }
  124. None => {
  125. output += detail_to;
  126. }
  127. }
  128. }
  129. }
  130. Some(output)
  131. }
  132. fn check_valid(&self, rule: &str, already_visited: &mut Vec<String>) -> Option<bool> {
  133. already_visited.push(rule.to_string());
  134. let rule = self.passages.get(rule)?;
  135. let mut output = String::new();
  136. for (sep, piece) in rule.split('#').enumerate() {
  137. // search keys
  138. if sep % 2 == 0 {
  139. output += piece;
  140. } else {
  141. let (_, detail_to) = piece.split_once('~').unwrap_or((piece, piece));
  142. if already_visited
  143. .iter()
  144. .find(|passage| *passage == detail_to)
  145. .is_none()
  146. {
  147. return self.check_valid(&detail_to, already_visited);
  148. } else {
  149. return Some(false);
  150. }
  151. }
  152. }
  153. Some(true)
  154. }
  155. }
  156. #[derive(Debug)]
  157. enum DetailError {
  158. Io(IoError),
  159. InfiniteLoop,
  160. FlattenError,
  161. EmptyFile,
  162. }
  163. fn header() -> String {
  164. r#"<!DOCTYPE html>
  165. <html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
  166. <head>
  167. <meta charset="utf-8" />
  168. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
  169. <title>"#
  170. .to_string()
  171. }
  172. fn title() -> String {
  173. r#"</title>
  174. <style>
  175. details {
  176. margin-left: 1em;
  177. }
  178. </style>
  179. </head>
  180. <body>
  181. "#
  182. .to_string()
  183. }
  184. fn footer() -> String {
  185. r#"</body>
  186. </html>
  187. "#
  188. .to_string()
  189. }