PropertyParser refactor

This commit is contained in:
ef3d0c3e 2024-11-01 22:15:33 +01:00
parent fc7ff70090
commit 357c8a18bd
20 changed files with 987 additions and 1094 deletions

View file

@ -7,7 +7,7 @@ use blockquote_style::AuthorPos::After;
use blockquote_style::AuthorPos::Before; use blockquote_style::AuthorPos::Before;
use blockquote_style::BlockquoteStyle; use blockquote_style::BlockquoteStyle;
use lsp::semantic::Semantics; use lsp::semantic::Semantics;
use regex::Match; use parser::util::escape_source;
use regex::Regex; use regex::Regex;
use runtime_format::FormatArgs; use runtime_format::FormatArgs;
use runtime_format::FormatKey; use runtime_format::FormatKey;
@ -24,6 +24,8 @@ use crate::elements::paragraph::Paragraph;
use crate::elements::text::Text; use crate::elements::text::Text;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*; use crate::parser::reports::macros::*;
use crate::parser::reports::*; use crate::parser::reports::*;
use crate::parser::rule::Rule; use crate::parser::rule::Rule;
@ -31,9 +33,6 @@ use crate::parser::source::Cursor;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::source::VirtualSource; use crate::parser::source::VirtualSource;
use crate::parser::style::StyleHolder; use crate::parser::style::StyleHolder;
use crate::parser::util::escape_text;
use crate::parser::util::Property;
use crate::parser::util::PropertyParser;
#[derive(Debug)] #[derive(Debug)]
pub struct Blockquote { pub struct Blockquote {
@ -185,15 +184,15 @@ impl BlockquoteRule {
let mut props = HashMap::new(); let mut props = HashMap::new();
props.insert( props.insert(
"author".to_string(), "author".to_string(),
Property::new(false, "Quote author".to_string(), None), Property::new("Quote author".to_string(), None),
); );
props.insert( props.insert(
"cite".to_string(), "cite".to_string(),
Property::new(false, "Quote source".to_string(), None), Property::new("Quote source".to_string(), None),
); );
props.insert( props.insert(
"url".to_string(), "url".to_string(),
Property::new(false, "Quote source url".to_string(), None), Property::new("Quote source url".to_string(), None),
); );
Self { Self {
@ -202,29 +201,6 @@ impl BlockquoteRule {
properties: PropertyParser { properties: props }, properties: PropertyParser { properties: props },
} }
} }
fn parse_properties(
&self,
m: Match,
) -> Result<(Option<String>, Option<String>, Option<String>), String> {
let processed = escape_text('\\', "]", m.as_str());
let pm = self.properties.parse(processed.as_str())?;
let author = pm
.get("author", |_, s| -> Result<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
let cite = pm
.get("cite", |_, s| -> Result<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
let url = pm
.get("url", |_, s| -> Result<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
Ok((author, cite, url))
}
} }
impl Rule for BlockquoteRule { impl Rule for BlockquoteRule {
@ -265,23 +241,35 @@ impl Rule for BlockquoteRule {
end_cursor = end_cursor.at(captures.get(0).unwrap().end()); end_cursor = end_cursor.at(captures.get(0).unwrap().end());
// Properties // Properties
let mut author = None; let prop_source = escape_source(
let mut cite = None; end_cursor.source.clone(),
let mut url = None; captures.get(1).map_or(0..0, |m| m.range()),
if let Some(properties) = captures.get(1) { "Blockquote Properties".into(),
match self.parse_properties(properties) { '\\',
Err(err) => { "]",
report_err!( );
&mut reports, let properties =
cursor.source.clone(), match self
"Invalid Blockquote Properties".into(), .properties
span(properties.range(), err) .parse("Blockquote", &mut reports, state, prop_source.into())
); {
return (end_cursor, reports); Some(props) => props,
} None => return (end_cursor, reports),
Ok(props) => (author, cite, url) = props, };
} 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)) = if let Some((sems, tokens)) =
Semantics::from_source(cursor.source.clone(), &state.shared.lsp) 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); sems.add(start..start + 1, tokens.blockquote_marker);
if let Some(props) = captures.get(1).map(|m| m.range()) { 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.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); sems.add(props.end..props.end + 1, tokens.blockquote_props_sep);
} }
} }

View file

