config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

ox-icalendar.el (48806B)


      1 ;;; ox-icalendar.el --- iCalendar Backend for Org Export Engine -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2004-2024 Free Software Foundation, Inc.
      4 
      5 ;; Author: Carsten Dominik <carsten.dominik@gmail.com>
      6 ;;      Nicolas Goaziou <mail@nicolasgoaziou.fr>
      7 ;; Maintainer: Jack Kamm <jackkamm@gmail.com>
      8 ;; Keywords: outlines, hypermedia, calendar, text
      9 ;; URL: https://orgmode.org
     10 
     11 ;; This file is part of GNU Emacs.
     12 
     13 ;; GNU Emacs is free software: you can redistribute it and/or modify
     14 ;; it under the terms of the GNU General Public License as published by
     15 ;; the Free Software Foundation, either version 3 of the License, or
     16 ;; (at your option) any later version.
     17 
     18 ;; GNU Emacs is distributed in the hope that it will be useful,
     19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     21 ;; GNU General Public License for more details.
     22 
     23 ;; You should have received a copy of the GNU General Public License
     24 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     25 
     26 ;;; Commentary:
     27 ;;
     28 ;; This library implements an iCalendar backend for Org generic
     29 ;; exporter.  See Org manual for more information.
     30 ;;
     31 ;; It is expected to conform to RFC 5545.
     32 
     33 ;;; Code:
     34 
     35 (require 'org-macs)
     36 (org-assert-version)
     37 
     38 (require 'cl-lib)
     39 (require 'org-agenda)
     40 (require 'ox-ascii)
     41 (declare-function org-bbdb-anniv-export-ical "ol-bbdb" nil)
     42 (declare-function org-at-heading-p "org" (&optional _))
     43 (declare-function org-back-to-heading "org" (&optional invisible-ok))
     44 (declare-function org-next-visible-heading "org" (arg))
     45 
     46 
     47 
     48 ;;; User-Configurable Variables
     49 
     50 (defgroup org-export-icalendar nil
     51   "Options specific for iCalendar export backend."
     52   :tag "Org Export iCalendar"
     53   :group 'org-export)
     54 
     55 (defcustom org-icalendar-combined-agenda-file "~/org.ics"
     56   "The file name for the iCalendar file covering all agenda files.
     57 This file is created with the command `\\[org-icalendar-combine-agenda-files]'.
     58 The file name should be absolute.  It will be overwritten without warning."
     59   :group 'org-export-icalendar
     60   :type 'file)
     61 
     62 (defcustom org-icalendar-alarm-time 0
     63   "Number of minutes for triggering an alarm for exported timed events.
     64 
     65 A zero value (the default) turns off the definition of an alarm trigger
     66 for timed events.  If non-zero, alarms are created.
     67 
     68 - a single alarm per entry is defined
     69 - The alarm will go off N minutes before the event
     70 - only a DISPLAY action is defined."
     71   :group 'org-export-icalendar
     72   :version "24.1"
     73   :type 'integer)
     74 
     75 (defcustom org-icalendar-force-alarm nil
     76   "Non-nil means alarm will be created even if is set to zero.
     77 
     78 This overrides default behavior where zero means no alarm.  With
     79 this set to non-nil and alarm set to zero, alarm will be created
     80 and will fire at the event start."
     81   :group 'org-export-icalendar
     82   :type 'boolean
     83   :package-version '(Org . "9.6")
     84   :safe #'booleanp)
     85 
     86 (defcustom org-icalendar-combined-name "OrgMode"
     87   "Calendar name for the combined iCalendar representing all agenda files."
     88   :group 'org-export-icalendar
     89   :type 'string)
     90 
     91 (defcustom org-icalendar-combined-description ""
     92   "Calendar description for the combined iCalendar (all agenda files)."
     93   :group 'org-export-icalendar
     94   :type 'string)
     95 
     96 (defcustom org-icalendar-exclude-tags nil
     97   "Tags that exclude a tree from export.
     98 This variable allows specifying different exclude tags from other
     99 backends.  It can also be set with the ICALENDAR_EXCLUDE_TAGS
    100 keyword."
    101   :group 'org-export-icalendar
    102   :type '(repeat (string :tag "Tag")))
    103 
    104 (defcustom org-icalendar-scheduled-summary-prefix "S: "
    105   "String prepended to exported scheduled headlines."
    106   :group 'org-export-icalendar
    107   :type 'string
    108   :package-version '(Org . "9.6")
    109   :safe #'stringp)
    110 
    111 
    112 (defcustom org-icalendar-deadline-summary-prefix "DL: "
    113   "String prepended to exported headlines with a deadline."
    114   :group 'org-export-icalendar
    115   :type 'string
    116   :package-version '(Org . "9.6")
    117   :safe #'stringp)
    118 
    119 (defcustom org-icalendar-use-deadline '(event-if-not-todo todo-due)
    120   "Contexts where iCalendar export should use a deadline time stamp.
    121 
    122 This is a list with possibly several symbols in it.  Valid symbols are:
    123 
    124 `event-if-todo'
    125 
    126   Deadlines in TODO entries become calendar events.
    127 
    128 `event-if-todo-not-done'
    129 
    130   Deadlines in TODO entries with not-DONE state become events.
    131 
    132 `event-if-not-todo'
    133 
    134   Deadlines in non-TODO entries become calendar events.
    135 
    136 `todo-due'
    137 
    138   Use deadlines in TODO entries as due-dates."
    139   :group 'org-export-icalendar
    140   :type
    141   '(set :greedy t
    142 	(const :tag "DEADLINE in non-TODO entries become events"
    143 	       event-if-not-todo)
    144 	(const :tag "DEADLINE in TODO entries become events"
    145 	       event-if-todo)
    146 	(const :tag "DEADLINE in TODO entries with not-DONE state become events"
    147 	       event-if-todo-not-done)
    148 	(const :tag "DEADLINE in TODO entries become due-dates"
    149 	       todo-due)))
    150 
    151 (defcustom org-icalendar-use-scheduled '(todo-start)
    152   "Contexts where iCalendar export should use a scheduling time stamp.
    153 
    154 This is a list with possibly several symbols in it.  Valid symbols are:
    155 
    156 `event-if-todo'
    157 
    158   Scheduling time stamps in TODO entries become an event.
    159 
    160 `event-if-todo-not-done'
    161 
    162   Scheduling time stamps in TODO entries with not-DONE state
    163   become events.
    164 
    165 `event-if-not-todo'
    166 
    167   Scheduling time stamps in non-TODO entries become an event.
    168 
    169 `todo-start'
    170 
    171   Scheduling time stamps in TODO entries become start date.  (See
    172   also `org-icalendar-todo-unscheduled-start', which controls the
    173   start date for TODO entries without a scheduling time stamp)"
    174   :group 'org-export-icalendar
    175   :type
    176   '(set :greedy t
    177 	(const :tag "SCHEDULED timestamps in non-TODO entries become events"
    178 	       event-if-not-todo)
    179 	(const :tag "SCHEDULED timestamps in TODO entries become events"
    180 	       event-if-todo)
    181 	(const :tag "SCHEDULED in TODO entries with not-DONE state become events"
    182 	       event-if-todo-not-done)
    183 	(const :tag "SCHEDULED in TODO entries become start date"
    184 	       todo-start)))
    185 
    186 (defcustom org-icalendar-categories '(local-tags category)
    187   "Items that should be entered into the \"categories\" field.
    188 
    189 This is a list of symbols, the following are valid:
    190 `category'    The Org mode category of the current file or tree
    191 `todo-state'  The todo state, if any
    192 `local-tags'  The tags, defined in the current line
    193 `all-tags'    All tags, including inherited ones."
    194   :group 'org-export-icalendar
    195   :type '(repeat
    196 	  (choice
    197 	   (const :tag "The file or tree category" category)
    198 	   (const :tag "The TODO state" todo-state)
    199 	   (const :tag "Tags defined in current line" local-tags)
    200 	   (const :tag "All tags, including inherited ones" all-tags))))
    201 
    202 (defcustom org-icalendar-with-timestamps 'active
    203   "Non-nil means make an event from plain time stamps.
    204 
    205 It can be set to `active', `inactive', t or nil, in order to make
    206 an event from, respectively, only active timestamps, only
    207 inactive ones, all of them or none.
    208 
    209 This variable has precedence over `org-export-with-timestamps'.
    210 It can also be set with the #+OPTIONS line, e.g. \"<:t\"."
    211   :group 'org-export-icalendar
    212   :type '(choice
    213 	  (const :tag "All timestamps" t)
    214 	  (const :tag "Only active timestamps" active)
    215 	  (const :tag "Only inactive timestamps" inactive)
    216 	  (const :tag "No timestamp" nil)))
    217 
    218 (defcustom org-icalendar-include-todo nil
    219   "Non-nil means create VTODO components from TODO items.
    220 
    221 Valid values are:
    222 nil                  don't include any task.
    223 t                    include tasks that are not in DONE state.
    224 `unblocked'          include all TODO items that are not blocked.
    225 `all'                include both done and not done items.
    226 \\(\"TODO\" ...)       include specific TODO keywords."
    227   :group 'org-export-icalendar
    228   :type '(choice
    229 	  (const :tag "None" nil)
    230 	  (const :tag "Unfinished" t)
    231 	  (const :tag "Unblocked" unblocked)
    232 	  (const :tag "All" all)
    233 	  (repeat :tag "Specific TODO keywords"
    234 		  (string :tag "Keyword"))))
    235 
    236 (defcustom org-icalendar-todo-unscheduled-start 'recurring-deadline-warning
    237   "Exported start date of unscheduled TODOs.
    238 
    239 If `org-icalendar-use-scheduled' contains `todo-start' and a task
    240 has a \"SCHEDULED\" timestamp, that is always used as the start
    241 date.  Otherwise, this variable controls whether a start date is
    242 exported and what its value is.
    243 
    244 Note that the iCalendar spec RFC 5545 does not generally require
    245 tasks to have a start date, except for repeating tasks which do
    246 require a start date.  However some iCalendar programs ignore the
    247 requirement for repeating tasks, and allow repeating deadlines
    248 without a matching start date.
    249 
    250 This variable has no effect when `org-icalendar-include-todo' is nil.
    251 
    252 Valid values are:
    253 `recurring-deadline-warning'  If deadline repeater present,
    254                               use `org-deadline-warning-days' as start.
    255 `deadline-warning'            If deadline present,
    256                               use `org-deadline-warning-days' as start.
    257 `current-datetime'            Use the current date-time as start.
    258 nil                           Never add a start time for unscheduled tasks."
    259   :group 'org-export-icalendar
    260   :type '(choice
    261 	  (const :tag "Warning days if deadline recurring" recurring-deadline-warning)
    262 	  (const :tag "Warning days if deadline present" deadline-warning)
    263 	  (const :tag "Now" current-datetime)
    264 	  (const :tag "No start date" nil))
    265   :package-version '(Org . "9.7")
    266   :safe #'symbolp)
    267 
    268 (defcustom org-icalendar-include-bbdb-anniversaries nil
    269   "Non-nil means a combined iCalendar file should include anniversaries.
    270 The anniversaries are defined in the BBDB database."
    271   :group 'org-export-icalendar
    272   :type 'boolean)
    273 
    274 (defcustom org-icalendar-include-sexps t
    275   "Non-nil means export to iCalendar files should also cover sexp entries.
    276 These are entries like in the diary, but directly in an Org file."
    277   :group 'org-export-icalendar
    278   :type 'boolean)
    279 
    280 (defcustom org-icalendar-include-body t
    281   "Amount of text below headline to be included in iCalendar export.
    282 This is a number of characters that should maximally be included.
    283 Properties, scheduling and clocking lines will always be removed.
    284 The text will be inserted into the DESCRIPTION field."
    285   :group 'org-export-icalendar
    286   :type '(choice
    287 	  (const :tag "Nothing" nil)
    288 	  (const :tag "Everything" t)
    289 	  (integer :tag "Max characters")))
    290 
    291 (defcustom org-icalendar-store-UID nil
    292   "Non-nil means store any created UIDs in properties.
    293 
    294 The iCalendar standard requires that all entries have a unique identifier.
    295 Org will create these identifiers as needed.  When this variable is non-nil,
    296 the created UIDs will be stored in the ID property of the entry.  Then the
    297 next time this entry is exported, it will be exported with the same UID,
    298 superseding the previous form of it.  This is essential for
    299 synchronization services.
    300 
    301 This variable is not turned on by default because we want to avoid creating
    302 a property drawer in every entry if people are only playing with this feature,
    303 or if they are only using it locally."
    304   :group 'org-export-icalendar
    305   :type 'boolean)
    306 
    307 (defcustom org-icalendar-timezone (getenv "TZ")
    308   "The time zone string for iCalendar export.
    309 When nil or the empty string, use output
    310 from (current-time-zone)."
    311   :group 'org-export-icalendar
    312   :type '(choice
    313 	  (const :tag "Unspecified" nil)
    314 	  (string :tag "Time zone")))
    315 
    316 (defcustom org-icalendar-date-time-format ":%Y%m%dT%H%M%S"
    317   "Format-string for exporting icalendar DATE-TIME.
    318 
    319 See `format-time-string' for a full documentation.  The only
    320 difference is that `org-icalendar-timezone' is used for %Z.
    321 
    322 Interesting value are:
    323  - \":%Y%m%dT%H%M%S\" for local time
    324  - \";TZID=%Z:%Y%m%dT%H%M%S\" for local time with explicit timezone
    325  - \":%Y%m%dT%H%M%SZ\" for time expressed in Universal Time"
    326   :group 'org-export-icalendar
    327   :version "24.1"
    328   :type '(choice
    329 	  (const :tag "Local time" ":%Y%m%dT%H%M%S")
    330 	  (const :tag "Explicit local time" ";TZID=%Z:%Y%m%dT%H%M%S")
    331 	  (const :tag "Universal time" ":%Y%m%dT%H%M%SZ")
    332 	  (string :tag "Explicit format")))
    333 
    334 (defcustom org-icalendar-ttl nil
    335   "Time to live for the exported calendar.
    336 
    337 Subscribing clients to the exported ics file can derive the time
    338 interval to read the file again from the server.  One example of such
    339 client is Nextcloud calendar, which respects the setting of
    340 X-PUBLISHED-TTL in ICS files.  Setting `org-icalendar-ttl' to \"PT1H\"
    341 would advise a server to reload the file every hour.
    342 
    343 See https://icalendar.org/iCalendar-RFC-5545/3-8-2-5-duration.html
    344 for a complete description of possible specifications of this
    345 option.  For example, \"PT1H\" stands for 1 hour and
    346 \"PT0H27M34S\" stands for 0 hours, 27 minutes and 34 seconds.
    347 
    348 The default value is nil, which means no such option is set in
    349 the ICS file. This option can also be set on a per-document basis
    350 with the ICAL-TTL export keyword."
    351   :group 'org-export-icalendar
    352   :type '(choice
    353           (const :tag "No refresh period" nil)
    354           (const :tag "One hour" "PT1H")
    355           (const :tag "One day" "PT1D")
    356           (const :tag "One week" "PT7D")
    357           (string :tag "Other"))
    358   :package-version '(Org . "9.7"))
    359 
    360 (defvar org-icalendar-after-save-hook nil
    361   "Hook run after an iCalendar file has been saved.
    362 This hook is run with the name of the file as argument.  A good
    363 way to use this is to tell a desktop calendar application to
    364 re-read the iCalendar file.")
    365 
    366 
    367 
    368 ;;; Define Backend
    369 
    370 (org-export-define-derived-backend 'icalendar 'ascii
    371   :translate-alist '((clock . nil)
    372 		     (footnote-definition . nil)
    373 		     (footnote-reference . nil)
    374 		     (headline . org-icalendar-entry)
    375                      (inner-template . org-icalendar-inner-template)
    376 		     (inlinetask . nil)
    377 		     (planning . nil)
    378 		     (section . nil)
    379 		     (template . org-icalendar-template))
    380   :options-alist
    381   '((:exclude-tags
    382      "ICALENDAR_EXCLUDE_TAGS" nil org-icalendar-exclude-tags split)
    383     (:with-timestamps nil "<" org-icalendar-with-timestamps)
    384     ;; Other variables.
    385     (:icalendar-alarm-time nil nil org-icalendar-alarm-time)
    386     (:icalendar-categories nil nil org-icalendar-categories)
    387     (:icalendar-date-time-format nil nil org-icalendar-date-time-format)
    388     (:icalendar-include-bbdb-anniversaries nil nil org-icalendar-include-bbdb-anniversaries)
    389     (:icalendar-include-body nil nil org-icalendar-include-body)
    390     (:icalendar-include-sexps nil nil org-icalendar-include-sexps)
    391     (:icalendar-include-todo nil nil org-icalendar-include-todo)
    392     (:icalendar-store-UID nil nil org-icalendar-store-UID)
    393     (:icalendar-timezone nil nil org-icalendar-timezone)
    394     (:icalendar-use-deadline nil nil org-icalendar-use-deadline)
    395     (:icalendar-use-scheduled nil nil org-icalendar-use-scheduled)
    396     (:icalendar-scheduled-summary-prefix nil nil org-icalendar-scheduled-summary-prefix)
    397     (:icalendar-deadline-summary-prefix nil nil org-icalendar-deadline-summary-prefix)
    398     (:icalendar-ttl "ICAL-TTL" nil org-icalendar-ttl))
    399   :filters-alist
    400   '((:filter-headline . org-icalendar-clear-blank-lines))
    401   :menu-entry
    402   '(?c "Export to iCalendar"
    403        ((?f "Current file" org-icalendar-export-to-ics)
    404 	(?a "All agenda files"
    405 	    (lambda (a s v b) (org-icalendar-export-agenda-files a)))
    406 	(?c "Combine all agenda files"
    407 	    (lambda (a s v b) (org-icalendar-combine-agenda-files a))))))
    408 
    409 
    410 
    411 ;;; Internal Functions
    412 
    413 (defun org-icalendar-create-uid (file &optional bell)
    414   "Set ID property on headlines missing it in FILE.
    415 When optional argument BELL is non-nil, inform the user with
    416 a message if the file was modified."
    417   (let (modified-flag)
    418     (org-map-entries
    419      (lambda ()
    420        (let ((entry (org-element-at-point)))
    421 	 (unless (org-element-property :ID entry)
    422 	   (org-id-get-create)
    423 	   (setq modified-flag t)
    424 	   (forward-line))))
    425      nil nil 'comment)
    426     (when (and bell modified-flag)
    427       (message "ID properties created in file \"%s\"" file)
    428       (sit-for 2))))
    429 
    430 (defun org-icalendar-blocked-headline-p (headline info)
    431   "Non-nil when HEADLINE is considered to be blocked.
    432 
    433 INFO is a plist used as a communication channel.
    434 
    435 A headline is blocked when either
    436 
    437   - it has children which are not all in a completed state;
    438 
    439   - it has a parent with the property :ORDERED:, and there are
    440     siblings prior to it with incomplete status;
    441 
    442   - its parent is blocked because it has siblings that should be
    443     done first or is a child of a blocked grandparent entry."
    444   (or
    445    ;; Check if any child is not done.
    446    (org-element-map (org-element-contents headline) 'headline
    447      (lambda (hl) (eq (org-element-property :todo-type hl) 'todo))
    448      info 'first-match)
    449    ;; Check :ORDERED: node property.
    450    (catch 'blockedp
    451      (let ((current headline))
    452        (dolist (parent (org-element-lineage headline))
    453 	 (cond
    454 	  ((not (org-element-property :todo-keyword parent))
    455 	   (throw 'blockedp nil))
    456 	  ((org-not-nil (org-element-property :ORDERED parent))
    457 	   (let ((sibling current))
    458 	     (while (setq sibling (org-export-get-previous-element
    459 				   sibling info))
    460 	       (when (eq (org-element-property :todo-type sibling) 'todo)
    461 		 (throw 'blockedp t)))))
    462 	  (t (setq current parent))))))))
    463 
    464 (defun org-icalendar-use-UTC-date-time-p ()
    465   "Non-nil when `org-icalendar-date-time-format' requires UTC time."
    466   (char-equal (elt org-icalendar-date-time-format
    467 		   (1- (length org-icalendar-date-time-format)))
    468 	      ?Z))
    469 
    470 (defun org-icalendar-convert-timestamp (timestamp keyword &optional end tz)
    471   "Convert TIMESTAMP to iCalendar format.
    472 
    473 TIMESTAMP is a timestamp object.  KEYWORD is added in front of
    474 it, in order to make a complete line (e.g. \"DTSTART\").
    475 
    476 When optional argument END is non-nil, use end of time range.
    477 Also increase the hour by two (if time string contains a time),
    478 or the day by one (if it does not contain a time) when no
    479 explicit ending time is specified.
    480 
    481 When optional argument TZ is non-nil, timezone data time will be
    482 added to the timestamp.  It can be the string \"UTC\", to use UTC
    483 time, or a string in the IANA TZ database
    484 format (e.g. \"Europe/London\").  In either case, the value of
    485 `org-icalendar-date-time-format' will be ignored."
    486   (let* ((year-start (org-element-property :year-start timestamp))
    487 	 (year-end (org-element-property :year-end timestamp))
    488 	 (month-start (org-element-property :month-start timestamp))
    489 	 (month-end (org-element-property :month-end timestamp))
    490 	 (day-start (org-element-property :day-start timestamp))
    491 	 (day-end (org-element-property :day-end timestamp))
    492 	 (hour-start (org-element-property :hour-start timestamp))
    493 	 (hour-end (org-element-property :hour-end timestamp))
    494 	 (minute-start (org-element-property :minute-start timestamp))
    495 	 (minute-end (org-element-property :minute-end timestamp))
    496 	 (with-time-p minute-start)
    497 	 (equal-bounds-p
    498 	  (equal (list year-start month-start day-start hour-start minute-start)
    499 		 (list year-end month-end day-end hour-end minute-end)))
    500 	 (mi (cond ((not with-time-p) 0)
    501 		   ((not end) minute-start)
    502 		   ((and org-agenda-default-appointment-duration equal-bounds-p)
    503 		    (+ minute-end org-agenda-default-appointment-duration))
    504 		   (t minute-end)))
    505 	 (h (cond ((not with-time-p) 0)
    506 		  ((not end) hour-start)
    507 		  ((or (not equal-bounds-p)
    508 		       org-agenda-default-appointment-duration)
    509 		   hour-end)
    510 		  (t (+ hour-end 2))))
    511 	 (d (cond ((not end) day-start)
    512 		  ((not with-time-p) (1+ day-end))
    513 		  (t day-end)))
    514 	 (m (if end month-end month-start))
    515 	 (y (if end year-end year-start)))
    516     (concat
    517      keyword
    518      (format-time-string
    519       (cond ((string-equal tz "UTC") ":%Y%m%dT%H%M%SZ")
    520 	    ((not with-time-p) ";VALUE=DATE:%Y%m%d")
    521 	    ((stringp tz) (concat ";TZID=" tz ":%Y%m%dT%H%M%S"))
    522 	    (t (replace-regexp-in-string "%Z"
    523 					 org-icalendar-timezone
    524 					 org-icalendar-date-time-format
    525 					 t)))
    526       ;; Convert timestamp into internal time in order to use
    527       ;; `format-time-string' and fix any mistake (i.e. MI >= 60).
    528       (org-encode-time 0 mi h d m y)
    529       (and (or (string-equal tz "UTC")
    530 	       (and (null tz)
    531 		    with-time-p
    532 		    (org-icalendar-use-UTC-date-time-p)))
    533 	   t)))))
    534 
    535 (defun org-icalendar-dtstamp ()
    536   "Return DTSTAMP property, as a string."
    537   (format-time-string "DTSTAMP:%Y%m%dT%H%M%SZ" nil t))
    538 
    539 (defun org-icalendar-get-categories (entry info)
    540   "Return categories according to `org-icalendar-categories'.
    541 ENTRY is a headline or an inlinetask element.  INFO is a plist
    542 used as a communication channel."
    543   (mapconcat
    544    #'identity
    545    (org-uniquify
    546     (let (categories)
    547       (dolist (type org-icalendar-categories (nreverse categories))
    548 	(cl-case type
    549 	  (category
    550 	   (push (org-export-get-category entry info) categories))
    551 	  (todo-state
    552 	   (let ((todo (org-element-property :todo-keyword entry)))
    553 	     (and todo (push todo categories))))
    554 	  (local-tags
    555 	   (setq categories
    556 		 (append (nreverse (org-export-get-tags entry info))
    557 			 categories)))
    558 	  (all-tags
    559 	   (setq categories
    560 		 (append (nreverse (org-export-get-tags entry info nil t))
    561 			 categories)))))))
    562    ","))
    563 
    564 (defun org-icalendar-transcode-diary-sexp (sexp uid summary)
    565   "Transcode a diary sexp into iCalendar format.
    566 SEXP is the diary sexp being transcoded, as a string.  UID is the
    567 unique identifier for the entry.  SUMMARY defines a short summary
    568 or subject for the event."
    569   (when (require 'icalendar nil t)
    570     (org-element-normalize-string
    571      (with-temp-buffer
    572        (let ((sexp (if (not (string-match "\\`<%%" sexp)) sexp
    573 		     (concat (substring sexp 1 -1) " " summary))))
    574 	 (put-text-property 0 1 'uid uid sexp)
    575 	 (insert sexp "\n"))
    576        (org-diary-to-ical-string (current-buffer))))))
    577 
    578 (defun org-icalendar-cleanup-string (s)
    579   "Cleanup string S according to RFC 5545."
    580   (when s
    581     ;; Protect "\", "," and ";" characters. and replace newline
    582     ;; characters with literal \n.
    583     (replace-regexp-in-string
    584      "[ \t]*\n" "\\n"
    585      (replace-regexp-in-string "[\\,;]" "\\\\\\&" s)
    586      nil t)))
    587 
    588 (defun org-icalendar-fold-string (s)
    589   "Fold string S according to RFC 5545."
    590   (org-element-normalize-string
    591    (mapconcat
    592     (lambda (line)
    593       ;; Limit each line to a maximum of 75 characters.  If it is
    594       ;; longer, fold it by using "\r\n " as a continuation marker.
    595       (let ((len (length line)))
    596 	(if (<= len 75) line
    597 	  (let ((folded-line (substring line 0 75))
    598 		(chunk-start 75)
    599 		chunk-end)
    600 	    ;; Since continuation marker takes up one character on the
    601 	    ;; line, real contents must be split at 74 chars.
    602 	    (while (< (setq chunk-end (+ chunk-start 74)) len)
    603 	      (setq folded-line
    604 		    (concat folded-line "\n "
    605 			    (substring line chunk-start chunk-end))
    606 		    chunk-start chunk-end))
    607 	    (concat folded-line "\n " (substring line chunk-start))))))
    608     (org-split-string s "\n") "\n")))
    609 
    610 (defun org-icalendar--post-process-file (file)
    611   "Post-process the exported iCalendar FILE.
    612 Converts line endings to dos-style CRLF as per RFC 5545, then
    613 runs `org-icalendar-after-save-hook'."
    614   (with-temp-buffer
    615     (insert-file-contents file)
    616     (let ((coding-system-for-write (coding-system-change-eol-conversion
    617                                     last-coding-system-used 'dos)))
    618       (write-region nil nil file)))
    619   (run-hook-with-args 'org-icalendar-after-save-hook file)
    620   nil)
    621 
    622 
    623 ;;; Filters
    624 
    625 (defun org-icalendar-clear-blank-lines (headline _backend _info)
    626   "Remove blank lines in HEADLINE export.
    627 HEADLINE is a string representing a transcoded headline.
    628 BACKEND and INFO are ignored."
    629   (replace-regexp-in-string "^\\(?:[ \t]*\n\\)+" "" headline))
    630 
    631 
    632 
    633 ;;; Transcode Functions
    634 
    635 ;;;; Headline and Inlinetasks
    636 
    637 ;; The main function is `org-icalendar-entry', which extracts
    638 ;; information from a headline or an inlinetask (summary,
    639 ;; description...) and then delegates code generation to
    640 ;; `org-icalendar--vtodo' and `org-icalendar--vevent', depending
    641 ;; on the component needed.
    642 
    643 ;; Obviously, `org-icalendar--valarm' handles alarms, which can
    644 ;; happen within a VTODO component.
    645 
    646 (defun org-icalendar-entry (entry contents info)
    647   "Transcode ENTRY element into iCalendar format.
    648 
    649 ENTRY is either a headline or an inlinetask.  CONTENTS is
    650 ignored.  INFO is a plist used as a communication channel.
    651 
    652 This function is called on every headline, the section below
    653 it (minus inlinetasks) being its contents.  It tries to create
    654 VEVENT and VTODO components out of scheduled date, deadline date,
    655 plain timestamps, diary sexps.  It also calls itself on every
    656 inlinetask within the section."
    657   (unless (org-element-property :footnote-section-p entry)
    658     (let* ((type (org-element-type entry))
    659 	   ;; Determine contents really associated to the entry.  For
    660 	   ;; a headline, limit them to section, if any.  For an
    661 	   ;; inlinetask, this is every element within the task.
    662 	   (inside
    663 	    (if (eq type 'inlinetask)
    664 		(cons 'org-data (cons nil (org-element-contents entry)))
    665 	      (let ((first (car (org-element-contents entry))))
    666 		(and (org-element-type-p first 'section)
    667 		     (cons 'org-data
    668 			   (cons nil (org-element-contents first))))))))
    669       (concat
    670        (let ((todo-type (org-element-property :todo-type entry))
    671 	     (uid (or (org-element-property :ID entry) (org-id-new)))
    672 	     (summary (org-icalendar-cleanup-string
    673 		       (or
    674                         (let ((org-property-separators '(("SUMMARY" . "\n"))))
    675                           (org-entry-get entry "SUMMARY" 'selective))
    676 			(org-export-data
    677 			 (org-element-property :title entry) info))))
    678 	     (loc
    679               (let ((org-property-separators '(("LOCATION" . "\n"))))
    680                 (org-icalendar-cleanup-string
    681                  (org-entry-get entry "LOCATION" 'selective))))
    682 	     (class (org-icalendar-cleanup-string
    683 		     (org-export-get-node-property
    684 		      :CLASS entry
    685 		      (org-property-inherit-p "CLASS"))))
    686 	     ;; Build description of the entry from associated section
    687 	     ;; (headline) or contents (inlinetask).
    688 	     (desc
    689 	      (org-icalendar-cleanup-string
    690 	       (or (let ((org-property-separators '(("DESCRIPTION" . "\n"))))
    691                      (org-entry-get entry "DESCRIPTION" 'selective))
    692 		   (let ((contents (org-export-data inside info)))
    693 		     (cond
    694 		      ((not (org-string-nw-p contents)) nil)
    695 		      ((wholenump org-icalendar-include-body)
    696 		       (let ((contents (org-trim contents)))
    697 			 (substring
    698 			  contents 0 (min (length contents)
    699 					  org-icalendar-include-body))))
    700 		      (org-icalendar-include-body (org-trim contents)))))))
    701 	     (cat (org-icalendar-get-categories entry info))
    702 	     (tz (org-export-get-node-property
    703 		  :TIMEZONE entry
    704 		  (org-property-inherit-p "TIMEZONE"))))
    705 	 (concat
    706 	  ;; Events: Delegate to `org-icalendar--vevent' to generate
    707 	  ;; "VEVENT" component from scheduled, deadline, or any
    708 	  ;; timestamp in the entry.
    709 	  (let ((deadline (org-element-property :deadline entry))
    710 		(use-deadline (plist-get info :icalendar-use-deadline))
    711                 (deadline-summary-prefix (org-icalendar-cleanup-string
    712                                           (plist-get info :icalendar-deadline-summary-prefix))))
    713 	    (and deadline
    714 		 (pcase todo-type
    715 		   (`todo (or (memq 'event-if-todo-not-done use-deadline)
    716 			      (memq 'event-if-todo use-deadline)))
    717 		   (`done (memq 'event-if-todo use-deadline))
    718 		   (_ (memq 'event-if-not-todo use-deadline)))
    719 		 (org-icalendar--vevent
    720 		  entry deadline (concat "DL-" uid)
    721 		  (concat deadline-summary-prefix summary)
    722                   loc desc cat tz class)))
    723 	  (let ((scheduled (org-element-property :scheduled entry))
    724 		(use-scheduled (plist-get info :icalendar-use-scheduled))
    725                 (scheduled-summary-prefix (org-icalendar-cleanup-string
    726                                            (plist-get info :icalendar-scheduled-summary-prefix))))
    727 	    (and scheduled
    728 		 (pcase todo-type
    729 		   (`todo (or (memq 'event-if-todo-not-done use-scheduled)
    730 			      (memq 'event-if-todo use-scheduled)))
    731 		   (`done (memq 'event-if-todo use-scheduled))
    732 		   (_ (memq 'event-if-not-todo use-scheduled)))
    733 		 (org-icalendar--vevent
    734 		  entry scheduled (concat "SC-" uid)
    735 		  (concat scheduled-summary-prefix summary)
    736                   loc desc cat tz class)))
    737 	  ;; When collecting plain timestamps from a headline and its
    738 	  ;; title, skip inlinetasks since collection will happen once
    739 	  ;; ENTRY is one of them.
    740 	  (let ((counter 0))
    741 	    (mapconcat
    742 	     #'identity
    743 	     (org-element-map (cons (org-element-property :title entry)
    744 				    (org-element-contents inside))
    745 		 'timestamp
    746 	       (lambda (ts)
    747 		 (when (let ((type (org-element-property :type ts)))
    748 			 (cl-case (plist-get info :with-timestamps)
    749 			   (active (memq type '(active active-range)))
    750 			   (inactive (memq type '(inactive inactive-range)))
    751 			   ((t) t)))
    752 		   (let ((uid (format "TS%d-%s" (cl-incf counter) uid)))
    753 		     (org-icalendar--vevent
    754 		      entry ts uid summary loc desc cat tz class))))
    755 	       info nil (and (eq type 'headline) 'inlinetask))
    756 	     ""))
    757 	  ;; Task: First check if it is appropriate to export it.  If
    758 	  ;; so, call `org-icalendar--vtodo' to transcode it into
    759 	  ;; a "VTODO" component.
    760 	  (when (and todo-type
    761 		     (pcase (plist-get info :icalendar-include-todo)
    762 		       (`all t)
    763 		       (`unblocked
    764 			(and (eq type 'headline)
    765 			     (not (org-icalendar-blocked-headline-p
    766 				 entry info))))
    767                        ;; unfinished
    768 		       (`t (eq todo-type 'todo))
    769                        ((and (pred listp) kwd-list)
    770                         (member (org-element-property :todo-keyword entry) kwd-list))))
    771 	    (org-icalendar--vtodo entry uid summary loc desc cat tz class))
    772 	  ;; Diary-sexp: Collect every diary-sexp element within ENTRY
    773 	  ;; and its title, and transcode them.  If ENTRY is
    774 	  ;; a headline, skip inlinetasks: they will be handled
    775 	  ;; separately.
    776 	  (when org-icalendar-include-sexps
    777 	    (let ((counter 0))
    778 	      (mapconcat #'identity
    779 			 (org-element-map
    780 			     (cons (org-element-property :title entry)
    781 				   (org-element-contents inside))
    782 			     'diary-sexp
    783 			   (lambda (sexp)
    784 			     (org-icalendar-transcode-diary-sexp
    785 			      (org-element-property :value sexp)
    786 			      (format "DS%d-%s" (cl-incf counter) uid)
    787 			      summary))
    788 			   info nil (and (eq type 'headline) 'inlinetask))
    789 			 "")))))
    790        ;; If ENTRY is a headline, call current function on every
    791        ;; inlinetask within it.  In agenda export, this is independent
    792        ;; from the mark (or lack thereof) on the entry.
    793        (when (eq type 'headline)
    794 	 (mapconcat #'identity
    795 		    (org-element-map inside 'inlinetask
    796 		      (lambda (task) (org-icalendar-entry task nil info))
    797 		      info) ""))
    798        ;; Don't forget components from inner entries.
    799        contents))))
    800 
    801 (defun org-icalendar--rrule (unit value)
    802   "Format RRULE icalendar entry for UNIT frequency and VALUE interval.
    803 UNIT is a symbol `hour', `day', `week', `month', or `year'."
    804   (format "RRULE:FREQ=%s;INTERVAL=%d"
    805 	  (cl-case unit
    806 	    (hour "HOURLY") (day "DAILY") (week "WEEKLY")
    807 	    (month "MONTHLY") (year "YEARLY"))
    808 	  value))
    809 
    810 (defun org-icalendar--vevent
    811     (entry timestamp uid summary location description categories timezone class)
    812   "Create a VEVENT component.
    813 
    814 ENTRY is either a headline or an inlinetask element.  TIMESTAMP
    815 is a timestamp object defining the date-time of the event.  UID
    816 is the unique identifier for the event.  SUMMARY defines a short
    817 summary or subject for the event.  LOCATION defines the intended
    818 venue for the event.  DESCRIPTION provides the complete
    819 description of the event.  CATEGORIES defines the categories the
    820 event belongs to.  TIMEZONE specifies a time zone for this event
    821 only.  CLASS contains the visibility attribute.  Three of them
    822 \\(\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are predefined, others
    823 should be treated as \"PRIVATE\" if they are unknown to the iCalendar server.
    824 
    825 Return VEVENT component as a string."
    826   (if (eq (org-element-property :type timestamp) 'diary)
    827       (org-icalendar-transcode-diary-sexp
    828        (org-element-property :raw-value timestamp) uid summary)
    829     (concat "BEGIN:VEVENT\n"
    830 	    (org-icalendar-dtstamp) "\n"
    831 	    "UID:" uid "\n"
    832 	    (org-icalendar-convert-timestamp timestamp "DTSTART" nil timezone) "\n"
    833 	    (org-icalendar-convert-timestamp timestamp "DTEND" t timezone) "\n"
    834 	    ;; RRULE.
    835             (when (org-element-property :repeater-type timestamp)
    836               (concat (org-icalendar--rrule
    837                        (org-element-property :repeater-unit timestamp)
    838                        (org-element-property :repeater-value timestamp))
    839                       "\n"))
    840 	    "SUMMARY:" summary "\n"
    841 	    (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
    842 	    (and (org-string-nw-p class) (format "CLASS:%s\n" class))
    843 	    (and (org-string-nw-p description)
    844 		 (format "DESCRIPTION:%s\n" description))
    845 	    "CATEGORIES:" categories "\n"
    846 	    ;; VALARM.
    847 	    (org-icalendar--valarm entry timestamp summary)
    848 	    "END:VEVENT\n")))
    849 
    850 (defun org-icalendar--repeater-type (elem)
    851   "Return ELEM's repeater-type if supported, else warn and return nil."
    852   (let ((repeater-value (org-element-property :repeater-value elem))
    853         (repeater-type (org-element-property :repeater-type elem)))
    854     (cond
    855      ((not (and repeater-type
    856                 repeater-value
    857                 (> repeater-value 0)))
    858       nil)
    859      ;; TODO Add catch-up to supported repeaters (use EXDATE to implement)
    860      ((not (memq repeater-type '(cumulate)))
    861       (org-display-warning
    862        (format "Repeater-type %s not currently supported by iCalendar export"
    863                (symbol-name repeater-type)))
    864       nil)
    865      (repeater-type))))
    866 
    867 (defun org-icalendar--vtodo
    868     (entry uid summary location description categories timezone class)
    869   "Create a VTODO component.
    870 
    871 ENTRY is either a headline or an inlinetask element.  UID is the
    872 unique identifier for the task.  SUMMARY defines a short summary
    873 or subject for the task.  LOCATION defines the intended venue for
    874 the task.  CLASS sets the task class (e.g. confidential).  DESCRIPTION
    875 provides the complete description of the task.  CATEGORIES defines the
    876 categories the task belongs to.  TIMEZONE specifies a time zone for
    877 this TODO only.
    878 
    879 Return VTODO component as a string."
    880   (let* ((sc (and (memq 'todo-start org-icalendar-use-scheduled)
    881 		  (org-element-property :scheduled entry)))
    882          (dl (and (memq 'todo-due org-icalendar-use-deadline)
    883                   (org-element-property :deadline entry)))
    884          (sc-repeat-p (org-icalendar--repeater-type sc))
    885          (dl-repeat-p (org-icalendar--repeater-type dl))
    886          (repeat-value (or (org-element-property :repeater-value sc)
    887                            (org-element-property :repeater-value dl)))
    888          (repeat-unit (or (org-element-property :repeater-unit sc)
    889                           (org-element-property :repeater-unit dl)))
    890          (repeat-until (and sc-repeat-p (not dl-repeat-p) dl))
    891          (start
    892           (cond
    893            (sc)
    894            ((eq org-icalendar-todo-unscheduled-start 'current-datetime)
    895             (let ((now (decode-time)))
    896 	      (list 'timestamp
    897 		    (list :type 'active
    898 			  :minute-start (nth 1 now)
    899 			  :hour-start (nth 2 now)
    900 			  :day-start (nth 3 now)
    901 			  :month-start (nth 4 now)
    902 			  :year-start (nth 5 now)))))
    903            ((or (and (eq org-icalendar-todo-unscheduled-start
    904                          'deadline-warning)
    905                      dl)
    906                 (and (eq org-icalendar-todo-unscheduled-start
    907                          'recurring-deadline-warning)
    908                      dl-repeat-p))
    909             (let ((dl-raw (org-element-property :raw-value dl)))
    910               (with-temp-buffer
    911 	        (insert dl-raw)
    912                 (goto-char (point-min))
    913 	        (org-timestamp-down-day (org-get-wdays dl-raw))
    914 	        (org-element-timestamp-parser)))))))
    915     (concat "BEGIN:VTODO\n"
    916 	    "UID:TODO-" uid "\n"
    917 	    (org-icalendar-dtstamp) "\n"
    918             (when start (concat (org-icalendar-convert-timestamp
    919                                  start "DTSTART" nil timezone)
    920                                 "\n"))
    921 	    (when (and dl (not repeat-until))
    922 	      (concat (org-icalendar-convert-timestamp
    923 		       dl "DUE" nil timezone)
    924 		      "\n"))
    925             ;; RRULE
    926             (cond
    927              ;; SCHEDULED, DEADLINE have different repeaters
    928              ((and dl-repeat-p
    929                    (not (and (eq repeat-value (org-element-property
    930                                                :repeater-value dl))
    931                              (eq repeat-unit (org-element-property
    932                                               :repeater-unit dl)))))
    933               ;; TODO Implement via RDATE with changing DURATION
    934               (org-display-warning "Not yet implemented: \
    935 different repeaters on SCHEDULED and DEADLINE.  Skipping.")
    936               nil)
    937              ;; DEADLINE has repeater but SCHEDULED doesn't
    938              ((and dl-repeat-p (and sc (not sc-repeat-p)))
    939               ;; TODO SCHEDULED should only apply to first instance;
    940               ;; use RDATE with custom DURATION to implement that
    941               (org-display-warning "Not yet implemented: \
    942 repeater on DEADLINE but not SCHEDULED.  Skipping.")
    943               nil)
    944              ((or sc-repeat-p dl-repeat-p)
    945               (concat
    946                (org-icalendar--rrule repeat-unit repeat-value)
    947                ;; add UNTIL part to RRULE
    948                (when repeat-until
    949                  (let* ((start-time
    950                          (org-element-property :minute-start start))
    951                         ;; RFC5545 requires UTC iff DTSTART is not local time
    952                         (local-time-p
    953                          (and (not timezone)
    954                               (equal org-icalendar-date-time-format
    955                                      ":%Y%m%dT%H%M%S")))
    956                         (encoded
    957                          (org-encode-time
    958                           0
    959                           (or (org-element-property :minute-start repeat-until)
    960                               0)
    961                           (or (org-element-property :hour-start repeat-until)
    962                               0)
    963                           (org-element-property :day-start repeat-until)
    964                           (org-element-property :month-start repeat-until)
    965                           (org-element-property :year-start repeat-until))))
    966                    (concat ";UNTIL="
    967                            (cond
    968                             ((not start-time)
    969                              (format-time-string "%Y%m%d" encoded))
    970                             (local-time-p
    971                              (format-time-string "%Y%m%dT%H%M%S" encoded))
    972                             ((format-time-string "%Y%m%dT%H%M%SZ"
    973                                                  encoded t))))))
    974                "\n")))
    975 	    "SUMMARY:" summary "\n"
    976 	    (and (org-string-nw-p location) (format "LOCATION:%s\n" location))
    977 	    (and (org-string-nw-p class) (format "CLASS:%s\n" class))
    978 	    (and (org-string-nw-p description)
    979 		 (format "DESCRIPTION:%s\n" description))
    980 	    "CATEGORIES:" categories "\n"
    981 	    "SEQUENCE:1\n"
    982 	    (format "PRIORITY:%d\n"
    983 		    (let ((pri (or (org-element-property :priority entry)
    984 				   org-priority-default)))
    985 		      (floor (- 9 (* 8. (/ (float (- org-priority-lowest pri))
    986 					   (- org-priority-lowest
    987 					      org-priority-highest)))))))
    988 	    (format "STATUS:%s\n"
    989 		    (if (eq (org-element-property :todo-type entry) 'todo)
    990 			"NEEDS-ACTION"
    991 		      "COMPLETED"))
    992 	    "END:VTODO\n")))
    993 
    994 (defun org-icalendar--valarm (entry timestamp summary)
    995   "Create a VALARM component.
    996 
    997 ENTRY is the calendar entry triggering the alarm.  TIMESTAMP is
    998 the start date-time of the entry.  SUMMARY defines a short
    999 summary or subject for the task.
   1000 
   1001 Return VALARM component as a string, or nil if it isn't allowed."
   1002   ;; Create a VALARM entry if the entry is timed.  This is not very
   1003   ;; general in that:
   1004   ;; (a) only one alarm per entry is defined,
   1005   ;; (b) only minutes are allowed for the trigger period ahead of the
   1006   ;;     start time,
   1007   ;; (c) only a DISPLAY action is defined.                       [ESF]
   1008   (let ((alarm-time
   1009 	 (let ((warntime
   1010 		(org-element-property :APPT_WARNTIME entry)))
   1011 	   (if warntime (string-to-number warntime) nil))))
   1012     (and (or (and alarm-time
   1013 		  (> alarm-time 0))
   1014 	     (> org-icalendar-alarm-time 0)
   1015 	     org-icalendar-force-alarm)
   1016 	 (org-element-property :hour-start timestamp)
   1017 	 (format "BEGIN:VALARM
   1018 ACTION:DISPLAY
   1019 DESCRIPTION:%s
   1020 TRIGGER:-P0DT0H%dM0S
   1021 END:VALARM\n"
   1022 		 summary
   1023                  (cond
   1024                   ((and alarm-time org-icalendar-force-alarm) alarm-time)
   1025                   ((and alarm-time (not (zerop alarm-time))) alarm-time)
   1026                   (t org-icalendar-alarm-time))))))
   1027 
   1028 ;;;; Template
   1029 
   1030 (defun org-icalendar-inner-template (contents _)
   1031   "Return document body string after iCalendar conversion.
   1032 CONTENTS is the transcoded contents string."
   1033   contents)
   1034 
   1035 (defun org-icalendar-template (contents info)
   1036   "Return complete document string after iCalendar conversion.
   1037 CONTENTS is the transcoded contents string.  INFO is a plist used
   1038 as a communication channel."
   1039   (org-icalendar--vcalendar
   1040    ;; Name.
   1041    (if (not (plist-get info :input-file)) (buffer-name (buffer-base-buffer))
   1042      (file-name-nondirectory
   1043       (file-name-sans-extension (plist-get info :input-file))))
   1044    ;; Owner.
   1045    (if (not (plist-get info :with-author)) ""
   1046      (org-export-data (plist-get info :author) info))
   1047    ;; Timezone.
   1048    (or (org-string-nw-p org-icalendar-timezone) (format-time-string "%Z"))
   1049    ;; Description.
   1050    (org-export-data (plist-get info :title) info)
   1051    ;; TTL
   1052    (plist-get info :icalendar-ttl)
   1053    contents))
   1054 
   1055 (defun org-icalendar--vcalendar (name owner tz description ttl contents)
   1056   "Create a VCALENDAR component.
   1057 NAME, OWNER, TZ, DESCRIPTION, TTL and CONTENTS are all strings giving,
   1058 respectively, the name of the calendar, its owner, the timezone
   1059 used, a short description, time to live (refresh period) and
   1060 the other components included."
   1061   (org-icalendar-fold-string
   1062    (concat (format "BEGIN:VCALENDAR
   1063 VERSION:2.0
   1064 X-WR-CALNAME:%s
   1065 PRODID:-//%s//Emacs with Org mode//EN
   1066 X-WR-TIMEZONE:%s
   1067 X-WR-CALDESC:%s\n"
   1068 		   (org-icalendar-cleanup-string name)
   1069 		   (org-icalendar-cleanup-string owner)
   1070 		   (org-icalendar-cleanup-string tz)
   1071 		   (org-icalendar-cleanup-string description))
   1072            (when ttl (format "X-PUBLISHED-TTL:%s\n"
   1073                              (org-icalendar-cleanup-string ttl)))
   1074            "CALSCALE:GREGORIAN\n"
   1075 	   contents
   1076 	   "END:VCALENDAR\n")))
   1077 
   1078 
   1079 
   1080 ;;; Interactive Functions
   1081 
   1082 ;;;###autoload
   1083 (defun org-icalendar-export-to-ics
   1084     (&optional async subtreep visible-only body-only)
   1085   "Export current buffer to an iCalendar file.
   1086 
   1087 If narrowing is active in the current buffer, only export its
   1088 narrowed part.
   1089 
   1090 If a region is active, export that region.
   1091 
   1092 A non-nil optional argument ASYNC means the process should happen
   1093 asynchronously.  The resulting file should be accessible through
   1094 the `org-export-stack' interface.
   1095 
   1096 When optional argument SUBTREEP is non-nil, export the sub-tree
   1097 at point, extracting information from the headline properties
   1098 first.
   1099 
   1100 When optional argument VISIBLE-ONLY is non-nil, don't export
   1101 contents of hidden elements.
   1102 
   1103 When optional argument BODY-ONLY is non-nil, only write code
   1104 between \"BEGIN:VCALENDAR\" and \"END:VCALENDAR\".
   1105 
   1106 Return ICS file name."
   1107   (interactive)
   1108   (let ((file (buffer-file-name (buffer-base-buffer))))
   1109     (when (and file org-icalendar-store-UID)
   1110       (org-icalendar-create-uid file 'warn-user)))
   1111   ;; Export part.  Since this backend is backed up by `ascii', ensure
   1112   ;; links will not be collected at the end of sections.
   1113   (let ((outfile (org-export-output-file-name ".ics" subtreep)))
   1114     (org-export-to-file 'icalendar outfile
   1115       async subtreep visible-only body-only
   1116       '(:ascii-charset utf-8 :ascii-links-to-notes nil)
   1117       #'org-icalendar--post-process-file)))
   1118 
   1119 ;;;###autoload
   1120 (defun org-icalendar-export-agenda-files (&optional async)
   1121   "Export all agenda files to iCalendar files.
   1122 When optional argument ASYNC is non-nil, export happens in an
   1123 external process."
   1124   (interactive)
   1125   (if async
   1126       ;; Asynchronous export is not interactive, so we will not call
   1127       ;; `org-check-agenda-file'.  Instead we remove any non-existent
   1128       ;; agenda file from the list.
   1129       (let ((files (cl-remove-if-not #'file-exists-p (org-agenda-files t))))
   1130 	(org-export-async-start
   1131 	    (lambda (results)
   1132 	      (dolist (f results) (org-export-add-to-stack f 'icalendar)))
   1133 	  `(let (output-files)
   1134 	     (dolist (file ',files outputfiles)
   1135 	       (with-current-buffer (org-get-agenda-file-buffer file)
   1136 		 (push (expand-file-name (org-icalendar-export-to-ics))
   1137 		       output-files))))))
   1138     (let ((files (org-agenda-files t)))
   1139       (org-agenda-prepare-buffers files)
   1140       (unwind-protect
   1141 	  (dolist (file files)
   1142 	    (catch 'nextfile
   1143 	      (org-check-agenda-file file)
   1144 	      (with-current-buffer (org-get-agenda-file-buffer file)
   1145 		(condition-case err
   1146                     (org-icalendar-export-to-ics)
   1147                   (error
   1148                    (warn "Exporting %s to icalendar failed: %s"
   1149                          file
   1150                          (error-message-string err))
   1151                    (signal (car err) (cdr err)))))))
   1152 	(org-release-buffers org-agenda-new-buffers)))))
   1153 
   1154 ;;;###autoload
   1155 (defun org-icalendar-combine-agenda-files (&optional async)
   1156   "Combine all agenda files into a single iCalendar file.
   1157 
   1158 A non-nil optional argument ASYNC means the process should happen
   1159 asynchronously.  The resulting file should be accessible through
   1160 the `org-export-stack' interface.
   1161 
   1162 The file is stored under the name chosen in
   1163 `org-icalendar-combined-agenda-file'."
   1164   (interactive)
   1165   (if async
   1166       (let ((files (cl-remove-if-not #'file-exists-p (org-agenda-files t))))
   1167 	(org-export-async-start
   1168 	    (lambda (_)
   1169 	      (org-export-add-to-stack
   1170 	       (expand-file-name org-icalendar-combined-agenda-file)
   1171 	       'icalendar))
   1172 	  `(apply #'org-icalendar--combine-files ',files)))
   1173     (apply #'org-icalendar--combine-files (org-agenda-files t))))
   1174 
   1175 (defun org-icalendar-export-current-agenda (file)
   1176   "Export current agenda view to an iCalendar FILE.
   1177 This function assumes major mode for current buffer is
   1178 `org-agenda-mode'."
   1179   (let* ((org-export-use-babel)		;don't evaluate Babel blocks
   1180 	 (contents
   1181 	  (org-export-string-as
   1182 	   (with-output-to-string
   1183 	     (save-excursion
   1184 	       (let ((p (point-min))
   1185 		     (seen nil))	;prevent duplicates
   1186 		 (while (setq p (next-single-property-change p 'org-hd-marker))
   1187 		   (let ((m (get-text-property p 'org-hd-marker)))
   1188 		     (when (and m (not (member m seen)))
   1189 		       (push m seen)
   1190 		       (with-current-buffer (marker-buffer m)
   1191 			 (org-with-wide-buffer
   1192 			  (goto-char (marker-position m))
   1193 			  (princ
   1194 			   (org-element-normalize-string
   1195 			    (buffer-substring (point)
   1196 					      (org-entry-end-position))))))))
   1197 		   (forward-line)))))
   1198 	   'icalendar t
   1199 	   '(:ascii-charset utf-8 :ascii-links-to-notes nil
   1200 			    :icalendar-include-todo all))))
   1201     (with-temp-file file
   1202       (insert
   1203        (org-icalendar--vcalendar
   1204 	org-icalendar-combined-name
   1205 	user-full-name
   1206 	(or (org-string-nw-p org-icalendar-timezone) (format-time-string "%Z"))
   1207 	org-icalendar-combined-description
   1208 	org-icalendar-ttl
   1209 	contents)))
   1210     (org-icalendar--post-process-file file)))
   1211 
   1212 (defun org-icalendar--combine-files (&rest files)
   1213   "Combine entries from multiple files into an iCalendar file.
   1214 FILES is a list of files to build the calendar from."
   1215   ;; At the end of the process, all buffers related to FILES are going
   1216   ;; to be killed.  Make sure to only kill the ones opened in the
   1217   ;; process.
   1218   (let ((org-agenda-new-buffers nil))
   1219     (unwind-protect
   1220 	(progn
   1221 	  (with-temp-file org-icalendar-combined-agenda-file
   1222 	    (insert
   1223 	     (org-icalendar--vcalendar
   1224 	      ;; Name.
   1225 	      org-icalendar-combined-name
   1226 	      ;; Owner.
   1227 	      user-full-name
   1228 	      ;; Timezone.
   1229 	      (or (org-string-nw-p org-icalendar-timezone)
   1230 		  (format-time-string "%Z"))
   1231 	      ;; Description.
   1232 	      org-icalendar-combined-description
   1233 	      ;; TTL (Refresh period)
   1234 	      org-icalendar-ttl
   1235 	      ;; Contents.
   1236 	      (concat
   1237 	       ;; Agenda contents.
   1238 	       (mapconcat
   1239 		(lambda (file)
   1240 		  (catch 'nextfile
   1241 		    (org-check-agenda-file file)
   1242 		    (with-current-buffer (org-get-agenda-file-buffer file)
   1243 		      ;; Create ID if necessary.
   1244 		      (when org-icalendar-store-UID
   1245 			(org-icalendar-create-uid file t))
   1246 		      (org-export-as
   1247 		       'icalendar nil nil t
   1248 		       '(:ascii-charset utf-8 :ascii-links-to-notes nil)))))
   1249 		files "")
   1250 	       ;; BBDB anniversaries.
   1251 	       (when (and org-icalendar-include-bbdb-anniversaries
   1252 			  (require 'ol-bbdb nil t))
   1253 		 (with-output-to-string (org-bbdb-anniv-export-ical)))))))
   1254 	  (org-icalendar--post-process-file org-icalendar-combined-agenda-file))
   1255       (org-release-buffers org-agenda-new-buffers))))
   1256 
   1257 
   1258 (provide 'ox-icalendar)
   1259 
   1260 ;; Local variables:
   1261 ;; generated-autoload-file: "org-loaddefs.el"
   1262 ;; End:
   1263 
   1264 ;;; ox-icalendar.el ends here