From ac0c4050eb845c958d20b84f1dfcbe9290eeb71d Mon Sep 17 00:00:00 2001
From: ef3d0c3e <ef3d0c3e@pundalik.org>
Date: Sun, 28 Jul 2024 14:20:58 +0200
Subject: [PATCH] Experimental navigation

---
 src/compiler/compiler.rs   |  57 ++++++++++++++++----
 src/compiler/mod.rs        |   1 +
 src/compiler/navigation.rs | 103 +++++++++++++++++++++++++++++++++++
 src/main.rs                | 107 ++++++++++++++++++++++++++-----------
 4 files changed, 226 insertions(+), 42 deletions(-)
 create mode 100644 src/compiler/navigation.rs

diff --git a/src/compiler/compiler.rs b/src/compiler/compiler.rs
index a36b89f..d12bf43 100644
--- a/src/compiler/compiler.rs
+++ b/src/compiler/compiler.rs
@@ -9,6 +9,9 @@ use crate::document::document::Document;
 use crate::document::document::ElemReference;
 use crate::document::variable::Variable;
 
+use super::navigation::NavEntry;
+use super::navigation::Navigation;
+
 #[derive(Clone, Copy)]
 pub enum Target {
 	HTML,
@@ -18,8 +21,8 @@ pub enum Target {
 pub struct Compiler {
 	target: Target,
 	cache: Option<RefCell<Connection>>,
-	// TODO:
 	reference_count: RefCell<HashMap<String, HashMap<String, usize>>>,
+	// TODO: External references, i.e resolved later
 }
 
 impl Compiler {
@@ -130,37 +133,71 @@ impl Compiler {
 		result
 	}
 
+	pub fn navigation(&self, navigation: &Navigation, document: &dyn Document) -> String
+	{
+		let mut result = String::new();
+		match self.target()
+		{
+			Target::HTML => {
+				result += r#"<ul id="navbar">"#;
+
+				fn process(result: &mut String, name: &String, ent: &NavEntry, depth: usize)
+				{
+					let ent_path = ent.path.as_ref()
+						.map_or("#".to_string(),|path| path.clone());
+					result.push_str(format!(r#"<li><a href="{ent_path}">{name}</a></li>"#).as_str());
+
+					if let Some(children) = ent.children.as_ref()
+					{
+						result.push_str("<ul>");
+						for (name, ent) in children
+						{
+							process(result, name, ent, depth+1);
+						}
+						result.push_str("</ul>");
+					}
+				}
+
+				for (name, ent) in &navigation.entries
+				{
+					process(&mut result, name, ent, 0);
+				}
+				
+
+				result += r#"</ul>"#;
+			},
+			_ => todo!("")
+		}
+		result
+	}
+
 	pub fn footer(&self, _document: &dyn Document) -> String {
 		let mut result = String::new();
 		match self.target() {
 			Target::HTML => {
 				result += "</body></html>";
 			}
-			Target::LATEX => {}
+			Target::LATEX => todo!("")
 		}
 		result
 	}
 
-	pub fn compile(&self, document: &dyn Document) -> String {
+	pub fn compile(&self, navigation: &Navigation, document: &dyn Document) -> String {
 		let mut out = String::new();
 		let borrow = document.content().borrow();
 
 		// Header
 		out += self.header(document).as_str();
 
+		// Navigation
+		out += self.navigation(navigation, document).as_str();
+
 		// Body
 		for i in 0..borrow.len() {
 			let elem = &borrow[i];
-			//let prev = match i
-			//{
-			//	0 => None,
-			//	_ => borrow.get(i-1),
-			//};
-			//let next = borrow.get(i+1);
 
 			match elem.compile(self, document) {
 				Ok(result) => {
-					//println!("Elem: {}\nCompiled to: {result}", elem.to_string());
 					out.push_str(result.as_str())
 				}
 				Err(err) => println!("Unable to compile element: {err}\n{}", elem.to_string()),
diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs
index 59d8df7..ecf558f 100644
--- a/src/compiler/mod.rs
+++ b/src/compiler/mod.rs
@@ -1 +1,2 @@
 pub mod compiler;
+pub mod navigation;
diff --git a/src/compiler/navigation.rs b/src/compiler/navigation.rs
new file mode 100644
index 0000000..722d563
--- /dev/null
+++ b/src/compiler/navigation.rs
@@ -0,0 +1,103 @@
+use std::collections::HashMap;
+
+use crate::document::document::Document;
+
+#[derive(Debug)]
+pub struct NavEntry {
+	pub(crate) name: String,
+	pub(crate) path: Option<String>,
+	pub(crate) children: Option<HashMap<String, NavEntry>>,
+}
+
+#[derive(Debug)]
+pub struct Navigation {
+	pub(crate) entries: HashMap<String, NavEntry>,
+}
+
+pub fn create_navigation(docs: &Vec<Box<dyn Document>>) -> Result<Navigation, String> {
+	let mut nav = Navigation {
+		entries: HashMap::new(),
+	};
+
+	for doc in docs {
+		let cat = doc.get_variable("nav.category");
+		let subcat = doc.get_variable("nav.subcategory");
+		let title = doc
+			.get_variable("nav.title")
+			.or(doc.get_variable("doc.title"));
+		let path = doc.get_variable("compiler.output");
+
+		let (cat, title, path) = match (cat, title, path) {
+			(Some(cat), Some(title), Some(path)) => (cat, title, path),
+			_ => {
+				println!(
+					"Skipping navigation generation for `{}`",
+					doc.source().name()
+				);
+				continue;
+			}
+		};
+
+		if let Some(subcat) = subcat {
+			// Get parent entry
+			let cat_name = cat.to_string();
+			let mut pent = match nav.entries.get_mut(cat_name.as_str()) {
+				Some(pent) => pent,
+				None => {
+					// Create parent entry
+					nav.entries.insert(
+						cat_name.clone(),
+						NavEntry {
+							name: cat_name.clone(),
+							path: None,
+							children: Some(HashMap::new()),
+						},
+					);
+					nav.entries.get_mut(cat_name.as_str()).unwrap()
+				}
+			};
+
+			// Insert into parent
+			let subcat_name = subcat.to_string();
+			if let Some(previous) = pent.children.as_mut().unwrap().insert(
+				subcat_name.clone(),
+				NavEntry {
+					name: subcat_name.clone(),
+					path: Some(path.to_string()),
+					children: None,
+				},
+			) {
+				return Err(format!(
+					"Duplicate subcategory:\n{subcat:#?}\nclashes with:\n{previous:#?}"
+				));
+			}
+		} else {
+			// Get entry
+			let cat_name = cat.to_string();
+			let mut ent = match nav.entries.get_mut(cat_name.as_str()) {
+				Some(ent) => ent,
+				None => {
+					// Create parent entry
+					nav.entries.insert(
+						cat_name.clone(),
+						NavEntry {
+							name: cat_name.clone(),
+							path: None,
+							children: Some(HashMap::new()),
+						},
+					);
+					nav.entries.get_mut(cat_name.as_str()).unwrap()
+				}
+			};
+
+			if let Some(path) = ent.path.as_ref() {
+				return Err(format!(
+					"Duplicate category:\n{subcat:#?}\nwith previous path:\n{path}"
+				));
+			}
+			ent.path = Some(path.to_string());
+		}
+	}
+
+	Ok(nav)
+}
diff --git a/src/main.rs b/src/main.rs
index d00b9bc..a19a37d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,6 +11,10 @@ use std::process::ExitCode;
 use std::rc::Rc;
 
 use compiler::compiler::Compiler;
+use compiler::compiler::Target;
+use compiler::navigation::create_navigation;
+use compiler::navigation::Navigation;
+use document::document::Document;
 use getopts::Options;
 use parser::langparser::LangParser;
 use parser::parser::Parser;
@@ -38,15 +42,41 @@ NML version: 0.4\n"
 	);
 }
 
-fn process(
-	parser: &LangParser,
-	db_path: &Option<String>,
-	input: &String,
+fn compile(
+	target: Target,
+	doc: &Box<dyn Document>,
 	output: &String,
-	debug_opts: &Vec<String>,
+	db_path: &Option<String>,
+	navigation: &Navigation,
 	multi_mode: bool,
 ) -> bool {
-	println!("Processing {input}...");
+	let compiler = Compiler::new(target, db_path.clone());
+
+	// Get output from file
+	if multi_mode {
+		let out_file = match doc.get_variable("compiler.output") {
+			None => {
+				eprintln!("Missing required variable `compiler.output` for multifile mode");
+				return false;
+			}
+			Some(var) => output.clone() + "/" + var.to_string().as_str(),
+		};
+
+		let out = compiler.compile(navigation, doc.as_ref());
+		std::fs::write(out_file, out).is_ok()
+	} else {
+		let out = compiler.compile(navigation, doc.as_ref());
+		std::fs::write(output, out).is_ok()
+	}
+}
+
+fn parse(
+	input: &String,
+	debug_opts: &Vec<String>,
+) -> Result<Box<dyn Document<'static>>, String> {
+	println!("Parsing {input}...");
+	let parser = LangParser::default();
+
 	// Parse
 	let source = SourceFile::new(input.to_string(), None).unwrap();
 	let doc = parser.parse(Rc::new(source), None);
@@ -78,28 +108,10 @@ fn process(
 	}
 
 	if parser.has_error() {
-		println!("Compilation aborted due to errors while parsing");
-		return false;
+		return Err("Parsing failed aborted due to errors while parsing".to_string())
 	}
 
-	let compiler = Compiler::new(compiler::compiler::Target::HTML, db_path.clone());
-
-	// Get output from file
-	if multi_mode {
-		let out_file = match doc.get_variable("compiler.output") {
-			None => {
-				eprintln!("Missing required variable `compiler.output` for multifile mode");
-				return false;
-			}
-			Some(var) => output.clone() + "/" + var.to_string().as_str(),
-		};
-
-		let out = compiler.compile(doc.as_ref());
-		std::fs::write(out_file, out).is_ok()
-	} else {
-		let out = compiler.compile(doc.as_ref());
-		std::fs::write(output, out).is_ok()
-	}
+	Ok(doc)
 }
 
 fn main() -> ExitCode {
@@ -164,7 +176,8 @@ fn main() -> ExitCode {
 
 	let debug_opts = matches.opt_strs("z");
 	let db_path = matches.opt_str("d");
-	let parser = LangParser::default();
+
+	let mut docs = vec![];
 
 	if input_meta.is_dir() {
 		if db_path.is_none() {
@@ -208,14 +221,44 @@ fn main() -> ExitCode {
 				continue;
 			}
 
-			if !process(&parser, &db_path, &path, &output, &debug_opts, true) {
-				eprintln!("Processing aborted");
-				return ExitCode::FAILURE;
+			match parse(&path, &debug_opts)
+			{
+				Ok(doc) => docs.push(doc),
+				Err(e) => {
+					eprintln!("{e}");
+					return ExitCode::FAILURE;
+				}
 			}
 		}
 	} else {
-		if !process(&parser, &db_path, &input, &output, &debug_opts, false) {
-			eprintln!("Processing aborted");
+		match parse(&input, &debug_opts)
+		{
+			Ok(doc) => docs.push(doc),
+			Err(e) => {
+				eprintln!("{e}");
+				return ExitCode::FAILURE;
+			}
+		}
+	}
+
+	// Build navigation
+	let navigation = match create_navigation(&docs)
+	{
+		Ok(nav) => nav,
+		Err(e) => {
+			eprintln!("{e}");
+			return ExitCode::FAILURE;
+		}
+	};
+
+	println!("{navigation:#?}");
+
+	let multi_mode = input_meta.is_dir();
+	for doc in docs
+	{
+		if !compile(Target::HTML, &doc, &output, &db_path, &navigation, multi_mode)
+		{
+			eprintln!("Compilation failed, processing aborted");
 			return ExitCode::FAILURE;
 		}
 	}