diff --git a/lisp/org.el b/lisp/org.el index 1191879..8c3ac01 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -15460,7 +15460,7 @@ but in some other way.") (defconst org-default-properties '("ARCHIVE" "CATEGORY" "SUMMARY" "DESCRIPTION" "CUSTOM_ID" - "LOCATION" "LOGGING" "COLUMNS" "VISIBILITY" + "LOCATION" "LOGGING" "COLUMNS" "VISIBILITY" "TIMEZONE" "TABLE_EXPORT_FORMAT" "TABLE_EXPORT_FILE" "EXPORT_OPTIONS" "EXPORT_TEXT" "EXPORT_FILE_NAME" "EXPORT_TITLE" "EXPORT_AUTHOR" "EXPORT_DATE" "UNNUMBERED" diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el index 7d7c850..51088b6 100644 --- a/lisp/ox-icalendar.el +++ b/lisp/ox-icalendar.el @@ -216,6 +216,16 @@ from (current-time-zone)." (const :tag "Unspecified" nil) (string :tag "Time zone"))) +(defvar org-icalendar-vtimezone-table nil + "A \"TZID\" to \"VTIMEZONE\" table. +Specify this as a list of the form ((TZID . VTIMEZONE) ...) such +that VTIMEZONE is an iCalendar string declaring a timezone +corresponding to the TZID string. Use CRLF separators, also at +the end of the string. A table entry is required for every +timezone that is referenced in calls to +`org-icalendar-convert-timestamp', with the exception of +\"UTC\".") + (defcustom org-icalendar-date-time-format ":%Y%m%dT%H%M%S" "Format-string for exporting icalendar DATE-TIME. @@ -340,6 +350,12 @@ A headline is blocked when either (char-equal (elt org-icalendar-date-time-format (1- (length org-icalendar-date-time-format))) ?Z)) +(defvar org-icalendar--used-tzid-set nil + "A hash table with referenced TZID values, or nil. +Used to keep track of referenced timezone identifiers in +`org-icalendar-convert-timestamp', if set to a hash table. Only +the keys of the table matter, and values are ignored.") + (defvar org-agenda-default-appointment-duration) ; From org-agenda.el. (defun org-icalendar-convert-timestamp (timestamp keyword &optional end tz) "Convert TIMESTAMP to iCalendar format. @@ -390,21 +406,20 @@ format (e.g. \"Europe/London\"). In either case, the value of (concat keyword (format-time-string - (cond ((string-equal tz "UTC") ":%Y%m%dT%H%M%SZ") - ((not with-time-p) ";VALUE=DATE:%Y%m%d") - ((stringp tz) (concat ";TZID=" tz ":%Y%m%dT%H%M%S")) + (cond ((not with-time-p) ";VALUE=DATE:%Y%m%d") + ((string-equal tz "UTC") ":%Y%m%dT%H%M%SZ") + ((stringp tz) + (when org-icalendar--used-tzid-set + (puthash tz t org-icalendar--used-tzid-set)) + (concat ";TZID=" tz ":%Y%m%dT%H%M%S")) (t (replace-regexp-in-string "%Z" org-icalendar-timezone org-icalendar-date-time-format t))) ;; Convert timestamp into internal time in order to use ;; `format-time-string' and fix any mistake (i.e. MI >= 60). - (encode-time 0 mi h d m y) - (and (or (string-equal tz "UTC") - (and (null tz) - with-time-p - (org-icalendar-use-UTC-date-time-p))) - t))))) + (encode-time 0 mi h d m y t) + t)))) (defun org-icalendar-dtstamp () "Return DTSTAMP property, as a string." @@ -674,7 +689,7 @@ Return VEVENT component as a string." "CATEGORIES:" categories "\n" ;; VALARM. (org-icalendar--valarm entry timestamp summary) - "END:VEVENT")))) + "END:VEVENT\r")))) (defun org-icalendar--vtodo (entry uid summary location description categories timezone) @@ -726,7 +741,7 @@ Return VTODO component as a string." (if (eq (org-element-property :todo-type entry) 'todo) "NEEDS-ACTION" "COMPLETED")) - "END:VTODO")))) + "END:VTODO\r")))) (defun org-icalendar--valarm (entry timestamp summary) "Create a VALARM component. @@ -748,11 +763,11 @@ Return VALARM component as a string, or nil if it isn't allowed." (if warntime (string-to-number warntime) 0)))) (and (or (> alarm-time 0) (> org-icalendar-alarm-time 0)) (org-element-property :hour-start timestamp) - (format "BEGIN:VALARM -ACTION:DISPLAY -DESCRIPTION:%s -TRIGGER:-P0DT0H%dM0S -END:VALARM\n" + (format "BEGIN:VALARM\r +ACTION:DISPLAY\r +DESCRIPTION:%s\r +TRIGGER:-P0DT0H%dM0S\r +END:VALARM\r\n" summary (if (zerop alarm-time) org-icalendar-alarm-time alarm-time))))) @@ -783,19 +798,31 @@ as a communication channel." NAME, OWNER, TZ, DESCRIPTION and CONTENTS are all strings giving, respectively, the name of the calendar, its owner, the timezone used, a short description and the other components included." - (concat (format "BEGIN:VCALENDAR -VERSION:2.0 -X-WR-CALNAME:%s -PRODID:-//%s//Emacs with Org mode//EN -X-WR-TIMEZONE:%s -X-WR-CALDESC:%s -CALSCALE:GREGORIAN\n" - (org-icalendar-cleanup-string name) - (org-icalendar-cleanup-string owner) - (org-icalendar-cleanup-string tz) - (org-icalendar-cleanup-string description)) - contents - "END:VCALENDAR\n")) + (let* ((tzids (and org-icalendar--used-tzid-set + (let ((xs nil)) + (maphash (lambda (k v) + (push k xs)) + org-icalendar--used-tzid-set) + xs))) + (tzdefs (mapcar + (lambda (tzid) + (let ((entry (assoc tzid org-icalendar-vtimezone-table))) + (unless entry + (error "Missing VTIMEZONE definition for TZID %S" tzid)) + (cdr entry))) + tzids))) + (concat (format "BEGIN:VCALENDAR\r +VERSION:2.0\r +X-WR-CALNAME:%s\r +PRODID:-//%s//Emacs with Org mode//EN\r +X-WR-CALDESC:%s\r +CALSCALE:GREGORIAN\r\n" + (org-icalendar-cleanup-string name) + (org-icalendar-cleanup-string owner) + (org-icalendar-cleanup-string description)) + (apply 'concat tzdefs) + contents + "END:VCALENDAR\r\n"))) @@ -832,7 +859,8 @@ Return ICS file name." (org-icalendar-create-uid file 'warn-user))) ;; Export part. Since this back-end is backed up by `ascii', ensure ;; links will not be collected at the end of sections. - (let ((outfile (org-export-output-file-name ".ics" subtreep))) + (let ((org-icalendar--used-tzid-set (make-hash-table :test 'equal)) + (outfile (org-export-output-file-name ".ics" subtreep))) (org-export-to-file 'icalendar outfile async subtreep visible-only body-only '(:ascii-charset utf-8 :ascii-links-to-notes nil) @@ -931,7 +959,8 @@ FILES is a list of files to build the calendar from." ;; At the end of the process, all buffers related to FILES are going ;; to be killed. Make sure to only kill the ones opened in the ;; process. - (let ((org-agenda-new-buffers nil)) + (let ((org-agenda-new-buffers nil) + (org-icalendar--used-tzid-set (make-hash-table :test 'equal))) (unwind-protect (progn (with-temp-file org-icalendar-combined-agenda-file