Subject: [PATCH 1/2] New parser for iCalendar (RFC5545)

From: Richard Lawrence <[email-masked]>
Date: Fri, 20 Dec 2024 20:47:48 +0100

Tags: patch

Here is a draft patch implementing a new parser for iCalendar data. This
code implements the grammar of RFC5545, functions to parse this grammar
to an abstract syntax tree, functions to validate syntax trees,
functions to print syntax trees, and a test suite for the parser and
printer functions containing all the examples from RFC5545.  The code is
organized as follows:

lisp/calendar/icalendar-ast.el: defines the abstract syntax tree,
  including the validation functions
lisp/calendar/icalendar-macs.el: defines the icalendar-define-param,
  icalendar-define-property, and icalendar-define-component macros
lisp/calendar/icalendar-parser.el: defines the parsing and printing
  functions, and all of the individual parameters, properties, and
  components defined in the RFC.
test/lisp/calendar/icalendar-parser-tests.el: the test suite.
  All the tests pass on my machine with Emacs 29.1 and with Emacs master.

Looking forward to your feedback! This is a (very?) large patch, so
please let me know if it would be better to submit it another way.

Thanks,
Richard

>From cba266129de7575fc2272348e87695bb5f9cf6df Mon Sep 17 00:00:00 2001
From: Richard Lawrence <[email-masked]>
Date: Thu, 19 Dec 2024 14:30:57 +0100
Subject: [PATCH 2/3] New parser for RFC5545

Import parser code and tests from external repository
Split imported code into icalendar-ast.el, icalendar-macs.el,
  icalendar-parser.el
Make type metadata available at compile time
Fix all compilation warnings

