155 lines
5.6 KiB
Rust
155 lines
5.6 KiB
Rust
use std::{iter::Peekable, mem, str::Chars};
|
|
|
|
use itertools::Itertools;
|
|
|
|
/// Parses a query into tokens, taking into account delimiters.
|
|
pub fn parse_query_into_tokens(query: &str) -> Vec<String> {
|
|
let parser = SentenceParser {
|
|
chars: query.chars().peekable(),
|
|
active_delimiter: None,
|
|
active_token: String::new(),
|
|
};
|
|
|
|
parser.into_iter().collect_vec()
|
|
}
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
enum WordDelimiter {
|
|
Separator,
|
|
DoubleQuote,
|
|
SingleQuote,
|
|
Backtick,
|
|
Whitespace,
|
|
}
|
|
|
|
fn convert_char_to_delimiter(c: char) -> Option<WordDelimiter> {
|
|
match c {
|
|
'\'' => Some(WordDelimiter::SingleQuote),
|
|
'"' => Some(WordDelimiter::DoubleQuote),
|
|
'`' => Some(WordDelimiter::Backtick),
|
|
',' | '.' | '!' | '?' => Some(WordDelimiter::Separator),
|
|
c if c.is_whitespace() => Some(WordDelimiter::Whitespace),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Parse a sentence into tokens for natural language classificiation.
|
|
/// The main difference from the default "split_whitespace" parser is
|
|
/// for matching double quotes and single quotes, we would keep their
|
|
/// closed content as one token.
|
|
struct SentenceParser<'a> {
|
|
chars: Peekable<Chars<'a>>,
|
|
active_delimiter: Option<WordDelimiter>,
|
|
active_token: String,
|
|
}
|
|
|
|
impl Iterator for SentenceParser<'_> {
|
|
type Item = String;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
while let Some(c) = self.chars.next() {
|
|
let delimiter = convert_char_to_delimiter(c);
|
|
let next_delimiter = self.chars.peek().map(|c| convert_char_to_delimiter(*c));
|
|
|
|
match delimiter {
|
|
Some(WordDelimiter::Whitespace) if self.active_delimiter.is_none() => {
|
|
if self.active_token.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
return Some(mem::take(&mut self.active_token));
|
|
}
|
|
Some(WordDelimiter::Separator) if self.active_delimiter.is_none() => {
|
|
if self.active_token.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// If next_delimiter is not whitespace or None, this means the delimiter
|
|
// is in the middle of a word. In this case, push the plain text character
|
|
// to the active token.
|
|
if next_delimiter
|
|
.map(|c| c == Some(WordDelimiter::Whitespace))
|
|
.unwrap_or(true)
|
|
{
|
|
return Some(mem::take(&mut self.active_token));
|
|
} else {
|
|
self.active_token.push(c);
|
|
}
|
|
}
|
|
Some(WordDelimiter::DoubleQuote) => {
|
|
let complete_quote =
|
|
if self.active_delimiter == Some(WordDelimiter::DoubleQuote) {
|
|
self.active_delimiter = None;
|
|
true
|
|
} else if !self.active_token.is_empty() || self.active_delimiter.is_some() {
|
|
false
|
|
} else {
|
|
self.active_delimiter = Some(WordDelimiter::DoubleQuote);
|
|
false
|
|
};
|
|
|
|
self.active_token.push(c);
|
|
if complete_quote {
|
|
let token = mem::take(&mut self.active_token);
|
|
// Skip empty quotes since this could be an in progress edit.
|
|
if token == "\"\"" {
|
|
continue;
|
|
}
|
|
return Some(token);
|
|
}
|
|
}
|
|
Some(WordDelimiter::Backtick) => {
|
|
let complete_quote = if self.active_delimiter == Some(WordDelimiter::Backtick) {
|
|
self.active_delimiter = None;
|
|
true
|
|
} else if !self.active_token.is_empty() || self.active_delimiter.is_some() {
|
|
false
|
|
} else {
|
|
self.active_delimiter = Some(WordDelimiter::Backtick);
|
|
false
|
|
};
|
|
|
|
self.active_token.push(c);
|
|
if complete_quote {
|
|
let token = mem::take(&mut self.active_token);
|
|
return Some(token);
|
|
}
|
|
}
|
|
Some(WordDelimiter::SingleQuote) => {
|
|
let complete_quote =
|
|
if self.active_delimiter == Some(WordDelimiter::SingleQuote) {
|
|
self.active_delimiter = None;
|
|
true
|
|
} else if !self.active_token.is_empty() || self.active_delimiter.is_some() {
|
|
false
|
|
} else {
|
|
self.active_delimiter = Some(WordDelimiter::SingleQuote);
|
|
false
|
|
};
|
|
|
|
self.active_token.push(c);
|
|
if complete_quote {
|
|
let token = mem::take(&mut self.active_token);
|
|
// Skip empty quotes since this could be an in progress edit.
|
|
if token == "''" {
|
|
continue;
|
|
}
|
|
return Some(token);
|
|
}
|
|
}
|
|
_ => self.active_token.push(c),
|
|
}
|
|
}
|
|
|
|
if self.active_token.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(mem::take(&mut self.active_token))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "parser_tests.rs"]
|
|
pub mod tests;
|