Add full support for iCalendar (RFC5545) data

This is a fix for Bug#74994 that replaces the existing support
in icalendar.el.  It implements a full parser, recurrence rule
and time zone calculations, diary import and export, and a
major mode with syntax highlighting for iCalendar data.  It
obsoletes most of the code in icalendar.el.

In addition to Bug#74994, the proposal to update Emacs' iCalendar
support was discussed on emacs-devel in this thread:
https://lists.gnu.org/archive/html/emacs-devel/2024-10/msg00425.html

icalendar.el pre-dates the current standard (RFC5545), contains numerous
bugs, is not well documented, and could not easily be updated or
extended; starting fresh was the simplest path to creating an iCalendar
library that other Emacs applications and packages can rely on.  It was
decided to leave icalendar.el's code in place for posterity, but declare
it obsolete.  Most of the changes in icalendar.el simply consist of such
declarations.  The old To Do list has also been deleted.

A few changes in icalendar.el, however, consist of new code for
library-wide functions and options, especially error handling.  In
particular:

* lisp/calendar/icalendar.el: Log iCalendar library errors in a single
buffer.
(icalendar-errors-mode): New mode for it.
(icalendar-uid-format): Change the default value to "%h", a hash
value (for privacy).
(icalendar-make-uid): New function, to replace 'icalendar--create-uid'.
(icalendar-debug-level, icalendar-vcalendar-prodid): New option.
(icalendar-vcalendar-version): New constant.
* lisp/calendar/icalendar.el (icalendar-import-format)
(icalendar-import-format-summary, icalendar-import-format-description)
(icalendar-import-format-location, icalendar-import-format-organizer)
(icalendar-import-format-url, icalendar-import-format-uid)
(icalendar-import-format-status, icalendar-import-format-class)
(icalendar-recurring-start-year, icalendar-export-hidden-diary-entries)
(icalendar-export-sexp-enumerate-all, icalendar-export-alarms,
icalendar-debug, icalendar--weekday-array, icalendar--dmsg)
(icalendar--get-unfolded-buffer icalendar--clean-up-line-endings)
(icalendar--rris, icalendar--read-element)
(icalendar--get-event-property, icalendar--get-event-properties)
(icalendar--get-event-property-attributes)
(icalendar--get-children, icalendar--all-events, icalendar--split-value)
(icalendar--convert-tz-offset, icalendar--parse-vtimezone)
(icalendar--get-most-recent-observance)
(icalendar--convert-all-timezones, icalendar--find-time-zone)
(icalendar--decode-isodatetime)
(icalendar--decode-isoduration, icalendar--add-decoded-times)
(icalendar--datetime-to-american-date)
(icalendar--datetime-to-european-date, icalendar--datetime-to-iso-date)
(icalendar--datetime-to-diary-date, icalendar--datetime-to-colontime)
(icalendar--get-month-number, icalendar--get-weekday-number)
(icalendar--get-weekday-numbers, icalendar--get-weekday-abbrev)
(icalendar--date-to-isodate, icalendar--datestring-to-isodate)
(icalendar--diarytime-to-isotime, icalendar--convert-string-for-export)
(icalendar--convert-string-for-import, icalendar-export-file)
(icalendar-export-region, icalendar--create-uid)
(icalendar--parse-summary-and-rest, icalendar--create-ical-alarm)
(icalendar--do-create-ical-alarm, icalendar--convert-ordinary-to-ical)
(icalendar-first-weekday-of-year, icalendar--convert-weekly-to-ical)
(icalendar--convert-yearly-to-ical, icalendar--convert-sexp-to-ical)
(icalendar--convert-block-to-ical, icalendar--convert-float-to-ical)
(icalendar--convert-date-to-ical, icalendar--convert-cyclic-to-ical)
(icalendar--convert-anniversary-to-ical, icalendar-import-file)
(icalendar-import-buffer, icalendar--format-ical-event)
(icalendar--convert-to-ical, icalendar--convert-ical-to-diary)
(icalendar--convert-recurring-to-diary)
(icalendar--convert-non-recurring-all-day-to-diary)
(icalendar--convert-non-recurring-not-all-day-to-diary)
(icalendar--add-diary-entry, icalendar-import-format-sample): Mark them
as obsolete.

In addition to the changes above, the new iCalendar library consists of
the following:

* lisp/calendar/diary-icalendar.el:
* lisp/calendar/icalendar-ast.el:
* lisp/calendar/icalendar-macs.el:
* lisp/calendar/icalendar-mode.el:
* lisp/calendar/icalendar-parser.el:
* lisp/calendar/icalendar-recur.el:
* lisp/calendar/icalendar-utils.el: New files

A few changes were made to existing files dealing with the calendar and
diary:

* lisp/calendar/calendar.el (calendar-date-from-day-of-year): New
function, extracted from calendar-goto-day-of-year.
* lisp/calendar/cal-move.el (calendar-goto-day-of-year): Use it.
* lisp/calendar/cal-dst.el (calendar-dst-find-data): Improve docstring.
* lisp/calendar/calendar.el (diary-date-insertion-form): New option.
(diary-american-date-insertion-form, diary-european-date-insertion-form)
(diary-iso-date-insertion-form): New constants.
* lisp/calendar/diary-lib.el (diary-insert-entry): Use the new
'diary-date-insertion-form' option.
(diary-time-regexp): Add FIXME to an existing comment.

The user-facing aspects of the above changes are documented in the Emacs
manual and the NEWS file:

* doc/emacs/calendar.texi (Diary Conversion): Update manual section to
describe the new importer and exporter.
* doc/emacs/emacs.texi (Detailed node listing): Update to include the
new nodes in docs/emacs/calendar.texi.
* etc/NEWS: Briefly describe the new library, major mode, and options.

The remainder of the changes apply to test files.

The following changes introduce new test files related to the new diary
importer and exporter:

* test/lisp/calendar/diary-icalendar-tests.el (Diary import and export):
Tests for diary-icalendar.  In addition to new tests for the exporter,
the existing import tests for icalendar.el have been ported here; these
use the existing iCalendar files in
test/lisp/calendar/icalendar-resources.  (A few new input .ics files
have also been added to this directory; see below.)
* test/lisp/calendar/diary-icalendar-resources: New directory containing
expected outputs for the import tests in diary-icalendar-tests.el.
(These have the same or similar names to the output files for the old
importer, in test/lisp/calendar/icalendar-resources, but different
contents.  Thus they live in a new directory.)
* test/lisp/calendar/icalendar-resources/import-legacy-function.ics: New
input file to test backward compatibility of the new importer with a
function as the value of 'icalendar-import-format', now obsolete.
* test/lisp/calendar/icalendar-resources/import-legacy-vars.ics: New
input file to test backward compatibility of the new importer with
values for options provided by icalendar.el which are now obsolete.
* test/lisp/calendar/icalendar-resources/import-with-attachment.ics: New
input file to test import of base64-encoded attachments.
* icalendar-resources/import-time-format-12hr-blank.ics: New input file
to test import with a custom value of 'diary-icalendar-time-format'.

Two other new test files provide unit tests for the main functions of
the library:

* test/lisp/calendar/icalendar-parser-tests.el (Parser): Tests for
icalendar-parser.  Most of these are derived from examples in RFC5545,
to ensure the parser implements the standard.
* test/lisp/calendar/icalendar-recur-tests.el (Recurrence rules): Tests
for icalendar-recur.  Most of these are derived from examples in RFC5545,
to ensure the recurrence rule interpreter implements the standard.

A few of the existing test files for icalendar.el have also been
modified.  Besides the specific changes mentioned below, the modified
.ics files also now use CR-LF line endings, as required by RFC5545:

* test/lisp/calendar/icalendar-tests.el (icalendar-deftest-obsolete):
New macro.
* test/lisp/calendar/icalendar-resources/import-non-recurring-all-day.ics:
Correct a malformed VALUE parameter.
* test/lisp/calendar/icalendar-resources/import-rrule-anniversary.ics:
Correct representation of a recurring event.
*
test/lisp/calendar/icalendar-resources/import-rrule-daily-with-exceptions.ics:
Add a required VALUE parameter.
* test/lisp/calendar/icalendar-resources/import-rrule-daily.ics:
* test/lisp/calendar/icalendar-resources/import-rrule-monthly-no-end.ics:
* test/lisp/calendar/icalendar-resources/import-rrule-monthly-with-end.ics:
* test/lisp/calendar/icalendar-resources/import-rrule-weekly.ics:
Correct a malformed RRULE property.
This commit is contained in:
Richard Lawrence 2024-12-19 14:30:57 +01:00 committed by Stefan Monnier
parent 0aabe62b64
commit c685cf336a
106 changed files with 21901 additions and 303 deletions

View file