All parser tests pass, also when compiled and run by `make check'
---
 lisp/calendar/icalendar-ast.el               |  536 +++
 lisp/calendar/icalendar-macs.el              |  809 ++++
 lisp/calendar/icalendar-parser.el            | 4090 ++++++++++++++++++
 lisp/calendar/icalendar-uri-schemes.el       |  444 ++
 test/lisp/calendar/icalendar-parser-tests.el | 1796 ++++++++
 5 files changed, 7675 insertions(+)
 create mode 100644 lisp/calendar/icalendar-ast.el
 create mode 100644 lisp/calendar/icalendar-macs.el
 create mode 100644 lisp/calendar/icalendar-parser.el
 create mode 100644 lisp/calendar/icalendar-uri-schemes.el
 create mode 100644 test/lisp/calendar/icalendar-parser-tests.el

diff --git a/lisp/calendar/icalendar-ast.el b/lisp/calendar/icalendar-ast.el
new file mode 100644
index 00000000000..19411767fbc
--- /dev/null
+++ b/lisp/calendar/icalendar-ast.el
@@ -0,0 +1,536 @@
+;;; icalendar-ast.el --- Syntax trees for iCalendar  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: Richard Lawrence <[email-masked]>
+;; Created: October 2024
+;; Keywords: calendar
+;; Human-Keywords: calendar, iCalendar
+
+;; This file is part of GNU Emacs.
+
+;; This file is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this file.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file defines the abstract syntax tree representation for
+;; iCalendar data.
+
+
+;;; Code:
+(require 'cl-lib)
+
+;;; Type symbols and metadata
+
+;; All nodes in the syntax treee have a type symbol as their first element.
+;; We use the following symbol properties (all prefixed with 'icalendar-')
+;; to associate type symbols with various important data about the type:
+;;
+;; is-type - t (marks this symbol as an icalendar type)
+;; is-value, is-param, is-property, or is-component - t
+;;   (specifies what sort of value this type represents)
+;; list-sep - for property and parameters types, a string (typically
+;;   "," or ";") which separates individual printed values, if the
+;;   type allows lists of values. If this is non-nil, syntax nodes of
+;;   this type should always have a list of values in their VALUE
+;;   field (even if there is only one value)
+;; matcher - a function to match this type. This function matches the
+;;   regular expression defined under the type's name; it is used to provide
+;;   syntax highlighting in `icalendar-mode'
+;; begin-rx, end-rx - for component-types, an `rx' regular expression which
+;;   matches the BEGIN and END lines that form its boundaries
+;; value-rx - an `rx' regular expression which matches individual values
+;;   of this type, with no consideration for quoting or lists of values.
+;;   (For value types, this is just a synonym for the rx definition
+;;   under the type's symbol)
+;; values-rx - for types that accept lists of values, an `rx' regular
+;;   expression which matches the whole list (including quotes, if required)
+;; full-value-rx - for property and parameter types, an `rx' regular
+;;   expression which matches a valid value expression in group 2, or
+;;   an invalid value in group 3
+;; value-reader - for value types, a function which creates syntax
+;;   nodes of this type given a string representing their value
+;; value-printer - for value types, a function to print individual
+;;   values of this type. It accepts a value and returns its string
+;;   representation.
+;; default-value - for property and parameter types, a string
+;;   representing a default value for nodes of this type. This is the
+;;   value assumed when no node of this type is present in the
+;;   relevant part of the syntax tree.
+;; substitute-value - for parameter types, a string representing a value
+;;   which will be substituted at parse times for unrecognized values.
+;;   (This is normally the same as default-value, but differs from it
+;;   in at least one case in RFC5545, thus it is stored separately.)
+;; default-type - for property types which can have values of multiple
+;;   types, this is the default type when no type for the value is
+;;   specified in the parameters. Any type of value other than this
+;;   one requires a VALUE=... parameter when the property is read or printed.
+;; other-types - for property types which can have values of multiple types,
+;;   this is a list of other types that the property can accept.
+;; child-spec - for property and component types, a plist describing the
+;;   required and optional child nodes. See `icalendar-define-property' and
+;;   `icalendar-define-component' for details.
+;; other-validator - a function to perform type-specific validation
+;;   for nodes of this type. If present, this function will be called
+;;   by `icalendar-ast-node-valid-p' during validation.
+;; type-documentation - a string documenting the type. This documentation is
+;;   printed in the help buffer when `describe-symbol' is called on TYPE.
+;; link - a hyperlink to the documentation of the type in the relevant standard
+
+(defun ical:type-symbol-p (symbol)
+  "Return non-nil if SYMBOL is an iCalendar type symbol.
+
+This function only checks that SYMBOL has been marked as a type;
+it returns t for value types defined by `icalendar-define-type',
+but also e.g. for types defined by `icalendar-define-param' and
+`icalendar-define-property'. To check that SYMBOL names a value
+type for property or parameter values, see
+`icalendar-value-type-symbol-p' and
+`icalendar-printable-value-type-symbol-p'."
+  (and (symbolp symbol)
+       (get symbol 'ical:is-type)))
+
+(defun ical:value-type-symbol-p (symbol)
+  "Return non-nil if SYMBOL is a type symbol representing a value
+type, i.e., a type for an iCalendar property or parameter value
+defined by `icalendar-define-type'.
+
+This means that SYMBOL must both satisfy
+`icalendar-type-symbol-p' and have the property
+`icalendar-is-value'. It does not require the type to be
+associated with a print name in `icalendar-value-types';
+for that see `icalendar-printable-value-type-symbol-p'."
+  (and (ical:type-symbol-p symbol)
+       (get symbol 'ical:is-value)))
+
+(defun ical:expects-list-of-values-p (type)
+  "Return non-nil if the syntax node type named by TYPE accepts a
+list of values. This is never t for value types or component
+types. For property and parameter types defined with
+`ical:define-param' and `ical:define-property', it is true if the
+:list-sep argument was specified in the definition."
+  (and (ical:type-symbol-p type)
+       (get type 'ical:list-sep)))
+
+(defun ical:param-type-symbol-p (type)
+  "Return non-nil if TYPE is a type symbol for an iCalendar
+parameter."
+  (and (ical:type-symbol-p type)
+       (get type 'ical:is-param)))
+
+(defun ical:property-type-symbol-p (type)
+  "Return non-nil if TYPE is a type symbol for an iCalendar
+property."
+  (and (ical:type-symbol-p type)
+       (get type 'ical:is-property)))
+
+(defun ical:component-type-symbol-p (type)
+  "Return non-nil if TYPE is a type symbol for an iCalendar
+component."
+  (and (ical:type-symbol-p type)
+       (get type 'ical:is-component)))
+
+;; TODO: we could define other accessors here for the other metadata
+;; properties, but at the moment I see no advantage to this; they would
+;; all just be long-winded wrappers around `get'.
+
+
+;;; AST metadata from parser.
+
+;; This is intended to serve the same role as the
+;; `:standard-properties' array in `org-element-ast', though that name
+;; would be confusing in the context of RFC5545.
+(cl-defstruct (ical:meta (:constructor ical:-make-meta))
+  "Structure containing meta information in an iCalendar syntax
+node. Do not rely on this representation; it may change."
+  (buffer nil
+    :type (or null buffer)
+    :documentation "The buffer from which this node was parsed")
+  (parent nil
+    :type ical:ast-node-p
+    :documentation "The parent node to which this node belongs")
+  (begin nil
+    :type (or null integer-or-marker)
+    :documentation "The position at which the content of this node begins")
+  (end nil
+    :type (or null integer-or-marker)
+    :documentation "The position at which the content of this node ends")
+  (value-begin nil
+    :type (or null integer-or-marker)
+    :documentation "The position at which the value of this node begins")
+  (value-end nil
+    :type (or null integer-or-marker)
+    :documentation "The position at which the value of this node ends")
+  (original-value nil
+    :type (or null string)
+    :documentation "The original representation of the value as parsed.
+This can differ from the value stored in the node if e.g. the
+standard requires an unrecognized value to be treated the same as
+a certain default")
+  (original-name nil
+    :type (or null string)
+    :documentation
+    "The original representation of the parameter, property, or component
+name as parsed. This can differ from the name corresponding to the node's type
+if e.g. the standard requires parsing a node of an unrecognized type"))
+
+
+;;; AST representation
+
+;; Every syntax node has the format (TYPE META VALUE CHILDREN) where:
+;;
+;; TYPE is a type symbol (typically defined with ical:define-type,
+;; ical:define-param, ical:define-property, or ical:define-component;
+;; see Type Metadata, above)
+;;
+;; META is a struct containing parsing metadata about the node (see
+;; `ical:meta' above)
+;;
+;; VALUE is the node's value, if any.
+;; Depending on TYPE, VALUE can be:
+;; - nil (e.g. component nodes have no value)
+;; - an Elisp data structure representing one of the basic iCalendar
+;;   value types (e.g. a date, a period, or text)
+;; - a syntax node
+;; - a list of one of the above. This is the case if `ical:values-list-p'
+;;   returns t for TYPE.
+;;
+;; CHILDREN is a list of syntax nodes. For component nodes, a list of
+;; property nodes. For property nodes, a list containing parameter
+;; nodes. nil for all other nodes.
+;;
+;; We define general accessors and a constructor `ical:make-ast-node'
+;; for this representation here:
+(defsubst ical:ast-node-type (node)
+  "Return the symbol naming the type of iCalendar syntax node NODE."
+  (car node))
+
+(defsubst ical:ast-node-value (node)
+  "Return the value of iCalendar syntax node NODE.
+In component nodes, this is nil. Otherwise, it is a syntax node
+representing an iCalendar (property or parameter) value."
+  (nth 2 node))
+
+(defsubst ical:ast-node-children (node)
+  "Return the children of iCalendar syntax node NODE.
+In component nodes, this is a list of property nodes and/or
+subcomponent nodes. In property nodes, this is a list of
+parameter nodes. Otherwise the list is nil."
+  (nth 3 node))
+
+(defun ical:ast-node-p (val)
+  "Return non-nil if VAL is an iCalendar syntax node"
+  (and (listp val)
+       (length= val 4)
+       (ical:type-symbol-p (ical:ast-node-type val))))
+
+(defun ical:-keyword-to-slot-name (kw)
+  "Convert a keyword like :slotname to plain symbol \\='slotname"
+  (intern (string-trim (downcase (symbol-name kw)) ":")))
+
+(defun ical:ast-node-meta-get (node keyword)
+  "Get metadata key KEYWORD from NODE. The possible KEYWORDs are the
+slot names of `ical:meta'."
+  (let ((meta (cadr node))
+        (kw (ical:-keyword-to-slot-name keyword)))
+    (cl-struct-slot-value 'ical:meta kw meta)))
+
+(defun ical:ast-node-meta-set (node keyword value)
+  "Set metadata key KEYWORD in NODE to VALUE. The possible KEYWORDs
+are the slot names of `ical:meta'."
+  (let ((meta (cadr node))
+        (kw (ical:-keyword-to-slot-name keyword)))
+    (setf (cl-struct-slot-value 'ical:meta kw meta) value)))
+
+(defun ical:ast-node-first-child-of (type node)
+  "Return the first child of NODE of type TYPE, or nil if there is
+no such child."
+  (assq type (ical:ast-node-children node)))
+
+(defun ical:ast-node-children-of (type node)
+  "Return a list of all the children of NODE of type TYPE, or nil if
+there are none."
+  (seq-filter (lambda (c) (eq type (ical:ast-node-type c)))
+              (ical:ast-node-children node)))
+
+(defun ical:-ast-node-adopt (parent value children)
+  "Make syntax node PARENT the parent node of each syntax node in
+VALUE and CHILDREN. This sets `:parent' meta property in each
+node to PARENT, sets VALUE as PARENT's value, and appends
+CHILDREN to any existing children of PARENT's. Returns the
+modified PARENT. Both VALUE and CHILDREN may be lists. If VALUE
+is nil, PARENT's value is not modified."
+  (let* ((is-list-val (ical:expects-list-of-values-p
+                       (ical:ast-node-type parent)))
+         (to-adopt (cond
+                    ((and value is-list-val)
+                     (append value children))
+                    (value
+                     (cons value children))
+                    (t children))))
+    (dolist (child to-adopt)
+      (when (ical:ast-node-p child)
+        (ical:ast-node-meta-set child :parent parent))))
+  (when value
+    (setf (nth 2 parent) value))
+  (setf (nth 3 parent) (nconc (ical:ast-node-children parent)
+                              children))
+  parent)
+
+(cl-defun ical:make-ast-node (type
+                              &key value
+                                   children
+                                   buffer
+                                   begin
+                                   end
+                                   value-begin
+                                   value-end
+                                   parent
+                                   original-value
+                                   original-name)
+  "Construct an iCalendar syntax node of type TYPE.
+
+The following keyword arguments are accepted:
+
+:value - if given, should be a single syntax node. In value
+  nodes, this is the Elisp value parsed from a property or
+  parameter's value string. In parameter and property nodes, this
+  is a value node. In component nodes, it should be nil.
+
+:children - if given, should be a list of syntax nodes. In
+  property nodes, these should be the parameters of the property.
+  In component nodes, these should be the properties or
+  subcomponents of the component. It should otherwise be nil.
+
+The following keyword arguments, if given, represent syntactic
+metadata for the node; see the definition of `ical:meta' for
+more:
+
+:buffer - buffer from which VALUE was parsed
+:begin - position at which this node begins in BUFFER
+:end - position at which this node ends in BUFFER
+:value-begin - position at which VALUE begins in BUFFER
+:value-end - position at which VALUE ends in BUFFER
+:parent - the parent node of the node to be created
+:original-value - a string containing the original, uninterpreted value
+  of the node. This can differ from (a string represented by) VALUE
+  if e.g. a default VALUE was substituted for an unrecognized but
+  syntactically correct value.
+:original-name - a string containing the original, uninterpreted name
+  of the parameter, property or component this node represents.
+  This can differ from (a string representing) TYPE
+  if e.g. a default TYPE was substituted for an unrecognized but
+  syntactically correct one."
+  (let* ((meta (ical:-make-meta :buffer buffer
+                                :begin begin
+                                :value-begin value-begin
+                                :end end
+                                :value-end value-end
+                                :parent parent
+                                :original-value original-value
+                                :original-name original-name))
+         (node (list type meta nil nil)))
+    (ical:-ast-node-adopt node value children)))
+
+
+;;; Validation:
+
+;; Errors at the validation stage:
+;; e.g. property/param values did not match, or are of the wrong type,
+;; or required properties not present in a component
+(define-error 'ical:validation-error "Invalid iCalendar data")
+
+(defun ical:param-node-p (node)
+  "Return non-nil if NODE is an iCalendar syntax node whose type
+is a parameter type."
+  (and (ical:ast-node-p node)
+       (ical:param-type-symbol-p (ical:ast-node-type node))))
+
+(defun ical:property-node-p (node)
+  "Return non-nil if NODE is an iCalendar syntax node whose type
+is a property type."
+  (and (ical:ast-node-p node)
+       (ical:property-type-symbol-p (ical:ast-node-type node))))
+
+(defun ical:component-node-p (node)
+  "Return non-nil if NODE is an iCalendar syntax node whose type
+is a component type."
+  (and (ical:ast-node-p node)
+       (ical:component-type-symbol-p (ical:ast-node-type node))))
+
+(defun ical:ast-node-valid-meta-p (node)
+  "Validate that NODE's metadata is an appropriate struct. Signals
+an `icalendar-validation-error' if NODE's metadata is invalid, or
+returns NODE."
+  (unless (cl-typep (nth 1 node) 'ical:meta)
+    (signal 'ical:validation-error
+            (list "Invalid metadata struct in node"
+                  node))))
+
+(defun ical:ast-node-valid-value-p (node)
+  "Validate that NODE's value satisfies the requirements of its type.
+Signals an `icalendar-validation-error' if NODE's value is
+invalid, or returns NODE."
+  (let* ((type (ical:ast-node-type node))
+         (value (ical:ast-node-value node)))
+    (cond ((ical:value-type-symbol-p type)
+           (unless (cl-typep value type) ; see `ical:define-type'
+             (signal 'ical:validation-error
+                     (list (format "Invalid value for `%s' node: %s"
+                                   type value)
+                           node)))
+           node)
+          ((ical:component-node-p node)
+           ;; component types have no value, so no need to check anything
+           node)
+          ((and (or (ical:param-type-symbol-p type)
+                    (ical:property-type-symbol-p type))
+                (null (get type 'ical:value-type))
+                (stringp value))
+           ;; property and param nodes with no value type are assumed to contain
+           ;; strings which match a value regex:
+           (unless (string-match (rx-to-string (get type 'ical:value-rx)) value)
+             (signal 'ical:validation-error
+                     (list (format "Invalid string value for `%s' node: %s"
+                                   type value)
+                           node)))
+           node)
+          ;; otherwise this is a param or property node which itself
+          ;; should have one or more syntax nodes as a value, so
+          ;; recurse on value(s):
+          ((ical:expects-list-of-values-p type)
+           (unless (listp value) ;; TODO: check elements' types...?
+             (signal 'ical:validation-error
+                     (list (format "Expected list of values for `%s' node"
+                                   type)
+                           node)))
+           (mapc #'ical:ast-node-valid-value-p value)
+           node)
+          (t
+           (unless (ical:ast-node-p value)
+             (signal 'ical:validation-error
+                     (list (format "Invalid value for `%s' node: %s"
+                                   type value)
+                           node)))
+           (ical:ast-node-valid-value-p value)))))
+
+(defun ical:count-children-by-type (node)
+  "Return an alist mapping type symbols to the number of child nodes
+of that type in NODE."
+  (let ((children (ical:ast-node-children node))
+        (map nil))
+    (dolist (child children map)
+      (let* ((type (ical:ast-node-type child))
+             (n (alist-get type map)))
+        (setf (alist-get type map) (1+ (or n 0)))))))
+
+(defun ical:ast-node-valid-children-p (node)
+  "Validate that NODE's children satisfy the :child-spec associated
+with its type by `icalendar-define-component',
+`icalendar-define-property', `icalendar-define-param', or
+`icalendar-define-type'. Signals an `icalendar-validation-error'
+if NODE is invalid, or returns NODE.
+
+Note that this function does not check that the children of NODE
+are themselves valid; for that, see `ical:ast-node-valid-p'."
+  (let* ((type (ical:ast-node-type node))
+         (child-spec (get type 'ical:child-spec))
+         (child-counts (ical:count-children-by-type node)))
+
+    (when child-spec
+
+      (dolist (child-type (plist-get child-spec :one))
+        (unless (= 1 (alist-get child-type child-counts 0))
+          (signal 'ical:validation-error
+                  (list (format "iCalendar `%s' node must contain exactly one `%s'"
+                                type child-type)
+                        node))))
+
+      (dolist (child-type (plist-get child-spec :one-or-more))
+        (unless (<= 1 (alist-get child-type child-counts 0))
+          (signal 'ical:validation-error
+                  (list (format "iCalendar `%s' node must contain one or more `%s'"
+                                type child-type)
+                        node))))
+
+      (dolist (child-type (plist-get child-spec :zero-or-one))
+        (unless (<= (alist-get child-type child-counts 0)
+                    1)
+          (signal 'ical:validation-error
+                  (list (format "iCalendar `%s' node may contain at most one `%s'"
+                                type child-type)
+                        node))))
+
+      ;; check that all child nodes are allowed:
+      (unless (plist-get child-spec :allow-others)
+        (let ((allowed-types (append (plist-get child-spec :one)
+                                     (plist-get child-spec :one-or-more)
+                                     (plist-get child-spec :zero-or-one)
+                                     (plist-get child-spec :zero-or-more)))
+              (appearing-types (mapcar #'car child-counts)))
+
+          (dolist (child-type appearing-types)
+            (unless (member child-type allowed-types)
+              (signal 'ical:validation-error
+                      (list (format "`%s' may not contain `%s'"
+                                    type child-type)
+                            node)))))))
+    ;; success:
+    node))
+
+(defun ical:ast-node-valid-p (node &optional recursively)
+  "Check that NODE is a valid iCalendar syntax node.
+By default, the check will only validate NODE itself, but if
+RECURSIVELY is non-nil, it will recursively check all its
+descendants as well. Signals an `icalendar-validation-error' if
+NODE is invalid, or returns NODE."
+  (unless (ical:ast-node-p node)
+    (signal 'ical:validation-error
+            (list "Not an iCalendar syntax node"
+                  node)))
+
+  (ical:ast-node-valid-meta-p node)
+  (ical:ast-node-valid-value-p node)
+  (ical:ast-node-valid-children-p node)
+
+  (let* ((type (ical:ast-node-type node))
+         (other-validator (get type 'ical:other-validator)))
+
+    (unless (ical:type-symbol-p type)
+      (signal 'ical:validation-error
+              (list (format "Node's type `%s' is not an iCalendar type symbol"
+                            type)
+                    node)))
+
+    (when (and other-validator (not (functionp other-validator)))
+      (signal 'ical:validation-error
+              (list (format "Bad validator function `%s' for type `%s'"
+                            other-validator type))))
+
+    (when other-validator
+      (funcall other-validator node)))
+
+  (let ((children (ical:ast-node-children node)))
+    (when (and recursively (not (null children)))
+      (dolist (c children)
+        (ical:ast-node-valid-p c recursively))))
+
+  ;; success:
+  node)
+
+(provide 'icalendar-ast)
+;; Local Variables:
+;; read-symbol-shorthands: (("ical:" . "icalendar-"))
+;; End:
+;;; icalendar-ast.el ends here
diff --git a/lisp/calendar/icalendar-macs.el b/lisp/calendar/icalendar-macs.el
new file mode 100644
index 00000000000..2030efc5e6d
--- /dev/null
+++ b/lisp/calendar/icalendar-macs.el
@@ -0,0 +1,809 @@
+;;; icalendar-macs.el --- Macros for iCalendar  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: Richard Lawrence <[email-masked]>
+;; Created: October 2024
+;; Keywords: calendar
+;; Human-Keywords: calendar, iCalendar
+
+;; This file is part of GNU Emacs.
+
+;; This file is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this file.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file defines the macros `ical:define-type', `ical:define-param',
+;; `ical:define-property' and `ical:define-component', used in
+;; icalendar-parser.el to define the particular value types, parameters,
+;; properties and components in the standard as type symbols.
+
+
+(require 'cl-lib)
+
+(declare-function ical:value-type-symbol-p "icalendar-ast.el")
+
+;; Some utilities:
+(defun ical:protected-intern (sym-name)
+  "Call `intern' on SYM-NAME and return the result, but warn if the
+resulting symbol already has icalendar-relevant properties."
+  (let ((sym (intern sym-name)))
+    (when (or (fboundp sym)
+              (get sym 'rx-definition)
+              (get sym 'ical:is-type))
+      (warn "Symbol `%s' already has iCalendar properties" sym))
+    sym))
+
+(defun ical:format-child-spec (child-spec)
+  "Format CHILD-SPEC as a table for use in symbol documentation."
+  (concat
+   (format "%-30s%6s\n" "Type" "Number")
+   (make-string 36 ?-) "\n"
+   (mapconcat
+    (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "1"))
+    (plist-get child-spec :one))
+   (mapconcat
+    (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "1+"))
+    (plist-get child-spec :one-or-more))
+   (mapconcat
+    (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "0-1"))
+    (plist-get child-spec :zero-or-one))
+   (mapconcat
+    (lambda (type) (format "%-30s%-6s\n" (format "`%s'" type) "0+"))
+    (plist-get child-spec :zero-or-more))))
+
+
+;; Define value types:
+(cl-defmacro ical:define-type (symbolic-name print-name doc specifier matcher
+                               &key link
+                                    (reader #'identity)
+                                    (printer #'identity))
+  "Define an iCalendar value type named SYMBOLIC-NAME.
+
+PRINT-NAME should be the string used to represent this type in
+the value of an `icalendar-valuetypeparam' property parameter, or
+nil if this is not a type that should be specified there. DOC
+should be a documentation string for the type. SPECIFIER should
+be a type specifier in the sense of `cl-deftype'. MATCHER should
+be an RX definition body (see `rx-define'; argument lists are not
+supported).
+
+Before the type is defined with `cl-deftype', a function will be
+defined named `icalendar-match-PRINT-NAME-value'
+(or `icalendar-match-OTHER-value', if PRINT-NAME is nil, where
+OTHER is derived from SYMBOLIC-NAME by removing any prefix
+\"icalendar-\" and suffix \"value\"). This function takes a
+string argument and matches it against MATCHER. This function may
+thus occur in SPECIFIER (e.g. in a (satisfies ...) clause).
+
+See the functions `icalendar-read-value-node',
+`icalendar-parse-value-node', and `icalendar-print-value-node' to
+convert values defined with this macro to and from their text
+representation in iCalendar format.
+
+The following keyword arguments are accepted:
+
+:reader - a function to read data of this type. It will be passed
+  a string matching MATCHER and should return an Elisp data structure.
+  Its name does not need to be quoted. (default: identity)
+
+:printer - a function to convert an Elisp data structure of this
+  type to a string. Its name does not need to be quoted.
+  (default: identity)
+
+:link - a string containing an URL for further documentation of this type"
+  (let* (;; Related functions:
+         (type-dname (if print-name
+                         (downcase print-name)
+                       (string-trim
+                        (symbol-name symbolic-name)
+                        "icalendar-" "value")))
+         (matcher-name (ical:protected-intern
+                        (concat "icalendar-match-" type-dname "-value")))
+
+         ;; Documentation:
+         (header "It names a value type defined by `icalendar-define-type'.")
+         (matcher-doc (format
+"Strings representing values of this type can be matched with
+`%s'.\n" matcher-name))
+         (reader-doc (format "They can be read with `%s'\n" reader))
+         (printer-doc (format "and printed with `%s'." printer))
+         (full-doc (concat header "\n\n" doc "\n\n"
+                           matcher-doc reader-doc printer-doc "\n\n"
+"A syntax node of this type can be read with
+`icalendar-read-value-node' or parsed with `icalendar-parse-value-node',
+and printed with `icalendar-print-value-node'.")))
+
+    `(progn
+       ;; Type metadata needs to be available at both compile time and
+       ;; run time. In particular, `ical:value-type-symbol-p' needs to
+       ;; work at compile time.
+       (eval-and-compile
+         (setplist (quote ,symbolic-name)
+                   (list
+                    'ical:is-type t
+                    'ical:is-value t
+                    'ical:matcher (function ,matcher-name)
+                    'ical:value-rx (quote ,symbolic-name)
+                    'ical:value-reader (function ,reader)
+                    'ical:value-printer (function ,printer)
+                    'ical:type-documentation ,full-doc
+                    'ical:link ,link)))
+
+       (rx-define ,symbolic-name
+         ,matcher)
+
+       (defun ,matcher-name (s)
+         ,(format "Match string S against rx `%s'." symbolic-name)
+         (string-match (rx ,symbolic-name) s))
+
+       (cl-deftype ,symbolic-name () ,specifier)
+
+       ;; Store the association between the print name and the type
+       ;; symbol in ical:value-types. The check against print name
+       ;; here allows us to also define value types that aren't
+       ;; "really" types according to the standard, like
+       ;; `ical:geo-coordinates'. Only types that have a
+       ;; print-name can be specified in a VALUE parameter.
+       (when ,print-name
+         (push (cons ,print-name (quote ,symbolic-name)) ical:value-types)))))
+
+;; TODO: not sure this is needed. I've only used it once in the parser.
+(cl-defmacro ical:define-keyword-type (symbolic-name print-name doc matcher
+                                       &key link
+                                            (reader 'intern)
+                                            (printer 'symbol-name))
+  "Like `icalendar-define-type', except that string values matching MATCHER
+are assumed to be type-specific keywords that should be interned
+as symbols when read. (Thus no type specifier is necessary: it is
+always just \\='symbol.) Their printed representation is their
+symbol name."
+  `(ical:define-type ,symbolic-name ,print-name ,doc
+                     'symbol
+                     ,matcher
+                     :link ,link
+                     :reader ,reader
+                     :printer ,printer))
+
+
+;; Define parameters:
+(cl-defmacro ical:define-param (symbolic-name param-name doc value
+                                &key quoted
+                                     list-sep
+                                     default
+                                     (unrecognized default)
+                                     ((:name-face name-face)
+                                      'ical:parameter-name nondefault-name-face)
+                                     ((:value-face value-face)
+                                      'ical:parameter-value nondefault-value-face)
+                                     ((:warn-face warn-face)
+                                      'ical:warning nondefault-warn-face)
+                                     extra-faces
+                                     link)
+  "Define iCalendar parameter PARAM-NAME under the symbol SYMBOLIC-NAME.
+PARAM-NAME should be the parameter name as it should appear in
+iCalendar data.
+
+VALUE should either be a symbol for a value type defined with
+`icalendar-define-type', or an `rx' regular expression. If it is
+a type symbol, the regex, reader and printer functions associated
+with that type will be used when parsing and serializing values.
+If it is a regular expression, it is assumed that the values of
+this parameter are strings which match that regular expression.
+
+An `rx' regular expression named SYMBOLIC-NAME which matches the
+parameter is defined:
+  Group 1 of this regex matches PARAM-NAME
+    (or any valid parameter name, if PARAM-NAME is nil).
+  Group 2 matches VALUE, which specifies a correct value
+    for this parameter according to RFC5545.
+  Group 3, if matched, contains any parameter value which does
+    *not* match VALUE, and is incorrect according to the standard.
+
+This regex matches the entire string representing this parameter,
+from \";\" to the end of its value. Another regular expression
+named `SYMBOLIC-NAME-value' is also defined to match just the
+value part, after \";PARAM-NAME=\", with groups 2 and 3 as above.
+
+A function to match the complete parameter expression called
+`icalendar-match-PARAM-NAME-param' is defined
+(or `icalendar-match-OTHER-param-value' if PARAM-NAME is nil,
+where OTHER is derived from SYMBOLIC-NAME by removing any prefix
+`icalendar-' and suffix `param'). This function is used
+to provide syntax highlighting in `icalendar-mode'.
+
+See the functions `icalendar-read-param-value',
+`icalendar-parse-param-value', `icalendar-parse-params' and
+`icalendar-print-param-node' to convert parameters defined with
+this macro to and from their text representation in iCalendar
+format.
+
+The following keyword arguments are accepted:
+
+:default - a (string representing the) default value, if the
+  parameter is not specified on a given property.
+
+:unrecognized - a (string representing the) value which must be
+  substituted for values that are not recognized but syntactically
+  correct according to RFC5545. Unrecognized values must be in match
+  group 5 of the regex determined by VALUE. An unrecognized value will
+  be preserved in the syntax tree metadata and printed instead of this
+  value when the node is printed. Defaults to any value specified for
+  :default.
+
+:quoted - non-nil if values of this parameter must always be surrounded
+  by (double-)quotation marks when printed, according to RFC5545.
+
+:list-sep - if the parameter accepts a list of values, this should be a
+  string which separates the values (typically \",\"). If :list-sep is
+  non-nil, the value string will first be split on the separator, then
+  if :quoted is non-nil, the individual values will be unquoted, then
+  each value will be read according to VALUE and collected into a list
+  when parsing.  When printing, the inverse happens: values are quoted
+  if :quoted is non-nil, then joined with :list-sep. Passing this
+  argument marks SYMBOLIC-NAME as a type that accepts a list of values
+  for `icalendar-expects-list-of-values-p'.
+
+:name-face - a face symbol for highlighting the property name
+  (default: ical:parameter-name)
+
+:value-face - a face symbol for highlighting valid property values
+  (default: ical:parameter-value)
+
+:warn-face - a face symbol for highlighting invalid property values
+  (default: ical:warning)
+
+:extra-faces - a list of the form accepted for HIGHLIGHT in
+  `font-lock-keywords'.  In particular,
+    ((GROUPNUM FACENAME [OVERRIDE [LAXMATCH]]) ...)
+  can be used to apply different faces to different
+  match subgroups.
+
+:link - a string containing a URL for documentation of this parameter.
+  The URL will be provided in the documentation shown by
+  `describe-symbol' for SYMBOLIC-NAME."
+  (let* (;; Related function names:
+         (param-dname (if param-name
+                          (downcase param-name)
+                        (string-trim (symbol-name symbolic-name)
+                                     "icalendar-" "param")))
+         (matcher-name (ical:protected-intern
+                        (concat "icalendar-match-" param-dname "-param")))
+
+         (type-predicate-name
+          (ical:protected-intern (concat "icalendar-" param-dname "-param-p")))
+         ;; Value regexes:
+         (qvalue-rx (if quoted `(seq ?\" ,value ?\") value))
+         (values-rx (when list-sep
+                     `(seq ,qvalue-rx (zero-or-more ,list-sep ,qvalue-rx))))
+         (full-value-rx-name (ical:protected-intern
+                               (concat (symbol-name symbolic-name) "-value")))
+         ;; Faces:
+         (has-faces (or nondefault-name-face nondefault-value-face
+                        nondefault-warn-face extra-faces))
+         ;; Documentation:
+         (header "It names a parameter type defined by `icalendar-define-param'.")
+         (val-list (if list-sep (concat "VAL1" list-sep "VAL2" list-sep "...")
+                     "VAL"))
+         (s (if list-sep "s" "")) ; to make plurals
+         (val-doc (concat "VAL" s " "
+                          "must be " (unless list-sep "a ") (when quoted "quoted ")
+                          (if (ical:value-type-symbol-p value)
+                              (format "`%s' value%s" (symbol-name value) s)
+                            (format "string%s matching rx `%s'" s value))))
+         (syntax-doc (format "Syntax: %s=%s\n%s"
+                             (or param-name "(NAME)") val-list val-doc))
+         (full-doc (concat header "\n\n" doc "\n\n" syntax-doc)))
+
+    `(progn
+       ;; Type metadata needs to be available at both compile time and
+       ;; run time. In particular, `ical:value-type-symbol-p' needs to
+       ;; work at compile time.
+       (eval-and-compile
+         (setplist (quote ,symbolic-name)
+                   (list
+                    'ical:is-type t
+                    'ical:is-param t
+                    'ical:matcher (function ,matcher-name)
+                    'ical:default-value ,default
+                    'ical:is-quoted ,quoted
+                    'ical:list-sep ,list-sep
+                    'ical:substitute-value ,unrecognized
+                    'ical:matcher (function ,matcher-name)
+                    'ical:value-type
+                    (when (ical:value-type-symbol-p (quote ,value))
+                      (quote ,value))
+                    'ical:value-rx (quote ,value)
+                    'ical:values-rx (quote ,values-rx)
+                    'ical:full-value-rx (quote ,full-value-rx-name)
+                    'ical:type-documentation ,full-doc
+                    'ical:link ,link)))
+
+       ;; Regex which matches just the value of the parameter:
+       ;; Group 2: correct values of the parameter, and
+       ;; Group 3: incorrect values up to the next parameter
+       (rx-define ,full-value-rx-name
+         (or (group-n 2 ,(or values-rx qvalue-rx))
+             (group-n 3 ical:param-value)))
+
+       ;; Regex which matches the full parameter:
+       ;; Group 1: the parameter name,
+       ;; Group 2: correct values of the parameter, and
+       ;; Group 3: incorrect values up to the next parameter
+       (rx-define ,symbolic-name
+         (seq ";"
+              ;; if the parameter name has no printed form, the best we
+              ;; can do is match ical:param-name:
+              (group-n 1 ,(or param-name 'ical:param-name))
+              "="
+              ,full-value-rx-name))
+
+       ;; CL-type to represent syntax nodes for this parameter:
+       (defun ,type-predicate-name (node)
+         ,(format "Return non-nil if NODE represents a %s parameter" param-name)
+         (and (ical:ast-node-p node)
+              (eq (ical:ast-node-type node) (quote ,symbolic-name))))
+
+       (cl-deftype ,symbolic-name () '(satisfies ,type-predicate-name))
+
+       ;; Matcher for the full param string, for syntax highlighting:
+       (defun ,matcher-name (limit)
+         ,(format "Matcher for %s parameter (defined by define-param)" param-name)
+         (re-search-forward (rx ,symbolic-name) limit t))
+
+       ;; Entry for font-lock-keywords in icalendar-mode:
+       (when ,has-faces
+         ;; Avoid circular load of icalendar-mode.el in
+         ;; icalendar-parser.el (which does not use the *-face
+         ;; keywords), while still allowing external code to add to
+         ;; font-lock-keywords dynamically:
+         (require 'icalendar-mode)
+         (push (quote (,matcher-name
+                       (1 (quote ,name-face) t t)
+                       (2 (quote ,value-face) t t)
+                       (3 (quote ,warn-face) t t)
+                       ,@extra-faces))
+               ical:font-lock-keywords))
+
+       ;; Associate the print name with the type symbol for
+       ;; `ical:parse-params' and `ical:print-param':
+       (when ,param-name
+         (push (cons ,param-name (quote ,symbolic-name)) ical:param-types))
+       ;; TODO: integrate param-name with eldoc in icalendar-mode
+       )))
+
+
+;; Define properties:
+(cl-defmacro ical:define-property (symbolic-name property-name doc value
+                                   &key default
+                                        (unrecognized default)
+                                        (default-type
+                                         (if (ical:value-type-symbol-p value)
+                                             value
+                                           'ical:text))
+                                        other-types
+                                        list-sep
+                                        child-spec
+                                        other-validator
+                                        ((:name-face name-face)
+                                         'ical:property-name nondefault-name-face)
+                                        ((:value-face value-face)
+                                         'ical:property-value nondefault-value-face)
+                                        ((:warn-face warn-face)
+                                         'ical:warning nondefault-warn-face)
+                                        extra-faces
+                                        link)
+  "Define iCalendar property PROPERTY-NAME under SYMBOLIC-NAME.
+PROPERTY-NAME should be the property name as it should appear in
+iCalendar data.
+
+VALUE should either be a symbol for a value type defined with
+`icalendar-define-type', or an `rx' regular expression. If it is
+a type symbol, the regex, reader and printer functions associated
+with that type will be used when parsing and serializing the
+property's value. If it is a regular expression, it is assumed
+that the values are strings of type `icalendar-text' which match
+that regular expression.
+
+An `rx' regular expression named SYMBOLIC-NAME is defined to
+match the property:
+  Group 1 of this regex matches PROPERTY-NAME.
+  Group 2 matches VALUE.
+  Group 3, if matched, contains any property value which does
+   *not* match VALUE, and is incorrect according to the standard.
+  Group 4, if matched, contains the (unparsed) property parameters;
+   its boundaries can be used for parsing these.
+
+This regex matches the entire string representing this property,
+from the beginning of the content line to the end of its value.
+Another regular expression named `SYMBOLIC-NAME-value' is also
+defined to match just the value part, after the separating colon,
+with groups 2 and 3 as above.
+
+A function to match the complete property expression called
+`icalendar-match-PROPERTY-NAME-property' is defined. This
+function is used to provide syntax highlighting in
+`icalendar-mode'.
+
+See the functions `icalendar-read-property-value',
+`icalendar-parse-property-value', `icalendar-parse-property', and
+`icalendar-print-property-node' to convert properties defined
+with this macro to and from their text representation in
+iCalendar format.
+
+The following keyword arguments are accepted:
+
+:default - a (string representing the) default value, if
+  the property is not specified in a given component.
+
+:unrecognized - a (string representing the) value which must be
+  substituted for values that are not recognized but
+  syntactically correct according to RFC5545. Unrecognized values
+  must be in match group 5 of the regex determined by VALUE. An
+  unrecognized value will be preserved in the syntax tree
+  metadata and printed instead of this value when the node is
+  printed. Defaults to any value specified for :default.
+
+:default-type - a type symbol naming the default type of the
+  property's value. If the property's value differs from this
+  type, an `icalendar-valuetypeparam' parameter will be added to
+  the property's syntax node and printed when the node is
+  printed. Default is VALUE if VALUE is a value type symbol,
+  otherwise the type `icalendar-text'.
+
+:other-types - a list of type symbols naming value types other
+  than :default-type. These represent alternative types for the
+  property's value. If parsing the property's value under its
+  default type fails, these types will be tried in turn, and only
+  if the property's value matches none of them will an error be
+  signaled.
+
+:list-sep - if the property accepts a list of values, this should
+  be a string which separates the values (typically \",\"). If
+  :list-sep is non-nil, the value string will first be split on
+  the separator, then each value will be read according to VALUE
+  and collected into a list when parsing. When printing, the
+  inverse happens: values are printed individually and then
+  joined with :list-sep. Passing this argument marks
+  SYMBOLIC-NAME as a type that accepts a list of values for
+  `icalendar-expects-list-of-values-p'.
+
+:child-spec - a plist mapping the following keywords to lists
+of type symbols:
+  :one           - parameters that must appear exactly once
+  :one-or-more   - parameters that must appear at least once and
+                   may appear more than once
+  :zero-or-one   - parameters that must appear at most once
+  :zero-or-more  - parameters that may appear more than once
+  :allow-others  - if non-nil, other parameters besides those listed in
+                   the above are allowed to appear. (In this case, a
+                   :zero-or-more clause is redundant.)
+
+:other-validator - a function to perform any additional validation of
+  the component, beyond what `icalendar-ast-node-valid-p' checks.
+  This function should accept one argument, a syntax node. It
+  should return non-nil if the node is valid, or signal an
+  `icalendar-validation-error' if it is not. Its name does not
+  need to be quoted.
+
+:name-face - a face symbol for highlighting the property name
+  (default: `ical:property-name')
+
+:value-face - a face symbol for highlighting valid property values
+  (default: `ical:property-value')
+
+:warn-face - a face symbol for highlighting invalid property values
+  (default: `ical:warning')
+
+:extra-faces - a list of the form for HIGHLIGHT in `font-lock-keywords'.
+  In particular, ((GROUPNUM FACENAME [OVERRIDE [LAXMATCH]])...)
+  can be used to apply different faces to different match subgroups.
+
+:link - a string containing a URL for documentation of this property"
+  (let* (;; Value RX:
+        (full-value-rx-name
+         (ical:protected-intern
+          (concat (symbol-name symbolic-name) "-property-value")))
+        (values-rx (when list-sep
+                    `(seq ,value (zero-or-more ,list-sep ,value))))
+        ;; Related functions:
+        (property-dname (if property-name
+                            (downcase property-name)
+                          (string-trim (symbol-name symbolic-name)
+                                       "icalendar-" "-property")))
+        (matcher-name (ical:protected-intern
+                       (concat "icalendar-match-"
+                               property-dname
+                               "-property")))
+        (type-predicate-name
+         (ical:protected-intern (concat "icalendar-"
+                                        property-dname
+                                        "-property-p")))
+        ;; Faces:
+        (has-faces (or nondefault-name-face nondefault-value-face
+                       nondefault-warn-face extra-faces))
+        ;; Documentation:
+        (header "It names a property type defined by `icalendar-define-property'.")
+        (val-list (if list-sep (concat "VAL1" list-sep "VAL2" list-sep "...")
+                    "VAL"))
+        (default-doc (if default (format "The default value is: \"%s\"\n" default)
+                       ""))
+        (s (if list-sep "s" "")) ; to make plurals
+        (val-doc (concat "VAL" s " "
+                         "must be " (unless list-sep "a ")
+                         (format "value%s of one of the following types:\n" s)
+                         (string-join
+                          (cons
+                           (format "`%s' (default)" default-type)
+                           (mapcar (lambda (type) (format "`%s'" type))
+                                   other-types))
+                          "\n")
+                         default-doc))
+        (name-doc (if property-name "" "NAME must match rx `icalendar-name'"))
+        (syntax-doc (format "Syntax: %s[;PARAM...]:%s\n%s\n%s\n"
+                            (or property-name "NAME") val-list name-doc val-doc))
+        (child-doc
+         (concat
+          "The following parameters are required or allowed\n"
+          "as children in syntax nodes of this type:\n\n"
+          (ical:format-child-spec child-spec)
+          (when (plist-get child-spec :allow-others)
+            "\nOther parameters of any type are also allowed.\n")))
+        (full-doc (concat header "\n\n" doc "\n\n" syntax-doc "\n\n" child-doc)))
+
+    `(progn
+       ;; Type metadata needs to be available at both compile time and
+       ;; run time. In particular, `ical:value-type-symbol-p' needs to
+       ;; work at compile time.
+       (eval-and-compile
+         (setplist (quote ,symbolic-name)
+                   (list
+                    'ical:is-type t
+                    'ical:is-property t
+                    'ical:matcher (function ,matcher-name)
+                    'ical:default-value ,default
+                    'ical:default-type (quote ,default-type)
+                    'ical:other-types (quote ,other-types)
+                    'ical:list-sep ,list-sep
+                    'ical:substitute-value ,unrecognized
+                    'ical:value-type
+                    (when (ical:value-type-symbol-p (quote ,value))
+                      (quote ,value))
+                    'ical:value-rx (quote ,value)
+                    'ical:values-rx (quote ,values-rx)
+                    'ical:full-value-rx (quote ,full-value-rx-name)
+                    'ical:child-spec (quote ,child-spec)
+                    'ical:other-validator (function ,other-validator)
+                    'ical:type-documentation ,full-doc
+                    'ical:link ,link)))
+
+       ;; Value regex which matches:
+       ;; Group 2: correct values of the property, and
+       ;; Group 3: incorrect values up to end-of-line (for syntax warnings)
+       (rx-define ,full-value-rx-name
+         (or (group-n 2 ,(or values-rx value))
+             (group-n 3 (zero-or-more any))))
+
+       ;; Full property regex which matches:
+       ;; Group 1: the property name,
+       ;; Group 2: correct values of the property, and
+       ;; Group 3: incorrect values up to end-of-line (for syntax warnings)
+       (rx-define ,symbolic-name
+         (seq line-start
+              (group-n 1 ,(or property-name 'ical:name))
+              (group-n 4 (zero-or-more ical:other-param-safe))
+              ":"
+              ,full-value-rx-name
+              line-end))
+
+       ;; Matcher:
+       (defun ,matcher-name (limit)
+         ,(format "Matcher for `%s' property (defined by define-property)"
+                  symbolic-name)
+         (re-search-forward (rx ,symbolic-name) limit t))
+
+       ;; CL-type to represent syntax nodes for this property:
+       (defun ,type-predicate-name (node)
+         ,(format "Return non-nil if NODE represents a %s property" property-name)
+         (and (ical:ast-node-p node)
+              (eq (ical:ast-node-type node) (quote ,symbolic-name))))
+
+       (cl-deftype ,symbolic-name () '(satisfies ,type-predicate-name))
+
+       ;; Associate the print name with the type symbol for
+       ;; `icalendar-parse-property', `icalendar-print-property-node', etc.:
+       (when ,property-name
+         (push (cons ,property-name (quote ,symbolic-name)) ical:property-types))
+
+       ;; Generate an entry for font-lock-keywords in icalendar-mode:
+       (when ,has-faces
+         ;; Avoid circular load of icalendar-mode.el in
+         ;; icalendar-parser.el (which does not use the *-face
+         ;; keywords), while still allowing external code to add to
+         ;; font-lock-keywords dynamically:
+         (require 'icalendar-mode)
+         (push (quote (,matcher-name
+                       (1 (quote ,name-face) t t)
+                       (2 (quote ,value-face) t t)
+                       (3 (quote ,warn-face) t t)
+                       ,@extra-faces))
+               ical:font-lock-keywords)))))
+
+
+;; Define components:
+(cl-defmacro ical:define-component (symbolic-name component-name doc
+                                    &key
+                                    ((:keyword-face keyword-face)
+                                     'ical:keyword nondefault-keyword-face)
+                                    ((:name-face name-face)
+                                     'ical:component-name nondefault-name-face)
+                                    child-spec
+                                    other-validator
+                                    link)
+  "Define iCalendar component COMPONENT-NAME under SYMBOLIC-NAME.
+COMPONENT-NAME should be the name of the component as it should
+appear in iCalendar data.
+
+Regular expressions to match the component boundaries are defined
+named `COMPONENT-NAME-begin' and `COMPONENT-NAME-end' (or
+`OTHER-begin' and `OTHER-end', where `OTHER' is derived from
+SYMBOLIC-NAME by removing any prefix `icalendar-' and suffix
+`-component' if COMPONENT-NAME is nil).
+  Group 1 of these regexes matches the \"BEGIN\" or \"END\"
+    keyword that marks a component boundary.
+  Group 2 matches the component name.
+
+A function to match the component boundaries is defined called
+`icalendar-match-COMPONENT-NAME-component' (or
+`icalendar-match-OTHER-component', with OTHER as above). This
+function is used to provide syntax highlighting in
+`icalendar-mode'.
+
+The following keyword arguments are accepted:
+
+:child-spec - a plist mapping the following keywords to lists
+of type symbols:
+  :one           - properties or components that must appear exactly once
+  :one-or-more   - properties or components that must appear at least once and
+                   may appear more than once
+  :zero-or-one   - properties or components that must appear at most once
+  :zero-or-more  - properties or components that may appear more than once
+  :allow-others  - if non-nil, other children besides those listed in the above
+                   are allowed to appear. (In this case, a :zero-or-more clause
+                   is redundant.)
+
+:other-validator - a function to perform any additional validation of
+  the component, beyond what `icalendar-ast-node-valid-p' checks.
+  This function should accept one argument, a syntax node. It
+  should return non-nil if the node is valid, or signal an
+  `icalendar-validation-error' if it is not. Its name does not
+  need to be quoted.
+
+:keyword-face - a face symbol for highlighting the BEGIN/END keyword
+  (default: ical:keyword)
+
+:name-face - a face symbol for highlighting the component name
+  (default: ical:component-name)
+
+:link - a string containing a URL for documentation of this component"
+  (let* (;; Regexes:
+         (name-rx (or component-name 'ical:name))
+         (component-dname (if component-name
+                              (downcase component-name)
+                            (string-trim (symbol-name symbolic-name)
+                                         "icalendar-" "-component")))
+         (begin-rx-name (ical:protected-intern
+                         (concat "icalendar-" component-dname "-begin")))
+         (end-rx-name (ical:protected-intern
+                       (concat "icalendar-" component-dname "-end")))
+         ;; Related functions:
+         (matcher-name
+          (ical:protected-intern
+           (concat "icalendar-match-" component-dname "-component")))
+         (type-predicate-name
+          (ical:protected-intern
+           (concat "icalendar-" component-dname "-component-p")))
+         ;; Faces:
+         (has-faces (or nondefault-name-face nondefault-keyword-face))
+         ;; Documentation:
+         (header "It names a component type defined by
+`icalendar-define-component'.")
+         (name-doc (if (not component-name)
+                       "\nNAME must match rx `icalendar-name'"
+                     ""))
+         (syntax-doc (format "Syntax:\nBEGIN:%s\n[contentline ...]\nEND:%1$s%s"
+                             (or component-name "NAME")
+                             name-doc))
+         (child-doc
+          (concat
+           "The following properties and components are required or "
+           "allowed\nas children in syntax nodes of this type:\n\n"
+           (ical:format-child-spec child-spec)
+           (when (plist-get child-spec :allow-others)
+             "\nOther properties and components of any type are also allowed.\n")))
+         (full-doc (concat header "\n\n" doc "\n\n" syntax-doc "\n\n" child-doc)))
+
+    `(progn
+       ;; Type metadata needs to be available at both compile time and
+       ;; run time. In particular, `ical:value-type-symbol-p' needs to
+       ;; work at compile time.
+       (eval-and-compile
+         (setplist (quote ,symbolic-name)
+                   (list
+                    'ical:is-type t
+                    'ical:is-component t
+                    'ical:matcher (function ,matcher-name)
+                    'ical:begin-rx (quote ,begin-rx-name)
+                    'ical:end-rx (quote ,end-rx-name)
+                    'ical:child-spec (quote ,child-spec)
+                    'ical:other-validator (function ,other-validator)
+                    'ical:type-documentation ,full-doc
+                    'ical:link ,link)))
+
+       ;; Regexes which match:
+       ;; Group 1: BEGIN or END, and
+       ;; Group 2: the component name
+       (rx-define ,begin-rx-name
+         (seq line-start
+              (group-n 1 "BEGIN")
+              ":"
+              (group-n 2 ,name-rx)
+              line-end))
+
+       (rx-define ,end-rx-name
+         (seq line-start
+              (group-n 1  "END")
+              ":"
+              (group-n 2 ,name-rx)
+              line-end))
+
+       (defun ,matcher-name (limit)
+         ,(format "Matcher for %s component boundaries"
+                  (or component-name "unrecognized"))
+           (re-search-forward (rx (or ,begin-rx-name ,end-rx-name)) limit t))
+
+       ;; CL-type to represent syntax nodes for this component:
+       (defun ,type-predicate-name (node)
+         ,(format "Return non-nil if NODE represents a %s component"
+                  (or component-name "unrecognized"))
+         (and (ical:ast-node-p node)
+              (eq (ical:ast-node-type node) (quote ,symbolic-name))))
+
+       (cl-deftype ,symbolic-name () '(satisfies ,type-predicate-name))
+
+       ;; Generate an entry for font-lock-keywords in icalendar-mode:
+       (when ,has-faces
+         ;; Avoid circular load of icalendar-mode.el in
+         ;; icalendar-parser.el (which does not use the *-face
+         ;; keywords), while still allowing external code to add to
+         ;; font-lock-keywords dynamically:
+         (require 'icalendar-mode)
+         (push (quote (,matcher-name
+                       (1 (quote ,keyword-face) t t)
+                       (2 (quote ,name-face) t t)))
+               ical:font-lock-keywords))
+
+       ;; Associate the print name with the type symbol for
+       ;; `icalendar-parse-component', `icalendar-print-component' etc.:
+       (when ,component-name
+         (push (cons ,component-name (quote ,symbolic-name)) ical:component-types))
+
+       ;; TODO: integrate component-name with eldoc in icalendar-mode
+       )))
+
+(provide 'icalendar-macs)
+;; Local Variables:
+;; read-symbol-shorthands: (("ical:" . "icalendar-"))
+;; End:
+;;; icalendar-macs.el ends here
diff --git a/lisp/calendar/icalendar-parser.el b/lisp/calendar/icalendar-parser.el
new file mode 100644
index 00000000000..bc9524ff389
--- /dev/null
+++ b/lisp/calendar/icalendar-parser.el
@@ -0,0 +1,4090 @@
+;;; icalendar-parser.el --- Parse iCalendar grammar  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: Richard Lawrence <[email-masked]>
+;; 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.
+;;
+;; The regular expressions defined by the `ical:define-*' macros are
+;; also used to create entries for `font-lock-keywords', which are
+;; gathered into several constants along the way, and used to provide
+;; syntax highlighting in icalendar-mode.el. 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-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-component-node'
+;;   `icalendar-print-property-node'
+;;   `icalendar-print-params'
+
+;;; Code:
+
+(require 'icalendar-macs)
+(require 'icalendar-ast)
+(require 'cl-lib)
+(require 'subr-x)
+(require 'seq)
+(require 'rx)
+(require 'calendar)
+(require 'time-date)
+(require 'simple)
+(require 'help-mode)
+
+;;; 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.
+
+(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. 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."
+  (with-current-buffer (or buffer (current-buffer))
+    (with-restriction start end
+      (goto-char (point-min))
+      (while (re-search-forward (rx (seq "\r\n" (or " " "\t")))
+                                nil t)
+        (replace-match "" nil nil)))))
+
+(defun ical:unfold-region (start end &optional buffer)
+  "Unfold a 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).
+
+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)))
+      ;; 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
+        (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))))
+
+(defun ical:unfolded-buffer-from-region (start end &optional buffer)
+  "Create a new buffer with the same contents as the region between
+START and END (in BUFFER, if provided) and perform 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
+                     (concat (buffer-name old-buffer)
+                             "~UNFOLDED")))) ;; TODO: again, move to modeline?
+    (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)))
+    uf-buffer))
+
+(defun ical:unfolded-buffer-from-buffer (buffer)
+  "Create a new buffer with the same contents as BUFFER and perform
+line unfolding 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:unfolded-buffer-from-file (filename &optional visit beg end)
+    "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'. Optional arguments VISIT, BEG, END
+are as in `insert-file-contents'. Returns the new buffer."
+    (unless (and (file-exists-p filename)
+                 (file-readable-p filename))
+      (error "File cannot be read: %s" filename))
+    ;; TODO: instead of messing with the buffer name, it might be more
+    ;; useful to keep track of the folding state in a variable and
+    ;; display it somewhere else in the mode line
+    (let ((uf-buffer (generate-new-buffer (concat (file-name-nondirectory filename)
+                                                  "~UNFOLDED"))))
+      (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))
+        (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)))
+      uf-buffer))
+
+(defun ical:fold-region (begin end &optional use-tabs)
+  "Fold all content lines in the region 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."
+  ;; TODO: also make this a command so it can be run manually?
+  (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
+           (concat
+            ;; if \n will be translated to \r\n on save (EOL type 1,
+            ;; "DOS"), just insert \n, otherwise the full fold sequence:
+            ;; FIXME: is buffer-file-coding-system the only relevant one here?
+            ;; What if the buffer is not visiting a file, but has come from a
+            ;; process, represents a mime part in an email, etc.?
+            (if (eq 1 (coding-system-eol-type buffer-file-coding-system))
+                "\n"
+              "\r\n")
+            ;; leading whitespace after line break:
+            (if use-tabs "\t" " "))))
+      (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))))
+          (insert line-fold)
+          (set-marker eol (point)))
+        (setq bol (goto-char (1+ eol)))))))
+
+(defun ical:contains-folded-lines-p ()
+  "Determine whether the current buffer contains folded content
+lines that should be unfolded for parsing and display purposes.
+If it does, return the position at the end of the first fold."
+  (save-excursion
+    (goto-char (point-min))
+    (re-search-forward (rx (seq line-start (or " " "\t")))
+                       nil t)))
+
+(defun ical:contains-unfolded-lines-p ()
+  "Determine whether the current buffer contains long content lines
+that should be folded before saving or transmitting. If it does,
+return the position at the beginning of the first line that
+requires folding."
+  (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))))
+
+
+;; 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")
+
+;; 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")
+
+;;;; Some utilities:
+(defun ical:parse-one-of (types limit)
+  "Parse a value of one of the TYPES, which should be a list of type
+symbols, from point up to LIMIT. 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
+      (signal 'ical:parse-error
+              (list (format "Unable to parse any of %s between %d and %d"
+                            types start limit))))
+    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)
+                   (signal 'ical:parse-error
+                           (list (format "Expected list of values matching '%s'"
+                                         value-regex)
+                                 s)))
+                 (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)
+  "Returns non-nil if each value in LIST satisfies TYPE according to
+`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 val :original-value)
+             (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 (signal 'ical:print-error
+                   (list (format "Don't know how to print value: %s" val)
+                         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 alnum "-")))
+
+(rx-define ical:x-name
+  (seq "X-"
+      (zero-or-one (>= 3 (any alnum)) "-") ; Vendor ID
+      (one-or-more (any alnum "-")))) ; 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.
+
+(defconst ical:value-types nil ;; populated by define-type
+  "Alist mapping value type strings in `icalendar-valuetypeparam'
+parameters to type symbols defined with `icalendar-define-type'")
+
+(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 :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)
+      (signal 'ical:parse-error
+              (list (format "No %s value between %d and %s"
+                            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 is a type symbol representing a printable
+iCalendar value type, i.e., 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).
+
+This means that 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 an iCalendar 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 (signal 'ical:parse-error
+                     (list "Expected 'TRUE' or 'FALSE'" 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'."
+  (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)))
+    (make-decoded-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'."
+  ;; FIXME: this should probably be defined alongside the
+  ;; other decoded-time-* functions!
+  (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)
+  ;; FIXME: this should probably be defined alongside the
+  ;; other decoded-time-* functions!
+  (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'."
+  (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)))
+    (list second minute hour day month year
+          (calendar-day-of-week (list month day year))
+          -1 ; DST information not available
+          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
+         (and
+          (cl-typep seconds 'integer)
+          (cl-typep minutes 'integer)
+          (cl-typep hours 'integer)
+          (cl-typep days 'integer)
+          (not (and (zerop seconds) (zerop minutes) (zerop hours)
+                    (zerop days)))))))
+
+(ical:define-type ical:dur-value "DUR-VALUE" ; avoid name clashes with 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-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-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))))
+
+(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)))))
+    (list start end 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. (This representation allows END to
+be computed from DUR and cached, and also distinguishes DUR and
+END, which might both be decoded times.)"
+  '(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
+(as part of a BYDAY recur rule part). See `icalendar-read-weekdaynum'
+for the value format."
+  (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'."
+  (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
+  ;; Note: "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), part of a
+recurrence rule BYDAY value"
+  (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 gory 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 specified 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 specified in
+RECUR-VALUE, or 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
+  (not (or ?\" ?\; ?: ?\\ ?, ical:control))) ;; TODO: is this correct?
+
+(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 the region of text in 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 (min (point) (mark))) buf)
+      (set-marker e (or end (max (point) (mark))))
+      (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, replacing
+literal '\\n' and '\\N' with newline, and removing 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, replacing literal '\\n' and '\\N'
+with newline, and removing 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, replacing newlines with
+literal '\\n', and escaping 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 S, replacing newlines with '\\n', and escaping
+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 a 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'."
+  (let ((val (ical:ast-node-value node)))
+    (if (stringp val)
+           val
+      (let* ((beg (ical:text-region-begin val))
+             (buf (marker-buffer beg))
+             (end (ical:text-region-end val)))
+        (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
+(require 'icalendar-uri-schemes)
+(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. We make an effort to parse the scheme, even though this
+  ;; is an open-ended list, because otherwise the regex is either too
+  ;; permissive or too complicated to be useful. (ical:binary, in
+  ;; particular, matches a subset of the characters allowed in a URI).
+  ;; TODO: should we parse more structure here?
+  (seq (group-n 11 ical:uri-scheme)
+       ":"
+       (group-n 12
+         (one-or-more
+          (any alnum ?- ?. ?_ ?~                   ; 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
+
+(defconst ical:params-font-lock-keywords nil ;; populated by ical:define-param
+  "Entries for iCalendar property parameters in `font-lock-keywords'.")
+
+(defconst 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-of 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
+                        :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))))
+    (unless (re-search-forward full-value-regex limit t)
+      (signal 'ical:parse-error
+              (list (format "Unable to parse `%s' value between %d and %d"
+                            type (point) limit))))
+    (when (match-string 3)
+      (signal 'ical:parse-error
+              (list (format "Invalid value for `%s' parameter" 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 nil))
+    (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 (or (alist-get (upcase param-name)
+                                               ical:param-types
+                                               nil nil #'equal)
+                                    'ical:otherparam))
+                    (param-node (ical:parse-param-value param-type limit)))
+          (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))
+         (list-sep (get param-type 'ical:list-sep))
+
+         (val/s (ical:ast-node-value node))
+         (printed (if (and list-sep (listp val/s))
+                      (mapcar #'ical:default-value-printer val/s)
+                    (ical:default-value-printer val/s)))
+         ;; add quotes to each value as needed, even if :quoted
+         ;; does not require it:
+         (must-quote (get param-type 'ical:is-quoted))
+         (quoted (if (listp printed)
+                     (mapcar
+                      (lambda (v) (ical:maybe-quote-param-value v must-quote))
+                      printed)
+                   (ical:maybe-quote-param-value printed must-quote)))
+         (val-str (or (ical:ast-node-meta-get node :original-value)
+                      (if (and list-sep (listp quoted))
+                          (string-join quoted list-sep)
+                        quoted)))
+         (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 node :original-name))))
+    (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."
+  (apply #'concat
+    (mapcar #'ical:print-param-node
+            param-nodes)))
+
+;; Parameter definitions in RFC5545:
+
+(ical:define-param ical:altrepparam "ALTREP"
+  "Alternate text representation (URI)"
+  ical:uri
+  :quoted t
+  :value-face ical:uri
+  :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"
+  :value-face ical:keyword
+  :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 ","
+  :value-face ical:uri
+  :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 ","
+  :value-face ical:uri
+  :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
+   :value-face ical:uri
+   :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"
+  :value-face ical:keyword
+  :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 alnum ?! ?# ?$ ?& ?. ?+ ?- ?^ ?_)))
+
+(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 that 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"
+  :value-face ical:keyword
+  :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 alnum ?-)))
+
+(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 ","
+  :value-face ical:uri
+  :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"
+  :value-face ical:keyword
+  :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"
+  :value-face ical:keyword
+  :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"
+  :value-face ical:keyword
+  :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"
+  :value-face ical:keyword
+  :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"
+  :value-face ical:keyword
+  :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"
+  :value-face ical:keyword
+  :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 for
+  ;; now, because coding around the exception is not worth it: it
+  ;; requires some hackery to work 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
+  :value-face ical:uri
+  :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."
+  ;; TODO: "This parameter MUST be specified on the "DTSTART","DTEND",
+  ;; "DUE", "EXDATE", and "RDATE" properties when either a DATE-TIME
+  ;; or TIME value type is specified and when the value is neither a
+  ;; UTC or a "floating" time."
+  ;; TODO: "The "TZID" property parameter MUST NOT be applied to DATE
+  ;; properties and DATE-TIME or TIME properties whose time values are
+  ;; specified in UTC."
+  (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
+  :value-face ical:keyword
+  :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
+  :name-face font-lock-comment-face
+  :value-face font-lock-comment-face)
+
+(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:
+
+(defconst ical:properties-font-lock-keywords
+  nil ;; populated by ical:define-property
+  "Entries for iCalendar properties in `font-lock-keywords'.")
+
+(defconst 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
+                        :value value
+                        :original-value unrecognized-val
+                        :children 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 ((full-value-regex (rx-to-string (get type 'ical:full-value-rx))))
+
+    (unless (re-search-forward full-value-regex limit t)
+      (signal 'ical:parse-error
+              (list (format "Unable to parse `%s' property value between %d and %d"
+                            type (point) limit))))
+
+    (when (match-string 3)
+      (signal 'ical:parse-error
+              (list (format "Invalid value for `%s' property" type)
+                    (match-string 3))))
+
+    (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."
+  (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)))
+         (params (ical:ast-node-children node))
+         (value (ical:ast-node-value node))
+         (value-str
+          (or (ical:ast-node-meta-get node :original-value)
+              (if list-sep
+                  (string-join (mapcar #'ical:default-value-printer value)
+                               list-sep)
+                (ical:default-value-printer value))))
+         (name-str (or property-name
+                       (ical:ast-node-meta-get node :original-name))))
+
+    (unless (and (stringp name-str)
+                 (length> name-str 0))
+      (signal 'ical:print-error
+              (list (format "Unknown property name for type `%s'" type)
+                    type node)))
+
+    (concat name-str
+            (ical:print-params params)
+            ":"
+            value-str
+            ;; TODO: make line ending sensitive to coding system?
+            "\r\n")))
+
+(defun ical:maybe-add-value-param (property-node)
+  "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))
+              (signal 'ical:validation-error
+                      (list (format (concat "Mismatching VALUE parameter. "
+                                            "VALUE specifies %s but "
+                                            "property value has type %s")
+                                    expected-type value-type))))
+          ;; 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
+                                      :value (ical:make-ast-node
+                                              'ical:printed-value-type
+                                              :value value-type)))
+                 (new-params (cons valuetype-param
+                                   (ical:ast-node-children property-node))))
+            (setf (nth 3 property-node) new-params))))
+
+      ;; Return the modified property node:
+      property-node)))
+
+(defun ical:value-type-from-params (params)
+  "If there is an `icalendar-valuetypeparam' in PARAMS, return the
+type symbol associated with the value type it specifies."
+  (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 nil)
+          (line-end nil)
+          (property-name nil)
+          (params nil))
+
+      ;; Property name
+      (unless (re-search-forward (rx ical:property-start) limit t)
+        (signal 'ical:parse-error
+                (list (format (concat "Malformed property at line %d, position %d:"
+                                      "could not match property name")
+                              (line-number-at-pos (point))
+                              (line-beginning-position)))))
+
+      (setq property-name (match-string 1))
+      (setq line-begin (line-beginning-position))
+      (setq line-end (line-end-position))
+
+      ;; Parameters
+      (when (looking-at ";")
+        (setq params (ical:parse-params line-end)))
+      ;; TODO: param validation?
+
+      (unless (looking-at ":")
+        (signal 'ical:parse-error
+                (list (format (concat "Malformed property at line %d, position %d:"
+                                      "missing colon before value")
+                              (line-number-at-pos (point))
+                              (point)))))
+      (forward-char)
+
+      ;; Value
+      (let* ((known-type (alist-get (upcase property-name)
+                                    ical:property-types
+                                    nil nil #'equal))
+             (property-type (or known-type 'ical:other-property))
+             (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))
+          (signal 'ical:parse-error
+                  (list (format "Property value did not consume line %d: %s"
+                                (line-number-at-pos (point))
+                                (ical:default-value-printer
+                                 (ical:ast-node-value node))))))
+
+        ;; Set point up for the next property parser:
+        (while (not (bolp))
+          (forward-char))
+
+        ;; 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))
+
+        ;; 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))
+  :value-face ical:keyword
+  :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, though RFC5545 does not specify any
+particular way of creating such an 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
+  :extra-faces ((13 'ical:uri t t)
+                (14 'ical:binary-data t t))
+  :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))))
+        (signal 'ical:validation-error
+                (list (concat "`icalendar-binary' attachment requires "
+                              "'VALUE=BINARY' parameter")
+                      node)))
+      (unless (and (ical:ast-node-p encodingparam)
+                   (equal "BASE64" (ical:ast-node-value encodingparam)))
+        (signal 'ical:validation-error
+                (list (concat "`icalendar-binary' attachment requires "
+                              "'ENCODING=BASE64' parameter")
+                      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))
+  :value-face ical:keyword
+  :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
+  :value-face ical:numeric-types
+  :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))
+  :value-face ical:numeric-types
+  :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))
+  :value-face ical:numeric-types
+  :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-keyword-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."
+  ;; 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))
+  :value-face ical:keyword
+  :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
+
+(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
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:keyword
+  :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 timezone 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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:uri
+  :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))
+  :value-face ical:uri
+  :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))
+  :value-face ical:uri
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:uri
+  :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)
+  :list-sep ","
+  :child-spec (:zero-or-one (ical:valuetypeparam ical:tzidparam)
+               :zero-or-more (ical:otherparam))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:recurrence-rule
+  :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)))
+  :default-type ical:text
+  :child-spec (:zero-or-more (ical:otherparam))
+  :value-face ical:keyword
+  :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))
+  :value-face ical:numeric-types
+  :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)
+  :child-spec (:zero-or-one (ical:valuetypeparam ical:trigrelparam)
+               :zero-or-more (ical:otherparam))
+  :other-validator ical:trigger-validator
+  :value-face ical:date-time-types
+  :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)
+          (signal 'ical:validation-error
+                  (list (concat "Explicit `icalendar-valuetypeparam' required in "
+                                "`icalendar-trigger' with non-duration value")
+                        node)))
+        (when (ical:ast-node-first-child-of 'ical:trigrelparam node)
+          (signal 'ical:validation-error
+                  (list (concat "`icalendar-trigrelparam' not allowed in "
+                                "`icalendar-trigger' with non-duration value"))))
+        (unless (ical:date-time-is-utc-p dt-value)
+          (signal 'ical:validation-error
+                  (list (concat "`icalendar-date-time' value of "
+                                "`icalendar-trigger' must be in UTC time")
+                        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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:date-time-types
+  :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))
+  :value-face ical:numeric-types
+  :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")
+
+(defconst ical:ignored-properties-font-lock-keywords
+  `((,(rx ical:other-property) (1 'ical:ignored keep)
+                               (2 'ical:ignored keep)))
+  "Entries for iCalendar ignored properties in `font-lock-keywords'.")
+
+(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
+
+(defconst ical:components-font-lock-keywords
+  nil ;; populated by ical:define-component
+  "Entries for iCalendar components in `font-lock-keywords'.")
+
+(defconst 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)
+      (signal 'ical:parse-error
+              (list (format "Not at start of a component at line %d, position %d"
+                            (line-number-at-pos (point))
+                            (point)))))
+
+    (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))
+           (children nil))
+
+      ;; Find end of component:
+      (save-excursion
+        (if (re-search-forward
+             (rx-to-string `(seq line-start "END:" ,component-name line-end))
+             limit t)
+            (setq end-pos (match-end 0)
+                  body-end-pos (1- (match-beginning 0))) ; end of prev. line
+          (signal 'ical:parse-error
+                  (list (format (concat "Matching END: of component %s not found "
+                                        "between %d and %d")
+                                component-name begin-pos limit)))))
+
+      (while (not (bolp)) (forward-char))
+
+      ;; Parse the properties and subcomponents of this component:
+      (while (<= (point) body-end-pos)
+        (push (ical:parse-property-or-component end-pos)
+              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:
+      (ical:make-ast-node component-type
+                          :children (nreverse children)
+                          :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))))
+
+(defun ical:parse-property-or-component (limit)
+  "Parse a component or a property at point.
+Point should be at the beginning of a line which begins a
+component or contains a property."
+  (cond ((looking-at (rx line-start "BEGIN:" ical:name line-end))
+         (icalendar-parse-component limit))
+        ((looking-at (rx line-start ical:name))
+         (icalendar-parse-property (line-end-position)))
+        (t (signal 'ical:parse-error
+                   (list (format (concat "Not at start of property or component "
+                                         "at line %d, position %d")
+                                 (line-number-at-pos (point))
+                                 (point)))))))
+
+(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 node :original-name)
+                   (car (rassq type ical:component-types))))
+         (children (ical:ast-node-children node)))
+
+    (unless name
+      (signal 'ical:print-error
+              (list (format "Unknown component name for type `%s'" type)
+                    type node)))
+
+    (concat
+     ;; TODO: should line ending be sensitive to buffer coding system?
+     (format "BEGIN:%s\r\n" name)
+     (apply #'concat
+            (mapcar #'ical:print-property-or-component children))
+     (format "END:%s\r\n" name))))
+
+(defun ical:print-property-or-component (node)
+  "Serialize a property or component node NODE to a string."
+  (let ((type (ical:ast-node-type node)))
+    (cond ((get type 'ical:is-property)
+           (ical:print-property-node node))
+          ((get type 'ical:is-component)
+           (ical:print-component-node node))
+          (t (signal 'ical:print-error
+                     (list (format "Not a component or property 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:recurid
+                             ical:rrule)
+               :zero-or-more (ical:attach
+                              ical:attendee
+                              ical:categories
+                              ical:comment
+                              ical:contact
+                              ical:exdate
+                              ical:rstatus
+                              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)
+  "When component NODE has an `icalendar-rrule', validate that its
+`icalendar-dtstart', `icalendar-rdate', and `icalendar-exdate' properties
+satisfy the requirements imposed by this rule."
+  (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 (mapcan #'ical:ast-node-value rdates)))
+         (exdates (ical:ast-node-children-of 'ical:exdate node))
+         (excluded (when exdates (mapcan #'ical:ast-node-value exdates))))
+    (when rrule
+      (unless dtstart
+        (signal 'ical:validation-error
+                (list (concat "An `icalendar-rrule' requires an "
+                              "`icalendar-dtstart' property")
+                      node)))
+      (when included
+        (unless (ical:list-of-p (ical:ast-node-type start) included)
+          (signal 'ical:validation-error
+                 (list (concat "`icalendar-rdate' values must agree with type "
+                              "of `icalendar-dtstart' property")
+                       node))))
+      (when excluded
+        (unless (ical:list-of-p (ical:ast-node-type start) excluded)
+          (signal 'ical:validation-error
+                 (list (concat "`icalendar-exdate' values must agree with type "
+                              "of `icalendar-dtstart' property")
+                       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))
+            (signal 'ical:validation-error
+                    (list (concat "`icalendar-rrule' must not contain time-based "
+                                  "rules when `icalendar-dtstart' is a plain date")
+                          node))))
+        (when until
+          (unless (eq (ical:ast-node-type start)
+                      (ical:ast-node-type until))
+            (signal 'ical:validation-error
+                    (list (concat "`icalendar-rrule' UNTIL clause must agree with "
+                                  "type of `icalendar-dtstart' property")
+                          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)))
+                (signal 'ical:validation-error
+                        (list (concat "`icalendar-rrule' UNTIL clause must be in "
+                                      "local time if `icalendar-dtstart' is")
+                              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)))
+                (signal 'ical:validation-error
+                        (list (concat "`icalendar-rrule' UNTIL clause must be in "
+                                      "UTC time if `icalendar-dtstart' has a "
+                                      "defined time zone")
+                              node)))))
+          ;; "In the case of the "STANDARD" and "DAYLIGHT"
+          ;; sub-components the UNTIL rule part MUST always be
+          ;; specified as a date with UTC time":
+          (when (memq (ical:ast-node-type node) '(ical:standard ical:daylight))
+            (unless (ical:date-time-is-utc-p until)
+              (signal 'ical:validation-error
+                      (list (concat "`icalendar-rrule' UNTIL clause must be in "
+                                      "UTC time in `icalendar-standard' and "
+                                      "`icalendar-daylight' components")
+                            node)))))))
+    ;; Success:
+    node))
+
+(defun ical:vevent-validator (node)
+  "Additional validator for an `icalendar-vevent' 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
+VEVENT nodes; it is not normally necessary to call it directly."
+  (let* ((duration (ical:ast-node-first-child-of 'ical:duration node))
+         (dtend (ical:ast-node-first-child-of 'ical:dtend node)))
+    (when (and dtend duration)
+      (signal 'ical:validation-error
+              (list (concat "`icalendar-dtend' and `icalendar-duration' "
+                            "properties must not appear in the same "
+                            "`icalendar-vevent'")
+                    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)
+      (signal 'ical:validation-error
+              (list (concat "`icalendar-due' and `icalendar-duration' properties "
+                            "must not appear in the same `icalendar-vtodo'")
+                    node)))
+    (when (and duration (not dtstart))
+      (signal 'ical:validation-error
+              (list (concat "`icalendar-duration' requires `icalendar-dtstart' "
+                            "property in the same `icalendar-vtodo'")
+                    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:recurid
+                             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:rstatus
+                              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:rstatus
+                              ical:other-property))
+  :link "https://www.rfc-editor.org/rfc/rfc5545#section-3.6.4")
+
+(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 timezone 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)))
+      (signal 'ical:validation-error
+              (list (concat "`icalendar-timezone' must have at least one "
+                            "`icalendar-standard' or `icalendar-daylight' child")
+                    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))
+      (signal 'ical:validation-error
+              (list (concat "`icalendar-valarm' node with `icalendar-duration' "
+                            "must also have `icalendar-repeat' property")
+                    node)))
+
+    (when (and repeat (not duration))
+      (signal 'ical:validation-error
+              (list (concat "`icalendar-valarm' node with `icalendar-repeat' "
+                            "must also have `icalendar-duration' property")
+                    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)
+               (signal 'ical:validation-error
+                       (list (concat "AUDIO `icalendar-valarm' may not have "
+                                     "more than one `icalendar-attach'")
+                             node)))
+             node)
+
+            ((equal "DISPLAY" action-str)
+             (unless (= 1 (alist-get 'ical:description child-counts 0))
+               (signal 'ical:validation-error
+                       (list (concat "DISPLAY `icalendar-valarm' must have "
+                                     "exactly one `icalendar-description'")
+                             node)))
+             node)
+
+            ((equal "EMAIL" action-str)
+             (unless (= 1 (alist-get 'ical:summary child-counts 0))
+               (signal 'ical:validation-error
+                       (list (concat "EMAIL `icalendar-valarm' must have "
+                                     "exactly one `icalendar-summary'")
+                             node)))
+             (unless (= 1 (alist-get 'ical:description child-counts 0))
+               (signal 'ical:validation-error
+                       (list (concat "EMAIL `icalendar-valarm' must have "
+                                     "exactly one `icalendar-description'")
+                             node)))
+             (unless (<= 1 (alist-get 'ical:attendee child-counts 0))
+               (signal 'ical:validation-error
+                       (list (concat "EMAIL `icalendar-valarm' must have "
+                                     "at least one `icalendar-attendee'")
+                             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:
+             (warn (format "Unknown ACTION value in VALARM: %s" action-str))
+             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.
+;; TODO: 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 timezone 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 search NODE for `icalendar-tzidparam' nodes and
+return a list of their values"
+  (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 timezone component has a TZID property and
+                         ;; extract its string value:
+                         (when (ical:ast-node-valid-p tz)
+                           (ical:text-to-string
+                            (ical:ast-node-value
+                             (ical:ast-node-first-child-of 'ical:tzid tz)))))
+                       tz-children))
+         (appearing-tzids (ical:all-tzidparams-in node)))
+    (unless comp-children
+      (signal 'ical:validation-error
+              (list (concat "`icalendar-vcalendar' must contain "
+                            "at least one component")
+                    node)))
+
+    (let ((seen nil))
+      (dolist (tzid appearing-tzids)
+        (unless (member tzid seen)
+          (unless (member tzid defined-tzs)
+            (signal 'ical:validation-error
+                    (list (format "No VTIMEZONE with TZID '%s' in calendar" tzid)
+                          node))))
+        (push tzid seen)))
+
+    ;; success:
+    node))
+
+;; TODO: parse-calendar and print-calendar functions.  parse-component
+;; is sufficient to parse all the syntax in a calendar, but a
+;; calendar-level parsing function is needed to add support for
+;; timezones. This function should ensure that every
+;; `icalendar-tzidparam' in the calendar has a corresponding
+;; `icalendar-vtimezone' component, and modify the zone information of
+;; the parsed date-time according to the offset in that timezone (and
+;; the print function should do the inverse). Calculating the offsets,
+;; however, is dependent on an implementation of recurrence rules which
+;; is still in the works.
+
+
+
+
+;;; Documentation for all of the above via `describe-symbol':
+(defun icalendar-documented-symbol-p (sym)
+  "iCalendar symbol predicate for `describe-symbol-backends'"
+  (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 icalendar-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))
+       (unintern sym obarray))))
+
+  (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-"))
+;; End:
+;;; icalendar-parser.el ends here
diff --git a/lisp/calendar/icalendar-uri-schemes.el b/lisp/calendar/icalendar-uri-schemes.el
new file mode 100644
index 00000000000..c94a36c13d8
--- /dev/null
+++ b/lisp/calendar/icalendar-uri-schemes.el
@@ -0,0 +1,444 @@
+;;; icalendar-uri-schemes.el --- URI schemes in iCalendar -*- lexical-binding:t -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: Richard Lawrence <[email-masked]>
+;; Maintainer: [email-masked]
+;; 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 one (large) regular expression, ical:uri-scheme,
+;; to match URI schemes registered with IANA.
+;;
+;; The schemes are listed at:
+;;   https://www.iana.org/assignments/uri-schemes/uri-schemes.txt
+;; Note the licensing terms for this list, available at:
+;;   https://www.iana.org/help/licensing-terms
+;; which as of 2024-10-24 says:
+;;
+;;   IANA and IETF desire to (a) dedicate any applicable copyright
+;;   rights that they may own in the Protocol Registries to the public
+;;   domain, and (b) license any copyright or related rights for which
+;;   they are a licensee (with a right to sublicense) to the broadest
+;;   extent that they are permitted to do so. Accordingly, both IANA
+;;   and IETF affirm that any applicable rights that they may have in
+;;   the Protocol Registries are subject to the Creative Commons CC0
+;;   1.0 dedication found at
+;;   https://creativecommons.org/publicdomain/zero/1.0/legalcode
+;;
+;; This file is current as of 2024-10-24.
+
+;;; Code:
+(require 'rx)
+
+(rx-define ical:uri-scheme (or
+"aaa"
+"aaas"
+"about"
+"acap"
+"acct"
+"acd"
+"acr"
+"adiumxtra"
+"adt"
+"afp"
+"afs"
+"aim"
+"amss"
+"android"
+"appdata"
+"apt"
+"ar"
+"ark"
+"at"
+"attachment"
+"aw"
+"barion"
+"bb"
+"beshare"
+"bitcoin"
+"bitcoincash"
+"blob"
+"bluetooth"
+"bolo"
+"brid"
+"browserext"
+"cabal"
+"calculator"
+"callto"
+"cap"
+"cast"
+"casts"
+"chrome"
+"chrome-extension"
+"cid"
+"coap"
+"coap+tcp"
+"coap+ws"
+"coaps"
+"coaps+tcp"
+"coaps+ws"
+"com-eventbrite-attendee"
+"content"
+"content-type"
+"crid"
+"cstr"
+"cvs"
+"dab"
+"dat"
+"data"
+"dav"
+"dhttp"
+"diaspora"
+"dict"
+"did"
+"dis"
+"dlna-playcontainer"
+"dlna-playsingle"
+"dns"
+"dntp"
+"doi"
+"dpp"
+"drm"
+"drop"
+"dtmi"
+"dtn"
+"dvb"
+"dvx"
+"dweb"
+"ed2k"
+"eid"
+"elsi"
+"embedded"
+"ens"
+"ethereum"
+"example"
+"facetime"
+"fax"
+"feed"
+"feedready"
+"fido"
+"file"
+"filesystem"
+"finger"
+"first-run-pen-experience"
+"fish"
+"fm"
+"ftp"
+"fuchsia-pkg"
+"geo"
+"gg"
+"git"
+"gitoid"
+"gizmoproject"
+"go"
+"gopher"
+"graph"
+"grd"
+"gtalk"
+"h323"
+"ham"
+"hcap"
+"hcp"
+"hs20"
+"http"
+"https"
+"hxxp"
+"hxxps"
+"hydrazone"
+"hyper"
+"iax"
+"icap"
+"icon"
+"im"
+"imap"
+"info"
+"iotdisco"
+"ipfs"
+"ipn"
+"ipns"
+"ipp"
+"ipps"
+"irc"
+"irc6"
+"ircs"
+"iris"
+"iris.beep"
+"iris.lwz"
+"iris.xpc"
+"iris.xpcs"
+"isostore"
+"itms"
+"jabber"
+"jar"
+"jms"
+"keyparc"
+"lastfm"
+"lbry"
+"ldap"
+"ldaps"
+"leaptofrogans"
+"lid"
+"lorawan"
+"lpa"
+"lvlt"
+"machineProvisioningProgressReporter"
+"magnet"
+"mailserver"
+"mailto"
+"maps"
+"market"
+"matrix"
+"message"
+"microsoft.windows.camera"
+"microsoft.windows.camera.multipicker"
+"microsoft.windows.camera.picker"
+"mid"
+"mms"
+"modem"
+"mongodb"
+"moz"
+"ms-access"
+"ms-appinstaller"
+"ms-browser-extension"
+"ms-calculator"
+"ms-drive-to"
+"ms-enrollment"
+"ms-excel"
+"ms-eyecontrolspeech"
+"ms-gamebarservices"
+"ms-gamingoverlay"
+"ms-getoffice"
+"ms-help"
+"ms-infopath"
+"ms-inputapp"
+"ms-launchremotedesktop"
+"ms-lockscreencomponent-config"
+"ms-media-stream-id"
+"ms-meetnow"
+"ms-mixedrealitycapture"
+"ms-mobileplans"
+"ms-newsandinterests"
+"ms-officeapp"
+"ms-people"
+"ms-personacard"
+"ms-project"
+"ms-powerpoint"
+"ms-publisher"
+"ms-recall"
+"ms-remotedesktop"
+"ms-remotedesktop-launch"
+"ms-restoretabcompanion"
+"ms-screenclip"
+"ms-screensketch"
+"ms-search"
+"ms-search-repair"
+"ms-secondary-screen-controller"
+"ms-secondary-screen-setup"
+"ms-settings"
+"ms-settings-airplanemode"
+"ms-settings-bluetooth"
+"ms-settings-camera"
+"ms-settings-cellular"
+"ms-settings-cloudstorage"
+"ms-settings-connectabledevices"
+"ms-settings-displays-topology"
+"ms-settings-emailandaccounts"
+"ms-settings-language"
+"ms-settings-location"
+"ms-settings-lock"
+"ms-settings-nfctransactions"
+"ms-settings-notifications"
+"ms-settings-power"
+"ms-settings-privacy"
+"ms-settings-proximity"
+"ms-settings-screenrotation"
+"ms-settings-wifi"
+"ms-settings-workplace"
+"ms-spd"
+"ms-stickers"
+"ms-sttoverlay"
+"ms-transit-to"
+"ms-useractivityset"
+"ms-virtualtouchpad"
+"ms-visio"
+"ms-walk-to"
+"ms-whiteboard"
+"ms-whiteboard-cmd"
+"ms-word"
+"msnim"
+"msrp"
+"msrps"
+"mss"
+"mt"
+"mtqp"
+"mumble"
+"mupdate"
+"mvn"
+"mvrp"
+"mvrps"
+"news"
+"nfs"
+"ni"
+"nih"
+"nntp"
+"notes"
+"num"
+"ocf"
+"oid"
+"onenote"
+"onenote-cmd"
+"opaquelocktoken"
+"openid"
+"openpgp4fpr"
+"otpauth"
+"p1"
+"pack"
+"palm"
+"paparazzi"
+"payment"
+"payto"
+"pkcs11"
+"platform"
+"pop"
+"pres"
+"prospero"
+"proxy"
+"pwid"
+"psyc"
+"pttp"
+"qb"
+"query"
+"quic-transport"
+"redis"
+"rediss"
+"reload"
+"res"
+"resource"
+"rmi"
+"rsync"
+"rtmfp"
+"rtmp"
+"rtsp"
+"rtsps"
+"rtspu"
+"sarif"
+"secondlife"
+"secret-token"
+"service"
+"session"
+"sftp"
+"sgn"
+"shc"
+"shttp"
+"sieve"
+"simpleledger"
+"simplex"
+"sip"
+"sips"
+"skype"
+"smb"
+"smp"
+"sms"
+"smtp"
+"snews"
+"snmp"
+"soap.beep"
+"soap.beeps"
+"soldat"
+"spiffe"
+"spotify"
+"ssb"
+"ssh"
+"starknet"
+"steam"
+"stun"
+"stuns"
+"submit"
+"svn"
+"swh"
+"swid"
+"swidpath"
+"tag"
+"taler"
+"teamspeak"
+"tel"
+"teliaeid"
+"telnet"
+"tftp"
+"things"
+"thismessage"
+"thzp"
+"tip"
+"tn3270"
+"tool"
+"turn"
+"turns"
+"tv"
+"udp"
+"unreal"
+"upt"
+"urn"
+"ut2004"
+"uuid-in-package"
+"v-event"
+"vemmi"
+"ventrilo"
+"ves"
+"videotex"
+"vnc"
+"view-source"
+"vscode"
+"vscode-insiders"
+"vsls"
+"w3"
+"wais"
+"web3"
+"wcr"
+"webcal"
+"web+ap"
+"wifi"
+"wpid"
+"ws"
+"wss"
+"wtai"
+"wyciwyg"
+"xcon"
+"xcon-userid"
+"xfire"
+"xmlrpc.beep"
+"xmlrpc.beeps"
+"xmpp"
+"xftp"
+"xrcp"
+"xri"
+"ymsgr"
+"z39.50"
+"z39.50r"
+"z39.50s"
+))
+
+(provide 'icalendar-uri-schemes)
+
+;; Local Variables:
+;; read-symbol-shorthands: (("ical:" . "icalendar-"))
+;; End:
+;;; icalendar-uri-schemes.el ends here
diff --git a/test/lisp/calendar/icalendar-parser-tests.el b/test/lisp/calendar/icalendar-parser-tests.el
new file mode 100644
index 00000000000..a087fc93751
--- /dev/null
+++ b/test/lisp/calendar/icalendar-parser-tests.el
@@ -0,0 +1,1796 @@
+;;; tests/icalendar-parser.el --- Tests for icalendar-parser  -*- lexical-binding: t; -*-
+
+(require 'cl-lib)
+(require 'ert)
+(require 'icalendar-parser)
+
+(cl-defmacro ict:parse/print-test (string &key expected parser type printer source)
+  "Create a test which parses STRING, prints the resulting parse
+tree, and compares the printed version with STRING (or with
+EXPECTED, if given). If they are the same, the test passes.
+PARSER and PRINTER should be the parser and printer functions
+appropriate to STRING. TYPE, if given, should be the type of
+object PARSER is expected to parse; it will be passed as PARSER's
+first argument. SOURCE should be a symbol; it is used to name the
+test."
+  (let ((parser-form
+         (if type
+             `(funcall (function ,parser) (quote ,type) (point-max))
+           `(funcall (function ,parser) (point-max)))))
+    `(ert-deftest ,(intern (concat "ict:parse/print-" (symbol-name source))) ()
+       ,(format "Parse and reprint example from `%s'; pass if they match" source)
+       (let* ((parse-buf (get-buffer-create "*iCalendar Parse Test*"))
+              (print-buf (get-buffer-create "*iCalendar Print Test*"))
+              (unparsed ,string)
+              (expected (or ,expected unparsed))
+              (printed nil))
+         (set-buffer parse-buf)
+         (erase-buffer)
+         (insert unparsed)
+         (goto-char (point-min))
+         (let ((parsed ,parser-form))
+           (should (icalendar-ast-node-valid-p parsed))
+           (set-buffer print-buf)
+           (erase-buffer)
+           (insert (funcall (function ,printer) parsed))
+           ;; TODO: this may need adjusting if printers become coding-system aware
+           (decode-coding-region (point-min) (point-max) 'utf-8-dos)
+           (setq printed (buffer-substring-no-properties (point-min) (point-max)))
+           (should (equal expected printed)))))))
+
+(ict:parse/print-test
+"ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.1.1/1)
+
+(ict:parse/print-test
+"RDATE;VALUE=DATE:19970304,19970504,19970704,19970904\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.1.1/2)
+
+(ict:parse/print-test
+"ATTACH:http://example.com/public/quarterly-report.doc\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.1.3/1)
+
+(ict:parse/print-test
+;; Corrected. The original contains invalid base64 data; it was
+;; missing the final "=", as noted in errata ID 5602.
+;; The decoded string should read:
+;; The quick brown fox jumps over the lazy dog.
+"ATTACH;FMTTYPE=text/plain;ENCODING=BASE64;VALUE=BINARY:VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4=\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.1.3/2)
+
+(ict:parse/print-test
+"DESCRIPTION;ALTREP=\"cid:[email-masked]\":The Fall'98 Wild Wizards Conference - - Las Vegas\\, NV\\, USA\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2/1)
+
+(ict:parse/print-test
+"DESCRIPTION;ALTREP=\"CID:[email-masked]\": Project XYZ Review Meeting will include the following agenda items: (a) Market Overview\\, (b) Finances\\, (c) Project Management\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.1/1)
+
+(ict:parse/print-test
+"ORGANIZER;CN=\"John Smith\":mailto:[email-masked]\n"
+;; CN param value does not require quotes, so they're missing when
+;; re-printed:
+:expected "ORGANIZER;CN=John Smith:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.2/1)
+
+(ict:parse/print-test
+"ATTENDEE;CUTYPE=GROUP:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.3/1)
+
+(ict:parse/print-test
+"ATTENDEE;DELEGATED-FROM=\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.4/1)
+
+(ict:parse/print-test
+"ATTENDEE;DELEGATED-TO=\"mailto:[email-masked]\",\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.5/1)
+
+(ict:parse/print-test
+"ORGANIZER;DIR=\"ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.6/1)
+
+(ict:parse/print-test
+"ATTACH;FMTTYPE=text/plain;ENCODING=BASE64;VALUE=BINARY:TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2ljaW5nIGVsaXQsIHNlZCBkbyBlaXVzbW9kIHRlbXBvciBpbmNpZGlkdW50IHV0IGxhYm9yZSBldCBkb2xvcmUgbWFnbmEgYWxpcXVhLiBVdCBlbmltIGFkIG1pbmltIHZlbmlhbSwgcXVpcyBub3N0cnVkIGV4ZXJjaXRhdGlvbiB1bGxhbWNvIGxhYm9yaXMgbmlzaSB1dCBhbGlxdWlwIGV4IGVhIGNvbW1vZG8gY29uc2VxdWF0LiBEdWlzIGF1dGUgaXJ1cmUgZG9sb3IgaW4gcmVwcmVoZW5kZXJpdCBpbiB2b2x1cHRhdGUgdmVsaXQgZXNzZSBjaWxsdW0gZG9sb3JlIGV1IGZ1Z2lhdCBudWxsYSBwYXJpYXR1ci4gRXhjZXB0ZXVyIHNpbnQgb2NjYWVjYXQgY3VwaWRhdGF0IG5vbiBwcm9pZGVudCwgc3VudCBpbiBjdWxwYSBxdWkgb2ZmaWNpYSBkZXNlcnVudCBtb2xsaXQgYW5pbSBpZCBlc3QgbGFib3J1bS4=\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.7/1)
+
+(ict:parse/print-test
+"ATTACH;FMTTYPE=application/msword:ftp://example.com/pub/docs/agenda.doc\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.8/1)
+
+(ict:parse/print-test
+"FREEBUSY;FBTYPE=BUSY:19980415T133000Z/19980415T170000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.9/1)
+
+(ict:parse/print-test
+"SUMMARY;LANGUAGE=en-US:Company Holiday Party\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.10/1)
+
+(ict:parse/print-test
+"LOCATION;LANGUAGE=en:Germany\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.10/2)
+
+(ict:parse/print-test
+"LOCATION;LANGUAGE=no:Tyskland\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.10/3)
+
+(ict:parse/print-test
+"ATTENDEE;MEMBER=\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.11/1)
+
+(ict:parse/print-test
+"ATTENDEE;MEMBER=\"mailto:[email-masked]\",\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.11/2)
+
+(ict:parse/print-test
+"ATTENDEE;PARTSTAT=DECLINED:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.12/1)
+
+(ict:parse/print-test
+"RECURRENCE-ID;RANGE=THISANDFUTURE:19980401T133000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.13/1)
+
+(ict:parse/print-test
+"TRIGGER;RELATED=END:PT5M\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.14/1)
+
+(ict:parse/print-test
+"RELATED-TO;RELTYPE=SIBLING:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.15/1)
+
+(ict:parse/print-test
+"ATTENDEE;ROLE=CHAIR:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.16/1)
+
+(ict:parse/print-test
+"ATTENDEE;RSVP=TRUE:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.17/1)
+
+(ict:parse/print-test
+"ORGANIZER;SENT-BY=\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.18/1)
+
+(ict:parse/print-test
+"DTSTART;TZID=America/New_York:19980119T020000\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.19/1)
+
+(ict:parse/print-test
+"DTEND;TZID=America/New_York:19980119T030000\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.2.19/2)
+
+(ict:parse/print-test
+"ATTACH;FMTTYPE=image/vnd.microsoft.icon;ENCODING=BASE64;VALUE=BINARY:AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAgIAAAICAgADAwMAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAAAAAABNEMQAAAAAAAkQgAAAAAAJEREQgAAACECQ0QgEgAAQxQzM0E0AABERCRCREQAADRDJEJEQwAAAhA0QwEQAAAAAEREAAAAAAAAREQAAAAAAAAkQgAAAAAAAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.3.1/1)
+
+(ict:parse/print-test
+"TRUE"
+:type icalendar-boolean
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.2/1)
+
+(ict:parse/print-test
+"mailto:[email-masked]"
+:type icalendar-cal-address
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.3/1)
+
+(ict:parse/print-test
+"19970714"
+:type icalendar-date
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.4/1)
+
+(ict:parse/print-test
+;; 'Floating' time:
+"19980118T230000"
+:type icalendar-date-time
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.5/1)
+
+(ict:parse/print-test
+;; UTC time:
+"19980119T070000Z"
+:type icalendar-date-time
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.5/2)
+
+(ict:parse/print-test
+;; Leap second (seconds = 60)
+"19970630T235960Z"
+:type icalendar-date-time
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.5/3)
+
+(ict:parse/print-test
+;; Local time:
+"DTSTART:19970714T133000\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.3.5/4)
+
+(ict:parse/print-test
+;; UTC time:
+"DTSTART:19970714T173000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.3.5/5)
+
+(ict:parse/print-test
+;; Local time with TZ identifier:
+"DTSTART;TZID=America/New_York:19970714T133000\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.3.5/6)
+
+(ict:parse/print-test
+"P15DT5H0M20S"
+:expected "P15DT5H20S"
+:type icalendar-dur-value
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.6/1)
+
+(ict:parse/print-test
+"P7W"
+:type icalendar-dur-value
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.6/2)
+
+(ict:parse/print-test
+"1000000.0000001"
+:type icalendar-float
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.7/1)
+
+(ict:parse/print-test
+"1.333"
+:type icalendar-float
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.7/2)
+
+(ict:parse/print-test
+"-3.14"
+:type icalendar-float
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.7/3)
+
+(ict:parse/print-test
+"1234567890"
+:type icalendar-integer
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.8/1)
+
+(ict:parse/print-test
+"-1234567890"
+:type icalendar-integer
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.8/2)
+
+(ict:parse/print-test
+"+1234567890"
+;; "+" sign isn't required, so it's not re-printed:
+:expected "1234567890"
+:type icalendar-integer
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.8/3)
+
+(ict:parse/print-test
+"432109876"
+:type icalendar-integer
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.8/4)
+
+(ict:parse/print-test
+"19970101T180000Z/19970102T070000Z"
+:type icalendar-period
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.9/1)
+
+(ict:parse/print-test
+"19970101T180000Z/PT5H30M"
+:type icalendar-period
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.9/2)
+
+(ict:parse/print-test
+"FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1"
+:type icalendar-recur
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.10/1)
+
+(ict:parse/print-test
+"FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU;BYHOUR=8,9;BYMINUTE=30"
+:type icalendar-recur
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.10/2)
+
+(ict:parse/print-test
+"FREQ=DAILY;COUNT=10;INTERVAL=2"
+:type icalendar-recur
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.10/3)
+
+(ict:parse/print-test
+"Project XYZ Final Review\\nConference Room - 3B\\nCome Prepared."
+:type icalendar-text
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.11/1)
+
+(ict:parse/print-test
+;; Local time:
+"230000"
+:type icalendar-time
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.12/1)
+
+(ict:parse/print-test
+;; UTC time:
+"070000Z"
+:type icalendar-time
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.12/2)
+
+(ict:parse/print-test
+;; Local time:
+"083000"
+:type icalendar-time
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.12/3)
+
+(ict:parse/print-test
+;; UTC time:
+"133000Z"
+:type icalendar-time
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.12/4)
+
+(ict:parse/print-test
+;; Local time with TZ identifier:
+"SOMETIMEPROP;TZID=America/New_York;VALUE=TIME:083000\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.3.12/5)
+
+(ict:parse/print-test
+"http://example.com/my-report.txt"
+:type icalendar-uri
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.13/1)
+
+(ict:parse/print-test
+"-0500"
+:type icalendar-utc-offset
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc5545-sec3.3.14/1)
+
+(ict:parse/print-test
+"+0100"
+:type icalendar-utc-offset
+:parser icalendar-parse-value-node
+:printer icalendar-print-value-node
+:source rfc55453.3.14/1)
+
+(ict:parse/print-test
+"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//hacksw/handcal//NONSGML v1.0//EN
+BEGIN:VEVENT
+UID:[email-masked]
+DTSTAMP:19970610T172345Z
+DTSTART:19970714T170000Z
+DTEND:19970715T040000Z
+SUMMARY:Bastille Day Party
+END:VEVENT
+END:VCALENDAR
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.4/1)
+
+(ict:parse/print-test
+"DTSTART:19960415T133000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.5/1)
+
+(ict:parse/print-test
+"BEGIN:VEVENT
+UID:[email-masked]
+DTSTAMP:19970901T130000Z
+DTSTART:19970903T163000Z
+DTEND:19970903T190000Z
+SUMMARY:Annual Employee Review
+CLASS:PRIVATE
+CATEGORIES:BUSINESS,HUMAN RESOURCES
+END:VEVENT
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.1/1)
+
+(ict:parse/print-test
+"BEGIN:VEVENT
+UID:[email-masked]
+DTSTAMP:19970901T130000Z
+DTSTART:19970401T163000Z
+DTEND:19970402T010000Z
+SUMMARY:Laurel is in sensitivity awareness class.
+CLASS:PUBLIC
+CATEGORIES:BUSINESS,HUMAN RESOURCES
+TRANSP:TRANSPARENT
+END:VEVENT
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.1/2)
+
+(ict:parse/print-test
+"BEGIN:VEVENT
+UID:[email-masked]
+DTSTAMP:19970901T130000Z
+DTSTART;VALUE=DATE:19971102
+SUMMARY:Our Blissful Anniversary
+TRANSP:TRANSPARENT
+CLASS:CONFIDENTIAL
+CATEGORIES:ANNIVERSARY,PERSONAL,SPECIAL OCCASION
+RRULE:FREQ=YEARLY
+END:VEVENT
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.1/3)
+
+(ict:parse/print-test
+"BEGIN:VEVENT
+UID:[email-masked]
+DTSTAMP:20070423T123432Z
+DTSTART;VALUE=DATE:20070628
+DTEND;VALUE=DATE:20070709
+SUMMARY:Festival International de Jazz de Montreal
+TRANSP:TRANSPARENT
+END:VEVENT
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.1/4)
+
+(ict:parse/print-test
+"BEGIN:VTODO
+UID:[email-masked]
+DTSTAMP:20070313T123432Z
+DUE;VALUE=DATE:20070501
+SUMMARY:Submit Quebec Income Tax Return for 2006
+CLASS:CONFIDENTIAL
+CATEGORIES:FAMILY,FINANCE
+STATUS:NEEDS-ACTION
+END:VTODO
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.2/1)
+
+(ict:parse/print-test
+"BEGIN:VTODO
+UID:[email-masked]
+DTSTAMP:20070514T103211Z
+DTSTART:20070514T110000Z
+DUE:20070709T130000Z
+COMPLETED:20070707T100000Z
+SUMMARY:Submit Revised Internet-Draft
+PRIORITY:1
+STATUS:NEEDS-ACTION
+END:VTODO
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.2/2)
+
+(ict:parse/print-test
+"BEGIN:VJOURNAL
+UID:[email-masked]
+DTSTAMP:19970901T130000Z
+DTSTART;VALUE=DATE:19970317
+SUMMARY:Staff meeting minutes
+DESCRIPTION:1. Staff meeting: Participants include Joe\\,Lisa\\, and Bob. Aurora project plans were reviewed. There is currently no budget reserves for this project. Lisa will escalate to management. Next meeting on Tuesday.\\n 2. Telephone Conference: ABC Corp. sales representative called to discuss new printer. Promised to get us a demo by Friday.\\n3. Henry Miller (Handsoff Insurance): Car was totaled by tree. Is looking into a loaner car. 555-2323 (tel).
+END:VJOURNAL
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.3/1)
+
+(ict:parse/print-test
+"BEGIN:VFREEBUSY
+UID:[email-masked]
+ORGANIZER:mailto:[email-masked]
+ATTENDEE:mailto:[email-masked]
+DTSTART:19971015T050000Z
+DTEND:19971016T050000Z
+DTSTAMP:19970901T083000Z
+END:VFREEBUSY
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.4/1)
+
+(ict:parse/print-test
+"BEGIN:VFREEBUSY
+UID:[email-masked]
+ORGANIZER:mailto:[email-masked]
+ATTENDEE:mailto:[email-masked]
+DTSTAMP:19970901T100000Z
+FREEBUSY:19971015T050000Z/PT8H30M,19971015T160000Z/PT5H30M,19971015T223000Z/PT6H30M
+URL:http://example.com/pub/busy/jpublic-01.ifb
+COMMENT:This iCalendar file contains busy time information for the next three months.
+END:VFREEBUSY
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.4/2)
+
+(ict:parse/print-test
+;; Corrected. Original has invalid value in ORGANIZER
+"BEGIN:VFREEBUSY
+UID:[email-masked]
+DTSTAMP:19970901T120000Z
+ORGANIZER:mailto:[email-masked]
+DTSTART:19980313T141711Z
+DTEND:19980410T141711Z
+FREEBUSY:19980314T233000Z/19980315T003000Z
+FREEBUSY:19980316T153000Z/19980316T163000Z
+FREEBUSY:19980318T030000Z/19980318T040000Z
+URL:http://www.example.com/calendar/busytime/jsmith.ifb
+END:VFREEBUSY
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.4/3)
+
+(ict:parse/print-test
+"BEGIN:VTIMEZONE
+TZID:America/New_York
+LAST-MODIFIED:20050809T050000Z
+BEGIN:DAYLIGHT
+DTSTART:19670430T020000
+RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19740106T020000
+RDATE:19750223T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19760425T020000
+RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+END:VTIMEZONE
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.5/1)
+
+(ict:parse/print-test
+"BEGIN:VTIMEZONE
+TZID:America/New_York
+LAST-MODIFIED:20050809T050000Z
+BEGIN:STANDARD
+DTSTART:20071104T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.5/2)
+
+(ict:parse/print-test
+"BEGIN:VTIMEZONE
+TZID:America/New_York
+LAST-MODIFIED:20050809T050000Z
+TZURL:http://zones.example.com/tz/America-New_York.ics
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.5/3)
+
+(ict:parse/print-test
+"BEGIN:VTIMEZONE
+TZID:Fictitious
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=19980404T070000Z
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.5/4)
+
+(ict:parse/print-test
+"BEGIN:VTIMEZONE
+TZID:Fictitious
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=19980404T070000Z
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:19990424T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=4
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.5/5)
+
+(ict:parse/print-test
+"BEGIN:VALARM
+TRIGGER;VALUE=DATE-TIME:19970317T133000Z
+REPEAT:4
+DURATION:PT15M
+ACTION:AUDIO
+ATTACH;FMTTYPE=audio/basic:ftp://example.com/pub/sounds/bell-01.aud
+END:VALARM
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.6/1)
+
+(ict:parse/print-test
+"BEGIN:VALARM
+TRIGGER:-PT30M
+REPEAT:2
+DURATION:PT15M
+ACTION:DISPLAY
+DESCRIPTION:Breakfast meeting with executive\\nteam at 8:30 AM EST.
+END:VALARM
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.6/2)
+
+(ict:parse/print-test
+"BEGIN:VALARM
+TRIGGER;RELATED=END:-P2D
+ACTION:EMAIL
+ATTENDEE:mailto:[email-masked]
+SUMMARY:*** REMINDER: SEND AGENDA FOR WEEKLY STAFF MEETING ***
+DESCRIPTION:A draft agenda needs to be sent out to the attendees to the weekly managers meeting (MGR-LIST). Attached is a pointer the document template for the agenda file.
+ATTACH;FMTTYPE=application/msword:http://example.com/templates/agenda.doc
+END:VALARM
+"
+:parser icalendar-parse-component
+:printer icalendar-print-component-node
+:source rfc5545-sec3.6.6/3)
+
+(ict:parse/print-test
+"CALSCALE:GREGORIAN\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.7.1/1)
+
+(ict:parse/print-test
+"METHOD:REQUEST\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.7.2/1)
+
+(ict:parse/print-test
+"PRODID:-//ABC Corporation//NONSGML My Product//EN\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.7.3/1)
+
+(ict:parse/print-test
+"VERSION:2.0\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.7./1)
+
+(ict:parse/print-test
+"ATTACH:CID:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.1/1)
+
+(ict:parse/print-test
+"ATTACH;FMTTYPE=application/postscript:ftp://example.com/pub/reports/r-960812.ps\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.1/2)
+
+(ict:parse/print-test
+"CATEGORIES:APPOINTMENT,EDUCATION\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.2/1)
+
+(ict:parse/print-test
+"CATEGORIES:MEETING\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.2/2)
+
+(ict:parse/print-test
+"CLASS:PUBLIC\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.3/1)
+
+(ict:parse/print-test
+"COMMENT:The meeting really needs to include both ourselves and the customer. We can't hold this meeting without them. As a matter of fact\\, the venue for the meeting ought to be at their site. - - John\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.4/1)
+
+(ict:parse/print-test
+"DESCRIPTION:Meeting to provide technical review for \"Phoenix\" design.\\nHappy Face Conference Room. Phoenix design team MUST attend this meeting.\\nRSVP to team leader.\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.5/1)
+
+(ict:parse/print-test
+"GEO:37.386013;-122.082932\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.6/1)
+
+(ict:parse/print-test
+"LOCATION:Conference Room - F123\\, Bldg. 002\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.7/1)
+
+(ict:parse/print-test
+"LOCATION;ALTREP=\"http://xyzcorp.com/conf-rooms/f123.vcf\":Conference Room - F123\\, Bldg. 002\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.7/2)
+
+(ict:parse/print-test
+"PERCENT-COMPLETE:39\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.8/1)
+
+(ict:parse/print-test
+"PRIORITY:1\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.9/1)
+
+(ict:parse/print-test
+"PRIORITY:2\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.9/2)
+
+(ict:parse/print-test
+"PRIORITY:0\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.9/3)
+
+(ict:parse/print-test
+"RESOURCES:EASEL,PROJECTOR,VCR\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.10/1)
+
+(ict:parse/print-test
+"RESOURCES;LANGUAGE=fr:Nettoyeur haute pression\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.10/2)
+
+(ict:parse/print-test
+"STATUS:TENTATIVE\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.11/1)
+
+(ict:parse/print-test
+"STATUS:NEEDS-ACTION\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.11/2)
+
+(ict:parse/print-test
+"STATUS:DRAFT\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.11/3)
+
+(ict:parse/print-test
+"SUMMARY:Department Party\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.1.12/1)
+
+(ict:parse/print-test
+"COMPLETED:19960401T150000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.1/1)
+
+(ict:parse/print-test
+"DTEND:19960401T150000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.2/1)
+
+(ict:parse/print-test
+"DTEND;VALUE=DATE:19980704\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.2/2)
+
+(ict:parse/print-test
+"DUE:19980430T000000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.3/1)
+
+(ict:parse/print-test
+"DTSTART:19980118T073000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.4/1)
+
+(ict:parse/print-test
+"DURATION:PT1H0M0S\n"
+;; 0M and 0S are not re-printed because they don't contribute to the duration:
+:expected "DURATION:PT1H\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.5/1)
+
+(ict:parse/print-test
+"DURATION:PT15M\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.5/2)
+
+(ict:parse/print-test
+"FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:19970308T160000Z/PT8H30M\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.6/1)
+
+(ict:parse/print-test
+"FREEBUSY;FBTYPE=FREE:19970308T160000Z/PT3H,19970308T200000Z/PT1H\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.6/2)
+
+(ict:parse/print-test
+"FREEBUSY;FBTYPE=FREE:19970308T160000Z/PT3H,19970308T200000Z/PT1H,19970308T230000Z/19970309T000000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.6/3)
+
+(ict:parse/print-test
+"TRANSP:TRANSPARENT\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.7/1)
+
+(ict:parse/print-test
+"TRANSP:OPAQUE\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.2.7/2)
+
+(ict:parse/print-test
+"TZID:America/New_York\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.1/1)
+
+(ict:parse/print-test
+"TZID:America/New_York\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.1/2)
+
+(ict:parse/print-test
+"TZID:/example.org/America/New_York\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.1/3)
+
+(ict:parse/print-test
+"TZNAME:EST\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.2/1)
+
+(ict:parse/print-test
+"TZNAME;LANGUAGE=fr-CA:HNE\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.2/2)
+
+(ict:parse/print-test
+"TZOFFSETFROM:-0500\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.3/1)
+
+(ict:parse/print-test
+"TZOFFSETFROM:+1345\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.3/2)
+
+(ict:parse/print-test
+"TZOFFSETTO:-0400\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.4/1)
+
+(ict:parse/print-test
+"TZOFFSETTO:+1245\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.4/2)
+
+(ict:parse/print-test
+"TZURL:http://timezones.example.org/tz/America-Los_Angeles.ics\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.3.5/1)
+
+(ict:parse/print-test
+"ATTENDEE;MEMBER=\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/1)
+
+(ict:parse/print-test
+"ATTENDEE;DELEGATED-FROM=\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/2)
+
+(ict:parse/print-test
+"ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Henry Cabot:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/3)
+
+(ict:parse/print-test
+"ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM=\"mailto:[email-masked]\";PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/4)
+
+(ict:parse/print-test
+"ATTENDEE;CN=John Smith;DIR=\"ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/5)
+
+(ict:parse/print-test
+"ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;DELEGATED-FROM=\"mailto:[email-masked]\";CN=Henry Cabot:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/6)
+
+(ict:parse/print-test
+"ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO=\"mailto:[email-masked]\";CN=The Big Cheese:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/7)
+
+(ict:parse/print-test
+"ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/8)
+
+(ict:parse/print-test
+;; Corrected. Original lacks quotes around SENT-BY address.
+"ATTENDEE;SENT-BY=\"mailto:[email-masked]\";CN=John Smith:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.1/9)
+
+(ict:parse/print-test
+"CONTACT:Jim Dolittle\\, ABC Industries\\, +1-919-555-1234\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.2/1)
+
+(ict:parse/print-test
+;; Corrected. Original contained unallowed backslash in ldap: URI
+"CONTACT;ALTREP=\"ldap://example.com:6666/o=ABC%20Industries,c=US???(cn=Jim%20Dolittle)\":Jim Dolittle\\, ABC Industries\\,+1-919-555-1234\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.2/2)
+
+(ict:parse/print-test
+"CONTACT;ALTREP=\"CID:[email-masked]\":Jim Dolittle\\, ABC Industries\\, +1-919-555-1234\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.2/3)
+
+(ict:parse/print-test
+"CONTACT;ALTREP=\"http://example.com/pdi/jdoe.vcf\":Jim Dolittle\\, ABC Industries\\, +1-919-555-1234\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.2/4)
+
+(ict:parse/print-test
+"ORGANIZER;CN=John Smith:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.3/1)
+
+(ict:parse/print-test
+"ORGANIZER;CN=JohnSmith;DIR=\"ldap://example.com:6666/o=DC%20Associates,c=US???(cn=John%20Smith)\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.3/2)
+
+(ict:parse/print-test
+"ORGANIZER;SENT-BY=\"mailto:[email-masked]\":mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.3/3)
+
+(ict:parse/print-test
+"RECURRENCE-ID;VALUE=DATE:19960401\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.4/1)
+
+(ict:parse/print-test
+"RECURRENCE-ID;RANGE=THISANDFUTURE:19960120T120000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.4/2)
+
+(ict:parse/print-test
+"RELATED-TO:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.5/1)
+
+(ict:parse/print-test
+"RELATED-TO:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.5/2)
+
+(ict:parse/print-test
+"URL:http://example.com/pub/calendars/jsmith/mytime.ics\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.6/1)
+
+(ict:parse/print-test
+"UID:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.4.7/1)
+
+(ict:parse/print-test
+"EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.1/1)
+
+(ict:parse/print-test
+"RDATE:19970714T123000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.2/1)
+
+(ict:parse/print-test
+"RDATE;TZID=America/New_York:19970714T083000\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.2/2)
+
+(ict:parse/print-test
+"RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.2/3)
+
+(ict:parse/print-test
+"RDATE;VALUE=DATE:19970101,19970120,19970217,19970421,19970526,19970704,19970901,19971014,19971128,19971129,19971225\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.2/4)
+
+(ict:parse/print-test
+"RRULE:FREQ=DAILY;COUNT=10\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/1)
+
+(ict:parse/print-test
+"RRULE:FREQ=DAILY;UNTIL=19971224T000000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/2)
+
+(ict:parse/print-test
+"RRULE:FREQ=DAILY;INTERVAL=2\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/3)
+
+(ict:parse/print-test
+"RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/4)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;UNTIL=20000131T140000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/5)
+
+(ict:parse/print-test
+"RRULE:FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/6)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;COUNT=10\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/7)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;UNTIL=19971224T000000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/8)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;INTERVAL=2;WKST=SU\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/9)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/10)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;COUNT=10;WKST=SU;BYDAY=TU,TH\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/11)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/12)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=8;WKST=SU;BYDAY=TU,TH\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/13)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/14)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;UNTIL=19971224T000000Z;BYDAY=1FR\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/15)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/16)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;COUNT=6;BYDAY=-2MO\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/17)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;BYMONTHDAY=-3\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/18)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=2,15\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/19)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;COUNT=10;BYMONTHDAY=1,-1\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/20)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/21)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=TU\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/22)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;COUNT=10;BYMONTH=6,7\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/23)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/24)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/25)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;BYDAY=20MO\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/26)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;BYWEEKNO=20;BYDAY=MO\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/27)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=TH\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/28)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;BYDAY=TH;BYMONTH=6,7,8\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/29)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/30)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;BYDAY=SA;BYMONTHDAY=7,8,9,10,11,12,13\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/31)
+
+(ict:parse/print-test
+"RRULE:FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/32)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/33)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/34)
+
+(ict:parse/print-test
+"RRULE:FREQ=HOURLY;INTERVAL=3;UNTIL=19970902T170000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/35)
+
+(ict:parse/print-test
+"RRULE:FREQ=MINUTELY;INTERVAL=15;COUNT=6\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/36)
+
+(ict:parse/print-test
+"RRULE:FREQ=MINUTELY;INTERVAL=90;COUNT=4\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/37)
+
+(ict:parse/print-test
+"RRULE:FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/38)
+
+(ict:parse/print-test
+"RRULE:FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/39)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/40)
+
+(ict:parse/print-test
+"RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/41)
+
+(ict:parse/print-test
+"RRULE:FREQ=MONTHLY;BYMONTHDAY=15,30;COUNT=5\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.5.3/42)
+
+(ict:parse/print-test
+"ACTION:AUDIO\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.6.1/1)
+
+(ict:parse/print-test
+"ACTION:DISPLAY\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.6.1/2)
+
+(ict:parse/print-test
+"REPEAT:4\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.6.2/1)
+
+(ict:parse/print-test
+"TRIGGER:-PT15M\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.6.3/1)
+
+(ict:parse/print-test
+"TRIGGER;RELATED=END:PT5M\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.6.3/2)
+
+(ict:parse/print-test
+"TRIGGER;VALUE=DATE-TIME:19980101T050000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.6.3/3)
+
+(ict:parse/print-test
+"CREATED:19960329T133000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.7.1/1)
+
+(ict:parse/print-test
+"DTSTAMP:19971210T080000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.7.2/1)
+
+(ict:parse/print-test
+"LAST-MODIFIED:19960817T133000Z\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.7.3/1)
+
+(ict:parse/print-test
+"SEQUENCE:0\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.7.4/1)
+
+(ict:parse/print-test
+"SEQUENCE:2\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.7.4/2)
+
+(ict:parse/print-test
+"DRESSCODE:CASUAL\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.1/1)
+
+(ict:parse/print-test
+"NON-SMOKING;VALUE=BOOLEAN:TRUE\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.1/2)
+
+(ict:parse/print-test
+"X-ABC-MMSUBJ;VALUE=URI;FMTTYPE=audio/basic:http://www.example.org/mysubj.au\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.2/1)
+
+(ict:parse/print-test
+"REQUEST-STATUS:2.0;Success\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.3/1)
+
+(ict:parse/print-test
+"REQUEST-STATUS:3.1;Invalid property value;DTSTART:96-Apr-01\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.3/2)
+
+(ict:parse/print-test
+"REQUEST-STATUS:2.8; Success\\, repeating event ignored. Scheduled as a single event.;RRULE:FREQ=WEEKLY\\;INTERVAL=2\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.3/3)
+
+(ict:parse/print-test
+"REQUEST-STATUS:4.1;Event conflict.  Date-time is busy.\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.3/4)
+
+(ict:parse/print-test
+"REQUEST-STATUS:3.7;Invalid calendar user;ATTENDEE:mailto:[email-masked]\n"
+:parser icalendar-parse-property
+:printer icalendar-print-property-node
+:source rfc5545-sec3.8.8.3/5)
+
+(ict:parse/print-test
+"BEGIN:VCALENDAR
+PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:19960704T120000Z
+UID:[email-masked]
+ORGANIZER:mailto:[email-masked]
+DTSTART:19960918T143000Z
+DTEND:19960920T220000Z
+STATUS:CONFIRMED
+CATEGORIES:CONFERENCE
+SUMMARY:Networld+Interop Conference
+DESCRIPTION:Networld+Interop Conference and Exhibit\\nAtlanta World Congress Center\\nAtlanta\\, Georgia
+END:VEVENT
+END:VCALENDAR
+"
+:parser icalendar-parse-component ;; TODO: use calendar parser once one exists
+:printer icalendar-print-component-node
+:source rfc5545-sec4/1)
+
+(ict:parse/print-test
+"BEGIN:VCALENDAR
+PRODID:-//RDU Software//NONSGML HandCal//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:America/New_York
+BEGIN:STANDARD
+DTSTART:19981025T020000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19990404T020000
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:19980309T231000Z
+UID:guid-1.example.com
+ORGANIZER:mailto:[email-masked]
+ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=GROUP:mailto:[email-masked]
+DESCRIPTION:Project XYZ Review Meeting
+CATEGORIES:MEETING
+CLASS:PUBLIC
+CREATED:19980309T130000Z
+SUMMARY:XYZ Project Review
+DTSTART;TZID=America/New_York:19980312T083000
+DTEND;TZID=America/New_York:19980312T093000
+LOCATION:1CP Conference Room 4350
+END:VEVENT
+END:VCALENDAR
+"
+:parser icalendar-parse-component ;; TODO: use calendar parser once one exists
+:printer icalendar-print-component-node
+:source rfc5545-sec4/2)
+
+(ict:parse/print-test
+"BEGIN:VCALENDAR
+METHOD:xyz
+VERSION:2.0
+PRODID:-//ABC Corporation//NONSGML My Product//EN
+BEGIN:VEVENT
+DTSTAMP:19970324T120000Z
+SEQUENCE:0
+UID:[email-masked]
+ORGANIZER:mailto:[email-masked]
+ATTENDEE;RSVP=TRUE:mailto:[email-masked]
+DTSTART:19970324T123000Z
+DTEND:19970324T210000Z
+CATEGORIES:MEETING,PROJECT
+CLASS:PUBLIC
+SUMMARY:Calendaring Interoperability Planning Meeting
+DESCRIPTION:Discuss how we can test c&s interoperability\\nusing iCalendar and other IETF standards.
+LOCATION:LDB Lobby
+ATTACH;FMTTYPE=application/postscript:ftp://example.com/pub/conf/bkgrnd.ps
+END:VEVENT
+END:VCALENDAR
+"
+:parser icalendar-parse-component ;; TODO: use calendar parser once one exists
+:printer icalendar-print-component-node
+:source rfc5545-sec4/3)
+
+(ict:parse/print-test
+"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//ABC Corporation//NONSGML My Product//EN
+BEGIN:VTODO
+DTSTAMP:19980130T134500Z
+SEQUENCE:2
+UID:[email-masked]
+ORGANIZER:mailto:[email-masked]
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:[email-masked]
+DUE:19980415T000000
+STATUS:NEEDS-ACTION
+SUMMARY:Submit Income Taxes
+BEGIN:VALARM
+ACTION:AUDIO
+TRIGGER:19980403T120000Z
+ATTACH;FMTTYPE=audio/basic:http://example.com/pub/audio-files/ssbanner.aud
+REPEAT:4
+DURATION:PT1H
+END:VALARM
+END:VTODO
+END:VCALENDAR
+"
+:parser icalendar-parse-component ;; TODO: use calendar parser once one exists
+:printer icalendar-print-component-node
+:source rfc5545-sec4/4)
+
+(ict:parse/print-test
+"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//ABC Corporation//NONSGML My Product//EN
+BEGIN:VJOURNAL
+DTSTAMP:19970324T120000Z
+UID:[email-masked]
+ORGANIZER:mailto:[email-masked]
+STATUS:DRAFT
+CLASS:PUBLIC
+CATEGORIES:Project Report,XYZ,Weekly Meeting
+DESCRIPTION:Project xyz Review Meeting Minutes\\nAgenda\\n1. Review of project version 1.0 requirements.\\n2.Definitionof project processes.\\n3. Review of project schedule.\\nParticipants: John Smith\\, Jane Doe\\, Jim Dandy\\n-It was decided that the requirements need to be signed off byproduct marketing.\\n-P roject processes were accepted.\\n-Project schedule needs to account for scheduled holidaysand employee vacation time. Check with HR for specificdates.\\n-New schedule will be distributed by Friday.\\n-Next weeks meeting is cancelled. No meeting until 3/23.
+END:VJOURNAL
+END:VCALENDAR
+"
+:parser icalendar-parse-component ;; TODO: use calendar parser once one exists
+:printer icalendar-print-component-node
+:source rfc5545-sec4/5)
+
+(ict:parse/print-test
+"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//RDU Software//NONSGML HandCal//EN
+BEGIN:VFREEBUSY
+ORGANIZER:mailto:[email-masked]
+DTSTART:19980313T141711Z
+DTEND:19980410T141711Z
+FREEBUSY:19980314T233000Z/19980315T003000Z
+FREEBUSY:19980316T153000Z/19980316T163000Z
+FREEBUSY:19980318T030000Z/19980318T040000Z
+URL:http://www.example.com/calendar/busytime/jsmith.ifb
+END:VFREEBUSY
+END:VCALENDAR
+"
+:parser icalendar-parse-component ;; TODO: use calendar parser once one exists
+:printer icalendar-print-component-node
+:source rfc5545-sec4/6)
+
+
+
+;; Local Variables:
+;; read-symbol-shorthands: (("ict:" . "icalendar-test-"))
+;; End:
+;;; tests/icalendar-parser.el ends here
-- 
2.39.5