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
pub fn refname<S: AsRef<str>>(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<Rc<dyn Variable>> {
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<usize, rusqlite::Error> {
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>"), "&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
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::<HashMap<String, Option<String>>>();

View file

@ -87,33 +87,29 @@ impl Element for Blockquote {
match compiler.target() {
HTML => {
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();
if self.cite.is_some() || self.author.is_some() {
result += r#"<p class="blockquote-author">"#;
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 += "</p>";
}
Ok(result)
@ -126,25 +122,21 @@ impl Element for Blockquote {
result += "<blockquote>";
}
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::<DocumentEnd>().is_some() {}
else if elem.downcast_ref::<Blockquote>().is_some()
{
if in_paragraph
{
if elem.downcast_ref::<DocumentEnd>().is_some() {
} else if elem.downcast_ref::<Blockquote>().is_some() {
if in_paragraph {
result += "</p>";
in_paragraph = false;
}
result += elem
.compile(compiler, document, cursor + result.len())?
.as_str();
}
else
{
} else {
if !in_paragraph {
result += "<p>";
in_paragraph = true;
@ -154,8 +146,7 @@ impl Element for Blockquote {
.as_str();
}
}
if in_paragraph
{
if in_paragraph {
result += "</p>";
}
result += "</blockquote>";
@ -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<Box<dyn Element>> = vec![];
for mut elem in parsed_doc.content().borrow_mut().drain(..)
{
if let Some(paragraph) = elem.downcast_mut::<Paragraph>()
{
if let Some(last) = parsed_content.last()
{
if last.kind() == ElemKind::Inline
{
let mut parsed_content: Vec<Box<dyn Element>> = vec![];
for mut elem in parsed_doc.content().borrow_mut().drain(..) {
if let Some(paragraph) = elem.downcast_mut::<Paragraph>() {
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<dyn Element>);
}
}
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);
}
else
{
} else {
reports.push(
Report::build(ReportKind::Error, token.source(), token.range.start)
.with_message("Unable to Parse Blockquote Entry")

View file

@ -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<String, String> {
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<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 {
@ -91,14 +124,34 @@ impl Element for ExternalReference {
Target::HTML => {
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 {
result +=
format!("\">{}</a>", Compiler::sanitize(Target::HTML, caption)).as_str();
} 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)
}
_ => 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<String, ()> {
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::<reference_style::ExternalReferenceStyle>()
.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("<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[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=\"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>"));
}
}