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);
+ }
}