mirror of https://github.com/perkeep/perkeep.git
362 lines
6.9 KiB
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
|
|
}
|