Add some tests

This commit is contained in:
ef3d0c3e 2024-07-24 09:05:57 +02:00
parent 2617cbe507
commit 5ccd8048c2
8 changed files with 579 additions and 354 deletions

View file

@ -6,7 +6,7 @@ use mlua::{Function, Lua};
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use syntect::{easy::HighlightLines, highlighting::ThemeSet, parsing::SyntaxSet}; use syntect::{easy::HighlightLines, highlighting::ThemeSet, parsing::SyntaxSet};
use crate::{cache::cache::{Cached, CachedError}, compiler::compiler::{Compiler, Target}, document::{document::Document, element::{ElemKind, Element}}, parser::{parser::Parser, rule::RegexRule, source::{Source, Token}, util::{self, Property, PropertyParser}}}; use crate::{cache::cache::{Cached, CachedError}, compiler::compiler::{Compiler, Target}, document::{document::Document, element::{ElemKind, Element}}, parser::{parser::Parser, rule::RegexRule, source::{Source, Token}, util::{self, Property, PropertyMapError, PropertyParser}}};
use lazy_static::lazy_static; use lazy_static::lazy_static;
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -341,18 +341,32 @@ impl RegexRule for CodeRule
|prop, value| value.parse::<usize>().map_err(|e| (prop, e))) |prop, value| value.parse::<usize>().map_err(|e| (prop, e)))
{ {
Ok((_prop, offset)) => offset, Ok((_prop, offset)) => offset,
Err((prop, e)) => { Err(e) => match e {
reports.push( PropertyMapError::ParseError((prop, err)) => {
Report::build(ReportKind::Error, token.source(), token.start()) reports.push(
.with_message("Invalid Code Property") Report::build(ReportKind::Error, token.source(), token.start())
.with_label( .with_message("Invalid Code Property")
Label::new((token.source().clone(), token.start()+1..token.end())) .with_label(
.with_message(format!("Property `line_offset: {}` cannot be converted: {}", Label::new((token.source().clone(), token.start()+1..token.end()))
prop.fg(parser.colors().info), .with_message(format!("Property `line_offset: {}` cannot be converted: {}",
e.fg(parser.colors().error))) prop.fg(parser.colors().info),
.with_color(parser.colors().warning)) err.fg(parser.colors().error)))
.finish()); .with_color(parser.colors().warning))
return reports; .finish());
return reports;
},
PropertyMapError::NotFoundError(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Code Property")
.with_label(
Label::new((token.source().clone(), token.start()+1..token.end()))
.with_message(format!("Property `{}` doesn't exist",
err.fg(parser.colors().info)))
.with_color(parser.colors().warning))
.finish());
return reports;
}
} }
}; };

View file

@ -1,6 +1,6 @@
use mlua::{Error::BadArgument, Function, Lua}; use mlua::{Error::BadArgument, Function, Lua};
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use crate::{compiler::compiler::Compiler, document::{document::Document, element::{ElemKind, Element}}, lua::kernel::CTX, parser::{parser::Parser, rule::RegexRule, source::{Source, Token}, util::{self, Property, PropertyParser}}}; use crate::{compiler::compiler::Compiler, document::{document::Document, element::{ElemKind, Element}}, lua::kernel::CTX, parser::{parser::Parser, rule::RegexRule, source::{Source, Token}, util::{self, Property, PropertyMapError, PropertyParser}}};
use ariadne::{Fmt, Label, Report, ReportKind}; use ariadne::{Fmt, Label, Report, ReportKind};
use std::{collections::HashMap, ops::Range, rc::Rc, str::FromStr, sync::Arc}; use std::{collections::HashMap, ops::Range, rc::Rc, str::FromStr, sync::Arc};
@ -138,18 +138,32 @@ impl RegexRule for RawRule
|prop, value| ElemKind::from_str(value.as_str()).map_err(|e| (prop, e))) |prop, value| ElemKind::from_str(value.as_str()).map_err(|e| (prop, e)))
{ {
Ok((_prop, kind)) => kind, Ok((_prop, kind)) => kind,
Err((prop, e)) => { Err(e) => match e {
reports.push( PropertyMapError::ParseError((prop, err)) => {
Report::build(ReportKind::Error, token.source(), token.start()) reports.push(
.with_message("Invalid Raw Code Property") Report::build(ReportKind::Error, token.source(), token.start())
.with_label( .with_message("Invalid Raw Code Property")
Label::new((token.source().clone(), token.range.clone())) .with_label(
.with_message(format!("Property `kind: {}` cannot be converted: {}", Label::new((token.source().clone(), token.range.clone()))
prop.fg(parser.colors().info), .with_message(format!("Property `kind: {}` cannot be converted: {}",
e.fg(parser.colors().error))) prop.fg(parser.colors().info),
.with_color(parser.colors().warning)) err.fg(parser.colors().error)))
.finish()); .with_color(parser.colors().warning))
return reports; .finish());
return reports;
},
PropertyMapError::NotFoundError(err) => {
reports.push(
Report::build(ReportKind::Error, token.source(), token.start())
.with_message("Invalid Code Property")
.with_label(
Label::new((token.source().clone(), token.start()+1..token.end()))
.with_message(format!("Property `{}` doesn't exist",
err.fg(parser.colors().info)))
.with_color(parser.colors().warning))
.finish());
return reports;
}
} }
}; };

