Add: Predicates outline-path, outline-path-segment

This commit is contained in:
Adam Porter 2019-10-07 12:41:23 -05:00
parent 8d680a8e79
commit c06e5518eb
3 changed files with 96 additions and 0 deletions

View file

@ -178,6 +178,10 @@ Arguments are listed next to predicate names, where applicable.
+ ~habit~ :: Return non-nil if entry is a habit. + ~habit~ :: Return non-nil if entry is a habit.
+ ~heading (&rest regexps)~ :: Return non-nil if current entry's heading matches all ~REGEXPS~ (regexp strings). + ~heading (&rest regexps)~ :: Return non-nil if current entry's heading matches all ~REGEXPS~ (regexp strings).
+ ~level (level-or-comparator &optional level)~ :: Return non-nil if current heading's outline level matches arguments. The following forms are accepted: ~(level NUMBER)~: Matches if heading level is ~NUMBER~. ~(level NUMBER NUMBER)~: Matches if heading level is equal to or between NUMBERs. ~(level COMPARATOR NUMBER)~: Matches if heading level compares to ~NUMBER~ with ~COMPARATOR~. ~COMPARATOR~ may be ~<~, ~<=~, ~>~, or ~>=~. + ~level (level-or-comparator &optional level)~ :: Return non-nil if current heading's outline level matches arguments. The following forms are accepted: ~(level NUMBER)~: Matches if heading level is ~NUMBER~. ~(level NUMBER NUMBER)~: Matches if heading level is equal to or between NUMBERs. ~(level COMPARATOR NUMBER)~: Matches if heading level compares to ~NUMBER~ with ~COMPARATOR~. ~COMPARATOR~ may be ~<~, ~<=~, ~>~, or ~>=~.
+ =outline-path (&rest strings)= :: Return non-nil if current node's outline path matches all of =STRINGS=. Each string may appear as a substring in any part of the node's outline path. For example, the path =Food/Fruits/Grapes= would match ~(olp "Fruit" "Grape")~.
- Aliases: =olp=.
+ =outline-path-segment (&rest strings)= :: Return non-nil if current node's outline path matches =STRINGS=. Matches =STRINGS= as a contiguous segment of the outline path. Each string is compared as a substring. For example the path =Food/Fruits/Grapes= would match ~(olps "Fruit" "Grape")~ but not ~(olps "Food" "Grape")~.
- Aliases: =olps=.
+ =path (&rest regexps)= :: Return non-nil if current heading's buffer's filename path matches any of =REGEXPS= (regexp strings). Without arguments, return non-nil if buffer is file-backed. + =path (&rest regexps)= :: Return non-nil if current heading's buffer's filename path matches any of =REGEXPS= (regexp strings). Without arguments, return non-nil if buffer is file-backed.
+ ~priority (&optional comparator-or-priority priority)~ :: Return non-nil if current heading has a certain priority. ~COMPARATOR-OR-PRIORITY~ should be either a comparator function, like ~<=~, or a priority string, like "A" (in which case (~=~ will be the comparator). If ~COMPARATOR-OR-PRIORITY~ is a comparator, ~PRIORITY~ should be a priority string. If both arguments are nil, return non-nil if heading has any defined priority. + ~priority (&optional comparator-or-priority priority)~ :: Return non-nil if current heading has a certain priority. ~COMPARATOR-OR-PRIORITY~ should be either a comparator function, like ~<=~, or a priority string, like "A" (in which case (~=~ will be the comparator). If ~COMPARATOR-OR-PRIORITY~ is a comparator, ~PRIORITY~ should be a priority string. If both arguments are nil, return non-nil if heading has any defined priority.
+ ~property (property &optional value)~ :: Return non-nil if current entry has ~PROPERTY~ (a string), and optionally ~VALUE~ (a string). Note that property inheritance is currently /not/ enabled for this predicate. If you need to test with inheritance, you could use a custom predicate form, like ~(org-entry-get (point) "PROPERTY" 'inherit)~. + ~property (property &optional value)~ :: Return non-nil if current entry has ~PROPERTY~ (a string), and optionally ~VALUE~ (a string). Note that property inheritance is currently /not/ enabled for this predicate. If you need to test with inheritance, you could use a custom predicate form, like ~(org-entry-get (point) "PROPERTY" 'inherit)~.
@ -365,6 +369,9 @@ Expands into a call to ~org-ql-select~ with the same arguments. For convenience
** 0.4-pre ** 0.4-pre
*Added*
+ Predicates =outline-path= (alias =olp=) and =outline-path-segment= (alias =olps=).
*Internal* *Internal*
+ Added generic node data cache to speed up recursive, tree-based queries. + Added generic node data cache to speed up recursive, tree-based queries.

View file

@ -426,6 +426,18 @@ Returns cons (INHERITED-TAGS . LOCAL-TAGS)."
org-ql-tags-cache)) org-ql-tags-cache))
(puthash position all-tags tags-cache)))) (puthash position all-tags tags-cache))))
(defun org-ql--outline-path ()
"Return outline path for heading at point."
(save-excursion
(let ((heading (nth 4 (org-heading-components))))
(if (org-up-heading-safe)
;; MAYBE: It seems wrong to call the cache function from
;; inside this function, like a violation of separation of
;; concern. Can this be rewritten to not work that way?
(append (org-ql--value-at (point) #'org-ql--outline-path)
(list heading))
(list heading)))))
;; TODO: Use --value-at for tags cache. ;; TODO: Use --value-at for tags cache.
(defun org-ql--value-at (position fn) (defun org-ql--value-at (position fn)
@ -556,6 +568,14 @@ Replaces bare strings with (regexp) selectors, and appropriate
(ts-apply :hour 23 :minute 59 :second 59)))) (ts-apply :hour 23 :minute 59 :second 59))))
`(,pred :to ,to))) `(,pred :to ,to)))
;; Outline paths.
(`(,(or 'outline-path 'olp) . ,strings)
;; Regexp quote headings.
`(outline-path ,@(mapcar #'regexp-quote strings)))
(`(,(or 'outline-path-segment 'olps) . ,strings)
;; Regexp quote headings.
`(outline-path-segment ,@(mapcar #'regexp-quote strings)))
;; Priorities ;; Priorities
(`(priority ,(and (or '= '< '> '<= '>=) comparator) ,letter) (`(priority ,(and (or '= '< '> '<= '>=) comparator) ,letter)
;; Quote comparator. ;; Quote comparator.
@ -922,6 +942,44 @@ Tests both inherited and local tags."
(when (tags-p local) (when (tags-p local)
(seq-intersection tags local)))))))) (seq-intersection tags local))))))))
;; MAYBE: Preambles for outline-path predicates. Not sure if possible without complicated logic.
(org-ql--defpred (outline-path olp) (&rest regexps)
"Return non-nil if current node's outline path matches all of REGEXPS.
Each string is compared as a regexp to each element of the node's
outline path with `string-match'. For example, if an entry's
outline path were \"Food/Fruits/Grapes\", it would match any of
the following queries:
(olp \"Food\")
(olp \"Fruits\")
(olp \"Food\" \"Fruits\")
(olp \"Fruits\" \"Grapes\")
(olp \"Food\" \"Grapes\")"
(let ((entry-olp (org-ql--value-at (point) #'org-ql--outline-path)))
(cl-loop for h in regexps
always (cl-member h entry-olp :test #'string-match))))
(org-ql--defpred (outline-path-segment olps) (&rest regexps)
"Return non-nil if current node's outline path matches segment REGEXPS.
Matches REGEXPS as a contiguous segment of the outline path.
Each regexp is compared to each element of the node's outline
path with `string-match'. For example, if an entry's outline
path were \"Food/Fruits/Grapes\", it would match any of the
following queries:
(olp \"Food\")
(olp \"Fruit\")
(olp \"Food\" \"Fruit\")
(olp \"Fruit\" \"Grape\")
But it would not match the following, because they do not match a
contiguous segment of the outline path:
(olp \"Food\" \"Grape\")"
;; MAYBE: Allow anchored matching.
(org-ql--infix-p regexps (org-ql--value-at (point) #'org-ql--outline-path)))
(org-ql--defpred (tags-inherited tags-i itags) (&rest tags) (org-ql--defpred (tags-inherited tags-i itags) (&rest tags)
"Return non-nil if current heading's inherited tags include one or more of TAGS (a list of strings). "Return non-nil if current heading's inherited tags include one or more of TAGS (a list of strings).
If TAGS is nil, return non-nil if heading has any inherited tags." If TAGS is nil, return non-nil if heading has any inherited tags."
@ -1302,6 +1360,18 @@ A and B are Org headline elements."
(a-priority t) (a-priority t)
(b-priority nil))))) (b-priority nil)))))
(defun org-ql--infix-p (infix list)
"Return non-nil if INFIX is an infix of LIST.
Each element of INFIX is compared using `string-match', so each
element should be a regexp string."
(cl-loop with infix-length = (length infix)
while (and list
(>= (length list) infix-length))
thereis (cl-loop for i in infix
for l in list
always (string-match i l))
do (pop list)))
;;;;; Plain query parsing ;;;;; Plain query parsing
;; This section implements parsing of "plain," non-Lisp queries using the `peg' ;; This section implements parsing of "plain," non-Lisp queries using the `peg'

View file

@ -479,6 +479,25 @@ RESULTS should be a list of strings as returned by
(org-ql-expect ((heading "Take over" "world")) (org-ql-expect ((heading "Take over" "world"))
'("Take over the world")))) '("Take over the world"))))
(describe "(outline-path)"
(org-ql-it "with one argument"
(org-ql-expect ((outline-path "symphony"))
'("Write a symphony")))
(org-ql-it "with two arguments"
(org-ql-expect ((outline-path "idea" "symphony"))
'("Write a symphony"))))
(describe "(outline-path-segment)"
(org-ql-it "with one argument"
(org-ql-expect ((outline-path-segment "symphony"))
'("Write a symphony")))
(org-ql-it "with a contiguous segment"
(org-ql-expect ((outline-path-segment "idea" "symphony"))
'("Write a symphony")))
(org-ql-it "with a non-contiguous segment"
(org-ql-expect ((outline-path-segment "data" "symphony"))
nil)))
(describe "(path)" (describe "(path)"
(org-ql-it "without arguments" (org-ql-it "without arguments"
(org-ql-expect ((path)) (org-ql-expect ((path))