@ -6,6 +6,7 @@ use crypto::digest::Digest;
use crypto::sha2::Sha512; use crypto::sha2::Sha512;
use mlua::Function; use mlua::Function;
use mlua::Lua; use mlua::Lua;
use parser::util::escape_source;
use regex::Captures; use regex::Captures;
use regex::Regex; use regex::Regex;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
@ -23,13 +24,12 @@ use crate::lsp::semantic::Semantics;
use crate::lua::kernel::CTX; use crate::lua::kernel::CTX;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*; use crate::parser::reports::macros::*;
use crate::parser::reports::*; use crate::parser::reports::*;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Token; 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 crate::parser::util::{self};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -291,11 +291,7 @@ impl CodeRule {
let mut props = HashMap::new(); let mut props = HashMap::new();
props.insert( props.insert(
"line_offset".to_string(), "line_offset".to_string(),
Property::new( Property::new("Line number offset".to_string(), Some("1".to_string())),
true,
"Line number offset".to_string(),
Some("1".to_string()),
),
); );
Self { Self {
re: [ re: [
@ -332,39 +328,22 @@ impl RegexRule for CodeRule {
) -> Vec<Report> { ) -> Vec<Report> {
let mut reports = vec![]; let mut reports = vec![];
let properties = match matches.get(1) { // Properties
None => match self.properties.default() { let prop_source = escape_source(
Ok(properties) => properties, token.source(),
Err(e) => { matches.get(1).map_or(0..0, |m| m.range()),
report_err!( "Code Properties".into(),
&mut reports, '\\',
token.source(), "]",
"Invalid Code Properties".into(), );
span( let properties =
token.range.clone(), match self
format!("Code is missing properties: {e}") .properties
) .parse("Code", &mut reports, state, prop_source.into())
); {
return reports; Some(props) => props,
} None => 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,
}
}
};
let code_lang = match matches.get(2) { let code_lang = match matches.get(2) {
None => "Plain Text".to_string(), 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(); let code_name = name.as_str().trim_end().trim_start().to_string();
(!code_name.is_empty()).then_some(code_name) (!code_name.is_empty()).then_some(code_name)
}); });
let line_offset = match properties.get("line_offset", |prop, value| { let line_offset = match properties.get(&mut reports, "line_offset", |_, value| {
value.parse::<usize>().map_err(|e| (prop, e)) value.value.parse::<usize>()
}) { }) {
Ok((_prop, offset)) => offset, Some(line_offset) => line_offset,
Err(e) => match e { _ => return reports,
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;
}
},
}; };
state.push( state.push(
@ -520,7 +467,6 @@ impl RegexRule for CodeRule {
); );
if let Some(props) = matches.get(1).map(|m| m.range()) { 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.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); sems.add(props.end..props.end + 1, tokens.code_props_sep);
} }
if let Some(lang) = matches.get(2).map(|m| m.range()) { if let Some(lang) = matches.get(2).map(|m| m.range()) {
@ -794,8 +740,10 @@ test code
validate_semantics!(state, source.clone(), 0, validate_semantics!(state, source.clone(), 0,
code_sep { delta_line == 1, delta_start == 0, length == 3 }; code_sep { delta_line == 1, delta_start == 0, length == 3 };
code_props_sep { delta_line == 0, delta_start == 3, length == 1 }; code_props_sep { delta_line == 0, delta_start == 3, length == 1 };
code_props { delta_line == 0, delta_start == 1, length == 14 }; prop_name { delta_line == 0, delta_start == 1, length == 11 };
code_props_sep { delta_line == 0, delta_start == 14, length == 1 }; 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_lang { delta_line == 0, delta_start == 1, length == 2 };
code_title { delta_line == 0, delta_start == 3, length == 6 }; code_title { delta_line == 0, delta_start == 3, length == 6 };
code_content { delta_line == 1, delta_start == 0, length == 10 }; code_content { delta_line == 1, delta_start == 0, length == 10 };

View file

@ -55,9 +55,8 @@ impl CustomStyle for LuaCustomStyle {
let mut ctx = KernelContext::new(location.clone(), state, document); let mut ctx = KernelContext::new(location.clone(), state, document);
let mut reports = vec![]; let mut reports = vec![];
kernel.run_with_context(&mut ctx, |lua| { kernel.run_with_context(&mut ctx, |_lua| {
if let Err(err) = self.start.call::<_, ()>(()) if let Err(err) = self.start.call::<_, ()>(()) {
{
report_err!( report_err!(
&mut reports, &mut reports,
location.source(), location.source(),
@ -71,6 +70,7 @@ impl CustomStyle for LuaCustomStyle {
} }
}); });
reports.extend(ctx.reports);
reports reports
} }
@ -85,9 +85,8 @@ impl CustomStyle for LuaCustomStyle {
let mut ctx = KernelContext::new(location.clone(), state, document); let mut ctx = KernelContext::new(location.clone(), state, document);
let mut reports = vec![]; let mut reports = vec![];
kernel.run_with_context(&mut ctx, |lua| { kernel.run_with_context(&mut ctx, |_lua| {
if let Err(err) = self.end.call::<_, ()>(()) if let Err(err) = self.end.call::<_, ()>(()) {
{
report_err!( report_err!(
&mut reports, &mut reports,
location.source(), location.source(),
@ -101,6 +100,7 @@ impl CustomStyle for LuaCustomStyle {
} }
}); });
reports.extend(ctx.reports);
reports reports
} }
} }
@ -344,7 +344,13 @@ impl Rule for CustomStyleRule {
bindings.push(( bindings.push((
"define_toggled".into(), "define_toggled".into(),
lua.create_function( 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 mut result = Ok(());
let style = LuaCustomStyle { let style = LuaCustomStyle {

View file

@ -5,9 +5,8 @@ use std::sync::Once;
use crate::lua::kernel::CTX; use crate::lua::kernel::CTX;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::util::Property; use crate::parser::property::Property;
use crate::parser::util::PropertyMapError; use crate::parser::property::PropertyParser;
use crate::parser::util::PropertyParser;
use ariadne::Fmt; use ariadne::Fmt;
use crypto::digest::Digest; use crypto::digest::Digest;
use crypto::sha2::Sha512; use crypto::sha2::Sha512;
@ -18,6 +17,7 @@ use lsp::semantic::Semantics;
use mlua::Error::BadArgument; use mlua::Error::BadArgument;
use mlua::Function; use mlua::Function;
use mlua::Lua; use mlua::Lua;
use parser::util::escape_source;
use regex::Captures; use regex::Captures;
use regex::Regex; use regex::Regex;
@ -166,14 +166,13 @@ impl GraphRule {
props.insert( props.insert(
"layout".to_string(), "layout".to_string(),
Property::new( Property::new(
true,
"Graphviz layout engine see <https://graphviz.org/docs/layouts/>".to_string(), "Graphviz layout engine see <https://graphviz.org/docs/layouts/>".to_string(),
Some("dot".to_string()), Some("dot".to_string()),
), ),
); );
props.insert( props.insert(
"width".to_string(), "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 { Self {
re: [Regex::new( re: [Regex::new(
@ -239,98 +238,31 @@ impl RegexRule for GraphRule {
}; };
// Properties // Properties
let properties = match matches.get(1) { let prop_source = escape_source(
None => match self.properties.default() { token.source(),
Ok(properties) => properties, matches.get(1).map_or(0..0, |m| m.range()),
Err(e) => { "Graphviz Properties".into(),
report_err!( '\\',
&mut reports, "]",
token.source(), );
"Invalid Graph Properties".into(), let properties =
span( match self
token.range.clone(), .properties
format!("Graph is missing property: {e}") .parse("Graphviz", &mut reports, state, prop_source.into())
) {
); Some(props) => props,
return reports; None => return reports,
} };
}, let (graph_layout, graph_width) = match (
Some(props) => { properties.get(&mut reports, "layout", |_, value| {
let processed = layout_from_str(value.value.as_str())
util::escape_text('\\', "]", props.as_str().trim_start().trim_end()); }),
match self.properties.parse(processed.as_str()) { properties.get(&mut reports, "width", |_, value| {
Err(e) => { Result::<_, String>::Ok(value.value.clone())
report_err!( }),
&mut reports, ) {
token.source(), (Some(graph_layout), Some(graph_width)) => (graph_layout, graph_width),
"Invalid Graph Properties".into(), _ => return reports,
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<String, ()> {
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"),
},
}; };
state.push( state.push(
@ -348,7 +280,6 @@ impl RegexRule for GraphRule {
sems.add(range.start..range.start + 7, tokens.graph_sep); sems.add(range.start..range.start + 7, tokens.graph_sep);
if let Some(props) = matches.get(1).map(|m| m.range()) { 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.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(props.end..props.end + 1, tokens.graph_props_sep);
} }
sems.add(matches.get(2).unwrap().range(), tokens.graph_content); sems.add(matches.get(2).unwrap().range(), tokens.graph_content);
@ -490,8 +421,10 @@ digraph {
validate_semantics!(state, source.clone(), 0, validate_semantics!(state, source.clone(), 0,
graph_sep { delta_line == 1, delta_start == 0, length == 7 }; graph_sep { delta_line == 1, delta_start == 0, length == 7 };
graph_props_sep { delta_line == 0, delta_start == 7, length == 1 }; graph_props_sep { delta_line == 0, delta_start == 7, length == 1 };
graph_props { delta_line == 0, delta_start == 1, length == 9 }; prop_name { delta_line == 0, delta_start == 1, length == 5 };
graph_props_sep { delta_line == 0, delta_start == 9, length == 1 }; 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 == 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 == 10 };
graph_content { delta_line == 1, delta_start == 0, length == 2 }; graph_content { delta_line == 1, delta_start == 0, length == 2 };

View file

@ -15,11 +15,13 @@ use crate::parser::rule::RegexRule;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::state::RuleState; use crate::parser::state::RuleState;
use crate::parser::state::Scope; use crate::parser::state::Scope;
use crate::parser::util::escape_text;
use ariadne::Fmt; use ariadne::Fmt;
use mlua::Error::BadArgument; use mlua::Error::BadArgument;
use mlua::Function; use mlua::Function;
use mlua::Lua; use mlua::Lua;
use parser::source::Source;
use parser::source::VirtualSource;
use parser::util::escape_source;
use regex::Captures; use regex::Captures;
use regex::Match; use regex::Match;
use regex::Regex; use regex::Regex;
@ -54,8 +56,8 @@ impl FromStr for LayoutToken {
mod default_layouts { mod default_layouts {
use crate::parser::layout::LayoutType; use crate::parser::layout::LayoutType;
use crate::parser::util::Property; use crate::parser::property::Property;
use crate::parser::util::PropertyParser; use crate::parser::property::PropertyParser;
use super::*; use super::*;
@ -68,7 +70,6 @@ mod default_layouts {
properties.insert( properties.insert(
"style".to_string(), "style".to_string(),
Property::new( Property::new(
true,
"Additional style for the split".to_string(), "Additional style for the split".to_string(),
Some("".to_string()), Some("".to_string()),
), ),
@ -83,46 +84,38 @@ mod default_layouts {
fn expects(&self) -> Range<usize> { 1..1 } fn expects(&self) -> Range<usize> { 1..1 }
fn parse_properties(&self, properties: &str) -> Result<Option<Box<dyn Any>>, String> { fn parse_properties(
let props = if properties.is_empty() { &self,
self.0.default() reports: &mut Vec<Report>,
} else { state: &ParserState,
self.0.parse(properties) token: Token,
} ) -> Option<Box<dyn Any>> {
.map_err(|err| { let properties = match self.0.parse("Centered Layout", reports, state, token) {
format!( Some(props) => props,
"Failed to parse properties for layout {}: {err}", None => return None,
self.name() };
)
})?;
let style = props let style = match properties.get(reports, "style", |_, value| {
.get("style", |_, value| -> Result<String, ()> { Result::<_, String>::Ok(value.value.clone())
Ok(value.clone()) }) {
}) Some(style) => style,
.map_err(|err| format!("Failed to parse style: {err:#?}")) _ => return None,
.map(|(_, value)| value)?; };
Ok(Some(Box::new(style))) Some(Box::new(style))
} }
fn compile( fn compile(
&self, &self,
token: LayoutToken, token: LayoutToken,
_id: usize, _id: usize,
properties: &Option<Box<dyn Any>>, properties: &Box<dyn Any>,
compiler: &Compiler, compiler: &Compiler,
_document: &dyn Document, _document: &dyn Document,
) -> Result<String, String> { ) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let style = match properties let style = match properties.downcast_ref::<String>().unwrap().as_str() {
.as_ref()
.unwrap()
.downcast_ref::<String>()
.unwrap()
.as_str()
{
"" => "".to_string(), "" => "".to_string(),
str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)), str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)),
}; };
@ -146,7 +139,6 @@ mod default_layouts {
properties.insert( properties.insert(
"style".to_string(), "style".to_string(),
Property::new( Property::new(
true,
"Additional style for the split".to_string(), "Additional style for the split".to_string(),
Some("".to_string()), Some("".to_string()),
), ),
@ -161,46 +153,38 @@ mod default_layouts {
fn expects(&self) -> Range<usize> { 2..usize::MAX } fn expects(&self) -> Range<usize> { 2..usize::MAX }
fn parse_properties(&self, properties: &str) -> Result<Option<Box<dyn Any>>, String> { fn parse_properties(
let props = if properties.is_empty() { &self,
self.0.default() reports: &mut Vec<Report>,
} else { state: &ParserState,
self.0.parse(properties) token: Token,
} ) -> Option<Box<dyn Any>> {
.map_err(|err| { let properties = match self.0.parse("Split Layout", reports, state, token) {
format!( Some(props) => props,
"Failed to parse properties for layout {}: {err}", None => return None,
self.name() };
)
})?;
let style = props let style = match properties.get(reports, "style", |_, value| {
.get("style", |_, value| -> Result<String, ()> { Result::<_, String>::Ok(value.value.clone())
Ok(value.clone()) }) {
}) Some(style) => style,
.map_err(|err| format!("Failed to parse style: {err:#?}")) _ => return None,
.map(|(_, value)| value)?; };
Ok(Some(Box::new(style))) Some(Box::new(style))
} }
fn compile( fn compile(
&self, &self,
token: LayoutToken, token: LayoutToken,
_id: usize, _id: usize,
properties: &Option<Box<dyn Any>>, properties: &Box<dyn Any>,
compiler: &Compiler, compiler: &Compiler,
_document: &dyn Document, _document: &dyn Document,
) -> Result<String, String> { ) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let style = match properties let style = match properties.downcast_ref::<String>().unwrap().as_str() {
.as_ref()
.unwrap()
.downcast_ref::<String>()
.unwrap()
.as_str()
{
"" => "".to_string(), "" => "".to_string(),
str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)), str => format!(r#" style={}"#, Compiler::sanitize(compiler.target(), str)),
}; };
@ -225,11 +209,7 @@ mod default_layouts {
let mut properties = HashMap::new(); let mut properties = HashMap::new();
properties.insert( properties.insert(
"title".to_string(), "title".to_string(),
Property::new( Property::new("Spoiler title".to_string(), Some("".to_string())),
true,
"Spoiler title".to_string(),
Some("".to_string())
),
); );
Self(PropertyParser { properties }) Self(PropertyParser { properties })
@ -241,50 +221,45 @@ mod default_layouts {
fn expects(&self) -> Range<usize> { 1..1 } fn expects(&self) -> Range<usize> { 1..1 }
fn parse_properties(&self, properties: &str) -> Result<Option<Box<dyn Any>>, String> { fn parse_properties(
let props = if properties.is_empty() { &self,
self.0.default() reports: &mut Vec<Report>,
} else { state: &ParserState,
self.0.parse(properties) token: Token,
} ) -> Option<Box<dyn Any>> {
.map_err(|err| { let properties = match self.0.parse("Spoiler Layout", reports, state, token) {
format!( Some(props) => props,
"Failed to parse properties for layout {}: {err}", None => return None,
self.name() };
)
})?;
let title = props let title = match properties.get(reports, "title", |_, value| {
.get("title", |_, value| -> Result<String, ()> { Result::<_, String>::Ok(value.value.clone())
Ok(value.clone()) }) {
}) Some(title) => title,
.map_err(|err| format!("Failed to parse style: {err:#?}")) _ => return None,
.map(|(_, value)| value)?; };
Ok(Some(Box::new(title))) Some(Box::new(title))
} }
fn compile( fn compile(
&self, &self,
token: LayoutToken, token: LayoutToken,
_id: usize, _id: usize,
properties: &Option<Box<dyn Any>>, properties: &Box<dyn Any>,
compiler: &Compiler, compiler: &Compiler,
_document: &dyn Document, _document: &dyn Document,
) -> Result<String, String> { ) -> Result<String, String> {
match compiler.target() { match compiler.target() {
Target::HTML => { Target::HTML => {
let title = properties let title = properties.downcast_ref::<String>().unwrap();
.as_ref()
.unwrap()
.downcast_ref::<String>()
.unwrap();
match token { match token {
LayoutToken::Begin => Ok(format!( LayoutToken::Begin => Ok(format!(
r#"<details class="spoiler"><summary>{}</summary>"#, Compiler::sanitize(compiler.target(), title) r#"<details class="spoiler"><summary>{}</summary>"#,
Compiler::sanitize(compiler.target(), title)
)), )),
LayoutToken::End => Ok(r#"</details>"#.to_string()), LayoutToken::End => Ok(r#"</details>"#.to_string()),
_ => panic!() _ => panic!(),
} }
} }
_ => todo!(""), _ => todo!(""),
@ -299,7 +274,7 @@ struct Layout {
pub(self) layout: Rc<dyn LayoutType>, pub(self) layout: Rc<dyn LayoutType>,
pub(self) id: usize, pub(self) id: usize,
pub(self) token: LayoutToken, pub(self) token: LayoutToken,
pub(self) properties: Option<Box<dyn Any>>, pub(self) properties: Box<dyn Any>,
} }
impl Element for Layout { impl Element for Layout {
@ -330,9 +305,12 @@ impl RuleState for LayoutState {
let doc_borrow = document.content().borrow(); let doc_borrow = document.content().borrow();
let at = doc_borrow.last().map_or( let at = doc_borrow.last().map_or(
Token::new(document.source().content().len()..document.source().content().len(), document.source()), Token::new(
|last| last.location().to_owned() document.source().content().len()..document.source().content().len(),
); document.source(),
),
|last| last.location().to_owned(),
);
for (tokens, layout_type) in &self.stack { for (tokens, layout_type) in &self.stack {
let start = tokens.first().unwrap(); let start = tokens.first().unwrap();
@ -406,42 +384,23 @@ impl LayoutRule {
pub fn parse_properties<'a>( pub fn parse_properties<'a>(
mut reports: &mut Vec<Report>, mut reports: &mut Vec<Report>,
state: &ParserState,
token: &Token, token: &Token,
layout_type: Rc<dyn LayoutType>, layout_type: Rc<dyn LayoutType>,
properties: Option<Match>, m: Option<Match>,
) -> Result<Option<Box<dyn Any>>, ()> { ) -> Option<Box<dyn Any>> {
match properties { let prop_source = escape_source(
None => match layout_type.parse_properties("") { token.source(),
Ok(props) => Ok(props), m.map_or(0..0, |m| m.range()),
Err(err) => { format!("Layout {} Properties", layout_type.name()),
report_err!( '\\',
&mut reports, "]",
token.source(), );
"Invalid Layout Properties".into(), layout_type.parse_properties(
span( reports,
token.start() + 1..token.end(), state,
format!("Layout is missing required property: {err}") Token::new(0..prop_source.content().len(), prop_source),
) )
);
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(())
}
}
}
}
} }
} }
@ -546,12 +505,13 @@ impl RegexRule for LayoutRule {
// Parse properties // Parse properties
let properties = match LayoutRule::parse_properties( let properties = match LayoutRule::parse_properties(
&mut reports, &mut reports,
state,
&token, &token,
layout_type.clone(), layout_type.clone(),
matches.get(1), matches.get(1),
) { ) {
Ok(props) => props, Some(props) => props,
Err(()) => return reports, None => return reports,
}; };
state.push( state.push(
@ -590,7 +550,6 @@ impl RegexRule for LayoutRule {
); );
if let Some(props) = matches.get(1).map(|m| m.range()) { 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.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(props.end..props.end + 1, tokens.layout_props_sep);
} }
sems.add(matches.get(2).unwrap().range(), tokens.layout_type); sems.add(matches.get(2).unwrap().range(), tokens.layout_type);
@ -650,12 +609,13 @@ impl RegexRule for LayoutRule {
// Parse properties // Parse properties
let properties = match LayoutRule::parse_properties( let properties = match LayoutRule::parse_properties(
&mut reports, &mut reports,
state,
&token, &token,
layout_type.clone(), layout_type.clone(),
matches.get(1), matches.get(1),
) { ) {
Ok(props) => props, Some(props) => props,
Err(()) => return reports, None => return reports,
}; };
if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) 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()) { 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.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(props.end..props.end + 1, tokens.layout_props_sep);
} }
} }
@ -732,12 +691,13 @@ impl RegexRule for LayoutRule {
// Parse properties // Parse properties
let properties = match LayoutRule::parse_properties( let properties = match LayoutRule::parse_properties(
&mut reports, &mut reports,
state,
&token, &token,
layout_type.clone(), layout_type.clone(),
matches.get(1), matches.get(1),
) { ) {
Ok(props) => props, Some(props) => props,
Err(()) => return reports, None => return reports,
}; };
let layout_type = layout_type.clone(); 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()) { 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.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(props.end..props.end + 1, tokens.layout_props_sep);
} }
} }
@ -803,8 +762,8 @@ impl RegexRule for LayoutRule {
Ok(token) => token, Ok(token) => token,
}; };
CTX.with_borrow(|ctx| { CTX.with_borrow_mut(|ctx| {
ctx.as_ref().map(|ctx| { ctx.as_mut().map(|ctx| {
// Make sure the rule state has been initialized // Make sure the rule state has been initialized
let rule_state = LayoutRule::initialize_state(ctx.state); let rule_state = LayoutRule::initialize_state(ctx.state);
@ -827,17 +786,18 @@ impl RegexRule for LayoutRule {
}; };
// Parse properties // Parse properties
let layout_properties = match layout_type.parse_properties(properties.as_str()) { let prop_source = Rc::new(VirtualSource::new(ctx.location.clone(), ":LUA:Layout Properties".into(), properties)) as Rc<dyn Source>;
Err(err) => { let layout_properties = match layout_type.parse_properties(&mut ctx.reports, ctx.state, prop_source.into()) {
None => {
result = Err(BadArgument { result = Err(BadArgument {
to: Some("push".to_string()), to: Some("push".to_string()),
pos: 3, pos: 3,
name: Some("properties".to_string()), name: Some("properties".to_string()),
cause: Arc::new(mlua::Error::external(err)), cause: Arc::new(mlua::Error::external("Failed to parse properties")),
}); });
return; return;
}, },
Ok(properties) => properties, Some(properties) => properties,
}; };
let id = match layout_token { let id = match layout_token {
@ -1162,8 +1122,10 @@ mod tests {
layout_sep { delta_line == 1, delta_start == 1, length == 2 }; layout_sep { delta_line == 1, delta_start == 1, length == 2 };
layout_token { delta_line == 0, delta_start == 2, length == 11 }; layout_token { delta_line == 0, delta_start == 2, length == 11 };
layout_props_sep { delta_line == 0, delta_start == 11, length == 1 }; layout_props_sep { delta_line == 0, delta_start == 11, length == 1 };
layout_props { delta_line == 0, delta_start == 1, length == 8 }; prop_name { delta_line == 0, delta_start == 1, length == 5 };
layout_props_sep { delta_line == 0, delta_start == 8, length == 1 }; 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_sep { delta_line == 1, delta_start == 0, length == 2 };
layout_token { delta_line == 0, delta_start == 2, length == 10 }; layout_token { delta_line == 0, delta_start == 2, length == 10 };
); );

View file

@ -13,6 +13,8 @@ use crate::document::element::Element;
use crate::lsp::semantic::Semantics; use crate::lsp::semantic::Semantics;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*; use crate::parser::reports::macros::*;
use crate::parser::reports::Report; use crate::parser::reports::Report;
use crate::parser::reports::*; use crate::parser::reports::*;
@ -21,11 +23,7 @@ use crate::parser::source::Cursor;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::source::VirtualSource; use crate::parser::source::VirtualSource;
use crate::parser::util; use crate::parser::util;
use crate::parser::util::escape_text; use parser::util::escape_source;
use crate::parser::util::Property;
use crate::parser::util::PropertyMapError;
use crate::parser::util::PropertyParser;
use regex::Match;
use regex::Regex; use regex::Regex;
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
@ -137,11 +135,11 @@ impl ListRule {
let mut props = HashMap::new(); let mut props = HashMap::new();
props.insert( props.insert(
"offset".to_string(), "offset".to_string(),
Property::new(false, "Entry numbering offset".to_string(), None), Property::new("Entry numbering offset".to_string(), None),
); );
props.insert( props.insert(
"bullet".to_string(), "bullet".to_string(),
Property::new(false, "Entry bullet".to_string(), None), Property::new("Entry bullet".to_string(), None),
); );
Self { Self {
@ -193,28 +191,6 @@ impl ListRule {
} }
} }
fn parse_properties(&self, m: Match) -> Result<(Option<usize>, Option<String>), 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::<usize>()) {
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<String, ()> { Ok(s.to_string()) })
.map(|(_, s)| s)
.ok();
Ok((offset, bullet))
}
fn parse_depth(depth: &str, document: &dyn Document, offset: usize) -> Vec<(bool, usize)> { fn parse_depth(depth: &str, document: &dyn Document, offset: usize) -> Vec<(bool, usize)> {
let mut parsed = vec![]; let mut parsed = vec![];
let prev_entry = document let prev_entry = document
@ -305,28 +281,46 @@ impl Rule for ListRule {
end_cursor = end_cursor.at(captures.get(0).unwrap().end()); end_cursor = end_cursor.at(captures.get(0).unwrap().end());
// Properties // Properties
let mut offset = None; let prop_source = escape_source(
let mut bullet = None; end_cursor.source.clone(),
if let Some(properties) = captures.get(2) { captures.get(2).map_or(0..0, |m| m.range()),
match self.parse_properties(properties) { "List Properties".into(),
Err(err) => { '\\',
report_err!( "]",
&mut reports, );
cursor.source.clone(), let properties = match self.properties.parse(
"Invalid List Entry Properties".into(), "List",
span(properties.range(), err) &mut reports,
); state,
return (cursor.at(captures.get(0).unwrap().end()), reports); 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::<usize>()
}),
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::<ListEntry>()
.and_then(|prev| prev.bullet.clone()),
)
} else {
(offset, bullet)
} }
Ok(props) => (offset, bullet) = props,
} }
} _ => return (end_cursor, reports),
// Get bullet from previous entry if it exists };
if bullet.is_none() {
bullet = document
.last_element::<ListEntry>()
.and_then(|prev| prev.bullet.clone())
}
// Depth // Depth
let depth = ListRule::parse_depth( let depth = ListRule::parse_depth(
@ -341,7 +335,6 @@ impl Rule for ListRule {
sems.add(captures.get(1).unwrap().range(), tokens.list_bullet); sems.add(captures.get(1).unwrap().range(), tokens.list_bullet);
if let Some(props) = captures.get(2).map(|m| m.range()) { 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.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); sems.add(props.end..props.end + 1, tokens.list_props_sep);
} }
} }
@ -528,8 +521,10 @@ mod tests {
validate_semantics!(state, source.clone(), 0, validate_semantics!(state, source.clone(), 0,
list_bullet { delta_line == 1, delta_start == 1, length == 1 }; list_bullet { delta_line == 1, delta_start == 1, length == 1 };
list_props_sep { delta_line == 0, 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 }; prop_name { delta_line == 0, delta_start == 1, length == 6 };
list_props_sep { delta_line == 0, delta_start == 8, length == 1 }; 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 == 8, length == 2 };
style_marker { delta_line == 0, delta_start == 6, length == 2 }; style_marker { delta_line == 0, delta_start == 6, length == 2 };
list_bullet { delta_line == 2, delta_start == 1, length == 2 }; list_bullet { delta_line == 2, delta_start == 1, length == 2 };

View file

@ -5,7 +5,6 @@ use ariadne::Fmt;
use lsp::semantic::Semantics; use lsp::semantic::Semantics;
use parser::util::escape_source; use parser::util::escape_source;
use regex::Captures; use regex::Captures;
use regex::Match;
use regex::Regex; use regex::Regex;
use regex::RegexBuilder; use regex::RegexBuilder;
@ -20,16 +19,14 @@ use crate::document::element::ReferenceableElement;
use crate::document::references::validate_refname; use crate::document::references::validate_refname;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*; use crate::parser::reports::macros::*;
use crate::parser::reports::*; use crate::parser::reports::*;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::util; use crate::parser::util;
use crate::parser::util::parse_paragraph; 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::paragraph::Paragraph;
use super::reference::InternalReference; use super::reference::InternalReference;
@ -245,19 +242,15 @@ impl MediaRule {
let mut props = HashMap::new(); let mut props = HashMap::new();
props.insert( props.insert(
"type".to_string(), "type".to_string(),
Property::new( Property::new("Override for the media type detection".to_string(), None),
false,
"Override for the media type detection".to_string(),
None,
),
); );
props.insert( props.insert(
"width".to_string(), "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( props.insert(
"caption".to_string(), "caption".to_string(),
Property::new(false, "Medium caption".to_string(), None), Property::new("Medium caption".to_string(), None),
); );
Self { Self {
re: [RegexBuilder::new( re: [RegexBuilder::new(
@ -280,47 +273,6 @@ impl MediaRule {
Ok(trimmed) Ok(trimmed)
} }
fn parse_properties(
&self,
mut reports: &mut Vec<Report>,
token: &Token,
m: &Option<Match>,
) -> Option<PropertyMap> {
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<MediaType> { fn detect_filetype(filename: &str) -> Option<MediaType> {
let sep = match filename.rfind('.') { let sep = match filename.rfind('.') {
Some(pos) => pos, Some(pos) => pos,
@ -390,64 +342,61 @@ impl RegexRule for MediaRule {
}; };
// Properties // Properties
let properties = match self.parse_properties(&mut reports, &token, &matches.get(3)) { let prop_source = escape_source(
Some(pm) => pm, 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, None => return reports,
}; };
let media_type = match Self::detect_filetype(uri.as_str()) { let (media_type, caption, width) = match (
Some(media_type) => media_type, properties.get_opt(&mut reports, "type", |_, value| {
None => match properties.get("type", |prop, value| { MediaType::from_str(value.value.as_str())
MediaType::from_str(value.as_str()).map_err(|e| (prop, e)) }),
}) { properties.get_opt(&mut reports, "caption", |_, value| {
Ok((_prop, kind)) => kind, Result::<_, String>::Ok(value.value.clone())
Err(e) => match e { }),
PropertyMapError::ParseError((prop, err)) => { properties.get_opt(&mut reports, "width", |_, value| {
report_err!( Result::<_, String>::Ok(value.value.clone())
&mut reports, }),
token.source(), ) {
"Invalid Media Property".into(), (Some(media_type), Some(caption), Some(width)) => {
span( if media_type.is_none() {
token.start() + 1..token.end(), match Self::detect_filetype(uri.as_str()) {
format!( None => {
"Property `type: {}` cannot be converted: {}", report_err!(
prop.fg(state.parser.colors().info), &mut reports,
err.fg(state.parser.colors().error) 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) => { } else {
report_err!( (media_type.unwrap(), caption, width)
&mut reports, }
token.source(), }
"Invalid Media Property".into(), _ => return reports,
span(
token.start() + 1..token.end(),
format!("{err}. Required because mediatype could not be detected")
)
);
return reports;
}
},
},
}; };
let width = properties
.get("width", |_, value| -> Result<String, ()> {
Ok(value.clone())
})
.ok()
.map(|(_, s)| s);
let caption = properties
.get("caption", |_, value| -> Result<String, ()> {
Ok(value.clone())
})
.ok()
.map(|(_, value)| value);
if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) { if let Some((sems, tokens)) = Semantics::from_source(token.source(), &state.shared.lsp) {
sems.add( sems.add(
matches.get(0).unwrap().start()..matches.get(0).unwrap().start() + 1, matches.get(0).unwrap().start()..matches.get(0).unwrap().start() + 1,
@ -476,7 +425,6 @@ impl RegexRule for MediaRule {
// Props // Props
if let Some(props) = matches.get(3) { if let Some(props) = matches.get(3) {
sems.add(props.start() - 1..props.start(), tokens.media_props_sep); 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); sems.add(props.end()..props.end() + 1, tokens.media_props_sep);
} }
} }

View file

@ -17,5 +17,5 @@ pub mod section;
pub mod style; pub mod style;
pub mod tex; pub mod tex;
pub mod text; pub mod text;
pub mod variable;
pub mod toc; pub mod toc;
pub mod variable;

View file

@ -6,18 +6,18 @@ use crate::lsp::semantic::Semantics;
use crate::lua::kernel::CTX; use crate::lua::kernel::CTX;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*; use crate::parser::reports::macros::*;
use crate::parser::reports::*; use crate::parser::reports::*;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Token; 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 crate::parser::util::{self};
use ariadne::Fmt; use ariadne::Fmt;
use mlua::Error::BadArgument; use mlua::Error::BadArgument;
use mlua::Function; use mlua::Function;
use mlua::Lua; use mlua::Lua;
use parser::util::escape_source;
use regex::Captures; use regex::Captures;
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
@ -59,7 +59,6 @@ impl RawRule {
props.insert( props.insert(
"kind".to_string(), "kind".to_string(),
Property::new( Property::new(
true,
"Element display kind".to_string(), "Element display kind".to_string(),
Some("inline".to_string()), Some("inline".to_string()),
), ),
@ -127,77 +126,28 @@ impl RegexRule for RawRule {
} }
}; };
let properties = match matches.get(1) { let prop_source = escape_source(
None => match self.properties.default() { token.source(),
Ok(properties) => properties, matches.get(1).map_or(0..0, |m| m.range()),
Err(e) => { "Raw Properties".into(),
report_err!( '\\',
&mut reports, "]",
token.source(), );
"Invalid Raw Code".into(), let properties = match self.properties.parse(
span( "Raw Code",
token.range.clone(), &mut reports,
format!("Raw code is missing properties: {e}") state,
) Token::new(0..prop_source.content().len(), prop_source),
); ) {
return reports; Some(props) => props,
} None => 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 raw_kind: ElemKind = match properties.get("kind", |prop, value| { let raw_kind = match properties.get(&mut reports, "kind", |_, value| {
ElemKind::from_str(value.as_str()).map_err(|e| (prop, e)) ElemKind::from_str(value.value.as_str())
}) { }) {
Ok((_prop, kind)) => kind, None => return reports,
Err(e) => match e { Some(raw_kind) => raw_kind,
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;
}
},
}; };
state.push( state.push(
@ -214,7 +164,6 @@ impl RegexRule for RawRule {
sems.add(range.start..range.start + 2, tokens.raw_sep); sems.add(range.start..range.start + 2, tokens.raw_sep);
if let Some(props) = matches.get(1).map(|m| m.range()) { 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.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(props.end..props.end + 1, tokens.raw_props_sep);
} }
sems.add(matches.get(2).unwrap().range(), tokens.raw_content); sems.add(matches.get(2).unwrap().range(), tokens.raw_content);
@ -356,8 +305,10 @@ Break%<nml.raw.push("block", "Raw")>%NewParagraph%<nml.raw.push("inline", "<b>")
validate_semantics!(state, source.clone(), 0, validate_semantics!(state, source.clone(), 0,
raw_sep { delta_line == 1, delta_start == 0, length == 2 }; raw_sep { delta_line == 1, delta_start == 0, length == 2 };
raw_props_sep { delta_line == 0, delta_start == 2, length == 1 }; raw_props_sep { delta_line == 0, delta_start == 2, length == 1 };
raw_props { delta_line == 0, delta_start == 1, length == 10 }; prop_name { delta_line == 0, delta_start == 1, length == 4 };
raw_props_sep { delta_line == 0, delta_start == 10, length == 1 }; 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_content { delta_line == 0, delta_start == 1, length == 4 };
raw_sep { delta_line == 0, delta_start == 4, length == 2 }; raw_sep { delta_line == 0, delta_start == 4, length == 2 };
raw_sep { delta_line == 1, delta_start == 0, length == 2 }; raw_sep { delta_line == 1, delta_start == 0, length == 2 };

View file

@ -1,9 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use parser::util::escape_source;
use reference_style::ExternalReferenceStyle; use reference_style::ExternalReferenceStyle;
use regex::Captures; use regex::Captures;
use regex::Match;
use regex::Regex; use regex::Regex;
use runtime_format::FormatArgs; use runtime_format::FormatArgs;
use runtime_format::FormatKey; use runtime_format::FormatKey;
@ -19,15 +19,13 @@ use crate::document::references::validate_refname;
use crate::lsp::semantic::Semantics; use crate::lsp::semantic::Semantics;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*; use crate::parser::reports::macros::*;
use crate::parser::reports::*; use crate::parser::reports::*;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::style::StyleHolder; 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)] #[derive(Debug)]
pub struct InternalReference { pub struct InternalReference {
@ -168,58 +166,13 @@ impl ReferenceRule {
let mut props = HashMap::new(); let mut props = HashMap::new();
props.insert( props.insert(
"caption".to_string(), "caption".to_string(),
Property::new( Property::new("Override the display of the reference".to_string(), None),
false,
"Override the display of the reference".to_string(),
None,
),
); );
Self { Self {
re: [Regex::new(r"&\{(.*?)\}(?:\[((?:\\.|[^\\\\])*?)\])?").unwrap()], re: [Regex::new(r"&\{(.*?)\}(?:\[((?:\\.|[^\\\\])*?)\])?").unwrap()],
properties: PropertyParser { properties: props }, properties: PropertyParser { properties: props },
} }
} }
fn parse_properties(
&self,
mut reports: &mut Vec<Report>,
token: &Token,
m: &Option<Match>,
) -> Option<PropertyMap> {
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 { impl RegexRule for ReferenceRule {
@ -280,17 +233,29 @@ impl RegexRule for ReferenceRule {
}; };
// Properties // Properties
let properties = match self.parse_properties(&mut reports, &token, &matches.get(2)) { let prop_source = escape_source(
Some(pm) => pm, 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, None => return reports,
}; };
let caption = properties let caption = match properties.get_opt(&mut reports, "caption", |_, value| {
.get("caption", |_, value| -> Result<String, ()> { Result::<_, String>::Ok(value.value.clone())
Ok(value.clone()) }) {
}) Some(caption) => caption,
.ok() None => return reports,
.map(|(_, s)| s); };
if let Some(refdoc) = refdoc { if let Some(refdoc) = refdoc {
// Get style // Get style
@ -370,7 +335,6 @@ impl RegexRule for ReferenceRule {
matches.get(2).map(|m| m.range()), matches.get(2).map(|m| m.range()),
) { ) {
sems.add(props.start - 1..props.start, tokens.reference_props_sep); 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); sems.add(props.end..props.end + 1, tokens.reference_props_sep);
} }

View file

@ -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); hints.add(matches.get(0).unwrap().end(), result);
} }
} else if kind == 2 } else if kind == 2
// Eval and Parse // 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()); hints.add(matches.get(0).unwrap().end(), result.clone());
} }
@ -229,7 +233,6 @@ impl RegexRule for ScriptRule {
result, result,
)) as Rc<dyn Source>; )) as Rc<dyn Source>;
state.with_state(|new_state| { state.with_state(|new_state| {
new_state.parser.parse_into( new_state.parser.parse_into(
new_state, new_state,
@ -295,6 +298,7 @@ impl RegexRule for ScriptRule {
sems.add(range.end - 2..range.end, tokens.script_sep); 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) { if let Some(hints) = Hints::from_source(token.source(), &state.shared.lsp) {
let mut label = String::new(); let mut label = String::new();
ctx.redirects.iter().for_each(|redir| { ctx.redirects.iter().for_each(|redir| {
@ -305,6 +309,8 @@ impl RegexRule for ScriptRule {
hints.add(matches.get(0).unwrap().end(), label); hints.add(matches.get(0).unwrap().end(), label);
} }
} }
// TODO: Process reports
reports reports
} }
} }

View file

@ -12,8 +12,8 @@ use crypto::digest::Digest;
use crypto::sha2::Sha512; use crypto::sha2::Sha512;
use mlua::Function; use mlua::Function;
use mlua::Lua; use mlua::Lua;
use parser::util::escape_source;
use regex::Captures; use regex::Captures;
use regex::Match;
use regex::Regex; use regex::Regex;
use crate::cache::cache::Cached; use crate::cache::cache::Cached;
@ -27,15 +27,13 @@ use crate::lsp::semantic::Semantics;
use crate::lua::kernel::CTX; use crate::lua::kernel::CTX;
use crate::parser::parser::ParseMode; use crate::parser::parser::ParseMode;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::property::Property;
use crate::parser::property::PropertyParser;
use crate::parser::reports::macros::*; use crate::parser::reports::macros::*;
use crate::parser::reports::*; use crate::parser::reports::*;
use crate::parser::rule::RegexRule; use crate::parser::rule::RegexRule;
use crate::parser::source::Token; use crate::parser::source::Token;
use crate::parser::util; 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)] #[derive(Debug, PartialEq, Eq)]
enum TexKind { enum TexKind {
@ -235,19 +233,15 @@ impl TexRule {
let mut props = HashMap::new(); let mut props = HashMap::new();
props.insert( props.insert(
"env".to_string(), "env".to_string(),
Property::new( Property::new("Tex environment".to_string(), Some("main".to_string())),
true,
"Tex environment".to_string(),
Some("main".to_string()),
),
); );
props.insert( props.insert(
"kind".to_string(), "kind".to_string(),
Property::new(false, "Element display kind".to_string(), None), Property::new("Element display kind".to_string(), None),
); );
props.insert( props.insert(
"caption".to_string(), "caption".to_string(),
Property::new(false, "Latex caption".to_string(), None), Property::new("Latex caption".to_string(), None),
); );
Self { Self {
re: [ re: [
@ -258,47 +252,6 @@ impl TexRule {
properties: PropertyParser { properties: props }, properties: PropertyParser { properties: props },
} }
} }
fn parse_properties(
&self,
mut reports: &mut Vec<Report>,
token: &Token,
m: &Option<Match>,
) -> Option<PropertyMap> {
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 { impl RegexRule for TexRule {
@ -358,67 +311,52 @@ impl RegexRule for TexRule {
}; };
// Properties // Properties
let properties = match self.parse_properties(&mut reports, &token, &matches.get(1)) { let prop_source = escape_source(
Some(pm) => pm, 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, None => return reports,
}; };
// Tex kind let (tex_kind, caption, tex_env) = match (
let tex_kind = match properties.get("kind", |prop, value| { properties.get_or(
TexKind::from_str(value.as_str()).map_err(|e| (prop, e)) &mut reports,
}) { "kind",
Ok((_prop, kind)) => kind, if index == 1 {
Err(e) => match e { TexKind::Inline
PropertyMapError::ParseError((prop, err)) => { } else {
report_err!( TexKind::Block
&mut reports, },
token.source(), |_, value| TexKind::from_str(value.value.as_str()),
"Invalid Tex Property".into(), ),
span( properties.get_opt(&mut reports, "caption", |_, value| {
token.range.clone(), Result::<_, String>::Ok(value.value.clone())
format!( }),
"Property `kind: {}` cannot be converted: {}", properties.get(&mut reports, "env", |_, value| {
prop.fg(state.parser.colors().info), Result::<_, String>::Ok(value.value.clone())
err.fg(state.parser.colors().error) }),
) ) {
) (Some(tex_kind), Some(caption), Some(tex_env)) => (tex_kind, caption, tex_env),
); _ => return reports,
return reports;
}
PropertyMapError::NotFoundError(_) => {
if index == 1 {
TexKind::Inline
} else {
TexKind::Block
}
}
},
}; };
// Caption
let caption = properties
.get("caption", |_, value| -> Result<String, ()> {
Ok(value.clone())
})
.ok()
.map(|(_, value)| value);
// Environ
let tex_env = properties
.get("env", |_, value| -> Result<String, ()> {
Ok(value.clone())
})
.ok()
.map(|(_, value)| value)
.unwrap();
state.push( state.push(
document, document,
Box::new(Tex { Box::new(Tex {
mathmode: index == 1, mathmode: index == 1,
location: token.clone(), location: token.clone(),
kind: tex_kind, kind: tex_kind,
env: tex_env.to_string(), env: tex_env,
tex: tex_content, tex: tex_content,
caption, caption,
}), }),
@ -432,7 +370,6 @@ impl RegexRule for TexRule {
); );
if let Some(props) = matches.get(1).map(|m| m.range()) { 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.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(props.end..props.end + 1, tokens.tex_props_sep);
} }
sems.add(matches.get(2).unwrap().range(), tokens.tex_content); 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, 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 == false, tex == "Non Math \\LaTeX", env == "another" };
Tex { mathmode == true, tex == "e^{i\\pi}=-1", 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()) }; 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, validate_document!(doc.content().borrow(), 0,
Paragraph { 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 == 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 == "e^{i\\pi}=-1", env == "another" };
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()) };
@ -634,8 +571,10 @@ $[kind=inline]\LaTeX$
validate_semantics!(state, source.clone(), 0, validate_semantics!(state, source.clone(), 0,
tex_sep { delta_line == 1, delta_start == 0, length == 1 }; tex_sep { delta_line == 1, delta_start == 0, length == 1 };
tex_props_sep { delta_line == 0, delta_start == 1, length == 1 }; tex_props_sep { delta_line == 0, delta_start == 1, length == 1 };
tex_props { delta_line == 0, delta_start == 1, length == 11 }; prop_name { delta_line == 0, delta_start == 1, length == 4 };
tex_props_sep { delta_line == 0, delta_start == 11, length == 1 }; 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_content { delta_line == 0, delta_start == 1, length == 6 };
tex_sep { delta_line == 0, delta_start == 6, length == 1 }; tex_sep { delta_line == 0, delta_start == 6, length == 1 };
); );

View file

@ -96,6 +96,11 @@ pub struct Tokens {
pub section_kind: (u32, u32), pub section_kind: (u32, u32),
pub section_name: (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 comment: (u32, u32),
pub link_display_sep: (u32, u32), pub link_display_sep: (u32, u32),
@ -117,7 +122,6 @@ pub struct Tokens {
pub reference_doc: (u32, u32), pub reference_doc: (u32, u32),
pub reference_link: (u32, u32), pub reference_link: (u32, u32),
pub reference_props_sep: (u32, u32), pub reference_props_sep: (u32, u32),
pub reference_props: (u32, u32),
pub variable_operator: (u32, u32), pub variable_operator: (u32, u32),
pub variable_kind: (u32, u32), pub variable_kind: (u32, u32),
@ -135,7 +139,6 @@ pub struct Tokens {
pub code_sep: (u32, u32), pub code_sep: (u32, u32),
pub code_props_sep: (u32, u32), pub code_props_sep: (u32, u32),
pub code_props: (u32, u32),
pub code_lang: (u32, u32), pub code_lang: (u32, u32),
pub code_title: (u32, u32), pub code_title: (u32, u32),
pub code_content: (u32, u32), pub code_content: (u32, u32),
@ -148,31 +151,25 @@ pub struct Tokens {
pub list_bullet: (u32, u32), pub list_bullet: (u32, u32),
pub list_props_sep: (u32, u32), pub list_props_sep: (u32, u32),
pub list_props: (u32, u32),
pub blockquote_marker: (u32, u32), pub blockquote_marker: (u32, u32),
pub blockquote_props_sep: (u32, u32), pub blockquote_props_sep: (u32, u32),
pub blockquote_props: (u32, u32),
pub raw_sep: (u32, u32), pub raw_sep: (u32, u32),
pub raw_props_sep: (u32, u32), pub raw_props_sep: (u32, u32),
pub raw_props: (u32, u32),
pub raw_content: (u32, u32), pub raw_content: (u32, u32),
pub tex_sep: (u32, u32), pub tex_sep: (u32, u32),
pub tex_props_sep: (u32, u32), pub tex_props_sep: (u32, u32),
pub tex_props: (u32, u32),
pub tex_content: (u32, u32), pub tex_content: (u32, u32),
pub graph_sep: (u32, u32), pub graph_sep: (u32, u32),
pub graph_props_sep: (u32, u32), pub graph_props_sep: (u32, u32),
pub graph_props: (u32, u32),
pub graph_content: (u32, u32), pub graph_content: (u32, u32),
pub layout_sep: (u32, u32), pub layout_sep: (u32, u32),
pub layout_token: (u32, u32), pub layout_token: (u32, u32),
pub layout_props_sep: (u32, u32), pub layout_props_sep: (u32, u32),
pub layout_props: (u32, u32),
pub layout_type: (u32, u32), pub layout_type: (u32, u32),
pub toc_sep: (u32, u32), pub toc_sep: (u32, u32),
@ -185,7 +182,6 @@ pub struct Tokens {
pub media_uri_sep: (u32, u32), pub media_uri_sep: (u32, u32),
pub media_uri: (u32, u32), pub media_uri: (u32, u32),
pub media_props_sep: (u32, u32), pub media_props_sep: (u32, u32),
pub media_props: (u32, u32),
} }
impl Tokens { impl Tokens {
@ -196,6 +192,11 @@ impl Tokens {
section_kind: token!("enum"), section_kind: token!("enum"),
section_name: token!("string"), section_name: token!("string"),
prop_equal: token!("operator"),
prop_comma: token!("operator"),
prop_name: token!("class"),
prop_value: token!("enum"),
comment: token!("comment"), comment: token!("comment"),
link_display_sep: token!("macro"), link_display_sep: token!("macro"),
@ -217,7 +218,6 @@ impl Tokens {
reference_doc: token!("function"), reference_doc: token!("function"),
reference_link: token!("macro"), reference_link: token!("macro"),
reference_props_sep: token!("operator"), reference_props_sep: token!("operator"),
reference_props: token!("enum"),
variable_operator: token!("operator"), variable_operator: token!("operator"),
variable_kind: token!("operator"), variable_kind: token!("operator"),
@ -235,7 +235,6 @@ impl Tokens {
code_sep: token!("operator"), code_sep: token!("operator"),
code_props_sep: token!("operator"), code_props_sep: token!("operator"),
code_props: token!("enum"),
code_lang: token!("function"), code_lang: token!("function"),
code_title: token!("number"), code_title: token!("number"),
code_content: token!("string"), code_content: token!("string"),
@ -248,31 +247,25 @@ impl Tokens {
list_bullet: token!("macro"), list_bullet: token!("macro"),
list_props_sep: token!("operator"), list_props_sep: token!("operator"),
list_props: token!("enum"),
blockquote_marker: token!("macro"), blockquote_marker: token!("macro"),
blockquote_props_sep: token!("operator"), blockquote_props_sep: token!("operator"),
blockquote_props: token!("enum"),
raw_sep: token!("operator"), raw_sep: token!("operator"),
raw_props_sep: token!("operator"), raw_props_sep: token!("operator"),
raw_props: token!("enum"),
raw_content: token!("string"), raw_content: token!("string"),
tex_sep: token!("modifier"), tex_sep: token!("modifier"),
tex_props_sep: token!("operator"), tex_props_sep: token!("operator"),
tex_props: token!("enum"),
tex_content: token!("string"), tex_content: token!("string"),
graph_sep: token!("modifier"), graph_sep: token!("modifier"),
graph_props_sep: token!("operator"), graph_props_sep: token!("operator"),
graph_props: token!("enum"),
graph_content: token!("string"), graph_content: token!("string"),
layout_sep: token!("number"), layout_sep: token!("number"),
layout_token: token!("number"), layout_token: token!("number"),
layout_props_sep: token!("operator"), layout_props_sep: token!("operator"),
layout_props: token!("enum"),
layout_type: token!("function"), layout_type: token!("function"),
toc_sep: token!("number"), toc_sep: token!("number"),
@ -285,7 +278,6 @@ impl Tokens {
media_uri_sep: token!("macro"), media_uri_sep: token!("macro"),
media_uri: token!("function"), media_uri: token!("function"),
media_props_sep: token!("operator"), media_props_sep: token!("operator"),
media_props: token!("enum"),
} }
} }
} }

View file

@ -6,6 +6,7 @@ use mlua::Lua;
use crate::document::document::Document; use crate::document::document::Document;
use crate::parser::parser::Parser; use crate::parser::parser::Parser;
use crate::parser::parser::ParserState; use crate::parser::parser::ParserState;
use crate::parser::reports::Report;
use crate::parser::source::Token; use crate::parser::source::Token;
/// Redirected data from lua execution /// Redirected data from lua execution
@ -21,6 +22,7 @@ pub struct KernelContext<'a, 'b, 'c> {
pub state: &'a ParserState<'a, 'b>, pub state: &'a ParserState<'a, 'b>,
pub document: &'c dyn Document<'c>, pub document: &'c dyn Document<'c>,
pub redirects: Vec<KernelRedirect>, pub redirects: Vec<KernelRedirect>,
pub reports: Vec<Report>,
} }
impl<'a, 'b, 'c> KernelContext<'a, 'b, 'c> { impl<'a, 'b, 'c> KernelContext<'a, 'b, 'c> {
@ -34,6 +36,7 @@ impl<'a, 'b, 'c> KernelContext<'a, 'b, 'c> {
state, state,
document, document,
redirects: vec![], redirects: vec![],
reports: vec![],
} }
} }
} }

View file

@ -105,12 +105,11 @@ impl<'b> Parser for LangParser<'b> {
.downcast_rc::<SourceFile>() .downcast_rc::<SourceFile>()
.ok() .ok()
.map(|source| { .map(|source| {
if source.path().is_empty() // Test mode if source.path().is_empty()
// Test mode
{ {
None None
} } else {
else
{
let start = if source.path().starts_with("file:///") { let start = if source.path().starts_with("file:///") {
7 7
} else { } else {
@ -119,7 +118,9 @@ impl<'b> Parser for LangParser<'b> {
let mut path = PathBuf::from(&source.path()[start..]); let mut path = PathBuf::from(&source.path()[start..]);
match path.canonicalize() { match path.canonicalize() {
Ok(cano) => path = cano, 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(); path.pop();
Some(path) 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(&current_dir) { if let Err(err) = std::env::set_current_dir(&current_dir) {
println!( println!(
"Failed to set working directory to `{}`: {err} {source:#?}", "Failed to set working directory to `{}`: {err} {source:#?}",

View file

@ -7,13 +7,22 @@ use crate::compiler::compiler::Compiler;
use crate::document::document::Document; use crate::document::document::Document;
use crate::elements::layout::LayoutToken; use crate::elements::layout::LayoutToken;
use super::parser::ParserState;
use super::reports::Report;
use super::source::Token;
/// Represents the type of a layout /// Represents the type of a layout
pub trait LayoutType: core::fmt::Debug { pub trait LayoutType: core::fmt::Debug {
/// Name of the layout /// Name of the layout
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
/// Parses layout properties /// Parses layout properties
fn parse_properties(&self, properties: &str) -> Result<Option<Box<dyn Any>>, String>; fn parse_properties(
&self,
reports: &mut Vec<Report>,
state: &ParserState,
token: Token,
) -> Option<Box<dyn Any>>;
/// Expected number of blocks /// Expected number of blocks
fn expects(&self) -> Range<usize>; fn expects(&self) -> Range<usize>;
@ -23,7 +32,7 @@ pub trait LayoutType: core::fmt::Debug {
&self, &self,
token: LayoutToken, token: LayoutToken,
id: usize, id: usize,
properties: &Option<Box<dyn Any>>, properties: &Box<dyn Any>,
compiler: &Compiler, compiler: &Compiler,
document: &dyn Document, document: &dyn Document,
) -> Result<String, String>; ) -> Result<String, String>;

View file

@ -2,6 +2,7 @@ pub mod customstyle;
pub mod langparser; pub mod langparser;
pub mod layout; pub mod layout;
pub mod parser; pub mod parser;
pub mod property;
pub mod reports; pub mod reports;
pub mod rule; pub mod rule;
pub mod source; pub mod source;

522
src/parser/property.rs Normal file
View file

@ -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<String>,
}
impl Property {
pub fn new(description: String, default: Option<String>) -> 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<usize>,
pub value_range: Range<usize>,
pub value: String,
}
pub struct PropertyMap<'a> {
pub token: Token,
pub rule_name: &'a str,
pub state: &'a ParserState<'a, 'a>,
pub properties: HashMap<String, (&'a Property, PropertyValue)>,
}
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<T, E, F>(&self, mut reports: &mut Vec<Report>, key: &str, f: F) -> Option<T>
where
F: FnOnce(&Property, &PropertyValue) -> Result<T, E>,
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<T, E, F>(
&self,
mut reports: &mut Vec<Report>,
key: &str,
default: T,
f: F,
) -> Option<T>
where
F: FnOnce(&Property, &PropertyValue) -> Result<T, E>,
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<T>)` on success
/// * `None` if the parsing function `f` fails
/// (Note) In this case, reports should have been set
pub fn get_opt<T, E, F>(
&self,
mut reports: &mut Vec<Report>,
key: &str,
f: F,
) -> Option<Option<T>>
where
F: FnOnce(&Property, &PropertyValue) -> Result<T, E>,
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<String, Property>,
}
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::<i32>()).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<Report>,
state: &'a ParserState<'a, 'a>,
token: Token,
) -> Option<PropertyMap<'a>> {
let mut pm = PropertyMap::new(token.clone(), rule_name, state);
let mut try_insert = |name: &String,
name_range: Range<usize>,
value: &String,
value_range: Range<usize>|
-> 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<dyn Source>;
let pm = parser
.parse("Test", &mut reports, &state, source.into())
.unwrap();
// Ok
assert_eq!(
pm.get(&mut reports, "width", |_, s| s.value.parse::<i32>())
.unwrap(),
15
);
assert_eq!(
pm.get(&mut reports, "length", |_, s| s.value.parse::<i32>())
.unwrap(),
-10
);
assert_eq!(
pm.get(&mut reports, "angle", |_, s| s.value.parse::<f64>())
.unwrap(),
180f64
);
assert_eq!(
pm.get(&mut reports, "angle", |_, s| s.value.parse::<i32>())
.unwrap(),
180
);
assert_eq!(
pm.get(&mut reports, "weight", |_, s| s.value.parse::<f32>())
.unwrap(),
0.42f32
);
assert_eq!(
pm.get(&mut reports, "weight", |_, s| s.value.parse::<f64>())
.unwrap(),
0.42f64
);
// Error
assert!(pm
.get(&mut reports, "length", |_, s| s.value.parse::<u32>())
.is_none());
assert!(pm
.get(&mut reports, "height", |_, s| s.value.parse::<f64>())
.is_none());
}
}

View file

@ -322,3 +322,12 @@ impl Token {
pub fn end(&self) -> usize { self.range.end } pub fn end(&self) -> usize { self.range.end }
} }
impl From<Rc<dyn Source>> for Token {
fn from(source: Rc<dyn Source>) -> Self {
Self {
range: 0..source.content().len(),
source,
}
}
}

View file

@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
@ -233,226 +232,6 @@ pub fn parse_paragraph<'a>(
Ok(paragraph.downcast::<Paragraph>().unwrap()) Ok(paragraph.downcast::<Paragraph>().unwrap())
} }
#[derive(Debug)]
pub struct Property {
required: bool,
description: String,
default: Option<String>,
}
impl Property {
pub fn new(required: bool, description: String, default: Option<String>) -> 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<E> {
ParseError(E),
NotFoundError(String),
}
#[derive(Debug)]
pub struct PropertyMap<'a> {
pub(crate) properties: HashMap<String, (&'a Property, String)>,
}
impl<'a> PropertyMap<'a> {
pub fn new() -> Self {
Self {
properties: HashMap::new(),
}
}
pub fn get<T, Error, F: FnOnce(&'a Property, &String) -> Result<T, Error>>(
&self,
name: &str,
f: F,
) -> Result<(&'a Property, T), PropertyMapError<Error>> {
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<String, Property>,
}
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<PropertyMap<'_>, 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::<i32>()).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<PropertyMap<'_>, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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(),);
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::<i32>()).unwrap().1, 15);
assert_eq!(pm.get("length", |_, s| s.parse::<i32>()).unwrap().1, -10);
assert_eq!(pm.get("angle", |_, s| s.parse::<f64>()).unwrap().1, 180f64);
assert_eq!(pm.get("angle", |_, s| s.parse::<i32>()).unwrap().1, 180);
assert_eq!(
pm.get("weight", |_, s| s.parse::<f32>()).unwrap().1,
0.42f32
);
assert_eq!(
pm.get("weight", |_, s| s.parse::<f64>()).unwrap().1,
0.42f64
);
// Error
assert!(pm.get("length", |_, s| s.parse::<u32>()).is_err());
assert!(pm.get("height", |_, s| s.parse::<f64>()).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::<f32>())
.unwrap()
.1,
0.15f32
);
}
} }