Subject: [PATCH 2/2] New major mode icalendar-mode

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

Tags: patch

Here's a second patch which uses the parser provided by the last
patch to implement a new major mode, icalendar-mode, which for now just
provides syntax highlighting and some basic commands for folding and
unfolding lines.

Again, this is a draft; there's still plenty to be done. I'm looking
forward to your feedback.

Thanks,
Richard

>From b0f3ee82d3e936c6e09d2fa44dc1f9ec933ac2b6 Mon Sep 17 00:00:00 2001
From: Richard Lawrence <[email-masked]>
Date: Fri, 20 Dec 2024 11:15:42 +0100
Subject: [PATCH 3/3] New major mode icalendar-mode

Import icalendar-mode.el from external repo
Move font lock setup from ical:define-* macros into constants in
  icalendar-mode.el
Check that face keywords are non-nil, and require icalendar-mode,
  when adding to icalendar-font-lock-keywords in ical:define-* macros
---
 lisp/calendar/icalendar-macs.el   |  10 +-
 lisp/calendar/icalendar-mode.el   | 609 ++++++++++++++++++++++++++++++
 lisp/calendar/icalendar-parser.el |  75 +---
 3 files changed, 614 insertions(+), 80 deletions(-)
 create mode 100644 lisp/calendar/icalendar-mode.el

diff --git a/lisp/calendar/icalendar-macs.el b/lisp/calendar/icalendar-macs.el
index 2030efc5e6d..fab78fce866 100644
--- a/lisp/calendar/icalendar-macs.el
+++ b/lisp/calendar/icalendar-macs.el
@@ -379,9 +379,7 @@ ical:define-param
        ;; 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
-       )))
+         (push (cons ,param-name (quote ,symbolic-name)) ical:param-types)))))
 
 
 ;; Define properties:
@@ -797,10 +795,8 @@ ical:define-component
        ;; 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
