diff --git a/lisp/gnus/gnus-search.el b/lisp/gnus/gnus-search.el index 2a8069d400..5574061457 100644 --- a/lisp/gnus/gnus-search.el +++ b/lisp/gnus/gnus-search.el @@ -82,6 +82,7 @@ (require 'gnus-sum) (require 'message) (require 'gnus-util) +(require 'peg) (require 'eieio) (eval-when-compile (require 'cl-lib)) (autoload 'eieio-build-class-alist "eieio-opt") @@ -390,8 +391,29 @@ gnus-search-contact-tables ;;; Search language -;; This "language" was generalized from the original IMAP search query -;; parsing routine. +;; Here's our attempt at using the PEG library to rewrite the parser. + +(defvar gnus-search-query-pexs + '((query (+ (or compound-term term))) + (term (or subquery prefixed-term kv-term value) term-end) + (subquery "(" query ")" + `(query -- (if (= 1 (length query)) query (list query)))) + (prefixed-term (or negated-term near-term)) + (negated-term (or "not " "-") term + `(term -- (list 'not term))) + (near-term "near " term + `(term -- (list 'near term))) + (compound-term (or or-terms and-terms)) + (or-terms (or subquery prefixed-term term) "or " (or subquery prefixed-term term) + `(t1 t2 -- (list 'or t1 t2))) + (and-terms (or subquery prefixed-term term) "and " (or subquery prefixed-term term) + `(t1 t2 -- (list 'and t1 t2))) + (value (or quoted-value plain-value)) + (plain-value (substring (+ [word]))) + (quoted-value "\"" (substring (+ (not "\"") (any))) "\"") + (kv-term plain-value ":" value + `(k v -- (gnus-search-query-parse-kv k v))) + (term-end (opt (+ [space]))))) (defun gnus-search-parse-query (string) "Turn STRING into an s-expression based query. @@ -459,108 +481,26 @@ gnus-search-parse-query structured query. Malformed, unusable or invalid queries will typically be silently ignored." (with-temp-buffer - ;; Set up the parsing environment. (insert string) (goto-char (point-min)) - ;; Now, collect the output terms and return them. - (let (out) - (while (not (gnus-search-query-end-of-input)) - (push (gnus-search-query-next-expr) out)) - (reverse out)))) - -(defun gnus-search-query-next-expr (&optional count halt) - "Return the next expression from the current buffer." - (let ((term (gnus-search-query-next-term count)) - (next (gnus-search-query-peek-symbol))) - ;; Deal with top-level expressions. And, or, not, near... What - ;; else? Notmuch also provides xor and adj. It also provides a - ;; "nearness" parameter for near and adj. - (cond - ;; Handle 'expr or expr' - ((and (eq next 'or) - (null halt)) - (list 'or term (gnus-search-query-next-expr 2))) - ;; Handle 'near operator. - ((eq next 'near) - (let ((near-next (gnus-search-query-next-expr 2))) - (if (and (stringp term) - (stringp near-next)) - (list 'near term near-next) - (signal 'gnus-search-parse-error - (list "\"Near\" keyword must appear between two plain strings."))))) - ;; Anything else - (t term)))) - -(defun gnus-search-query-next-term (&optional count) - "Return the next TERM from the current buffer." - (let ((term (gnus-search-query-next-symbol count))) - ;; What sort of term is this? - (cond - ;; negated term - ((eq term 'not) (list 'not (gnus-search-query-next-expr nil 'halt))) - ;; generic term - (t term)))) - -(defun gnus-search-query-peek-symbol () - "Return the next symbol from the current buffer, but don't consume it." - (save-excursion - (gnus-search-query-next-symbol))) - -(defun gnus-search-query-next-symbol (&optional count) - "Return the next symbol from the current buffer, or nil if we are -at the end of the buffer. If supplied COUNT skips some symbols before -returning the one at the supplied position." - (when (and (numberp count) (> count 1)) - (gnus-search-query-next-symbol (1- count))) - (let ((case-fold-search t)) - ;; end of input stream? - (unless (gnus-search-query-end-of-input) - ;; No, return the next symbol from the stream. - (cond - ;; Negated expression -- return it and advance one char. - ((looking-at "-") (forward-char 1) 'not) - ;; List expression -- we parse the content and return this as a list. - ((looking-at "(") - (gnus-search-parse-query (gnus-search-query-return-string ")" t))) - ;; Keyword input -- return a symbol version. - ((looking-at "\\band\\b") (forward-char 3) 'and) - ((looking-at "\\bor\\b") (forward-char 2) 'or) - ((looking-at "\\bnot\\b") (forward-char 3) 'not) - ((looking-at "\\bnear\\b") (forward-char 4) 'near) - ;; Plain string, no keyword - ((looking-at "[\"/]?\\b[^:]+\\([[:blank:]]\\|\\'\\)") - (gnus-search-query-return-string - (when (looking-at-p "[\"/]") t))) - ;; Assume a K:V expression. - (t (let ((key (gnus-search-query-expand-key - (buffer-substring - (point) - (progn - (re-search-forward ":" (point-at-eol) t) - (1- (point)))))) - (value (gnus-search-query-return-string - (when (looking-at-p "[\"/]") t)))) - (gnus-search-query-parse-kv key value))))))) + (with-peg-rules gnus-search-query-pexs + peg-run (peg query)))) (defun gnus-search-query-parse-kv (key value) "Handle KEY and VALUE, parsing and expanding as necessary. -This may result in (key value) being turned into a larger query -structure. - In the simplest case, they are simply consed together. String KEY is converted to a symbol." - (let () ;; return - (cond - ((member key gnus-search-date-keys) - (when (string= "after" key) - (setq key "since")) - (setq value (gnus-search-query-parse-date value))) - ((equal key "mark") - (setq value (gnus-search-query-parse-mark value))) - ((string= "message-id" key) - (setq key "id"))) - (or nil ;; return - (cons (intern key) value)))) + (setq key (gnus-search-query-expand-key key)) + (cond + ((member key gnus-search-date-keys) + (when (string= "after" key) + (setq key "since")) + (setq value (gnus-search-query-parse-date value))) + ((equal key "mark") + (setq value (gnus-search-query-parse-mark value))) + ((string= "message-id" key) + (setq key "id"))) + (cons (intern key) value)) (defun gnus-search-query-parse-date (value &optional rel-date) "Interpret VALUE as a date specification. @@ -647,44 +587,6 @@ gnus-search-query-expand-key ;; We completed to a unique known key. comp)))) -(defun gnus-search-query-return-string (&optional delimited trim) - "Return a string from the current buffer. -If DELIMITED is non-nil, assume the next character is a delimiter -character, and return everything between point and the next -occurrence of the delimiter, including the delimiters themselves. -If TRIM is non-nil, do not return the delimiters. Otherwise, -return one word." - ;; This function cannot handle nested delimiters, as it's not a - ;; proper parser. Ie, you cannot parse "to:bob or (from:bob or - ;; (cc:bob or bcc:bob))". - (let ((start (point)) - (delimiter (if (stringp delimited) - delimited - (when delimited - (char-to-string (char-after))))) - end) - (if delimiter - (progn - (when trim - ;; Skip past first delimiter if we're trimming. - (forward-char 1)) - (while (not end) - (unless (search-forward delimiter nil t (unless trim 2)) - (signal 'gnus-search-parse-error - (list (format "Unmatched delimited input with %s in query" delimiter)))) - (let ((here (point))) - (unless (equal (buffer-substring (- here 2) (- here 1)) "\\") - (setq end (if trim (1- (point)) (point)) - start (if trim (1+ start) start)))))) - (setq end (progn (re-search-forward "\\([[:blank:]]+\\|$\\)" (point-max) t) - (match-beginning 0)))) - (buffer-substring-no-properties start end))) - -(defun gnus-search-query-end-of-input () - "Are we at the end of input?" - (skip-chars-forward "[:blank:]") - (looking-at "$")) - ;;; Search engines ;; Search engines are implemented as classes. This is good for two