Merge upstream Org (from commit acbbe2)
[bpt/emacs.git] / lisp / org / org-icalendar.el
index 1c4d7d6..a2d2117 100644 (file)
@@ -1,12 +1,10 @@
 ;;; 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: 7.01
 ;;
 ;; This file is part of GNU Emacs.
 ;;
@@ -30,8 +28,7 @@
 
 (require 'org-exp)
 
-(eval-when-compile
-  (require 'cl))
+(eval-when-compile (require 'cl))
 
 (declare-function org-bbdb-anniv-export-ical "org-bbdb" nil)
 
@@ -47,14 +44,27 @@ 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
+  :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 representing all agenda files."
+  "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
@@ -62,6 +72,12 @@ The file name should be absolute, the file will be overwritten without warning."
   :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:
@@ -177,12 +193,39 @@ or if they are only using it locally."
 
 (defcustom org-icalendar-timezone (getenv "TZ")
   "The time zone string for iCalendar export.
-When nil of the empty string, use the abbreviation retrieved from Emacs."
+When nil or the empty string, use output from \(current-time-zone\)."
   :group 'org-export-icalendar
   :type '(choice
          (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
@@ -213,7 +256,7 @@ The file is stored under the name `org-combined-agenda-icalendar-file'."
 If COMBINE is non-nil, combine all calendar entries into a single large
 file and store it under the name `org-combined-agenda-icalendar-file'."
   (save-excursion
-    (org-prepare-agenda-buffers files)
+    (org-agenda-prepare-buffers files)
     (let* ((dir (org-export-directory
                 :ical (list :publishing-directory
                             org-export-publishing-directory)))
@@ -244,20 +287,19 @@ file and store it under the name `org-combined-agenda-icalendar-file'."
          (let ((standard-output ical-buffer))
            (if combine
                (and (not started) (setq started t)
-                    (org-start-icalendar-file org-icalendar-combined-name))
-             (org-start-icalendar-file category))
-           (org-print-icalendar-entries combine)
+                    (org-icalendar-start-file org-icalendar-combined-name))
+             (org-icalendar-start-file category))
+           (org-icalendar-print-entries combine)
            (when (or (and combine (not files)) (not combine))
              (when (and combine org-icalendar-include-bbdb-anniversaries)
                (require 'org-bbdb)
                (org-bbdb-anniv-export-ical))
-             (org-finish-icalendar-file)
+             (org-icalendar-finish-file)
              (set-buffer ical-buffer)
              (run-hooks 'org-before-save-iCalendar-file-hook)
              (save-buffer)
              (run-hooks 'org-after-save-iCalendar-file-hook)
-             (and (boundp 'org-wait) (numberp org-wait) (sit-for org-wait))
-             ))))
+             (and (boundp 'org-wait) (numberp org-wait) (sit-for org-wait))))))
       (org-release-buffers org-agenda-new-buffers))))
 
 (defvar org-before-save-iCalendar-file-hook nil
@@ -271,18 +313,18 @@ A good way to use this is to tell a desktop calendar application to re-read
 the iCalendar file.")
 
 (defvar org-agenda-default-appointment-duration) ; defined in org-agenda.el
-(defun org-print-icalendar-entries (&optional combine)
+(defun org-icalendar-print-entries (&optional combine)
   "Print iCalendar entries for the current Org-mode file to `standard-output'.
 When COMBINE is non nil, add the category to each line."
   (require 'org-agenda)
   (let ((re1 (concat org-ts-regexp "\\|<%%([^>\n]+>"))
        (re2 (concat "--?-?\\(" org-ts-regexp "\\)"))
-       (dts (org-ical-ts-to-string
+       (dts (org-icalendar-ts-to-string
              (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 alarm-time
        (sexp-buffer (get-buffer-create "*ical-tmp*")))
     (org-refresh-category-properties)
     (save-excursion
@@ -297,10 +339,11 @@ When COMBINE is non nil, add the category to each line."
              (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"))
@@ -314,28 +357,33 @@ 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-time (org-entry-get nil "APPT_WARNTIME")
+               alarm-time (if alarm-time (string-to-number alarm-time) 0)
+               alarm ""
                deadlinep nil scheduledp nil)
+         (setq tmp (buffer-substring (max (point-min) (- pos org-ds-keyword-length)) pos)
+               deadlinep (string-match org-deadline-regexp tmp)
+               scheduledp (string-match org-scheduled-regexp tmp)
+               todo (org-get-todo-state))
+         ;; donep (org-entry-is-done-p)
          (if (looking-at re2)
              (progn
                (goto-char (match-end 0))
                (setq ts2 (match-string 1)
                      inc (not (string-match "[0-9]\\{1,2\\}:[0-9][0-9]" ts2))))
-           (setq tmp (buffer-substring (max (point-min)
-                                            (- pos org-ds-keyword-length))
-                                       pos)
-                 ts2 (if (string-match "[0-9]\\{1,2\\}:[0-9][0-9]-\\([0-9]\\{1,2\\}:[0-9][0-9]\\)" ts)
+           (setq ts2 (if (string-match "[0-9]\\{1,2\\}:[0-9][0-9]-\\([0-9]\\{1,2\\}:[0-9][0-9]\\)" ts)
                          (progn
                            (setq inc nil)
                            (replace-match "\\1" t nil ts))
-                       ts)
-                 deadlinep (string-match org-deadline-regexp tmp)
-                 scheduledp (string-match org-scheduled-regexp tmp)
-                 todo (org-get-todo-state)
-                 ;; donep (org-entry-is-done-p)
-                 ))
+                       ts)))
          (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
@@ -352,16 +400,27 @@ When COMBINE is non nil, add the category to each line."
          (if (or (string-match org-tr-regexp hd)
                  (string-match org-ts-regexp hd))
              (setq hd (replace-match "" t t hd)))
-         (if (string-match "\\+\\([0-9]+\\)\\([dwmy]\\)>" ts)
+         (if (string-match "\\+\\([0-9]+\\)\\([hdwmy]\\)>" ts)
              (setq rrule
                    (concat "\nRRULE:FREQ="
                            (cdr (assoc
                                  (match-string 2 ts)
-                                 '(("d" . "DAILY")("w" . "WEEKLY")
+                                 '(("h" . "HOURLY")("d" . "DAILY")("w" . "WEEKLY")
                                    ("m" . "MONTHLY")("y" . "YEARLY"))))
                            ";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 (or (> alarm-time 0) (> 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 (or alarm-time org-icalendar-alarm-time)))
+             (setq alarm "")))
          (if (string-match org-bracket-link-regexp summary)
              (setq summary
                    (replace-match (if (match-end 3)
@@ -372,23 +431,27 @@ When COMBINE is non nil, add the category to each line."
          (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")
-                          (org-ical-ts-to-string ts2 "DTEND" inc)
+                          (org-icalendar-ts-to-string ts "DTSTART")
+                          (org-icalendar-ts-to-string ts2 "DTEND" inc)
                           rrule summary
                           (if (and desc (string-match "\\S-" desc))
                               (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))
@@ -415,7 +478,7 @@ 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 org-icalendar-verify-function
@@ -447,7 +510,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
@@ -459,13 +522,13 @@ END:VEVENT\n"
                    due (and (member 'todo-due org-icalendar-use-deadline)
                             (org-entry-get nil "DEADLINE"))
                    start (and (member 'todo-start org-icalendar-use-scheduled)
-                            (org-entry-get nil "SCHEDULED"))
+                              (org-entry-get nil "SCHEDULED"))
                    categories (org-export-get-categories)
                    uid (if org-icalendar-store-UID
                            (org-id-get-create)
                          (or (org-id-get) (org-id-new))))
-             (and due (setq due (org-ical-ts-to-string due "DUE")))
-             (and start (setq start (org-ical-ts-to-string start "DTSTART")))
+             (and due (setq due (org-icalendar-ts-to-string due "DUE")))
+             (and start (setq start (org-icalendar-ts-to-string start "DTSTART")))
 
              (if (string-match org-bracket-link-regexp hd)
                  (setq hd (replace-match (if (match-end 3) (match-string 3 hd)
@@ -522,10 +585,10 @@ characters."
   (if (not s)
       nil
     (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))))
+       (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))))
       (setq s (replace-regexp-in-string "[[:space:]]+" " " s)))
     (let ((start 0))
       (while (string-match "\\([,;]\\)" s start)
@@ -568,7 +631,7 @@ not used right now."
     (when (string-match "[;,:]" s) (setq s (concat "\"" s "\"")))
     s))
 
-(defun org-start-icalendar-file (name)
+(defun org-icalendar-start-file (name)
   "Start an iCalendar file by inserting the header."
   (let ((user user-full-name)
        (name (or name "unknown"))
@@ -585,11 +648,11 @@ X-WR-TIMEZONE:%s
 X-WR-CALDESC:%s
 CALSCALE:GREGORIAN\n" name user timezone description))))
 
-(defun org-finish-icalendar-file ()
+(defun org-icalendar-finish-file ()
   "Finish an iCalendar file by inserting the END statement."
   (princ "END:VCALENDAR\n"))
 
-(defun org-ical-ts-to-string (s keyword &optional inc)
+(defun org-icalendar-ts-to-string (s keyword &optional inc)
   "Take a time string S and convert it to iCalendar format.
 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
@@ -610,10 +673,19 @@ a time), or the day by one (if it does not contain a time)."
                (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)))))
+      (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
+;; Local variables:
+;; generated-autoload-file: "org-loaddefs.el"
+;; End:
+
 ;;; org-icalendar.el ends here