summaryrefslogtreecommitdiffstats
path: root/src/rulequery.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/rulequery.cpp')
-rw-r--r--src/rulequery.cpp199
1 files changed, 197 insertions, 2 deletions
diff --git a/src/rulequery.cpp b/src/rulequery.cpp
index ac8bf18..d4d8966 100644
--- a/src/rulequery.cpp
+++ b/src/rulequery.cpp
@@ -18,7 +18,9 @@
#include "rulequery.h"
+#include <QPair>
#include <QStringList>
+#include <QVector>
namespace {
@@ -89,6 +91,130 @@ QString compileTerm(const RuleTerm &term)
return isNegated(term.op) ? QStringLiteral("not ") + body : body;
}
+/// Splits on whitespace, keeping a double-quoted run as one token. Returns
+/// false when a quote is left open, which is a query this builder will not
+/// represent.
+bool tokenise(const QString &query, QStringList *out)
+{
+ QString current;
+ bool inQuotes = false;
+ bool has = false;
+
+ for (int i = 0; i < query.size(); ++i) {
+ const QChar c = query.at(i);
+ if (c == QLatin1Char('"')) {
+ inQuotes = !inQuotes;
+ current += c;
+ has = true;
+ } else if (!inQuotes && c.isSpace()) {
+ if (has) {
+ out->append(current);
+ current.clear();
+ has = false;
+ }
+ } else {
+ current += c;
+ has = true;
+ }
+ }
+
+ if (inQuotes)
+ return false;
+ if (has)
+ out->append(current);
+ return true;
+}
+
+bool fieldForPrefix(const QString &prefix, RuleTerm::Field *out)
+{
+ static const QVector<QPair<QString, RuleTerm::Field>> table = {
+ {QStringLiteral("from"), RuleTerm::From},
+ {QStringLiteral("to"), RuleTerm::To},
+ {QStringLiteral("cc"), RuleTerm::Cc},
+ {QStringLiteral("subject"), RuleTerm::Subject},
+ {QStringLiteral("tag"), RuleTerm::Tag},
+ {QStringLiteral("path"), RuleTerm::Folder},
+ {QStringLiteral("attachment"), RuleTerm::Attachment},
+ {QStringLiteral("date"), RuleTerm::Date},
+ };
+
+ for (const auto &entry : table) {
+ if (entry.first == prefix) {
+ *out = entry.second;
+ return true;
+ }
+ }
+ return false;
+}
+
+/// Parses ONE token into a term. Returns false for anything this builder does
+/// not represent, which is not the same as invalid: notmuch accepts far more
+/// than this.
+bool parseTerm(const QString &token, RuleTerm *out)
+{
+ const int colon = token.indexOf(QLatin1Char(':'));
+ if (colon <= 0)
+ return false;
+
+ RuleTerm::Field field;
+ if (!fieldForPrefix(token.left(colon), &field))
+ return false;
+
+ QString value = token.mid(colon + 1);
+ if (value.isEmpty())
+ return false;
+
+ bool quoted = false;
+ if (value.size() >= 2 && value.startsWith(QLatin1Char('"'))
+ && value.endsWith(QLatin1Char('"'))) {
+ value = value.mid(1, value.size() - 2);
+ quoted = true;
+ }
+ // A quote anywhere else means a shape this builder does not emit.
+ if (value.contains(QLatin1Char('"')))
+ return false;
+
+ out->field = field;
+
+ if (field == RuleTerm::Date) {
+ if (value.startsWith(QStringLiteral(".."))) {
+ out->op = RuleTerm::Before;
+ out->value = value.mid(2);
+ } else if (value.endsWith(QStringLiteral(".."))) {
+ out->op = RuleTerm::After;
+ out->value = value.chopped(2);
+ } else {
+ return false; // A two-sided range is not a row.
+ }
+ return !out->value.isEmpty();
+ }
+
+ if (field == RuleTerm::Folder) {
+ // Only the recursive form is representable; a bare path means
+ // something different to notmuch and must not be silently rewritten.
+ if (!value.endsWith(QStringLiteral("/**")))
+ return false;
+ value = value.chopped(3);
+ out->op = RuleTerm::Is;
+ out->value = value;
+ return !value.isEmpty();
+ }
+
+ // Tag and Attachment compile unquoted (see needsQuotes), so their
+ // operator must not be inferred from the quoting: reading a quoted tag
+ // back as a quoting operator would compile it unquoted and change the
+ // stored string.
+ if (field == RuleTerm::Attachment)
+ out->op = RuleTerm::Has;
+ else if (field == RuleTerm::Tag)
+ out->op = RuleTerm::Is;
+ else
+ out->op = quoted ? RuleTerm::Is : RuleTerm::Contains;
+
+ out->value = value;
+ return true;
+}
+
} // namespace
bool operator==(const RuleTerm &a, const RuleTerm &b)
@@ -133,6 +259,75 @@ QString RuleQuery::compile() const
RuleQuery RuleQuery::parse(const QString &query)
{
- Q_UNUSED(query);
- return RuleQuery();
+ RuleQuery out;
+
+ const QString trimmed = query.trimmed();
+ if (trimmed.isEmpty()) {
+ // An empty query is a rule with no rows yet, not a failure.
+ out.parsed = true;
+ return out;
+ }
+
+ QStringList tokens;
+ if (!tokenise(trimmed, &tokens))
+ return RuleQuery();
+
+ // Walk the chain: term, operator, term, ... Anything else rejects whole.
+ bool sawOr = false;
+ bool sawAnd = false;
+ int i = 0;
+
+ while (i < tokens.size()) {
+ bool negated = false;
+ if (tokens.at(i).compare(QStringLiteral("not"),
+ Qt::CaseInsensitive) == 0) {
+ negated = true;
+ ++i;
+ if (i >= tokens.size())
+ return RuleQuery();
+ }
+
+ RuleTerm term;
+ if (!parseTerm(tokens.at(i), &term))
+ return RuleQuery();
+
+ if (negated) {
+ // `not date:` has no row form: "not before" is "after", which the
+ // unnegated operators already express.
+ if (term.field == RuleTerm::Date)
+ return RuleQuery();
+ // The block IS the negation, so the row is stored un-negated and
+ // compile() re-applies the `and not`.
+ out.exclusions.append(term);
+ } else {
+ out.terms.append(term);
+ }
+ ++i;
+
+ if (i >= tokens.size())
+ break;
+
+ const QString glue = tokens.at(i).toLower();
+ if (glue == QStringLiteral("and")) {
+ sawAnd = true;
+ } else if (glue == QStringLiteral("or")) {
+ sawOr = true;
+ } else {
+ return RuleQuery(); // Not a joining word: unrepresentable.
+ }
+ ++i;
+ if (i >= tokens.size())
+ return RuleQuery(); // Trailing operator.
+ }
+
+ // Mixed and/or without parentheses is ambiguous to a reader and binds in
+ // a way the rows cannot show. Reject rather than guess.
+ if (sawAnd && sawOr)
+ return RuleQuery();
+ if (out.terms.isEmpty())
+ return RuleQuery();
+
+ out.join = sawOr ? Any : All;
+ out.parsed = true;
+ return out;
}