References for images

This commit is contained in:
ef3d0c3e 2024-07-26 15:06:09 +02:00
parent c2cfd3f72c
commit e5ba622e0d
9 changed files with 332 additions and 8 deletions

View file

@ -1,6 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use crate::compiler::compiler::Compiler; use crate::compiler::compiler::Compiler;
use crate::elements::reference::Reference;
use crate::parser::source::Token; use crate::parser::source::Token;
use downcast_rs::impl_downcast; use downcast_rs::impl_downcast;
use downcast_rs::Downcast; use downcast_rs::Downcast;
@ -68,6 +69,9 @@ pub trait ReferenceableElement: Element {
/// Key for refcounting /// Key for refcounting
fn refcount_key(&self) -> &'static str; 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>;
} }
impl core::fmt::Debug for dyn ReferenceableElement { impl core::fmt::Debug for dyn ReferenceableElement {

View file

@ -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(); let trimmed = name.trim_start().trim_end();
if trimmed.is_empty() { if trimmed.is_empty() {
return Err("Refname cannot be empty".to_string()); return Err("Refname cannot be empty".to_string());
} }
for c in trimmed.chars() { for c in trimmed.chars() {
if c.is_ascii_punctuation() { if c.is_ascii_punctuation() && !(c == '.' || c == '_') {
return Err(format!( return Err(format!(
"Refname `{trimmed}` cannot contain punctuation codepoint: `{c}`" "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());
}
} }

View file

@ -21,6 +21,7 @@ use crate::document::element::ElemKind;
use crate::document::element::Element; use crate::document::element::Element;
use crate::document::element::ReferenceableElement; use crate::document::element::ReferenceableElement;
use crate::document::references::validate_refname; use crate::document::references::validate_refname;
use crate::parser::parser::Parser;
use crate::parser::parser::ReportColors; use crate::parser::parser::ReportColors;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Source; use crate::parser::source::Source;
@ -34,6 +35,7 @@ use crate::parser::util::PropertyMapError;
use crate::parser::util::PropertyParser; use crate::parser::util::PropertyParser;
use super::paragraph::Paragraph; use super::paragraph::Paragraph;
use super::reference::Reference;
#[derive(Debug)] #[derive(Debug)]
pub enum MediaType { pub enum MediaType {
@ -186,6 +188,32 @@ impl ReferenceableElement for Medium {
fn reference_name(&self) -> Option<&String> { Some(&self.reference) } fn reference_name(&self) -> Option<&String> { Some(&self.reference) }
fn refcount_key(&self) -> &'static str { "medium" } fn refcount_key(&self) -> &'static str { "medium" }
fn compile_reference(
&self,
compiler: &Compiler,
_document: &dyn Document,
reference: &Reference,
refid: usize,
) -> Result<String, String> {
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#"<a class="medium-ref">{caption}<img src="{}"></a>"#,
self.uri
)),
_ => todo!(""),
}
}
_ => todo!(""),
}
}
} }
pub struct MediaRule { pub struct MediaRule {
@ -296,7 +324,7 @@ impl RegexRule for MediaRule {
fn on_regex_match<'a>( fn on_regex_match<'a>(
&self, &self,
_: usize, _: usize,
parser: &dyn crate::parser::parser::Parser, parser: &dyn Parser,
document: &'a (dyn Document<'a> + 'a), document: &'a (dyn Document<'a> + 'a),
token: Token, token: Token,
matches: Captures, matches: Captures,
@ -305,7 +333,7 @@ impl RegexRule for MediaRule {
let refname = match ( let refname = match (
matches.get(1).unwrap(), 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(), (_, Ok(refname)) => refname.to_string(),
(m, Err(err)) => { (m, Err(err)) => {

View file

@ -14,3 +14,4 @@ pub mod tex;
pub mod graphviz; pub mod graphviz;
pub mod raw; pub mod raw;
pub mod media; pub mod media;
pub mod reference;

211
src/elements/reference.rs Normal file
View file

@ -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<String>,
}
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<String, String> {
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<Match>,
) -> Result<PropertyMap, Report<'_, (Rc<dyn Source>, Range<usize>)>> {
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<Report<'_, (Rc<dyn Source>, Range<usize>)>> {
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<String, ()> {
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![]
}
}

View file

@ -16,6 +16,7 @@ use super::tex::TexRule;
use super::text::TextRule; use super::text::TextRule;
use super::variable::VariableRule; use super::variable::VariableRule;
use super::variable::VariableSubstitutionRule; use super::variable::VariableSubstitutionRule;
use super::reference::ReferenceRule;
pub fn register<P: Parser>(parser: &mut P) { pub fn register<P: Parser>(parser: &mut P) {
parser.add_rule(Box::new(CommentRule::new()), None).unwrap(); parser.add_rule(Box::new(CommentRule::new()), None).unwrap();
@ -35,4 +36,5 @@ pub fn register<P: Parser>(parser: &mut P) {
parser.add_rule(Box::new(SectionRule::new()), None).unwrap(); parser.add_rule(Box::new(SectionRule::new()), None).unwrap();
parser.add_rule(Box::new(LinkRule::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(TextRule::default()), None).unwrap();
parser.add_rule(Box::new(ReferenceRule::new()), None).unwrap();
} }

View file

@ -52,6 +52,16 @@ impl ReferenceableElement for Section {
fn reference_name(&self) -> Option<&String> { self.reference.as_ref() } fn reference_name(&self) -> Option<&String> { self.reference.as_ref() }
fn refcount_key(&self) -> &'static str { "section" } fn refcount_key(&self) -> &'static str { "section" }
fn compile_reference(
&self,
compiler: &Compiler,
document: &dyn Document,
reference: &super::reference::Reference,
refid: usize,
) -> Result<String, String> {
todo!()
}
} }
pub struct SectionRule { pub struct SectionRule {

View file

@ -85,8 +85,7 @@ fn main() {
println!("-- END AST DEBUGGING --"); println!("-- END AST DEBUGGING --");
} }
if debug_opts.contains(&"ref".to_string()) if debug_opts.contains(&"ref".to_string()) {
{
println!("-- BEGIN REFERENCES DEBUGGING --"); println!("-- BEGIN REFERENCES DEBUGGING --");
let sc = doc.scope().borrow(); let sc = doc.scope().borrow();
sc.referenceable.iter().for_each(|(name, reference)| { sc.referenceable.iter().for_each(|(name, reference)| {

View file

@ -97,7 +97,7 @@ div.medium p.medium-refname {
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
color: #9424af; color: #d367c1;
} }
div.medium p { div.medium p {
@ -108,3 +108,28 @@ div.medium p {
text-align: justify; 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);
}