mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-02-16 17:24:23 +00:00
Fix some misuses of `defconst` and `%s`, avoid uses of the old dynbound dialect of `eval`, delete redundant `:group` arguments, and prefer #' to quote function names. And stop creating alists for completion tables since lists of strings work just as well. * lisp/calendar/appt.el: Delete redundant `:group` arguments. Prefer #' to quote function names. * lisp/calendar/cal-hebrew.el (solar-time-string): Declare function. (calendar-hebrew-read-date, calendar-hebrew-list-yahrzeits): * lisp/calendar/cal-persia.el (calendar-persian-read-date): * lisp/calendar/cal-mayan.el (calendar-mayan-read-haab-date) (calendar-mayan-read-tzolkin-date): * lisp/calendar/cal-julian.el (calendar-julian-goto-date): * lisp/calendar/cal-islam.el (calendar-islamic-read-date): * lisp/calendar/cal-coptic.el (calendar-coptic-read-date): * lisp/calendar/cal-bahai.el (calendar-bahai-read-date): Completion tables can be lists of strings for decades. * lisp/calendar/cal-china.el: Delete redundant `:group` arguments. (calendar-chinese-zodiac-sign-on-or-after) (calendar-chinese-new-moon-on-or-after): Use lexical dialect also for `eval`. * lisp/calendar/cal-dst.el: Delete redundant `:group` arguments. Use lexical dialect also for `eval`. * lisp/calendar/cal-html.el: Delete redundant `:group` arguments. * lisp/calendar/cal-menu.el: Prefer #' to quote function names. * lisp/calendar/cal-tex.el: Delete redundant `:group` arguments. (cal-tex-weekly-common): Use lexical dialect also for `eval`. * lisp/calendar/calendar.el (calendar-mode-map): Prefer #' to quote function names. * lisp/calendar/diary-icalendar.el (di:rrule-sexp-to-recurrence) (di:other-sexp-to-recurrence): Use lexical dialect also for `eval`. * lisp/calendar/diary-lib.el: Delete redundant `:group` arguments. Prefer #' to quote function names. (diary-sexp-entry, diary-offset, diary-remind): Use lexical dialect also for `eval`. * lisp/calendar/icalendar-macs.el (ical:define-param): Fix misuse of `%s` in `format`. * lisp/calendar/icalendar-parser.el (ical:value-types) (ical:param-types, ical:property-types, ical:component-types): Don't use `defconst` on variables we mutate. * lisp/calendar/icalendar-recur.el (icr:current-tz-to-vtimezone) (icr:current-tz-to-vtimezone): Use lexical dialect also for `eval`. * lisp/calendar/icalendar.el: Delete redundant `:group` arguments. * lisp/calendar/lunar.el: Prefer #' to quote function names. * lisp/calendar/solar.el (solar-sunrise-sunset-string) (calendar-sunrise-sunset-month): Use lexical dialect also for `eval`. * lisp/calendar/timeclock.el: Prefer #' to quote function names.
4887 lines
201 KiB
EmacsLisp
4887 lines
201 KiB
EmacsLisp
;;; icalendar-parser.el --- Parse iCalendar grammar -*- 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 regular expressions, constants and functions that
|
||
;; implement the iCalendar grammar according to RFC5545.
|
||
;;
|
||
;; iCalendar data is grouped into *components*, such as events or
|
||
;; to-do items. Each component contains one or more *content lines*,
|
||
;; which each contain a *property* name and its *value*, and possibly
|
||
;; also property *parameters* with additional data that affects the
|
||
;; interpretation of the property.
|
||
;;
|
||
;; The macros `ical:define-type', `ical:define-param',
|
||
;; `ical:define-property' and `ical:define-component', defined in
|
||
;; icalendar-macs.el, each create rx-style regular expressions for one
|
||
;; of these categories in the grammar and are used here to define the
|
||
;; particular value types, parameters, properties and components in the
|
||
;; standard as type symbols. These type symbols store all the metadata
|
||
;; about the relevant types, and are used for type-based dispatch in the
|
||
;; parser and printer functions. In the abstract syntax tree, each node
|
||
;; contains a type symbol naming its type. A number of other regular
|
||
;; expressions which encode basic categories of the grammar are also
|
||
;; defined in this file.
|
||
;;
|
||
;; The following functions provide the high-level interface to the parser:
|
||
;;
|
||
;; `icalendar-parse-and-index'
|
||
;; `icalendar-parse'
|
||
;; `icalendar-parse-calendar'
|
||
;; `icalendar-parse-component'
|
||
;; `icalendar-parse-property'
|
||
;; `icalendar-parse-params'
|
||
;;
|
||
;; The format of the abstract syntax tree which these functions create
|
||
;; is documented in icalendar-ast.el. Nodes in this tree can be
|
||
;; serialized to iCalendar format with the corresponding printer
|
||
;; functions:
|
||
;;
|
||
;; `icalendar-print-calendar-node'
|
||
;; `icalendar-print-component-node'
|
||
;; `icalendar-print-property-node'
|
||
;; `icalendar-print-params'
|
||
|
||
;;; Code:
|
||
|
||
(require 'icalendar)
|
||
(eval-when-compile (require 'icalendar-macs))
|
||
(require 'icalendar-ast)
|
||
(eval-when-compile (require 'cl-lib))
|
||
(require 'subr-x)
|
||
(require 'seq)
|
||
(require 'rx)
|
||
(require 'calendar)
|
||
(require 'time-date)
|
||
(require 'simple)
|
||
(require 'help-mode)
|
||
|
||
;;; Customization
|
||
(defgroup icalendar-parser nil
|
||
"iCalendar parsing options."
|
||
:version "31.1"
|
||
:group 'icalendar
|
||
:prefix 'icalendar)
|
||
|
||
(defcustom ical:parse-strictly nil
|
||
"When non-nil, iCalendar data will be parsed strictly.
|
||
|
||
By default, the iCalendar parser accepts certain harmless deviations
|
||
from RFC5545 that are common in real-world data (e.g., unescaped commas
|
||
in text values). Setting this to t will cause the parser to produce
|
||
errors instead of silently accepting such data."
|
||
:version "31.1"
|
||
:type '(choice (const :tag "Ignore minor errors" nil)
|
||
(const :tag "Parse strictly" t)))
|
||
|
||
;;; Functions for folding and unfolding
|
||
;;
|
||
;; According to RFC5545, iCalendar content lines longer than 75 octets
|
||
;; should be *folded* by inserting extra line breaks and leading
|
||
;; whitespace to continue the line. Such lines must be *unfolded*
|
||
;; before they can be parsed. Unfolding can only reliably happen
|
||
;; before Emacs decodes a region of text, because decoding potentially
|
||
;; replaces the CR-LF line endings which terminate content lines.
|
||
;; Programs that can control when decoding happens should use the
|
||
;; stricter `ical:unfold-undecoded-region' to unfold text; programs
|
||
;; that must work with decoded data should use the looser
|
||
;; `ical:unfold-region'. `ical:fold-region' will fold content lines
|
||
;; using line breaks appropriate to the buffer's coding system.
|
||
;;
|
||
;; All the parsing-related code belows assumes that lines have
|
||
;; already been unfolded if necessary.
|
||
(defcustom ical:pre-unfolding-hook nil
|
||
"Hook run before unfolding iCalendar data.
|
||
|
||
The functions in this hook will be run before the iCalendar data is
|
||
\"unfolded\", i.e., before whitespace introduced for breaking long lines
|
||
is removed (see `icalendar-unfold-region' and
|
||
`icalendar-unfold-undecoded-region'). If you routinely receive
|
||
iCalendar data that is not correctly folded, you can add functions to
|
||
this hook which clean up that data before unfolding is attempted.
|
||
|
||
Each function should accept zero arguments and should perform its
|
||
operation on the entire current buffer."
|
||
:version "31.1"
|
||
:type '(hook)
|
||
:options '(ical:fix-line-endings))
|
||
|
||
(defun ical:fix-line-endings ()
|
||
"Convert all line endings to LF.
|
||
This function is intended to be used from `icalendar-pre-unfolding-hook'
|
||
(which see) to make files with inconsistent line endings parseable."
|
||
(when ical:parse-strictly
|
||
(ical:warn
|
||
(concat "Converting line endings to LF causes parsing "
|
||
"errors when `icalendar-parse-strictly' is non-nil.")))
|
||
(goto-char (point-min))
|
||
(while (re-search-forward "\r\n?" nil t)
|
||
(replace-match "\n")))
|
||
|
||
(defun ical:unfold-undecoded-region (start end &optional buffer)
|
||
"Unfold an undecoded region in BUFFER between START and END.
|
||
If omitted, BUFFER defaults to the current buffer.
|
||
|
||
\"Unfolding\" means removing the whitespace characters inserted to
|
||
continue lines longer than 75 octets (see `icalendar-fold-region'
|
||
for the folding operation). RFC5545 specifies these whitespace
|
||
characters to be a CR-LF sequence followed by a single space or
|
||
tab character. Unfolding can only be done reliably before a
|
||
region is decoded, since decoding potentially replaces CR-LF line
|
||
endings.
|
||
|
||
When `icalendar-parse-strictly' is non-nil, this function searches
|
||
strictly for CR-LF sequences and will fail if they have already been
|
||
replaced, so it should only be called with a region that has not yet
|
||
been decoded. Otherwise, it also searches for folds containing
|
||
Unix-style LF line endings, since these are common in real data."
|
||
(with-current-buffer (or buffer (current-buffer))
|
||
(let ((modp (buffer-modified-p)))
|
||
(with-restriction start end
|
||
(run-hooks 'ical:pre-unfolding-hook)
|
||
(goto-char (point-min))
|
||
;; Testing reveals that a *significant* amount of real-world data
|
||
;; does not use CR-LF line endings, even if it is otherwise
|
||
;; OK. So unless we're explicitly parsing strictly, we allow the
|
||
;; CR to be missing, as we do in `icalendar-unfold-region':
|
||
(let ((fold (if ical:parse-strictly (rx (seq "\r\n" (or " " "\t")))
|
||
(rx (seq (zero-or-one "\r") "\n" (or " " "\t"))))))
|
||
(while (re-search-forward fold nil t)
|
||
(replace-match "" nil nil)))
|
||
;; merely unfolding should not mark the buffer as modified;
|
||
;; this prevents querying the user before killing it:
|
||
(set-buffer-modified-p modp)))))
|
||
|
||
(defun ical:unfold-region (start end &optional buffer)
|
||
"Unfold region between START and END in BUFFER (default: current buffer).
|
||
|
||
\"Unfolding\" means removing the whitespace characters inserted to
|
||
continue lines longer than 75 octets (see `icalendar-fold-region'
|
||
for the folding operation).
|
||
|
||
Returns the new end position after unfolding finishes. Thus this
|
||
function is a suitable FROM-FN (decoding function) for `format-alist'.
|
||
|
||
WARNING: Unfolding can only be done reliably before text is
|
||
decoded, since decoding potentially replaces CR-LF line endings.
|
||
Unfolding an already-decoded region could lead to unexpected
|
||
results, such as displaying multibyte characters incorrectly,
|
||
depending on the contents and the coding system used.
|
||
|
||
This function attempts to do the right thing even if the region
|
||
is already decoded. If it is still undecoded, it is better to
|
||
call `icalendar-unfold-undecoded-region' directly instead, and
|
||
decode it afterward."
|
||
;; TODO: also make this a command so it can be run manually?
|
||
(with-current-buffer (or buffer (current-buffer))
|
||
(let ((was-multibyte enable-multibyte-characters)
|
||
(start-char (position-bytes start))
|
||
(end-char (position-bytes end))
|
||
(end-marker (make-marker)))
|
||
;; set a marker at the original end position so we can return
|
||
;; the updated position later:
|
||
(set-marker end-marker end)
|
||
;; we put the buffer in unibyte mode and later restore its
|
||
;; previous state, so that if the buffer was already multibyte,
|
||
;; any multibyte characters where line folds broke up their
|
||
;; bytes can be reinterpreted:
|
||
(set-buffer-multibyte nil)
|
||
(with-restriction start-char end-char
|
||
(run-hooks 'ical:pre-unfolding-hook)
|
||
(goto-char (point-min))
|
||
;; since we can't be sure that line folds have a leading CR
|
||
;; in already-decoded regions, do the best we can:
|
||
(while (re-search-forward (rx (seq (zero-or-one "\r") "\n"
|
||
(or " " "\t")))
|
||
nil t)
|
||
(replace-match "" nil nil)))
|
||
;; restore previous state, possibly reinterpreting characters:
|
||
(set-buffer-multibyte was-multibyte)
|
||
;; return the new end of the region, for format.el conversion:
|
||
(marker-position end-marker))))
|
||
|
||
(defun ical:unfolded-buffer-from-region (start end &optional buffer)
|
||
"Create a new, unfolded buffer with the same contents as the region.
|
||
|
||
Copies the buffer contents between START and END (in BUFFER, if
|
||
provided) to a new buffer and performs line unfolding in the new buffer
|
||
with `icalendar-unfold-region'. That function can in some cases have
|
||
undesirable effects; see its docstring. If BUFFER is visiting a file, it
|
||
may be better to reload its contents from that file and perform line
|
||
unfolding before decoding; see `icalendar-unfolded-buffer-from-file'.
|
||
Returns the new buffer."
|
||
(let* ((old-buffer (or buffer (current-buffer)))
|
||
(contents (with-current-buffer old-buffer
|
||
(buffer-substring start end)))
|
||
(uf-buffer (generate-new-buffer ;; TODO: again, move to modeline?
|
||
(concat " *UNFOLDED:" (buffer-name old-buffer)))))
|
||
(with-current-buffer uf-buffer
|
||
(insert contents)
|
||
(ical:unfold-region (point-min) (point-max))
|
||
;; ensure we'll use CR-LF line endings on write, even if they weren't
|
||
;; in the source data. The standard also says UTF-8 is the default
|
||
;; encoding, so use 'prefer-utf-8-dos when last-coding-system-used
|
||
;; is nil.
|
||
(setq buffer-file-coding-system
|
||
(if last-coding-system-used
|
||
(coding-system-change-eol-conversion last-coding-system-used
|
||
'dos)
|
||
'prefer-utf-8-dos))
|
||
;; inhibit auto-save-mode, which will otherwise create save
|
||
;; files containing the unfolded data; these are probably
|
||
;; not useful to the user and a nuisance when running tests:
|
||
(auto-save-mode -1))
|
||
uf-buffer))
|
||
|
||
(defun ical:unfolded-buffer-from-buffer (buffer)
|
||
"Create a new, unfolded buffer with the same contents as BUFFER.
|
||
|
||
Copies the contents of BUFFER to a new buffer and performs line
|
||
unfolding there with `icalendar-unfold-region'. That function can in
|
||
some cases have undesirable effects; see its docstring. If BUFFER is
|
||
visiting a file, it may be better to reload its contents from that file
|
||
and perform line unfolding before decoding; see
|
||
`icalendar-unfolded-buffer-from-file'. Returns the new buffer."
|
||
(with-current-buffer buffer
|
||
(ical:unfolded-buffer-from-region (point-min) (point-max) buffer)))
|
||
|
||
(defun ical:find-unfolded-buffer-visiting (filename)
|
||
"Find an existing unfolded buffer visiting FILENAME."
|
||
;; FIXME: I was previously using
|
||
;; (find-buffer-visiting filename #'ical:unfolded-p)
|
||
;; for this, but found that it would sometimes return nil even when an
|
||
;; unfolded buffer already existed for FILENAME, leading to buffers
|
||
;; getting unfolded and parsed multiple times. Hence this kludge.
|
||
(catch 'unfolded
|
||
(let ((exp-name (expand-file-name filename)))
|
||
(dolist (buf (match-buffers "UNFOLDED"))
|
||
(when (and (equal exp-name (buffer-file-name buf))
|
||
(ical:unfolded-p buf))
|
||
(throw 'unfolded buf))))))
|
||
|
||
(defun ical:unfolded-buffer-from-file (filename &optional visit beg end)
|
||
"Return a buffer visiting FILENAME with unfolded lines.
|
||
|
||
If an unfolded buffer is already visiting FILENAME, return
|
||
it. Otherwise, create a new buffer with the contents of FILENAME and
|
||
perform line unfolding with `icalendar-unfold-undecoded-region', then
|
||
decode the buffer, setting an appropriate value for
|
||
`buffer-file-coding-system', and return the new buffer. Optional
|
||
arguments VISIT, BEG, END are as in `insert-file-contents'."
|
||
(unless (and (file-exists-p filename)
|
||
(file-readable-p filename))
|
||
(error "File cannot be read: %s" filename))
|
||
(or (ical:find-unfolded-buffer-visiting filename)
|
||
(let ((uf-buffer
|
||
(generate-new-buffer
|
||
(concat " *UNFOLDED:" (file-name-nondirectory filename)))))
|
||
(with-current-buffer uf-buffer
|
||
(set-buffer-multibyte nil)
|
||
(insert-file-contents-literally filename visit beg end t)
|
||
(ical:unfold-undecoded-region (point-min) (point-max))
|
||
;; now proceed with decoding:
|
||
(set-buffer-multibyte t)
|
||
(decode-coding-inserted-region (point-min) (point-max) filename)
|
||
;; ensure we'll use CR-LF line endings on write, even if they weren't
|
||
;; in the source data. The standard also says UTF-8 is the default
|
||
;; encoding, so use 'prefer-utf-8-dos when last-coding-system-used
|
||
;; is nil. FIXME: for some reason, this doesn't seem to run at all!
|
||
(setq buffer-file-coding-system
|
||
(if last-coding-system-used
|
||
(coding-system-change-eol-conversion last-coding-system-used
|
||
'dos)
|
||
'prefer-utf-8-dos))
|
||
;; restore buffer name after renaming by set-visited-file-name:
|
||
(let ((bname (buffer-name)))
|
||
(set-visited-file-name filename t)
|
||
(rename-buffer bname))
|
||
;; merely unfolding should not mark the buffer as modified;
|
||
;; this prevents querying the user before killing it:
|
||
(set-buffer-modified-p nil)
|
||
;; inhibit auto-save-mode, which will otherwise create save
|
||
;; files containing the unfolded data; these are probably
|
||
;; not useful to the user and a nuisance when running tests:
|
||
(auto-save-mode -1))
|
||
uf-buffer)))
|
||
|
||
(defun ical:fold-region (begin end &optional annotate-only use-tabs)
|
||
"Fold content lines between BEGIN and END when longer than 75 octets.
|
||
|
||
\"Folding\" means inserting a line break and a single space
|
||
character at the beginning of the new line. If USE-TABS is
|
||
non-nil, insert a tab character instead of a single space.
|
||
|
||
RFC5545 specifies that lines longer than 75 *octets* (excluding
|
||
the line-ending CR-LF sequence) must be folded, and allows that
|
||
some implementations might fold lines in the middle of a
|
||
multibyte character. This function takes care not to do that in a
|
||
buffer where `enable-multibyte-characters' is non-nil, and only
|
||
folds between character boundaries. If the buffer is in unibyte
|
||
mode, however, and contains undecoded multibyte data, it may fold
|
||
lines in the middle of a multibyte character.
|
||
|
||
By default, this function modifies the region by inserting line folds.
|
||
If the optional argument ANNOTATE-ONLY is non-nil, it will instead leave
|
||
the buffer unmodified, and return a list of \"annotations\"
|
||
\(POSITION . LINE-FOLD), indicating where line folds in the region should
|
||
be inserted. This output is suitable for a function in
|
||
`write-region-annotation-functions'; `icalendar-folding-annotations'
|
||
is a wrapper for this function which can be added to that list."
|
||
;; TODO: also make this a command so it can be run manually?
|
||
(let (annotations)
|
||
(save-excursion
|
||
(goto-char begin)
|
||
(when (not (bolp))
|
||
(let ((inhibit-field-text-motion t))
|
||
(beginning-of-line)))
|
||
(let ((bol (point))
|
||
(eol (make-marker))
|
||
(reg-end (make-marker))
|
||
(line-fold (if use-tabs "\n\t" "\n ")))
|
||
(set-marker reg-end end)
|
||
(while (< bol reg-end)
|
||
(let ((inhibit-field-text-motion t))
|
||
(end-of-line))
|
||
(set-marker eol (point))
|
||
(when (< 75 (- (position-bytes (marker-position eol))
|
||
(position-bytes bol)))
|
||
(goto-char
|
||
;; the max of 75 excludes the two CR-LF
|
||
;; characters we're about to add:
|
||
(byte-to-position (+ 75 (position-bytes bol))))
|
||
(if annotate-only
|
||
(push (cons (point) line-fold) annotations)
|
||
(insert line-fold))
|
||
(set-marker eol (point)))
|
||
(setq bol (goto-char (1+ eol))))))
|
||
;; Return annotations, or nil if we modified the buffer directly:
|
||
(nreverse annotations)))
|
||
|
||
(defun ical:folding-annotations (start end &optional buffer)
|
||
"Return a list of annotations for folding lines in the region.
|
||
|
||
This function is a wrapper for `icalendar-fold-region' that provides the
|
||
interface to be used from `write-region-annotation-functions', which
|
||
see."
|
||
;; start may be nil or a string; see `write-region'
|
||
(if (stringp start)
|
||
(let ((buf (generate-new-buffer " *icalendar-folded*")))
|
||
(set-buffer buf)
|
||
(insert start)
|
||
(ical:fold-region (point-min) (point-max) t))
|
||
|
||
(when (bufferp buffer) (set-buffer buffer))
|
||
(ical:fold-region (or start (point-min))
|
||
(if start end (point-max))
|
||
t)))
|
||
|
||
(defun ical:contains-folded-lines-p (&optional buffer)
|
||
"Return non-nil if BUFFER contains folded content lines.
|
||
|
||
BUFFER defaults to the current buffer. Folded content lines need to be
|
||
unfolded before parsing the buffer or performing syntax
|
||
highlighting. Returns the position at the end of the first fold, or nil."
|
||
(with-current-buffer (or buffer (current-buffer))
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(re-search-forward (rx (seq line-start (or " " "\t")))
|
||
nil t))))
|
||
|
||
(defun ical:unfolded-p (&optional buffer)
|
||
"Return non-nil if BUFFER does not contain any folded content lines.
|
||
BUFFER defaults to the current buffer."
|
||
(not (ical:contains-folded-lines-p buffer)))
|
||
|
||
(defun ical:contains-unfolded-lines-p (&optional buffer)
|
||
"Return non-nil if BUFFER contains long content lines that should be folded.
|
||
|
||
Lines longer than 75 bytes need to folded before saving or transmitting
|
||
the data in BUFFER (default: current buffer). If BUFFER contains such
|
||
lines, return the position at the beginning of the first line that
|
||
requires folding."
|
||
(with-current-buffer (or buffer (current-buffer))
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(let ((bol (point))
|
||
(eol (make-marker)))
|
||
(catch 'unfolded-line
|
||
(while (< bol (point-max))
|
||
(let ((inhibit-field-text-motion t))
|
||
(end-of-line))
|
||
(set-marker eol (point))
|
||
;; the max of 75 excludes the two CR-LF characters
|
||
;; after position eol:
|
||
(when (< 75 (- (position-bytes (marker-position eol))
|
||
(position-bytes bol)))
|
||
(throw 'unfolded-line bol))
|
||
(setq bol (goto-char (1+ eol))))
|
||
nil)))))
|
||
|
||
(defun ical:folded-p (&optional buffer)
|
||
"Return non-nil if BUFFER contains no content lines that require folding.
|
||
BUFFER defaults to the current buffer."
|
||
(not (ical:contains-unfolded-lines-p buffer)))
|
||
|
||
|
||
;; Parsing-related code starts here. All the parsing code assumes that
|
||
;; content lines have already been unfolded.
|
||
|
||
;;;; Error handling:
|
||
|
||
;; Errors at the parsing stage:
|
||
;; e.g. value does not match expected regex
|
||
(define-error 'ical:parse-error "Could not parse iCalendar data" 'ical:error)
|
||
|
||
(cl-defun ical:signal-parse-error (msg &key (buffer (current-buffer))
|
||
(position (point))
|
||
(severity 2)
|
||
(line (line-number-at-pos position))
|
||
column restart-at)
|
||
(signal 'ical:parse-error
|
||
(list :message msg
|
||
:line line
|
||
:column column
|
||
:severity severity
|
||
:position position
|
||
:buffer buffer
|
||
:restart-at restart-at)))
|
||
|
||
(defun ical:handle-parse-error (err-data &optional skip-msg err-buffer)
|
||
(let* ((err-sym (car err-data))
|
||
(err-plist (cdr err-data))
|
||
(buf (plist-get err-plist :buffer))
|
||
(restart-pos (plist-get err-plist :restart-at))
|
||
(new-msg
|
||
(concat (plist-get err-plist :message)
|
||
"..."
|
||
(cond (skip-msg skip-msg)
|
||
(restart-pos (format "skipping to %d" restart-pos))
|
||
(t "skipping")))))
|
||
(setq err-plist (plist-put err-plist :message new-msg))
|
||
(setq err-plist (plist-put err-plist :severity 1))
|
||
(ical:handle-generic-error (cons err-sym err-plist) err-buffer)
|
||
(when restart-pos
|
||
(with-current-buffer buf
|
||
(goto-char restart-pos)))))
|
||
|
||
;; Errors at the printing stage:
|
||
;; e.g. default print function doesn't know how to print value
|
||
(define-error 'ical:print-error "Unable to print iCalendar data" 'ical:error)
|
||
|
||
(cl-defun ical:signal-print-error (msg &key (severity 2) node)
|
||
(signal 'ical:print-error
|
||
(list :message msg
|
||
:node node
|
||
:buffer (ical:ast-node-meta-get :buffer node)
|
||
:severity severity
|
||
:position (ical:ast-node-meta-get :begin node))))
|
||
|
||
(defun ical:handle-print-error (err-data &optional skip-msg err-buffer)
|
||
(let* ((err-sym (car err-data))
|
||
(err-plist (cdr err-data))
|
||
(new-msg (concat (plist-get err-plist :message)
|
||
"..."
|
||
(or skip-msg "skipping"))))
|
||
(setq err-plist (plist-put err-plist :message new-msg))
|
||
(setq err-plist (plist-put err-plist :severity 1))
|
||
(ical:handle-generic-error (cons err-sym err-plist) err-buffer))
|
||
(ical:handle-generic-error err-data err-buffer))
|
||
|
||
;;;; Some utilities:
|
||
(defun ical:parse-from-string (type s)
|
||
"Parse string S to an iCalendar syntax node of type TYPE.
|
||
S should not contain folded content lines."
|
||
;; TODO: support unfolding?
|
||
(with-temp-buffer
|
||
(insert s)
|
||
(goto-char (point-min))
|
||
(cond ((ical:component-type-symbol-p type)
|
||
(ical:parse-component (point-max)))
|
||
((ical:property-type-symbol-p type)
|
||
(ical:parse-property (point-max)))
|
||
((ical:param-type-symbol-p type)
|
||
(unless (looking-at-p ";")
|
||
(insert ";")
|
||
(backward-char))
|
||
(ical:parse-params (point-max)))
|
||
((ical:value-type-symbol-p type)
|
||
(ical:parse-value-node type (point-max)))
|
||
(t
|
||
(error "Don't know how to parse type %s" type)))))
|
||
|
||
(defun ical:parse-one-of (types limit)
|
||
"Parse a value, from point up to LIMIT, of one of the TYPES.
|
||
|
||
TYPES should be a list of type symbols. For each type in TYPES, the
|
||
parser function associated with that type will be called at point. The
|
||
return value of the first successful parser function is returned. If
|
||
none of the parser functions are able to parse a value, an
|
||
`icalendar-parse-error' is signaled."
|
||
(let* ((value nil)
|
||
(start (point))
|
||
(type (car types))
|
||
(parser (get type 'ical:value-parser))
|
||
(rest (cdr types)))
|
||
(while (and parser (not value))
|
||
(condition-case nil
|
||
(setq value (funcall parser limit))
|
||
(ical:parse-error
|
||
;; value of this type not found, so try again:
|
||
(goto-char start)
|
||
(setq type (car rest)
|
||
rest (cdr rest)
|
||
parser (get type 'ical:value-parser)))))
|
||
(unless value
|
||
(ical:signal-parse-error
|
||
(format "Unable to parse any of %s between %d and %d" types start limit)
|
||
:position start))
|
||
value))
|
||
|
||
(defun ical:read-list-with (reader string
|
||
&optional value-regex separators omit-nulls trim)
|
||
"Read a list of values from STRING with READER.
|
||
|
||
READER should be a reader function that accepts a single string argument.
|
||
SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string'.
|
||
SEPARATORS defaults to \"[^\\][,;]\". TRIM defaults to matching a
|
||
double quote character.
|
||
|
||
VALUE-REGEX should be a regular expression if READER assumes that
|
||
individual substrings in STRING have previously been matched
|
||
against this regex. In this case, each value in S is placed in a
|
||
temporary buffer and the match against VALUE-REGEX is performed
|
||
before READER is called."
|
||
(let* ((wrapped-reader
|
||
(if (not value-regex)
|
||
;; no need for temp buffer:
|
||
reader
|
||
;; match the regex in a temp buffer before calling reader:
|
||
(lambda (s)
|
||
(with-temp-buffer
|
||
(insert s)
|
||
(goto-char (point-min))
|
||
(unless (looking-at value-regex)
|
||
(ical:signal-parse-error
|
||
(format "Expected list of values matching '%s'" value-regex)))
|
||
(funcall reader (match-string 0))))))
|
||
(seps (or separators "[^\\][,;]"))
|
||
(trm (or trim "\""))
|
||
(raw-values (split-string string seps omit-nulls trm)))
|
||
|
||
(unless (functionp reader)
|
||
(signal 'ical:parser-error
|
||
(list (format "`%s' is not a reader function" reader))))
|
||
|
||
(mapcar wrapped-reader raw-values)))
|
||
|
||
(defun ical:read-list-of (type string
|
||
&optional separators omit-nulls trim)
|
||
"Read a list of values of type TYPE from STRING.
|
||
|
||
TYPE should be a value type symbol. The reader function
|
||
associated with that type will be called to read the successive
|
||
values in STRING, and the values will be returned as a list of
|
||
syntax nodes.
|
||
|
||
SEPARATORS, OMIT-NULLS, and TRIM are as in `split-string' and
|
||
will be passed on, if provided, to `icalendar-read-list-with'."
|
||
(let* ((reader (lambda (s) (ical:read-value-node type s)))
|
||
(val-regex (rx-to-string (get type 'ical:value-rx))))
|
||
(ical:read-list-with reader string val-regex
|
||
separators omit-nulls trim)))
|
||
|
||
(defun ical:list-of-p (list type)
|
||
"Return non-nil if each value in LIST satisfies TYPE.
|
||
TYPE should be a type specifier for `cl-typep'."
|
||
(seq-every-p (lambda (val) (cl-typep val type)) list))
|
||
|
||
(defun ical:default-value-printer (val)
|
||
"Default printer for a *single* property or parameter value.
|
||
|
||
If VAL is a string, just return it unchanged.
|
||
|
||
Otherwise, VAL should be a syntax node representing a value. In
|
||
that case, return the original string value if another was
|
||
substituted at parse time, or look up the printer function for
|
||
the node's type and call it on the value inside the node.
|
||
|
||
For properties and parameters that only allow a single value,
|
||
this function should be a sufficient value printer. It is not
|
||
sufficient for those that allow lists of values, or which have
|
||
other special requirements like quoting or escaping."
|
||
(cond ((stringp val) val)
|
||
((and (ical:ast-node-p val)
|
||
(get (ical:ast-node-type val) 'ical:value-printer))
|
||
(or (ical:ast-node-meta-get :original-value val)
|
||
(let* ((stored-value (ical:ast-node-value val))
|
||
(type (ical:ast-node-type val))
|
||
(printer (get type 'ical:value-printer)))
|
||
(funcall printer stored-value))))
|
||
;; TODO: other cases to make things easy?
|
||
;; e.g. symbols print as their names?
|
||
(t (ical:signal-print-error
|
||
(format "Don't know how to print value: %s" val)))))
|
||
|
||
|
||
;;; Section 3.1: Content lines
|
||
|
||
;; Regexp constants for parsing:
|
||
|
||
;; In the following regexps and define-* declarations, because
|
||
;; Emacs does not have named groups, we observe the following
|
||
;; convention so that the regexps can be combined in sensible ways:
|
||
;;
|
||
;; - Groups 1 through 5 are reserved for the highest-level regexes
|
||
;; created by define-param, define-property and define-component and
|
||
;; used in the match-* functions. Group 1 always represents a 'key'
|
||
;; (e.g. param or property name), group 2 always represents a
|
||
;; correctly parsed value for that key, and group 3 (if matched) an
|
||
;; invalid or unknown value.
|
||
;;
|
||
;; Groups 4 and 5 are reserved for other information in these
|
||
;; highest-level regexes, such as the parameter string between a
|
||
;; property name and its value, or unrecognized values allowed by
|
||
;; the standard and required to be treated like a default value.
|
||
;;
|
||
;; - Groups 6 through 10 are currently unused
|
||
;; - Groups 11 through 20 are reserved for significant sub-expressions
|
||
;; of individual value expressions, e.g. the number of weeks in a
|
||
;; duration value. The various read-* functions rely on these groups
|
||
;; when converting iCalendar data to Elisp data structures.
|
||
|
||
(rx-define ical:iana-token
|
||
(one-or-more (any "A-Za-z0-9" "-")))
|
||
|
||
(rx-define ical:x-name
|
||
(seq "X-"
|
||
(zero-or-one (>= 3 (any "A-Za-z0-9")) "-") ; Vendor ID
|
||
(one-or-more (any "A-Za-z0-9" "-")))) ; Name
|
||
|
||
(rx-define ical:name
|
||
(or ical:iana-token ical:x-name))
|
||
|
||
(rx-define ical:crlf
|
||
(seq #x12 #xa))
|
||
|
||
(rx-define ical:control
|
||
;; All the controls except HTAB
|
||
(any (#x00 . #x08) (#x0A . #x1F) #x7F))
|
||
|
||
;; TODO: double check that "nonascii" class actually corresponds to
|
||
;; the range in the standard
|
||
(rx-define ical:safe-char
|
||
;; Any character except ical:control, ?\", ?\;, ?:, ?,
|
||
(any #x09 #x20 #x21 (#x23 . #x2B) (#x2D . #x39) (#x3C . #x7E) nonascii))
|
||
|
||
(rx-define ical:qsafe-char
|
||
;; Any character except ical:control and ?\"
|
||
(any #x09 #x20 #x21 (#x23 . #x7E) nonascii))
|
||
|
||
(rx-define ical:quoted-string
|
||
(seq ?\" (zero-or-more ical:qsafe-char) ?\"))
|
||
|
||
(rx-define ical:paramtext
|
||
;; RFC5545 allows *zero* characters here, but that would mean we could
|
||
;; have parameters like ;FOO=;BAR="somethingelse", and what would then
|
||
;; be the value of FOO? I see no reason to allow this and it breaks
|
||
;; parameter parsing so I have required at least one char here
|
||
(one-or-more ical:safe-char))
|
||
|
||
(rx-define ical:param-name
|
||
(or ical:iana-token ical:x-name))
|
||
|
||
(rx-define ical:param-value
|
||
(or ical:paramtext ical:quoted-string))
|
||
|
||
(rx-define ical:value-char
|
||
(any #x09 #x20 (#x21 . #x7E) nonascii))
|
||
|
||
(rx-define ical:value
|
||
(zero-or-more ical:value-char))
|
||
|
||
;; some helpers for brevity, not defined in the standard:
|
||
(rx-define ical:comma-list (item-rx)
|
||
(seq item-rx
|
||
(zero-or-more (seq ?, item-rx))))
|
||
|
||
(rx-define ical:semicolon-list (item-rx)
|
||
(seq item-rx
|
||
(zero-or-more (seq ?\; item-rx))))
|
||
|
||
|
||
;;; Section 3.3: Property Value Data Types
|
||
|
||
;; Note: These definitions are here (out of order with respect to the
|
||
;; standard) because a few of them are already required for property
|
||
;; parameter definitions (section 3.2) below.
|
||
|
||
(defvar ical:value-types nil ;; populated by define-type
|
||
"Alist mapping value type strings to type symbols.
|
||
Value type strings are those which can appear in `icalendar-valuetypeparam'
|
||
parameters and specify the type of a property's value.")
|
||
|
||
(defun ical:read-value-node (type s)
|
||
"Read an iCalendar value of type TYPE from string S to a syntax node.
|
||
Returns a syntax node containing the value."
|
||
(let ((reader (get type 'ical:value-reader)))
|
||
(ical:make-ast-node type (list :value (funcall reader s)))))
|
||
|
||
(defun ical:parse-value-node (type limit)
|
||
"Parse an iCalendar value of type TYPE from point up to LIMIT.
|
||
Returns a syntax node containing the value."
|
||
(let ((value-regex (rx-to-string (get type 'ical:value-rx))))
|
||
|
||
(unless (re-search-forward value-regex limit t)
|
||
(ical:signal-parse-error
|
||
(format "No %s value between %d and %d" type (point) limit)))
|
||
|
||
(let ((begin (match-beginning 0))
|
||
(end (match-end 0))
|
||
(node (ical:read-value-node type (match-string 0))))
|
||
(ical:ast-node-meta-set node :buffer (current-buffer))
|
||
(ical:ast-node-meta-set node :begin begin)
|
||
(ical:ast-node-meta-set node :end end)
|
||
|
||
node)))
|
||
|
||
(defun ical:print-value-node (node)
|
||
"Serialize an iCalendar syntax NODE containing a value to a string."
|
||
(let* ((type (ical:ast-node-type node))
|
||
(value-printer (get type 'ical:value-printer)))
|
||
(funcall value-printer (ical:ast-node-value node))))
|
||
|
||
(defun ical:printable-value-type-symbol-p (symbol)
|
||
"Return non-nil if SYMBOL represents a printable iCalendar value type.
|
||
|
||
This means that SYMBOL names a type for a property or parameter value
|
||
defined by `icalendar-define-type' which has a print name (mainly for
|
||
use in `icalendar-valuetypeparam' parameters). That is, SYMBOL must *both*
|
||
satisfy `icalendar-value-type-symbol-p' and be associated with a print
|
||
name in `icalendar-value-types'."
|
||
(and (ical:value-type-symbol-p symbol)
|
||
(rassq symbol ical:value-types)))
|
||
|
||
(defun ical:value-node-p (node)
|
||
"Return non-nil if NODE is a syntax node whose type is a value type."
|
||
(and (ical:ast-node-p node)
|
||
(ical:value-type-symbol-p (ical:ast-node-type node))))
|
||
|
||
;;;; 3.3.1 Binary
|
||
;; from https://www.rfc-editor.org/rfc/rfc4648#section-4:
|
||
(rx-define ical:base64char
|
||
(any (?A . ?Z) (?a . ?z) (?0 . ?9) ?+ ?/))
|
||
|
||
(ical:define-type ical:binary "BINARY"
|
||
"Type for Binary values.
|
||
|
||
The parsed and printed representations are the same: a string of characters
|
||
representing base64-encoded data."
|
||
'(and string (satisfies ical:match-binary-value))
|
||
(seq (zero-or-more (= 4 ical:base64char))
|
||
(zero-or-one (or (seq (= 2 ical:base64char) "==")
|
||
(seq (= 3 ical:base64char) "="))))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.1")
|
||
|
||
;;;; 3.3.2 Boolean
|
||
(defun ical:read-boolean (s)
|
||
"Read an `icalendar-boolean' value from a string S.
|
||
S should be a match against rx `icalendar-boolean'."
|
||
(let ((upcased (upcase s)))
|
||
(cond ((equal upcased "TRUE") t)
|
||
((equal upcased "FALSE") nil)
|
||
(t (ical:signal-parse-error
|
||
(format "Expected 'TRUE' or 'FALSE'; got %s" s))))))
|
||
|
||
(defun ical:print-boolean (b)
|
||
"Serialize an `icalendar-boolean' value B to a string."
|
||
(if b "TRUE" "FALSE"))
|
||
|
||
(ical:define-type ical:boolean "BOOLEAN"
|
||
"Type for Boolean values.
|
||
|
||
When printed, either the string 'TRUE' or 'FALSE'.
|
||
When read, either t or nil."
|
||
'boolean
|
||
(or "TRUE" "FALSE")
|
||
:reader ical:read-boolean
|
||
:printer ical:print-boolean
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.2")
|
||
|
||
;;;; 3.3.3 Calendar User Address
|
||
;; Defined with URI, below
|
||
|
||
;; Dates and Times:
|
||
|
||
;;;; 3.3.4 Date
|
||
(cl-deftype ical:numeric-year () '(integer 0 9999))
|
||
(cl-deftype ical:numeric-month () '(integer 1 12))
|
||
(cl-deftype ical:numeric-monthday () '(integer 1 31))
|
||
|
||
(rx-define ical:year
|
||
(= 4 digit))
|
||
|
||
(rx-define ical:month
|
||
(= 2 digit))
|
||
|
||
(rx-define ical:mday
|
||
(= 2 digit))
|
||
|
||
(defun ical:read-date (s)
|
||
"Read an `icalendar-date' from a string S.
|
||
S should be a match against rx `icalendar-date'."
|
||
(let ((year (string-to-number (substring s 0 4)))
|
||
(month (string-to-number (substring s 4 6)))
|
||
(day (string-to-number (substring s 6 8))))
|
||
(list month day year)))
|
||
|
||
(defun ical:print-date (d)
|
||
"Serialize an `icalendar-date' to a string."
|
||
(format "%04d%02d%02d"
|
||
(calendar-extract-year d)
|
||
(calendar-extract-month d)
|
||
(calendar-extract-day d)))
|
||
|
||
(ical:define-type ical:date "DATE"
|
||
"Type for Date values.
|
||
|
||
When printed, a date is a string of digits in YYYYMMDD format.
|
||
|
||
When read, a date is a list (MONTH DAY YEAR), with the three
|
||
values being integers in the appropriate ranges; see calendar.el
|
||
for functions that work with this representation."
|
||
'(and (satisfies calendar-date-is-valid-p))
|
||
(seq ical:year ical:month ical:mday)
|
||
:reader ical:read-date
|
||
:printer ical:print-date
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.4")
|
||
|
||
;;;; 3.3.12 Time
|
||
;; (Defined here so that ical:time RX can be used in ical:date-time)
|
||
(cl-deftype ical:numeric-hour () '(integer 0 23))
|
||
(cl-deftype ical:numeric-minute () '(integer 0 59))
|
||
(cl-deftype ical:numeric-second () '(integer 0 60)) ; 60 represents a leap second
|
||
|
||
(defun ical:read-time (s)
|
||
"Read an `icalendar-time' from a string S.
|
||
S should be a match against rx `icalendar-time'."
|
||
(require 'icalendar-utils) ; for ical:make-date-time; avoids circular require
|
||
(declare-function ical:make-date-time "icalendar-utils")
|
||
(let ((hour (string-to-number (substring s 0 2)))
|
||
(minute (string-to-number (substring s 2 4)))
|
||
(second (string-to-number (substring s 4 6)))
|
||
(utcoffset (if (and (length= s 7)
|
||
(equal "Z" (substring s 6 7)))
|
||
0
|
||
;; unknown/'floating' time zone:
|
||
nil)))
|
||
(ical:make-date-time :second second
|
||
:minute minute
|
||
:hour hour
|
||
:zone utcoffset)))
|
||
|
||
(defun ical:print-time (time)
|
||
"Serialize an `icalendar-time' to a string."
|
||
(format "%02d%02d%02d%s"
|
||
(decoded-time-hour time)
|
||
(decoded-time-minute time)
|
||
(decoded-time-second time)
|
||
(if (eql 0 (decoded-time-zone time))
|
||
"Z" "")))
|
||
|
||
(defun ical:-decoded-time-p (val)
|
||
"Return non-nil if VAL is a valid decoded *time*.
|
||
This predicate does not check date-related values in VAL;
|
||
for that, see `icalendar--decoded-date-time-p'."
|
||
(and (listp val)
|
||
(length= val 9)
|
||
(cl-typep (decoded-time-second val) 'ical:numeric-second)
|
||
(cl-typep (decoded-time-minute val) 'ical:numeric-minute)
|
||
(cl-typep (decoded-time-hour val) 'ical:numeric-hour)
|
||
(cl-typep (decoded-time-dst val) '(member t nil -1))
|
||
(cl-typep (decoded-time-zone val) '(or integer null))))
|
||
|
||
(ical:define-type ical:time "TIME"
|
||
"Type for Time values.
|
||
|
||
When printed, a time is a string of six digits HHMMSS, followed
|
||
by the letter 'Z' if it is in UTC.
|
||
|
||
When read, a time is a decoded time, i.e. a list in the format
|
||
(SEC MINUTE HOUR DAY MONTH YEAR DOW DST UTCOFF). See
|
||
`decode-time' for the specifics of the individual values. When
|
||
read, the DAY, MONTH, YEAR, and DOW fields are nil, and these
|
||
fields and DST are ignored when printed."
|
||
'(satisfies ical:-decoded-time-p)
|
||
(seq (= 6 digit) (zero-or-one ?Z))
|
||
:reader ical:read-time
|
||
:printer ical:print-time
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.12")
|
||
|
||
;;;; 3.3.5 Date-Time
|
||
(defun ical:-decoded-date-time-p (val)
|
||
(and (listp val)
|
||
(length= val 9)
|
||
(cl-typep (decoded-time-second val) 'ical:numeric-second)
|
||
(cl-typep (decoded-time-minute val) 'ical:numeric-minute)
|
||
(cl-typep (decoded-time-hour val) 'ical:numeric-hour)
|
||
(cl-typep (decoded-time-day val) 'ical:numeric-monthday)
|
||
(cl-typep (decoded-time-month val) 'ical:numeric-month)
|
||
(cl-typep (decoded-time-year val) 'ical:numeric-year)
|
||
(calendar-date-is-valid-p (list (decoded-time-month val)
|
||
(decoded-time-day val)
|
||
(decoded-time-year val)))
|
||
;; FIXME: the weekday slot value should be automatically
|
||
;; calculated from month, day, and year, like:
|
||
;; (calendar-day-of-week (list month day year))
|
||
;; Although `ical:read-date-time' does this correctly,
|
||
;; `make-decoded-time' does not. Thus we can't use
|
||
;; `make-decoded-time' to construct valid `ical:date-time'
|
||
;; values unless this check is turned off,
|
||
;; which means it's annoying to write tests and anything
|
||
;; that uses cl-typecase to dispatch on values created by
|
||
;; `make-decoded-time':
|
||
;; (cl-typep (decoded-time-weekday val) '(integer 0 6))
|
||
(cl-typep (decoded-time-dst val) '(member t nil -1))
|
||
(cl-typep (decoded-time-zone val) '(or integer null))))
|
||
|
||
(defun ical:read-date-time (s)
|
||
"Read an `icalendar-date-time' from a string S.
|
||
S should be a match against rx `icalendar-date-time'."
|
||
(require 'icalendar-utils) ; for ical:make-date-time; avoids circular requires
|
||
(let ((year (string-to-number (substring s 0 4)))
|
||
(month (string-to-number (substring s 4 6)))
|
||
(day (string-to-number (substring s 6 8)))
|
||
;; "T" is index 8
|
||
(hour (string-to-number (substring s 9 11)))
|
||
(minute (string-to-number (substring s 11 13)))
|
||
(second (string-to-number (substring s 13 15)))
|
||
(utcoffset (if (and (length= s 16)
|
||
(equal "Z" (substring s 15 16)))
|
||
0
|
||
;; unknown/'floating' time zone:
|
||
nil)))
|
||
(ical:make-date-time :second second
|
||
:minute minute
|
||
:hour hour
|
||
:day day
|
||
:month month
|
||
:year year
|
||
:zone utcoffset)))
|
||
|
||
(defun ical:print-date-time (datetime)
|
||
"Serialize an `icalendar-date-time' to a string."
|
||
(format "%04d%02d%02dT%02d%02d%02d%s"
|
||
(decoded-time-year datetime)
|
||
(decoded-time-month datetime)
|
||
(decoded-time-day datetime)
|
||
(decoded-time-hour datetime)
|
||
(decoded-time-minute datetime)
|
||
(decoded-time-second datetime)
|
||
(if (ical:date-time-is-utc-p datetime)
|
||
"Z" "")))
|
||
|
||
(defun ical:date-time-is-utc-p (datetime)
|
||
"Return non-nil if DATETIME is in UTC time."
|
||
(let ((offset (decoded-time-zone datetime)))
|
||
(and offset (= 0 offset))))
|
||
|
||
(ical:define-type ical:date-time "DATE-TIME"
|
||
"Type for Date-Time values.
|
||
|
||
When printed, a date-time is a string of digits like:
|
||
YYYYMMDDTHHMMSS
|
||
where the 'T' is literal, and separates the date string from the
|
||
time string.
|
||
|
||
When read, a date-time is a decoded time, i.e. a list in the format
|
||
(SEC MINUTE HOUR DAY MONTH YEAR DOW DST UTCOFF). See
|
||
`decode-time' for the specifics of the individual values."
|
||
'(satisfies ical:-decoded-date-time-p)
|
||
(seq ical:date ?T ical:time)
|
||
:reader ical:read-date-time
|
||
:printer ical:print-date-time
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.5")
|
||
|
||
;;;; 3.3.6 Duration
|
||
(rx-define ical:dur-second
|
||
(seq (group-n 19 (one-or-more digit)) ?S))
|
||
|
||
(rx-define ical:dur-minute
|
||
(seq (group-n 18 (one-or-more digit)) ?M (zero-or-one ical:dur-second)))
|
||
|
||
(rx-define ical:dur-hour
|
||
(seq (group-n 17 (one-or-more digit)) ?H (zero-or-one ical:dur-minute)))
|
||
|
||
(rx-define ical:dur-day
|
||
(seq (group-n 16 (one-or-more digit)) ?D))
|
||
|
||
(rx-define ical:dur-week
|
||
(seq (group-n 15 (one-or-more digit)) ?W))
|
||
|
||
(rx-define ical:dur-time
|
||
(seq ?T (or ical:dur-hour ical:dur-minute ical:dur-second)))
|
||
|
||
(rx-define ical:dur-date
|
||
(seq ical:dur-day (zero-or-one ical:dur-time)))
|
||
|
||
(defun ical:read-dur-value (s)
|
||
"Read an `icalendar-dur-value' from a string S.
|
||
S should be a match against rx `icalendar-dur-value'."
|
||
;; TODO: this smells like a design flaw. Silence the byte compiler for now.
|
||
(ignore s)
|
||
(let ((sign (if (equal (match-string 20) "-") -1 1)))
|
||
(if (match-string 15)
|
||
;; dur-value specified in weeks, so just return an integer:
|
||
(* sign (string-to-number (match-string 15)))
|
||
;; otherwise, make a time delta from the other units:
|
||
(let* ((days (match-string 16))
|
||
(ndays (* sign (if days (string-to-number days) 0)))
|
||
(hours (match-string 17))
|
||
(nhours (* sign (if hours (string-to-number hours) 0)))
|
||
(minutes (match-string 18))
|
||
(nminutes (* sign (if minutes (string-to-number minutes) 0)))
|
||
(seconds (match-string 19))
|
||
(nseconds (* sign (if seconds (string-to-number seconds) 0))))
|
||
(make-decoded-time :second nseconds :minute nminutes :hour nhours
|
||
:day ndays)))))
|
||
|
||
(defun ical:print-dur-value (dur)
|
||
"Serialize an `icalendar-dur-value' to a string."
|
||
(if (integerp dur)
|
||
;; dur-value specified in weeks can only contain weeks:
|
||
(format "%sP%dW" (if (< dur 0) "-" "") (abs dur))
|
||
;; otherwise, show all the time units present:
|
||
(let* ((days+- (or (decoded-time-day dur) 0))
|
||
(hours+- (or (decoded-time-hour dur) 0))
|
||
(minutes+- (or (decoded-time-minute dur) 0))
|
||
(seconds+- (or (decoded-time-second dur) 0))
|
||
;; deal with the possibility of mixed positive and negative values
|
||
;; in a time delta list:
|
||
(sum (+ seconds+-
|
||
(* 60 minutes+-)
|
||
(* 60 60 hours+-)
|
||
(* 60 60 24 days+-)))
|
||
(abssum (abs sum))
|
||
(days (/ abssum (* 60 60 24)))
|
||
(sumnodays (mod abssum (* 60 60 24)))
|
||
(hours (/ sumnodays (* 60 60)))
|
||
(sumnohours (mod sumnodays (* 60 60)))
|
||
(minutes (/ sumnohours 60))
|
||
(seconds (mod sumnohours 60))
|
||
(sign (when (< sum 0) "-"))
|
||
(time-sep (unless (and (zerop hours) (zerop minutes) (zerop seconds))
|
||
"T")))
|
||
(concat sign
|
||
"P"
|
||
(unless (zerop days) (format "%dD" days))
|
||
time-sep
|
||
(unless (zerop hours) (format "%dH" hours))
|
||
(unless (zerop minutes) (format "%dM" minutes))
|
||
(unless (zerop seconds) (format "%dS" seconds))))))
|
||
|
||
(defun ical:-time-delta-p (val)
|
||
(and (listp val)
|
||
(length= val 9)
|
||
(let ((seconds (decoded-time-second val))
|
||
(minutes (decoded-time-minute val))
|
||
(hours (decoded-time-hour val))
|
||
(days (decoded-time-day val))) ; other values in list are ignored
|
||
(or (and (integerp seconds) (not (zerop seconds)))
|
||
(and (integerp minutes) (not (zerop minutes)))
|
||
(and (integerp hours) (not (zerop hours)))
|
||
(and (integerp days) (not (zerop days)))))))
|
||
|
||
(ical:define-type ical:dur-value "DURATION"
|
||
"Type for Duration values.
|
||
|
||
When printed, a duration is a string containing:
|
||
- possibly a +/- sign
|
||
- the letter 'P'
|
||
- one or more sequences of digits followed by a letter representing a unit
|
||
of time: 'W' for weeks, 'D' for days, etc. Units smaller than a day are
|
||
separated from days by the letter 'T'. If a duration is specified in weeks,
|
||
other units of time are not allowed.
|
||
|
||
For example, a duration of 15 days, 5 hours, and 20 seconds would be printed:
|
||
P15DT5H0M20S
|
||
and a duration of 7 weeks would be printed:
|
||
P7W
|
||
|
||
When read, a duration is either an integer, in which case it
|
||
represents a number of weeks, or a decoded time, in which case it
|
||
must represent a time delta in the sense of `decoded-time-add'.
|
||
Note that, in the time delta representation, units of time longer
|
||
than a day are not supported and will be ignored if present.
|
||
|
||
This type is named `icalendar-dur-value' rather than
|
||
`icalendar-duration' for consistency with the text of RFC5545 and
|
||
so that its name does not collide with the symbol for the
|
||
`DURATION' property."
|
||
'(or integer (satisfies ical:-time-delta-p))
|
||
;; Group 15: weeks
|
||
;; Group 16: days
|
||
;; Group 17: hours
|
||
;; Group 18: minutes
|
||
;; Group 19: seconds
|
||
;; Group 20: sign
|
||
(seq
|
||
(group-n 20 (zero-or-one (or ?+ ?-)))
|
||
?P
|
||
(or ical:dur-date ical:dur-time ical:dur-week))
|
||
:reader ical:read-dur-value
|
||
:printer ical:print-dur-value
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.6")
|
||
|
||
|
||
;;;; 3.3.7 Float
|
||
(ical:define-type ical:float "FLOAT"
|
||
"Type for Float values.
|
||
|
||
When printed, possibly a sign + or -, followed by a sequence of digits,
|
||
and possibly a decimal. When read, an Elisp float value."
|
||
'(float * *)
|
||
(seq
|
||
(zero-or-one (or ?+ ?-))
|
||
(one-or-more digit)
|
||
(zero-or-one (seq ?. (one-or-more digit))))
|
||
:reader string-to-number
|
||
:printer number-to-string
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.7")
|
||
|
||
;;;; 3.3.8 Integer
|
||
(ical:define-type ical:integer "INTEGER"
|
||
"Type for Integer values.
|
||
|
||
When printed, possibly a sign + or -, followed by a sequence of digits.
|
||
When read, an Elisp integer value between -2147483648 and 2147483647."
|
||
'(integer -2147483648 2147483647)
|
||
(seq
|
||
(zero-or-one (or ?+ ?-))
|
||
(one-or-more digit))
|
||
:reader string-to-number
|
||
:printer number-to-string
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.8")
|
||
|
||
;;;; 3.3.9 Period
|
||
(defsubst ical:period-start (period)
|
||
"Return the `icalendar-date-time' which marks the start of PERIOD."
|
||
(car period))
|
||
|
||
(defsubst ical:period--defined-end (period)
|
||
"Return the `icalendar-date-time' which marks the end of PERIOD, or nil."
|
||
(cadr period))
|
||
|
||
(defsubst ical:period-dur-value (period)
|
||
"Return the `icalendar-dur-value' which gives the length of PERIOD, or nil."
|
||
(caddr period))
|
||
|
||
(defun ical:period-end (period &optional vtimezone)
|
||
"Return the `icalendar-date-time' which marks the end of PERIOD.
|
||
If the end is not explicitly specified, it will be computed from the
|
||
period's start and duration. VTIMEZONE, if given, should be the
|
||
`icalendar-vtimezone' in which to compute the end time."
|
||
(require 'icalendar-utils) ; for date/time-add-duration; avoids circular import
|
||
(declare-function ical:date/time-add-duration "icalendar-utils")
|
||
(or (ical:period--defined-end period)
|
||
;; compute end from duration and cache it:
|
||
(setf (cadr period)
|
||
(ical:date/time-add-duration
|
||
(ical:period-start period)
|
||
(ical:period-dur-value period)
|
||
vtimezone))))
|
||
|
||
(defun ical:period-p (val)
|
||
(and (listp val)
|
||
(length= val 3)
|
||
(cl-typep (ical:period-start val) 'ical:date-time)
|
||
(cl-typep (ical:period-end val) '(or null ical:date-time))
|
||
(cl-typep (ical:period-dur-value val) '(or null ical:dur-value))))
|
||
|
||
(cl-defun ical:make-period (start &key end duration)
|
||
"Make an `icalendar-period' value.
|
||
|
||
START and END (if given) should be `icalendar-date-time' values.
|
||
DURATION, if given, should be an `icalendar-dur-value'. It is an error
|
||
to pass both END and DURATION, or neither."
|
||
(when (and end duration)
|
||
(signal 'wrong-type-argument (list end duration)))
|
||
(unless (or end duration)
|
||
(signal 'wrong-type-argument (list end duration)))
|
||
(list start end duration))
|
||
|
||
(defun ical:read-period (s)
|
||
"Read an `icalendar-period' from a string S.
|
||
S should have been matched against rx `icalendar-period'."
|
||
;; TODO: this smells like a design flaw. Silence the byte compiler for now.
|
||
(ignore s)
|
||
(let ((start (ical:read-date-time (match-string 11)))
|
||
(end (when (match-string 12) (ical:read-date-time (match-string 12))))
|
||
(dur (when (match-string 13) (ical:read-dur-value (match-string 13)))))
|
||
(ical:make-period start :end end :duration dur)))
|
||
|
||
(defun ical:print-period (per)
|
||
"Serialize an `icalendar-period' to a string."
|
||
(let ((start (ical:period-start per))
|
||
(end (ical:period-end per))
|
||
(dur (ical:period-dur-value per)))
|
||
(concat (ical:print-date-time start)
|
||
"/"
|
||
(if dur
|
||
(ical:print-dur-value dur)
|
||
(ical:print-date-time end)))))
|
||
|
||
(ical:define-type ical:period "PERIOD"
|
||
"Type for Period values.
|
||
|
||
A period of time is specified as a starting date-time together
|
||
with either an explicit date-time as its end, or a duration which
|
||
gives its length and implicitly marks its end.
|
||
|
||
When printed, the starting date-time is separated from the end or
|
||
duration by a / character.
|
||
|
||
When read, a period is represented as a list (START END DUR), where
|
||
START is an `icalendar-date-time', END is either an
|
||
`icalendar-date-time' or nil, and DUR is either an `icalendar-dur-value'
|
||
or nil. See the functions `icalendar-make-period',
|
||
`icalendar-period-start', `icalendar-period-end', and
|
||
`icalendar-period-dur-value' to work with period values."
|
||
'(satisfies ical:period-p)
|
||
(seq (group-n 11 ical:date-time)
|
||
"/"
|
||
(or (group-n 12 ical:date-time)
|
||
(group-n 13 ical:dur-value)))
|
||
:reader ical:read-period
|
||
:printer ical:print-period
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.9")
|
||
|
||
;;;; 3.3.10 Recurrence rules:
|
||
(rx-define ical:freq
|
||
(or "SECONDLY" "MINUTELY" "HOURLY" "DAILY" "WEEKLY" "MONTHLY" "YEARLY"))
|
||
|
||
(rx-define ical:weekday
|
||
(or "SU" "MO" "TU" "WE" "TH" "FR" "SA"))
|
||
|
||
(rx-define ical:ordwk
|
||
(** 1 2 digit)) ; 1 to 53
|
||
|
||
(rx-define ical:weekdaynum
|
||
;; Group 19: Week num, if present
|
||
;; Group 20: week day abbreviation
|
||
(seq (zero-or-one
|
||
(group-n 19 (seq (zero-or-one (or ?+ ?-))
|
||
ical:ordwk)))
|
||
(group-n 20 ical:weekday)))
|
||
|
||
(rx-define ical:weeknum
|
||
(seq (zero-or-one (or ?+ ?-))
|
||
ical:ordwk))
|
||
|
||
(rx-define ical:monthdaynum
|
||
(seq (zero-or-one (or ?+ ?-))
|
||
(** 1 2 digit))) ; 1 to 31
|
||
|
||
(rx-define ical:monthnum
|
||
(seq (zero-or-one (or ?+ ?-))
|
||
(** 1 2 digit))) ; 1 to 12
|
||
|
||
(rx-define ical:yeardaynum
|
||
(seq (zero-or-one (or ?+ ?-))
|
||
(** 1 3 digit))) ; 1 to 366
|
||
|
||
(defconst ical:weekday-numbers
|
||
'(("SU" . 0)
|
||
("MO" . 1)
|
||
("TU" . 2)
|
||
("WE" . 3)
|
||
("TH" . 4)
|
||
("FR" . 5)
|
||
("SA" . 6))
|
||
"Alist mapping two-letter weekday abbreviations to numbers 0 to 6.
|
||
Weekday abbreviations in recurrence rule parts are translated to
|
||
and from numbers for compatibility with calendar-* and
|
||
decoded-time-* functions.")
|
||
|
||
(defun ical:read-weekdaynum (s)
|
||
"Read a weekday abbreviation to a number.
|
||
If the abbreviation is preceded by an offset, read a dotted
|
||
pair (WEEKDAY . OFFSET). Thus \"SU\" becomes 0, \"-1SU\"
|
||
becomes (0 . -1), etc. S should have been matched against
|
||
`icalendar-weekdaynum'."
|
||
;; TODO: this smells like a design flaw. Silence the byte compiler for now.
|
||
(ignore s)
|
||
(let ((dayno (cdr (assoc (match-string 20) ical:weekday-numbers)))
|
||
(weekno (match-string 19)))
|
||
(if weekno
|
||
(cons dayno (string-to-number weekno))
|
||
dayno)))
|
||
|
||
(defun ical:print-weekdaynum (val)
|
||
"Serialize a number or dotted pair VAL to a string.
|
||
The result is in the format required for a BYDAY recurrence rule clause.
|
||
See `icalendar-read-weekdaynum' for the format of VAL."
|
||
(if (consp val)
|
||
(let* ((dayno (car val))
|
||
(day (car (rassq dayno ical:weekday-numbers)))
|
||
(offset (cdr val)))
|
||
(concat (number-to-string offset) day))
|
||
;; number alone just stands for a day:
|
||
(car (rassq val ical:weekday-numbers))))
|
||
|
||
(defun ical:read-recur-rule-part (s)
|
||
"Read an `icalendar-recur-rule-part' from string S.
|
||
S should have been matched against `icalendar-recur-rule-part'.
|
||
The return value is a list (KEYWORD VALUE), where VALUE may
|
||
itself be a list, depending on the values allowed by KEYWORD."
|
||
;; TODO: this smells like a design flaw. Silence the byte compiler for now.
|
||
(ignore s)
|
||
(let ((keyword (intern (upcase (match-string 11))))
|
||
(values (match-string 12)))
|
||
(list keyword
|
||
(cl-case keyword
|
||
(FREQ (intern (upcase values)))
|
||
(UNTIL (if (length> values 8)
|
||
(ical:read-date-time values)
|
||
(ical:read-date values)))
|
||
((COUNT INTERVAL)
|
||
(string-to-number values))
|
||
((BYSECOND BYMINUTE BYHOUR BYMONTHDAY BYYEARDAY BYWEEKNO BYMONTH BYSETPOS)
|
||
(ical:read-list-with #'string-to-number values nil ","))
|
||
(BYDAY
|
||
(ical:read-list-with #'ical:read-weekdaynum values
|
||
(rx ical:weekdaynum) ","))
|
||
(WKST (cdr (assoc values ical:weekday-numbers)))))))
|
||
|
||
(defun ical:print-recur-rule-part (part)
|
||
"Serialize recur rule part PART to a string."
|
||
(let ((keyword (car part))
|
||
(values (cadr part))
|
||
values-str)
|
||
(cl-case keyword
|
||
(FREQ (setq values-str (symbol-name values)))
|
||
(UNTIL (setq values-str (cl-typecase values
|
||
(ical:date-time (ical:print-date-time values))
|
||
(ical:date (ical:print-date values)))))
|
||
((COUNT INTERVAL)
|
||
(setq values-str (number-to-string values)))
|
||
((BYSECOND BYMINUTE BYHOUR BYMONTHDAY BYYEARDAY BYWEEKNO BYMONTH BYSETPOS)
|
||
(setq values-str (string-join (mapcar #'number-to-string values)
|
||
",")))
|
||
(BYDAY
|
||
(setq values-str (string-join (mapcar #'ical:print-weekdaynum values)
|
||
",")))
|
||
(WKST (setq values-str (car (rassq values ical:weekday-numbers)))))
|
||
|
||
(concat (symbol-name keyword) "=" values-str)))
|
||
|
||
(rx-define ical:recur-rule-part
|
||
;; Group 11: keyword
|
||
;; Group 12: value(s)
|
||
(or (seq (group-n 11 "FREQ") "=" (group-n 12 ical:freq))
|
||
(seq (group-n 11 "UNTIL") "=" (group-n 12 (or ical:date-time ical:date)))
|
||
(seq (group-n 11 "COUNT") "=" (group-n 12 (one-or-more digit)))
|
||
(seq (group-n 11 "INTERVAL") "=" (group-n 12 (one-or-more digit)))
|
||
(seq (group-n 11 "BYSECOND") "=" (group-n 12 ; 0 to 60
|
||
(ical:comma-list (** 1 2 digit))))
|
||
(seq (group-n 11 "BYMINUTE") "=" (group-n 12 ; 0 to 59
|
||
(ical:comma-list (** 1 2 digit))))
|
||
(seq (group-n 11 "BYHOUR") "=" (group-n 12 ; 0 to 23
|
||
(ical:comma-list (** 1 2 digit)))) ; 0 to 23
|
||
(seq (group-n 11 "BYDAY") "=" (group-n 12 ; weeknum? daynum, e.g. SU or 34SU
|
||
(ical:comma-list ical:weekdaynum)))
|
||
(seq (group-n 11 "BYMONTHDAY") "=" (group-n 12
|
||
(ical:comma-list ical:monthdaynum)))
|
||
(seq (group-n 11 "BYYEARDAY") "=" (group-n 12
|
||
(ical:comma-list ical:yeardaynum)))
|
||
(seq (group-n 11 "BYWEEKNO") "=" (group-n 12 (ical:comma-list ical:weeknum)))
|
||
(seq (group-n 11 "BYMONTH") "=" (group-n 12 (ical:comma-list ical:monthnum)))
|
||
(seq (group-n 11 "BYSETPOS") "=" (group-n 12
|
||
(ical:comma-list ical:yeardaynum)))
|
||
(seq (group-n 11 "WKST") "=" (group-n 12 ical:weekday))))
|
||
|
||
(defun ical:read-recur (s)
|
||
"Read a recurrence rule value from string S.
|
||
S should be a match against rx `icalendar-recur'."
|
||
;; TODO: let's switch to keywords and a plist, so we can more easily
|
||
;; write these clauses also in diary sexp entries without so many parens
|
||
(ical:read-list-with #'ical:read-recur-rule-part s (rx ical:recur-rule-part) ";"))
|
||
|
||
(defun ical:print-recur (val)
|
||
"Serialize a recurrence rule value VAL to a string."
|
||
;; RFC5545 sec. 3.3.10: "to ensure backward compatibility with
|
||
;; applications that pre-date this revision of iCalendar the
|
||
;; FREQ rule part MUST be the first rule part specified in a
|
||
;; RECUR value."
|
||
(string-join
|
||
(cons
|
||
(ical:print-recur-rule-part (assq 'FREQ val))
|
||
(mapcar #'ical:print-recur-rule-part
|
||
(seq-filter (lambda (part) (not (eq 'FREQ (car part))))
|
||
val)))
|
||
";"))
|
||
|
||
(defconst ical:-recur-value-types
|
||
;; `list-of' is not a cl-type specifier, just a symbol here; it is
|
||
;; handled specially when checking types in `ical:recur-value-p':
|
||
'(FREQ (member YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY)
|
||
UNTIL (or ical:date-time ical:date)
|
||
COUNT (integer 1 *)
|
||
INTERVAL (integer 1 *)
|
||
BYSECOND (list-of (integer 0 60))
|
||
BYMINUTE (list-of (integer 0 59))
|
||
BYHOUR (list-of (integer 0 23))
|
||
BYDAY (list-of (or (integer 0 6) (satisfies ical:dayno-offset-p)))
|
||
BYMONTHDAY (list-of (or (integer -31 -1) (integer 1 31)))
|
||
BYYEARDAY (list-of (or (integer -366 -1) (integer 1 366)))
|
||
BYWEEKNO (list-of (or (integer -53 -1) (integer 1 53)))
|
||
BYMONTH (list-of (integer 1 12)) ; unlike the others, months cannot be negative
|
||
BYSETPOS (list-of (or (integer -366 -1) (integer 1 366)))
|
||
WKST (integer 0 6))
|
||
"Plist mapping `icalendar-recur' keywords to type specifiers.")
|
||
|
||
(defun ical:dayno-offset-p (val)
|
||
"Return non-nil if VAL is a pair (DAYNO . OFFSET).
|
||
DAYNO must be in [0..6] and OFFSET in [-53..53], excluding 0."
|
||
(and (consp val)
|
||
(cl-typep (car val) '(integer 0 6))
|
||
(cl-typep (cdr val) '(or (integer -53 -1) (integer 1 53)))))
|
||
|
||
(defun ical:recur-value-p (vals)
|
||
"Return non-nil if VALS is an iCalendar recurrence rule value."
|
||
(and (listp vals)
|
||
;; FREQ is always required:
|
||
(assq 'FREQ vals)
|
||
;; COUNT and UNTIL are mutually exclusive if present:
|
||
(not (and (assq 'COUNT vals) (assq 'UNTIL vals)))
|
||
;; If BYSETPOS is present, another BYXXX clause must be too:
|
||
(or (not (assq 'BYSETPOS vals))
|
||
(assq 'BYMONTH vals)
|
||
(assq 'BYWEEKNO vals)
|
||
(assq 'BYYEARDAY vals)
|
||
(assq 'BYMONTHDAY vals)
|
||
(assq 'BYDAY vals)
|
||
(assq 'BYHOUR vals)
|
||
(assq 'BYMINUTE vals)
|
||
(assq 'BYSECOND vals))
|
||
(let ((freq (ical:recur-freq vals))
|
||
(byday (ical:recur-by* 'BYDAY vals))
|
||
(byweekno (ical:recur-by* 'BYWEEKNO vals))
|
||
(bymonthday (ical:recur-by* 'BYMONTHDAY vals))
|
||
(byyearday (ical:recur-by* 'BYYEARDAY vals)))
|
||
(and
|
||
;; "The BYDAY rule part MUST NOT be specified with a numeric
|
||
;; value when the FREQ rule part is not set to MONTHLY or
|
||
;; YEARLY."
|
||
(or (not (consp (car byday)))
|
||
(memq freq '(MONTHLY YEARLY)))
|
||
;; "The BYDAY rule part MUST NOT be specified with a numeric
|
||
;; value with the FREQ rule part set to YEARLY when the
|
||
;; BYWEEKNO rule part is specified." This also covers:
|
||
;; "[The BYWEEKNO] rule part MUST NOT be used when the FREQ
|
||
;; rule part is set to anything other than YEARLY."
|
||
(or (not byweekno)
|
||
(and (eq freq 'YEARLY)
|
||
(not (consp (car byday)))))
|
||
;; "The BYMONTHDAY rule part MUST NOT be specified when the
|
||
;; FREQ rule part is set to WEEKLY."
|
||
(not (and bymonthday (eq freq 'WEEKLY)))
|
||
;; "The BYYEARDAY rule part MUST NOT be specified when the
|
||
;; FREQ rule part is set to DAILY, WEEKLY, or MONTHLY."
|
||
(not (and byyearday (memq freq '(DAILY WEEKLY MONTHLY))))))
|
||
;; check types of all rule parts:
|
||
(seq-every-p
|
||
(lambda (kv)
|
||
(when (consp kv)
|
||
(let* ((keyword (car kv))
|
||
(val (cadr kv))
|
||
(type (plist-get ical:-recur-value-types keyword)))
|
||
(and keyword val type
|
||
(if (and (consp type)
|
||
(eq (car type) 'list-of))
|
||
(ical:list-of-p val (cadr type))
|
||
(cl-typep val type))))))
|
||
vals)))
|
||
|
||
(ical:define-type ical:recur "RECUR"
|
||
"Type for Recurrence Rule values.
|
||
|
||
When printed, a recurrence rule value looks like
|
||
KEY1=VAL1;KEY2=VAL2;...
|
||
where the VALs may themselves be lists or have other syntactic
|
||
structure; see RFC5545 sec. 3.3.10 for all the details.
|
||
|
||
The KEYs and their associated value types when read are as follows.
|
||
The first is required:
|
||
'(FREQ (member YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY)
|
||
These two are mutually exclusive; at most one may appear:
|
||
UNTIL (or icalendar-date-time icalendar-date)
|
||
COUNT (integer 1 *)
|
||
All others are optional:
|
||
INTERVAL (integer 1 *)
|
||
BYSECOND (list-of (integer 0 60))
|
||
BYMINUTE (list-of (integer 0 59))
|
||
BYHOUR (list-of (integer 0 23))
|
||
BYDAY (list-of (or (integer 0 6) ; day of week
|
||
(pair (integer 0 6) ; (day of week . offset)
|
||
(integer -53 53))) ; except 0
|
||
BYMONTHDAY (list-of (integer -31 31)) ; except 0
|
||
BYYEARDAY (list-of (integer -366 366)) ; except 0
|
||
BYWEEKNO (list-of (integer -53 53)) ; except 0
|
||
BYMONTH (list-of (integer 1 12)) ; months cannot be negative
|
||
BYSETPOS (list-of (integer -366 366)) ; except 0
|
||
WKST (integer 0 6))
|
||
|
||
When read, these KEYs and their associated VALs are gathered into
|
||
an alist.
|
||
|
||
In general, the VALs consist of integers or lists of integers.
|
||
Abbreviations for weekday names are translated into integers
|
||
0 (=Sunday) through 6 (=Saturday), for compatibility with
|
||
calendar.el and decoded-time-* functions.
|
||
|
||
Some examples:
|
||
|
||
1) Printed: FREQ=DAILY;COUNT=10;INTERVAL=2
|
||
Meaning: 10 occurrences that occur every other day
|
||
Read: ((FREQ DAILY)
|
||
(COUNT 10)
|
||
(INTERVAL 2))
|
||
|
||
2) Printed: FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA
|
||
Meaning: Every day in January of every year until 2000/01/31 at 14:00 UTC
|
||
Read: ((FREQ YEARLY)
|
||
(UNTIL (0 0 14 31 1 2000 1 -1 0))
|
||
(BYMONTH (1))
|
||
(BYDAY (0 1 2 3 4 5 6)))
|
||
|
||
3) Printed: FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2
|
||
Meaning: Every month on the second-to-last weekday of the month
|
||
Read: ((FREQ MONTHLY)
|
||
(BYDAY (1 2 3 4 5))
|
||
(BYSETPOS (-2)))
|
||
|
||
Notice that singleton values are still wrapped in a list when the
|
||
KEY accepts a list of values, but not when the KEY always has a
|
||
single (e.g. integer) value."
|
||
'(satisfies ical:recur-value-p)
|
||
(ical:semicolon-list ical:recur-rule-part)
|
||
:reader ical:read-recur
|
||
:printer ical:print-recur
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10")
|
||
|
||
(defun ical:recur-freq (recur-value)
|
||
"Return the frequency in RECUR-VALUE."
|
||
(car (alist-get 'FREQ recur-value)))
|
||
|
||
(defun ical:recur-interval-size (recur-value)
|
||
"Return the interval size in RECUR-VALUE, or the default of 1."
|
||
(or (car (alist-get 'INTERVAL recur-value)) 1))
|
||
|
||
(defun ical:recur-until (recur-value)
|
||
"Return the UNTIL date(-time) in RECUR-VALUE."
|
||
(car (alist-get 'UNTIL recur-value)))
|
||
|
||
(defun ical:recur-count (recur-value)
|
||
"Return the COUNT in RECUR-VALUE."
|
||
(car (alist-get 'COUNT recur-value)))
|
||
|
||
(defun ical:recur-weekstart (recur-value)
|
||
"Return the weekday which starts the work week in RECUR-VALUE.
|
||
If no starting weekday is specified in RECUR-VALUE, returns the default,
|
||
1 (= Monday)."
|
||
(or (car (alist-get 'WKST recur-value)) 1))
|
||
|
||
(defun ical:recur-by* (byunit recur-value)
|
||
"Return the values in the BYUNIT clause in RECUR-VALUE.
|
||
BYUNIT should be a symbol: \\='BYMONTH, \\='BYDAY, etc.
|
||
See `icalendar-recur' for all the possible BYUNIT values."
|
||
(car (alist-get byunit recur-value)))
|
||
|
||
;;;; 3.3.11 Text
|
||
(rx-define ical:escaped-char
|
||
(seq ?\\ (or ?\\ ?\; ?, ?N ?n)))
|
||
|
||
(rx-define ical:text-safe-char
|
||
;; "Any character except CONTROLs not needed by the current character
|
||
;; set, DQUOTE, ";", ":", "\", "," "
|
||
(any #x09 #x20 #x21 ; htab, space, and "!"
|
||
(#x23 . #x2B) (#x2D . #x39) ; "#".."9" skipping #x2C=","
|
||
(#x3C . #x5B) (#x5D . #x7E) ; "<".."~" skipping #x5C="\"
|
||
nonascii))
|
||
|
||
(defun ical:text-region-p (val)
|
||
"Return t if VAL represents a region of text."
|
||
(and (listp val)
|
||
(markerp (car val))
|
||
(not (null (marker-buffer (car val))))
|
||
(markerp (cdr val))))
|
||
|
||
(defun ical:make-text-region (&optional buffer begin end)
|
||
"Return an object that represents a region of text.
|
||
The region is taken from BUFFER between BEGIN and END. BUFFER defaults
|
||
to the current buffer, and BEGIN and END default to point and mark in
|
||
BUFFER."
|
||
(let ((buf (or buffer (current-buffer)))
|
||
(b (make-marker))
|
||
(e (make-marker)))
|
||
(with-current-buffer buf
|
||
(set-marker b (or begin (region-beginning)) buf)
|
||
(set-marker e (or end (region-end)))
|
||
(cons b e))))
|
||
|
||
(defsubst ical:text-region-begin (r)
|
||
"Return the marker at the beginning of the text region R."
|
||
(car r))
|
||
|
||
(defsubst ical:text-region-end (r)
|
||
"Return the marker at the end of the text region R."
|
||
(cdr r))
|
||
|
||
(defun ical:unescape-text-in-region (begin end)
|
||
"Unescape the text between BEGIN and END.
|
||
Unescaping replaces literal '\\n' and '\\N' with newline, and removes
|
||
backslashes that escape commas, semicolons, and backslashes."
|
||
(with-restriction begin end
|
||
(save-excursion
|
||
(replace-string-in-region "\\N" "\n" (point-min) (point-max))
|
||
(replace-string-in-region "\\n" "\n" (point-min) (point-max))
|
||
(replace-string-in-region "\\," "," (point-min) (point-max))
|
||
(replace-string-in-region "\\;" ";" (point-min) (point-max)))
|
||
(replace-string-in-region (concat "\\" "\\") "\\" (point-min) (point-max))))
|
||
|
||
(defun ical:unescape-text-string (s)
|
||
"Unescape the text in string S.
|
||
Unescaping replaces literal '\\n' and '\\N' with newline, and removes
|
||
backslashes that escape commas, semicolons, and backslashes."
|
||
(with-temp-buffer
|
||
(insert s)
|
||
(ical:unescape-text-in-region (point-min) (point-max))
|
||
(buffer-string)))
|
||
|
||
(defun ical:escape-text-in-region (begin end)
|
||
"Escape the text between BEGIN and END in the current buffer.
|
||
Escaping replaces newlines with literal '\\n', and escapes commas,
|
||
semicolons and backslashes with a backslash."
|
||
(with-restriction begin end
|
||
(save-excursion
|
||
;; replace backslashes first, so the ones introduced when
|
||
;; escaping other characters don't end up double-escaped:
|
||
(replace-string-in-region "\\" (concat "\\" "\\") (point-min) (point-max))
|
||
(replace-string-in-region "\n" "\\n" (point-min) (point-max))
|
||
(replace-string-in-region "," "\\," (point-min) (point-max))
|
||
(replace-string-in-region ";" "\\;" (point-min) (point-max)))))
|
||
|
||
(defun ical:escape-text-string (s)
|
||
"Escape the text in string S.
|
||
Escaping replaces newlines with literal '\\n', and escapes commas,
|
||
semicolons and backslashes with a backslash."
|
||
(with-temp-buffer
|
||
(insert s)
|
||
(ical:escape-text-in-region (point-min) (point-max))
|
||
(buffer-string)))
|
||
|
||
(defun ical:read-text (s)
|
||
"Read an `icalendar-text' value from a string S.
|
||
S should be a match against rx `icalendar-text'."
|
||
(ical:unescape-text-string s))
|
||
|
||
(defun ical:print-text (val)
|
||
"Serialize an iCalendar text value.
|
||
VAL may be a string or text region (see `icalendar-make-text-region').
|
||
The text will be escaped before printing. If VAL is a region, the text
|
||
it contains will not be modified; it is copied before escaping."
|
||
(if (stringp val)
|
||
(ical:escape-text-string val)
|
||
;; val is a region, so copy and escape its contents:
|
||
(let* ((beg (ical:text-region-begin val))
|
||
(buf (marker-buffer beg))
|
||
(end (ical:text-region-end val)))
|
||
(with-temp-buffer
|
||
(insert-buffer-substring buf (marker-position beg) (marker-position end))
|
||
(ical:escape-text-in-region (point-min) (point-max))
|
||
(buffer-string)))))
|
||
|
||
(defun ical:text-to-string (node)
|
||
"Return the value of an `icalendar-text' NODE as a string.
|
||
The returned string is *not* escaped. For that, see `icalendar-print-text'."
|
||
(ical:with-node-value node nil
|
||
(if (stringp value) value
|
||
;; Otherwise the value is a text region:
|
||
(let* ((beg (ical:text-region-begin value))
|
||
(buf (marker-buffer beg))
|
||
(end (ical:text-region-end value)))
|
||
(with-current-buffer buf
|
||
(buffer-substring (marker-position beg) (marker-position end)))))))
|
||
|
||
;; TODO: would it be useful to add a third representation, namely a
|
||
;; function or thunk? So that e.g. Org can pre-process its own syntax
|
||
;; and return a plain text string to use in the description?
|
||
(ical:define-type ical:text "TEXT"
|
||
"Type for Text values.
|
||
|
||
Text values can be represented in Elisp in two ways: as strings,
|
||
or as buffer regions. For values which aren't expected to change,
|
||
such as property values in a text/calendar email attachment, use
|
||
strings. For values which are user-editable and might change
|
||
between parsing and serializing to iCalendar format, use a
|
||
region. In that case, a text value contains two markers BEGIN and
|
||
END which mark the bounds of the region. See
|
||
`icalendar-make-text-region' to create such values, and
|
||
`icalendar-text-region-begin' and `icalendar-text-region-end' to
|
||
access the markers.
|
||
|
||
Certain characters in text values are required to be escaped by
|
||
the iCalendar standard. These characters should NOT be
|
||
pre-escaped when inserting them into the parse tree. Instead,
|
||
`icalendar-print-text' takes care of escaping text values, and
|
||
`icalendar-read-text' takes care of unescaping them, when parsing and
|
||
printing iCalendar data."
|
||
'(or string (satisfies ical:text-region-p))
|
||
(zero-or-more (or ical:text-safe-char ?: ?\" ical:escaped-char))
|
||
:reader ical:read-text
|
||
:printer ical:print-text
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.11")
|
||
|
||
;; 3.3.12 Time - Defined above
|
||
|
||
;;;; 3.3.13 URI
|
||
;; see https://www.rfc-editor.org/rfc/rfc3986#section-3
|
||
(rx-define ical:uri-with-scheme
|
||
;; Group 11: URI scheme; see icalendar-uri-schemes.el
|
||
;; Group 12: rest of URI after ":"
|
||
;; This regex mostly just scans for all characters allowed by RFC3986,
|
||
;; except we make an effort to parse the scheme, because otherwise the
|
||
;; regex is either too permissive (ical:binary, in particular, matches
|
||
;; a subset of the characters allowed in a URI) or too complicated to
|
||
;; be useful.
|
||
;; TODO: use url-parse.el to parse to struct?
|
||
(seq (group-n 11 (any "a-zA-Z") (zero-or-more (any ?- ?+ ?. "A-Za-z0-9")))
|
||
":"
|
||
(group-n 12
|
||
(one-or-more
|
||
(any "A-Za-z0-9" ?- ?. ?_ ?~ ; unreserved chars
|
||
?: ?/ ?? ?# ?\[ ?\] ?@ ; gen-delims
|
||
?! ?$ ?& ?' ?\( ?\) ?* ?+ ?, ?\; ?= ; sub-delims
|
||
?%))))) ; for %-encoding
|
||
|
||
(ical:define-type ical:uri "URI"
|
||
"Type for URI values.
|
||
|
||
The parsed and printed representations are the same: a URI string."
|
||
'(satisfies ical:match-uri-value)
|
||
ical:uri-with-scheme
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.13")
|
||
|
||
;;;; 3.3.3 Calendar User Address
|
||
(ical:define-type ical:cal-address "CAL-ADDRESS"
|
||
"Type for Calendar User Address values.
|
||
|
||
The parsed and printed representations are the same: a URI string.
|
||
Typically, this should be a \"mailto:\" URI.
|
||
|
||
RFC5545 says: \"*When used to address an Internet email transport
|
||
address* for a calendar user, the value MUST be a mailto URI,
|
||
as defined by [RFC2368]\"
|
||
|
||
Since it is unclear whether there are Calendar User Address values
|
||
which are not used to address email, this type does not enforce the use
|
||
of the mailto: scheme, but be prepared for problems if you create
|
||
values of this type with any other scheme."
|
||
'(and string (satisfies ical:match-cal-address-value))
|
||
ical:uri-with-scheme
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.3")
|
||
|
||
;;;; 3.3.14 UTC Offset
|
||
(defun ical:read-utc-offset (s)
|
||
"Read a UTC offset from a string.
|
||
S should be a match against rx `icalendar-utc-offset'"
|
||
(let ((sign (if (equal (substring s 0 1) "-") -1 1))
|
||
(nhours (string-to-number (substring s 1 3)))
|
||
(nminutes (string-to-number (substring s 3 5)))
|
||
(nseconds (if (length= s 7)
|
||
(string-to-number (substring s 5 7))
|
||
0)))
|
||
(* sign (+ nseconds (* 60 nminutes) (* 60 60 nhours)))))
|
||
|
||
(defun ical:print-utc-offset (utcoff)
|
||
"Serialize a UTC offset to a string."
|
||
(let* ((sign (if (< utcoff 0) "-" "+"))
|
||
(absoff (abs utcoff))
|
||
(nhours (/ absoff (* 60 60)))
|
||
(no-hours (mod absoff (* 60 60)))
|
||
(nminutes (/ no-hours 60))
|
||
(nseconds (mod no-hours 60)))
|
||
(if (zerop nseconds)
|
||
(format "%s%02d%02d" sign nhours nminutes)
|
||
(format "%s%02d%02d%02d" sign nhours nminutes nseconds))))
|
||
|
||
(ical:define-type ical:utc-offset "UTC-OFFSET"
|
||
"Type for UTC Offset values.
|
||
|
||
When printed, a sign followed by a string of digits, like +HHMM
|
||
or -HHMMSS. When read, an integer representing the number of
|
||
seconds offset from UTC. This representation is for compatibility
|
||
with `decode-time' and related functions."
|
||
'(integer -999999 999999)
|
||
(seq (or ?+ ?-) ; + is not optional for positive values!
|
||
(= 4 digit) ; HHMM
|
||
(zero-or-one (= 2 digit))) ; SS
|
||
:reader ical:read-utc-offset
|
||
:printer ical:print-utc-offset
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.3.14")
|
||
|
||
|
||
;;; Section 3.2: Property Parameters
|
||
|
||
(defvar ical:param-types nil ;; populated by ical:define-param
|
||
"Alist mapping printed parameter names to type symbols.")
|
||
|
||
(defun ical:maybe-quote-param-value (s &optional always)
|
||
"Add quotes around param value string S if required.
|
||
If ALWAYS is non-nil, add quotes to S regardless of its contents."
|
||
(if (or always
|
||
(not (string-match (rx ical:paramtext) s))
|
||
(< (match-end 0) (length s)))
|
||
(concat "\"" s "\"")
|
||
s))
|
||
|
||
(defun ical:read-param-value (type s)
|
||
"Read a value for a parameter of type TYPE from a string S.
|
||
S should have already been matched against the regex for TYPE and
|
||
the match data should be available to this function. Returns a
|
||
syntax node of type TYPE containing the read value.
|
||
|
||
If TYPE accepts a list of values, S will be split on the list
|
||
separator for TYPE and read individually."
|
||
(let* ((value-type (get type 'ical:value-type)) ; if nil, value is just a string
|
||
(value-regex (when (get type 'ical:value-rx)
|
||
(rx-to-string (get type 'ical:value-rx))))
|
||
(list-sep (get type 'ical:list-sep))
|
||
(substitute-val (get type 'ical:substitute-value))
|
||
(unrecognized-val (match-string 5)) ; see :unrecognized in define-param
|
||
(raw-val (if unrecognized-val substitute-val s))
|
||
(one-val-reader (if (ical:value-type-symbol-p value-type)
|
||
(lambda (s) (ical:read-value-node value-type s))
|
||
#'identity)) ; value is just a string
|
||
;; values may be quoted even if :quoted does not require it,
|
||
;; so they need to be stripped of quotes. read-list-with does
|
||
;; this by default; in the single value case, use string-trim
|
||
(read-val (if list-sep
|
||
(ical:read-list-with one-val-reader raw-val
|
||
value-regex list-sep)
|
||
(funcall one-val-reader
|
||
(string-trim raw-val "\"" "\"")))))
|
||
(ical:make-ast-node type
|
||
(list :value read-val
|
||
:original-value unrecognized-val))))
|
||
|
||
(defun ical:parse-param-value (type limit)
|
||
"Parse the value for a parameter of type TYPE from point up to LIMIT.
|
||
TYPE should be a type symbol for an iCalendar parameter type.
|
||
This function expects point to be at the start of the value
|
||
string, after the parameter name and the equals sign. Returns a
|
||
syntax node representing the parameter."
|
||
(let ((full-value-regex (rx-to-string (get type 'ical:full-value-rx))))
|
||
;; By far the most common invalid data seem to be text values that
|
||
;; contain unescaped characters (e.g. commas in addresses). These
|
||
;; are harmless as long as the parameter accepts arbitrary text and
|
||
;; does not expect a list of values. The only such parameter
|
||
;; defined in RFC5545 is `ical:cnparam', so we treat this as a
|
||
;; special case and loosen the official regexp to accept anything up
|
||
;; to the start of the next param or property value:
|
||
(when (and (eq type 'ical:cnparam)
|
||
(not ical:parse-strictly))
|
||
(setq full-value-regex
|
||
(rx (group-n 2 (or ical:quoted-string
|
||
(zero-or-more (not (any ?: ?\;))))))))
|
||
|
||
(unless (re-search-forward full-value-regex limit t)
|
||
(ical:signal-parse-error
|
||
(format "Unable to parse `%s' value between %d and %d"
|
||
type (point) limit)))
|
||
(when (match-string 3)
|
||
(ical:signal-parse-error
|
||
(format "Invalid value for `%s' parameter: %s" type (match-string 3))))
|
||
|
||
(let ((value-begin (match-beginning 2))
|
||
(value-end (match-end 2))
|
||
(node (ical:read-param-value type (match-string 2))))
|
||
(ical:ast-node-meta-set node :buffer (current-buffer))
|
||
;; :begin must be set by parse-params
|
||
(ical:ast-node-meta-set node :value-begin value-begin)
|
||
(ical:ast-node-meta-set node :value-end value-end)
|
||
(ical:ast-node-meta-set node :end value-end)
|
||
|
||
node)))
|
||
|
||
(defun ical:parse-params (limit)
|
||
"Parse the parameter string of the current property, up to LIMIT.
|
||
Point should be at the \";\" at the start of the first parameter.
|
||
Returns a list of parameters, which may be nil if none are present.
|
||
After parsing, point is at the end of the parameter string and the
|
||
start of the property value string."
|
||
(let (params param-node)
|
||
(rx-let ((ical:param-start (seq ";" (group-n 1 ical:param-name) "=")))
|
||
(while (re-search-forward (rx ical:param-start) limit t)
|
||
(when-let* ((begin (match-beginning 1))
|
||
(param-name (match-string 1))
|
||
(param-type (alist-get (upcase param-name)
|
||
ical:param-types
|
||
'ical:otherparam
|
||
nil #'equal)))
|
||
(condition-case err
|
||
(setq param-node (ical:parse-param-value param-type limit))
|
||
(ical:parse-error
|
||
(ical:handle-parse-error err (format "Skipping bad %s parameter"
|
||
param-name))
|
||
(setq param-node nil)))
|
||
(when param-node
|
||
(ical:ast-node-meta-set param-node :begin begin)
|
||
;; store the original param name if we didn't recognize it:
|
||
(when (eq param-type 'ical:otherparam)
|
||
(ical:ast-node-meta-set param-node :original-name param-name))
|
||
(push param-node params))))
|
||
(nreverse params))))
|
||
|
||
(defun ical:print-param-node (node)
|
||
"Serialize a parameter syntax node NODE to a string.
|
||
NODE should be a syntax node whose type is an iCalendar
|
||
parameter type."
|
||
(let* ((param-type (ical:ast-node-type node))
|
||
(param-name (car (rassq param-type ical:param-types)))
|
||
(name-str (or param-name
|
||
;; set by parse-params for unrecognized params:
|
||
(ical:ast-node-meta-get :original-name node))))
|
||
|
||
(unless (and name-str (stringp name-str) (not (equal name-str "")))
|
||
(ical:signal-print-error "No printable parameter name" :node node))
|
||
|
||
(let* ((list-sep (get param-type 'ical:list-sep))
|
||
(val/s (ical:ast-node-value node))
|
||
(vals (if (and list-sep (listp val/s))
|
||
val/s
|
||
(list val/s)))
|
||
;; any ical:print-error here propagates:
|
||
(printed (mapcar #'ical:default-value-printer vals))
|
||
;; add quotes to each value as needed, even if :quoted
|
||
;; does not require it:
|
||
(must-quote (get param-type 'ical:is-quoted))
|
||
(quoted (mapcar
|
||
(lambda (v) (ical:maybe-quote-param-value v must-quote))
|
||
printed))
|
||
(val-str (or (ical:ast-node-meta-get :original-value node)
|
||
(string-join quoted list-sep)
|
||
quoted)))
|
||
|
||
(unless (and (stringp val-str) (not (equal val-str "")))
|
||
(ical:signal-print-error "Unable to print parameter value" :node node))
|
||
|
||
(format ";%s=%s" name-str val-str))))
|
||
|
||
(defun ical:print-params (param-nodes)
|
||
"Print the property parameter nodes in PARAM-NODES.
|
||
Returns the printed parameter list as a string."
|
||
(let (param-strs)
|
||
(dolist (node param-nodes)
|
||
(condition-case err
|
||
(push (ical:print-param-node node) param-strs)
|
||
(ical:print-error
|
||
(ical:handle-print-error err))))
|
||
(apply #'concat (nreverse param-strs))))
|
||
|
||
;; Parameter definitions in RFC5545:
|
||
|
||
(ical:define-param ical:altrepparam "ALTREP"
|
||
"Alternate text representation (URI)"
|
||
ical:uri
|
||
:quoted t
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.1")
|
||
|
||
(ical:define-param ical:cnparam "CN"
|
||
"Common Name"
|
||
ical:param-value
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.2")
|
||
|
||
(ical:define-param ical:cutypeparam "CUTYPE"
|
||
"Calendar User Type"
|
||
(or "INDIVIDUAL"
|
||
"GROUP"
|
||
"RESOURCE"
|
||
"ROOM"
|
||
"UNKNOWN"
|
||
(group-n 5
|
||
(or ical:x-name ical:iana-token)))
|
||
:default "INDIVIDUAL"
|
||
;; "Applications MUST treat x-name and iana-token values they
|
||
;; don't recognize the same way as they would the UNKNOWN
|
||
;; value":
|
||
:unrecognized "UNKNOWN"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.3")
|
||
|
||
(ical:define-param ical:delfromparam "DELEGATED-FROM"
|
||
"Delegators.
|
||
|
||
This is a comma-separated list of quoted `icalendar-cal-address' URIs,
|
||
typically specified on the `icalendar-attendee' property. The users in
|
||
this list have delegated their participation to the user which is
|
||
the value of the property."
|
||
ical:cal-address
|
||
:quoted t
|
||
:list-sep ","
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.4")
|
||
|
||
(ical:define-param ical:deltoparam "DELEGATED-TO"
|
||
"Delegatees.
|
||
|
||
This is a comma-separated list of quoted `icalendar-cal-address' URIs,
|
||
typically specified on the `icalendar-attendee' property. The users in
|
||
this list have been delegated to participate by the user which is
|
||
the value of the property."
|
||
ical:cal-address
|
||
:quoted t
|
||
:list-sep ","
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.5")
|
||
|
||
(ical:define-param ical:dirparam "DIR"
|
||
"Directory Entry Reference.
|
||
|
||
This parameter may be specified on properties with a
|
||
`icalendar-cal-address' value type. It is a quoted URI which specifies
|
||
a reference to a directory entry associated with the calendar
|
||
user which is the value of the property."
|
||
ical:uri
|
||
:quoted t
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.6")
|
||
|
||
(ical:define-param ical:encodingparam "ENCODING"
|
||
"Inline Encoding, either \"8BIT\" (text, default) or \"BASE64\" (binary).
|
||
|
||
If \"BASE64\", the property value is base64-encoded binary data.
|
||
This parameter must be specified if the `icalendar-valuetypeparam'
|
||
is \"BINARY\"."
|
||
(or "8BIT" "BASE64")
|
||
:default "8BIT"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.7")
|
||
|
||
(rx-define ical:mimetype
|
||
(seq ical:mimetype-regname "/" ical:mimetype-regname))
|
||
|
||
;; from https://www.rfc-editor.org/rfc/rfc4288#section-4.2:
|
||
(rx-define ical:mimetype-regname
|
||
(** 1 127 (any "A-Za-z0-9" ?! ?# ?$ ?& ?. ?+ ?- ?^ ?_)))
|
||
|
||
(ical:define-param ical:fmttypeparam "FMTTYPE"
|
||
"Format Type (Mimetype per RFC4288)
|
||
|
||
Specifies the media type of the object referenced in the property value,
|
||
for example \"text/plain\" or \"text/html\".
|
||
Valid media types are defined in RFC4288; see
|
||
URL `https://www.rfc-editor.org/rfc/rfc4288#section-4.2'"
|
||
ical:mimetype
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.8")
|
||
|
||
(ical:define-param ical:fbtypeparam "FBTYPE"
|
||
"Free/Busy Time Type. Default is \"BUSY\".
|
||
|
||
RFC5545 gives the following meanings to the values:
|
||
|
||
FREE: the time interval is free for scheduling.
|
||
BUSY: the time interval is busy because one or more events have
|
||
been scheduled for that interval.
|
||
BUSY-UNAVAILABLE: the time interval is busy and the interval
|
||
can not be scheduled.
|
||
BUSY-TENTATIVE: the time interval is busy because one or more
|
||
events have been tentatively scheduled for that interval.
|
||
Other values are treated like BUSY."
|
||
(or "FREE"
|
||
"BUSY-UNAVAILABLE"
|
||
"BUSY-TENTATIVE"
|
||
"BUSY"
|
||
ical:x-name
|
||
ical:iana-token)
|
||
:default "BUSY"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.9")
|
||
|
||
;; TODO: see https://www.rfc-editor.org/rfc/rfc5646#section-2.1
|
||
(rx-define ical:rfc5646-lang
|
||
(one-or-more (any "A-Za-z0-9" ?-)))
|
||
|
||
(ical:define-param ical:languageparam "LANGUAGE"
|
||
"Language tag (per RFC5646)
|
||
|
||
This parameter specifies the language of the property value as a
|
||
language tag, for example \"en-US\" for US English or \"no\" for
|
||
Norwegian. Valid language tags are defined in RFC5646; see
|
||
URL `https://www.rfc-editor.org/rfc/rfc5646'"
|
||
ical:rfc5646-lang
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.10")
|
||
|
||
(ical:define-param ical:memberparam "MEMBER"
|
||
"Group or List Membership.
|
||
|
||
This is a comma-separated list of quoted `icalendar-cal-address'
|
||
values. These are addresses of groups or lists of which the user
|
||
in the property value is a member."
|
||
ical:cal-address
|
||
:quoted t
|
||
:list-sep ","
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.11")
|
||
|
||
(ical:define-param ical:partstatparam "PARTSTAT"
|
||
"Participation status.
|
||
|
||
The value specifies the participation status of the calendar user
|
||
in the property value. They have different interpretations
|
||
depending on whether they occur in a VEVENT, VTODO or VJOURNAL
|
||
component. RFC5545 gives the values the following meanings:
|
||
|
||
NEEDS-ACTION (all): needs action by the user
|
||
ACCEPTED (all): accepted by the user
|
||
DECLINED (all): declined by the user
|
||
TENTATIVE (VEVENT, VTODO): tentatively accepted by the user
|
||
DELEGATED (VEVENT, VTODO): delegated by the user
|
||
COMPLETED (VTODO): completed at the `icalendar-date-time' in the
|
||
VTODO's `icalendar-completed' property
|
||
IN-PROCESS (VTODO): in the process of being completed"
|
||
(or "NEEDS-ACTION"
|
||
"ACCEPTED"
|
||
"DECLINED"
|
||
"TENTATIVE"
|
||
"DELEGATED"
|
||
"COMPLETED"
|
||
"IN-PROCESS"
|
||
(group-n 5 (or ical:x-name
|
||
ical:iana-token)))
|
||
;; "Applications MUST treat x-name and iana-token values
|
||
;; they don't recognize the same way as they would the
|
||
;; NEEDS-ACTION value."
|
||
:default "NEEDS-ACTION"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.12")
|
||
|
||
(ical:define-param ical:rangeparam "RANGE"
|
||
"Recurrence Identifier Range.
|
||
|
||
Specifies the effective range of recurrence instances of the property's value.
|
||
The value \"THISANDFUTURE\" is the only value compliant with RFC5545;
|
||
legacy applications might also produce \"THISANDPRIOR\"."
|
||
"THISANDFUTURE"
|
||
:default "THISANDFUTURE"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.13")
|
||
|
||
(ical:define-param ical:trigrelparam "RELATED"
|
||
"Alarm Trigger Relationship.
|
||
|
||
This parameter may be specified on properties whose values give
|
||
an alarm trigger as an `icalendar-duration'. If the parameter
|
||
value is \"START\" (the default), the alarm triggers relative to
|
||
the start of the component; similarly for \"END\"."
|
||
(or "START" "END")
|
||
:default "START"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.14")
|
||
|
||
(ical:define-param ical:reltypeparam "RELTYPE"
|
||
"Relationship type.
|
||
|
||
This parameter specifies a hierarchical relationship between the
|
||
calendar component referenced in a `icalendar-related-to'
|
||
property and the calendar component in which it occurs.
|
||
\"PARENT\" means the referenced component is superior to this
|
||
one, \"CHILD\" that the referenced component is subordinate to
|
||
this one, and \"SIBLING\" means they are peers."
|
||
(or "PARENT"
|
||
"CHILD"
|
||
"SIBLING"
|
||
(group-n 5 (or ical:x-name
|
||
ical:iana-token)))
|
||
;; "Applications MUST treat x-name and iana-token values they don't
|
||
;; recognize the same way as they would the PARENT value."
|
||
:default "PARENT"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.15")
|
||
|
||
(ical:define-param ical:roleparam "ROLE"
|
||
"Participation role.
|
||
|
||
This parameter specifies the participation role of the calendar
|
||
user in the property value. RFC5545 gives the parameter values
|
||
the following meanings:
|
||
CHAIR: chair of the calendar entity
|
||
REQ-PARTICIPANT (default): user's participation is required
|
||
OPT-PARTICIPANT: user's participation is optional
|
||
NON-PARTICIPANT: user is copied for information purposes only"
|
||
(or "CHAIR"
|
||
"REQ-PARTICIPANT"
|
||
"OPT-PARTICIPANT"
|
||
"NON-PARTICIPANT"
|
||
(group-n 5 (or ical:x-name
|
||
ical:iana-token)))
|
||
;; "Applications MUST treat x-name and iana-token values
|
||
;; they don't recognize the same way as they would the
|
||
;; REQ-PARTICIPANT value."
|
||
:default "REQ-PARTICIPANT"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.16")
|
||
|
||
(ical:define-param ical:rsvpparam "RSVP"
|
||
"RSVP expectation.
|
||
|
||
This parameter is an `icalendar-boolean' which specifies whether
|
||
the calendar user in the property value is expected to reply to
|
||
the Organizer of a VEVENT or VTODO."
|
||
ical:boolean
|
||
:default "FALSE"
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.17")
|
||
|
||
(ical:define-param ical:sentbyparam "SENT-BY"
|
||
"Sent by.
|
||
|
||
This parameter specifies a calendar user that is acting on behalf
|
||
of the user in the property value."
|
||
;; "The parameter value MUST be a mailto URI as defined in [RFC2368]"
|
||
;; Weirdly, this is the only place in the standard I've seen "mailto:"
|
||
;; be *required* for a cal-address. We ignore this requirement because
|
||
;; coding around the exception is not worth it: it requires working
|
||
;; around the fact that two different types, the looser and the more
|
||
;; stringent cal-address, would need to have the same print name.
|
||
ical:cal-address
|
||
:quoted t
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.18")
|
||
|
||
(ical:define-param ical:tzidparam "TZID"
|
||
"Time Zone identifier.
|
||
|
||
This parameter identifies the VTIMEZONE component in the calendar
|
||
which should be used to interpret the time value given in the
|
||
property. The value of this parameter must be equal to the value
|
||
of the TZID property in that VTIMEZONE component; there must be
|
||
exactly one such component for every unique value of this
|
||
parameter in the calendar."
|
||
(seq (zero-or-one "/") ical:paramtext)
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.19")
|
||
|
||
(defun ical:read-value-type (s)
|
||
"Read a value type from string S.
|
||
S should contain the printed representation of a value type in a \"VALUE=...\"
|
||
property parameter. If S represents a known type in `icalendar-value-types',
|
||
it is read as the associated type symbol. Otherwise S is returned unchanged."
|
||
(let ((type-assoc (assoc s ical:value-types)))
|
||
(if type-assoc
|
||
(cdr type-assoc)
|
||
s)))
|
||
|
||
(defun ical:print-value-type (type)
|
||
"Print a value type TYPE.
|
||
TYPE should be an iCalendar type symbol naming a known value type
|
||
defined with `icalendar-define-type', or a string naming an
|
||
unknown type. If it is a symbol, return the associated printed
|
||
representation for the type from `icalendar-value-types'.
|
||
Otherwise return TYPE."
|
||
(if (symbolp type)
|
||
(car (rassq type ical:value-types))
|
||
type))
|
||
|
||
(ical:define-type ical:printed-value-type nil
|
||
"Type to represent values of the `icalendar-valuetypeparam' parameter.
|
||
|
||
When read, if the type named by the parameter is a known value
|
||
type in `icalendar-value-types', it is represented as a type
|
||
symbol for that value type. If it is an unknown value type, it is
|
||
represented as a string. When printed, a string is returned
|
||
unchanged; a type symbol is printed as the associated name in
|
||
`icalendar-value-types'.
|
||
|
||
This is not a type defined by RFC5545; it is defined here to
|
||
facilitate parsing of the `icalendar-valuetypeparam' parameter."
|
||
'(or string (satisfies ical:printable-value-type-symbol-p))
|
||
(or "BINARY"
|
||
"BOOLEAN"
|
||
"CAL-ADDRESS"
|
||
"DATE-TIME"
|
||
"DATE"
|
||
"DURATION"
|
||
"FLOAT"
|
||
"INTEGER"
|
||
"PERIOD"
|
||
"RECUR"
|
||
"TEXT"
|
||
"TIME"
|
||
"URI"
|
||
"UTC-OFFSET"
|
||
;; Note: "Applications MUST preserve the value data for x-name
|
||
;; and iana-token values that they don't recognize without
|
||
;; attempting to interpret or parse the value data." So in this
|
||
;; case we don't specify :default or :unrecognized in the
|
||
;; parameter definition, and we don't put the value in group 5;
|
||
;; the reader will just preserve whatever string matches here.
|
||
ical:x-name
|
||
ical:iana-token)
|
||
:reader ical:read-value-type
|
||
:printer ical:print-value-type)
|
||
|
||
(ical:define-param ical:valuetypeparam "VALUE"
|
||
"Property value data type.
|
||
|
||
This parameter is used to specify the value type of the
|
||
containing property's value, if it is not of the default value
|
||
type."
|
||
ical:printed-value-type
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.2.20")
|
||
|
||
(ical:define-param ical:otherparam nil ; don't add to ical:param-types
|
||
"Parameter with an unknown name.
|
||
|
||
This is not a parameter type defined by RFC5545; it represents
|
||
parameters with an unknown name (matching rx `icalendar-param-name')
|
||
whose values must be parsed and preserved but not further
|
||
interpreted."
|
||
ical:param-value)
|
||
|
||
(rx-define ical:other-param-safe
|
||
;; we use this rx to skip params when matching properties and
|
||
;; their values. Thus we *don't* capture the param names and param values
|
||
;; in numbered groups here, which would clobber the groups of the enclosing
|
||
;; expression.
|
||
(seq ";"
|
||
(or ical:iana-token ical:x-name)
|
||
"="
|
||
(ical:comma-list ical:param-value)))
|
||
|
||
|
||
;;; Properties:
|
||
|
||
(defvar ical:property-types nil ;; populated by ical:define-property
|
||
"Alist mapping printed property names to type symbols.")
|
||
|
||
(defun ical:read-property-value (type s &optional params)
|
||
"Read a value for the property type TYPE from a string S.
|
||
|
||
TYPE should be a type symbol for an iCalendar property type
|
||
defined with `icalendar-define-property'. The property value is
|
||
assumed to be of TYPE's default value type, unless an
|
||
`icalendar-valuetypeparam' parameter appears in PARAMS, in which
|
||
case a value of that type will be read. S should have already
|
||
been matched against TYPE's value regex and the match data should
|
||
be available to this function. Returns a property syntax node of
|
||
type TYPE containing the read value and the list of PARAMS.
|
||
|
||
If TYPE accepts lists of values, they will be split from S on the
|
||
list separator and read separately."
|
||
(let* ((value-type (or (ical:value-type-from-params params)
|
||
(get type 'ical:default-type)))
|
||
(list-sep (get type 'ical:list-sep))
|
||
(unrecognized-val (match-string 5))
|
||
(raw-val (if unrecognized-val
|
||
(get type 'ical:substitute-value)
|
||
s))
|
||
(value (if list-sep
|
||
(ical:read-list-of value-type raw-val list-sep)
|
||
(ical:read-value-node value-type raw-val))))
|
||
(ical:make-ast-node type
|
||
(list :value value
|
||
:original-value unrecognized-val)
|
||
params)))
|
||
|
||
(defun ical:parse-property-value (type limit &optional params)
|
||
"Parse a value for the property type TYPE from point up to LIMIT.
|
||
This function expects point to be at the start of the value
|
||
expression, after \"PROPERTY-NAME[PARAM...]:\". Returns a syntax
|
||
node of type TYPE containing the parsed value and the list of
|
||
PARAMS."
|
||
(let ((start (point))
|
||
(full-value-regex (rx-to-string (get type 'ical:full-value-rx))))
|
||
|
||
;; By far the most common invalid data seem to be text values that
|
||
;; contain unescaped characters (e.g. commas in addresses in
|
||
;; LOCATION). These are harmless as long as the property accepts
|
||
;; any text value, accepts no other types of values, and does not
|
||
;; expect a list of values. So we treat this as a special case and
|
||
;; loosen the regexp to accept any non-control character until eol:
|
||
(when (and (eq 'ical:text (get type 'ical:default-type))
|
||
(equal (rx-to-string 'ical:text t)
|
||
(rx-to-string (get type 'ical:value-rx) t))
|
||
(null (get type 'ical:other-types))
|
||
(not (ical:expects-list-of-values-p type))
|
||
(not ical:parse-strictly))
|
||
(setq full-value-regex
|
||
(rx (group-n 2 (zero-or-more (not (any control))))
|
||
line-end)))
|
||
|
||
(unless (re-search-forward full-value-regex limit t)
|
||
(ical:signal-parse-error
|
||
(format "Unable to parse `%s' property value between %d and %d"
|
||
type start limit)
|
||
:restart-at (1+ limit)))
|
||
|
||
(when (match-string 3)
|
||
(ical:signal-parse-error
|
||
(format "Invalid value for `%s' property: %s" type (match-string 3))
|
||
:restart-at (1+ limit)))
|
||
|
||
(let* ((value-begin (match-beginning 2))
|
||
(value-end (match-end 2))
|
||
(end value-end)
|
||
(node (ical:read-property-value type (match-string 2) params)))
|
||
(ical:ast-node-meta-set node :buffer (current-buffer))
|
||
;; 'begin must be set by parse-property
|
||
(ical:ast-node-meta-set node :value-begin value-begin)
|
||
(ical:ast-node-meta-set node :value-end value-end)
|
||
(ical:ast-node-meta-set node :end end)
|
||
|
||
node)))
|
||
|
||
(defun ical:print-property-node (node)
|
||
"Serialize a property syntax node NODE to a string."
|
||
(setq node (ical:maybe-add-value-param node))
|
||
(let* ((type (ical:ast-node-type node))
|
||
(list-sep (get type 'ical:list-sep))
|
||
(property-name (car (rassq type ical:property-types)))
|
||
(name-str (or property-name
|
||
(ical:ast-node-meta-get :original-name node)))
|
||
(params (ical:ast-node-children node))
|
||
(value (ical:ast-node-value node))
|
||
(value-str
|
||
(or (ical:ast-node-meta-get :original-value node)
|
||
;; any ical:print-error here propagates:
|
||
(if list-sep
|
||
(string-join (mapcar #'ical:default-value-printer value)
|
||
list-sep)
|
||
(ical:default-value-printer value)))))
|
||
|
||
(unless (and (stringp name-str) (length> name-str 0))
|
||
(ical:signal-print-error
|
||
(format "Unknown property name for type `%s'" type)
|
||
:node node))
|
||
|
||
(concat name-str
|
||
(ical:print-params params)
|
||
":"
|
||
value-str
|
||
"\n")))
|
||
|
||
(defun ical:maybe-add-value-param (property-node)
|
||
"Add a VALUE parameter to PROPERTY-NODE if necessary.
|
||
|
||
If the type of PROPERTY-NODE's value is not the same as its
|
||
default-type, check that its parameter list contains an
|
||
`icalendar-valuetypeparam' specifying that type as the type for
|
||
the value. If not, add such a parameter to PROPERTY-NODE's list
|
||
of parameters. Returns the possibly-modified PROPERTY-NODE.
|
||
|
||
If the parameter list already contains a value type parameter for
|
||
a type other than the property value's type, an
|
||
`icalendar-validation-error' is signaled.
|
||
|
||
If PROPERTY's value is a list, the type of the first element will
|
||
be assumed to be the type for all the values in the list. If the
|
||
list is empty, no change will be made to PROPERTY's parameters."
|
||
(catch 'no-value-type
|
||
(let* ((property-type (ical:ast-node-type property-node))
|
||
(value/s (ical:ast-node-value property-node))
|
||
(value (if (and (ical:expects-list-of-values-p property-type)
|
||
(listp value/s))
|
||
(car value/s)
|
||
value/s))
|
||
(value-type (cond ((stringp value) 'ical:text)
|
||
((ical:ast-node-p value)
|
||
(ical:ast-node-type value))
|
||
;; if we can't determine a type from the value, bail:
|
||
(t (throw 'no-value-type property-node))))
|
||
(params (ical:ast-node-children property-node))
|
||
(expected-type (ical:value-type-from-params params)))
|
||
|
||
(when (not (eq value-type (get property-type 'ical:default-type)))
|
||
(if expected-type
|
||
(when (not (eq value-type expected-type))
|
||
(ical:signal-validation-error
|
||
(format (concat "Mismatching VALUE parameter. VALUE specifies %s "
|
||
"but property value has type %s")
|
||
expected-type value-type)
|
||
:node property-node))
|
||
;; the value isn't of the default type, but we didn't find a
|
||
;; VALUE parameter, so add one now:
|
||
(let* ((valuetype-param
|
||
(ical:make-ast-node 'ical:valuetypeparam
|
||
(list :value (ical:make-ast-node
|
||
'ical:printed-value-type
|
||
(list :value value-type)))))
|
||
(new-params (cons valuetype-param
|
||
(ical:ast-node-children property-node))))
|
||
(apply #'ical:ast-node-set-children property-node new-params))))
|
||
|
||
;; Return the modified property node:
|
||
property-node)))
|
||
|
||
(defun ical:value-type-from-params (params)
|
||
"Return the type symbol associated with any VALUE parameter in PARAMS.
|
||
PARAMS should be a list of parameter nodes. The type symbol specified by
|
||
the first `icalendar-valuetypeparam' in PARAMS, or nil, will be returned."
|
||
(catch 'found
|
||
(dolist (param params)
|
||
(when (ical:value-param-p param)
|
||
(let ((type (ical:ast-node-value
|
||
(ical:ast-node-value param))))
|
||
(throw 'found type))))))
|
||
|
||
(defun ical:parse-property (limit)
|
||
"Parse the current property, up to LIMIT.
|
||
|
||
Point should be at the beginning of a property line; LIMIT should be the
|
||
position at the end of the line.
|
||
|
||
Returns a syntax node for the property. After parsing, point is at the
|
||
beginning of the next content line."
|
||
(rx-let ((ical:property-start (seq line-start (group-n 1 ical:name))))
|
||
(let (line-begin line-end property-name property-type params node)
|
||
;; Property name
|
||
(unless (re-search-forward (rx ical:property-start) limit t)
|
||
(ical:signal-parse-error
|
||
"Malformed property: could not match property name"
|
||
:restart-at (1+ limit)))
|
||
|
||
(setq property-name (match-string 1))
|
||
(setq line-begin (line-beginning-position))
|
||
(setq line-end (line-end-position))
|
||
|
||
;; Parameters
|
||
(when (looking-at-p ";")
|
||
(setq params (ical:parse-params line-end)))
|
||
|
||
(unless (looking-at-p ":")
|
||
(ical:signal-parse-error
|
||
"Malformed property: parameters did not end at colon"
|
||
:restart-at (1+ limit)))
|
||
(forward-char)
|
||
|
||
;; Value
|
||
(setq property-type (alist-get (upcase property-name)
|
||
ical:property-types
|
||
'ical:other-property
|
||
nil #'equal))
|
||
(setq node (ical:parse-property-value property-type limit params))
|
||
|
||
;; sanity check, since e.g. invalid base64 data might not
|
||
;; match all the way to the end of the line, as test
|
||
;; rfc5545-sec3.1.3/2 initially revealed
|
||
(unless (eql (point) (line-end-position))
|
||
(ical:signal-parse-error
|
||
(format "%s property value did not consume line: %s"
|
||
property-name
|
||
(ical:default-value-printer (ical:ast-node-value node)))
|
||
:restart-at (1+ limit)))
|
||
|
||
;; value, children are set in ical:read-property-value,
|
||
;; value-begin, value-end, end in ical:parse-property-value.
|
||
;; begin and original-name are only available here:
|
||
(ical:ast-node-meta-set node :begin line-begin)
|
||
(when (eq property-type 'ical:other-property)
|
||
(ical:ast-node-meta-set node :original-name property-name))
|
||
|
||
;; Set point up for the next property parser.
|
||
(while (not (bolp))
|
||
(forward-char))
|
||
|
||
;; Return the syntax node
|
||
node)))
|
||
|
||
|
||
;;;; Section 3.7: Calendar Properties
|
||
(ical:define-property ical:calscale "CALSCALE"
|
||
"Calendar scale.
|
||
|
||
This property specifies the time scale of an
|
||
`icalendar-vcalendar' object. The only scale defined by RFC5545
|
||
is \"GREGORIAN\", which is the default."
|
||
;; only allowed value:
|
||
"GREGORIAN"
|
||
:default "GREGORIAN"
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.1")
|
||
|
||
(ical:define-property ical:method "METHOD"
|
||
"Method for a scheduling request.
|
||
|
||
When an `icalendar-vcalendar' is sent in a MIME message, this property
|
||
specifies the semantics of the request in the message: e.g. it is
|
||
a request to publish the calendar object, or a reply to an
|
||
invitation. This property and the MIME message's \"method\"
|
||
parameter value must be the same.
|
||
|
||
RFC5545 does not define any methods, but RFC5546 does; see
|
||
URL `https://www.rfc-editor.org/rfc/rfc5546.html#section-3.2'"
|
||
;; TODO: implement methods in RFC5546?
|
||
ical:iana-token
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.2")
|
||
|
||
(ical:define-property ical:prodid "PRODID"
|
||
"Product Identifier.
|
||
|
||
This property identifies the program that created an
|
||
`icalendar-vcalendar' object. It must be specified exactly once in a
|
||
calendar object. Its value should be a globally unique identifier for
|
||
the program. RFC5545 suggests using an ISO \"Formal Public Identifier\";
|
||
see URL `https://en.wikipedia.org/wiki/Formal_Public_Identifier'."
|
||
ical:text
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.3")
|
||
|
||
(ical:define-property ical:version "VERSION"
|
||
"Version (2.0 corresponds to RFC5545).
|
||
|
||
This property specifies the version number of the iCalendar
|
||
specification to which an `icalendar-vcalendar' object conforms,
|
||
and must be specified exactly once in a calendar object. It is
|
||
either the string \"2.0\" or a string like MIN;MAX specifying
|
||
minimum and maximum versions of future revisions of the
|
||
specification."
|
||
(or "2.0"
|
||
;; minver ";" maxver
|
||
(seq ical:iana-token ?\; ical:iana-token))
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.7.4")
|
||
|
||
|
||
;;;; Section 3.8:
|
||
;;;;; Section 3.8.1: Descriptive Component Properties
|
||
|
||
(ical:define-property ical:attach "ATTACH"
|
||
"Attachment.
|
||
|
||
This property specifies a file attached to an iCalendar
|
||
component, either via a URI, or as encoded binary data. In
|
||
`icalendar-valarm' components, it is used to specify the
|
||
notification sent by the alarm."
|
||
;; Groups 11, 12 are used in ical:uri
|
||
(or (group-n 13 ical:uri)
|
||
(group-n 14 ical:binary))
|
||
:default-type ical:uri
|
||
:other-types (ical:binary)
|
||
:child-spec (:zero-or-one (ical:fmttypeparam
|
||
ical:valuetypeparam
|
||
ical:encodingparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:other-validator ical:attach-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.1")
|
||
|
||
(defun ical:attach-validator (node)
|
||
"Additional validator for an `icalendar-attach' NODE.
|
||
Checks that NODE has a correct `icalendar-encodingparam' and
|
||
`icalendar-valuetypeparam' if its value is an `icalendar-binary'.
|
||
|
||
This function is called by `icalendar-ast-node-valid-p' for
|
||
ATTACH nodes; it is not normally necessary to call it directly."
|
||
(let* ((value-node (ical:ast-node-value node))
|
||
(value-type (ical:ast-node-type value-node))
|
||
(valtypeparam (ical:ast-node-first-child-of 'ical:valuetypeparam node))
|
||
(encodingparam (ical:ast-node-first-child-of 'ical:encodingparam node)))
|
||
|
||
(when (eq value-type 'ical:binary)
|
||
(unless (and (ical:ast-node-p valtypeparam)
|
||
(eq 'ical:binary
|
||
(ical:ast-node-value ; unwrap inner printed-value-type
|
||
(ical:ast-node-value valtypeparam))))
|
||
(ical:signal-validation-error
|
||
"`icalendar-binary' attachment requires 'VALUE=BINARY' parameter"
|
||
:node node))
|
||
(unless (and (ical:ast-node-p encodingparam)
|
||
(equal "BASE64" (ical:ast-node-value encodingparam)))
|
||
(ical:signal-validation-error
|
||
"`icalendar-binary' attachment requires 'ENCODING=BASE64' parameter"
|
||
:node node)))
|
||
;; success:
|
||
node))
|
||
|
||
(ical:define-property ical:categories "CATEGORIES"
|
||
"Categories.
|
||
|
||
This property lists categories or subtypes of an iCalendar
|
||
component for e.g. searching or filtering. The categories can be
|
||
any `icalendar-text' value."
|
||
ical:text
|
||
:list-sep ","
|
||
:child-spec (:zero-or-one (ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.2")
|
||
|
||
(ical:define-property ical:class "CLASS"
|
||
"(Access) Classification.
|
||
|
||
This property specifies the scope of access that the calendar
|
||
owner intends for a given component, e.g. public or private."
|
||
(or "PUBLIC"
|
||
"PRIVATE"
|
||
"CONFIDENTIAL"
|
||
(group-n 5
|
||
(or ical:iana-token
|
||
ical:x-name)))
|
||
;; "If not specified in a component that allows this property, the
|
||
;; default value is PUBLIC. Applications MUST treat x-name and
|
||
;; iana-token values they don't recognize the same way as they would
|
||
;; the PRIVATE value."
|
||
:default "PUBLIC"
|
||
:unrecognized "PRIVATE"
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.3")
|
||
|
||
(ical:define-property ical:comment "COMMENT"
|
||
"Comment to calendar user.
|
||
|
||
This property can be specified multiple times in calendar components,
|
||
and can contain any `icalendar-text' value."
|
||
ical:text
|
||
:child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.4")
|
||
|
||
(ical:define-property ical:description "DESCRIPTION"
|
||
"Description.
|
||
|
||
This property should be a longer, more complete description of
|
||
the calendar component than is contained in the
|
||
`icalendar-summary' property. In a `icalendar-vjournal'
|
||
component, it is used to capture a journal entry, and may be
|
||
specified multiple times. Otherwise it may only be specified
|
||
once. In an `icalendar-valarm' component, it contains the
|
||
notification text for a DISPLAY or EMAIL alarm."
|
||
ical:text
|
||
:child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.5")
|
||
|
||
(defun ical:read-geo-coordinates (s)
|
||
"Read an `icalendar-geo-coordinates' value from string S."
|
||
(let ((vals (mapcar #'string-to-number (string-split s ";"))))
|
||
(cons (car vals) (cadr vals))))
|
||
|
||
(defun ical:print-geo-coordinates (val)
|
||
"Serialize an `icalendar-geo-coordinates' value to a string."
|
||
(concat (number-to-string (car val)) ";" (number-to-string (cdr val))))
|
||
|
||
(defun ical:geo-coordinates-p (val)
|
||
"Return non-nil if VAL is an `icalendar-geo-coordinates' value."
|
||
(and (floatp (car val)) (floatp (cdr val))))
|
||
|
||
(ical:define-type ical:geo-coordinates nil ; don't add to ical:value-types
|
||
"Type for global positions.
|
||
|
||
This is not a type defined by RFC5545; it is defined here to
|
||
facilitate parsing the `icalendar-geo' property. When printed, it
|
||
is represented as a pair of `icalendar-float' values separated by
|
||
a semicolon, like LATITUDE;LONGITUDE. When read, it is a dotted
|
||
pair of Elisp floats (LATITUDE . LONGITUDE)."
|
||
'(satisfies ical:geo-coordinates-p)
|
||
(seq ical:float ";" ical:float)
|
||
:reader ical:read-geo-coordinates
|
||
:printer ical:print-geo-coordinates)
|
||
|
||
(ical:define-property ical:geo "GEO"
|
||
"Global position of a component as a pair LATITUDE;LONGITUDE.
|
||
|
||
Both values are floats representing a number of degrees. The
|
||
latitude value is north of the equator if positive, and south of
|
||
the equator if negative. The longitude value is east of the prime
|
||
meridian if positive, and west of it if negative."
|
||
ical:geo-coordinates
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.6")
|
||
|
||
(ical:define-property ical:location "LOCATION"
|
||
"Location.
|
||
|
||
This property describes the intended location or venue of a
|
||
component, e.g. a particular room or building, with an
|
||
`icalendar-text' value. RFC5545 suggests using the
|
||
`icalendar-altrep' parameter on this property to provide more
|
||
structured location information."
|
||
ical:text
|
||
:child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.7")
|
||
|
||
;; TODO: type for percentages?
|
||
(ical:define-property ical:percent-complete "PERCENT-COMPLETE"
|
||
"Percent Complete.
|
||
|
||
This property describes progress toward the completion of an
|
||
`icalendar-vtodo' component. It can appear at most once in such a
|
||
component. If this TODO is assigned to multiple people, the value
|
||
represents the completion state for each person individually. The
|
||
value should be between 0 and 100 (though this is not currently
|
||
enforced here)."
|
||
ical:integer
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.8")
|
||
|
||
;; TODO: type for priority values?
|
||
(ical:define-property ical:priority "PRIORITY"
|
||
"Priority.
|
||
|
||
This property describes the priority of a component. 0 means an
|
||
undefined priority. Other values range from 1 (highest priority)
|
||
to 9 (lowest priority). See RFC5545 for suggestions on how to
|
||
represent other priority schemes with this property."
|
||
ical:integer
|
||
:default "0"
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.9")
|
||
|
||
(ical:define-property ical:resources "RESOURCES"
|
||
"Resources for an activity.
|
||
|
||
This property is a list of `icalendar-text' values that describe
|
||
any resources required or foreseen for the activity represented
|
||
by a component, e.g. a projector and screen for a meeting."
|
||
ical:text
|
||
:list-sep ","
|
||
:child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.10")
|
||
|
||
(ical:define-type ical:status-keyword nil
|
||
"Keyword value of a STATUS property.
|
||
|
||
This is not a real type defined by RFC5545; it is defined here to
|
||
facilitate parsing that property."
|
||
'(and string (satisfies ical:match-status-keyword-value))
|
||
;; Note that this type does NOT allow arbitrary text:
|
||
(or "TENTATIVE"
|
||
"CONFIRMED"
|
||
"CANCELLED"
|
||
"NEEDS-ACTION"
|
||
"COMPLETED"
|
||
"IN-PROCESS"
|
||
"DRAFT"
|
||
"FINAL"))
|
||
|
||
(ical:define-property ical:status "STATUS"
|
||
"Overall status or confirmation.
|
||
|
||
This property is a keyword used by an Organizer to inform
|
||
Attendees about the status of a component, e.g. whether an
|
||
`icalendar-vevent' has been cancelled, whether an
|
||
`icalendar-vtodo' has been completed, or whether an
|
||
`icalendar-vjournal' is still in draft form. It can be specified
|
||
at most once on these components."
|
||
ical:status-keyword
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.11")
|
||
|
||
(ical:define-property ical:summary "SUMMARY"
|
||
"Short summary.
|
||
|
||
This property provides a short, one-line description of a
|
||
component for display purposes. In an EMAIL `icalendar-valarm',
|
||
it is used as the subject of the email. A longer description of
|
||
the component can be provided in the `icalendar-description'
|
||
property."
|
||
ical:text
|
||
:child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.12")
|
||
|
||
;;;;; Section 3.8.2: Date and Time Component Properties
|
||
|
||
(defun ical:property-w/tzid-validator (node)
|
||
"Additional validator for property NODE with `icalendar-tzid' parameters.
|
||
Checks that this parameter does not occur in combination with an
|
||
`icalendar-date' value or an `icalendar-date-time' in UTC time."
|
||
(ical:with-property node
|
||
((ical:tzidparam :first tzidnode))
|
||
(when (and tzidnode (eq value-type 'ical:date))
|
||
(icalendar-signal-validation-error
|
||
"Property cannot contain `icalendar-tzidparam' with `icalendar-date' value"
|
||
:node node))
|
||
(when (and tzidnode (eq value-type 'ical:date-time)
|
||
(ical:date-time-is-utc-p value))
|
||
(icalendar-signal-validation-error
|
||
"Property cannot contain `icalendar-tzidparam' in combination with UTC time"
|
||
:node node))))
|
||
|
||
(ical:define-property ical:completed "COMPLETED"
|
||
"Time completed.
|
||
|
||
This property is a timestamp that records the date and time when
|
||
an `icalendar-vtodo' was actually completed. The value must be an
|
||
`icalendar-date-time' with a UTC time."
|
||
ical:date-time
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.1")
|
||
|
||
(ical:define-property ical:dtend "DTEND"
|
||
"End time of an event or free/busy block.
|
||
|
||
This property's value specifies when an `icalendar-vevent' or
|
||
`icalendar-freebusy' ends. Its value must be of the same type as
|
||
the value of the component's corresponding `icalendar-dtstart'
|
||
property. The value is a non-inclusive bound, i.e., the value of
|
||
this property must be the first time or date *after* the end of
|
||
the event or free/busy block."
|
||
(or ical:date-time
|
||
ical:date)
|
||
:default-type ical:date-time
|
||
:other-types (ical:date)
|
||
:child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:other-validator ical:property-w/tzid-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.2")
|
||
|
||
(ical:define-property ical:due "DUE"
|
||
"Due date.
|
||
|
||
This property specifies the date (and possibly time) by which an
|
||
`icalendar-todo' item is expected to be completed, i.e., its
|
||
deadline. If the component also has an `icalendar-dtstart'
|
||
property, the two properties must have the same value type, and
|
||
the value of the DTSTART property must be earlier than the value
|
||
of this property."
|
||
(or ical:date-time
|
||
ical:date)
|
||
:default-type ical:date-time
|
||
:other-types (ical:date)
|
||
:child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:other-validator ical:property-w/tzid-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.3")
|
||
|
||
(ical:define-property ical:dtstart "DTSTART"
|
||
"Start time of a component.
|
||
|
||
This property's value specifies when a component starts. In an
|
||
`icalendar-vevent', it specifies the start of the event. In an
|
||
`icalendar-vfreebusy', it specifies the start of the free/busy
|
||
block. In `icalendar-standard' and `icalendar-daylight'
|
||
sub-components, it defines the start time of a time zone
|
||
specification.
|
||
|
||
It is required in any component with an `icalendar-rrule'
|
||
property, and in any `icalendar-vevent' component contained in a
|
||
calendar that does not have a `icalendar-method' property.
|
||
|
||
Its value must be of the same type as the value of the
|
||
component's corresponding `icalendar-dtend' property. In an
|
||
`icalendar-vtodo' component, it must also be of the same type as
|
||
the value of an `icalendar-due' property (if present)."
|
||
(or ical:date-time
|
||
ical:date)
|
||
:default-type ical:date-time
|
||
:other-types (ical:date)
|
||
:child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:other-validator ical:property-w/tzid-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.4")
|
||
|
||
(ical:define-property ical:duration "DURATION"
|
||
"Duration.
|
||
|
||
This property specifies a duration of time for a component.
|
||
In an `icalendar-vevent', it can be used to implicitly specify
|
||
the end of the event, instead of an explicit `icalendar-dtend'.
|
||
In an `icalendar-vtodo', it can likewise be used to implicitly specify
|
||
the due date, instead of an explicit `icalendar-due'.
|
||
In an `icalendar-valarm', it used to specify the delay period
|
||
before the alarm repeats.
|
||
|
||
If a related `icalendar-dtstart' property has an `icalendar-date'
|
||
value, then the duration must be given as a number of weeks or days."
|
||
ical:dur-value
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.5")
|
||
|
||
(ical:define-property ical:freebusy "FREEBUSY"
|
||
"Free/Busy Times.
|
||
|
||
This property specifies a list of periods of free or busy time in
|
||
an `icalendar-vfreebusy' component. Whether it specifies free or
|
||
busy times is determined by its `icalendar-fbtype' parameter. The
|
||
times in each period must be in UTC format."
|
||
ical:period
|
||
:list-sep ","
|
||
:child-spec (:zero-or-one (ical:fbtypeparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.6")
|
||
|
||
(ical:define-property ical:transp "TRANSP"
|
||
"Time Transparency for free/busy searches.
|
||
|
||
Note that this property only allows two values: \"TRANSPARENT\"
|
||
or \"OPAQUE\". An OPAQUE value means that the component consumes
|
||
time on a calendar. TRANSPARENT means it does not, and thus is
|
||
invisible to free/busy time searches."
|
||
;; Note that this does NOT allow arbitrary text:
|
||
(or "TRANSPARENT"
|
||
"OPAQUE")
|
||
:default "OPAQUE"
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.2.7")
|
||
|
||
;;;;; Section 3.8.3: Time Zone Component Properties
|
||
|
||
(ical:define-property ical:tzid "TZID"
|
||
"Time Zone Identifier.
|
||
|
||
This property specifies the unique identifier for a time zone in
|
||
an `icalendar-vtimezone' component, and is a required property of
|
||
that component. This is an identifier that `icalendar-tzidparam'
|
||
parameters in other components may then refer to."
|
||
(seq (zero-or-one "/") ical:text)
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.1")
|
||
|
||
(ical:define-property ical:tzname "TZNAME"
|
||
"Time Zone Name.
|
||
|
||
This property specifies a customary name for a time zone in
|
||
`icalendar-daylight' and `icalendar-standard' sub-components."
|
||
ical:text
|
||
:child-spec (:zero-or-one (ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.2")
|
||
|
||
(ical:define-property ical:tzoffsetfrom "TZOFFSETFROM"
|
||
"Time Zone Offset (prior to observance).
|
||
|
||
This property specifies the time zone offset that is in use
|
||
*prior to* this time zone observance. It is used to calculate the
|
||
absolute time at which the observance takes place. It is a
|
||
required property of an `icalendar-vtimezone' component. Positive
|
||
numbers indicate time east of the prime meridian (ahead of UTC).
|
||
Negative numbers indicate time west of the prime meridian (behind
|
||
UTC)."
|
||
ical:utc-offset
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.3")
|
||
|
||
(ical:define-property ical:tzoffsetto "TZOFFSETTO"
|
||
"Time Zone Offset (in this observance).
|
||
|
||
This property specifies the time zone offset that is in use *in*
|
||
this time zone observance. It is used to calculate the absolute
|
||
time at which a new observance takes place. It is a required
|
||
property of `icalendar-standard' and `icalendar-daylight'
|
||
components. Positive numbers indicate time east of the prime
|
||
meridian (ahead of UTC). Negative numbers indicate time west of
|
||
the prime meridian (behind UTC)."
|
||
ical:utc-offset
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.4")
|
||
|
||
(ical:define-property ical:tzurl "TZURL"
|
||
"Time Zone URL.
|
||
|
||
This property specifies a URL where updated versions of an
|
||
`icalendar-vtimezone' component are published."
|
||
ical:uri
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.3.5")
|
||
|
||
;;;;; Section 3.8.4: Relationship Component Properties
|
||
|
||
(ical:define-property ical:attendee "ATTENDEE"
|
||
"Attendee.
|
||
|
||
This property specfies a participant in a `icalendar-vevent',
|
||
`icalendar-vtodo', or `icalendar-valarm'. It is required when the
|
||
containing component represents event, task, or notification for
|
||
a *group* of people, but not for components that simply represent
|
||
these items in a single user's calendar (in that case, it should
|
||
not be specified). The property can be specified multiple times,
|
||
once for each participant in the event or task. In an
|
||
EMAIL-category VALARM component, this property specifies the
|
||
address of the user(s) who should receive the notification email.
|
||
|
||
The parameters `icalendar-roleparam', `icalendar-partstatparam',
|
||
`icalendar-rsvpparam', `icalendar-delfromparam', and
|
||
`icalendar-deltoparam' are especially relevant for further
|
||
specifying the roles of each participant in the containing
|
||
component."
|
||
ical:cal-address
|
||
:child-spec (:zero-or-one (ical:cutypeparam
|
||
ical:memberparam
|
||
ical:roleparam
|
||
ical:partstatparam
|
||
ical:rsvpparam
|
||
ical:deltoparam
|
||
ical:delfromparam
|
||
ical:sentbyparam
|
||
ical:cnparam
|
||
ical:dirparam
|
||
ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.1")
|
||
|
||
(ical:define-property ical:contact "CONTACT"
|
||
"Contact.
|
||
|
||
This property provides textual contact information relevant to an
|
||
`icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal', or
|
||
`icalendar-vfreebusy'."
|
||
ical:text
|
||
:child-spec (:zero-or-one (ical:altrepparam ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.2")
|
||
|
||
(ical:define-property ical:organizer "ORGANIZER"
|
||
"Organizer.
|
||
|
||
This property specifies the organizer of a group-scheduled
|
||
`icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'.
|
||
It is required in those components if they represent a calendar
|
||
entity with multiple participants. In an `icalendar-vfreebusy'
|
||
component, it used to specify the user requesting free or busy
|
||
time, or the user who published the calendar that the free/busy
|
||
information comes from."
|
||
ical:cal-address
|
||
:child-spec (:zero-or-one (ical:cnparam
|
||
ical:dirparam
|
||
ical:sentbyparam
|
||
ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.3")
|
||
|
||
(ical:define-property ical:recurrence-id "RECURRENCE-ID"
|
||
"Recurrence ID.
|
||
|
||
This property is used together with the `icalendar-uid' and
|
||
`icalendar-sequence' properties to identify a specific instance
|
||
of a recurring `icalendar-vevent', `icalendar-vtodo', or
|
||
`icalendar-vjournal' component. The property value is the
|
||
original value of the `icalendar-dtstart' property of the
|
||
recurrence instance. Its value must have the same type as that
|
||
property's value, and both must specify times in the same way
|
||
(either local or UTC)."
|
||
(or ical:date-time
|
||
ical:date)
|
||
:default-type ical:date-time
|
||
:other-types (ical:date)
|
||
:child-spec (:zero-or-one (ical:valuetypeparam
|
||
ical:tzidparam
|
||
ical:rangeparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.4")
|
||
|
||
(ical:define-property ical:related-to "RELATED-TO"
|
||
"Related To (component UID).
|
||
|
||
This property specifies the `icalendar-uid' value of a different,
|
||
related calendar component. It can be specified on an
|
||
`icalendar-vevent', `icalendar-vtodo', or `icalendar-vjournal'
|
||
component. An `icalendar-reltypeparam' can be used to specify the
|
||
relationship type."
|
||
ical:text
|
||
:child-spec (:zero-or-one (ical:reltypeparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.5")
|
||
|
||
(ical:define-property ical:url "URL"
|
||
"Uniform Resource Locator.
|
||
|
||
This property specifies the URL associated with an
|
||
`icalendar-vevent', `icalendar-vtodo', `icalendar-vjournal', or
|
||
`icalendar-vfreebusy' component."
|
||
ical:uri
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.6")
|
||
|
||
;; TODO: UID should probably be its own type
|
||
(ical:define-property ical:uid "UID"
|
||
"Unique Identifier.
|
||
|
||
This property specifies a globally unique identifier for the
|
||
containing component, and is required in an `icalendar-vevent',
|
||
`icalendar-vtodo', `icalendar-vjournal', or `icalendar-vfreebusy'
|
||
component.
|
||
|
||
RFC5545 requires that the program generating the UID guarantee
|
||
that it be unique, and recommends generating it in a format which
|
||
includes a timestamp on the left hand side of an '@' character,
|
||
and the domain name or IP address of the host on the right-hand
|
||
side."
|
||
ical:text
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.4.7")
|
||
|
||
;;;;; Section 3.8.5: Recurrence Component Properties
|
||
|
||
(ical:define-property ical:exdate "EXDATE"
|
||
"Exception Date-Times.
|
||
|
||
This property defines a list of exceptions to a recurrence rule
|
||
in an `icalendar-vevent', `icalendar-todo', `icalendar-vjournal',
|
||
`icalendar-standard', or `icalendar-daylight' component. Together
|
||
with the `icalendar-dtstart', `icalendar-rrule', and
|
||
`icalendar-rdate' properties, it defines the recurrence set of
|
||
the component."
|
||
(or ical:date-time
|
||
ical:date)
|
||
:default-type ical:date-time
|
||
:other-types (ical:date)
|
||
:list-sep ","
|
||
:child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:other-validator ical:property-w/tzid-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.1")
|
||
|
||
(ical:define-property ical:rdate "RDATE"
|
||
"Recurrence Date-Times.
|
||
|
||
This property defines a list of date-times or dates on which an
|
||
`icalendar-vevent', `icalendar-todo', `icalendar-vjournal',
|
||
`icalendar-standard', or `icalendar-daylight' component recurs.
|
||
Together with the `icalendar-dtstart', `icalendar-rrule', and
|
||
`icalendar-exdate' properties, it defines the recurrence set of
|
||
the component."
|
||
(or ical:period
|
||
ical:date-time
|
||
ical:date)
|
||
:default-type ical:date-time
|
||
:other-types (ical:date ical:period)
|
||
:list-sep ","
|
||
:child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:other-validator ical:property-w/tzid-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.2")
|
||
|
||
(ical:define-property ical:rrule "RRULE"
|
||
"Recurrence Rule.
|
||
|
||
This property defines a rule or repeating pattern for the dates
|
||
and times on which an `icalendar-vevent', `icalendar-todo',
|
||
`icalendar-vjournal', `icalendar-standard', or
|
||
`icalendar-daylight' component recurs. Together with the
|
||
`icalendar-dtstart', `icalendar-rdate', and `icalendar-exdate'
|
||
properties, it defines the recurrence set of the component."
|
||
ical:recur
|
||
;; TODO: faces for subexpressions?
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5.3")
|
||
|
||
;;;;; Section 3.8.6: Alarm Component Properties
|
||
|
||
(ical:define-property ical:action "ACTION"
|
||
"Action (when alarm triggered).
|
||
|
||
This property defines the action to be taken when the containing
|
||
`icalendar-valarm' component is triggered. It is a required
|
||
property in an alarm component."
|
||
(or "AUDIO"
|
||
"DISPLAY"
|
||
"EMAIL"
|
||
(group-n 5
|
||
(or ical:iana-token
|
||
ical:x-name)))
|
||
;; "Applications MUST ignore alarms with x-name and iana-token values
|
||
;; they don't recognize." This substitute is not defined in the
|
||
;; standard but is the simplest way to parse such alarms:
|
||
:unrecognized "IGNORE"
|
||
:default-type ical:text
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.1")
|
||
|
||
(ical:define-property ical:repeat "REPEAT"
|
||
"Repeat Count (after initial trigger).
|
||
|
||
This property specifies the number of times an `icalendar-valarm'
|
||
should repeat after it is initially triggered. This property,
|
||
along with the `icalendar-duration' property, is required if the
|
||
alarm triggers more than once."
|
||
ical:integer
|
||
:default "0"
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.2")
|
||
|
||
(ical:define-property ical:trigger "TRIGGER"
|
||
"Trigger.
|
||
|
||
This property specifies when an `icalendar-valarm' should
|
||
trigger. If the value is an `icalendar-dur-value', it represents
|
||
a time of that duration relative to the start or end of a related
|
||
`icalendar-vevent' or `icalendar-vtodo'. Whether the trigger
|
||
applies to the start time or end time of the related component
|
||
can be specified with the `icalendar-trigrelparam' parameter. A
|
||
positive duration value triggers after the start or end of the
|
||
related component; a negative duration value triggers before.
|
||
|
||
If the value is an `icalendar-date-time', it must be in UTC
|
||
format, and it triggers at the specified time."
|
||
(or ical:dur-value
|
||
ical:date-time)
|
||
:default-type ical:dur-value
|
||
:other-types (ical:date-time)
|
||
:child-spec (:zero-or-one (ical:valuetypeparam ical:trigrelparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:other-validator ical:trigger-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.6.3")
|
||
|
||
(defun ical:trigger-validator (node)
|
||
"Additional validator for an `icalendar-trigger' NODE.
|
||
Checks that NODE has valid parameters depending on the type of its value.
|
||
|
||
This function is called by `icalendar-ast-node-valid-p' for
|
||
TRIGGER nodes; it is not normally necessary to call it directly."
|
||
(let* ((params (ical:ast-node-children node))
|
||
(value-node (ical:ast-node-value node))
|
||
(value-type (and value-node (ical:ast-node-type value-node))))
|
||
(when (eq value-type 'ical:date-time)
|
||
(let ((expl-type (ical:value-type-from-params params))
|
||
(dt-value (ical:ast-node-value value-node)))
|
||
(unless (eq expl-type 'ical:date-time)
|
||
(ical:signal-validation-error
|
||
(concat "Explicit `icalendar-valuetypeparam' required in "
|
||
"`icalendar-trigger' with non-duration value")
|
||
:node node))
|
||
(when (ical:ast-node-first-child-of 'ical:trigrelparam node)
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-trigrelparam' not allowed in "
|
||
"`icalendar-trigger' with non-duration value")
|
||
:node node))
|
||
(unless (ical:date-time-is-utc-p dt-value)
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-date-time' value of `icalendar-trigger' "
|
||
"must be in UTC time")
|
||
:node node))))
|
||
;; success:
|
||
node))
|
||
|
||
;;;;; Section 3.8.7: Change Management Component Properties
|
||
|
||
(ical:define-property ical:created "CREATED"
|
||
"Date-Time Created.
|
||
|
||
This property specifies the date and time when the calendar user
|
||
initially created an `icalendar-vevent', `icalendar-vtodo', or
|
||
`icalendar-vjournal' in the calendar database. The value must be
|
||
in UTC time."
|
||
ical:date-time
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.1")
|
||
|
||
(ical:define-property ical:dtstamp "DTSTAMP"
|
||
"Timestamp (of last revision or instance creation).
|
||
|
||
In an `icalendar-vevent', `icalendar-vtodo',
|
||
`icalendar-vjournal', or `icalendar-vfreebusy', this property
|
||
specifies the date and time when the calendar user last revised
|
||
the component's data in the calendar database. (In this case, it
|
||
is equivalent to the `icalendar-last-modified' property.)
|
||
|
||
If this property is specified on an `icalendar-vcalendar' object
|
||
which contains an `icalendar-method' property, it specifies the
|
||
date and time when that instance of the calendar object was
|
||
created. In this case, it differs from the `icalendar-creation'
|
||
and `icalendar-last-modified' properties: whereas those specify
|
||
the time the underlying data was created and last modified in the
|
||
calendar database, this property specifies when the calendar
|
||
object *representing* that data was created.
|
||
|
||
The value must be in UTC time."
|
||
ical:date-time
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.2")
|
||
|
||
(ical:define-property ical:last-modified "LAST-MODIFIED"
|
||
"Last Modified timestamp.
|
||
|
||
This property specifies when the data in an `icalendar-vevent',
|
||
`icalendar-vtodo', `icalendar-vjournal', or `icalendar-vtimezone'
|
||
was last modified in the calendar database."
|
||
ical:date-time
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.3")
|
||
|
||
(ical:define-property ical:sequence "SEQUENCE"
|
||
"Revision Sequence Number.
|
||
|
||
This property specifies the number of the current revision in a
|
||
sequence of revisions in an `icalendar-vevent',
|
||
`icalendar-vtodo', or `icalendar-vjournal' component. It starts
|
||
at 0 and should be incremented monotonically every time the
|
||
Organizer makes a significant revision to the calendar data that
|
||
component represents."
|
||
ical:integer
|
||
:default "0"
|
||
:child-spec (:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.7.4")
|
||
|
||
;;;;; Section 3.8.8: Miscellaneous Component Properties
|
||
;; IANA and X- properties should be parsed and printed but can be ignored:
|
||
(ical:define-property ical:other-property nil ; don't add to ical:property-types
|
||
"IANA or X-name property.
|
||
|
||
This property type corresponds to the IANA Properties and
|
||
Non-Standard Properties defined in RFC5545; it represents
|
||
properties with an unknown name (matching rx
|
||
`icalendar-iana-token' or `icalendar-x-name') whose values must
|
||
be parsed and preserved but not further interpreted. Its value
|
||
may be set to any type with the `icalendar-valuetypeparam'
|
||
parameter."
|
||
ical:value
|
||
:default-type ical:text
|
||
;; "The default value type is TEXT. The value type can be set to any
|
||
;; value type." TODO: should we specify :other-types? Without it, a
|
||
;; VALUE param will be required to parse anything other than text,
|
||
;; but that seems reasonable.
|
||
:child-spec (:allow-others t)
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8")
|
||
|
||
(defun ical:read-req-status-info (s)
|
||
"Read a request status value from S.
|
||
S should have been previously matched against `icalendar-request-status-info'."
|
||
;; TODO: this smells like a design flaw. Silence the byte compiler for now.
|
||
(ignore s)
|
||
(let ((code (match-string 11))
|
||
(desc (match-string 12))
|
||
(exdata (match-string 13)))
|
||
(list code (ical:read-text desc) (when exdata (ical:read-text exdata)))))
|
||
|
||
(defun ical:print-req-status-info (rsi)
|
||
"Serialize request status info value RSI to a string."
|
||
(let ((code (car rsi))
|
||
(desc (cadr rsi))
|
||
(exdata (caddr rsi)))
|
||
(if exdata
|
||
(format "%s;%s;%s" code (ical:print-text desc) (ical:print-text exdata))
|
||
(format "%s;%s" code (ical:print-text desc)))))
|
||
|
||
(defun ical:req-status-info-p (val)
|
||
"Return non-nil if VAL is an `icalendar-request-status-info' value."
|
||
(and (listp val)
|
||
(length= val 3)
|
||
(stringp (car val))
|
||
(stringp (cadr val))
|
||
(cl-typep (caddr val) '(or string null))))
|
||
|
||
(ical:define-type ical:req-status-info nil
|
||
"Type for REQUEST-STATUS property values.
|
||
|
||
When read, a list (CODE DESCRIPTION EXCEPTION). CODE is a hierarchical
|
||
numerical code, represented as a string, with the following meanings:
|
||
1.xx Preliminary success
|
||
2.xx Successful
|
||
3.xx Client Error
|
||
4.xx Scheduling Error
|
||
DESCRIPTION is a longer description of the request status, also a string.
|
||
EXCEPTION (which may be nil) is textual data describing an error.
|
||
|
||
When printed, the three elements are separated by semicolons, like
|
||
CODE;DESCRIPTION;EXCEPTION
|
||
or
|
||
CODE;DESCRIPTION
|
||
if EXCEPTION is nil.
|
||
|
||
This is not a type defined by RFC5545; it is defined here to
|
||
facilitate parsing the `icalendar-request-status' property."
|
||
'(satisfies ical:req-status-info-p)
|
||
(seq
|
||
;; statcode: hierarchical status code
|
||
(group-n 11
|
||
(seq (one-or-more digit)
|
||
(** 1 2 (seq ?. (one-or-more digit)))))
|
||
?\;
|
||
;; statdesc: status description
|
||
(group-n 12 ical:text)
|
||
;; exdata: exception data
|
||
(zero-or-one (seq ?\; (group-n 13 ical:text))))
|
||
:reader ical:read-req-status-info
|
||
:printer ical:print-req-status-info
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8.3")
|
||
|
||
(ical:define-property ical:request-status "REQUEST-STATUS"
|
||
"Request status"
|
||
ical:req-status-info
|
||
:child-spec (:zero-or-one (ical:languageparam)
|
||
:zero-or-more (ical:otherparam))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.8.8.3")
|
||
|
||
|
||
;;; Section 3.6: Calendar Components
|
||
|
||
(defvar ical:component-types nil ;; populated by ical:define-component
|
||
"Alist mapping printed component names to type symbols.")
|
||
|
||
(defun ical:parse-component (limit)
|
||
"Parse an iCalendar component from point up to LIMIT.
|
||
Point should be at the start of the component, i.e., at the start
|
||
of a line that looks like \"BEGIN:[COMPONENT-NAME]\". After parsing,
|
||
point is at the beginning of the next line following the component
|
||
\(or end of the buffer). Returns a syntax node representing the component."
|
||
(let ((begin-pos nil)
|
||
(body-begin-pos nil)
|
||
(end-pos nil)
|
||
(body-end-pos nil)
|
||
(begin-regex (rx line-start "BEGIN:" (group-n 2 ical:name) line-end)))
|
||
|
||
(unless (re-search-forward begin-regex limit t)
|
||
(ical:signal-parse-error "Not at start of a component"))
|
||
|
||
(setq begin-pos (match-beginning 0)
|
||
body-begin-pos (1+ (match-end 0))) ; start of next line
|
||
|
||
(let* ((component-name (match-string 2))
|
||
(known-type (alist-get (upcase component-name)
|
||
ical:component-types
|
||
nil nil #'equal))
|
||
(component-type (or known-type 'ical:other-component))
|
||
child children)
|
||
|
||
;; Find end of component:
|
||
(save-excursion
|
||
(if (re-search-forward (concat "^END:" component-name "$") limit t)
|
||
(setq end-pos (match-end 0)
|
||
body-end-pos (1- (match-beginning 0))) ; end of prev. line
|
||
(ical:signal-parse-error
|
||
(format "Matching 'END:%s' not found between %d and %d"
|
||
component-name begin-pos limit)
|
||
:restart-at (1+ limit))))
|
||
|
||
(while (not (bolp))
|
||
(forward-char))
|
||
|
||
;; Parse the properties and subcomponents of this component:
|
||
(while (<= (point) body-end-pos)
|
||
(condition-case err
|
||
(setq child (ical:parse-property-or-component end-pos))
|
||
(ical:parse-error
|
||
(ical:handle-parse-error err)
|
||
(setq child nil)))
|
||
(when child (push child children)))
|
||
|
||
;; Set point up for the next parser:
|
||
(goto-char end-pos)
|
||
(while (and (< (point) (point-max)) (not (bolp)))
|
||
(forward-char))
|
||
|
||
;; Return the syntax node for the component:
|
||
(when children
|
||
(ical:make-ast-node component-type
|
||
(list
|
||
:original-name
|
||
(when (eq component-type 'ical:other-component)
|
||
component-name)
|
||
:buffer (current-buffer)
|
||
:begin begin-pos
|
||
:end end-pos
|
||
:value-begin body-begin-pos
|
||
:value-end body-end-pos)
|
||
(nreverse children))))))
|
||
|
||
(defun ical:parse-property-or-component (limit)
|
||
"Parse a component or a property at point, up to LIMIT.
|
||
Point should be at the beginning of a line which begins a
|
||
component or contains a property."
|
||
(cond ((looking-at-p (rx line-start "BEGIN:" ical:name line-end))
|
||
(ical:parse-component limit))
|
||
((looking-at-p (rx line-start ical:name))
|
||
(ical:parse-property (line-end-position)))
|
||
(t (ical:signal-parse-error
|
||
"Not at start of property or component"
|
||
:restart-at ; find start of next content line:
|
||
(save-excursion
|
||
(if (re-search-forward (rx line-start ical:name) nil t)
|
||
(match-beginning 0)
|
||
(point-max)))))))
|
||
|
||
(defun ical:print-component-node (node)
|
||
"Serialize a component syntax node NODE to a string."
|
||
(let* ((type (ical:ast-node-type node))
|
||
(name (or (ical:ast-node-meta-get :original-name node)
|
||
(car (rassq type ical:component-types))))
|
||
(children (ical:ast-node-children node))
|
||
body)
|
||
|
||
(unless name
|
||
(ical:signal-print-error
|
||
(format "Unknown component name for type `%s'" type)
|
||
:node node))
|
||
|
||
(dolist (child children)
|
||
(condition-case err
|
||
(setq body
|
||
(concat body (ical:print-property-or-component child)))
|
||
(ical:print-error
|
||
(if (ical:ast-node-required-child-p child node)
|
||
(ical:signal-print-error
|
||
(format
|
||
"Unable to print required `%s' %s in `%s' component. Error was:\n%s"
|
||
(ical:ast-node-type child)
|
||
(if (ical:component-node-p child) "subcomponent" "property")
|
||
(ical:ast-node-type node)
|
||
(plist-get (cdr err) :message))
|
||
:node node)
|
||
(ical:handle-print-error err)))))
|
||
(concat
|
||
(format "BEGIN:%s\n" name)
|
||
body
|
||
(format "END:%s\n" name))))
|
||
|
||
(defun ical:print-property-or-component (node)
|
||
"Serialize a property or component node NODE to a string."
|
||
(cond ((ical:property-node-p node)
|
||
(ical:print-property-node node))
|
||
((ical:component-node-p node)
|
||
(ical:print-component-node node))
|
||
(t (ical:signal-print-error "Not a component or property node"
|
||
:node node))))
|
||
|
||
(ical:define-component ical:vevent "VEVENT"
|
||
"Represents an event.
|
||
|
||
This component contains properties which describe an event, such
|
||
as its start and end time (`icalendar-dtstart' and
|
||
`icalendar-dtend') and a summary (`icalendar-summary') and
|
||
description (`icalendar-description'). It may also contain
|
||
`icalendar-valarm' components as subcomponents which describe
|
||
reminder notifications related to the event. Event components can
|
||
only be direct children of an `icalendar-vcalendar'; they cannot
|
||
be subcomponents of any other component."
|
||
:child-spec (:one (ical:dtstamp ical:uid)
|
||
:zero-or-one (ical:dtstart
|
||
;; TODO: dtstart required if METHOD not present
|
||
;; in parent calendar
|
||
ical:class
|
||
ical:created
|
||
ical:description
|
||
ical:dtend
|
||
ical:duration
|
||
ical:geo
|
||
ical:last-modified
|
||
ical:location
|
||
ical:organizer
|
||
ical:priority
|
||
ical:sequence
|
||
ical:status
|
||
ical:summary
|
||
ical:transp
|
||
ical:url
|
||
ical:recurrence-id
|
||
ical:rrule)
|
||
:zero-or-more (ical:attach
|
||
ical:attendee
|
||
ical:categories
|
||
ical:comment
|
||
ical:contact
|
||
ical:exdate
|
||
ical:request-status
|
||
ical:related-to
|
||
ical:resources
|
||
ical:rdate
|
||
ical:other-property
|
||
ical:valarm))
|
||
:other-validator ical:vevent-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.1")
|
||
|
||
(defun ical:rrule-validator (node)
|
||
"Validate that NODE has the properties required by a recurrence rule.
|
||
|
||
NODE should represent an iCalendar component. When NODE has an
|
||
`icalendar-rrule' property, this function validates that its
|
||
`icalendar-dtstart', `icalendar-rdate', and `icalendar-exdate'
|
||
properties satisfy the requirements imposed by this rule.
|
||
|
||
This function is called by the additional validator functions for
|
||
component nodes (e.g. `icalendar-vevent-validator'); it is not normally
|
||
necessary to call it directly."
|
||
(let* ((rrule (ical:ast-node-first-child-of 'ical:rrule node))
|
||
(recval (when rrule (ical:ast-node-value rrule)))
|
||
(dtstart (ical:ast-node-first-child-of 'ical:dtstart node))
|
||
(start (when dtstart (ical:ast-node-value dtstart)))
|
||
(rdates (ical:ast-node-children-of 'ical:rdate node))
|
||
(included (when rdates
|
||
(mapcar #'ical:ast-node-value
|
||
(apply #'append
|
||
(mapcar #'ical:ast-node-value rdates))))))
|
||
(when rrule
|
||
(unless dtstart
|
||
(ical:signal-validation-error
|
||
"An `icalendar-rrule' requires an `icalendar-dtstart' property"
|
||
:node node))
|
||
(when included
|
||
;; ""RDATE" in this usage [i.e., in STANDARD and DAYLIGHT
|
||
;; subcomponents] MUST be specified as a date with local time
|
||
;; value, relative to the UTC offset specified in the
|
||
;; "TZOFFSETFROM" property."
|
||
(when (and (memq (ical:ast-node-type node) '(ical:standard ical:daylight)))
|
||
(unless (ical:list-of-p included 'ical:date-time)
|
||
(ical:signal-validation-error
|
||
(format
|
||
(concat "`icalendar-rdate' values must be `icalendar-date-time' "
|
||
"values in %s components")
|
||
(ical:ast-node-type node))
|
||
:node node))
|
||
(when (seq-some #'decoded-time-zone included)
|
||
(ical:signal-validation-error
|
||
(format
|
||
(concat "`icalendar-rdate' values must be in local (\"floating\")"
|
||
"time in %s components")
|
||
(ical:ast-node-type node))
|
||
:node node))))
|
||
|
||
(let* ((freq (car (alist-get 'FREQ recval)))
|
||
(until (car (alist-get 'UNTIL recval))))
|
||
(when (eq 'ical:date (ical:ast-node-type start))
|
||
(when (or (memq freq '(HOURLY MINUTELY SECONDLY))
|
||
(assq 'BYSECOND recval)
|
||
(assq 'BYMINUTE recval)
|
||
(assq 'BYHOUR recval))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-rrule' must not contain time-based "
|
||
"rules when `icalendar-dtstart' is a plain date")
|
||
:node node)))
|
||
(when until
|
||
(unless (eq (ical:ast-node-type start)
|
||
(ical:ast-node-type until))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-rrule' UNTIL clause must agree with "
|
||
"type of `icalendar-dtstart' property")
|
||
:node node))
|
||
(when (eq 'ical:date-time (ical:ast-node-type until))
|
||
(let ((until-zone
|
||
(decoded-time-zone (ical:ast-node-value until)))
|
||
(start-zone
|
||
(decoded-time-zone (ical:ast-node-value start))))
|
||
;; "If the "DTSTART" property is specified as a date
|
||
;; with local time, then the UNTIL rule part MUST also
|
||
;; be specified as a date with local time":
|
||
(when (and (null start-zone) (not (null until-zone)))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-rrule' UNTIL clause must be in "
|
||
"local time if `icalendar-dtstart' is")
|
||
:node node))
|
||
;; "If the "DTSTART" property is specified as a date
|
||
;; with UTC time or a date with local time and time zone
|
||
;; reference, then the UNTIL rule part MUST be specified
|
||
;; as a date with UTC time":
|
||
(when (and (integerp start-zone)
|
||
(not (ical:date-time-is-utc-p until)))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-rrule' UNTIL clause must be in UTC time "
|
||
"if `icalendar-dtstart' has a defined time zone")
|
||
:node node))))
|
||
(when (memq (ical:ast-node-type node) '(ical:standard ical:daylight))
|
||
;; "In the case of the "STANDARD" and "DAYLIGHT"
|
||
;; sub-components the UNTIL rule part MUST always be
|
||
;; specified as a date with UTC time":
|
||
(unless (ical:date-time-is-utc-p until)
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-rrule' UNTIL clause must be in UTC time in "
|
||
"`icalendar-standard' and `icalendar-daylight' components")
|
||
:node node))))
|
||
|
||
;; "DTSTART in this usage [i.e., in STANDARD and DAYLIGHT
|
||
;; subcomponents] MUST be specified as a date with a local
|
||
;; time value."
|
||
(when (memq (ical:ast-node-type node) '(ical:standard ical:daylight))
|
||
(unless (eq 'ical:date-time (ical:ast-node-type start))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-dtstart' must be an `icalendar-date-time' in "
|
||
"`icalendar-standard' and `icalendar-daylight' components")
|
||
:node node))
|
||
|
||
(when (decoded-time-zone (ical:ast-node-value start))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-dtstart' must be in local (\"floating\") time in "
|
||
"`icalendar-standard' and `icalendar-daylight' components")
|
||
:node node)))))
|
||
|
||
;; Success:
|
||
node))
|
||
|
||
(defun ical:vevent-validator (node)
|
||
"Additional validator for an `icalendar-vevent' NODE.
|
||
Checks that NODE has does not have both `icalendar-duration' and
|
||
`icalendar-dtend' properties, and calls `icalendar-rrule-validator'.
|
||
|
||
This function is called by `icalendar-ast-node-valid-p' for
|
||
VEVENT nodes; it is not normally necessary to call it directly."
|
||
(let* ((duration (ical:ast-node-first-child-of 'ical:duration node))
|
||
(dur-value (when duration (ical:ast-node-value
|
||
(ical:ast-node-value duration))))
|
||
(dtend (ical:ast-node-first-child-of 'ical:dtend node))
|
||
(dtstart (ical:ast-node-first-child-of 'ical:dtstart node)))
|
||
(when (and dtend duration)
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-dtend' and `icalendar-duration' properties must "
|
||
"not appear in the same `icalendar-vevent'")
|
||
:node node))
|
||
;; don't allow time-based durations with dates
|
||
;; TODO: check that the standard disallows this...?
|
||
(when (and dtstart duration
|
||
(eq 'ical:date (ical:ast-node-type dtstart))
|
||
(or (not (integerp dur-value))
|
||
(decoded-time-hour dur-value)
|
||
(decoded-time-minute dur-value)
|
||
(decoded-time-second dur-value)))
|
||
(ical:signal-validation-error
|
||
(concat "Event with `icalendar-date' value in `icalendar-dtstart' "
|
||
"cannot have time units in `icalendar-duration'")
|
||
:node node))
|
||
|
||
(ical:rrule-validator node)
|
||
;; success:
|
||
node))
|
||
|
||
(ical:define-component ical:vtodo "VTODO"
|
||
"Represents a To-Do item or task.
|
||
|
||
This component contains properties which describe a to-do item or
|
||
task, such as its due date (`icalendar-due') and a summary
|
||
(`icalendar-summary') and description (`icalendar-description').
|
||
It may also contain `icalendar-valarm' components as
|
||
subcomponents which describe reminder notifications related to
|
||
the task. To-do components can only be direct children of an
|
||
`icalendar-vcalendar'; they cannot be subcomponents of any other
|
||
component."
|
||
:child-spec (:one (ical:dtstamp ical:uid)
|
||
:zero-or-one (ical:class
|
||
ical:completed
|
||
ical:created
|
||
ical:description
|
||
ical:dtstart
|
||
ical:due
|
||
ical:duration
|
||
ical:geo
|
||
ical:last-modified
|
||
ical:location
|
||
ical:organizer
|
||
ical:percent-complete
|
||
ical:priority
|
||
ical:recurrence-id
|
||
ical:sequence
|
||
ical:status
|
||
ical:summary
|
||
ical:url
|
||
ical:rrule)
|
||
:zero-or-more (ical:attach
|
||
ical:attendee
|
||
ical:categories
|
||
ical:comment
|
||
ical:contact
|
||
ical:exdate
|
||
ical:request-status
|
||
ical:related-to
|
||
ical:resources
|
||
ical:rdate
|
||
ical:other-property
|
||
ical:valarm))
|
||
:other-validator ical:vtodo-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.2")
|
||
|
||
(defun ical:vtodo-validator (node)
|
||
"Additional validator for an `icalendar-vtodo' NODE.
|
||
Checks that NODE has conformant `icalendar-due',
|
||
`icalendar-duration', and `icalendar-dtstart' properties, and calls
|
||
`icalendar-rrule-validator'.
|
||
|
||
This function is called by `icalendar-ast-node-valid-p' for
|
||
VTODO nodes; it is not normally necessary to call it directly."
|
||
(let* ((due (ical:ast-node-first-child-of 'ical:due node))
|
||
(duration (ical:ast-node-first-child-of 'ical:duration node))
|
||
(dtstart (ical:ast-node-first-child-of 'ical:dtstart node)))
|
||
(when (and due duration)
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-due' and `icalendar-duration' properties "
|
||
"must not appear in the same `icalendar-vtodo'")
|
||
:node node))
|
||
(when (and duration (not dtstart))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-duration' requires `icalendar-dtstart' "
|
||
"property in the same `icalendar-vtodo'")
|
||
:node node)))
|
||
(ical:rrule-validator node)
|
||
;; success:
|
||
node)
|
||
|
||
(ical:define-component ical:vjournal "VJOURNAL"
|
||
"Represents a journal entry.
|
||
|
||
This component contains properties which describe a journal
|
||
entry, which might be any longer-form data (e.g., meeting notes,
|
||
a diary entry, or information needed to complete a task). It can
|
||
be associated with an `icalendar-vevent' or `icalendar-vtodo' via
|
||
the `icalendar-related-to' property. A journal entry does not
|
||
take up time in a calendar, and plays no role in searches for
|
||
free or busy time. Journal components can only be direct children
|
||
of `icalendar-vcalendar'; they cannot be subcomponents of any
|
||
other component."
|
||
:child-spec (:one (ical:dtstamp ical:uid)
|
||
:zero-or-one (ical:class
|
||
ical:created
|
||
ical:dtstart
|
||
ical:last-modified
|
||
ical:organizer
|
||
ical:recurrence-id
|
||
ical:sequence
|
||
ical:status
|
||
ical:summary
|
||
ical:url
|
||
ical:rrule)
|
||
:zero-or-more (ical:attach
|
||
ical:attendee
|
||
ical:categories
|
||
ical:comment
|
||
ical:contact
|
||
ical:description
|
||
ical:exdate
|
||
ical:related-to
|
||
ical:rdate
|
||
ical:request-status
|
||
ical:other-property)
|
||
:other-validator ical:rrule-validator)
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.3")
|
||
|
||
(ical:define-component ical:vfreebusy "VFREEBUSY"
|
||
"Represents a published set of free/busy time blocks, or a request
|
||
or response for such blocks.
|
||
|
||
The free/busy information is represented by the
|
||
`icalendar-freebusy' property (which may be given more than once)
|
||
and the related `icalendar-fbtype' parameter. Note that
|
||
recurrence properties (`icalendar-rrule', `icalendar-rdate', and
|
||
`icalendar-exdate') are NOT permitted in this component.
|
||
|
||
When used to publish blocks of free/busy time in a user's
|
||
schedule, the `icalendar-organizer' property specifies the user.
|
||
|
||
When used to request free/busy time in a user's schedule, or to
|
||
respond to such a request, the `icalendar-attendee' property
|
||
specifies the user whose time is being requested, and the
|
||
`icalendar-organizer' property specifies the user making the
|
||
request.
|
||
|
||
Free/busy components can only be direct children
|
||
of `icalendar-vcalendar'; they cannot be subcomponents of any
|
||
other component, and cannot contain subcomponents."
|
||
:child-spec (:one (ical:dtstamp ical:uid)
|
||
:zero-or-one (ical:contact
|
||
ical:dtstart
|
||
ical:dtend
|
||
ical:organizer
|
||
ical:url)
|
||
:zero-or-more (ical:attendee
|
||
ical:comment
|
||
ical:freebusy
|
||
ical:request-status
|
||
ical:other-property))
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.4")
|
||
|
||
;; TODO: RFC7808 defines additional properties that are relevant here:
|
||
;; https://www.rfc-editor.org/rfc/rfc7808.html#section-7
|
||
(ical:define-component ical:vtimezone "VTIMEZONE"
|
||
"Represents a time zone.
|
||
|
||
A time zone is identified by an `icalendar-tzid' property, which
|
||
is required in this component. Times in other calendar components
|
||
can be specified in local time in this time zone with the
|
||
`icalendar-tzidparam' parameter. An `icalendar-vcalendar' object
|
||
must contain exactly one `icalendar-vtimezone' component for each
|
||
unique time zone identifier used in the calendar.
|
||
|
||
Besides the time zone identifier, a time zone component must
|
||
contain at least one `icalendar-standard' or `icalendar-daylight'
|
||
subcomponent, which describe the observance of standard or
|
||
daylight time in the time zone, including the dates of the
|
||
observance and the relevant offsets from UTC time."
|
||
:child-spec (:one (ical:tzid)
|
||
:zero-or-one (ical:last-modified
|
||
ical:tzurl)
|
||
:zero-or-more (ical:standard
|
||
ical:daylight
|
||
ical:other-property))
|
||
:other-validator ical:vtimezone-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")
|
||
|
||
(defun ical:vtimezone-validator (node)
|
||
"Additional validator for an `icalendar-vtimezone' NODE.
|
||
Checks that NODE has at least one `icalendar-standard' or
|
||
`icalendar-daylight' child.
|
||
|
||
This function is called by `icalendar-ast-node-valid-p' for
|
||
VTIMEZONE nodes; it is not normally necessary to call it directly."
|
||
(let ((child-counts (ical:count-children-by-type node)))
|
||
(when (and (= 0 (alist-get 'ical:standard child-counts 0))
|
||
(= 0 (alist-get 'ical:daylight child-counts 0)))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-vtimezone' must have at least one "
|
||
"`icalendar-standard' or `icalendar-daylight' child")
|
||
:node node)))
|
||
|
||
;; success:
|
||
node)
|
||
|
||
(ical:define-component ical:standard "STANDARD"
|
||
"Represents a Standard Time observance in a time zone.
|
||
|
||
The observance has a start time, specified by an
|
||
`icalendar-dtstart' property, which is required in this component
|
||
and must be in *local* time format. The observance may have a
|
||
recurring onset (e.g. each year on a particular day or date)
|
||
described by the `icalendar-rrule' and `icalendar-rdate'
|
||
properties. An end date for the observance, if there is one, must
|
||
be specified in the UNTIL clause of the `icalendar-rrule' in UTC
|
||
time.
|
||
|
||
The offset from UTC time when the observance begins is specified
|
||
in the `icalendar-tzoffsetfrom' property, which is required. The
|
||
offset from UTC time while the observance is in effect is
|
||
specified by the `icalendar-tzoffsetto' property, which is also
|
||
required. A common identifier for the time zone observance can be
|
||
specified in the `icalendar-tzname' property. Other explanatory
|
||
comments can be provided in `icalendar-comment'.
|
||
|
||
This component must be a direct child of an `icalendar-vtimezone'
|
||
component and cannot contain other subcomponents."
|
||
:child-spec (:one (ical:dtstart
|
||
ical:tzoffsetto
|
||
ical:tzoffsetfrom)
|
||
:zero-or-one (ical:rrule)
|
||
:zero-or-more (ical:comment
|
||
ical:rdate
|
||
ical:tzname
|
||
ical:other-property)
|
||
:other-validator ical:rrule-validator)
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")
|
||
|
||
(ical:define-component ical:daylight "DAYLIGHT"
|
||
"Represents a Daylight Savings Time observance in a time zone.
|
||
|
||
The observance has a start time, specified by an
|
||
`icalendar-dtstart' property, which is required in this component
|
||
and must be in *local* time format. The observance may have a
|
||
recurring onset (e.g. each year on a particular day or date)
|
||
described by the `icalendar-rrule' and `icalendar-rdate'
|
||
properties. An end date for the observance, if there is one, must
|
||
be specified in the UNTIL clause of the `icalendar-rrule' in UTC
|
||
time.
|
||
|
||
The offset from UTC time when the observance begins is specified
|
||
in the `icalendar-tzoffsetfrom' property, which is required. The
|
||
offset from UTC time while the observance is in effect is
|
||
specified by the `icalendar-tzoffsetto' property, which is also
|
||
required. A common identifier for the time zone observance can be
|
||
specified in the `icalendar-tzname' property. Other
|
||
explanatory comments can be provided in `icalendar-comment'.
|
||
|
||
This component must be a direct child of an `icalendar-vtimezone'
|
||
component and cannot contain other subcomponents."
|
||
:child-spec (:one (ical:dtstart
|
||
ical:tzoffsetto
|
||
ical:tzoffsetfrom)
|
||
:zero-or-one (ical:rrule)
|
||
:zero-or-more (ical:comment
|
||
ical:rdate
|
||
ical:tzname
|
||
ical:other-property)
|
||
:other-validator ical:rrule-validator)
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.5")
|
||
|
||
(ical:define-component ical:valarm "VALARM"
|
||
"Represents an alarm.
|
||
|
||
An alarm is a notification or reminder for an event or task. The
|
||
type of notification is determined by this component's
|
||
`icalendar-action' property: it may be an AUDIO, DISPLAY, or
|
||
EMAIL notification.
|
||
If it is an audio alarm, it can include an
|
||
`icalendar-attach' property specifying the audio to be rendered.
|
||
If it is a DISPLAY alarm, it must include an `icalendar-description'
|
||
property containing the text to be displayed.
|
||
If it is an EMAIL alarm, it must include both an
|
||
`icalendar-summary' and an `icalendar-description', which specify
|
||
the subject and body of the email, and one or more
|
||
`icalendar-attendee' properties, which specify the recipients.
|
||
|
||
The required `icalendar-trigger' property specifies when the
|
||
alarm triggers. If the alarm repeats, then `icalendar-duration'
|
||
and `icalendar-repeat' properties are also both required.
|
||
|
||
This component must occur as a direct child of an
|
||
`icalendar-vevent' or `icalendar-vtodo' component, and cannot
|
||
contain any subcomponents."
|
||
:child-spec (:one (ical:action ical:trigger)
|
||
:zero-or-one (ical:duration ical:repeat)
|
||
:zero-or-more (ical:summary
|
||
ical:description
|
||
ical:attendee
|
||
ical:attach
|
||
ical:other-property))
|
||
:other-validator ical:valarm-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.6")
|
||
|
||
(defun ical:valarm-validator (node)
|
||
"Additional validator function for `icalendar-valarm' components.
|
||
Checks that NODE has the right properties corresponding to its
|
||
`icalendar-action' type, e.g., that an EMAIL alarm has a
|
||
subject (`icalendar-summary') and recipients (`icalendar-attendee').
|
||
|
||
This function is called by `icalendar-ast-node-valid-p' for
|
||
VALARM nodes; it is not normally necessary to call it directly."
|
||
(let* ((action (ical:ast-node-first-child-of 'ical:action node))
|
||
(duration (ical:ast-node-first-child-of 'ical:duration node))
|
||
(repeat (ical:ast-node-first-child-of 'ical:repeat node))
|
||
(child-counts (ical:count-children-by-type node)))
|
||
|
||
(when (and duration (not repeat))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-valarm' node with `icalendar-duration' "
|
||
"must also have `icalendar-repeat' property")
|
||
:node node))
|
||
|
||
(when (and repeat (not duration))
|
||
(ical:signal-validation-error
|
||
(concat "`icalendar-valarm' node with `icalendar-repeat' "
|
||
"must also have `icalendar-duration' property")
|
||
:node node))
|
||
|
||
(let ((action-str (upcase (ical:text-to-string
|
||
(ical:ast-node-value action)))))
|
||
(cond ((equal "AUDIO" action-str)
|
||
(unless (<= (alist-get 'ical:attach child-counts 0) 1)
|
||
(ical:signal-validation-error
|
||
(concat "AUDIO `icalendar-valarm' may not have "
|
||
"more than one `icalendar-attach'")
|
||
:node node))
|
||
node)
|
||
|
||
((equal "DISPLAY" action-str)
|
||
(unless (= 1 (alist-get 'ical:description child-counts 0))
|
||
(ical:signal-validation-error
|
||
(concat "DISPLAY `icalendar-valarm' must have "
|
||
"exactly one `icalendar-description'")
|
||
:node node))
|
||
node)
|
||
|
||
((equal "EMAIL" action-str)
|
||
(unless (= 1 (alist-get 'ical:summary child-counts 0))
|
||
(ical:signal-validation-error
|
||
(concat "EMAIL `icalendar-valarm' must have "
|
||
"exactly one `icalendar-summary'")
|
||
:node node))
|
||
(unless (= 1 (alist-get 'ical:description child-counts 0))
|
||
(ical:signal-validation-error
|
||
(concat "EMAIL `icalendar-valarm' must have "
|
||
"exactly one `icalendar-description'")
|
||
:node node))
|
||
(unless (<= 1 (alist-get 'ical:attendee child-counts 0))
|
||
(ical:signal-validation-error
|
||
(concat "EMAIL `icalendar-valarm' must have "
|
||
"at least one `icalendar-attendee'")
|
||
:node node))
|
||
node)
|
||
|
||
(t
|
||
;; "Applications MUST ignore alarms with x-name and iana-token
|
||
;; values they don't recognize." So this is not a validation-error:
|
||
(ical:warn
|
||
(format "Unknown ACTION value in VALARM: %s" action-str)
|
||
:buffer (ical:ast-node-meta-get node :buffer)
|
||
:position (ical:ast-node-meta-get node :value-begin))
|
||
node)))))
|
||
|
||
(ical:define-component ical:other-component nil
|
||
"Component type for unrecognized component names.
|
||
|
||
This component type corresponds to the IANA and X-name components
|
||
allowed by RFC5545 sec. 3.6; it represents components with an
|
||
unknown name (matching rx `icalendar-iana-token' or
|
||
`icalendar-x-name') which must be parsed and preserved but not
|
||
further interpreted."
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6")
|
||
|
||
;; Technically VCALENDAR is not a "component", but for the
|
||
;; purposes of parsing and syntax highlighting, it looks just like
|
||
;; one, so we define it as such here.
|
||
;; (If this becomes a problem, modify `ical:component-node-p'
|
||
;; to return nil for VCALENDAR components.)
|
||
(ical:define-component ical:vcalendar "VCALENDAR"
|
||
"Calendar Object.
|
||
|
||
This is the top-level data structure defined by RFC5545. A
|
||
VCALENDAR must contain the calendar properties `icalendar-prodid'
|
||
and `icalendar-version', and may contain the calendar properties
|
||
`icalendar-method' and `icalendar-calscale'.
|
||
|
||
It must also contain at least one VEVENT, VTODO, VJOURNAL,
|
||
VFREEBUSY, or other component, and for every unique
|
||
`icalendar-tzidparam' value appearing in a property within these
|
||
components, the calendar object must contain an
|
||
`icalendar-vtimezone' defining a time zone with that TZID."
|
||
:child-spec (:one (ical:prodid ical:version)
|
||
:zero-or-one (ical:calscale ical:method)
|
||
:zero-or-more (ical:other-property
|
||
ical:vevent
|
||
ical:vtodo
|
||
ical:vjournal
|
||
ical:vfreebusy
|
||
ical:vtimezone
|
||
ical:other-component))
|
||
:other-validator ical:vcalendar-validator
|
||
:link "https://www.rfc-editor.org/rfc/rfc5545#section-3.4")
|
||
|
||
(defun ical:all-tzidparams-in (node)
|
||
"Recursively find all `icalendar-tzidparam' values in NODE and its children."
|
||
(cond ((ical:tzid-param-p node)
|
||
(list (ical:ast-node-value node)))
|
||
((ical:param-node-p node)
|
||
nil)
|
||
(t ;; TODO: could prune search here when properties don't allow tzidparam
|
||
(seq-uniq (mapcan #'ical:all-tzidparams-in
|
||
(ical:ast-node-children node))))))
|
||
|
||
(defun ical:vcalendar-validator (node)
|
||
"Additional validator for `icalendar-vcalendar' NODE.
|
||
|
||
Checks that NODE has at least one component child and that all of the
|
||
`ical-tzidparam' values appearing in subcomponents have a corresponding
|
||
`icalendar-vtimezone' definition.
|
||
|
||
This function is called by `icalendar-ast-node-valid-p' for
|
||
VCALENDAR nodes; it is not normally necessary to call it directly."
|
||
(let* ((children (ical:ast-node-children node))
|
||
(comp-children (seq-filter #'ical:component-node-p children))
|
||
(tz-children (seq-filter #'ical:vtimezone-component-p children))
|
||
(defined-tzs
|
||
(mapcar
|
||
(lambda (tz)
|
||
;; ensure vtimezone component has a TZID property and
|
||
;; extract its string value:
|
||
(when (ical:ast-node-valid-p tz)
|
||
(ical:with-component tz ((ical:tzid :value-node tzid-text))
|
||
(ical:text-to-string tzid-text))))
|
||
tz-children))
|
||
(appearing-tzids (ical:all-tzidparams-in node)))
|
||
(unless comp-children
|
||
(ical:signal-validation-error
|
||
"`icalendar-vcalendar' must contain at least one component"
|
||
:node node))
|
||
|
||
(let ((seen nil))
|
||
(dolist (tzid appearing-tzids)
|
||
(unless (member tzid seen)
|
||
(unless (member tzid defined-tzs)
|
||
(ical:signal-validation-error
|
||
(format "No `icalendar-vtimezone' with TZID '%s' in calendar" tzid)
|
||
:node node)))
|
||
(push tzid seen)))
|
||
|
||
;; success:
|
||
node))
|
||
|
||
(defun ical:contains-vcalendar-p (&optional buffer)
|
||
"Determine whether BUFFER contains \"BEGIN:VCALENDAR\".
|
||
|
||
If so, then BUFFER is a candidate for parsing with, e.g.,
|
||
`icalendar-parse-calendar'. BUFFER defaults to the current
|
||
buffer. Returns the position where parsing should start, or nil."
|
||
(with-current-buffer (or buffer (current-buffer))
|
||
(save-excursion
|
||
(goto-char (point-min))
|
||
(when (re-search-forward "^BEGIN:VCALENDAR" nil t)
|
||
(beginning-of-line)
|
||
(point)))))
|
||
|
||
;; `icalendar-parse-component' is sufficient to parse all the syntax in
|
||
;; a calendar, but a calendar-level parsing function is needed to add
|
||
;; support for time zones. This function ensures that every
|
||
;; `icalendar-tzidparam' in the calendar has a corresponding
|
||
;; `icalendar-vtimezone' component, and modifies the zone information of
|
||
;; the parsed date-time according to the offset in that time zone.
|
||
(defun ical:parse-calendar (limit)
|
||
"Parse an `icalendar-vcalendar' object from point up to LIMIT.
|
||
Point should be at the start of the calendar object, i.e., at the start
|
||
of a line that looks like \"BEGIN:VCALENDAR\". After parsing, point is
|
||
at the beginning of the next line following the calendar (or end of the
|
||
buffer). Returns a syntax node representing the calendar."
|
||
(require 'icalendar-recur) ; for icr:tz-set-zones-in; avoids circular require
|
||
(declare-function icr:tz-set-zones-in "icalendar-recur")
|
||
(unless (looking-at-p "^BEGIN:VCALENDAR")
|
||
(ical:signal-parse-error "Not at start of VCALENDAR"))
|
||
(let ((cal-node (ical:parse-component limit)))
|
||
;(when (ical:ast-node-valid-p cal-node t)
|
||
(ical:with-component cal-node
|
||
((ical:vtimezone :all tzs))
|
||
;; After parsing the whole calendar, set the zone and dst slots
|
||
;; in all date-times which are relative to a time zone defined
|
||
;; in the calendar:
|
||
;; (TODO: if this proves too slow in general, we could instead
|
||
;; do it lazily when individual components are queried somehow.
|
||
;; But I'm not convinced that will actually save any time, because
|
||
;; if we're parsing, we're probably already in the middle of a
|
||
;; function that will immediately query all these times, e.g.
|
||
;; `diary-icalendar-import-buffer'.)
|
||
(dolist (comp (ical:ast-node-children cal-node))
|
||
(unless (ical:vtimezone-component-p comp)
|
||
(icr:tz-set-zones-in tzs comp))));)
|
||
cal-node))
|
||
|
||
;; TODO: should we do anything to *create* VTIMEZONE nodes in VCALENDAR
|
||
;; when they're required but don't exist?
|
||
(defun ical:print-calendar-node (vcalendar)
|
||
"Serialize an `icalendar-vcalendar' VCALENDAR to a string.
|
||
|
||
If VCALENDAR is not a valid `icalendar-vcalendar', an
|
||
`icalendar-validation-error' will be signaled. Any errors that arise
|
||
during printing will be logged in the buffer returned by
|
||
`icalendar-error-buffer'."
|
||
(when (ical:ast-node-valid-p vcalendar t)
|
||
(condition-case err
|
||
(ical:print-component-node vcalendar)
|
||
(ical:print-error
|
||
(ical:handle-print-error err)))))
|
||
|
||
|
||
;;; High-level parsing and printing functions.
|
||
(defun ical:parse (&optional buffer)
|
||
"Parse an `icalendar-vcalendar' object in BUFFER (default: current buffer).
|
||
|
||
An unfolded copy of BUFFER (see `icalendar-unfolded-buffer-from-buffer')
|
||
will first be obtained if necessary. Parsing will begin at the first
|
||
occurrence of \"BEGIN:VCALENDAR\" in the unfolded buffer.
|
||
|
||
The buffer may be tidied up by user functions before parsing begins; see
|
||
`icalendar-pre-unfolding-hook' and `icalendar-pre-parsing-hook'.
|
||
|
||
If parsing is successful, the VCALENDAR object is returned. Otherwise,
|
||
nil is returned, a warning is issued, and errors are logged in the
|
||
buffer returned by `icalendar-error-buffer'."
|
||
(let* ((buf (or buffer (current-buffer)))
|
||
(unfolded (cond ((ical:unfolded-p buf) buf)
|
||
((buffer-file-name buf)
|
||
(ical:unfolded-buffer-from-file (buffer-file-name buf)))
|
||
(t (ical:unfolded-buffer-from-buffer buf)))))
|
||
(ical:init-error-buffer)
|
||
(with-current-buffer unfolded
|
||
(run-hooks 'ical:pre-parsing-hook)
|
||
(let ((cal-start (ical:contains-vcalendar-p))
|
||
vcalendar)
|
||
(unless cal-start
|
||
(ical:signal-parse-error "Buffer does not contain \"BEGIN:VCALENDAR\""))
|
||
(save-excursion
|
||
(goto-char cal-start)
|
||
(ical:condition-case err
|
||
(setq vcalendar (ical:parse-calendar (point-max)))
|
||
(ical:parse-error
|
||
(ical:handle-parse-error err)
|
||
(warn "Errors while parsing %s; see buffer %s"
|
||
buffer (buffer-name (ical:error-buffer))))))
|
||
vcalendar))))
|
||
|
||
;; TODO: The function `ical:print' below is not really useful yet.
|
||
;; Feels like it's needed for completeness but interface needs more thought.
|
||
;; Should this instead be a generic function that prints any
|
||
;; kind of node at point? at a given marker?
|
||
;; What about the coding system? If we want to use this function to print
|
||
;; iCalendar data to stdout, need to set up coding system correctly and
|
||
;; perform line folding.
|
||
;; Etc.
|
||
;;
|
||
;; (defun ical:print (vcalendar &optional buffer pos)
|
||
;; "Insert VCALENDAR as a string at position POS in BUFFER.
|
||
;;
|
||
;; VCALENDAR should be an `icalendar-vcalendar'. BUFFER defaults to the
|
||
;; current buffer and POS defaults to point.
|
||
;;
|
||
;; If printing is successful, VCALENDAR is returned. Otherwise, nil is
|
||
;; returned, a warning is issued, and errors are logged in the buffer
|
||
;; returned by `icalendar-error-buffer'."
|
||
;; (with-current-buffer (or buffer (current-buffer))
|
||
;; (when pos (goto-char pos))
|
||
;; (condition-case err
|
||
;; (insert (ical:print-calendar-node vcalendar))
|
||
;; (ical:print-error
|
||
;; (ical:handle-print-error err)
|
||
;; (setq vcalendar nil) ; return
|
||
;; (warn "Errors while printing; see buffer %s"
|
||
;; (buffer-name (ical:error-buffer)))))
|
||
;; vcalendar))
|
||
|
||
|
||
;;; Pre-parsing cleanup
|
||
;;
|
||
;; The following functions are based on observed syntax errors in
|
||
;; real-world data and can help clean up such data before parsing.
|
||
;; More functions can be added here based on user feedback.
|
||
(defcustom ical:pre-parsing-hook nil
|
||
"Hook run by `icalendar-parse' before parsing iCalendar data.
|
||
|
||
If you routinely receive iCalendar data in an incorrect format, you can
|
||
add functions to this hook which clean up that data before parsing is
|
||
attempted. The functions in this hook will be run after the iCalendar
|
||
data has been \"unfolded\" but before parsing begins. (If you need to
|
||
clean up data before unfolding happens, see
|
||
`icalendar-pre-unfolding-hook'.)
|
||
|
||
Each function should accept zero arguments and should perform its
|
||
operation on the entire current buffer."
|
||
:version "31.1"
|
||
:type '(hook)
|
||
:options '(ical:fix-blank-lines
|
||
ical:fix-hyphenated-dates
|
||
ical:fix-missing-mailtos))
|
||
|
||
(defun ical:fix-blank-lines ()
|
||
"Remove blank lines.
|
||
This function is intended to be used from `icalendar-pre-parsing-hook',
|
||
which see."
|
||
(goto-char (point-min))
|
||
(while (re-search-forward (rx "\n" (zero-or-more space) line-end)
|
||
nil t)
|
||
(replace-match "" nil nil)))
|
||
|
||
(defun ical:fix-hyphenated-dates ()
|
||
"Correct dates in \"YYYY-MM-DD...\" format to \"YYYYMMDD...\" format.
|
||
This function is intended to be used from `icalendar-pre-parsing-hook',
|
||
which see."
|
||
(goto-char (point-min))
|
||
(while (re-search-forward
|
||
(rx line-start
|
||
(or "COMPLETED" "DTEND" "DUE" "DTSTART" "RECURRENCE-ID"
|
||
"EXDATE" "RDATE" "CREATED" "DTSTAMP" "LAST-MODIFIED")
|
||
(zero-or-more ical:other-param-safe)
|
||
":")
|
||
nil t)
|
||
(unless (looking-at-p (rx (or ical:date ical:date-time)))
|
||
(while (re-search-forward ; exdate, rdate allow lists
|
||
(rx (group-n 1 (= 4 digit))
|
||
"-"
|
||
(group-n 2 (= 2 digit))
|
||
"-"
|
||
(group-n 3 (= 2 digit)))
|
||
(line-end-position) t)
|
||
(replace-match "\\1\\2\\3" nil nil)))))
|
||
|
||
(defun ical:fix-missing-mailtos ()
|
||
"Insert \"mailto:\" when it is missing before email addresses.
|
||
This function is intended to be used from `icalendar-pre-parsing-hook',
|
||
which see."
|
||
;; fix property values in properties that require an address:
|
||
(goto-char (point-min))
|
||
(while (re-search-forward
|
||
(rx line-start (or "ORGANIZER" "ATTENDEE")
|
||
(zero-or-more ical:other-param-safe) ":")
|
||
nil t)
|
||
(unless (looking-at-p (rx ical:cal-address))
|
||
(when (looking-at
|
||
(rx
|
||
;; match local part of mail address: all the characters
|
||
;; allowed after a URI scheme, *except*
|
||
;; ?@ (so we can match that after) and
|
||
;; ?: (in case we're looking at a non-"mailto:" scheme)
|
||
(group-n 1
|
||
(one-or-more
|
||
(any "A-Za-z0-9" ?- ?. ?_ ?~ ?/ ?? ?# ?\[ ?\] ?! ?$ ?& ?'
|
||
?\( ?\) ?* ?+ ?, ?\; ?= ?%)))
|
||
"@"))
|
||
(when (or (< (length (match-string 0)) 7)
|
||
(not (equal "mailto:"
|
||
(substring (downcase (match-string 0)) 0 7))))
|
||
(replace-match "mailto:\\1" nil nil nil 1)))))
|
||
|
||
;; fix parameter values in parameters that require an address:
|
||
(goto-char (point-min))
|
||
(while (re-search-forward
|
||
(rx line-start ical:name
|
||
(zero-or-more icalendar-other-param-safe)
|
||
";"
|
||
(or "DELEGATED-FROM" "DELEGATED-TO" "MEMBER" "SENT-BY")
|
||
"=")
|
||
nil t)
|
||
(unless (looking-at-p (rx ical:cal-address))
|
||
(while ; DELEGATED* params accept lists
|
||
(looking-at
|
||
(rx
|
||
?\" ; values of these params must always be quoted
|
||
(group-n 1 ; matches local part of mail address as above
|
||
(one-or-more
|
||
(any "A-Za-z0-9" ?- ?. ?_ ?~ ?/ ?? ?# ?\[ ?\] ?! ?$ ?& ?'
|
||
?\( ?\) ?* ?+ ?, ?= ?%)))
|
||
"@"
|
||
(zero-or-more (not ?\"))
|
||
?\"
|
||
(zero-or-one ",")))
|
||
(when (or (< (length (match-string 1)) 7)
|
||
(not (equal "mailto:"
|
||
(substring (downcase (match-string 1)) 0 7))))
|
||
(replace-match "mailto:\\1" nil nil nil 1))
|
||
(goto-char (match-end 0))))))
|
||
|
||
|
||
;;; Caching and indexing parse trees
|
||
;;
|
||
;; The following functions provide a simple in-memory cache and index
|
||
;; for faster access to parsed iCalendar data by date, UID, and other
|
||
;; fields of interest. The index and parse tree are stored in a
|
||
;; buffer-local variable of the parsed buffer and not recomputed if the
|
||
;; buffer hasn't changed. Most users of the library should just call
|
||
;; `icalendar-parse-and-index' to get both the parse tree and a
|
||
;; reference to the index, and get objects of interest from them
|
||
;; with `icalendar-index-get'.
|
||
(defun ical:make-index ()
|
||
"Create an empty index of iCalendar components."
|
||
(list :bydate (make-hash-table :test #'equal) ;; date => list of components
|
||
:byuid (make-hash-table :test #'equal) ;; UID => component
|
||
:bytzid (make-hash-table :test #'equal) ;; tzid => vtimezone
|
||
:recurring (list))) ;; list of components
|
||
|
||
(defun ical:index-insert-tz (index vtimezone)
|
||
"Insert VTIMEZONE into INDEX."
|
||
(ical:with-component vtimezone
|
||
((ical:tzid :value tzid))
|
||
(let ((tzid-index (plist-get index :bytzid)))
|
||
(puthash tzid vtimezone tzid-index)
|
||
;; Update and return the index:
|
||
(plist-put index :bytzid tzid-index))))
|
||
|
||
|
||
(defun ical:index-insert (index component)
|
||
"Insert COMPONENT into INDEX."
|
||
(require 'icalendar-recur) ; avoid circular imports
|
||
(require 'icalendar-utils) ;
|
||
(declare-function icr:recurrences-to-count "icalendar-recur")
|
||
(declare-function ical:date/time-to-local "icalendar-utils")
|
||
(declare-function ical:date/time-to-date "icalendar-utils")
|
||
(declare-function ical:dates-until "icalendar-utils")
|
||
|
||
(ical:with-component component
|
||
((ical:dtstart :first dtstart-node :value dtstart)
|
||
(ical:dtend :first dtend-node :value dtend)
|
||
(ical:due :value due)
|
||
(ical:duration :value duration)
|
||
(ical:rrule :value recur-value)
|
||
(ical:rdate :all rdate-nodes)
|
||
(ical:exdate :all exdate-nodes)
|
||
(ical:uid :value uid))
|
||
(let ((date-index (plist-get index :bydate))
|
||
(uid-index (plist-get index :byuid))
|
||
(tzid-index (plist-get index :bytzid))
|
||
(recurring (plist-get index :recurring))
|
||
(rdates
|
||
(mapcar #'ical:ast-node-value
|
||
(apply #'append (mapcar #'ical:ast-node-value rdate-nodes))))
|
||
(exdates
|
||
(mapcar #'ical:ast-node-value
|
||
(apply #'append (mapcar #'ical:ast-node-value exdate-nodes))))
|
||
dates)
|
||
;; Everything with a UID goes into the uid-index:
|
||
(when uid
|
||
(puthash uid component uid-index))
|
||
;; For all top-level components, we gather a list of dates on which
|
||
;; they recur for date-index, or put them in the recurring list:
|
||
(when dtstart
|
||
(cond
|
||
;; If the component has an RRULE that specifies a fixed number
|
||
;; of recurrences, compute them now and index them for each date
|
||
;; in each recurrence:
|
||
((and recur-value (ical:recur-count recur-value))
|
||
(let* ((tz (gethash (ical:with-param-of dtstart-node 'ical:tzidparam)
|
||
tzid-index))
|
||
(recs (cons dtstart (icr:recurrences-to-count component tz))))
|
||
(dolist (rec recs)
|
||
(let ((end-time
|
||
(when duration (ical:date/time-add-duration rec duration))))
|
||
(setq dates
|
||
(append dates
|
||
(if end-time (ical:dates-until rec end-time t)
|
||
(list (ical:date/time-to-date
|
||
(ical:date/time-to-local rec))))))))))
|
||
;; Same with RDATEs when there's no RRULE:
|
||
((and rdates (not recur-value))
|
||
(dolist (rec (cons dtstart rdates))
|
||
(unless (or (cl-typep rec 'ical:period) (member rec exdates))
|
||
(let ((end-time
|
||
(when duration
|
||
(ical:date/time-add-duration rec duration))))
|
||
(setq dates
|
||
(append dates
|
||
(if end-time (ical:dates-until rec end-time t)
|
||
(list (ical:date/time-to-date
|
||
(ical:date/time-to-local rec))))))))
|
||
(when (cl-typep rec 'ical:period)
|
||
(let* ((start (ical:period-start rec))
|
||
(end (or (ical:period-end rec)
|
||
(ical:date/time-add-duration
|
||
start (ical:period-dur-value rec)))))
|
||
(setq dates (append dates (ical:dates-until start end t)))))))
|
||
;; A non-recurring event also gets an index entry for each date
|
||
;; until its end time:
|
||
((not recur-value)
|
||
(let ((end-time
|
||
(or dtend due
|
||
(when duration
|
||
(ical:date/time-add-duration dtstart duration)))))
|
||
(setq dates (if end-time (ical:dates-until dtstart end-time t)
|
||
(list
|
||
(ical:date/time-to-date
|
||
(ical:date/time-to-local dtstart)))))))
|
||
;; Otherwise, we put off the computation of recurrences until queried:
|
||
(t (push component recurring)))
|
||
|
||
(dolist (date (seq-uniq dates))
|
||
(let ((others (gethash date date-index)))
|
||
;; TODO: wonder if we should normalize, and instead store UIDs
|
||
;; in the date index, then look them up by UID when queried.
|
||
(puthash date (cons component others) date-index))))
|
||
|
||
;; Return the updated index:
|
||
(setq index (plist-put index :byuid uid-index))
|
||
(setq index (plist-put index :bytzid tzid-index))
|
||
(setq index (plist-put index :bydate date-index))
|
||
(setq index (plist-put index :recurring recurring))
|
||
index)))
|
||
|
||
(defun ical:index-populate-from-calendar (index vcalendar)
|
||
"Insert all components in VCALENDAR into INDEX."
|
||
(let* ((tzs (ical:ast-node-children-of 'ical:vtimezone vcalendar))
|
||
(vevents (ical:ast-node-children-of 'ical:vevent vcalendar))
|
||
(vjournals (ical:ast-node-children-of 'ical:vjournal vcalendar))
|
||
(vtodos (ical:ast-node-children-of 'ical:vtodo vcalendar))
|
||
;; TODO: customizable selection? what about valarms?
|
||
(to-index (append vevents vjournals vtodos)))
|
||
|
||
;; First insert the tzs, so that they're available when inserting
|
||
;; the others by date:
|
||
(dolist (tz tzs)
|
||
(setq index (ical:index-insert-tz index tz)))
|
||
|
||
(dolist (component to-index)
|
||
(setq index (ical:index-insert index component)))
|
||
index))
|
||
|
||
(cl-defun ical:index-get (index &rest args &key date uid tzid)
|
||
"Get an iCalendar component from INDEX by date, UID, or TZID.
|
||
|
||
INDEX should be a reference to a parse tree index as returned by
|
||
`icalendar-parse-and-index', which see. The index can be queried by:
|
||
|
||
:uid UID (string, see `icalendar-uid') - returns the component with that
|
||
UID.
|
||
|
||
:tzid TZID (string, see `icalendar-tzid' and `icalendar-tzidparam') -
|
||
returns the `icalendar-vtimezone' component with that TZID.
|
||
|
||
:date DT (an `icalendar-date', i.e. a list (M D Y)) - returns a list of
|
||
the components occurring (or recurring) on that date.
|
||
|
||
Only one keyword argument can be queried at a time."
|
||
(require 'icalendar-recur) ; avoid circular imports
|
||
(require 'icalendar-utils) ;
|
||
|
||
(declare-function icr:find-interval "icalendar-recur")
|
||
(declare-function icr:recurrences-in-interval "icalendar-recur")
|
||
(declare-function ical:date/time-in-period-p "icalendar-utils")
|
||
(declare-function ical:date/time<= "icalendar-utils")
|
||
(declare-function ical:date/time< "icalendar-utils")
|
||
(declare-function ical:date/time-add-duration "icalendar-utils")
|
||
|
||
(when (length> args 2)
|
||
(error "Only one keyword argument can be queried"))
|
||
(cond (uid (gethash uid (plist-get index :byuid)))
|
||
(tzid (gethash tzid (plist-get index :bytzid)))
|
||
(date
|
||
(let ((computed (gethash date (plist-get index :bydate)))
|
||
(recurring (plist-get index :recurring)))
|
||
(dolist (component recurring)
|
||
(ical:with-component component
|
||
((ical:dtstart :first dtstart-node :value dtstart)
|
||
(ical:rrule :value recur-value)
|
||
(ical:rdate :all rdate-nodes)
|
||
(ical:duration :value duration))
|
||
(unless (ical:date/time<= date dtstart)
|
||
(let* ((tz (ical:with-param-of dtstart-node 'ical:tzidparam nil
|
||
(gethash value (plist-get index :bytzid))))
|
||
(int (icr:find-interval date dtstart recur-value tz))
|
||
(recs (icr:recurrences-in-interval int component tz)))
|
||
(catch 'found
|
||
(dolist (rec recs)
|
||
(let* ((local-rec (ical:date/time-to-local rec))
|
||
(end
|
||
(when duration
|
||
(ical:date/time-add-duration local-rec duration)))
|
||
(rec-dates
|
||
(if end (ical:dates-until local-rec end t)
|
||
(list (ical:date/time-to-date local-rec)))))
|
||
(when (member date rec-dates)
|
||
(push component computed)
|
||
(throw 'found nil))))
|
||
(dolist (node rdate-nodes)
|
||
;; normal RDATE recurrences have already been
|
||
;; checked above, but we check whether `date'
|
||
;; occurs in any RDATE period values here:
|
||
(when (eq 'ical:period
|
||
(ical:value-type-from-params
|
||
(ical:ast-node-children node)))
|
||
(let* ((tz
|
||
(ical:with-param-of node 'ical:tzidparam nil
|
||
(gethash value (plist-get index :bytzid)))))
|
||
(ical:with-property node nil
|
||
(dolist (period values)
|
||
(when (ical:date/time-in-period-p date period tz)
|
||
(push component computed)
|
||
(throw 'found nil))))))))))))
|
||
computed))
|
||
(t (error "At least one of :uid, :tzid, or :date is required"))))
|
||
|
||
;; Buffer local variable to cache the index and parse tree.
|
||
;; Format: (TICKS VCALENDAR INDEX)
|
||
;; TICKS is the value of (buffer-modified-tick) at last parse
|
||
(defvar-local ical:-parsed-calendar-and-index '(0 nil nil))
|
||
|
||
(defun ical:parse-and-index (&optional buffer-or-file)
|
||
"Parse and index the first iCalendar VCALENDAR object in BUFFER-OR-FILE.
|
||
|
||
Returns a list (VCALENDAR INDEX), where VCALENDAR is the parsed
|
||
`icalendar-vcalendar' syntax tree. The index can then be queried to
|
||
retrieve components from this calendar by UID, TZID, or date; see
|
||
`icalendar-index-get'.
|
||
|
||
BUFFER-OR-FILE may be a buffer or a string containing a filename; it
|
||
defaults to the current buffer. If it is a filename, an unfolded buffer
|
||
containing its data will be found, or created if necessary (see
|
||
`icalendar-unfolded-buffer-from-file'). The resulting buffer must
|
||
contain an iCalendar VCALENDAR object, which will be parsed and indexed.
|
||
|
||
The results of parsing and indexing are cached in buffer-local
|
||
variables, and subsequent calls with the same BUFFER-OR-FILE will return
|
||
the cached results as long as the buffer has not been modified in the
|
||
meantime."
|
||
(let* ((buffer (cond ((null buffer-or-file) (current-buffer))
|
||
((bufferp buffer-or-file) buffer-or-file)
|
||
((and (stringp buffer-or-file)
|
||
(file-exists-p buffer-or-file))
|
||
(find-buffer-visiting buffer-or-file))))
|
||
(file-name (cond (buffer (buffer-file-name buffer))
|
||
((and (stringp buffer-or-file)
|
||
(file-exists-p buffer-or-file))
|
||
(expand-file-name buffer-or-file))))
|
||
(unfolded (cond ((and buffer (ical:unfolded-p buffer))
|
||
buffer)
|
||
(file-name
|
||
(or (ical:find-unfolded-buffer-visiting file-name)
|
||
(ical:unfolded-buffer-from-file file-name)))
|
||
(buffer
|
||
(ical:unfolded-buffer-from-buffer buffer))
|
||
(t
|
||
(error "Unable to get unfolded buffer for '%s'"
|
||
buffer-or-file)))))
|
||
(with-current-buffer unfolded
|
||
(when (ical:contains-vcalendar-p)
|
||
(if (eql (car ical:-parsed-calendar-and-index) (buffer-modified-tick))
|
||
(cdr ical:-parsed-calendar-and-index)
|
||
(message "Parsing and indexing iCalendar data in %s..." (buffer-name))
|
||
(let ((vcalendar (ical:parse)))
|
||
(when vcalendar
|
||
(setq ical:-parsed-calendar-and-index
|
||
(list
|
||
(buffer-modified-tick)
|
||
vcalendar
|
||
(ical:index-populate-from-calendar (ical:make-index)
|
||
vcalendar)))
|
||
(message "Parsing and indexing iCalendar data in %s...Done."
|
||
(buffer-name))
|
||
(cdr ical:-parsed-calendar-and-index))))))))
|
||
|
||
|
||
|
||
;;; Documentation for all of the above via `describe-symbol':
|
||
(defun ical:documented-symbol-p (sym)
|
||
"Return non-nil if SYM is a symbol with iCalendar documentation."
|
||
(or (get sym 'icalendar-type-documentation)
|
||
;; grammatical categories defined with rx-define, but with no
|
||
;; other special icalendar docs:
|
||
(and (get sym 'rx-definition)
|
||
(length> (symbol-name sym) 10)
|
||
(equal "icalendar-" (substring (symbol-name sym) 0 10)))))
|
||
|
||
(defun ical:documentation (sym buf frame)
|
||
"iCalendar documentation backend for `describe-symbol-backends'."
|
||
(ignore buf frame) ; Silence the byte compiler
|
||
(with-help-window (help-buffer)
|
||
(with-current-buffer standard-output
|
||
(let* ((type-doc (get sym 'icalendar-type-documentation))
|
||
(link (get sym 'icalendar-link))
|
||
(rx-def (get sym 'rx-definition))
|
||
(rx-doc (when rx-def
|
||
(with-output-to-string
|
||
(pp rx-def))))
|
||
(value-rx-def (get sym 'ical:value-rx))
|
||
(value-rx-doc (when value-rx-def
|
||
(with-output-to-string
|
||
(pp value-rx-def))))
|
||
(values-rx-def (get sym 'ical:values-rx))
|
||
(values-rx-doc (when values-rx-def
|
||
(with-output-to-string
|
||
(pp values-rx-def))))
|
||
|
||
(full-doc
|
||
(concat
|
||
(when type-doc
|
||
(format "`%s' is an iCalendar type:\n\n%s\n\n"
|
||
sym type-doc))
|
||
(when link
|
||
(format "For further information see\nURL `%s'\n\n" link))
|
||
;; FIXME: this is probably better done in rx.el!
|
||
;; TODO: could also generalize this to recursively
|
||
;; search rx-def for any symbol that starts with "icalendar-"...
|
||
(when rx-def
|
||
(format "`%s' is an iCalendar grammar category.
|
||
Its `rx' definition is:\n\n%s%s%s"
|
||
sym
|
||
rx-doc
|
||
(if value-rx-def
|
||
(format "\nIndividual values must match:\n%s"
|
||
value-rx-doc)
|
||
"")
|
||
(if values-rx-def
|
||
(format "\nLists of values must match:\n%s"
|
||
values-rx-doc)
|
||
"")))
|
||
"\n")))
|
||
|
||
(insert full-doc)
|
||
full-doc))))
|
||
|
||
|
||
(defconst ical:describe-symbol-backend
|
||
'(nil icalendar-documented-symbol-p icalendar-documentation)
|
||
"Entry for icalendar documentation in `describe-symbol-backends'.")
|
||
|
||
(push ical:describe-symbol-backend describe-symbol-backends)
|
||
|
||
;; Unloading:
|
||
(defun ical:parser-unload-function ()
|
||
"Unload function for `icalendar-parser'."
|
||
(mapatoms
|
||
(lambda (sym)
|
||
(when (string-match "^icalendar-" (symbol-name sym))
|
||
(makunbound sym)
|
||
(fmakunbound sym))))
|
||
|
||
(setq describe-symbol-backends
|
||
(remq ical:describe-symbol-backend describe-symbol-backends))
|
||
;; Proceed with normal unloading:
|
||
nil)
|
||
|
||
(provide 'icalendar-parser)
|
||
|
||
;; Local Variables:
|
||
;; read-symbol-shorthands: (("ical:" . "icalendar-") ("icr:" . "icalendar-recur-"))
|
||
;; End:
|
||
;;; icalendar-parser.el ends here
|