-       )))
+         (push (cons ,component-name (quote ,symbolic-name))
+               ical:component-types)))))
 
 (provide 'icalendar-macs)
 ;; Local Variables:
diff --git a/lisp/calendar/icalendar-mode.el b/lisp/calendar/icalendar-mode.el
new file mode 100644
index 00000000000..0a0d339c89c
--- /dev/null
+++ b/lisp/calendar/icalendar-mode.el
@@ -0,0 +1,609 @@
+;;; icalendar-mode.el --- Major mode for iCalendar format  -*- lexical-binding: t; -*-
+;;;
+
+;; Copyright (C) 2024 Richard Lawrence
+
+;; Author: Richard Lawrence <[email-masked]>
+;; Keywords: calendar
+
+;; This file is part of GNU Emacs.
+
+;; This file is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this file.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file defines icalendar-mode, a major mode for editing
+;; iCalendar data. It defines a syntax table, faces, hooks, and
+;; commands for the mode and sets up syntax highlighting via
+;; font-lock-mode. Syntax highlighting uses the entries for
+;; font-lock-keywords already gathered in icalendar-parser.el, which
+;; see.
+
+;; When activated, icalendar-mode offers to unfold content lines if
+;; necessary, and switch to a new buffer containing the unfolded data;
+;; see `ical:maybe-switch-to-unfolded-buffer'. This is because the
+;; parsing facilities, and thus syntax highlighting, assume that
+;; content lines have already been unfolded. When a buffer is saved,
+;; icalendar-mode also offers to fold long content if necessary, as
+;; required by RFC5545; see `ical:before-save-checks'.
+
+;;; Code:
+
+(require 'icalendar-parser)
+
+;; Faces and font lock:
+(defgroup ical:faces
+  '((ical:property-name custom-face)
+    (ical:property-value custom-face)
+    (ical:parameter-name custom-face)
+    (ical:parameter-value custom-face)
+    (ical:component-name custom-face)
+    (ical:keyword custom-face)
+    (ical:binary-data custom-face)
+    (ical:date-time-types custom-face)
+    (ical:numeric-types custom-face)
+    (ical:recurrence-rule custom-face)
+    (ical:warning custom-face)
+    (ical:ignored custom-face))
+  "Faces for icalendar-mode.") ; TODO: :group
+
+(defface ical:property-name
+  '((default . (:inherit font-lock-keyword-face)))
+  "Face for iCalendar property names")
+
+(defface ical:property-value
+  '((default . (:inherit default)))
+  "Face for iCalendar property values")
+
+(defface ical:parameter-name
+  '((default . (:inherit font-lock-property-name-face)))
+  "Face for iCalendar parameter names")
+
+(defface ical:parameter-value
+  '((default . (:inherit font-lock-property-use-face)))
+  "Face for iCalendar parameter values")
+
+(defface ical:component-name
+  '((default . (:inherit font-lock-constant-face)))
+  "Face for iCalendar component names")
+
+(defface ical:keyword
+  '((default . (:inherit font-lock-keyword-face)))
+  "Face for other iCalendar keywords")
+
+(defface ical:binary-data
+  '((default . (:inherit font-lock-comment-face)))
+  "Face for iCalendar values that represent binary data")
+
+(defface ical:date-time-types
+  '((default . (:inherit font-lock-type-face)))
+  "Face for iCalendar values that represent dates, date-times,
+durations, periods, and UTC offsets")
+
+(defface ical:numeric-types
+  '((default . (:inherit ical:property-value-face)))
+  "Face for iCalendar values that represent integers, floats, and geolocations")
+
+(defface ical:recurrence-rule
+  '((default . (:inherit font-lock-type-face)))
+  "Face for iCalendar recurrence rule values")
+
+(defface ical:uri
+  '((default . (:inherit ical:property-value-face :underline t)))
+  "Face for iCalendar values that are URIs (including URLs and mail addresses)")
+
+(defface ical:warning
+  '((default . (:inherit font-lock-warning-face)))
+  "Face for iCalendar syntax errors")
+
+(defface ical:ignored
+  '((default . (:inherit font-lock-comment-face)))
+  "Face for iCalendar syntax which is parsed but ignored")
+
+;;; Font lock:
+(defconst ical:params-font-lock-keywords
+  '((ical:match-other-param
+     (1 'font-lock-comment-face t t)
+     (2 'font-lock-comment-face t t)
+     (3 'ical:warning t t))
+    (ical:match-value-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-tzid-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:parameter-value t t)
+     (3 'ical:warning t t))
+    (ical:match-sent-by-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-rsvp-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-role-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-reltype-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-related-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-range-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-partstat-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-member-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-language-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:parameter-value t t)
+     (3 'ical:warning t t))
+    (ical:match-fbtype-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-fmttype-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:parameter-value t t)
+     (3 'ical:warning t t))
+    (ical:match-encoding-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-dir-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-delegated-to-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-delegated-from-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-cutype-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-cn-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:parameter-value t t)
+     (3 'ical:warning t t))
+    (ical:match-altrep-param
+     (1 'ical:parameter-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t)))
+  "Entries for iCalendar property parameters in `font-lock-keywords'.")
+
+(defconst ical:properties-font-lock-keywords
+  '((ical:match-request-status-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-other-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-sequence-property
+     (1 'ical:property-name t t)
+     (2 'ical:numeric-types t t)
+     (3 'ical:warning t t))
+    (ical:match-last-modified-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-dtstamp-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-created-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-trigger-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-repeat-property
+     (1 'ical:property-name t t)
+     (2 'ical:numeric-types t t)
+     (3 'ical:warning t t))
+    (ical:match-action-property
+     (1 'ical:property-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-rrule-property
+     (1 'ical:property-name t t)
+     (2 'ical:recurrence-rule t t)
+     (3 'ical:warning t t))
+    (ical:match-rdate-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-exdate-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-uid-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-url-property
+     (1 'ical:property-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-related-to-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-recurrence-id-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-organizer-property
+     (1 'ical:property-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-contact-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-attendee-property
+     (1 'ical:property-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-tzurl-property
+     (1 'ical:property-name t t)
+     (2 'ical:uri t t)
+     (3 'ical:warning t t))
+    (ical:match-tzoffsetto-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-tzoffsetfrom-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-tzname-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-tzid-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-transp-property
+     (1 'ical:property-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-freebusy-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-duration-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-dtstart-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-due-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-dtend-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-completed-property
+     (1 'ical:property-name t t)
+     (2 'ical:date-time-types t t)
+     (3 'ical:warning t t))
+    (ical:match-summary-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-status-property
+     (1 'ical:property-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-resources-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-priority-property
+     (1 'ical:property-name t t)
+     (2 'ical:numeric-types t t)
+     (3 'ical:warning t t))
+    (ical:match-percent-complete-property
+     (1 'ical:property-name t t)
+     (2 'ical:numeric-types t t)
+     (3 'ical:warning t t))
+    (ical:match-location-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-geo-property
+     (1 'ical:property-name t t)
+     (2 'ical:numeric-types t t)
+     (3 'ical:warning t t))
+    (ical:match-description-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-comment-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-class-property
+     (1 'ical:property-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t))
+    (ical:match-categories-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-attach-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t)
+     (13 'ical:uri t t)
+     (14 'ical:binary-data t t))
+    (ical:match-version-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-prodid-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-method-property
+     (1 'ical:property-name t t)
+     (2 'ical:property-value t t)
+     (3 'ical:warning t t))
+    (ical:match-calscale-property
+     (1 'ical:property-name t t)
+     (2 'ical:keyword t t)
+     (3 'ical:warning t t)))
+  "Entries for iCalendar properties in `font-lock-keywords'.")
+
+(defconst ical:ignored-properties-font-lock-keywords
+  `((,(rx ical:other-property) (1 'ical:ignored keep)
+                               (2 'ical:ignored keep)))
+  "Entries for iCalendar ignored properties in `font-lock-keywords'.")
+
+(defconst ical:components-font-lock-keywords
+  '((ical:match-vcalendar-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-other-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-valarm-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-daylight-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-standard-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-vtimezone-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-vfreebusy-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-vjournal-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-vtodo-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t))
+    (ical:match-vevent-component
+     (1 'ical:keyword t t)
+     (2 'ical:component-name t t)))
+  "Entries for iCalendar components in `font-lock-keywords'.")
+
+(defvar ical:font-lock-keywords
+  (append ical:params-font-lock-keywords
+          ical:properties-font-lock-keywords
+          ical:components-font-lock-keywords
+          ical:ignored-properties-font-lock-keywords)
+  "Value of `font-lock-keywords' for icalendar-mode.")
+
+
+;; The major mode:
+
+;;; Mode hook
+(defvar ical:mode-hook nil
+  "Hook run when activating `ical:mode'.")
+
+(add-to-list 'auto-mode-alist '("\\.ics\\'" . icalendar-mode))
+
+;;; Syntax table
+(defvar ical:mode-syntax-table
+    (let ((st (make-syntax-table)))
+      ;; Characters for which the standard syntax table suffices:
+      ;; ; (punctuation): separates some property values, and property parameters
+      ;; " (string): begins and ends string values
+      ;; : (punctuation): separates property name (and parameters) from property
+      ;;                  values
+      ;; , (punctuation): separates values in a list
+      ;; CR, LF (whitespace): content line endings
+      ;; space (whitespace): when at the beginning of a line, continues the
+      ;;                     previous line
+
+      ;; Characters which need to be adjusted from the standard syntax table:
+      ;; = is punctuation, not a symbol constituent:
+      (modify-syntax-entry ?= ".   " st)
+      ;; / is punctuation, not a symbol constituent:
+      (modify-syntax-entry ?/ ".   " st)
+      st)
+    "Syntax table used in `ical:mode'.")
+
+
+;;; Commands
+
+;; TODO: is there a corresponding list by mimetype for buffers
+;; displaying message parts? Thought I saw this somewhere...
+
+(defun ical:switch-to-unfolded-buffer ()
+  "Switch to viewing the contents of the current buffer in a new
+buffer where content lines have been unfolded.
+
+`Folding' means inserting a line break and a single whitespace
+character to continue lines longer than 75 octets; `unfolding'
+means removing the extra whitespace inserted by folding. The
+iCalendar standard (RFC5545) requires folding lines when
+serializing data to iCalendar format, and unfolding before
+parsing it. In icalendar-mode, folded lines may not have proper
+syntax highlighting; this command allows you to view iCalendar
+data with proper syntax highlighting, as the parser sees it.
+
+If the current buffer is visiting a file, this function will
+offer to save the buffer first, and then reload the contents from
+the file, performing unfolding with `icalendar-unfold-undecoded-region'
+before decoding it. This is the most reliable way to unfold lines.
+
+If it is not visiting a file, it will unfold the new buffer
+with `icalendar-unfold-region'. This can in some cases have
+undesirable effects (see its docstring), so the original contents
+are preserved unchanged in the current buffer.
+
+In both cases, after switching to the new buffer, this command
+offers to kill the original buffer.
+
+It is recommended to turn off `auto-fill-mode' when viewing an
+unfolded buffer, so that filling does not interfere with syntax
+highlighting. This function offers to disable `auto-fill-mode' if
+it is enabled in the new buffer; consider using
+`visual-line-mode' instead."
+  (interactive)
+  (when (and buffer-file-name (buffer-modified-p))
+    (when (y-or-n-p (format "Save before reloading from %s?"
+                            (file-name-nondirectory buffer-file-name)))
+      (save-buffer)))
+  (let ((old-buffer (current-buffer))
+        (mmode major-mode)
+        (uf-buffer (if buffer-file-name
+                       (ical:unfolded-buffer-from-file buffer-file-name)
+                     (ical:unfolded-buffer-from-buffer (current-buffer)))))
+    (switch-to-buffer uf-buffer)
+    ;; restart original major mode, in case the new buffer is
+    ;; still in fundamental-mode: TODO: is this necessary?
+    (funcall mmode)
+    (when (y-or-n-p (format "Unfolded buffer is shown. Kill %s?"
+                            (buffer-name old-buffer)))
+      (kill-buffer old-buffer))
+    (when (and auto-fill-function
+               (y-or-n-p "Disable auto-fill-mode?"))
+      (auto-fill-mode -1))))
+
+(defun ical:maybe-switch-to-unfolded-buffer ()
+  "Check for folded lines and ask for confirmation before calling
+`icalendar-switch-to-unfolded-buffer', which see.
+
+This function is intended to be run via `icalendar-mode-hook'
+when `icalendar-mode' is activated."
+  (interactive)
+  (if (ical:contains-folded-lines-p)
+      (when (y-or-n-p "Buffer contains folded lines; unfold in new buffer?")
+        (ical:switch-to-unfolded-buffer))
+    ;; No need for unfolding, just inform the user:
+    (message "Buffer does not contain any lines to unfold")))
+
+(add-hook 'ical:mode-hook 'ical:maybe-switch-to-unfolded-buffer)
+
+(defun ical:before-save-checks ()
+  "Offer to change coding system and fold content lines in the
+current buffer when saving a buffer in `icalendar-mode'.
+
+The iCalendar standard requires CR-LF line endings, so if
+`buffer-file-coding-system' does not use a coding system which
+specifies them, this command offers to switch to a corresponding
+coding system which does.
+
+`Folding' means inserting a line break and a single whitespace
+character to continue lines longer than 75 octets. The iCalendar
+standard requires folding lines when serializing data to
+iCalendar format, so if the buffer contains unfolded lines, this
+command asks you whether you want to fold them."
+  (interactive)
+  (when (eq major-mode 'ical:mode)
+    (let* ((cs buffer-file-coding-system)
+           (suggested-cs (if cs (coding-system-change-eol-conversion cs 'dos)
+                           'prefer-utf-8-dos)))
+      (when (and (not (coding-system-equal cs suggested-cs))
+                 (y-or-n-p
+                  (format "Current coding system %s does not use CR-LF line endings. Change to %s for save?" cs suggested-cs)))
+        (set-buffer-file-coding-system suggested-cs))
+      (when (and (ical:contains-unfolded-lines-p)
+                 (y-or-n-p "Fold content lines before saving?"))
+        (ical:fold-region (point-min) (point-max))))))
+
+(add-hook 'before-save-hook 'ical:before-save-checks)
+
+;;; Mode definition
+(define-derived-mode ical:mode text-mode "iCalendar"
+  "Major mode for viewing and editing iCalendar (RFC5545) data.
+
+This mode provides syntax highlighting for iCalendar components,
+properties, values, and property parameters, and commands to deal
+with folding and unfolding iCalendar content lines.
+
+`Folding' means inserting whitespace characters to continue long
+lines; `unfolding' means removing the extra whitespace inserted
+by folding. The iCalendar standard requires folding lines when
+serializing data to iCalendar format, and unfolding before
+parsing it.
+
+Thus icalendar-mode's syntax highlighting is designed to work with
+unfolded lines. When icalendar-mode is activated, it will offer to
+unfold lines; see `icalendar-switch-to-unfolded-buffer'. It will also
+offer to fold lines when saving a buffer to a file; see
+`icalendar-before-save-checks'. That function also offers to convert the
+line endings in the file to CR-LF, as the standard requires."
+  :group 'icalendar
+  :syntax-table ical:mode-syntax-table
+  ;; TODO: Keymap?
+  ;; TODO: buffer-local variables?
+  ;; TODO: indent-line-function and indentation variables
+  ;; TODO: mode-specific menu and context menus
+  ;; TODO: eldoc integration
+  ;; TODO: completion of keywords
+  ;; TODO: hook for folding in change-major-mode-hook?
+  (progn
+    (setq font-lock-defaults '(ical:font-lock-keywords nil t))))
+
+(provide 'icalendar-mode)
+
+;; Local Variables:
+;; read-symbol-shorthands: (("ical:" . "icalendar-"))
+;; End:
+;;; icalendar-mode.el ends here
diff --git a/lisp/calendar/icalendar-parser.el b/lisp/calendar/icalendar-parser.el
index bc9524ff389..08cc4dbd0fb 100644
--- a/lisp/calendar/icalendar-parser.el
+++ b/lisp/calendar/icalendar-parser.el
@@ -41,12 +41,7 @@
 ;; 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
+;; contains a type symbol naming its type. A number of other regular
 ;; expressions which encode basic categories of the grammar are also
 ;; defined in this file.
 ;;
@@ -1619,9 +1614,6 @@ ical:utc-offset
 
 ;;; 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")
 
@@ -1756,7 +1748,6 @@ ical:altrepparam
   "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"
@@ -1778,7 +1769,6 @@ ical:cutypeparam
   ;; 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"
@@ -1791,7 +1781,6 @@ ical:delfromparam
   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"
@@ -1804,7 +1793,6 @@ ical:deltoparam
   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"
@@ -1816,7 +1804,6 @@ ical:dirparam
 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"
@@ -1827,7 +1814,6 @@ ical:encodingparam
 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
@@ -1867,7 +1853,6 @@ ical:fbtypeparam
       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
@@ -1893,7 +1878,6 @@ ical:memberparam
   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"
@@ -1925,7 +1909,6 @@ ical:partstatparam
   ;; 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"
@@ -1936,7 +1919,6 @@ ical:rangeparam
 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"
@@ -1948,7 +1930,6 @@ ical:trigrelparam
 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"
@@ -1968,7 +1949,6 @@ ical:reltypeparam
   ;; "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"
@@ -1991,7 +1971,6 @@ ical:roleparam
   ;; 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"
@@ -2002,7 +1981,6 @@ ical:rsvpparam
 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"
@@ -2019,7 +1997,6 @@ ical:sentbyparam
   ;; 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"
@@ -2107,7 +2084,6 @@ ical:valuetypeparam
 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
@@ -2117,9 +2093,7 @@ ical:otherparam
 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)
+  ical:param-value)
 
 (rx-define ical:other-param-safe
   ;; we use this rx to skip params when matching properties and
@@ -2134,10 +2108,6 @@ ical:other-param-safe
 
 ;;; 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")
 
@@ -2373,7 +2343,6 @@ ical:calscale
   "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"
@@ -2440,8 +2409,6 @@ ical:attach
                              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)
@@ -2504,7 +2471,6 @@ ical:class
   :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"
@@ -2566,7 +2532,6 @@ ical:geo
 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")
 
@@ -2595,7 +2560,6 @@ ical:percent-complete
 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?
@@ -2609,7 +2573,6 @@ ical:priority
   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"
@@ -2650,7 +2613,6 @@ ical:status
 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"
@@ -2675,7 +2637,6 @@ ical:completed
 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")
 
@@ -2694,7 +2655,6 @@ ical:dtend
   :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"
@@ -2712,7 +2672,6 @@ ical:due
   :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"
@@ -2739,7 +2698,6 @@ ical:dtstart
   :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"
@@ -2757,7 +2715,6 @@ ical:duration
 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"
@@ -2771,7 +2728,6 @@ ical:freebusy
   :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"
@@ -2786,7 +2742,6 @@ ical:transp
       "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
@@ -2824,7 +2779,6 @@ ical:tzoffsetfrom
 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"
@@ -2839,7 +2793,6 @@ ical:tzoffsetto
 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"
@@ -2849,7 +2802,6 @@ ical:tzurl
 `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
@@ -2885,7 +2837,6 @@ ical:attendee
                              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"
@@ -2915,7 +2866,6 @@ ical:organizer
                              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"
@@ -2937,7 +2887,6 @@ ical:recurrence-id
                              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"
@@ -2961,7 +2910,6 @@ ical:url
 `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
@@ -2998,7 +2946,6 @@ ical:exdate
   :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"
@@ -3018,7 +2965,6 @@ ical:rdate
   :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"
@@ -3033,7 +2979,6 @@ ical:rrule
   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
@@ -3052,7 +2997,6 @@ ical:action
             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"
@@ -3065,7 +3009,6 @@ ical:repeat
   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"
@@ -3087,7 +3030,6 @@ ical:trigger
   :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)
@@ -3130,7 +3072,6 @@ ical:created
 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"
@@ -3154,7 +3095,6 @@ ical:dtstamp
 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"
@@ -3165,7 +3105,6 @@ ical:last-modified
 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"
@@ -3180,7 +3119,6 @@ ical:sequence
   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
@@ -3204,11 +3142,6 @@ ical:other-property
   :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'."
@@ -3281,10 +3214,6 @@ ical:request-status
 
 ;;; 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")
 
-- 
2.39.5