From 357c8a18bdab301807a3942b5b2bf7a90246a4fa Mon Sep 17 00:00:00 2001 From: ef3d0c3e Date: Fri, 1 Nov 2024 22:15:33 +0100 Subject: [PATCH] PropertyParser refactor --- src/elements/blockquote.rs | 83 +++--- src/elements/code.rs | 108 ++------ src/elements/customstyle.rs | 20 +- src/elements/graphviz.rs | 133 +++------ src/elements/layout.rs | 254 ++++++++---------- src/elements/list.rs | 99 ++++--- src/elements/media.rs | 158 ++++------- src/elements/mod.rs | 2 +- src/elements/raw.rs | 101 ++----- src/elements/reference.rs | 84 ++---- src/elements/script.rs | 12 +- src/elements/tex.rs | 155 ++++------- src/lsp/semantic.rs | 28 +- src/lua/kernel.rs | 3 + src/parser/langparser.rs | 14 +- src/parser/layout.rs | 13 +- src/parser/mod.rs | 1 + src/parser/property.rs | 522 ++++++++++++++++++++++++++++++++++++ src/parser/source.rs | 9 + src/parser/util.rs | 282 ------------------- 20 files changed, 987 insertions(+), 1094 deletions(-) create mode 100644 src/parser/property.rs diff --git a/src/elements/blockquote.rs b/src/elements/blockquote.rs index 0040a96..91f6cd3 100644 --- a/src/elements/blockquote.rs +++ b/src/elements/blockquote.rs @@ -7,7 +7,7 @@ use blockquote_style::AuthorPos::After; use blockquote_style::AuthorPos::Before; use blockquote_style::BlockquoteStyle; use lsp::semantic::Semantics; -use regex::Match; +use parser::util::escape_source; use regex::Regex; use runtime_format::FormatArgs; use runtime_format::FormatKey; @@ -24,6 +24,8 @@ use crate::elements::paragraph::Paragraph; use crate::elements::text::Text; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use crate::parser::reports::macros::*; use crate::parser::reports::*; use crate::parser::rule::Rule; @@ -31,9 +33,6 @@ use crate::parser::source::Cursor; use crate::parser::source::Token; use crate::parser::source::VirtualSource; use crate::parser::style::StyleHolder; -use crate::parser::util::escape_text; -use crate::parser::util::Property; -use crate::parser::util::PropertyParser; #[derive(Debug)] pub struct Blockquote { @@ -185,15 +184,15 @@ impl BlockquoteRule { let mut props = HashMap::new(); props.insert( "author".to_string(), - Property::new(false, "Quote author".to_string(), None), + Property::new("Quote author".to_string(), None), ); props.insert( "cite".to_string(), - Property::new(false, "Quote source".to_string(), None), + Property::new("Quote source".to_string(), None), ); props.insert( "url".to_string(), - Property::new(false, "Quote source url".to_string(), None), + Property::new("Quote source url".to_string(), None), ); Self { @@ -202,29 +201,6 @@ impl BlockquoteRule { properties: PropertyParser { properties: props }, } } - - fn parse_properties( - &self, - m: Match, - ) -> Result<(Option, Option, Option), String> { - let processed = escape_text('\\', "]", m.as_str()); - let pm = self.properties.parse(processed.as_str())?; - - let author = pm - .get("author", |_, s| -> Result { Ok(s.to_string()) }) - .map(|(_, s)| s) - .ok(); - let cite = pm - .get("cite", |_, s| -> Result { Ok(s.to_string()) }) - .map(|(_, s)| s) - .ok(); - let url = pm - .get("url", |_, s| -> Result { Ok(s.to_string()) }) - .map(|(_, s)| s) - .ok(); - - Ok((author, cite, url)) - } } impl Rule for BlockquoteRule { @@ -265,23 +241,35 @@ impl Rule for BlockquoteRule { end_cursor = end_cursor.at(captures.get(0).unwrap().end()); // Properties - let mut author = None; - let mut cite = None; - let mut url = None; - if let Some(properties) = captures.get(1) { - match self.parse_properties(properties) { - Err(err) => { - report_err!( - &mut reports, - cursor.source.clone(), - "Invalid Blockquote Properties".into(), - span(properties.range(), err) - ); - return (end_cursor, reports); - } - Ok(props) => (author, cite, url) = props, - } - } + let prop_source = escape_source( + end_cursor.source.clone(), + captures.get(1).map_or(0..0, |m| m.range()), + "Blockquote Properties".into(), + '\\', + "]", + ); + let properties = + match self + .properties + .parse("Blockquote", &mut reports, state, prop_source.into()) + { + Some(props) => props, + None => return (end_cursor, reports), + }; + let (author, cite, url) = match ( + properties.get_opt(&mut reports, "author", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + properties.get_opt(&mut reports, "cite", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + properties.get_opt(&mut reports, "url", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + ) { + (Some(author), Some(cite), Some(url)) => (author, cite, url), + _ => return (end_cursor, reports), + }; if let Some((sems, tokens)) = Semantics::from_source(cursor.source.clone(), &state.shared.lsp) @@ -295,7 +283,6 @@ impl Rule for BlockquoteRule { sems.add(start..start + 1, tokens.blockquote_marker); if let Some(props) = captures.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.blockquote_props_sep); - sems.add(props.clone(), tokens.blockquote_props); sems.add(props.end..props.end + 1, tokens.blockquote_props_sep); } } diff --git a/src/elements/code.rs b/src/elements/code.rs index 50fad92..8739f56 100644 --- a/src/elements/code.rs +++ b/src/elements/code.rs @@ -6,6 +6,7 @@ use crypto::digest::Digest; use crypto::sha2::Sha512; use mlua::Function; use mlua::Lua; +use parser::util::escape_source; use regex::Captures; use regex::Regex; use syntect::easy::HighlightLines; @@ -23,13 +24,12 @@ use crate::lsp::semantic::Semantics; use crate::lua::kernel::CTX; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use crate::parser::reports::macros::*; use crate::parser::reports::*; use crate::parser::rule::RegexRule; use crate::parser::source::Token; -use crate::parser::util::Property; -use crate::parser::util::PropertyMapError; -use crate::parser::util::PropertyParser; use crate::parser::util::{self}; use lazy_static::lazy_static; @@ -291,11 +291,7 @@ impl CodeRule { let mut props = HashMap::new(); props.insert( "line_offset".to_string(), - Property::new( - true, - "Line number offset".to_string(), - Some("1".to_string()), - ), + Property::new("Line number offset".to_string(), Some("1".to_string())), ); Self { re: [ @@ -332,39 +328,22 @@ impl RegexRule for CodeRule { ) -> Vec { let mut reports = vec![]; - let properties = match matches.get(1) { - None => match self.properties.default() { - Ok(properties) => properties, - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Code Properties".into(), - span( - token.range.clone(), - format!("Code is missing properties: {e}") - ) - ); - return reports; - } - }, - Some(props) => { - let processed = - util::escape_text('\\', "]", props.as_str().trim_start().trim_end()); - match self.properties.parse(processed.as_str()) { - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Code Properties".into(), - span(props.range(), e) - ); - return reports; - } - Ok(properties) => properties, - } - } - }; + // Properties + let prop_source = escape_source( + token.source(), + matches.get(1).map_or(0..0, |m| m.range()), + "Code Properties".into(), + '\\', + "]", + ); + let properties = + match self + .properties + .parse("Code", &mut reports, state, prop_source.into()) + { + Some(props) => props, + None => return reports, + }; let code_lang = match matches.get(2) { None => "Plain Text".to_string(), @@ -429,43 +408,11 @@ impl RegexRule for CodeRule { let code_name = name.as_str().trim_end().trim_start().to_string(); (!code_name.is_empty()).then_some(code_name) }); - let line_offset = match properties.get("line_offset", |prop, value| { - value.parse::().map_err(|e| (prop, e)) + let line_offset = match properties.get(&mut reports, "line_offset", |_, value| { + value.value.parse::() }) { - Ok((_prop, offset)) => offset, - Err(e) => match e { - PropertyMapError::ParseError((prop, err)) => { - report_err!( - &mut reports, - token.source(), - "Invalid Code Property".into(), - span( - token.start() + 1..token.end(), - format!( - "Property `line_offset: {}` cannot be converted: {}", - prop.fg(state.parser.colors().info), - err.fg(state.parser.colors().error) - ) - ) - ); - return reports; - } - PropertyMapError::NotFoundError(err) => { - report_err!( - &mut reports, - token.source(), - "Invalid Code Property".into(), - span( - token.start() + 1..token.end(), - format!( - "Property `{}` doesn't exist", - err.fg(state.parser.colors().info) - ) - ) - ); - return reports; - } - }, + Some(line_offset) => line_offset, + _ => return reports, }; state.push( @@ -520,7 +467,6 @@ impl RegexRule for CodeRule { ); if let Some(props) = matches.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.code_props_sep); - sems.add(props.clone(), tokens.code_props); sems.add(props.end..props.end + 1, tokens.code_props_sep); } if let Some(lang) = matches.get(2).map(|m| m.range()) { @@ -794,8 +740,10 @@ test code validate_semantics!(state, source.clone(), 0, code_sep { delta_line == 1, delta_start == 0, length == 3 }; code_props_sep { delta_line == 0, delta_start == 3, length == 1 }; - code_props { delta_line == 0, delta_start == 1, length == 14 }; - code_props_sep { delta_line == 0, delta_start == 14, length == 1 }; + prop_name { delta_line == 0, delta_start == 1, length == 11 }; + prop_equal { delta_line == 0, delta_start == 11, length == 1 }; + prop_value { delta_line == 0, delta_start == 1, length == 2 }; + code_props_sep { delta_line == 0, delta_start == 2, length == 1 }; code_lang { delta_line == 0, delta_start == 1, length == 2 }; code_title { delta_line == 0, delta_start == 3, length == 6 }; code_content { delta_line == 1, delta_start == 0, length == 10 }; diff --git a/src/elements/customstyle.rs b/src/elements/customstyle.rs index ff0112f..ae4f2ec 100644 --- a/src/elements/customstyle.rs +++ b/src/elements/customstyle.rs @@ -55,9 +55,8 @@ impl CustomStyle for LuaCustomStyle { let mut ctx = KernelContext::new(location.clone(), state, document); let mut reports = vec![]; - kernel.run_with_context(&mut ctx, |lua| { - if let Err(err) = self.start.call::<_, ()>(()) - { + kernel.run_with_context(&mut ctx, |_lua| { + if let Err(err) = self.start.call::<_, ()>(()) { report_err!( &mut reports, location.source(), @@ -71,6 +70,7 @@ impl CustomStyle for LuaCustomStyle { } }); + reports.extend(ctx.reports); reports } @@ -85,9 +85,8 @@ impl CustomStyle for LuaCustomStyle { let mut ctx = KernelContext::new(location.clone(), state, document); let mut reports = vec![]; - kernel.run_with_context(&mut ctx, |lua| { - if let Err(err) = self.end.call::<_, ()>(()) - { + kernel.run_with_context(&mut ctx, |_lua| { + if let Err(err) = self.end.call::<_, ()>(()) { report_err!( &mut reports, location.source(), @@ -101,6 +100,7 @@ impl CustomStyle for LuaCustomStyle { } }); + reports.extend(ctx.reports); reports } } @@ -344,7 +344,13 @@ impl Rule for CustomStyleRule { bindings.push(( "define_toggled".into(), lua.create_function( - |_, (name, token, on_start, on_end): (String, String, mlua::Function, mlua::Function)| { + |_, + (name, token, on_start, on_end): ( + String, + String, + mlua::Function, + mlua::Function, + )| { let mut result = Ok(()); let style = LuaCustomStyle { diff --git a/src/elements/graphviz.rs b/src/elements/graphviz.rs index e913885..f0fd3c6 100644 --- a/src/elements/graphviz.rs +++ b/src/elements/graphviz.rs @@ -5,9 +5,8 @@ use std::sync::Once; use crate::lua::kernel::CTX; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; -use crate::parser::util::Property; -use crate::parser::util::PropertyMapError; -use crate::parser::util::PropertyParser; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use ariadne::Fmt; use crypto::digest::Digest; use crypto::sha2::Sha512; @@ -18,6 +17,7 @@ use lsp::semantic::Semantics; use mlua::Error::BadArgument; use mlua::Function; use mlua::Lua; +use parser::util::escape_source; use regex::Captures; use regex::Regex; @@ -166,14 +166,13 @@ impl GraphRule { props.insert( "layout".to_string(), Property::new( - true, "Graphviz layout engine see ".to_string(), Some("dot".to_string()), ), ); props.insert( "width".to_string(), - Property::new(true, "SVG width".to_string(), Some("100%".to_string())), + Property::new("SVG width".to_string(), Some("100%".to_string())), ); Self { re: [Regex::new( @@ -239,98 +238,31 @@ impl RegexRule for GraphRule { }; // Properties - let properties = match matches.get(1) { - None => match self.properties.default() { - Ok(properties) => properties, - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Graph Properties".into(), - span( - token.range.clone(), - format!("Graph is missing property: {e}") - ) - ); - return reports; - } - }, - Some(props) => { - let processed = - util::escape_text('\\', "]", props.as_str().trim_start().trim_end()); - match self.properties.parse(processed.as_str()) { - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Graph Properties".into(), - span(props.range(), e) - ); - return reports; - } - Ok(properties) => properties, - } - } - }; - - // Property "layout" - let graph_layout = match properties.get("layout", |prop, value| { - layout_from_str(value.as_str()).map_err(|e| (prop, e)) - }) { - Ok((_prop, kind)) => kind, - Err(e) => match e { - PropertyMapError::ParseError((prop, err)) => { - report_err!( - &mut reports, - token.source(), - "Invalid Graph Property".into(), - span( - token.range.clone(), - format!( - "Property `{}` cannot be converted: {}", - prop.fg(state.parser.colors().info), - err.fg(state.parser.colors().error) - ) - ) - ); - return reports; - } - PropertyMapError::NotFoundError(err) => { - report_err!( - &mut reports, - token.source(), - "Invalid Graph Property".into(), - span(token.start() + 1..token.end(), err) - ); - return reports; - } - }, - }; - - // FIXME: You can escape html, make sure we escape single " - // Property "width" - let graph_width = match properties.get("width", |_, value| -> Result { - Ok(value.clone()) - }) { - Ok((_, kind)) => kind, - Err(e) => match e { - PropertyMapError::NotFoundError(err) => { - report_err!( - &mut reports, - token.source(), - "Invalid Graph Property".into(), - span( - token.start() + 1..token.end(), - format!( - "Property `{}` is missing", - err.fg(state.parser.colors().info) - ) - ) - ); - return reports; - } - _ => panic!("Unknown error"), - }, + let prop_source = escape_source( + token.source(), + matches.get(1).map_or(0..0, |m| m.range()), + "Graphviz Properties".into(), + '\\', + "]", + ); + let properties = + match self + .properties + .parse("Graphviz", &mut reports, state, prop_source.into()) + { + Some(props) => props, + None => return reports, + }; + let (graph_layout, graph_width) = match ( + properties.get(&mut reports, "layout", |_, value| { + layout_from_str(value.value.as_str()) + }), + properties.get(&mut reports, "width", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + ) { + (Some(graph_layout), Some(graph_width)) => (graph_layout, graph_width), + _ => return reports, }; state.push( @@ -348,7 +280,6 @@ impl RegexRule for GraphRule { sems.add(range.start..range.start + 7, tokens.graph_sep); if let Some(props) = matches.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.graph_props_sep); - sems.add(props.clone(), tokens.graph_props); sems.add(props.end..props.end + 1, tokens.graph_props_sep); } sems.add(matches.get(2).unwrap().range(), tokens.graph_content); @@ -490,8 +421,10 @@ digraph { validate_semantics!(state, source.clone(), 0, graph_sep { delta_line == 1, delta_start == 0, length == 7 }; graph_props_sep { delta_line == 0, delta_start == 7, length == 1 }; - graph_props { delta_line == 0, delta_start == 1, length == 9 }; - graph_props_sep { delta_line == 0, delta_start == 9, length == 1 }; + prop_name { delta_line == 0, delta_start == 1, length == 5 }; + prop_equal { delta_line == 0, delta_start == 5, length == 1 }; + prop_value { delta_line == 0, delta_start == 1, length == 3 }; + graph_props_sep { delta_line == 0, delta_start == 3, length == 1 }; graph_content { delta_line == 0, delta_start == 1, length == 1 }; graph_content { delta_line == 1, delta_start == 0, length == 10 }; graph_content { delta_line == 1, delta_start == 0, length == 2 }; diff --git a/src/elements/layout.rs b/src/elements/layout.rs index fb1b95b..dc1e4f4 100644 --- a/src/elements/layout.rs +++ b/src/elements/layout.rs @@ -15,11 +15,13 @@ use crate::parser::rule::RegexRule; use crate::parser::source::Token; use crate::parser::state::RuleState; use crate::parser::state::Scope; -use crate::parser::util::escape_text; use ariadne::Fmt; use mlua::Error::BadArgument; use mlua::Function; use mlua::Lua; +use parser::source::Source; +use parser::source::VirtualSource; +use parser::util::escape_source; use regex::Captures; use regex::Match; use regex::Regex; @@ -54,8 +56,8 @@ impl FromStr for LayoutToken { mod default_layouts { use crate::parser::layout::LayoutType; - use crate::parser::util::Property; - use crate::parser::util::PropertyParser; + use crate::parser::property::Property; + use crate::parser::property::PropertyParser; use super::*; @@ -68,7 +70,6 @@ mod default_layouts { properties.insert( "style".to_string(), Property::new( - true, "Additional style for the split".to_string(), Some("".to_string()), ), @@ -83,46 +84,38 @@ mod default_layouts { fn expects(&self) -> Range { 1..1 } - fn parse_properties(&self, properties: &str) -> Result>, String> { - let props = if properties.is_empty() { - self.0.default() - } else { - self.0.parse(properties) - } - .map_err(|err| { - format!( - "Failed to parse properties for layout {}: {err}", - self.name() - ) - })?; + fn parse_properties( + &self, + reports: &mut Vec, + state: &ParserState, + token: Token, + ) -> Option> { + let properties = match self.0.parse("Centered Layout", reports, state, token) { + Some(props) => props, + None => return None, + }; - let style = props - .get("style", |_, value| -> Result { - Ok(value.clone()) - }) - .map_err(|err| format!("Failed to parse style: {err:#?}")) - .map(|(_, value)| value)?; + let style = match properties.get(reports, "style", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }) { + Some(style) => style, + _ => return None, + }; - Ok(Some(Box::new(style))) + Some(Box::new(style)) } fn compile( &self, token: LayoutToken, _id: usize, - properties: &Option>, + properties: &Box, compiler: &Compiler, _document: &dyn Document, ) -> Result { match compiler.target() { Target::HTML => { - let style = match properties - .as_ref() - .unwrap() - .downcast_ref::() - .unwrap() - .as_str() - { + let style = match properties.downcast_ref::().unwrap().as_str() { "" => "".to_string(), str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)), }; @@ -146,7 +139,6 @@ mod default_layouts { properties.insert( "style".to_string(), Property::new( - true, "Additional style for the split".to_string(), Some("".to_string()), ), @@ -161,46 +153,38 @@ mod default_layouts { fn expects(&self) -> Range { 2..usize::MAX } - fn parse_properties(&self, properties: &str) -> Result>, String> { - let props = if properties.is_empty() { - self.0.default() - } else { - self.0.parse(properties) - } - .map_err(|err| { - format!( - "Failed to parse properties for layout {}: {err}", - self.name() - ) - })?; + fn parse_properties( + &self, + reports: &mut Vec, + state: &ParserState, + token: Token, + ) -> Option> { + let properties = match self.0.parse("Split Layout", reports, state, token) { + Some(props) => props, + None => return None, + }; - let style = props - .get("style", |_, value| -> Result { - Ok(value.clone()) - }) - .map_err(|err| format!("Failed to parse style: {err:#?}")) - .map(|(_, value)| value)?; + let style = match properties.get(reports, "style", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }) { + Some(style) => style, + _ => return None, + }; - Ok(Some(Box::new(style))) + Some(Box::new(style)) } fn compile( &self, token: LayoutToken, _id: usize, - properties: &Option>, + properties: &Box, compiler: &Compiler, _document: &dyn Document, ) -> Result { match compiler.target() { Target::HTML => { - let style = match properties - .as_ref() - .unwrap() - .downcast_ref::() - .unwrap() - .as_str() - { + let style = match properties.downcast_ref::().unwrap().as_str() { "" => "".to_string(), str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)), }; @@ -225,11 +209,7 @@ mod default_layouts { let mut properties = HashMap::new(); properties.insert( "title".to_string(), - Property::new( - true, - "Spoiler title".to_string(), - Some("".to_string()) - ), + Property::new("Spoiler title".to_string(), Some("".to_string())), ); Self(PropertyParser { properties }) @@ -241,50 +221,45 @@ mod default_layouts { fn expects(&self) -> Range { 1..1 } - fn parse_properties(&self, properties: &str) -> Result>, String> { - let props = if properties.is_empty() { - self.0.default() - } else { - self.0.parse(properties) - } - .map_err(|err| { - format!( - "Failed to parse properties for layout {}: {err}", - self.name() - ) - })?; + fn parse_properties( + &self, + reports: &mut Vec, + state: &ParserState, + token: Token, + ) -> Option> { + let properties = match self.0.parse("Spoiler Layout", reports, state, token) { + Some(props) => props, + None => return None, + }; - let title = props - .get("title", |_, value| -> Result { - Ok(value.clone()) - }) - .map_err(|err| format!("Failed to parse style: {err:#?}")) - .map(|(_, value)| value)?; + let title = match properties.get(reports, "title", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }) { + Some(title) => title, + _ => return None, + }; - Ok(Some(Box::new(title))) + Some(Box::new(title)) } fn compile( &self, token: LayoutToken, _id: usize, - properties: &Option>, + properties: &Box, compiler: &Compiler, _document: &dyn Document, ) -> Result { match compiler.target() { Target::HTML => { - let title = properties - .as_ref() - .unwrap() - .downcast_ref::() - .unwrap(); + let title = properties.downcast_ref::().unwrap(); match token { LayoutToken::Begin => Ok(format!( - r#"
{}"#, Compiler::sanitize(compiler.target(), title) + r#"
{}"#, + Compiler::sanitize(compiler.target(), title) )), LayoutToken::End => Ok(r#"
"#.to_string()), - _ => panic!() + _ => panic!(), } } _ => todo!(""), @@ -299,7 +274,7 @@ struct Layout { pub(self) layout: Rc, pub(self) id: usize, pub(self) token: LayoutToken, - pub(self) properties: Option>, + pub(self) properties: Box, } impl Element for Layout { @@ -330,9 +305,12 @@ impl RuleState for LayoutState { let doc_borrow = document.content().borrow(); let at = doc_borrow.last().map_or( - Token::new(document.source().content().len()..document.source().content().len(), document.source()), - |last| last.location().to_owned() - ); + Token::new( + document.source().content().len()..document.source().content().len(), + document.source(), + ), + |last| last.location().to_owned(), + ); for (tokens, layout_type) in &self.stack { let start = tokens.first().unwrap(); @@ -406,42 +384,23 @@ impl LayoutRule { pub fn parse_properties<'a>( mut reports: &mut Vec, + state: &ParserState, token: &Token, layout_type: Rc, - properties: Option, - ) -> Result>, ()> { - match properties { - None => match layout_type.parse_properties("") { - Ok(props) => Ok(props), - Err(err) => { - report_err!( - &mut reports, - token.source(), - "Invalid Layout Properties".into(), - span( - token.start() + 1..token.end(), - format!("Layout is missing required property: {err}") - ) - ); - Err(()) - } - }, - Some(props) => { - let content = escape_text('\\', "]", props.as_str()); - match layout_type.parse_properties(content.as_str()) { - Ok(props) => Ok(props), - Err(err) => { - report_err!( - &mut reports, - token.source(), - "Invalid Layout Properties".into(), - span(props.range(), err) - ); - Err(()) - } - } - } - } + m: Option, + ) -> Option> { + let prop_source = escape_source( + token.source(), + m.map_or(0..0, |m| m.range()), + format!("Layout {} Properties", layout_type.name()), + '\\', + "]", + ); + layout_type.parse_properties( + reports, + state, + Token::new(0..prop_source.content().len(), prop_source), + ) } } @@ -546,12 +505,13 @@ impl RegexRule for LayoutRule { // Parse properties let properties = match LayoutRule::parse_properties( &mut reports, + state, &token, layout_type.clone(), matches.get(1), ) { - Ok(props) => props, - Err(()) => return reports, + Some(props) => props, + None => return reports, }; state.push( @@ -590,7 +550,6 @@ impl RegexRule for LayoutRule { ); if let Some(props) = matches.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.layout_props_sep); - sems.add(props.clone(), tokens.layout_props); sems.add(props.end..props.end + 1, tokens.layout_props_sep); } sems.add(matches.get(2).unwrap().range(), tokens.layout_type); @@ -650,12 +609,13 @@ impl RegexRule for LayoutRule { // Parse properties let properties = match LayoutRule::parse_properties( &mut reports, + state, &token, layout_type.clone(), matches.get(1), ) { - Ok(props) => props, - Err(()) => return reports, + Some(props) => props, + None => return reports, }; if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) @@ -671,7 +631,6 @@ impl RegexRule for LayoutRule { ); if let Some(props) = matches.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.layout_props_sep); - sems.add(props.clone(), tokens.layout_props); sems.add(props.end..props.end + 1, tokens.layout_props_sep); } } @@ -732,12 +691,13 @@ impl RegexRule for LayoutRule { // Parse properties let properties = match LayoutRule::parse_properties( &mut reports, + state, &token, layout_type.clone(), matches.get(1), ) { - Ok(props) => props, - Err(()) => return reports, + Some(props) => props, + None => return reports, }; let layout_type = layout_type.clone(); @@ -757,7 +717,6 @@ impl RegexRule for LayoutRule { ); if let Some(props) = matches.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.layout_props_sep); - sems.add(props.clone(), tokens.layout_props); sems.add(props.end..props.end + 1, tokens.layout_props_sep); } } @@ -803,8 +762,8 @@ impl RegexRule for LayoutRule { Ok(token) => token, }; - CTX.with_borrow(|ctx| { - ctx.as_ref().map(|ctx| { + CTX.with_borrow_mut(|ctx| { + ctx.as_mut().map(|ctx| { // Make sure the rule state has been initialized let rule_state = LayoutRule::initialize_state(ctx.state); @@ -827,17 +786,18 @@ impl RegexRule for LayoutRule { }; // Parse properties - let layout_properties = match layout_type.parse_properties(properties.as_str()) { - Err(err) => { + let prop_source = Rc::new(VirtualSource::new(ctx.location.clone(), ":LUA:Layout Properties".into(), properties)) as Rc; + let layout_properties = match layout_type.parse_properties(&mut ctx.reports, ctx.state, prop_source.into()) { + None => { result = Err(BadArgument { to: Some("push".to_string()), pos: 3, name: Some("properties".to_string()), - cause: Arc::new(mlua::Error::external(err)), + cause: Arc::new(mlua::Error::external("Failed to parse properties")), }); return; }, - Ok(properties) => properties, + Some(properties) => properties, }; let id = match layout_token { @@ -1162,8 +1122,10 @@ mod tests { layout_sep { delta_line == 1, delta_start == 1, length == 2 }; layout_token { delta_line == 0, delta_start == 2, length == 11 }; layout_props_sep { delta_line == 0, delta_start == 11, length == 1 }; - layout_props { delta_line == 0, delta_start == 1, length == 8 }; - layout_props_sep { delta_line == 0, delta_start == 8, length == 1 }; + prop_name { delta_line == 0, delta_start == 1, length == 5 }; + prop_equal { delta_line == 0, delta_start == 5, length == 1 }; + prop_value { delta_line == 0, delta_start == 1, length == 2 }; + layout_props_sep { delta_line == 0, delta_start == 2, length == 1 }; layout_sep { delta_line == 1, delta_start == 0, length == 2 }; layout_token { delta_line == 0, delta_start == 2, length == 10 }; ); diff --git a/src/elements/list.rs b/src/elements/list.rs index edc01a2..3640b50 100644 --- a/src/elements/list.rs +++ b/src/elements/list.rs @@ -13,6 +13,8 @@ use crate::document::element::Element; use crate::lsp::semantic::Semantics; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use crate::parser::reports::macros::*; use crate::parser::reports::Report; use crate::parser::reports::*; @@ -21,11 +23,7 @@ use crate::parser::source::Cursor; use crate::parser::source::Token; use crate::parser::source::VirtualSource; use crate::parser::util; -use crate::parser::util::escape_text; -use crate::parser::util::Property; -use crate::parser::util::PropertyMapError; -use crate::parser::util::PropertyParser; -use regex::Match; +use parser::util::escape_source; use regex::Regex; #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -137,11 +135,11 @@ impl ListRule { let mut props = HashMap::new(); props.insert( "offset".to_string(), - Property::new(false, "Entry numbering offset".to_string(), None), + Property::new("Entry numbering offset".to_string(), None), ); props.insert( "bullet".to_string(), - Property::new(false, "Entry bullet".to_string(), None), + Property::new("Entry bullet".to_string(), None), ); Self { @@ -193,28 +191,6 @@ impl ListRule { } } - fn parse_properties(&self, m: Match) -> Result<(Option, Option), String> { - let processed = escape_text('\\', "]", m.as_str()); - let pm = self.properties.parse(processed.as_str())?; - - let offset = match pm.get("offset", |_, s| s.parse::()) { - Ok((_, val)) => Some(val), - Err(err) => match err { - PropertyMapError::ParseError(err) => { - return Err(format!("Failed to parse `offset`: {err}")) - } - PropertyMapError::NotFoundError(_) => None, - }, - }; - - let bullet = pm - .get("bullet", |_, s| -> Result { Ok(s.to_string()) }) - .map(|(_, s)| s) - .ok(); - - Ok((offset, bullet)) - } - fn parse_depth(depth: &str, document: &dyn Document, offset: usize) -> Vec<(bool, usize)> { let mut parsed = vec![]; let prev_entry = document @@ -305,28 +281,46 @@ impl Rule for ListRule { end_cursor = end_cursor.at(captures.get(0).unwrap().end()); // Properties - let mut offset = None; - let mut bullet = None; - if let Some(properties) = captures.get(2) { - match self.parse_properties(properties) { - Err(err) => { - report_err!( - &mut reports, - cursor.source.clone(), - "Invalid List Entry Properties".into(), - span(properties.range(), err) - ); - return (cursor.at(captures.get(0).unwrap().end()), reports); + let prop_source = escape_source( + end_cursor.source.clone(), + captures.get(2).map_or(0..0, |m| m.range()), + "List Properties".into(), + '\\', + "]", + ); + let properties = match self.properties.parse( + "List", + &mut reports, + state, + Token::new(0..prop_source.content().len(), prop_source), + ) { + Some(props) => props, + None => return (end_cursor, reports), + }; + + let (offset, bullet) = match ( + properties.get_opt(&mut reports, "offset", |_, value| { + value.value.parse::() + }), + properties.get_opt(&mut reports, "bullet", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + ) { + (Some(offset), Some(bullet)) => { + // Get bullet from previous entry if it exists + if bullet.is_none() { + ( + offset, + document + .last_element::() + .and_then(|prev| prev.bullet.clone()), + ) + } else { + (offset, bullet) } - Ok(props) => (offset, bullet) = props, } - } - // Get bullet from previous entry if it exists - if bullet.is_none() { - bullet = document - .last_element::() - .and_then(|prev| prev.bullet.clone()) - } + _ => return (end_cursor, reports), + }; // Depth let depth = ListRule::parse_depth( @@ -341,7 +335,6 @@ impl Rule for ListRule { sems.add(captures.get(1).unwrap().range(), tokens.list_bullet); if let Some(props) = captures.get(2).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.list_props_sep); - sems.add(props.clone(), tokens.list_props); sems.add(props.end..props.end + 1, tokens.list_props_sep); } } @@ -528,8 +521,10 @@ mod tests { validate_semantics!(state, source.clone(), 0, list_bullet { delta_line == 1, delta_start == 1, length == 1 }; list_props_sep { delta_line == 0, delta_start == 1, length == 1 }; - list_props { delta_line == 0, delta_start == 1, length == 8 }; - list_props_sep { delta_line == 0, delta_start == 8, length == 1 }; + prop_name { delta_line == 0, delta_start == 1, length == 6 }; + prop_equal { delta_line == 0, delta_start == 6, length == 1 }; + prop_value { delta_line == 0, delta_start == 1, length == 1 }; + list_props_sep { delta_line == 0, delta_start == 1, length == 1 }; style_marker { delta_line == 0, delta_start == 8, length == 2 }; style_marker { delta_line == 0, delta_start == 6, length == 2 }; list_bullet { delta_line == 2, delta_start == 1, length == 2 }; diff --git a/src/elements/media.rs b/src/elements/media.rs index 32c1795..88092ee 100644 --- a/src/elements/media.rs +++ b/src/elements/media.rs @@ -5,7 +5,6 @@ use ariadne::Fmt; use lsp::semantic::Semantics; use parser::util::escape_source; use regex::Captures; -use regex::Match; use regex::Regex; use regex::RegexBuilder; @@ -20,16 +19,14 @@ use crate::document::element::ReferenceableElement; use crate::document::references::validate_refname; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use crate::parser::reports::macros::*; use crate::parser::reports::*; use crate::parser::rule::RegexRule; use crate::parser::source::Token; use crate::parser::util; use crate::parser::util::parse_paragraph; -use crate::parser::util::Property; -use crate::parser::util::PropertyMap; -use crate::parser::util::PropertyMapError; -use crate::parser::util::PropertyParser; use super::paragraph::Paragraph; use super::reference::InternalReference; @@ -245,19 +242,15 @@ impl MediaRule { let mut props = HashMap::new(); props.insert( "type".to_string(), - Property::new( - false, - "Override for the media type detection".to_string(), - None, - ), + Property::new("Override for the media type detection".to_string(), None), ); props.insert( "width".to_string(), - Property::new(false, "Override for the media width".to_string(), None), + Property::new("Override for the media width".to_string(), None), ); props.insert( "caption".to_string(), - Property::new(false, "Medium caption".to_string(), None), + Property::new("Medium caption".to_string(), None), ); Self { re: [RegexBuilder::new( @@ -280,47 +273,6 @@ impl MediaRule { Ok(trimmed) } - fn parse_properties( - &self, - mut reports: &mut Vec, - token: &Token, - m: &Option, - ) -> Option { - match m { - None => match self.properties.default() { - Ok(properties) => Some(properties), - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Media Properties".into(), - span( - token.range.clone(), - format!("Media is missing required property: {e}") - ) - ); - None - } - }, - Some(props) => { - let processed = - util::escape_text('\\', "]", props.as_str().trim_start().trim_end()); - match self.properties.parse(processed.as_str()) { - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Media Properties".into(), - span(props.range(), e) - ); - None - } - Ok(properties) => Some(properties), - } - } - } - } - fn detect_filetype(filename: &str) -> Option { let sep = match filename.rfind('.') { Some(pos) => pos, @@ -390,64 +342,61 @@ impl RegexRule for MediaRule { }; // Properties - let properties = match self.parse_properties(&mut reports, &token, &matches.get(3)) { - Some(pm) => pm, + let prop_source = escape_source( + token.source(), + matches.get(3).map_or(0..0, |m| m.range()), + "Media Properties".into(), + '\\', + "]", + ); + let properties = match self.properties.parse( + "Media", + &mut reports, + state, + Token::new(0..prop_source.content().len(), prop_source), + ) { + Some(props) => props, None => return reports, }; - let media_type = match Self::detect_filetype(uri.as_str()) { - Some(media_type) => media_type, - None => match properties.get("type", |prop, value| { - MediaType::from_str(value.as_str()).map_err(|e| (prop, e)) - }) { - Ok((_prop, kind)) => kind, - Err(e) => match e { - PropertyMapError::ParseError((prop, err)) => { - report_err!( - &mut reports, - token.source(), - "Invalid Media Property".into(), - span( - token.start() + 1..token.end(), - format!( - "Property `type: {}` cannot be converted: {}", - prop.fg(state.parser.colors().info), - err.fg(state.parser.colors().error) + let (media_type, caption, width) = match ( + properties.get_opt(&mut reports, "type", |_, value| { + MediaType::from_str(value.value.as_str()) + }), + properties.get_opt(&mut reports, "caption", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + properties.get_opt(&mut reports, "width", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + ) { + (Some(media_type), Some(caption), Some(width)) => { + if media_type.is_none() { + match Self::detect_filetype(uri.as_str()) { + None => { + report_err!( + &mut reports, + token.source(), + "Invalid Media Property".into(), + span( + token.start() + 1..token.end(), + format!( + "Failed to detect media type for `{}`", + uri.fg(state.parser.colors().info) + ) ) - ) - ); - return reports; + ); + return reports; + } + Some(media_type) => (media_type, caption, width), } - PropertyMapError::NotFoundError(err) => { - report_err!( - &mut reports, - token.source(), - "Invalid Media Property".into(), - span( - token.start() + 1..token.end(), - format!("{err}. Required because mediatype could not be detected") - ) - ); - return reports; - } - }, - }, + } else { + (media_type.unwrap(), caption, width) + } + } + _ => return reports, }; - let width = properties - .get("width", |_, value| -> Result { - Ok(value.clone()) - }) - .ok() - .map(|(_, s)| s); - - let caption = properties - .get("caption", |_, value| -> Result { - Ok(value.clone()) - }) - .ok() - .map(|(_, value)| value); - if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) { sems.add( matches.get(0).unwrap().start()..matches.get(0).unwrap().start() + 1, @@ -476,7 +425,6 @@ impl RegexRule for MediaRule { // Props if let Some(props) = matches.get(3) { sems.add(props.start() - 1..props.start(), tokens.media_props_sep); - sems.add(props.range(), tokens.media_props); sems.add(props.end()..props.end() + 1, tokens.media_props_sep); } } diff --git a/src/elements/mod.rs b/src/elements/mod.rs index 25f106d..b4910d2 100644 --- a/src/elements/mod.rs +++ b/src/elements/mod.rs @@ -17,5 +17,5 @@ pub mod section; pub mod style; pub mod tex; pub mod text; -pub mod variable; pub mod toc; +pub mod variable; diff --git a/src/elements/raw.rs b/src/elements/raw.rs index d21b9d6..68fe1a9 100644 --- a/src/elements/raw.rs +++ b/src/elements/raw.rs @@ -6,18 +6,18 @@ use crate::lsp::semantic::Semantics; use crate::lua::kernel::CTX; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use crate::parser::reports::macros::*; use crate::parser::reports::*; use crate::parser::rule::RegexRule; use crate::parser::source::Token; -use crate::parser::util::Property; -use crate::parser::util::PropertyMapError; -use crate::parser::util::PropertyParser; use crate::parser::util::{self}; use ariadne::Fmt; use mlua::Error::BadArgument; use mlua::Function; use mlua::Lua; +use parser::util::escape_source; use regex::Captures; use regex::Regex; use std::collections::HashMap; @@ -59,7 +59,6 @@ impl RawRule { props.insert( "kind".to_string(), Property::new( - true, "Element display kind".to_string(), Some("inline".to_string()), ), @@ -127,77 +126,28 @@ impl RegexRule for RawRule { } }; - let properties = match matches.get(1) { - None => match self.properties.default() { - Ok(properties) => properties, - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Raw Code".into(), - span( - token.range.clone(), - format!("Raw code is missing properties: {e}") - ) - ); - return reports; - } - }, - Some(props) => { - let processed = - util::escape_text('\\', "]", props.as_str().trim_start().trim_end()); - match self.properties.parse(processed.as_str()) { - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Raw Code Properties".into(), - span(props.range(), e) - ); - return reports; - } - Ok(properties) => properties, - } - } + let prop_source = escape_source( + token.source(), + matches.get(1).map_or(0..0, |m| m.range()), + "Raw Properties".into(), + '\\', + "]", + ); + let properties = match self.properties.parse( + "Raw Code", + &mut reports, + state, + Token::new(0..prop_source.content().len(), prop_source), + ) { + Some(props) => props, + None => return reports, }; - let raw_kind: ElemKind = match properties.get("kind", |prop, value| { - ElemKind::from_str(value.as_str()).map_err(|e| (prop, e)) + let raw_kind = match properties.get(&mut reports, "kind", |_, value| { + ElemKind::from_str(value.value.as_str()) }) { - Ok((_prop, kind)) => kind, - Err(e) => match e { - PropertyMapError::ParseError((prop, err)) => { - report_err!( - &mut reports, - token.source(), - "Invalid Raw Code Properties".into(), - span( - token.range.clone(), - format!( - "Property `kind: {}` cannot be converted: {}", - prop.fg(state.parser.colors().info), - err.fg(state.parser.colors().error) - ) - ) - ); - return reports; - } - PropertyMapError::NotFoundError(err) => { - report_err!( - &mut reports, - token.source(), - "Invalid Raw Code Properties".into(), - span( - token.range.clone(), - format!( - "Property `{}` is missing", - err.fg(state.parser.colors().info) - ) - ) - ); - return reports; - } - }, + None => return reports, + Some(raw_kind) => raw_kind, }; state.push( @@ -214,7 +164,6 @@ impl RegexRule for RawRule { sems.add(range.start..range.start + 2, tokens.raw_sep); if let Some(props) = matches.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.raw_props_sep); - sems.add(props.clone(), tokens.raw_props); sems.add(props.end..props.end + 1, tokens.raw_props_sep); } sems.add(matches.get(2).unwrap().range(), tokens.raw_content); @@ -356,8 +305,10 @@ Break%%NewParagraph%") validate_semantics!(state, source.clone(), 0, raw_sep { delta_line == 1, delta_start == 0, length == 2 }; raw_props_sep { delta_line == 0, delta_start == 2, length == 1 }; - raw_props { delta_line == 0, delta_start == 1, length == 10 }; - raw_props_sep { delta_line == 0, delta_start == 10, length == 1 }; + prop_name { delta_line == 0, delta_start == 1, length == 4 }; + prop_equal { delta_line == 0, delta_start == 4, length == 1 }; + prop_value { delta_line == 0, delta_start == 1, length == 5 }; + raw_props_sep { delta_line == 0, delta_start == 5, length == 1 }; raw_content { delta_line == 0, delta_start == 1, length == 4 }; raw_sep { delta_line == 0, delta_start == 4, length == 2 }; raw_sep { delta_line == 1, delta_start == 0, length == 2 }; diff --git a/src/elements/reference.rs b/src/elements/reference.rs index ed6faa9..1694033 100644 --- a/src/elements/reference.rs +++ b/src/elements/reference.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::rc::Rc; +use parser::util::escape_source; use reference_style::ExternalReferenceStyle; use regex::Captures; -use regex::Match; use regex::Regex; use runtime_format::FormatArgs; use runtime_format::FormatKey; @@ -19,15 +19,13 @@ use crate::document::references::validate_refname; use crate::lsp::semantic::Semantics; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use crate::parser::reports::macros::*; use crate::parser::reports::*; use crate::parser::rule::RegexRule; use crate::parser::source::Token; use crate::parser::style::StyleHolder; -use crate::parser::util; -use crate::parser::util::Property; -use crate::parser::util::PropertyMap; -use crate::parser::util::PropertyParser; #[derive(Debug)] pub struct InternalReference { @@ -168,58 +166,13 @@ impl ReferenceRule { let mut props = HashMap::new(); props.insert( "caption".to_string(), - Property::new( - false, - "Override the display of the reference".to_string(), - None, - ), + Property::new("Override the display of the reference".to_string(), None), ); Self { re: [Regex::new(r"&\{(.*?)\}(?:\[((?:\\.|[^\\\\])*?)\])?").unwrap()], properties: PropertyParser { properties: props }, } } - - fn parse_properties( - &self, - mut reports: &mut Vec, - token: &Token, - m: &Option, - ) -> Option { - match m { - None => match self.properties.default() { - Ok(properties) => Some(properties), - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Reference Properties".into(), - span( - token.range.clone(), - format!("Reference is missing required property: {e}") - ) - ); - None - } - }, - Some(props) => { - let processed = - util::escape_text('\\', "]", props.as_str().trim_start().trim_end()); - match self.properties.parse(processed.as_str()) { - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Reference Properties".into(), - span(props.range(), e) - ); - None - } - Ok(properties) => Some(properties), - } - } - } - } } impl RegexRule for ReferenceRule { @@ -280,17 +233,29 @@ impl RegexRule for ReferenceRule { }; // Properties - let properties = match self.parse_properties(&mut reports, &token, &matches.get(2)) { - Some(pm) => pm, + let prop_source = escape_source( + token.source(), + matches.get(2).map_or(0..0, |m| m.range()), + "Reference Properties".into(), + '\\', + "]", + ); + let properties = match self.properties.parse( + "Reference", + &mut reports, + state, + Token::new(0..prop_source.content().len(), prop_source), + ) { + Some(props) => props, None => return reports, }; - let caption = properties - .get("caption", |_, value| -> Result { - Ok(value.clone()) - }) - .ok() - .map(|(_, s)| s); + let caption = match properties.get_opt(&mut reports, "caption", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }) { + Some(caption) => caption, + None => return reports, + }; if let Some(refdoc) = refdoc { // Get style @@ -370,7 +335,6 @@ impl RegexRule for ReferenceRule { matches.get(2).map(|m| m.range()), ) { sems.add(props.start - 1..props.start, tokens.reference_props_sep); - sems.add(props.clone(), tokens.reference_props); sems.add(props.end..props.end + 1, tokens.reference_props_sep); } diff --git a/src/elements/script.rs b/src/elements/script.rs index 0570623..0771f43 100644 --- a/src/elements/script.rs +++ b/src/elements/script.rs @@ -213,13 +213,17 @@ impl RegexRule for ScriptRule { ); } - if let Some(hints) = Hints::from_source(token.source(), &state.shared.lsp) { + if let Some(hints) = + Hints::from_source(token.source(), &state.shared.lsp) + { hints.add(matches.get(0).unwrap().end(), result); } } else if kind == 2 // Eval and Parse { - if let Some(hints) = Hints::from_source(token.source(), &state.shared.lsp) { + if let Some(hints) = + Hints::from_source(token.source(), &state.shared.lsp) + { hints.add(matches.get(0).unwrap().end(), result.clone()); } @@ -229,7 +233,6 @@ impl RegexRule for ScriptRule { result, )) as Rc; - state.with_state(|new_state| { new_state.parser.parse_into( new_state, @@ -295,6 +298,7 @@ impl RegexRule for ScriptRule { sems.add(range.end - 2..range.end, tokens.script_sep); } + // Process redirects as hints if let Some(hints) = Hints::from_source(token.source(), &state.shared.lsp) { let mut label = String::new(); ctx.redirects.iter().for_each(|redir| { @@ -305,6 +309,8 @@ impl RegexRule for ScriptRule { hints.add(matches.get(0).unwrap().end(), label); } } + + // TODO: Process reports reports } } diff --git a/src/elements/tex.rs b/src/elements/tex.rs index 1ec342c..9f55a93 100644 --- a/src/elements/tex.rs +++ b/src/elements/tex.rs @@ -12,8 +12,8 @@ use crypto::digest::Digest; use crypto::sha2::Sha512; use mlua::Function; use mlua::Lua; +use parser::util::escape_source; use regex::Captures; -use regex::Match; use regex::Regex; use crate::cache::cache::Cached; @@ -27,15 +27,13 @@ use crate::lsp::semantic::Semantics; use crate::lua::kernel::CTX; use crate::parser::parser::ParseMode; use crate::parser::parser::ParserState; +use crate::parser::property::Property; +use crate::parser::property::PropertyParser; use crate::parser::reports::macros::*; use crate::parser::reports::*; use crate::parser::rule::RegexRule; use crate::parser::source::Token; use crate::parser::util; -use crate::parser::util::Property; -use crate::parser::util::PropertyMap; -use crate::parser::util::PropertyMapError; -use crate::parser::util::PropertyParser; #[derive(Debug, PartialEq, Eq)] enum TexKind { @@ -235,19 +233,15 @@ impl TexRule { let mut props = HashMap::new(); props.insert( "env".to_string(), - Property::new( - true, - "Tex environment".to_string(), - Some("main".to_string()), - ), + Property::new("Tex environment".to_string(), Some("main".to_string())), ); props.insert( "kind".to_string(), - Property::new(false, "Element display kind".to_string(), None), + Property::new("Element display kind".to_string(), None), ); props.insert( "caption".to_string(), - Property::new(false, "Latex caption".to_string(), None), + Property::new("Latex caption".to_string(), None), ); Self { re: [ @@ -258,47 +252,6 @@ impl TexRule { properties: PropertyParser { properties: props }, } } - - fn parse_properties( - &self, - mut reports: &mut Vec, - token: &Token, - m: &Option, - ) -> Option { - match m { - None => match self.properties.default() { - Ok(properties) => Some(properties), - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Tex Properties".into(), - span( - token.range.clone(), - format!("Tex is missing required property: {e}") - ) - ); - None - } - }, - Some(props) => { - let processed = - util::escape_text('\\', "]", props.as_str().trim_start().trim_end()); - match self.properties.parse(processed.as_str()) { - Err(e) => { - report_err!( - &mut reports, - token.source(), - "Invalid Tex Properties".into(), - span(props.range(), e) - ); - None - } - Ok(properties) => Some(properties), - } - } - } - } } impl RegexRule for TexRule { @@ -358,67 +311,52 @@ impl RegexRule for TexRule { }; // Properties - let properties = match self.parse_properties(&mut reports, &token, &matches.get(1)) { - Some(pm) => pm, + let prop_source = escape_source( + token.source(), + matches.get(1).map_or(0..0, |m| m.range()), + "Tex Properties".into(), + '\\', + "]", + ); + let properties = match self.properties.parse( + "Raw Code", + &mut reports, + state, + Token::new(0..prop_source.content().len(), prop_source), + ) { + Some(props) => props, None => return reports, }; - // Tex kind - let tex_kind = match properties.get("kind", |prop, value| { - TexKind::from_str(value.as_str()).map_err(|e| (prop, e)) - }) { - Ok((_prop, kind)) => kind, - Err(e) => match e { - PropertyMapError::ParseError((prop, err)) => { - report_err!( - &mut reports, - token.source(), - "Invalid Tex Property".into(), - span( - token.range.clone(), - format!( - "Property `kind: {}` cannot be converted: {}", - prop.fg(state.parser.colors().info), - err.fg(state.parser.colors().error) - ) - ) - ); - return reports; - } - PropertyMapError::NotFoundError(_) => { - if index == 1 { - TexKind::Inline - } else { - TexKind::Block - } - } - }, + let (tex_kind, caption, tex_env) = match ( + properties.get_or( + &mut reports, + "kind", + if index == 1 { + TexKind::Inline + } else { + TexKind::Block + }, + |_, value| TexKind::from_str(value.value.as_str()), + ), + properties.get_opt(&mut reports, "caption", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + properties.get(&mut reports, "env", |_, value| { + Result::<_, String>::Ok(value.value.clone()) + }), + ) { + (Some(tex_kind), Some(caption), Some(tex_env)) => (tex_kind, caption, tex_env), + _ => return reports, }; - // Caption - let caption = properties - .get("caption", |_, value| -> Result { - Ok(value.clone()) - }) - .ok() - .map(|(_, value)| value); - - // Environ - let tex_env = properties - .get("env", |_, value| -> Result { - Ok(value.clone()) - }) - .ok() - .map(|(_, value)| value) - .unwrap(); - state.push( document, Box::new(Tex { mathmode: index == 1, location: token.clone(), kind: tex_kind, - env: tex_env.to_string(), + env: tex_env, tex: tex_content, caption, }), @@ -432,7 +370,6 @@ impl RegexRule for TexRule { ); if let Some(props) = matches.get(1).map(|m| m.range()) { sems.add(props.start - 1..props.start, tokens.tex_props_sep); - sems.add(props.clone(), tokens.tex_props); sems.add(props.end..props.end + 1, tokens.tex_props_sep); } sems.add(matches.get(2).unwrap().range(), tokens.tex_content); @@ -570,7 +507,7 @@ $[kind=block,env=another] e^{i\pi}=-1$ ); validate_document!(doc.content().borrow(), 0, - Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) }; + Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\\\".to_string()) }; Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another" }; Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" }; Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) }; @@ -604,7 +541,7 @@ $[env=another] e^{i\pi}=-1$ validate_document!(doc.content().borrow(), 0, Paragraph { - Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) }; + Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\\\".to_string()) }; Tex { mathmode == false, tex == "Non Math \\LaTeX", env == "another", caption == Some("Enclosed ].".to_string()) }; Tex { mathmode == true, tex == "e^{i\\pi}=-1", env == "another" }; Tex { mathmode == true, tex == "1+1=2", env == "main", caption == Some("Some, text\\".to_string()) }; @@ -634,8 +571,10 @@ $[kind=inline]\LaTeX$ validate_semantics!(state, source.clone(), 0, tex_sep { delta_line == 1, delta_start == 0, length == 1 }; tex_props_sep { delta_line == 0, delta_start == 1, length == 1 }; - tex_props { delta_line == 0, delta_start == 1, length == 11 }; - tex_props_sep { delta_line == 0, delta_start == 11, length == 1 }; + prop_name { delta_line == 0, delta_start == 1, length == 4 }; + prop_equal { delta_line == 0, delta_start == 4, length == 1 }; + prop_value { delta_line == 0, delta_start == 1, length == 6 }; + tex_props_sep { delta_line == 0, delta_start == 6, length == 1 }; tex_content { delta_line == 0, delta_start == 1, length == 6 }; tex_sep { delta_line == 0, delta_start == 6, length == 1 }; ); diff --git a/src/lsp/semantic.rs b/src/lsp/semantic.rs index e61e9dc..77ec64f 100644 --- a/src/lsp/semantic.rs +++ b/src/lsp/semantic.rs @@ -96,6 +96,11 @@ pub struct Tokens { pub section_kind: (u32, u32), pub section_name: (u32, u32), + pub prop_equal: (u32, u32), + pub prop_comma: (u32, u32), + pub prop_name: (u32, u32), + pub prop_value: (u32, u32), + pub comment: (u32, u32), pub link_display_sep: (u32, u32), @@ -117,7 +122,6 @@ pub struct Tokens { pub reference_doc: (u32, u32), pub reference_link: (u32, u32), pub reference_props_sep: (u32, u32), - pub reference_props: (u32, u32), pub variable_operator: (u32, u32), pub variable_kind: (u32, u32), @@ -135,7 +139,6 @@ pub struct Tokens { pub code_sep: (u32, u32), pub code_props_sep: (u32, u32), - pub code_props: (u32, u32), pub code_lang: (u32, u32), pub code_title: (u32, u32), pub code_content: (u32, u32), @@ -148,31 +151,25 @@ pub struct Tokens { pub list_bullet: (u32, u32), pub list_props_sep: (u32, u32), - pub list_props: (u32, u32), pub blockquote_marker: (u32, u32), pub blockquote_props_sep: (u32, u32), - pub blockquote_props: (u32, u32), pub raw_sep: (u32, u32), pub raw_props_sep: (u32, u32), - pub raw_props: (u32, u32), pub raw_content: (u32, u32), pub tex_sep: (u32, u32), pub tex_props_sep: (u32, u32), - pub tex_props: (u32, u32), pub tex_content: (u32, u32), pub graph_sep: (u32, u32), pub graph_props_sep: (u32, u32), - pub graph_props: (u32, u32), pub graph_content: (u32, u32), pub layout_sep: (u32, u32), pub layout_token: (u32, u32), pub layout_props_sep: (u32, u32), - pub layout_props: (u32, u32), pub layout_type: (u32, u32), pub toc_sep: (u32, u32), @@ -185,7 +182,6 @@ pub struct Tokens { pub media_uri_sep: (u32, u32), pub media_uri: (u32, u32), pub media_props_sep: (u32, u32), - pub media_props: (u32, u32), } impl Tokens { @@ -196,6 +192,11 @@ impl Tokens { section_kind: token!("enum"), section_name: token!("string"), + prop_equal: token!("operator"), + prop_comma: token!("operator"), + prop_name: token!("class"), + prop_value: token!("enum"), + comment: token!("comment"), link_display_sep: token!("macro"), @@ -217,7 +218,6 @@ impl Tokens { reference_doc: token!("function"), reference_link: token!("macro"), reference_props_sep: token!("operator"), - reference_props: token!("enum"), variable_operator: token!("operator"), variable_kind: token!("operator"), @@ -235,7 +235,6 @@ impl Tokens { code_sep: token!("operator"), code_props_sep: token!("operator"), - code_props: token!("enum"), code_lang: token!("function"), code_title: token!("number"), code_content: token!("string"), @@ -248,31 +247,25 @@ impl Tokens { list_bullet: token!("macro"), list_props_sep: token!("operator"), - list_props: token!("enum"), blockquote_marker: token!("macro"), blockquote_props_sep: token!("operator"), - blockquote_props: token!("enum"), raw_sep: token!("operator"), raw_props_sep: token!("operator"), - raw_props: token!("enum"), raw_content: token!("string"), tex_sep: token!("modifier"), tex_props_sep: token!("operator"), - tex_props: token!("enum"), tex_content: token!("string"), graph_sep: token!("modifier"), graph_props_sep: token!("operator"), - graph_props: token!("enum"), graph_content: token!("string"), layout_sep: token!("number"), layout_token: token!("number"), layout_props_sep: token!("operator"), - layout_props: token!("enum"), layout_type: token!("function"), toc_sep: token!("number"), @@ -285,7 +278,6 @@ impl Tokens { media_uri_sep: token!("macro"), media_uri: token!("function"), media_props_sep: token!("operator"), - media_props: token!("enum"), } } } diff --git a/src/lua/kernel.rs b/src/lua/kernel.rs index 4aa81bd..0d16197 100644 --- a/src/lua/kernel.rs +++ b/src/lua/kernel.rs @@ -6,6 +6,7 @@ use mlua::Lua; use crate::document::document::Document; use crate::parser::parser::Parser; use crate::parser::parser::ParserState; +use crate::parser::reports::Report; use crate::parser::source::Token; /// Redirected data from lua execution @@ -21,6 +22,7 @@ pub struct KernelContext<'a, 'b, 'c> { pub state: &'a ParserState<'a, 'b>, pub document: &'c dyn Document<'c>, pub redirects: Vec, + pub reports: Vec, } impl<'a, 'b, 'c> KernelContext<'a, 'b, 'c> { @@ -34,6 +36,7 @@ impl<'a, 'b, 'c> KernelContext<'a, 'b, 'c> { state, document, redirects: vec![], + reports: vec![], } } } diff --git a/src/parser/langparser.rs b/src/parser/langparser.rs index 6d7792f..967757b 100644 --- a/src/parser/langparser.rs +++ b/src/parser/langparser.rs @@ -105,12 +105,11 @@ impl<'b> Parser for LangParser<'b> { .downcast_rc::() .ok() .map(|source| { - if source.path().is_empty() // Test mode + if source.path().is_empty() + // Test mode { None - } - else - { + } else { let start = if source.path().starts_with("file:///") { 7 } else { @@ -119,7 +118,9 @@ impl<'b> Parser for LangParser<'b> { let mut path = PathBuf::from(&source.path()[start..]); match path.canonicalize() { Ok(cano) => path = cano, - Err(err) => eprintln!("Failed to canonicalize path `{}`: {err}", source.path()), + Err(err) => { + eprintln!("Failed to canonicalize path `{}`: {err}", source.path()) + } } path.pop(); Some(path) @@ -217,8 +218,7 @@ impl<'b> Parser for LangParser<'b> { ); } - if path.is_some() - { + if path.is_some() { if let Err(err) = std::env::set_current_dir(¤t_dir) { println!( "Failed to set working directory to `{}`: {err} {source:#?}", diff --git a/src/parser/layout.rs b/src/parser/layout.rs index febb9ef..dc0c440 100644 --- a/src/parser/layout.rs +++ b/src/parser/layout.rs @@ -7,13 +7,22 @@ use crate::compiler::compiler::Compiler; use crate::document::document::Document; use crate::elements::layout::LayoutToken; +use super::parser::ParserState; +use super::reports::Report; +use super::source::Token; + /// Represents the type of a layout pub trait LayoutType: core::fmt::Debug { /// Name of the layout fn name(&self) -> &'static str; /// Parses layout properties - fn parse_properties(&self, properties: &str) -> Result>, String>; + fn parse_properties( + &self, + reports: &mut Vec, + state: &ParserState, + token: Token, + ) -> Option>; /// Expected number of blocks fn expects(&self) -> Range; @@ -23,7 +32,7 @@ pub trait LayoutType: core::fmt::Debug { &self, token: LayoutToken, id: usize, - properties: &Option>, + properties: &Box, compiler: &Compiler, document: &dyn Document, ) -> Result; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 64668f2..b06600f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2,6 +2,7 @@ pub mod customstyle; pub mod langparser; pub mod layout; pub mod parser; +pub mod property; pub mod reports; pub mod rule; pub mod source; diff --git a/src/parser/property.rs b/src/parser/property.rs new file mode 100644 index 0000000..f1a18b4 --- /dev/null +++ b/src/parser/property.rs @@ -0,0 +1,522 @@ +use std::collections::HashMap; +use std::fmt::Display; +use std::ops::Range; + +use ariadne::Fmt; +use lsp::semantic::Semantics; + +use crate::parser::reports::macros::*; +use crate::parser::reports::*; + +use super::parser::ParserState; +use super::reports::Report; +use super::source::Token; + +#[derive(Debug)] +pub struct Property { + description: String, + default: Option, +} + +impl Property { + pub fn new(description: String, default: Option) -> Self { + Self { + description, + default, + } + } +} + +impl core::fmt::Display for Property { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.default.as_ref() { + None => write!(f, "{}", self.description), + Some(default) => write!(f, "{} (Default: {})", self.description, default), + } + } +} + +/// The parsed value of a property +/// +/// If property was set using default, the ranges correspond to the total range of the property string +/// The ranges are used to provide diagnostics as well as semantics via the language server. +#[derive(Debug)] +pub struct PropertyValue { + pub name_range: Range, + pub value_range: Range, + pub value: String, +} + +pub struct PropertyMap<'a> { + pub token: Token, + pub rule_name: &'a str, + pub state: &'a ParserState<'a, 'a>, + pub properties: HashMap, +} + +impl<'a> PropertyMap<'a> { + pub fn new(token: Token, rule_name: &'a str, state: &'a ParserState<'a, 'a>) -> Self { + Self { + token, + rule_name, + state, + properties: HashMap::new(), + } + } + + /// Get a value by key + /// + /// # Returned value + /// + /// * `Some(T)` on success + /// * `None` if the key is not found or the parsing function `f` fails + /// (Note) In this case, reports should have been set + pub fn get(&self, mut reports: &mut Vec, key: &str, f: F) -> Option + where + F: FnOnce(&Property, &PropertyValue) -> Result, + E: Display, + { + match self.properties.get(key) { + None => report_err!( + &mut reports, + self.token.source(), + format!("Failed to parse {} properties", self.rule_name), + span( + self.token.range.clone(), + format!( + "Missing property {}", + key.fg(self.state.parser.colors().info), + ) + ), + ), + Some((prop, val)) => match f(prop, val) { + Err(err) => report_err!( + &mut reports, + self.token.source(), + format!("Failed to parse {} properties", self.rule_name), + span( + val.value_range.clone(), + format!( + "Unable to parse property {}: {err}", + key.fg(self.state.parser.colors().info), + ) + ), + ), + Ok(parsed) => return Some(parsed), + }, + } + None + } + + /// Get a value by key with a default + /// + /// # Returned value + /// + /// * `Some(T)` on success + /// * `None` if the parsing function `f` fails + /// (Note) In this case, reports should have been set + pub fn get_or( + &self, + mut reports: &mut Vec, + key: &str, + default: T, + f: F, + ) -> Option + where + F: FnOnce(&Property, &PropertyValue) -> Result, + E: Display, + { + match self.properties.get(key) { + None => return Some(default), + Some((prop, val)) => match f(prop, val) { + Err(err) => report_err!( + &mut reports, + self.token.source(), + format!("Failed to parse {} properties", self.rule_name), + span( + val.value_range.clone(), + format!( + "Unable to parse property {}: {err}", + key.fg(self.state.parser.colors().info), + ) + ), + ), + Ok(parsed) => return Some(parsed), + }, + } + None + } + + /// Get an optional value by key + /// + /// # Returned value + /// + /// * `Some(Option)` on success + /// * `None` if the parsing function `f` fails + /// (Note) In this case, reports should have been set + pub fn get_opt( + &self, + mut reports: &mut Vec, + key: &str, + f: F, + ) -> Option> + where + F: FnOnce(&Property, &PropertyValue) -> Result, + E: Display, + { + match self.properties.get(key) { + None => return Some(None), + Some((prop, val)) => match f(prop, val) { + Err(err) => report_err!( + &mut reports, + self.token.source(), + format!("Failed to parse {} properties", self.rule_name), + span( + val.value_range.clone(), + format!( + "Unable to parse property {}: {err}", + key.fg(self.state.parser.colors().info), + ) + ), + ), + Ok(parsed) => return Some(Some(parsed)), + }, + } + None + } +} + +#[derive(Debug)] +pub struct PropertyParser { + pub properties: HashMap, +} + +impl PropertyParser { + fn allowed_properties(&self, state: &ParserState) -> String { + self.properties + .iter() + .fold(String::new(), |out, (name, prop)| { + out + format!( + "\n - {} : {}", + name.fg(state.parser.colors().info), + prop.description + ) + .as_str() + }) + } + + /// Parses properties string "prop1=value1, prop2 = val\,2" -> {prop1: value1, prop2: val,2} + /// + /// # Key-value pair + /// + /// Property names/values are separated by a single '=' that cannot be escaped. + /// Therefore names cannot contain the '=' character. + /// + /// # Language Server + /// + /// This function also processes properties to add them to the language server's semantics. + /// It uses the [`Semantics::add_to_queue()`] so it is safe to add other semantics after this function call. + /// + /// # Example + /// + /// ``` + /// let mut properties = HashMap::new(); + /// properties.insert("width".to_string(), + /// Property::new("Width of the element in em".to_string(), None)); + /// + /// let parser = PropertyParser { properties }; + /// let source = VirtualSource::new(.. "width=15" ..) + /// let properties = match parser.parse("Element", &mut reports, &state, source).unwrap() + /// { + /// Some(properties) => properties, + /// None => return reports, + /// }; + /// + /// assert_eq!(properties.get(&mut reports, "width", |_, value| value.parse::()).unwrap(), 15); + /// ``` + /// # Return value + /// + /// `Some(properties)` is returned on success. On failure, `None` is returned and the reports will have been populated. + /// + /// Note: Only ',' inside values can be escaped, other '\' are treated literally + pub fn parse<'a>( + &'a self, + rule_name: &'a str, + mut reports: &mut Vec, + state: &'a ParserState<'a, 'a>, + token: Token, + ) -> Option> { + let mut pm = PropertyMap::new(token.clone(), rule_name, state); + let mut try_insert = |name: &String, + name_range: Range, + value: &String, + value_range: Range| + -> bool { + let trimmed_name = name.trim_start().trim_end(); + let trimmed_value = value.trim_start().trim_end(); + let prop = match self.properties.get(trimmed_name) { + None => { + report_err!( + &mut reports, + token.source(), + format!("Failed to parse {rule_name} properties"), + span( + name_range, + format!( + "Unknown property {}, allowed properties:{}", + name.fg(state.parser.colors().info), + self.allowed_properties(state) + ) + ), + ); + return false; + } + Some(prop) => prop, + }; + + if let Some((_, previous)) = pm.properties.insert( + trimmed_name.to_string(), + ( + prop, + PropertyValue { + name_range: name_range.clone(), + value_range: value_range.clone(), + value: trimmed_value.to_string(), + }, + ), + ) { + report_err!( + &mut reports, + token.source(), + format!("Failed to parse {rule_name} properties"), + span( + name_range.start..value_range.end, + format!( + "Duplicate property {}, current value: {}", + name.fg(state.parser.colors().info), + trimmed_value.fg(state.parser.colors().info), + ) + ), + span( + previous.value_range.clone(), + format!( + "Previous value: {}", + previous.value.fg(state.parser.colors().info), + ) + ) + ); + } + + true + }; + + if token.range.len() != 0 { + let mut in_name = true; + let mut name = String::new(); + let mut name_range = token.start()..token.start(); + let mut value = String::new(); + let mut value_range = token.start()..token.start(); + let mut escaped = 0usize; + for (pos, c) in token.source().content()[token.range.clone()].char_indices() { + if c == '\\' { + escaped += 1; + } else if c == '=' && in_name { + name_range.end = pos; + value_range.start = pos + 1; + in_name = false; + (0..escaped).for_each(|_| name.push('\\')); + escaped = 0; + } else if c == ',' && !in_name { + if escaped % 2 == 0 + // Not escaped + { + (0..escaped / 2).for_each(|_| value.push('\\')); + escaped = 0; + in_name = true; + value_range.end = pos; + + if !try_insert(&name, name_range.clone(), &value, value_range.clone()) { + return None; + } + name_range.start = pos + 1; + + name.clear(); + value.clear(); + } else { + (0..(escaped - 1) / 2).for_each(|_| value.push('\\')); + value.push(','); + escaped = 0; + } + } else { + if in_name { + (0..escaped).for_each(|_| name.push('\\')); + name.push(c); + } else { + (0..escaped).for_each(|_| value.push('\\')); + value.push(c); + } + escaped = 0; + } + } + (0..escaped).for_each(|_| value.push('\\')); + if !in_name && value.trim_end().trim_start().is_empty() { + report_err!( + &mut reports, + token.source(), + format!("Failed to parse {rule_name} properties"), + span( + value_range.start..token.end(), + format!("Expected value after last '='",) + ), + ); + return None; + } else if name.is_empty() || value.is_empty() { + report_err!( + &mut reports, + token.source(), + format!("Failed to parse {rule_name} properties"), + span( + name_range.start..token.end(), + format!("Expected name/value pair after last ','",) + ), + ); + return None; + } + + value_range.end = token.end(); + if !try_insert(&name, name_range.clone(), &value, value_range.clone()) { + return None; + } + } + + if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) { + for (_, (_, value)) in &pm.properties { + if value.name_range.start != 0 { + sems.add_to_queue( + value.name_range.start - 1..value.name_range.start, + tokens.prop_comma, + ); + } + sems.add_to_queue(value.name_range.clone(), tokens.prop_name); + sems.add_to_queue( + value.name_range.end..value.value_range.start, + tokens.prop_equal, + ); + sems.add_to_queue(value.value_range.clone(), tokens.prop_value); + } + //sems.add(matches.get(0).unwrap().start()..matches.get(0).unwrap().start()+1, tokens.media_sep); + // Refname + //sems.add(matches.get(0).unwrap().start()+1..matches.get(0).unwrap().start()+2, tokens.media_refname_sep); + //sems.add(matches.get(1).unwrap().range(), tokens.media_refname); + //sems.add(matches.get(1).unwrap().end()..matches.get(1).unwrap().end()+1, tokens.media_refname_sep); + } + + // Insert missing properties with a default + for (name, prop) in &self.properties { + if pm.properties.contains_key(name) { + continue; + } + if let Some(default) = &prop.default { + pm.properties.insert( + name.to_owned(), + ( + prop, + PropertyValue { + name_range: token.range.clone(), + value_range: token.range.clone(), + value: default.to_owned(), + }, + ), + ); + } + } + + Some(pm) + } +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use parser::langparser::LangParser; + use parser::source::Source; + use parser::source::SourceFile; + + use super::*; + + #[test] + fn property_parser_tests() { + let mut properties = HashMap::new(); + properties.insert( + "width".to_string(), + Property::new("Width of the element in em".to_string(), None), + ); + properties.insert( + "length".to_string(), + Property::new("Length in cm".to_string(), None), + ); + properties.insert( + "angle".to_string(), + Property::new("Angle in degrees".to_string(), Some("180".to_string())), + ); + properties.insert( + "weight".to_string(), + Property::new("Weight in %".to_string(), Some("0.42".to_string())), + ); + + let langparser = LangParser::default(); + let state = ParserState::new(&langparser, None); + let mut reports = vec![]; + + let parser = PropertyParser { properties }; + let source = Rc::new(SourceFile::with_content( + "".into(), + "width=15,length=-10".into(), + None, + )) as Rc; + let pm = parser + .parse("Test", &mut reports, &state, source.into()) + .unwrap(); + + // Ok + assert_eq!( + pm.get(&mut reports, "width", |_, s| s.value.parse::()) + .unwrap(), + 15 + ); + assert_eq!( + pm.get(&mut reports, "length", |_, s| s.value.parse::()) + .unwrap(), + -10 + ); + assert_eq!( + pm.get(&mut reports, "angle", |_, s| s.value.parse::()) + .unwrap(), + 180f64 + ); + assert_eq!( + pm.get(&mut reports, "angle", |_, s| s.value.parse::()) + .unwrap(), + 180 + ); + assert_eq!( + pm.get(&mut reports, "weight", |_, s| s.value.parse::()) + .unwrap(), + 0.42f32 + ); + assert_eq!( + pm.get(&mut reports, "weight", |_, s| s.value.parse::()) + .unwrap(), + 0.42f64 + ); + + // Error + assert!(pm + .get(&mut reports, "length", |_, s| s.value.parse::()) + .is_none()); + assert!(pm + .get(&mut reports, "height", |_, s| s.value.parse::()) + .is_none()); + } +} diff --git a/src/parser/source.rs b/src/parser/source.rs index 275d3a2..328a185 100644 --- a/src/parser/source.rs +++ b/src/parser/source.rs @@ -322,3 +322,12 @@ impl Token { pub fn end(&self) -> usize { self.range.end } } + +impl From> for Token { + fn from(source: Rc) -> Self { + Self { + range: 0..source.content().len(), + source, + } + } +} diff --git a/src/parser/util.rs b/src/parser/util.rs index c3ada4b..50afd95 100644 --- a/src/parser/util.rs +++ b/src/parser/util.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::ops::Range; use std::rc::Rc; @@ -233,226 +232,6 @@ pub fn parse_paragraph<'a>( Ok(paragraph.downcast::().unwrap()) } -#[derive(Debug)] -pub struct Property { - required: bool, - description: String, - default: Option, -} - -impl Property { - pub fn new(required: bool, description: String, default: Option) -> Self { - Self { - required, - description, - default, - } - } -} - -impl core::fmt::Display for Property { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.default.as_ref() { - None => write!( - f, - "{} {}", - ["[Opt]", "[Req]"][self.required as usize], - self.description - ), - Some(default) => write!( - f, - "{} {} (Deafult: {})", - ["[Opt]", "[Req]"][self.required as usize], - self.description, - default - ), - } - } -} - -#[derive(Debug)] -pub enum PropertyMapError { - ParseError(E), - NotFoundError(String), -} - -#[derive(Debug)] -pub struct PropertyMap<'a> { - pub(crate) properties: HashMap, -} - -impl<'a> PropertyMap<'a> { - pub fn new() -> Self { - Self { - properties: HashMap::new(), - } - } - - pub fn get Result>( - &self, - name: &str, - f: F, - ) -> Result<(&'a Property, T), PropertyMapError> { - let (prop, value) = match self.properties.get(name) { - Some(found) => found, - None => { - return Err(PropertyMapError::NotFoundError(format!( - "Property `{name}` not found" - ))) - } - }; - - match f(prop, value) { - Ok(parsed) => Ok((*prop, parsed)), - Err(err) => Err(PropertyMapError::ParseError(err)), - } - } -} - -#[derive(Debug)] -pub struct PropertyParser { - pub properties: HashMap, -} - -impl PropertyParser { - /// Attempts to build a default propertymap - /// - /// Returns an error if at least one [`Property`] is required and doesn't provide a default - pub fn default(&self) -> Result, String> { - let mut properties = PropertyMap::new(); - - for (name, prop) in &self.properties { - match (prop.required, prop.default.as_ref()) { - (true, None) => return Err(format!("Missing property `{name}` {prop}")), - (false, None) => {} - (_, Some(default)) => { - properties - .properties - .insert(name.clone(), (prop, default.clone())); - } - } - } - - Ok(properties) - } - - /// Parses properties string "prop1=value1, prop2 = val\,2" -> {prop1: value1, prop2: val,2} - /// - /// # Key-value pair - /// - /// Property names/values are separated by a single '=' that cannot be escaped. - /// Therefore names cannot contain the '=' character. - /// - /// # Example - /// - /// ``` - /// let mut properties = HashMap::new(); - /// properties.insert("width".to_string(), - /// Property::new(true, "Width of the element in em".to_string(), None)); - /// - /// let parser = PropertyParser { properties }; - /// let pm = parser.parse("width=15").unwrap(); - /// - /// assert_eq!(pm.get("width", |_, s| s.parse::()).unwrap().1, 15); - /// ``` - /// # Return value - /// - /// Returns the parsed property map, or an error if either: - /// * A required property is missing - /// * An unknown property is present - /// * A duplicate property is present - /// - /// Note: Only ',' inside values can be escaped, other '\' are treated literally - pub fn parse(&self, content: &str) -> Result, String> { - let mut properties = PropertyMap::new(); - let mut try_insert = |name: &String, value: &String| -> Result<(), String> { - let trimmed_name = name.trim_end().trim_start(); - let trimmed_value = value.trim_end().trim_start(); - let prop = match self.properties.get(trimmed_name) - { - None => return Err(format!("Unknown property name: `{trimmed_name}` (with value: `{trimmed_value}`). Valid properties are:\n{}", - self.properties.iter().fold(String::new(), - |out, (name, prop)| out + format!(" - {name}: {prop}\n").as_str()))), - Some(prop) => prop - }; - - if let Some((_, previous)) = properties - .properties - .insert(trimmed_name.to_string(), (prop, trimmed_value.to_string())) - { - return Err(format!("Duplicate property `{trimmed_name}`, previous value: `{previous}` current value: `{trimmed_value}`")); - } - - Ok(()) - }; - - let mut in_name = true; - let mut name = String::new(); - let mut value = String::new(); - let mut escaped = 0usize; - for c in content.chars() { - if c == '\\' { - escaped += 1; - } else if c == '=' && in_name { - in_name = false; - (0..escaped).for_each(|_| name.push('\\')); - escaped = 0; - } else if c == ',' && !in_name { - if escaped % 2 == 0 - // Not escaped - { - (0..escaped / 2).for_each(|_| value.push('\\')); - escaped = 0; - in_name = true; - - try_insert(&name, &value)?; - - name.clear(); - value.clear(); - } else { - (0..(escaped - 1) / 2).for_each(|_| value.push('\\')); - value.push(','); - escaped = 0; - } - } else { - if in_name { - (0..escaped).for_each(|_| name.push('\\')); - name.push(c) - } else { - (0..escaped).for_each(|_| value.push('\\')); - value.push(c) - } - escaped = 0; - } - } - (0..escaped).for_each(|_| value.push('\\')); - if !in_name && value.trim_end().trim_start().is_empty() { - return Err("Expected a value after last `=`".to_string()); - } else if name.is_empty() || value.is_empty() { - return Err("Expected non empty property list.".to_string()); - } - - try_insert(&name, &value)?; - - if let Err(e) = self.properties.iter().try_for_each(|(key, prop)| { - if !properties.properties.contains_key(key) { - if let Some(default) = &prop.default { - properties - .properties - .insert(key.clone(), (prop, default.clone())); - } else if prop.required { - return Err(format!("Missing required property: {prop}")); - } - } - Ok(()) - }) { - Err(e) - } else { - Ok(properties) - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -548,65 +327,4 @@ mod tests { assert_eq!(escape_text('\\', ")", "A\\)B\\"), "A)B".to_string(),); assert_eq!(escape_text('\\', ")", "A\\)B\\\\"), "A)B\\".to_string(),); } - - #[test] - fn property_parser_tests() { - let mut properties = HashMap::new(); - properties.insert( - "width".to_string(), - Property::new(true, "Width of the element in em".to_string(), None), - ); - properties.insert( - "length".to_string(), - Property::new(false, "Length in cm".to_string(), None), - ); - properties.insert( - "angle".to_string(), - Property::new( - true, - "Angle in degrees".to_string(), - Some("180".to_string()), - ), - ); - properties.insert( - "weight".to_string(), - Property::new(false, "Weight in %".to_string(), Some("0.42".to_string())), - ); - - let parser = PropertyParser { properties }; - let pm = parser.parse("width=15,length=-10").unwrap(); - - // Ok - assert_eq!(pm.get("width", |_, s| s.parse::()).unwrap().1, 15); - assert_eq!(pm.get("length", |_, s| s.parse::()).unwrap().1, -10); - assert_eq!(pm.get("angle", |_, s| s.parse::()).unwrap().1, 180f64); - assert_eq!(pm.get("angle", |_, s| s.parse::()).unwrap().1, 180); - assert_eq!( - pm.get("weight", |_, s| s.parse::()).unwrap().1, - 0.42f32 - ); - assert_eq!( - pm.get("weight", |_, s| s.parse::()).unwrap().1, - 0.42f64 - ); - - // Error - assert!(pm.get("length", |_, s| s.parse::()).is_err()); - assert!(pm.get("height", |_, s| s.parse::()).is_err()); - - // Missing property - assert!(parser.parse("length=15").is_err()); - - // Defaults - assert!(parser.parse("width=15").is_ok()); - assert_eq!( - parser - .parse("width=0,weight=0.15") - .unwrap() - .get("weight", |_, s| s.parse::()) - .unwrap() - .1, - 0.15f32 - ); - } }