From 48d2064d0c8ccb012a0cb6bf9fe560701a2f99ef Mon Sep 17 00:00:00 2001
From: ef3d0c3e <ef3d0c3e@pundalik.org>
Date: Sat, 3 Aug 2024 09:33:21 +0200
Subject: [PATCH] Added element styling

---
 src/document/element.rs   |  10 ++-
 src/document/mod.rs       |   1 +
 src/document/style.rs     |  66 +++++++++++++++++++
 src/elements/elemstyle.rs | 134 ++++++++++++++++++++++++++++++++++++++
 src/elements/mod.rs       |   1 +
 src/elements/registrar.rs |   2 +
 src/elements/section.rs   | 132 +++++++++++++++++++++++++++++++------
 src/elements/variable.rs  |   6 +-
 src/parser/langparser.rs  |  21 ++++++
 src/parser/parser.rs      |   3 +-
 src/parser/rule.rs        |  12 +++-
 11 files changed, 359 insertions(+), 29 deletions(-)
 create mode 100644 src/document/style.rs
 create mode 100644 src/elements/elemstyle.rs

diff --git a/src/document/element.rs b/src/document/element.rs
index 24eec1c..59ac9a6 100644
--- a/src/document/element.rs
+++ b/src/document/element.rs
@@ -3,6 +3,7 @@ use std::str::FromStr;
 use crate::compiler::compiler::Compiler;
 use crate::elements::reference::Reference;
 use crate::parser::source::Token;
+use crate::parser::util::PropertyParser;
 use downcast_rs::impl_downcast;
 use downcast_rs::Downcast;
 
@@ -62,10 +63,15 @@ pub trait ReferenceableElement: Element {
 	fn refcount_key(&self) -> &'static str;
 
 	/// Creates the reference element
-	fn compile_reference(&self, compiler: &Compiler, document: &dyn Document, reference: &Reference, refid: usize) -> Result<String, String>;
+	fn compile_reference(
+		&self,
+		compiler: &Compiler,
+		document: &dyn Document,
+		reference: &Reference,
+		refid: usize,
+	) -> Result<String, String>;
 }
 
