Reference styling

This commit is contained in:
ef3d0c3e 2024-08-26 10:59:15 +02:00
parent ea0a0cf5b1
commit ffd1903a65
4 changed files with 221 additions and 77 deletions

View file

@ -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<S: AsRef<str>>(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 += "&amp;",
'<' => out += "&lt;",
'>' => out += "&gt;",
'"' => out += "&quot;",
_ => out.push(c),
}
}
out
}
_ => todo!("Sanitize not implemented"),
}
}
/// Gets a reference name /// Gets a reference name
pub fn refname<S: AsRef<str>>(target: Target, str: S) -> String { pub fn refname<S: AsRef<str>>(target: Target, str: S) -> String {
Self::sanitize(target, str).replace(' ', "_") Self::sanitize(target, str).replace(' ', "_")
@ -131,9 +176,7 @@ impl<'a> Compiler<'a> {
document: &dyn Document, document: &dyn Document,
var_name: &'static str, var_name: &'static str,
) -> Option<Rc<dyn Variable>> { ) -> Option<Rc<dyn Variable>> {
document document.get_variable(var_name).or_else(|| {
.get_variable(var_name)
.or_else(|| {
println!( println!(
"Missing variable `{var_name}` in {}", "Missing variable `{var_name}` in {}",
document.source().name() document.source().name()
@ -308,7 +351,7 @@ impl CompiledDocument {
.ok() .ok()
} }
/// Inserts [`CompiledDocument`] into cache /// Interts [`CompiledDocument`] into cache
pub fn insert_cache(&self, con: &Connection) -> Result<usize, rusqlite::Error> { pub fn insert_cache(&self, con: &Connection) -> Result<usize, rusqlite::Error> {
con.execute( con.execute(
Self::sql_insert_query(), 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>"), "&lt;a&gt;");
assert_eq!(Compiler::sanitize(Target::HTML, "&lt;"), "&amp;lt;");
assert_eq!(Compiler::sanitize(Target::HTML, "\""), "&quot;");
assert_eq!(Compiler::sanitize_format(Target::HTML, "{<>&\"}"), "{<>&\"}");
assert_eq!(Compiler::sanitize_format(Target::HTML, "{{<>}}"), "{{&lt;&gt;}}");
assert_eq!(Compiler::sanitize_format(Target::HTML, "{{<"), "{{&lt;");
}
}

View file

@ -231,7 +231,7 @@ pub fn create_navigation(
// Sort entries // Sort entries
fn sort_entries(nav: &mut NavEntries) { fn sort_entries(nav: &mut NavEntries) {
let mut entrymap = nav let entrymap = nav
.entries .entries
.iter() .iter()
.map(|ent| (ent.title.clone(), ent.previous.clone())) .map(|ent| (ent.title.clone(), ent.previous.clone()))
@ -250,8 +250,8 @@ pub fn create_navigation(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rand::prelude::SliceRandom;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rand::RngCore;
use crate::compiler::process::process_from_memory; use crate::compiler::process::process_from_memory;
@ -288,12 +288,10 @@ mod tests {
]; ];
let mut shuffled = entries.clone(); let mut shuffled = entries.clone();
for _ in 0..10 { for _ in 0..10 {
for i in 0..5 { let mut rng = OsRng {};
let pos = OsRng.next_u64() % entries.len() as u64; shuffled.shuffle(&mut rng);
shuffled.swap(i, pos as usize);
}
let mut entrymap = shuffled let entrymap = shuffled
.iter() .iter()
.map(|ent| (ent.title.clone(), ent.previous.clone())) .map(|ent| (ent.title.clone(), ent.previous.clone()))
.collect::<HashMap<String, Option<String>>>(); .collect::<HashMap<String, Option<String>>>();

View file

@ -87,33 +87,29 @@ impl Element for Blockquote {
match compiler.target() { match compiler.target() {
HTML => { HTML => {
let mut result = r#"<div class="blockquote-content">"#.to_string(); let mut result = r#"<div class="blockquote-content">"#.to_string();
let format_author = || -> Result<String, FormatError> { let format_author = || -> Result<String, String> {
let mut result = String::new(); let mut result = String::new();
if self.cite.is_some() || self.author.is_some() { if self.cite.is_some() || self.author.is_some() {
result += r#"<p class="blockquote-author">"#; result += r#"<p class="blockquote-author">"#;
let fmt_pair = FmtPair(compiler.target(), self); 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) => { (true, true) => {
let args = Compiler::sanitize_format(fmt_pair.0, self.style.format[0].as_str())
FormatArgs::new(self.style.format[0].as_str(), &fmt_pair);
args.status()?;
result += args.to_string().as_str();
} }
(true, false) => { (true, false) => {
let args = Compiler::sanitize_format(fmt_pair.0, self.style.format[1].as_str())
FormatArgs::new(self.style.format[1].as_str(), &fmt_pair);
args.status()?;
result += args.to_string().as_str();
} }
(false, false) => { (false, false) => {
let args = Compiler::sanitize_format(fmt_pair.0, self.style.format[2].as_str())
FormatArgs::new(self.style.format[2].as_str(), &fmt_pair);
args.status()?;
result += args.to_string().as_str();
} }
_ => panic!(""), _ => 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 += "</p>"; result += "</p>";
} }
Ok(result) Ok(result)
@ -126,25 +122,21 @@ impl Element for Blockquote {
result += "<blockquote>"; result += "<blockquote>";
} }
if self.style.author_pos == Before { 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; let mut in_paragraph = false;
for elem in &self.content { for elem in &self.content {
if elem.downcast_ref::<DocumentEnd>().is_some() {} if elem.downcast_ref::<DocumentEnd>().is_some() {
else if elem.downcast_ref::<Blockquote>().is_some() } else if elem.downcast_ref::<Blockquote>().is_some() {
{ if in_paragraph {
if in_paragraph
{
result += "</p>"; result += "</p>";
in_paragraph = false; in_paragraph = false;
} }
result += elem result += elem
.compile(compiler, document, cursor + result.len())? .compile(compiler, document, cursor + result.len())?
.as_str(); .as_str();
} } else {
else
{
if !in_paragraph { if !in_paragraph {
result += "<p>"; result += "<p>";
in_paragraph = true; in_paragraph = true;
@ -154,8 +146,7 @@ impl Element for Blockquote {
.as_str(); .as_str();
} }
} }
if in_paragraph if in_paragraph {
{
result += "</p>"; result += "</p>";
} }
result += "</blockquote>"; result += "</blockquote>";
@ -299,7 +290,6 @@ impl Rule for BlockquoteRule {
// Content // Content
let entry_start = captures.get(0).unwrap().start(); let entry_start = captures.get(0).unwrap().start();
let mut entry_content = captures.get(2).unwrap().as_str().to_string(); 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) { while let Some(captures) = self.continue_re.captures_at(content, end_cursor.pos) {
if captures.get(0).unwrap().start() != end_cursor.pos { if captures.get(0).unwrap().start() != end_cursor.pos {
break; break;
@ -308,17 +298,12 @@ impl Rule for BlockquoteRule {
end_cursor = end_cursor.at(captures.get(0).unwrap().end()); end_cursor = end_cursor.at(captures.get(0).unwrap().end());
let trimmed = captures.get(1).unwrap().as_str().trim_start().trim_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 += "\n";
entry_content += trimmed; entry_content += trimmed;
} }
}
// Parse entry content // Parse entry content
let token = Token::new(entry_start..end_cursor.pos, end_cursor.source.clone()); let token = Token::new(entry_start..end_cursor.pos, end_cursor.source.clone());
println!("{entry_content}.");
let entry_src = Rc::new(VirtualSource::new( let entry_src = Rc::new(VirtualSource::new(
token.clone(), token.clone(),
"Blockquote Entry".to_string(), "Blockquote Entry".to_string(),
@ -326,33 +311,31 @@ impl Rule for BlockquoteRule {
)); ));
// Parse content // Parse content
let parsed_doc = state.with_state(|new_state| { 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 // Extract paragraph and nested blockquotes
let mut parsed_content : Vec<Box<dyn Element>> = vec![]; let mut parsed_content: Vec<Box<dyn Element>> = vec![];
for mut elem in parsed_doc.content().borrow_mut().drain(..) for mut elem in parsed_doc.content().borrow_mut().drain(..) {
{ if let Some(paragraph) = elem.downcast_mut::<Paragraph>() {
if let Some(paragraph) = elem.downcast_mut::<Paragraph>() if let Some(last) = parsed_content.last() {
{ if last.kind() == ElemKind::Inline {
if let Some(last) = parsed_content.last()
{
if last.kind() == ElemKind::Inline
{
parsed_content.push(Box::new(Text { parsed_content.push(Box::new(Text {
location: Token::new(last.location().end()..last.location().end(), last.location().source()), location: Token::new(
content: " ".to_string() last.location().end()..last.location().end(),
last.location().source(),
),
content: " ".to_string(),
}) as Box<dyn Element>); }) as Box<dyn Element>);
} }
} }
parsed_content.extend(std::mem::take(&mut paragraph.content)); parsed_content.extend(std::mem::take(&mut paragraph.content));
} } else if elem.downcast_ref::<Blockquote>().is_some() {
else if elem.downcast_ref::<Blockquote>().is_some()
{
parsed_content.push(elem); parsed_content.push(elem);
} } else {
else
{
reports.push( reports.push(
Report::build(ReportKind::Error, token.source(), token.range.start) Report::build(ReportKind::Error, token.source(), token.range.start)
.with_message("Unable to Parse Blockquote Entry") .with_message("Unable to Parse Blockquote Entry")

View file

@ -5,9 +5,13 @@ use std::rc::Rc;
use ariadne::Label; use ariadne::Label;
use ariadne::Report; use ariadne::Report;
use ariadne::ReportKind; use ariadne::ReportKind;
use reference_style::ExternalReferenceStyle;
use regex::Captures; use regex::Captures;
use regex::Match; use regex::Match;
use regex::Regex; use regex::Regex;
use runtime_format::FormatArgs;
use runtime_format::FormatKey;
use runtime_format::FormatKeyError;
use crate::compiler::compiler::Compiler; use crate::compiler::compiler::Compiler;
use crate::compiler::compiler::Target; use crate::compiler::compiler::Target;
@ -21,6 +25,7 @@ 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;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::style::StyleHolder;
use crate::parser::util; use crate::parser::util;
use crate::parser::util::Property; use crate::parser::util::Property;
use crate::parser::util::PropertyMap; use crate::parser::util::PropertyMap;
@ -52,7 +57,12 @@ impl Element for InternalReference {
) -> Result<String, String> { ) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { 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(); let elem = document.get_from_reference(&elemref).unwrap();
elem.compile_reference( elem.compile_reference(
@ -72,6 +82,29 @@ pub struct ExternalReference {
pub(self) location: Token, pub(self) location: Token,
pub(self) reference: CrossReference, pub(self) reference: CrossReference,
pub(self) caption: Option<String>, pub(self) caption: Option<String>,
pub(self) style: Rc<reference_style::ExternalReferenceStyle>,
}
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 { impl Element for ExternalReference {
@ -91,14 +124,34 @@ impl Element for ExternalReference {
Target::HTML => { Target::HTML => {
let mut result = "<a href=\"".to_string(); let mut result = "<a href=\"".to_string();
compiler.insert_crossreference(cursor + result.len(), self.reference.clone()); // Link position
let crossreference_pos = cursor + result.len();
if let Some(caption) = &self.caption { if let Some(caption) = &self.caption {
result += result +=
format!("\">{}</a>", Compiler::sanitize(Target::HTML, caption)).as_str(); format!("\">{}</a>", Compiler::sanitize(Target::HTML, caption)).as_str();
} else { } else {
result += format!("\">{}</a>", 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!("\">{}</a>", args.to_string()).as_str();
} }
// Add crossreference
compiler.insert_crossreference(crossreference_pos, self.reference.clone());
Ok(result) Ok(result)
} }
_ => todo!(""), _ => todo!(""),
@ -223,7 +276,7 @@ impl RegexRule for ReferenceRule {
); );
return reports; return reports;
} }
Ok(refname) => (None, refname.to_string()) Ok(refname) => (None, refname.to_string()),
} }
} }
} else { } else {
@ -244,9 +297,20 @@ impl RegexRule for ReferenceRule {
.get("caption", |_, value| -> Result<String, ()> { .get("caption", |_, value| -> Result<String, ()> {
Ok(value.clone()) Ok(value.clone())
}) })
.ok().map(|(_, s)| s); .ok()
.map(|(_, s)| s);
if let Some(refdoc) = refdoc { if let Some(refdoc) = refdoc {
// Get style
let style = state
.shared
.styles
.borrow()
.current(reference_style::STYLE_KEY)
.downcast_rc::<reference_style::ExternalReferenceStyle>()
.unwrap();
// §{#refname}
if refdoc.is_empty() { if refdoc.is_empty() {
state.push( state.push(
document, document,
@ -254,8 +318,10 @@ impl RegexRule for ReferenceRule {
location: token, location: token,
reference: CrossReference::Unspecific(refname), reference: CrossReference::Unspecific(refname),
caption, caption,
style,
}), }),
); );
// §{docname#refname}
} else { } else {
state.push( state.push(
document, document,
@ -263,6 +329,7 @@ impl RegexRule for ReferenceRule {
location: token, location: token,
reference: CrossReference::Specific(refdoc, refname), reference: CrossReference::Specific(refdoc, refname),
caption, caption,
style,
}), }),
); );
} }
@ -279,6 +346,36 @@ impl RegexRule for ReferenceRule {
reports 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)] #[cfg(test)]
@ -372,15 +469,22 @@ mod tests {
r#" r#"
@html.page_title = 2 @html.page_title = 2
@@style.external_reference = {
"format_unspecific": "[UNSPECIFIC {refname}]",
"format_specific": "[SPECIFIC {refdoc}:{refname}]"
}
§{#ref}[caption=from 0] §{#ref}[caption=from 0]
§{#ref}
§{#ref2}[caption=from 1] §{#ref2}[caption=from 1]
§{b#ref2}
"# "#
.into(), .into(),
], ],
) )
.unwrap(); .unwrap();
assert!(result[1].0.borrow().body.starts_with("<div class=\"content\"><p><a href=\"a.html#Referenceable_section\">#ref</a><a href=\"a.html#Referenceable_section\">a#ref</a></p>")); assert!(result[1].0.borrow().body.starts_with("<div class=\"content\"><p><a href=\"a.html#Referenceable_section\">(#ref)</a><a href=\"a.html#Referenceable_section\">(a#ref)</a></p>"));
assert!(result[2].0.borrow().body.starts_with("<div class=\"content\"><p><a href=\"a.html#Referenceable_section\">from 0</a><a href=\"b.html#Another_Referenceable_section\">from 1</a></p>")); assert!(result[2].0.borrow().body.starts_with("<div class=\"content\"><p><a href=\"a.html#Referenceable_section\">from 0</a><a href=\"a.html#Referenceable_section\">[UNSPECIFIC ref]</a><a href=\"b.html#Another_Referenceable_section\">from 1</a><a href=\"b.html#Another_Referenceable_section\">[SPECIFIC b:ref2]</a></p>"));
} }
} }