Convert consecutive FSF copyright years to ranges.
[bpt/emacs.git] / lisp / org / org-icalendar.el
index 23e739f..52b7f79 100644 (file)
@@ -1,12 +1,12 @@
 ;;; org-icalendar.el --- iCalendar export for Org-mode
 
-;; Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009
+;; Copyright (C) 2004-2011
 ;;   Free Software Foundation, Inc.
 
 ;; Author: Carsten Dominik <carsten at orgmode dot org>
 ;; Keywords: outlines, hypermedia, calendar, wp
 ;; Homepage: http://orgmode.org
-;; Version: 6.31a
+;; Version: 7.4
 ;;
 ;; 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
@@ -42,11 +47,32 @@ The file name should be absolute, the file will be overwritten without warning."
   :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
+  :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
+  :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-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:
@@ -99,11 +125,11 @@ all-tags    All tags, including inherited ones."
           (const :tag "All tags, including inherited ones" all-tags))))
 
 (defcustom org-icalendar-include-todo nil
-  "Non-nil means, export to iCalendar files should also cover TODO items.
+  "Non-nil means export to iCalendar files should also cover TODO items.
 Valid values are:
-nil         don't inlcude any TODO items
+nil         don't include any TODO items
 t           include all TODO items that are not in a DONE state
-unblocked   include all TODO idems that are not blocked
+unblocked   include all TODO items that are not blocked
 all         include both done and not done items."
   :group 'org-export-icalendar
   :type '(choice
@@ -112,14 +138,25 @@ all         include both done and not done items."
          (const :tag "Unblocked" unblocked)
          (const :tag "All" all)))
 
+(defvar org-icalendar-verify-function nil
+  "Function to verify entries for iCalendar export.
+This can be set to a function that will be called at each entry that
+is considered for export to iCalendar.  When the function returns nil,
+the entry will be skipped.  When it returns a non-nil value, the entry
+will be considered for export.
+This is used internally when an agenda buffer is exported to an ics file,
+to make sure that only entries currently listed in the agenda will end
+up in the ics file.  But for normal iCalendar export, you can use this
+for whatever you need.")
+
 (defcustom org-icalendar-include-bbdb-anniversaries nil
-  "Non-nil means, a combined iCalendar files should include anniversaries.
+  "Non-nil means a combined iCalendar files should include anniversaries.
 The anniversaries are define in the BBDB database."
   :group 'org-export-icalendar
   :type 'boolean)
 
 (defcustom org-icalendar-include-sexps t
-  "Non-nil means, export to iCalendar files should also cover sexp entries.
+  "Non-nil means export to iCalendar files should also cover sexp entries.
 These are entries like in the diary, but directly in an Org-mode file."
   :group 'org-export-icalendar
   :type 'boolean)
@@ -136,12 +173,12 @@ The text will be inserted into the DESCRIPTION field."
          (integer :tag "Max characters")))
 
 (defcustom org-icalendar-store-UID nil
-  "Non-nil means, store any created UIDs in properties.
+  "Non-nil means store any created UIDs in properties.
 The iCalendar standard requires that all entries have a unique identifier.
 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,
@@ -157,6 +194,13 @@ When nil of the empty string, use the abbreviation retrieved from Emacs."
          (const :tag "Unspecified" nil)
          (string :tag "Time zone")))
 
+(defcustom org-icalendar-use-UTC-date-time ()
+  "Non-nil force the use of the universal time for iCalendar DATE-TIME.
+The iCalendar DATE-TIME can be expressed with local time or universal Time,
+universal time could be more compatible with some external tools."
+  :group 'org-export-icalendar
+  :type 'boolean)
+
 ;;; iCalendar export
 
 ;;;###autoload
@@ -169,7 +213,7 @@ file, but with extension `.ics'."
 
 ;;;###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)
@@ -256,7 +300,7 @@ When COMBINE is non nil, add the category to each line."
              "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
+       tmp pri categories location summary desc uid alarm
        (sexp-buffer (get-buffer-create "*ical-tmp*")))
     (org-refresh-category-properties)
     (save-excursion
@@ -264,8 +308,8 @@ When COMBINE is non nil, add the category to each line."
       (while (re-search-forward re1 nil t)
        (catch :skip
          (org-agenda-skip)
-         (when (boundp 'org-icalendar-verify-function)
-           (unless (funcall org-icalendar-verify-function)
+         (when org-icalendar-verify-function
+           (unless (save-match-data (funcall org-icalendar-verify-function))
              (outline-next-heading)
              (backward-char 1)
              (throw :skip nil)))
@@ -274,7 +318,7 @@ When COMBINE is non nil, add the category to each line."
                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"))
@@ -288,6 +332,7 @@ When COMBINE is non nil, add the category to each line."
                        (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
@@ -307,6 +352,9 @@ When COMBINE is non nil, add the category to each line."
                  todo (org-get-todo-state)
                  ;; donep (org-entry-is-done-p)
                  ))
+         (when (and (not org-icalendar-use-plain-timestamp)
+                    (not deadlinep) (not scheduledp))
+           (throw :skip t))
          (when (and
                 deadlinep
                 (if todo
@@ -333,6 +381,17 @@ When COMBINE is non nil, add the category to each line."
                            ";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:-P0D0H%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)
@@ -349,7 +408,7 @@ 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")
@@ -359,7 +418,8 @@ END:VEVENT\n"
                               (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))
@@ -368,6 +428,11 @@ END:VEVENT\n"
        (while (re-search-forward "^&?%%(" nil t)
          (catch :skip
            (org-agenda-skip)
+           (when org-icalendar-verify-function
+             (unless (save-match-data (funcall org-icalendar-verify-function))
+               (outline-next-heading)
+               (backward-char 1)
+               (throw :skip nil)))
            (setq b (match-beginning 0))
            (goto-char (1- (match-end 0)))
            (forward-sexp 1)
@@ -381,10 +446,10 @@ END:VEVENT\n"
       (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 (boundp 'org-icalendar-verify-function)
+           (when org-icalendar-verify-function
              (unless (save-match-data
                        (funcall org-icalendar-verify-function))
                (outline-next-heading)
@@ -413,7 +478,7 @@ END:VEVENT\n"
                        ((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
@@ -487,11 +552,12 @@ whitespace, newlines, drawers, and timestamps, and cut it down to MAXLENGTH
 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)
@@ -539,14 +605,16 @@ not used right now."
        (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."
@@ -557,24 +625,30 @@ CALSCALE:GREGORIAN\n" name user timezone))))
 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 (if org-icalendar-use-UTC-date-time 
+                                 ":%Y%m%dT%H%M%SZ"
+                                 ":%Y%m%dT%H%M%S")
+                   ";VALUE=DATE:%Y%m%d"))
+      (concat keyword (format-time-string fmt time 
+                                         (and org-icalendar-use-UTC-date-time 
+                                              have-time))))))
 
 (provide 'org-icalendar)
 
-;; arch-tag: 2dee2b6e-9211-4aee-8a47-a3c7e5bc30cf
 ;;; org-icalendar.el ends here