View file

@ -1,6 +1,6 @@
use std::rc::Rc; use std::{cell::{RefCell, RefMut}, collections::HashMap, rc::Rc};
use crate::parser::source::{Cursor, Source}; use crate::{document::{document::Document, element::Element}, lua::kernel::{Kernel, KernelHolder}, parser::{parser::{Parser, ReportColors}, rule::Rule, source::{Cursor, Source}, state::StateHolder}};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LineCursor pub struct LineCursor
@ -56,26 +56,6 @@ impl LineCursor
//eprintln!("({}, {c:#?}) ({} {})", self.pos, self.line, self.line_pos); //eprintln!("({}, {c:#?}) ({} {})", self.pos, self.line, self.line_pos);
prev = Some(c); prev = Some(c);
} }
/*
self.source.content()
.as_str()[start..pos+1]
.char_indices()
.for_each(|(at, c)| {
self.pos = at+start;
if c == '\n'
{
self.line += 1;
self.line_pos = 0;
}
else
{
self.line_pos += c.len_utf8();
}
});
*/
} }
else if pos < self.pos else if pos < self.pos
{ {
@ -114,3 +94,53 @@ impl From<&LineCursor> for Cursor
} }
} }
} }
#[derive(Debug)]
pub struct LsParser
{
rules: Vec<Box<dyn Rule>>,
colors: ReportColors,
// Parser state
pub state: RefCell<StateHolder>,
pub kernels: RefCell<HashMap<String, Kernel>>,
}
impl Parser for LsParser
{
fn colors(&self) -> &ReportColors { &self.colors }
fn rules(&self) -> &Vec<Box<dyn Rule>> { &self.rules }
fn rules_mut(&mut self) -> &mut Vec<Box<dyn Rule>> { &mut self.rules }
fn state(&self) -> std::cell::Ref<'_, StateHolder> { self.state.borrow() }
fn state_mut(&self) -> std::cell::RefMut<'_, StateHolder> { self.state.borrow_mut() }
fn push<'a>(&self, doc: &dyn Document, elem: Box<dyn Element>) {
todo!()
}
fn parse<'a>(&self, source: Rc<dyn Source>, parent: Option<&'a dyn Document<'a>>) -> Box<dyn Document<'a>+'a> {
todo!()
}
fn parse_into<'a>(&self, source: Rc<dyn Source>, document: &'a dyn Document<'a>) {
todo!()
}
}
impl KernelHolder for LsParser
{
fn get_kernel(&self, name: &str)
-> Option<RefMut<'_, Kernel>> {
RefMut::filter_map(self.kernels.borrow_mut(),
|map| map.get_mut(name)).ok()
}
fn insert_kernel(&self, name: String, kernel: Kernel)
-> RefMut<'_, Kernel> {
//TODO do not get
self.kernels.borrow_mut()
.insert(name.clone(), kernel);
self.get_kernel(name.as_str()).unwrap()
}
}

View file