-
 pub trait ContainerElement: Element {
 	/// Gets the contained elements
 	fn contained(&self) -> &Vec<Box<dyn Element>>;
diff --git a/src/document/mod.rs b/src/document/mod.rs
index bff3340..8fb316f 100644
--- a/src/document/mod.rs
+++ b/src/document/mod.rs
@@ -3,3 +3,4 @@ pub mod references;
 pub mod langdocument;
 pub mod element;
 pub mod variable;
+pub mod style;
diff --git a/src/document/style.rs b/src/document/style.rs
new file mode 100644
index 0000000..fcf8334
--- /dev/null
+++ b/src/document/style.rs
@@ -0,0 +1,66 @@
+use std::cell::Ref;
+use std::cell::RefMut;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use downcast_rs::impl_downcast;
+use downcast_rs::Downcast;
+
+/// Styling for an element
+pub trait ElementStyle: Downcast + core::fmt::Debug {
+	/// The style key
+	fn key(&self) -> &'static str;
+
+	/// Attempts to create a new style from a [`json`] string
+	///
+	/// # Errors
+	///
+	/// Will fail if deserialization fails
+	fn from_json(&self, json: &str) -> Result<Rc<dyn ElementStyle>, String>;
+
+	/// Serializes sytle into json string
+	fn to_json(&self) -> String;
+}
+impl_downcast!(ElementStyle);
+
+pub trait StyleHolder {
+	/// gets a reference to all defined styles
+	fn styles(&self) -> Ref<'_, HashMap<String, Rc<dyn ElementStyle>>>;
+
+	/// gets a (mutable) reference to all defined styles
+	fn styles_mut(&self) -> RefMut<'_, HashMap<String, Rc<dyn ElementStyle>>>;
+
+	/// Checks if a given style key is registered
+	fn is_registered(&self, style_key: &str) -> bool {
+		self.styles().contains_key(style_key)
+	}
+
+	/// Gets the current active style for an element
+	/// NOTE: Will panic if a style is not defined for a given element
+	/// If you need to process user input, use [`is_registered`]
+	fn current_style(&self, style_key: &str) -> Rc<dyn ElementStyle> {
+		self.styles().get(style_key).map(|rc| rc.clone()).unwrap()
+	}
+
+	/// Sets the [`style`]
+	fn set_style(&self, style: Rc<dyn ElementStyle>) {
+		self.styles_mut().insert(style.key().to_string(), style);
+	}
+}
+
+#[macro_export]
+macro_rules! impl_elementstyle {
+	($t:ty, $key:expr) => {
+		impl ElementStyle for $t {
+			fn key(&self) -> &'static str { $key }
+
+			fn from_json(&self, json: &str) -> Result<std::rc::Rc<dyn ElementStyle>, String> {
+				serde_json::from_str::<$t>(json)
+					.map_err(|e| e.to_string())
+					.map(|obj| std::rc::Rc::new(obj) as std::rc::Rc<dyn ElementStyle>)
+			}
+
+			fn to_json(&self) -> String { serde_json::to_string(self).unwrap() }
+		}
+	};
+}
diff --git a/src/elements/elemstyle.rs b/src/elements/elemstyle.rs
new file mode 100644
index 0000000..3277332
--- /dev/null
+++ b/src/elements/elemstyle.rs
@@ -0,0 +1,134 @@
+use std::ops::Range;
+use std::rc::Rc;
+
+use ariadne::{Fmt, Label, Report, ReportKind};
+use regex::{Captures, Regex};
+
+use crate::document::document::Document;
+use crate::document::{self};
+use crate::parser::parser::Parser;
+use crate::parser::rule::RegexRule;
+use crate::parser::source::Source;
+use crate::parser::source::Token;
+
+use super::variable::VariableRule;
+
+pub struct ElemStyleRule {
+	re: [Regex; 1],
+}
+
+impl ElemStyleRule {
+	pub fn new() -> Self {
+		Self {
+			re: [Regex::new(r"(?:^|\n)@@(.*?)=((?:\\\n|.)*)").unwrap()],
+		}
+	}
+}
+
+impl RegexRule for ElemStyleRule {
+	fn name(&self) -> &'static str { "Element Style" }
+
+	fn regexes(&self) -> &[regex::Regex] { &self.re }
+
+	fn on_regex_match<'a>(
+		&self,
+		_: usize,
+		parser: &dyn Parser,
+		_document: &'a dyn Document,
+		token: Token,
+		matches: Captures,
+	) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
+		let mut reports = vec![];
+
+		let style = if let Some(key) = matches.get(1)
+		{
+			let trimmed = key.as_str().trim_start().trim_end();
+			
+			// Check if empty
+			if trimmed.is_empty()
+			{
+				reports.push(
+				Report::build(ReportKind::Error, token.source(), key.start())
+					.with_message("Empty Style Key")
+					.with_label(
+						Label::new((token.source(), key.range()))
+						.with_message(format!(
+								"Expected a non-empty style key",
+						))
+						.with_color(parser.colors().error),
+					)
+					.finish());
+				return reports;
+			}
+
+			// Check if key exists
+			if !parser.is_registered(trimmed)
+			{
+				reports.push(
+				Report::build(ReportKind::Error, token.source(), key.start())
+					.with_message("Unknown Style Key")
+					.with_label(
+						Label::new((token.source(), key.range()))
+						.with_message(format!(
+								"Could not find a style with key: {}",
+								trimmed.fg(parser.colors().info)
+						))
+						.with_color(parser.colors().error),
+					)
+					.finish());
+
+				return reports;
+			}
+			
+			parser.current_style(trimmed)
+		} else { panic!("Unknown error") };
+
+		// Get value
+		let new_style = if let Some(value) = matches.get(2) {
+			let value_str = match VariableRule::validate_value(value.as_str()) {
+				Err(err) => {
+					reports.push(
+						Report::build(ReportKind::Error, token.source(), value.start())
+							.with_message("Invalid Style Value")
+							.with_label(
+								Label::new((token.source(), value.range()))
+								.with_message(format!(
+										"Value `{}` is not allowed: {err}",
+										value.as_str().fg(parser.colors().highlight)
+								))
+								.with_color(parser.colors().error),
+							)
+							.finish());
+					return reports;
+				}
+				Ok(value) => value,
+			};
+
+			// Attempt to serialize
+			match style.from_json(value_str.as_str())
+			{
+				Err(err) => {
+					reports.push(
+						Report::build(ReportKind::Error, token.source(), value.start())
+						.with_message("Invalid Style Value")
+						.with_label(
+							Label::new((token.source(), value.range()))
+							.with_message(format!(
+									"Failed to serialize `{}` into style with key `{}`: {err}",
+									value_str.fg(parser.colors().highlight),
+									style.key().fg(parser.colors().info)
+							))
+							.with_color(parser.colors().error),
+						)
+						.finish());
+						return reports;
+				},
+				Ok(style) => style,
+			}
+		} else { panic!("Unknown error") };
+
+		parser.set_style(new_style);
+
+		reports
+	}
+}
diff --git a/src/elements/mod.rs b/src/elements/mod.rs
index 9128fc6..066b14c 100644
--- a/src/elements/mod.rs
+++ b/src/elements/mod.rs
@@ -16,3 +16,4 @@ pub mod style;
 pub mod tex;
 pub mod text;
 pub mod variable;
