From 0d6eb1d1a75883a5fd5c538a3aabdc2328ef3dde Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 31 May 2020 21:00:19 +0200 Subject: [PATCH] Add metadata syntax with formatting --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 30 +++++++++++++++++++ src/format/html.rs | 31 ++++++++++++++++++- src/parsing/elements.rs | 12 +++++++- src/parsing/parser.rs | 66 +++++++++++++++++++++++++++++++++++++---- src/parsing/tokens.rs | 5 ++++ 7 files changed, 139 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 205ec40..0de36ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "snekdown" -version = "0.4.0" +version = "0.5.0" dependencies = [ "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index e52a1b2..a821ca3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snekdown" -version = "0.4.0" +version = "0.5.0" authors = ["trivernis "] edition = "2018" license-file = "LICENSE" diff --git a/README.md b/README.md index 69c44aa..8bc6dae 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,33 @@ Header with rows |--------|--------|------- | row | row | row ``` + +### Metadata + +Additional metadata can be provided for some elements. + +```md +String value +[key = value] + +String value +[key = "String value"] + +Integer value +[key = 123] + +Float value +[key = 1.23] + +Boolean +[key] + +Boolean +[key = false] + +Placeholder +[key = [[placeholder]]] + +Formatting +[author = "The Great snek" date = [[date]] time = [[time]] display = "author - date at time"] +``` \ No newline at end of file diff --git a/src/format/html.rs b/src/format/html.rs index fc5260f..bd1b959 100644 --- a/src/format/html.rs +++ b/src/format/html.rs @@ -71,6 +71,18 @@ impl ToHtml for Block { } } +impl ToHtml for MetadataValue { + fn to_html(&self) -> String { + match self { + MetadataValue::String(string) => encode_minimal(string), + MetadataValue::Integer(num) => format!("{}", num), + MetadataValue::Placeholder(ph) => ph.lock().unwrap().to_html(), + MetadataValue::Bool(b) => format!("{}", b), + MetadataValue::Float(f) => format!("{}", f), + } + } +} + impl ToHtml for Document { fn to_html(&self) -> String { let inner = self @@ -251,7 +263,7 @@ impl ToHtml for Quote { if let Some(meta) = self.metadata.clone() { format!( "
{}
", - text, encode_minimal(meta.data.as_str()) + text, meta.to_html() ) } else { format!("
{}
", text) @@ -368,3 +380,20 @@ 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 format = format.clone(); + self.data + .iter() + .for_each(|(k, v)| format = format.replace(k, v.to_html().as_str())); + + format + } else { + self.data.iter().fold("".to_string(), |s, (k, v)| { + format!("{} {}={},", s, k, v.to_html()) + }) + } + } +} diff --git a/src/parsing/elements.rs b/src/parsing/elements.rs index 41191c9..241aef5 100644 --- a/src/parsing/elements.rs +++ b/src/parsing/elements.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::{Arc, Mutex}; pub const SECTION: &str = "section"; @@ -23,6 +24,15 @@ macro_rules! test_block { }; } +#[derive(Clone, Debug)] +pub enum MetadataValue { + String(String), + Integer(i64), + Float(f64), + Bool(bool), + Placeholder(Arc>), +} + #[derive(Clone, Debug)] pub enum Element { Block(Box), @@ -130,7 +140,7 @@ pub struct ImportAnchor { #[derive(Clone, Debug)] pub struct InlineMetadata { - pub(crate) data: String, + pub(crate) data: HashMap, } #[derive(Clone, Debug)] diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs index 9af03b6..e9934cc 100644 --- a/src/parsing/parser.rs +++ b/src/parsing/parser.rs @@ -2,6 +2,7 @@ use super::elements::*; use super::tokens::*; use crate::parsing::placeholders::ProcessPlaceholders; use crossbeam_utils::sync::WaitGroup; +use std::collections::HashMap; use std::error::Error; use std::fmt; use std::fmt::{Display, Formatter}; @@ -532,21 +533,76 @@ impl Parser { // if no meta open tag is present, the parsing has failed return Err(ParseError::new(self.index)); } - let mut text = String::new(); - while let Some(character) = self.next_char() { + let mut values = HashMap::new(); + let _ = self.next_char(); + while let Ok((key, value)) = self.parse_metadata_pair() { + values.insert(key, value); if self.check_special(&META_CLOSE) || self.check_linebreak() { // abort the parsing of the inner content when encountering a closing tag or linebreak break; } - text.push(character); } - if self.check_linebreak() || text.len() == 0 { + if self.check_linebreak() || values.len() == 0 { // if there was a linebreak (the metadata wasn't closed) or there is no inner data // return an error return Err(self.revert_with_error(start_index)); } - Ok(InlineMetadata { data: text }) + Ok(InlineMetadata { data: values }) + } + + fn parse_metadata_pair(&mut self) -> Result<(String, MetadataValue), ParseError> { + self.seek_inline_whitespace(); + let mut name = String::new(); + name.push(self.current_char); + while let Some(char) = self.next_char() { + if self.check_special_group(&[META_CLOSE, EQ, SPACE, LB]) { + break; + } + name.push(char); + } + self.seek_inline_whitespace(); + let mut value = MetadataValue::Bool(true); + if self.check_special(&EQ) { + let _ = self.next_char(); + self.seek_inline_whitespace(); + if let Ok(ph) = self.parse_placeholder() { + value = MetadataValue::Placeholder(ph); + } else { + let quoted_string = self.check_special_group("ES); + let mut raw_value = String::new(); + if !quoted_string { + raw_value.push(self.current_char); + } + while let Some(char) = self.next_char() { + if self.check_special_group(&[META_CLOSE, LB]) + || (quoted_string && self.check_special_group("ES)) + || (!quoted_string && self.check_special(&SPACE)) + { + break; + } + raw_value.push(char); + } + if self.check_special_group("ES) { + let _ = self.next_char(); + } + value = if quoted_string { + MetadataValue::String(raw_value) + } else if raw_value.to_lowercase().as_str() == "true" { + MetadataValue::Bool(true) + } else if raw_value.to_lowercase().as_str() == "false" { + MetadataValue::Bool(false) + } else if let Ok(num) = raw_value.parse::() { + MetadataValue::Integer(num) + } else if let Ok(num) = raw_value.parse::() { + MetadataValue::Float(num) + } else { + MetadataValue::String(raw_value) + } + } + } + + Ok((name, value)) } /// parses an import and starts a new task to parse the document of the import diff --git a/src/parsing/tokens.rs b/src/parsing/tokens.rs index a15da9e..a50d275 100644 --- a/src/parsing/tokens.rs +++ b/src/parsing/tokens.rs @@ -20,6 +20,9 @@ pub(crate) const GT: char = '>'; pub(crate) const LT: char = '<'; pub(crate) const BANG: char = '!'; pub(crate) const SPACE: char = ' '; +pub(crate) const EQ: char = '='; +pub(crate) const DOUBLE_QUOTE: char = '"'; +pub(crate) const SINGLE_QUOTE: char = '\''; // aliases @@ -40,6 +43,8 @@ pub(crate) const PHOLDER_CLOSE: char = L_BRACKET; // groups +pub(crate) const QUOTES: [char; 2] = [SINGLE_QUOTE, DOUBLE_QUOTE]; + pub(crate) const BLOCK_SPECIAL_CHARS: [char; 7] = [ HASH, MINUS,