Ihor Radchenko <[email protected]> writes: > #+ICALENDAR_VCALENDAR: X-MICROSOFT-CDO-INTENDEDSTATUS:FREE > or an equivalent export snippet, but ox-icalendar does not support such > thing. I believe that it should be considered a bug - we should provide > some means to produce text to be exported verbatim from inside Org files.
The following patch provides initial support for #+ICALENDAR keywords and for icalendar export blocks. Delightfully, syntax highlighting for icalendar blocks worked out of the box with no extra effort, when icalendar-mode.el (Emacs 31) is available. I think this should also be useful for the recently discussed iCal->Org importer -- it will allow us to include obscure iCal properties without information loss, by putting them into icalendar blocks.
>From 199c115ebd57c44ed32bdb82c2d12cffb35f11ed Mon Sep 17 00:00:00 2001 From: Jack Kamm <[email protected]> Date: Thu, 12 Mar 2026 09:02:26 -0700 Subject: [PATCH] ox-icalendar: Add export blocks and keywords * lisp/ox-icalendar.el (org-export-define-derived-backend): Add export-block and keyword to `:translate-alist' (org-icalendar-entry): Limit DESCRIPTION to paragraph elements only. Extract literal iCalendar text from export-block and keyword elements to pass to `org-icalendar--vevent' and `org-icalendar--vtodo'. (org-icalendar--export-block): New function for export-block. (org-icalendar--keyword): New function for keyword export. (org-icalendar--vevent): Add argument `literal-ical' to append literal iCalendar test to the VEVENT. (org-icalendar--vtodo): Add argument `literal-ical' to append literal iCalendar test to the VTODO. * testing/lisp/test-ox-icalendar.el (test-ox-icalendar/export-block): New test for icalendar export-block. (test-ox-icalendar/keyword): New test for icalendar keywords. --- etc/ORG-NEWS | 32 +++++++++++++++ lisp/ox-icalendar.el | 65 ++++++++++++++++++++++++------- testing/lisp/test-ox-icalendar.el | 42 ++++++++++++++++++++ 3 files changed, 124 insertions(+), 15 deletions(-) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 230a88396..13c982a0c 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -24,6 +24,38 @@ Please send Org bug reports to mailto:[email protected]. # We list the most important features, and the features that may # require user action to be used. +*** iCalendar export blocks and keywords + +The iCalendar exporter now allows using export blocks and keywords +to insert literal text into the exported document. For example: + +#+begin_src org + ,* An event + :PROPERTIES: + :ID: abc123 + :END: + <2026-03-12 Thu> + + ,#+ICALENDAR: CONTACT:Jim Dolittle\, ABC Industries\, +1-919-555-1234 + + ,#+begin_export icalendar + ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Henry + Cabot:mailto:[email protected] + ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="mailto:bob@ + example.com";PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@ + example.com + ,#+end_export +#+end_src + +Note that text from export blocks and keywords is inserted literally +into the exported iCalendar without any syntax checking. When +icalendar-mode.el (from Emacs 31) is available, text in icalendar +export blocks will have syntax highlighting. + +A current limitation is that export blocks and keywords are only +implemented for events and todos, and not yet for calendar-wide +properties. + ** New and changed options # Changes dealing with changing default values of customizations, diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el index 9fc1e54cb..21142e943 100644 --- a/lisp/ox-icalendar.el +++ b/lisp/ox-icalendar.el @@ -376,8 +376,10 @@ (defvar org-icalendar-after-save-hook nil (org-export-define-derived-backend 'icalendar 'ascii :translate-alist '((clock . nil) + (export-block . org-icalendar--export-block) (footnote-definition . nil) (footnote-reference . nil) + (keyword . org-icalendar--keyword) (headline . org-icalendar-entry) (inner-template . org-icalendar-inner-template) (inlinetask . nil) @@ -568,6 +570,8 @@ (defun org-icalendar-get-categories (entry info) categories))))))) ",")) +;; TODO: this should also support other fields such as DESCRIPTION, +;; CATEGORIES, LOCATION, LITERAL-ICAL, etc (defun org-icalendar-transcode-diary-sexp (sexp uid summary) "Transcode a diary sexp into iCalendar format. SEXP is the diary sexp being transcoded, as a string. UID is the @@ -696,7 +700,11 @@ (defun org-icalendar-entry (entry contents info) (org-icalendar-cleanup-string (or (let ((org-property-separators '(("DESCRIPTION" . "\n")))) (org-entry-get entry "DESCRIPTION" 'selective)) - (let ((contents (org-export-data inside info))) + (let ((contents (string-join (org-element-map + (org-element-contents inside) + 'paragraph + (lambda (pg) (org-export-data pg info)) + info)))) (cond ((not (org-string-nw-p contents)) nil) ((wholenump org-icalendar-include-body) @@ -708,7 +716,13 @@ (defun org-icalendar-entry (entry contents info) (cat (org-icalendar-get-categories entry info)) (tz (org-export-get-node-property :TIMEZONE entry - (org-property-inherit-p "TIMEZONE")))) + (org-property-inherit-p "TIMEZONE"))) + (literal-ical (string-join (org-element-map + (org-element-contents inside) + '(export-block keyword) + (lambda (pg) (org-export-data pg info)) + info) + "\n"))) (concat ;; Events: Delegate to `org-icalendar--vevent' to generate ;; "VEVENT" component from scheduled, deadline, or any @@ -726,7 +740,7 @@ (defun org-icalendar-entry (entry contents info) (org-icalendar--vevent entry deadline (concat "DL-" uid) (concat deadline-summary-prefix summary) - loc desc cat tz class))) + loc desc cat tz class literal-ical))) (let ((scheduled (org-element-property :scheduled entry)) (use-scheduled (plist-get info :icalendar-use-scheduled)) (scheduled-summary-prefix (org-icalendar-cleanup-string @@ -740,7 +754,7 @@ (defun org-icalendar-entry (entry contents info) (org-icalendar--vevent entry scheduled (concat "SC-" uid) (concat scheduled-summary-prefix summary) - loc desc cat tz class))) + loc desc cat tz class literal-ical))) ;; When collecting plain timestamps from a headline and its ;; title, skip inlinetasks since collection will happen once ;; ENTRY is one of them. @@ -756,7 +770,7 @@ (defun org-icalendar-entry (entry contents info) (org-element-property :type ts)) (let ((uid (format "TS%d-%s" (cl-incf counter) uid))) (org-icalendar--vevent - entry ts uid summary loc desc cat tz class)))) + entry ts uid summary loc desc cat tz class literal-ical)))) info nil (and (eq type 'headline) 'inlinetask)) "")) ;; Task: First check if it is appropriate to export it. If @@ -773,7 +787,7 @@ (defun org-icalendar-entry (entry contents info) (`t (eq todo-type 'todo)) ((and (pred listp) kwd-list) (member (org-element-property :todo-keyword entry) kwd-list)))) - (org-icalendar--vtodo entry uid summary loc desc cat tz class)) + (org-icalendar--vtodo entry uid summary loc desc cat tz class literal-ical)) ;; Diary-sexp: Collect every diary-sexp element within ENTRY ;; and its title, and transcode them. If ENTRY is ;; a headline, skip inlinetasks: they will be handled @@ -803,6 +817,22 @@ (defun org-icalendar-entry (entry contents info) ;; Don't forget components from inner entries. contents)))) +(defun org-icalendar--export-block (export-block _contents _info) + "Transcode a EXPORT-BLOCK element from Org to iCalendar. +CONTENTS is nil. INFO is a plist holding contextual information." + (when (equal (org-element-property :type export-block) "ICALENDAR") + (org-remove-indentation (org-element-property :value export-block)))) + +(defun org-icalendar--keyword (keyword contents info) + "Transcode a KEYWORD element into Beamer code. +CONTENTS is nil. INFO is a plist used as a communication +channel." + (let ((key (org-element-property :key keyword)) + (value (org-element-property :value keyword))) + ;; Handle specifically BEAMER and TOC (headlines only) keywords. + ;; Otherwise, fallback to `latex' backend. + (when (equal key "ICALENDAR") value))) + (defun org-icalendar--rrule (unit value) "Format RRULE icalendar entry for UNIT frequency and VALUE interval. UNIT is a symbol `hour', `day', `week', `month', or `year'." @@ -813,7 +843,7 @@ (defun org-icalendar--rrule (unit value) value)) (defun org-icalendar--vevent - (entry timestamp uid summary location description categories timezone class) + (entry timestamp uid summary location description categories timezone class literal-ical) "Create a VEVENT component. ENTRY is either a headline or an inlinetask element. TIMESTAMP @@ -826,6 +856,7 @@ (defun org-icalendar--vevent only. CLASS contains the visibility attribute. Three of them \\(\"PUBLIC\", \"CONFIDENTIAL\", and \"PRIVATE\") are predefined, others should be treated as \"PRIVATE\" if they are unknown to the iCalendar server. +LITERAL-ICAL is additional iCalendar text to be added to the entry. Return VEVENT component as a string." (if (eq (org-element-property :type timestamp) 'diary) @@ -850,6 +881,8 @@ (defun org-icalendar--vevent "CATEGORIES:" categories "\n" ;; VALARM. (org-icalendar--valarm entry timestamp summary) + ;; additional literal iCalendar text + literal-ical (unless (equal literal-ical "") "\n") "END:VEVENT\n"))) (defun org-icalendar--repeater-type (elem) @@ -870,16 +903,16 @@ (defun org-icalendar--repeater-type (elem) (repeater-type)))) (defun org-icalendar--vtodo - (entry uid summary location description categories timezone class) + (entry uid summary location description categories timezone class literal-ical) "Create a VTODO component. -ENTRY is either a headline or an inlinetask element. UID is the -unique identifier for the task. SUMMARY defines a short summary -or subject for the task. LOCATION defines the intended venue for -the task. CLASS sets the task class (e.g. confidential). DESCRIPTION -provides the complete description of the task. CATEGORIES defines the -categories the task belongs to. TIMEZONE specifies a time zone for -this TODO only. +ENTRY is either a headline or an inlinetask element. UID is the unique +identifier for the task. SUMMARY defines a short summary or subject for +the task. LOCATION defines the intended venue for the task. CLASS sets +the task class (e.g. confidential). DESCRIPTION provides the complete +description of the task. CATEGORIES defines the categories the task +belongs to. TIMEZONE specifies a time zone for this TODO only. +LITERAL-ICAL is additional iCalendar text to be added to the entry. Return VTODO component as a string." (let* ((sc (and (memq 'todo-start org-icalendar-use-scheduled) @@ -994,6 +1027,8 @@ (defun org-icalendar--vtodo (if (eq (org-element-property :todo-type entry) 'todo) "NEEDS-ACTION" "COMPLETED")) + ;; additional literal iCalendar text + literal-ical (unless (equal literal-ical "") "\n") "END:VTODO\n"))) (defun org-icalendar--valarm (entry timestamp summary) diff --git a/testing/lisp/test-ox-icalendar.el b/testing/lisp/test-ox-icalendar.el index 8c0ab6377..be5a2b891 100644 --- a/testing/lisp/test-ox-icalendar.el +++ b/testing/lisp/test-ox-icalendar.el @@ -158,5 +158,47 @@ (ert-deftest test-ox-icalendar/exclude-diary-timestamp () (should (not (search-forward "RRULE:FREQ=MONTHLY;BYDAY=1SU" nil t))))) (when (file-exists-p tmp-ics) (delete-file tmp-ics))))) +(ert-deftest test-ox-icalendar/export-block () + "Test every line of iCalendar export has CRLF ending." + (let ((tmp-ics (org-test-with-temp-text-in-file + "* Test event +:PROPERTIES: +:ID: b17d8f92-1beb-442e-be4d-d2060fa3c7ff +:END: +<2023-03-30 Thu> + +#+begin_export icalendar +CONTACT:Jim Dolittle\, ABC Industries\, +1-919-555-1234 +#+end_export" + (expand-file-name (org-icalendar-export-to-ics))))) + (unwind-protect + (with-temp-buffer + (insert-file-contents tmp-ics) + (save-excursion + (should (search-forward "SUMMARY:Test event"))) + (save-excursion + (should (search-forward "CONTACT:Jim Dolittle\, ABC Industries\, +1-919-555-1234")))) + (when (file-exists-p tmp-ics) (delete-file tmp-ics))))) + +(ert-deftest test-ox-icalendar/keyword () + "Test every line of iCalendar export has CRLF ending." + (let ((tmp-ics (org-test-with-temp-text-in-file + "* Test event +:PROPERTIES: +:ID: b17d8f92-1beb-442e-be4d-d2060fa3c7ff +:END: +<2023-03-30 Thu> + +#+ICALENDAR: CONTACT:Jim Dolittle\, ABC Industries\, +1-919-555-1234" + (expand-file-name (org-icalendar-export-to-ics))))) + (unwind-protect + (with-temp-buffer + (insert-file-contents tmp-ics) + (save-excursion + (should (search-forward "SUMMARY:Test event"))) + (save-excursion + (should (search-forward "CONTACT:Jim Dolittle\, ABC Industries\, +1-919-555-1234")))) + (when (file-exists-p tmp-ics) (delete-file tmp-ics))))) + (provide 'test-ox-icalendar) ;;; test-ox-icalendar.el ends here -- 2.53.0