+pub mod elemstyle;
diff --git a/src/elements/registrar.rs b/src/elements/registrar.rs
index 5b08177..13ed156 100644
--- a/src/elements/registrar.rs
+++ b/src/elements/registrar.rs
@@ -2,6 +2,7 @@ use crate::parser::parser::Parser;
 
 use super::code::CodeRule;
 use super::comment::CommentRule;
+use super::elemstyle::ElemStyleRule;
 use super::graphviz::GraphRule;
 use super::import::ImportRule;
 use super::layout::LayoutRule;
@@ -24,6 +25,7 @@ pub fn register<P: Parser>(parser: &mut P) {
 	parser.add_rule(Box::new(ParagraphRule::new()), None).unwrap();
 	parser.add_rule(Box::new(ImportRule::new()), None).unwrap();
 	parser.add_rule(Box::new(ScriptRule::new()), None).unwrap();
+	parser.add_rule(Box::new(ElemStyleRule::new()), None).unwrap();
 	parser.add_rule(Box::new(VariableRule::new()), None).unwrap();
 	parser.add_rule(Box::new(VariableSubstitutionRule::new()), None).unwrap();
 	parser.add_rule(Box::new(RawRule::new()), None).unwrap();
diff --git a/src/elements/section.rs b/src/elements/section.rs
index 6d2be6b..ae9e77c 100644
--- a/src/elements/section.rs
+++ b/src/elements/section.rs
@@ -17,7 +17,8 @@ use mlua::Error::BadArgument;
 use mlua::Function;
 use mlua::Lua;
 use regex::Regex;
-use section_kind::NO_NUMBER;
+use section_style::SectionLinkPos;
+use section_style::SectionStyle;
 use std::ops::Range;
 use std::rc::Rc;
 use std::sync::Arc;
@@ -25,42 +26,78 @@ use std::sync::Arc;
 #[derive(Debug)]
 pub struct Section {
 	pub(self) location: Token,
-	pub(self) title: String,             // Section title
-	pub(self) depth: usize,              // Section depth
-	pub(self) kind: u8,                  // Section kind, e.g numbered, unnumbred, ...
-	pub(self) reference: Option<String>, // Section reference name
+	/// Title of the section
+	pub(self) title: String,
+	/// Depth i.e number of '#'
+	pub(self) depth: usize,
+	/// [`section_kind`]
+	pub(self) kind: u8,
+	/// Section reference name
+	pub(self) reference: Option<String>,
+	/// Style of the section
+	pub(self) style: Rc<section_style::SectionStyle>,
 }
 
 impl Element for Section {
 	fn location(&self) -> &Token { &self.location }
 	fn kind(&self) -> ElemKind { ElemKind::Block }
 	fn element_name(&self) -> &'static str { "Section" }
-	fn as_referenceable(&self) -> Option<&dyn ReferenceableElement> { Some(self) }
 	fn compile(&self, compiler: &Compiler, _document: &dyn Document) -> Result<String, String> {
 		match compiler.target() {
 			Target::HTML => {
-				let mut number = String::new();
-
-				if (self.kind & NO_NUMBER) != NO_NUMBER {
+				// Section numbering
+				let number = if (self.kind & section_kind::NO_NUMBER) == section_kind::NO_NUMBER {
 					let numbering = compiler.section_counter(self.depth);
-					number = numbering
-						.iter()
-						.map(|n| n.to_string())
-						.collect::<Vec<_>>()
-						.join(".");
-					number += " ";
+					let number = " ".to_string()
+						+ numbering
+							.iter()
+							.map(|n| n.to_string())
+							.collect::<Vec<_>>()
+							.join(".")
+							.as_str();
+					number
+				} else {
+					String::new()
+				};
+
+				if self.style.link_pos == SectionLinkPos::None {
+					return Ok(format!(
+						r#"<h{0} id="{1}">{number}{2}</h{0}>"#,
+						self.depth,
+						Compiler::refname(compiler.target(), self.title.as_str()),
+						Compiler::sanitize(compiler.target(), self.title.as_str())
+					));
 				}
 
-				Ok(format!(
-					r#"<h{0} id="{1}">{number}{2}</h{0}>"#,
-					self.depth,
-					Compiler::refname(compiler.target(), self.title.as_str()),
-					Compiler::sanitize(compiler.target(), self.title.as_str())
-				))
+				let refname = Compiler::refname(compiler.target(), self.title.as_str());
+				let link = format!(
+					"<a class=\"section-link\" href=\"#{refname}\">{}</a>",
+					Compiler::sanitize(compiler.target(), self.style.link.as_str())
+				);
+
+				if self.style.link_pos == SectionLinkPos::After {
+					Ok(format!(
+						r#"<h{0} id="{1}">{number}{2}{link}</h{0}>"#,
+						self.depth,
+						Compiler::refname(compiler.target(), self.title.as_str()),
+						Compiler::sanitize(compiler.target(), self.title.as_str())
+					))
+				} else
+				// Before
+				{
+					Ok(format!(
+						r#"<h{0} id="{1}">{link}{number}{2}</h{0}>"#,
+						self.depth,
+						Compiler::refname(compiler.target(), self.title.as_str()),
+						Compiler::sanitize(compiler.target(), self.title.as_str())
+					))
+				}
 			}
 			Target::LATEX => Err("Unimplemented compiler".to_string()),
 		}
 	}
+
+	fn as_referenceable(&self) -> Option<&dyn ReferenceableElement> { Some(self) }
 }
 
 impl ReferenceableElement for Section {
@@ -252,6 +289,12 @@ impl RegexRule for SectionRule {
 			_ => panic!("Empty section name"),
 		};
 
+		// Get style
+		let style = parser
+			.current_style(section_style::STYLE_KEY)
+			.downcast_rc::<SectionStyle>()
+			.unwrap();
+
 		parser.push(
 			document,
 			Box::new(Section {
@@ -260,6 +303,7 @@ impl RegexRule for SectionRule {
 				depth: section_depth,
 				kind: section_kind,
 				reference: section_refname,
+				style,
 			}),
 		);
 
@@ -292,6 +336,13 @@ impl RegexRule for SectionRule {
 
 					CTX.with_borrow(|ctx| {
 						ctx.as_ref().map(|ctx| {
+							// Get style
+							let style = ctx
+								.parser
+								.current_style(section_style::STYLE_KEY)
+								.downcast_rc::<SectionStyle>()
+								.unwrap();
+
 							ctx.parser.push(
 								ctx.document,
 								Box::new(Section {
@@ -300,6 +351,7 @@ impl RegexRule for SectionRule {
 									depth,
 									kind,
 									reference,
+									style,
 								}),
 							);
 						})
@@ -313,4 +365,42 @@ impl RegexRule for SectionRule {
 
 		Some(bindings)
 	}
+
+	fn register_styles(&self, parser: &dyn Parser) {
+		parser.set_style(Rc::new(SectionStyle::default()));
+	}
+}
+
+mod section_style {
+	use serde::Deserialize;
+	use serde::Serialize;
+
+	use crate::document::style::ElementStyle;
+	use crate::impl_elementstyle;
+
+	pub static STYLE_KEY: &'static str = "style.section";
+
+	#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
+	pub enum SectionLinkPos {
+		Before,
+		After,
+		None,
+	}
+
+	#[derive(Debug, Serialize, Deserialize)]
+	pub struct SectionStyle {
+		pub link_pos: SectionLinkPos,
+		pub link: String,
+	}
+
+	impl Default for SectionStyle {
+		fn default() -> Self {
+			Self {
+				link_pos: SectionLinkPos::After,
+				link: "🔗".to_string(),
+			}
+		}
+	}
+
+	impl_elementstyle!(SectionStyle, STYLE_KEY);
 }
diff --git a/src/elements/variable.rs b/src/elements/variable.rs
index dd21a31..e8607f2 100644
--- a/src/elements/variable.rs
+++ b/src/elements/variable.rs
@@ -85,7 +85,7 @@ impl VariableRule {
 		return Ok(name);
 	}
 
-	pub fn validate_value(_colors: &ReportColors, original_value: &str) -> Result<String, String> {
+	pub fn validate_value(original_value: &str) -> Result<String, String> {
 		let mut escaped = 0usize;
 		let mut result = String::new();
 		for c in original_value.trim_start().trim_end().chars() {
@@ -93,7 +93,7 @@ impl VariableRule {
 				escaped += 1
 			} else if c == '\n' {
 				match escaped {
-					0 => return Err("Unknown error wile capturing variable".to_string()),
+					0 => return Err("Unknown error wile capturing value".to_string()),
 					// Remove '\n'
 					1 => {}
 					// Insert '\n'
@@ -202,7 +202,7 @@ impl RegexRule for VariableRule {
 		};
 
 		let var_value = match matches.get(3) {
-			Some(value) => match VariableRule::validate_value(&parser.colors(), value.as_str()) {
+			Some(value) => match VariableRule::validate_value(value.as_str()) {
 				Ok(var_value) => var_value,
 				Err(msg) => {
 					result.push(
diff --git a/src/parser/langparser.rs b/src/parser/langparser.rs
index 0cc569d..3b70bcf 100644
--- a/src/parser/langparser.rs
+++ b/src/parser/langparser.rs
@@ -1,3 +1,4 @@
+use std::cell::Ref;
 use std::cell::RefCell;
 use std::cell::RefMut;
 use std::collections::HashMap;
@@ -15,6 +16,8 @@ use crate::document::element::DocumentEnd;
 use crate::document::element::ElemKind;
 use crate::document::element::Element;
 use crate::document::langdocument::LangDocument;
+use crate::document::style::ElementStyle;
+use crate::document::style::StyleHolder;
 use crate::elements::paragraph::Paragraph;
 use crate::elements::registrar::register;
 use crate::elements::text::Text;
@@ -42,6 +45,7 @@ pub struct LangParser {
 	pub err_flag: RefCell<bool>,
 	pub state: RefCell<StateHolder>,
 	pub kernels: RefCell<HashMap<String, Kernel>>,
+	pub styles: RefCell<HashMap<String, Rc<dyn ElementStyle>>>,
 }
 
 impl LangParser {
@@ -52,12 +56,21 @@ impl LangParser {
 			err_flag: RefCell::new(false),
 			state: RefCell::new(StateHolder::new()),
 			kernels: RefCell::new(HashMap::new()),
+			styles: RefCell::new(HashMap::new()),
 		};
+		// Register rules
 		register(&mut s);
 
+
+		// Register default kernel
 		s.kernels
 			.borrow_mut()
 			.insert("main".to_string(), Kernel::new(&s));
+
+		// Register default styles
+		for rule in &s.rules {
+			rule.register_styles(&s);
+		}
 		s
 	}
 
@@ -292,3 +305,11 @@ impl KernelHolder for LangParser {
 		self.get_kernel(name.as_str()).unwrap()
 	}
 }
+
+impl StyleHolder for LangParser {
+	fn styles(&self) -> Ref<'_, HashMap<String, Rc<dyn ElementStyle>>> { self.styles.borrow() }
+
+	fn styles_mut(&self) -> RefMut<'_, HashMap<String, Rc<dyn ElementStyle>>> {
+		self.styles.borrow_mut()
+	}
+}
diff --git a/src/parser/parser.rs b/src/parser/parser.rs
index f41bf31..4d28018 100644
--- a/src/parser/parser.rs
+++ b/src/parser/parser.rs
@@ -10,6 +10,7 @@ use super::source::Source;
 use super::state::StateHolder;
 use crate::document::document::Document;
 use crate::document::element::Element;
+use crate::document::style::StyleHolder;
 use crate::lua::kernel::KernelHolder;
 use ariadne::Color;
 
@@ -41,7 +42,7 @@ impl ReportColors {
 	}
 }
 
-pub trait Parser: KernelHolder {
+pub trait Parser: KernelHolder + StyleHolder {
 	/// Gets the colors for formatting errors
 	///
 	/// When colors are disabled, all colors should resolve to empty string
diff --git a/src/parser/rule.rs b/src/parser/rule.rs
index cdb7c5a..55be741 100644
--- a/src/parser/rule.rs
+++ b/src/parser/rule.rs
@@ -25,7 +25,10 @@ pub trait Rule {
 		match_data: Option<Box<dyn Any>>,
 	) -> (Cursor, Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>>);
 	/// Export bindings to lua
-	fn lua_bindings<'lua>(&self, _lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>>;
+	fn lua_bindings<'lua>(&self, _lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>> { None }
+
+	/// Registers default styles
+	fn register_styles(&self, _parser: &dyn Parser) {}
 }
 
 impl core::fmt::Debug for dyn Rule {
@@ -89,7 +92,8 @@ pub trait RegexRule {
 		matches: regex::Captures,
 	) -> Vec<Report<'_, (Rc<dyn Source>, Range<usize>)>>;
 
-	fn lua_bindings<'lua>(&self, _lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>>;
+	fn lua_bindings<'lua>(&self, _lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>> { None }
+	fn register_styles(&self, _parser: &dyn Parser) {}
 }
 
 impl<T: RegexRule> Rule for T {
@@ -147,4 +151,8 @@ impl<T: RegexRule> Rule for T {
 	fn lua_bindings<'lua>(&self, lua: &'lua Lua) -> Option<Vec<(String, Function<'lua>)>> {
 		self.lua_bindings(lua)
 	}
+
+	fn register_styles(&self, parser: &dyn Parser) {
+		self.register_styles(parser);
+	}
 }