Commit | Line | Data |
---|---|---|
271672fa BG |
1 | ;;; ox-icalendar.el --- iCalendar Back-End for Org Export Engine |
2 | ||
ba318903 | 3 | ;; Copyright (C) 2004-2014 Free Software Foundation, Inc. |
271672fa BG |
4 | |
5 | ;; Author: Carsten Dominik <carsten at orgmode dot org> | |
6 | ;; Nicolas Goaziou <n dot goaziou at gmail dot com> | |
7 | ;; Keywords: outlines, hypermedia, calendar, wp | |
8 | ;; Homepage: http://orgmode.org | |
9 | ||
439d6b1c GM |
10 | ;; This file is part of GNU Emacs. |
11 | ||
271672fa BG |
12 | ;; GNU Emacs is free software: you can redistribute it and/or modify |
13 | ;; it under the terms of the GNU General Public License as published by | |
14 | ;; the Free Software Foundation, either version 3 of the License, or | |
15 | ;; (at your option) any later version. | |
16 | ||
17 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
20 | ;; GNU General Public License for more details. | |
21 | ||
22 | ;; You should have received a copy of the GNU General Public License | |
23 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | |
24 | ||
25 | ;;; Commentary: | |
26 | ;; | |
27 | ;; This library implements an iCalendar back-end for Org generic | |
28 | ;; exporter. See Org manual for more information. | |
29 | ;; | |
30 | ;; It is expected to conform to RFC 5545. | |
31 | ||
32 | ;;; Code: | |
33 | ||
34 | (eval-when-compile (require 'cl)) | |
35 | (require 'ox-ascii) | |
36 | (declare-function org-bbdb-anniv-export-ical "org-bbdb" nil) | |
37 | ||
38 | ||
39 | \f | |
40 | ;;; User-Configurable Variables | |
41 | ||
42 | (defgroup org-export-icalendar nil | |
43 | "Options specific for iCalendar export back-end." | |
44 | :tag "Org Export iCalendar" | |
45 | :group 'org-export) | |
46 | ||
47 | (defcustom org-icalendar-combined-agenda-file "~/org.ics" | |
48 | "The file name for the iCalendar file covering all agenda files. | |
49 | This file is created with the command \\[org-icalendar-combine-agenda-files]. | |
50 | The file name should be absolute. It will be overwritten without warning." | |
51 | :group 'org-export-icalendar | |
52 | :type 'file) | |
53 | ||
54 | (defcustom org-icalendar-alarm-time 0 | |
55 | "Number of minutes for triggering an alarm for exported timed events. | |
56 | ||
57 | A zero value (the default) turns off the definition of an alarm trigger | |
58 | for timed events. If non-zero, alarms are created. | |
59 | ||
60 | - a single alarm per entry is defined | |
61 | - The alarm will go off N minutes before the event | |
62 | - only a DISPLAY action is defined." | |
63 | :group 'org-export-icalendar | |
64 | :version "24.1" | |
65 | :type 'integer) | |
66 | ||
67 | (defcustom org-icalendar-combined-name "OrgMode" | |
68 | "Calendar name for the combined iCalendar representing all agenda files." | |
69 | :group 'org-export-icalendar | |
70 | :type 'string) | |
71 | ||
72 | (defcustom org-icalendar-combined-description "" | |
73 | "Calendar description for the combined iCalendar (all agenda files)." | |
74 | :group 'org-export-icalendar | |
75 | :type 'string) | |
76 | ||
77 | (defcustom org-icalendar-exclude-tags nil | |
78 | "Tags that exclude a tree from export. | |
79 | This variable allows to specify different exclude tags from other | |
80 | back-ends. It can also be set with the ICAL_EXCLUDE_TAGS | |
81 | keyword." | |
82 | :group 'org-export-icalendar | |
83 | :type '(repeat (string :tag "Tag"))) | |
84 | ||
85 | (defcustom org-icalendar-use-deadline '(event-if-not-todo todo-due) | |
86 | "Contexts where iCalendar export should use a deadline time stamp. | |
87 | ||
88 | This is a list with several symbols in it. Valid symbol are: | |
89 | `event-if-todo' Deadlines in TODO entries become calendar events. | |
90 | `event-if-not-todo' Deadlines in non-TODO entries become calendar events. | |
91 | `todo-due' Use deadlines in TODO entries as due-dates" | |
92 | :group 'org-export-icalendar | |
93 | :type '(set :greedy t | |
94 | (const :tag "Deadlines in non-TODO entries become events" | |
95 | event-if-not-todo) | |
96 | (const :tag "Deadline in TODO entries become events" | |
97 | event-if-todo) | |
98 | (const :tag "Deadlines in TODO entries become due-dates" | |
99 | todo-due))) | |
100 | ||
101 | (defcustom org-icalendar-use-scheduled '(todo-start) | |
102 | "Contexts where iCalendar export should use a scheduling time stamp. | |
103 | ||
104 | This is a list with several symbols in it. Valid symbol are: | |
105 | `event-if-todo' Scheduling time stamps in TODO entries become an event. | |
106 | `event-if-not-todo' Scheduling time stamps in non-TODO entries become an event. | |
107 | `todo-start' Scheduling time stamps in TODO entries become start date. | |
108 | Some calendar applications show TODO entries only after | |
109 | that date." | |
110 | :group 'org-export-icalendar | |
111 | :type '(set :greedy t | |
112 | (const :tag | |
113 | "SCHEDULED timestamps in non-TODO entries become events" | |
114 | event-if-not-todo) | |
115 | (const :tag "SCHEDULED timestamps in TODO entries become events" | |
116 | event-if-todo) | |
117 | (const :tag "SCHEDULED in TODO entries become start date" | |
118 | todo-start))) | |
119 | ||
120 | (defcustom org-icalendar-categories '(local-tags category) | |
121 | "Items that should be entered into the \"categories\" field. | |
122 | ||
123 | This is a list of symbols, the following are valid: | |
124 | `category' The Org mode category of the current file or tree | |
125 | `todo-state' The todo state, if any | |
126 | `local-tags' The tags, defined in the current line | |
127 | `all-tags' All tags, including inherited ones." | |
128 | :group 'org-export-icalendar | |
129 | :type '(repeat | |
130 | (choice | |
131 | (const :tag "The file or tree category" category) | |
132 | (const :tag "The TODO state" todo-state) | |
133 | (const :tag "Tags defined in current line" local-tags) | |
134 | (const :tag "All tags, including inherited ones" all-tags)))) | |
135 | ||
136 | (defcustom org-icalendar-with-timestamps 'active | |
137 | "Non-nil means make an event from plain time stamps. | |
138 | ||
139 | It can be set to `active', `inactive', t or nil, in order to make | |
140 | an event from, respectively, only active timestamps, only | |
141 | inactive ones, all of them or none. | |
142 | ||
143 | This variable has precedence over `org-export-with-timestamps'. | |
144 | It can also be set with the #+OPTIONS line, e.g. \"<:t\"." | |
145 | :group 'org-export-icalendar | |
146 | :type '(choice | |
147 | (const :tag "All timestamps" t) | |
148 | (const :tag "Only active timestamps" active) | |
149 | (const :tag "Only inactive timestamps" inactive) | |
150 | (const :tag "No timestamp" nil))) | |
151 | ||
152 | (defcustom org-icalendar-include-todo nil | |
153 | "Non-nil means create VTODO components from TODO items. | |
154 | ||
155 | Valid values are: | |
156 | nil don't include any task. | |
157 | t include tasks that are not in DONE state. | |
158 | `unblocked' include all TODO items that are not blocked. | |
159 | `all' include both done and not done items." | |
160 | :group 'org-export-icalendar | |
161 | :type '(choice | |
162 | (const :tag "None" nil) | |
163 | (const :tag "Unfinished" t) | |
164 | (const :tag "Unblocked" unblocked) | |
165 | (const :tag "All" all) | |
166 | (repeat :tag "Specific TODO keywords" | |
167 | (string :tag "Keyword")))) | |
168 | ||
169 | (defcustom org-icalendar-include-bbdb-anniversaries nil | |
170 | "Non-nil means a combined iCalendar file should include anniversaries. | |
171 | The anniversaries are defined in the BBDB database." | |
172 | :group 'org-export-icalendar | |
173 | :type 'boolean) | |
174 | ||
175 | (defcustom org-icalendar-include-sexps t | |
176 | "Non-nil means export to iCalendar files should also cover sexp entries. | |
177 | These are entries like in the diary, but directly in an Org mode | |
178 | file." | |
179 | :group 'org-export-icalendar | |
180 | :type 'boolean) | |
181 | ||
182 | (defcustom org-icalendar-include-body t | |
183 | "Amount of text below headline to be included in iCalendar export. | |
184 | This is a number of characters that should maximally be included. | |
185 | Properties, scheduling and clocking lines will always be removed. | |
186 | The text will be inserted into the DESCRIPTION field." | |
187 | :group 'org-export-icalendar | |
188 | :type '(choice | |
189 | (const :tag "Nothing" nil) | |
190 | (const :tag "Everything" t) | |
191 | (integer :tag "Max characters"))) | |
192 | ||
193 | (defcustom org-icalendar-store-UID nil | |
194 | "Non-nil means store any created UIDs in properties. | |
195 | ||
196 | The iCalendar standard requires that all entries have a unique identifier. | |
197 | Org will create these identifiers as needed. When this variable is non-nil, | |
198 | the created UIDs will be stored in the ID property of the entry. Then the | |
199 | next time this entry is exported, it will be exported with the same UID, | |
200 | superseding the previous form of it. This is essential for | |
201 | synchronization services. | |
202 | ||
203 | This variable is not turned on by default because we want to avoid creating | |
204 | a property drawer in every entry if people are only playing with this feature, | |
205 | or if they are only using it locally." | |
206 | :group 'org-export-icalendar | |
207 | :type 'boolean) | |
208 | ||
209 | (defcustom org-icalendar-timezone (getenv "TZ") | |
210 | "The time zone string for iCalendar export. | |
211 | When nil or the empty string, use output | |
212 | from (current-time-zone)." | |
213 | :group 'org-export-icalendar | |
214 | :type '(choice | |
215 | (const :tag "Unspecified" nil) | |
216 | (string :tag "Time zone"))) | |
217 | ||
218 | (defcustom org-icalendar-date-time-format ":%Y%m%dT%H%M%S" | |
219 | "Format-string for exporting icalendar DATE-TIME. | |
220 | ||
221 | See `format-time-string' for a full documentation. The only | |
222 | difference is that `org-icalendar-timezone' is used for %Z. | |
223 | ||
224 | Interesting value are: | |
225 | - \":%Y%m%dT%H%M%S\" for local time | |
226 | - \";TZID=%Z:%Y%m%dT%H%M%S\" for local time with explicit timezone | |
227 | - \":%Y%m%dT%H%M%SZ\" for time expressed in Universal Time" | |
228 | :group 'org-export-icalendar | |
229 | :version "24.1" | |
230 | :type '(choice | |
231 | (const :tag "Local time" ":%Y%m%dT%H%M%S") | |
232 | (const :tag "Explicit local time" ";TZID=%Z:%Y%m%dT%H%M%S") | |
233 | (const :tag "Universal time" ":%Y%m%dT%H%M%SZ") | |
234 | (string :tag "Explicit format"))) | |
235 | ||
236 | (defvar org-icalendar-after-save-hook nil | |
237 | "Hook run after an iCalendar file has been saved. | |
238 | This hook is run with the name of the file as argument. A good | |
239 | way to use this is to tell a desktop calendar application to | |
240 | re-read the iCalendar file.") | |
241 | ||
242 | ||
243 | \f | |
244 | ;;; Define Back-End | |
245 | ||
246 | (org-export-define-derived-backend 'icalendar 'ascii | |
247 | :translate-alist '((clock . ignore) | |
248 | (footnote-definition . ignore) | |
249 | (footnote-reference . ignore) | |
250 | (headline . org-icalendar-entry) | |
251 | (inlinetask . ignore) | |
252 | (planning . ignore) | |
253 | (section . ignore) | |
254 | (inner-template . (lambda (c i) c)) | |
255 | (template . org-icalendar-template)) | |
256 | :options-alist | |
257 | '((:exclude-tags | |
258 | "ICALENDAR_EXCLUDE_TAGS" nil org-icalendar-exclude-tags split) | |
259 | (:with-timestamps nil "<" org-icalendar-with-timestamps) | |
260 | (:with-vtodo nil nil org-icalendar-include-todo) | |
261 | ;; The following property will be non-nil when export has been | |
262 | ;; started from org-agenda-mode. In this case, any entry without | |
263 | ;; a non-nil "ICALENDAR_MARK" property will be ignored. | |
264 | (:icalendar-agenda-view nil nil nil)) | |
265 | :filters-alist | |
266 | '((:filter-headline . org-icalendar-clear-blank-lines)) | |
267 | :menu-entry | |
268 | '(?c "Export to iCalendar" | |
269 | ((?f "Current file" org-icalendar-export-to-ics) | |
270 | (?a "All agenda files" | |
271 | (lambda (a s v b) (org-icalendar-export-agenda-files a))) | |
272 | (?c "Combine all agenda files" | |
273 | (lambda (a s v b) (org-icalendar-combine-agenda-files a)))))) | |
274 | ||
275 | ||
276 | \f | |
277 | ;;; Internal Functions | |
278 | ||
279 | (defun org-icalendar-create-uid (file &optional bell h-markers) | |
280 | "Set ID property on headlines missing it in FILE. | |
281 | When optional argument BELL is non-nil, inform the user with | |
282 | a message if the file was modified. With optional argument | |
283 | H-MARKERS non-nil, it is a list of markers for the headlines | |
284 | which will be updated." | |
285 | (let ((pt (if h-markers (goto-char (car h-markers)) (point-min))) | |
286 | modified-flag) | |
287 | (org-map-entries | |
288 | (lambda () | |
289 | (let ((entry (org-element-at-point))) | |
290 | (unless (or (< (point) pt) (org-element-property :ID entry)) | |
291 | (org-id-get-create) | |
292 | (setq modified-flag t) | |
293 | (forward-line)) | |
294 | (when h-markers (setq org-map-continue-from (pop h-markers))))) | |
295 | nil nil 'comment) | |
296 | (when (and bell modified-flag) | |
297 | (message "ID properties created in file \"%s\"" file) | |
298 | (sit-for 2)))) | |
299 | ||
300 | (defun org-icalendar-blocked-headline-p (headline info) | |
301 | "Non-nil when HEADLINE is considered to be blocked. | |
302 | ||
303 | INFO is a plist used as a communication channel. | |
304 | ||
305 | a headline is blocked when either: | |
306 | ||
307 | - It has children which are not all in a completed state. | |
308 | ||
309 | - It has a parent with the property :ORDERED:, and there are | |
310 | siblings prior to it with incomplete status. | |
311 | ||
312 | - Its parent is blocked because it has siblings that should be | |
313 | done first or is a child of a blocked grandparent entry." | |
314 | (or | |
315 | ;; Check if any child is not done. | |
316 | (org-element-map headline 'headline | |
317 | (lambda (hl) (eq (org-element-property :todo-type hl) 'todo)) | |
318 | info 'first-match) | |
319 | ;; Check :ORDERED: node property. | |
320 | (catch 'blockedp | |
321 | (let ((current headline)) | |
322 | (mapc (lambda (parent) | |
323 | (cond | |
324 | ((not (org-element-property :todo-keyword parent)) | |
325 | (throw 'blockedp nil)) | |
326 | ((org-not-nil (org-element-property :ORDERED parent)) | |
327 | (let ((sibling current)) | |
328 | (while (setq sibling (org-export-get-previous-element | |
329 | sibling info)) | |
330 | (when (eq (org-element-property :todo-type sibling) 'todo) | |
331 | (throw 'blockedp t))))) | |
332 | (t (setq current parent)))) | |
333 | (org-export-get-genealogy headline)) | |
334 | nil)))) | |
335 | ||
336 | (defun org-icalendar-use-UTC-date-time-p () | |
337 | "Non-nil when `org-icalendar-date-time-format' requires UTC time." | |
338 | (char-equal (elt org-icalendar-date-time-format | |
339 | (1- (length org-icalendar-date-time-format))) ?Z)) | |
340 | ||
341 | (defvar org-agenda-default-appointment-duration) ; From org-agenda.el. | |
342 | (defun org-icalendar-convert-timestamp (timestamp keyword &optional end utc) | |
343 | "Convert TIMESTAMP to iCalendar format. | |
344 | ||
345 | TIMESTAMP is a timestamp object. KEYWORD is added in front of | |
346 | it, in order to make a complete line (e.g. \"DTSTART\"). | |
347 | ||
348 | When optional argument END is non-nil, use end of time range. | |
349 | Also increase the hour by two (if time string contains a time), | |
350 | or the day by one (if it does not contain a time) when no | |
351 | explicit ending time is specified. | |
352 | ||
353 | When optional argument UTC is non-nil, time will be expressed in | |
354 | Universal Time, ignoring `org-icalendar-date-time-format'." | |
355 | (let* ((year-start (org-element-property :year-start timestamp)) | |
356 | (year-end (org-element-property :year-end timestamp)) | |
357 | (month-start (org-element-property :month-start timestamp)) | |
358 | (month-end (org-element-property :month-end timestamp)) | |
359 | (day-start (org-element-property :day-start timestamp)) | |
360 | (day-end (org-element-property :day-end timestamp)) | |
361 | (hour-start (org-element-property :hour-start timestamp)) | |
362 | (hour-end (org-element-property :hour-end timestamp)) | |
363 | (minute-start (org-element-property :minute-start timestamp)) | |
364 | (minute-end (org-element-property :minute-end timestamp)) | |
365 | (with-time-p minute-start) | |
366 | (equal-bounds-p | |
367 | (equal (list year-start month-start day-start hour-start minute-start) | |
368 | (list year-end month-end day-end hour-end minute-end))) | |
369 | (mi (cond ((not with-time-p) 0) | |
370 | ((not end) minute-start) | |
371 | ((and org-agenda-default-appointment-duration equal-bounds-p) | |
372 | (+ minute-end org-agenda-default-appointment-duration)) | |
373 | (t minute-end))) | |
374 | (h (cond ((not with-time-p) 0) | |
375 | ((not end) hour-start) | |
376 | ((or (not equal-bounds-p) | |
377 | org-agenda-default-appointment-duration) | |
378 | hour-end) | |
379 | (t (+ hour-end 2)))) | |
380 | (d (cond ((not end) day-start) | |
381 | ((not with-time-p) (1+ day-end)) | |
382 | (t day-end))) | |
383 | (m (if end month-end month-start)) | |
384 | (y (if end year-end year-start))) | |
385 | (concat | |
386 | keyword | |
387 | (format-time-string | |
388 | (cond (utc ":%Y%m%dT%H%M%SZ") | |
389 | ((not with-time-p) ";VALUE=DATE:%Y%m%d") | |
390 | (t (replace-regexp-in-string "%Z" | |
391 | org-icalendar-timezone | |
392 | org-icalendar-date-time-format | |
393 | t))) | |
394 | ;; Convert timestamp into internal time in order to use | |
395 | ;; `format-time-string' and fix any mistake (i.e. MI >= 60). | |
396 | (encode-time 0 mi h d m y) | |
397 | (or utc (and with-time-p (org-icalendar-use-UTC-date-time-p))))))) | |
398 | ||
399 | (defun org-icalendar-dtstamp () | |
400 | "Return DTSTAMP property, as a string." | |
401 | (format-time-string "DTSTAMP:%Y%m%dT%H%M%SZ" nil t)) | |
402 | ||
403 | (defun org-icalendar-get-categories (entry info) | |
404 | "Return categories according to `org-icalendar-categories'. | |
405 | ENTRY is a headline or an inlinetask element. INFO is a plist | |
406 | used as a communication channel." | |
407 | (mapconcat | |
408 | 'identity | |
409 | (org-uniquify | |
410 | (let (categories) | |
411 | (mapc (lambda (type) | |
412 | (case type | |
413 | (category | |
414 | (push (org-export-get-category entry info) categories)) | |
415 | (todo-state | |
416 | (let ((todo (org-element-property :todo-keyword entry))) | |
417 | (and todo (push todo categories)))) | |
418 | (local-tags | |
419 | (setq categories | |
420 | (append (nreverse (org-export-get-tags entry info)) | |
421 | categories))) | |
422 | (all-tags | |
423 | (setq categories | |
424 | (append (nreverse (org-export-get-tags entry info nil t)) | |
425 | categories))))) | |
426 | org-icalendar-categories) | |
427 | ;; Return list of categories, following specified order. | |
428 | (nreverse categories))) ",")) | |
429 | ||
430 | (defun org-icalendar-transcode-diary-sexp (sexp uid summary) | |
431 | "Transcode a diary sexp into iCalendar format. | |
432 | SEXP is the diary sexp being transcoded, as a string. UID is the | |
433 | unique identifier for the entry. SUMMARY defines a short summary | |
434 | or subject for the event." | |
435 | (when (require 'icalendar nil t) | |
436 | (org-element-normalize-string | |
437 | (with-temp-buffer | |
438 | (let ((sexp (if (not (string-match "\\`<%%" sexp)) sexp | |
439 | (concat (substring sexp 1 -1) " " summary)))) | |
440 | (put-text-property 0 1 'uid uid sexp) | |
441 | (insert sexp "\n")) | |
442 | (org-diary-to-ical-string (current-buffer)))))) | |
443 | ||
444 | (defun org-icalendar-cleanup-string (s) | |
445 | "Cleanup string S according to RFC 5545." | |
446 | (when s | |
447 | ;; Protect "\", "," and ";" characters. and replace newline | |
448 | ;; characters with literal \n. | |
449 | (replace-regexp-in-string | |
450 | "[ \t]*\n" "\\n" | |
451 | (replace-regexp-in-string "[\\,;]" "\\\&" s) | |
452 | nil t))) | |
453 | ||
454 | (defun org-icalendar-fold-string (s) | |
455 | "Fold string S according to RFC 5545." | |
456 | (org-element-normalize-string | |
457 | (mapconcat | |
458 | (lambda (line) | |
459 | ;; Limit each line to a maximum of 75 characters. If it is | |
460 | ;; longer, fold it by using "\n " as a continuation marker. | |
461 | (let ((len (length line))) | |
462 | (if (<= len 75) line | |
463 | (let ((folded-line (substring line 0 75)) | |
464 | (chunk-start 75) | |
465 | chunk-end) | |
466 | ;; Since continuation marker takes up one character on the | |
467 | ;; line, real contents must be split at 74 chars. | |
468 | (while (< (setq chunk-end (+ chunk-start 74)) len) | |
469 | (setq folded-line | |
470 | (concat folded-line "\n " | |
471 | (substring line chunk-start chunk-end)) | |
472 | chunk-start chunk-end)) | |
473 | (concat folded-line "\n " (substring line chunk-start)))))) | |
474 | (org-split-string s "\n") "\n"))) | |
475 | ||
476 | ||
477 | \f | |
478 | ;;; Filters | |
479 | ||
480 | (defun org-icalendar-clear-blank-lines (headline back-end info) | |
481 | "Remove trailing blank lines in HEADLINE export. | |
482 | HEADLINE is a string representing a transcoded headline. | |
483 | BACK-END and INFO are ignored." | |
484 | (replace-regexp-in-string "^\\(?:[ \t]*\n\\)*" "" headline)) | |
485 | ||
486 | ||
487 | \f | |
488 | ;;; Transcode Functions | |
489 | ||
490 | ;;;; Headline and Inlinetasks | |
491 | ||
492 | ;; The main function is `org-icalendar-entry', which extracts | |
493 | ;; information from a headline or an inlinetask (summary, | |
494 | ;; description...) and then delegates code generation to | |
495 | ;; `org-icalendar--vtodo' and `org-icalendar--vevent', depending | |
496 | ;; on the component needed. | |
497 | ||
498 | ;; Obviously, `org-icalendar--valarm' handles alarms, which can | |
499 | ;; happen within a VTODO component. | |
500 | ||
501 | (defun org-icalendar-entry (entry contents info) | |
502 | "Transcode ENTRY element into iCalendar format. | |
503 | ||
504 | ENTRY is either a headline or an inlinetask. CONTENTS is | |
505 | ignored. INFO is a plist used as a communication channel. | |
506 | ||
507 | This function is called on every headline, the section below | |
508 | it (minus inlinetasks) being its contents. It tries to create | |
509 | VEVENT and VTODO components out of scheduled date, deadline date, | |
510 | plain timestamps, diary sexps. It also calls itself on every | |
511 | inlinetask within the section." | |
512 | (unless (org-element-property :footnote-section-p entry) | |
513 | (let* ((type (org-element-type entry)) | |
514 | ;; Determine contents really associated to the entry. For | |
515 | ;; a headline, limit them to section, if any. For an | |
516 | ;; inlinetask, this is every element within the task. | |
517 | (inside | |
518 | (if (eq type 'inlinetask) | |
519 | (cons 'org-data (cons nil (org-element-contents entry))) | |
520 | (let ((first (car (org-element-contents entry)))) | |
521 | (and (eq (org-element-type first) 'section) | |
522 | (cons 'org-data | |
523 | (cons nil (org-element-contents first)))))))) | |
524 | (concat | |
525 | (unless (and (plist-get info :icalendar-agenda-view) | |
526 | (not (org-element-property :ICALENDAR-MARK entry))) | |
527 | (let ((todo-type (org-element-property :todo-type entry)) | |
528 | (uid (or (org-element-property :ID entry) (org-id-new))) | |
529 | (summary (org-icalendar-cleanup-string | |
530 | (or (org-element-property :SUMMARY entry) | |
531 | (org-export-data | |
532 | (org-element-property :title entry) info)))) | |
533 | (loc (org-icalendar-cleanup-string | |
534 | (org-element-property :LOCATION entry))) | |
535 | ;; Build description of the entry from associated | |
536 | ;; section (headline) or contents (inlinetask). | |
537 | (desc | |
538 | (org-icalendar-cleanup-string | |
539 | (or (org-element-property :DESCRIPTION entry) | |
540 | (let ((contents (org-export-data inside info))) | |
541 | (cond | |
542 | ((not (org-string-nw-p contents)) nil) | |
543 | ((wholenump org-icalendar-include-body) | |
544 | (let ((contents (org-trim contents))) | |
545 | (substring | |
546 | contents 0 (min (length contents) | |
547 | org-icalendar-include-body)))) | |
548 | (org-icalendar-include-body (org-trim contents))))))) | |
549 | (cat (org-icalendar-get-categories entry info))) | |
550 | (concat | |
551 | ;; Events: Delegate to `org-icalendar--vevent' to | |
552 | ;; generate "VEVENT" component from scheduled, deadline, | |
553 | ;; or any timestamp in the entry. | |
554 | (let ((deadline (org-element-property :deadline entry))) | |
555 | (and deadline | |
556 | (memq (if todo-type 'event-if-todo 'event-if-not-todo) | |
557 | org-icalendar-use-deadline) | |
558 | (org-icalendar--vevent | |
559 | entry deadline (concat "DL-" uid) | |
560 | (concat "DL: " summary) loc desc cat))) | |
561 | (let ((scheduled (org-element-property :scheduled entry))) | |
562 | (and scheduled | |
563 | (memq (if todo-type 'event-if-todo 'event-if-not-todo) | |
564 | org-icalendar-use-scheduled) | |
565 | (org-icalendar--vevent | |
566 | entry scheduled (concat "SC-" uid) | |
567 | (concat "S: " summary) loc desc cat))) | |
568 | ;; When collecting plain timestamps from a headline and | |
569 | ;; its title, skip inlinetasks since collection will | |
570 | ;; happen once ENTRY is one of them. | |
571 | (let ((counter 0)) | |
572 | (mapconcat | |
573 | 'identity | |
574 | (org-element-map (cons (org-element-property :title entry) | |
575 | (org-element-contents inside)) | |
576 | 'timestamp | |
577 | (lambda (ts) | |
578 | (let ((uid (format "TS%d-%s" (incf counter) uid))) | |
579 | (org-icalendar--vevent entry ts uid summary loc desc cat))) | |
580 | info nil (and (eq type 'headline) 'inlinetask)) | |
581 | "")) | |
582 | ;; Task: First check if it is appropriate to export it. | |
583 | ;; If so, call `org-icalendar--vtodo' to transcode it | |
584 | ;; into a "VTODO" component. | |
585 | (when (and todo-type | |
586 | (case (plist-get info :with-vtodo) | |
587 | (all t) | |
588 | (unblocked | |
589 | (and (eq type 'headline) | |
590 | (not (org-icalendar-blocked-headline-p | |
591 | entry info)))) | |
592 | ('t (eq todo-type 'todo)))) | |
593 | (org-icalendar--vtodo entry uid summary loc desc cat)) | |
594 | ;; Diary-sexp: Collect every diary-sexp element within | |
595 | ;; ENTRY and its title, and transcode them. If ENTRY is | |
596 | ;; a headline, skip inlinetasks: they will be handled | |
597 | ;; separately. | |
598 | (when org-icalendar-include-sexps | |
599 | (let ((counter 0)) | |
600 | (mapconcat 'identity | |
601 | (org-element-map | |
602 | (cons (org-element-property :title entry) | |
603 | (org-element-contents inside)) | |
604 | 'diary-sexp | |
605 | (lambda (sexp) | |
606 | (org-icalendar-transcode-diary-sexp | |
607 | (org-element-property :value sexp) | |
608 | (format "DS%d-%s" (incf counter) uid) | |
609 | summary)) | |
610 | info nil (and (eq type 'headline) 'inlinetask)) | |
611 | "")))))) | |
612 | ;; If ENTRY is a headline, call current function on every | |
613 | ;; inlinetask within it. In agenda export, this is independent | |
614 | ;; from the mark (or lack thereof) on the entry. | |
615 | (when (eq type 'headline) | |
616 | (mapconcat 'identity | |
617 | (org-element-map inside 'inlinetask | |
618 | (lambda (task) (org-icalendar-entry task nil info)) | |
619 | info) "")) | |
620 | ;; Don't forget components from inner entries. | |
621 | contents)))) | |
622 | ||
623 | (defun org-icalendar--vevent | |
624 | (entry timestamp uid summary location description categories) | |
625 | "Create a VEVENT component. | |
626 | ||
627 | ENTRY is either a headline or an inlinetask element. TIMESTAMP | |
628 | is a timestamp object defining the date-time of the event. UID | |
629 | is the unique identifier for the event. SUMMARY defines a short | |
630 | summary or subject for the event. LOCATION defines the intended | |
631 | venue for the event. DESCRIPTION provides the complete | |
632 | description of the event. CATEGORIES defines the categories the | |
633 | event belongs to. | |
634 | ||
635 | Return VEVENT component as a string." | |
636 | (org-icalendar-fold-string | |
637 | (if (eq (org-element-property :type timestamp) 'diary) | |
638 | (org-icalendar-transcode-diary-sexp | |
639 | (org-element-property :raw-value timestamp) uid summary) | |
640 | (concat "BEGIN:VEVENT\n" | |
641 | (org-icalendar-dtstamp) "\n" | |
642 | "UID:" uid "\n" | |
643 | (org-icalendar-convert-timestamp timestamp "DTSTART") "\n" | |
644 | (org-icalendar-convert-timestamp timestamp "DTEND" t) "\n" | |
645 | ;; RRULE. | |
646 | (when (org-element-property :repeater-type timestamp) | |
647 | (format "RRULE:FREQ=%s;INTERVAL=%d\n" | |
648 | (case (org-element-property :repeater-unit timestamp) | |
649 | (hour "HOURLY") (day "DAILY") (week "WEEKLY") | |
650 | (month "MONTHLY") (year "YEARLY")) | |
651 | (org-element-property :repeater-value timestamp))) | |
652 | "SUMMARY:" summary "\n" | |
653 | (and (org-string-nw-p location) (format "LOCATION:%s\n" location)) | |
654 | (and (org-string-nw-p description) | |
655 | (format "DESCRIPTION:%s\n" description)) | |
656 | "CATEGORIES:" categories "\n" | |
657 | ;; VALARM. | |
658 | (org-icalendar--valarm entry timestamp summary) | |
659 | "END:VEVENT")))) | |
660 | ||
661 | (defun org-icalendar--vtodo | |
662 | (entry uid summary location description categories) | |
663 | "Create a VTODO component. | |
664 | ||
665 | ENTRY is either a headline or an inlinetask element. UID is the | |
666 | unique identifier for the task. SUMMARY defines a short summary | |
667 | or subject for the task. LOCATION defines the intended venue for | |
668 | the task. DESCRIPTION provides the complete description of the | |
669 | task. CATEGORIES defines the categories the task belongs to. | |
670 | ||
671 | Return VTODO component as a string." | |
672 | (let ((start (or (and (memq 'todo-start org-icalendar-use-scheduled) | |
673 | (org-element-property :scheduled entry)) | |
674 | ;; If we can't use a scheduled time for some | |
675 | ;; reason, start task now. | |
676 | (let ((now (decode-time (current-time)))) | |
677 | (list 'timestamp | |
678 | (list :type 'active | |
679 | :minute-start (nth 1 now) | |
680 | :hour-start (nth 2 now) | |
681 | :day-start (nth 3 now) | |
682 | :month-start (nth 4 now) | |
683 | :year-start (nth 5 now))))))) | |
684 | (org-icalendar-fold-string | |
685 | (concat "BEGIN:VTODO\n" | |
686 | "UID:TODO-" uid "\n" | |
687 | (org-icalendar-dtstamp) "\n" | |
688 | (org-icalendar-convert-timestamp start "DTSTART") "\n" | |
689 | (and (memq 'todo-due org-icalendar-use-deadline) | |
690 | (org-element-property :deadline entry) | |
691 | (concat (org-icalendar-convert-timestamp | |
692 | (org-element-property :deadline entry) "DUE") | |
693 | "\n")) | |
694 | "SUMMARY:" summary "\n" | |
695 | (and (org-string-nw-p location) (format "LOCATION:%s\n" location)) | |
696 | (and (org-string-nw-p description) | |
697 | (format "DESCRIPTION:%s\n" description)) | |
698 | "CATEGORIES:" categories "\n" | |
699 | "SEQUENCE:1\n" | |
700 | (format "PRIORITY:%d\n" | |
701 | (let ((pri (or (org-element-property :priority entry) | |
702 | org-default-priority))) | |
703 | (floor (- 9 (* 8. (/ (float (- org-lowest-priority pri)) | |
704 | (- org-lowest-priority | |
705 | org-highest-priority))))))) | |
706 | (format "STATUS:%s\n" | |
707 | (if (eq (org-element-property :todo-type entry) 'todo) | |
708 | "NEEDS-ACTION" | |
709 | "COMPLETED")) | |
710 | "END:VTODO")))) | |
711 | ||
712 | (defun org-icalendar--valarm (entry timestamp summary) | |
713 | "Create a VALARM component. | |
714 | ||
715 | ENTRY is the calendar entry triggering the alarm. TIMESTAMP is | |
716 | the start date-time of the entry. SUMMARY defines a short | |
717 | summary or subject for the task. | |
718 | ||
719 | Return VALARM component as a string, or nil if it isn't allowed." | |
720 | ;; Create a VALARM entry if the entry is timed. This is not very | |
721 | ;; general in that: | |
722 | ;; (a) only one alarm per entry is defined, | |
723 | ;; (b) only minutes are allowed for the trigger period ahead of the | |
724 | ;; start time, | |
725 | ;; (c) only a DISPLAY action is defined. [ESF] | |
726 | (let ((alarm-time | |
727 | (let ((warntime | |
728 | (org-element-property :APPT_WARNTIME entry))) | |
729 | (if warntime (string-to-number warntime) 0)))) | |
730 | (and (or (> alarm-time 0) (> org-icalendar-alarm-time 0)) | |
731 | (org-element-property :hour-start timestamp) | |
732 | (format "BEGIN:VALARM | |
733 | ACTION:DISPLAY | |
734 | DESCRIPTION:%s | |
735 | TRIGGER:-P0DT0H%dM0S | |
736 | END:VALARM\n" | |
737 | summary | |
738 | (if (zerop alarm-time) org-icalendar-alarm-time alarm-time))))) | |
739 | ||
740 | ||
741 | ;;;; Template | |
742 | ||
743 | (defun org-icalendar-template (contents info) | |
744 | "Return complete document string after iCalendar conversion. | |
745 | CONTENTS is the transcoded contents string. INFO is a plist used | |
746 | as a communication channel." | |
747 | (org-icalendar--vcalendar | |
748 | ;; Name. | |
749 | (if (not (plist-get info :input-file)) (buffer-name (buffer-base-buffer)) | |
750 | (file-name-nondirectory | |
751 | (file-name-sans-extension (plist-get info :input-file)))) | |
752 | ;; Owner. | |
753 | (if (not (plist-get info :with-author)) "" | |
754 | (org-export-data (plist-get info :author) info)) | |
755 | ;; Timezone. | |
756 | (if (org-string-nw-p org-icalendar-timezone) org-icalendar-timezone | |
757 | (cadr (current-time-zone))) | |
758 | ;; Description. | |
759 | (org-export-data (plist-get info :title) info) | |
760 | contents)) | |
761 | ||
762 | (defun org-icalendar--vcalendar (name owner tz description contents) | |
763 | "Create a VCALENDAR component. | |
764 | NAME, OWNER, TZ, DESCRIPTION and CONTENTS are all strings giving, | |
765 | respectively, the name of the calendar, its owner, the timezone | |
766 | used, a short description and the other components included." | |
767 | (concat (format "BEGIN:VCALENDAR | |
768 | VERSION:2.0 | |
769 | X-WR-CALNAME:%s | |
770 | PRODID:-//%s//Emacs with Org mode//EN | |
771 | X-WR-TIMEZONE:%s | |
772 | X-WR-CALDESC:%s | |
773 | CALSCALE:GREGORIAN\n" | |
774 | (org-icalendar-cleanup-string name) | |
775 | (org-icalendar-cleanup-string owner) | |
776 | (org-icalendar-cleanup-string tz) | |
777 | (org-icalendar-cleanup-string description)) | |
778 | contents | |
779 | "END:VCALENDAR\n")) | |
780 | ||
781 | ||
782 | \f | |
783 | ;;; Interactive Functions | |
784 | ||
785 | ;;;###autoload | |
786 | (defun org-icalendar-export-to-ics | |
787 | (&optional async subtreep visible-only body-only) | |
788 | "Export current buffer to an iCalendar file. | |
789 | ||
790 | If narrowing is active in the current buffer, only export its | |
791 | narrowed part. | |
792 | ||
793 | If a region is active, export that region. | |
794 | ||
795 | A non-nil optional argument ASYNC means the process should happen | |
796 | asynchronously. The resulting file should be accessible through | |
797 | the `org-export-stack' interface. | |
798 | ||
799 | When optional argument SUBTREEP is non-nil, export the sub-tree | |
800 | at point, extracting information from the headline properties | |
801 | first. | |
802 | ||
803 | When optional argument VISIBLE-ONLY is non-nil, don't export | |
804 | contents of hidden elements. | |
805 | ||
806 | When optional argument BODY-ONLY is non-nil, only write code | |
807 | between \"BEGIN:VCALENDAR\" and \"END:VCALENDAR\". | |
808 | ||
809 | Return ICS file name." | |
810 | (interactive) | |
811 | (let ((file (buffer-file-name (buffer-base-buffer)))) | |
812 | (when (and file org-icalendar-store-UID) | |
813 | (org-icalendar-create-uid file 'warn-user))) | |
814 | ;; Export part. Since this back-end is backed up by `ascii', ensure | |
815 | ;; links will not be collected at the end of sections. | |
816 | (let ((outfile (org-export-output-file-name ".ics" subtreep))) | |
817 | (org-export-to-file 'icalendar outfile | |
818 | async subtreep visible-only body-only '(:ascii-charset utf-8) | |
819 | (lambda (file) | |
820 | (run-hook-with-args 'org-icalendar-after-save-hook file) nil)))) | |
821 | ||
822 | ;;;###autoload | |
823 | (defun org-icalendar-export-agenda-files (&optional async) | |
824 | "Export all agenda files to iCalendar files. | |
825 | When optional argument ASYNC is non-nil, export happens in an | |
826 | external process." | |
827 | (interactive) | |
828 | (if async | |
829 | ;; Asynchronous export is not interactive, so we will not call | |
830 | ;; `org-check-agenda-file'. Instead we remove any non-existent | |
831 | ;; agenda file from the list. | |
832 | (let ((files (org-remove-if-not 'file-exists-p (org-agenda-files t)))) | |
833 | (org-export-async-start | |
834 | (lambda (results) | |
835 | (mapc (lambda (f) (org-export-add-to-stack f 'icalendar)) | |
836 | results)) | |
837 | `(let (output-files) | |
838 | (mapc (lambda (file) | |
839 | (with-current-buffer (org-get-agenda-file-buffer file) | |
840 | (push (expand-file-name (org-icalendar-export-to-ics)) | |
841 | output-files))) | |
842 | ',files) | |
843 | output-files))) | |
844 | (let ((files (org-agenda-files t))) | |
845 | (org-agenda-prepare-buffers files) | |
846 | (unwind-protect | |
847 | (mapc (lambda (file) | |
848 | (catch 'nextfile | |
849 | (org-check-agenda-file file) | |
850 | (with-current-buffer (org-get-agenda-file-buffer file) | |
851 | (org-icalendar-export-to-ics)))) | |
852 | files) | |
853 | (org-release-buffers org-agenda-new-buffers))))) | |
854 | ||
855 | ;;;###autoload | |
856 | (defun org-icalendar-combine-agenda-files (&optional async) | |
857 | "Combine all agenda files into a single iCalendar file. | |
858 | ||
859 | A non-nil optional argument ASYNC means the process should happen | |
860 | asynchronously. The resulting file should be accessible through | |
861 | the `org-export-stack' interface. | |
862 | ||
863 | The file is stored under the name chosen in | |
864 | `org-icalendar-combined-agenda-file'." | |
865 | (interactive) | |
866 | (if async | |
867 | (let ((files (org-remove-if-not 'file-exists-p (org-agenda-files t)))) | |
868 | (org-export-async-start | |
869 | (lambda (dummy) | |
870 | (org-export-add-to-stack | |
871 | (expand-file-name org-icalendar-combined-agenda-file) | |
872 | 'icalendar)) | |
873 | `(apply 'org-icalendar--combine-files nil ',files))) | |
874 | (apply 'org-icalendar--combine-files nil (org-agenda-files t)))) | |
875 | ||
876 | (defun org-icalendar-export-current-agenda (file) | |
877 | "Export current agenda view to an iCalendar FILE. | |
878 | This function assumes major mode for current buffer is | |
879 | `org-agenda-mode'." | |
880 | (let (org-export-babel-evaluate ; Don't evaluate Babel block | |
881 | (org-icalendar-combined-agenda-file file) | |
882 | (marker-list | |
883 | ;; Collect the markers pointing to entries in the current | |
884 | ;; agenda buffer. | |
885 | (let (markers) | |
886 | (save-excursion | |
887 | (goto-char (point-min)) | |
888 | (while (not (eobp)) | |
889 | (let ((m (or (org-get-at-bol 'org-hd-marker) | |
890 | (org-get-at-bol 'org-marker)))) | |
891 | (and m (push m markers))) | |
892 | (beginning-of-line 2))) | |
893 | (nreverse markers)))) | |
894 | (apply 'org-icalendar--combine-files | |
895 | ;; Build restriction alist. | |
896 | (let (restriction) | |
897 | ;; Sort markers in each association within RESTRICTION. | |
898 | (mapcar (lambda (x) (setcdr x (sort (copy-sequence (cdr x)) '<)) x) | |
899 | (dolist (m marker-list restriction) | |
900 | (let* ((pos (marker-position m)) | |
901 | (file (buffer-file-name | |
902 | (org-base-buffer (marker-buffer m)))) | |
903 | (file-markers (assoc file restriction))) | |
904 | ;; Add POS in FILE association if one exists | |
905 | ;; or create a new association for FILE. | |
906 | (if file-markers (push pos (cdr file-markers)) | |
907 | (push (list file pos) restriction)))))) | |
908 | (org-agenda-files nil 'ifmode)))) | |
909 | ||
910 | (defun org-icalendar--combine-files (restriction &rest files) | |
911 | "Combine entries from multiple files into an iCalendar file. | |
912 | RESTRICTION, when non-nil, is an alist where key is a file name | |
913 | and value a list of buffer positions pointing to entries that | |
914 | should appear in the calendar. It only makes sense if the | |
915 | function was called from an agenda buffer. FILES is a list of | |
916 | files to build the calendar from." | |
917 | (org-agenda-prepare-buffers files) | |
918 | (unwind-protect | |
919 | (progn | |
920 | (with-temp-file org-icalendar-combined-agenda-file | |
921 | (insert | |
922 | (org-icalendar--vcalendar | |
923 | ;; Name. | |
924 | org-icalendar-combined-name | |
925 | ;; Owner. | |
926 | user-full-name | |
927 | ;; Timezone. | |
928 | (or (org-string-nw-p org-icalendar-timezone) | |
929 | (cadr (current-time-zone))) | |
930 | ;; Description. | |
931 | org-icalendar-combined-description | |
932 | ;; Contents. | |
933 | (concat | |
934 | ;; Agenda contents. | |
935 | (mapconcat | |
936 | (lambda (file) | |
937 | (catch 'nextfile | |
938 | (org-check-agenda-file file) | |
939 | (with-current-buffer (org-get-agenda-file-buffer file) | |
940 | (let ((marks (cdr (assoc (expand-file-name file) | |
941 | restriction)))) | |
942 | ;; Create ID if necessary. | |
943 | (when org-icalendar-store-UID | |
944 | (org-icalendar-create-uid file t marks)) | |
945 | (unless (and restriction (not marks)) | |
946 | ;; Add a hook adding :ICALENDAR_MARK: property | |
947 | ;; to each entry appearing in agenda view. | |
948 | ;; Use `apply-partially' because the function | |
949 | ;; still has to accept one argument. | |
950 | (let ((org-export-before-processing-hook | |
951 | (cons (apply-partially | |
952 | (lambda (m-list dummy) | |
953 | (mapc (lambda (m) | |
954 | (org-entry-put | |
955 | m "ICALENDAR-MARK" "t")) | |
956 | m-list)) | |
957 | (sort marks '>)) | |
958 | org-export-before-processing-hook))) | |
959 | (org-export-as | |
960 | 'icalendar nil nil t | |
961 | (list :ascii-charset 'utf-8 | |
962 | :icalendar-agenda-view restriction)))))))) | |
963 | files "") | |
964 | ;; BBDB anniversaries. | |
965 | (when (and org-icalendar-include-bbdb-anniversaries | |
966 | (require 'org-bbdb nil t)) | |
3c8b09ca | 967 | (with-output-to-string (org-bbdb-anniv-export-ical))))))) |
271672fa BG |
968 | (run-hook-with-args 'org-icalendar-after-save-hook |
969 | org-icalendar-combined-agenda-file)) | |
970 | (org-release-buffers org-agenda-new-buffers))) | |
971 | ||
972 | ||
973 | (provide 'ox-icalendar) | |
974 | ||
975 | ;; Local variables: | |
976 | ;; generated-autoload-file: "org-loaddefs.el" | |
977 | ;; End: | |
978 | ||
979 | ;;; ox-icalendar.el ends here |