@ -1005,7 +1005,7 @@ entries.
* Adding to Diary:: Commands to create diary entries.
* Special Diary Entries:: Anniversaries, blocks of dates, cyclic entries, etc.
* Appointments:: Reminders when it's time to do something.
* Importing Diary:: Converting diary events to/from other formats.
* Diary Conversion:: Converting diary events to/from other formats.
@end menu
@node Format of Diary File
@ -1549,71 +1549,286 @@ clock. The command @kbd{M-x appt-add} adds entries to the appointment
list without affecting your diary file. You delete entries from the
appointment list with @kbd{M-x appt-delete}.
@node Importing Diary
@node Diary Conversion
@subsection Importing and Exporting Diary Entries
@cindex importing diary entries
@cindex diary import
@cindex diary export
You can transfer diary entries between Emacs diary files and a
variety of other formats.
You can transfer diary entries between Emacs diary files and other
formats.
@menu
* Diary iCalendar Import:: Importing iCalendar data to the Diary.
* Diary iCalendar Display:: Displaying iCalendar data without importing.
* Diary iCalendar Export:: Exporting Diary entries to iCalendar.
* Diary Outlook Import:: Importing Outlook appointments to the Diary.
@end menu
@node Diary iCalendar Import
@subsubsection Importing iCalendar data as Diary Entries
@cindex import iCalendar to diary
@cindex iCalendar support in diary
@dfn{iCalendar} is an Internet standard format for exchanging calendar
data. Many calendar applications can export and import data in
iCalendar format. iCalendar data is also often sent as email
attachments. iCalendar data usually uses the @file{.ics} file
extension, and is sent with the `text/calendar' @acronym{MIME} type in
email. (@xref{Mail Misc}, for more information on @acronym{MIME} and
email attachments.)
The @code{diary-icalendar} package allows you to make use of iCalendar
data with the Emacs diary. You can import and export data between
iCalendar format and your Emacs diary file, and also display iCalendar
data directly in the diary.
The following commands will import iCalendar data to your diary file:
@ftable @code
@item diary-icalendar-import-file
Imports an iCalendar file to an Emacs diary file.
@item diary-icalendar-import-buffer
Imports iCalendar data from the current buffer to an Emacs diary file.
@end ftable
@code{diary-icalendar-import-buffer} is also suitable for importing
iCalendar data from email attachments. For example, with the Rmail mail
client, you could use:
@example
(add-hook 'rmail-show-message-hook #'diary-icalendar-import-buffer)
@end example
Diary import depends on a number of user-customizable variables, which
are in the @code{diary-icalendar-import} customization group. You can
review and customize these variables with @kbd{M-x customize-group}.
@xref{Customization Groups}.
iCalendar data is grouped into @dfn{components} which represent calendar
events (the VEVENT component), tasks (VTODO), and other text data
(VJOURNAL). Because these components contain different types of data,
they are imported by different functions, determined by the following
variables:
@vtable @code
@item diary-icalendar-vevent-skeleton-command
Function to format VEVENT components for the diary.
@item diary-icalendar-vtodo-skeleton-command
Function to format VTODO components for the diary.
@item diary-icalendar-vjournal-skeleton-command
Function to format VJOURNAL components for the diary.
@end vtable
You can customize the format of the imported diary entries by writing
your own formatting functions. It is convenient (but not required) to
express such functions as templates called @dfn{skeletons}.
@ifinfo
@xref{Top, Autotyping, The Autotype Manual, autotype}, for more about
skeletons.
@end ifinfo
For example, suppose you only want to import the date, time, summary,
and location of each calendar event, and to write them on a single line
like:
@example
2025/11/11 Summary @@ Some Location
@end example
@noindent
Then you could write the import formatting function as a skeleton and
set it to the value of @code{diary-icalendar-vevent-skeleton-command} as
follows:
@lisp
@group
(require 'skeleton)
(define-skeleton simple-vevent
"Format a VEVENT summary and location on a single line"
nil
start-to-end & " " & summary & " "
(when location "@@ ") & location "\n")
(setopt diary-icalendar-vevent-skeleton-command #'simple-vevent)
@end group
@end lisp
The variables @code{start-to-end}, @code{summary} and @code{location} in
this example are dynamically bound to appropriate values when the
skeleton is called. See the docstring of
@code{diary-icalendar-vevent-skeleton-command} for more information.
Any errors encountered during import will be reported in a buffer named
@file{*icalendar-errors*}. You can review these errors with the
@code{next-error} command. @xref{Compilation Mode}. If you regularly
need to import malformed iCalendar data, there are several hooks
available for this purpose; see the @code{icalendar-parser}
customization group.
@node Diary iCalendar Display
@subsubsection Displaying iCalendar entries in the Diary
@cindex display iCalendar in diary
If you primarily store your calendar data outside of Emacs, but still
want to see it in the Emacs calendar and diary, you can do so by
including an iCalendar file from your diary file.
Suppose, for example, that you download your calendar from an
external server to a file called @file{Appointments.ics}. Then you can
include this file in your diary by writing a line like
@example
#include "path/to/Appointments.ics"
@end example
@noindent
in your diary file. You must also set up some hooks to display the
data in that file as diary entries and mark them in the calendar:
@lisp
@group
(add-hook 'diary-mark-entries-hook
#'diary-mark-included-diary-files)
(add-hook 'diary-nongregorian-marking-hook
#'diary-icalendar-mark-entries)
(add-hook 'diary-list-entries-hook
#'diary-include-other-diary-files)
(add-hook 'diary-nongregorian-listing-hook
#'diary-icalendar-display-entries)
@end group
@end lisp
@noindent
Events, tasks, and journal entries in @file{Appointments.ics} will then show
up on the appropriate days when you display the diary from the calendar.
@xref{Displaying the Diary}.
The advantage of doing this is that you don't need to synchronize the
data between the calendar server and your diary file. This is simpler
and more reliable than regularly importing and exporting between diary
and iCalendar format.
@findex diary-icalendar-mailcap-viewer
You can also display iCalendar attachments in email messages
without importing them to your diary file using the function
@code{diary-icalendar-mailcap-viewer}. You can add this function, for
example, to the variable @code{mailcap-user-mime-data}; see its docstring
for more information.
Displaying iCalendar entries uses the same infrastructure as importing
them, so customizing the import format will also change the format of
the displayed entries. @xref{Diary iCalendar Import}.
@node Diary iCalendar Export
@subsubsection Exporting Diary Entries to iCalendar
@cindex export diary to iCalendar
The following commands will export diary entries in iCalendar format:
@ftable @code
@item diary-icalendar-export-file
Exports a diary file to iCalendar format.
@item diary-icalendar-export-region
Exports a region of diary text to iCalendar format.
@end ftable
iCalendar export depends on a number of user-customizable variables, which
are in the @code{diary-icalendar-export} customization group. You can
review and customize these variables with @kbd{M-x customize-group}.
@xref{Customization Groups}.
Exporting diary entries to iCalendar requires you to respect certain
conventions in your diary, so that iCalendar properties can be parsed
from your diary entries.
By default, the exporter will use the first line of the entry (after the
date and time) as the iCalendar summary and the rest of the entry as its
iCalendar description. Other iCalendar properties can also be encoded in
the entry on separate lines, like this:
@example
@group
2025/11/11 Bender's birthday bash
Location: Robot House
Attendees:
Fry <philip.fry@@mars.edu>
Günter <guenter@@mars.edu>
@end group
@end example
@noindent
This format matches the format produced by the default import
functions.
@vindex diary-icalendar-address-regexp
@vindex diary-icalendar-class-regexp
@vindex diary-icalendar-description-regexp
@vindex diary-icalendar-location-regexp
@vindex diary-icalendar-organizer-regexp
@vindex diary-icalendar-status-regexp
@vindex diary-icalendar-summary-regexp
@vindex diary-icalendar-todo-regexp
@vindex diary-icalendar-uid-regexp
@vindex diary-icalendar-url-regexp
If you customize the import format, or you want to export diary entries
in a different format, you will need to customize the export variables
to detect the format of your diary entries. The most common iCalendar
properties are parsed from diary entries using regular expressions. See
the variables named @code{diary-icalendar-*-regexp} in the
@code{diary-icalendar-export} customization group to modify how these
properties are parsed.
@vindex diary-icalendar-other-properties-parser
If you need to export other iCalendar properties, or do more
complicated parsing, you can define a function to do so and set it as
the value of the variable @code{diary-icalendar-other-properties-parser};
see its docstring for details.
@vindex diary-icalendar-export-linewise
By default, the exporter assumes that each diary entry represents a
single iCalendar event. If you like to keep your diary in a
one-entry-per-day format, with different events on continuation
lines within the same entry, you can still export such entries as
distinct iCalendar events. To do this, set the variable
@code{diary-icalendar-export-linewise} to a non-nil value.
For example, after setting this variable, an entry like:
@example
@group
2025-05-03
9AM Lab meeting
Günter to present on new assay
Start experiment A
12:30-1:30PM Lunch with Phil
16:00 Experiment A finishes; move to freezer
@end group
@end example
@noindent
will be exported as four events, each on the same day, but with
different start times (except for the second event, ``Start experiment
A'', which has no start time). See the docstring of
@code{diary-icalendar-export-linewise} for more information.
@node Diary Outlook Import
@subsubsection Importing Outlook appointments as Diary Entries
@cindex diary outlook import
@vindex diary-outlook-formats
You can import diary entries from Outlook-generated appointment
@vindex diary-from-outlook-function
You can also import diary entries from Outlook-generated appointment
messages. While viewing such a message in Rmail or Gnus, do @kbd{M-x
diary-from-outlook} to import the entry. You can make this command
recognize additional appointment message formats by customizing the
variable @code{diary-outlook-formats}. Other mail clients can set
@code{diary-from-outlook-function} to an appropriate value.
@c FIXME the name of the RFC is hardly very relevant.
@cindex iCalendar support
The icalendar package allows you to transfer data between your Emacs
diary file and iCalendar files, which are defined in @cite{RFC
2445---Internet Calendaring and Scheduling Core Object Specification
(iCalendar)} (as well as the earlier vCalendar format).
@c Importing works for ordinary (i.e., non-recurring) events, but
@c (at present) may not work correctly (if at all) for recurring events.
@c Exporting of diary files into iCalendar files should work correctly
@c for most diary entries. This feature is a work in progress, so the
@c commands may evolve in future.
@findex icalendar-import-buffer
The command @code{icalendar-import-buffer} extracts
iCalendar data from the current buffer and adds it to your
diary file. This function is also suitable for automatic extraction of
iCalendar data; for example with the Rmail mail client one could use:
@example
(add-hook 'rmail-show-message-hook 'icalendar-import-buffer)
@end example
@findex icalendar-import-file
The command @code{icalendar-import-file} imports an iCalendar file
and adds the results to an Emacs diary file. For example:
@example
(icalendar-import-file "/here/is/calendar.ics"
"/there/goes/ical-diary")
@end example
@noindent
You can use an @code{#include} directive to add the import file contents
to the main diary file, if these are different files.
@iftex
@xref{Fancy Diary Display,,, emacs-xtra, Specialized Emacs Features}.
@end iftex
@ifnottex
@xref{Fancy Diary Display}.
@end ifnottex
@findex icalendar-export-file
@findex icalendar-export-region
@cindex export diary
Use @code{icalendar-export-file} to interactively export an entire
Emacs diary file to iCalendar format. To export only a part of a diary
file, mark the relevant area, and call @code{icalendar-export-region}.
In both cases, Emacs appends the result to the target file.
@node Daylight Saving
@section Daylight Saving Time

View file

@ -1020,7 +1020,14 @@ The Diary
* Adding to Diary:: Commands to create diary entries.
* Special Diary Entries:: Anniversaries, blocks of dates, cyclic entries, etc.
* Appointments:: Reminders when it's time to do something.
* Importing Diary:: Converting diary events to/from other formats.
* Diary Conversion:: Converting diary events to/from other formats.
Diary Conversion
* Diary iCalendar Import:: Importing iCalendar data to the Diary.
* Diary iCalendar Display:: Displaying iCalendar data without importing.
* Diary iCalendar Export:: Exporting Diary entries to iCalendar.
* Diary Outlook Import:: Importing Outlook appointments to the Diary.
@ifnottex
More advanced features of the Calendar and Diary

View file

@ -3084,6 +3084,34 @@ The user options 'calendar-mark-holidays-flag' and
'calendar-mark-diary-entries-flag' are not modified anymore when
changing the marking state in the calendar buffer.
*** New library for iCalendar data.
A new library has been added to the calendar for handling iCalendar
(RFC5545) data. The library is designed for reuse in other parts of
Emacs and in third-party packages. Package authors can find the new
library in the Emacs distribution under lisp/calendar/icalendar-*.el.
Most of the functions and variables in the older icalendar.el have been
marked obsolete and now suggest appropriate replacements from the new
library. diary-icalendar.el provides replacements for the diary-related
features from icalendar.el; see below.
** Diary
*** New user option 'diary-date-insertion-form'.
This user option determines how dates are inserted into the diary by
Lisp functions. Its value is a pseudo-pattern of the same type as in
'diary-date-forms'. It is used by 'diary-insert-entry' when inserting
entries from the calendar, or when importing them from other formats.
+++
*** New library 'diary-icalendar'.
This library reimplements features previously provided by icalendar.el:
import from iCalendar format to the diary, and export from the diary to
iCalendar. It also adds the ability to include iCalendar files in the
diary and display and mark their contents in the calendar without
importing them to the diary file. The library uses the new iCalendar
library (see above) and makes diary import and export more customizable.
** Calc
*** New user option 'calc-string-maximum-character'.
@ -3247,6 +3275,11 @@ value. Previously, only 'hi-lock-face-buffer' supported this.
* New Modes and Packages in Emacs 31.1
** New major mode 'icalendar-mode'.
A major mode for displaying and editing iCalendar (RFC5545) data. This
mode handles line unfolding and fontification, including highlighting
syntax errors in invalid data.
** New minor mode 'delete-trailing-whitespace-mode'.
A simple buffer-local mode that runs 'delete-trailing-whitespace'
before saving the buffer.

View file

@ -226,7 +226,7 @@ The result has the proper form for `calendar-daylight-savings-starts'."
(car candidate-rules)))
;; TODO it might be better to extract this information directly from
;; the system timezone database. But cross-platform...?
;; the system timezone database. But cross-platform...?
;; See thread
;; https://lists.gnu.org/r/emacs-pretest-bug/2006-11/msg00060.html
(defun calendar-dst-find-data (&optional time)
@ -309,7 +309,9 @@ system knows:
UTC-DIFF is an integer specifying the number of minutes difference between
standard time in the current time zone and Coordinated Universal Time
(Greenwich Mean Time). A negative value means west of Greenwich.
DST-OFFSET is an integer giving the daylight saving time offset in minutes.
DST-OFFSET is an integer giving the daylight saving time offset in minutes
relative to UTC-DIFF. (That is, the total UTC offset during daylight saving
time is UTC-DIFF + DST-OFFSET minutes.)
STD-ZONE is a string giving the name of the time zone when no seasonal time
adjustment is in effect.
DST-ZONE is a string giving the name of the time zone when there is a seasonal

View file

@ -431,11 +431,7 @@ Interactively, prompt for YEAR and DAY number."
(calendar-day-number (calendar-current-date))
last)))
(list year day)))
(calendar-goto-date
(calendar-gregorian-from-absolute
(if (< 0 day)
(+ -1 day (calendar-absolute-from-gregorian (list 1 1 year)))
(+ 1 day (calendar-absolute-from-gregorian (list 12 31 year))))))
(calendar-goto-date (calendar-date-from-day-of-year year day))
(or noecho (calendar-print-day-of-year)))
(provide 'cal-move)

View file

@ -871,7 +871,15 @@ current word of the diary entry, so in no case can the pattern match more than
a portion of the first word of the diary entry.
For examples of three common styles, see `diary-american-date-forms',
`diary-european-date-forms', and `diary-iso-date-forms'."
`diary-european-date-forms', and `diary-iso-date-forms'.
If you customize this variable, you should also customize the variable
`diary-date-insertion-form' to contain a pseudo-pattern which produces
dates that match one of the forms in this variable. (If
`diary-date-insertion-form' does not correspond to one of the patterns
in this variable, then the diary will not recognize such dates,
including those inserted into the diary from the calendar with
`diary-insert-entry'.)"
:type '(repeat (choice (cons :tag "Backup"
:value (backup . nil)
(const backup)
@ -895,6 +903,52 @@ For examples of three common styles, see `diary-american-date-forms',
(diary))))
:group 'diary)
(defconst diary-american-date-insertion-form '(month "/" day "/" year)
"Pseudo-pattern for American dates in `diary-date-insertion-form'")
(defconst diary-european-date-insertion-form '(day "/" month "/" year)
"Pseudo-pattern for European dates in `diary-date-insertion-form'")
(defconst diary-iso-date-insertion-form '(year "/" month "/" day)
"Pseudo-pattern for ISO dates in `diary-date-insertion-form'")
(defcustom diary-date-insertion-form
(cond ((eq calendar-date-style 'iso) diary-iso-date-insertion-form)
((eq calendar-date-style 'european) diary-european-date-insertion-form)
(t diary-american-date-insertion-form))
"Pseudo-pattern describing how to format a date for a new diary entry.
A pseudo-pattern is a list of expressions that can include the symbols
`month', `day', and `year' (all numbers in string form), and `monthname'
and `dayname' (both alphabetic strings). For example, a typical American
form would be
(month \"/\" day \"/\" (substring year -2))
whereas
((format \"%9s, %9s %2s, %4s\" dayname monthname day year))
would give the usual American style in fixed-length fields.
This pattern will be used by `calendar-date-string' (which see) to
format dates when inserting them with `diary-insert-entry', or when
importing them from other formats into the diary.
If you customize this variable, you should also customize the variable
`diary-date-forms' to include a pseudo-pattern which matches dates
produced by this pattern. (If there is no corresponding pattern in
`diary-date-forms', then the diary will not recognize such dates,
including those inserted into the diary from the calendar with
`diary-insert-entry'.)"
:version "31.1"
:type 'sexp
:risky t
:set-after '(calendar-date-style diary-american-date-insertion-form
diary-european-date-insertion-form
diary-iso-date-insertion-form)
:group 'diary)
;; Next three are provided to aid in setting calendar-date-display-form.
(defcustom calendar-iso-date-display-form '((format "%s-%.2d-%.2d" year
(string-to-number month)
@ -1028,7 +1082,9 @@ The valid styles are described in the documentation of `calendar-date-style'."
calendar-month-header
(symbol-value (intern-soft (format "calendar-%s-month-header" style)))
diary-date-forms
(symbol-value (intern-soft (format "diary-%s-date-forms" style))))
(symbol-value (intern-soft (format "diary-%s-date-forms" style)))
diary-date-insertion-form
(symbol-value (intern-soft (format "diary-%s-date-insertion-form" style))))
(calendar-redraw)
(calendar-update-mode-line))
@ -1298,6 +1354,16 @@ return negative results."
(/ offset-years 400)
(calendar-day-number '(12 31 -1))))))) ; days in year 1 BC
;; This function is the inverse of `calendar-day-number':
(defun calendar-date-from-day-of-year (year dayno)
"Return the date of the DAYNO-th day in YEAR.
DAYNO must be an integer between -366 and 366."
(calendar-gregorian-from-absolute
(+ (if (< dayno 0)
(+ 1 dayno (if (calendar-leap-year-p year) 366 365))
dayno)
(calendar-absolute-from-gregorian (list 12 31 (1- year))))))
;;;###autoload
(defun calendar (&optional arg)
"Display a three-month Gregorian calendar.

File diff suppressed because it is too large Load diff

View file

@ -2120,8 +2120,9 @@ show the diary buffer."
Prefix argument ARG makes the entry nonmarking."
(interactive
(list current-prefix-arg last-nonmenu-event))
(diary-make-entry (calendar-date-string (calendar-cursor-to-date t event) t t)
arg))
(calendar-dlet ((calendar-date-display-form diary-date-insertion-form))
(diary-make-entry (calendar-date-string (calendar-cursor-to-date t event) t t)
arg)))
;;;###cal-autoload
(defun diary-insert-weekly-entry (arg)
@ -2318,6 +2319,7 @@ return a font-lock pattern matching array of MONTHS and marking SYMBOL."
;; Accepted formats: 10:00 10.00 10h00 10h 10am 10:00am 10.00am
;; Use of "." as a separator annoyingly matches numbers, eg "123.45".
;; Hence often prefix this with "\\(^\\|\\s-\\)."
;; FIXME.
(concat "[0-9]?[0-9]\\([AaPp][mM]\\|\\("
"[Hh]\\([0-9][0-9]\\)?\\|[:.][0-9][0-9]"
"\\)\\([AaPp][Mm]\\)?\\)")

View file

@ -0,0 +1,927 @@
;;; icalendar-ast.el --- Syntax trees for iCalendar -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Free Software Foundation, Inc.
;; Author: Richard Lawrence <rwl@recursewithless.net>
;; Created: October 2024
;; Keywords: calendar
;; Human-Keywords: calendar, iCalendar
;; This file is part of GNU Emacs.
;; This file is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this file. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This file defines the abstract syntax tree representation for
;; iCalendar data. The AST is based on `org-element-ast' (which see;
;; that feature will eventually be renamed and moved out of the Org tree
;; into the main tree).
;; This file contains low-level functions for constructing and
;; manipulating the AST, most of which are minimal wrappers around the
;; functions provided by `org-element-ast'. This low-level API is
;; primarily used by `icalendar-parser'. It also contains a higher-level
;; API for constructing AST nodes in Lisp code. Finally, it defines
;; functions for validating AST nodes.
;; There are three main pieces of data in an AST node: its type, its
;; value, and its child nodes. Nodes which represent iCalendar
;; components have no values; they are simply containers for their
;; children. Nodes which represent data of the base iCalendar data
;; types have no children; they are the leaf nodes in the syntax tree.
;; The main low-level accessors for these data in AST nodes are:
;;
;; `icalendar-ast-node-type'
;; `icalendar-ast-node-value'
;; `icalendar-ast-node-children'
;; `icalendar-ast-node-children-of'
;; `icalendar-ast-node-first-child-of'
;; To construct AST nodes in Lisp code, see especially the high-level macros:
;;
;; `icalendar-make-vcalendar'
;; `icalendar-make-vtimezone'
;; `icalendar-make-vevent'
;; `icalendar-make-vtodo'
;; `icalendar-make-vjournal'
;; `icalendar-make-property'
;; `icalendar-make-param'
;;
;; These macros wrap the macro `icalendar-make-node-from-templates',
;; which allows writing iCalendar syntax tree nodes as Lisp templates.
;; Constructing nodes with these macros automatically validates them
;; with the function `icalendar-ast-node-valid-p', which signals an
;; `icalendar-validation-error' if the node is not valid acccording to
;; RFC5545.
;;; Code:
(eval-when-compile (require 'icalendar-macs))
(require 'icalendar)
(require 'org-element-ast)
(require 'cl-lib)
;;; Type symbols and metadata
;; All nodes in the syntax tree have a type symbol as their first element.
;; We use the following symbol properties (all prefixed with 'icalendar-')
;; to associate type symbols with various important data about the type:
;;
;; is-type - t (marks this symbol as an icalendar type)
;; is-value, is-param, is-property, or is-component - t
;; (specifies what sort of value this type represents)
;; list-sep - for property and parameters types, a string (typically
;; "," or ";") which separates individual printed values, if the
;; type allows lists of values. If this is non-nil, syntax nodes of
;; this type should always have a list of values in their VALUE
;; field (even if there is only one value)
;; matcher - a function to match this type. This function matches the
;; regular expression defined under the type's name; it is used to provide
;; syntax highlighting in `icalendar-mode'
;; begin-rx, end-rx - for component-types, an `rx' regular expression which
;; matches the BEGIN and END lines that form its boundaries
;; value-rx - an `rx' regular expression which matches individual values
;; of this type, with no consideration for quoting or lists of values.
;; (For value types, this is just a synonym for the rx definition
;; under the type's symbol)
;; values-rx - for types that accept lists of values, an `rx' regular
;; expression which matches the whole list (including quotes, if required)
;; full-value-rx - for property and parameter types, an `rx' regular
;; expression which matches a valid value expression in group 2, or
;; an invalid value in group 3
;; value-reader - for value types, a function which creates syntax
;; nodes of this type given a string representing their value
;; value-printer - for value types, a function to print individual
;; values of this type. It accepts a value and returns its string
;; representation.
;; default-value - for property and parameter types, a string
;; representing a default value for nodes of this type. This is the
;; value assumed when no node of this type is present in the
;; relevant part of the syntax tree.
;; substitute-value - for parameter types, a string representing a value
;; which will be substituted at parse times for unrecognized values.
;; (This is normally the same as default-value, but differs from it
;; in at least one case in RFC5545, thus it is stored separately.)
;; default-type - for property types which can accept values of multiple
;; types, this is the default type when no type for the value is
;; specified in the parameters. Any type of value other than this
;; one requires a VALUE=... parameter when the property is read or printed.
;; other-types - for property types which can accept values of multiple types,
;; this is a list of other types that the property can accept.
;; value-type - for param types, this is the value type which the parameter
;; can accept.
;; child-spec - for property and component types, a plist describing the
;; required and optional child nodes. See `icalendar-define-property' and
;; `icalendar-define-component' for details.
;; other-validator - a function to perform type-specific validation
;; for nodes of this type. If present, this function will be called
;; by `icalendar-ast-node-valid-p' during validation.
;; type-documentation - a string documenting the type. This documentation is
;; printed in the help buffer when `describe-symbol' is called on TYPE.
;; link - a hyperlink to the documentation of the type in the relevant standard
(defun ical:type-symbol-p (symbol)
"Return non-nil if SYMBOL is an iCalendar type symbol.
This function only checks that SYMBOL has been marked as a type;
it returns t for value types defined by `icalendar-define-type',
but also e.g. for types defined by `icalendar-define-param' and
`icalendar-define-property'. To check that SYMBOL names a value
type for property or parameter values, see
`icalendar-value-type-symbol-p' and
`icalendar-printable-value-type-symbol-p'."
(and (symbolp symbol)
(get symbol 'ical:is-type)))
(defun ical:value-type-symbol-p (symbol)
"Return non-nil if SYMBOL is a type symbol for a value type.
This means that SYMBOL must both satisfy `icalendar-type-symbol-p' and
have the property `icalendar-is-value'. It does not require the type to
be associated with a print name in `icalendar-value-types'; for that see
`icalendar-printable-value-type-symbol-p'."
(and (ical:type-symbol-p symbol)
(get symbol 'ical:is-value)))
(defun ical:expects-list-of-values-p (type)
"Return non-nil if TYPE expects a list of values.
This is never t for value types or component types. For property and
parameter types defined with `icalendar-define-param' and
`icalendar-define-property', it is true if the :list-sep argument was
specified in the definition."
(and (ical:type-symbol-p type)
(get type 'ical:list-sep)))
(defun ical:param-type-symbol-p (type)
"Return non-nil if TYPE is a type symbol for an iCalendar parameter."
(and (ical:type-symbol-p type)
(get type 'ical:is-param)))
(defun ical:property-type-symbol-p (type)
"Return non-nil if TYPE is a type symbol for an iCalendar property."
(and (ical:type-symbol-p type)
(get type 'ical:is-property)))
(defun ical:component-type-symbol-p (type)
"Return non-nil if TYPE is a type symbol for an iCalendar component."
(and (ical:type-symbol-p type)
(get type 'ical:is-component)))
;; TODO: we could define other accessors here for the other metadata
;; properties, but at the moment I see no advantage to this; they would
;; all just be long-winded wrappers around `get'.
;; The basic, low-level API for the AST, mostly intended for use by
;; `icalendar-parser'. These functions are mostly aliases and simple
;; wrappers around functions provided by `org-element-ast', which does
;; the heavy lifting.
(defalias 'ical:ast-node-type #'org-element-type)
(defsubst ical:ast-node-value (node)
"Return the value of iCalendar syntax node NODE.
In component nodes, this is nil. Otherwise, it is a syntax node
representing an iCalendar (property or parameter) value."
(org-element-property :value node))
(defalias 'ical:ast-node-children #'org-element-contents)
;; TODO: probably don't want &rest form for this
(defalias 'ical:ast-node-set-children #'org-element-set-contents)
(defalias 'ical:ast-node-adopt-children #'org-element-adopt-elements)
(defalias 'ical:ast-node-meta-get #'org-element-property)
(defalias 'ical:ast-node-meta-set #'org-element-put-property)
(defun ical:ast-node-set-type (node type)
"Set the type of iCalendar syntax node NODE to TYPE.
This function is probably not what you want! It directly modifies the
type of NODE in-place, which could make the node invalid if its value or
children do not match the new TYPE. If you do not know in advance that
the data in NODE is compatible with the new TYPE, it is better to
construct a new syntax node."
(setcar node type))
(defun ical:ast-node-set-value (node value)
"Set the value of iCalendar syntax node NODE to VALUE."
(ical:ast-node-meta-set node :value value))
(defun ical:make-ast-node (type props &optional children)
"Construct a syntax node of TYPE with meta-properties PROPS and CHILDREN.
This is a low-level constructor. If you are constructing iCalendar
syntax nodes directly in Lisp code, consider using one of the
higher-level macros based on `icalendar-make-node-from-templates'
instead, which expand to calls to this function but also perform type
checking and validation.
TYPE should be an iCalendar type symbol. CHILDREN, if given, should be
a list of syntax nodes. In property nodes, these should be the
parameters of the property. In component nodes, these should be the
properties or subcomponents of the component. CHILDREN should otherwise
be nil.
PROPS should be a plist with any of the following keywords:
:value - in value nodes, this should be the Elisp value parsed from a
property or parameter's value string. In parameter and property nodes,
this should be a value node or list of value nodes. In component
nodes, it should not be present.
:buffer - buffer from which VALUE was parsed
:begin - position at which this node begins in BUFFER
:end - position at which this node ends in BUFFER
:value-begin - position at which VALUE begins in BUFFER
:value-end - position at which VALUE ends in BUFFER
:original-value - a string containing the original, uninterpreted value
of the node. This can differ from (a string represented by) VALUE
if e.g. a default VALUE was substituted for an unrecognized but
syntactically correct value.
:original-name - a string containing the original, uninterpreted name
of the parameter, property or component this node represents.
This can differ from (a string representing) TYPE
if e.g. a default TYPE was substituted for an unrecognized but
syntactically correct one."
;; automatically mark :value as a "secondary property" for org-element-ast
(let ((full-props (if (plist-member props :value)
(plist-put props :secondary (list :value))
props)))
(apply #'org-element-create type full-props children)))
(defun ical:ast-node-p (val)
"Return non-nil if VAL is an iCalendar syntax node."
(and (listp val)
(length> val 1)
(ical:type-symbol-p (ical:ast-node-type val))
(plistp (cadr val))
(listp (ical:ast-node-children val))))
(defun ical:param-node-p (node)
"Return non-nil if NODE is a syntax node whose type is a parameter type."
(and (ical:ast-node-p node)
(ical:param-type-symbol-p (ical:ast-node-type node))))
(defun ical:property-node-p (node)
"Return non-nil if NODE is a syntax node whose type is a property type."
(and (ical:ast-node-p node)
(ical:property-type-symbol-p (ical:ast-node-type node))))
(defun ical:component-node-p (node)
"Return non-nil if NODE is a syntax node whose type is a component type."
(and (ical:ast-node-p node)
(ical:component-type-symbol-p (ical:ast-node-type node))))
(defun ical:ast-node-first-child-of (type node)
"Return the first child of NODE of type TYPE, or nil."
(assq type (ical:ast-node-children node)))
(defun ical:ast-node-children-of (type node)
"Return a list of all the children of NODE of type TYPE."
(seq-filter (lambda (c) (eq type (ical:ast-node-type c)))
(ical:ast-node-children node)))
;; A high-level API for constructing iCalendar syntax nodes in Lisp code:
(declare-function ical:list-of-p "icalendar-parser")
(defun ical:type-of (value &optional types)
"Find the iCalendar type symbol for the type to which VALUE belongs.
TYPES, if specified, should be a list of type symbols to check.
TYPES defaults to all type symbols listed in `icalendar-value-types'."
(require 'icalendar-parser) ; for ical:value-types, ical:list-of-p
(catch 'found
(when (ical:ast-node-p value)
(throw 'found (ical:ast-node-type value)))
;; FIXME: the warning here is spurious, given that icalendar-parser
;; is require'd above:
(with-suppressed-warnings ((free-vars ical:value-types))
(dolist (type (or types (mapcar #'cdr ical:value-types)))
(if (ical:expects-list-of-values-p type)
(when (ical:list-of-p value type)
(throw 'found type))
(when (cl-typep value type)
(throw 'found type)))))))
;; A more flexible constructor for value nodes which can choose the
;; correct type from a list. This helps keep templates succinct and easy
;; to use in `icalendar-make-node-from-templates', and related macros
;; below.
(defun ical:make-value-node-of (type value)
"Make an iCalendar syntax node of type TYPE containing VALUE as its value.
TYPE should be a symbol for an iCalendar value type, and VALUE should be
a value of that type. If TYPE is the symbol \\='plain-text, VALUE should
be a string, and in that case VALUE is returned as-is.
TYPE may also be a list of type symbols; in that case, the first type in
the list which VALUE satisfies is used as the returned node's type. If
the list is nil, VALUE will be checked against all types in
`icalendar-value-types'.
If VALUE is nil, and `icalendar-boolean' is not (in) TYPE, nil is
returned. Otherwise, a \\='wrong-type-argument error is signaled if
VALUE does not satisfy (any type in) TYPE."
(require 'icalendar-parser)
(cond
((and (null value)
(not (if (listp type) (memq 'ical:boolean type)
(eq 'ical:boolean type))))
;; Instead of signaling an error, we just return nil in this case.
;; This allows the `ical:make-*' macros higher up the stack to
;; filter out templates that evaluate to nil at run time:
nil)
((eq type 'plain-text)
(unless (stringp value)
(signal 'wrong-type-argument (list 'stringp value)))
value)
((symbolp type)
(unless (ical:value-type-symbol-p type)
(signal 'wrong-type-argument (list 'icalendar-value-type-symbol-p type)))
(if (ical:expects-list-of-values-p type)
(unless (ical:list-of-p value type)
(signal 'wrong-type-argument (list `(list-of ,type) value)))
(unless (cl-typep value type)
(signal 'wrong-type-argument (list type value)))
(ical:make-ast-node type (list :value value))))
((listp type)
;; N.B. nil is allowed; in that case, `ical:type-of' will check all
;; types in `ical:value-types':
(let ((the-type (ical:type-of value type)))
(if the-type
(ical:make-ast-node the-type (list :value value))
(signal 'wrong-type-argument
(list (if (length> type 1) (cons 'or type) (car type))
value)))))
(t (signal 'wrong-type-argument (list '(or symbolp listp) type)))))
(defmacro ical:make-param (type value)
"Construct an iCalendar parameter node of TYPE with value VALUE.
TYPE should be an iCalendar type symbol satisfying
`icalendar-param-type-symbol-p'; it should not be quoted.
VALUE should evaluate to a value appropriate for TYPE. In particular, if
TYPE expects a list of values (see `icalendar-expects-list-p'), VALUE
should be such a list. If necessary, the value(s) in VALUE will be
wrapped in syntax nodes indicating their type.
For example,
(icalendar-make-param icalendar-deltoparam
(list \"mailto:minionA@example.com\" \"mailto:minionB@example.com\"))
will return an `icalendar-deltoparam' node whose value is a list of
`icalendar-cal-address' nodes containing the two addresses.
The resulting syntax node is checked for validity by
`icalendar-ast-node-valid-p' before it is returned."
;; TODO: support `ical:otherparam'
(unless (ical:param-type-symbol-p type)
(error "Not an iCalendar param type: %s" type))
(let ((value-type (or (get type 'ical:value-type) 'plain-text))
(needs-list (ical:expects-list-of-values-p type)))
`(let* ((raw-value ,value)
(value-type (quote ,value-type))
(value
,(if needs-list
'(if (seq-every-p #'ical:ast-node-p raw-value)
raw-value
(mapcar
(lambda (c) (ical:make-value-node-of value-type c))
raw-value))
'(if (ical:ast-node-p raw-value)
raw-value
(ical:make-value-node-of value-type raw-value)))))
(when value
(ical:ast-node-valid-p
(ical:make-ast-node
(quote ,type)
(list :value value)))))))
(defmacro ical:make-property (type value &rest param-templates)
"Construct an iCalendar property node of TYPE with value VALUE.
TYPE should be an iCalendar type symbol satisfying
`icalendar-property-type-symbol-p'; it should not be quoted.
VALUE should evaluate to a value appropriate for TYPE. In particular, if
TYPE expects a list of values (see
`icalendar-expects-list-of-values-p'), VALUE should be such a list. If
necessary, the value(s) in VALUE will be wrapped in syntax nodes
indicating their type. If VALUE is not of the default value type for
TYPE, an `icalendar-valuetypeparam' will automatically be added to TEMPLATES.
Each element of PARAM-TEMPLATES should represent a parameter node; see
`icalendar-make-node-from-templates' for the format of such TEMPLATES.
A template can also have the form (@ L), where L evaluates to a list of
parameter nodes to be added to the component.
PARAM-TEMPLATES which evaluate to nil are removed when the property node
is constructed.
For example,
(icalendar-make-property icalendar-rdate (list \\='(2 1 2025) \\='(3 1 2025)))
will return an `icalendar-rdate' node whose value is a list of
`icalendar-date' nodes containing the dates above as their values.
The resulting syntax node is checked for validity by
`icalendar-ast-node-valid-p' before it is returned."
;; TODO: support `ical:other-property', maybe like
;; (ical:other-property "X-NAME" value ...)
(unless (ical:property-type-symbol-p type)
(error "Not an iCalendar property type: %s" type))
(let ((value-types (cons (get type 'ical:default-type)
(get type 'ical:other-types)))
(needs-list (ical:expects-list-of-values-p type))
params-expr children lists-of-children)
(dolist (c param-templates)
(cond ((and (listp c) (ical:type-symbol-p (car c)))
;; c is a template for a child node, so it should be
;; recursively expanded:
(push (cons 'ical:make-node-from-templates c)
children))
((and (listp c) (eq '@ (car c)))
;; c is a template (@ L) where L evaluates to a list of children:
(push (cadr c) lists-of-children))
(t
;; otherwise, just pass c through as is; this allows
;; interleaving templates with other expressions that
;; evaluate to syntax nodes:
(push c children))))
(when (or children lists-of-children)
(setq params-expr
`(seq-filter #'identity
(append (list ,@children) ,@lists-of-children))))
`(let* ((raw-value ,value)
(value-types (quote ,value-types))
(value
,(if needs-list
'(if (seq-every-p #'ical:ast-node-p raw-value)
raw-value
(mapcar
(lambda (c) (ical:make-value-node-of value-types c))
raw-value))
'(if (ical:ast-node-p raw-value)
raw-value
(ical:make-value-node-of value-types raw-value)))))
(when value
(ical:ast-node-valid-p
(ical:maybe-add-value-param
(ical:make-ast-node
(quote ,type)
(list :value value)
,params-expr)))))))
(defmacro ical:make-component (type &rest templates)
"Construct an iCalendar component node of TYPE from TEMPLATES.
TYPE should be an iCalendar type symbol satisfying
`icalendar-component-type-symbol-p'; it should not be quoted.
Each expression in TEMPLATES should represent a child node of the
component; see `icalendar-make-node-from-templates' for the format of
such TEMPLATES. A template can also have the form (@ L), where L
evaluates to a list of child nodes to be added to the component.
Any value in TEMPLATES that evaluates to nil will be removed before the
component node is constructed.
If TYPE is `icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal',
or `icalendar-vfreebusy', the properties `icalendar-dtstamp' and
`icalendar-uid' will be automatically provided, if they are absent in
TEMPLATES. Likewise, if TYPE is `icalendar-vcalendar', the properties
`icalendar-prodid', `icalendar-version', and `icalendar-calscale' will
be automatically provided if absent.
For example,
(icalendar-make-component icalendar-vevent
(icalendar-summary \"Party\")
(icalendar-location \"Robot House\")
(@ list-of-other-properties))
will return an `icalendar-vevent' node containing the provided
properties as well as `icalendar-dtstamp' and `icalendar-uid'
properties.
The resulting syntax node is checked for validity by
`icalendar-ast-node-valid-p' before it is returned."
;; TODO: support `ical:other-component', maybe like
;; (ical:other-component (:x-name "X-NAME") templates ...)
(unless (ical:component-type-symbol-p type)
(error "Not an iCalendar component type: %s" type))
;; Add templates for required properties automatically if we can:
(when (memq type '(ical:vevent ical:vtodo ical:vjournal ical:vfreebusy))
(unless (assq 'ical:dtstamp templates)
(push '(ical:dtstamp (decode-time nil t))
templates))
(unless (assq 'ical:uid templates)
(push `(ical:uid ,(ical:make-uid templates))
templates)))
(when (eq type 'ical:vcalendar)
(unless (assq 'ical:prodid templates)
(push `(ical:prodid ,ical:vcalendar-prodid)
templates))
(unless (assq 'ical:version templates)
(push `(ical:version ,ical:vcalendar-version)
templates))
(unless (assq 'ical:calscale templates)
(push '(ical:calscale "GREGORIAN")
templates)))
(when (null templates)
(error "At least one template is required"))
(let (children lists-of-children)
(dolist (c templates)
(cond ((and (listp c) (ical:type-symbol-p (car c)))
;; c is a template for a child node, so it should be
;; recursively expanded:
(push (cons 'ical:make-node-from-templates c)
children))
((and (listp c) (eq '@ (car c)))
;; c is a template (@ L) where L evaluates to a list of children:
(push (cadr c) lists-of-children))
(t
;; otherwise, just pass c through as is; this allows
;; interleaving templates with other expressions that
;; evaluate to syntax nodes:
(push c children))))
(setq children (nreverse children)
lists-of-children (nreverse lists-of-children))
(when (or children lists-of-children)
`(ical:ast-node-valid-p
(ical:make-ast-node
(quote ,type)
nil
(seq-filter #'identity
(append (list ,@children) ,@lists-of-children)))))))
;; TODO: allow disabling the validity check??
(defmacro ical:make-node-from-templates (type &rest templates)
"Construct an iCalendar syntax node of TYPE from TEMPLATES.
TYPE should be an iCalendar type symbol; it should not be quoted. This
macro (and the derived macros `icalendar-make-vcalendar',
`icalendar-make-vevent', `icalendar-make-vtodo',
`icalendar-make-vjournal', `icalendar-make-vfreebusy',
`icalendar-make-valarm', `icalendar-make-vtimezone',
`icalendar-make-standard', and `icalendar-make-daylight') makes it easy
to write iCalendar syntax nodes of TYPE as Lisp code.
Each expression in TEMPLATES represents a child node of the constructed
node. It must either evaluate to such a node, or it must have one of
the following forms:
\(VALUE-TYPE VALUE) - constructs a node of VALUE-TYPE containing the
value VALUE.
\(PARAM-TYPE VALUE) - constructs a parameter node of PARAM-TYPE
containing the VALUE.
\(PROPERTY-TYPE VALUE [PARAM ...]) - constructs a property node of
PROPERTY-TYPE containing the value VALUE and PARAMs as child
nodes. Each PARAM should be a template (PARAM-TYPE VALUE), as above,
or any other expression that evaluates to a parameter node.
\(COMPONENT-TYPE CHILD [CHILD ...]) - constructs a component node of
COMPONENT-TYPE with CHILDs as child nodes. Each CHILD should either be
a template for a property (as above), a template for a
sub-component (of the same form), or any other expression that
evaluates to an iCalendar syntax node.
If TYPE is an iCalendar component or property type, a TEMPLATE can also
have the form (@ L), where L evaluates to a list of child nodes to be
added to the component or property node.
For example, an iCalendar VEVENT could be written like this:
(icalendar-make-node-from-templates icalendar-vevent
(icalendar-dtstamp (decode-time (current-time) 0))
(icalendar-uid \"some-unique-id\")
(icalendar-summary \"Party\")
(icalendar-location \"Robot House\")
(icalendar-organizer \"mailto:bender@mars.edu\")
(icalendar-attendee \"mailto:philip.j.fry@mars.edu\"
(icalendar-partstatparam \"ACCEPTED\"))
(icalendar-attendee \"mailto:gunther@mars.edu\"
(icalendar-partstatparam \"DECLINED\"))
(icalendar-categories (list \"MISCHIEF\" \"DOUBLE SECRET PROBATION\"))
(icalendar-dtstart (icalendar-make-date-time :year 3003 :month 3 :day 13
:hour 22 :minute 0 :second 0)
(icalendar-tzidparam \"Mars/University_Time\")))
Before the constructed node is returned, it is validated by
`icalendar-ast-node-valid-p'."
(cond
((not (ical:type-symbol-p type))
(error "Not an iCalendar type symbol: %s" type))
((ical:value-type-symbol-p type)
`(ical:ast-node-valid-p
(ical:make-value-node-of (quote ,type) ,(car templates))))
((ical:param-type-symbol-p type)
`(ical:make-param ,type ,(car templates)))
((ical:property-type-symbol-p type)
`(ical:make-property ,type ,(car templates) ,@(cdr templates)))
((ical:component-type-symbol-p type)
`(ical:make-component ,type ,@templates))))
(defmacro ical:make-vcalendar (&rest templates)
"Construct an iCalendar VCALENDAR object from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-vcalendar' for the permissible child types.
If TEMPLATES does not contain templates for the `icalendar-prodid' and
`icalendar-version' properties, they will be automatically added; see
the variables `icalendar-vcalendar-prodid' and
`icalendar-vcalendar-version'."
`(ical:make-node-from-templates ical:vcalendar ,@templates))
(defmacro ical:make-vevent (&rest templates)
"Construct an iCalendar VEVENT node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-vevent' for the permissible child types.
If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
`icalendar-uid' properties (both required), they will be automatically
provided."
`(ical:make-node-from-templates ical:vevent ,@templates))
(defmacro ical:make-vtodo (&rest templates)
"Construct an iCalendar VTODO node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-vtodo' for the permissible child types.
If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
`icalendar-uid' properties (both required), they will be automatically
provided."
`(ical:make-node-from-templates ical:vtodo ,@templates))
(defmacro ical:make-vjournal (&rest templates)
"Construct an iCalendar VJOURNAL node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-vjournal' for the permissible child types.
If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
`icalendar-uid' properties (both required), they will be automatically
provided."
`(ical:make-node-from-templates ical:vjournal ,@templates))
(defmacro ical:make-vfreebusy (&rest templates)
"Construct an iCalendar VFREEBUSY node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-vfreebusy' for the permissible child types.
If TEMPLATES does not contain templates for the `icalendar-dtstamp' and
`icalendar-uid' properties (both required), they will be automatically
provided."
`(ical:make-node-from-templates ical:vfreebusy ,@templates))
(defmacro ical:make-valarm (&rest templates)
"Construct an iCalendar VALARM node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-valarm' for the permissible child types."
`(ical:make-node-from-templates ical:valarm ,@templates))
(defmacro ical:make-vtimezone (&rest templates)
"Construct an iCalendar VTIMEZONE node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-vtimezone' for the permissible child types."
`(ical:make-node-from-templates ical:vtimezone ,@templates))
(defmacro ical:make-standard (&rest templates)
"Construct an iCalendar STANDARD node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-standard' for the permissible child types."
`(ical:make-node-from-templates ical:standard ,@templates))
(defmacro ical:make-daylight (&rest templates)
"Construct an iCalendar DAYLIGHT node from TEMPLATES.
See `icalendar-make-node-from-templates' for the format of TEMPLATES.
See `icalendar-daylight' for the permissible child types."
`(ical:make-node-from-templates ical:daylight ,@templates))
;;; Validation:
;; Errors at the validation stage:
;; e.g. property/param values did not match, or are of the wrong type,
;; or required properties not present in a component
(define-error 'ical:validation-error "Invalid iCalendar data" 'ical:error)
(cl-defun ical:signal-validation-error (msg &key node (severity 2))
(signal 'ical:validation-error
(list :message msg
:buffer (ical:ast-node-meta-get :buffer node)
:position (ical:ast-node-meta-get :begin node)
:severity severity
:node node)))
(defun ical:ast-node-required-child-p (child parent)
"Return non-nil if CHILD is required by PARENT's node type."
(let* ((type (ical:ast-node-type parent))
(child-spec (get type 'ical:child-spec))
(child-type (ical:ast-node-type child)))
(or (memq child-type (plist-get child-spec :one))
(memq child-type (plist-get child-spec :one-or-more)))))
(declare-function ical:printable-value-type-symbol-p "icalendar-parser")
(defun ical:ast-node-valid-value-p (node)
"Validate that NODE's value satisfies the requirements of its type.
Signals an `icalendar-validation-error' if NODE's value is
invalid, or returns NODE."
(require 'icalendar-parser) ; for ical:printable-value-type-symbol-p
(let* ((type (ical:ast-node-type node))
(value (ical:ast-node-value node))
(valtype-param (when (ical:property-type-symbol-p type)
(ical:with-param-of node 'ical:valuetypeparam)))
(allowed-types
(cond ((ical:printable-value-type-symbol-p valtype-param)
;; with an explicit `VALUE=sometype' param, this is the
;; only allowed type:
(list valtype-param))
((and (ical:param-type-symbol-p type)
(get type 'ical:value-type))
(list (get type 'ical:value-type)))
((ical:property-type-symbol-p type)
(cons (get type 'ical:default-type)
(get type 'ical:other-types)))
(t nil))))
(cond ((ical:value-type-symbol-p type)
(unless (cl-typep value type) ; see `ical:define-type'
(ical:signal-validation-error
(format "Invalid value for `%s' node: %s" type value)
:node node))
node)
((ical:component-node-p node)
;; component types have no value, so no need to check anything
node)
((and (or (ical:param-type-symbol-p type)
(ical:property-type-symbol-p type))
(null (get type 'ical:value-type))
(stringp value))
;; property and param nodes with no value type are assumed to contain
;; strings which match a value regex:
(unless (string-match (rx-to-string (get type 'ical:value-rx)) value)
(ical:signal-validation-error
(format "Invalid string value for `%s' node: %s" type value)
:node node))
node)
;; otherwise this is a param or property node which itself
;; should have one or more syntax nodes as a value, so
;; recurse on value(s):
((ical:expects-list-of-values-p type)
(unless (listp value)
(ical:signal-validation-error
(format "Expected list of values for `%s' node" type)
:node node))
(when allowed-types
(dolist (v value)
(unless (memq (ical:ast-node-type v) allowed-types)
(ical:signal-validation-error
(format "Value of unexpected type `%s' in `%s' node"
(ical:ast-node-type v) type)
:node node))))
(mapc #'ical:ast-node-valid-value-p value)
node)
(t
(unless (ical:ast-node-p value)
(ical:signal-validation-error
(format "Invalid value for `%s' node: %s" type value)
:node node))
(when allowed-types
(unless (memq (ical:ast-node-type value) allowed-types)
(ical:signal-validation-error
(format "Value of unexpected type `%s' in `%s' node"
(ical:ast-node-type value) type)
:node node)))
(ical:ast-node-valid-value-p value)))))
(defun ical:count-children-by-type (node)
"Count NODE's children by type.
Returns an alist mapping type symbols to the number of NODE's children
of that type."
(let ((children (ical:ast-node-children node))
(map nil))
(dolist (child children map)
(let* ((type (ical:ast-node-type child))
(n (alist-get type map)))
(setf (alist-get type map) (1+ (or n 0)))))))
(defun ical:ast-node-valid-children-p (node)
"Validate that NODE's children satisfy its type's :child-spec.
The :child-spec is associated with NODE's type by
`icalendar-define-component', `icalendar-define-property',
`icalendar-define-param', or `icalendar-define-type', which see.
Signals an `icalendar-validation-error' if NODE is invalid, or returns
NODE.
Note that this function does not check that the children of NODE
are themselves valid; for that, see `ical:ast-node-valid-p'."
(let* ((type (ical:ast-node-type node))
(child-spec (get type 'ical:child-spec))
(child-counts (ical:count-children-by-type node)))
(when child-spec
(dolist (child-type (plist-get child-spec :one))
(unless (= 1 (alist-get child-type child-counts 0))
(ical:signal-validation-error
(format "iCalendar `%s' node must contain exactly one `%s'"
type child-type)
:node node)))
(dolist (child-type (plist-get child-spec :one-or-more))
(unless (<= 1 (alist-get child-type child-counts 0))
(ical:signal-validation-error
(format "iCalendar `%s' node must contain one or more `%s'"
type child-type)
:node node)))
(dolist (child-type (plist-get child-spec :zero-or-one))
(unless (<= (alist-get child-type child-counts 0)
1)
(ical:signal-validation-error
(format "iCalendar `%s' node may contain at most one `%s'"
type child-type)
:node node)))
;; check that all child nodes are allowed:
(unless (plist-get child-spec :allow-others)
(let ((allowed-types (append (plist-get child-spec :one)
(plist-get child-spec :one-or-more)
(plist-get child-spec :zero-or-one)
(plist-get child-spec :zero-or-more)))
(appearing-types (mapcar #'car child-counts)))
(dolist (child-type appearing-types)
(unless (member child-type allowed-types)
(ical:signal-validation-error
(format "`%s' may not contain `%s'" type child-type)
:node node))))))
;; success:
node))
(defun ical:ast-node-valid-p (node &optional recursively)
"Check that NODE is a valid iCalendar syntax node.
By default, the check will only validate NODE itself, but if
RECURSIVELY is non-nil, it will recursively check all its
descendants as well. Signals an `icalendar-validation-error' if
NODE is invalid, or returns NODE."
(unless (ical:ast-node-p node)
(ical:signal-validation-error
"Not an iCalendar syntax node"
:node node))
(ical:ast-node-valid-value-p node)
(ical:ast-node-valid-children-p node)
(let* ((type (ical:ast-node-type node))
(other-validator (get type 'ical:other-validator)))
(unless (ical:type-symbol-p type)
(ical:signal-validation-error
(format "Node's type `%s' is not an iCalendar type symbol" type)
:node node))
(when (and other-validator (not (functionp other-validator)))
(ical:signal-validation-error
(format "Bad validator function `%s' for type `%s'" other-validator type)))
(when other-validator
(funcall other-validator node)))
(when recursively
(dolist (c (ical:ast-node-children node))
(ical:ast-node-valid-p c recursively)))
;; success:
node)
(provide 'icalendar-ast)
;; Local Variables:
;; read-symbol-shorthands: (("ical:" . "icalendar-"))
;; End:
;;; icalendar-ast.el ends here

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,610 @@
;;; icalendar-mode.el --- Major mode for iCalendar format -*- lexical-binding: t; -*-
;;;
;; Copyright (C) 2024 Richard Lawrence
;; Author: Richard Lawrence <rwl@recursewithless.net>
;; Keywords: calendar
;; This file is part of GNU Emacs.
;; This file is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this file. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This file defines icalendar-mode, a major mode for iCalendar data.
;; Its main job is to provide syntax highlighting using the matching
;; functions created for iCalendar syntax in icalendar-parser.el, and to
;; perform line unfolding and folding via format conversion.
;; When activated, icalendar-mode unfolds content lines if necessary.
;; This is because the parsing functions, and thus syntax highlighting,
;; assume that content lines have already been unfolded. When a buffer
;; is saved, icalendar-mode also automatically folds long content if
;; necessary, as required by RFC5545.
;;; Code:
(require 'icalendar-parser)
(require 'format)
;; Faces and font lock:
(defgroup ical:faces
'((ical:property-name custom-face)
(ical:property-value custom-face)
(ical:parameter-name custom-face)
(ical:parameter-value custom-face)
(ical:component-name custom-face)
(ical:keyword custom-face)
(ical:binary-data custom-face)
(ical:date-time-types custom-face)
(ical:numeric-types custom-face)
(ical:recurrence-rule custom-face)
(ical:warning custom-face)
(ical:ignored custom-face))
"Faces for `icalendar-mode'."
:version "31.1"
:group 'icalendar
:prefix 'icalendar)
(defface ical:property-name
'((default . (:inherit font-lock-keyword-face)))
"Face for iCalendar property names.")
(defface ical:property-value
'((default . (:inherit default)))
"Face for iCalendar property values.")
(defface ical:parameter-name
'((default . (:inherit font-lock-property-name-face)))
"Face for iCalendar parameter names.")
(defface ical:parameter-value
'((default . (:inherit font-lock-property-use-face)))
"Face for iCalendar parameter values.")
(defface ical:component-name
'((default . (:inherit font-lock-constant-face)))
"Face for iCalendar component names.")
(defface ical:keyword
'((default . (:inherit font-lock-keyword-face)))
"Face for other iCalendar keywords.")
(defface ical:binary-data
'((default . (:inherit font-lock-comment-face)))
"Face for iCalendar values that represent binary data.")
(defface ical:date-time-types
'((default . (:inherit font-lock-type-face)))
"Face for iCalendar values that represent time.
These include dates, date-times, durations, periods, and UTC offsets.")
(defface ical:numeric-types
'((default . (:inherit ical:property-value-face)))
"Face for iCalendar values that represent integers, floats, and geolocations.")
(defface ical:recurrence-rule
'((default . (:inherit font-lock-type-face)))
"Face for iCalendar recurrence rule values.")
(defface ical:uri
'((default . (:inherit ical:property-value-face :underline t)))
"Face for iCalendar values that are URIs (including URLs and mail addresses).")
(defface ical:warning
'((default . (:inherit font-lock-warning-face)))
"Face for iCalendar syntax errors.")
(defface ical:ignored
'((default . (:inherit font-lock-comment-face)))
"Face for iCalendar syntax which is parsed but ignored.")
;;; Font lock:
(defconst ical:params-font-lock-keywords
'((ical:match-other-param
(1 'font-lock-comment-face t t)
(2 'font-lock-comment-face t t)
(3 'ical:warning t t))
(ical:match-value-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-tzid-param
(1 'ical:parameter-name t t)
(2 'ical:parameter-value t t)
(3 'ical:warning t t))
(ical:match-sent-by-param
(1 'ical:parameter-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-rsvp-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-role-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-reltype-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-related-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-range-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-partstat-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-member-param
(1 'ical:parameter-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-language-param
(1 'ical:parameter-name t t)
(2 'ical:parameter-value t t)
(3 'ical:warning t t))
(ical:match-fbtype-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-fmttype-param
(1 'ical:parameter-name t t)
(2 'ical:parameter-value t t)
(3 'ical:warning t t))
(ical:match-encoding-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-dir-param
(1 'ical:parameter-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-delegated-to-param
(1 'ical:parameter-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-delegated-from-param
(1 'ical:parameter-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-cutype-param
(1 'ical:parameter-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-cn-param
(1 'ical:parameter-name t t)
(2 'ical:parameter-value t t)
(3 'ical:warning t t))
(ical:match-altrep-param
(1 'ical:parameter-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t)))
"Entries for iCalendar property parameters in `font-lock-keywords'.")
(defconst ical:properties-font-lock-keywords
'((ical:match-request-status-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-other-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-sequence-property
(1 'ical:property-name t t)
(2 'ical:numeric-types t t)
(3 'ical:warning t t))
(ical:match-last-modified-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-dtstamp-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-created-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-trigger-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-repeat-property
(1 'ical:property-name t t)
(2 'ical:numeric-types t t)
(3 'ical:warning t t))
(ical:match-action-property
(1 'ical:property-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-rrule-property
(1 'ical:property-name t t)
(2 'ical:recurrence-rule t t)
(3 'ical:warning t t))
(ical:match-rdate-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-exdate-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-uid-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-url-property
(1 'ical:property-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-related-to-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-recurrence-id-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-organizer-property
(1 'ical:property-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-contact-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-attendee-property
(1 'ical:property-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-tzurl-property
(1 'ical:property-name t t)
(2 'ical:uri t t)
(3 'ical:warning t t))
(ical:match-tzoffsetto-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-tzoffsetfrom-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-tzname-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-tzid-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-transp-property
(1 'ical:property-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-freebusy-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-duration-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-dtstart-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-due-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-dtend-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-completed-property
(1 'ical:property-name t t)
(2 'ical:date-time-types t t)
(3 'ical:warning t t))
(ical:match-summary-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-status-property
(1 'ical:property-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-resources-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-priority-property
(1 'ical:property-name t t)
(2 'ical:numeric-types t t)
(3 'ical:warning t t))
(ical:match-percent-complete-property
(1 'ical:property-name t t)
(2 'ical:numeric-types t t)
(3 'ical:warning t t))
(ical:match-location-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-geo-property
(1 'ical:property-name t t)
(2 'ical:numeric-types t t)
(3 'ical:warning t t))
(ical:match-description-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-comment-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-class-property
(1 'ical:property-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t))
(ical:match-categories-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-attach-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t)
(13 'ical:uri t t)
(14 'ical:binary-data t t))
(ical:match-version-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-prodid-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-method-property
(1 'ical:property-name t t)
(2 'ical:property-value t t)
(3 'ical:warning t t))
(ical:match-calscale-property
(1 'ical:property-name t t)
(2 'ical:keyword t t)
(3 'ical:warning t t)))
"Entries for iCalendar properties in `font-lock-keywords'.")
(defconst ical:ignored-properties-font-lock-keywords
`((,(rx ical:other-property) (1 'ical:ignored keep t)
(2 'ical:ignored keep t)))
"Entries for iCalendar ignored properties in `font-lock-keywords'.")
(defconst ical:components-font-lock-keywords
'((ical:match-vcalendar-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-other-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-valarm-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-daylight-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-standard-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-vtimezone-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-vfreebusy-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-vjournal-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-vtodo-component
(1 'ical:keyword t t)
(2 'ical:component-name t t))
(ical:match-vevent-component
(1 'ical:keyword t t)
(2 'ical:component-name t t)))
"Entries for iCalendar components in `font-lock-keywords'.")
(defvar ical:font-lock-keywords
(append ical:params-font-lock-keywords
ical:properties-font-lock-keywords
ical:components-font-lock-keywords
ical:ignored-properties-font-lock-keywords)
"Value of `font-lock-keywords' for `icalendar-mode'.")
;; The major mode:
;;; Mode hook
(defvar ical:mode-hook nil
"Hook run when activating `icalendar-mode'.")
;;; Activating the mode for .ics files:
(add-to-list 'auto-mode-alist '("\\.ics\\'" . icalendar-mode))
;;; Syntax table
(defvar ical:mode-syntax-table
(let ((st (make-syntax-table)))
;; Characters for which the standard syntax table suffices:
;; ; (punctuation): separates some property values, and property parameters
;; " (string): begins and ends string values
;; : (punctuation): separates property name (and parameters) from property
;; values
;; , (punctuation): separates values in a list
;; CR, LF (whitespace): content line endings
;; space (whitespace): when at the beginning of a line, continues the
;; previous line
;; Characters which need to be adjusted from the standard syntax table:
;; = is punctuation, not a symbol constituent:
(modify-syntax-entry ?= ". " st)
;; / is punctuation, not a symbol constituent:
(modify-syntax-entry ?/ ". " st)
st)
"Syntax table used in `icalendar-mode'.")
;;; Coding systems
;; Provide a hint to the decoding system that iCalendar files use DOS
;; line endings. This appears to be the simplest way to ensure that
;; `find-file' will correctly decode an iCalendar file, since decoding
;; happens before icalendar-mode starts.
(add-to-list 'file-coding-system-alist '("\\.ics\\'" . undecided-dos))
;;; Format conversion
;; We use the format conversion infrastructure provided by format.el,
;; `insert-file-contents', and `write-region' to automatically perform
;; line unfolding when icalendar-mode starts in a buffer, and line
;; folding when it is saved to a file. See Info node `(elisp)Format
;; Conversion' for more.
(defconst ical:format-definition
'(text/calendar "iCalendar format"
nil ; no regexp - icalendar-mode runs decode instead
ical:unfold-region ; decoding function
ical:folding-annotations ; encoding function
nil ; encoding function does not modify buffer
nil ; no need to activate a minor mode
t) ; preserve the format when saving
"Entry for iCalendar format in `format-alist'.")
(add-to-list 'format-alist ical:format-definition)
(defun ical:-format-decode-buffer ()
"Call `format-decode-buffer' with the \\='text/calendar format.
This function is intended to be run from `icalendar-mode-hook'."
(format-decode-buffer 'text/calendar))
(add-hook 'ical:mode-hook #'ical:-format-decode-buffer -90)
(defun ical:-disable-auto-fill ()
"Disable `auto-fill-mode' in iCalendar buffers.
Auto-fill-mode interferes with line folding and syntax highlighting, so
it is off by default in iCalendar buffers. This function is intended to
be run from `icalendar-mode-hook'."
(when auto-fill-function
(auto-fill-mode -1)))
(add-hook 'ical:mode-hook #'ical:-disable-auto-fill -91)
;;; Commands
(defun ical:switch-to-unfolded-buffer ()
"Switch to a new buffer with content lines unfolded.
The new buffer will contain the same data as the current buffer, but
with content lines unfolded (before decoding, if possible).
`Folding' means inserting a line break and a single whitespace
character to continue lines longer than 75 octets; `unfolding'
means removing the extra whitespace inserted by folding. The
iCalendar standard (RFC5545) requires folding lines when
serializing data to iCalendar format, and unfolding before
parsing it. In `icalendar-mode', folded lines may not have proper
syntax highlighting; this command allows you to view iCalendar
data with proper syntax highlighting, as the parser sees it.
If the current buffer is visiting a file, this function will
offer to save the buffer first, and then reload the contents from
the file, performing unfolding with `icalendar-unfold-undecoded-region'
before decoding it. This is the most reliable way to unfold lines.
If it is not visiting a file, it will unfold the new buffer
with `icalendar-unfold-region'. This can in some cases have
undesirable effects (see its docstring), so the original contents
are preserved unchanged in the current buffer.
In both cases, after switching to the new buffer, this command
offers to kill the original buffer.
It is recommended to turn off `auto-fill-mode' when viewing an
unfolded buffer, so that filling does not interfere with syntax
highlighting. This function offers to disable `auto-fill-mode' if
it is enabled in the new buffer; consider using
`visual-line-mode' instead."
(interactive)
(when (and buffer-file-name (buffer-modified-p))
(when (y-or-n-p (format "Save before reloading from %s?"
(file-name-nondirectory buffer-file-name)))
(save-buffer)))
(let ((old-buffer (current-buffer))
(mmode major-mode)
(uf-buffer (if buffer-file-name
(ical:unfolded-buffer-from-file buffer-file-name)
(ical:unfolded-buffer-from-buffer (current-buffer)))))
(switch-to-buffer uf-buffer)
;; restart original major mode, in case the new buffer is
;; still in fundamental-mode: TODO: is this necessary?
(funcall mmode)
(when (y-or-n-p (format "Unfolded buffer is shown. Kill %s?"
(buffer-name old-buffer)))
(kill-buffer old-buffer))
(when (and auto-fill-function (y-or-n-p "Disable auto-fill-mode?"))
(auto-fill-mode -1))))
;;; Mode definition
;;;###autoload
(define-derived-mode icalendar-mode text-mode "iCalendar"
"Major mode for viewing and editing iCalendar (RFC5545) data.
This mode provides syntax highlighting for iCalendar components,
properties, values, and property parameters, and defines a format to
automatically handle folding and unfolding iCalendar content lines.
`Folding' means inserting whitespace characters to continue long
lines; `unfolding' means removing the extra whitespace inserted
by folding. The iCalendar standard requires folding lines when
serializing data to iCalendar format, and unfolding before
parsing it.
Thus icalendar-mode's syntax highlighting is designed to work with
unfolded lines. When `icalendar-mode' is activated in a buffer, it will
automatically unfold lines using a file format conversion, and
automatically fold lines when saving the buffer to a file; see Info
node `(elisp)Format Conversion' for more information. It also disables
`auto-fill-mode' if it is active, since filling interferes with line
folding and syntax highlighting. Consider using `visual-line-mode' in
`icalendar-mode' instead."
:group 'icalendar
:syntax-table ical:mode-syntax-table
;; TODO: Keymap?
;; TODO: buffer-local variables?
;; TODO: indent-line-function and indentation variables
;; TODO: mode-specific menu and context menus
;; TODO: eldoc integration
;; TODO: completion of keywords
;; TODO: hook for folding in change-major-mode-hook?
(progn
(setq font-lock-defaults '(ical:font-lock-keywords nil t))))
(provide 'icalendar-mode)
;; Local Variables:
;; read-symbol-shorthands: (("ical:" . "icalendar-"))
;; End:
;;; icalendar-mode.el ends here

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,749 @@
;;; icalendar-utils.el --- iCalendar utility functions -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Richard Lawrence
;; Author: Richard Lawrence <rwl@recursewithless.net>
;; Created: January 2025
;; Keywords: calendar
;; This file is part of GNU Emacs.
;; This file is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this file. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This file contains a variety of utility functions to work with
;; iCalendar data which are used throughout the rest of the iCalendar
;; library. Most of the functions here deal with calendar and clock
;; arithmetic, and help smooth over the type distinction between plain
;; dates and date-times.
;;; Code:
(require 'cl-lib)
(require 'calendar)
(eval-when-compile (require 'icalendar-macs))
(require 'icalendar-parser)
;; Accessors for commonly used properties
(defun ical:component-dtstart (component)
"Return the value of the `icalendar-dtstart' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:dtstart nil value))
(defun ical:component-dtend (component)
"Return the value of the `icalendar-dtend' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:dtend nil value))
(defun ical:component-rdate (component)
"Return the value of the `icalendar-rdate' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:rdate nil value))
(defun ical:component-summary (component)
"Return the value of the `icalendar-summary' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:summary nil value))
(defun ical:component-description (component)
"Return the value of the `icalendar-description' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:description nil value))
(defun ical:component-tzname (component)
"Return the value of the `icalendar-tzname' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:tzname nil value))
(defun ical:component-uid (component)
"Return the value of the `icalendar-uid' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:uid nil value))
(defun ical:component-url (component)
"Return the value of the `icalendar-url' property of COMPONENT.
COMPONENT can be any component node."
(ical:with-property-of component 'ical:url nil value))
(defun ical:property-tzid (property)
"Return the value of the `icalendar-tzid' parameter of PROPERTY."
(ical:with-param-of property 'ical:tzidparam nil value))
;; String manipulation
(defun ical:strip-mailto (s)
"Remove \"mailto:\" case-insensitively from the start of S."
(let ((case-fold-search t))
(replace-regexp-in-string "^mailto:" "" s)))
;; Date/time
;; N.B. Notation: "date/time" is used in function names when a function
;; can accept either `icalendar-date' or `icalendar-date-time' values;
;; in contrast, "date-time" means it accepts *only*
;; `icalendar-date-time' values, not plain dates.
;; TODO: turn all the 'date/time' functions into methods dispatched by
;; type?
(defun ical:date-time-to-date (dt)
"Convert an `icalendar-date-time' value DT to an `icalendar-date'."
(list (decoded-time-month dt)
(decoded-time-day dt)
(decoded-time-year dt)))
(cl-defun ical:date-to-date-time (dt &key (hour 0) (minute 0) (second 0) (tz nil))
"Convert an `icalendar-date' value DT to an `icalendar-date-time'.
The following keyword arguments are accepted:
:hour, :minute, :second - integers representing a local clock time on date DT
:tz - an `icalendar-vtimezone' in which to interpret this clock time
If these arguments are all unspecified, the hour, minute, and second
slots of the returned date-time will be zero, and it will contain no
time zone information. See `icalendar-make-date-time' for more on these
arguments."
(ical:make-date-time
:year (calendar-extract-year dt)
:month (calendar-extract-month dt)
:day (calendar-extract-day dt)
:hour hour
:minute minute
:second second
:tz tz))
(defun ical:date/time-to-date (dt)
"Extract a Gregorian date from DT.
An `icalendar-date' value is returned unchanged.
An `icalendar-date-time' value is converted to an `icalendar-date'."
(if (cl-typep dt 'ical:date)
dt
(ical:date-time-to-date dt)))
;; Type-aware accessors for date/time slots that work for both ical:date
;; and ical:date-time:
;; NOTE: cl-typecase ONLY works here if dt is valid according to
;; `ical:-decoded-date-time-p'! May need to adjust this if it's
;; necessary to work with incomplete decoded-times
(defun ical:date/time-year (dt)
"Return DT's year slot.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(cl-typecase dt
(ical:date (calendar-extract-year dt))
(ical:date-time (decoded-time-year dt))))
(defun ical:date/time-month (dt)
"Return DT's month slot.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(cl-typecase dt
(ical:date (calendar-extract-month dt))
(ical:date-time (decoded-time-month dt))))
(defun ical:date/time-monthday (dt)
"Return DT's day of the month slot.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(cl-typecase dt
(ical:date (calendar-extract-day dt))
(ical:date-time (decoded-time-day dt))))
(defun ical:date/time-weekno (dt &optional weekstart)
"Return DT's ISO week number.
DT may be either an `icalendar-date' or an `icalendar-date-time'.
WEEKSTART defaults to 1; it represents the day which starts the week,
and should be an integer between 0 (= Sunday) and 6 (= Saturday)."
;; TODO: Add support for weekstart.
;; calendar-iso-from-absolute doesn't support this yet.
(when (and weekstart (not (= weekstart 1)))
(error "Support for WEEKSTART other than 1 (=Monday) not implemented yet"))
(let* ((gdate (ical:date/time-to-date dt))
(isodate (calendar-iso-from-absolute
(calendar-absolute-from-gregorian gdate)))
(weekno (car isodate)))
weekno))
(defun ical:date/time-weekday (dt)
"Return DT's day of the week.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(cl-typecase dt
(ical:date (calendar-day-of-week dt))
(ical:date-time
(or (decoded-time-weekday dt)
;; compensate for possibly-nil weekday slot if the date-time
;; has been constructed by `make-decoded-time'; cf. comment
;; in `icalendar--decoded-date-time-p':
(calendar-day-of-week (ical:date-time-to-date dt))))))
(defun ical:date/time-hour (dt)
"Return DT's hour slot, or nil.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(when (cl-typep dt 'ical:date-time)
(decoded-time-hour dt)))
(defun ical:date/time-minute (dt)
"Return DT's minute slot, or nil.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(when (cl-typep dt 'ical:date-time)
(decoded-time-minute dt)))
(defun ical:date/time-second (dt)
"Return DT's second slot, or nil.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(when (cl-typep dt 'ical:date-time)
(decoded-time-second dt)))
(defun ical:date/time-zone (dt)
"Return DT's time zone slot, or nil.
DT may be either an `icalendar-date' or an `icalendar-date-time'."
(when (cl-typep dt 'ical:date-time)
(decoded-time-zone dt)))
;;; Date/time comparisons and arithmetic:
(defun ical:date< (dt1 dt2)
"Return non-nil if date DT1 is strictly earlier than date DT2.
DT1 and DT2 must both be `icalendar-date' values of the form (MONTH DAY YEAR)."
(< (calendar-absolute-from-gregorian dt1)
(calendar-absolute-from-gregorian dt2)))
(defun ical:date<= (dt1 dt2)
"Return non-nil if date DT1 is earlier than or the same date as DT2.
DT1 and DT2 must both be `icalendar-date' values of the form (MONTH DAY YEAR)."
(or (calendar-date-equal dt1 dt2) (ical:date< dt1 dt2)))
(defun ical:date-time-locally-earlier (dt1 dt2 &optional or-equal)
"Return non-nil if date-time DT1 is locally earlier than DT2.
Unlike `icalendar-date-time<', this function assumes both times are
local to some time zone and does not consider their zone information.
If OR-EQUAL is non-nil, this function acts like `<=' rather than `<':
it will return non-nil if DT1 and DT2 are locally the same time."
(let ((year1 (decoded-time-year dt1))
(year2 (decoded-time-year dt2))
(month1 (decoded-time-month dt1))
(month2 (decoded-time-month dt2))
(day1 (decoded-time-day dt1))
(day2 (decoded-time-day dt2))
(hour1 (decoded-time-hour dt1))
(hour2 (decoded-time-hour dt2))
(minute1 (decoded-time-minute dt1))
(minute2 (decoded-time-minute dt2))
(second1 (decoded-time-second dt1))
(second2 (decoded-time-second dt2)))
(or (< year1 year2)
(and (= year1 year2)
(or (< month1 month2)
(and (= month1 month2)
(or (< day1 day2)
(and (= day1 day2)
(or (< hour1 hour2)
(and (= hour1 hour2)
(or (< minute1 minute2)
(and (= minute1 minute2)
(if or-equal
(<= second1 second2)
(< second1 second2))))))))))))))
(defun ical:date-time-locally< (dt1 dt2)
"Return non-nil if date-time DT1 is locally strictly earlier than DT2.
Unlike `icalendar-date-time<', this function assumes both times are
local to some time zone and does not consider their zone information."
(ical:date-time-locally-earlier dt1 dt2 nil))
(defun ical:date-time-locally<= (dt1 dt2)
"Return non-nil if date-time DT1 is locally earlier than, or equal to, DT2.
Unlike `icalendar-date-time<=', this function assumes both times are
local to some time zone and does not consider their zone information."
(ical:date-time-locally-earlier dt1 dt2 t))
(defun ical:date-time< (dt1 dt2)
"Return non-nil if date-time DT1 is strictly earlier than DT2.
DT1 and DT2 must both be decoded times, and either both or neither
should have time zone information.
If one has a time zone offset and the other does not, the offset
returned from `current-time-zone' is used as the missing offset; if
`current-time-zone' cannot provide this information, an error is
signaled."
(let ((zone1 (decoded-time-zone dt1))
(zone2 (decoded-time-zone dt2)))
(cond ((and (integerp zone1) (integerp zone2))
(time-less-p (encode-time dt1) (encode-time dt2)))
((and (null zone1) (null zone2))
(ical:date-time-locally< dt1 dt2))
(t
;; Cf. RFC5545 Sec. 3.3.5:
;; "The recipient of an iCalendar object with a property value
;; consisting of a local time, without any relative time zone
;; information, SHOULD interpret the value as being fixed to whatever
;; time zone the "ATTENDEE" is in at any given moment. This means
;; that two "Attendees", in different time zones, receiving the same
;; event definition as a floating time, may be participating in the
;; event at different actual times. Floating time SHOULD only be
;; used where that is the reasonable behavior."
;; I'm interpreting this to mean that if we get here, where
;; one date-time has zone information and the other doesn't,
;; we should use the offset from (current-time-zone).
(let* ((user-tz (current-time-zone))
(user-offset (car user-tz))
(dt1z (ical:date-time-variant dt1 :zone (or zone1 user-offset)))
(dt2z (ical:date-time-variant dt2 :zone (or zone2 user-offset))))
(if user-offset
(time-less-p (encode-time dt1z) (encode-time dt2z))
(error "Too little zone information for comparison: %s %s"
dt1 dt2)))))))
;; Two different notions of equality are relevant to decoded times:
;; strict equality (`icalendar-date-time=') of all slots, or
;; simultaneity (`icalendar-date-time-simultaneous-p').
;; Most tests probably want the strict notion, because it distinguishes
;; between simultaneous events decoded into different time zones,
;; whereas most user-facing functions (e.g. sorting events by date and time)
;; probably want simultaneity.
(defun ical:date-time= (dt1 dt2)
"Return non-nil if DT1 and DT2 are decoded-times with identical slot values.
Note that this function returns nil if DT1 and DT2 represent times in
different time zones, even if they are simultaneous. For the latter, see
`icalendar-date-time-simultaneous-p'."
(equal dt1 dt2))
(defun ical:date-time-locally-simultaneous-p (dt1 dt2)
"Return non-nil if DT1 and DT2 are locally simultaneous date-times.
Note that this function ignores zone information in dt1 and dt2. It
returns non-nil if DT1 and DT2 represent the same clock time in
different time zones, even if they encode to different absolute times."
(and (eq (decoded-time-year dt1) (decoded-time-year dt2))
(eq (decoded-time-month dt1) (decoded-time-month dt2))
(eq (decoded-time-day dt1) (decoded-time-day dt2))
(eq (decoded-time-hour dt1) (decoded-time-hour dt2))
(eq (decoded-time-minute dt1) (decoded-time-minute dt2))
(eq (decoded-time-second dt1) (decoded-time-second dt2))))
(defun ical:date-time-simultaneous-p (dt1 dt2)
"Return non-nil if DT1 and DT2 are simultaneous date-times.
This function returns non-nil if DT1 and DT2 encode to the same Lisp
timestamp. Thus they can count as simultaneous even if they represent
times in different timezones. If both date-times lack an offset from
UTC, they are treated as simultaneous if they encode to the same
timestamp in UTC.
If only one date-time has an offset, they are treated as
non-simultaneous if they represent different clock times according to
`icalendar-date-time-locally-simultaneous-p'. Otherwise an error is
signaled."
(let ((zone1 (decoded-time-zone dt1))
(zone2 (decoded-time-zone dt2)))
(cond ((and (integerp zone1) (integerp zone2))
(time-equal-p (encode-time dt1) (encode-time dt2)))
((and (null zone1) (null zone2))
(time-equal-p (encode-time (ical:date-time-variant dt1 :zone 0))
(encode-time (ical:date-time-variant dt2 :zone 0))))
(t
;; Best effort:
;; TODO: I'm not convinced this is the right thing to do yet.
;; Might want to be stricter here and fix the problem of comparing
;; times with and without zone information elsewhere.
(if (ical:date-time-locally-simultaneous-p dt1 dt2)
(error "Missing zone information: %s %s" dt1 dt2)
nil)))))
(defun ical:date-time<= (dt1 dt2)
"Return non-nil if DT1 is earlier than, or simultaneous with, DT2.
DT1 and DT2 must both be decoded times, and either both or neither must have
time zone information."
(or (ical:date-time< dt1 dt2)
(ical:date-time-simultaneous-p dt1 dt2)))
(defun ical:date/time< (dt1 dt2)
"Return non-nil if DT1 is strictly earlier than DT2.
DT1 and DT2 must be either `icalendar-date' or `icalendar-date-time'
values. If they are not of the same type, only the date in the
`icalendar-date-time' value will be considered."
(cl-typecase dt1
(ical:date
(if (cl-typep dt2 'ical:date)
(ical:date< dt1 dt2)
(ical:date< dt1 (ical:date-time-to-date dt2))))
(ical:date-time
(if (cl-typep dt2 'ical:date-time)
(ical:date-time< dt1 dt2)
(ical:date< (ical:date-time-to-date dt1) dt2)))))
(defun ical:date/time<= (dt1 dt2)
"Return non-nil if DT1 is earlier than or simultaneous to DT2.
DT1 and DT2 must be either `icalendar-date' or `icalendar-date-time'
values. If they are not of the same type, only the date in the
`icalendar-date-time' value will be considered."
(cl-typecase dt1
(ical:date
(if (cl-typep dt2 'ical:date)
(ical:date<= dt1 dt2)
(ical:date<= dt1 (ical:date-time-to-date dt2))))
(ical:date-time
(if (cl-typep dt2 'ical:date-time)
(ical:date-time<= dt1 dt2)
(ical:date<= (ical:date-time-to-date dt1) dt2)))))
(defun ical:date/time-min (&rest dts)
"Return the earliest date or date-time among DTS.
The DTS may be any `icalendar-date' or `icalendar-date-time' values, and
will be ordered by `icalendar-date/time<='."
(car (sort dts :lessp #'ical:date/time<=)))
(defun ical:date/time-max (&rest dts)
"Return the latest date or date-time among DTS.
The DTS may be any `icalendar-date' or `icalendar-date-time' values, and
will be ordered by `icalendar-date/time<='."
(car (sort dts :reverse t :lessp #'ical:date/time<=)))
(defun ical:date-add (date unit n)
"Add N UNITs to DATE.
UNIT should be `:year', `:month', `:week', or `:day'; time units will be
ignored. N may be a positive or negative integer."
(if (memq unit '(:hour :minute :second))
date
(let* ((dt (ical:make-date-time :year (calendar-extract-year date)
:month (calendar-extract-month date)
:day (calendar-extract-day date)))
(delta (if (eq unit :week)
(make-decoded-time :day (* 7 n))
(make-decoded-time unit n)))
(new-dt (decoded-time-add dt delta)))
(ical:date-time-to-date new-dt))))
(declare-function icalendar-recur-tz-decode-time "icalendar-recur")
(defun ical:date-time-add (dt delta &optional vtimezone)
"Like `decoded-time-add', but also updates weekday and time zone slots.
DT and DELTA should be `icalendar-date-time' values (decoded times), as
in `decoded-time-add'. VTIMEZONE, if given, should be an
`icalendar-vtimezone'. The resulting date-time will be given the offset
determined by VTIMEZONE at the local time determined by adding DELTA to
DT.
This function assumes that time units in DELTA larger than an hour
should not affect the local clock time in the result, even when crossing
an observance boundary in VTIMEZONE. This means that e.g. if DT is at
9AM daylight savings time on the day before the transition to standard
time, then the result of adding a DELTA of two days will be at 9AM
standard time, even though this is not exactly 48 hours later. Adding a
DELTA of 48 hours, on the other hand, will result in a time exactly 48
hours later, but at a different local time."
(require 'icalendar-recur) ; for icr:tz-decode-time; avoids circular requires
(if (not vtimezone)
;; the simple case: we have no time zone info, so just use
;; `decoded-time-add':
(let ((sum (decoded-time-add dt delta)))
(ical:date-time-variant sum))
;; `decoded-time-add' does not take time zone shifts into account,
;; so we need to do the adjustment ourselves. We first add the units
;; larger than an hour using `decoded-time-add', holding the clock
;; time fixed, as described in the docstring. Then we add the time
;; units as a fixed number of seconds and re-decode the resulting
;; absolute time into the time zone.
(let* ((cal-delta (make-decoded-time :year (or (decoded-time-year delta) 0)
:month (or (decoded-time-month delta) 0)
:day (or (decoded-time-day delta) 0)))
(cal-sum (decoded-time-add dt cal-delta))
(dt-w/zone (ical:date-time-variant cal-sum
:tz vtimezone))
(secs-delta (+ (or (decoded-time-second delta) 0)
(* 60 (or (decoded-time-minute delta) 0))
(* 60 60 (or (decoded-time-hour delta) 0))))
(sum-ts (time-add (encode-time dt-w/zone) secs-delta)))
(icalendar-recur-tz-decode-time sum-ts vtimezone))))
;; TODO: rework so that it's possible to add dur-values to plain dates.
;; Perhaps rename this to "date/time-inc" or so, or use kwargs to allow
;; multiple units, or...
(defun ical:date/time-add (dt unit n &optional vtimezone)
"Add N UNITs to DT.
DT should be an `icalendar-date' or `icalendar-date-time'. UNIT should
be `:year', `:month', `:week', `:day', `:hour', `:minute', or `:second';
time units will be ignored if DT is an `icalendar-date'. N may be a
positive or negative integer."
(cl-typecase dt
(ical:date-time
(let ((delta (if (eq unit :week) (make-decoded-time :day (* 7 n))
(make-decoded-time unit n))))
(ical:date-time-add dt delta vtimezone)))
(ical:date (ical:date-add dt unit n))))
(defun ical:date/time-add-duration (start duration &optional vtimezone)
"Return the end date(-time) which is a length of DURATION after START.
START should be an `icalendar-date' or `icalendar-date-time'; the
returned value will be of the same type as START. DURATION should be an
`icalendar-dur-value'. VTIMEZONE, if specified, should be the
`icalendar-vtimezone' representing the time zone of START."
(if (integerp duration)
;; number of weeks:
(setq duration (make-decoded-time :day (* 7 duration))))
(cl-typecase start
(ical:date
(ical:date-time-to-date
(ical:date-time-add (ical:date-to-date-time start) duration)))
(ical:date-time
(ical:date-time-add start duration vtimezone))))
(defun ical:duration-between (start end)
"Return the duration between START and END.
START should be an `icalendar-date' or `icalendar-date-time'; END must
be of the same type as START. The returned value is an
`icalendar-dur-value', i.e., a time delta in the sense of
`decoded-time-add'."
(cl-typecase start
(ical:date
(make-decoded-time :day (- (calendar-absolute-from-gregorian end)
(calendar-absolute-from-gregorian start))))
(ical:date-time
(let* ((start-abs (time-convert (encode-time start) 'integer))
(end-abs (time-convert (encode-time end) 'integer))
(dur-secs (- end-abs start-abs))
(days (/ dur-secs (* 60 60 24)))
(dur-nodays (mod dur-secs (* 60 60 24)))
(hours (/ dur-nodays (* 60 60)))
(dur-nohours (mod dur-nodays (* 60 60)))
(minutes (/ dur-nohours 60))
(seconds (mod dur-nohours 60)))
(make-decoded-time :day days
:hour hours :minute minutes :second seconds)))))
(defun ical:date/time-to-local (dt)
"Reinterpret DT in Emacs local time if necessary.
If DT is an `icalendar-date-time', encode and re-decode it into Emacs
local time. If DT is an `icalendar-date', return it unchanged."
(cl-typecase dt
(ical:date dt)
(ical:date-time
(ical:date-time-variant ; ensure weekday is present too
(decode-time (encode-time dt))))))
(declare-function icalendar-recur-subintervals-to-dates "icalendar-recur")
(defun ical:dates-until (start end &optional locally)
"Return a list of `icalendar-date' values between START and END.
START and END may be either `icalendar-date' or `icalendar-date-time'
values. START is an inclusive lower bound, and END is an exclusive
upper bound. (Note, however, that if END is a date-time and its time is
after midnight, then its date will be included in the returned list.)
If LOCALLY is non-nil and START and END are date-times, these will be
interpreted into Emacs local time, so that the dates returned are valid
for the local time zone."
(require 'icalendar-recur)
(when locally
(when (cl-typep start 'ical:date-time)
(setq start (ical:date/time-to-local start)))
(when (cl-typep end 'ical:date-time)
(setq end (ical:date/time-to-local end))))
(cl-typecase start
(ical:date
(cl-typecase end
(ical:date
(icalendar-recur-subintervals-to-dates
(list (list (ical:date-to-date-time start)
(ical:date-to-date-time end)))))
(ical:date-time
(icalendar-recur-subintervals-to-dates
(list (list (ical:date-to-date-time start) end))))))
(ical:date-time
(cl-typecase end
(ical:date
(icalendar-recur-subintervals-to-dates
(list (list start (ical:date-to-date-time end)))))
(ical:date-time
(icalendar-recur-subintervals-to-dates (list (list start end))))))))
(cl-defun ical:make-date-time (&key second minute hour day month year
(dst -1 given-dst) zone tz)
"Make an `icalendar-date-time' from the given keyword arguments.
This function is like `make-decoded-time', except that it automatically
sets the weekday slot set based on the date arguments, and it accepts an
additional keyword argument: `:tz'. If provided, its value should be an
`icalendar-vtimezone', and the `:zone' and `:dst' arguments should not
be provided. In this case, the zone and dst slots in the returned
date-time will be adjusted to the correct values in the given time zone
for the local time represented by the remaining arguments."
(when (and tz (or zone given-dst))
(error "Possibly conflicting time zone data in args"))
(apply #'ical:date-time-variant (make-decoded-time)
`(:second ,second :minute ,minute :hour ,hour
:day ,day :month ,month :year ,year
;; Don't pass these keywords unless they were given explicitly.
;; TODO: is there a cleaner way to write this?
,@(when tz (list :tz tz))
,@(when given-dst (list :dst dst))
,@(when zone (list :zone zone)))))
(declare-function icalendar-recur-tz-set-zone "icalendar-recur")
(cl-defun ical:date-time-variant (dt &key second minute hour
day month year
(dst -1 given-dst)
(zone nil given-zone)
tz)
"Return a variant of DT with slots modified as in the given arguments.
DT should be an `icalendar-date-time'; the keyword arguments have the
same meanings as in `make-decoded-time'. The returned variant will have
slot values as specified by the arguments or copied from DT, except that
the weekday slot will be updated if necessary, and the zone and dst
fields will not be set unless given explicitly (because varying the date
and clock time generally invalidates the time zone information in DT).
One additional keyword argument is accepted: `:tz'. If provided, its
value should be an `icalendar-vtimezone', an `icalendar-utc-offset', or
the symbol \\='preserve. If it is a time zone component, the zone and
dst slots in the returned variant will be adjusted to the correct
values in the given time zone for the local time represented by the
variant. If it is a UTC offset, the variant's zone slot will contain
this value, but its dst slot will not be adjusted. If it is the symbol
\\='preserve, then both the zone and dst fields are copied from DT into
the variant."
(require 'icalendar-recur) ; for icr:tz-set-zone; avoids circular requires
(let ((variant
(make-decoded-time :second (or second (decoded-time-second dt))
:minute (or minute (decoded-time-minute dt))
:hour (or hour (decoded-time-hour dt))
:day (or day (decoded-time-day dt))
:month (or month (decoded-time-month dt))
:year (or year (decoded-time-year dt))
;; For zone and dst slots, trust the value
;; if explicitly specified or explicitly
;; requested to preserve, but not otherwise
:dst (cond (given-dst dst)
((eq 'preserve tz) (decoded-time-dst dt))
(t -1))
:zone (cond (given-zone zone)
((eq 'preserve tz) (decoded-time-zone dt))
(t nil)))))
;; update weekday slot when possible, since it depends on the date
;; slots, which might have changed. (It's not always possible,
;; because pure time values are also represented as decoded-times,
;; with empty date slots.)
(unless (or (null (decoded-time-year variant))
(null (decoded-time-month variant))
(null (decoded-time-day variant)))
(setf (decoded-time-weekday variant)
(calendar-day-of-week (ical:date-time-to-date variant))))
;; if given a time zone or UTC offset, update zone and dst slots,
;; which also might have changed:
(when (and tz (not (eq 'preserve tz)))
(icalendar-recur-tz-set-zone variant tz))
variant))
(defun ical:date/time-in-period-p (dt period &optional vtimezone)
"Return non-nil if DT occurs within PERIOD.
DT can be an `icalendar-date' or `icalendar-date-time' value. PERIOD
should be an `icalendar-period' value. VTIMEZONE, if given, is passed
to `icalendar-period-end' to compute the end time of the period if it
was not specified explicitly."
(and (ical:date/time<= (ical:period-start period) dt)
(ical:date/time< dt (ical:period-end period vtimezone))))
;; TODO: surely this exists already?
(defun ical:time<= (a b)
"Compare two Lisp timestamps A and B: is A <= B?"
(or (time-equal-p a b)
(time-less-p a b)))
(defun ical:number-of-weeks (year &optional weekstart)
"Return the number of weeks in (Gregorian) YEAR.
RFC5545 defines week 1 as the first week to include at least four days
in the year. Weeks are assumed to start on Monday (= 1) unless WEEKSTART
is specified, in which case it should be an integer between 0 (= Sunday)
and 6 (= Saturday)."
;; There are 53 weeks in a year if Jan 1 is the fourth day after
;; WEEKSTART, e.g. if the week starts on Monday and Jan 1 is a
;; Thursday, or in a leap year if Jan 1 is the third day after WEEKSTART
(let* ((jan1wd (calendar-day-of-week (list 1 1 year)))
(delta (mod (- jan1wd (or weekstart 1)) 7)))
(if (or (= 4 delta)
(and (= 3 delta) (calendar-leap-year-p year)))
53
52)))
(defun ical:start-of-weekno (weekno year &optional weekstart)
"Return the start of the WEEKNOth week in the (Gregorian) YEAR.
RFC5545 defines week 1 as the first week to include at least four days
in the year. Weeks are assumed to start on Monday (= 1) unless WEEKSTART
is specified, in which case it should be an integer between 0 (= Sunday)
and 6 (= Saturday). The returned value is an `icalendar-date'.
If WEEKNO is negative, it refers to the WEEKNOth week before the end of
the year: -1 is the last week of the year, -2 second to last, etc."
(calendar-gregorian-from-absolute
(+
(* 7 (if (< 0 weekno)
(1- weekno)
(+ 1 weekno (ical:number-of-weeks year weekstart))))
(calendar-dayname-on-or-before
(or weekstart 1)
;; Three days after Jan 1. gives us the nearest occurrence;
;; see `calendar-dayname-on-or-before'
(+ 3 (calendar-absolute-from-gregorian (list 1 1 year)))))))
(defun ical:nth-weekday-in (n weekday year &optional month)
"Return the Nth WEEKDAY in YEAR or MONTH.
If MONTH is specified, it refers to MONTH in YEAR, and N acts as an
index for WEEKDAYs within the month. Otherwise, N acts as an index for
WEEKDAYs within the entire YEAR.
N should be an integer. If N<0, it counts from the end of the month or
year: if N=-1, it refers to the last WEEKDAY in the month or year, if
N=-2 the second to last, and so on."
(if month
(calendar-nth-named-day n weekday month year)
(let* ((jan1 (calendar-absolute-from-gregorian (list 1 1 year)))
(dec31 (calendar-absolute-from-gregorian (list 12 31 year))))
;; Adapted from `calendar-nth-named-absday'.
;; TODO: we could generalize that function to make month an optional
;; argument, but that would mean changing its interface.
(calendar-gregorian-from-absolute
(if (> n 0)
(+ (* 7 (1- n))
(calendar-dayname-on-or-before
weekday
(+ 6 jan1)))
(+ (* 7 (1+ n))
(calendar-dayname-on-or-before
weekday
dec31)))))))
(provide 'icalendar-utils)
;; Local Variables:
;; read-symbol-shorthands: (("ical:" . "icalendar-"))
;; End:
;;; icalendar-utils.el ends here

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
&5/15/2012 15:00-15:30 Query
Location: phone
Status: confirmed
Organizer: A. Luser <a.luser@foo.com>
Attendee: Luser, Other <other.luser@foo.com> (needs-action)
Access: public
UID: 040000008200E00074C5B7101A82E0080000000020FFAED0CFEFCC01000000000000000010000000575268034ECDB649A15349B1BF240F15
Description: Whassup?

View file

@ -0,0 +1,8 @@
&15/5/2012 15:00-15:30 Query
Location: phone
Status: confirmed
Organizer: A. Luser <a.luser@foo.com>
Attendee: Luser, Other <other.luser@foo.com> (needs-action)
Access: public
UID: 040000008200E00074C5B7101A82E0080000000020FFAED0CFEFCC01000000000000000010000000575268034ECDB649A15349B1BF240F15
Description: Whassup?

View file

@ -0,0 +1,8 @@
&2012/5/15 15:00-15:30 Query
Location: phone
Status: confirmed
Organizer: A. Luser <a.luser@foo.com>
Attendee: Luser, Other <other.luser@foo.com> (needs-action)
Access: public
UID: 040000008200E00074C5B7101A82E0080000000020FFAED0CFEFCC01000000000000000010000000575268034ECDB649A15349B1BF240F15
Description: Whassup?

View file

@ -0,0 +1,6 @@
&12/8/2014 18:30-22:55 Norwegian til Tromsoe-Langnes -
Location: Stavanger-Sola
Category: Appointment
Access: public
UID: RFCALITEM1
Description: Fly med Norwegian, reservasjon. Fra Stavanger til Troms&#248; 8. des 2014 18:30, DY545Fly med Norwegian, reservasjon . Fra Stavanger til Troms&#248; 8. des 2014 21:00, DY390

View file

@ -0,0 +1,6 @@
&8/12/2014 18:30-22:55 Norwegian til Tromsoe-Langnes -
Location: Stavanger-Sola
Category: Appointment
Access: public
UID: RFCALITEM1
Description: Fly med Norwegian, reservasjon. Fra Stavanger til Troms&#248; 8. des 2014 18:30, DY545Fly med Norwegian, reservasjon . Fra Stavanger til Troms&#248; 8. des 2014 21:00, DY390

View file

@ -0,0 +1,6 @@
&2014/12/8 18:30-22:55 Norwegian til Tromsoe-Langnes -
Location: Stavanger-Sola
Category: Appointment
Access: public
UID: RFCALITEM1
Description: Fly med Norwegian, reservasjon. Fra Stavanger til Troms&#248; 8. des 2014 18:30, DY545Fly med Norwegian, reservasjon . Fra Stavanger til Troms&#248; 8. des 2014 21:00, DY390

View file

@ -0,0 +1,11 @@
&%%(diary-rrule :rule '((FREQ MONTHLY) (BYDAY ((3 . 1))) (INTERVAL 1))
:exclude
'((0 46 11 6 1 2016 3 -1 0) (0 46 11 3 2 2016 3 -1 0)
(0 46 11 2 3 2016 3 -1 0) (0 46 10 4 5 2016 3 -1 0)
(0 46 10 1 6 2016 3 -1 0))
:start '(0 46 12 2 12 2015 3 -1 nil) :duration
'(0 14 3 0 nil nil nil -1 nil)) Summary
Location: Loc
Access: private
UID: 9188710a-08a7-4061-bae3-d4cf4972599a
Description: Desc

View file

@ -0,0 +1 @@
&11/5/2018 21:00 event with same start/end time

View file

@ -0,0 +1 @@
&5/11/2018 21:00 event with same start/end time

View file

@ -0,0 +1 @@
&2018/11/5 21:00 event with same start/end time

View file

@ -0,0 +1,12 @@
&%%(diary-rrule :rule '((FREQ WEEKLY) (INTERVAL 1) (BYDAY (1 3 4 5)))
:start '(0 30 11 21 4 2010 3 -1 nil) :duration
'(0 30 0 0 nil nil nil -1 nil)) Scrum
Status: confirmed
Access: public
UID: 8814e3f9-7482-408f-996c-3bfe486a1262
&%%(diary-rrule :rule '((FREQ WEEKLY) (INTERVAL 1) (BYDAY (2 4))) :start
'(4 22 2010) :duration
'(nil nil nil 1 nil nil nil -1 nil)) Tues + Thurs thinking
Access: public
UID: 8814e3f9-7482-408f-996c-3bfe486a1263

View file

@ -0,0 +1,5 @@
&%%(diary-rrule :rule
'((FREQ DAILY) (UNTIL (12 29 2001)) (INTERVAL 1) (WKST 0))
:start '(12 21 2001)) Urlaub
Access: public
UID: 20041127T183329Z-18215-1001-4536-49109@andromeda

View file

@ -0,0 +1 @@
&%%(diary-block 2 17 2005 2 23 2005) duration

View file

@ -0,0 +1 @@
&%%(diary-block 17 2 2005 23 2 2005) duration

View file

@ -0,0 +1 @@
&%%(diary-block 2005 2 17 2005 2 23) duration

View file

@ -0,0 +1,10 @@
SUMMARY: Testing legacy `icalendar-import-format' function
DESCRIPTION: described
CLASS: private
LOCATION: somewhere
ORGANIZER: mailto:baz@example.com
STATUS: CONFIRMED
URL: http://example.com/foo/baz
UID: some-unique-id-here
DTSTART: 20250919T090000
DTEND: 20250919T113000

View file

@ -0,0 +1,8 @@
9/19/2025 09:00-11:30 Testing legacy `icalendar-import-format*' vars
CLASS=private
DESCRIPTION=described
LOCATION=somewhere
ORGANIZER=mailto:baz@example.com
STATUS=confirmed
URL=http://example.com/foo/baz
UID=some-unique-id-here

View file

@ -0,0 +1,8 @@
19/9/2025 09:00-11:30 Testing legacy `icalendar-import-format*' vars
CLASS=private
DESCRIPTION=described
LOCATION=somewhere
ORGANIZER=mailto:baz@example.com
STATUS=confirmed
URL=http://example.com/foo/baz
UID=some-unique-id-here

View file

@ -0,0 +1,8 @@
2025/9/19 09:00-11:30 Testing legacy `icalendar-import-format*' vars
CLASS=private
DESCRIPTION=described
LOCATION=somewhere
ORGANIZER=mailto:baz@example.com
STATUS=confirmed
URL=http://example.com/foo/baz
UID=some-unique-id-here

View file

@ -0,0 +1,7 @@
&7/23/2011 event-1
&7/24/2011 event-2
&7/25/2011 event-3a
&7/25/2011 event-3b

View file

@ -0,0 +1,7 @@
&23/7/2011 event-1
&24/7/2011 event-2
&25/7/2011 event-3a
&25/7/2011 event-3b

View file

@ -0,0 +1,7 @@
&2011/7/23 event-1
&2011/7/24 event-2
&2011/7/25 event-3a
&2011/7/25 event-3b

View file

@ -0,0 +1 @@
&9/19/2003 09:00-11:30 non-recurring

View file

@ -0,0 +1 @@
&19/9/2003 09:00-11:30 non-recurring

View file

@ -0,0 +1 @@
&2003/9/19 09:00-11:30 non-recurring

View file

@ -0,0 +1 @@
&9/19/2003 non-recurring allday

View file

@ -0,0 +1 @@
&19/9/2003 non-recurring allday

View file

@ -0,0 +1 @@
&2003/9/19 non-recurring allday

View file

@ -0,0 +1,4 @@
&11/23/2004 14:45-15:45 another example
Status: tentative
Access: private
UID: 6161a312-3902-11d9-b512-f764153bb28b

View file

@ -0,0 +1,4 @@
&23/11/2004 14:45-15:45 another example
Status: tentative
Access: private
UID: 6161a312-3902-11d9-b512-f764153bb28b

View file

@ -0,0 +1,4 @@
&2004/11/23 14:45-15:45 another example
Status: tentative
Access: private
UID: 6161a312-3902-11d9-b512-f764153bb28b

View file

@ -0,0 +1,4 @@
&%%(diary-block 7 19 2004 8 27 2004) Sommerferien
Status: tentative
Access: private
UID: 748f2da0-0d9b-11d8-97af-b4ec8686ea61

View file

@ -0,0 +1,4 @@
&%%(diary-block 19 7 2004 27 8 2004) Sommerferien
Status: tentative
Access: private
UID: 748f2da0-0d9b-11d8-97af-b4ec8686ea61

View file

@ -0,0 +1,4 @@
&%%(diary-block 2004 7 19 2004 8 27) Sommerferien
Status: tentative
Access: private
UID: 748f2da0-0d9b-11d8-97af-b4ec8686ea61

View file

@ -0,0 +1,4 @@
&11/23/2004 14:00-14:30 folded summary
Status: tentative
Access: private
UID: 04979712-3902-11d9-93dd-8f9f4afe08da

View file

@ -0,0 +1,4 @@
&23/11/2004 14:00-14:30 folded summary
Status: tentative
Access: private
UID: 04979712-3902-11d9-93dd-8f9f4afe08da

View file

@ -0,0 +1,4 @@
&2004/11/23 14:00-14:30 folded summary
Status: tentative
Access: private
UID: 04979712-3902-11d9-93dd-8f9f4afe08da

View file

@ -0,0 +1 @@
&9/19/2003 long summary

View file

@ -0,0 +1 @@
&19/9/2003 long summary

View file

@ -0,0 +1 @@
&2003/9/19 long summary

View file

@ -0,0 +1,10 @@
&5/9/2003 10:30-15:30 On-Site Interview
Location: Cccc
Status: confirmed
Organizer: Aaaaaa Aaaaa <aaaaaaa@aaaaaaa.com>
Attendees:
Xxxxxxxx Xxxxxxxxxxxx <xxxxxxxx@xxxxxxx.com> (needs-action)
Yyyyyyy Yyyyy <yyyyyyy@yyyyyyy.com> (needs-action)
Zzzz Zzzzzz <zzzzzz@zzzzzzz.com> (needs-action)
UID: 040000008200E00074C5B7101A82E0080000000080B6DE661216C301000000000000000010000000DB823520692542408ED02D7023F9DFF9
Description: 10:30am - Blah

View file

@ -0,0 +1,10 @@
&9/5/2003 10:30-15:30 On-Site Interview
Location: Cccc
Status: confirmed
Organizer: Aaaaaa Aaaaa <aaaaaaa@aaaaaaa.com>
Attendees:
Xxxxxxxx Xxxxxxxxxxxx <xxxxxxxx@xxxxxxx.com> (needs-action)
Yyyyyyy Yyyyy <yyyyyyy@yyyyyyy.com> (needs-action)
Zzzz Zzzzzz <zzzzzz@zzzzzzz.com> (needs-action)
UID: 040000008200E00074C5B7101A82E0080000000080B6DE661216C301000000000000000010000000DB823520692542408ED02D7023F9DFF9
Description: 10:30am - Blah

View file

@ -0,0 +1,8 @@
&6/23/2003 11:00-12:00 Dress Rehearsal for XXXX-XXXX
Location: 555 or TN 555-5555 ID 5555 & NochWas (see below)
Status: confirmed
Organizer: ABCD,TECHTRAINING(A-Americas,exgen1) <xxx@xxxxx.com>
Attendee:
AAAAA,AAAAA (A-AAAAAAA,ex1) <aaaaa_aaaaa@aaaaa.com> (needs-action)
UID: 040000008200E00074C5B7101A82E00800000000608AA7DA9835C3010000000000000000100000007C3A6D65EE726E40B7F3D69A23BD567E
Description: 753 Zeichen hier radiert

View file

@ -0,0 +1,8 @@
&23/6/2003 11:00-12:00 Dress Rehearsal for XXXX-XXXX
Location: 555 or TN 555-5555 ID 5555 & NochWas (see below)
Status: confirmed
Organizer: ABCD,TECHTRAINING(A-Americas,exgen1) <xxx@xxxxx.com>
Attendee:
AAAAA,AAAAA (A-AAAAAAA,ex1) <aaaaa_aaaaa@aaaaa.com> (needs-action)
UID: 040000008200E00074C5B7101A82E00800000000608AA7DA9835C3010000000000000000100000007C3A6D65EE726E40B7F3D69A23BD567E
Description: 753 Zeichen hier radiert

View file

@ -0,0 +1,6 @@
&6/23/2003 17:00-18:00 Updated: Dress Rehearsal for ABC01-15
Desc: Viele Zeichen standen hier früher
Location: 123 or TN 123-1234 ID abcd & SonstWo (see below)
Organizer: MAILTO:bbb@bbbbb.com
Status: CONFIRMED
UID: 040000008200E00074C5B7101A82E00800000000608AA7DA9835C3010000000000000000100000007C3A6D65EE726E40B7F3D69A23BD567E

View file

@ -0,0 +1,5 @@
&23/6/2003 09:00-10:00 Updated: Dress Rehearsal for ABC01-15
Location: 123 or TN 123-1234 ID abcd & SonstWo (see below)
Status: confirmed
UID: 040000008200E00074C5B7101A82E00800000000608AA7DA9835C3010000000000000000100000007C3A6D65EE726E40B7F3D69A23BD567E
Description: Viele Zeichen standen hier früher

View file

@ -0,0 +1,28 @@
&%%(diary-rrule :rule '((FREQ WEEKLY) (INTERVAL 1) (BYDAY (1))) :start
'(11 1 2004) :duration
'(nil nil nil 1 nil nil nil -1 nil)) Wwww aa hhhh
Status: tentative
Access: private
&%%(diary-rrule :rule '((FREQ WEEKLY) (INTERVAL 2) (BYDAY (5))) :start
'(0 0 14 12 11 2004 5 -1 nil) :duration
'(0 30 4 0 nil nil nil -1 nil)) MMM Aaaaaaaaa
Status: tentative
Access: private
&%%(diary-block 11 19 2004 11 19 2004) Rrrr/Cccccc ii Aaaaaaaa
Status: tentative
Access: private
Description: Vvvvv Rrrr aaa Cccccc
&11/23/2004 11:00-12:00 Hhhhhhhh
Status: tentative
Access: private
&11/23/2004 14:00-14:30 Jjjjj & Wwwww
Status: tentative
Access: private
&11/23/2004 14:45-15:45 BB Aaaaaaaa Bbbbb
Status: tentative
Access: private

View file

@ -0,0 +1,28 @@
&%%(diary-rrule :rule '((FREQ WEEKLY) (INTERVAL 1) (BYDAY (1))) :start
'(11 1 2004) :duration
'(nil nil nil 1 nil nil nil -1 nil)) Wwww aa hhhh
Status: tentative
Access: private
&%%(diary-rrule :rule '((FREQ WEEKLY) (INTERVAL 2) (BYDAY (5))) :start
'(0 0 14 12 11 2004 5 -1 nil) :duration
'(0 30 4 0 nil nil nil -1 nil)) MMM Aaaaaaaaa
Status: tentative
Access: private
&%%(diary-block 19 11 2004 19 11 2004) Rrrr/Cccccc ii Aaaaaaaa
Status: tentative
Access: private
Description: Vvvvv Rrrr aaa Cccccc
&23/11/2004 11:00-12:00 Hhhhhhhh
Status: tentative
Access: private
&23/11/2004 14:00-14:30 Jjjjj & Wwwww
Status: tentative
Access: private
&23/11/2004 14:45-15:45 BB Aaaaaaaa Bbbbb
Status: tentative
Access: private

View file

@ -0,0 +1,6 @@
&%%(diary-block 2 6 2005 2 6 2005) Waitangi Day
Status: confirmed
Category: Public Holiday
Access: private
UID: b60d398e-1dd1-11b2-a159-cf8cb05139f4
Description: abcdef

View file

@ -0,0 +1,6 @@
&%%(diary-block 6 2 2005 6 2 2005) Waitangi Day
Status: confirmed
Category: Public Holiday
Access: private
UID: b60d398e-1dd1-11b2-a159-cf8cb05139f4
Description: abcdef

View file

@ -0,0 +1,2 @@
&%%(diary-block 2 17 2005 2 23 2005) Hhhhhh Aaaaa ii Aaaaaaaa
UID: 6AFA7558-6994-11D9-8A3A-000A95A0E830-RID

View file

@ -0,0 +1,2 @@
&%%(diary-block 17 2 2005 23 2 2005) Hhhhhh Aaaaa ii Aaaaaaaa
UID: 6AFA7558-6994-11D9-8A3A-000A95A0E830-RID

View file

@ -0,0 +1,4 @@
&11/16/2014 07:00-08:00 NoDST
Location: Everywhere
UID: 20141116T171439Z-678877132@marudot.com
Description: Test event from timezone without DST

View file

@ -0,0 +1,4 @@
&16/11/2014 07:00-08:00 NoDST
Location: Everywhere
UID: 20141116T171439Z-678877132@marudot.com
Description: Test event from timezone without DST

View file

@ -0,0 +1 @@
&%%(diary-rrule :rule '((FREQ YEARLY)) :start '(8 15 2004)) Maria Himmelfahrt

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ WEEKLY) (COUNT 3) (INTERVAL 2)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule count bi-weekly 3 times

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ DAILY) (COUNT 14) (INTERVAL 1)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule count daily long

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ DAILY) (COUNT 1) (INTERVAL 1)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule count daily short

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ MONTHLY) (INTERVAL 2) (COUNT 5)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule count every second month

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ YEARLY) (INTERVAL 2) (COUNT 5)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule count every second year

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ MONTHLY) (INTERVAL 1) (COUNT 5)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule count monthly

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ YEARLY) (INTERVAL 1) (COUNT 5)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule count yearly

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ DAILY) (INTERVAL 2)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule daily

View file

@ -0,0 +1,4 @@
&%%(diary-rrule :rule '((FREQ DAILY) (INTERVAL 2)) :exclude
'((9 21 2003) (9 25 2003)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule daily with exceptions

View file

@ -0,0 +1,2 @@
&%%(diary-rrule :rule '((FREQ DAILY)) :start '(0 0 9 19 9 2003 5 -1 nil)
:duration '(0 30 2 0 nil nil nil -1 nil)) rrule daily

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ MONTHLY)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule monthly no end

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ MONTHLY) (UNTIL (8 19 2005))) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule monthly with end

View file

@ -0,0 +1,2 @@
&%%(diary-rrule :rule '((FREQ WEEKLY)) :start '(0 0 9 19 9 2003 5 -1 nil)
:duration '(0 30 2 0 nil nil nil -1 nil)) rrule weekly

View file

@ -0,0 +1,3 @@
&%%(diary-rrule :rule '((FREQ YEARLY) (INTERVAL 2)) :start
'(0 0 9 19 9 2003 5 -1 nil) :duration
'(0 30 2 0 nil nil nil -1 nil)) rrule yearly

View file

@ -0,0 +1 @@
&2003/9/19 9.00h-11.30h 12hr blank-padded

View file

@ -0,0 +1,3 @@
&2003/9/19 09:00 Has an attachment
Attachment: R3Jl.plain
UID: f9fee9a0-1231-4984-9078-f1357db352db

View file

@ -0,0 +1,3 @@
&2012/1/15 15:00-15:30 standardtime
&2012/12/15 11:00-11:30 daylightsavingtime

View file

@ -0,0 +1,2 @@
&9/19/2003 09:00-11:30 non-recurring
UID: 1234567890uid

View file

@ -0,0 +1,2 @@
&19/9/2003 09:00-11:30 non-recurring
UID: 1234567890uid

View file

@ -0,0 +1,2 @@
&2003/9/19 09:00-11:30 non-recurring
UID: 1234567890uid

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Testing legacy `icalendar-import-format' function
DESCRIPTION:described
CLASS:private
LOCATION:somewhere
ORGANIZER;CN="Baz Foo":mailto:baz@example.com
STATUS:CONFIRMED
URL:http://example.com/foo/baz
UID:some-unique-id-here
DTSTART;VALUE=DATE-TIME:20250919T090000
DTEND;VALUE=DATE-TIME:20250919T113000
END:VEVENT
END:VCALENDAR

View file

@ -0,0 +1,16 @@
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Testing legacy `icalendar-import-format*' vars
DESCRIPTION:described
CLASS:private
LOCATION:somewhere
ORGANIZER;CN="Baz Foo":mailto:baz@example.com
STATUS:CONFIRMED
URL:http://example.com/foo/baz
UID:some-unique-id-here
DTSTART;VALUE=DATE-TIME:20250919T090000
DTEND;VALUE=DATE-TIME:20250919T113000
END:VEVENT
END:VCALENDAR

View file

@ -1,9 +1,8 @@
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:non-recurring allday
DTSTART;VALUE=DATE-TIME:20030919
END:VEVENT
END:VCALENDAR
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:non-recurring allday
DTSTART;VALUE=DATE:20030919
END:VEVENT
END:VCALENDAR

View file

@ -1,11 +1,10 @@
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
DTSTART;VALUE=DATE:20040815
DTEND;VALUE=DATE:20040816
SUMMARY:Maria Himmelfahrt
RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=8
END:VEVENT
END:VCALENDAR
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
DTSTART;VALUE=DATE:20040815
SUMMARY:Maria Himmelfahrt
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR

View file

@ -1,12 +1,11 @@
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:rrule daily with exceptions
DTSTART;VALUE=DATE-TIME:20030919T090000
DTEND;VALUE=DATE-TIME:20030919T113000
RRULE:FREQ=DAILY;INTERVAL=2
EXDATE:20030921,20030925
END:VEVENT
END:VCALENDAR
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:rrule daily with exceptions
DTSTART;VALUE=DATE-TIME:20030919T090000
DTEND;VALUE=DATE-TIME:20030919T113000
RRULE:FREQ=DAILY;INTERVAL=2
EXDATE;VALUE=DATE:20030921,20030925
END:VEVENT
END:VCALENDAR

View file

@ -1,11 +1,11 @@
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:rrule daily
DTSTART;VALUE=DATE-TIME:20030919T090000
DTEND;VALUE=DATE-TIME:20030919T113000
RRULE:FREQ=DAILY;
END:VEVENT
END:VCALENDAR
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:rrule daily
DTSTART;VALUE=DATE-TIME:20030919T090000
DTEND;VALUE=DATE-TIME:20030919T113000
RRULE:FREQ=DAILY
END:VEVENT
END:VCALENDAR

View file

@ -1,11 +1,11 @@
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:rrule monthly no end
DTSTART;VALUE=DATE-TIME:20030919T090000
DTEND;VALUE=DATE-TIME:20030919T113000
RRULE:FREQ=MONTHLY;
END:VEVENT
END:VCALENDAR
BEGIN:VCALENDAR
PRODID:-//Emacs//NONSGML icalendar.el//EN
VERSION:2.0
BEGIN:VEVENT
SUMMARY:rrule monthly no end
DTSTART;VALUE=DATE-TIME:20030919T090000
DTEND;VALUE=DATE-TIME:20030919T113000
RRULE:FREQ=MONTHLY
END:VEVENT
END:VCALENDAR

Some files were not shown because too many files have changed in this diff Show more