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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -105,12 +105,11 @@ impl<'b> Parser for LangParser<'b> {
.downcast_rc::<SourceFile>()
.ok()
.map(|source| {
if source.path().is_empty() // Test mode
if source.path().is_empty()
// Test mode
{
None
}
else
{
} else {
let start = if source.path().starts_with("file:///") {
7
} else {
@ -119,7 +118,9 @@ impl<'b> Parser for LangParser<'b> {
let mut path = PathBuf::from(&source.path()[start..]);
match path.canonicalize() {
Ok(cano) => path = cano,
Err(err) => eprintln!("Failed to canonicalize path `{}`: {err}", source.path()),
Err(err) => {
eprintln!("Failed to canonicalize path `{}`: {err}", source.path())
}
}
path.pop();
Some(path)
@ -217,8 +218,7 @@ impl<'b> Parser for LangParser<'b> {
);
}
if path.is_some()
{
if path.is_some() {
if let Err(err) = std::env::set_current_dir(&current_dir) {
println!(
"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::elements::layout::LayoutToken;
use super::parser::ParserState;
use super::reports::Report;
use super::source::Token;
/// Represents the type of a layout
pub trait LayoutType: core::fmt::Debug {
/// Name of the layout
fn name(&self) -> &'static str;
/// Parses layout properties
fn parse_properties(&self, properties: &str) -> Result<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
fn expects(&self) -> Range<usize>;
@ -23,7 +32,7 @@ pub trait LayoutType: core::fmt::Debug {
&self,
token: LayoutToken,
id: usize,
properties: &Option<Box<dyn Any>>,
properties: &Box<dyn Any>,
compiler: &Compiler,
document: &dyn Document,
) -> Result<String, String>;

View file

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

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 }
}
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::rc::Rc;
@ -233,226 +232,6 @@ pub fn parse_paragraph<'a>(
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)]
mod tests {
use super::*;
@ -548,65 +327,4 @@ mod tests {
assert_eq!(escape_text('\\', ")", "A\\)B\\"), "A)B".to_string(),);
assert_eq!(escape_text('\\', ")", "A\\)B\\\\"), "A)B\\".to_string(),);
}
#[test]
fn property_parser_tests() {
let mut properties = HashMap::new();
properties.insert(
"width".to_string(),
Property::new(true, "Width of the element in em".to_string(), None),
);
properties.insert(
"length".to_string(),
Property::new(false, "Length in cm".to_string(), None),
);
properties.insert(
"angle".to_string(),
Property::new(
true,
"Angle in degrees".to_string(),
Some("180".to_string()),
),
);
properties.insert(
"weight".to_string(),
Property::new(false, "Weight in %".to_string(), Some("0.42".to_string())),
);
let parser = PropertyParser { properties };
let pm = parser.parse("width=15,length=-10").unwrap();
// Ok
assert_eq!(pm.get("width", |_, s| s.parse::<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
);
}
}