use std::marker::PhantomData;
mod parser_iterator;
mod chartest;
pub mod keyword_converter;
use crate::parser_iterator::ParserIterator;
use crate::chartest::*;
pub use crate::keyword_converter::KeywordConverter;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Token<K> {
Word(String),
Phrase{
text: String,
invert: bool,
},
KeyValue{
key: K,
value: String,
invert: bool,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ParserState {
Space,
Word,
Phrase,
KeyValue,
Prefixed,
}
pub struct Parser<CK,K>
where
CK: KeywordConverter<K>,
{
pub keyword_converter: CK,
phantom_keyword: PhantomData<K>,
}
impl<CK, K> Parser<CK, K>
where
CK: KeywordConverter<K>,
{
pub fn new(keyword_converter: CK) -> Self {
Self{
keyword_converter: keyword_converter,
phantom_keyword: PhantomData,
}
}
pub fn parse(&self, text: &str) -> Vec<Token<K>> {
let mut match_start: usize = 0;
let mut key_start: usize = 0;
let mut state: ParserState = ParserState::Space;
let mut iter = ParserIterator::new(text);
let mut result: Vec<Token<K>> = Vec::new();
let mut ok: bool;
let mut invert_flag: bool = false;
let mut prefix_key: Option<K> = None;
loop {
ok = false;
match state {
ParserState::Space => {
if iter.consume_chars(is_space_char) > 0 {
invert_flag = false;
}
match_start = iter.index();
match iter.peek() {
Some('-') => {
invert_flag = true;
iter.next();
if let Some(c) = iter.peek() {
if is_keyword_safe_first_char(c) {
state = ParserState::KeyValue;
} else if let Some(key) = self.keyword_converter.try_convert_prefix(c) {
state = ParserState::Prefixed;
iter.next();
prefix_key = Some(key);
} else {
state = ParserState::Phrase;
}
} else {
state = ParserState::Word;
}
},
Some(c) => {
if is_keyword_safe_first_char(c) {
state = ParserState::KeyValue;
} else if let Some(key) = self.keyword_converter.try_convert_prefix(c) {
state = ParserState::Prefixed;
iter.next();
prefix_key = Some(key);
} else {
state = ParserState::Phrase;
}
},
None => {
break;
}
}
if state != ParserState::Space {
key_start = iter.index();
}
ok = true;
},
ParserState::Word => {
iter.consume_chars(is_non_space_char);
result.push(Token::Word(text[match_start..iter.index()].to_string()));
state = ParserState::Space;
ok = true;
},
ParserState::Phrase => {
if let Some(value) = consume_quoted_literal(&mut iter) {
result.push(Token::Phrase{
text: value,
invert: invert_flag,
});
state = ParserState::Space;
ok = true;
}
},
ParserState::KeyValue => {
if iter.consume_chars(is_keyword_safe_char) > 0 {
let key_end = iter.index();
if iter.try_consume_char(':') {
if !is_token_end(iter.peek()) {
if let Some(key) = self.keyword_converter.try_convert_keyword(&text[key_start..key_end]) {
let value = consume_quick_quotable_chars(&mut iter, text);
result.push(Token::KeyValue{
key: key,
value: value,
invert: invert_flag,
});
ok = true;
state = ParserState::Space;
}
}
}
}
},
ParserState::Prefixed => {
let value = consume_quick_quotable_chars(&mut iter, text);
if value.len() > 0 {
if let Some(key) = prefix_key {
result.push(Token::KeyValue{
key: key,
value: value,
invert: invert_flag,
});
state = ParserState::Space;
ok = true;
}
prefix_key = None;
}
},
}
if !ok {
state = ParserState::Word;
}
}
return result;
}
}
fn consume_quick_quotable_chars(
iter: &mut ParserIterator,
text: &str
) -> String {
if let Some(value) = consume_quoted_literal(iter) {
return value;
}
if let Some(value) = consume_quoted(iter, ';',';') {
return value;
}
let start = iter.index();
iter.consume_chars(is_non_space_char);
return text[start..iter.index()].to_string();
}
fn consume_quoted_literal(iter: &mut ParserIterator) -> Option<String> {
let mut value: Option<String>;
value = consume_quoted(iter, '"','"');
if value.is_some() { return value; }
value = consume_quoted(iter, '»','«');
if value.is_some() { return value; }
value = consume_quoted(iter, '«','»');
if value.is_some() { return value; }
return None;
}
fn consume_quoted(iter: &mut ParserIterator, start_quote: char, end_quote: char) -> Option<String> {
if iter.peek() != Some(start_quote) {
return None;
}
iter.checkpoint();
iter.next(); let mut out: String = "".to_string();
loop {
match iter.next() {
Some('\\') => {
let peek = iter.peek();
if peek == Some(end_quote) {
iter.next();
out.push(end_quote);
} else if peek == Some('\\') {
iter.next();
out += "\\";
} else {
out += "\\"
}
},
Some(c) => {
if c == end_quote && is_token_end(iter.peek()) {
iter.drop_checkpoint();
return Some(out);
} else {
out.push(c);
}
},
None => {
break;
}
}
}
iter.restore();
return None;
}
#[cfg(test)]
mod tests {
use crate::keyword_converter::NullKeywordConverter;
use crate::keyword_converter::StringKeywordConverter;
use super::*;
#[test]
fn all_features() {
let mut converter = StringKeywordConverter::new();
converter.add_key("quick");
converter.add_key("foo");
converter.add_prefix('#');
converter.add_prefix('!');
let parser_config = Parser::new(converter);
assert_eq!(
parser_config.parse(" -#Hello -»world« -foo:\"bar \\\\\\\"bat\" quick:;quote text; !map foo:bar ~tilde bar:baz \t"),
vec![
Token::KeyValue{key: "#".to_string(), value:"Hello".to_string(), invert: true},
Token::Phrase{text: "world".to_string(), invert: true},
Token::KeyValue{key: "foo".to_string(), value:"bar \\\"bat".to_string(), invert: true},
Token::KeyValue{key: "quick".to_string(), value:"quote text".to_string(), invert: false},
Token::KeyValue{key: "!".to_string(), value:"map".to_string(), invert: false},
Token::KeyValue{key: "foo".to_string(), value:"bar".to_string(), invert: false},
Token::Word("~tilde".to_string()),
Token::Word("bar:baz".to_string()),
]
);
}
#[test]
fn empty() {
let converter = NullKeywordConverter::<()>::new();
let parser_config = Parser::new(converter);
assert_eq!(
parser_config.parse(""),
vec![]
);
}
#[test]
fn space_only() {
let converter = NullKeywordConverter::<()>::new();
let parser_config = Parser::new(converter);
assert_eq!(
parser_config.parse(" \t \n"),
vec![]
);
}
#[test]
fn null_keyword_converter() {
let converter = NullKeywordConverter::<()>::new();
let parser_config = Parser::new(converter);
assert_eq!(
parser_config.parse("word \"phrase\" not_key:value -«not phrase» #tag"),
vec![
Token::Word("word".to_string()),
Token::Phrase{text: "phrase".to_string(), invert: false},
Token::Word("not_key:value".to_string()),
Token::Phrase{text: "not phrase".to_string(), invert: true},
Token::Word("#tag".to_string()),
]
);
}
}