From 2dfd378d5f37896bdc52bcf0c099b88f5c475d99 Mon Sep 17 00:00:00 2001 From: Adam Porter Date: Wed, 9 Dec 2020 21:28:55 -0600 Subject: [PATCH] Add/Change: (heading-regexp), (heading) predicates Closes #64. Closes #68. Thanks to Feng Shu (@tumashu) for suggestions. --- README.org | 6 +++- org-ql.el | 42 +++++++++++++++++++---- org-ql.info | 82 ++++++++++++++++++++++++-------------------- tests/test-org-ql.el | 10 ++++++ 4 files changed, 95 insertions(+), 45 deletions(-) diff --git a/README.org b/README.org index 85a89e3..c91c9d5 100644 --- a/README.org +++ b/README.org @@ -207,8 +207,10 @@ Arguments are listed next to predicate names, where applicable. + =category (&optional categories)= :: Return non-nil if current heading is in one or more of ~CATEGORIES~ (a list of strings). + =done= :: Return non-nil if entry's ~TODO~ keyword is in ~org-done-keywords~. + =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 strings)= :: Return non-nil if current entry's heading matches all ~STRINGS~. Matching is done case-insensitively. - Aliases: =h=. ++ ~heading-regexp (&rest regexps)~ :: Return non-nil if current entry's heading matches all ~REGEXPS~ (regexp strings). Matching is done case-insensitively. + - Aliases: ~h*~. + =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 ~>=~. + =link (&optional description-or-target &key description target regexp-p)= :: Return non-nil if current heading contains a link matching arguments. ~DESCRIPTION-OR-TARGET~ is matched against the link's description and target. Alternatively, one or both of ~DESCRIPTION~ and ~TARGET~ may be matched separately. Without arguments, return non-nil if any link is found. + =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")~. @@ -518,9 +520,11 @@ Simple links may also be written manually in either sexp or non-sexp form, like: *Added* + Macro =org-ql-defpred=, used to define search predicates. (See [[file:examples/defpred.org][tutorial]].) ++ Predicate ~heading-regexp~, which matches regular expressions against heading text (alias: ~h*~). *Changed* + Helm support (including the command =helm-org-ql=) has been moved to a separate package, =helm-org-ql=. ++ Predicate ~heading~ now matches plain strings instead of regular expressions. *Internal* + Predicates are now defined more cleanly with a macro (=org-ql-defpred=) that consolidates functionality related to each predicate. This will also allow users to more easily define custom predicates. diff --git a/org-ql.el b/org-ql.el index 5e1257a..3f2154b 100644 --- a/org-ql.el +++ b/org-ql.el @@ -1027,28 +1027,56 @@ predicates." (list :regexp (rx bol (0+ space) ":STYLE:" (1+ space) "habit" (0+ space) eol)))) :body (org-is-habit-p)) -(org-ql-defpred (heading h) (&rest regexps) - "Return non-nil if current entry's heading matches all REGEXPS (regexp strings)." +(org-ql-defpred (heading h) (&rest strings) + "Return non-nil if current entry's heading matches all STRINGS. +Matching is done case-insensitively." :normalizers ((`(,predicate-names . ,args) ;; "h" alias. `(heading ,@args))) + ;; TODO: Adjust regexp to avoid matching in tag list. + :preambles ((`(,predicate-names ,string) + ;; Only one string: match with preamble, then let predicate confirm (because + ;; the match could be in e.g. the tags rather than the heading text). + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) + ,string) + 'no-group) + :case-fold t :query query)) + (`(,predicate-names . ,strings) + ;; Multiple strings: use preamble to match against first + ;; string, then let the predicate match the rest. + (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) + ,(car strings)) + 'no-group) + :case-fold t :query query))) + ;; TODO: In Org 9.2+, `org-get-heading' takes 2 more arguments. + :body (let ((heading (org-get-heading 'no-tags 'no-todo)) + (case-fold-search t)) + (--all? (string-match it heading) strings))) + +(org-ql-defpred (heading-regexp h*) (&rest regexps) + "Return non-nil if current entry's heading matches all REGEXPS (regexp strings). +Matching is done case-insensitively." + :normalizers ((`(,predicate-names . ,args) + ;; "h" alias. + `(heading-regexp ,@args))) ;; MAYBE: Adjust regexp to avoid matching in tag list. :preambles ((`(,predicate-names ,regexp) ;; Only one regexp: match with preamble, then let predicate confirm (because ;; the match could be in e.g. the tags rather than the heading text). (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) - ,regexp) + (regexp ,regexp)) 'no-group) - :query query)) + :case-fold t :query query)) (`(,predicate-names . ,regexps) ;; Multiple regexps: use preamble to match against first ;; regexp, then let the predicate match the rest. (list :regexp (rx-to-string `(seq bol (1+ "*") (1+ blank) (0+ nonl) - ,(car regexps)) + (regexp ,(car regexps))) 'no-group) - :query query))) + :case-fold t :query query))) ;; TODO: In Org 9.2+, `org-get-heading' takes 2 more arguments. - :body (let ((heading (org-get-heading 'no-tags 'no-todo))) + :body (let ((heading (org-get-heading 'no-tags 'no-todo)) + (case-fold-search t)) (--all? (string-match it heading) regexps))) (org-ql-defpred level (level-or-comparator &optional level) diff --git a/org-ql.info b/org-ql.info index ae676fb..3a6d274 100644 --- a/org-ql.info +++ b/org-ql.info @@ -401,10 +401,14 @@ Arguments are listed next to predicate names, where applicable. Return non-nil if entry’s ‘TODO’ keyword is in ‘org-done-keywords’. ‘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 strings)’ + Return non-nil if current entry’s heading matches all ‘STRINGS’. + Matching is done case-insensitively. • Aliases: h. +‘‘heading-regexp (&rest regexps)’’ + Return non-nil if current entry’s heading matches all ‘REGEXPS’ + (regexp strings). Matching is done case-insensitively. + • Aliases: ‘h*’. ‘level (level-or-comparator &optional level)’ Return non-nil if current heading’s outline level matches arguments. The following forms are accepted: ‘(level NUMBER)’: @@ -962,10 +966,14 @@ File: README.info, Node: 06-pre, Next: 05, Up: Changelog *Added* • Macro org-ql-defpred, used to define search predicates. (See tutorial (examples/defpred.org).) + • Predicate ‘heading-regexp’, which matches regular expressions + against heading text (alias: ‘h*’). *Changed* • Helm support (including the command helm-org-ql) has been moved to a separate package, helm-org-ql. + • Predicate ‘heading’ now matches plain strings instead of regular + expressions. *Internal* • Predicates are now defined more cleanly with a macro @@ -1477,40 +1485,40 @@ Node: org-ql-sparse-tree7544 Node: Queries8344 Node: Non-sexp query syntax9455 Node: General predicates11162 -Node: Ancestor/descendant predicates16575 -Node: Date/time predicates17703 -Node: Functions / Macros20358 -Node: Agenda-like views20656 -Node: Listing / acting-on results22061 -Node: Custom predicates27683 -Node: Dynamic block31174 -Node: Links33872 -Node: Tips34559 -Node: Changelog34877 -Node: 06-pre35573 -Node: 0536196 -Node: 04937673 -Node: 04837947 -Node: 04738294 -Node: 04638689 -Node: 04539089 -Node: 04439448 -Node: 04339805 -Node: 04240000 -Node: 04140161 -Node: 0440402 -Node: 03244335 -Node: 03144714 -Node: 0344911 -Node: 02347886 -Node: 02248114 -Node: 02148382 -Node: 0248581 -Node: 0152616 -Node: Notes52717 -Node: Comparison with Org Agenda searches52879 -Node: org-sidebar53751 -Node: License54030 +Node: Ancestor/descendant predicates16805 +Node: Date/time predicates17933 +Node: Functions / Macros20588 +Node: Agenda-like views20886 +Node: Listing / acting-on results22291 +Node: Custom predicates27913 +Node: Dynamic block31404 +Node: Links34102 +Node: Tips34789 +Node: Changelog35107 +Node: 06-pre35803 +Node: 0536638 +Node: 04938115 +Node: 04838389 +Node: 04738736 +Node: 04639131 +Node: 04539531 +Node: 04439890 +Node: 04340247 +Node: 04240442 +Node: 04140603 +Node: 0440844 +Node: 03244777 +Node: 03145156 +Node: 0345353 +Node: 02348328 +Node: 02248556 +Node: 02148824 +Node: 0249023 +Node: 0153058 +Node: Notes53159 +Node: Comparison with Org Agenda searches53321 +Node: org-sidebar54193 +Node: License54472  End Tag Table diff --git a/tests/test-org-ql.el b/tests/test-org-ql.el index 98fb630..f294c70 100644 --- a/tests/test-org-ql.el +++ b/tests/test-org-ql.el @@ -656,6 +656,16 @@ RESULTS should be a list of strings as returned by (org-ql-expect ('(heading "Take over" "world")) '("Take over the world")))) + (describe "(heading-regexp)" + (org-ql-it "with one argument" + (org-ql-expect ('(heading-regexp "w.rld")) + ;; NOTE: This--correctly--does not match the "Skype with president of + ;; Antarctica" heading, which has the tag ":world:" in its heading line. + '("Take over the world"))) + (org-ql-it "with two arguments" + (org-ql-expect ('(h* "T.ke over" "wor.*")) + '("Take over the world")))) + (describe "(link)" (org-ql-it "without arguments" (org-ql-expect ('(link))