From e5ba622e0d721a630e29c204d0c459b487cf0463 Mon Sep 17 00:00:00 2001 From: ef3d0c3e Date: Fri, 26 Jul 2024 15:06:09 +0200 Subject: [PATCH] References for images --- src/document/element.rs | 4 + src/document/references.rs | 50 ++++++++- src/elements/media.rs | 32 +++++- src/elements/mod.rs | 1 + src/elements/reference.rs | 211 +++++++++++++++++++++++++++++++++++++ src/elements/registrar.rs | 2 + src/elements/section.rs | 10 ++ src/main.rs | 3 +- style.css | 27 ++++- 9 files changed, 332 insertions(+), 8 deletions(-) create mode 100644 src/elements/reference.rs diff --git a/src/document/element.rs b/src/document/element.rs index d51c0c8..cde0615 100644 --- a/src/document/element.rs +++ b/src/document/element.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use crate::compiler::compiler::Compiler; +use crate::elements::reference::Reference; use crate::parser::source::Token; use downcast_rs::impl_downcast; use downcast_rs::Downcast; @@ -68,6 +69,9 @@ pub trait ReferenceableElement: Element { /// Key for refcounting fn refcount_key(&self) -> &'static str; + + /// Creates the reference element + fn compile_reference(&self, compiler: &Compiler, document: &dyn Document, reference: &Reference, refid: usize) -> Result; } impl core::fmt::Debug for dyn ReferenceableElement { diff --git a/src/document/references.rs b/src/document/references.rs index b9f0b8b..6ce161a 100644 --- a/src/document/references.rs +++ b/src/document/references.rs @@ -1,11 +1,17 @@ -pub fn validate_refname(name: &str) -> Result<&str, String> { +use super::document::Document; + +pub fn validate_refname<'a>( + document: &dyn Document, + name: &'a str, + check_duplicate: bool, +) -> Result<&'a str, String> { let trimmed = name.trim_start().trim_end(); if trimmed.is_empty() { return Err("Refname cannot be empty".to_string()); } for c in trimmed.chars() { - if c.is_ascii_punctuation() { + if c.is_ascii_punctuation() && !(c == '.' || c == '_') { return Err(format!( "Refname `{trimmed}` cannot contain punctuation codepoint: `{c}`" )); @@ -24,5 +30,43 @@ pub fn validate_refname(name: &str) -> Result<&str, String> { } } - Ok(trimmed) + if check_duplicate && document.get_reference(trimmed).is_some() { + Err(format!("Refname `{trimmed}` is already in use!")) + } else { + Ok(trimmed) + } +} + +#[cfg(test)] +pub mod tests { + use std::rc::Rc; + + use super::*; + use crate::parser::langparser::LangParser; + use crate::parser::parser::Parser; + use crate::parser::source::SourceFile; + + #[test] + fn validate_refname_tests() { + let source = Rc::new(SourceFile::with_content( + "".to_string(), + "#{ref} Section".to_string(), + None, + )); + let parser = LangParser::default(); + let doc = parser.parse(source, None); + + assert_eq!(validate_refname(&*doc, " abc ", true), Ok("abc")); + assert_eq!( + validate_refname(&*doc, " Some_reference ", true), + Ok("Some_reference") + ); + assert!(validate_refname(&*doc, "", true).is_err()); + assert!(validate_refname(&*doc, "\n", true).is_err()); + assert!(validate_refname(&*doc, "'", true).is_err()); + assert!(validate_refname(&*doc, "]", true).is_err()); + + // Duplicate + assert!(validate_refname(&*doc, "ref", true).is_err()); + } } diff --git a/src/elements/media.rs b/src/elements/media.rs index 1923cb4..ece4fe0 100644 --- a/src/elements/media.rs +++ b/src/elements/media.rs @@ -21,6 +21,7 @@ use crate::document::element::ElemKind; use crate::document::element::Element; use crate::document::element::ReferenceableElement; use crate::document::references::validate_refname; +use crate::parser::parser::Parser; use crate::parser::parser::ReportColors; use crate::parser::rule::RegexRule; use crate::parser::source::Source; @@ -34,6 +35,7 @@ use crate::parser::util::PropertyMapError; use crate::parser::util::PropertyParser; use super::paragraph::Paragraph; +use super::reference::Reference; #[derive(Debug)] pub enum MediaType { @@ -186,6 +188,32 @@ impl ReferenceableElement for Medium { fn reference_name(&self) -> Option<&String> { Some(&self.reference) } fn refcount_key(&self) -> &'static str { "medium" } + + fn compile_reference( + &self, + compiler: &Compiler, + _document: &dyn Document, + reference: &Reference, + refid: usize, + ) -> Result { + match compiler.target() { + Target::HTML => { + let caption = reference + .caption() + .map_or(format!("({refid})"), |cap| cap.clone()); + + // TODO Handle other kind of media + match self.media_type { + MediaType::IMAGE => Ok(format!( + r#"{caption}"#, + self.uri + )), + _ => todo!(""), + } + } + _ => todo!(""), + } + } } pub struct MediaRule { @@ -296,7 +324,7 @@ impl RegexRule for MediaRule { fn on_regex_match<'a>( &self, _: usize, - parser: &dyn crate::parser::parser::Parser, + parser: &dyn Parser, document: &'a (dyn Document<'a> + 'a), token: Token, matches: Captures, @@ -305,7 +333,7 @@ impl RegexRule for MediaRule { let refname = match ( matches.get(1).unwrap(), - validate_refname(matches.get(1).unwrap().as_str()), + validate_refname(document, matches.get(1).unwrap().as_str(), true), ) { (_, Ok(refname)) => refname.to_string(), (m, Err(err)) => { diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 8389ac4..75a200d 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -14,3 +14,4 @@ pub mod tex; pub mod graphviz; pub mod raw; pub mod media; +pub mod reference; diff --git a/src/elements/reference.rs b/src/elements/reference.rs new file mode 100644 index 0000000..3d4afe1 --- /dev/null +++ b/src/elements/reference.rs @@ -0,0 +1,211 @@ +use std::collections::HashMap; +use std::ops::Range; +use std::rc::Rc; + +use ariadne::Fmt; +use ariadne::Label; +use ariadne::Report; +use ariadne::ReportKind; +use regex::Captures; +use regex::Match; +use regex::Regex; + +use crate::compiler::compiler::Compiler; +use crate::compiler::compiler::Target; +use crate::document::document::Document; +use crate::document::element::ElemKind; +use crate::document::element::Element; +use crate::document::references::validate_refname; +use crate::parser::parser::Parser; +use crate::parser::parser::ReportColors; +use crate::parser::rule::RegexRule; +use crate::parser::source::Source; +use crate::parser::source::Token; +use crate::parser::util; +use crate::parser::util::Property; +use crate::parser::util::PropertyMap; +use crate::parser::util::PropertyParser; + +#[derive(Debug)] +pub struct Reference { + pub(self) location: Token, + pub(self) refname: String, + pub(self) caption: Option, +} + +impl Reference { + pub fn caption(&self) -> Option<&String> { self.caption.as_ref() } +} + +impl Element for Reference { + fn location(&self) -> &Token { &self.location } + + fn kind(&self) -> ElemKind { ElemKind::Inline } + + fn element_name(&self) -> &'static str { "Reference" } + + fn to_string(&self) -> String { format!("{self:#?}") } + + fn compile(&self, compiler: &Compiler, document: &dyn Document) -> Result { + match compiler.target() { + Target::HTML => { + let elemref = document.get_reference(self.refname.as_str()).unwrap(); + let elem = document.get_from_reference(&elemref).unwrap(); + + elem.compile_reference( + compiler, + document, + self, + compiler.reference_id(document, elemref), + ) + } + _ => todo!(""), + } + } +} + +pub struct ReferenceRule { + re: [Regex; 1], + properties: PropertyParser, +} + +impl ReferenceRule { + pub fn new() -> Self { + let mut props = HashMap::new(); + props.insert( + "caption".to_string(), + Property::new( + false, + "Override the display of the reference".to_string(), + None, + ), + ); + Self { + re: [Regex::new(r"ยง\{(.*)\}(\[((?:\\.|[^\\\\])*?)\])?").unwrap()], + properties: PropertyParser::new(props), + } + } + + fn parse_properties( + &self, + colors: &ReportColors, + token: &Token, + m: &Option, + ) -> Result, Range)>> { + match m { + None => match self.properties.default() { + Ok(properties) => Ok(properties), + Err(e) => Err( + Report::build(ReportKind::Error, token.source(), token.start()) + .with_message("Invalid Media Properties") + .with_label( + Label::new((token.source().clone(), token.range.clone())) + .with_message(format!("Media is missing required property: {e}")) + .with_color(colors.error), + ) + .finish(), + ), + }, + Some(props) => { + let processed = + util::process_escaped('\\', "]", props.as_str().trim_start().trim_end()); + match self.properties.parse(processed.as_str()) { + Err(e) => Err( + Report::build(ReportKind::Error, token.source(), props.start()) + .with_message("Invalid Media Properties") + .with_label( + Label::new((token.source().clone(), props.range())) + .with_message(e) + .with_color(colors.error), + ) + .finish(), + ), + Ok(properties) => Ok(properties), + } + } + } + } +} + +impl RegexRule for ReferenceRule { + fn name(&self) -> &'static str { "Reference" } + + fn regexes(&self) -> &[regex::Regex] { &self.re } + + fn on_regex_match<'a>( + &self, + _: usize, + parser: &dyn Parser, + document: &'a (dyn Document<'a> + 'a), + token: Token, + matches: Captures, + ) -> Vec, Range)>> { + let mut reports = vec![]; + + let refname = match ( + matches.get(1).unwrap(), + validate_refname(document, matches.get(1).unwrap().as_str(), false), + ) { + (m, Ok(refname)) => { + if document.get_reference(refname).is_none() { + reports.push( + Report::build(ReportKind::Error, token.source(), m.start()) + .with_message("Uknown Reference Refname") + .with_label( + Label::new((token.source().clone(), m.range())).with_message( + format!( + "Could not find element with reference: `{}`", + refname.fg(parser.colors().info) + ), + ), + ) + .finish(), + ); + return reports; + } + refname.to_string() + } + (m, Err(err)) => { + reports.push( + Report::build(ReportKind::Error, token.source(), m.start()) + .with_message("Invalid Reference Refname") + .with_label( + Label::new((token.source().clone(), m.range())).with_message(err), + ) + .finish(), + ); + return reports; + } + }; + // Properties + let properties = match self.parse_properties(parser.colors(), &token, &matches.get(3)) { + Ok(pm) => pm, + Err(report) => { + reports.push(report); + return reports; + } + }; + + let caption = properties + .get("caption", |_, value| -> Result { + Ok(value.clone()) + }) + .ok() + .and_then(|(_, s)| Some(s)); + + parser.push( + document, + Box::new(Reference { + location: token, + refname, + caption, + }), + ); + + reports + } + + fn lua_bindings<'lua>(&self, _lua: &'lua mlua::Lua) -> Vec<(String, mlua::Function<'lua>)> { + vec![] + } +} diff --git a/src/elements/registrar.rs b/src/elements/registrar.rs index 9e14258..7e74512 100644 --- a/src/elements/registrar.rs +++ b/src/elements/registrar.rs @@ -16,6 +16,7 @@ use super::tex::TexRule; use super::text::TextRule; use super::variable::VariableRule; use super::variable::VariableSubstitutionRule; +use super::reference::ReferenceRule; pub fn register(parser: &mut P) { parser.add_rule(Box::new(CommentRule::new()), None).unwrap(); @@ -35,4 +36,5 @@ pub fn register(parser: &mut P) { parser.add_rule(Box::new(SectionRule::new()), None).unwrap(); parser.add_rule(Box::new(LinkRule::new()), None).unwrap(); parser.add_rule(Box::new(TextRule::default()), None).unwrap(); + parser.add_rule(Box::new(ReferenceRule::new()), None).unwrap(); } diff --git a/src/elements/section.rs b/src/elements/section.rs index 3b9dbe3..7638610 100644 --- a/src/elements/section.rs +++ b/src/elements/section.rs @@ -52,6 +52,16 @@ impl ReferenceableElement for Section { fn reference_name(&self) -> Option<&String> { self.reference.as_ref() } fn refcount_key(&self) -> &'static str { "section" } + + fn compile_reference( + &self, + compiler: &Compiler, + document: &dyn Document, + reference: &super::reference::Reference, + refid: usize, + ) -> Result { + todo!() + } } pub struct SectionRule { diff --git a/src/main.rs b/src/main.rs index 77295e3..badbc34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,8 +85,7 @@ fn main() { println!("-- END AST DEBUGGING --"); } - if debug_opts.contains(&"ref".to_string()) - { + if debug_opts.contains(&"ref".to_string()) { println!("-- BEGIN REFERENCES DEBUGGING --"); let sc = doc.scope().borrow(); sc.referenceable.iter().for_each(|(name, reference)| { diff --git a/style.css b/style.css index e962481..f62fb69 100644 --- a/style.css +++ b/style.css @@ -97,7 +97,7 @@ div.medium p.medium-refname { text-align: center; font-weight: bold; - color: #9424af; + color: #d367c1; } div.medium p { @@ -108,3 +108,28 @@ div.medium p { text-align: justify; } + +a.medium-ref { + display: inline; + + font-weight: bold; + color: #d367c1; +} + +a.medium-ref:hover { + background: #334; +} + +a.medium-ref img { + display: none; + margin: 1.3em 0 0 0; +} + +a:hover.medium-ref img { + max-width: 50%; + margin: auto; + display: inline-block; + position: absolute; + + box-shadow: 0px 0px 6px 2px rgba(0,0,0,0.75); +}