@ -59,7 +59,7 @@ pub fn provide(semantic_tokens: &mut Vec<SemanticToken>, cursor: &mut LineCursor
} }
} }
pub fn semantic_token_from_document(document: &Document) -> Vec<SemanticToken> pub fn semantic_token_from_document(document: &dyn Document) -> Vec<SemanticToken>
{ {
let mut semantic_tokens = vec![]; let mut semantic_tokens = vec![];

View file

@ -101,32 +101,7 @@ impl Parser for LangParser
fn colors(&self) -> &ReportColors { &self.colors } fn colors(&self) -> &ReportColors { &self.colors }
fn rules(&self) -> &Vec<Box<dyn Rule>> { &self.rules } fn rules(&self) -> &Vec<Box<dyn Rule>> { &self.rules }
fn add_rule(&mut self, rule: Box<dyn Rule>, after: Option<&'static str>) fn rules_mut(&mut self) -> &mut Vec<Box<dyn Rule>> { &mut self.rules }
{
// Error on duplicate rule
let rule_name = (*rule).name();
self.rules.iter().for_each(|rule| {
if (*rule).name() != rule_name { return; }
panic!("Attempted to introduce duplicate rule: `{rule_name}`");
});
match after
{
Some(name) => {
let before = self.rules.iter()
.enumerate()
.find(|(_pos, r)| (r).name() == name);
match before
{
Some((pos, _)) => self.rules.insert(pos+1, rule),
_ => panic!("Unable to find rule named `{name}`, to insert rule `{}` after it", rule.name())
}
}
_ => self.rules.push(rule)
}
}
fn state(&self) -> std::cell::Ref<'_, StateHolder> { self.state.borrow() } fn state(&self) -> std::cell::Ref<'_, StateHolder> { self.state.borrow() }
fn state_mut(&self) -> std::cell::RefMut<'_, StateHolder> { self.state.borrow_mut() } fn state_mut(&self) -> std::cell::RefMut<'_, StateHolder> { self.state.borrow_mut() }

View file

@ -48,7 +48,39 @@ pub trait Parser: KernelHolder
fn colors(&self) -> &ReportColors; fn colors(&self) -> &ReportColors;
fn rules(&self) -> &Vec<Box<dyn Rule>>; fn rules(&self) -> &Vec<Box<dyn Rule>>;
fn add_rule(&mut self, rule: Box<dyn Rule>, after: Option<&'static str>); fn rules_mut(&mut self) -> &mut Vec<Box<dyn Rule>>;
fn add_rule(&mut self, rule: Box<dyn Rule>, after: Option<&'static str>) -> Result<(), String>
{
// Error on duplicate rule
let rule_name = (*rule).name();
if let Err(e) = self.rules().iter().try_for_each(|rule| {
if (*rule).name() != rule_name { return Ok(()); }
return Err(format!("Attempted to introduce duplicate rule: `{rule_name}`"));
})
{
return Err(e)
}
match after
{
Some(name) => {
let before = self.rules().iter()
.enumerate()
.find(|(_pos, r)| (r).name() == name);
match before
{
Some((pos, _)) => self.rules_mut().insert(pos+1, rule),
_ => return Err(format!("Unable to find rule named `{name}`, to insert rule `{}` after it", rule.name()))
}
}
_ => self.rules_mut().push(rule)
}
Ok(())
}
fn state(&self) -> Ref<'_, StateHolder>; fn state(&self) -> Ref<'_, StateHolder>;
fn state_mut(&self) -> RefMut<'_, StateHolder>; fn state_mut(&self) -> RefMut<'_, StateHolder>;

View file

@ -2,90 +2,88 @@ use std::collections::HashMap;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::{document::{document::{Document, DocumentAccessors}, element::ElemKind}, elements::paragraph::Paragraph}; use crate::{
document::{
document::{Document, DocumentAccessors},
element::ElemKind,
},
elements::paragraph::Paragraph,
};
/// Processes text for escape characters and paragraphing /// Processes text for escape characters and paragraphing
pub fn process_text(document: &dyn Document, content: &str) -> String pub fn process_text(document: &dyn Document, content: &str) -> String {
{ let mut escaped = false;
let mut escaped = false; let mut newlines = 0usize; // Consecutive newlines
let mut newlines = 0usize; // Consecutive newlines //println!("Processing: [{content}]");
//println!("Processing: [{content}]"); let processed = content
let processed = content .graphemes(true)
.grapheme_indices(true) .fold((String::new(), None), |(mut out, prev), g| {
.fold((String::new(), None), if newlines != 0 && g != "\n" {
|(mut out, prev), (_pos, g)| { newlines = 0;
if newlines != 0 && g != "\n"
{
newlines = 0;
// Add a whitespace if necessary // Add a whitespace if necessary
match out.chars().last() match out.chars().last() {
{ Some(c) => {
Some(c) => { // NOTE: \n is considered whitespace, so previous codepoint can be \n
// NOTE: \n is considered whitespace, so previous codepoint can be \n // (Which can only be done by escaping it)
// (Which can only be done by escaping it) if !c.is_whitespace() || c == '\n' {
if !c.is_whitespace() || c == '\n' out += " ";
{ }
out += " "; }
} None => {
} if document
None => { .last_element::<Paragraph>()
if document.last_element::<Paragraph>() .and_then(|par| {
.and_then(|par| par.find_back(|e| e.kind() != ElemKind::Invisible) par.find_back(|e| e.kind() != ElemKind::Invisible)
.and_then(|e| Some(e.kind() == ElemKind::Inline))) .and_then(|e| Some(e.kind() == ElemKind::Inline))
.unwrap_or(false) })
{ .unwrap_or(false)
out += " "; {
} out += " ";
} // Don't output anything }
} } // Don't output anything
} }
}
// Output grapheme literally when escaped // Output grapheme literally when escaped
if escaped if escaped {
{ escaped = false;
escaped = false; return (out + g, Some(g));
return (out + g, Some(g)); }
} // Increment newlines counter
// Increment newlines counter else if g == "\n" {
else if g == "\n" newlines += 1;
{ return (out, Some(g));
newlines += 1; }
return (out, Some(g)); // Determine if escaped
} else if g == "\\" {
// Determine if escaped escaped = !escaped;
else if g == "\\" return (out, Some(g));
{ }
escaped = !escaped; // Whitespaces
return (out, Some(g)); else if g.chars().count() == 1 && g.chars().last().unwrap().is_whitespace() {
} // Content begins with whitespace
// Whitespaces if prev.is_none() {
else if g.chars().count() == 1 && g.chars().last().unwrap().is_whitespace() if document.last_element::<Paragraph>().is_some() {
{ return (out + g, Some(g));
// Content begins with whitespace } else {
if prev.is_none() return (out, Some(g));
{ }
if document.last_element::<Paragraph>().is_some() }
{ // Consecutive whitespaces are converted to a single whitespace
return (out+g, Some(g)); else if prev.unwrap().chars().count() == 1
} && prev.unwrap().chars().last().unwrap().is_whitespace()
else {
{ return (out, Some(g));
return (out, Some(g)); }
} }
}
// Consecutive whitespaces are converted to a single whitespace
else if prev.unwrap().chars().count() == 1 &&
prev.unwrap().chars().last().unwrap().is_whitespace()
{
return (out, Some(g));
}
}
return (out + g, Some(g)); return (out + g, Some(g));
}).0.to_string(); })
.0
.to_string();
return processed; return processed;
} }
/// Processed a string and escapes a single token out of it /// Processed a string and escapes a single token out of it
@ -94,104 +92,124 @@ pub fn process_text(document: &dyn Document, content: &str) -> String
/// # Example /// # Example
/// ``` /// ```
/// assert_eq!(process_escaped('\\', "%", "escaped: \\%, also escaped: \\\\\\%, untouched: \\a"), /// assert_eq!(process_escaped('\\', "%", "escaped: \\%, also escaped: \\\\\\%, untouched: \\a"),
/// "escaped: %, also escaped: \\%, untouched \\a"); /// "escaped: %, also escaped: \\%, untouched: \\a");
/// ``` /// ```
pub fn process_escaped<S: AsRef<str>>(escape: char, token: &'static str, content: S) -> String pub fn process_escaped<S: AsRef<str>>(escape: char, token: &'static str, content: S) -> String {
{ let mut processed = String::new();
let mut processed = String::new(); let mut escaped = 0;
let mut escaped = 0; let mut token_it = token.chars().peekable();
let mut token_it = token.chars().peekable(); for c in content
for c in content.as_ref().chars() .as_ref()
.as_str() .chars()
.trim_start() .as_str()
.trim_end() .trim_start()
.chars() .trim_end()
{ .chars()
if c == escape {
{ if c == escape {
escaped += 1; escaped += 1;
} } else if escaped % 2 == 1 && token_it.peek().map_or(false, |p| *p == c) {
else if escaped % 2 == 1 && token_it.peek().map_or(false, |p| *p == c) let _ = token_it.next();
{ if token_it.peek() == None {
let _ = token_it.next(); (0..(escaped / 2)).for_each(|_| processed.push(escape));
if token_it.peek() == None escaped = 0;
{ token_it = token.chars().peekable();
(0..((escaped-1)/2)) processed.push_str(token);
.for_each(|_| processed.push(escape)); }
escaped = 0; } else {
token_it = token.chars().peekable(); if escaped != 0 {
processed.push_str(token); // Add untouched escapes
} (0..escaped).for_each(|_| processed.push('\\'));
} token_it = token.chars().peekable();
else escaped = 0;
{ }
if escaped != 0 processed.push(c);
{ }
// Add untouched escapes }
(0..escaped).for_each(|_| processed.push('\\')); // Add trailing escapes
token_it = token.chars().peekable(); (0..escaped).for_each(|_| processed.push('\\'));
escaped = 0;
}
processed.push(c);
}
}
// Add trailing escapes
(0..escaped).for_each(|_| processed.push('\\'));
processed processed
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Property pub struct Property {
{ required: bool,
required: bool, description: String,
description: String, default: Option<String>,
default: Option<String>,
} }
impl Property { impl Property {
pub fn new(required: bool, description: String, default: Option<String>) -> Self { pub fn new(required: bool, description: String, default: Option<String>) -> Self {
Self { required, description, default } Self {
required,
description,
default,
}
} }
} }
impl core::fmt::Display for Property impl core::fmt::Display for Property {
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.default.as_ref() match self.default.as_ref() {
{ None => write!(
None => write!(f, "{} {}", f,
["[Opt]", "[Req]"][self.required as usize], "{} {}",
self.description), ["[Opt]", "[Req]"][self.required as usize],
Some(default) => write!(f, "{} {} (Deafult: {})", self.description
["[Opt]", "[Req]"][self.required as usize], ),
self.description, Some(default) => write!(
default) f,
} "{} {} (Deafult: {})",
["[Opt]", "[Req]"][self.required as usize],
self.description,
default
),
}
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct PropertyMap<'a> pub enum PropertyMapError<E> {
{ ParseError(E),
pub(crate) properties: HashMap<String, (&'a Property, String)> NotFoundError(String),
}
#[derive(Debug)]
pub struct PropertyMap<'a> {
pub(crate) properties: HashMap<String, (&'a Property, String)>,
} }
impl<'a> PropertyMap<'a> { impl<'a> PropertyMap<'a> {
pub fn new() -> Self { pub fn new() -> Self {
Self { properties: HashMap::new() } Self {
properties: HashMap::new(),
}
} }
pub fn get<T, Error, F: FnOnce(&'a Property, &String) -> Result<T, Error>>(&self, name: &str, f: F) pub fn get<T, Error, F: FnOnce(&'a Property, &String) -> Result<T, Error>>(
-> Result<(&'a Property, T), Error> { &self,
let (prop, value) = self.properties.get(name).unwrap(); 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"
)))
}
};
f(prop, value).and_then(|value| Ok((*prop, value))) match f(prop, value) {
} Ok(parsed) => Ok((*prop, parsed)),
Err(err) => Err(PropertyMapError::ParseError(err)),
}
}
} }
pub struct PropertyParser { pub struct PropertyParser {
properties: HashMap<String, Property>, properties: HashMap<String, Property>,
} }
impl PropertyParser { impl PropertyParser {
@ -199,63 +217,60 @@ impl PropertyParser {
Self { properties } Self { properties }
} }
/// Attempts to build a default propertymap /// Attempts to build a default propertymap
/// ///
/// Returns an error if at least one [`Property`] is required and doesn't provide a default /// Returns an error if at least one [`Property`] is required and doesn't provide a default
pub fn default(&self) -> Result<PropertyMap<'_>, String> { pub fn default(&self) -> Result<PropertyMap<'_>, String> {
let mut properties = PropertyMap::new(); let mut properties = PropertyMap::new();
for (name, prop) in &self.properties for (name, prop) in &self.properties {
{ match (prop.required, prop.default.as_ref()) {
match (prop.required, prop.default.as_ref()) (true, None) => return Err(format!("Missing property `{name}` {prop}")),
{ (false, None) => {}
(true, None) => return Err(format!("Missing property `{name}` {prop}")), (_, Some(default)) => {
(false, None) => {}, properties
(_, Some(default)) => { .properties
properties.properties.insert( .insert(name.clone(), (prop, default.clone()));
name.clone(), }
(prop, default.clone()) }
); }
}
}
}
Ok(properties) Ok(properties)
} }
/// Parses properties string "prop1=value1, prop2 = val\,2" -> {prop1: value1, prop2: val,2} /// Parses properties string "prop1=value1, prop2 = val\,2" -> {prop1: value1, prop2: val,2}
/// ///
/// # Key-value pair /// # Key-value pair
/// ///
/// Property names/values are separated by a single '=' that cannot be escaped. /// Property names/values are separated by a single '=' that cannot be escaped.
/// Therefore names cannot contain the '=' character. /// Therefore names cannot contain the '=' character.
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// let properties = HashMap::new(); /// let mut properties = HashMap::new();
/// properties.insert("width", Property::new(true, "Width of the element in em", None)); /// properties.insert("width".to_string(),
/// /// Property::new(true, "Width of the element in em".to_string(), None));
/// let parser = PropertyParser::new(properties); ///
/// let pm = parser.parse("width=15").unwrap(); /// let parser = PropertyParser::new(properties);
/// /// let pm = parser.parse("width=15").unwrap();
/// assert!(pm.get("width", |_, val| val.parse::<i32>()) == Ok(15)); ///
/// ``` /// assert_eq!(pm.get("width", |_, s| s.parse::<i32>()).unwrap().1, 15);
/// # Return value /// ```
/// /// # Return value
/// Returns the parsed property map, or an error if either: ///
/// * A required property is missing /// Returns the parsed property map, or an error if either:
/// * An unknown property is present /// * A required property is missing
/// * A duplicate property is present /// * 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> { /// Note: Only ',' inside values can be escaped, other '\' are treated literally
let mut properties = PropertyMap::new(); pub fn parse(&self, content: &str) -> Result<PropertyMap<'_>, String> {
let mut try_insert = |name: &String, value: &String| let mut properties = PropertyMap::new();
-> Result<(), String> { let mut try_insert = |name: &String, value: &String| -> Result<(), String> {
let trimmed_name = name.trim_end().trim_start(); let trimmed_name = name.trim_end().trim_start();
let trimmed_value = value.trim_end().trim_start(); let trimmed_value = value.trim_end().trim_start();
let prop = match self.properties.get(trimmed_name) 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{}", None => return Err(format!("Unknown property name: `{trimmed_name}` (with value: `{trimmed_value}`). Valid properties are:\n{}",
self.properties.iter().fold(String::new(), self.properties.iter().fold(String::new(),
@ -263,81 +278,226 @@ impl PropertyParser {
Some(prop) => prop Some(prop) => prop
}; };
if let Some((_, previous)) = properties.properties.insert( if let Some((_, previous)) = properties
trimmed_name.to_string(), .properties
(prop, trimmed_value.to_string())) .insert(trimmed_name.to_string(), (prop, trimmed_value.to_string()))
{ {
return Err(format!("Duplicate property `{trimmed_name}`, previous value: `{previous}` current value: `{trimmed_value}`")) return Err(format!("Duplicate property `{trimmed_name}`, previous value: `{previous}` current value: `{trimmed_value}`"));
} }
Ok(()) Ok(())
}; };
let mut in_name = true; let mut in_name = true;
let mut name = String::new(); let mut name = String::new();
let mut value = String::new(); let mut value = String::new();
let mut escaped = 0usize; let mut escaped = 0usize;
for c in content.chars() for c in content.chars() {
{ if c == '\\' {
if c == '\\' escaped += 1;
{ } else if c == '=' && in_name {
escaped += 1; in_name = false;
} (0..escaped).for_each(|_| name.push('\\'));
else if c == '=' && in_name escaped = 0;
{ } else if c == ',' && !in_name {
in_name = false; if escaped % 2 == 0
(0..escaped).for_each(|_| name.push('\\')); // Not escaped
escaped = 0; {
} (0..escaped / 2).for_each(|_| value.push('\\'));
else if c == ',' && !in_name escaped = 0;
{ in_name = true;
if escaped % 2 == 0 // Not escaped
{
(0..escaped/2).for_each(|_| value.push('\\'));
escaped = 0;
in_name = true;
if let Err(e) = try_insert(&name, &value) { if let Err(e) = try_insert(&name, &value) {
return Err(e) return Err(e);
} }
name.clear(); name.clear();
value.clear(); value.clear();
} } else {
else (0..(escaped - 1) / 2).for_each(|_| value.push('\\'));
{ value.push(',');
(0..(escaped-1)/2).for_each(|_| value.push('\\')); escaped = 0;
value.push(','); }
escaped = 0; } else {
} if in_name {
} (0..escaped).for_each(|_| name.push('\\'));
else name.push(c)
{ } else {
if in_name { (0..escaped).for_each(|_| value.push('\\'));
(0..escaped).for_each(|_| name.push('\\')); value.push(c)
name.push(c) }
} escaped = 0;
else { }
(0..escaped).for_each(|_| value.push('\\')); }
value.push(c) if !in_name && value.trim_end().trim_start().is_empty() {
} return Err("Expected a value after last `=`".to_string());
escaped = 0; } else if name.is_empty() || value.is_empty() {
} return Err("Expected non empty property list.".to_string());
} }
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());
}
if let Err(e) = try_insert(&name, &value) { if let Err(e) = try_insert(&name, &value) {
return Err(e) return Err(e);
} }
// TODO: Missing properties if let Err(e) = self.properties.iter().try_for_each(|(key, prop)| {
if !properties.properties.contains_key(key) {
Ok(properties) 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::*;
use crate::{
document::langdocument::LangDocument,
elements::{comment::Comment, style::Style, text::Text},
parser::source::{SourceFile, Token},
};
use std::rc::Rc;
#[test]
fn process_text_tests() {
let source = Rc::new(SourceFile::with_content(
"".to_string(),
"".to_string(),
None,
));
let doc = LangDocument::new(source.clone(), None);
assert_eq!(process_text(&doc, "a\nb"), "a b");
assert_eq!(process_text(&doc, "a\n\nb"), "a b"); // Should never happen but why not
assert_eq!(process_text(&doc, "a\\b"), "ab");
assert_eq!(process_text(&doc, "a\\\nb"), "a\nb");
assert_eq!(process_text(&doc, "a\\\\b"), "a\\b");
assert_eq!(process_text(&doc, "a\\\\\nb"), "a\\ b");
assert_eq!(process_text(&doc, "\na"), "a");
let tok = Token::new(0..0, source);
doc.push(Box::new(Paragraph::new(tok.clone())));
// A space is appended as previous element is inline
(&doc as &dyn Document)
.last_element_mut::<Paragraph>()
.unwrap()
.push(Box::new(Text::new(tok.clone(), "TEXT".to_string())));
assert_eq!(process_text(&doc, "\na"), " a");
(&doc as &dyn Document)
.last_element_mut::<Paragraph>()
.unwrap()
.push(Box::new(Style::new(tok.clone(), 0, false)));
assert_eq!(process_text(&doc, "\na"), " a");
// Comments are ignored (kind => Invisible)
(&doc as &dyn Document)
.last_element_mut::<Paragraph>()
.unwrap()
.push(Box::new(Comment::new(tok.clone(), "COMMENT".to_string())));
assert_eq!(process_text(&doc, "\na"), " a");
}
#[test]
fn process_escaped_tests() {
assert_eq!(
process_escaped(
'\\',
"%",
"escaped: \\%, also escaped: \\\\\\%, untouched: \\a"
),
"escaped: %, also escaped: \\%, untouched: \\a"
);
assert_eq!(
process_escaped('"', "><)))°>", "Escaped fish: \"><)))°>"),
"Escaped fish: ><)))°>".to_string()
);
assert_eq!(
process_escaped('\\', "]", "Escaped \\]"),
"Escaped ]".to_string()
);
assert_eq!(
process_escaped('\\', "]", "Unescaped \\\\]"),
"Unescaped \\\\]".to_string()
);
assert_eq!(
process_escaped('\\', "]", "Escaped \\\\\\]"),
"Escaped \\]".to_string()
);
assert_eq!(
process_escaped('\\', "]", "Unescaped \\\\\\\\]"),
"Unescaped \\\\\\\\]".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::new(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
);
}
} }

View file

@ -51,9 +51,9 @@ impl Backend {
let parser = LangParser::default(); let parser = LangParser::default();
let doc = parser.parse(Rc::new(source), None); let doc = parser.parse(Rc::new(source), None);
let semantic_tokens = semantic_token_from_document(&doc); //let semantic_tokens = semantic_token_from_document(&doc);
self.semantic_token_map //self.semantic_token_map
.insert(params.uri.to_string(), semantic_tokens); // .insert(params.uri.to_string(), semantic_tokens);
} }
} }