Skip to main content

senax_common/
fulltext.rs

1use nom::{
2    Err, IResult, Input, Parser,
3    branch::alt,
4    character::complete::char,
5    combinator::map,
6    error::ErrorKind,
7    multi::many0,
8    sequence::{delimited, preceded, terminated},
9};
10
11pub enum Word {
12    And(String),
13    Not(String),
14    Or(Vec<String>),
15}
16
17pub struct Query(pub Vec<Word>);
18
19impl Query {
20    pub fn db_query(&self) -> String {
21        let mut v = Vec::new();
22        for w in &self.0 {
23            match w {
24                Word::And(s) => {
25                    if !s.is_empty() {
26                        v.push(format!(r#"+"{s}""#));
27                    }
28                }
29                Word::Not(s) => {
30                    if !s.is_empty() {
31                        v.push(format!(r#"-"{s}""#));
32                    }
33                }
34                Word::Or(l) => {
35                    let s = l
36                        .iter()
37                        .filter(|i| !i.is_empty())
38                        .map(|i| format!(r#""{i}""#))
39                        .collect::<Vec<_>>()
40                        .join(" ");
41                    if !s.is_empty() {
42                        v.push(format!(r#"+({s})"#))
43                    }
44                }
45            }
46        }
47        let mut r = v.join(" ");
48        r.retain(|c| !char::is_control(c) && c != '\\');
49        r
50    }
51}
52
53pub fn parse(i: &str) -> Query {
54    let w = parse_words(i).unwrap();
55    let mut v = w.1;
56    if !w.0.is_empty() {
57        let i = w.0.replace(['"', '(', ')'], " ");
58        for r in parse_words(&i).unwrap().1 {
59            v.push(r);
60        }
61    }
62    Query(v)
63}
64
65fn parse_words(i: &str) -> IResult<&str, Vec<Word>> {
66    preceded(space, many0(parse_query)).parse(i)
67}
68
69fn parse_query(i: &str) -> IResult<&str, Word> {
70    terminated(alt((parse_not, parse_and, parse_or, parse_word)), space).parse(i)
71}
72
73fn parse_word(i: &str) -> IResult<&str, Word> {
74    map(alt((quoted, word)), |w| Word::And(w.to_string())).parse(i)
75}
76
77fn parse_and(i: &str) -> IResult<&str, Word> {
78    map(preceded(char('+'), alt((quoted, word))), |w| {
79        Word::And(w.to_string())
80    })
81    .parse(i)
82}
83
84fn parse_not(i: &str) -> IResult<&str, Word> {
85    map(preceded(char('-'), alt((quoted, word))), |w| {
86        Word::Not(w.to_string())
87    })
88    .parse(i)
89}
90
91fn parse_or(i: &str) -> IResult<&str, Word> {
92    map(
93        delimited(
94            alt((char('('), char('('))),
95            many0(delimited(
96                space,
97                alt((
98                    quoted,
99                    delimited(char('('), take_until_unbalanced('(', ')'), char(')')),
100                    delimited(char('('), take_until_unbalanced('(', ')'), char(')')),
101                    word,
102                )),
103                space,
104            )),
105            alt((char(')'), char(')'))),
106        ),
107        |w| Word::Or(w.iter().map(|v| v.to_string()).collect()),
108    )
109    .parse(i)
110}
111
112// https://docs.rs/parse-hyperlinks/0.23.3/src/parse_hyperlinks/lib.rs.html#41
113pub fn take_until_unbalanced(
114    opening_bracket: char,
115    closing_bracket: char,
116) -> impl Fn(&str) -> IResult<&str, &str> {
117    move |i: &str| {
118        let mut index = 0;
119        let mut bracket_counter = 0;
120        while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket][..]) {
121            index += n;
122            let mut it = i[index..].chars();
123            match it.next().unwrap_or_default() {
124                c if c == opening_bracket => {
125                    bracket_counter += 1;
126                    index += opening_bracket.len_utf8();
127                }
128                c if c == closing_bracket => {
129                    // Closing bracket.
130                    bracket_counter -= 1;
131                    index += closing_bracket.len_utf8();
132                }
133                // Can not happen.
134                _ => unreachable!(),
135            };
136            // We found the unmatched closing bracket.
137            if bracket_counter == -1 {
138                // We do not consume it.
139                index -= closing_bracket.len_utf8();
140                return Ok((&i[index..], &i[0..index]));
141            };
142        }
143        if bracket_counter == 0 {
144            Ok(("", i))
145        } else {
146            Err(Err::Error(nom::error::make_error(i, ErrorKind::TakeUntil)))
147        }
148    }
149}
150
151fn quoted(i: &str) -> IResult<&str, &str> {
152    delimited(char('"'), quoted_word, char('"')).parse(i)
153}
154
155fn word(input: &str) -> IResult<&str, &str> {
156    input.split_at_position1_complete(
157        |c| char::is_whitespace(c) || c == '"' || c == '(' || c == ')' || c == '(' || c == ')',
158        ErrorKind::AlphaNumeric,
159    )
160}
161
162fn quoted_word(input: &str) -> IResult<&str, &str> {
163    input.split_at_position_complete(|c| c == '"')
164}
165
166fn space(input: &str) -> IResult<&str, &str> {
167    input.split_at_position_complete(|c| !char::is_whitespace(c))
168}
169
170pub fn escape_like(input: &str) -> String {
171    let mut escaped = String::with_capacity(input.len());
172
173    for ch in input.chars() {
174        match ch {
175            '%' | '_' | '\\' => {
176                escaped.push('\\');
177                escaped.push(ch);
178            }
179            _ => escaped.push(ch),
180        }
181    }
182    escaped
183}
184
185pub fn to_like_contains(input: &str) -> String {
186    format!("%{}%", escape_like(input))
187}
188
189#[cfg(test)]
190mod tests {
191
192    use super::*;
193    #[test]
194    fn test() {
195        assert_eq!(parse("a テスト").db_query(), r#"+"a" +"テスト""#);
196        assert_eq!(parse(" a b ").db_query(), r#"+"a" +"b""#);
197        assert_eq!(parse(r#" a "bb "#).db_query(), r#"+"a" +"bb""#);
198        assert_eq!(parse(r#" a (bb "#).db_query(), r#"+"a" +"bb""#);
199        assert_eq!(parse("a -b").db_query(), r#"+"a" -"b""#);
200        assert_eq!(parse(r#""a(a""#).db_query(), r#"+"a(a""#);
201        assert_eq!(parse("a (b c)").db_query(), r#"+"a" +("b" "c")"#);
202        assert_eq!(parse("a(b c)").db_query(), r#"+"a" +("b" "c")"#);
203        assert_eq!(parse("a(b (c d))").db_query(), r#"+"a" +("b" "c d")"#);
204        assert_eq!(parse("a () b").db_query(), r#"+"a" +"b""#);
205        assert_eq!(parse(r#"a "" b"#).db_query(), r#"+"a" +"b""#);
206        assert_eq!(
207            parse(r#"a "cc dd)\\" b"#).db_query(),
208            r#"+"a" +"cc dd)" +"b""#
209        );
210        assert_eq!(parse("a (b c)").db_query(), r#"+"a" +("b" "c")"#);
211        assert_eq!(parse("a(b (c d))").db_query(), r#"+"a" +("b" "c d")"#);
212    }
213}