From 26dd50700305d613b86fe8789f13c8253f75984a Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 13 Aug 2026 11:12:47 +0200 Subject: feat(rulequery): parse an or-group with trailing exclusions --- src/rulequery.cpp | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) (limited to 'src/rulequery.cpp') diff --git a/src/rulequery.cpp b/src/rulequery.cpp index d4d8966..31996fa 100644 --- a/src/rulequery.cpp +++ b/src/rulequery.cpp @@ -215,6 +215,35 @@ bool parseTerm(const QString &token, RuleTerm *out) return true; } +/// Splits `(A or B) and not C and not D` into its group and its remainder. +/// Returns false when the query does not start with a balanced group. +bool splitLeadingGroup(const QString &query, QString *group, QString *rest) +{ + if (!query.startsWith(QLatin1Char('('))) + return false; + + int depth = 0; + bool inQuotes = false; + for (int i = 0; i < query.size(); ++i) { + const QChar c = query.at(i); + if (c == QLatin1Char('"')) + inQuotes = !inQuotes; + if (inQuotes) + continue; + if (c == QLatin1Char('(')) + ++depth; + else if (c == QLatin1Char(')')) { + --depth; + if (depth == 0) { + *group = query.mid(1, i - 1).trimmed(); + *rest = query.mid(i + 1).trimmed(); + return true; + } + } + } + return false; +} + } // namespace bool operator==(const RuleTerm &a, const RuleTerm &b) @@ -268,6 +297,58 @@ RuleQuery RuleQuery::parse(const QString &query) return out; } + QString group; + QString rest; + if (splitLeadingGroup(trimmed, &group, &rest)) { + // Only one nested shape is representable: an `or` group followed by + // `and not` exclusions. Anything else rejects whole. + if (group.contains(QLatin1Char('('))) + return RuleQuery(); + + const RuleQuery inner = parse(group); + if (!inner.parsed || inner.join != Any || !inner.exclusions.isEmpty()) + return RuleQuery(); + + out.join = Any; + out.terms = inner.terms; + + if (rest.isEmpty()) { + // A group with nothing after it compiles back WITHOUT parens, + // since compile() only adds them when exclusions follow. Round + // trip would break, so this is not representable. + return RuleQuery(); + } + + // The remainder must be nothing but `and not ` repetitions. + QStringList tail; + if (!tokenise(rest, &tail)) + return RuleQuery(); + + int i = 0; + while (i < tail.size()) { + if (tail.at(i).compare(QStringLiteral("and"), + Qt::CaseInsensitive) != 0) + return RuleQuery(); + ++i; + if (i >= tail.size() + || tail.at(i).compare(QStringLiteral("not"), + Qt::CaseInsensitive) != 0) + return RuleQuery(); + ++i; + if (i >= tail.size()) + return RuleQuery(); + + RuleTerm term; + if (!parseTerm(tail.at(i), &term)) + return RuleQuery(); + out.exclusions.append(term); + ++i; + } + + out.parsed = true; + return out; + } + QStringList tokens; if (!tokenise(trimmed, &tokens)) return RuleQuery(); -- cgit v1.2.3