;;; org-icalendar.el --- iCalendar export for Org-mode
-;; Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010
-;; Free Software Foundation, Inc.
+;; Copyright (C) 2004-2012 Free Software Foundation, Inc.
;; Author: Carsten Dominik <carsten at orgmode dot org>
;; Keywords: outlines, hypermedia, calendar, wp
;; Homepage: http://orgmode.org
-;; Version: 6.35i
;;
;; This file is part of GNU Emacs.
;;
;;
;;; Commentary:
+;;; Code:
+
(require 'org-exp)
+(eval-when-compile
+ (require 'cl))
+
(declare-function org-bbdb-anniv-export-ical "org-bbdb" nil)
(defgroup org-export-icalendar nil
:group 'org-export-icalendar
:type 'file)
+(defcustom org-icalendar-alarm-time 0
+ "Number of minutes for triggering an alarm for exported timed events.
+A zero value (the default) turns off the definition of an alarm trigger
+for timed events. If non-zero, alarms are created.
+
+- a single alarm per entry is defined
+- The alarm will go off N minutes before the event
+- only a DISPLAY action is defined."
+ :group 'org-export-icalendar
+ :version "24.1"
+ :type 'integer)
+
(defcustom org-icalendar-combined-name "OrgMode"
"Calendar name for the combined iCalendar representing all agenda files."
:group 'org-export-icalendar
:type 'string)
+(defcustom org-icalendar-combined-description nil
+ "Calendar description for the combined iCalendar (all agenda files)."
+ :group 'org-export-icalendar
+ :version "24.1"
+ :type 'string)
+
(defcustom org-icalendar-use-plain-timestamp t
"Non-nil means make an event from every plain time stamp."
:group 'org-export-icalendar
:type 'boolean)
+(defcustom org-icalendar-honor-noexport-tag nil
+ "Non-nil means don't export entries with a tag in `org-export-exclude-tags'."
+ :group 'org-export-icalendar
+ :version "24.1"
+ :type 'boolean)
+
(defcustom org-icalendar-use-deadline '(event-if-not-todo todo-due)
"Contexts where iCalendar export should use a deadline time stamp.
This is a list with several symbols in it. Valid symbol are:
Org will create these identifiers as needed. When this variable is non-nil,
the created UIDs will be stored in the ID property of the entry. Then the
next time this entry is exported, it will be exported with the same UID,
-superceding the previous form of it. This is essential for
+superseding the previous form of it. This is essential for
synchronization services.
This variable is not turned on by default because we want to avoid creating
a property drawer in every entry if people are only playing with this feature,
(const :tag "Unspecified" nil)
(string :tag "Time zone")))
+;; Backward compatibility with previous variable
+(defvar org-icalendar-use-UTC-date-time nil)
+(defcustom org-icalendar-date-time-format
+ (if org-icalendar-use-UTC-date-time
+ ":%Y%m%dT%H%M%SZ"
+ ":%Y%m%dT%H%M%S")
+ "Format-string for exporting icalendar DATE-TIME.
+See `format-time-string' for a full documentation. The only
+difference is that `org-icalendar-timezone' is used for %Z.
+
+Interesting value are:
+ - \":%Y%m%dT%H%M%S\" for local time
+ - \";TZID=%Z:%Y%m%dT%H%M%S\" for local time with explicit timezone
+ - \":%Y%m%dT%H%M%SZ\" for time expressed in Universal Time"
+
+ :group 'org-export-icalendar
+ :version "24.1"
+ :type '(choice
+ (const :tag "Local time" ":%Y%m%dT%H%M%S")
+ (const :tag "Explicit local time" ";TZID=%Z:%Y%m%dT%H%M%S")
+ (const :tag "Universal time" ":%Y%m%dT%H%M%SZ")
+ (string :tag "Explicit format")))
+
+(defun org-icalendar-use-UTC-date-timep ()
+ (char-equal (elt org-icalendar-date-time-format
+ (1- (length org-icalendar-date-time-format))) ?Z))
+
;;; iCalendar export
;;;###autoload
;;;###autoload
(defun org-export-icalendar-all-agenda-files ()
- "Export all files in `org-agenda-files' to iCalendar .ics files.
+ "Export all files in the variable `org-agenda-files' to iCalendar .ics files.
Each iCalendar file will be located in the same directory as the Org-mode
file, but with extension `.ics'."
(interactive)
(format-time-string (cdr org-time-stamp-formats) (current-time))
"DTSTART"))
hd ts ts2 state status (inc t) pos b sexp rrule
- scheduledp deadlinep todo prefix due start
- tmp pri categories location summary desc uid
+ scheduledp deadlinep todo prefix due start tags
+ tmp pri categories location summary desc uid alarm
(sexp-buffer (get-buffer-create "*ical-tmp*")))
(org-refresh-category-properties)
(save-excursion
(throw :skip nil)))
(setq pos (match-beginning 0)
ts (match-string 0)
+ tags (org-get-tags-at)
inc t
hd (condition-case nil
(org-icalendar-cleanup-string
- (org-get-heading))
+ (org-get-heading t))
(error (throw :skip nil)))
summary (org-icalendar-cleanup-string
(org-entry-get nil "SUMMARY"))
(org-id-get-create)
(or (org-id-get) (org-id-new)))
categories (org-export-get-categories)
+ alarm ""
deadlinep nil scheduledp nil)
(if (looking-at re2)
(progn
(when (and (not org-icalendar-use-plain-timestamp)
(not deadlinep) (not scheduledp))
(throw :skip t))
+ ;; don't export entries with a :noexport: tag
+ (when (and org-icalendar-honor-noexport-tag
+ (delq nil (mapcar (lambda(x)
+ (member x org-export-exclude-tags)) tags)))
+ (throw :skip t))
(when (and
deadlinep
(if todo
";INTERVAL=" (match-string 1 ts)))
(setq rrule ""))
(setq summary (or summary hd))
+ ;; create an alarm entry if the entry is timed. this is not very general in that:
+ ;; (a) only one alarm per entry is defined,
+ ;; (b) only minutes are allowed for the trigger period ahead of the start time, and
+ ;; (c) only a DISPLAY action is defined.
+ ;; [ESF]
+ (let ((t1 (ignore-errors (org-parse-time-string ts 'nodefault))))
+ (if (and (> org-icalendar-alarm-time 0)
+ (car t1) (nth 1 t1) (nth 2 t1))
+ (setq alarm (format "\nBEGIN:VALARM\nACTION:DISPLAY\nDESCRIPTION:%s\nTRIGGER:-P0DT0H%dM0S\nEND:VALARM" summary org-icalendar-alarm-time))
+ (setq alarm ""))
+ )
(if (string-match org-bracket-link-regexp summary)
(setq summary
(replace-match (if (match-end 3)
(if scheduledp (setq summary (concat "S: " summary)))
(if (string-match "\\`<%%" ts)
(with-current-buffer sexp-buffer
- (insert (substring ts 1 -1) " " summary "\n"))
+ (let ((entry (substring ts 1 -1)))
+ (put-text-property 0 1 'uid
+ (concat " " prefix uid) entry)
+ (insert entry " " summary "\n")))
(princ (format "BEGIN:VEVENT
UID: %s
%s
%s%s
SUMMARY:%s%s%s
-CATEGORIES:%s
+CATEGORIES:%s%s
END:VEVENT\n"
(concat prefix uid)
(org-ical-ts-to-string ts "DTSTART")
(concat "\nDESCRIPTION: " desc) "")
(if (and location (string-match "\\S-" location))
(concat "\nLOCATION: " location) "")
- categories)))))
+ categories
+ alarm)))))
(when (and org-icalendar-include-sexps
(condition-case nil (require 'icalendar) (error nil))
(fboundp 'icalendar-export-region))
(when org-icalendar-include-todo
(setq prefix "TODO-")
(goto-char (point-min))
- (while (re-search-forward org-todo-line-regexp nil t)
+ (while (re-search-forward org-complex-heading-regexp nil t)
(catch :skip
(org-agenda-skip)
(when org-icalendar-verify-function
((eq org-icalendar-include-todo t)
;; include everything that is not done
(member state org-not-done-keywords))))
- (setq hd (match-string 3)
+ (setq hd (match-string 4)
summary (org-icalendar-cleanup-string
(org-entry-get nil "SUMMARY"))
desc (org-icalendar-cleanup-string
characters."
(if (not s)
nil
- (when is-body
+ (if is-body
(let ((re (concat "\\(" org-drawer-regexp "\\)[^\000]*?:END:.*\n?"))
(re2 (concat "^[ \t]*" org-keyword-time-regexp ".*\n?")))
(while (string-match re s) (setq s (replace-match "" t t s)))
- (while (string-match re2 s) (setq s (replace-match "" t t s)))))
+ (while (string-match re2 s) (setq s (replace-match "" t t s))))
+ (setq s (replace-regexp-in-string "[[:space:]]+" " " s)))
(let ((start 0))
(while (string-match "\\([,;]\\)" s start)
(setq start (+ (match-beginning 0) 2)
(name (or name "unknown"))
(timezone (if (> (length org-icalendar-timezone) 0)
org-icalendar-timezone
- (cadr (current-time-zone)))))
+ (cadr (current-time-zone))))
+ (description org-icalendar-combined-description))
(princ
(format "BEGIN:VCALENDAR
VERSION:2.0
X-WR-CALNAME:%s
PRODID:-//%s//Emacs with Org-mode//EN
X-WR-TIMEZONE:%s
-CALSCALE:GREGORIAN\n" name user timezone))))
+X-WR-CALDESC:%s
+CALSCALE:GREGORIAN\n" name user timezone description))))
(defun org-finish-icalendar-file ()
"Finish an iCalendar file by inserting the END statement."
KEYWORD is added in front, to make a complete line like DTSTART....
When INC is non-nil, increase the hour by two (if time string contains
a time), or the day by one (if it does not contain a time)."
- (let ((t1 (org-parse-time-string s 'nodefault))
+ (let ((t1 (ignore-errors (org-parse-time-string s 'nodefault)))
t2 fmt have-time time)
- (if (and (car t1) (nth 1 t1) (nth 2 t1))
- (setq t2 t1 have-time t)
- (setq t2 (org-parse-time-string s)))
- (let ((s (car t2)) (mi (nth 1 t2)) (h (nth 2 t2))
- (d (nth 3 t2)) (m (nth 4 t2)) (y (nth 5 t2)))
- (when inc
- (if have-time
- (if org-agenda-default-appointment-duration
- (setq mi (+ org-agenda-default-appointment-duration mi))
- (setq h (+ 2 h)))
- (setq d (1+ d))))
- (setq time (encode-time s mi h d m y)))
- (setq fmt (if have-time ":%Y%m%dT%H%M%S" ";VALUE=DATE:%Y%m%d"))
- (concat keyword (format-time-string fmt time))))
+ (if (not t1)
+ ""
+ (if (and (car t1) (nth 1 t1) (nth 2 t1))
+ (setq t2 t1 have-time t)
+ (setq t2 (org-parse-time-string s)))
+ (let ((s (car t2)) (mi (nth 1 t2)) (h (nth 2 t2))
+ (d (nth 3 t2)) (m (nth 4 t2)) (y (nth 5 t2)))
+ (when inc
+ (if have-time
+ (if org-agenda-default-appointment-duration
+ (setq mi (+ org-agenda-default-appointment-duration mi))
+ (setq h (+ 2 h)))
+ (setq d (1+ d))))
+ (setq time (encode-time s mi h d m y)))
+ (setq fmt (if have-time
+ (replace-regexp-in-string "%Z"
+ org-icalendar-timezone
+ org-icalendar-date-time-format)
+ ";VALUE=DATE:%Y%m%d"))
+ (concat keyword (format-time-string fmt time
+ (and (org-icalendar-use-UTC-date-timep)
+ have-time))))))
(provide 'org-icalendar)
-;; arch-tag: 2dee2b6e-9211-4aee-8a47-a3c7e5bc30cf
;;; org-icalendar.el ends here