[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] master 556e03b 7/8: Overhauled parsing method, and added conditio
From: |
Ian Dunn |
Subject: |
[elpa] master 556e03b 7/8: Overhauled parsing method, and added conditional blocks |
Date: |
Sat, 23 Dec 2017 14:09:23 -0500 (EST) |
branch: master
commit 556e03be1068d746e3d672185987c433302229fa
Author: Ian Dunn <address@hidden>
Commit: Ian Dunn <address@hidden>
Overhauled parsing method, and added conditional blocks
Also bumped version to 1.0beta3
---
org-edna-tests.el | 277 ++++++++++++++++++++++++++----
org-edna.el | 506 +++++++++++++++++++++++++++++++++++++++++-------------
org-edna.info | 287 +++++++++++++++++++++++--------
org-edna.org | 105 ++++++++++-
4 files changed, 940 insertions(+), 235 deletions(-)
diff --git a/org-edna-tests.el b/org-edna-tests.el
index d3911db..a6b3554 100644
--- a/org-edna-tests.el
+++ b/org-edna-tests.el
@@ -70,89 +70,293 @@
(ert-deftest org-edna-parse-form-no-arguments ()
(let* ((input-string "test-string")
- (parsed (org-edna-parse-form input-string)))
+ (parsed (org-edna-parse-string-form input-string)))
(should parsed)
- (should (= (length parsed) 4))
- (pcase-let* ((`(,token ,args ,modifier ,pos) parsed))
- (should (eq token 'test-string))
+ (should (= (length parsed) 2))
+ (pcase-let* ((`((,key . ,args) ,pos) parsed))
+ (should (eq key 'test-string))
(should (not args))
- (should (not modifier))
(should (= pos 11)))))
(ert-deftest org-edna-parse-form-no-arguments-modifier ()
(let* ((input-string "!test-string")
- (parsed (org-edna-parse-form input-string)))
+ (parsed (org-edna-parse-string-form input-string)))
(should parsed)
- (should (= (length parsed) 4))
- (pcase-let* ((`(,token ,args ,modifier ,pos) parsed))
- (should (eq token 'test-string))
+ (should (= (length parsed) 2))
+ (pcase-let* ((`((,key . ,args) ,pos) parsed))
+ (should (eq key '!test-string))
(should (not args))
- (should (eq modifier '!))
(should (= pos 12)))))
(ert-deftest org-edna-parse-form-single-argument ()
(let* ((input-string "test-string(abc)")
- (parsed (org-edna-parse-form input-string)))
+ (parsed (org-edna-parse-string-form input-string)))
(should parsed)
- (should (= (length parsed) 4))
- (pcase-let* ((`(,token ,args ,modifier ,pos) parsed))
- (should (eq token 'test-string))
+ (should (= (length parsed) 2))
+ (pcase-let* ((`((,key . ,args) ,pos) parsed))
+ (should (eq key 'test-string))
(should (= (length args) 1))
(should (symbolp (nth 0 args)))
(should (eq (nth 0 args) 'abc))
- (should (not modifier))
(should (= pos (length input-string))))))
(ert-deftest org-edna-parse-form-string-argument ()
(let* ((input-string "test-string(abc \"def (ghi)\")")
- (parsed (org-edna-parse-form input-string)))
+ (parsed (org-edna-parse-string-form input-string)))
(should parsed)
- (should (= (length parsed) 4))
- (pcase-let* ((`(,token ,args ,modifier ,pos) parsed))
- (should (eq token 'test-string))
+ (should (= (length parsed) 2))
+ (pcase-let* ((`((,key . ,args) ,pos) parsed))
+ (should (eq key 'test-string))
(should (= (length args) 2))
(should (symbolp (nth 0 args)))
(should (eq (nth 0 args) 'abc))
(should (stringp (nth 1 args)))
(should (string-equal (nth 1 args) "def (ghi)"))
- (should (not modifier))
(should (= pos (length input-string))))))
(ert-deftest org-edna-parse-form-multiple-forms ()
(let ((input-string "test-string1 test-string2")
pos)
- (pcase-let* ((`(,token1 ,args1 ,modifier1 ,pos1) (org-edna-parse-form
input-string)))
- ;; (should (and token1 args1 modifier1 pos1))
- (should (eq token1 'test-string1))
+ (pcase-let* ((`((,key1 . ,args1) ,pos1) (org-edna-parse-string-form
input-string)))
+ (should (eq key1 'test-string1))
(should (not args1))
- (should (not modifier1))
(should (= pos1 13))
(setq pos pos1))
- (pcase-let* ((`(,token2 ,args2 ,modifier2 ,pos2) (org-edna-parse-form
(substring input-string pos))))
- (should (eq token2 'test-string2))
+ (pcase-let* ((`((,key2 . ,args2) ,pos2) (org-edna-parse-string-form
(substring input-string pos))))
+ (should (eq key2 'test-string2))
(should (not args2))
- (should (not modifier2))
(should (= pos2 12)))))
(ert-deftest org-edna-parse-form-empty-argument-list ()
(let ((input-string "test-string1()"))
- (pcase-let* ((`(,token1 ,args1 ,modifier1 ,pos1) (org-edna-parse-form
input-string)))
- (should (eq token1 'test-string1))
+ (pcase-let* ((`((,key1 ,args1) ,pos1) (org-edna-parse-string-form
input-string)))
+ (should (eq key1 'test-string1))
(should (not args1))
- (should (not modifier1))
(should (= pos1 (length input-string))))))
(ert-deftest org-edna-parse-form-condition ()
(let ((input-string "variable-set?()"))
- (pcase-let* ((`(,token1 ,args1 ,modifier1 ,pos1) (org-edna-parse-form
input-string))
- (`(,type . ,func) (org-edna--function-for-key token1)))
- (should (eq token1 'variable-set?))
+ (pcase-let* ((`((,key1 . ,args1) ,pos1) (org-edna-parse-string-form
input-string))
+ (`(,modifier1 . ,key1) (org-edna-break-modifier key1))
+ (`(,type . ,func) (org-edna--function-for-key key1)))
+ (should (eq key1 'variable-set?))
(should (not args1))
(should (not modifier1))
(should (= pos1 (length input-string)))
(should (eq type 'condition))
(should (eq func 'org-edna-condition/variable-set?)))))
+(ert-deftest org-edna-form-to-sexp-no-arguments ()
+ (let* ((input-string "self")
+ (sexp (org-edna-string-form-to-sexp-form input-string 'condition)))
+ (should (equal
+ sexp
+ '((self)
+ (!done?))))))
+
+(ert-deftest org-edna-form-to-sexp-arguments ()
+ (let* ((input-string "match(\"checklist\") todo!(TODO)")
+ (sexp (org-edna-string-form-to-sexp-form input-string 'action)))
+ (should (equal
+ sexp
+ '((match "checklist")
+ (todo! TODO))))))
+
+(ert-deftest org-edna-form-to-sexp-if-no-else ()
+ (let* ((input-string "if match(\"checklist\") done? then self todo!(TODO)
endif")
+ (sexp (org-edna-string-form-to-sexp-form input-string 'action)))
+ (should (equal
+ sexp
+ '((if ((match "checklist")
+ (done?))
+ ((self)
+ (todo! TODO))
+ nil))))))
+
+(ert-deftest org-edna-form-to-sexp-if-else ()
+ (let* ((input-string "if match(\"checklist\") done? then self todo!(TODO)
else siblings todo!(DONE) endif")
+ (sexp (org-edna-string-form-to-sexp-form input-string 'action)))
+ (should (equal
+ sexp
+ '((if ((match "checklist")
+ (done?))
+ ((self)
+ (todo! TODO))
+ ((siblings)
+ (todo! DONE))))))))
+
+(ert-deftest org-edna-expand-sexp-form ()
+ ;; Override cl-gentemp so we have a repeatable test
+ (cl-letf* (((symbol-function 'cl-gentemp) (lambda (&optional prefix) (intern
(format "%s1" prefix))))
+ (input-sexp '((self)
+ (!done?)))
+ (output-form (org-edna--expand-sexp-form input-sexp)))
+ (should (equal
+ output-form
+ '(let ((targets1 nil)
+ (consideration1 nil)
+ (blocking-entry1 nil))
+ (setq targets1 (org-edna--add-targets targets1
(org-edna-finder/self)))
+ (setq blocking-entry1
+ (or blocking-entry1
+ (org-edna--handle-condition 'org-edna-condition/done?
+ '! 'nil targets1
+ consideration1))))))))
+
+(ert-deftest org-edna-expand-sexp-form-multiple ()
+ (cl-letf* ((target-ctr 0)
+ (consideration-ctr 0)
+ (blocking-entry-ctr 0)
+ ((symbol-function 'cl-gentemp)
+ (lambda (&optional prefix)
+ (let ((ctr (pcase prefix
+ ("targets" (cl-incf target-ctr))
+ ("consideration" (cl-incf consideration-ctr))
+ ("blocking-entry" (cl-incf blocking-entry-ctr))
+ (_ 0))))
+ (intern (format "%s%s" prefix ctr)))))
+ (input-sexp '(((match "checklist")
+ (todo! DONE))
+ ((siblings)
+ (todo! TODO))))
+ (expected-form
+ '(let ((targets1 nil)
+ (consideration1 nil)
+ (blocking-entry1 nil))
+ ;; Don't need a new set of variables
+ (progn
+ (setq targets1
+ (org-edna--add-targets targets1
+ (org-edna-finder/match
"checklist")))
+ (org-edna--handle-action 'org-edna-action/todo!
+ targets1
+ (point-marker)
+ '(DONE)))
+ ;; No new set of variables here either
+ (progn
+ (setq targets1
+ (org-edna--add-targets targets1
+ (org-edna-finder/siblings)))
+ (org-edna--handle-action 'org-edna-action/todo!
+ targets1
+ (point-marker)
+ '(TODO)))))
+ (output-form (org-edna--expand-sexp-form input-sexp)))
+ (should (equal output-form expected-form))))
+
+(ert-deftest org-edna-expand-sexp-form-if-else ()
+ (cl-letf* ((target-ctr 0)
+ (consideration-ctr 0)
+ (blocking-entry-ctr 0)
+ ((symbol-function 'cl-gentemp)
+ (lambda (&optional prefix)
+ (let ((ctr (pcase prefix
+ ("targets" (cl-incf target-ctr))
+ ("consideration" (cl-incf consideration-ctr))
+ ("blocking-entry" (cl-incf blocking-entry-ctr))
+ (_ 0))))
+ (intern (format "%s%s" prefix ctr)))))
+ (input-sexp '((if
+ ((match "checklist")
+ (done\?))
+ ((self)
+ (todo! TODO))
+ ((siblings)
+ (todo! DONE)))))
+ (expected-form '(let
+ ((targets1 nil)
+ (consideration1 nil)
+ (blocking-entry1 nil))
+ (if
+ ;; No inheritance in the conditional scope
+ (not
+ (let
+ ((targets2 nil)
+ (consideration2 nil)
+ (blocking-entry2 nil))
+ ;; Add targets for checklist match
+ (setq targets2
+ (org-edna--add-targets targets2
+
(org-edna-finder/match "checklist")))
+ ;; Handle condition
+ (setq blocking-entry2
+ (or blocking-entry2
+ (org-edna--handle-condition
'org-edna-condition/done\? 'nil 'nil targets2 consideration2)))))
+ ;; Use the top-level scope for then case
+ (progn
+ ;; Add targets for self finder
+ (setq targets1
+ (org-edna--add-targets targets1
+
(org-edna-finder/self)))
+ ;; Mark as TODO
+ (org-edna--handle-action
'org-edna-action/todo! targets1
+ (point-marker)
+ '(TODO)))
+ ;; Use the top-level scope for the else case
+ (progn
+ ;; Find siblings
+ (setq targets1
+ (org-edna--add-targets targets1
+
(org-edna-finder/siblings)))
+ ;; Mark as DONE
+ (org-edna--handle-action
'org-edna-action/todo! targets1
+ (point-marker)
+ '(DONE))))))
+
+ (output-form (org-edna--expand-sexp-form input-sexp)))
+ (should (equal output-form expected-form))))
+
+(ert-deftest org-edna-expand-sexp-form-if-no-else ()
+ (cl-letf* ((target-ctr 0)
+ (consideration-ctr 0)
+ (blocking-entry-ctr 0)
+ ((symbol-function 'cl-gentemp)
+ (lambda (&optional prefix)
+ (let ((ctr (pcase prefix
+ ("targets" (cl-incf target-ctr))
+ ("consideration" (cl-incf consideration-ctr))
+ ("blocking-entry" (cl-incf blocking-entry-ctr))
+ (_ 0))))
+ (intern (format "%s%s" prefix ctr)))))
+ (input-sexp '((if
+ ((match "checklist")
+ (done\?))
+ ((self)
+ (todo! TODO)))))
+ (expected-form '(let
+ ((targets1 nil)
+ (consideration1 nil)
+ (blocking-entry1 nil))
+ (if
+ ;; No inheritance in the conditional scope
+ (not
+ (let
+ ((targets2 nil)
+ (consideration2 nil)
+ (blocking-entry2 nil))
+ ;; Add targets for checklist match
+ (setq targets2
+ (org-edna--add-targets targets2
+
(org-edna-finder/match "checklist")))
+ ;; Handle condition
+ (setq blocking-entry2
+ (or blocking-entry2
+ (org-edna--handle-condition
'org-edna-condition/done\? 'nil 'nil targets2 consideration2)))))
+ ;; Use the top-level scope for then case
+ (progn
+ ;; Add targets for self finder
+ (setq targets1
+ (org-edna--add-targets targets1
+
(org-edna-finder/self)))
+ ;; Mark as TODO
+ (org-edna--handle-action
'org-edna-action/todo! targets1
+ (point-marker)
+ '(TODO)))
+ ;; End with a nil
+ nil)))
+ (output-form (org-edna--expand-sexp-form input-sexp)))
+ (should (equal output-form expected-form))))
+
;; Finders
@@ -189,11 +393,12 @@
(ert-deftest org-edna-finder/match-blocker ()
(let* ((org-agenda-files `(,org-edna-test-file))
(heading (org-id-find "caccd0a6-d400-410a-9018-b0635b07a37e" t))
- (blocker (org-entry-get heading "BLOCKER")))
+ (blocker (org-entry-get heading "BLOCKER"))
+ blocking-entry)
(should (string-equal "match(\"test&1\")" blocker))
(org-with-point-at heading
- (org-edna-process-form blocker 'condition))
- (should (string-equal (substring-no-properties org-block-entry-blocking)
+ (setq blocking-entry (org-edna-process-form blocker 'condition)))
+ (should (string-equal (substring-no-properties blocking-entry)
"TODO Tagged Heading 1 :1:test:"))))
(ert-deftest org-edna-finder/file ()
diff --git a/org-edna.el b/org-edna.el
index f14bcc7..5f60fb9 100644
--- a/org-edna.el
+++ b/org-edna.el
@@ -7,7 +7,7 @@
;; Keywords: convenience, text, org
;; URL: https://savannah.nongnu.org/projects/org-edna-el/
;; Package-Requires: ((emacs "25.1") (seq "2.19") (org "9.0.5"))
-;; Version: 1.0beta2
+;; Version: 1.0beta3
;; This file is part of GNU Emacs.
@@ -62,29 +62,40 @@ properties used during actions or conditions."
:group 'org-edna
:type 'boolean)
-(defmacro org-edna--syntax-error (msg form pos)
+;;; Form Parsing
+
+;; 3 types of "forms" here
+;;
+;; 1. String form; this is what you see in a BLOCKER or TRIGGER property
+;; 2. Edna sexp form; this is the intermediary form, and form used in
org-edna-form
+;; 3. Lisp form; a form that can be evaluated by Emacs
+
+(defmacro org-edna--syntax-error (msg form error-form)
"Signal an Edna syntax error.
MSG will be reported to the user and should describe the error.
FORM is the form that generated the error.
-POS is the position in FORM at which the error occurred."
- `(signal 'invalid-read-syntax (list :msg ,msg :form ,form :pos ,pos)))
+ERROR-FORM is the sub-form in FORM at which the error occurred."
+ `(signal 'invalid-read-syntax (list :msg ,msg :form ,form :error-form
,error-form)))
(defun org-edna--print-syntax-error (error-plist)
"Prints the syntax error from ERROR-PLIST."
- (let ((msg (plist-get error-plist :msg))
- (form (plist-get error-plist :form))
- (pos (plist-get error-plist :pos)))
+ (let* ((msg (plist-get error-plist :msg))
+ (form (plist-get error-plist :form))
+ (error-form (plist-get error-plist :error-form))
+ (pos (string-match-p (symbol-name (car error-form)) form)))
(message
"Org Edna Syntax Error: %s\n%s\n%s"
msg form (concat (make-string pos ?\ ) "^"))))
(defun org-edna--transform-arg (arg)
- "Transform ARG.
+ "Transform argument ARG.
Currently, the following are handled:
-- UUIDs (as determined by `org-uuidgen-p') are converted to strings"
+- UUIDs (as determined by `org-uuidgen-p') are converted to strings
+
+Everything else is returned as is."
(pcase arg
((and (pred symbolp)
(let (pred org-uuidgen-p) (symbol-name arg)))
@@ -92,44 +103,33 @@ Currently, the following are handled:
(_
arg)))
-(defun org-edna-parse-form (form &optional start)
- "Parse Edna form FORM starting at position START."
- (setq start (or start 0))
- (pcase-let* ((`(,token . ,pos) (read-from-string form start))
- (modifier nil)
- (args nil))
- (unless token
- (org-edna--syntax-error "Invalid Token" form start))
- ;; Check for either end of string or an opening parenthesis
- (unless (or (equal pos (length form))
- (equal (string-match-p "\\s-" form pos) pos)
- (equal (string-match-p "(" form pos) pos))
- (org-edna--syntax-error "Invalid character in form" form pos))
- ;; Parse arguments if we have any
- (when (equal (string-match-p "(" form pos) pos)
- (pcase-let* ((`(,new-args . ,new-pos) (read-from-string form pos)))
- (setq pos new-pos
- args (mapcar #'org-edna--transform-arg new-args))))
- ;; Check for a modifier
- (when (string-match "^\\([!]\\)\\(.*\\)" (symbol-name token))
- (setq modifier (intern (match-string 1 (symbol-name token))))
- (setq token (intern (match-string 2 (symbol-name token)))))
- ;; Move across any whitespace
- (when (string-match "\\s-+" form pos)
- (setq pos (match-end 0)))
- (list token args modifier pos)))
+(defun org-edna-break-modifier (token)
+ "Break TOKEN into a modifier and base token.
+
+A modifier is a single character.
+
+Return (MODIFIER . TOKEN), even if MODIFIER is nil."
+ (if token
+ (let (modifier)
+ (when (string-match "^\\([!]\\)\\(.*\\)" (symbol-name token))
+ (setq modifier (intern (match-string 1 (symbol-name token))))
+ (setq token (intern (match-string 2 (symbol-name token)))))
+ (cons modifier token))
+ ;; Still return something
+ '(nil . nil)))
(defun org-edna--function-for-key (key)
"Determine the Edna function for KEY.
KEY should be a symbol, the keyword for which to find the Edna
-function."
+function.
+
+If KEY is an invalid Edna keyword, then return nil."
(cond
- ;; Just return nil if it's not a symbol; `org-edna-process-form' will handle
- ;; the rest
+ ;; Just return nil if it's not a symbol
((or (not key)
(not (symbolp key))))
- ((eq key 'consideration)
+ ((memq key '(consideration consider))
;; Function is ignored here
(cons 'consideration 'identity))
((string-suffix-p "!" (symbol-name key))
@@ -148,8 +148,170 @@ function."
(when (fboundp func-sym)
(cons 'finder func-sym))))))
+(defun org-edna-parse-string-form (form &optional start)
+ "Parse Edna string form FORM starting at position START.
+
+Return (SEXP-FORM POS)
+
+SEXP-FORM is the sexp form of FORM starting at START.
+POS is the position in FORM where parsing ended."
+ (setq start (or start 0))
+ (pcase-let* ((`(,token . ,pos) (read-from-string form start))
+ (args nil))
+ (unless token
+ (org-edna--syntax-error "Invalid Token" form start))
+ ;; Check for either end of string or an opening parenthesis
+ (unless (or (equal pos (length form))
+ (equal (string-match-p "\\s-" form pos) pos)
+ (equal (string-match-p "(" form pos) pos))
+ (org-edna--syntax-error "Invalid character in form" form pos))
+ ;; Parse arguments if we have any
+ (when (equal (string-match-p "(" form pos) pos)
+ (pcase-let* ((`(,new-args . ,new-pos) (read-from-string form pos)))
+ (setq pos new-pos
+ args (mapcar #'org-edna--transform-arg new-args))))
+ ;; Move across any whitespace
+ (when (string-match "\\s-+" form pos)
+ (setq pos (match-end 0)))
+ (list (cons token args) pos)))
+
+(defun org-edna--convert-form (string &optional pos)
+ "Convert string form STRING into a flat sexp form.
+
+POS is the position in STRING from which to start conversion.
+
+Returns (FLAT-FORM END-POS) where
+
+FLAT-FORM is the flat sexp form
+END-POS is the position in STRING where parsing ended.
+
+Example:
+
+siblings todo!(TODO) => ((siblings) (todo! TODO))"
+ (let ((pos (or pos 0))
+ final-form)
+ (while (< pos (length string))
+ (pcase-let* ((`(,form ,new-pos) (org-edna-parse-string-form string pos)))
+ (setq final-form (append final-form (list form)))
+ (setq pos new-pos)))
+ (cons final-form pos)))
+
+(defun org-edna--normalize-sexp-form (form action-or-condition &optional
from-string)
+ "Normalize flat sexp form FORM into a full edna sexp form.
+
+ACTION-OR-CONDITION is either 'action or 'condition, indicating
+which of the two types is allowed in FORM.
+
+FROM-STRING is used internally, and is non-nil if FORM was
+originally a string.
+
+Returns (NORMALIZED-FORM REMAINING-FORM), where REMAINING-FORM is
+the remainder of FORM after the current scope was parsed."
+ (let* ((remaining-form (copy-sequence form))
+ (state 'finder)
+ final-form
+ need-break)
+ (while (and remaining-form (not need-break))
+ (let ((current-form (pop remaining-form)))
+ (pcase (car current-form)
+ ('if
+ ;; Check the car of each r*-form for the expected
+ ;; ending. If it doesn't match, throw an error.
+ (let (cond-form then-form else-form have-else)
+ (pcase-let* ((`(,temp-form ,r-form)
+ (org-edna--normalize-sexp-form
+ remaining-form
+ ;; Only allow conditions in cond forms
+ 'condition
+ from-string)))
+ ;; Use car-safe to catch r-form = nil
+ (unless (equal (car-safe r-form) '(then))
+ (org-edna--syntax-error
+ "Malformed if-construct; expected then terminator"
+ from-string current-form))
+ (setq cond-form temp-form
+ remaining-form (cdr r-form)))
+ (pcase-let* ((`(,temp-form ,r-form)
+ (org-edna--normalize-sexp-form remaining-form
+
action-or-condition
+ from-string)))
+ (unless (member (car-safe r-form) '((else) (endif)))
+ (org-edna--syntax-error
+ "Malformed if-construct; expected else or endif
terminator"
+ from-string current-form))
+ (setq have-else (equal (car r-form) '(else))
+ then-form temp-form
+ remaining-form (cdr r-form)))
+ (when have-else
+ (pcase-let* ((`(,temp-form ,r-form)
+ (org-edna--normalize-sexp-form remaining-form
+
action-or-condition
+ from-string)))
+ (unless (equal (car-safe r-form) '(endif))
+ (org-edna--syntax-error "Malformed if-construct;
expected endif terminator"
+ from-string current-form))
+ (setq else-form temp-form
+ remaining-form (cdr r-form))))
+ (push `(if ,cond-form ,then-form ,else-form) final-form)))
+ ((or 'then 'else 'endif)
+ (setq need-break t)
+ ;; Push the object back on remaining-form so the if knows where we
are
+ (setq remaining-form (cons current-form remaining-form)))
+ (_
+ ;; Determine the type of the form
+ ;; If we need to change state, return from this scope
+ (pcase-let* ((`(,type . ,func) (org-edna--function-for-key (car
current-form))))
+ (unless (and type func)
+ (org-edna--syntax-error "Unrecognized Form"
+ from-string current-form))
+ (pcase type
+ ('finder
+ (unless (memq state '(finder consideration))
+ ;; We changed back to finders, so we need to start a new
scope
+ (setq need-break t)))
+ ('action
+ (unless (eq action-or-condition 'action)
+ (org-edna--syntax-error "Actions aren't allowed in this
context"
+ from-string current-form)))
+ ('condition
+ (unless (eq action-or-condition 'condition)
+ (org-edna--syntax-error "Conditions aren't allowed in this
context"
+ from-string current-form))))
+ ;; Update state
+ (setq state type)
+ (if need-break ;; changing state
+ ;; Keep current-form on remaining-form so we have it for the
+ ;; next scope, since we didn't process it here.
+ (setq remaining-form (cons current-form remaining-form))
+ (push current-form final-form)))))))
+ (when (and (eq state 'finder)
+ (eq action-or-condition 'condition))
+ ;; Finders have to have something at the end, so we need to add that
+ ;; something. No default actions, so this must be a blocker.
+ (push '(!done?) final-form))
+ (list (nreverse final-form) remaining-form)))
+
+(defun org-edna-string-form-to-sexp-form (string-form action-or-condition)
+ "Parse string form STRING-FORM into an Edna sexp form.
+
+ACTION-OR-CONDITION is either 'action or 'condition, indicating
+which of the two types is allowed in STRING-FORM."
+ (car
+ (org-edna--normalize-sexp-form
+ (car (org-edna--convert-form string-form))
+ action-or-condition
+ string-form)))
+
(defun org-edna--handle-condition (func mod args targets consideration)
- "Handle a condition."
+ "Handle a condition.
+
+FUNC is the condition function.
+MOD is the modifier to pass to FUNC.
+ARGS are any arguments to pass to FUNC.
+TARGETS is a list of targets on which to operate.
+CONSIDERATION is the consideration symbol, if any."
+ (when (seq-empty-p targets)
+ (message "Warning: Condition specified without targets"))
;; Check the condition at each target
(when-let* ((blocks
(mapcar
@@ -160,69 +322,112 @@ function."
;; Apply consideration
(org-edna-handle-consideration consideration blocks)))
-(defun org-edna-process-form (form action-or-condition)
- "Process FORM.
-
-ACTION-OR-CONDITION is a symbol, either 'action or 'condition,
-indicating whether FORM accepts actions or conditions."
- (let ((targets)
- (blocking-entry)
- (consideration 'all)
- (state nil) ;; Type of operation
- ;; Keep track of the current heading
- (last-entry (point-marker))
- (pos 0))
- (while (< pos (length form))
- (pcase-let* ((`(,key ,args ,mod ,new-pos) (org-edna-parse-form form pos))
- (`(,type . ,func) (org-edna--function-for-key key)))
- (unless (and key type func)
- (org-edna--syntax-error "Unrecognized Form" form pos))
- (pcase type
- ('finder
- (unless (eq state 'finder)
- ;; We just executed some actions, so reset the entries.
- (setq targets nil))
- (setq state 'finder)
- (let ((markers (apply func args)))
- (setq targets (seq-uniq `(,@targets ,@markers)))))
- ('action
- (unless (eq action-or-condition 'action)
- (org-edna--syntax-error "Actions aren't allowed in this context"
form pos))
- (unless targets
- (message "Warning: Action specified without targets"))
- (setq state 'action)
- (dolist (target targets)
- (org-with-point-at target
- (apply func last-entry args))))
- ('condition
- (unless (eq action-or-condition 'condition)
- (org-edna--syntax-error "Conditions aren't allowed in this
context" form pos))
- (unless targets
- (message "Warning: Condition specified without targets"))
- (setq state 'condition)
- (setq blocking-entry
- (or blocking-entry ;; We're already blocking
- (org-edna--handle-condition func mod args targets
consideration))))
- ('consideration
- (unless (= (length args) 1)
- (org-edna--syntax-error "Consideration requires a single
argument" form pos))
- ;; Consideration must be at the start of the targets, so clear out
- ;; any old targets.
- (setq targets nil
- consideration (nth 0 args))))
- (setq pos new-pos)))
- ;; We exhausted the input string, but didn't find a condition when we were
- ;; expecting one.
- (when (and (eq action-or-condition 'condition) ;; Looking for conditions
- (eq state 'finder) ;; but haven't found any
- (not blocking-entry)) ;; ever
- (setq blocking-entry
- (org-edna--handle-condition 'org-edna-condition/done?
- t nil targets consideration)))
- ;; Only blockers care about the return value, and this will be non-nil if
- ;; the entry should be blocked.
- (setq org-block-entry-blocking blocking-entry)
- (not blocking-entry)))
+(defun org-edna--add-targets (old-targets new-targets)
+ "Add targets in NEW-TARGETS to OLD-TARGETS.
+
+Neither argument is modified."
+ (seq-uniq (append old-targets new-targets)))
+
+(defun org-edna--handle-action (action targets last-entry args)
+ "Process ACTION on TARGETS.
+
+LAST-ENTRY is the source entry.
+ARGS is a list of arguments to pass to ACTION."
+ (when (seq-empty-p targets)
+ (message "Warning: Action specified without targets"))
+ (dolist (target targets)
+ (org-with-point-at target
+ (apply action last-entry args))))
+
+(defun org-edna--expand-single-sexp-form (single-form
+ target-var
+ consideration-var
+ blocking-var)
+ "Expand sexp form SINGLE-FORM into a Lisp form.
+
+TARGET-VAR, BLOCKING-VAR, and CONSIDERATION-VAR are symbols that
+correspond to internal variables."
+ (pcase-let* ((`(,mkey . ,args) single-form)
+ (`(,mod . ,key) (org-edna-break-modifier mkey))
+ (`(,type . ,func) (org-edna--function-for-key key)))
+ (pcase type
+ ('finder
+ `(setq ,target-var (org-edna--add-targets ,target-var (,func ,@args))))
+ ('action
+ `(org-edna--handle-action ',func ,target-var (point-marker) ',args))
+ ('condition
+ `(setq ,blocking-var (or ,blocking-var
+ (org-edna--handle-condition ',func ',mod ',args
+ ,target-var
+
,consideration-var))))
+ ('consideration
+ `(setq ,consideration-var ,(nth 0 args))))))
+
+(defun org-edna--expand-sexp-form (form &optional
+ use-old-scope
+ old-target-var
+ old-consideration-var
+ old-blocking-var)
+ "Expand sexp form FORM into a Lisp form.
+
+USE-OLD-SCOPE, OLD-TARGET-VAR, OLD-CONSIDERATION-VAR, and
+OLD-BLOCKING-VAR are used internally."
+ (when form
+ ;; We inherit the original targets, consideration, and blocking-entry when
+ ;; we create a new scope in an if-construct.
+ (let* ((target-var (if use-old-scope old-target-var (cl-gentemp
"targets")))
+ (consideration-var (if use-old-scope
+ old-consideration-var
+ (cl-gentemp "consideration")))
+ (blocking-var (if use-old-scope
+ old-blocking-var
+ (cl-gentemp "blocking-entry")))
+ ;; These won't be used if use-old-scope is non-nil
+ (let-binds `((,target-var ,old-target-var)
+ (,consideration-var ,old-consideration-var)
+ (,blocking-var ,old-blocking-var)))
+ (wrapper-form (if use-old-scope
+ '(progn)
+ `(let (,@let-binds)))))
+ (pcase form
+ (`(if ,cond ,then . ,else)
+ ;; Don't pass the old variables into the condition form; it should be
+ ;; evaluated on its own to avoid clobbering the old targets.
+ `(if (not ,(org-edna--expand-sexp-form cond))
+ ,(org-edna--expand-sexp-form
+ then
+ '(progn)
+ old-target-var old-consideration-var old-blocking-var)
+ ,(when else
+ (org-edna--expand-sexp-form
+ ;; else is wrapped in a list, so take the first argument
+ (car else)
+ '(progn)
+ old-target-var old-consideration-var old-blocking-var))))
+ ((pred (lambda (arg) (symbolp (car arg))))
+ (org-edna--expand-single-sexp-form
+ form old-target-var old-consideration-var old-blocking-var))
+ (_
+ ;; List of forms
+ ;; Only use new variables if we're asked to
+ `(,@wrapper-form
+ ,@(mapcar
+ (lambda (f) (org-edna--expand-sexp-form
+ f '(progn) target-var consideration-var blocking-var))
+ form)))))))
+
+(defun org-edna-eval-sexp-form (sexp-form)
+ "Evaluate Edna sexp form SEXP-FORM."
+ (eval
+ (org-edna--expand-sexp-form sexp-form)))
+
+(defun org-edna-process-form (string-form action-or-condition)
+ "Process STRING-FORM.
+
+ACTION-OR-CONDITION is either 'action or 'condition, indicating
+which of the two types is allowed in STRING-FORM."
+ (org-edna-eval-sexp-form
+ (org-edna-string-form-to-sexp-form string-form action-or-condition)))
@@ -273,7 +478,8 @@ See `org-edna-run' for CHANGE-PLIST explanation.
This shouldn't be run from outside of `org-blocker-hook'."
(org-edna-run change-plist
(if-let* ((form (org-entry-get pos "BLOCKER" org-edna-use-inheritance)))
- (org-edna-process-form form 'condition)
+ ;; Return nil if there is no blocking entry
+ (not (setq org-block-entry-blocking (org-edna-process-form form
'condition)))
t)))
;;;###autoload
@@ -324,7 +530,7 @@ SCOPE defaults to agenda, and SKIP defaults to nil.
;; ID finder
(defun org-edna-finder/ids (&rest ids)
- "Find a list of headings with given IDs.
+ "Find a list of headings with given IDS.
Edna Syntax: ids(ID1 ID2 ...)
@@ -357,7 +563,11 @@ Edna Syntax: self"
(point-marker)))
(defun org-edna-goto-sibling (&optional previous wrap)
- "Move to the next sibling on the same level as the current heading."
+ "Move to the next sibling on the same level as the current heading.
+
+If PREVIOUS is non-nil, go to the previous sibling.
+f WRAP is non-nil, wrap around when the beginning (or end) is
+reached."
(let ((next (save-excursion
(if previous (org-get-last-sibling) (org-get-next-sibling)))))
(cond
@@ -384,12 +594,12 @@ Edna Syntax: self"
START is a point or marker from which to start collection.
-BACKWARDS means go backward through the level instead of forward.
+BACKWARD means go backward through the level instead of forward.
If WRAP is non-nil, wrap around when the end of the current level
is reached.
-If INCLUDE-START is non-nil, include the current point."
+If INCLUDE-POINT is non-nil, include the current point."
(org-with-wide-buffer
(let ((markers))
(goto-char start)
@@ -402,6 +612,12 @@ If INCLUDE-START is non-nil, include the current point."
(nreverse markers))))
(defun org-edna-collect-ancestors (&optional with-self)
+ "Collect the ancestors of the current subtree.
+
+If WITH-SELF is non-nil, include the current subtree in the list
+of ancestors.
+
+Return a list of markers for the ancestors."
(let ((markers))
(when with-self
(push (point-marker) markers))
@@ -411,6 +627,12 @@ If INCLUDE-START is non-nil, include the current point."
(nreverse markers)))
(defun org-edna-collect-descendants (&optional with-self)
+ "Collect the descendants of the current subtree.
+
+If WITH-SELF is non-nil, include the current subtree in the list
+of descendants.
+
+Return a list of markers for the descendants."
(let ((targets
(org-with-wide-buffer
(org-map-entries
@@ -422,7 +644,7 @@ If INCLUDE-START is non-nil, include the current point."
targets))
(defun org-edna-entry-has-tags-p (&rest tags)
- "Returns non-nil if the current entry has any tags in TAGS."
+ "Return non-nil if the current entry has any tags in TAGS."
(when-let* ((entry-tags (org-get-tags-at)))
(seq-intersection tags entry-tags)))
@@ -491,8 +713,7 @@ All arguments are symbols, unless noted otherwise.
- scheduled-up: Scheduled time, farthest first
- scheduled-down: Scheduled time, closest first
- deadline-up: Deadline time, farthest first
-- deadline-down: Deadline time, closest first
-"
+- deadline-down: Deadline time, closest first"
(let (targets
sortfun
reverse-sort
@@ -1021,8 +1242,8 @@ required."
(when (= delta 0) (setq delta -7)))
(when (> n 1) (setq delta (+ delta (* (1- n) (if (= dir ?-) -7
7)))))
(list delta "d" rel))))
- (if (or (not have-landing)
- (member what '("M" "h"))) ;; Don't change landing for minutes
or hours
+ (if (or (not have-landing)
+ (member what '("M" "h"))) ;; Don't change landing for minutes
or hours
ret ;; Don't worry about landing, just return
(pcase-let* ((`(,del ,what _) ret)
(mod-index (cdr (assoc what type-strings)))
@@ -1076,7 +1297,10 @@ MONTH may be a month string or an integer. Use 0 for the
following or previous month.
DAY is an optional integer. If not given, it will be 1 (for
-forward) or the last day of MONTH (backward)."
+forward) or the last day of MONTH (backward).
+
+Time is computed relative to either THIS-TIME (+/-) or
+DEFAULT (++/--)."
(require 'parse-time)
(let* ((case-fold-search t)
(weekdays (mapcar 'car parse-time-weekdays))
@@ -1163,7 +1387,10 @@ forward) or the last day of MONTH (backward)."
(list (- abs-days-then abs-days-now) "d" rel)))))
(defun org-edna--handle-planning (type last-entry args)
- "Handle planning of type TYPE."
+ "Handle planning of type TYPE.
+
+LAST-ENTRY is a marker to the source entry.
+ARGS is a list of arguments; currently, only the first is used."
(let* ((arg (nth 0 args))
(last-ts (org-with-point-at last-entry (org-edna--get-planning-info
type)))
(this-ts (org-edna--get-planning-info type))
@@ -1508,19 +1735,37 @@ starting from target's position."
(defun org-edna-handle-consideration (consideration blocks)
"Handle consideration CONSIDERATION.
-Edna Syntax: consideration(all) [1]
-Edna Syntax: consideration(N) [2]
-Edna Syntax: consideration(P) [3]
+Edna Syntax: consider(all) [1]
+Edna Syntax: consider(N) [2]
+Edna Syntax: consider(P) [3]
+Edna Syntax: consider(any) [4]
Form 1: consider all targets when evaluating conditions.
Form 2: consider the condition met if only N of the targets pass.
-Form 3: consider the condition met if only P% of the targets pass."
- (let ((first-block (seq-find #'identity blocks))
- (total-blocks (seq-length blocks)))
+Form 3: consider the condition met if only P% of the targets pass.
+Form 4: consider the condition met if any target meets it
+
+If CONSIDERATION is nil, default to 'all.
+
+The \"consideration\" keyword is also provided. It functions the
+same as \"consider\"."
+ ;; BLOCKS is a list of blocking entries; if one isn't blocked, its entry will
+ ;; be nil.
+ (let ((consideration (or consideration 'all))
+ (first-block (seq-find #'identity blocks))
+ (total-blocks (seq-length blocks))
+ (fulfilled (seq-count #'not blocks)))
(pcase consideration
('all
;; All of them must be fulfilled, so find the first one that isn't.
first-block)
+ ('any
+ ;; Any of them can be fulfilled, so find the first one that is
+ (if (> fulfilled 0)
+ ;; Have one fulfilled
+ nil
+ ;; None of them are fulfilled
+ first-block))
((pred integerp)
;; A fixed number of them must be fulfilled, so check how many aren't.
(let* ((fulfilled (seq-count #'not blocks)))
@@ -1548,6 +1793,7 @@ Form 3: consider the condition met if only P% of the
targets pass."
:group 'org-edna)
(defun org-edna-in-edit-buffer-p ()
+ "Return non-nil if inside the Edna edit buffer."
(string-equal (buffer-name) org-edna-edit-buffer-name))
(defun org-edna-replace-newlines (string)
@@ -1560,6 +1806,7 @@ Form 3: consider the condition met if only P% of the
targets pass."
(marker-position second-marker)))
(defun org-edna-edit-blocker-section-text ()
+ "Collect the BLOCKER section text from an edit buffer."
(when (org-edna-in-edit-buffer-p)
(let ((original-text (org-edna-edit-text-between-markers
org-edna-blocker-section-marker
@@ -1569,6 +1816,7 @@ Form 3: consider the condition met if only P% of the
targets pass."
(org-edna-replace-newlines (match-string 1 original-text))))))
(defun org-edna-edit-trigger-section-text ()
+ "Collect the TRIGGER section text from an edit buffer."
(when (org-edna-in-edit-buffer-p)
(let ((original-text (org-edna-edit-text-between-markers
org-edna-trigger-section-marker
@@ -1625,6 +1873,7 @@ the source buffer. Finish with `C-c C-c' or abort with
`C-c C-k'\n\n")
(add-hook 'completion-at-point-functions 'org-edna-completion-at-point nil
t)))
(defun org-edna-edit-finish ()
+ "Finish an Edna property edit."
(interactive)
(let ((blocker (org-edna-edit-blocker-section-text))
(trigger (org-edna-edit-trigger-section-text))
@@ -1641,6 +1890,7 @@ the source buffer. Finish with `C-c C-c' or abort with
`C-c C-k'\n\n")
(kill-buffer org-edna-edit-buffer-name)))
(defun org-edna-edit-abort ()
+ "Abort an Edna property edit."
(interactive)
(let ((pos-marker org-edna-edit-original-marker)
(wc org-window-configuration)
@@ -1675,6 +1925,9 @@ the source buffer. Finish with `C-c C-c' or abort with
`C-c C-k'\n\n")
(point-max-marker)))
(defun org-edna--collect-keywords (keyword-type &optional suffix)
+ "Collect known Edna keywords of type KEYWORD-TYPE.
+
+SUFFIX is an additional suffix to use when matching keywords."
(let* ((suffix (or suffix ""))
(edna-sym-list)
(edna-rx (rx-to-string `(and
@@ -1693,12 +1946,15 @@ the source buffer. Finish with `C-c C-c' or abort with
`C-c C-k'\n\n")
edna-sym-list))
(defun org-edna--collect-finders ()
+ "Return a list of finder keywords."
(org-edna--collect-keywords "finder"))
(defun org-edna--collect-actions ()
+ "Return a list of action keywords."
(org-edna--collect-keywords "action" "!"))
(defun org-edna--collect-conditions ()
+ "Return a list of condition keywords."
(org-edna--collect-keywords "condition" "?"))
(defun org-edna-completions-for-blocker ()
@@ -1713,6 +1969,10 @@ the source buffer. Finish with `C-c C-c' or abort with
`C-c C-k'\n\n")
,@(org-edna--collect-actions)))
(defun org-edna-completion-table-function (string pred action)
+ "Completion table function for Edna keywords.
+
+See `minibuffer-completion-table' for description of STRING,
+PRED, and ACTION."
(let ((completions (cond
;; Don't offer completion inside of arguments
((> (syntax-ppss-depth (syntax-ppss)) 0) nil)
@@ -1735,6 +1995,7 @@ the source buffer. Finish with `C-c C-c' or abort with
`C-c C-k'\n\n")
(cycle-sort-function . identity)))))))
(defun org-edna-completion-at-point ()
+ "Complete the Edna keyword at point."
(when-let* ((bounds (bounds-of-thing-at-point 'symbol)))
(list (car bounds) (cdr bounds) 'org-edna-completion-table-function)))
@@ -1743,6 +2004,9 @@ the source buffer. Finish with `C-c C-c' or abort with
`C-c C-k'\n\n")
(declare-function lm-report-bug "lisp-mnt" (topic))
(defun org-edna-submit-bug-report (topic)
+ "Submit a bug report to the Edna developers.
+
+TOPIC is the topic for the bug report."
(interactive "sTopic: ")
(require 'lisp-mnt)
(let* ((src-file (locate-library "org-edna.el" t))
diff --git a/org-edna.info b/org-edna.info
index eddda64..1da4b0c 100644
--- a/org-edna.info
+++ b/org-edna.info
@@ -78,6 +78,7 @@ Advanced Features
* Conditions:: More than just DONE headings
* Consideration:: Only some of them
+* Conditional Forms:: If/Then/Else
* Setting the Properties:: The easy way to set BLOCKER and TRIGGER
Conditions
@@ -106,8 +107,17 @@ Contributing
Changelog
+* 1.0beta3: 10beta3.
* 1.0beta2: 10beta2.
+1.0beta3
+
+* Conditional Forms: Conditional Forms (1).
+* Overhauled Internal Parsing::
+* Fixed consideration keywords::
+* Added 'any consideration::
+
+
1.0beta2
* Added interactive keyword editor with completion::
@@ -1005,6 +1015,7 @@ Advanced Features
* Conditions:: More than just DONE headings
* Consideration:: Only some of them
+* Conditional Forms:: If/Then/Else
* Setting the Properties:: The easy way to set BLOCKER and TRIGGER
@@ -1122,32 +1133,39 @@ Any condition can be negated by using ’!’ before the
condition.
heading tagged “test” does *not* have the property PROP set to “1”.
-File: org-edna.info, Node: Consideration, Next: Setting the Properties,
Prev: Conditions, Up: Advanced Features
+File: org-edna.info, Node: Consideration, Next: Conditional Forms, Prev:
Conditions, Up: Advanced Features
Consideration
=============
“Consideration” is a special keyword that’s only valid for blockers.
+ This says “Allow a task to complete if CONSIDERATION of its targets
+pass the given condition”.
+
This keyword can allow specifying only a portion of tasks to
consider:
1. consider(PERCENT)
2. consider(NUMBER)
3. consider(all) (Default)
+ 4. consider(any)
(1) tells the blocker to only consider some portion of the targets.
If at least PERCENT of them are in a DONE state, allow the task to be
-set to DONE. PERCENT must be a decimal.
+set to DONE. PERCENT must be a decimal, and doesn’t need to include a
+%-sign.
(2) tells the blocker to only consider NUMBER of the targets.
(3) tells the blocker to consider all following targets.
- A consideration must be specified before the targets to which it
+ (4) tells the blocker to allow passage if any of the targets pass.
+
+ A consideration must be specified before the conditions to which it
applies:
- consider(0.5) siblings consider(all) match("find_me")
+ consider(0.5) siblings match("find_me") consider(all) !done?
The above code will allow task completion if at least half the
siblings are complete, and all tasks tagged “find_me” are complete.
@@ -1160,8 +1178,81 @@ are complete.
If no consideration is given, ALL is assumed.
+ Both “consider” and “consideration” are valid keywords; they both
+mean the same thing.
+
+
+File: org-edna.info, Node: Conditional Forms, Next: Setting the Properties,
Prev: Consideration, Up: Advanced Features
+
+Conditional Forms
+=================
+
+Let’s say you’ve got the following checklist:
+
+ * TODO Nightly
+ DEADLINE: <2017-12-22 Fri 22:00 +1d>
+ :PROPERTIES:
+ :ID: 12345
+ :BLOCKER: match("nightly")
+ :TRIGGER: match("nightly") todo!(TODO)
+ :END:
+ * TODO Prepare Tomorrow's Lunch
:nightly:
+ * TODO Lock Back Door
:nightly:
+ * TODO Feed Dog
:nightly:
+
+ You don’t know in what order you want to perform each task, nor
+should it matter. However, you also want the parent heading, “Nightly”,
+to be marked as DONE when you’re finished with the last task.
+
+ There are two solutions to this: 1. Have each task attempt to mark
+“Nightly” as DONE, which will spam blocking messages after each task.
+
+ The second is to use conditional forms. Conditional forms are
+simple; it’s just if/then/else/endif:
+
+ if CONDITION then THEN else ELSE endif
+
+ Here’s how that reads:
+
+ “If CONDITION would not block, execute THEN. Otherwise, execute
+ELSE.”
+
+ For our nightly entries, this looks as follows:
+
+ * TODO Prepare Tomorrow's Lunch
:nightly:
+ :PROPERTIES:
+ :TRIGGER: if match("nightly") then ids(12345) todo!(DONE) endif
+ :END:
+
+ Thus, we replicate our original blocking condition on all of them, so
+it won’t trigger the original until the last one is marked DONE.
+
+ Occasionally, you may find that you’d rather execute a form if the
+condition *would* block. There are two options.
+
+ The first is confusing: use ‘consider(any)’. This will tell Edna to
+pass so long as one of the targets meets the condition. This is the
+opposite of Edna’s standard operation, which only allows passage if all
+targets meet the condition.
+
+ * TODO Prepare Tomorrow's Lunch
:nightly:
+ :PROPERTIES:
+ :TRIGGER: if consider(any) match("nightly") then ids(12345)
todo!(DONE) endif
+ :END:
+
+ The second is a lot easier to understand: just switch the then and
+else clauses:
+
+ * TODO Prepare Tomorrow's Lunch
:nightly:
+ :PROPERTIES:
+ :TRIGGER: if match("nightly") then else ids(12345) todo!(DONE) endif
+ :END:
+
+ The conditional block tells it to evaluate that section. Thus, you
+can conditionally add targets, or conditionally check conditions.
+
-File: org-edna.info, Node: Setting the Properties, Prev: Consideration, Up:
Advanced Features
+File: org-edna.info, Node: Setting the Properties, Prev: Conditional Forms,
Up: Advanced Features
Setting the Properties
======================
@@ -1377,10 +1468,56 @@ Changelog
* Menu:
+* 1.0beta3: 10beta3.
* 1.0beta2: 10beta2.
-File: org-edna.info, Node: 10beta2, Up: Changelog
+File: org-edna.info, Node: 10beta3, Next: 10beta2, Up: Changelog
+
+1.0beta3
+========
+
+HUGE addition here
+
+* Menu:
+
+* Conditional Forms: Conditional Forms (1).
+* Overhauled Internal Parsing::
+* Fixed consideration keywords::
+* Added 'any consideration::
+
+
+File: org-edna.info, Node: Conditional Forms (1), Next: Overhauled Internal
Parsing, Up: 10beta3
+
+Conditional Forms
+-----------------
+
+ • See *note Conditional Forms:: for more information
+
+
+File: org-edna.info, Node: Overhauled Internal Parsing, Next: Fixed
consideration keywords, Prev: Conditional Forms (1), Up: 10beta3
+
+Overhauled Internal Parsing
+---------------------------
+
+
+File: org-edna.info, Node: Fixed consideration keywords, Next: Added 'any
consideration, Prev: Overhauled Internal Parsing, Up: 10beta3
+
+Fixed consideration keywords
+----------------------------
+
+ • Both consider and consideration are accepted now
+
+
+File: org-edna.info, Node: Added 'any consideration, Prev: Fixed
consideration keywords, Up: 10beta3
+
+Added ’any consideration
+------------------------
+
+ • Allows passage if just one target is fulfilled
+
+
+File: org-edna.info, Node: 10beta2, Prev: 10beta3, Up: Changelog
1.0beta2
========
@@ -1400,7 +1537,7 @@ File: org-edna.info, Node: Added interactive keyword
editor with completion, N
Added interactive keyword editor with completion
------------------------------------------------
-See *note Setting the Properties:: for how to do that
+ • See *note Setting the Properties:: for how to do that
File: org-edna.info, Node: New uses of schedule! and deadline!, Next: New
``relatives'' finder, Prev: Added interactive keyword editor with completion,
Up: 10beta2
@@ -1437,71 +1574,77 @@ New finders
Tag Table:
Node: Top225
-Node: Copying3713
-Node: Introduction4530
-Node: Installation and Setup5478
-Node: Basic Operation6271
-Node: Blockers8122
-Node: Triggers8408
-Node: Syntax8670
-Node: Basic Features9360
-Node: Finders9663
-Node: ancestors11428
-Node: children12022
-Node: descendants12432
-Node: file12954
-Node: first-child13703
-Node: ids13963
-Node: match14624
-Node: next-sibling15262
-Node: next-sibling-wrap15519
-Node: olp15833
-Node: org-file16245
-Node: parent16890
-Node: previous-sibling17088
-Node: previous-sibling-wrap17349
-Node: relatives17628
-Node: rest-of-siblings21249
-Node: rest-of-siblings-wrap21534
-Node: self21883
-Node: siblings22044
-Node: siblings-wrap22281
-Node: Actions22585
-Node: Scheduled/Deadline23327
-Node: TODO State26902
-Node: Archive27270
-Node: Chain Property27590
-Node: Clocking27873
-Node: Property28285
-Node: Priority28607
-Node: Tag29176
-Node: Effort29393
-Node: Advanced Features29782
-Node: Conditions30120
-Node: done30735
-Node: headings30899
-Node: todo-state31275
-Node: variable-set31531
-Node: has-property31960
-Node: re-search32229
-Node: Negating Conditions32589
-Node: Consideration32976
-Node: Setting the Properties34208
-Node: Extending Edna35288
-Node: Naming Conventions35778
-Node: Finders (1)36241
-Node: Actions (1)36607
-Node: Conditions (1)37072
-Node: Contributing37962
-Node: Bugs38513
-Node: Development38865
-Node: Documentation40018
-Node: Changelog40463
-Node: 10beta240587
-Node: Added interactive keyword editor with completion40853
-Node: New uses of schedule! and deadline!41145
-Node: New ``relatives'' finder41640
-Node: New finders42036
+Node: Copying3930
+Node: Introduction4747
+Node: Installation and Setup5695
+Node: Basic Operation6488
+Node: Blockers8339
+Node: Triggers8625
+Node: Syntax8887
+Node: Basic Features9577
+Node: Finders9880
+Node: ancestors11645
+Node: children12239
+Node: descendants12649
+Node: file13171
+Node: first-child13920
+Node: ids14180
+Node: match14841
+Node: next-sibling15479
+Node: next-sibling-wrap15736
+Node: olp16050
+Node: org-file16462
+Node: parent17107
+Node: previous-sibling17305
+Node: previous-sibling-wrap17566
+Node: relatives17845
+Node: rest-of-siblings21466
+Node: rest-of-siblings-wrap21751
+Node: self22100
+Node: siblings22261
+Node: siblings-wrap22498
+Node: Actions22802
+Node: Scheduled/Deadline23544
+Node: TODO State27119
+Node: Archive27487
+Node: Chain Property27807
+Node: Clocking28090
+Node: Property28502
+Node: Priority28824
+Node: Tag29393
+Node: Effort29610
+Node: Advanced Features29999
+Node: Conditions30383
+Node: done30998
+Node: headings31162
+Node: todo-state31538
+Node: variable-set31794
+Node: has-property32223
+Node: re-search32492
+Node: Negating Conditions32852
+Node: Consideration33239
+Node: Conditional Forms34808
+Node: Setting the Properties37464
+Node: Extending Edna38548
+Node: Naming Conventions39038
+Node: Finders (1)39501
+Node: Actions (1)39867
+Node: Conditions (1)40332
+Node: Contributing41222
+Node: Bugs41773
+Node: Development42125
+Node: Documentation43278
+Node: Changelog43723
+Node: 10beta343868
+Node: Conditional Forms (1)44126
+Node: Overhauled Internal Parsing44325
+Node: Fixed consideration keywords44522
+Node: Added 'any consideration44781
+Node: 10beta244996
+Node: Added interactive keyword editor with completion45278
+Node: New uses of schedule! and deadline!45577
+Node: New ``relatives'' finder46072
+Node: New finders46468
End Tag Table
diff --git a/org-edna.org b/org-edna.org
index 7009570..c23f80a 100644
--- a/org-edna.org
+++ b/org-edna.org
@@ -928,24 +928,30 @@ tagged "test" does *not* have the property PROP set to
"1".
"Consideration" is a special keyword that's only valid for blockers.
+This says "Allow a task to complete if CONSIDERATION of its targets pass the
+given condition".
+
This keyword can allow specifying only a portion of tasks to consider:
1. consider(PERCENT)
2. consider(NUMBER)
3. consider(all) (Default)
+4. consider(any)
(1) tells the blocker to only consider some portion of the targets. If at
least
PERCENT of them are in a DONE state, allow the task to be set to DONE. PERCENT
-must be a decimal.
+must be a decimal, and doesn't need to include a %-sign.
(2) tells the blocker to only consider NUMBER of the targets.
(3) tells the blocker to consider all following targets.
-A consideration must be specified before the targets to which it applies:
+(4) tells the blocker to allow passage if any of the targets pass.
+
+A consideration must be specified before the conditions to which it applies:
#+BEGIN_EXAMPLE
-consider(0.5) siblings consider(all) match("find_me")
+consider(0.5) siblings match("find_me") consider(all) !done?
#+END_EXAMPLE
The above code will allow task completion if at least half the siblings are
@@ -960,6 +966,86 @@ are complete, and at least two of ID3, ID4, ID5, and ID6
are complete.
If no consideration is given, ALL is assumed.
+Both "consider" and "consideration" are valid keywords; they both mean the same
+thing.
+
+** Conditional Forms
+:PROPERTIES:
+:CUSTOM_ID: conditional_forms
+:DESCRIPTION: If/Then/Else
+:END:
+
+Let's say you've got the following checklist:
+
+#+begin_src org
+,* TODO Nightly
+ DEADLINE: <2017-12-22 Fri 22:00 +1d>
+ :PROPERTIES:
+ :ID: 12345
+ :BLOCKER: match("nightly")
+ :TRIGGER: match("nightly") todo!(TODO)
+ :END:
+,* TODO Prepare Tomorrow's Lunch :nightly:
+,* TODO Lock Back Door :nightly:
+,* TODO Feed Dog :nightly:
+#+end_src
+
+You don't know in what order you want to perform each task, nor should it
+matter. However, you also want the parent heading, "Nightly", to be marked as
+DONE when you're finished with the last task.
+
+There are two solutions to this: 1. Have each task attempt to mark "Nightly" as
+DONE, which will spam blocking messages after each task.
+
+The second is to use conditional forms. Conditional forms are simple; it's
just
+if/then/else/endif:
+
+#+begin_quote
+if CONDITION then THEN else ELSE endif
+#+end_quote
+
+Here's how that reads:
+
+"If CONDITION would not block, execute THEN. Otherwise, execute ELSE."
+
+For our nightly entries, this looks as follows:
+
+#+begin_src org
+,* TODO Prepare Tomorrow's Lunch :nightly:
+ :PROPERTIES:
+ :TRIGGER: if match("nightly") then ids(12345) todo!(DONE) endif
+ :END:
+#+end_src
+
+Thus, we replicate our original blocking condition on all of them, so it won't
+trigger the original until the last one is marked DONE.
+
+Occasionally, you may find that you'd rather execute a form if the condition
+*would* block. There are two options.
+
+The first is confusing: use ~consider(any)~. This will tell Edna to pass so
+long as one of the targets meets the condition. This is the opposite of Edna's
+standard operation, which only allows passage if all targets meet the
condition.
+
+#+begin_src org
+,* TODO Prepare Tomorrow's Lunch :nightly:
+ :PROPERTIES:
+ :TRIGGER: if consider(any) match("nightly") then ids(12345) todo!(DONE)
endif
+ :END:
+#+end_src
+
+The second is a lot easier to understand: just switch the then and else
clauses:
+
+#+begin_src org
+,* TODO Prepare Tomorrow's Lunch :nightly:
+ :PROPERTIES:
+ :TRIGGER: if match("nightly") then else ids(12345) todo!(DONE) endif
+ :END:
+#+end_src
+
+The conditional block tells it to evaluate that section. Thus, you can
+conditionally add targets, or conditionally check conditions.
+
** Setting the Properties
:PROPERTIES:
:DESCRIPTION: The easy way to set BLOCKER and TRIGGER
@@ -991,7 +1077,6 @@ of any valid keyword within the BLOCKER or TRIGGER
sections using
When finished, type ~C-c C-c~ to apply the changes, or ~C-c C-k~ to throw out
your
changes.
-
* Extending Edna
:PROPERTIES:
:DESCRIPTION: What else can it do?
@@ -1149,16 +1234,24 @@ making any changes:
1. Update the info page in the repository with ~C-c C-e i i~
2. If you're updating the HTML documentation, switch to a theme that can easily
be read on a white background; we recommend the "adwaita" theme
-
* Changelog
:PROPERTIES:
:DESCRIPTION: List of changes by version
:END:
+** 1.0beta3
+HUGE addition here
+*** Conditional Forms
+- See [[#conditional_forms][Conditional Forms]] for more information
+*** Overhauled Internal Parsing
+*** Fixed consideration keywords
+- Both consider and consideration are accepted now
+*** Added 'any consideration
+- Allows passage if just one target is fulfilled
** 1.0beta2
Big release here, with three new features.
*** Added interactive keyword editor with completion
-See [[#setting_keywords][Setting the Properties]] for how to do that
+- See [[#setting_keywords][Setting the Properties]] for how to do that
*** New uses of schedule! and deadline!
- New "float" form that mimics diary-float
- [elpa] master updated (9a85042 -> 9b8c043), Ian Dunn, 2017/12/23
- [elpa] master 7847539 1/8: Added section to documentation for changes, Ian Dunn, 2017/12/23
- [elpa] master 50a903e 2/8: Added section in the documentation about updating the documentation, Ian Dunn, 2017/12/23
- [elpa] master ebd814a 3/8: Fixed "Setting the Properties" custom_id and title, Ian Dunn, 2017/12/23
- [elpa] master 058b53b 4/8: Added links in "Changelog", Ian Dunn, 2017/12/23
- [elpa] master e897bee 6/8: Don't export special strings for documentation, Ian Dunn, 2017/12/23
- [elpa] master aa718f5 5/8: Updated info pages, Ian Dunn, 2017/12/23
- [elpa] master 556e03b 7/8: Overhauled parsing method, and added conditional blocks,
Ian Dunn <=
- [elpa] master 9b8c043 8/8: Merge commit '556e03be1068d746e3d672185987c433302229fa', Ian Dunn, 2017/12/23