[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH] ox-icalendar: Unscheduled tasks & repeating tasks
From: |
Jack Kamm |
Subject: |
[PATCH] ox-icalendar: Unscheduled tasks & repeating tasks |
Date: |
Sun, 11 Jun 2023 08:35:50 -0700 |
Hello,
I am attaching an updated patch for ox-icalendar unscheduled and
repeating TODOs, incorporating some of Ihor's feedback to my RFC some
months ago.
Compared to my original RFC, here are the main changes:
- For unscheduled TODOs with repeating deadline, the deadline warning
days is used as the start time by default, in order to comply with
the iCalendar spec which demands a start time in this case.
- Previously I had separate patches for unscheduled and repeating
TODOs, but now I combine them into a single patch because of the way
repeats and start times are intertwined for repeating deadlines.
- New customization `org-icalendar-todo-unscheduled-start' controls
the exported start time for unscheduled TODOs. It replaces
`org-icalendar-todo-force-scheduling' from my previous version of
the patch.
- In case of a SCHEDULED repeater, and a DEADLINE with no repeater,
the task repeats until the deadline, using the RRULE UNTIL keyword.
- Added linting for the case where SCHEDULED and DEADLINE have
mismatching repeaters.
- Added several tests for ox-icalendar, and a test for the new lint as well.
There are still a few cases that are not yet handled, but they are
less common and will take some more work to implement, so I would
prefer to leave them to future patches:
- Case where SCHEDULED and DEADLINE have mismatched repeaters. We can
use RDATE with differing DURATION for this.
- Case where DEADLINE has repeater but SCHEDULED does not. We can use
RDATE for the first instance, and RRULE for the subsequent repeats.
- Case of catch-up "++" repeaters. We can use EXDATE to exclude
repeats before today.
- Case of restart ".+" repeaters. I don't think iCalendar can handle
this case, and we should ignore it.
>From 1135e3e7cb08353892c439b085d3bf0bf1072ecb Mon Sep 17 00:00:00 2001
From: Jack Kamm <jackkamm@gmail.com>
Date: Sun, 11 Jun 2023 07:50:20 -0700
Subject: [PATCH] ox-icalendar: Add support for unscheduled and repeating TODOs
* lisp/ox-icalendar.el (org-icalendar-todo-unscheduled-start): New
customization to control the exported start time of unscheduled tasks.
(org-icalendar--rrule): Helper function for RRULE export.
(org-icalendar--vevent): Use the new helper function for RRULE.
(org-icalendar--vtodo): Change how unscheduled TODOs are handled using
the new customization option. Export SCHEDULED and DEADLINE
repeaters. In case of SCHEDULED repeater and a DEADLINE without
repeater, treat DEADLINE as RRULE UNTIL. Emit a warning for tricky
edge cases that are not yet implemented.
* testing/lisp/test-ox-icalendar.el
(test-ox-icalendar/todo-repeater-shared): Test for exporting shared
SCHEDULED/DEADLINE repeater.
(test-ox-icalendar/todo-repeating-deadline-warndays): Test using
warning days as DTSTART of repeating deadline.
(test-ox-icalendar/todo-repeater-until): Test using DEADLINE as RRULE
UNTIL.
(test-ox-icalendar/todo-repeater-until-utc): Test RRULE UNTIL is in
UTC format when DTSTART is not in local time format.
* lisp/org-lint.el (org-lint-mismatched-planning-repeaters): Add lint
for mismatched SCHEDULED and DEADLINE repeaters.
* testing/lisp/test-org-lint.el
(test-org-lint/mismatched-planning-repeaters): Add test for linting of
mismatched SCHEDULED and DEADLINE repeaters.
---
etc/ORG-NEWS | 64 ++++++++++++
lisp/org-lint.el | 34 ++++++
lisp/ox-icalendar.el | 165 +++++++++++++++++++++++++-----
testing/lisp/test-org-lint.el | 7 ++
testing/lisp/test-ox-icalendar.el | 74 ++++++++++++++
5 files changed, 320 insertions(+), 24 deletions(-)
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 7e7015064..a24caddfe 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -50,6 +50,21 @@ ox-icalendar. In particular, older versions of org-caldav
may
encounter issues, and users are advised to update to the most recent
version of org-caldav. See
[[https://github.com/dengste/org-caldav/commit/618bf4cdc9be140ca1993901d017b7f18297f1b8][this
org-caldav commit]] for more information.
+*** Icalendar export of unscheduled TODOs no longer have start time of today
+
+For TODOs without a scheduled start time, ox-icalendar no longer
+forces them to have a scheduled start time of today when exporting.
+
+Instead, the new customization ~org-icalendar-todo-unscheduled-start~
+controls the exported start date for unscheduled tasks. Its default
+is ~recurring-deadline-warning~ which will export unscheduled tasks
+with no start date, unless it has a recurring deadline (in which case
+the iCalendar spec demands a start date, and
+~org-deadline-warning-days~ is used for that).
+
+To revert to the old behavior, set
+~org-icalendar-todo-unscheduled-start~ to ~current-datetime~.
+
** New and changed options
*** Commands affected by ~org-fold-catch-invisible-edits~ can now be customized
@@ -188,6 +203,28 @@ default settings of "Body only", "Visible only", and "Force
publishing" in the ~org-export-dispatch~ UI to be customized,
respectively.
+*** New option ~org-icalendar-todo-unscheduled-start~ to control unscheduled
TODOs in ox-icalendar
+
+~org-icalendar-todo-unscheduled-start~ controls how ox-icalendar
+exports the starting datetime for unscheduled TODOs. Note this option
+only has an effect when ~org-icalendar-include-todo~ is non-nil.
+
+By default, ox-icalendar will not export a start datetime for
+unscheduled TODOs, except in cases where the iCalendar spec demands a
+start (specifically, for recurring deadlines, in which case
+~org-deadline-warning-days~ is used).
+
+Currently implemented options are:
+
+- ~recurring-deadline-warning~: The default as described above.
+- ~deadline-warning~: Use ~org-deadline-warning-days~ to set the start
+ time if the unscheduled task has a deadline (recurring or not).
+- ~current-datetime~: Revert to old behavior, using the current
+ datetime as the start of unscheduled tasks.
+- ~nil~: Never add a start time for unscheduled tasks. For repeating
+ tasks this technically violates the iCalendar spec, but some
+ iCalendar programs support this usage.
+
** New features
*** ~org-insert-todo-heading-respect-content~ now accepts prefix arguments
@@ -230,6 +267,33 @@ editing with Emacs while a ~:session~ block executes.
When ~org-return-follows-link~ is non-nil and cursor is over an
org-cite citation, ~org-return~ will call ~org-open-at-point~.
+*** Add support for repeating tasks in iCalendar export
+
+Repeating Scheduled and Deadline timestamps in TODOs are now exported
+as recurring tasks in iCalendar export.
+
+In case the TODO has just a single planning timestamp (Scheduled or
+Deadline, but not both), its repeater is used as the iCalendar
+recurrence rule (RRULE).
+
+If the TODO has both Scheduled and Deadline planning timestamps, then
+the following cases are implemented:
+
+- If both have the same repeater, then it is used as the RRULE.
+- Scheduled has repeater but Deadline does not: the Scheduled repeater
+ is used as RRULE, and Deadline is used as UNTIL (the end date for
+ the repeater). This is similar to ~repeated-after-deadline~ in
+ ~org-agenda-skip-scheduled-if-deadline-is-shown~.
+
+The following 2 cases are not yet implemented, and the repeater is
+skipped (with a warning) if the ox-icalendar export encounters them:
+
+- Deadline has a repeater but Scheduled does not.
+- Scheduled and Deadline have different repeaters.
+
+Also note that only vanilla repeaters are currently exported; the
+special repeaters ~++~ and ~.+~ are skipped.
+
** Miscellaneous
*** =org-crypt.el= now applies initial visibility settings to decrypted entries
diff --git a/lisp/org-lint.el b/lisp/org-lint.el
index c2ed007ab..bec1340c5 100644
--- a/lisp/org-lint.el
+++ b/lisp/org-lint.el
@@ -70,6 +70,7 @@
;; - non-footnote definitions in footnote section,
;; - probable invalid keywords,
;; - invalid blocks,
+;; - mismatched repeaters in planning info line,
;; - misplaced planning info line,
;; - probable incomplete drawers,
;; - probable indented diary-sexps,
@@ -882,6 +883,34 @@ (defun org-lint-colon-in-name (ast)
"Name \"%s\" contains a colon; Babel cannot use it as input"
name)))))))
+(defun org-lint-mismatched-planning-repeaters (ast)
+ (org-element-map ast 'planning
+ (lambda (e)
+ (let* ((scheduled (org-element-property :scheduled e))
+ (deadline (org-element-property :deadline e))
+ (scheduled-repeater-type (org-element-property
+ :repeater-type scheduled))
+ (deadline-repeater-type (org-element-property
+ :repeater-type deadline))
+ (scheduled-repeater-value (org-element-property
+ :repeater-value scheduled))
+ (deadline-repeater-value (org-element-property
+ :repeater-value deadline)))
+ (when (and scheduled deadline
+ (memq scheduled-repeater-type '(cumulate catch-up))
+ (memq deadline-repeater-type '(cumulate catch-up))
+ (> scheduled-repeater-value 0)
+ (> deadline-repeater-value 0)
+ (not
+ (and
+ (eq scheduled-repeater-type deadline-repeater-type)
+ (eq (org-element-property :repeater-unit scheduled)
+ (org-element-property :repeater-unit deadline))
+ (eql scheduled-repeater-value deadline-repeater-value))))
+ (list
+ (org-element-property :begin e)
+ "Different repeaters in SCHEDULED and DEADLINE timestamps."))))))
+
(defun org-lint-misplaced-planning-info (_)
(let ((case-fold-search t)
reports)
@@ -1488,6 +1517,11 @@ (org-lint-add-checker 'invalid-block
#'org-lint-invalid-block
:trust 'low)
+(org-lint-add-checker 'mismatched-planning-repeaters
+ "Report mismatched repeaters in planning info line"
+ #'org-lint-mismatched-planning-repeaters
+ :trust 'low)
+
(org-lint-add-checker 'misplaced-planning-info
"Report misplaced planning info line"
#'org-lint-misplaced-planning-info
diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el
index 163b3b983..8c569752b 100644
--- a/lisp/ox-icalendar.el
+++ b/lisp/ox-icalendar.el
@@ -231,6 +231,38 @@ (defcustom org-icalendar-include-todo nil
(repeat :tag "Specific TODO keywords"
(string :tag "Keyword"))))
+(defcustom org-icalendar-todo-unscheduled-start 'recurring-deadline-warning
+ "Exported start date of unscheduled TODOs.
+
+If `org-icalendar-use-scheduled' contains `todo-start' and a task
+has a \"SCHEDULED\" timestamp, that is always used as the start
+date. Otherwise, this variable controls whether a start date is
+exported and what its value is.
+
+Note that the iCalendar spec RFC 5545 does not generally require
+tasks to have a start date, except for repeating tasks which do
+require a start date. However some iCalendar programs ignore the
+requirement for repeating tasks, and allow repeating deadlines
+without a matching start date.
+
+This variable has no effect when `org-icalendar-include-todo' is nil.
+
+Valid values are:
+`recurring-deadline-warning' If deadline repeater present,
+ use `org-deadline-warning-days' as start.
+`deadline-warning' If deadline present,
+ use `org-deadline-warning-days' as start.
+`current-datetime' Use the current date-time as start.
+nil Never add a start time for unscheduled tasks."
+ :group 'org-export-icalendar
+ :type '(choice
+ (const :tag "Warning days if deadline recurring"
recurring-deadline-warning)
+ (const :tag "Warning days if deadline present" deadline-warning)
+ (const :tag "Now" current-datetime)
+ (const :tag "No start date" nil))
+ :package-version '(Org . "9.7")
+ :safe #'symbolp)
+
(defcustom org-icalendar-include-bbdb-anniversaries nil
"Non-nil means a combined iCalendar file should include anniversaries.
The anniversaries are defined in the BBDB database."
@@ -731,6 +763,13 @@ (defun org-icalendar-entry (entry contents info)
;; Don't forget components from inner entries.
contents))))
+(defun org-icalendar--rrule (unit value)
+ (format "RRULE:FREQ=%s;INTERVAL=%d"
+ (cl-case unit
+ (hour "HOURLY") (day "DAILY") (week "WEEKLY")
+ (month "MONTHLY") (year "YEARLY"))
+ value))
+
(defun org-icalendar--vevent
(entry timestamp uid summary location description categories timezone
class)
"Create a VEVENT component.
@@ -756,12 +795,11 @@ (\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are
predefined, others
(org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone)
"\n"
(org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
;; RRULE.
- (when (org-element-property :repeater-type timestamp)
- (format "RRULE:FREQ=%s;INTERVAL=%d\n"
- (cl-case (org-element-property :repeater-unit timestamp)
- (hour "HOURLY") (day "DAILY") (week "WEEKLY")
- (month "MONTHLY") (year "YEARLY"))
- (org-element-property :repeater-value timestamp)))
+ (when (org-element-property :repeater-type timestamp)
+ (concat (org-icalendar--rrule
+ (org-element-property :repeater-unit timestamp)
+ (org-element-property :repeater-value timestamp))
+ "\n"))
"SUMMARY:" summary "\n"
(and (org-string-nw-p location) (format "LOCATION:%s\n" location))
(and (org-string-nw-p class) (format "CLASS:%s\n" class))
@@ -784,27 +822,106 @@ (defun org-icalendar--vtodo
TIMEZONE specifies a time zone for this TODO only.
Return VTODO component as a string."
- (let ((start (or (and (memq 'todo-start org-icalendar-use-scheduled)
- (org-element-property :scheduled entry))
- ;; If we can't use a scheduled time for some
- ;; reason, start task now.
- (let ((now (decode-time)))
- (list 'timestamp
- (list :type 'active
- :minute-start (nth 1 now)
- :hour-start (nth 2 now)
- :day-start (nth 3 now)
- :month-start (nth 4 now)
- :year-start (nth 5 now)))))))
+ (let* ((sc (and (memq 'todo-start org-icalendar-use-scheduled)
+ (org-element-property :scheduled entry)))
+ (dl (and (memq 'todo-due org-icalendar-use-deadline)
+ (org-element-property :deadline entry)))
+ ;; TODO Implement catch-up repeaters using EXDATE
+ (sc-repeat-p (and (eq (org-element-property :repeater-type sc)
+ 'cumulate)
+ (> (org-element-property :repeater-value sc) 0)))
+ (dl-repeat-p (and (eq (org-element-property :repeater-type dl)
+ 'cumulate)
+ (> (org-element-property :repeater-value dl) 0)))
+ (repeat-value (or (org-element-property :repeater-value sc)
+ (org-element-property :repeater-value dl)))
+ (repeat-unit (or (org-element-property :repeater-unit sc)
+ (org-element-property :repeater-unit dl)))
+ (repeat-until (and sc-repeat-p (not dl-repeat-p) dl))
+ (start
+ (cond
+ (sc)
+ ((eq org-icalendar-todo-unscheduled-start 'current-datetime)
+ (let ((now (decode-time)))
+ (list 'timestamp
+ (list :type 'active
+ :minute-start (nth 1 now)
+ :hour-start (nth 2 now)
+ :day-start (nth 3 now)
+ :month-start (nth 4 now)
+ :year-start (nth 5 now)))))
+ ((or (and (eq org-icalendar-todo-unscheduled-start
+ 'deadline-warning)
+ dl)
+ (and (eq org-icalendar-todo-unscheduled-start
+ 'recurring-deadline-warning)
+ dl-repeat-p))
+ (let ((dl-raw (org-element-property :raw-value dl)))
+ (with-temp-buffer
+ (insert dl-raw)
+ (goto-char (point-min))
+ (org-timestamp-down-day (org-get-wdays dl-raw))
+ (org-element-timestamp-parser)))))))
(concat "BEGIN:VTODO\n"
"UID:TODO-" uid "\n"
(org-icalendar-dtstamp) "\n"
- (org-icalendar-convert-timestamp start "DTSTART" nil timezone) "\n"
- (and (memq 'todo-due org-icalendar-use-deadline)
- (org-element-property :deadline entry)
- (concat (org-icalendar-convert-timestamp
- (org-element-property :deadline entry) "DUE" nil
timezone)
- "\n"))
+ (when start (concat (org-icalendar-convert-timestamp
+ start "DTSTART" nil timezone)
+ "\n"))
+ (when (and dl (not repeat-until))
+ (concat (org-icalendar-convert-timestamp
+ dl "DUE" nil timezone)
+ "\n"))
+ ;; RRULE
+ (cond
+ ;; SCHEDULED, DEADLINE have different repeaters
+ ((and dl-repeat-p
+ (not (and (eq repeat-value (org-element-property
+ :repeater-value dl))
+ (eq repeat-unit (org-element-property
+ :repeater-unit dl)))))
+ ;; TODO Implement via RDATE with changing DURATION
+ (warn "Not yet implemented: \
+different repeaters on SCHEDULED and DEADLINE. Skipping.")
+ nil)
+ ;; DEADLINE has repeater but SCHEDULED doesn't
+ ((and dl-repeat-p (and sc (not sc-repeat-p)))
+ ;; TODO SCHEDULED should only apply to first instance;
+ ;; use RDATE with custom DURATION to implement that
+ (warn "Not yet implemented: \
+repeater on DEADLINE but not SCHEDULED. Skipping.")
+ nil)
+ ((or sc-repeat-p dl-repeat-p)
+ (concat
+ (org-icalendar--rrule repeat-unit repeat-value)
+ ;; add UNTIL part to RRULE
+ (when repeat-until
+ (let* ((start-time
+ (org-element-property :minute-start start))
+ ;; RFC5545 requires UTC iff DTSTART is not local time
+ (local-time-p
+ (and (not timezone)
+ (equal org-icalendar-date-time-format
+ ":%Y%m%dT%H%M%S")))
+ (encoded
+ (org-encode-time
+ 0
+ (or (org-element-property :minute-start repeat-until)
+ 0)
+ (or (org-element-property :hour-start repeat-until)
+ 0)
+ (org-element-property :day-start repeat-until)
+ (org-element-property :month-start repeat-until)
+ (org-element-property :year-start repeat-until))))
+ (concat ";UNTIL="
+ (cond
+ ((not start-time)
+ (format-time-string "%Y%m%d" encoded))
+ (local-time-p
+ (format-time-string "%Y%m%dT%H%M%S" encoded))
+ ((format-time-string "%Y%m%dT%H%M%SZ"
+ encoded t))))))
+ "\n")))
"SUMMARY:" summary "\n"
(and (org-string-nw-p location) (format "LOCATION:%s\n" location))
(and (org-string-nw-p class) (format "CLASS:%s\n" class))
diff --git a/testing/lisp/test-org-lint.el b/testing/lisp/test-org-lint.el
index 6ee1b1fab..f61b8647c 100644
--- a/testing/lisp/test-org-lint.el
+++ b/testing/lisp/test-org-lint.el
@@ -406,6 +406,13 @@ (ert-deftest test-org-lint/colon-in-name ()
(org-test-with-temp-text "#+name: name\n| a |"
(org-lint '(colon-in-name)))))
+(ert-deftest test-org-lint/mismatched-planning-repeaters ()
+ "Test `org-lint-mismatched-planning-repeaters' checker."
+ (should
+ (org-test-with-temp-text "* H
+DEADLINE: <2023-03-26 Sun +2w> SCHEDULED: <2023-03-26 Sun +1w>"
+ (org-lint '(mismatched-planning-repeaters)))))
+
(ert-deftest test-org-lint/misplaced-planning-info ()
"Test `org-lint-misplaced-planning-info' checker."
(should
diff --git a/testing/lisp/test-ox-icalendar.el
b/testing/lisp/test-ox-icalendar.el
index bfc756d51..6a0c961d7 100644
--- a/testing/lisp/test-ox-icalendar.el
+++ b/testing/lisp/test-ox-icalendar.el
@@ -40,5 +40,79 @@ (ert-deftest test-ox-icalendar/crlf-endings ()
(should (eql 1 (coding-system-eol-type last-coding-system-used))))
(when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+(ert-deftest test-ox-icalendar/todo-repeater-shared ()
+ "Test shared repeater on todo scheduled and deadline."
+ (let* ((org-icalendar-include-todo 'all)
+ (tmp-ics (org-test-with-temp-text-in-file
+ "* TODO Both repeating
+DEADLINE: <2023-04-02 Sun +1m> SCHEDULED: <2023-03-26 Sun +1m>"
+ (expand-file-name (org-icalendar-export-to-ics)))))
+ (unwind-protect
+ (with-temp-buffer
+ (insert-file-contents tmp-ics)
+ (save-excursion
+ (should (search-forward "DTSTART;VALUE=DATE:20230326")))
+ (save-excursion
+ (should (search-forward "DUE;VALUE=DATE:20230402")))
+ (save-excursion
+ (should (search-forward "RRULE:FREQ=MONTHLY;INTERVAL=1"))))
+ (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(ert-deftest test-ox-icalendar/todo-repeating-deadline-warndays ()
+ "Test repeating deadline with DTSTART as warning days."
+ (let* ((org-icalendar-include-todo 'all)
+ (org-icalendar-todo-unscheduled-start 'recurring-deadline-warning)
+ (tmp-ics (org-test-with-temp-text-in-file
+ "* TODO Repeating deadline
+DEADLINE: <2023-04-02 Sun +2w -3d>"
+ (expand-file-name (org-icalendar-export-to-ics)))))
+ (unwind-protect
+ (with-temp-buffer
+ (insert-file-contents tmp-ics)
+ (save-excursion
+ (should (search-forward "DTSTART;VALUE=DATE:20230330")))
+ (save-excursion
+ (should (search-forward "DUE;VALUE=DATE:20230402")))
+ (save-excursion
+ (should (search-forward "RRULE:FREQ=WEEKLY;INTERVAL=2"))))
+ (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(ert-deftest test-ox-icalendar/todo-repeater-until ()
+ "Test repeater on todo scheduled until deadline."
+ (let* ((org-icalendar-include-todo 'all)
+ (tmp-ics (org-test-with-temp-text-in-file
+ "* TODO Repeating scheduled with nonrepeating deadline
+DEADLINE: <2023-05-01 Mon> SCHEDULED: <2023-03-26 Sun +3d>"
+ (expand-file-name (org-icalendar-export-to-ics)))))
+ (unwind-protect
+ (with-temp-buffer
+ (insert-file-contents tmp-ics)
+ (save-excursion
+ (should (search-forward "DTSTART;VALUE=DATE:20230326")))
+ (save-excursion
+ (should (not (re-search-forward "^DUE" nil t))))
+ (save-excursion
+ (should (search-forward
"RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=20230501"))))
+ (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
+(ert-deftest test-ox-icalendar/todo-repeater-until-utc ()
+ "Test that UNTIL is in UTC when DTSTART is not in local time format."
+ (let* ((org-icalendar-include-todo 'all)
+ (org-icalendar-date-time-format ":%Y%m%dT%H%M%SZ")
+ (tmp-ics (org-test-with-temp-text-in-file
+ "* TODO Repeating scheduled with nonrepeating deadline
+DEADLINE: <2023-05-02 Tue> SCHEDULED: <2023-03-26 Sun 15:00 +3d>"
+ (expand-file-name (org-icalendar-export-to-ics)))))
+ (unwind-protect
+ (with-temp-buffer
+ (insert-file-contents tmp-ics)
+ (save-excursion
+ (should (re-search-forward "DTSTART:2023032.T..0000")))
+ (save-excursion
+ (should (not (re-search-forward "^DUE" nil t))))
+ (save-excursion
+ (should (re-search-forward
"RRULE:FREQ=DAILY;INTERVAL=3;UNTIL=2023050.T..0000Z"))))
+ (when (file-exists-p tmp-ics) (delete-file tmp-ics)))))
+
(provide 'test-ox-icalendar)
;;; test-ox-icalendar.el ends here
--
2.40.1
- [PATCH] ox-icalendar: Unscheduled tasks & repeating tasks,
Jack Kamm <=