perkeep/pkg/search/expr.go

362 lines
6.9 KiB
Go

/*
Copyright 2013 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package search
import (
"context"
"fmt"
"log"
"strconv"
"strings"
)
const seeDocs = "\nSee: https://perkeep.org/doc/search-ui"
var (
noMatchingOpening = "No matching opening parenthesis"
noMatchingClosing = "No matching closing parenthesis"
noLiteralSupport = "No support for literals yet"
noQuotedLiteralSupport = "No support for quoted literals yet"
expectedAtom = "Expected an atom"
predicateError = "Predicates do not start with a colon"
trailingTokens = "After parsing finished there is still input left"
)
type parseExpError struct {
mesg string
t token
}
func (e parseExpError) Error() string {
return fmt.Sprintf("%s at position %d, token: %q %s", e.mesg, e.t.start, e.t.val, seeDocs)
}
func newParseExpError(mesg string, t token) error {
return parseExpError{mesg: mesg, t: t}
}
func andConst(a, b *Constraint) *Constraint {
return &Constraint{
Logical: &LogicalConstraint{
Op: "and",
A: a,
B: b,
},
}
}
func orConst(a, b *Constraint) *Constraint {
return &Constraint{
Logical: &LogicalConstraint{
Op: "or",
A: a,
B: b,
},
}
}
func notConst(a *Constraint) *Constraint {
return &Constraint{
Logical: &LogicalConstraint{
Op: "not",
A: a,
},
}
}
type parser struct {
tokens chan token
peeked *token
ctx context.Context
}
func newParser(ctx context.Context, exp string) parser {
_, tokens := lex(exp)
return parser{tokens: tokens, ctx: ctx}
}
func (p *parser) next() *token {
if p.peeked != nil {
t := p.peeked
p.peeked = nil
return t
}
return p.readInternal()
}
func (p *parser) peek() *token {
if p.peeked == nil {
p.peeked = p.readInternal()
}
return p.peeked
}
// ReadInternal should not be called directly, use 'next' or 'peek'
func (p *parser) readInternal() *token {
for t := range p.tokens {
return &t
}
return &token{tokenEOF, "", -1}
}
func (p *parser) stripNot() (negated bool) {
for {
switch p.peek().typ {
case tokenNot:
p.next()
negated = !negated
continue
}
return negated
}
}
func (p *parser) parseExp() (c *Constraint, err error) {
if p.peek().typ == tokenEOF {
return
}
c, err = p.parseOperand()
if err != nil {
return
}
for {
switch p.peek().typ {
case tokenAnd:
p.next()
case tokenOr:
p.next()
return p.parseOrRHS(c)
case tokenClose, tokenEOF:
return
}
c, err = p.parseAndRHS(c)
if err != nil {
return
}
}
}
func (p *parser) parseGroup() (c *Constraint, err error) {
i := p.next()
switch i.typ {
case tokenOpen:
c, err = p.parseExp()
if err != nil {
return
}
if p.peek().typ == tokenClose {
p.next()
return
} else {
err = newParseExpError(noMatchingClosing, *i)
return
}
}
err = newParseExpError("internal: do not call parseGroup when not on a '('", *i)
return
}
func (p *parser) parseOrRHS(lhs *Constraint) (c *Constraint, err error) {
var rhs *Constraint
c = lhs
for {
rhs, err = p.parseAnd()
if err != nil {
return
}
c = orConst(c, rhs)
switch p.peek().typ {
case tokenOr:
p.next()
case tokenAnd, tokenClose, tokenEOF:
return
}
}
}
func (p *parser) parseAnd() (c *Constraint, err error) {
for {
c, err = p.parseOperand()
if err != nil {
return
}
switch p.peek().typ {
case tokenAnd:
p.next()
case tokenOr, tokenClose, tokenEOF:
return
}
return p.parseAndRHS(c)
}
}
func (p *parser) parseAndRHS(lhs *Constraint) (c *Constraint, err error) {
var rhs *Constraint
c = lhs
for {
rhs, err = p.parseOperand()
if err != nil {
return
}
c = andConst(c, rhs)
switch p.peek().typ {
case tokenOr, tokenClose, tokenEOF:
return
case tokenAnd:
p.next()
continue
}
return
}
}
func (p *parser) parseOperand() (c *Constraint, err error) {
negated := p.stripNot()
i := p.peek()
switch i.typ {
case tokenError:
err = newParseExpError(i.val, *i)
return
case tokenEOF:
err = newParseExpError(expectedAtom, *i)
return
case tokenClose:
err = newParseExpError(noMatchingOpening, *i)
return
case tokenLiteral, tokenQuotedLiteral, tokenPredicate, tokenColon, tokenArg:
c, err = p.parseAtom()
case tokenOpen:
c, err = p.parseGroup()
}
if err != nil {
return
}
if negated {
c = notConst(c)
}
return
}
// AtomWords returns the parsed atom, the starting position of this
// atom and an error.
func (p *parser) atomWords() (a atom, start int, err error) {
i := p.peek()
start = i.start
a = atom{}
switch i.typ {
case tokenLiteral:
err = newParseExpError(noLiteralSupport, *i)
return
case tokenQuotedLiteral:
err = newParseExpError(noQuotedLiteralSupport, *i)
return
case tokenColon:
err = newParseExpError(predicateError, *i)
return
case tokenPredicate:
i := p.next()
a.predicate = i.val
}
for {
switch p.peek().typ {
case tokenColon:
p.next()
continue
case tokenArg:
i := p.next()
a.args = append(a.args, i.val)
continue
case tokenQuotedArg:
i := p.next()
var uq string
uq, err = strconv.Unquote(i.val)
if err != nil {
return
}
a.args = append(a.args, uq)
continue
}
return
}
}
func (p *parser) parseAtom() (*Constraint, error) {
a, start, err := p.atomWords()
if err != nil {
return nil, err
}
faultToken := func() token {
return token{
typ: tokenError,
val: a.String(),
start: start,
}
}
var c *Constraint
for _, k := range keywords {
matched, err := k.Match(a)
if err != nil {
return nil, newParseExpError(err.Error(), faultToken())
}
if matched {
c, err = k.Predicate(p.ctx, a.args)
if err != nil {
return nil, newParseExpError(err.Error(), faultToken())
}
return c, nil
}
}
t := faultToken()
err = newParseExpError(fmt.Sprintf("Unknown search predicate: %q", t.val), t)
log.Printf(err.Error())
return nil, err
}
func parseExpression(ctx context.Context, exp string) (*SearchQuery, error) {
base := &Constraint{
Permanode: &PermanodeConstraint{
SkipHidden: true,
},
}
sq := &SearchQuery{
Constraint: base,
}
exp = strings.TrimSpace(exp)
if exp == "" {
return sq, nil
}
p := newParser(ctx, exp)
c, err := p.parseExp()
if err != nil {
return nil, err
}
lastToken := p.next()
if lastToken.typ != tokenEOF {
switch lastToken.typ {
case tokenClose:
return nil, newParseExpError(noMatchingOpening, *lastToken)
}
return nil, newParseExpError(trailingTokens, *lastToken)
}
if c != nil {
sq.Constraint = andConst(base, c)
}
return sq, nil
}