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