From 792d991fb2ed6aff52a0c1c3c57151802ded31c3 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 7 Jun 2020 17:28:36 +0200 Subject: [PATCH] Improve template syntax Templates are still wip. Currently they can be used in metadata by starting the value with % --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- src/format/html.rs | 50 ++++++- src/format/mod.rs | 4 +- src/parsing/bibliography.rs | 4 +- src/parsing/configuration/mod.rs | 5 +- src/parsing/elements.rs | 221 +++++++++++++++++++++++++++++++ src/parsing/inline.rs | 26 ++++ src/parsing/parser.rs | 55 +++++++- src/parsing/tokens.rs | 6 + 11 files changed, 364 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4625950..f482965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,7 +566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "snekdown" -version = "0.14.0" +version = "0.15.0" dependencies = [ "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 17d4c30..ab2fad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snekdown" -version = "0.14.0" +version = "0.15.0" authors = ["trivernis "] edition = "2018" license-file = "LICENSE" diff --git a/README.md b/README.md index 8125247..fa1423c 100644 --- a/README.md +++ b/README.md @@ -200,5 +200,5 @@ The end goal is to have a markup language with features similar to LaTeX. - [ ] Cross References - [ ] Figures - [ ] EPUB Rendering (PDF is too hard) -- [ ] Custom Elements via templates +- [ ] Custom Elements via templates (50%) - [ ] Custom Stylesheets \ No newline at end of file diff --git a/src/format/html.rs b/src/format/html.rs index 5c265f1..8cac8c6 100644 --- a/src/format/html.rs +++ b/src/format/html.rs @@ -1,5 +1,6 @@ -use crate::format::Template; +use crate::format::PlaceholderTemplate; use crate::parsing::bibliography::{BibEntry, BibReference}; +use crate::parsing::configuration::Value; use crate::parsing::elements::*; use htmlescape::{encode_attribute, encode_minimal}; use minify::html::minify; @@ -61,6 +62,7 @@ impl ToHtml for Inline { Inline::Emoji(emoji) => emoji.to_html(), Inline::Colored(colored) => colored.to_html(), Inline::BibReference(bibref) => bibref.lock().unwrap().to_html(), + Inline::TemplateVar(var) => var.lock().unwrap().to_html(), } } } @@ -88,6 +90,7 @@ impl ToHtml for MetadataValue { MetadataValue::Placeholder(ph) => ph.lock().unwrap().to_html(), MetadataValue::Bool(b) => format!("{}", b), MetadataValue::Float(f) => format!("{}", f), + MetadataValue::Template(t) => t.to_html(), } } } @@ -416,7 +419,7 @@ impl ToHtml for Anchor { impl ToHtml for InlineMetadata { fn to_html(&self) -> String { if let Some(MetadataValue::String(format)) = self.data.get("display") { - let mut template = Template::new(format.clone()); + let mut template = PlaceholderTemplate::new(format.clone()); self.data .iter() .for_each(|(k, v)| template.add_replacement(k, v.to_html().as_str())); @@ -483,7 +486,25 @@ impl ToHtml for BibEntry { } if let Some(display) = &self.display { let display = display.lock().unwrap(); - let mut template = Template::new(display.get().as_string()); + if let Value::Template(template) = display.get() { + let replacements = self + .as_map() + .iter() + .map(|(k, v)| { + ( + k.clone(), + Element::Inline(Box::new(Inline::Plain(PlainText { + value: v.clone(), + }))), + ) + }) + .collect(); + return template + .render(replacements) + .iter() + .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())); + } + let mut template = PlaceholderTemplate::new(display.get().as_string()); template.set_replacements(self.as_map()); format!( "{}", @@ -506,3 +527,26 @@ impl ToHtml for BibEntry { } } } + +impl ToHtml for Template { + fn to_html(&self) -> String { + self.text + .iter() + .fold("".to_string(), |a, b| format!("{}{}", a, b.to_html())) + } +} + +impl ToHtml for TemplateVariable { + fn to_html(&self) -> String { + if let Some(value) = &self.value { + format!( + "{}{}{}", + encode_minimal(self.prefix.as_str()), + value.to_html(), + encode_minimal(self.suffix.as_str()) + ) + } else { + "".to_string() + } + } +} diff --git a/src/format/mod.rs b/src/format/mod.rs index c2cf5b7..af04881 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -3,12 +3,12 @@ use std::collections::HashMap; pub mod html; -pub struct Template { +pub struct PlaceholderTemplate { value: String, replacements: HashMap, } -impl Template { +impl PlaceholderTemplate { pub fn empty() -> Self { Self::new(String::new()) } diff --git a/src/parsing/bibliography.rs b/src/parsing/bibliography.rs index 8d85064..ac03fe8 100644 --- a/src/parsing/bibliography.rs +++ b/src/parsing/bibliography.rs @@ -1,4 +1,4 @@ -use crate::format::Template; +use crate::format::PlaceholderTemplate; use crate::parsing::configuration::keys::{BIB_DISPLAY, BIB_HIDE_UNUSED}; use crate::parsing::configuration::{ConfigRefEntry, Configuration, Value}; use crate::parsing::elements::Metadata; @@ -141,7 +141,7 @@ impl BibReference { let entry = entry.lock().unwrap(); if let Some(display) = &self.display { let display = display.lock().unwrap(); - let mut template = Template::new(display.get().as_string()); + let mut template = PlaceholderTemplate::new(display.get().as_string()); template.set_replacements(entry.as_map()); return template.render(); } diff --git a/src/parsing/configuration/mod.rs b/src/parsing/configuration/mod.rs index 414a323..6b406df 100644 --- a/src/parsing/configuration/mod.rs +++ b/src/parsing/configuration/mod.rs @@ -2,7 +2,7 @@ use crate::parsing::configuration::config::RootConfig; use crate::parsing::configuration::keys::{ BIB_DISPLAY, BIB_HIDE_UNUSED, BIB_REF_DISPLAY, META_AUTHOR, META_DATE, META_TITLE, }; -use crate::parsing::elements::MetadataValue; +use crate::parsing::elements::{MetadataValue, Template}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -15,6 +15,7 @@ pub enum Value { Bool(bool), Float(f64), Integer(i64), + Template(Template), } #[derive(Clone, Debug)] @@ -36,6 +37,7 @@ impl Value { Value::Integer(int) => format!("{}", int), Value::Float(f) => format!("{:02}", f), Value::Bool(b) => format!("{}", b), + _ => "".to_string(), } } } @@ -135,6 +137,7 @@ impl Configuration { MetadataValue::Bool(bool) => self.set(key, Value::Bool(bool)), MetadataValue::Float(f) => self.set(key, Value::Float(f)), MetadataValue::Integer(i) => self.set(key, Value::Integer(i)), + MetadataValue::Template(t) => self.set(key, Value::Template(t)), _ => {} } } diff --git a/src/parsing/elements.rs b/src/parsing/elements.rs index 5d919de..4195637 100644 --- a/src/parsing/elements.rs +++ b/src/parsing/elements.rs @@ -20,6 +20,7 @@ pub enum MetadataValue { Float(f64), Bool(bool), Placeholder(Arc>), + Template(Template), } #[derive(Clone, Debug)] @@ -161,6 +162,7 @@ pub enum Inline { Emoji(Emoji), Colored(Colored), BibReference(Arc>), + TemplateVar(Arc>), } #[derive(Clone, Debug)] @@ -245,6 +247,20 @@ pub struct Colored { pub(crate) color: String, } +#[derive(Clone, Debug)] +pub struct Template { + pub(crate) text: Vec, + pub(crate) variables: HashMap>>, +} + +#[derive(Clone, Debug)] +pub struct TemplateVariable { + pub(crate) prefix: String, + pub(crate) name: String, + pub(crate) suffix: String, + pub(crate) value: Option, +} + // implementations impl Document { @@ -592,3 +608,208 @@ impl Metadata for InlineMetadata { } } } + +impl Element { + pub fn get_template_variables(&self) -> Vec>> { + match self { + Element::Block(block) => block + .get_template_variables() + .iter() + .filter_map(|e| e.clone()) + .collect(), + Element::Inline(inline) => vec![inline.get_template_variable()] + .iter() + .filter_map(|e| e.clone()) + .collect(), + Element::Line(line) => line + .get_template_variables() + .iter() + .filter_map(|e| e.clone()) + .collect(), + } + } + + pub fn freeze_variables(&mut self) -> Option>> { + match self { + Element::Block(b) => b.freeze_template_variables(), + Element::Line(l) => l.freeze_variables(), + Element::Inline(i) => return i.freeze_variables(), + } + + None + } +} + +impl Block { + pub fn get_template_variables(&self) -> Vec>>> { + match self { + Block::Section(sec) => sec + .elements + .iter() + .map(|e| e.get_template_variables()) + .flatten() + .collect(), + Block::Paragraph(par) => par + .elements + .iter() + .map(|l| l.get_template_variables()) + .flatten() + .collect(), + Block::Quote(q) => q + .text + .iter() + .map(|t| t.subtext.iter().map(|i| i.get_template_variable())) + .flatten() + .collect(), + _ => Vec::new(), + } + } + + pub fn freeze_template_variables(&mut self) { + match self { + Block::Section(s) => s + .elements + .iter_mut() + .for_each(|b| b.freeze_template_variables()), + Block::Paragraph(p) => p.elements.iter_mut().for_each(|l| l.freeze_variables()), + Block::Quote(q) => q.text.iter_mut().for_each(|t| { + t.subtext = t + .subtext + .iter_mut() + .map(|i| { + if let Some(t) = i.freeze_variables() { + Inline::TemplateVar(t) + } else { + (*i).clone() + } + }) + .collect() + }), + _ => {} + } + } +} + +impl Line { + pub fn get_template_variables(&self) -> Vec>>> { + match self { + Line::Text(line) => line + .subtext + .iter() + .map(|s| s.get_template_variable()) + .collect(), + _ => Vec::new(), + } + } + + pub fn freeze_variables(&mut self) { + match self { + Line::Text(text) => { + text.subtext = text + .subtext + .iter_mut() + .map(|i| { + if let Some(t) = i.freeze_variables() { + Inline::TemplateVar(t) + } else { + (*i).clone() + } + }) + .collect() + } + _ => {} + } + } +} + +impl Inline { + pub fn get_template_variable(&self) -> Option>> { + match self { + Inline::TemplateVar(temp) => Some(Arc::clone(temp)), + Inline::Colored(col) => col.value.get_template_variable(), + Inline::Superscript(sup) => sup.value.get_template_variable(), + Inline::Striked(striked) => striked.value.get_template_variable(), + Inline::Underlined(under) => under.value.get_template_variable(), + Inline::Italic(it) => it.value.get_template_variable(), + Inline::Bold(bo) => bo.value.get_template_variable(), + _ => None, + } + } + + pub fn freeze_variables(&mut self) -> Option>> { + match self { + Inline::TemplateVar(temp) => { + let temp = temp.lock().unwrap(); + return Some(Arc::new(Mutex::new((*temp).clone()))); + } + Inline::Colored(col) => { + if let Some(temp) = col.value.freeze_variables() { + col.value = Box::new(Inline::TemplateVar(temp)) + } + } + Inline::Superscript(sup) => { + if let Some(temp) = sup.value.freeze_variables() { + sup.value = Box::new(Inline::TemplateVar(temp)) + } + } + Inline::Striked(striked) => { + if let Some(temp) = striked.value.freeze_variables() { + striked.value = Box::new(Inline::TemplateVar(temp)) + } + } + Inline::Underlined(under) => { + if let Some(temp) = under.value.freeze_variables() { + under.value = Box::new(Inline::TemplateVar(temp)) + } + } + Inline::Italic(it) => { + if let Some(temp) = it.value.freeze_variables() { + it.value = Box::new(Inline::TemplateVar(temp)) + } + } + Inline::Bold(bo) => { + if let Some(temp) = bo.value.freeze_variables() { + bo.value = Box::new(Inline::TemplateVar(temp)) + } + } + _ => {} + } + None + } +} + +impl Template { + pub fn render(&self, replacements: HashMap) -> Vec { + replacements.iter().for_each(|(k, r)| { + if let Some(v) = self.variables.get(k) { + v.lock().unwrap().set_value(r.clone()) + } + }); + let elements = self + .text + .iter() + .map(|e| { + let mut e = e.clone(); + if let Some(template) = e.freeze_variables() { + Element::Inline(Box::new(Inline::TemplateVar(template))) + } else { + e + } + }) + .collect(); + self.variables + .iter() + .for_each(|(_, v)| v.lock().unwrap().reset()); + + elements + } +} + +impl TemplateVariable { + pub fn set_value(&mut self, value: Element) { + self.value = Some(value) + } + pub fn reset(&mut self) { + self.value = None + } +} diff --git a/src/parsing/inline.rs b/src/parsing/inline.rs index 6b26982..b575536 100644 --- a/src/parsing/inline.rs +++ b/src/parsing/inline.rs @@ -22,6 +22,7 @@ pub(crate) trait ParseInline { fn parse_emoji(&mut self) -> ParseResult; fn parse_colored(&mut self) -> ParseResult; fn parse_bibref(&mut self) -> ParseResult>>; + fn parse_template_variable(&mut self) -> ParseResult>>; fn parse_plain(&mut self) -> ParseResult; } @@ -40,6 +41,11 @@ impl ParseInline for Parser { /// parses Inline, the formatting parts of a line (Text) fn parse_inline(&mut self) -> ParseResult<Inline> { + if self.parse_variables { + if let Ok(var) = self.parse_template_variable() { + return Ok(Inline::TemplateVar(var)); + } + } if self.check_special(&PIPE) || self.check_linebreak() { Err(ParseError::new(self.index)) } else if self.check_eof() { @@ -249,6 +255,25 @@ impl ParseInline for Parser { Ok(ref_entry) } + /// parses a template variable {prefix{name}suffix} + fn parse_template_variable(&mut self) -> ParseResult<Arc<Mutex<TemplateVariable>>> { + let start_index = self.index; + self.assert_special(&TEMP_VAR_OPEN, start_index)?; + self.skip_char(); + let prefix = self.get_string_until_or_revert(&[TEMP_VAR_OPEN], &[LB], start_index)?; + self.skip_char(); + let name = self.get_string_until_or_revert(&[TEMP_VAR_CLOSE], &[LB], start_index)?; + self.skip_char(); + let suffix = self.get_string_until_or_revert(&[TEMP_VAR_CLOSE], &[LB], start_index)?; + self.skip_char(); + Ok(Arc::new(Mutex::new(TemplateVariable { + value: None, + name, + prefix, + suffix, + }))) + } + /// parses plain text as a string until it encounters an unescaped special inline char fn parse_plain(&mut self) -> ParseResult<PlainText> { if self.check_linebreak() { @@ -259,6 +284,7 @@ impl ParseInline for Parser { while let Some(ch) = self.next_char() { if self.check_special_group(&INLINE_SPECIAL_CHARS) || self.check_special_group(&self.inline_break_at) + || (self.parse_variables && self.check_special(&TEMP_VAR_OPEN)) { break; } diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index f5f0760..eccc859 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -25,10 +25,12 @@ pub struct Parser { paths: Arc<Mutex<Vec<String>>>, wg: WaitGroup, is_child: bool, + pub(crate) block_break_at: Vec<char>, pub(crate) inline_break_at: Vec<char>, pub(crate) document: Document, pub(crate) previous_char: char, pub(crate) reader: Box<dyn BufRead>, + pub(crate) parse_variables: bool, } impl Parser { @@ -118,8 +120,10 @@ impl Parser { is_child, previous_char: ' ', inline_break_at: Vec::new(), + block_break_at: Vec::new(), document: Document::new(!is_child), reader, + parse_variables: false, } } @@ -426,6 +430,8 @@ impl Parser { self.seek_inline_whitespace(); if let Ok(ph) = self.parse_placeholder() { value = MetadataValue::Placeholder(ph); + } else if let Ok(template) = self.parse_template() { + value = MetadataValue::Template(template) } else { let quoted_string = self.check_special_group(&QUOTES); let parse_until = if quoted_string { @@ -501,7 +507,9 @@ impl Parser { while let Ok(token) = self.parse_line() { paragraph.add_element(token); let start_index = self.index; - if self.check_special_sequence_group(&BLOCK_SPECIAL_CHARS) { + if self.check_special_sequence_group(&BLOCK_SPECIAL_CHARS) + || self.check_special_group(&self.block_break_at) + { self.revert_to(start_index)?; break; } @@ -772,7 +780,7 @@ impl Parser { let mut text = TextLine::new(); while let Ok(subtext) = self.parse_inline() { text.add_subtext(subtext); - if self.check_eof() { + if self.check_eof() || self.check_special_group(&self.inline_break_at) { break; } } @@ -787,4 +795,47 @@ impl Parser { Err(ParseError::eof(self.index)) } } + + /// parses a template + fn parse_template(&mut self) -> ParseResult<Template> { + let start_index = self.index; + self.assert_special(&TEMPLATE, start_index)?; + self.skip_char(); + if self.check_special(&TEMPLATE) { + return Err(self.revert_with_error(start_index)); + } + let mut elements = Vec::new(); + self.block_break_at.push(TEMPLATE); + self.inline_break_at.push(TEMPLATE); + self.parse_variables = true; + while let Ok(e) = self.parse_block() { + elements.push(Element::Block(Box::new(e))); + if self.check_special(&TEMPLATE) { + break; + } + } + self.parse_variables = false; + self.block_break_at.clear(); + self.inline_break_at.clear(); + self.assert_special(&TEMPLATE, start_index)?; + self.skip_char(); + let vars: HashMap<String, Arc<Mutex<TemplateVariable>>> = elements + .iter() + .map(|e| e.get_template_variables()) + .flatten() + .map(|e: Arc<Mutex<TemplateVariable>>| { + let name; + { + name = e.lock().unwrap().name.clone(); + }; + + (name, e) + }) + .collect(); + + Ok(Template { + text: elements, + variables: vars, + }) + } } diff --git a/src/parsing/tokens.rs b/src/parsing/tokens.rs index 123245a..2a711d5 100644 --- a/src/parsing/tokens.rs +++ b/src/parsing/tokens.rs @@ -28,6 +28,9 @@ pub(crate) const UP: char = '^'; pub(crate) const COLON: char = ':'; pub(crate) const PARAGRAPH: char = 'ยง'; pub(crate) const SEMICOLON: char = ';'; +pub(crate) const R_BRACE: char = '{'; +pub(crate) const L_BRACE: char = '}'; +pub(crate) const PERCENT: char = '%'; // aliases @@ -57,6 +60,9 @@ pub(crate) const BIBREF_CLOSE: char = L_BRACKET; pub(crate) const BIB_KEY_OPEN: char = R_BRACKET; pub(crate) const BIB_KEY_CLOSE: char = L_BRACKET; pub(crate) const BIB_DATA_START: char = COLON; +pub(crate) const TEMP_VAR_OPEN: char = R_BRACE; +pub(crate) const TEMP_VAR_CLOSE: char = L_BRACE; +pub(crate) const TEMPLATE: char = PERCENT; pub(crate) const ITALIC: char = ASTERISK; pub(crate) const MONOSPACE: char = BACKTICK;