From ffd1903a6588f9cc4b2716353826f6aea693d739 Mon Sep 17 00:00:00 2001 From: ef3d0c3e Date: Mon, 26 Aug 2024 10:59:15 +0200 Subject: [PATCH] Reference styling --- src/compiler/compiler.rs | 79 +++++++++++++++++++++---- src/compiler/navigation.rs | 12 ++-- src/elements/blockquote.rs | 89 +++++++++++----------------- src/elements/reference.rs | 118 ++++++++++++++++++++++++++++++++++--- 4 files changed, 221 insertions(+), 77 deletions(-) diff --git a/src/compiler/compiler.rs b/src/compiler/compiler.rs index 6e6b11e..678e3f1 100644 --- a/src/compiler/compiler.rs +++ b/src/compiler/compiler.rs @@ -77,6 +77,51 @@ impl<'a> Compiler<'a> { } } + /// Sanitizes a format string for a [`Target`] + /// + /// # Notes + /// + /// This function may process invalid format string, which will be caught later + /// by runtime_format. + pub fn sanitize_format>(target: Target, str: S) -> String { + match target { + Target::HTML => { + let mut out = String::new(); + + let mut braces = 0; + for c in str.as_ref().chars() { + if c == '{' { + out.push(c); + braces += 1; + continue; + } else if c == '}' { + out.push(c); + if braces != 0 { + braces -= 1; + } + continue; + } + // Inside format args + if braces % 2 == 1 { + out.push(c); + continue; + } + + match c { + '&' => out += "&", + '<' => out += "<", + '>' => out += ">", + '"' => out += """, + _ => out.push(c), + } + } + + out + } + _ => todo!("Sanitize not implemented"), + } + } + /// Gets a reference name pub fn refname>(target: Target, str: S) -> String { Self::sanitize(target, str).replace(' ', "_") @@ -131,15 +176,13 @@ impl<'a> Compiler<'a> { document: &dyn Document, var_name: &'static str, ) -> Option> { - document - .get_variable(var_name) - .or_else(|| { - println!( - "Missing variable `{var_name}` in {}", - document.source().name() - ); - None - }) + document.get_variable(var_name).or_else(|| { + println!( + "Missing variable `{var_name}` in {}", + document.source().name() + ); + None + }) } let mut result = String::new(); @@ -308,7 +351,7 @@ impl CompiledDocument { .ok() } - /// Inserts [`CompiledDocument`] into cache + /// Interts [`CompiledDocument`] into cache pub fn insert_cache(&self, con: &Connection) -> Result { con.execute( Self::sql_insert_query(), @@ -324,3 +367,19 @@ impl CompiledDocument { ) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sanitize_test() { + assert_eq!(Compiler::sanitize(Target::HTML, ""), "<a>"); + assert_eq!(Compiler::sanitize(Target::HTML, "<"), "&lt;"); + assert_eq!(Compiler::sanitize(Target::HTML, "\""), """); + + assert_eq!(Compiler::sanitize_format(Target::HTML, "{<>&\"}"), "{<>&\"}"); + assert_eq!(Compiler::sanitize_format(Target::HTML, "{{<>}}"), "{{<>}}"); + assert_eq!(Compiler::sanitize_format(Target::HTML, "{{<"), "{{<"); + } +} diff --git a/src/compiler/navigation.rs b/src/compiler/navigation.rs index 4818f0b..840cd2f 100644 --- a/src/compiler/navigation.rs +++ b/src/compiler/navigation.rs @@ -231,7 +231,7 @@ pub fn create_navigation( // Sort entries fn sort_entries(nav: &mut NavEntries) { - let mut entrymap = nav + let entrymap = nav .entries .iter() .map(|ent| (ent.title.clone(), ent.previous.clone())) @@ -250,8 +250,8 @@ pub fn create_navigation( #[cfg(test)] mod tests { + use rand::prelude::SliceRandom; use rand::rngs::OsRng; - use rand::RngCore; use crate::compiler::process::process_from_memory; @@ -288,12 +288,10 @@ mod tests { ]; let mut shuffled = entries.clone(); for _ in 0..10 { - for i in 0..5 { - let pos = OsRng.next_u64() % entries.len() as u64; - shuffled.swap(i, pos as usize); - } + let mut rng = OsRng {}; + shuffled.shuffle(&mut rng); - let mut entrymap = shuffled + let entrymap = shuffled .iter() .map(|ent| (ent.title.clone(), ent.previous.clone())) .collect::>>(); diff --git a/src/elements/blockquote.rs b/src/elements/blockquote.rs index fbd40b5..2e4b226 100644 --- a/src/elements/blockquote.rs +++ b/src/elements/blockquote.rs @@ -87,33 +87,29 @@ impl Element for Blockquote { match compiler.target() { HTML => { let mut result = r#"
"#.to_string(); - let format_author = || -> Result { + let format_author = || -> Result { let mut result = String::new(); if self.cite.is_some() || self.author.is_some() { result += r#"

"#; let fmt_pair = FmtPair(compiler.target(), self); - match (self.author.is_some(), self.cite.is_some()) { + let format_string = match (self.author.is_some(), self.cite.is_some()) { (true, true) => { - let args = - FormatArgs::new(self.style.format[0].as_str(), &fmt_pair); - args.status()?; - result += args.to_string().as_str(); + Compiler::sanitize_format(fmt_pair.0, self.style.format[0].as_str()) } (true, false) => { - let args = - FormatArgs::new(self.style.format[1].as_str(), &fmt_pair); - args.status()?; - result += args.to_string().as_str(); + Compiler::sanitize_format(fmt_pair.0, self.style.format[1].as_str()) } (false, false) => { - let args = - FormatArgs::new(self.style.format[2].as_str(), &fmt_pair); - args.status()?; - result += args.to_string().as_str(); + Compiler::sanitize_format(fmt_pair.0, self.style.format[2].as_str()) } _ => panic!(""), - } + }; + let args = FormatArgs::new(format_string.as_str(), &fmt_pair); + args.status().map_err(|err| { + format!("Failed to format Blockquote style `{format_string}`: {err}") + })?; + result += args.to_string().as_str(); result += "

"; } Ok(result) @@ -126,25 +122,21 @@ impl Element for Blockquote { result += "
"; } if self.style.author_pos == Before { - result += format_author().map_err(|err| err.to_string())?.as_str(); + result += format_author()?.as_str(); } let mut in_paragraph = false; for elem in &self.content { - if elem.downcast_ref::().is_some() {} - else if elem.downcast_ref::
().is_some() - { - if in_paragraph - { + if elem.downcast_ref::().is_some() { + } else if elem.downcast_ref::
().is_some() { + if in_paragraph { result += "

"; in_paragraph = false; } result += elem .compile(compiler, document, cursor + result.len())? .as_str(); - } - else - { + } else { if !in_paragraph { result += "

"; in_paragraph = true; @@ -154,8 +146,7 @@ impl Element for Blockquote { .as_str(); } } - if in_paragraph - { + if in_paragraph { result += "

"; } result += "
"; @@ -299,7 +290,6 @@ impl Rule for BlockquoteRule { // Content let entry_start = captures.get(0).unwrap().start(); let mut entry_content = captures.get(2).unwrap().as_str().to_string(); - println!("f={entry_content}"); while let Some(captures) = self.continue_re.captures_at(content, end_cursor.pos) { if captures.get(0).unwrap().start() != end_cursor.pos { break; @@ -308,17 +298,12 @@ impl Rule for BlockquoteRule { end_cursor = end_cursor.at(captures.get(0).unwrap().end()); let trimmed = captures.get(1).unwrap().as_str().trim_start().trim_end(); - println!("tr={trimmed}"); - //if !trimmed.is_empty() - { - entry_content += "\n"; - entry_content += trimmed; - } + entry_content += "\n"; + entry_content += trimmed; } // Parse entry content let token = Token::new(entry_start..end_cursor.pos, end_cursor.source.clone()); - println!("{entry_content}."); let entry_src = Rc::new(VirtualSource::new( token.clone(), "Blockquote Entry".to_string(), @@ -326,33 +311,31 @@ impl Rule for BlockquoteRule { )); // Parse content let parsed_doc = state.with_state(|new_state| { - new_state.parser.parse(new_state, entry_src, Some(document)).0 + new_state + .parser + .parse(new_state, entry_src, Some(document)) + .0 }); - + // Extract paragraph and nested blockquotes - let mut parsed_content : Vec> = vec![]; - for mut elem in parsed_doc.content().borrow_mut().drain(..) - { - if let Some(paragraph) = elem.downcast_mut::() - { - if let Some(last) = parsed_content.last() - { - if last.kind() == ElemKind::Inline - { + let mut parsed_content: Vec> = vec![]; + for mut elem in parsed_doc.content().borrow_mut().drain(..) { + if let Some(paragraph) = elem.downcast_mut::() { + if let Some(last) = parsed_content.last() { + if last.kind() == ElemKind::Inline { parsed_content.push(Box::new(Text { - location: Token::new(last.location().end()..last.location().end(), last.location().source()), - content: " ".to_string() + location: Token::new( + last.location().end()..last.location().end(), + last.location().source(), + ), + content: " ".to_string(), }) as Box); } } parsed_content.extend(std::mem::take(&mut paragraph.content)); - } - else if elem.downcast_ref::
().is_some() - { + } else if elem.downcast_ref::
().is_some() { parsed_content.push(elem); - } - else - { + } else { reports.push( Report::build(ReportKind::Error, token.source(), token.range.start) .with_message("Unable to Parse Blockquote Entry") diff --git a/src/elements/reference.rs b/src/elements/reference.rs index fb06fe4..332dd11 100644 --- a/src/elements/reference.rs +++ b/src/elements/reference.rs @@ -5,9 +5,13 @@ use std::rc::Rc; use ariadne::Label; use ariadne::Report; use ariadne::ReportKind; +use reference_style::ExternalReferenceStyle; use regex::Captures; use regex::Match; use regex::Regex; +use runtime_format::FormatArgs; +use runtime_format::FormatKey; +use runtime_format::FormatKeyError; use crate::compiler::compiler::Compiler; use crate::compiler::compiler::Target; @@ -21,6 +25,7 @@ use crate::parser::parser::ReportColors; use crate::parser::rule::RegexRule; use crate::parser::source::Source; use crate::parser::source::Token; +use crate::parser::style::StyleHolder; use crate::parser::util; use crate::parser::util::Property; use crate::parser::util::PropertyMap; @@ -52,7 +57,12 @@ impl Element for InternalReference { ) -> Result { match compiler.target() { Target::HTML => { - let elemref = document.get_reference(self.refname.as_str()).ok_or(format!("Unable to find reference `{}` in current document", self.refname))?; + let elemref = document + .get_reference(self.refname.as_str()) + .ok_or(format!( + "Unable to find reference `{}` in current document", + self.refname + ))?; let elem = document.get_from_reference(&elemref).unwrap(); elem.compile_reference( @@ -72,6 +82,29 @@ pub struct ExternalReference { pub(self) location: Token, pub(self) reference: CrossReference, pub(self) caption: Option, + pub(self) style: Rc, +} + +struct FmtPair<'a>(Target, &'a ExternalReference); + +impl FormatKey for FmtPair<'_> { + fn fmt(&self, key: &str, f: &mut std::fmt::Formatter<'_>) -> Result<(), FormatKeyError> { + match &self.1.reference { + CrossReference::Unspecific(refname) => match key { + "refname" => write!(f, "{}", Compiler::sanitize(self.0, refname)) + .map_err(FormatKeyError::Fmt), + _ => Err(FormatKeyError::UnknownKey), + }, + CrossReference::Specific(refdoc, refname) => match key { + "refdoc" => { + write!(f, "{}", Compiler::sanitize(self.0, refdoc)).map_err(FormatKeyError::Fmt) + } + "refname" => write!(f, "{}", Compiler::sanitize(self.0, refname)) + .map_err(FormatKeyError::Fmt), + _ => Err(FormatKeyError::UnknownKey), + }, + } + } } impl Element for ExternalReference { @@ -91,14 +124,34 @@ impl Element for ExternalReference { Target::HTML => { let mut result = "{}", Compiler::sanitize(Target::HTML, caption)).as_str(); } else { - result += format!("\">{}", self.reference).as_str(); + // Use style + let fmt_pair = FmtPair(compiler.target(), self); + let format_string = match &self.reference { + CrossReference::Unspecific(_) => Compiler::sanitize_format( + fmt_pair.0, + self.style.format_unspecific.as_str(), + ), + CrossReference::Specific(_, _) => Compiler::sanitize_format( + fmt_pair.0, + self.style.format_specific.as_str(), + ), + }; + let args = FormatArgs::new(format_string.as_str(), &fmt_pair); + args.status().map_err(|err| { + format!("Failed to format ExternalReference style `{format_string}`: {err}") + })?; + + result += format!("\">{}", args.to_string()).as_str(); } + // Add crossreference + compiler.insert_crossreference(crossreference_pos, self.reference.clone()); Ok(result) } _ => todo!(""), @@ -223,7 +276,7 @@ impl RegexRule for ReferenceRule { ); return reports; } - Ok(refname) => (None, refname.to_string()) + Ok(refname) => (None, refname.to_string()), } } } else { @@ -244,9 +297,20 @@ impl RegexRule for ReferenceRule { .get("caption", |_, value| -> Result { Ok(value.clone()) }) - .ok().map(|(_, s)| s); + .ok() + .map(|(_, s)| s); if let Some(refdoc) = refdoc { + // Get style + let style = state + .shared + .styles + .borrow() + .current(reference_style::STYLE_KEY) + .downcast_rc::() + .unwrap(); + + // §{#refname} if refdoc.is_empty() { state.push( document, @@ -254,8 +318,10 @@ impl RegexRule for ReferenceRule { location: token, reference: CrossReference::Unspecific(refname), caption, + style, }), ); + // §{docname#refname} } else { state.push( document, @@ -263,6 +329,7 @@ impl RegexRule for ReferenceRule { location: token, reference: CrossReference::Specific(refdoc, refname), caption, + style, }), ); } @@ -279,6 +346,36 @@ impl RegexRule for ReferenceRule { reports } + + fn register_styles(&self, holder: &mut StyleHolder) { + holder.set_current(Rc::new(ExternalReferenceStyle::default())); + } +} + +mod reference_style { + use serde::Deserialize; + use serde::Serialize; + + use crate::impl_elementstyle; + + pub static STYLE_KEY: &str = "style.external_reference"; + + #[derive(Debug, Serialize, Deserialize)] + pub struct ExternalReferenceStyle { + pub format_unspecific: String, + pub format_specific: String, + } + + impl Default for ExternalReferenceStyle { + fn default() -> Self { + Self { + format_unspecific: "(#{refname})".into(), + format_specific: "({refdoc}#{refname})".into(), + } + } + } + + impl_elementstyle!(ExternalReferenceStyle, STYLE_KEY); } #[cfg(test)] @@ -372,15 +469,22 @@ mod tests { r#" @html.page_title = 2 +@@style.external_reference = { + "format_unspecific": "[UNSPECIFIC {refname}]", + "format_specific": "[SPECIFIC {refdoc}:{refname}]" +} + §{#ref}[caption=from 0] +§{#ref} §{#ref2}[caption=from 1] +§{b#ref2} "# .into(), ], ) .unwrap(); - assert!(result[1].0.borrow().body.starts_with("

#refa#ref

")); - assert!(result[2].0.borrow().body.starts_with("

from 0from 1

")); + assert!(result[1].0.borrow().body.starts_with("

(#ref)(a#ref)

")); + assert!(result[2].0.borrow().body.starts_with("