Reference styling
This commit is contained in:
parent
ea0a0cf5b1
commit
ffd1903a65
4 changed files with 221 additions and 77 deletions
|
@ -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 += "&",
|
||||
'<' => out += "<",
|
||||
'>' => out += ">",
|
||||
'"' => out += """,
|
||||
_ => 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>"), "<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, "{{<"), "{{<");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>>();
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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>"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue